@investoday/investoday-api 1.8.7 → 1.8.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,9 +16,16 @@ Configure the API key once:
16
16
  investoday-api init
17
17
  ```
18
18
 
19
+ Enable or disable background auto update during initialization:
20
+
21
+ ```bash
22
+ investoday-api init --auto-update
23
+ investoday-api init --no-auto-update
24
+ ```
25
+
19
26
  Get an API key from:
20
27
 
21
- - https://data-api.investoday.net/login
28
+ - https://data-api.investoday.net/user/api-key
22
29
 
23
30
  ## Usage
24
31
 
@@ -26,6 +33,14 @@ Get an API key from:
26
33
  investoday-api --help
27
34
  investoday-api init
28
35
  investoday-api config status
36
+ investoday-api config path
37
+ investoday-api config remove
38
+ investoday-api update status
39
+ investoday-api update run
40
+ investoday-api update enable
41
+ investoday-api update disable
42
+ investoday-api update register
43
+ investoday-api update unregister
29
44
  investoday-api list
30
45
  investoday-api list 沪深京数据
31
46
  investoday-api search-api query=违规处罚 tool_ids=list_stock_violation_penalt
@@ -48,11 +63,61 @@ investoday-api search key=贵州茅台 type=11
48
63
  investoday-api fund/daily-quotes --method POST fundCode=000001 beginDate=2024-01-01 endDate=2024-12-31
49
64
  ```
50
65
 
66
+ ## Background updates
67
+
68
+ `investoday-api` can periodically update the CLI package and installed skills after user authorization.
69
+
70
+ ```bash
71
+ # Show task registration status, last run, errors, next run, and local/remote versions
72
+ investoday-api update status
73
+
74
+ # Run one update immediately; skips when auto update is disabled
75
+ investoday-api update run
76
+
77
+ # Enable auto update and register the user-level scheduled task
78
+ investoday-api update enable
79
+
80
+ # Disable auto update and unregister the scheduled task
81
+ investoday-api update disable
82
+
83
+ # Register or repair the scheduled task without changing the enabled flag
84
+ investoday-api update register
85
+
86
+ # Unregister only the scheduled task without changing the enabled flag
87
+ investoday-api update unregister
88
+ ```
89
+
90
+ The default manifest URL is:
91
+
92
+ ```text
93
+ https://storage.txyun.investoday.net/application/skill-store/configs/investoday-api.manifest.json
94
+ ```
95
+
96
+ For debugging, staging, or private deployments, override it with:
97
+
98
+ ```bash
99
+ INVESTODAY_API_UPDATE_MANIFEST_URL=https://example.com/investoday-api.manifest.json investoday-api update status
100
+ ```
101
+
102
+ ## Config commands
103
+
104
+ ```bash
105
+ # Show whether the API key is configured and where it is loaded from
106
+ investoday-api config status
107
+
108
+ # Print the local config file path
109
+ investoday-api config path
110
+
111
+ # Remove the local config and legacy credential files
112
+ investoday-api config remove
113
+ ```
114
+
51
115
  ## Notes
52
116
 
53
- - Uses the local encrypted config created by `investoday-api init`
117
+ - Uses the local JSON config created by `investoday-api init`
54
118
  - Only calls `https://data-api.investoday.net/data`
55
119
  - Bundles endpoint metadata for `list` and `search-api`
56
120
  - `search-api` defaults to JSON output and includes params, response fields, and `exampleCommand`; use `--text` for a human-readable summary
57
121
  - `search-api` only accepts structured inputs such as `query=` and `tool_ids=`; `query=` accepts one value and supports comma-separated keywords
122
+ - Background update settings are stored in the local JSON config and can be inspected with `investoday-api update status`
58
123
  - Prints the API response `data` field as formatted JSON
package/data/openapi.json CHANGED
@@ -6326,6 +6326,127 @@
6326
6326
  ]
6327
6327
  }
6328
6328
  },
