claude-scionos 2.0.1 → 3.0.1

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/1.png ADDED
Binary file
package/CHANGELOG.md CHANGED
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.0.1] - 2026-01-11
9
+
10
+ ### Added
11
+ - **Model Mapping & Proxy**: Integrated local proxy to transparently map Claude models to **GLM-4.7** or **MiniMax-M2.1**.
12
+ - **Active Token Validation**: Now validates the `ANTHROPIC_AUTH_TOKEN` against the `routerlab.ch` API in real-time before launching.
13
+ - **Interactive Menu**: Added a selection menu at startup to choose the model strategy (Default vs Mapped).
14
+ - **Pro Branding**: New professional "ScioNos ✕ Claude Code" banner with corporate colors.
15
+
16
+ ### Improved
17
+ - **Error Handling**: Better distinction between missing executable (`ENOENT`) and permission errors (`EACCES`).
18
+ - **User Interface**: Clearer validation steps and visual feedback.
19
+
20
+ ## [2.2.0] - 2026-01-06
21
+
22
+ ### Added
23
+ - **Auto-Installation**: Prompts users to automatically install Claude Code CLI (`npm install -g`) if missing.
24
+ - **Native Path Detection**: Now detects Claude Code installations in native paths (`~/.local/bin`, Windows Apps, etc.) per official docs.
25
+ - **SIGTERM Support**: Added handling for `SIGTERM` signals (Docker, CI/CD) to cleanly stop the child process.
26
+
27
+ ### Fixed
28
+ - **Crash on Config-Only**: Fixed a critical bug where the wrapper would crash if a configuration file existed but the CLI executable was missing.
29
+ - **Recursion Safety**: Now launches the detected absolute path of the executable instead of the generic command name, preventing potential loop issues.
30
+ - **Error Logging**: Errors are now correctly sent to `stderr` instead of `stdout`.
31
+
32
+ ## [2.1.0] - 2026-01-06
33
+
34
+ ### Added
35
+ - **Debug Mode**: New `--scionos-debug` flag for detailed diagnostic output
36
+ - **Test Infrastructure**: Added Vitest test suite covering core detection logic
37
+ - **Linting**: Fixed development environment and linting rules
38
+
39
+ ### Fixed
40
+ - **Windows Path Handling**: Fixed an issue where `where claude` returned multiple paths on Windows
41
+ - **Signal Handling**: Improved `SIGINT` (Ctrl+C) handling to prevent wrapper from killing Claude prematurely
42
+
8
43
  ## [2.0.0] - 2025-12-12
9
44
 
10
45
  ### ⚠️ BREAKING CHANGES
package/README.fr.md CHANGED
@@ -41,11 +41,11 @@ L'objectif est d'offrir une couche d'exécution propre, isolée et professionnel
41
41
  ### 📌 Points clés
42
42
 
43
43
  - 🔒 **Isolation du jeton** — Le jeton d'authentification n'est jamais écrit sur le disque
44
+ - 🔄 **Mapping de Modèles** — Redirection transparente vers **GLM-4.7** ou **MiniMax-M2.1** via proxy local
44
45
  - 💾 **Zéro persistance** — Aucun fichier temporaire ni configuration locale stockés
45
46
  - 🧩 **Compatibilité totale** — Fonctionne parfaitement avec la CLI officielle Claude Code
46
47
  - 🔐 **Stockage en mémoire uniquement** — Toutes les informations d'identification sont détruites à la fin du processus
47
48
  - 🚀 **Démarrage rapide** — Exécution en une seule commande via `npx`
48
- - 🧪 **Prêt pour CI/CD** — Adapté aux workflows automatisés et pipelines
49
49
 
50
50
  ---
51
51
 
@@ -54,14 +54,10 @@ L'objectif est d'offrir une couche d'exécution propre, isolée et professionnel
54
54
  Avant d'utiliser `claude-scionos`, assurez-vous d'avoir :
55
55
 
