jbai-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # jbai-cli
2
+
3
+ **Use AI coding tools with your JetBrains AI subscription** — no separate API keys needed.
4
+
5
+ One token, all tools: Claude Code, Codex, Aider, Gemini, OpenCode.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g jbai-cli
11
+ ```
12
+
13
+ ## Setup (2 minutes)
14
+
15
+ ### Step 1: Get your token
16
+
17
+ 1. Go to [platform.jetbrains.ai](https://platform.jetbrains.ai/) (or [staging](https://platform.stgn.jetbrains.ai/))
18
+ 2. Click your **Profile** icon (top right)
19
+ 3. Click **"Copy Developer Token"**
20
+
21
+ ### Step 2: Save your token
22
+
23
+ ```bash
24
+ jbai token set
25
+ # Paste your token when prompted
26
+ ```
27
+
28
+ ### Step 3: Verify it works
29
+
30
+ ```bash
31
+ jbai test
32
+ ```
33
+
34
+ Expected output:
35
+ ```
36
+ Testing JetBrains AI Platform (staging)
37
+
38
+ 1. OpenAI Proxy (GPT): ✅ Working
39
+ 2. Anthropic Proxy (Claude): ✅ Working
40
+ 3. Google Proxy (Gemini): ✅ Working
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ### Claude Code
46
+ ```bash
47
+ jbai-claude
48
+ ```
49
+
50
+ ### Codex CLI
51
+ ```bash
52
+ # Interactive mode
53
+ jbai-codex
54
+
55
+ # One-shot task
56
+ jbai-codex exec "explain this codebase"
57
+ ```
58
+
59
+ ### Aider
60
+ ```bash
61
+ jbai-aider
62
+ ```
63
+
64
+ ### Gemini
65
+ ```bash
66
+ # Interactive chat
67
+ jbai-gemini
68
+
69
+ # One-shot question
70
+ jbai-gemini "What is Kubernetes?"
71
+ ```
72
+
73
+ ### OpenCode
74
+ ```bash
75
+ jbai-opencode
76
+ ```
77
+
78
+ ## Using Different Models
79
+
80
+ Each tool has a sensible default, but you can specify any available model:
81
+
82
+ ```bash
83
+ # Claude with Opus
84
+ jbai-claude --model claude-opus-4-1-20250805
85
+
86
+ # Codex with GPT-5
87
+ jbai-codex --model gpt-5-2025-08-07
88
+
89
+ # Gemini with Pro
90
+ jbai-gemini --model gemini-2.5-pro "Your question"
91
+ ```
92
+
93
+ ### Available Models
94
+
95
+ **Claude (Anthropic)**
96
+ | Model | Notes |
97
+ |-------|-------|
98
+ | `claude-sonnet-4-5-20250929` | Default, recommended |
99
+ | `claude-opus-4-1-20250805` | Most capable |
100
+ | `claude-sonnet-4-20250514` | |
101
+ | `claude-3-7-sonnet-20250219` | |
102
+ | `claude-3-5-haiku-20241022` | Fast |
103
+
104
+ **GPT (OpenAI)**
105
+ | Model | Notes |
106
+ |-------|-------|
107
+ | `gpt-4o-2024-11-20` | Default |
108
+ | `gpt-5-2025-08-07` | Latest |
109
+ | `gpt-5.1-2025-11-13` | |
110
+ | `gpt-5-mini-2025-08-07` | Fast |
111
+ | `o3-2025-04-16` | Reasoning |
112
+ | `o3-mini-2025-01-31` | |
113
+
114
+ **Gemini (Google)**
115
+ | Model | Notes |
116
+ |-------|-------|
117
+ | `gemini-2.5-flash` | Default, fast |
118
+ | `gemini-2.5-pro` | More capable |
119
+ | `gemini-3-pro-preview` | Preview |
120
+ | `gemini-3-flash-preview` | Preview |
121
+
122
+ ## Commands Reference
123
+
124
+ | Command | Description |
125
+ |---------|-------------|
126
+ | `jbai help` | Show help |
127
+ | `jbai token` | Show token status |
128
+ | `jbai token set` | Set/update token |
129
+ | `jbai test` | Test API connections |
130
+ | `jbai models` | List all models |
131
+ | `jbai env staging` | Use staging environment |
132
+ | `jbai env production` | Use production environment |
133
+
134
+ ## Prerequisites
135
+
136
+ Install the underlying tools you want to use:
137
+
138
+ | Tool | Install Command |
139
+ |------|-----------------|
140
+ | Claude Code | `npm i -g @anthropic-ai/claude-code` |
141
+ | Codex | `npm i -g @openai/codex` |
142
+ | Aider | `pip install aider-chat` |
143
+ | OpenCode | `go install github.com/opencode-ai/opencode@latest` |
144
+ | Gemini | Built-in, no install needed |
145
+
146
+ ## Token Management
147
+
148
+ ```bash
149
+ # Check token status (shows expiry date)
150
+ jbai token
151
+
152
+ # Update expired token
153
+ jbai token set
154
+ ```
155
+
156
+ Tokens are stored securely at `~/.jbai/token`
157
+
158
+ ## Switching Environments
159
+
160
+ ```bash
161
+ # Staging (default) - for testing
162
+ jbai env staging
163
+
164
+ # Production - for real work
165
+ jbai env production
166
+ ```
167
+
168
+ > **Note**: Staging and production use different tokens. Get the right one from the corresponding platform URL.
169
+
170
+ ## How It Works
171
+
172
+ jbai-cli uses JetBrains AI Platform's **Guarded Proxy**, which provides API-compatible endpoints:
173
+
174
+ - OpenAI API → `api.jetbrains.ai/user/v5/llm/openai/v1`
175
+ - Anthropic API → `api.jetbrains.ai/user/v5/llm/anthropic/v1`
176
+ - Google Vertex → `api.jetbrains.ai/user/v5/llm/google/v1/vertex`
177
+
178
+ Your JetBrains AI token authenticates all requests via the `Grazie-Authenticate-JWT` header.
179
+
180
+ ## Troubleshooting
181
+
182
+ ### "Token expired"
183
+ ```bash
184
+ jbai token set
185
+ # Get fresh token from platform.jetbrains.ai
186
+ ```
187
+
188
+ ### "Claude Code not found"
189
+ ```bash
190
+ npm install -g @anthropic-ai/claude-code
191
+ ```
192
+
193
+ ### "Connection failed"
194
+ ```bash
195
+ # Test which endpoints work
196
+ jbai test
197
+
198
+ # Check your environment
199
+ jbai token
200
+ ```
201
+
202
+ ### Wrong environment
203
+ ```bash
204
+ # Staging token won't work with production
205
+ jbai env staging # if using staging token
206
+ jbai env production # if using production token
207
+ ```
208
+
209
+ ## License
210
+
211
+ MIT
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+ const config = require('../lib/config');
5
+
6
+ const token = config.getToken();
7
+ if (!token) {
8
+ console.error('❌ No token found. Run: jbai token set');
9
+ process.exit(1);
10
+ }
11
+
12
+ if (config.isTokenExpired(token)) {
13
+ console.error('⚠️ Token expired. Run: jbai token refresh');
14
+ process.exit(1);
15
+ }
16
+
17
+ const endpoints = config.getEndpoints();
18
+ const args = process.argv.slice(2);
19
+
20
+ // Build aider arguments
21
+ const hasModel = args.includes('--model');
22
+ const aiderArgs = [
23
+ '--openai-api-base', endpoints.openai,
24
+ '--openai-api-key', 'placeholder',
25
+ '--extra-headers', JSON.stringify({ 'Grazie-Authenticate-JWT': token })
26
+ ];
27
+
28
+ if (!hasModel) {
29
+ aiderArgs.push('--model', config.MODELS.openai.default);
30
+ }
31
+ aiderArgs.push(...args);
32
+
33
+ const child = spawn('aider', aiderArgs, {
34
+ stdio: 'inherit',
35
+ env: process.env
36
+ });
37
+
38
+ child.on('error', (err) => {
39
+ if (err.code === 'ENOENT') {
40
+ console.error('❌ Aider not found. Install: pip install aider-chat');
41
+ } else {
42
+ console.error(`Error: ${err.message}`);
43
+ }
44
+ process.exit(1);
45
+ });
46
+
47
+ child.on('exit', (code) => process.exit(code || 0));
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+ const config = require('../lib/config');
5
+
6
+ const token = config.getToken();
7
+ if (!token) {
8
+ console.error('❌ No token found. Run: jbai token set');
9
+ process.exit(1);
10
+ }
11
+
12
+ if (config.isTokenExpired(token)) {
13
+ console.error('⚠️ Token expired. Run: jbai token refresh');
14
+ process.exit(1);
15
+ }
16
+
17
+ const endpoints = config.getEndpoints();
18
+ const args = process.argv.slice(2);
19
+
20
+ // Check if model specified
21
+ const hasModel = args.includes('--model') || args.includes('-m');
22
+ const finalArgs = hasModel ? args : ['--model', config.MODELS.claude.default, ...args];
23
+
24
+ // Set environment for Claude Code
25
+ const env = {
26
+ ...process.env,
27
+ ANTHROPIC_BASE_URL: endpoints.anthropic,
28
+ ANTHROPIC_API_KEY: token,
29
+ ANTHROPIC_AUTH_TOKEN: token
30
+ };
31
+
32
+ const child = spawn('claude', finalArgs, {
33
+ stdio: 'inherit',
34
+ env
35
+ });
36
+
37
+ child.on('error', (err) => {
38
+ if (err.code === 'ENOENT') {
39
+ console.error('❌ Claude Code not found. Install: npm install -g @anthropic-ai/claude-code');
40
+ } else {
41
+ console.error(`Error: ${err.message}`);
42
+ }
43
+ process.exit(1);
44
+ });
45
+
46
+ child.on('exit', (code) => process.exit(code || 0));
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const config = require('../lib/config');
8
+
9
+ const token = config.getToken();
10
+ if (!token) {
11
+ console.error('❌ No token found. Run: jbai token set');
12
+ process.exit(1);
13
+ }
14
+
15
+ if (config.isTokenExpired(token)) {
16
+ console.error('⚠️ Token expired. Run: jbai token refresh');
17
+ process.exit(1);
18
+ }
19
+
20
+ const endpoints = config.getEndpoints();
21
+ const env = config.getEnvironment();
22
+ const providerName = env === 'staging' ? 'jbai-staging' : 'jbai';
23
+ const envVarName = env === 'staging' ? 'GRAZIE_STAGING_TOKEN' : 'GRAZIE_API_TOKEN';
24
+
25
+ // Ensure Codex config exists
26
+ const codexDir = path.join(os.homedir(), '.codex');
27
+ const codexConfig = path.join(codexDir, 'config.toml');
28
+
29
+ if (!fs.existsSync(codexDir)) {
30
+ fs.mkdirSync(codexDir, { recursive: true });
31
+ }
32
+
33
+ // Check if our provider is configured
34
+ let configContent = '';
35
+ if (fs.existsSync(codexConfig)) {
36
+ configContent = fs.readFileSync(codexConfig, 'utf-8');
37
+ }
38
+
39
+ if (!configContent.includes(`[model_providers.${providerName}]`)) {
40
+ const providerConfig = `
41
+ # JetBrains AI (${env})
42
+ [model_providers.${providerName}]
43
+ name = "JetBrains AI (${env})"
44
+ base_url = "${endpoints.openai}"
45
+ env_http_headers = { "Grazie-Authenticate-JWT" = "${envVarName}" }
46
+ wire_api = "responses"
47
+ `;
48
+ fs.appendFileSync(codexConfig, providerConfig);
49
+ console.log(`✅ Added ${providerName} provider to Codex config`);
50
+ }
51
+
52
+ const args = process.argv.slice(2);
53
+ const hasModel = args.includes('--model');
54
+ const finalArgs = ['-c', `model_provider=${providerName}`];
55
+
56
+ if (!hasModel) {
57
+ finalArgs.push('--model', config.MODELS.openai.default);
58
+ }
59
+ finalArgs.push(...args);
60
+
61
+ const childEnv = {
62
+ ...process.env,
63
+ [envVarName]: token
64
+ };
65
+
66
+ const child = spawn('codex', finalArgs, {
67
+ stdio: 'inherit',
68
+ env: childEnv
69
+ });
70
+
71
+ child.on('error', (err) => {
72
+ if (err.code === 'ENOENT') {
73
+ console.error('❌ Codex not found. Install: npm install -g @openai/codex');
74
+ } else {
75
+ console.error(`Error: ${err.message}`);
76
+ }
77
+ process.exit(1);
78
+ });
79
+
80
+ child.on('exit', (code) => process.exit(code || 0));
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+
3
+ const https = require('https');
4
+ const readline = require('readline');
5
+ const config = require('../lib/config');
6
+
7
+ const token = config.getToken();
8
+ if (!token) {
9
+ console.error('❌ No token found. Run: jbai token set');
10
+ process.exit(1);
11
+ }
12
+
13
+ if (config.isTokenExpired(token)) {
14
+ console.error('⚠️ Token expired. Run: jbai token refresh');
15
+ process.exit(1);
16
+ }
17
+
18
+ const endpoints = config.getEndpoints();
19
+ const args = process.argv.slice(2);
20
+
21
+ // Get model from args or use default
22
+ let model = config.MODELS.gemini.default;
23
+ const modelIdx = args.indexOf('--model');
24
+ if (modelIdx !== -1 && args[modelIdx + 1]) {
25
+ model = args[modelIdx + 1];
26
+ }
27
+
28
+ // If prompt provided as argument, run one-shot
29
+ const prompt = args.filter((a, i) =>
30
+ a !== '--model' && (modelIdx === -1 || i !== modelIdx + 1)
31
+ ).join(' ');
32
+
33
+ if (prompt) {
34
+ runPrompt(prompt, model);
35
+ } else {
36
+ runInteractive(model);
37
+ }
38
+
39
+ async function runPrompt(prompt, model) {
40
+ const url = `${endpoints.google}/v1/projects/default/locations/default/publishers/google/models/${model}:generateContent`;
41
+
42
+ try {
43
+ const result = await httpPost(url, {
44
+ contents: [{ role: 'user', parts: [{ text: prompt }] }]
45
+ }, { 'Grazie-Authenticate-JWT': token });
46
+
47
+ if (result.candidates && result.candidates[0]) {
48
+ console.log(result.candidates[0].content.parts[0].text);
49
+ } else if (result.error) {
50
+ console.error(`Error: ${result.error.message}`);
51
+ }
52
+ } catch (e) {
53
+ console.error(`Error: ${e.message}`);
54
+ }
55
+ }
56
+
57
+ async function runInteractive(model) {
58
+ console.log(`Gemini Interactive (${model})`);
59
+ console.log('Type your message, press Enter to send. Ctrl+C to exit.\n');
60
+
61
+ const rl = readline.createInterface({
62
+ input: process.stdin,
63
+ output: process.stdout
64
+ });
65
+
66
+ const history = [];
67
+
68
+ const askQuestion = () => {
69
+ rl.question('You: ', async (input) => {
70
+ if (!input.trim()) {
71
+ askQuestion();
72
+ return;
73
+ }
74
+
75
+ history.push({ role: 'user', parts: [{ text: input }] });
76
+
77
+ const url = `${endpoints.google}/v1/projects/default/locations/default/publishers/google/models/${model}:generateContent`;
78
+
79
+ try {
80
+ const result = await httpPost(url, { contents: history }, { 'Grazie-Authenticate-JWT': token });
81
+
82
+ if (result.candidates && result.candidates[0]) {
83
+ const response = result.candidates[0].content.parts[0].text;
84
+ history.push({ role: 'model', parts: [{ text: response }] });
85
+ console.log(`\nGemini: ${response}\n`);
86
+ } else if (result.error) {
87
+ console.error(`Error: ${result.error.message}\n`);
88
+ }
89
+ } catch (e) {
90
+ console.error(`Error: ${e.message}\n`);
91
+ }
92
+
93
+ askQuestion();
94
+ });
95
+ };
96
+
97
+ rl.on('close', () => {
98
+ console.log('\nGoodbye!');
99
+ process.exit(0);
100
+ });
101
+
102
+ askQuestion();
103
+ }
104
+
105
+ function httpPost(url, body, headers) {
106
+ return new Promise((resolve, reject) => {
107
+ const urlObj = new URL(url);
108
+ const data = JSON.stringify(body);
109
+
110
+ const req = https.request({
111
+ hostname: urlObj.hostname,
112
+ port: 443,
113
+ path: urlObj.pathname,
114
+ method: 'POST',
115
+ headers: {
116
+ 'Content-Type': 'application/json',
117
+ 'Content-Length': Buffer.byteLength(data),
118
+ ...headers
119
+ }
120
+ }, (res) => {
121
+ let body = '';
122
+ res.on('data', chunk => body += chunk);
123
+ res.on('end', () => {
124
+ try {
125
+ resolve(JSON.parse(body));
126
+ } catch {
127
+ reject(new Error('Invalid response'));
128
+ }
129
+ });
130
+ });
131
+
132
+ req.on('error', reject);
133
+ req.write(data);
134
+ req.end();
135
+ });
136
+ }
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+ const config = require('../lib/config');
5
+
6
+ const token = config.getToken();
7
+ if (!token) {
8
+ console.error('❌ No token found. Run: jbai token set');
9
+ process.exit(1);
10
+ }
11
+
12
+ if (config.isTokenExpired(token)) {
13
+ console.error('⚠️ Token expired. Run: jbai token refresh');
14
+ process.exit(1);
15
+ }
16
+
17
+ const endpoints = config.getEndpoints();
18
+ const args = process.argv.slice(2);
19
+
20
+ // Check if model specified
21
+ const hasModel = args.includes('--model') || args.includes('-m');
22
+ const finalArgs = hasModel ? args : ['--model', config.MODELS.claude.default, ...args];
23
+
24
+ // Set environment for OpenCode
25
+ const env = {
26
+ ...process.env,
27
+ ANTHROPIC_BASE_URL: endpoints.anthropic,
28
+ ANTHROPIC_API_KEY: token,
29
+ OPENAI_API_BASE: endpoints.openai,
30
+ OPENAI_API_KEY: token
31
+ };
32
+
33
+ const child = spawn('opencode', finalArgs, {
34
+ stdio: 'inherit',
35
+ env
36
+ });
37
+
38
+ child.on('error', (err) => {
39
+ if (err.code === 'ENOENT') {
40
+ console.error('❌ OpenCode not found. Install: go install github.com/opencode-ai/opencode@latest');
41
+ } else {
42
+ console.error(`Error: ${err.message}`);
43
+ }
44
+ process.exit(1);
45
+ });
46
+
47
+ child.on('exit', (code) => process.exit(code || 0));
package/bin/jbai.js ADDED
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+ const readline = require('readline');
5
+ const https = require('https');
6
+ const config = require('../lib/config');
7
+
8
+ const VERSION = require('../package.json').version;
9
+
10
+ const HELP = `
11
+ jbai-cli v${VERSION} - JetBrains AI Platform CLI Tools
12
+
13
+ COMMANDS:
14
+ jbai token Show token status
15
+ jbai token set Set token interactively
16
+ jbai token refresh Refresh expired token
17
+ jbai test Test all API endpoints
18
+ jbai env [staging|production] Switch environment
19
+ jbai models List available models
20
+ jbai help Show this help
21
+
22
+ TOOL WRAPPERS:
23
+ jbai-claude Launch Claude Code with JetBrains AI
24
+ jbai-codex Launch Codex CLI with JetBrains AI
25
+ jbai-aider Launch Aider with JetBrains AI
26
+ jbai-gemini Launch Gemini with JetBrains AI
27
+ jbai-opencode Launch OpenCode with JetBrains AI
28
+
29
+ EXAMPLES:
30
+ jbai token set # Set your token
31
+ jbai-claude # Start Claude Code
32
+ jbai-codex exec "explain code" # Run Codex task
33
+ jbai-aider # Start Aider
34
+
35
+ TOKEN:
36
+ Get token: ${config.getEndpoints().tokenUrl}
37
+ Stored at: ${config.TOKEN_FILE}
38
+ `;
39
+
40
+ async function showTokenStatus() {
41
+ const token = config.getToken();
42
+
43
+ if (!token) {
44
+ console.log('❌ No token found');
45
+ console.log(` Run: jbai token set`);
46
+ console.log(` Get token: ${config.getEndpoints().tokenUrl}`);
47
+ return;
48
+ }
49
+
50
+ console.log(`Token file: ${config.TOKEN_FILE}`);
51
+ console.log(`Token length: ${token.length} chars`);
52
+ console.log(`Environment: ${config.getEnvironment()}`);
53
+
54
+ const expiry = config.getTokenExpiry(token);
55
+ if (expiry) {
56
+ const now = new Date();
57
+ const daysLeft = Math.floor((expiry - now) / (1000 * 60 * 60 * 24));
58
+
59
+ if (config.isTokenExpired(token)) {
60
+ console.log(`⚠️ Token EXPIRED: ${expiry.toLocaleString()}`);
61
+ console.log(` Run: jbai token refresh`);
62
+ } else {
63
+ console.log(`✅ Expires: ${expiry.toLocaleString()} (${daysLeft} days left)`);
64
+ }
65
+ }
66
+ }
67
+
68
+ async function setToken() {
69
+ const endpoints = config.getEndpoints();
70
+ console.log(`Get token from: ${endpoints.tokenUrl}`);
71
+ console.log(`Click Profile → 'Copy Developer Token'\n`);
72
+
73
+ const rl = readline.createInterface({
74
+ input: process.stdin,
75
+ output: process.stdout
76
+ });
77
+
78
+ return new Promise((resolve) => {
79
+ rl.question('Paste your token: ', (token) => {
80
+ rl.close();
81
+
82
+ if (!token || !token.includes('.')) {
83
+ console.log('❌ Invalid token format');
84
+ resolve(false);
85
+ return;
86
+ }
87
+
88
+ config.setToken(token);
89
+ console.log('\n✅ Token saved!');
90
+ showTokenStatus();
91
+ resolve(true);
92
+ });
93
+ });
94
+ }
95
+
96
+ async function testEndpoints() {
97
+ const token = config.getToken();
98
+ if (!token) {
99
+ console.log('❌ No token found. Run: jbai token set');
100
+ return;
101
+ }
102
+
103
+ const endpoints = config.getEndpoints();
104
+ console.log(`Testing JetBrains AI Platform (${config.getEnvironment()})\n`);
105
+
106
+ // Test OpenAI
107
+ process.stdout.write('1. OpenAI Proxy (GPT): ');
108
+ try {
109
+ const result = await httpPost(
110
+ `${endpoints.openai}/chat/completions`,
111
+ { model: 'gpt-4o-2024-11-20', messages: [{ role: 'user', content: 'Say OK' }], max_tokens: 5 },
112
+ { 'Grazie-Authenticate-JWT': token }
113
+ );
114
+ console.log(result.choices ? '✅ Working' : '❌ Failed');
115
+ } catch (e) {
116
+ console.log(`❌ ${e.message}`);
117
+ }
118
+
119
+ // Test Anthropic
120
+ process.stdout.write('2. Anthropic Proxy (Claude): ');
121
+ try {
122
+ const result = await httpPost(
123
+ `${endpoints.anthropic}/messages`,
124
+ { model: 'claude-sonnet-4-5-20250929', messages: [{ role: 'user', content: 'Say OK' }], max_tokens: 10 },
125
+ { 'Grazie-Authenticate-JWT': token, 'anthropic-version': '2023-06-01' }
126
+ );
127
+ console.log(result.content ? '✅ Working' : '❌ Failed');
128
+ } catch (e) {
129
+ console.log(`❌ ${e.message}`);
130
+ }
131
+
132
+ // Test Google
133
+ process.stdout.write('3. Google Proxy (Gemini): ');
134
+ try {
135
+ const result = await httpPost(
136
+ `${endpoints.google}/v1/projects/default/locations/default/publishers/google/models/gemini-2.5-flash:generateContent`,
137
+ { contents: [{ role: 'user', parts: [{ text: 'Say OK' }] }] },
138
+ { 'Grazie-Authenticate-JWT': token }
139
+ );
140
+ console.log(result.candidates ? '✅ Working' : '❌ Failed');
141
+ } catch (e) {
142
+ console.log(`❌ ${e.message}`);
143
+ }
144
+ }
145
+
146
+ function httpPost(url, body, headers) {
147
+ return new Promise((resolve, reject) => {
148
+ const urlObj = new URL(url);
149
+ const data = JSON.stringify(body);
150
+
151
+ const req = https.request({
152
+ hostname: urlObj.hostname,
153
+ port: 443,
154
+ path: urlObj.pathname,
155
+ method: 'POST',
156
+ headers: {
157
+ 'Content-Type': 'application/json',
158
+ 'Content-Length': Buffer.byteLength(data),
159
+ ...headers
160
+ }
161
+ }, (res) => {
162
+ let body = '';
163
+ res.on('data', chunk => body += chunk);
164
+ res.on('end', () => {
165
+ try {
166
+ resolve(JSON.parse(body));
167
+ } catch {
168
+ reject(new Error('Invalid response'));
169
+ }
170
+ });
171
+ });
172
+
173
+ req.on('error', reject);
174
+ req.write(data);
175
+ req.end();
176
+ });
177
+ }
178
+
179
+ function showModels() {
180
+ console.log('Available Models:\n');
181
+
182
+ console.log('Claude (Anthropic):');
183
+ config.MODELS.claude.available.forEach((m, i) => {
184
+ const def = m === config.MODELS.claude.default ? ' (default)' : '';
185
+ console.log(` - ${m}${def}`);
186
+ });
187
+
188
+ console.log('\nGPT (OpenAI):');
189
+ config.MODELS.openai.available.forEach((m, i) => {
190
+ const def = m === config.MODELS.openai.default ? ' (default)' : '';
191
+ console.log(` - ${m}${def}`);
192
+ });
193
+
194
+ console.log('\nGemini (Google):');
195
+ config.MODELS.gemini.available.forEach((m, i) => {
196
+ const def = m === config.MODELS.gemini.default ? ' (default)' : '';
197
+ console.log(` - ${m}${def}`);
198
+ });
199
+ }
200
+
201
+ function setEnvironment(env) {
202
+ if (!['staging', 'production'].includes(env)) {
203
+ console.log('Usage: jbai env [staging|production]');
204
+ console.log(`Current: ${config.getEnvironment()}`);
205
+ return;
206
+ }
207
+
208
+ const cfg = config.getConfig();
209
+ cfg.environment = env;
210
+ config.setConfig(cfg);
211
+ console.log(`✅ Switched to ${env}`);
212
+ console.log(` Token URL: ${config.ENDPOINTS[env].tokenUrl}`);
213
+ }
214
+
215
+ // Main
216
+ const [,, command, ...args] = process.argv;
217
+
218
+ switch (command) {
219
+ case 'token':
220
+ if (args[0] === 'set' || args[0] === 'refresh') {
221
+ setToken();
222
+ } else {
223
+ showTokenStatus();
224
+ }
225
+ break;
226
+ case 'test':
227
+ testEndpoints();
228
+ break;
229
+ case 'models':
230
+ showModels();
231
+ break;
232
+ case 'env':
233
+ setEnvironment(args[0]);
234
+ break;
235
+ case 'help':
236
+ case '--help':
237
+ case '-h':
238
+ case undefined:
239
+ console.log(HELP);
240
+ break;
241
+ case 'version':
242
+ case '--version':
243
+ case '-v':
244
+ console.log(`jbai-cli v${VERSION}`);
245
+ break;
246
+ default:
247
+ console.log(`Unknown command: ${command}`);
248
+ console.log('Run: jbai help');
249
+ }
package/lib/config.js ADDED
@@ -0,0 +1,141 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.jbai');
6
+ const TOKEN_FILE = path.join(CONFIG_DIR, 'token');
7
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
8
+
9
+ const ENDPOINTS = {
10
+ staging: {
11
+ base: 'https://api.stgn.jetbrains.ai',
12
+ openai: 'https://api.stgn.jetbrains.ai/user/v5/llm/openai/v1',
13
+ anthropic: 'https://api.stgn.jetbrains.ai/user/v5/llm/anthropic/v1',
14
+ google: 'https://api.stgn.jetbrains.ai/user/v5/llm/google/v1/vertex',
15
+ tokenUrl: 'https://platform.stgn.jetbrains.ai/'
16
+ },
17
+ production: {
18
+ base: 'https://api.jetbrains.ai',
19
+ openai: 'https://api.jetbrains.ai/user/v5/llm/openai/v1',
20
+ anthropic: 'https://api.jetbrains.ai/user/v5/llm/anthropic/v1',
21
+ google: 'https://api.jetbrains.ai/user/v5/llm/google/v1/vertex',
22
+ tokenUrl: 'https://platform.jetbrains.ai/'
23
+ }
24
+ };
25
+
26
+ const MODELS = {
27
+ claude: {
28
+ default: 'claude-sonnet-4-5-20250929',
29
+ available: [
30
+ 'claude-sonnet-4-5-20250929',
31
+ 'claude-opus-4-1-20250805',
32
+ 'claude-sonnet-4-20250514',
33
+ 'claude-3-7-sonnet-20250219',
34
+ 'claude-3-5-haiku-20241022'
35
+ ]
36
+ },
37
+ openai: {
38
+ default: 'gpt-4o-2024-11-20',
39
+ available: [
40
+ 'gpt-4o-2024-11-20',
41
+ 'gpt-5-2025-08-07',
42
+ 'gpt-5.1-2025-11-13',
43
+ 'gpt-5-mini-2025-08-07',
44
+ 'o3-2025-04-16',
45
+ 'o3-mini-2025-01-31'
46
+ ]
47
+ },
48
+ gemini: {
49
+ default: 'gemini-2.5-flash',
50
+ available: [
51
+ 'gemini-2.5-flash',
52
+ 'gemini-2.5-pro',
53
+ 'gemini-3-pro-preview',
54
+ 'gemini-3-flash-preview'
55
+ ]
56
+ }
57
+ };
58
+
59
+ function ensureConfigDir() {
60
+ if (!fs.existsSync(CONFIG_DIR)) {
61
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
62
+ }
63
+ }
64
+
65
+ function getToken() {
66
+ if (!fs.existsSync(TOKEN_FILE)) {
67
+ return null;
68
+ }
69
+ return fs.readFileSync(TOKEN_FILE, 'utf-8').trim();
70
+ }
71
+
72
+ function setToken(token) {
73
+ ensureConfigDir();
74
+ fs.writeFileSync(TOKEN_FILE, token.trim(), { mode: 0o600 });
75
+ }
76
+
77
+ function getConfig() {
78
+ ensureConfigDir();
79
+ if (!fs.existsSync(CONFIG_FILE)) {
80
+ return { environment: 'staging' };
81
+ }
82
+ try {
83
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
84
+ } catch {
85
+ return { environment: 'staging' };
86
+ }
87
+ }
88
+
89
+ function setConfig(config) {
90
+ ensureConfigDir();
91
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
92
+ }
93
+
94
+ function getEnvironment() {
95
+ const config = getConfig();
96
+ return config.environment || 'staging';
97
+ }
98
+
99
+ function getEndpoints() {
100
+ return ENDPOINTS[getEnvironment()];
101
+ }
102
+
103
+ function parseJWT(token) {
104
+ try {
105
+ const payload = token.split('.')[1];
106
+ const decoded = Buffer.from(payload, 'base64url').toString('utf-8');
107
+ return JSON.parse(decoded);
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ function getTokenExpiry(token) {
114
+ const payload = parseJWT(token);
115
+ if (!payload || !payload.exp) return null;
116
+ return new Date(payload.exp * 1000);
117
+ }
118
+
119
+ function isTokenExpired(token) {
120
+ const expiry = getTokenExpiry(token);
121
+ if (!expiry) return true;
122
+ return expiry < new Date();
123
+ }
124
+
125
+ module.exports = {
126
+ CONFIG_DIR,
127
+ TOKEN_FILE,
128
+ CONFIG_FILE,
129
+ ENDPOINTS,
130
+ MODELS,
131
+ ensureConfigDir,
132
+ getToken,
133
+ setToken,
134
+ getConfig,
135
+ setConfig,
136
+ getEnvironment,
137
+ getEndpoints,
138
+ parseJWT,
139
+ getTokenExpiry,
140
+ isTokenExpired
141
+ };
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+
3
+ const config = require('./config');
4
+
5
+ console.log(`
6
+ ╔══════════════════════════════════════════════════════════════╗
7
+ ║ jbai-cli installed! ║
8
+ ╚══════════════════════════════════════════════════════════════╝
9
+
10
+ Next steps:
11
+
12
+ 1. Get your token from: ${config.ENDPOINTS.staging.tokenUrl}
13
+ (Click Profile → 'Copy Developer Token')
14
+
15
+ 2. Set your token:
16
+ $ jbai token set
17
+
18
+ 3. Start using AI tools:
19
+ $ jbai-claude # Claude Code
20
+ $ jbai-codex # OpenAI Codex
21
+ $ jbai-aider # Aider
22
+ $ jbai-gemini # Gemini
23
+
24
+ Run 'jbai help' for more options.
25
+ `);
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "jbai-cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI wrappers to use AI coding tools (Claude Code, Codex, Aider, Gemini) with JetBrains AI Platform",
5
+ "keywords": [
6
+ "jetbrains",
7
+ "ai",
8
+ "claude",
9
+ "codex",
10
+ "aider",
11
+ "gemini",
12
+ "cli",
13
+ "openai",
14
+ "anthropic"
15
+ ],
16
+ "author": "Andrii Shchupliak",
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/ashchupliak/jbai-cli"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/ashchupliak/jbai-cli/issues"
24
+ },
25
+ "homepage": "https://github.com/ashchupliak/jbai-cli#readme",
26
+ "bin": {
27
+ "jbai": "./bin/jbai.js",
28
+ "jbai-claude": "./bin/jbai-claude.js",
29
+ "jbai-codex": "./bin/jbai-codex.js",
30
+ "jbai-aider": "./bin/jbai-aider.js",
31
+ "jbai-gemini": "./bin/jbai-gemini.js",
32
+ "jbai-opencode": "./bin/jbai-opencode.js"
33
+ },
34
+ "files": [
35
+ "bin/",
36
+ "lib/",
37
+ "README.md"
38
+ ],
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ },
42
+ "scripts": {
43
+ "postinstall": "node lib/postinstall.js",
44
+ "test": "node bin/jbai.js test"
45
+ }
46
+ }