jbai-cli 1.7.0 → 1.8.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.
@@ -2,76 +2,69 @@
2
2
 
3
3
  const { runWithHandoff, stripHandoffFlag } = require('../lib/interactive-handoff');
4
4
  const config = require('../lib/config');
5
+ const { ensureToken } = require('../lib/ensure-token');
5
6
 
6
- const token = config.getToken();
7
- if (!token) {
8
- console.error('❌ No token found. Run: jbai token set');
9
- process.exit(1);
10
- }
7
+ (async () => {
8
+ const token = await ensureToken();
9
+ const endpoints = config.getEndpoints();
10
+ let args = process.argv.slice(2);
11
+ const handoffConfig = stripHandoffFlag(args);
12
+ args = handoffConfig.args;
11
13
 
12
- if (config.isTokenExpired(token)) {
13
- console.error('⚠️ Token expired. Run: jbai token refresh');
14
- process.exit(1);
15
- }
14
+ // Check for super mode (--super, --yolo, -s)
15
+ const superFlags = ['--super', '--yolo', '-s'];
16
+ const superMode = args.some(a => superFlags.includes(a));
17
+ args = args.filter(a => !superFlags.includes(a));
16
18
 
17
- const endpoints = config.getEndpoints();
18
- let args = process.argv.slice(2);
19
- const handoffConfig = stripHandoffFlag(args);
20
- args = handoffConfig.args;
19
+ // Check if model specified
20
+ const hasModel = args.includes('--model') || args.includes('-m');
21
+ let finalArgs = hasModel ? args : ['--model', config.MODELS.claude.default, ...args];
21
22
 
22
- // Check for super mode (--super, --yolo, -s)
23
- const superFlags = ['--super', '--yolo', '-s'];
24
- const superMode = args.some(a => superFlags.includes(a));
25
- args = args.filter(a => !superFlags.includes(a));
23
+ // Add super mode flags
24
+ if (superMode) {
25
+ finalArgs = ['--dangerously-skip-permissions', ...finalArgs];
26
+ console.log('🚀 Super mode: --dangerously-skip-permissions enabled');
27
+ }
26
28
 
27
- // Check if model specified
28
- const hasModel = args.includes('--model') || args.includes('-m');
29
- let finalArgs = hasModel ? args : ['--model', config.MODELS.claude.default, ...args];
29
+ // Set environment for Claude Code
30
+ // Use ANTHROPIC_CUSTOM_HEADERS for the Grazie auth header
31
+ // Remove /v1 from endpoint - Claude Code adds it automatically
32
+ const baseUrl = endpoints.anthropic.replace(/\/v1$/, '');
33
+ const env = {
34
+ ...process.env,
35
+ ANTHROPIC_BASE_URL: baseUrl,
36
+ ANTHROPIC_API_KEY: 'placeholder',
37
+ ANTHROPIC_CUSTOM_HEADERS: `Grazie-Authenticate-JWT: ${token}`
38
+ };
30
39
 
31
- // Add super mode flags
32
- if (superMode) {
33
- finalArgs = ['--dangerously-skip-permissions', ...finalArgs];
34
- console.log('🚀 Super mode: --dangerously-skip-permissions enabled');
35
- }
40
+ // Remove any existing auth token that might conflict
41
+ delete env.ANTHROPIC_AUTH_TOKEN;
36
42
 
37
- // Set environment for Claude Code
38
- // Use ANTHROPIC_CUSTOM_HEADERS for the Grazie auth header
39
- // Remove /v1 from endpoint - Claude Code adds it automatically
40
- const baseUrl = endpoints.anthropic.replace(/\/v1$/, '');
41
- const env = {
42
- ...process.env,
43
- ANTHROPIC_BASE_URL: baseUrl,
44
- ANTHROPIC_API_KEY: 'placeholder',
45
- ANTHROPIC_CUSTOM_HEADERS: `Grazie-Authenticate-JWT: ${token}`
46
- };
47
-
48
- // Remove any existing auth token that might conflict
49
- delete env.ANTHROPIC_AUTH_TOKEN;
50
-
51
- const child = runWithHandoff({
52
- command: 'claude',
53
- args: finalArgs,
54
- env,
55
- toolName: 'jbai-claude',
56
- handoffDefaults: {
57
- enabled: !handoffConfig.disabled,
58
- grazieToken: token,
59
- grazieEnvironment: config.getEnvironment() === 'production' ? 'PRODUCTION' : 'STAGING',
60
- grazieModel: config.MODELS.claude.default,
61
- cwd: process.cwd(),
62
- },
63
- });
64
-
65
- if (child && typeof child.on === 'function') {
66
- child.on('error', (err) => {
67
- if (err.code === 'ENOENT') {
68
- const tool = config.TOOLS.claude;
69
- console.error(`❌ ${tool.name} not found.\n`);
70
- console.error(`Install with: ${tool.install}`);
71
- console.error(`Or run: jbai install claude`);
72
- } else {
73
- console.error(`Error: ${err.message}`);
74
- }
75
- process.exit(1);
43
+ const child = runWithHandoff({
44
+ command: 'claude',
45
+ args: finalArgs,
46
+ env,
47
+ toolName: 'jbai-claude',
48
+ handoffDefaults: {
49
+ enabled: !handoffConfig.disabled,
50
+ grazieToken: token,
51
+ grazieEnvironment: config.getEnvironment() === 'production' ? 'PRODUCTION' : 'STAGING',
52
+ grazieModel: config.MODELS.claude.default,
53
+ cwd: process.cwd(),
54
+ },
76
55
  });
77
- }
56
+
57
+ if (child && typeof child.on === 'function') {
58
+ child.on('error', (err) => {
59
+ if (err.code === 'ENOENT') {
60
+ const tool = config.TOOLS.claude;
61
+ console.error(`❌ ${tool.name} not found.\n`);
62
+ console.error(`Install with: ${tool.install}`);
63
+ console.error(`Or run: jbai install claude`);
64
+ } else {
65
+ console.error(`Error: ${err.message}`);
66
+ }
67
+ process.exit(1);
68
+ });
69
+ }
70
+ })();
package/bin/jbai-codex.js CHANGED
@@ -5,47 +5,39 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
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 environment = config.getEnvironment();
22
- const providerName = environment === 'staging' ? 'jbai-staging' : 'jbai';
23
- const envVarName = environment === 'staging' ? 'GRAZIE_STAGING_TOKEN' : 'GRAZIE_API_TOKEN';
24
- let args = process.argv.slice(2);
25
- const handoffConfig = stripHandoffFlag(args);
26
- args = handoffConfig.args;
27
-
28
- // Check for super mode (--super, --yolo, -s)
29
- const superFlags = ['--super', '--yolo', '-s'];
30
- const superMode = args.some(a => superFlags.includes(a));
31
- args = args.filter(a => !superFlags.includes(a));
32
-
33
- // Ensure Codex config exists
34
- const codexDir = path.join(os.homedir(), '.codex');
35
- const codexConfig = path.join(codexDir, 'config.toml');
36
-
37
- if (!fs.existsSync(codexDir)) {
38
- fs.mkdirSync(codexDir, { recursive: true });
39
- }
40
-
41
- // Check if our provider is configured
42
- let configContent = '';
43
- if (fs.existsSync(codexConfig)) {
44
- configContent = fs.readFileSync(codexConfig, 'utf-8');
45
- }
46
-
47
- if (!configContent.includes(`[model_providers.${providerName}]`)) {
48
- const providerConfig = `
8
+ const { ensureToken } = require('../lib/ensure-token');
9
+
10
+ (async () => {
11
+ const token = await ensureToken();
12
+ const endpoints = config.getEndpoints();
13
+ const environment = config.getEnvironment();
14
+ const providerName = environment === 'staging' ? 'jbai-staging' : 'jbai';
15
+ const envVarName = environment === 'staging' ? 'GRAZIE_STAGING_TOKEN' : 'GRAZIE_API_TOKEN';
16
+ let args = process.argv.slice(2);
17
+ const handoffConfig = stripHandoffFlag(args);
18
+ args = handoffConfig.args;
19
+
20
+ // Check for super mode (--super, --yolo, -s)
21
+ const superFlags = ['--super', '--yolo', '-s'];
22
+ const superMode = args.some(a => superFlags.includes(a));
23
+ args = args.filter(a => !superFlags.includes(a));
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 = `
49
41
  # JetBrains AI (${environment})
50
42
  [model_providers.${providerName}]
51
43
  name = "JetBrains AI (${environment})"
@@ -53,54 +45,68 @@ base_url = "${endpoints.openai}"
53
45
  env_http_headers = { "Grazie-Authenticate-JWT" = "${envVarName}" }
54
46
  wire_api = "responses"
55
47
  `;
56
- fs.appendFileSync(codexConfig, providerConfig);
57
- console.log(`✅ Added ${providerName} provider to Codex config`);
58
- }
59
-
60
- const hasModel = args.includes('--model');
61
- const finalArgs = ['-c', `model_provider=${providerName}`];
62
-
63
- if (!hasModel) {
64
- finalArgs.push('--model', config.MODELS.codex?.default || config.MODELS.openai.default);
65
- }
66
-
67
- // Add super mode flags (full-auto)
68
- if (superMode) {
69
- finalArgs.push('--full-auto');
70
- console.log('🚀 Super mode: --full-auto enabled');
71
- }
72
-
73
- finalArgs.push(...args);
74
-
75
- const childEnv = {
76
- ...process.env,
77
- [envVarName]: token
78
- };
79
-
80
- const child = runWithHandoff({
81
- command: 'codex',
82
- args: finalArgs,
83
- env: childEnv,
84
- toolName: 'jbai-codex',
85
- handoffDefaults: {
86
- enabled: !handoffConfig.disabled,
87
- grazieToken: token,
88
- grazieEnvironment: environment === 'production' ? 'PRODUCTION' : 'STAGING',
89
- grazieModel: config.MODELS.claude.default,
90
- cwd: process.cwd(),
91
- },
92
- });
93
-
94
- if (child && typeof child.on === 'function') {
95
- child.on('error', (err) => {
96
- if (err.code === 'ENOENT') {
97
- const tool = config.TOOLS.codex;
98
- console.error(`❌ ${tool.name} not found.\n`);
99
- console.error(`Install with: ${tool.install}`);
100
- console.error(`Or run: jbai install codex`);
101
- } else {
102
- console.error(`Error: ${err.message}`);
103
- }
104
- process.exit(1);
48
+ fs.appendFileSync(codexConfig, providerConfig);
49
+ console.log(`✅ Added ${providerName} provider to Codex config`);
50
+ }
51
+
52
+ // Point Codex model picker to our proxy (serves our model list instead of chatgpt.com)
53
+ const proxyModelUrl = 'http://localhost:18080/';
54
+ if (!configContent.includes('chatgpt_base_url')) {
55
+ fs.appendFileSync(codexConfig, `\nchatgpt_base_url = "${proxyModelUrl}"\n`);
56
+ console.log(' Set chatgpt_base_url to jbai-proxy for model picker');
57
+ } else if (!configContent.includes(proxyModelUrl)) {
58
+ // Update existing chatgpt_base_url
59
+ configContent = fs.readFileSync(codexConfig, 'utf-8');
60
+ const updated = configContent.replace(/^chatgpt_base_url\s*=\s*"[^"]*"/m, `chatgpt_base_url = "${proxyModelUrl}"`);
61
+ fs.writeFileSync(codexConfig, updated);
62
+ console.log(' Updated chatgpt_base_url to jbai-proxy');
63
+ }
64
+
65
+ const hasModel = args.includes('--model');
66
+ const finalArgs = ['-c', `model_provider=${providerName}`];
67
+
68
+ if (!hasModel) {
69
+ finalArgs.push('--model', config.MODELS.codex?.default || config.MODELS.openai.default);
70
+ }
71
+
72
+ // Add super mode flags (full-auto)
73
+ if (superMode) {
74
+ finalArgs.push('--full-auto');
75
+ console.log('🚀 Super mode: --full-auto enabled');
76
+ }
77
+
78
+ finalArgs.push(...args);
79
+
80
+ const childEnv = {
81
+ ...process.env,
82
+ [envVarName]: token
83
+ };
84
+
85
+ const child = runWithHandoff({
86
+ command: 'codex',
87
+ args: finalArgs,
88
+ env: childEnv,
89
+ toolName: 'jbai-codex',
90
+ handoffDefaults: {
91
+ enabled: !handoffConfig.disabled,
92
+ grazieToken: token,
93
+ grazieEnvironment: environment === 'production' ? 'PRODUCTION' : 'STAGING',
94
+ grazieModel: config.MODELS.claude.default,
95
+ cwd: process.cwd(),
96
+ },
105
97
  });
106
- }
98
+
99
+ if (child && typeof child.on === 'function') {
100
+ child.on('error', (err) => {
101
+ if (err.code === 'ENOENT') {
102
+ const tool = config.TOOLS.codex;
103
+ console.error(`❌ ${tool.name} not found.\n`);
104
+ console.error(`Install with: ${tool.install}`);
105
+ console.error(`Or run: jbai install codex`);
106
+ } else {
107
+ console.error(`Error: ${err.message}`);
108
+ }
109
+ process.exit(1);
110
+ });
111
+ }
112
+ })();
@@ -8,70 +8,63 @@
8
8
 
9
9
  const { runWithHandoff, stripHandoffFlag } = require('../lib/interactive-handoff');
10
10
  const config = require('../lib/config');
11
+ const { ensureToken } = require('../lib/ensure-token');
11
12
 
12
- const token = config.getToken();
13
- if (!token) {
14
- console.error('❌ No token found. Run: jbai token set');
15
- process.exit(1);
16
- }
13
+ (async () => {
14
+ const token = await ensureToken();
15
+ const endpoints = config.getEndpoints();
16
+ let args = process.argv.slice(2);
17
+ const handoffConfig = stripHandoffFlag(args);
18
+ args = handoffConfig.args;
17
19
 
18
- if (config.isTokenExpired(token)) {
19
- console.error('⚠️ Token expired. Run: jbai token refresh');
20
- process.exit(1);
21
- }
20
+ // Check for super mode (--super, --yolo, -s)
21
+ const superFlags = ['--super', '--yolo', '-s'];
22
+ const superMode = args.some(a => superFlags.includes(a));
23
+ args = args.filter(a => !superFlags.includes(a));
22
24
 
23
- const endpoints = config.getEndpoints();
24
- let args = process.argv.slice(2);
25
- const handoffConfig = stripHandoffFlag(args);
26
- args = handoffConfig.args;
25
+ // Check if model specified
26
+ const hasModel = args.includes('--model') || args.includes('-m');
27
+ let finalArgs = hasModel ? args : ['--model', config.MODELS.gemini.default, ...args];
27
28
 
28
- // Check for super mode (--super, --yolo, -s)
29
- const superFlags = ['--super', '--yolo', '-s'];
30
- const superMode = args.some(a => superFlags.includes(a));
31
- args = args.filter(a => !superFlags.includes(a));
29
+ // Add super mode flags (auto-confirm) - Gemini uses --yolo
30
+ if (superMode) {
31
+ finalArgs = ['--yolo', ...finalArgs];
32
+ console.log('🚀 Super mode: --yolo (auto-confirm) enabled');
33
+ }
32
34
 
33
- // Check if model specified
34
- const hasModel = args.includes('--model') || args.includes('-m');
35
- let finalArgs = hasModel ? args : ['--model', config.MODELS.gemini.default, ...args];
35
+ // Set environment for Gemini CLI
36
+ // Uses GEMINI_CLI_CUSTOM_HEADERS for auth (supported since Nov 2025)
37
+ const env = {
38
+ ...process.env,
39
+ GEMINI_BASE_URL: endpoints.google,
40
+ GEMINI_API_KEY: 'placeholder',
41
+ GEMINI_CLI_CUSTOM_HEADERS: `Grazie-Authenticate-JWT: ${token}`
42
+ };
36
43
 
37
- // Add super mode flags (auto-confirm) - Gemini uses --yolo
38
- if (superMode) {
39
- finalArgs = ['--yolo', ...finalArgs];
40
- console.log('🚀 Super mode: --yolo (auto-confirm) enabled');
41
- }
42
-
43
- // Set environment for Gemini CLI
44
- // Uses GEMINI_CLI_CUSTOM_HEADERS for auth (supported since Nov 2025)
45
- const env = {
46
- ...process.env,
47
- GEMINI_BASE_URL: endpoints.google,
48
- GEMINI_API_KEY: 'placeholder',
49
- GEMINI_CLI_CUSTOM_HEADERS: `Grazie-Authenticate-JWT: ${token}`
50
- };
51
-
52
- const child = runWithHandoff({
53
- command: 'gemini',
54
- args: finalArgs,
55
- env,
56
- toolName: 'jbai-gemini',
57
- handoffDefaults: {
58
- enabled: !handoffConfig.disabled,
59
- grazieToken: token,
60
- grazieEnvironment: config.getEnvironment() === 'production' ? 'PRODUCTION' : 'STAGING',
61
- grazieModel: config.MODELS.claude.default,
62
- cwd: process.cwd(),
63
- },
64
- });
65
-
66
- if (child && typeof child.on === 'function') {
67
- child.on('error', (err) => {
68
- if (err.code === 'ENOENT') {
69
- console.error(`❌ Gemini CLI not found.\n`);
70
- console.error(`Install with: npm install -g @google/gemini-cli`);
71
- console.error(`Or run: jbai install gemini`);
72
- } else {
73
- console.error(`Error: ${err.message}`);
74
- }
75
- process.exit(1);
44
+ const child = runWithHandoff({
45
+ command: 'gemini',
46
+ args: finalArgs,
47
+ env,
48
+ toolName: 'jbai-gemini',
49
+ handoffDefaults: {
50
+ enabled: !handoffConfig.disabled,
51
+ grazieToken: token,
52
+ grazieEnvironment: config.getEnvironment() === 'production' ? 'PRODUCTION' : 'STAGING',
53
+ grazieModel: config.MODELS.claude.default,
54
+ cwd: process.cwd(),
55
+ },
76
56
  });
77
- }
57
+
58
+ if (child && typeof child.on === 'function') {
59
+ child.on('error', (err) => {
60
+ if (err.code === 'ENOENT') {
61
+ console.error(`❌ Gemini CLI not found.\n`);
62
+ console.error(`Install with: npm install -g @google/gemini-cli`);
63
+ console.error(`Or run: jbai install gemini`);
64
+ } else {
65
+ console.error(`Error: ${err.message}`);
66
+ }
67
+ process.exit(1);
68
+ });
69
+ }
70
+ })();
@@ -5,164 +5,155 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
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 environment = config.getEnvironment();
22
- let args = process.argv.slice(2);
23
- const handoffConfig = stripHandoffFlag(args);
24
- args = handoffConfig.args;
25
-
26
- // Check for super mode (--super, --yolo, -s)
27
- const superFlags = ['--super', '--yolo', '-s'];
28
- const superMode = args.some(a => superFlags.includes(a));
29
- args = args.filter(a => !superFlags.includes(a));
30
-
31
- // Setup OpenCode config with JetBrains provider
32
- const configDir = path.join(os.homedir(), '.config', 'opencode');
33
- const configFile = path.join(configDir, 'opencode.json');
34
-
35
- if (!fs.existsSync(configDir)) {
36
- fs.mkdirSync(configDir, { recursive: true });
37
- }
38
-
39
- // Create or update OpenCode config with JetBrains provider
40
- const providerName = environment === 'staging' ? 'jbai-staging' : 'jbai';
41
- let opencodeConfig = {};
42
-
43
- if (fs.existsSync(configFile)) {
44
- try {
45
- opencodeConfig = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
46
- } catch {
47
- opencodeConfig = {};
8
+ const { ensureToken } = require('../lib/ensure-token');
9
+
10
+ (async () => {
11
+ const token = await ensureToken();
12
+ const endpoints = config.getEndpoints();
13
+ const environment = config.getEnvironment();
14
+ let args = process.argv.slice(2);
15
+ const handoffConfig = stripHandoffFlag(args);
16
+ args = handoffConfig.args;
17
+
18
+ // Check for super mode (--super, --yolo, -s)
19
+ const superFlags = ['--super', '--yolo', '-s'];
20
+ const superMode = args.some(a => superFlags.includes(a));
21
+ args = args.filter(a => !superFlags.includes(a));
22
+
23
+ // Setup OpenCode config with JetBrains provider
24
+ const configDir = path.join(os.homedir(), '.config', 'opencode');
25
+ const configFile = path.join(configDir, 'opencode.json');
26
+
27
+ if (!fs.existsSync(configDir)) {
28
+ fs.mkdirSync(configDir, { recursive: true });
48
29
  }
49
- }
50
-
51
- // Ensure provider section exists
52
- if (!opencodeConfig.provider) {
53
- opencodeConfig.provider = {};
54
- }
55
-
56
- // Environment variable name for the token
57
- const envVarName = environment === 'staging' ? 'GRAZIE_STAGING_TOKEN' : 'GRAZIE_API_TOKEN';
58
-
59
- // Provider names for OpenAI and Anthropic
60
- const anthropicProviderName = environment === 'staging' ? 'jbai-anthropic-staging' : 'jbai-anthropic';
61
-
62
- // Add/update JetBrains OpenAI provider with custom header (using env var reference)
63
- // Use OpenAI SDK to support max_completion_tokens for GPT-5.x
64
- opencodeConfig.provider[providerName] = {
65
- npm: '@ai-sdk/openai',
66
- name: `JetBrains AI OpenAI (${environment})`,
67
- options: {
68
- baseURL: endpoints.openai,
69
- apiKey: `{env:${envVarName}}`,
70
- headers: {
71
- 'Grazie-Authenticate-JWT': `{env:${envVarName}}`
30
+
31
+ // Create or update OpenCode config with JetBrains provider
32
+ const providerName = environment === 'staging' ? 'jbai-staging' : 'jbai';
33
+ let opencodeConfig = {};
34
+
35
+ if (fs.existsSync(configFile)) {
36
+ try {
37
+ opencodeConfig = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
38
+ } catch {
39
+ opencodeConfig = {};
72
40
  }
73
- },
74
- models: {}
75
- };
76
-
77
- // Add OpenAI models
78
- // OpenCode requires an output limit in the config.
79
- // For O-series models we keep a larger context window but still set output.
80
- config.MODELS.openai.available.forEach(model => {
81
- const isOSeries = /^o[1-9]/.test(model);
82
- opencodeConfig.provider[providerName].models[model] = {
83
- name: model,
84
- limit: isOSeries
85
- ? { context: 200000, output: 8192 }
86
- : { context: 128000, output: 8192 }
41
+ }
42
+
43
+ // Ensure provider section exists
44
+ if (!opencodeConfig.provider) {
45
+ opencodeConfig.provider = {};
46
+ }
47
+
48
+ // Environment variable name for the token
49
+ const envVarName = environment === 'staging' ? 'GRAZIE_STAGING_TOKEN' : 'GRAZIE_API_TOKEN';
50
+
51
+ // Provider names for OpenAI and Anthropic
52
+ const anthropicProviderName = environment === 'staging' ? 'jbai-anthropic-staging' : 'jbai-anthropic';
53
+
54
+ // Add/update JetBrains OpenAI provider with custom header (using env var reference)
55
+ // Use OpenAI SDK to support max_completion_tokens for GPT-5.x
56
+ opencodeConfig.provider[providerName] = {
57
+ npm: '@ai-sdk/openai',
58
+ name: `JetBrains AI OpenAI (${environment})`,
59
+ options: {
60
+ baseURL: endpoints.openai,
61
+ apiKey: `{env:${envVarName}}`,
62
+ headers: {
63
+ 'Grazie-Authenticate-JWT': `{env:${envVarName}}`
64
+ }
65
+ },
66
+ models: {}
87
67
  };
88
- });
89
-
90
- // Add JetBrains Anthropic provider for Claude models
91
- opencodeConfig.provider[anthropicProviderName] = {
92
- npm: '@ai-sdk/anthropic',
93
- name: `JetBrains AI Anthropic (${environment})`,
94
- options: {
95
- baseURL: endpoints.anthropic,
96
- apiKey: `{env:${envVarName}}`,
97
- headers: {
98
- 'Grazie-Authenticate-JWT': `{env:${envVarName}}`
99
- }
100
- },
101
- models: {}
102
- };
103
-
104
- // Add Claude models to Anthropic provider
105
- config.MODELS.claude.available.forEach(model => {
106
- opencodeConfig.provider[anthropicProviderName].models[model] = {
107
- name: model,
108
- limit: { context: 200000, output: 8192 }
68
+
69
+ // Add OpenAI models
70
+ // OpenCode requires an output limit in the config.
71
+ // For O-series models we keep a larger context window but still set output.
72
+ config.MODELS.openai.available.forEach(model => {
73
+ const isOSeries = /^o[1-9]/.test(model);
74
+ opencodeConfig.provider[providerName].models[model] = {
75
+ name: model,
76
+ limit: isOSeries
77
+ ? { context: 200000, output: 8192 }
78
+ : { context: 128000, output: 8192 }
79
+ };
80
+ });
81
+
82
+ // Add JetBrains Anthropic provider for Claude models
83
+ opencodeConfig.provider[anthropicProviderName] = {
84
+ npm: '@ai-sdk/anthropic',
85
+ name: `JetBrains AI Anthropic (${environment})`,
86
+ options: {
87
+ baseURL: endpoints.anthropic,
88
+ apiKey: `{env:${envVarName}}`,
89
+ headers: {
90
+ 'Grazie-Authenticate-JWT': `{env:${envVarName}}`
91
+ }
92
+ },
93
+ models: {}
109
94
  };
110
- });
111
-
112
- // NOTE: Gemini models are NOT available via Grazie OpenAI-compatible proxy.
113
- // The /user/v5/llm/google/v1/vertex endpoint only works with native Google API
114
- // format (used by jbai-gemini), not OpenAI chat/completions format.
115
- // Use `jbai gemini` instead for Gemini models.
116
-
117
- // Write config
118
- fs.writeFileSync(configFile, JSON.stringify(opencodeConfig, null, 2));
119
-
120
- // Check if model specified
121
- const hasModel = args.includes('--model') || args.includes('-m');
122
- let finalArgs = [];
123
-
124
- if (!hasModel) {
125
- // Use provider/model format for OpenCode
126
- finalArgs.push('--model', `${providerName}/${config.MODELS.openai.default}`);
127
- }
128
-
129
- // Note: OpenCode run is already non-interactive, no super mode needed
130
- if (superMode) {
131
- console.log('ℹ️ OpenCode run is already non-interactive (no super mode flag needed)');
132
- }
133
-
134
- finalArgs.push(...args);
135
-
136
- // Set the token in environment variable for OpenCode config to reference
137
- const childEnv = {
138
- ...process.env,
139
- [envVarName]: token
140
- };
141
-
142
- const child = runWithHandoff({
143
- command: 'opencode',
144
- args: finalArgs,
145
- env: childEnv,
146
- toolName: 'jbai-opencode',
147
- handoffDefaults: {
148
- enabled: !handoffConfig.disabled,
149
- grazieToken: token,
150
- grazieEnvironment: environment === 'production' ? 'PRODUCTION' : 'STAGING',
151
- grazieModel: config.MODELS.claude.default,
152
- cwd: process.cwd(),
153
- },
154
- });
155
-
156
- if (child && typeof child.on === 'function') {
157
- child.on('error', (err) => {
158
- if (err.code === 'ENOENT') {
159
- const tool = config.TOOLS.opencode;
160
- console.error(`❌ ${tool.name} not found.\n`);
161
- console.error(`Install with: ${tool.install}`);
162
- console.error(`Or run: jbai install opencode`);
163
- } else {
164
- console.error(`Error: ${err.message}`);
165
- }
166
- process.exit(1);
95
+
96
+ // Add Claude models to Anthropic provider
97
+ config.MODELS.claude.available.forEach(model => {
98
+ opencodeConfig.provider[anthropicProviderName].models[model] = {
99
+ name: model,
100
+ limit: { context: 200000, output: 8192 }
101
+ };
167
102
  });
168
- }
103
+
104
+ // NOTE: Gemini models are NOT available via Grazie OpenAI-compatible proxy.
105
+ // The /user/v5/llm/google/v1/vertex endpoint only works with native Google API
106
+ // format (used by jbai-gemini), not OpenAI chat/completions format.
107
+ // Use `jbai gemini` instead for Gemini models.
108
+
109
+ // Enable max permissions (yolo mode) by default
110
+ opencodeConfig.yolo = true;
111
+
112
+ // Write config
113
+ fs.writeFileSync(configFile, JSON.stringify(opencodeConfig, null, 2));
114
+
115
+ // Check if model specified
116
+ const hasModel = args.includes('--model') || args.includes('-m');
117
+ let finalArgs = ['--yolo'];
118
+
119
+ if (!hasModel) {
120
+ // Use provider/model format for OpenCode
121
+ finalArgs.push('--model', `${providerName}/${config.MODELS.openai.default}`);
122
+ }
123
+
124
+ finalArgs.push(...args);
125
+
126
+ // Set the token in environment variable for OpenCode config to reference
127
+ const childEnv = {
128
+ ...process.env,
129
+ [envVarName]: token
130
+ };
131
+
132
+ const child = runWithHandoff({
133
+ command: 'opencode',
134
+ args: finalArgs,
135
+ env: childEnv,
136
+ toolName: 'jbai-opencode',
137
+ handoffDefaults: {
138
+ enabled: !handoffConfig.disabled,
139
+ grazieToken: token,
140
+ grazieEnvironment: environment === 'production' ? 'PRODUCTION' : 'STAGING',
141
+ grazieModel: config.MODELS.claude.default,
142
+ cwd: process.cwd(),
143
+ },
144
+ });
145
+
146
+ if (child && typeof child.on === 'function') {
147
+ child.on('error', (err) => {
148
+ if (err.code === 'ENOENT') {
149
+ const tool = config.TOOLS.opencode;
150
+ console.error(`❌ ${tool.name} not found.\n`);
151
+ console.error(`Install with: ${tool.install}`);
152
+ console.error(`Or run: jbai install opencode`);
153
+ } else {
154
+ console.error(`Error: ${err.message}`);
155
+ }
156
+ process.exit(1);
157
+ });
158
+ }
159
+ })();
package/bin/jbai-proxy.js CHANGED
@@ -107,9 +107,17 @@ function resolveRoute(method, urlPath) {
107
107
  function buildModelsResponse() {
108
108
  const models = [];
109
109
  const now = Math.floor(Date.now() / 1000);
110
+ const seen = new Set();
110
111
 
111
112
  for (const m of config.MODELS.openai.available) {
112
113
  models.push({ id: m, object: 'model', created: now, owned_by: 'openai' });
114
+ seen.add(m);
115
+ }
116
+ // Include codex-only models (responses API only) that aren't already in the openai list
117
+ for (const m of config.MODELS.codex.available) {
118
+ if (!seen.has(m)) {
119
+ models.push({ id: m, object: 'model', created: now, owned_by: 'openai' });
120
+ }
113
121
  }
114
122
  for (const m of config.MODELS.claude.available) {
115
123
  models.push({ id: m, object: 'model', created: now, owned_by: 'anthropic' });
@@ -121,6 +129,34 @@ function buildModelsResponse() {
121
129
  return { object: 'list', data: models };
122
130
  }
123
131
 
132
+ // Codex CLI model picker response (matches chatgpt.com/backend-api/codex/models format)
133
+ function buildCodexModelsResponse() {
134
+ const descriptions = {
135
+ 'gpt-5.3-codex-api-preview': 'Latest GPT-5.3 Codex model. Designed for long-running, detailed software engineering tasks.',
136
+ 'gpt-5.2-codex': 'Latest frontier agentic coding model.',
137
+ 'gpt-5.2-pro-2025-12-11': 'GPT-5.2 Pro for deep reasoning and complex tasks.',
138
+ 'gpt-5.2-2025-12-11': 'Latest frontier model with improvements across knowledge, reasoning and coding.',
139
+ 'gpt-5.2': 'Latest frontier model (alias).',
140
+ 'gpt-5.1-codex-max': 'Codex-optimized flagship for deep and fast reasoning.',
141
+ 'gpt-5.1-codex': 'Codex-optimized for software engineering tasks.',
142
+ 'gpt-5.1-codex-mini': 'Optimized for codex. Cheaper, faster, but less capable.',
143
+ 'gpt-5.1-2025-11-13': 'GPT-5.1 general-purpose model.',
144
+ 'gpt-5-codex': 'GPT-5 Codex for agentic coding.',
145
+ 'gpt-5-2025-08-07': 'GPT-5 general-purpose model.',
146
+ 'o4-mini-2025-04-16': 'O4-mini reasoning model.',
147
+ 'o3-2025-04-16': 'O3 reasoning model.',
148
+ };
149
+
150
+ const models = config.MODELS.codex.available.map((id, i) => ({
151
+ slug: id,
152
+ name: id,
153
+ description: descriptions[id] || id,
154
+ default_active: i === 0,
155
+ }));
156
+
157
+ return { models };
158
+ }
159
+
124
160
  // ---------------------------------------------------------------------------
125
161
  // Proxy handler
126
162
  // ---------------------------------------------------------------------------
@@ -144,6 +180,17 @@ function proxy(req, res) {
144
180
  const route = resolveRoute(req.method, urlPath);
145
181
 
146
182
  if (!route) {
183
+ // Codex CLI model picker endpoint (overrides chatgpt.com/backend-api/codex/models)
184
+ if (urlPath === '/backend-api/codex/models') {
185
+ res.writeHead(200, {
186
+ 'Content-Type': 'application/json',
187
+ 'Access-Control-Allow-Origin': '*',
188
+ });
189
+ res.end(JSON.stringify(buildCodexModelsResponse()));
190
+ log(`[codex] GET /backend-api/codex/models → 200 (${Date.now() - startTime}ms)`);
191
+ return;
192
+ }
193
+
147
194
  // Health / info endpoint
148
195
  if (urlPath === '/' || urlPath === '/health') {
149
196
  const token = getToken();
package/bin/jbai.js CHANGED
@@ -41,7 +41,7 @@ jbai-cli v${VERSION} - JetBrains AI Platform CLI Tools
41
41
  COMMANDS:
42
42
  jbai token Show token status
43
43
  jbai token set Set token interactively
44
- jbai token refresh Refresh expired token
44
+ jbai token refresh Auto-refresh token via API (no UI needed)
45
45
  jbai test Test API endpoints (incl. Codex /responses)
46
46
  jbai handoff Continue task in Orca Lab
47
47
  jbai env [staging|production] Switch environment
@@ -98,13 +98,21 @@ async function showTokenStatus() {
98
98
  const expiry = config.getTokenExpiry(token);
99
99
  if (expiry) {
100
100
  const now = new Date();
101
- const daysLeft = Math.floor((expiry - now) / (1000 * 60 * 60 * 24));
101
+ const hoursLeft = Math.round((expiry - now) / (1000 * 60 * 60));
102
102
 
103
103
  if (config.isTokenExpired(token)) {
104
+ const refreshable = config.isTokenRefreshable(token);
104
105
  console.log(`⚠️ Token EXPIRED: ${expiry.toLocaleString()}`);
105
- console.log(` Run: jbai token refresh`);
106
+ if (refreshable) {
107
+ console.log(` Run: jbai token refresh (auto-refresh via API)`);
108
+ } else {
109
+ console.log(` Token expired >2 weeks ago. Get a new one: ${config.getEndpoints().tokenUrl}`);
110
+ }
111
+ } else if (config.isTokenExpiringSoon(token)) {
112
+ console.log(`⏳ Expiring soon: ${expiry.toLocaleString()} (${hoursLeft}h left)`);
113
+ console.log(` Run: jbai token refresh (auto-refresh via API)`);
106
114
  } else {
107
- console.log(`✅ Expires: ${expiry.toLocaleString()} (${daysLeft} days left)`);
115
+ console.log(`✅ Expires: ${expiry.toLocaleString()} (${hoursLeft}h left)`);
108
116
  }
109
117
  }
110
118
  }
@@ -137,13 +145,44 @@ async function setToken() {
137
145
  });
138
146
  }
139
147
 
140
- async function testEndpoints() {
148
+ async function refreshTokenCommand() {
141
149
  const token = config.getToken();
142
150
  if (!token) {
143
151
  console.log('❌ No token found. Run: jbai token set');
144
152
  return;
145
153
  }
146
154
 
155
+ if (!config.isTokenExpired(token) && !config.isTokenExpiringSoon(token)) {
156
+ const expiry = config.getTokenExpiry(token);
157
+ const hoursLeft = Math.round((expiry - Date.now()) / (1000 * 60 * 60));
158
+ console.log(`✅ Token is still valid (${hoursLeft}h left). Refreshing anyway...`);
159
+ }
160
+
161
+ if (config.isTokenExpired(token) && !config.isTokenRefreshable(token)) {
162
+ console.log('❌ Token expired more than 2 weeks ago. Cannot auto-refresh.');
163
+ console.log(` Get a new token: ${config.getEndpoints().tokenUrl}`);
164
+ return;
165
+ }
166
+
167
+ try {
168
+ console.log('🔄 Refreshing token via API...');
169
+ const newToken = await config.refreshTokenApi(token);
170
+ config.setToken(newToken);
171
+ console.log('✅ Token refreshed successfully!');
172
+ showTokenStatus();
173
+ } catch (e) {
174
+ console.log(`❌ Refresh failed: ${e.message}`);
175
+ console.log(` Get a new token manually: ${config.getEndpoints().tokenUrl}`);
176
+ }
177
+ }
178
+
179
+ async function testEndpoints() {
180
+ const token = await config.getValidToken();
181
+ if (!token) {
182
+ console.log('❌ No token found. Run: jbai token set');
183
+ return;
184
+ }
185
+
147
186
  const endpoints = config.getEndpoints();
148
187
  console.log(`Testing JetBrains AI Platform (${config.getEnvironment()})\n`);
149
188
  const defaultOpenAIModel = config.MODELS.openai.default;
@@ -567,8 +606,10 @@ const [,, command, ...args] = process.argv;
567
606
 
568
607
  switch (command) {
569
608
  case 'token':
570
- if (args[0] === 'set' || args[0] === 'refresh') {
609
+ if (args[0] === 'set') {
571
610
  setToken();
611
+ } else if (args[0] === 'refresh') {
612
+ refreshTokenCommand();
572
613
  } else {
573
614
  showTokenStatus();
574
615
  }
@@ -154,6 +154,33 @@ async function testGeminiModel(model) {
154
154
  }
155
155
  }
156
156
 
157
+ async function testCodexModel(model) {
158
+ try {
159
+ // Codex models use the OpenAI Responses API (not chat/completions)
160
+ const result = await httpPost(
161
+ `${endpoints.openai}/responses`,
162
+ {
163
+ model: model,
164
+ input: 'Say OK',
165
+ max_output_tokens: 50
166
+ },
167
+ { 'Grazie-Authenticate-JWT': token }
168
+ );
169
+
170
+ if (result.status === 200) {
171
+ return { success: true, message: 'OK' };
172
+ } else if (result.status === 404) {
173
+ return { success: false, message: `404 Not Found`, error: result.data.error?.message || 'Model not found' };
174
+ } else if (result.status === 400) {
175
+ return { success: false, message: `400 Bad Request`, error: result.data.error?.message || 'Invalid request' };
176
+ } else {
177
+ return { success: false, message: `Status ${result.status}`, error: result.data.error?.message || JSON.stringify(result.data).substring(0, 100) };
178
+ }
179
+ } catch (e) {
180
+ return { success: false, message: 'Error', error: e.message };
181
+ }
182
+ }
183
+
157
184
  async function runTests() {
158
185
  console.log(`\n${'='.repeat(70)}`);
159
186
  console.log(`${colors.cyan}JBAI-CLI E2E MODEL COMPATIBILITY TESTING${colors.reset}`);
@@ -162,6 +189,7 @@ async function runTests() {
162
189
 
163
190
  const results = {
164
191
  openai: { working: [], failing: [] },
192
+ codex: { working: [], failing: [] },
165
193
  anthropic: { working: [], failing: [] },
166
194
  gemini: { working: [], failing: [] }
167
195
  };
@@ -198,6 +226,26 @@ async function runTests() {
198
226
  }
199
227
  }
200
228
 
229
+ // Test Codex-only models (responses API only, not in openai.available)
230
+ const openaiSet = new Set(config.MODELS.openai.available);
231
+ const codexOnlyModels = config.MODELS.codex.available.filter(m => !openaiSet.has(m));
232
+ if (codexOnlyModels.length > 0) {
233
+ console.log(`\n${colors.cyan}Testing Codex-only models (jbai-codex, responses API)...${colors.reset}`);
234
+ console.log('-'.repeat(50));
235
+ for (const model of codexOnlyModels) {
236
+ process.stdout.write(` ${model.padEnd(35)} `);
237
+ const result = await testCodexModel(model);
238
+ if (result.success) {
239
+ console.log(`${colors.green}✓ ${result.message}${colors.reset}`);
240
+ results.codex.working.push(model);
241
+ } else {
242
+ console.log(`${colors.red}✗ ${result.message}${colors.reset}`);
243
+ console.log(` ${colors.dim}${result.error}${colors.reset}`);
244
+ results.codex.failing.push({ model, error: result.error });
245
+ }
246
+ }
247
+ }
248
+
201
249
  // Test Gemini models
202
250
  console.log(`\n${colors.cyan}Testing Gemini models (jbai-gemini)...${colors.reset}`);
203
251
  console.log('-'.repeat(50));
@@ -220,16 +268,17 @@ async function runTests() {
220
268
  console.log(`${'='.repeat(70)}`);
221
269
 
222
270
  const totalWorking = results.anthropic.working.length + results.openai.working.length +
223
- results.gemini.working.length;
271
+ results.codex.working.length + results.gemini.working.length;
224
272
  const totalFailing = results.anthropic.failing.length + results.openai.failing.length +
225
- results.gemini.failing.length;
273
+ results.codex.failing.length + results.gemini.failing.length;
226
274
 
227
275
  console.log(`\n${colors.green}Working Models: ${totalWorking}${colors.reset}`);
228
276
  console.log(`${colors.red}Failing Models: ${totalFailing}${colors.reset}`);
229
277
 
230
278
  console.log(`\n${colors.cyan}By Provider:${colors.reset}`);
231
279
  console.log(` Claude (Anthropic): ${colors.green}${results.anthropic.working.length}${colors.reset} working, ${colors.red}${results.anthropic.failing.length}${colors.reset} failing`);
232
- console.log(` OpenAI/GPT: ${colors.green}${results.openai.working.length}${colors.reset} working, ${colors.red}${results.openai.failing.length}${colors.reset} failing`);
280
+ console.log(` OpenAI/GPT (Chat): ${colors.green}${results.openai.working.length}${colors.reset} working, ${colors.red}${results.openai.failing.length}${colors.reset} failing`);
281
+ console.log(` Codex (Responses): ${colors.green}${results.codex.working.length}${colors.reset} working, ${colors.red}${results.codex.failing.length}${colors.reset} failing`);
233
282
  console.log(` Gemini (Google): ${colors.green}${results.gemini.working.length}${colors.reset} working, ${colors.red}${results.gemini.failing.length}${colors.reset} failing`);
234
283
 
235
284
  if (totalFailing > 0) {
@@ -243,6 +292,10 @@ async function runTests() {
243
292
  console.log(`\n OpenAI models:`);
244
293
  results.openai.failing.forEach(f => console.log(` - ${f.model}`));
245
294
  }
295
+ if (results.codex.failing.length > 0) {
296
+ console.log(`\n Codex models:`);
297
+ results.codex.failing.forEach(f => console.log(` - ${f.model}`));
298
+ }
246
299
  if (results.gemini.failing.length > 0) {
247
300
  console.log(`\n Gemini models:`);
248
301
  results.gemini.failing.forEach(f => console.log(` - ${f.model}`));
package/lib/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
+ const https = require('https');
4
5
 
5
6
  const CONFIG_DIR = path.join(os.homedir(), '.jbai');
6
7
  const TOKEN_FILE = path.join(CONFIG_DIR, 'token');
@@ -32,7 +33,9 @@ const MODELS = {
32
33
  claude: {
33
34
  default: 'claude-sonnet-4-5-20250929',
34
35
  available: [
35
- // Claude 4.5 series (latest)
36
+ // Claude 4.6 series (latest)
37
+ 'claude-opus-4-6',
38
+ // Claude 4.5 series
36
39
  'claude-opus-4-5-20251101',
37
40
  'claude-sonnet-4-5-20250929',
38
41
  'claude-haiku-4-5-20251001',
@@ -75,12 +78,26 @@ const MODELS = {
75
78
  ]
76
79
  },
77
80
  // Codex CLI uses OpenAI models via the "responses" API (wire_api = "responses")
78
- // Uses the same models as openai, just different API wire format
81
+ // Includes chat-capable models PLUS codex-only models (responses API only)
79
82
  codex: {
80
- default: 'gpt-5.2-2025-12-11',
83
+ default: 'gpt-5.3-codex-api-preview',
81
84
  available: [
85
+ // Codex-specific models (responses API only, NOT available via chat/completions)
86
+ 'gpt-5.3-codex-api-preview',
87
+ // GPT-5.x chat models (also work via responses API)
82
88
  'gpt-5.2-2025-12-11',
83
- 'gpt-5.2'
89
+ 'gpt-5.2',
90
+ 'gpt-5.1-2025-11-13',
91
+ 'gpt-5-2025-08-07',
92
+ 'gpt-5.2-codex',
93
+ 'gpt-5.2-pro-2025-12-11',
94
+ 'gpt-5.1-codex-max',
95
+ 'gpt-5.1-codex',
96
+ 'gpt-5.1-codex-mini',
97
+ 'gpt-5-codex',
98
+ // O-series (also work via responses API)
99
+ 'o4-mini-2025-04-16',
100
+ 'o3-2025-04-16'
84
101
  ]
85
102
  },
86
103
  gemini: {
@@ -98,7 +115,7 @@ const MODELS = {
98
115
  'gemini-2.0-flash-lite-001'
99
116
  ]
100
117
  }
101
- // NOTE: Other providers (DeepSeek, Mistral, Qwen, XAI, Meta) are available
118
+ // NOTE: Other providers (DeepSeek, Mistral, Qwen, XAI, Meta, Grok) are available
102
119
  // via Grazie native Chat API but NOT via the OpenAI-compatible proxy.
103
120
  // They are not supported by CLI tools that use OpenAI API format.
104
121
  };
@@ -176,6 +193,88 @@ function isTokenExpired(token) {
176
193
  return expiry < new Date();
177
194
  }
178
195
 
196
+ function isTokenExpiringSoon(token, thresholdMs = 60 * 60 * 1000) {
197
+ const expiry = getTokenExpiry(token);
198
+ if (!expiry) return true;
199
+ return (expiry.getTime() - Date.now()) < thresholdMs;
200
+ }
201
+
202
+ // Max age for refresh: token expired less than 2 weeks ago
203
+ function isTokenRefreshable(token) {
204
+ const expiry = getTokenExpiry(token);
205
+ if (!expiry) return false;
206
+ const twoWeeksMs = 14 * 24 * 60 * 60 * 1000;
207
+ return (Date.now() - expiry.getTime()) < twoWeeksMs;
208
+ }
209
+
210
+ function refreshTokenApi(token) {
211
+ return new Promise((resolve, reject) => {
212
+ const endpoints = ENDPOINTS[getEnvironment()];
213
+ const url = new URL(`${endpoints.base}/user/v5/auth/jwt/refresh/v3`);
214
+
215
+ const req = https.request({
216
+ hostname: url.hostname,
217
+ port: 443,
218
+ path: url.pathname,
219
+ method: 'POST',
220
+ headers: {
221
+ 'Content-Type': 'application/json',
222
+ 'Grazie-Authenticate-JWT': token
223
+ }
224
+ }, (res) => {
225
+ let body = '';
226
+ res.on('data', chunk => body += chunk);
227
+ res.on('end', () => {
228
+ if (res.statusCode !== 200) {
229
+ reject(new Error(`Refresh failed (HTTP ${res.statusCode}): ${body}`));
230
+ return;
231
+ }
232
+ try {
233
+ const json = JSON.parse(body);
234
+ if (!json.token) {
235
+ reject(new Error('No token in refresh response'));
236
+ return;
237
+ }
238
+ resolve(json.token);
239
+ } catch (e) {
240
+ reject(new Error(`Failed to parse refresh response: ${body}`));
241
+ }
242
+ });
243
+ });
244
+
245
+ req.on('error', reject);
246
+ req.write('');
247
+ req.end();
248
+ });
249
+ }
250
+
251
+ async function refreshToken() {
252
+ const token = getToken();
253
+ if (!token) {
254
+ throw new Error('No token found. Run: jbai token set');
255
+ }
256
+ if (!isTokenExpired(token) && !isTokenExpiringSoon(token)) {
257
+ return token; // still valid, no refresh needed
258
+ }
259
+ if (isTokenExpired(token) && !isTokenRefreshable(token)) {
260
+ throw new Error('Token expired more than 2 weeks ago. Get a new one from the UI.');
261
+ }
262
+ const newToken = await refreshTokenApi(token);
263
+ setToken(newToken);
264
+ return newToken;
265
+ }
266
+
267
+ async function getValidToken() {
268
+ const token = getToken();
269
+ if (!token) return null;
270
+ if (!isTokenExpiringSoon(token)) return token;
271
+ try {
272
+ return await refreshToken();
273
+ } catch {
274
+ return token; // return current token as fallback
275
+ }
276
+ }
277
+
179
278
  const TOOLS = {
180
279
  claude: {
181
280
  name: 'Claude Code',
@@ -216,5 +315,10 @@ module.exports = {
216
315
  getEndpoints,
217
316
  parseJWT,
218
317
  getTokenExpiry,
219
- isTokenExpired
318
+ isTokenExpired,
319
+ isTokenExpiringSoon,
320
+ isTokenRefreshable,
321
+ refreshTokenApi,
322
+ refreshToken,
323
+ getValidToken
220
324
  };
@@ -0,0 +1,31 @@
1
+ const config = require('./config');
2
+
3
+ /**
4
+ * Get a valid token, auto-refreshing if expiring soon.
5
+ * Exits the process if no token or refresh fails on an expired token.
6
+ */
7
+ async function ensureToken() {
8
+ let token = config.getToken();
9
+ if (!token) {
10
+ console.error('❌ No token found. Run: jbai token set');
11
+ process.exit(1);
12
+ }
13
+
14
+ if (config.isTokenExpiringSoon(token)) {
15
+ try {
16
+ console.error('🔄 Token expiring soon, auto-refreshing...');
17
+ token = await config.refreshToken();
18
+ console.error('✅ Token refreshed');
19
+ } catch (e) {
20
+ if (config.isTokenExpired(token)) {
21
+ console.error(`❌ Token expired and refresh failed: ${e.message}`);
22
+ process.exit(1);
23
+ }
24
+ // Token not expired yet, continue with current
25
+ }
26
+ }
27
+
28
+ return token;
29
+ }
30
+
31
+ module.exports = { ensureToken };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jbai-cli",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "CLI wrappers to use AI coding tools (Claude Code, Codex, Gemini CLI, OpenCode) with JetBrains AI Platform",
5
5
  "keywords": [
6
6
  "jetbrains",