6329
+ "/stock/finance/financial-strength": {
6330
+ "get": {
6331
+ "tags": [
6332
+ "股票财务实力的最新指标"
6333
+ ],
6334
+ "summary": "股票财务实力的最新指标",
6335
+ "description": "通过股票代码获取指定股票的财务实力最新指标,包括现金负债比率、股东权益比率、利息保障倍数等核心财务指标的当前值(TTM)及对应行业排名、历史行业排名,还有基本面趋势F分数、两年破产风险Z分数、财务造假嫌疑M分数、财务实力综合得分,以及股票代码和名称,可用于评估公司财务健康状况、开展个股财务分析等场景。",
6336
+ "operationId": "get_stock_finance_strength",
6337
+ "parameters": [
6338
+ {
6339
+ "name": "stockCode",
6340
+ "in": "query",
6341
+ "description": "股票代码",
6342
+ "required": true,
6343
+ "schema": {
6344
+ "type": "string"
6345
+ }
6346
+ }
6347
+ ],
6348
+ "responses": {
6349
+ "200": {
6350
+ "description": "成功响应",
6351
+ "content": {
6352
+ "application/json": {
6353
+ "schema": {
6354
+ "type": "object",
6355
+ "properties": {
6356
+ "f2190": {
6357
+ "type": "string",
6358
+ "description": "现金负债率当前值(TTM)",
6359
+ "example": ""
6360
+ },
6361
+ "f2190Rk": {
6362
+ "type": "string",
6363
+ "description": "现金负债率行业排名",
6364
+ "example": ""
6365
+ },
6366
+ "f2190RkHist": {
6367
+ "type": "string",
6368
+ "description": "现金负债率历史行业排名",
6369
+ "example": ""
6370
+ },
6371
+ "f2200": {
6372
+ "type": "string",
6373
+ "description": "股东权益比率当前值(TTM)",
6374
+ "example": ""
6375
+ },
6376
+ "f2200Rk": {
6377
+ "type": "string",
6378
+ "description": "股东权益比率行业排名",
6379
+ "example": ""
6380
+ },
6381
+ "f2200RkHist": {
6382
+ "type": "string",
6383
+ "description": "股东权益比率历史行业排名",
6384
+ "example": ""
6385
+ },
6386
+ "f2210": {
6387
+ "type": "string",
6388
+ "description": "利息保障倍数当前值(TTM)",
6389
+ "example": ""
6390
+ },
6391
+ "f2210Rk": {
6392
+ "type": "string",
6393
+ "description": "利息保障倍数行业排名【0-100】",
6394
+ "example": ""
6395
+ },
6396
+ "f2210RkHist": {
6397
+ "type": "string",
6398
+ "description": "利息保障倍数历史行业排名【0-100】",
6399
+ "example": ""
6400
+ },
6401
+ "f2220": {
6402
+ "type": "string",
6403
+ "description": "基本面趋势(F分数)当前值(TTM)",
6404
+ "example": ""
6405
+ },
6406
+ "f2230": {
6407
+ "type": "string",
6408
+ "description": "两年破产风险(Z分数)当前值(TTM)",
6409
+ "example": ""
6410
+ },
6411
+ "f2240": {
6412
+ "type": "string",
6413
+ "description": "财务造假嫌疑(M分数)当前值(TTM)",
6414
+ "example": ""
6415
+ },
6416
+ "f2520": {
6417
+ "type": "string",
6418
+ "description": "财务实力综合得分",
6419
+ "example": ""
6420
+ },
6421
+ "stockCode": {
6422
+ "type": "string",
6423
+ "description": "股票代码",
6424
+ "example": ""
6425
+ },
6426
+ "stockName": {
6427
+ "type": "string",
6428
+ "description": "股票名称",
6429
+ "example": ""
6430
+ }
6431
+ }
6432
+ }
6433
+ }
6434
+ }
6435
+ },
6436
+ "400": {
6437
+ "description": "无效请求"
6438
+ },
6439
+ "500": {
6440
+ "description": "服务器内部错误"
6441
+ }
6442
+ },
6443
+ "security": [
6444
+ {
6445
+ "ApiKeyAuth": []
6446
+ }
6447
+ ]
6448
+ }
6449
+ },
6329
6450
  "/stock/finance/financial-strength-ht": {
6330
6451
  "get": {
6331
6452
  "tags": [
package/data/tree.json CHANGED
@@ -1842,6 +1842,20 @@
1842
1842
  "apiAccessLevel": "L2",
1843
1843
  "apiMethod": "GET"
1844
1844
  },
1845
+ {
1846
+ "apiId": 10250,
1847
+ "apiName": "股票财务实力的最新指标",
1848
+ "apiType": "PROXY",
1849
+ "apiStatus": "release",
1850
+ "groupId": 1072,
1851
+ "remarks": null,
1852
+ "toolName": "get_stk_fin_strength",
1853
+ "toolId": "get_stock_finance_strength",
1854
+ "apiPath": "stock/finance/financial-strength",
1855
+ "enabled": 1,
1856
+ "apiAccessLevel": "L2",
1857
+ "apiMethod": "GET"
1858
+ },
1845
1859
  {
1846
1860
  "apiId": 10251,
1847
1861
  "apiName": "股票财务实力的历史指标",
package/lib/call-api.js CHANGED
@@ -9,6 +9,7 @@ const {
9
9
  saveCredentials,
10
10
  } = require("./config");
11
11
  const { version: PACKAGE_VERSION } = require("../package.json");
12
+ const { runUpdateCommand } = require("./update");
12
13
 
13
14
  const BASE_URL = "https://data-api.investoday.net/data";
14
15
  const REQUEST_TIMEOUT = 30_000;
@@ -36,6 +37,7 @@ function printHelp() {
36
37
  "Usage:\n" +
37
38
  " investoday-api init\n" +
38
39
  " investoday-api config status|path|remove\n" +
40
+ " investoday-api update run|status|enable|disable|register|unregister\n" +
39
41
  " investoday-api <endpoint> [key=value ...] [--method GET|POST]\n" +
40
42
  " investoday-api list [group-or-subgroup]\n" +
41
43
  " investoday-api search-api query=<query> [tool_ids=<tool_id,...>] [--text]\n" +
@@ -43,7 +45,8 @@ function printHelp() {
43
45
  " investoday-api --help\n\n" +
44
46
  "Commands:\n" +
45
47
  " init Initialize local InvestToday API key configuration\n" +
46
- " config Show, locate, or remove the local encrypted credentials\n" +
48
+ " config Show, locate, or remove the local configuration\n" +
49
+ " update Manage background updates for investoday-api and installed skills\n" +
47
50
  " list List available groups, subgroups, or endpoints from bundled metadata\n" +
48
51
  " search-api Search endpoints and return method, params, response fields, and example command\n\n" +
49
52
  "Examples:\n" +
@@ -74,9 +77,12 @@ function printInitHelp() {
74
77
  "Usage:\n" +
75
78
  " investoday-api init\n\n" +
76
79
  "Default flow:\n" +
77
- " Prompt for InvestToday API Key, verify it, then save it to local encrypted config.\n\n" +
80
+ " Prompt for InvestToday API Key, verify it, then save it to local JSON config.\n" +
81
+ ` Get your API Key from: ${API_KEY_MANAGE_URL}\n\n` +
78
82
  "Options:\n" +
79
- " --skip-verify Save without validating the API key first\n\n" +
83
+ " --skip-verify Save without validating the API key first\n" +
84
+ " --auto-update Enable background auto update without prompting\n" +
85
+ " --no-auto-update Disable background auto update without prompting\n\n" +
80
86
  `Local config: ${getCredentialsPath()}\n`
81
87
  );
82
88
  }
@@ -101,6 +107,7 @@ async function resolveInitApiKey(args) {
101
107
  exitWithError("ERROR: Interactive input is unavailable. Run 'investoday-api init' in an interactive terminal.");
102
108
  }
103
109
 
110
+ process.stderr.write(`请访问 ${API_KEY_MANAGE_URL} 获取 API Key。\n`);
104
111
  const apiKey = await askInput("InvestToday API Key: ");
105
112
  if (!String(apiKey || "").trim()) {
106
113
  exitWithError("ERROR: API key cannot be empty.");
@@ -122,18 +129,86 @@ async function verifyApiKey(apiKey) {
122
129
  if (apiKey && message.includes(apiKey)) {
123
130
  message = message.replaceAll(apiKey, "***");
124
131
  }
125
- throw new Error(`verification request failed: ${message}`);
132
+ const errorName = String(error.name || "");
133
+ const errorCode = String(error.code || error.cause && error.cause.code || "");
134
+ const errorType = errorName === "TimeoutError" || errorName === "AbortError" || errorCode.includes("TIMEOUT")
135
+ ? "network_timeout"
136
+ : "network_unreachable";
137
+ return {
138
+ ok: false,
139
+ errorType,
140
+ serverConnected: false,
141
+ message: errorType === "network_timeout"
142
+ ? "连接今日投资数据服务超时,请检查网络或代理后重试。"
143
+ : `无法连接今日投资数据服务,请检查网络、DNS、TLS 或代理后重试。${message ? ` ${message}` : ""}`.slice(0, 500),
144
+ };
126
145
  }
127
146
 
128
147
  if (!response.ok) {
129
148
  const body = await response.text().catch(() => "");
130
- throw new Error(`HTTP ${response.status}${body ? `: ${body.slice(0, 500)}` : ""}`);
149
+ let serverMessage = body.slice(0, 500);
150
+ try {
151
+ const payload = JSON.parse(body);
152
+ serverMessage = payload.message || payload.error || serverMessage;
153
+ } catch {
154
+ // keep plain text body
155
+ }
156
+ if (apiKey && serverMessage.includes(apiKey)) {
157
+ serverMessage = serverMessage.replaceAll(apiKey, "***");
158
+ }
159
+ return {
160
+ ok: false,
161
+ errorType: response.status >= 500 ? "server_error" : "invalid_api_key",
162
+ serverConnected: true,
163
+ message: serverMessage || `HTTP ${response.status}`,
164
+ };
131
165
  }
132
166
 
133
167
  const result = await response.json().catch(() => null);
168
+ if (!result) {
169
+ return {
170
+ ok: false,
171
+ errorType: "unexpected_response",
172
+ serverConnected: true,
173
+ message: "服务返回异常:响应不是合法 JSON。",
174
+ };
175
+ }
134
176
  if (!result || result.code !== 0) {
135
- throw new Error(result && result.message ? result.message : "invalid response");
177
+ let message = result && result.message ? result.message : "API Key 无效或无接口权限。";
178
+ if (apiKey && message.includes(apiKey)) {
179
+ message = message.replaceAll(apiKey, "***");
180
+ }
181
+ return {
182
+ ok: false,
183
+ errorType: "invalid_api_key",
184
+ serverConnected: true,
185
+ message,
186
+ };
136
187
  }
188
+ return { ok: true, errorType: null, serverConnected: true, message: result.message || "success" };
189
+ }
190
+
191
+ async function resolveAutoUpdateChoice(args) {
192
+ const enableArg = hasArg(args, "--auto-update");
193
+ const disableArg = hasArg(args, "--no-auto-update");
194
+ if (enableArg && disableArg) {
195
+ exitWithError("ERROR: --auto-update and --no-auto-update cannot be used together.");
196
+ }
197
+ if (enableArg) {
198
+ return true;
199
+ }
200
+ if (disableArg) {
201
+ return false;
202
+ }
203
+
204
+ const answer = await askInput(
205
+ "\n是否启用自动更新?\n" +
206
+ "启用后将后台定时更新 investoday-api 及 skill,以保障您能及时获取到我们最新的 API。\n" +
207
+ "默认:是\n" +
208
+ "[Y/n]: "
209
+ );
210
+ const normalized = String(answer || "").trim().toLowerCase();
211
+ return normalized !== "n" && normalized !== "no";
137
212
  }
138
213
 
139
214
  async function runInitCommand(args) {
@@ -142,7 +217,7 @@ async function runInitCommand(args) {
142
217
  return;
143
218
  }
144
219
 
145
- const allowed = new Set(["--skip-verify"]);
220
+ const allowed = new Set(["--skip-verify", "--auto-update", "--no-auto-update"]);
146
221
  for (const arg of args) {
147
222
  if (!allowed.has(arg)) {
148
223
  exitWithError(`ERROR: Unknown init option '${arg}'`);
@@ -152,12 +227,12 @@ async function runInitCommand(args) {
152
227
  const apiKey = await resolveInitApiKey(args);
153
228
  if (!hasArg(args, "--skip-verify")) {
154
229
  process.stderr.write("Verifying InvestToday API key...\n");
155
- try {
156
- await verifyApiKey(apiKey);
157
- } catch (error) {
230
+ const verification = await verifyApiKey(apiKey);
231
+ if (!verification.ok) {
158
232
  exitWithError(
159
233
  `InvestToday API Key verification failed.\n` +
160
- `Error: ${error.message}\n` +
234
+ `Error type: ${verification.errorType}\n` +
235
+ `Error: ${verification.message}\n` +
161
236
  `请登录 ${API_KEY_MANAGE_URL} 确认您的apiKey正确。`
162
237
  );
163
238
  }
@@ -169,10 +244,22 @@ async function runInitCommand(args) {
169
244
  exitWithError(`ERROR: Failed to save credentials: ${error.message}`);
170
245
  }
171
246
 
172
- process.stdout.write(
173
- "InvestToday API key 配置成功\n\n" +
174
- "初始化完成 ✅\n"
175
- );
247
+ process.stdout.write("InvestToday API key 配置成功\n");
248
+
249
+ const autoUpdateEnabled = await resolveAutoUpdateChoice(args);
250
+ if (autoUpdateEnabled) {
251
+ const enableResult = await runUpdateCommand(["enable"], { stdout: process.stdout, stderr: process.stderr });
252
+ if (enableResult && enableResult.ok) {
253
+ const runResult = await runUpdateCommand(["run"], { stdout: process.stdout, stderr: process.stderr });
254
+ if (runResult && runResult.ok === false) {
255
+ process.stderr.write("自动更新首次执行失败,可稍后运行 `investoday-api update status` 查看详情。\n");
256
+ }
257
+ }
258
+ } else {
259
+ await runUpdateCommand(["disable"], { stdout: process.stdout, stderr: process.stderr });
260
+ }
261
+
262
+ process.stdout.write("\n初始化完成 ✅\n");
176
263
  }
177
264
 
178
265
  function runConfigCommand(args) {
@@ -195,7 +282,7 @@ function runConfigCommand(args) {
195
282
 
196
283
  if (action === "remove") {
197
284
  removeCredentials();
198
- process.stdout.write("InvestToday local credentials removed.\n");
285
+ process.stdout.write("InvestToday local configuration removed.\n");
199
286
  return;
200
287
  }
201
288
 
@@ -208,7 +295,7 @@ function runConfigCommand(args) {
208
295
  localConfig: localConfigured ? "configured" : "missing",
209
296
  activeSource,
210
297
  configDir: getConfigDir(),
211
- credentialsFile: getCredentialsPath(),
298
+ configFile: getCredentialsPath(),
212
299
  }, null, 2) + "\n"
213
300
  );
214
301
  return;
@@ -755,6 +842,14 @@ async function main(argv = process.argv.slice(2)) {
755
842
  return;
756
843
  }
757
844
 
845
+ if (argv[0] === "update") {
846
+ const result = await runUpdateCommand(argv.slice(1), { stdout: process.stdout, stderr: process.stderr });
847
+ if (result && result.ok === false) {
848
+ process.exitCode = 1;
849
+ }
850
+ return;
851
+ }
852
+
758
853
  if (argv[0] === "list") {
759
854
  runListCommand(argv.slice(1));
760
855
  return;
package/lib/config.js CHANGED
@@ -1,13 +1,12 @@
1
- const crypto = require("node:crypto");
2
1
  const fs = require("node:fs");
3
2
  const os = require("node:os");
4
3
  const path = require("node:path");
5
4
 
6
5
  const CONFIG_DIR_ENV = "INVESTODAY_API_CONFIG_DIR";
7
6
  const API_KEY_ENV = "INVESTODAY_API_KEY";
8
- const CREDENTIALS_FILE = "credentials.enc";
9
- const KEY_FILE = ".encryption_key";
10
- const CIPHER_ALGORITHM = "aes-256-gcm";
7
+ const CONFIG_FILE = "investoday-api.config.json";
8
+ const LEGACY_CREDENTIALS_FILE = "credentials.enc";
9
+ const LEGACY_KEY_FILE = ".encryption_key";
11
10
 
12
11
  function getConfigDir(env = process.env) {
13
12
  if (env[CONFIG_DIR_ENV]) {
@@ -18,11 +17,15 @@ function getConfigDir(env = process.env) {
18
17
  }
19
18
 
20
19
  function getCredentialsPath(env = process.env) {
21
- return path.join(getConfigDir(env), CREDENTIALS_FILE);
20
+ return path.join(getConfigDir(env), CONFIG_FILE);
22
21
  }
23
22
 
24
- function getEncryptionKeyPath(env = process.env) {
25
- return path.join(getConfigDir(env), KEY_FILE);
23
+ function getLegacyCredentialsPath(env = process.env) {
24
+ return path.join(getConfigDir(env), LEGACY_CREDENTIALS_FILE);
25
+ }
26
+
27
+ function getLegacyKeyPath(env = process.env) {
28
+ return path.join(getConfigDir(env), LEGACY_KEY_FILE);
26
29
  }
27
30
 
28
31
  function ensureConfigDir(env = process.env) {
@@ -44,69 +47,16 @@ function atomicWriteFile(filePath, contents, mode = 0o600) {
44
47
  fs.renameSync(tempPath, filePath);
45
48
  }
46
49
 
47
- function loadOrCreateEncryptionKey(env = process.env) {
48
- ensureConfigDir(env);
49
- const keyPath = getEncryptionKeyPath(env);
50
- if (fs.existsSync(keyPath)) {
51
- const encoded = fs.readFileSync(keyPath, "utf8").trim();
52
- const key = Buffer.from(encoded, "base64");
53
- if (key.length !== 32) {
54
- throw new Error(`Invalid encryption key at ${keyPath}`);
55
- }
56
- return key;
57
- }
58
-
59
- const key = crypto.randomBytes(32);
60
- atomicWriteFile(keyPath, key.toString("base64"));
61
- return key;
50
+ function saveConfig(config, env = process.env) {
51
+ atomicWriteFile(getCredentialsPath(env), `${JSON.stringify(config || {}, null, 2)}\n`);
62
52
  }
63
53
 
64
- function loadEncryptionKey(env = process.env) {
65
- const keyPath = getEncryptionKeyPath(env);
66
- if (!fs.existsSync(keyPath)) {
67
- return null;
68
- }
69
-
70
- const encoded = fs.readFileSync(keyPath, "utf8").trim();
71
- const key = Buffer.from(encoded, "base64");
72
- if (key.length !== 32) {
73
- return null;
74
- }
75
- return key;
76
- }
77
-
78
- function encryptJson(value, key) {
79
- const iv = crypto.randomBytes(12);
80
- const cipher = crypto.createCipheriv(CIPHER_ALGORITHM, key, iv);
81
- const plaintext = Buffer.from(JSON.stringify(value), "utf8");
82
- const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
83
- const tag = cipher.getAuthTag();
84
-
85
- return {
86
- version: 1,
87
- algorithm: CIPHER_ALGORITHM,
88
- iv: iv.toString("base64"),
89
- tag: tag.toString("base64"),
90
- data: encrypted.toString("base64"),
91
- };
92
- }
93
-
94
- function decryptJson(payload, key) {
95
- if (!payload || payload.algorithm !== CIPHER_ALGORITHM) {
96
- throw new Error("Unsupported credentials format");
54
+ function removeLegacyCredentials(env = process.env) {
55
+ for (const filePath of [getLegacyCredentialsPath(env), getLegacyKeyPath(env)]) {
56
+ if (fs.existsSync(filePath)) {
57
+ fs.rmSync(filePath, { force: true });
58
+ }
97
59
  }
98
-
99
- const decipher = crypto.createDecipheriv(
100
- CIPHER_ALGORITHM,
101
- key,
102
- Buffer.from(payload.iv, "base64")
103
- );
104
- decipher.setAuthTag(Buffer.from(payload.tag, "base64"));
105
- const decrypted = Buffer.concat([
106
- decipher.update(Buffer.from(payload.data, "base64")),
107
- decipher.final(),
108
- ]);
109
- return JSON.parse(decrypted.toString("utf8"));
110
60
  }
111
61
 
112
62
  function saveCredentials(apiKey, env = process.env) {
@@ -115,35 +65,48 @@ function saveCredentials(apiKey, env = process.env) {
115
65
  throw new Error("API key cannot be empty");
116
66
  }
117
67
 
118
- const key = loadOrCreateEncryptionKey(env);
119
- const payload = encryptJson({
120
- apiKey: trimmedKey,
121
- createdAt: new Date().toISOString(),
122
- }, key);
123
- atomicWriteFile(getCredentialsPath(env), `${JSON.stringify(payload, null, 2)}\n`);
68
+ const existing = readConfig(env);
69
+ const payload = {
70
+ ...(existing || {}),
71
+ [API_KEY_ENV]: trimmedKey,
72
+ };
73
+ saveConfig(payload, env);
74
+ removeLegacyCredentials(env);
124
75
  }
125
76
 
126
- function readCredentials(env = process.env) {
127
- const credentialsPath = getCredentialsPath(env);
128
- const key = loadEncryptionKey(env);
129
- if (!key || !fs.existsSync(credentialsPath)) {
77
+ function readConfig(env = process.env) {
78
+ const configPath = getCredentialsPath(env);
79
+ if (!fs.existsSync(configPath)) {
130
80
  return null;
131
81
  }
132
82
 
133
83
  try {
134
- const payload = JSON.parse(fs.readFileSync(credentialsPath, "utf8"));
135
- return decryptJson(payload, key);
84
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
136
85
  } catch {
137
86
  return null;
138
87
  }
139
88
  }
140
89
 
90
+ function readCredentials(env = process.env) {
91
+ const config = readConfig(env);
92
+ if (!config) {
93
+ return null;
94
+ }
95
+
96
+ const apiKey = String(config[API_KEY_ENV] || "").trim();
97
+ if (!apiKey) {
98
+ return null;
99
+ }
100
+
101
+ return { apiKey };
102
+ }
103
+
141
104
  function removeCredentials(env = process.env) {
142
- for (const filePath of [getCredentialsPath(env), getEncryptionKeyPath(env)]) {
143
- if (fs.existsSync(filePath)) {
144
- fs.rmSync(filePath, { force: true });
145
- }
105
+ const configPath = getCredentialsPath(env);
106
+ if (fs.existsSync(configPath)) {
107
+ fs.rmSync(configPath, { force: true });
146
108
  }
109
+ removeLegacyCredentials(env);
147
110
  }
148
111
 
149
112
  function resolveApiKey(env = process.env) {
@@ -162,14 +125,20 @@ function resolveApiKey(env = process.env) {
162
125
 
163
126
  module.exports = {
164
127
  API_KEY_ENV,
128
+ CONFIG_FILE,
165
129
  CONFIG_DIR_ENV,
166
- CREDENTIALS_FILE,
167
- KEY_FILE,
130
+ CREDENTIALS_FILE: CONFIG_FILE,
131
+ LEGACY_CREDENTIALS_FILE,
132
+ LEGACY_KEY_FILE,
168
133
  getConfigDir,
169
134
  getCredentialsPath,
170
- getEncryptionKeyPath,
135
+ getLegacyCredentialsPath,
136
+ getLegacyKeyPath,
137
+ removeLegacyCredentials,
138
+ readConfig,
171
139
  readCredentials,
172
140
  removeCredentials,
173
141
  resolveApiKey,
142
+ saveConfig,
174
143
  saveCredentials,
175
144
  };