@autotask/atools-tool 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 aak1247
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # atools-tool
2
+
3
+ ATools CLI for OpenClaw:
4
+
5
+ - proxy compatibility for sub2api-style upstreams
6
+ - one-click installer
7
+ - interactive OpenClaw model config
8
+
9
+ ## Expected Usage
10
+
11
+ ```bash
12
+ atools-tool <command>
13
+ ```
14
+
15
+ Available command:
16
+
17
+ ```bash
18
+ atools-tool config-openclaw
19
+ ```
20
+
21
+ Interactive flow:
22
+
23
+ 1. Select default model: `gpt-5.2` / `gpt-5.3-codex` / `gpt-5.4`
24
+ 2. Configure reasoning: `off` / `on`
25
+ 3. Configure thinking strength: `off|minimal|low|medium|high|xhigh`
26
+ 4. Input API key (leave empty to keep current key)
27
+
28
+ ## Install (npm)
29
+
30
+ ```bash
31
+ npm i -g atools-tool
32
+ ```
33
+
34
+ Optional installer command:
35
+
36
+ ```bash
37
+ atools-tool-install --help
38
+ ```
39
+
40
+ ## Proxy Command
41
+
42
+ ```bash
43
+ atools-tool serve --port 18888 --upstream https://sub2api.atools.live
44
+ ```
45
+
46
+ ## Repo Install
47
+
48
+ ```bash
49
+ git clone https://github.com/aak1247/sub2api-openclaw-proxy.git
50
+ cd sub2api-openclaw-proxy
51
+ ./install.sh
52
+ ```
53
+
54
+ ## Bootstrap Install
55
+
56
+ ```bash
57
+ curl -fsSL https://raw.githubusercontent.com/aak1247/sub2api-openclaw-proxy/main/bootstrap-install.sh | bash
58
+ ```
59
+
60
+ ## What Installer Changes
61
+
62
+ - patches `~/.openclaw/openclaw.json`
63
+ - patches `~/.openclaw/agents/main/agent/models.json`
64
+ - patches `~/.openclaw/agents/main/agent/auth-profiles.json` (if API key is available)
65
+ - creates/enables `~/.config/systemd/user/openclaw-atools-proxy.service`
66
+ - restarts `openclaw-gateway.service` (unless `--no-restart`)
67
+
68
+ Default proxy log:
69
+
70
+ ```text
71
+ /tmp/openclaw/atools-compat-proxy.log
72
+ ```
73
+
74
+ ## Uninstall
75
+
76
+ ```bash
77
+ ./scripts/uninstall.sh
78
+ ```
79
+
80
+ ## License
81
+
82
+ MIT
package/bin/cli.mjs ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ import { createProxyServer } from '../lib/proxy-server.mjs';
3
+ import { runConfigOpenclaw } from '../lib/config-openclaw.mjs';
4
+
5
+ function parseArgs(argv) {
6
+ const out = {
7
+ cmd: 'serve',
8
+ openclawHome: undefined,
9
+ port: undefined,
10
+ upstream: undefined,
11
+ fallbackUpstream: undefined,
12
+ fallbackApiKey: undefined,
13
+ maxReqBytes: undefined,
14
+ logFile: undefined,
15
+ retryMax: undefined,
16
+ retryBaseMs: undefined
17
+ };
18
+
19
+ if (argv[0] && !argv[0].startsWith('-')) {
20
+ out.cmd = argv.shift();
21
+ }
22
+
23
+ for (let i = 0; i < argv.length; i += 1) {
24
+ const a = argv[i];
25
+ const next = () => argv[++i];
26
+
27
+ if (a === '--port') out.port = Number(next());
28
+ else if (a === '--openclaw-home') out.openclawHome = next();
29
+ else if (a === '--upstream') out.upstream = next();
30
+ else if (a === '--fallback-upstream') out.fallbackUpstream = next();
31
+ else if (a === '--fallback-api-key') out.fallbackApiKey = next();
32
+ else if (a === '--max-req-bytes') out.maxReqBytes = Number(next());
33
+ else if (a === '--log-file') out.logFile = next();
34
+ else if (a === '--retry-max') out.retryMax = Number(next());
35
+ else if (a === '--retry-base-ms') out.retryBaseMs = Number(next());
36
+ else if (a === '--help' || a === '-h') out.help = true;
37
+ else throw new Error(`unknown arg: ${a}`);
38
+ }
39
+ return out;
40
+ }
41
+
42
+ function printHelp() {
43
+ process.stdout.write(`atools-tool
44
+
45
+ Usage:
46
+ atools-tool <command> [options]
47
+
48
+ Commands:
49
+ serve Run proxy service
50
+ config-openclaw Interactive OpenClaw model/reasoning/thinking/key setup
51
+
52
+ Options:
53
+ --openclaw-home <path> OpenClaw home dir (default: ~/.openclaw)
54
+ --port <number> Listen port (default: 18888)
55
+ --upstream <url> Upstream base URL (default: https://sub2api.atools.live)
56
+ --fallback-upstream <url> Fallback base URL on primary 5xx
57
+ --fallback-api-key <key> Override Bearer key for fallback upstream
58
+ --max-req-bytes <number> Compact requests larger than this threshold (default: 68000)
59
+ --log-file <path> Log file (default: /tmp/openclaw/atools-compat-proxy.log)
60
+ --retry-max <number> Retry attempts on 5xx for /responses (default: 6)
61
+ --retry-base-ms <number> Retry base backoff ms (default: 300)
62
+ -h, --help Show help
63
+ `);
64
+ }
65
+
66
+ async function main() {
67
+ const args = parseArgs(process.argv.slice(2));
68
+
69
+ if (args.help) {
70
+ printHelp();
71
+ return;
72
+ }
73
+
74
+ if (args.cmd === 'config-openclaw') {
75
+ await runConfigOpenclaw({ openclawHome: args.openclawHome });
76
+ return;
77
+ }
78
+
79
+ if (args.cmd !== 'serve') {
80
+ throw new Error(`unsupported command: ${args.cmd}`);
81
+ }
82
+
83
+ const { port, upstream, fallbackUpstream, logFile } = await createProxyServer(args);
84
+ const fallbackNote = fallbackUpstream ? `, fallback=${fallbackUpstream}` : '';
85
+ process.stdout.write(`atools-tool listening on http://127.0.0.1:${port}, upstream=${upstream}${fallbackNote}, log=${logFile}\n`);
86
+ }
87
+
88
+ main().catch((err) => {
89
+ process.stderr.write(`${String(err?.stack || err)}\n`);
90
+ process.exit(1);
91
+ });
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { runInstallCli } from '../lib/install.mjs';
3
+
4
+ runInstallCli();
@@ -0,0 +1,174 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import process from 'node:process';
5
+ import readline from 'node:readline/promises';
6
+
7
+ const PROVIDER = 'sub2api';
8
+ const MODEL_OPTIONS = ['gpt-5.2', 'gpt-5.3-codex', 'gpt-5.4'];
9
+ const THINKING_OPTIONS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'];
10
+
11
+ function readJson(filePath) {
12
+ if (!fs.existsSync(filePath)) {
13
+ throw new Error(`missing file: ${filePath}`);
14
+ }
15
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
16
+ }
17
+
18
+ function writeJson(filePath, data) {
19
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
20
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
21
+ }
22
+
23
+ function backup(filePath) {
24
+ if (!fs.existsSync(filePath)) return;
25
+ const d = new Date();
26
+ const p = (n) => String(n).padStart(2, '0');
27
+ const stamp = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
28
+ fs.copyFileSync(filePath, `${filePath}.bak.${stamp}`);
29
+ }
30
+
31
+ async function askChoice(rl, question, options, defaultIndex = 0) {
32
+ while (true) {
33
+ process.stdout.write(`\n${question}\n`);
34
+ options.forEach((item, idx) => {
35
+ const mark = idx === defaultIndex ? ' (default)' : '';
36
+ process.stdout.write(`${idx + 1}. ${item}${mark}\n`);
37
+ });
38
+ const input = (await rl.question('请选择编号: ')).trim();
39
+ if (!input) return options[defaultIndex];
40
+ const n = Number(input);
41
+ if (Number.isInteger(n) && n >= 1 && n <= options.length) {
42
+ return options[n - 1];
43
+ }
44
+ process.stdout.write('输入无效,请重新输入。\n');
45
+ }
46
+ }
47
+
48
+ async function askKey(rl, currentKey) {
49
+ const masked = currentKey ? `${currentKey.slice(0, 6)}...${currentKey.slice(-4)}` : '未设置';
50
+ process.stdout.write(`\n4) 输入 API Key(当前: ${masked})\n`);
51
+ process.stdout.write('留空表示保持当前 key\n');
52
+ const input = (await rl.question('请输入 key: ')).trim();
53
+ return input || currentKey || '';
54
+ }
55
+
56
+ function ensureModel(providerObj, modelId, reasoning) {
57
+ providerObj.models = providerObj.models || [];
58
+ let model = providerObj.models.find((m) => m && m.id === modelId);
59
+ if (!model) {
60
+ model = {
61
+ id: modelId,
62
+ name: modelId,
63
+ api: 'openai-responses',
64
+ reasoning: false,
65
+ input: ['text'],
66
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
67
+ contextWindow: 200000,
68
+ maxTokens: 8192
69
+ };
70
+ providerObj.models.push(model);
71
+ }
72
+ model.reasoning = reasoning;
73
+ model.contextWindow = model.contextWindow || 200000;
74
+ model.maxTokens = model.maxTokens || 8192;
75
+ model.api = model.api || 'openai-responses';
76
+ }
77
+
78
+ export async function runConfigOpenclaw({ openclawHome } = {}) {
79
+ const home = openclawHome || process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw');
80
+ const openclawJsonPath = path.join(home, 'openclaw.json');
81
+ const modelsJsonPath = path.join(home, 'agents/main/agent/models.json');
82
+ const authProfilesPath = path.join(home, 'agents/main/agent/auth-profiles.json');
83
+
84
+ const openclawCfg = readJson(openclawJsonPath);
85
+ const modelsCfg = readJson(modelsJsonPath);
86
+ let authCfg = fs.existsSync(authProfilesPath)
87
+ ? readJson(authProfilesPath)
88
+ : { version: 1, profiles: {}, lastGood: {}, usageStats: {} };
89
+
90
+ const rl = readline.createInterface({
91
+ input: process.stdin,
92
+ output: process.stdout
93
+ });
94
+
95
+ try {
96
+ process.stdout.write(`OpenClaw Home: ${home}\n`);
97
+
98
+ const selectedModel = await askChoice(rl, '1) 选择默认模型', MODEL_OPTIONS, 1);
99
+ const reasoningSwitch = await askChoice(rl, '2) 配置推理开关', ['off', 'on'], 0);
100
+ const selectedThinking = await askChoice(rl, '3) 思考强度', THINKING_OPTIONS, 2);
101
+ const reasoning = reasoningSwitch === 'on';
102
+ const currentKey =
103
+ openclawCfg?.models?.providers?.[PROVIDER]?.apiKey ||
104
+ modelsCfg?.providers?.[PROVIDER]?.apiKey ||
105
+ authCfg?.profiles?.[`${PROVIDER}:default`]?.key ||
106
+ '';
107
+ const selectedKey = await askKey(rl, currentKey);
108
+
109
+ openclawCfg.models = openclawCfg.models || {};
110
+ openclawCfg.models.providers = openclawCfg.models.providers || {};
111
+ openclawCfg.models.providers[PROVIDER] = openclawCfg.models.providers[PROVIDER] || {};
112
+ openclawCfg.models.providers[PROVIDER].api = 'openai-responses';
113
+ openclawCfg.models.providers[PROVIDER].authHeader = true;
114
+ if (selectedKey) {
115
+ openclawCfg.models.providers[PROVIDER].apiKey = selectedKey;
116
+ openclawCfg.models.providers[PROVIDER].auth = 'api-key';
117
+ }
118
+ ensureModel(openclawCfg.models.providers[PROVIDER], selectedModel, reasoning);
119
+
120
+ openclawCfg.agents = openclawCfg.agents || {};
121
+ openclawCfg.agents.defaults = openclawCfg.agents.defaults || {};
122
+ openclawCfg.agents.defaults.model = openclawCfg.agents.defaults.model || {};
123
+ openclawCfg.agents.defaults.model.primary = `${PROVIDER}/${selectedModel}`;
124
+ openclawCfg.agents.defaults.thinkingDefault = selectedThinking;
125
+ openclawCfg.agents.defaults.models = openclawCfg.agents.defaults.models || {};
126
+ openclawCfg.agents.defaults.models[`${PROVIDER}/${selectedModel}`] =
127
+ openclawCfg.agents.defaults.models[`${PROVIDER}/${selectedModel}`] || { alias: `ATools ${selectedModel}` };
128
+
129
+ modelsCfg.providers = modelsCfg.providers || {};
130
+ modelsCfg.providers[PROVIDER] = modelsCfg.providers[PROVIDER] || {};
131
+ modelsCfg.providers[PROVIDER].api = 'openai-responses';
132
+ modelsCfg.providers[PROVIDER].authHeader = true;
133
+ if (selectedKey) {
134
+ modelsCfg.providers[PROVIDER].apiKey = selectedKey;
135
+ modelsCfg.providers[PROVIDER].auth = 'api-key';
136
+ }
137
+ ensureModel(modelsCfg.providers[PROVIDER], selectedModel, reasoning);
138
+
139
+ authCfg.version = authCfg.version || 1;
140
+ authCfg.profiles = authCfg.profiles || {};
141
+ authCfg.lastGood = authCfg.lastGood || {};
142
+ authCfg.usageStats = authCfg.usageStats || {};
143
+ if (selectedKey) {
144
+ authCfg.profiles[`${PROVIDER}:default`] = {
145
+ type: 'api_key',
146
+ provider: PROVIDER,
147
+ key: selectedKey
148
+ };
149
+ authCfg.lastGood[PROVIDER] = `${PROVIDER}:default`;
150
+ authCfg.usageStats[`${PROVIDER}:default`] = authCfg.usageStats[`${PROVIDER}:default`] || {
151
+ errorCount: 0,
152
+ lastUsed: Date.now()
153
+ };
154
+ }
155
+
156
+ backup(openclawJsonPath);
157
+ backup(modelsJsonPath);
158
+ backup(authProfilesPath);
159
+ writeJson(openclawJsonPath, openclawCfg);
160
+ writeJson(modelsJsonPath, modelsCfg);
161
+ writeJson(authProfilesPath, authCfg);
162
+
163
+ process.stdout.write('\n配置已更新:\n');
164
+ process.stdout.write(`- 默认模型: ${PROVIDER}/${selectedModel}\n`);
165
+ process.stdout.write(`- 推理开关: ${reasoning ? 'on' : 'off'}\n`);
166
+ process.stdout.write(`- 思考强度: ${selectedThinking}\n`);
167
+ process.stdout.write(`- API Key: ${selectedKey ? `${selectedKey.slice(0, 6)}...${selectedKey.slice(-4)}` : '未设置'}\n`);
168
+ process.stdout.write(`- 已写入: ${openclawJsonPath}\n`);
169
+ process.stdout.write(`- 已写入: ${modelsJsonPath}\n`);
170
+ process.stdout.write(`- 已写入: ${authProfilesPath}\n`);
171
+ } finally {
172
+ rl.close();
173
+ }
174
+ }
@@ -0,0 +1,405 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const DEFAULT_PROVIDER = 'sub2api';
8
+ const DEFAULT_MODEL = 'gpt-5.3-codex';
9
+ const DEFAULT_PORT = 18888;
10
+ const DEFAULT_UPSTREAM = 'https://sub2api.atools.live';
11
+ const DEFAULT_FALLBACK_UPSTREAM = 'https://gmn.chuangzuoli.com/v1';
12
+ const DEFAULT_LOG_FILE = '/tmp/openclaw/atools-compat-proxy.log';
13
+ const DEFAULT_SERVICE_NAME = 'openclaw-atools-proxy.service';
14
+ const OPENCLAW_GATEWAY_SERVICE = 'openclaw-gateway.service';
15
+
16
+ function info(msg) {
17
+ process.stdout.write(`[atools-tool] ${msg}\n`);
18
+ }
19
+
20
+ function fail(msg) {
21
+ process.stderr.write(`[atools-tool] ERROR: ${msg}\n`);
22
+ }
23
+
24
+ function nowStamp() {
25
+ const d = new Date();
26
+ const p = (n) => String(n).padStart(2, '0');
27
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
28
+ }
29
+
30
+ function readJsonIfExists(filePath) {
31
+ if (!fs.existsSync(filePath)) return null;
32
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
33
+ }
34
+
35
+ function writeJson(filePath, data) {
36
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
37
+ }
38
+
39
+ function backup(filePath) {
40
+ if (!fs.existsSync(filePath)) return null;
41
+ const dest = `${filePath}.bak.${nowStamp()}`;
42
+ fs.copyFileSync(filePath, dest);
43
+ return dest;
44
+ }
45
+
46
+ function ensureDir(dir) {
47
+ fs.mkdirSync(dir, { recursive: true });
48
+ }
49
+
50
+ function parseArgs(argv) {
51
+ const out = {
52
+ openclawHome: process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw'),
53
+ provider: DEFAULT_PROVIDER,
54
+ model: DEFAULT_MODEL,
55
+ port: DEFAULT_PORT,
56
+ upstream: DEFAULT_UPSTREAM,
57
+ fallbackUpstream: DEFAULT_FALLBACK_UPSTREAM,
58
+ maxReqBytes: 68000,
59
+ logFile: DEFAULT_LOG_FILE,
60
+ serviceName: DEFAULT_SERVICE_NAME,
61
+ apiKey: process.env.SUB_OAI_KEY || '',
62
+ fallbackApiKey: process.env.FALLBACK_OAI_KEY || '',
63
+ dryRun: false,
64
+ noRestart: false
65
+ };
66
+
67
+ for (let i = 0; i < argv.length; i += 1) {
68
+ const a = argv[i];
69
+ const next = () => argv[++i];
70
+
71
+ if (a === '--openclaw-home') out.openclawHome = next();
72
+ else if (a === '--provider') out.provider = next();
73
+ else if (a === '--model') out.model = next();
74
+ else if (a === '--port') out.port = Number(next());
75
+ else if (a === '--upstream') out.upstream = next();
76
+ else if (a === '--fallback-upstream') out.fallbackUpstream = next();
77
+ else if (a === '--max-req-bytes') out.maxReqBytes = Number(next());
78
+ else if (a === '--log-file') out.logFile = next();
79
+ else if (a === '--service-name') out.serviceName = next();
80
+ else if (a === '--api-key') out.apiKey = next();
81
+ else if (a === '--fallback-api-key') out.fallbackApiKey = next();
82
+ else if (a === '--dry-run') out.dryRun = true;
83
+ else if (a === '--no-restart') out.noRestart = true;
84
+ else if (a === '--help' || a === '-h') {
85
+ out.help = true;
86
+ } else {
87
+ throw new Error(`unknown arg: ${a}`);
88
+ }
89
+ }
90
+
91
+ if (!Number.isFinite(out.port) || out.port <= 0) {
92
+ throw new Error(`invalid --port: ${out.port}`);
93
+ }
94
+ return out;
95
+ }
96
+
97
+ function renderSystemdService({
98
+ serviceName,
99
+ nodeBin,
100
+ cliPath,
101
+ port,
102
+ upstream,
103
+ fallbackUpstream,
104
+ maxReqBytes,
105
+ fallbackApiKey,
106
+ logFile
107
+ }) {
108
+ const fallbackEnv = fallbackUpstream
109
+ ? `Environment=SUB2API_FALLBACK_UPSTREAM=${fallbackUpstream}\nEnvironment=SUB2API_FALLBACK_API_KEY=${fallbackApiKey || ''}\n`
110
+ : '';
111
+ return `[Unit]
112
+ Description=OpenClaw ATools Compatibility Proxy
113
+ After=network-online.target
114
+ Wants=network-online.target
115
+
116
+ [Service]
117
+ ExecStart=${nodeBin} ${cliPath} serve --port ${port} --upstream ${upstream} --log-file ${logFile}
118
+ Restart=always
119
+ RestartSec=2
120
+ Environment=HOME=${os.homedir()}
121
+ Environment=TMPDIR=/tmp
122
+ Environment=SUB2API_COMPAT_PORT=${port}
123
+ Environment=SUB2API_UPSTREAM=${upstream}
124
+ Environment=SUB2API_COMPAT_LOG=${logFile}
125
+ Environment=SUB2API_COMPAT_MAX_REQ_BYTES=${maxReqBytes}
126
+ Environment="SUB2API_COMPAT_USER_AGENT=codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal"
127
+ ${fallbackEnv}
128
+
129
+ [Install]
130
+ WantedBy=default.target
131
+ `;
132
+ }
133
+
134
+ function detectApiKey(openclawHome, explicitKey) {
135
+ if (explicitKey) return explicitKey;
136
+ const authPath = path.join(openclawHome, 'agents/main/agent/auth-profiles.json');
137
+ const auth = readJsonIfExists(authPath);
138
+ return auth?.profiles?.['sub2api:default']?.key || '';
139
+ }
140
+
141
+ function detectFallbackApiKey(openclawHome, explicitKey) {
142
+ if (explicitKey) return explicitKey;
143
+ const cfgPath = path.join(openclawHome, 'openclaw.json');
144
+ const cfg = readJsonIfExists(cfgPath);
145
+ return cfg?.models?.providers?.gmn?.apiKey || '';
146
+ }
147
+
148
+ function patchOpenclawConfig(openclawHome, provider, model, port, dryRun) {
149
+ const cfgPath = path.join(openclawHome, 'openclaw.json');
150
+ const cfg = readJsonIfExists(cfgPath);
151
+ if (!cfg) {
152
+ throw new Error(`missing openclaw config: ${cfgPath}`);
153
+ }
154
+
155
+ const baseUrl = `http://127.0.0.1:${port}/v1`;
156
+ cfg.models = cfg.models || {};
157
+ cfg.models.providers = cfg.models.providers || {};
158
+ cfg.models.providers[provider] = cfg.models.providers[provider] || {};
159
+
160
+ const providerObj = cfg.models.providers[provider];
161
+ providerObj.baseUrl = baseUrl;
162
+ providerObj.api = 'openai-responses';
163
+ providerObj.authHeader = true;
164
+ providerObj.headers = providerObj.headers || {};
165
+ providerObj.headers['User-Agent'] = 'codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal';
166
+ providerObj.headers.Accept = 'application/json';
167
+ providerObj.models = providerObj.models || [];
168
+
169
+ let exists = providerObj.models.find((m) => m && m.id === model);
170
+ if (!exists) {
171
+ exists = {
172
+ id: model,
173
+ name: model,
174
+ api: 'openai-responses',
175
+ reasoning: false,
176
+ input: ['text'],
177
+ cost: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0.175 },
178
+ contextWindow: 200000,
179
+ maxTokens: 8192
180
+ };
181
+ providerObj.models.push(exists);
182
+ }
183
+ exists.contextWindow = 200000;
184
+ exists.maxTokens = 8192;
185
+ exists.reasoning = false;
186
+
187
+ cfg.agents = cfg.agents || {};
188
+ cfg.agents.defaults = cfg.agents.defaults || {};
189
+ cfg.agents.defaults.model = cfg.agents.defaults.model || {};
190
+ cfg.agents.defaults.model.primary = `${provider}/${model}`;
191
+ cfg.agents.defaults.thinkingDefault = 'low';
192
+ cfg.agents.defaults.models = cfg.agents.defaults.models || {};
193
+ cfg.agents.defaults.models[`${provider}/${model}`] = cfg.agents.defaults.models[`${provider}/${model}`] || { alias: model };
194
+
195
+ if (dryRun) {
196
+ info(`dry-run: would patch ${cfgPath}`);
197
+ return;
198
+ }
199
+ const bak = backup(cfgPath);
200
+ if (bak) info(`backup created: ${bak}`);
201
+ writeJson(cfgPath, cfg);
202
+ info(`patched: ${cfgPath}`);
203
+ }
204
+
205
+ function patchAgentModels(openclawHome, provider, model, port, apiKey, dryRun) {
206
+ const modelsPath = path.join(openclawHome, 'agents/main/agent/models.json');
207
+ const models = readJsonIfExists(modelsPath);
208
+ if (!models) {
209
+ throw new Error(`missing agent models config: ${modelsPath}`);
210
+ }
211
+
212
+ const baseUrl = `http://127.0.0.1:${port}/v1`;
213
+ models.providers = models.providers || {};
214
+ models.providers[provider] = models.providers[provider] || {};
215
+ const providerObj = models.providers[provider];
216
+
217
+ providerObj.baseUrl = baseUrl;
218
+ providerObj.api = 'openai-responses';
219
+ providerObj.authHeader = true;
220
+ providerObj.headers = providerObj.headers || {};
221
+ providerObj.headers['User-Agent'] = 'codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal';
222
+ providerObj.headers.Accept = 'application/json';
223
+ providerObj.models = providerObj.models || [];
224
+
225
+ let exists = providerObj.models.find((m) => m && m.id === model);
226
+ if (!exists) {
227
+ exists = {
228
+ id: model,
229
+ name: model,
230
+ api: 'openai-responses',
231
+ reasoning: false,
232
+ input: ['text'],
233
+ cost: { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0.175 },
234
+ contextWindow: 200000,
235
+ maxTokens: 8192
236
+ };
237
+ providerObj.models.push(exists);
238
+ }
239
+ exists.contextWindow = 200000;
240
+ exists.maxTokens = 8192;
241
+ exists.reasoning = false;
242
+
243
+ if (apiKey) {
244
+ providerObj.apiKey = apiKey;
245
+ }
246
+
247
+ if (dryRun) {
248
+ info(`dry-run: would patch ${modelsPath}`);
249
+ return;
250
+ }
251
+ const bak = backup(modelsPath);
252
+ if (bak) info(`backup created: ${bak}`);
253
+ writeJson(modelsPath, models);
254
+ info(`patched: ${modelsPath}`);
255
+ }
256
+
257
+ function patchAuthProfiles(openclawHome, provider, apiKey, dryRun) {
258
+ if (!apiKey) {
259
+ info('no api key provided/detected, skip auth-profiles patch');
260
+ return;
261
+ }
262
+
263
+ const authPath = path.join(openclawHome, 'agents/main/agent/auth-profiles.json');
264
+ const auth = readJsonIfExists(authPath) || { version: 1, profiles: {}, lastGood: {}, usageStats: {} };
265
+
266
+ auth.profiles = auth.profiles || {};
267
+ auth.lastGood = auth.lastGood || {};
268
+ auth.usageStats = auth.usageStats || {};
269
+
270
+ auth.profiles[`${provider}:default`] = {
271
+ type: 'api_key',
272
+ provider,
273
+ key: apiKey
274
+ };
275
+ auth.lastGood[provider] = `${provider}:default`;
276
+ auth.usageStats[`${provider}:default`] = auth.usageStats[`${provider}:default`] || { errorCount: 0, lastUsed: Date.now() };
277
+
278
+ if (dryRun) {
279
+ info(`dry-run: would patch ${authPath}`);
280
+ return;
281
+ }
282
+
283
+ ensureDir(path.dirname(authPath));
284
+ if (fs.existsSync(authPath)) {
285
+ const bak = backup(authPath);
286
+ if (bak) info(`backup created: ${bak}`);
287
+ }
288
+ writeJson(authPath, auth);
289
+ info(`patched: ${authPath}`);
290
+ }
291
+
292
+ function runSystemctl(args, { dryRun } = {}) {
293
+ if (dryRun) {
294
+ info(`dry-run: systemctl --user ${args.join(' ')}`);
295
+ return;
296
+ }
297
+ execFileSync('systemctl', ['--user', ...args], { stdio: 'inherit' });
298
+ }
299
+
300
+ function installService({ serviceName, content, dryRun }) {
301
+ const serviceDir = path.join(os.homedir(), '.config/systemd/user');
302
+ const servicePath = path.join(serviceDir, serviceName);
303
+ ensureDir(serviceDir);
304
+
305
+ if (dryRun) {
306
+ info(`dry-run: would write ${servicePath}`);
307
+ } else {
308
+ if (fs.existsSync(servicePath)) {
309
+ const bak = backup(servicePath);
310
+ if (bak) info(`backup created: ${bak}`);
311
+ }
312
+ fs.writeFileSync(servicePath, content, 'utf8');
313
+ info(`wrote: ${servicePath}`);
314
+ }
315
+
316
+ runSystemctl(['daemon-reload'], { dryRun });
317
+ runSystemctl(['enable', '--now', serviceName], { dryRun });
318
+ }
319
+
320
+ function restartOpenclawGateway({ noRestart, dryRun }) {
321
+ if (noRestart) {
322
+ info('skip gateway restart (--no-restart)');
323
+ return;
324
+ }
325
+
326
+ try {
327
+ runSystemctl(['restart', OPENCLAW_GATEWAY_SERVICE], { dryRun });
328
+ } catch {
329
+ info(`gateway service not restarted (service may not exist): ${OPENCLAW_GATEWAY_SERVICE}`);
330
+ }
331
+ }
332
+
333
+ function printHelp() {
334
+ process.stdout.write(`atools-tool-install
335
+
336
+ Usage:
337
+ atools-tool-install [options]
338
+
339
+ Options:
340
+ --openclaw-home <path> Default: ~/.openclaw
341
+ --provider <name> Default: sub2api
342
+ --model <id> Default: gpt-5.3-codex
343
+ --port <number> Default: 18888
344
+ --upstream <url> Default: https://sub2api.atools.live
345
+ --fallback-upstream <url> Default: https://gmn.chuangzuoli.com/v1
346
+ --max-req-bytes <number> Default: 68000
347
+ --log-file <path> Default: /tmp/openclaw/atools-compat-proxy.log
348
+ --service-name <name> Default: openclaw-atools-proxy.service
349
+ --api-key <key> Prefer explicit key; fallback env SUB_OAI_KEY then auth-profiles
350
+ --fallback-api-key <key> Prefer explicit key; fallback env FALLBACK_OAI_KEY then openclaw gmn key
351
+ --no-restart Do not restart openclaw-gateway.service
352
+ --dry-run Print actions without changing files
353
+ -h, --help Show help
354
+ `);
355
+ }
356
+
357
+ export async function runInstall(rawArgs = process.argv.slice(2)) {
358
+ const args = parseArgs(rawArgs);
359
+
360
+ if (args.help) {
361
+ printHelp();
362
+ return;
363
+ }
364
+
365
+ const nodeBin = process.execPath;
366
+ const cliPath = fileURLToPath(new URL('../bin/cli.mjs', import.meta.url));
367
+ const apiKey = detectApiKey(args.openclawHome, args.apiKey);
368
+ const fallbackApiKey = detectFallbackApiKey(args.openclawHome, args.fallbackApiKey);
369
+
370
+ info(`openclawHome=${args.openclawHome}`);
371
+ info(`provider/model=${args.provider}/${args.model}`);
372
+ info(`proxy=http://127.0.0.1:${args.port}/v1 -> ${args.upstream}`);
373
+ if (args.fallbackUpstream) {
374
+ info(`fallback=${args.fallbackUpstream}`);
375
+ }
376
+
377
+ patchOpenclawConfig(args.openclawHome, args.provider, args.model, args.port, args.dryRun);
378
+ patchAgentModels(args.openclawHome, args.provider, args.model, args.port, apiKey, args.dryRun);
379
+ patchAuthProfiles(args.openclawHome, args.provider, apiKey, args.dryRun);
380
+
381
+ const serviceContent = renderSystemdService({
382
+ serviceName: args.serviceName,
383
+ nodeBin,
384
+ cliPath,
385
+ port: args.port,
386
+ upstream: args.upstream,
387
+ fallbackUpstream: args.fallbackUpstream,
388
+ maxReqBytes: args.maxReqBytes,
389
+ fallbackApiKey,
390
+ logFile: args.logFile
391
+ });
392
+
393
+ installService({ serviceName: args.serviceName, content: serviceContent, dryRun: args.dryRun });
394
+ restartOpenclawGateway({ noRestart: args.noRestart, dryRun: args.dryRun });
395
+
396
+ info('done');
397
+ info(`check proxy logs: ${args.logFile}`);
398
+ }
399
+
400
+ export function runInstallCli() {
401
+ runInstall().catch((err) => {
402
+ fail(String(err?.stack || err?.message || err));
403
+ process.exit(1);
404
+ });
405
+ }
@@ -0,0 +1,281 @@
1
+ import http from 'node:http';
2
+ import fs from 'node:fs';
3
+
4
+ const DEFAULT_PORT = 18888;
5
+ const DEFAULT_UPSTREAM = 'https://sub2api.atools.live';
6
+ const DEFAULT_LOG = '/tmp/openclaw/atools-compat-proxy.log';
7
+ const DEFAULT_USER_AGENT = 'codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal';
8
+ const DEFAULT_FALLBACK_UPSTREAM = '';
9
+ const DEFAULT_FALLBACK_API_KEY = '';
10
+ const DEFAULT_MAX_REQ_BYTES = 68000;
11
+ const DEFAULT_DROP_TOOLS_ON_COMPACT = false;
12
+
13
+ function ensureParentDir(filePath) {
14
+ const dir = filePath.replace(/\/[^/]*$/, '');
15
+ if (dir) {
16
+ fs.mkdirSync(dir, { recursive: true });
17
+ }
18
+ }
19
+
20
+ function appendLog(logFile, obj) {
21
+ try {
22
+ ensureParentDir(logFile);
23
+ const line = `${new Date().toISOString()} ${JSON.stringify(obj)}\n`;
24
+ fs.appendFileSync(logFile, line);
25
+ } catch {
26
+ // ignore logging failures
27
+ }
28
+ }
29
+
30
+ function normalizeContent(content) {
31
+ const toInputText = (text) => ({ type: 'input_text', text: String(text ?? '') });
32
+
33
+ if (typeof content === 'string') return [toInputText(content)];
34
+
35
+ if (Array.isArray(content)) {
36
+ return content.map((part) => {
37
+ if (typeof part === 'string') return toInputText(part);
38
+ if (!part || typeof part !== 'object') return toInputText('');
39
+
40
+ if (part.type === 'text' || part.type === 'output_text') {
41
+ return toInputText(part.text);
42
+ }
43
+ if (part.type === 'input_text') {
44
+ return { ...part, text: String(part.text ?? '') };
45
+ }
46
+ return part;
47
+ });
48
+ }
49
+
50
+ if (content && typeof content === 'object' && 'text' in content) {
51
+ return [toInputText(content.text)];
52
+ }
53
+
54
+ return [toInputText('')];
55
+ }
56
+
57
+ function normalizeResponsesPayload(payload) {
58
+ if (!payload || typeof payload !== 'object') return payload;
59
+ const out = { ...payload };
60
+
61
+ // Some upstream gateways reject/unstable with this field.
62
+ if ('previous_response_id' in out) delete out.previous_response_id;
63
+
64
+ if (typeof out.input === 'string') {
65
+ out.input = [{ role: 'user', content: normalizeContent(out.input) }];
66
+ } else if (Array.isArray(out.input)) {
67
+ out.input = out.input.map((item) => {
68
+ if (!item || typeof item !== 'object') return item;
69
+ if ('role' in item || item.type === 'message') {
70
+ const role = typeof item.role === 'string' ? item.role : 'user';
71
+ return { role, content: normalizeContent(item.content) };
72
+ }
73
+ return item;
74
+ });
75
+ }
76
+
77
+ return out;
78
+ }
79
+
80
+ function trimTextPart(part, maxChars) {
81
+ if (!part || typeof part !== 'object') return part;
82
+ if (part.type !== 'input_text') return part;
83
+ const text = String(part.text ?? '');
84
+ return { ...part, text: text.length > maxChars ? text.slice(0, maxChars) : text };
85
+ }
86
+
87
+ function compactResponsesPayload(payload, { dropToolsOnCompact = DEFAULT_DROP_TOOLS_ON_COMPACT } = {}) {
88
+ if (!payload || typeof payload !== 'object') return payload;
89
+ const out = { ...payload };
90
+
91
+ if (dropToolsOnCompact && Array.isArray(out.tools)) delete out.tools;
92
+ if (typeof out.instructions === 'string' && out.instructions.length > 4000) {
93
+ out.instructions = out.instructions.slice(0, 4000);
94
+ }
95
+
96
+ if (Array.isArray(out.input)) {
97
+ const normalized = out.input.map((item) => {
98
+ if (!item || typeof item !== 'object') return item;
99
+ if ('role' in item || item.type === 'message') {
100
+ return {
101
+ role: typeof item.role === 'string' ? item.role : 'user',
102
+ content: normalizeContent(item.content).map((part) => trimTextPart(part, 4000))
103
+ };
104
+ }
105
+ return item;
106
+ });
107
+ const systemMsgs = normalized.filter((m) => m && typeof m === 'object' && (m.role === 'system' || m.role === 'developer')).slice(-1);
108
+ const userMsgs = normalized.filter((m) => m && typeof m === 'object' && m.role === 'user').slice(-2);
109
+ const merged = [...systemMsgs, ...userMsgs];
110
+ out.input = merged.length > 0 ? merged : normalized.slice(-1);
111
+ }
112
+
113
+ if ('previous_response_id' in out) delete out.previous_response_id;
114
+ return out;
115
+ }
116
+
117
+ function sleep(ms) {
118
+ return new Promise((resolve) => setTimeout(resolve, ms));
119
+ }
120
+
121
+ async function forward(url, req, headers, body) {
122
+ return fetch(url, {
123
+ method: req.method,
124
+ headers,
125
+ body: ['GET', 'HEAD'].includes(req.method || '') ? undefined : body,
126
+ redirect: 'manual'
127
+ });
128
+ }
129
+
130
+ function buildHeaders(baseHeaders, bodyLen, apiKeyOverride = '') {
131
+ const headers = { ...baseHeaders };
132
+ headers['content-type'] = 'application/json';
133
+ headers['content-length'] = String(bodyLen);
134
+ if (apiKeyOverride) {
135
+ headers.authorization = `Bearer ${apiKeyOverride}`;
136
+ }
137
+ return headers;
138
+ }
139
+
140
+ export async function createProxyServer(options = {}) {
141
+ const port = Number(options.port ?? process.env.SUB2API_COMPAT_PORT ?? DEFAULT_PORT);
142
+ const upstream = options.upstream ?? process.env.SUB2API_UPSTREAM ?? DEFAULT_UPSTREAM;
143
+ const logFile = options.logFile ?? process.env.SUB2API_COMPAT_LOG ?? DEFAULT_LOG;
144
+ const retryMax = Number(options.retryMax ?? process.env.SUB2API_COMPAT_RETRY_MAX ?? 6);
145
+ const retryBaseMs = Number(options.retryBaseMs ?? process.env.SUB2API_COMPAT_RETRY_BASE_MS ?? 300);
146
+ const userAgent = options.userAgent ?? process.env.SUB2API_COMPAT_USER_AGENT ?? DEFAULT_USER_AGENT;
147
+ const fallbackUpstream = options.fallbackUpstream ?? process.env.SUB2API_FALLBACK_UPSTREAM ?? DEFAULT_FALLBACK_UPSTREAM;
148
+ const fallbackApiKey = options.fallbackApiKey ?? process.env.SUB2API_FALLBACK_API_KEY ?? DEFAULT_FALLBACK_API_KEY;
149
+ const maxReqBytes = Number(options.maxReqBytes ?? process.env.SUB2API_COMPAT_MAX_REQ_BYTES ?? DEFAULT_MAX_REQ_BYTES);
150
+ const dropToolsOnCompact = options.dropToolsOnCompact
151
+ ?? ['1', 'true', 'yes', 'on'].includes(String(process.env.SUB2API_COMPAT_DROP_TOOLS_ON_COMPACT || '').toLowerCase());
152
+
153
+ const server = http.createServer(async (req, res) => {
154
+ const startAt = Date.now();
155
+ try {
156
+ const url = new URL(req.url || '/', upstream);
157
+ const fallbackUrl = fallbackUpstream ? new URL(req.url || '/', fallbackUpstream) : null;
158
+ const chunks = [];
159
+ for await (const c of req) chunks.push(c);
160
+ const rawBody = Buffer.concat(chunks);
161
+
162
+ const baseHeaders = { ...req.headers };
163
+ delete baseHeaders.host;
164
+ delete baseHeaders.connection;
165
+ baseHeaders['user-agent'] = userAgent;
166
+
167
+ const isResponsesPath = req.method === 'POST' && url.pathname.endsWith('/responses');
168
+ let body = rawBody;
169
+ let compactBody = null;
170
+
171
+ if (isResponsesPath && rawBody.length > 0) {
172
+ try {
173
+ const parsed = JSON.parse(rawBody.toString('utf8'));
174
+ const normalized = normalizeResponsesPayload(parsed);
175
+ body = Buffer.from(JSON.stringify(normalized));
176
+ const compacted = compactResponsesPayload(normalized, { dropToolsOnCompact });
177
+ const compactBuf = Buffer.from(JSON.stringify(compacted));
178
+ if (compactBuf.length + 512 < body.length) {
179
+ compactBody = compactBuf;
180
+ }
181
+ } catch {
182
+ // Keep original body if parse fails.
183
+ }
184
+ }
185
+
186
+ let resp;
187
+ let attempts = 0;
188
+ let via = 'primary';
189
+ let primaryStatus = null;
190
+ let fallbackStatus = null;
191
+ let compacted = false;
192
+ const maxAttempts = isResponsesPath ? retryMax : 1;
193
+ while (attempts < maxAttempts) {
194
+ attempts += 1;
195
+ const shouldCompactNow = Boolean(compactBody && (attempts > 1 || body.length > maxReqBytes));
196
+ const requestBody = shouldCompactNow ? compactBody : body;
197
+ compacted = shouldCompactNow;
198
+ const primaryHeaders = isResponsesPath ? buildHeaders(baseHeaders, requestBody.length) : baseHeaders;
199
+ const fallbackHeaders = isResponsesPath
200
+ ? buildHeaders(baseHeaders, requestBody.length, fallbackApiKey)
201
+ : (fallbackApiKey ? { ...baseHeaders, authorization: `Bearer ${fallbackApiKey}` } : baseHeaders);
202
+
203
+ const primaryResp = await forward(url, req, primaryHeaders, requestBody);
204
+ primaryStatus = primaryResp.status;
205
+ if (primaryResp.status < 500) {
206
+ resp = primaryResp;
207
+ via = 'primary';
208
+ break;
209
+ }
210
+
211
+ if (fallbackUrl) {
212
+ const fbResp = await forward(fallbackUrl, req, fallbackHeaders, requestBody);
213
+ fallbackStatus = fbResp.status;
214
+ if (fbResp.status < 500) {
215
+ resp = fbResp;
216
+ via = 'fallback';
217
+ break;
218
+ }
219
+ resp = fbResp;
220
+ via = 'fallback';
221
+ } else {
222
+ resp = primaryResp;
223
+ via = 'primary';
224
+ }
225
+
226
+ if (attempts < maxAttempts) {
227
+ await sleep(retryBaseMs * attempts);
228
+ }
229
+ }
230
+
231
+ res.statusCode = resp.status;
232
+ resp.headers.forEach((value, key) => {
233
+ if (key.toLowerCase() === 'transfer-encoding') return;
234
+ res.setHeader(key, value);
235
+ });
236
+
237
+ const outBuffer = Buffer.from(await resp.arrayBuffer());
238
+ appendLog(logFile, {
239
+ method: req.method,
240
+ path: url.pathname,
241
+ status: resp.status,
242
+ attempts,
243
+ via,
244
+ primaryStatus,
245
+ fallbackStatus,
246
+ compacted,
247
+ reqBytes: rawBody.length,
248
+ upstreamReqBytes: compactBody && compacted ? compactBody.length : body.length,
249
+ respBytes: outBuffer.length,
250
+ durationMs: Date.now() - startAt
251
+ });
252
+ res.end(outBuffer);
253
+ } catch (err) {
254
+ const message = String(err?.message || err);
255
+ appendLog(logFile, {
256
+ method: req.method,
257
+ path: req.url,
258
+ status: 502,
259
+ error: message,
260
+ durationMs: Date.now() - startAt
261
+ });
262
+ res.statusCode = 502;
263
+ res.setHeader('content-type', 'application/json');
264
+ res.end(JSON.stringify({ error: 'compat-proxy-failed', message }));
265
+ }
266
+ });
267
+
268
+ await new Promise((resolve, reject) => {
269
+ server.once('error', reject);
270
+ server.listen(port, '127.0.0.1', () => resolve());
271
+ });
272
+
273
+ return {
274
+ server,
275
+ port,
276
+ upstream,
277
+ fallbackUpstream,
278
+ logFile,
279
+ close: () => new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())))
280
+ };
281
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@autotask/atools-tool",
3
+ "version": "0.1.2",
4
+ "description": "ATools CLI for OpenClaw proxy compatibility and interactive model configuration",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "aak1247",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/aak1247/sub2api-openclaw-proxy.git"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "homepage": "https://github.com/aak1247/sub2api-openclaw-proxy#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/aak1247/sub2api-openclaw-proxy/issues"
18
+ },
19
+ "keywords": [
20
+ "openclaw",
21
+ "atools",
22
+ "sub2api",
23
+ "openai-responses",
24
+ "proxy",
25
+ "codex"
26
+ ],
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "bin": {
31
+ "atools-tool": "bin/cli.mjs",
32
+ "atools-tool-install": "bin/install.mjs"
33
+ },
34
+ "scripts": {
35
+ "start": "node ./bin/cli.mjs serve",
36
+ "config:openclaw": "node ./bin/cli.mjs config-openclaw",
37
+ "install:local": "node ./bin/install.mjs",
38
+ "pack:check": "npm pack"
39
+ },
40
+ "files": [
41
+ "bin",
42
+ "lib",
43
+ "scripts",
44
+ "README.md",
45
+ "LICENSE"
46
+ ]
47
+ }
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
5
+ cd "$SCRIPT_DIR"
6
+
7
+ if ! command -v node >/dev/null 2>&1; then
8
+ echo "[atools-tool] ERROR: node is required" >&2
9
+ exit 1
10
+ fi
11
+
12
+ node ./bin/install.mjs "$@"
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SERVICE_NAME="${1:-openclaw-atools-proxy.service}"
5
+
6
+ systemctl --user disable --now "$SERVICE_NAME" || true
7
+ rm -f "$HOME/.config/systemd/user/$SERVICE_NAME"
8
+ systemctl --user daemon-reload || true
9
+ systemctl --user restart openclaw-gateway.service || true
10
+
11
+ echo "[atools-tool] removed $SERVICE_NAME"