56
56
  - **Node.js** version 22 ou supérieure ([Télécharger](https://nodejs.org/))
57
- - La CLI **Claude Code** installée globalement :
58
-
59
- ```bash
60
- npm install -g @anthropic-ai/claude-code
61
- ```
62
-
63
57
  - Un **ANTHROPIC_AUTH_TOKEN** valide depuis [https://routerlab.ch/keys](https://routerlab.ch/keys)
64
58
 
59
+ *(Note : Si la CLI **Claude Code** n'est pas installée, l'outil vous proposera de l'installer automatiquement.)*
60
+
65
61
  ---
66
62
 
67
63
  ### 📥 Installation
@@ -102,10 +98,22 @@ npx claude-scionos
102
98
 
103
99
  **Ce qui se passe :**
104
100
 
105
- 1. L'outil vérifie si la CLI Claude Code est installée
106
- 2. Vous invite à saisir votre `ANTHROPIC_AUTH_TOKEN`
107
- 3. Lance Claude Code avec le jeton stocké **uniquement en mémoire**
108
- 4. Nettoie automatiquement les informations d'identification à la sortie
101
+ 1. L'outil vérifie si la CLI Claude Code est installée (si non, propose l'**installation automatique**)
102
+ 2. Vous invite à saisir votre `ANTHROPIC_AUTH_TOKEN` et le valide instantanément
103
+ 3. **Menu de Sélection** : Vous choisissez la stratégie de modèle :
104
+ - *Default* : Utilise les modèles Anthropic (Opus/Sonnet/Haiku)
105
+ - *Force GLM-4.7* : Mappe toutes les requêtes vers GLM-4.7
106
+ - *Force MiniMax-M2.1* : Mappe toutes les requêtes vers MiniMax-M2.1
107
+ 4. Lance Claude Code (avec un proxy local transparent si un mapping est choisi)
108
+ 5. Nettoie automatiquement les informations d'identification à la sortie
109
+
110
+ #### Débogage
111
+
112
+ Si vous rencontrez des problèmes, utilisez le flag de débogage pour voir les informations détaillées :
113
+
114
+ ```bash
115
+ npx claude-scionos --scionos-debug
116
+ ```
109
117
 
110
118
  #### Exemple de session
111
119
 
@@ -144,7 +152,7 @@ Pour une liste complète des flags et commandes disponibles, consultez la [docum
144
152
  ### 🔍 Fonctionnement
145
153
 
146
154
  1. **Vérification** : Vérifie que la commande `claude` est disponible dans votre PATH
147
- 2. **Saisie du jeton** : Demande de manière sécurisée votre jeton d'authentification (entrée masquée)
155
+ 2. **Validation du jeton** : Demande et valide votre jeton en temps réel via l'API (assurant qu'il est fonctionnel avant le lancement)
148
156
  3. **Configuration de l'environnement** : Crée des variables d'environnement isolées :
149
157
  - `ANTHROPIC_BASE_URL` → `https://routerlab.ch`
150
158
  - `ANTHROPIC_AUTH_TOKEN` → Votre jeton (mémoire uniquement)
package/README.md CHANGED
@@ -41,11 +41,11 @@ The goal is to offer a clean, isolated, and professional execution layer fully c
41
41
  ### 📌 Key Features
42
42
 
43
43
  - 🔒 **Token Isolation** — Authentication token never written to disk
44
+ - 🔄 **Model Mapping** — Transparently route requests to **GLM-4.7** or **MiniMax-M2.1** via local proxy
44
45
  - 💾 **Zero Persistence** — No temporary files or local configuration stored
45
46
  - 🧩 **Full Compatibility** — Works seamlessly with the official Claude Code CLI
46
47
  - 🔐 **Memory-Only Storage** — All credentials destroyed on process exit
47
48
  - 🚀 **Quick Start** — Single command execution via `npx`
48
- - 🧪 **CI/CD Ready** — Suitable for automated workflows and pipelines
49
49
 
50
50
  ---
51
51
 
@@ -54,14 +54,10 @@ The goal is to offer a clean, isolated, and professional execution layer fully c
54
54
  Before using `claude-scionos`, ensure you have:
55
55
 
56
56
  - **Node.js** version 22 or later ([Download](https://nodejs.org/))
57
- - **Claude Code** CLI installed globally:
58
-
59
- ```bash
60
- npm install -g @anthropic-ai/claude-code
61
- ```
62
-
63
57
  - A valid **ANTHROPIC_AUTH_TOKEN** from [https://routerlab.ch/keys](https://routerlab.ch/keys)
64
58
 
59
+ *(Note: If **Claude Code** is not installed, the tool will offer to install it for you automatically.)*
60
+
65
61
  ---
66
62
 
67
63
  ### 📥 Installation
@@ -94,7 +90,7 @@ claude-scionos
94
90
 
95
91
  #### Basic Usage
96
92
 
97
- Simply execute the command:
93
+ Run the command:
98
94
 
99
95
  ```bash
100
96
  npx claude-scionos
@@ -102,24 +98,24 @@ npx claude-scionos
102
98
 
103
99
  **What happens:**
104
100
 
105
- 1. The tool checks if Claude Code CLI is installed
106
- 2. Prompts you to enter your `ANTHROPIC_AUTH_TOKEN`
107
- 3. Launches Claude Code with the token stored **only in memory**
108
- 4. Automatically cleans up credentials when you exit
101
+ 1. Checks if Claude Code CLI is installed (if not, offers **automatic installation**)
102
+ 2. Prompts for your `ANTHROPIC_AUTH_TOKEN` and validates it instantly
103
+ 3. **Selection Menu**: Choose your model strategy:
104
+ - *Default*: Use standard Anthropic models (Opus/Sonnet/Haiku)
105
+ - *Force GLM-4.7*: Maps all requests to GLM-4.7
106
+ - *Force MiniMax-M2.1*: Maps all requests to MiniMax-M2.1
107
+ 4. Launches Claude Code (starting a transparent local proxy if needed)
108
+ 5. Automatically cleans credentials on exit
109
109
 
110
- #### Example Session
110
+ #### Debugging
111
111
 
112
- ```bash
113
- $ npx claude-scionos
114
-
115
- Claude Code (via ScioNos)
116
- To retrieve your token, visit: https://routerlab.ch/keys
117
- ? Please enter your ANTHROPIC_AUTH_TOKEN: ********
112
+ If you encounter issues, you can run with the debug flag to see detailed diagnostic information:
118
113
 
119
- # Claude Code starts...
114
+ ```bash
115
+ npx claude-scionos --scionos-debug
120
116
  ```
121
117
 
122
- #### Command-Line Options
118
+ #### Command Line Options
123
119
 
124
120
  ```bash
125
121
  # Display version
@@ -144,7 +140,7 @@ For a complete list of available flags and commands, see the [official Claude Co
144
140
  ### 🔍 How It Works
145
141
 
146
142
  1. **Verification**: Checks if `claude` command is available in your PATH
147
- 2. **Token Input**: Securely prompts for your authentication token (masked input)
143
+ 2. **Token Validation**: Prompts for and validates your token in real-time via the API (ensuring it works before launch)
148
144
  3. **Environment Setup**: Creates isolated environment variables:
149
145
  - `ANTHROPIC_BASE_URL` → `https://routerlab.ch`
150
146
  - `ANTHROPIC_AUTH_TOKEN` → Your token (memory only)
package/index.js CHANGED
@@ -1,117 +1,366 @@
1
1
  #!/usr/bin/env node
2
-
3
- import chalk from 'chalk';
4
- import { password } from '@inquirer/prompts';
5
- import spawn from 'cross-spawn';
6
- import updateNotifier from 'update-notifier';
7
- import process from 'node:process';
8
- import { createRequire } from 'node:module';
9
- import { isClaudeCodeInstalled, detectOS, checkGitBashOnWindows, getInstallationInstructions } from './src/detectors/claude-only.js';
10
-
11
- const require = createRequire(import.meta.url);
12
- const pkg = require('./package.json');
13
-
14
- // Initialize update notifier
15
- updateNotifier({ pkg }).notify();
16
-
17
- // 0. Handle --version / -v flag
18
- if (process.argv.includes('--version') || process.argv.includes('-v')) {
19
- console.log(pkg.version);
20
- process.exit(0);
21
- }
22
-
23
- // 1. Enhanced System Detection
24
- console.log(chalk.cyan('🔍 Checking system configuration...'));
25
-
26
- // Detect OS and environment
27
- const osInfo = detectOS();
28
- console.log(chalk.gray(`✓ OS: ${osInfo.type} (${osInfo.arch})`));
29
- console.log(chalk.gray(`✓ Shell: ${osInfo.shell}`));
30
-
31
- // Check Claude Code installation
32
- const claudeStatus = isClaudeCodeInstalled();
33
-
34
- if (!claudeStatus.installed) {
35
- console.error(chalk.redBright('\n❌ Claude Code not found'));
36
-
37
- // Show detailed detection info
38
- console.log(chalk.yellow('\nDetection Details:'));
39
- console.log(chalk.gray(claudeStatus.details));
40
-
41
- // Show installation instructions
42
- const instructions = getInstallationInstructions(osInfo, claudeStatus);
43
- console.log(chalk.cyan(instructions));
44
-
45
- process.exit(1);
46
- }
47
-
48
- // Show Claude Code status
49
- console.log(chalk.green('\n✓ Claude Code detected'));
50
- console.log(chalk.gray(claudeStatus.details));
51
-
52
- // 2. Check Git Bash on Windows (if needed)
53
- if (process.platform === 'win32') {
54
- console.log(chalk.cyan('\n🔍 Checking Git Bash availability...'));
55
- const gitBashStatus = checkGitBashOnWindows();
56
-
57
- if (!gitBashStatus.available) {
58
- console.log(chalk.red('\n❌ Git Bash is required on Windows'));
59
- console.log(chalk.gray(gitBashStatus.message));
60
-
61
- // Show Git Bash installation instructions
62
- console.log(chalk.cyan('\n📥 Install Git for Windows:'));
63
- console.log(chalk.white(' https://git-scm.com/downloads/win\n'));
64
- console.log(chalk.cyan('⚙️ Or set the path manually:'));
65
- console.log(chalk.white(' set CLAUDE_CODE_GIT_BASH_PATH=C:\\Program Files\\Git\\bin\\bash.exe'));
66
- console.log(chalk.white(' (PowerShell: $env:CLAUDE_CODE_GIT_BASH_PATH="C:\\Program Files\\Git\\bin\\bash.exe")\n'));
67
- console.log(chalk.yellow('💡 After installation, restart your terminal and try again.\n'));
68
- process.exit(1);
69
- } else {
70
- console.log(chalk.green('✓ Git Bash available'));
71
- console.log(chalk.gray(gitBashStatus.message));
72
- }
73
- }
74
-
75
- // 3. Intro
76
- console.clear();
77
- console.log(chalk.cyan.bold("Claude Code (via ScioNos)"));
78
- console.log(chalk.gray(`Running on ${osInfo.type} with ${osInfo.shell}`));
79
-
80
- // 4. Token info
81
- console.log(chalk.blueBright("To retrieve your token, visit: https://routerlab.ch/keys"));
82
-
83
- // 5. Token input
84
- const token = await password({
85
- message: "Please enter your ANTHROPIC_AUTH_TOKEN:",
86
- validate: (input) => {
87
- if (!input || input.trim() === '') {
88
- return "Token cannot be empty.";
89
- }
90
- return true;
91
- },
92
- mask: '*'
93
- });
94
-
95
- // 6. Environment configuration
96
- const env = {
97
- ...process.env,
98
- ANTHROPIC_BASE_URL: "https://routerlab.ch",
99
- ANTHROPIC_AUTH_TOKEN: token,
100
- ANTHROPIC_API_KEY: "" // Force empty string
101
- };
102
-
103
- // 7. Launch Claude Code
104
- const args = process.argv.slice(2);
105
- const child = spawn('claude', args, {
106
- stdio: 'inherit',
107
- env: env
108
- });
109
-
110
- child.on('close', (code) => {
111
- process.exit(code);
112
- });
113
-
114
- child.on('error', (err) => {
115
- console.error(chalk.red(`Error launching Claude: ${err.message}`));
116
- process.exit(1);
117
- });
2
+
3
+ import chalk from 'chalk';
4
+ import { password, confirm, select } from '@inquirer/prompts';
5
+ import spawn from 'cross-spawn';
6
+ import updateNotifier from 'update-notifier';
7
+ import process from 'node:process';
8
+ import http from 'node:http';
9
+ import { createRequire } from 'node:module';
10
+ import { isClaudeCodeInstalled, detectOS, checkGitBashOnWindows, getInstallationInstructions } from './src/detectors/claude-only.js';
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const pkg = require('./package.json');
14
+
15
+ // Initialize update notifier
16
+ updateNotifier({ pkg }).notify();
17
+
18
+ // --- CONFIGURATION ---
19
+ const BASE_URL = "https://routerlab.ch";
20
+
21
+ // --- UTILS ---
22
+
23
+ /**
24
+ * Displays the application banner
25
+ */
26
+ function showBanner() {
27
+ console.clear();
28
+ const p = chalk.hex('#3b82f6'); // Primary (Scio)
29
+ const s = chalk.hex('#a855f7'); // Secondary (Nos)
30
+ const c = chalk.hex('#D97757'); // Claude Orange
31
+ const w = chalk.white;
32
+ const g = chalk.gray;
33
+ const border = g; // Gray border
34
+
35
+ console.log("");
36
+ console.log(border(" ┌──────────────────────────────────────────────────────────┐"));
37
+ console.log(border(" │ │"));
38
+ console.log(border(" │ ") + p.bold("Scio") + s.bold("Nos") + w.bold(" ✕ ") + c.bold("Claude Code") + border(" │"));
39
+ console.log(border(" │ │"));
40
+ console.log(border(" └──────────────────────────────────────────────────────────┘"));
41
+ console.log(g(` v${pkg.version}`));
42
+ console.log("");
43
+ }
44
+
45
+ /**
46
+ * Validates the API Token against RouterLab
47
+ */
48
+ async function validateToken(apiKey) {
49
+ try {
50
+ const response = await fetch(`${BASE_URL}/v1/models`, {
51
+ method: 'GET',
52
+ headers: {
53
+ 'x-api-key': apiKey,
54
+ 'anthropic-version': '2023-06-01'
55
+ }
56
+ });
57
+
58
+ if (response.ok) {
59
+ return { valid: true };
60
+ } else if (response.status === 401 || response.status === 403) {
61
+ return { valid: false, reason: 'auth_failed' };
62
+ } else {
63
+ return { valid: false, reason: 'server_error', status: response.status, statusText: response.statusText };
64
+ }
65
+ } catch (error) {
66
+ return { valid: false, reason: 'network_error', message: error.message };
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Starts a local proxy server to map models
72
+ * @param {string} targetModel - The model ID to map to (e.g., 'glm-4.7')
73
+ * @param {string} validToken - The validated Auth Token
74
+ * @returns {Promise<string>} - The local URL (e.g., http://127.0.0.1:45321)
75
+ */
76
+ function startProxyServer(targetModel, validToken) {
77
+ return new Promise((resolve, reject) => {
78
+ const server = http.createServer(async (req, res) => {
79
+ // Handle CORS preflight if necessary (usually strict for CLI tools but good practice)
80
+ if (req.method === 'OPTIONS') {
81
+ res.writeHead(200, {
82
+ 'Access-Control-Allow-Origin': '*',
83
+ 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
84
+ 'Access-Control-Allow-Headers': '*'
85
+ });
86
+ res.end();
87
+ return;
88
+ }
89
+
90
+ // We only care about interception if it's a message generation request
91
+ // Claude Code uses /v1/messages
92
+ if (req.method === 'POST' && req.url.includes('/messages')) {
93
+ const chunks = [];
94
+ req.on('data', chunk => chunks.push(chunk));
95
+ req.on('end', async () => {
96
+ try {
97
+ const bodyBuffer = Buffer.concat(chunks);
98
+ let bodyJson;
99
+ try {
100
+ bodyJson = JSON.parse(bodyBuffer.toString());
101
+ } catch (e) {
102
+ // If not JSON, forward as is
103
+ bodyJson = null;
104
+ }
105
+
106
+ // THE MAGIC: Swap the model
107
+ if (bodyJson && bodyJson.model) {
108
+ // Map any Claude model to our target
109
+ // Claude Code usually requests 'claude-3-opus-...' or 'claude-3-5-sonnet...'
110
+ // We force the target.
111
+ if (process.argv.includes('--scionos-debug')) {
112
+ console.log(chalk.yellow(`[Proxy] Swapping model ${bodyJson.model} -> ${targetModel}`));
113
+ }
114
+ bodyJson.model = targetModel;
115
+ }
116
+
117
+ // Prepare upstream request
118
+ const upstreamRes = await fetch(`${BASE_URL}${req.url}`, {
119
+ method: 'POST',
120
+ headers: {
121
+ ...req.headers,
122
+ 'host': new URL(BASE_URL).host,
123
+ 'x-api-key': validToken // Ensure we use the validated token
124
+ },
125
+ body: bodyJson ? JSON.stringify(bodyJson) : bodyBuffer
126
+ });
127
+
128
+ // Pipe response back
129
+ res.writeHead(upstreamRes.status, upstreamRes.headers);
130
+ if (upstreamRes.body) {
131
+ // @ts-ignore - Node fetch body is iterable
132
+ for await (const chunk of upstreamRes.body) {
133
+ res.write(chunk);
134
+ }
135
+ }
136
+ res.end();
137
+
138
+ } catch (error) {
139
+ console.error(chalk.red(`[Proxy Error] ${error.message}`));
140
+ res.writeHead(500);
141
+ res.end(JSON.stringify({ error: { message: "Scionos Proxy Error" } }));
142
+ }
143
+ });
144
+ } else {
145
+ // Passthrough for other endpoints (like /models potentially)
146
+ // In a simple CLI usage, direct passthrough might be enough,
147
+ // but since we changed the BASE_URL, we must forward everything.
148
+ // For simplicity in this script, we assume most traffic is POST /messages.
149
+ // If Claude Code calls GET /models, we should forward it too.
150
+
151
+ // Simple Redirect implementation for non-body requests
152
+ try {
153
+ const upstreamRes = await fetch(`${BASE_URL}${req.url}`, {
154
+ method: req.method,
155
+ headers: {
156
+ ...req.headers,
157
+ 'host': new URL(BASE_URL).host,
158
+ 'x-api-key': validToken
159
+ }
160
+ });
161
+ res.writeHead(upstreamRes.status, upstreamRes.headers);
162
+ if (upstreamRes.body) {
163
+ // @ts-ignore
164
+ for await (const chunk of upstreamRes.body) {
165
+ res.write(chunk);
166
+ }
167
+ }
168
+ res.end();
169
+ } catch (e) {
170
+ res.writeHead(502);
171
+ res.end();
172
+ }
173
+ }
174
+ });
175
+
176
+ // Listen on random port
177
+ server.listen(0, '127.0.0.1', () => {
178
+ const address = server.address();
179
+ const port = address.port;
180
+ resolve({ server, url: `http://127.0.0.1:${port}` });
181
+ });
182
+
183
+ server.on('error', (err) => reject(err));
184
+ });
185
+ }
186
+
187
+ // --- MAIN EXECUTION ---
188
+
189
+ // 0. Handle Flags
190
+ if (process.argv.includes('--version') || process.argv.includes('-v')) {
191
+ console.log(pkg.version);
192
+ process.exit(0);
193
+ }
194
+
195
+ const isDebug = process.argv.includes('--scionos-debug');
196
+
197
+ // 1. Show Banner
198
+ showBanner();
199
+
200
+ // 2. System Check
201
+ if (isDebug) console.log(chalk.cyan('🔍 Checking system configuration...'));
202
+ const osInfo = detectOS();
203
+ let claudeStatus = isClaudeCodeInstalled();
204
+
205
+ // Check Claude Code installation
206
+ if (!claudeStatus.installed) {
207
+ console.error(chalk.redBright('\n❌ Claude Code CLI not found'));
208
+ const shouldInstall = await confirm({
209
+ message: 'Claude Code CLI is not installed. Install globally via npm?',
210
+ default: true
211
+ });
212
+
213
+ if (shouldInstall) {
214
+ try {
215
+ console.log(chalk.cyan('\n📦 Installing @anthropic-ai/claude-code...'));
216
+ spawn.sync('npm', ['install', '-g', '@anthropic-ai/claude-code'], { stdio: 'inherit' });
217
+ claudeStatus = isClaudeCodeInstalled();
218
+ if (!claudeStatus.installed) {
219
+ console.warn(chalk.yellow('⚠ Installation finished, but executable not found immediately. Restart terminal recommended.'));
220
+ process.exit(0);
221
+ }
222
+ } catch (e) {
223
+ console.error(chalk.red(`\n❌ Installation failed: ${e.message}`));
224
+ console.error(chalk.cyan(getInstallationInstructions(osInfo)));
225
+ process.exit(1);
226
+ }
227
+ } else {
228
+ console.error(chalk.cyan(getInstallationInstructions(osInfo)));
229
+ process.exit(1);
230
+ }
231
+ }
232
+
233
+ // Check Git Bash on Windows
234
+ let gitBashPath = null;
235
+ if (process.platform === 'win32') {
236
+ const gitBashStatus = checkGitBashOnWindows();
237
+ if (!gitBashStatus.available) {
238
+ console.log(chalk.red('\n❌ Git Bash is required on Windows'));
239
+ console.log(chalk.cyan('Please install Git for Windows: https://git-scm.com/downloads/win'));
240
+ process.exit(1);
241
+ }
242
+ gitBashPath = gitBashStatus.path;
243
+ }
244
+
245
+ // 3. Token Loop
246
+ let token = "";
247
+ while (true) {
248
+ console.log(chalk.blueBright("To retrieve your token, visit: https://routerlab.ch/keys"));
249
+ token = await password({
250
+ message: "Please enter your ANTHROPIC_AUTH_TOKEN:",
251
+ mask: '*'
252
+ });
253
+
254
+ console.log(chalk.gray("Validating token..."));
255
+ const validation = await validateToken(token);
256
+
257
+ if (validation.valid) {
258
+ console.log(chalk.green("✓ Token validated."));
259
+ break;
260
+ } else if (validation.reason === 'auth_failed') {
261
+ console.log(chalk.red("❌ Invalid token (401/403). Try again."));
262
+ } else {
263
+ console.log(chalk.yellow(`⚠ Validation warning: ${validation.message || validation.status}`));
264
+ const ignore = await confirm({ message: "Continue anyway?", default: false });
265
+ if (ignore) break;
266
+ }
267
+ }
268
+
269
+ // 4. Model Selection
270
+ const modelChoice = await select({
271
+ message: 'Select Model Strategy:',
272
+ choices: [
273
+ {
274
+ name: 'Default (Use Claude Opus/Sonnet/Haiku natively)',
275
+ value: 'default',
276
+ description: 'Standard behavior. Claude decides which model to use.'
277
+ },
278
+ {
279
+ name: 'Force GLM-4.7 (Map all models to GLM-4.7)',
280
+ value: 'glm-4.7',
281
+ description: 'Intercepts traffic and routes everything to GLM-4.7'
282
+ },
283
+ {
284
+ name: 'Force MiniMax-M2.1 (Map all models to MiniMax)',
285
+ value: 'minimax-m2.1',
286
+ description: 'Intercepts traffic and routes everything to MiniMax-M2.1'
287
+ }
288
+ ]
289
+ });
290
+
291
+ // 5. Setup Environment & Proxy
292
+ let finalBaseUrl = BASE_URL;
293
+ let proxyServer = null;
294
+
295
+ if (modelChoice !== 'default') {
296
+ console.log(chalk.magenta(`\n🔮 Starting Local Proxy to map models to ${chalk.bold(modelChoice)}...`));
297
+ try {
298
+ const proxyInfo = await startProxyServer(modelChoice, token);
299
+ proxyServer = proxyInfo.server;
300
+ finalBaseUrl = proxyInfo.url; // e.g. http://127.0.0.1:54321
301
+ if (isDebug) console.log(chalk.gray(`✓ Proxy listening on ${finalBaseUrl}`));
302
+ } catch (err) {
303
+ console.error(chalk.red(`Failed to start proxy: ${err.message}`));
304
+ process.exit(1);
305
+ }
306
+ }
307
+
308
+ const env = {
309
+ ...process.env,
310
+ ANTHROPIC_BASE_URL: finalBaseUrl,
311
+ ANTHROPIC_AUTH_TOKEN: token,
312
+ ANTHROPIC_API_KEY: "" // Force empty
313
+ };
314
+
315
+ // 6. Launch Claude
316
+ const args = process.argv.slice(2).filter(arg => arg !== '--scionos-debug');
317
+ if (isDebug) {
318
+ console.log(chalk.yellow('\n--- DEBUG INFO ---'));
319
+ console.log(chalk.gray(`Endpoint: ${env.ANTHROPIC_BASE_URL}`));
320
+ console.log(chalk.gray(`Model Strategy: ${modelChoice}`));
321
+ console.log(chalk.yellow('------------------\n'));
322
+ }
323
+
324
+ console.log(chalk.green(`\n🚀 Launching Claude Code [${modelChoice}]...\n`));
325
+
326
+ const child = spawn(claudeStatus.cliPath, args, {
327
+ stdio: 'inherit',
328
+ env: env
329
+ });
330
+
331
+ // 7. Cleanup Handlers
332
+ const cleanup = () => {
333
+ if (proxyServer) {
334
+ if (isDebug) console.log(chalk.gray('\nStopping proxy server...'));
335
+ proxyServer.close();
336
+ }
337
+ };
338
+
339
+ child.on('exit', (code) => {
340
+ cleanup();
341
+ process.exit(code);
342
+ });
343
+
344
+ child.on('error', (err) => {
345
+ cleanup();
346
+ console.error(chalk.red(`\n❌ Error launching Claude CLI:`));
347
+ if (err.code === 'ENOENT') {
348
+ console.error(chalk.yellow(` Executable not found. Try 'npm install -g @anthropic-ai/claude-code'`));
349
+ } else if (err.code === 'EACCES') {
350
+ console.error(chalk.yellow(` Permission denied.`));
351
+ } else {
352
+ console.error(chalk.yellow(` ${err.message}`));
353
+ }
354
+ process.exit(1);
355
+ });
356
+
357
+ process.on('SIGINT', () => {
358
+ // Child handles SIGINT usually, but we ensure cleanup if wrapper is killed
359
+ // We don't exit here immediately to let Claude handle the interrupt
360
+ });
361
+
362
+ process.on('SIGTERM', () => {
363
+ if (child) child.kill('SIGTERM');
364
+ cleanup();
365
+ process.exit(0);
366
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-scionos",
3
- "version": "2.0.1",
3
+ "version": "3.0.1",
4
4
  "description": "Ephemeral and secure runner for Claude Code CLI in ScioNos environment",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -8,7 +8,7 @@
8
8
  "claude-scionos": "index.js"
9
9
  },
10
10
  "scripts": {
11
- "test": "echo \"Error: no test specified\" && exit 1",
11
+ "test": "vitest run",
12
12
  "lint": "eslint .",
13
13
  "release:patch": "npm version patch -m \"Chore: Bump version to %s\"",
14
14
  "release:minor": "npm version minor -m \"Chore: Bump version to %s\"",
@@ -44,6 +44,7 @@
44
44
  "devDependencies": {
45
45
  "@eslint/js": "^9.39.2",
46
46
  "eslint": "^9.39.2",
47
- "globals": "^17.0.0"
47
+ "globals": "^17.0.0",
48
+ "vitest": "^4.0.16"
48
49
  }
49
50
  }
@@ -6,86 +6,83 @@ import os from 'os';
6
6
 
7
7
  /**
8
8
  * Detects if Claude Code is installed and returns detailed status
9
- * @returns {{installed: boolean, path: string|null, configPath: string|null, cliAvailable: boolean, details: string}}
9
+ * @returns {{
10
+ * installed: boolean,
11
+ * path: string|null,
12
+ * configPath: string|null,
13
+ * cliAvailable: boolean,
14
+ * cliPath: string|null,
15
+ * details: string,
16
+ * configFound: boolean
17
+ * }}
10
18
  */
11
19
  function isClaudeCodeInstalled() {
12
20
  // Check for Claude Code directory in user home
13
21
  const claudeDir = path.join(os.homedir(), '.claude');
14
22
  const details = [];
15
23
 
24
+ // 1. Check for Configuration
25
+ let configFound = false;
16
26
  if (fs.existsSync(claudeDir)) {
17
- details.push(`✓ Found Claude directory: ${claudeDir}`);
18
-
19
- // Check for settings.json to confirm it's actually Claude Code
20
27
  const settingsPath = path.join(claudeDir, 'settings.json');
21
28
  if (fs.existsSync(settingsPath)) {
22
- details.push(`✓ Configuration file found: ${settingsPath}`);
23
-
24
- // Try to read the config to see if it's configured
25
- try {
26
- const config = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
27
- const hasBaseUrl = config.env?.ANTHROPIC_BASE_URL;
28
- const hasApiKey = config.env?.ANTHROPIC_API_KEY;
29
-
30
- if (hasBaseUrl && hasApiKey) {
31
- details.push(`✓ Claude Code is configured with custom API`);
32
- } else if (hasBaseUrl || hasApiKey) {
33
- details.push(`⚠ Claude Code is partially configured`);
34
- } else {
35
- details.push(`ℹ Claude Code installed but not configured`);
36
- }
37
- } catch (error) {
38
- details.push(`⚠ Could not read configuration file: ${error.message}`);
39
- }
40
-
41
- return {
42
- installed: true,
43
- path: claudeDir,
44
- configPath: settingsPath,
45
- cliAvailable: false, // Will check next
46
- details: details.join('\n ')
47
- };
48
- } else {
49
- details.push(`⚠ Directory exists but no settings.json found`);
29
+ configFound = true;
30
+ details.push(`✓ Configuration found at: ${settingsPath}`);
50
31
  }
51
32
  }
52
33
 
53
- // Also check for the claude CLI command in PATH
54
- try {
55
- const command = process.platform === 'win32' ? 'where claude' : 'which claude';
56
- const claudePath = execSync(command, { encoding: 'utf8' }).trim();
34
+ // 2. Check for Executable (CLI)
35
+ let cliPath = null;
57
36
 
58
- if (claudePath) {
59
- details.push(`✓ Claude CLI found in PATH: ${claudePath}`);
37
+ // 2a. Check Native Install Paths (per official docs)
38
+ const home = os.homedir();
39
+ const nativePaths = process.platform === 'win32'
40
+ ? [path.join(home, '.local', 'bin', 'claude.exe'), path.join(home, 'AppData', 'Local', 'Microsoft', 'WindowsApps', 'claude.exe')]
41
+ : [path.join(home, '.local', 'bin', 'claude'), '/opt/homebrew/bin/claude', '/usr/local/bin/claude'];
42
+
43
+ for (const p of nativePaths) {
44
+ if (fs.existsSync(p)) {
45
+ cliPath = p;
46
+ details.push(`✓ Found native binary: ${p}`);
47
+ break;
48
+ }
49
+ }
60
50
 
61
- // Try to get version to confirm it's working
62
- try {
63
- const version = execSync('claude --version', { encoding: 'utf8' }).trim();
64
- details.push(`✓ Version: ${version}`);
65
- } catch (error) {
66
- details.push(`⚠ CLI found but version check failed: ${error.message}`);
51
+ // 2b. Check PATH if not found yet
52
+ if (!cliPath) {
53
+ try {
54
+ const command = process.platform === 'win32' ? 'where claude' : 'which claude';
55
+ const output = execSync(command, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
56
+ const foundPath = output.split(/\r?\n/)[0].trim();
57
+ if (foundPath && fs.existsSync(foundPath)) {
58
+ cliPath = foundPath;
59
+ details.push(`✓ Found in PATH: ${foundPath}`);
67
60
  }
61
+ } catch (e) {
62
+ // Ignore error if not found in PATH
63
+ }
64
+ }
68
65
 
69
- return {
70
- installed: true,
71
- path: claudeDir && fs.existsSync(claudeDir) ? claudeDir : null,
72
- configPath: claudeDir && fs.existsSync(path.join(claudeDir, 'settings.json'))
73
- ? path.join(claudeDir, 'settings.json')
74
- : null,
75
- cliAvailable: true,
76
- cliPath: claudePath,
77
- details: details.join('\n ')
78
- };
66
+ // 3. Verify Version (if executable found)
67
+ if (cliPath) {
68
+ try {
69
+ // We use the full path to avoid recursion or alias issues
70
+ const version = execSync(`"${cliPath}" --version`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
71
+ details.push(`✓ Version: ${version}`);
72
+ } catch (error) {
73
+ details.push(`⚠ Executable found but version check failed: ${error.message}`);
79
74
  }
80
- } catch (error) {
81
- details.push(`✗ Claude CLI not found in PATH: ${error.message}`);
75
+ } else {
76
+ details.push(`✗ Executable 'claude' not found in PATH or standard locations.`);
82
77
  }
83
78
 
84
79
  return {
85
- installed: false,
86
- path: null,
87
- configPath: null,
88
- cliAvailable: false,
80
+ installed: !!cliPath, // STRICT: Only true if the binary is found
81
+ configFound: configFound,
82
+ path: claudeDir,
83
+ configPath: configFound ? path.join(claudeDir, 'settings.json') : null,
84
+ cliAvailable: !!cliPath,
85
+ cliPath: cliPath,
89
86
  details: details.join('\n ')
90
87
  };
91
88
  }
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import child_process from 'child_process';
6
+ import { isClaudeCodeInstalled, detectOS, checkGitBashOnWindows } from '../src/detectors/claude-only.js';
7
+
8
+ // Mock modules
9
+ vi.mock('fs');
10
+ vi.mock('os');
11
+ vi.mock('child_process');
12
+
13
+ describe('System Detectors', () => {
14
+ const originalPlatform = process.platform;
15
+ const originalEnv = process.env;
16
+
17
+ beforeEach(() => {
18
+ vi.resetAllMocks();
19
+ process.env = { ...originalEnv };
20
+ });
21
+
22
+ afterEach(() => {
23
+ Object.defineProperty(process, 'platform', { value: originalPlatform });
24
+ process.env = originalEnv;
25
+ });
26
+
27
+ describe('detectOS', () => {
28
+ it('should detect Windows', () => {
29
+ vi.mocked(os.platform).mockReturnValue('win32');
30
+ vi.mocked(os.arch).mockReturnValue('x64');
31
+ Object.defineProperty(process, 'platform', { value: 'win32' });
32
+ process.env.WINDIR = 'C:\\Windows'; // Simulate CMD
33
+
34
+ const result = detectOS();
35
+ expect(result.type).toBe('Windows');
36
+ expect(result.shell).toBe('Command Prompt (CMD)');
37
+ });
38
+
39
+ it('should detect macOS', () => {
40
+ vi.mocked(os.platform).mockReturnValue('darwin');
41
+ vi.mocked(os.arch).mockReturnValue('arm64');
42
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
43
+ process.env.SHELL = '/bin/zsh';
44
+
45
+ const result = detectOS();
46
+ expect(result.type).toBe('macOS');
47
+ expect(result.shell).toBe('Zsh');
48
+ });
49
+ });
50
+
51
+ describe('checkGitBashOnWindows', () => {
52
+ it('should return available if Git Bash exists in standard path on Windows', () => {
53
+ Object.defineProperty(process, 'platform', { value: 'win32' });
54
+ vi.mocked(fs.existsSync).mockImplementation((p) => p === 'C:\\Program Files\\Git\\bin\\bash.exe');
55
+
56
+ const result = checkGitBashOnWindows();
57
+ expect(result.available).toBe(true);
58
+ expect(result.path).toBe('C:\\Program Files\\Git\\bin\\bash.exe');
59
+ });
60
+
61
+ it('should return not available if no Git Bash found on Windows', () => {
62
+ Object.defineProperty(process, 'platform', { value: 'win32' });
63
+ vi.mocked(fs.existsSync).mockReturnValue(false);
64
+
65
+ const result = checkGitBashOnWindows();
66
+ expect(result.available).toBe(false);
67
+ });
68
+
69
+ it('should not require Git Bash on non-Windows', () => {
70
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
71
+
72
+ const result = checkGitBashOnWindows();
73
+ expect(result.available).toBe(true);
74
+ expect(result.message).toContain('Not required');
75
+ });
76
+ });
77
+
78
+ describe('isClaudeCodeInstalled', () => {
79
+ it('should detect Claude in PATH', () => {
80
+ // Mock homedir to avoid finding config file
81
+ vi.mocked(os.homedir).mockReturnValue('/home/user');
82
+
83
+ // Mock execSync to return a path
84
+ vi.mocked(child_process.execSync).mockImplementation((cmd) => {
85
+ if (cmd.includes('which claude') || cmd.includes('where claude')) {
86
+ return '/usr/local/bin/claude';
87
+ }
88
+ if (cmd.includes('--version')) {
89
+ return '0.0.1';
90
+ }
91
+ return '';
92
+ });
93
+
94
+ // Mock existsSync: false for config, true for CLI path
95
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
96
+ if (p === '/usr/local/bin/claude') return true;
97
+ return false;
98
+ });
99
+
100
+ const result = isClaudeCodeInstalled();
101
+ expect(result.installed).toBe(true);
102
+ expect(result.cliAvailable).toBe(true);
103
+ expect(result.cliPath).toBe('/usr/local/bin/claude');
104
+ });
105
+
106
+ it('should detect Claude from config file ONLY if CLI is also found', () => {
107
+ vi.mocked(os.homedir).mockReturnValue('/home/user');
108
+ // Simulate .claude directory and settings.json existence
109
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
110
+ if (p.endsWith('.claude')) return true;
111
+ if (p.endsWith('settings.json')) return true;
112
+ return false;
113
+ });
114
+
115
+ // Mock reading config
116
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
117
+ env: { ANTHROPIC_BASE_URL: 'https://api.anthropic.com' }
118
+ }));
119
+
120
+ // BUT execSync fails (CLI not found)
121
+ vi.mocked(child_process.execSync).mockImplementation(() => {
122
+ throw new Error('not found');
123
+ });
124
+
125
+ const result = isClaudeCodeInstalled();
126
+ // Updated logic: installed should be false if CLI is missing, even if config exists
127
+ expect(result.installed).toBe(false);
128
+ expect(result.configFound).toBe(true);
129
+ expect(result.cliAvailable).toBe(false);
130
+ expect(result.details).toContain('Executable \'claude\' not found');
131
+ });
132
+
133
+ it('should detect Claude in native path (Linux/Mac)', () => {
134
+ // Force Linux platform to ensure implementation looks for 'claude' not 'claude.exe'
135
+ Object.defineProperty(process, 'platform', { value: 'linux' });
136
+
137
+ // We simulate logic but path.join will use OS separators.
138
+ // On Windows, path.join produces backslashes.
139
+ // The code under test uses path.join.
140
+ const mockHome = '/home/user';
141
+ const expectedPath = path.join(mockHome, '.local', 'bin', 'claude');
142
+
143
+ vi.mocked(os.homedir).mockReturnValue(mockHome);
144
+
145
+ // Simulate native path existence
146
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
147
+ return p === expectedPath;
148
+ });
149
+
150
+ const result = isClaudeCodeInstalled();
151
+ expect(result.installed).toBe(true);
152
+ expect(result.cliPath).toBe(expectedPath);
153
+ });
154
+
155
+ it('should return false if neither CLI nor config found', () => {
156
+ vi.mocked(os.homedir).mockReturnValue('/home/user');
157
+ vi.mocked(fs.existsSync).mockReturnValue(false);
158
+ vi.mocked(child_process.execSync).mockImplementation(() => {
159
+ throw new Error('not found');
160
+ });
161
+
162
+ const result = isClaudeCodeInstalled();
163
+ expect(result.installed).toBe(false);
164
+ });
165
+ });
166
+ });
package/CLAUDE.md DELETED
@@ -1,90 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- claude-scionos is a lightweight Node.js CLI wrapper that provides ephemeral and secure execution of the official Claude Code CLI. It's designed for the ScioNos environment and focuses on in-memory credential management with zero disk persistence.
8
-
9
- ## Architecture
10
-
11
- The project is a single-file Node.js application (`index.js`) that:
12
-
13
- 1. **Validates prerequisites**: Checks if Claude Code CLI is installed and available in PATH
14
- 2. **Windows-specific check**: Verifies Git Bash installation (required by Claude Code on Windows)
15
- 3. **Secure token collection**: Uses `@inquirer/prompts` for masked password input
16
- 4. **Environment isolation**: Creates isolated environment variables with:
17
- - `ANTHROPIC_BASE_URL`: Set to `https://routerlab.ch` (hardcoded ScioNos endpoint)
18
- - `ANTHROPIC_AUTH_TOKEN`: User-provided token (memory only)
19
- - `ANTHROPIC_API_KEY`: Explicitly set to empty string
20
- 5. **Process spawning**: Launches Claude Code CLI with inherited stdio and custom environment
21
-
22
- ## Key Dependencies
23
-
24
- - `@inquirer/prompts`: For secure password input with masking
25
- - `cross-spawn`: Platform-agnostic process spawning
26
- - `chalk`: Terminal colors for user-friendly output
27
- - `update-notifier`: Automatic npm update notifications
28
- - `which`: Command availability checking
29
-
30
- ## Development Commands
31
-
32
- ```bash
33
- # Run locally during development
34
- node index.js
35
-
36
- # Lint code
37
- npm run lint
38
-
39
- # Version bumping (automated commit messages)
40
- npm run release:patch # Bump patch version
41
- npm run release:minor # Bump minor version
42
- npm run release:major # Bump major version
43
-
44
- # Check for vulnerabilities
45
- npm audit
46
- ```
47
-
48
- ## Testing
49
-
50
- Currently no automated tests are configured. When implementing tests, focus on:
51
- - Version flag handling (`--version`, `-v`)
52
- - Claude Code CLI detection
53
- - Windows Git Bash detection logic
54
- - Token validation and environment setup
55
-
56
- ## Release Process
57
-
58
- 1. Update version in `package.json` if not using npm scripts
59
- 2. Update `SESSION_SUMMARY.md` with changes
60
- 3. Commit changes with descriptive message
61
- 4. Publish to npm: `npm publish`
62
- 5. Create GitHub release with changelog
63
-
64
- ## Platform-Specific Behavior
65
-
66
- ### Windows
67
- - Requires Git Bash for Claude Code CLI to function
68
- - Detects Git Bash in standard locations:
69
- - `C:\Program Files\Git\bin\bash.exe` (64-bit)
70
- - `C:\Program Files (x86)\Git\bin\bash.exe` (32-bit)
71
- - Custom path via `CLAUDE_CODE_GIT_BASH_PATH` environment variable
72
- - Provides clear error messages with installation guidance
73
-
74
- ### Unix (macOS/Linux)
75
- - No additional requirements beyond Claude Code CLI
76
- - Standard Unix process spawning behavior
77
-
78
- ## Important Constants
79
-
80
- - `ANTHROPIC_BASE_URL`: `"https://routerlab.ch"` (ScioNos-specific endpoint)
81
- - Minimum Node.js version: 22
82
- - Package entry point: `index.js` with shebang for direct execution
83
-
84
- ## Code Style Notes
85
-
86
- - ES6 modules throughout (`import`/`export`)
87
- - Async/await pattern for asynchronous operations
88
- - Colored terminal output using chalk
89
- - Clear user error messages with actionable guidance
90
- - Process exit codes: 0 (success), 1 (error)