claude-scionos 4.1.9 → 4.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.fr.md CHANGED
@@ -7,10 +7,10 @@ _[🇬🇧 Read in English](./README.md)_
7
7
  ## Points clés
8
8
 
9
9
  - lancement guidé pour les nouveaux utilisateurs
10
- - `--strategy` pour choisir une stratégie sans menu
11
- - `--service llm` pour l'accès RouterLab LLM sur invitation
12
- - `--no-prompt` pour l'automatisation et la CI
13
- - `--list-strategies` pour voir les routes disponibles
10
+ - `--strategy` pour choisir une stratégie sans menu
11
+ - `--service llm` pour l'accès RouterLab LLM sur invitation
12
+ - `--no-prompt` pour l'automatisation et la CI
13
+ - `--list-strategies` pour voir les routes disponibles
14
14
  - `doctor` pour diagnostiquer rapidement un poste client
15
15
  - `auth login|status|change|logout|test` pour gérer le token
16
16
  - proxy local lancé uniquement si une stratégie mappée est choisie
@@ -18,8 +18,8 @@ _[🇬🇧 Read in English](./README.md)_
18
18
  ## Prérequis
19
19
 
20
20
  - Node.js 22 ou plus
21
- - un token RouterLab depuis [routerlab.ch/keys](https://routerlab.ch/keys)
22
- - ou un token d'invitation pour `--service llm`
21
+ - un token RouterLab depuis [routerlab.ch/keys](https://routerlab.ch/keys)
22
+ - ou un token d'invitation pour `--service llm`
23
23
  - sous Windows, Git Bash doit être installé pour Claude Code
24
24
 
25
25
  ## Installation
@@ -48,52 +48,52 @@ npx claude-scionos
48
48
  Commandes utiles :
49
49
 
50
50
  ```bash
51
- npx claude-scionos --list-strategies
52
- npx claude-scionos doctor
53
- npx claude-scionos auth login
54
- npx claude-scionos auth login --service llm
55
- npx claude-scionos auth test
56
- npx claude-scionos --strategy aws
57
- npx claude-scionos --service llm --strategy claude-glm-5
58
- npx claude-scionos --strategy aws --no-prompt -p "Résume ce dépôt"
59
- ```
60
-
61
- ## Services
62
-
63
- - le comportement par défaut utilise `https://routerlab.ch`
64
- - `--service llm` bascule le lanceur vers `https://llm.routerlab.ch`
65
- - `llm` est prévu pour un accès sur invitation
66
- - les tokens enregistrés avec `auth login --service llm` sont stockés séparément du token RouterLab par défaut
67
- - `llm` expose pour l'instant `claude-glm-5`, `claude-gpt-5.4` et `claude-qwen3.6-plus`
68
- - `routerlab` expose aussi `claude-gpt-5.4`
69
-
70
- ## Stratégies
71
-
72
- - `default` : utilise Claude Code normalement sans proxy local
73
- - `aws` : remappe les familles de modèles Claude vers les variantes Claude AWS de RouterLab
74
- - `claude-glm-5` : force toutes les requêtes vers `claude-glm-5`
75
- - `claude-minimax-m2.5` : force toutes les requêtes vers `claude-minimax-m2.5`
76
- - `claude-gpt-5.4` : force toutes les requêtes vers `claude-gpt-5.4`
77
- - `claude-qwen3.6-plus` : force toutes les requêtes vers `claude-qwen3.6-plus`
78
-
79
- Utilise `--list-strategies` pour voir les stratégies disponibles pour le service choisi et leur disponibilité réelle quand un token est disponible.
51
+ npx claude-scionos --list-strategies
52
+ npx claude-scionos doctor
53
+ npx claude-scionos auth login
54
+ npx claude-scionos auth login --service llm
55
+ npx claude-scionos auth test
56
+ npx claude-scionos --strategy aws
57
+ npx claude-scionos --service llm --strategy claude-glm-5
58
+ npx claude-scionos --strategy aws --no-prompt -p "Résume ce dépôt"
59
+ ```
60
+
61
+ ## Services
62
+
63
+ - le comportement par défaut utilise `https://routerlab.ch`
64
+ - `--service llm` bascule le lanceur vers `https://llm.routerlab.ch`
65
+ - `llm` est prévu pour un accès sur invitation
66
+ - les tokens enregistrés avec `auth login --service llm` sont stockés séparément du token RouterLab par défaut
67
+ - `llm` expose pour l'instant `claude-glm-5`, `claude-gpt-5.4` et `claude-qwen3.6-plus`
68
+ - `routerlab` expose aussi `claude-gpt-5.4`
69
+
70
+ ## Stratégies
71
+
72
+ - `default` : utilise Claude Code normalement sans proxy local
73
+ - `aws` : remappe les familles de modèles Claude vers les variantes Claude AWS de RouterLab
74
+ - `claude-glm-5` : force toutes les requêtes vers `claude-glm-5`
75
+ - `claude-minimax-m2.5` : force toutes les requêtes vers `claude-minimax-m2.5`
76
+ - `claude-gpt-5.4` : force toutes les requêtes vers `claude-gpt-5.4`
77
+ - `claude-qwen3.6-plus` : force toutes les requêtes vers `claude-qwen3.6-plus`
78
+
79
+ Utilise `--list-strategies` pour voir les stratégies disponibles pour le service choisi et leur disponibilité réelle quand un token est disponible.
80
80
 
81
81
  ## Gestion du token
82
82
 
83
83
  Ordre de résolution du token :
84
84
 
85
- 1. `ANTHROPIC_AUTH_TOKEN`
86
- 2. stockage local sécurisé via `claude-scionos auth login`
87
- 3. stockage local sécurisé spécifique au service via `claude-scionos auth login --service llm`
88
- 4. saisie manuelle en mode guidé
85
+ 1. `ANTHROPIC_AUTH_TOKEN`
86
+ 2. stockage local sécurisé via `claude-scionos auth login`
87
+ 3. stockage local sécurisé spécifique au service via `claude-scionos auth login --service llm`
88
+ 4. saisie manuelle en mode guidé
89
+
90
+ Backends de stockage sécurisés :
91
+
92
+ - Windows : fichier local chiffré via DPAPI, lié à l'utilisateur courant
93
+ - macOS : Keychain
94
+ - Linux : Secret Service via `secret-tool`
89
95
 
90
- Backends de stockage sécurisés :
91
-
92
- - Windows : fichier local chiffré via DPAPI, lié à l'utilisateur courant
93
- - macOS : Keychain
94
- - Linux : Secret Service via `secret-tool`
95
-
96
- Sous Windows, `claude-scionos` laisse maintenant PowerShell gérer uniquement le chiffrement et le déchiffrement DPAPI, tandis que Node.js écrit et lit directement le fichier sécurisé. Cela corrige le cas où le fichier du token était créé mais restait vide. Si un ancien fichier local est vide ou corrompu, `claude-scionos` le traite comme absent au lieu d'essayer de l'utiliser. Il faut relancer `claude-scionos auth login` pour enregistrer un nouveau token.
96
+ Sous Windows, `claude-scionos` laisse maintenant PowerShell gérer uniquement le chiffrement et le déchiffrement DPAPI, tandis que Node.js écrit et lit directement le fichier sécurisé. Cela corrige le cas où le fichier du token était créé mais restait vide. Si un ancien fichier local est vide ou corrompu, `claude-scionos` le traite comme absent au lieu d'essayer de l'utiliser. Il faut relancer `claude-scionos auth login` pour enregistrer un nouveau token.
97
97
 
98
98
  Gestion du token :
99
99
 
@@ -105,11 +105,11 @@ claude-scionos auth logout
105
105
  claude-scionos auth test
106
106
  ```
107
107
 
108
- ## Ce que veulent dire `--strategy` et `--no-prompt`
109
-
110
- - `--strategy <value>` évite le menu interactif et choisit directement la route
111
- - `--service <value>` change la cible RouterLab. `routerlab` reste le défaut et `llm` est réservé à l'accès sur invitation
112
- - `--no-prompt` désactive toutes les questions interactives
108
+ ## Ce que veulent dire `--strategy` et `--no-prompt`
109
+
110
+ - `--strategy <value>` évite le menu interactif et choisit directement la route
111
+ - `--service <value>` change la cible RouterLab. `routerlab` reste le défaut et `llm` est réservé à l'accès sur invitation
112
+ - `--no-prompt` désactive toutes les questions interactives
113
113
 
114
114
  Quand `--no-prompt` est utilisé, le lanceur doit déjà avoir un token via `ANTHROPIC_AUTH_TOKEN` ou via le stockage sécurisé.
115
115
 
@@ -132,14 +132,14 @@ Le wrapper transmet les arguments Claude Code habituels. Le proxy local n'est la
132
132
 
133
133
  `claude-scionos doctor` doit être la première commande à lancer quand un client signale un problème.
134
134
 
135
- Cas courants :
136
-
137
- - `Claude Code CLI not found` : installer `@anthropic-ai/claude-code`
138
- - `Git Bash is required on Windows` : installer Git for Windows
139
- - `ANTHROPIC_AUTH_TOKEN ... is required when using --no-prompt` : définir la variable d'environnement ou stocker le token au préalable
140
- - `Secure token file was created but no encrypted content was written` : mettre à jour vers `4.1.9` ou plus récent, puis relancer `claude-scionos auth login`
141
- - `Stored token` est indiqué comme absent sous Windows alors qu'un login a déjà été fait : relancer `claude-scionos auth login`, car le fichier DPAPI local peut être vide ou corrompu
142
- - `secret-tool not found` : installer un client Secret Service sous Linux ou utiliser la variable d'environnement
135
+ Cas courants :
136
+
137
+ - `Claude Code CLI not found` : installer `@anthropic-ai/claude-code`
138
+ - `Git Bash is required on Windows` : installer Git for Windows
139
+ - `ANTHROPIC_AUTH_TOKEN ... is required when using --no-prompt` : définir la variable d'environnement ou stocker le token au préalable
140
+ - `Secure token file was created but no encrypted content was written` : mettre à jour vers `4.1.10` ou plus récent, puis relancer `claude-scionos auth login`
141
+ - `Stored token` est indiqué comme absent sous Windows alors qu'un login a déjà été fait : relancer `claude-scionos auth login`, car le fichier DPAPI local peut être vide ou corrompu
142
+ - `secret-tool not found` : installer un client Secret Service sous Linux ou utiliser la variable d'environnement
143
143
 
144
144
  ## Développement
145
145
 
package/README.md CHANGED
@@ -7,10 +7,10 @@ _[🇫🇷 Lire en français](./README.fr.md)_
7
7
  ## Highlights
8
8
 
9
9
  - Guided launch for first-time users
10
- - `--strategy` to preselect a routing strategy
11
- - `--service llm` for invitation-only RouterLab LLM access
12
- - `--no-prompt` for automation and CI
13
- - `--list-strategies` to inspect available routes
10
+ - `--strategy` to preselect a routing strategy
11
+ - `--service llm` for invitation-only RouterLab LLM access
12
+ - `--no-prompt` for automation and CI
13
+ - `--list-strategies` to inspect available routes
14
14
  - `doctor` to diagnose local setup quickly
15
15
  - `auth login|status|change|logout|test` for secure token management
16
16
  - Local proxy only when a mapped strategy is selected
@@ -18,8 +18,8 @@ _[🇫🇷 Lire en français](./README.fr.md)_
18
18
  ## Requirements
19
19
 
20
20
  - Node.js 22 or later
21
- - A RouterLab token from [routerlab.ch/keys](https://routerlab.ch/keys)
22
- - Or an invitation token for `--service llm`
21
+ - A RouterLab token from [routerlab.ch/keys](https://routerlab.ch/keys)
22
+ - Or an invitation token for `--service llm`
23
23
  - On Windows, Git Bash must be installed for Claude Code
24
24
 
25
25
  ## Installation
@@ -48,52 +48,52 @@ npx claude-scionos
48
48
  Useful commands:
49
49
 
50
50
  ```bash
51
- npx claude-scionos --list-strategies
52
- npx claude-scionos doctor
53
- npx claude-scionos auth login
54
- npx claude-scionos auth login --service llm
55
- npx claude-scionos auth test
56
- npx claude-scionos --strategy aws
57
- npx claude-scionos --service llm --strategy claude-glm-5
58
- npx claude-scionos --strategy aws --no-prompt -p "Summarize this repo"
59
- ```
60
-
61
- ## Services
62
-
63
- - Default behavior uses `https://routerlab.ch`
64
- - `--service llm` switches the launcher to `https://llm.routerlab.ch`
65
- - `llm` is intended for invitation-only access
66
- - Tokens stored with `auth login --service llm` are kept separate from the default RouterLab token
67
- - `llm` currently exposes `claude-glm-5`, `claude-gpt-5.4`, and `claude-qwen3.6-plus`
68
- - `routerlab` also exposes `claude-gpt-5.4`
69
-
70
- ## Strategies
71
-
72
- - `default`: use Claude Code normally without the local proxy
73
- - `aws`: remap Claude model families to RouterLab AWS-backed Claude variants
74
- - `claude-glm-5`: force all requests to `claude-glm-5`
75
- - `claude-minimax-m2.5`: force all requests to `claude-minimax-m2.5`
76
- - `claude-gpt-5.4`: force all requests to `claude-gpt-5.4`
77
- - `claude-qwen3.6-plus`: force all requests to `claude-qwen3.6-plus`
78
-
79
- Use `--list-strategies` to see the strategies available for the selected service and their live availability when a token is available.
51
+ npx claude-scionos --list-strategies
52
+ npx claude-scionos doctor
53
+ npx claude-scionos auth login
54
+ npx claude-scionos auth login --service llm
55
+ npx claude-scionos auth test
56
+ npx claude-scionos --strategy aws
57
+ npx claude-scionos --service llm --strategy claude-glm-5
58
+ npx claude-scionos --strategy aws --no-prompt -p "Summarize this repo"
59
+ ```
60
+
61
+ ## Services
62
+
63
+ - Default behavior uses `https://routerlab.ch`
64
+ - `--service llm` switches the launcher to `https://llm.routerlab.ch`
65
+ - `llm` is intended for invitation-only access
66
+ - Tokens stored with `auth login --service llm` are kept separate from the default RouterLab token
67
+ - `llm` currently exposes `claude-glm-5`, `claude-gpt-5.4`, and `claude-qwen3.6-plus`
68
+ - `routerlab` also exposes `claude-gpt-5.4`
69
+
70
+ ## Strategies
71
+
72
+ - `default`: use Claude Code normally without the local proxy
73
+ - `aws`: remap Claude model families to RouterLab AWS-backed Claude variants
74
+ - `claude-glm-5`: force all requests to `claude-glm-5`
75
+ - `claude-minimax-m2.5`: force all requests to `claude-minimax-m2.5`
76
+ - `claude-gpt-5.4`: force all requests to `claude-gpt-5.4`
77
+ - `claude-qwen3.6-plus`: force all requests to `claude-qwen3.6-plus`
78
+
79
+ Use `--list-strategies` to see the strategies available for the selected service and their live availability when a token is available.
80
80
 
81
81
  ## Token Handling
82
82
 
83
83
  Token resolution order:
84
84
 
85
- 1. `ANTHROPIC_AUTH_TOKEN`
86
- 2. Secure local storage from `claude-scionos auth login`
87
- 3. Service-specific secure local storage from `claude-scionos auth login --service llm`
88
- 4. Manual prompt in guided mode
85
+ 1. `ANTHROPIC_AUTH_TOKEN`
86
+ 2. Secure local storage from `claude-scionos auth login`
87
+ 3. Service-specific secure local storage from `claude-scionos auth login --service llm`
88
+ 4. Manual prompt in guided mode
89
+
90
+ Secure storage backends:
91
+
92
+ - Windows: DPAPI-encrypted local file bound to the current user
93
+ - macOS: Keychain
94
+ - Linux: Secret Service via `secret-tool`
89
95
 
90
- Secure storage backends:
91
-
92
- - Windows: DPAPI-encrypted local file bound to the current user
93
- - macOS: Keychain
94
- - Linux: Secret Service via `secret-tool`
95
-
96
- On Windows, `claude-scionos` now lets PowerShell handle only DPAPI encryption and decryption, while Node.js writes and reads the secure token file directly. This fixes the case where the secure token file was created but left empty. If an older file is empty or corrupted, `claude-scionos` treats it as missing instead of trying to use it. Run `claude-scionos auth login` again to store a fresh token.
96
+ On Windows, `claude-scionos` now lets PowerShell handle only DPAPI encryption and decryption, while Node.js writes and reads the secure token file directly. This fixes the case where the secure token file was created but left empty. If an older file is empty or corrupted, `claude-scionos` treats it as missing instead of trying to use it. Run `claude-scionos auth login` again to store a fresh token.
97
97
 
98
98
  Manage the token with:
99
99
 
@@ -105,11 +105,11 @@ claude-scionos auth logout
105
105
  claude-scionos auth test
106
106
  ```
107
107
 
108
- ## What `--strategy` and `--no-prompt` mean
109
-
110
- - `--strategy <value>` skips the interactive strategy menu and selects the route directly
111
- - `--service <value>` switches between RouterLab targets. `routerlab` is the default and `llm` is invitation-only
112
- - `--no-prompt` disables every interactive question
108
+ ## What `--strategy` and `--no-prompt` mean
109
+
110
+ - `--strategy <value>` skips the interactive strategy menu and selects the route directly
111
+ - `--service <value>` switches between RouterLab targets. `routerlab` is the default and `llm` is invitation-only
112
+ - `--no-prompt` disables every interactive question
113
113
 
114
114
  When `--no-prompt` is used, the launcher must already have a token from `ANTHROPIC_AUTH_TOKEN` or secure storage.
115
115
 
@@ -132,14 +132,14 @@ The wrapper forwards regular Claude Code flags and arguments. The local proxy is
132
132
 
133
133
  `claude-scionos doctor` should be the first command to run when a client reports an issue.
134
134
 
135
- Common cases:
136
-
137
- - `Claude Code CLI not found`: install `@anthropic-ai/claude-code`
138
- - `Git Bash is required on Windows`: install Git for Windows
139
- - `ANTHROPIC_AUTH_TOKEN ... is required when using --no-prompt`: set the environment variable or store the token first
140
- - `Secure token file was created but no encrypted content was written`: update to `4.1.9` or later, then re-run `claude-scionos auth login`
141
- - `Stored token` is missing on Windows even though you already logged in: re-run `claude-scionos auth login` because the local DPAPI token file may be empty or corrupted
142
- - `secret-tool not found`: install a Secret Service client on Linux or rely on the environment variable
135
+ Common cases:
136
+
137
+ - `Claude Code CLI not found`: install `@anthropic-ai/claude-code`
138
+ - `Git Bash is required on Windows`: install Git for Windows
139
+ - `ANTHROPIC_AUTH_TOKEN ... is required when using --no-prompt`: set the environment variable or store the token first
140
+ - `Secure token file was created but no encrypted content was written`: update to `4.1.10` or later, then re-run `claude-scionos auth login`
141
+ - `Stored token` is missing on Windows even though you already logged in: re-run `claude-scionos auth login` because the local DPAPI token file may be empty or corrupted
142
+ - `secret-tool not found`: install a Secret Service client on Linux or rely on the environment variable
143
143
 
144
144
  ## Development
145
145
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
- {
2
- "name": "claude-scionos",
3
- "version": "4.1.9",
1
+ {
2
+ "name": "claude-scionos",
3
+ "version": "4.1.10",
4
4
  "description": "RouterLab launcher, strategy proxy and secure token wrapper for Claude Code CLI",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -20,7 +20,7 @@
20
20
  "release:patch": "npm version patch -m \"Chore: Bump version to %s\"",
21
21
  "release:minor": "npm version minor -m \"Chore: Bump version to %s\"",
22
22
  "release:major": "npm version major -m \"Chore: Bump version to %s\""
23
- },
23
+ },
24
24
  "keywords": [
25
25
  "claude",
26
26
  "scionos",
@@ -28,27 +28,27 @@
28
28
  "cli",
29
29
  "wrapper"
30
30
  ],
31
- "author": "ScioNos",
32
- "license": "MIT",
31
+ "author": "ScioNos",
32
+ "license": "MIT",
33
33
  "homepage": "https://routerlab.ch",
34
- "repository": {
35
- "type": "git",
36
- "url": "git+https://github.com/ScioNos/claude-scionos.git"
37
- },
38
- "bugs": {
39
- "url": "https://github.com/ScioNos/claude-scionos/issues"
40
- },
41
- "engines": {
42
- "node": ">=22"
43
- },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/ScioNos/claude-scionos.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/ScioNos/claude-scionos/issues"
40
+ },
41
+ "engines": {
42
+ "node": ">=22"
43
+ },
44
44
  "private": false,
45
45
  "dependencies": {
46
46
  "@inquirer/prompts": "^8.4.1"
47
47
  },
48
- "devDependencies": {
49
- "@eslint/js": "^10.0.1",
50
- "eslint": "^10.2.0",
51
- "globals": "^17.5.0",
52
- "vitest": "^4.1.4"
53
- }
54
- }
48
+ "devDependencies": {
49
+ "@eslint/js": "^10.0.1",
50
+ "eslint": "^10.2.0",
51
+ "globals": "^17.5.0",
52
+ "vitest": "^4.1.4"
53
+ }
54
+ }
package/src/routerlab.js CHANGED
@@ -36,6 +36,10 @@ const BASE_URL = SERVICES[DEFAULT_SERVICE].baseUrl;
36
36
  const TOKEN_HELP_URL = SERVICES[DEFAULT_SERVICE].tokenHelpUrl;
37
37
  const DEFAULT_ANTHROPIC_VERSION = '2023-06-01';
38
38
  const SECURE_STORAGE_SERVICE = 'claude-scionos';
39
+ const WINDOWS_POWERSHELL_MODULE_PATHS = [
40
+ 'C:\\Program Files\\WindowsPowerShell\\Modules',
41
+ 'C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\Modules',
42
+ ];
39
43
  const SECURE_STORAGE_ACCOUNT = SERVICES[DEFAULT_SERVICE].secureStorageAccount;
40
44
  const DEFAULT_CLAUDE_MODELS = [
41
45
  'claude-haiku-4-5-20251001',
@@ -69,10 +73,10 @@ const STRATEGIES = [
69
73
  {
70
74
  value: 'claude-glm-5',
71
75
  name: 'GLM-5',
72
- description: 'Forces all requests to claude-glm-5.',
73
- selectionDescription: 'Forces all requests to claude-glm-5.',
74
- mappedModels: ['claude-glm-5'],
75
- },
76
+ description: 'Forces all requests to claude-glm-5.',
77
+ selectionDescription: 'Forces all requests to claude-glm-5.',
78
+ mappedModels: ['claude-glm-5'],
79
+ },
76
80
  {
77
81
  value: 'claude-minimax-m2.5',
78
82
  name: 'MiniMax M2.5',
@@ -95,85 +99,85 @@ const STRATEGIES = [
95
99
  mappedModels: ['claude-qwen3.6-plus'],
96
100
  },
97
101
  ];
98
-
102
+
99
103
  async function fetchModels(apiKey, options = {}) {
100
104
  const {
101
105
  baseUrl = BASE_URL,
102
- anthropicVersion = DEFAULT_ANTHROPIC_VERSION,
103
- timeoutMs = 30000,
104
- } = options;
105
-
106
- try {
107
- const controller = new AbortController();
108
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
109
- const response = await fetch(`${baseUrl}/v1/models`, {
110
- method: 'GET',
111
- headers: {
112
- 'x-api-key': apiKey,
113
- 'anthropic-version': anthropicVersion,
114
- },
115
- signal: controller.signal,
116
- });
117
- clearTimeout(timeoutId);
118
-
119
- if (response.ok) {
120
- let payload = null;
121
- try {
122
- payload = await response.json();
123
- } catch {
124
- payload = null;
125
- }
126
-
127
- return {
128
- valid: true,
129
- models: payload ? extractModelIds(payload) : null,
130
- };
131
- }
132
-
133
- if (response.status === 401 || response.status === 403) {
134
- return {valid: false, reason: 'auth_failed'};
135
- }
136
-
137
- return {
138
- valid: false,
139
- reason: 'server_error',
140
- status: response.status,
141
- statusText: response.statusText,
142
- message: `Server responded with ${response.status} ${response.statusText}`.trim(),
143
- };
144
- } catch (error) {
145
- if (error.name === 'AbortError') {
146
- return {valid: false, reason: 'timeout', message: `Request timed out after ${Math.round(timeoutMs / 1000)}s`};
147
- }
148
-
149
- return {valid: false, reason: 'network_error', message: error.message};
150
- }
151
- }
152
-
153
- async function validateToken(apiKey, options = {}) {
154
- return fetchModels(apiKey, options);
155
- }
156
-
157
- function extractModelIds(payload) {
158
- if (!payload) {
159
- return [];
160
- }
161
-
162
- if (Array.isArray(payload)) {
163
- return payload.map((entry) => entry?.id ?? entry?.name ?? entry).filter(Boolean);
164
- }
165
-
166
- if (Array.isArray(payload.data)) {
167
- return payload.data.map((entry) => entry?.id ?? entry?.name ?? entry).filter(Boolean);
168
- }
169
-
170
- if (Array.isArray(payload.models)) {
171
- return payload.models.map((entry) => entry?.id ?? entry?.name ?? entry).filter(Boolean);
172
- }
173
-
174
- return [];
175
- }
176
-
106
+ anthropicVersion = DEFAULT_ANTHROPIC_VERSION,
107
+ timeoutMs = 30000,
108
+ } = options;
109
+
110
+ try {
111
+ const controller = new AbortController();
112
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
113
+ const response = await fetch(`${baseUrl}/v1/models`, {
114
+ method: 'GET',
115
+ headers: {
116
+ 'x-api-key': apiKey,
117
+ 'anthropic-version': anthropicVersion,
118
+ },
119
+ signal: controller.signal,
120
+ });
121
+ clearTimeout(timeoutId);
122
+
123
+ if (response.ok) {
124
+ let payload = null;
125
+ try {
126
+ payload = await response.json();
127
+ } catch {
128
+ payload = null;
129
+ }
130
+
131
+ return {
132
+ valid: true,
133
+ models: payload ? extractModelIds(payload) : null,
134
+ };
135
+ }
136
+
137
+ if (response.status === 401 || response.status === 403) {
138
+ return {valid: false, reason: 'auth_failed'};
139
+ }
140
+
141
+ return {
142
+ valid: false,
143
+ reason: 'server_error',
144
+ status: response.status,
145
+ statusText: response.statusText,
146
+ message: `Server responded with ${response.status} ${response.statusText}`.trim(),
147
+ };
148
+ } catch (error) {
149
+ if (error.name === 'AbortError') {
150
+ return {valid: false, reason: 'timeout', message: `Request timed out after ${Math.round(timeoutMs / 1000)}s`};
151
+ }
152
+
153
+ return {valid: false, reason: 'network_error', message: error.message};
154
+ }
155
+ }
156
+
157
+ async function validateToken(apiKey, options = {}) {
158
+ return fetchModels(apiKey, options);
159
+ }
160
+
161
+ function extractModelIds(payload) {
162
+ if (!payload) {
163
+ return [];
164
+ }
165
+
166
+ if (Array.isArray(payload)) {
167
+ return payload.map((entry) => entry?.id ?? entry?.name ?? entry).filter(Boolean);
168
+ }
169
+
170
+ if (Array.isArray(payload.data)) {
171
+ return payload.data.map((entry) => entry?.id ?? entry?.name ?? entry).filter(Boolean);
172
+ }
173
+
174
+ if (Array.isArray(payload.models)) {
175
+ return payload.models.map((entry) => entry?.id ?? entry?.name ?? entry).filter(Boolean);
176
+ }
177
+
178
+ return [];
179
+ }
180
+
177
181
  function canProceedWithValidation(validation) {
178
182
  return validation.valid === true;
179
183
  }
@@ -256,9 +260,9 @@ function assessStrategy(strategyValue, modelIds = [], serviceValue = DEFAULT_SER
256
260
  const strategy = findStrategy(strategyValue, serviceValue);
257
261
  if (!strategy) {
258
262
  return {
259
- available: false,
260
- level: 'unavailable',
261
- note: 'Unknown strategy.',
263
+ available: false,
264
+ level: 'unavailable',
265
+ note: 'Unknown strategy.',
262
266
  strategy: null,
263
267
  };
264
268
  }
@@ -293,17 +297,17 @@ function assessStrategy(strategyValue, modelIds = [], serviceValue = DEFAULT_SER
293
297
  strategy,
294
298
  };
295
299
  }
296
-
297
- if (presentCount > 0) {
298
- return {
300
+
301
+ if (presentCount > 0) {
302
+ return {
299
303
  available: true,
300
304
  level: 'partial',
301
305
  note: `Partially available on ${serviceLabel}.`,
302
306
  strategy,
303
307
  };
304
308
  }
305
-
306
- return {
309
+
310
+ return {
307
311
  available: false,
308
312
  level: 'unavailable',
309
313
  note: `Not reported by ${serviceLabel}.`,
@@ -383,30 +387,30 @@ function getStrategyChoices(modelIds = [], serviceValue = DEFAULT_SERVICE) {
383
387
  description: strategy.selectionDescription ?? strategy.description,
384
388
  }));
385
389
  }
386
-
387
- function getSecureStorageBackend() {
388
- if (process.platform === 'win32') {
389
- return {supported: true, backend: 'Windows DPAPI'};
390
- }
391
-
392
- if (process.platform === 'darwin') {
393
- return {supported: true, backend: 'macOS Keychain'};
394
- }
395
-
396
- if (process.platform === 'linux') {
397
- return commandExists('secret-tool')
398
- ? {supported: true, backend: 'Linux Secret Service'}
399
- : {supported: false, backend: 'Linux Secret Service', reason: '`secret-tool` not found'};
400
- }
401
-
402
- return {supported: false, backend: 'Unknown', reason: 'Unsupported operating system'};
403
- }
404
-
405
- function commandExists(command) {
406
- const result = spawnSync(command, ['--help'], {encoding: 'utf8'});
407
- return !result.error;
408
- }
409
-
390
+
391
+ function getSecureStorageBackend() {
392
+ if (process.platform === 'win32') {
393
+ return {supported: true, backend: 'Windows DPAPI'};
394
+ }
395
+
396
+ if (process.platform === 'darwin') {
397
+ return {supported: true, backend: 'macOS Keychain'};
398
+ }
399
+
400
+ if (process.platform === 'linux') {
401
+ return commandExists('secret-tool')
402
+ ? {supported: true, backend: 'Linux Secret Service'}
403
+ : {supported: false, backend: 'Linux Secret Service', reason: '`secret-tool` not found'};
404
+ }
405
+
406
+ return {supported: false, backend: 'Unknown', reason: 'Unsupported operating system'};
407
+ }
408
+
409
+ function commandExists(command) {
410
+ const result = spawnSync(command, ['--help'], {encoding: 'utf8'});
411
+ return !result.error;
412
+ }
413
+
410
414
  function runPowerShell(command, options = {}) {
411
415
  const {
412
416
  env = {},
@@ -415,41 +419,43 @@ function runPowerShell(command, options = {}) {
415
419
  const powershell = process.env.SystemRoot
416
420
  ? path.join(process.env.SystemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
417
421
  : 'powershell.exe';
422
+ const childEnv = {
423
+ ...process.env,
424
+ ...(process.platform === 'win32' ? {PSModulePath: WINDOWS_POWERSHELL_MODULE_PATHS.join(';')} : {}),
425
+ ...env,
426
+ };
418
427
 
419
428
  const result = spawnSync(powershell, ['-NoProfile', '-NonInteractive', '-Command', command], {
420
429
  encoding: 'utf8',
421
430
  input,
422
- env: {
423
- ...process.env,
424
- ...env,
425
- },
426
- });
427
-
428
- if (result.error) {
429
- throw result.error;
430
- }
431
-
432
- if (result.status !== 0) {
433
- throw new Error((result.stderr || result.stdout || 'PowerShell command failed').trim());
434
- }
435
-
436
- return result.stdout.trim();
437
- }
438
-
439
- function runCommand(command, args, options = {}) {
440
- const result = spawnSync(command, args, {
441
- encoding: 'utf8',
442
- input: options.input,
443
- env: options.env,
444
- });
445
-
446
- if (result.error) {
447
- throw result.error;
448
- }
449
-
450
- return result;
451
- }
452
-
431
+ env: childEnv,
432
+ });
433
+
434
+ if (result.error) {
435
+ throw result.error;
436
+ }
437
+
438
+ if (result.status !== 0) {
439
+ throw new Error((result.stderr || result.stdout || 'PowerShell command failed').trim());
440
+ }
441
+
442
+ return result.stdout.trim();
443
+ }
444
+
445
+ function runCommand(command, args, options = {}) {
446
+ const result = spawnSync(command, args, {
447
+ encoding: 'utf8',
448
+ input: options.input,
449
+ env: options.env,
450
+ });
451
+
452
+ if (result.error) {
453
+ throw result.error;
454
+ }
455
+
456
+ return result;
457
+ }
458
+
453
459
  function storeToken(token, serviceValue = DEFAULT_SERVICE) {
454
460
  const service = getServiceConfig(serviceValue);
455
461
  if (!service) {
@@ -459,7 +465,7 @@ function storeToken(token, serviceValue = DEFAULT_SERVICE) {
459
465
  const storage = getSecureStorageBackend();
460
466
  if (!storage.supported) {
461
467
  throw new Error(storage.reason || 'Secure storage is not available on this machine');
462
- }
468
+ }
463
469
 
464
470
  if (process.platform === 'win32') {
465
471
  const tokenFile = getWindowsTokenFile(service.value);
@@ -476,9 +482,9 @@ function storeToken(token, serviceValue = DEFAULT_SERVICE) {
476
482
 
477
483
  return storage;
478
484
  }
479
-
480
- if (process.platform === 'darwin') {
481
- const result = runCommand('security', [
485
+
486
+ if (process.platform === 'darwin') {
487
+ const result = runCommand('security', [
482
488
  'add-generic-password',
483
489
  '-U',
484
490
  '-a',
@@ -486,16 +492,16 @@ function storeToken(token, serviceValue = DEFAULT_SERVICE) {
486
492
  '-s',
487
493
  SECURE_STORAGE_SERVICE,
488
494
  '-w',
489
- token,
490
- ]);
491
-
492
- if (result.status !== 0) {
493
- throw new Error((result.stderr || result.stdout || 'Unable to store token in Keychain').trim());
494
- }
495
-
496
- return storage;
497
- }
498
-
495
+ token,
496
+ ]);
497
+
498
+ if (result.status !== 0) {
499
+ throw new Error((result.stderr || result.stdout || 'Unable to store token in Keychain').trim());
500
+ }
501
+
502
+ return storage;
503
+ }
504
+
499
505
  const result = runCommand('secret-tool', [
500
506
  'store',
501
507
  `--label=${service.secureStorageLabel}`,
@@ -506,14 +512,14 @@ function storeToken(token, serviceValue = DEFAULT_SERVICE) {
506
512
  ], {
507
513
  input: token,
508
514
  });
509
-
510
- if (result.status !== 0) {
511
- throw new Error((result.stderr || result.stdout || 'Unable to store token in Secret Service').trim());
512
- }
513
-
514
- return storage;
515
- }
516
-
515
+
516
+ if (result.status !== 0) {
517
+ throw new Error((result.stderr || result.stdout || 'Unable to store token in Secret Service').trim());
518
+ }
519
+
520
+ return storage;
521
+ }
522
+
517
523
  function getStoredToken(serviceValue = DEFAULT_SERVICE) {
518
524
  const service = getServiceConfig(serviceValue);
519
525
  if (!service) {
@@ -546,8 +552,8 @@ function getStoredToken(serviceValue = DEFAULT_SERVICE) {
546
552
  );
547
553
  return token || null;
548
554
  }
549
-
550
- if (process.platform === 'darwin') {
555
+
556
+ if (process.platform === 'darwin') {
551
557
  const result = runCommand('security', [
552
558
  'find-generic-password',
553
559
  '-a',
@@ -555,10 +561,10 @@ function getStoredToken(serviceValue = DEFAULT_SERVICE) {
555
561
  '-s',
556
562
  SECURE_STORAGE_SERVICE,
557
563
  '-w',
558
- ]);
559
- return result.status === 0 ? result.stdout.trim() || null : null;
560
- }
561
-
564
+ ]);
565
+ return result.status === 0 ? result.stdout.trim() || null : null;
566
+ }
567
+
562
568
  const result = runCommand('secret-tool', [
563
569
  'lookup',
564
570
  'service',
@@ -598,9 +604,9 @@ function deleteStoredToken(serviceValue = DEFAULT_SERVICE) {
598
604
  '-s',
599
605
  SECURE_STORAGE_SERVICE,
600
606
  ]);
601
- return result.status === 0;
602
- }
603
-
607
+ return result.status === 0;
608
+ }
609
+
604
610
  const result = runCommand('secret-tool', [
605
611
  'clear',
606
612
  'service',
@@ -616,10 +622,10 @@ function getStoredTokenStatus(serviceValue = DEFAULT_SERVICE) {
616
622
  const storedToken = getStoredToken(serviceValue);
617
623
  return {
618
624
  ...storage,
619
- stored: Boolean(storedToken),
620
- };
621
- }
622
-
625
+ stored: Boolean(storedToken),
626
+ };
627
+ }
628
+
623
629
  export {
624
630
  BASE_URL,
625
631
  DEFAULT_CLAUDE_MODELS,