virouter 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/bin/virouter.mjs +175 -0
- package/package.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# virouter
|
|
2
|
+
|
|
3
|
+
Interactive installer for Claude Code on Windows, macOS and Linux.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx virouter@latest
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
For a local Worker, override the origin before running the installer:
|
|
10
|
+
|
|
11
|
+
```powershell
|
|
12
|
+
$env:VIROUTER_API_ORIGIN="http://127.0.0.1:8787"
|
|
13
|
+
npx virouter@latest
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The installer asks for a `viro_...` API key and configures:
|
|
17
|
+
|
|
18
|
+
- `ANTHROPIC_BASE_URL=https://virouter-production.nplinh191202.workers.dev`
|
|
19
|
+
- `ANTHROPIC_API_KEY`
|
|
20
|
+
- `ANTHROPIC_MODEL=viro-max`
|
|
21
|
+
- `ANTHROPIC_SMALL_FAST_MODEL=viro-speed`
|
|
22
|
+
|
|
23
|
+
Then open a new terminal and run:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
claude
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
It does not install a local server or a model. The gateway remains hosted at
|
|
30
|
+
`https://virouter-production.nplinh191202.workers.dev`.
|
package/bin/virouter.mjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { createInterface } from 'node:readline/promises';
|
|
8
|
+
|
|
9
|
+
const API_ORIGIN = (process.env.VIROUTER_API_ORIGIN || 'https://virouter-production.nplinh191202.workers.dev').replace(/\/$/, '');
|
|
10
|
+
const MARKER_START = '# >>> VIROUTER CLAUDE CODE CONFIG >>>';
|
|
11
|
+
const MARKER_END = '# <<< VIROUTER CLAUDE CODE CONFIG <<<';
|
|
12
|
+
|
|
13
|
+
function detectedOs() {
|
|
14
|
+
if (process.platform === 'win32') return '1';
|
|
15
|
+
if (process.platform === 'darwin') return '2';
|
|
16
|
+
return '3';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function printHelp() {
|
|
20
|
+
console.log(`VIROUTER Claude Code installer
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
npx virouter@latest
|
|
24
|
+
npx virouter@latest --help
|
|
25
|
+
|
|
26
|
+
The installer configures Claude Code to use ${API_ORIGIN}.
|
|
27
|
+
For local testing, set VIROUTER_API_ORIGIN=http://127.0.0.1:8787.
|
|
28
|
+
It does not install a server or store a local model.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function readSecret(prompt) {
|
|
32
|
+
if (!process.stdin.isTTY || !process.stdin.setRawMode) {
|
|
33
|
+
const line = createInterface({ input: process.stdin, output: process.stdout });
|
|
34
|
+
const answer = await line.question(prompt);
|
|
35
|
+
line.close();
|
|
36
|
+
return answer.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
let value = '';
|
|
41
|
+
const onData = (chunk) => {
|
|
42
|
+
const text = chunk.toString();
|
|
43
|
+
for (const char of text) {
|
|
44
|
+
if (char === '\u0003') {
|
|
45
|
+
process.stdin.setRawMode(false);
|
|
46
|
+
process.stdin.pause();
|
|
47
|
+
reject(new Error('Cancelled.'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (char === '\r' || char === '\n') {
|
|
51
|
+
process.stdin.setRawMode(false);
|
|
52
|
+
process.stdin.pause();
|
|
53
|
+
process.stdin.off('data', onData);
|
|
54
|
+
process.stdout.write('\n');
|
|
55
|
+
resolve(value.trim());
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (char === '\u007f') value = value.slice(0, -1);
|
|
59
|
+
else value += char;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
process.stdout.write(prompt);
|
|
64
|
+
process.stdin.setRawMode(true);
|
|
65
|
+
process.stdin.resume();
|
|
66
|
+
process.stdin.on('data', onData);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function chooseOs(input) {
|
|
71
|
+
const answer = (await input.question(`Hệ điều hành [${detectedOs()}]: `)).trim() || detectedOs();
|
|
72
|
+
if (!['1', '2', '3'].includes(answer)) throw new Error('Chọn 1, 2 hoặc 3.');
|
|
73
|
+
return answer;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function configLines(apiKey) {
|
|
77
|
+
return [
|
|
78
|
+
`export ANTHROPIC_BASE_URL="${API_ORIGIN}"`,
|
|
79
|
+
`export ANTHROPIC_API_KEY="${apiKey}"`,
|
|
80
|
+
'export ANTHROPIC_MODEL="viro-max"',
|
|
81
|
+
'export ANTHROPIC_SMALL_FAST_MODEL="viro-speed"',
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function updateProfile(path, lines) {
|
|
86
|
+
const block = `${MARKER_START}\n${lines.join('\n')}\n${MARKER_END}`;
|
|
87
|
+
let current = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
88
|
+
const marker = new RegExp(`\\n?${escapeRegExp(MARKER_START)}[\\s\\S]*?${escapeRegExp(MARKER_END)}\\n?`, 'g');
|
|
89
|
+
current = current.replace(marker, '').replace(/\s*$/, '');
|
|
90
|
+
writeFileSync(path, current ? `${current}\n\n${block}\n` : `${block}\n`, { mode: 0o600 });
|
|
91
|
+
return path;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function configureWindows(apiKey) {
|
|
95
|
+
const values = {
|
|
96
|
+
ANTHROPIC_BASE_URL: API_ORIGIN,
|
|
97
|
+
ANTHROPIC_API_KEY: apiKey,
|
|
98
|
+
ANTHROPIC_MODEL: 'viro-max',
|
|
99
|
+
ANTHROPIC_SMALL_FAST_MODEL: 'viro-speed',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
for (const [name, value] of Object.entries(values)) {
|
|
103
|
+
execFileSync('setx', [name, value], { stdio: 'ignore' });
|
|
104
|
+
}
|
|
105
|
+
return 'Windows user environment';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function configureUnix(os, apiKey) {
|
|
109
|
+
const lines = configLines(apiKey);
|
|
110
|
+
const profiles = os === '2'
|
|
111
|
+
? [join(homedir(), '.zshrc'), join(homedir(), '.bashrc')]
|
|
112
|
+
: [join(homedir(), '.bashrc'), join(homedir(), '.zshrc')];
|
|
113
|
+
|
|
114
|
+
for (const profile of profiles) updateProfile(profile, lines);
|
|
115
|
+
return profiles.join(' and ');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function validateKey(apiKey) {
|
|
119
|
+
const controller = new AbortController();
|
|
120
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
121
|
+
try {
|
|
122
|
+
const response = await fetch(`${API_ORIGIN}/v1/me`, {
|
|
123
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
124
|
+
signal: controller.signal,
|
|
125
|
+
});
|
|
126
|
+
return response.ok;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
} finally {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function escapeRegExp(value) {
|
|
135
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function main() {
|
|
139
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
140
|
+
printHelp();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(`\nVIROUTER Claude Code installer\nTarget: ${API_ORIGIN}\n`);
|
|
145
|
+
let input;
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const apiKey = process.env.VIROUTER_API_KEY || await readSecret('Auth Token / API Key: ');
|
|
149
|
+
if (!/^viro_[A-Za-z0-9_-]{20,}$/.test(apiKey)) {
|
|
150
|
+
throw new Error('API key không hợp lệ. Key phải bắt đầu bằng viro_.');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
input = createInterface({ input: process.stdin, output: process.stdout });
|
|
154
|
+
const os = await chooseOs(input);
|
|
155
|
+
input.close();
|
|
156
|
+
input = undefined;
|
|
157
|
+
|
|
158
|
+
const valid = await validateKey(apiKey);
|
|
159
|
+
if (valid === false) {
|
|
160
|
+
throw new Error(`API key bị từ chối bởi ${API_ORIGIN}. Kiểm tra key hoặc VIROUTER_API_ORIGIN.`);
|
|
161
|
+
}
|
|
162
|
+
if (valid === null) console.log('Không kiểm tra được API lúc này. Vẫn tiếp tục lưu cấu hình.');
|
|
163
|
+
|
|
164
|
+
const location = os === '1' ? configureWindows(apiKey) : configureUnix(os, apiKey);
|
|
165
|
+
console.log(`\nĐã cấu hình Claude Code trong ${location}.`);
|
|
166
|
+
console.log('Đóng terminal này, mở terminal mới rồi chạy:');
|
|
167
|
+
console.log(' claude\n');
|
|
168
|
+
} catch (error) {
|
|
169
|
+
input?.close();
|
|
170
|
+
console.error(`\nLỗi: ${error instanceof Error ? error.message : String(error)}`);
|
|
171
|
+
process.exitCode = 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await main();
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "virouter",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Configure Claude Code to use the VIROUTER API gateway",
|
|
5
|
+
"keywords": ["claude-code", "anthropic", "openai", "api-gateway", "cli"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"virouter": "bin/virouter.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": ["bin", "README.md"],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=20"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"typecheck": "node --check bin/virouter.mjs"
|
|
20
|
+
}
|
|
21
|
+
}
|