@autotask/atools-tool 0.1.9 → 0.1.10

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
@@ -44,7 +44,7 @@ Recommended selections:
44
44
  1. `gpt-5.3-codex`
45
45
  2. `off`
46
46
  3. `low`
47
- 4. Input your sub2api-compatible key
47
+ 4. Input your ATools key (same as ATOOLS_OAI_KEY)
48
48
 
49
49
  ## Install (npm)
50
50
 
@@ -55,16 +55,16 @@ npm i -g @autotask/atools-tool
55
55
  Default full install (recommended):
56
56
 
57
57
  ```bash
58
- npm i -g @autotask/atools-tool && atools-tool-install && atools-tool openclaw
58
+ npm i -g @autotask/atools-tool && atools-tool install-proxy && atools-tool openclaw
59
59
  ```
60
60
 
61
61
  Optional installer command:
62
62
 
63
63
  ```bash
64
- atools-tool-install --help
64
+ atools-tool install-proxy --help
65
65
  ```
66
66
 
67
- `atools-tool-install` service behavior by platform:
67
+ `atools-tool install-proxy` service behavior by platform (legacy alias: `atools-tool-install`):
68
68
 
69
69
  - Linux: systemd user service (fallback to detached process if `systemctl --user` is unavailable)
70
70
  - macOS: launchd user agent (`~/Library/LaunchAgents`)
package/bin/cli.mjs CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { createProxyServer } from '../lib/proxy-server.mjs';
3
3
  import { runConfigOpenclaw } from '../lib/config-openclaw.mjs';
4
+ import { runConfigCodex } from '../lib/config-codex.mjs';
5
+ import { runInstall } from '../lib/install.mjs';
4
6
 
