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 +0 -0
- package/CHANGELOG.md +35 -0
- package/README.fr.md +20 -12
- package/README.md +18 -22
- package/index.js +365 -116
- package/package.json +4 -3
- package/src/detectors/claude-only.js +57 -60
- package/tests/detectors.test.js +166 -0
- package/CLAUDE.md +0 -90
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.
|
|
108
|
-
|
|
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. **
|
|
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
|
-
|
|
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.
|
|
106
|
-
2.
|
|
107
|
-
3.
|
|
108
|
-
|
|
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
|
-
####
|
|
110
|
+
#### Debugging
|
|
111
111
|
|
|
112
|
-
|
|
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
|
-
|
|
114
|
+
```bash
|
|
115
|
+
npx claude-scionos --scionos-debug
|
|
120
116
|
```
|
|
121
117
|
|
|
122
|
-
#### Command
|
|
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
|
|
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
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
console.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
console.log(
|
|
39
|
-
console.log(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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": "
|
|
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": "
|
|
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 {{
|
|
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
|
-
|
|
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
|
-
//
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
}
|
|
81
|
-
details.push(`✗
|
|
75
|
+
} else {
|
|
76
|
+
details.push(`✗ Executable 'claude' not found in PATH or standard locations.`);
|
|
82
77
|
}
|
|
83
78
|
|
|
84
79
|
return {
|
|
85
|
-
installed:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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)
|