5
7
  function parseArgs(argv) {
6
8
  const out = {
@@ -47,6 +49,8 @@ Commands:
47
49
  serve Run proxy service
48
50
  openclaw Interactive OpenClaw setup (sub2api proxy check + model/reasoning/thinking/key)
49
51
  config-openclaw Alias of 'openclaw'
52
+ install-proxy Install background proxy service for OpenClaw
53
+ codex Auto-configure Codex CLI to use sub2api (direct upstream)
50
54
 
51
55
  Options:
52
56
  --openclaw-home <path> OpenClaw home dir (default: ~/.openclaw)
@@ -62,13 +66,25 @@ Options:
62
66
  }
63
67
 
64
68
  async function main() {
65
- const args = parseArgs(process.argv.slice(2));
69
+ const argv = process.argv.slice(2);
70
+
71
+ if (argv[0] === 'install-proxy') {
72
+ await runInstall(argv.slice(1));
73
+ return;
74
+ }
75
+
76
+ const args = parseArgs(argv);
66
77
 
67
78
  if (args.help) {
68
79
  printHelp();
69
80
  return;
70
81
  }
71
82
 
83
+ if (args.cmd === 'codex') {
84
+ await runConfigCodex();
85
+ return;
86
+ }
87
+
72
88
  if (args.cmd === 'config-openclaw' || args.cmd === 'openclaw') {
73
89
  await runConfigOpenclaw({ openclawHome: args.openclawHome });
74
90
  return;
@@ -0,0 +1,429 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import readlinePromises from 'node:readline/promises';
5
+ import readline from 'node:readline';
6
+ import { spawnSync } from 'node:child_process';
7
+
8
+ const PROVIDER = 'atools';
9
+ const MODEL_OPTIONS = ['gpt-5.2', 'gpt-5.3-codex', 'gpt-5.4'];
10
+ const DEFAULT_MODEL_ID = 'gpt-5.3-codex';
11
+ const UPSTREAM_BASE_URL = 'https://sub2api.atools.live/v1';
12
+ const ENV_KEY = 'ATOOLS_OAI_KEY';
13
+
14
+ const USE_COLOR = Boolean(process.stdout.isTTY && process.env.NO_COLOR !== '1');
15
+ const ANSI = {
16
+ reset: '\x1b[0m',
17
+ bold: '\x1b[1m',
18
+ cyan: '\x1b[36m',
19
+ green: '\x1b[32m',
20
+ yellow: '\x1b[33m'
21
+ };
22
+
23
+ function withColor(text, ...codes) {
24
+ if (!USE_COLOR || !codes.length) return text;
25
+ const prefix = codes.join('');
26
+ return `${prefix}${text}${ANSI.reset}`;
27
+ }
28
+
29
+ function sectionTitle(text) {
30
+ return withColor(text, ANSI.cyan, ANSI.bold);
31
+ }
32
+
33
+ function ok(text) {
34
+ return withColor(text, ANSI.green);
35
+ }
36
+
37
+ function warn(text) {
38
+ return withColor(text, ANSI.yellow);
39
+ }
40
+
41
+ function bold(text) {
42
+ return withColor(text, ANSI.bold);
43
+ }
44
+
45
+ function nowStamp() {
46
+ const d = new Date();
47
+ const p = (n) => String(n).padStart(2, '0');
48
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
49
+ }
50
+
51
+ function backup(filePath) {
52
+ if (!fs.existsSync(filePath)) return null;
53
+ const dest = `${filePath}.bak.${nowStamp()}`;
54
+ fs.copyFileSync(filePath, dest);
55
+ return dest;
56
+ }
57
+
58
+ function ensureDir(dirPath) {
59
+ fs.mkdirSync(dirPath, { recursive: true });
60
+ }
61
+
62
+ function patchCodexConfigText(original, { modelId } = {}) {
63
+ const provider = PROVIDER;
64
+ const model = modelId || DEFAULT_MODEL_ID;
65
+ const baseUrl = UPSTREAM_BASE_URL;
66
+
67
+ if (!original || !original.trim()) {
68
+ return [
69
+ '# Codex config generated by atools-tool',
70
+ `model_provider = "${provider}"`,
71
+ `model = "${model}"`,
72
+ '',
73
+ `[model_providers.${provider}]`,
74
+ 'name = "ATools"',
75
+ `base_url = "${baseUrl}"`,
76
+ 'wire_api = "responses"',
77
+ 'requires_openai_auth = false',
78
+ `env_key = "${ENV_KEY}"`,
79
+ ''
80
+ ].join('\n');
81
+ }
82
+
83
+ const lines = original.replace(/\r\n/g, '\n').split('\n');
84
+ let currentSection = '';
85
+ let inProviderSection = false;
86
+ let sawProviderSection = false;
87
+ let sawModelProviderTop = false;
88
+ let sawModelTop = false;
89
+
90
+ const updated = [];
91
+
92
+ for (const rawLine of lines) {
93
+ let line = rawLine;
94
+ const trimmed = rawLine.trim();
95
+
96
+ const sectionMatch = trimmed.match(/^\[(.+?)\]\s*$/);
97
+ if (sectionMatch) {
98
+ currentSection = sectionMatch[1];
99
+ inProviderSection = currentSection === `model_providers.${provider}`;
100
+ if (inProviderSection) sawProviderSection = true;
101
+ updated.push(line);
102
+ continue;
103
+ }
104
+
105
+ if (!currentSection) {
106
+ if (/^model_provider\s*=/.test(trimmed)) {
107
+ line = `model_provider = "${provider}"`;
108
+ sawModelProviderTop = true;
109
+ } else if (/^model\s*=/.test(trimmed)) {
110
+ line = `model = "${model}"`;
111
+ sawModelTop = true;
112
+ }
113
+ updated.push(line);
114
+ continue;
115
+ }
116
+
117
+ if (inProviderSection) {
118
+ if (/^name\s*=/.test(trimmed)) {
119
+ line = 'name = "ATools"';
120
+ } else if (/^base_url\s*=/.test(trimmed)) {
121
+ line = `base_url = "${baseUrl}"`;
122
+ } else if (/^wire_api\s*=/.test(trimmed)) {
123
+ line = 'wire_api = "responses"';
124
+ } else if (/^requires_openai_auth\s*=/.test(trimmed)) {
125
+ line = 'requires_openai_auth = false';
126
+ } else if (/^env_key\s*=/.test(trimmed)) {
127
+ line = `env_key = "${ENV_KEY}"`;
128
+ }
129
+ }
130
+
131
+ updated.push(line);
132
+ }
133
+
134
+ let resultLines = updated;
135
+
136
+ const topInsert = [];
137
+ if (!sawModelProviderTop) topInsert.push(`model_provider = "${provider}"`);
138
+ if (!sawModelTop) topInsert.push(`model = "${model}"`);
139
+
140
+ if (topInsert.length > 0) {
141
+ const withTop = [];
142
+ let inserted = false;
143
+ for (let i = 0; i < resultLines.length; i += 1) {
144
+ if (!inserted && resultLines[i].trim() && !resultLines[i].trim().startsWith('#')) {
145
+ withTop.push(...topInsert, '');
146
+ inserted = true;
147
+ }
148
+ withTop.push(resultLines[i]);
149
+ }
150
+ if (!inserted) {
151
+ resultLines = [...topInsert, '', ...resultLines];
152
+ } else {
153
+ resultLines = withTop;
154
+ }
155
+ }
156
+
157
+ if (!sawProviderSection) {
158
+ if (resultLines.length && resultLines[resultLines.length - 1].trim() !== '') {
159
+ resultLines.push('');
160
+ }
161
+ resultLines.push(`[model_providers.${provider}]`);
162
+ resultLines.push('name = "ATools"');
163
+ resultLines.push(`base_url = "${baseUrl}"`);
164
+ resultLines.push('wire_api = "responses"');
165
+ resultLines.push('requires_openai_auth = false');
166
+ resultLines.push(`env_key = "${ENV_KEY}"`);
167
+ resultLines.push('');
168
+ }
169
+
170
+ return resultLines.join('\n');
171
+ }
172
+
173
+ function upsertExportLine(content, key, value) {
174
+ const line = `export ${key}=${value}`;
175
+ const re = new RegExp(`^\s*export\s+${key}=.*$`, 'm');
176
+ if (re.test(content)) {
177
+ return content.replace(re, line);
178
+ }
179
+ if (!content || !content.trim()) {
180
+ return `${line}\n`;
181
+ }
182
+ return `${content.replace(/\s*$/, '')}\n${line}\n`;
183
+ }
184
+
185
+ function detectPosixProfile() {
186
+ const shell = process.env.SHELL || '';
187
+ const home = os.homedir();
188
+ if (shell.includes('zsh')) return path.join(home, '.zshrc');
189
+ if (shell.includes('bash')) return path.join(home, '.bashrc');
190
+ return path.join(home, '.bashrc');
191
+ }
192
+
193
+ function persistEnvVar(key, value) {
194
+ if (!value) {
195
+ return { changed: false, reason: 'empty', type: '' };
196
+ }
197
+
198
+ if (process.platform === 'win32') {
199
+ try {
200
+ const out = spawnSync('setx', [key, value], { encoding: 'utf8' });
201
+ if (out.status !== 0) {
202
+ return {
203
+ changed: false,
204
+ type: 'windows',
205
+ reason: String(out.stderr || out.stdout || 'setx failed')
206
+ };
207
+ }
208
+ return { changed: true, type: 'windows', reason: '' };
209
+ } catch (err) {
210
+ return { changed: false, type: 'windows', reason: String(err) };
211
+ }
212
+ }
213
+
214
+ const profilePath = detectPosixProfile();
215
+ let content = '';
216
+ if (fs.existsSync(profilePath)) {
217
+ content = fs.readFileSync(profilePath, 'utf8');
218
+ }
219
+ const next = upsertExportLine(content, key, value);
220
+ if (next === content) {
221
+ return { changed: false, type: 'posix', path: profilePath };
222
+ }
223
+
224
+ if (content) {
225
+ backup(profilePath);
226
+ }
227
+ ensureDir(path.dirname(profilePath));
228
+ fs.writeFileSync(profilePath, next, 'utf8');
229
+ return { changed: true, type: 'posix', path: profilePath };
230
+ }
231
+
232
+ function canUseArrowMenu() {
233
+ return Boolean(
234
+ process.stdin.isTTY
235
+ && process.stdout.isTTY
236
+ && typeof process.stdin.setRawMode === 'function'
237
+ );
238
+ }
239
+
240
+ function askChoiceArrow(question, options, defaultIndex = 0) {
241
+ return new Promise((resolve, reject) => {
242
+ let index = defaultIndex;
243
+
244
+ const draw = () => {
245
+ readline.clearLine(process.stdout, 0);
246
+ readline.cursorTo(process.stdout, 0);
247
+ const label = `${index + 1}. ${options[index]}`;
248
+ const line = withColor(`> ${label}`, ANSI.cyan, ANSI.bold);
249
+ process.stdout.write(`${line} (↑/↓, Enter; 数字直接跳转)`);
250
+ };
251
+
252
+ const cleanup = () => {
253
+ process.stdin.off('keypress', onKeyPress);
254
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
255
+ process.stdin.setRawMode(false);
256
+ }
257
+ };
258
+
259
+ const onKeyPress = (str, key = {}) => {
260
+ if (key.ctrl && key.name === 'c') {
261
+ cleanup();
262
+ reject(new Error('aborted by user'));
263
+ return;
264
+ }
265
+ if (key.name === 'up') {
266
+ index = (index - 1 + options.length) % options.length;
267
+ draw();
268
+ return;
269
+ }
270
+ if (key.name === 'down') {
271
+ index = (index + 1) % options.length;
272
+ draw();
273
+ return;
274
+ }
275
+ if (key.name === 'return' || key.name === 'enter') {
276
+ cleanup();
277
+ readline.clearLine(process.stdout, 0);
278
+ readline.cursorTo(process.stdout, 0);
279
+ process.stdout.write(`已选择: ${options[index]}\n`);
280
+ resolve(options[index]);
281
+ return;
282
+ }
283
+ const n = Number(str);
284
+ if (Number.isInteger(n) && n >= 1 && n <= options.length) {
285
+ index = n - 1;
286
+ draw();
287
+ }
288
+ };
289
+
290
+ try {
291
+ process.stdout.write(`\n${question}\n`);
292
+ readline.emitKeypressEvents(process.stdin);
293
+ process.stdin.setRawMode(true);
294
+ process.stdin.resume();
295
+ process.stdin.on('keypress', onKeyPress);
296
+ draw();
297
+ } catch (err) {
298
+ cleanup();
299
+ reject(err);
300
+ }
301
+ });
302
+ }
303
+
304
+ async function askModel(rl, currentModel = DEFAULT_MODEL_ID) {
305
+ const defaultIndex = Math.max(0, MODEL_OPTIONS.indexOf(currentModel));
306
+ if (canUseArrowMenu()) {
307
+ return askChoiceArrow('1) 选择默认模型', MODEL_OPTIONS, defaultIndex);
308
+ }
309
+
310
+ while (true) {
311
+ process.stdout.write('\n1) 选择默认模型\n');
312
+ MODEL_OPTIONS.forEach((m, idx) => {
313
+ const mark = idx === defaultIndex ? ' (default)' : '';
314
+ process.stdout.write(` ${idx + 1}. ${m}${mark}\n`);
315
+ });
316
+ const input = (await rl.question('请选择编号: ')).trim();
317
+ if (!input) return MODEL_OPTIONS[defaultIndex];
318
+ const n = Number(input);
319
+ if (Number.isInteger(n) && n >= 1 && n <= MODEL_OPTIONS.length) {
320
+ return MODEL_OPTIONS[n - 1];
321
+ }
322
+ process.stdout.write('输入无效,请重新输入。\n');
323
+ }
324
+ }
325
+
326
+ async function askKey(rl, currentKey) {
327
+ const masked = currentKey ? `${currentKey.slice(0, 6)}...${currentKey.slice(-4)}` : '未设置';
328
+ process.stdout.write(`\n2) 输入 ATools API Key(当前: ${masked})\n`);
329
+ process.stdout.write('留空表示保持当前 key\n');
330
+ const input = (await rl.question('请输入 key: ')).trim();
331
+ return input || currentKey || '';
332
+ }
333
+
334
+ async function askKeyWithGuard(rl, currentKey) {
335
+ let selectedKey = await askKey(rl, currentKey);
336
+ while (/^sk-or-v1-/i.test(selectedKey)) {
337
+ process.stdout.write(`\n${warn('检测到你输入的是 OpenRouter key(sk-or-v1- 开头),这不适用于 ATools /v1 模式。')}\n`);
338
+ const choice = (await rl.question('是否重新输入 ATools key? [Y/n]: ')).trim().toLowerCase();
339
+ if (choice === 'n') break;
340
+ selectedKey = await askKey(rl, selectedKey);
341
+ }
342
+ return selectedKey;
343
+ }
344
+
345
+ export async function runConfigCodex({ codexHome } = {}) {
346
+ const home = codexHome || process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
347
+ const configPath = path.join(home, 'config.toml');
348
+
349
+ const existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
350
+
351
+ process.stdout.write(`${sectionTitle('Codex 配置')}:\n`);
352
+ process.stdout.write(`- Home: ${bold(home)}\n`);
353
+ process.stdout.write(`- Provider: ${bold(PROVIDER)}\n`);
354
+ process.stdout.write(`- Upstream Base URL: ${bold(UPSTREAM_BASE_URL)}\n`);
355
+ process.stdout.write(`- Env Var: ${bold(ENV_KEY)}\n`);
356
+
357
+ const initialKey = process.env[ENV_KEY] || process.env.SUB_OAI_KEY || '';
358
+
359
+ let selectedModel = DEFAULT_MODEL_ID;
360
+ let selectedKey = initialKey;
361
+
362
+ const rl = readlinePromises.createInterface({
363
+ input: process.stdin,
364
+ output: process.stdout
365
+ });
366
+
367
+ try {
368
+ selectedModel = await askModel(rl, DEFAULT_MODEL_ID);
369
+ selectedKey = await askKeyWithGuard(rl, initialKey);
370
+ } finally {
371
+ rl.close();
372
+ }
373
+
374
+ const next = patchCodexConfigText(existing, { modelId: selectedModel });
375
+
376
+ ensureDir(path.dirname(configPath));
377
+ if (existing && next !== existing) {
378
+ const bak = backup(configPath);
379
+ if (bak) {
380
+ process.stdout.write(`${ok(`[atools-tool] backup created: ${bak}`)}\n`);
381
+ }
382
+ }
383
+
384
+ if (next !== existing) {
385
+ fs.writeFileSync(configPath, `${next.trimEnd()}\n`, 'utf8');
386
+ }
387
+
388
+ let envResult = { changed: false, type: '', reason: '' };
389
+ if (selectedKey) {
390
+ envResult = persistEnvVar(ENV_KEY, selectedKey);
391
+ }
392
+
393
+ process.stdout.write(`\n${sectionTitle('[atools-tool] Codex 配置结果')}:\n`);
394
+ process.stdout.write(`- Config: ${bold(configPath)}\n`);
395
+ process.stdout.write(`- model_provider = "${PROVIDER}"\n`);
396
+ process.stdout.write(`- model = "${selectedModel}"\n`);
397
+ process.stdout.write(`- base_url = "${UPSTREAM_BASE_URL}"\n`);
398
+
399
+ process.stdout.write(`\n${sectionTitle('Environment (持久化)')}:\n`);
400
+ if (!selectedKey) {
401
+ process.stdout.write(`${warn('- ATools key 未提供,未更新环境变量。')}\n`);
402
+ } else if (envResult.type === 'windows') {
403
+ if (envResult.changed) {
404
+ process.stdout.write(`${ok(`- Persisted ${ENV_KEY} to Windows user env (setx).`)}\n`);
405
+ process.stdout.write('- 请重新打开终端,使新的环境变量生效。\n');
406
+ } else {
407
+ process.stdout.write(`${warn(`- Failed to persist ${ENV_KEY} via setx: ${envResult.reason}`)}\n`);
408
+ }
409
+ } else if (envResult.type === 'posix') {
410
+ if (envResult.changed) {
411
+ process.stdout.write(`${ok(`- Persisted ${ENV_KEY} to shell profile: ${envResult.path}`)}\n`);
412
+ process.stdout.write('- 请重新打开终端,或手动 source 该文件以生效。\n');
413
+ } else {
414
+ process.stdout.write(`- ${ENV_KEY} 已在 profile 中存在(${envResult.path}),未做修改。\n`);
415
+ }
416
+ } else {
417
+ process.stdout.write(`- Env var not persisted (reason: ${envResult.reason || 'unknown'}).\n`);
418
+ }
419
+
420
+ if (selectedKey) {
421
+ process.stdout.write(`\n${sectionTitle('Environment (当前会话手动生效命令)')}:\n`);
422
+ if (process.platform === 'win32') {
423
+ process.stdout.write(`- CMD: ${bold(`set ${ENV_KEY}=${selectedKey}`)}\n`);
424
+ process.stdout.write(`- PowerShell: ${bold(`$env:${ENV_KEY}="${selectedKey}"`)}\n`);
425
+ } else {
426
+ process.stdout.write(`- bash/zsh: ${bold(`export ${ENV_KEY}=${selectedKey}`)}\n`);
427
+ }
428
+ }
429
+ }
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url';
8
8
  import readlinePromises from 'node:readline/promises';
9
9
  import readline from 'node:readline';
10
10
 
11
- const PROVIDER = 'sub2api';
11
+ const PROVIDER = 'atools';
12
12
  const MODEL_OPTIONS = ['gpt-5.2', 'gpt-5.3-codex', 'gpt-5.4'];
13
13
  const THINKING_OPTIONS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'];
14
14
 
@@ -19,6 +19,38 @@ const PROXY_UPSTREAM = 'https://sub2api.atools.live';
19
19
  const PROXY_LOG_FILE = path.join(os.tmpdir(), 'openclaw', 'atools-compat-proxy.log');
20
20
  const PROXY_SERVICE = 'openclaw-atools-proxy.service';
21
21
 
22
+ const USE_COLOR = Boolean(process.stdout.isTTY && process.env.NO_COLOR !== '1');
23
+ const ANSI = {
24
+ reset: '\x1b[0m',
25
+ bold: '\x1b[1m',
26
+ cyan: '\x1b[36m',
27
+ green: '\x1b[32m',
28
+ yellow: '\x1b[33m'
29
+ };
30
+
31
+ function withColor(text, ...codes) {
32
+ if (!USE_COLOR || !codes.length) return text;
33
+ const prefix = codes.join("");
34
+ return prefix + text + ANSI.reset;
35
+ }
36
+
37
+ function sectionTitle(text) {
38
+ return withColor(text, ANSI.cyan, ANSI.bold);
39
+ }
40
+
41
+ function ok(text) {
42
+ return withColor(text, ANSI.green);
43
+ }
44
+
45
+ function warn(text) {
46
+ return withColor(text, ANSI.yellow);
47
+ }
48
+
49
+ function bold(text) {
50
+ return withColor(text, ANSI.bold);
51
+ }
52
+
53
+
22
54
  function readJson(filePath) {
23
55
  if (!fs.existsSync(filePath)) {
24
56
  throw new Error(`missing file: ${filePath}`);
@@ -71,59 +103,64 @@ async function askChoiceNumber(rl, question, options, defaultIndex = 0) {
71
103
  async function askChoiceArrow(question, options, defaultIndex = 0) {
72
104
  return new Promise((resolve, reject) => {
73
105
  let index = defaultIndex;
74
- process.stdout.write(`\n${question}\n`);
75
-
106
+ process.stdout.write("\n" + question + "\n");
107
+ let firstDraw = true;
76
108
  const draw = () => {
77
- readline.clearLine(process.stdout, 0);
78
- readline.cursorTo(process.stdout, 0);
79
- process.stdout.write(`> ${options[index]} (↑/↓, Enter)`);
109
+ if (!firstDraw) {
110
+ readline.moveCursor(process.stdout, 0, -options.length);
111
+ } else {
112
+ firstDraw = false;
113
+ }
114
+ for (let i = 0; i < options.length; i += 1) {
115
+ readline.clearLine(process.stdout, 0);
116
+ const prefix = i === index ? '> ' : ' ';
117
+ const label = String(i + 1) + ". " + String(options[i]);
118
+ let line = prefix + label;
119
+ if (i === index) {
120
+ line = withColor(line, ANSI.cyan, ANSI.bold) + ' (↑/↓, Enter; 数字直接跳转)';
121
+ }
122
+ process.stdout.write(line + "\n");
123
+ }
80
124
  };
81
-
82
125
  const cleanup = () => {
83
- process.stdin.off('keypress', onKeyPress);
84
- if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
126
+ process.stdin.off("keypress", onKeyPress);
127
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
85
128
  process.stdin.setRawMode(false);
86
129
  }
87
130
  };
88
-
89
131
  const onKeyPress = (str, key = {}) => {
90
- if (key.ctrl && key.name === 'c') {
132
+ if (key.ctrl && key.name === "c") {
91
133
  cleanup();
92
- reject(new Error('aborted by user'));
134
+ reject(new Error("aborted by user"));
93
135
  return;
94
136
  }
95
-
96
- if (key.name === 'up') {
137
+ if (key.name === "up") {
97
138
  index = (index - 1 + options.length) % options.length;
98
139
  draw();
99
140
  return;
100
141
  }
101
- if (key.name === 'down') {
142
+ if (key.name === "down") {
102
143
  index = (index + 1) % options.length;
103
144
  draw();
104
145
  return;
105
146
  }
106
- if (key.name === 'return' || key.name === 'enter') {
147
+ if (key.name === "return" || key.name === "enter") {
107
148
  cleanup();
108
- readline.clearLine(process.stdout, 0);
109
- readline.cursorTo(process.stdout, 0);
110
- process.stdout.write(`已选择: ${options[index]}\n`);
149
+ process.stdout.write("已选择: " + String(options[index]) + "\n");
111
150
  resolve(options[index]);
112
151
  return;
113
152
  }
114
-
115
153
  const n = Number(str);
116
154
  if (Number.isInteger(n) && n >= 1 && n <= options.length) {
117
155
  index = n - 1;
118
156
  draw();
119
157
  }
120
158
  };
121
-
122
159
  try {
123
160
  readline.emitKeypressEvents(process.stdin);
124
161
  process.stdin.setRawMode(true);
125
162
  process.stdin.resume();
126
- process.stdin.on('keypress', onKeyPress);
163
+ process.stdin.on("keypress", onKeyPress);
127
164
  draw();
128
165
  } catch (err) {
129
166
  cleanup();
@@ -207,7 +244,7 @@ async function askKey(rl, currentKey) {
207
244
  async function askKeyWithGuard(rl, currentKey) {
208
245
  let selectedKey = await askKey(rl, currentKey);
209
246
  while (/^sk-or-v1-/i.test(selectedKey)) {
210
- process.stdout.write('\n检测到你输入的是 OpenRouter key(sk-or-v1- 开头),这不适用于 sub2api /v1 代理模式。\n');
247
+ process.stdout.write('\n' + warn('检测到你输入的是 OpenRouter key(sk-or-v1- 开头),这不适用于 ATools /v1 模式。') + '\n');
211
248
  const choice = await askChoice(rl, '是否重新输入 key?', ['重新输入 key(推荐)', '继续使用当前 key'], 0);
212
249
  if (choice === '继续使用当前 key') break;
213
250
  selectedKey = await askKey(rl, selectedKey);
@@ -369,7 +406,7 @@ function buildProxyTroubleshootingHints({ linuxSystemdError } = {}) {
369
406
 
370
407
  if (/Failed to connect to bus|No medium found/i.test(err)) {
371
408
  hints.push('当前环境没有可用的 systemd 用户会话(常见于容器、root、无登录会话)。');
372
- hints.push('建议:直接重新执行 `atools-tool-install`,新版会自动降级到非-systemd进程模式。');
409
+ hints.push('建议:直接重新执行 `atools-tool install-proxy`(或兼容别名 `atools-tool-install`),新版会自动降级到非-systemd进程模式。');
373
410
  hints.push('如果是容器环境,请确保进程管理策略允许后台常驻进程。');
374
411
  return hints;
375
412
  }
@@ -495,9 +532,10 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
495
532
  let rl = null;
496
533
 
497
534
  try {
498
- process.stdout.write(`OpenClaw Home: ${home}\n`);
499
- process.stdout.write(`固定代理目标: ${PROXY_UPSTREAM}\n`);
500
- process.stdout.write(`固定 Provider Base URL: ${PROXY_BASE_URL}\n`);
535
+ process.stdout.write(sectionTitle('OpenClaw / ATools 配置') + ':\n');
536
+ process.stdout.write('- OpenClaw Home: ' + bold(home) + '\n');
537
+ process.stdout.write('- 固定代理目标: ' + bold(PROXY_UPSTREAM) + '\n');
538
+ process.stdout.write('- 固定 Provider Base URL: ' + bold(PROXY_BASE_URL) + '\n');
501
539
 
502
540
  const proxyStatus = await ensureProxyReady();
503
541
  if (!proxyStatus.ok) {
@@ -521,10 +559,13 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
521
559
  const selectedThinking = await askChoice(rl, '3) 思考强度', THINKING_OPTIONS, 2);
522
560
  const reasoning = reasoningSwitch === 'on';
523
561
  const currentKey =
524
- openclawCfg?.models?.providers?.[PROVIDER]?.apiKey ||
525
- modelsCfg?.providers?.[PROVIDER]?.apiKey ||
526
- authCfg?.profiles?.[`${PROVIDER}:default`]?.key ||
527
- '';
562
+ openclawCfg?.models?.providers?.[PROVIDER]?.apiKey
563
+ || openclawCfg?.models?.providers?.sub2api?.apiKey
564
+ || modelsCfg?.providers?.[PROVIDER]?.apiKey
565
+ || modelsCfg?.providers?.sub2api?.apiKey
566
+ || authCfg?.profiles?.[`${PROVIDER}:default`]?.key
567
+ || authCfg?.profiles?.['sub2api:default']?.key
568
+ || '';
528
569
  const selectedKey = await askKeyWithGuard(rl, currentKey);
529
570
 
530
571
  openclawCfg.models = openclawCfg.models || {};
@@ -553,8 +594,10 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
553
594
  if (Array.isArray(openclawCfg.agents.list)) {
554
595
  openclawCfg.agents.list = openclawCfg.agents.list.map((agent) => {
555
596
  if (!agent || typeof agent !== 'object') return agent;
556
- if (typeof agent.model === 'string' && agent.model.startsWith(`${PROVIDER}/`)) {
557
- return { ...agent, model: `${PROVIDER}/${selectedModel}` };
597
+ if (typeof agent.model === 'string') {
598
+ if (agent.model.startsWith(`${PROVIDER}/`) || agent.model.startsWith('sub2api/')) {
599
+ return { ...agent, model: `${PROVIDER}/${selectedModel}` };
600
+ }
558
601
  }
559
602
  return agent;
560
603
  });
package/lib/install.mjs CHANGED
@@ -4,7 +4,7 @@ import os from 'node:os';
4
4
  import { execFileSync, spawn } from 'node:child_process';
5
5
  import { fileURLToPath } from 'node:url';
6
6
 
7
- const DEFAULT_PROVIDER = 'sub2api';
7
+ const DEFAULT_PROVIDER = 'atools';
8
8
  const DEFAULT_MODEL = 'gpt-5.3-codex';
9
9
  const DEFAULT_FORCE_MODEL = '';
10
10
  const DEFAULT_PORT = 18888;
@@ -95,7 +95,7 @@ function parseArgs(argv) {
95
95
  maxReqBytes: 68000,
96
96
  logFile: DEFAULT_LOG_FILE,
97
97
  serviceName: DEFAULT_SERVICE_NAME,
98
- apiKey: process.env.SUB_OAI_KEY || '',
98
+ apiKey: process.env.ATOOLS_OAI_KEY || process.env.SUB_OAI_KEY || '',
99
99
  dryRun: false,
100
100
  noRestart: false
101
101
  };
@@ -193,7 +193,7 @@ function detectApiKey(openclawHome, explicitKey) {
193
193
  if (explicitKey) return explicitKey;
194
194
  const authPath = path.join(openclawHome, 'agents/main/agent/auth-profiles.json');
195
195
  const auth = readJsonIfExists(authPath);
196
- return auth?.profiles?.['sub2api:default']?.key || '';
196
+ return (auth?.profiles?.['atools:default']?.key || auth?.profiles?.['sub2api:default']?.key || '');
197
197
  }
198
198
 
199
199
  function patchOpenclawConfig(openclawHome, provider, model, port, dryRun) {
@@ -547,14 +547,14 @@ function restartOpenclawGateway({ noRestart, dryRun }) {
547
547
  }
548
548
 
549
549
  function printHelp() {
550
- process.stdout.write(`atools-tool-install
550
+ process.stdout.write(`atools-tool install-proxy
551
551
 
552
552
  Usage:
553
- atools-tool-install [options]
553
+ atools-tool install-proxy [options]
554
554
 
555
555
  Options:
556
556
  --openclaw-home <path> Default: ~/.openclaw
557
- --provider <name> Default: sub2api
557
+ --provider <name> Default: atools
558
558
  --model <id> Default: gpt-5.3-codex
559
559
  --force-model <id> Default: disabled (optional hard rewrite)
560
560
  --port <number> Default: 18888
@@ -562,7 +562,7 @@ Options:
562
562
  --max-req-bytes <number> Default: 68000
563
563
  --log-file <path> Default: <tmp>/openclaw/atools-compat-proxy.log
564
564
  --service-name <name> Linux: *.service; macOS/Windows: service id/task name
565
- --api-key <key> Prefer explicit key; otherwise use SUB_OAI_KEY or auth-profiles
565
+ --api-key <key> Prefer explicit key; otherwise use ATOOLS_OAI_KEY (or legacy SUB_OAI_KEY) or auth-profiles
566
566
  --no-restart Do not restart openclaw-gateway.service
567
567
  --dry-run Print actions without changing files
568
568
  -h, --help Show help
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autotask/atools-tool",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "ATools CLI for OpenClaw proxy compatibility and interactive model configuration",
5
5
  "type": "module",
6
6
  "license": "MIT",