jbai-cli 1.5.2 → 1.5.4
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 +24 -0
- package/bin/jbai-claude.js +28 -17
- package/bin/jbai-codex.js +28 -17
- package/bin/jbai-gemini.js +27 -16
- package/bin/jbai-opencode.js +33 -22
- package/bin/jbai.js +152 -1
- package/bin/test-clients.js +305 -0
- package/lib/config.js +16 -8
- package/lib/handoff.js +152 -0
- package/lib/interactive-handoff.js +200 -0
- package/package.json +10 -8
package/README.md
CHANGED
|
@@ -70,6 +70,29 @@ jbai-aider --model gemini/gemini-2.5-pro
|
|
|
70
70
|
jbai-opencode
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
### Handoff to Orca Lab (local)
|
|
74
|
+
```bash
|
|
75
|
+
# Continue a task in Orca Lab via local facade
|
|
76
|
+
jbai handoff --task "continue this work in orca-lab"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### In-session handoff (interactive tools)
|
|
80
|
+
While running `jbai-codex`, `jbai-claude`, `jbai-gemini`, or `jbai-opencode`:
|
|
81
|
+
- Press `Ctrl+]` to trigger a handoff to Orca Lab.
|
|
82
|
+
- The last prompt you typed is used as the task.
|
|
83
|
+
|
|
84
|
+
Optional environment variables:
|
|
85
|
+
- `ORCA_LAB_URL` (default: `http://localhost:3000`)
|
|
86
|
+
- `FACADE_JWT_TOKEN` (local facade auth)
|
|
87
|
+
- `GITHUB_TOKEN` / `GH_TOKEN` (private repos)
|
|
88
|
+
- `JBAI_HANDOFF_TASK` (fallback task if no prompt captured)
|
|
89
|
+
- `JBAI_HANDOFF_REPO` (override repo URL)
|
|
90
|
+
- `JBAI_HANDOFF_REF` (override git ref)
|
|
91
|
+
- `JBAI_HANDOFF_BRANCH` (override working branch)
|
|
92
|
+
- `JBAI_HANDOFF_ENV` (STAGING | PREPROD | PRODUCTION)
|
|
93
|
+
- `JBAI_HANDOFF_MODEL` (Claude model for Orca Lab agent)
|
|
94
|
+
- `JBAI_HANDOFF_OPEN` (set to `false` to avoid opening a browser)
|
|
95
|
+
|
|
73
96
|
## Super Mode (Skip Confirmations)
|
|
74
97
|
|
|
75
98
|
Add `--super` (or `--yolo` or `-s`) to any command to enable maximum permissions:
|
|
@@ -157,6 +180,7 @@ jbai-aider --model gemini/gemini-2.5-pro
|
|
|
157
180
|
| `jbai token set` | Set/update token |
|
|
158
181
|
| `jbai test` | Test API connections |
|
|
159
182
|
| `jbai models` | List all models |
|
|
183
|
+
| `jbai handoff` | Continue a task in Orca Lab |
|
|
160
184
|
| `jbai install` | Install all AI tools |
|
|
161
185
|
| `jbai install claude` | Install specific tool |
|
|
162
186
|
| `jbai doctor` | Check tool installation status |
|
package/bin/jbai-claude.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { runWithHandoff, stripHandoffFlag } = require('../lib/interactive-handoff');
|
|
4
4
|
const config = require('../lib/config');
|
|
5
5
|
|
|
6
6
|
const token = config.getToken();
|
|
@@ -16,6 +16,8 @@ if (config.isTokenExpired(token)) {
|
|
|
16
16
|
|
|
17
17
|
const endpoints = config.getEndpoints();
|
|
18
18
|
let args = process.argv.slice(2);
|
|
19
|
+
const handoffConfig = stripHandoffFlag(args);
|
|
20
|
+
args = handoffConfig.args;
|
|
19
21
|
|
|
20
22
|
// Check for super mode (--super, --yolo, -s)
|
|
21
23
|
const superFlags = ['--super', '--yolo', '-s'];
|
|
@@ -46,21 +48,30 @@ const env = {
|
|
|
46
48
|
// Remove any existing auth token that might conflict
|
|
47
49
|
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
48
50
|
|
|
49
|
-
const child =
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
},
|
|
52
63
|
});
|
|
53
64
|
|
|
54
|
-
child.on
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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);
|
|
76
|
+
});
|
|
77
|
+
}
|
package/bin/jbai-codex.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { runWithHandoff, stripHandoffFlag } = require('../lib/interactive-handoff');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
@@ -22,6 +22,8 @@ const environment = config.getEnvironment();
|
|
|
22
22
|
const providerName = environment === 'staging' ? 'jbai-staging' : 'jbai';
|
|
23
23
|
const envVarName = environment === 'staging' ? 'GRAZIE_STAGING_TOKEN' : 'GRAZIE_API_TOKEN';
|
|
24
24
|
let args = process.argv.slice(2);
|
|
25
|
+
const handoffConfig = stripHandoffFlag(args);
|
|
26
|
+
args = handoffConfig.args;
|
|
25
27
|
|
|
26
28
|
// Check for super mode (--super, --yolo, -s)
|
|
27
29
|
const superFlags = ['--super', '--yolo', '-s'];
|
|
@@ -75,21 +77,30 @@ const childEnv = {
|
|
|
75
77
|
[envVarName]: token
|
|
76
78
|
};
|
|
77
79
|
|
|
78
|
-
const child =
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
},
|
|
81
92
|
});
|
|
82
93
|
|
|
83
|
-
child.on
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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);
|
|
105
|
+
});
|
|
106
|
+
}
|
package/bin/jbai-gemini.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Uses GEMINI_CLI_CUSTOM_HEADERS and GEMINI_BASE_URL for authentication
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const {
|
|
9
|
+
const { runWithHandoff, stripHandoffFlag } = require('../lib/interactive-handoff');
|
|
10
10
|
const config = require('../lib/config');
|
|
11
11
|
|
|
12
12
|
const token = config.getToken();
|
|
@@ -22,6 +22,8 @@ if (config.isTokenExpired(token)) {
|
|
|
22
22
|
|
|
23
23
|
const endpoints = config.getEndpoints();
|
|
24
24
|
let args = process.argv.slice(2);
|
|
25
|
+
const handoffConfig = stripHandoffFlag(args);
|
|
26
|
+
args = handoffConfig.args;
|
|
25
27
|
|
|
26
28
|
// Check for super mode (--super, --yolo, -s)
|
|
27
29
|
const superFlags = ['--super', '--yolo', '-s'];
|
|
@@ -47,20 +49,29 @@ const env = {
|
|
|
47
49
|
GEMINI_CLI_CUSTOM_HEADERS: `Grazie-Authenticate-JWT: ${token}`
|
|
48
50
|
};
|
|
49
51
|
|
|
50
|
-
const child =
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
},
|
|
53
64
|
});
|
|
54
65
|
|
|
55
|
-
child.on
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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);
|
|
76
|
+
});
|
|
77
|
+
}
|
package/bin/jbai-opencode.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { runWithHandoff, stripHandoffFlag } = require('../lib/interactive-handoff');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
@@ -20,6 +20,8 @@ if (config.isTokenExpired(token)) {
|
|
|
20
20
|
const endpoints = config.getEndpoints();
|
|
21
21
|
const environment = config.getEnvironment();
|
|
22
22
|
let args = process.argv.slice(2);
|
|
23
|
+
const handoffConfig = stripHandoffFlag(args);
|
|
24
|
+
args = handoffConfig.args;
|
|
23
25
|
|
|
24
26
|
// Check for super mode (--super, --yolo, -s)
|
|
25
27
|
const superFlags = ['--super', '--yolo', '-s'];
|
|
@@ -58,9 +60,9 @@ const envVarName = environment === 'staging' ? 'GRAZIE_STAGING_TOKEN' : 'GRAZIE_
|
|
|
58
60
|
const anthropicProviderName = environment === 'staging' ? 'jbai-anthropic-staging' : 'jbai-anthropic';
|
|
59
61
|
|
|
60
62
|
// Add/update JetBrains OpenAI provider with custom header (using env var reference)
|
|
61
|
-
// Use
|
|
63
|
+
// Use OpenAI SDK to support max_completion_tokens for GPT-5.x
|
|
62
64
|
opencodeConfig.provider[providerName] = {
|
|
63
|
-
npm: '@ai-sdk/openai
|
|
65
|
+
npm: '@ai-sdk/openai',
|
|
64
66
|
name: `JetBrains AI OpenAI (${environment})`,
|
|
65
67
|
options: {
|
|
66
68
|
baseURL: endpoints.openai,
|
|
@@ -73,14 +75,14 @@ opencodeConfig.provider[providerName] = {
|
|
|
73
75
|
};
|
|
74
76
|
|
|
75
77
|
// Add OpenAI models
|
|
76
|
-
//
|
|
77
|
-
// For
|
|
78
|
+
// OpenCode requires an output limit in the config.
|
|
79
|
+
// For O-series models we keep a larger context window but still set output.
|
|
78
80
|
config.MODELS.openai.available.forEach(model => {
|
|
79
81
|
const isOSeries = /^o[1-9]/.test(model);
|
|
80
82
|
opencodeConfig.provider[providerName].models[model] = {
|
|
81
83
|
name: model,
|
|
82
84
|
limit: isOSeries
|
|
83
|
-
? { context: 200000
|
|
85
|
+
? { context: 200000, output: 8192 }
|
|
84
86
|
: { context: 128000, output: 8192 }
|
|
85
87
|
};
|
|
86
88
|
});
|
|
@@ -132,21 +134,30 @@ const childEnv = {
|
|
|
132
134
|
[envVarName]: token
|
|
133
135
|
};
|
|
134
136
|
|
|
135
|
-
const child =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
console.error(`Error: ${err.message}`);
|
|
148
|
-
}
|
|
149
|
-
process.exit(1);
|
|
137
|
+
const child = runWithHandoff({
|
|
138
|
+
command: 'opencode',
|
|
139
|
+
args: finalArgs,
|
|
140
|
+
env: childEnv,
|
|
141
|
+
toolName: 'jbai-opencode',
|
|
142
|
+
handoffDefaults: {
|
|
143
|
+
enabled: !handoffConfig.disabled,
|
|
144
|
+
grazieToken: token,
|
|
145
|
+
grazieEnvironment: environment === 'production' ? 'PRODUCTION' : 'STAGING',
|
|
146
|
+
grazieModel: config.MODELS.claude.default,
|
|
147
|
+
cwd: process.cwd(),
|
|
148
|
+
},
|
|
150
149
|
});
|
|
151
150
|
|
|
152
|
-
|
|
151
|
+
if (child && typeof child.on === 'function') {
|
|
152
|
+
child.on('error', (err) => {
|
|
153
|
+
if (err.code === 'ENOENT') {
|
|
154
|
+
const tool = config.TOOLS.opencode;
|
|
155
|
+
console.error(`❌ ${tool.name} not found.\n`);
|
|
156
|
+
console.error(`Install with: ${tool.install}`);
|
|
157
|
+
console.error(`Or run: jbai install opencode`);
|
|
158
|
+
} else {
|
|
159
|
+
console.error(`Error: ${err.message}`);
|
|
160
|
+
}
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|
|
163
|
+
}
|
package/bin/jbai.js
CHANGED
|
@@ -4,6 +4,7 @@ const { spawn, execSync } = require('child_process');
|
|
|
4
4
|
const readline = require('readline');
|
|
5
5
|
const https = require('https');
|
|
6
6
|
const config = require('../lib/config');
|
|
7
|
+
const { createHandoff } = require('../lib/handoff');
|
|
7
8
|
|
|
8
9
|
const TOOLS = {
|
|
9
10
|
claude: {
|
|
@@ -42,6 +43,7 @@ COMMANDS:
|
|
|
42
43
|
jbai token set Set token interactively
|
|
43
44
|
jbai token refresh Refresh expired token
|
|
44
45
|
jbai test Test API endpoints (incl. Codex /responses)
|
|
46
|
+
jbai handoff Continue task in Orca Lab
|
|
45
47
|
jbai env [staging|production] Switch environment
|
|
46
48
|
jbai models List available models
|
|
47
49
|
jbai install Install all AI tools (claude, codex, gemini, opencode)
|
|
@@ -66,6 +68,7 @@ EXAMPLES:
|
|
|
66
68
|
jbai-claude # Start Claude Code
|
|
67
69
|
jbai-codex exec "explain code" # Run Codex task
|
|
68
70
|
jbai-gemini # Start Gemini CLI
|
|
71
|
+
jbai handoff --task "fix lint" # Handoff task to Orca Lab
|
|
69
72
|
|
|
70
73
|
TOKEN:
|
|
71
74
|
Get token: ${config.getEndpoints().tokenUrl}
|
|
@@ -137,13 +140,22 @@ async function testEndpoints() {
|
|
|
137
140
|
|
|
138
141
|
const endpoints = config.getEndpoints();
|
|
139
142
|
console.log(`Testing JetBrains AI Platform (${config.getEnvironment()})\n`);
|
|
143
|
+
const defaultOpenAIModel = config.MODELS.openai.default;
|
|
144
|
+
const useCompletionTokens = defaultOpenAIModel.startsWith('gpt-5') ||
|
|
145
|
+
defaultOpenAIModel.startsWith('o1') ||
|
|
146
|
+
defaultOpenAIModel.startsWith('o3') ||
|
|
147
|
+
defaultOpenAIModel.startsWith('o4');
|
|
140
148
|
|
|
141
149
|
// Test OpenAI
|
|
142
150
|
process.stdout.write('1. OpenAI Proxy (Chat): ');
|
|
143
151
|
try {
|
|
144
152
|
const result = await httpPost(
|
|
145
153
|
`${endpoints.openai}/chat/completions`,
|
|
146
|
-
{
|
|
154
|
+
{
|
|
155
|
+
model: defaultOpenAIModel,
|
|
156
|
+
messages: [{ role: 'user', content: 'Say OK' }],
|
|
157
|
+
...(useCompletionTokens ? { max_completion_tokens: 5 } : { max_tokens: 5 })
|
|
158
|
+
},
|
|
147
159
|
{ 'Grazie-Authenticate-JWT': token }
|
|
148
160
|
);
|
|
149
161
|
const ok = result.statusCode === 200 && Array.isArray(result.json?.choices);
|
|
@@ -408,6 +420,142 @@ async function installTools(toolKey) {
|
|
|
408
420
|
console.log('Run: jbai doctor to verify');
|
|
409
421
|
}
|
|
410
422
|
|
|
423
|
+
const HANDOFF_HELP = `
|
|
424
|
+
jbai handoff - Continue a task in Orca Lab
|
|
425
|
+
|
|
426
|
+
Usage:
|
|
427
|
+
jbai handoff --task "your task"
|
|
428
|
+
jbai handoff "your task"
|
|
429
|
+
|
|
430
|
+
Options:
|
|
431
|
+
--task, -t Task description (or pass as positional)
|
|
432
|
+
--repo, -r Git repo URL (defaults to origin remote)
|
|
433
|
+
--ref Git ref (defaults to current branch)
|
|
434
|
+
--branch, -b Working branch name for the agent
|
|
435
|
+
--model, -m Claude model (default: ${config.MODELS.claude.default})
|
|
436
|
+
--grazie-env, -e STAGING | PREPROD | PRODUCTION
|
|
437
|
+
--grazie-token Override Grazie token (default: ~/.jbai/token)
|
|
438
|
+
--git-token, -g GitHub token (default: GITHUB_TOKEN/GH_TOKEN)
|
|
439
|
+
--facade-token, -f Facade JWT token (default: FACADE_JWT_TOKEN)
|
|
440
|
+
--orca-url, -o Orca Lab URL (default: http://localhost:3000)
|
|
441
|
+
--no-open Do not open the Orca Lab URL
|
|
442
|
+
--no-auto-start Do not auto-start the agent task
|
|
443
|
+
--help Show this help
|
|
444
|
+
`;
|
|
445
|
+
|
|
446
|
+
function parseArgs(argv) {
|
|
447
|
+
const opts = {};
|
|
448
|
+
const rest = [];
|
|
449
|
+
const shortMap = {
|
|
450
|
+
t: 'task',
|
|
451
|
+
r: 'repo',
|
|
452
|
+
b: 'branch',
|
|
453
|
+
m: 'model',
|
|
454
|
+
e: 'grazie-env',
|
|
455
|
+
g: 'git-token',
|
|
456
|
+
f: 'facade-token',
|
|
457
|
+
o: 'orca-url',
|
|
458
|
+
h: 'help'
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
for (let i = 0; i < argv.length; i++) {
|
|
462
|
+
const arg = argv[i];
|
|
463
|
+
if (arg === '--') {
|
|
464
|
+
rest.push(...argv.slice(i + 1));
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
if (arg.startsWith('--')) {
|
|
468
|
+
if (arg === '--no-open') {
|
|
469
|
+
opts.open = false;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (arg === '--no-auto-start') {
|
|
473
|
+
opts.autoStart = false;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
const key = arg.slice(2);
|
|
477
|
+
const next = argv[i + 1];
|
|
478
|
+
if (next && !next.startsWith('-')) {
|
|
479
|
+
opts[key] = next;
|
|
480
|
+
i++;
|
|
481
|
+
} else {
|
|
482
|
+
opts[key] = true;
|
|
483
|
+
}
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (arg.startsWith('-') && arg.length === 2) {
|
|
487
|
+
const short = arg.slice(1);
|
|
488
|
+
const key = shortMap[short];
|
|
489
|
+
if (!key) {
|
|
490
|
+
rest.push(arg);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const next = argv[i + 1];
|
|
494
|
+
if (next && !next.startsWith('-')) {
|
|
495
|
+
opts[key] = next;
|
|
496
|
+
i++;
|
|
497
|
+
} else {
|
|
498
|
+
opts[key] = true;
|
|
499
|
+
}
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
rest.push(arg);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return { opts, rest };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function readStdin() {
|
|
509
|
+
return new Promise((resolve) => {
|
|
510
|
+
let data = '';
|
|
511
|
+
process.stdin.setEncoding('utf8');
|
|
512
|
+
process.stdin.on('data', (chunk) => data += chunk);
|
|
513
|
+
process.stdin.on('end', () => resolve(data));
|
|
514
|
+
process.stdin.on('error', () => resolve(''));
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function handoffToOrca(rawArgs) {
|
|
519
|
+
const { opts, rest } = parseArgs(rawArgs);
|
|
520
|
+
if (opts.help) {
|
|
521
|
+
console.log(HANDOFF_HELP);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
let task = opts.task || rest.join(' ').trim();
|
|
526
|
+
if (!task && !process.stdin.isTTY) {
|
|
527
|
+
task = (await readStdin()).trim();
|
|
528
|
+
}
|
|
529
|
+
if (!task) {
|
|
530
|
+
console.log(HANDOFF_HELP);
|
|
531
|
+
process.exit(1);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
const result = await createHandoff({
|
|
536
|
+
task,
|
|
537
|
+
repoUrl: opts.repo,
|
|
538
|
+
ref: opts.ref,
|
|
539
|
+
branchName: opts.branch,
|
|
540
|
+
gitToken: opts['git-token'] || opts.gitToken,
|
|
541
|
+
facadeToken: opts['facade-token'] || opts.facadeToken,
|
|
542
|
+
orcaUrl: opts['orca-url'] || opts.orcaUrl,
|
|
543
|
+
grazieToken: opts['grazie-token'] || opts.grazieToken,
|
|
544
|
+
grazieEnvironment: opts['grazie-env'] || opts.grazieEnv,
|
|
545
|
+
grazieModel: opts.model,
|
|
546
|
+
autoStart: opts.autoStart !== false,
|
|
547
|
+
shouldOpen: opts.open !== false,
|
|
548
|
+
source: 'jbai-cli',
|
|
549
|
+
});
|
|
550
|
+
console.log('✅ Handoff created');
|
|
551
|
+
console.log(`Environment: ${result.environmentId}`);
|
|
552
|
+
console.log(`Open: ${result.environmentUrl}`);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
console.error(`❌ ${error instanceof Error ? error.message : 'Handoff failed'}`);
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
411
559
|
// Main
|
|
412
560
|
const [,, command, ...args] = process.argv;
|
|
413
561
|
|
|
@@ -422,6 +570,9 @@ switch (command) {
|
|
|
422
570
|
case 'test':
|
|
423
571
|
testEndpoints();
|
|
424
572
|
break;
|
|
573
|
+
case 'handoff':
|
|
574
|
+
handoffToOrca(args);
|
|
575
|
+
break;
|
|
425
576
|
case 'models':
|
|
426
577
|
showModels();
|
|
427
578
|
break;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* E2E Client Testing Script
|
|
5
|
+
* Tests that each jbai client can successfully execute a task with each available model.
|
|
6
|
+
* Uses the actual APIs to verify end-to-end functionality.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const https = require('https');
|
|
10
|
+
const config = require('../lib/config');
|
|
11
|
+
|
|
12
|
+
const token = config.getToken();
|
|
13
|
+
if (!token) {
|
|
14
|
+
console.error('❌ No token found. Run: jbai token set');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const endpoints = config.getEndpoints();
|
|
19
|
+
|
|
20
|
+
// Colors for terminal output
|
|
21
|
+
const colors = {
|
|
22
|
+
reset: '\x1b[0m',
|
|
23
|
+
green: '\x1b[32m',
|
|
24
|
+
red: '\x1b[31m',
|
|
25
|
+
yellow: '\x1b[33m',
|
|
26
|
+
cyan: '\x1b[36m',
|
|
27
|
+
dim: '\x1b[2m'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function httpPost(url, body, headers) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const urlObj = new URL(url);
|
|
33
|
+
const data = JSON.stringify(body);
|
|
34
|
+
|
|
35
|
+
const req = https.request({
|
|
36
|
+
hostname: urlObj.hostname,
|
|
37
|
+
port: 443,
|
|
38
|
+
path: urlObj.pathname + urlObj.search,
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
'Content-Length': Buffer.byteLength(data),
|
|
43
|
+
...headers
|
|
44
|
+
},
|
|
45
|
+
timeout: 60000
|
|
46
|
+
}, (res) => {
|
|
47
|
+
let body = '';
|
|
48
|
+
res.on('data', chunk => body += chunk);
|
|
49
|
+
res.on('end', () => {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(body);
|
|
52
|
+
resolve({ status: res.statusCode, data: parsed });
|
|
53
|
+
} catch {
|
|
54
|
+
resolve({ status: res.statusCode, data: body, raw: true });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
req.on('error', (e) => reject(e));
|
|
60
|
+
req.on('timeout', () => {
|
|
61
|
+
req.destroy();
|
|
62
|
+
reject(new Error('Timeout'));
|
|
63
|
+
});
|
|
64
|
+
req.write(data);
|
|
65
|
+
req.end();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Test Claude (Anthropic API)
|
|
70
|
+
async function testClaude(model) {
|
|
71
|
+
try {
|
|
72
|
+
const result = await httpPost(
|
|
73
|
+
`${endpoints.anthropic}/messages`,
|
|
74
|
+
{
|
|
75
|
+
model: model,
|
|
76
|
+
messages: [{ role: 'user', content: 'Reply with exactly: JBAI_OK' }],
|
|
77
|
+
max_tokens: 20
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
'Grazie-Authenticate-JWT': token,
|
|
81
|
+
'anthropic-version': '2023-06-01'
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (result.status === 200 && result.data.content) {
|
|
86
|
+
const response = result.data.content[0]?.text || '';
|
|
87
|
+
if (response.includes('JBAI_OK')) {
|
|
88
|
+
return { success: true, message: 'OK', response };
|
|
89
|
+
}
|
|
90
|
+
return { success: true, message: 'OK (response varied)', response };
|
|
91
|
+
} else if (result.status === 429) {
|
|
92
|
+
return { success: true, message: 'Rate limited (model works)', error: 'Rate limit' };
|
|
93
|
+
} else {
|
|
94
|
+
return { success: false, message: `Status ${result.status}`, error: result.data.error?.message || JSON.stringify(result.data).substring(0, 100) };
|
|
95
|
+
}
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return { success: false, message: 'Error', error: e.message };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Test Codex (OpenAI Responses API - used by codex CLI)
|
|
102
|
+
async function testCodex(model) {
|
|
103
|
+
try {
|
|
104
|
+
// Codex uses the OpenAI "responses" API format
|
|
105
|
+
// For testing, we use chat/completions which is what the proxy supports
|
|
106
|
+
const needsCompletionTokens = model.startsWith('gpt-5') || model.startsWith('o1') ||
|
|
107
|
+
model.startsWith('o3') || model.startsWith('o4');
|
|
108
|
+
|
|
109
|
+
const bodyParams = needsCompletionTokens
|
|
110
|
+
? { max_completion_tokens: 100 }
|
|
111
|
+
: { max_tokens: 20 };
|
|
112
|
+
|
|
113
|
+
const result = await httpPost(
|
|
114
|
+
`${endpoints.openai}/chat/completions`,
|
|
115
|
+
{
|
|
116
|
+
model: model,
|
|
117
|
+
messages: [{ role: 'user', content: 'Reply with exactly: JBAI_OK' }],
|
|
118
|
+
...bodyParams
|
|
119
|
+
},
|
|
120
|
+
{ 'Grazie-Authenticate-JWT': token }
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (result.status === 200 && result.data.choices) {
|
|
124
|
+
const response = result.data.choices[0]?.message?.content || '';
|
|
125
|
+
if (response.includes('JBAI_OK')) {
|
|
126
|
+
return { success: true, message: 'OK', response };
|
|
127
|
+
}
|
|
128
|
+
return { success: true, message: 'OK (response varied)', response };
|
|
129
|
+
} else if (result.status === 429) {
|
|
130
|
+
return { success: true, message: 'Rate limited (model works)', error: 'Rate limit' };
|
|
131
|
+
} else {
|
|
132
|
+
return { success: false, message: `Status ${result.status}`, error: result.data.error?.message || JSON.stringify(result.data).substring(0, 100) };
|
|
133
|
+
}
|
|
134
|
+
} catch (e) {
|
|
135
|
+
return { success: false, message: 'Error', error: e.message };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Test OpenCode (OpenAI Chat Completions API)
|
|
140
|
+
async function testOpenCode(model) {
|
|
141
|
+
// OpenCode uses the same OpenAI API as Codex
|
|
142
|
+
return testCodex(model);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Test Gemini (Vertex AI API)
|
|
146
|
+
async function testGemini(model) {
|
|
147
|
+
try {
|
|
148
|
+
const result = await httpPost(
|
|
149
|
+
`${endpoints.google}/v1/projects/default/locations/default/publishers/google/models/${model}:generateContent`,
|
|
150
|
+
{
|
|
151
|
+
contents: [{ role: 'user', parts: [{ text: 'Reply with exactly: JBAI_OK' }] }]
|
|
152
|
+
},
|
|
153
|
+
{ 'Grazie-Authenticate-JWT': token }
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (result.status === 200 && result.data.candidates) {
|
|
157
|
+
const response = result.data.candidates[0]?.content?.parts?.[0]?.text || '';
|
|
158
|
+
if (response.includes('JBAI_OK')) {
|
|
159
|
+
return { success: true, message: 'OK', response };
|
|
160
|
+
}
|
|
161
|
+
return { success: true, message: 'OK (response varied)', response };
|
|
162
|
+
} else if (result.status === 429) {
|
|
163
|
+
return { success: true, message: 'Rate limited (model works)', error: 'Rate limit' };
|
|
164
|
+
} else {
|
|
165
|
+
return { success: false, message: `Status ${result.status}`, error: result.data.error?.message || JSON.stringify(result.data).substring(0, 100) };
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
return { success: false, message: 'Error', error: e.message };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function runTests() {
|
|
173
|
+
console.log(`\n${'='.repeat(70)}`);
|
|
174
|
+
console.log(`${colors.cyan}JBAI-CLI E2E CLIENT TESTING${colors.reset}`);
|
|
175
|
+
console.log(`Environment: ${config.getEnvironment()}`);
|
|
176
|
+
console.log(`${'='.repeat(70)}\n`);
|
|
177
|
+
|
|
178
|
+
const results = {
|
|
179
|
+
claude: { working: [], failing: [] },
|
|
180
|
+
codex: { working: [], failing: [] },
|
|
181
|
+
opencode: { working: [], failing: [] },
|
|
182
|
+
gemini: { working: [], failing: [] }
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Test jbai-claude with all Claude models
|
|
186
|
+
console.log(`${colors.cyan}Testing jbai-claude with all Claude models...${colors.reset}`);
|
|
187
|
+
console.log('-'.repeat(50));
|
|
188
|
+
for (const model of config.MODELS.claude.available) {
|
|
189
|
+
process.stdout.write(` ${model.padEnd(35)} `);
|
|
190
|
+
const result = await testClaude(model);
|
|
191
|
+
if (result.success) {
|
|
192
|
+
console.log(`${colors.green}✓ ${result.message}${colors.reset}`);
|
|
193
|
+
results.claude.working.push(model);
|
|
194
|
+
} else {
|
|
195
|
+
console.log(`${colors.red}✗ ${result.message}${colors.reset}`);
|
|
196
|
+
console.log(` ${colors.dim}${result.error}${colors.reset}`);
|
|
197
|
+
results.claude.failing.push({ model, error: result.error });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Test jbai-codex with all Codex models
|
|
202
|
+
console.log(`\n${colors.cyan}Testing jbai-codex with all Codex models...${colors.reset}`);
|
|
203
|
+
console.log('-'.repeat(50));
|
|
204
|
+
for (const model of config.MODELS.codex.available) {
|
|
205
|
+
process.stdout.write(` ${model.padEnd(35)} `);
|
|
206
|
+
const result = await testCodex(model);
|
|
207
|
+
if (result.success) {
|
|
208
|
+
console.log(`${colors.green}✓ ${result.message}${colors.reset}`);
|
|
209
|
+
results.codex.working.push(model);
|
|
210
|
+
} else {
|
|
211
|
+
console.log(`${colors.red}✗ ${result.message}${colors.reset}`);
|
|
212
|
+
console.log(` ${colors.dim}${result.error}${colors.reset}`);
|
|
213
|
+
results.codex.failing.push({ model, error: result.error });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Test jbai-opencode with all OpenAI models
|
|
218
|
+
console.log(`\n${colors.cyan}Testing jbai-opencode with all OpenAI models...${colors.reset}`);
|
|
219
|
+
console.log('-'.repeat(50));
|
|
220
|
+
for (const model of config.MODELS.openai.available) {
|
|
221
|
+
process.stdout.write(` ${model.padEnd(35)} `);
|
|
222
|
+
const result = await testOpenCode(model);
|
|
223
|
+
if (result.success) {
|
|
224
|
+
console.log(`${colors.green}✓ ${result.message}${colors.reset}`);
|
|
225
|
+
results.opencode.working.push(model);
|
|
226
|
+
} else {
|
|
227
|
+
console.log(`${colors.red}✗ ${result.message}${colors.reset}`);
|
|
228
|
+
console.log(` ${colors.dim}${result.error}${colors.reset}`);
|
|
229
|
+
results.opencode.failing.push({ model, error: result.error });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Test jbai-gemini with all Gemini models
|
|
234
|
+
console.log(`\n${colors.cyan}Testing jbai-gemini with all Gemini models...${colors.reset}`);
|
|
235
|
+
console.log('-'.repeat(50));
|
|
236
|
+
for (const model of config.MODELS.gemini.available) {
|
|
237
|
+
process.stdout.write(` ${model.padEnd(35)} `);
|
|
238
|
+
const result = await testGemini(model);
|
|
239
|
+
if (result.success) {
|
|
240
|
+
console.log(`${colors.green}✓ ${result.message}${colors.reset}`);
|
|
241
|
+
results.gemini.working.push(model);
|
|
242
|
+
} else {
|
|
243
|
+
console.log(`${colors.red}✗ ${result.message}${colors.reset}`);
|
|
244
|
+
console.log(` ${colors.dim}${result.error}${colors.reset}`);
|
|
245
|
+
results.gemini.failing.push({ model, error: result.error });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Summary
|
|
250
|
+
console.log(`\n${'='.repeat(70)}`);
|
|
251
|
+
console.log(`${colors.cyan}SUMMARY${colors.reset}`);
|
|
252
|
+
console.log(`${'='.repeat(70)}`);
|
|
253
|
+
|
|
254
|
+
const totalWorking = results.claude.working.length + results.codex.working.length +
|
|
255
|
+
results.opencode.working.length + results.gemini.working.length;
|
|
256
|
+
const totalFailing = results.claude.failing.length + results.codex.failing.length +
|
|
257
|
+
results.opencode.failing.length + results.gemini.failing.length;
|
|
258
|
+
|
|
259
|
+
console.log(`\n${colors.green}Working: ${totalWorking}${colors.reset}`);
|
|
260
|
+
console.log(`${colors.red}Failing: ${totalFailing}${colors.reset}`);
|
|
261
|
+
|
|
262
|
+
console.log(`\n${colors.cyan}By Client:${colors.reset}`);
|
|
263
|
+
console.log(` jbai-claude: ${colors.green}${results.claude.working.length}${colors.reset} working, ${colors.red}${results.claude.failing.length}${colors.reset} failing`);
|
|
264
|
+
console.log(` jbai-codex: ${colors.green}${results.codex.working.length}${colors.reset} working, ${colors.red}${results.codex.failing.length}${colors.reset} failing`);
|
|
265
|
+
console.log(` jbai-opencode: ${colors.green}${results.opencode.working.length}${colors.reset} working, ${colors.red}${results.opencode.failing.length}${colors.reset} failing`);
|
|
266
|
+
console.log(` jbai-gemini: ${colors.green}${results.gemini.working.length}${colors.reset} working, ${colors.red}${results.gemini.failing.length}${colors.reset} failing`);
|
|
267
|
+
|
|
268
|
+
if (totalFailing > 0) {
|
|
269
|
+
console.log(`\n${colors.red}FAILING MODELS:${colors.reset}`);
|
|
270
|
+
|
|
271
|
+
if (results.claude.failing.length > 0) {
|
|
272
|
+
console.log(`\n jbai-claude:`);
|
|
273
|
+
results.claude.failing.forEach(f => console.log(` - ${f.model}: ${f.error}`));
|
|
274
|
+
}
|
|
275
|
+
if (results.codex.failing.length > 0) {
|
|
276
|
+
console.log(`\n jbai-codex:`);
|
|
277
|
+
results.codex.failing.forEach(f => console.log(` - ${f.model}: ${f.error}`));
|
|
278
|
+
}
|
|
279
|
+
if (results.opencode.failing.length > 0) {
|
|
280
|
+
console.log(`\n jbai-opencode:`);
|
|
281
|
+
results.opencode.failing.forEach(f => console.log(` - ${f.model}: ${f.error}`));
|
|
282
|
+
}
|
|
283
|
+
if (results.gemini.failing.length > 0) {
|
|
284
|
+
console.log(`\n jbai-gemini:`);
|
|
285
|
+
results.gemini.failing.forEach(f => console.log(` - ${f.model}: ${f.error}`));
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
console.log(`\n${colors.green}All clients work with all their available models!${colors.reset}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
console.log(`\n${'='.repeat(70)}\n`);
|
|
292
|
+
|
|
293
|
+
// Exit with error code if any failures
|
|
294
|
+
if (totalFailing > 0) {
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return results;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Run tests
|
|
302
|
+
runTests().catch(err => {
|
|
303
|
+
console.error('Test error:', err);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
});
|
package/lib/config.js
CHANGED
|
@@ -38,7 +38,6 @@ const MODELS = {
|
|
|
38
38
|
'claude-haiku-4-5-20251001',
|
|
39
39
|
// Claude 4.x series
|
|
40
40
|
'claude-opus-4-1-20250805',
|
|
41
|
-
'claude-opus-4-20250514',
|
|
42
41
|
'claude-sonnet-4-20250514',
|
|
43
42
|
// Claude 3.x series
|
|
44
43
|
'claude-3-7-sonnet-20250219',
|
|
@@ -75,15 +74,24 @@ const MODELS = {
|
|
|
75
74
|
'gpt-3.5-turbo-0125'
|
|
76
75
|
]
|
|
77
76
|
},
|
|
78
|
-
// Codex CLI uses the
|
|
77
|
+
// Codex CLI uses OpenAI models via the "responses" API (wire_api = "responses")
|
|
78
|
+
// Uses the same models as openai, just different API wire format
|
|
79
79
|
codex: {
|
|
80
|
-
default: '
|
|
80
|
+
default: 'o3-2025-04-16',
|
|
81
81
|
available: [
|
|
82
|
-
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
|
|
82
|
+
// O-series models (best for coding tasks)
|
|
83
|
+
'o3-2025-04-16',
|
|
84
|
+
'o3-mini-2025-01-31',
|
|
85
|
+
'o4-mini-2025-04-16',
|
|
86
|
+
// GPT-5.x series
|
|
87
|
+
'gpt-5.2-2025-12-11',
|
|
88
|
+
'gpt-5.2',
|
|
89
|
+
'gpt-5.1-2025-11-13',
|
|
90
|
+
'gpt-5-2025-08-07',
|
|
91
|
+
// GPT-4.x series
|
|
92
|
+
'gpt-4.1-2025-04-14',
|
|
93
|
+
'gpt-4o-2024-11-20',
|
|
94
|
+
'gpt-4-turbo-2024-04-09'
|
|
87
95
|
]
|
|
88
96
|
},
|
|
89
97
|
gemini: {
|
package/lib/handoff.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const config = require('./config');
|
|
3
|
+
|
|
4
|
+
function getGitOutput(command, cwd = process.cwd()) {
|
|
5
|
+
try {
|
|
6
|
+
return execSync(command, { stdio: ['ignore', 'pipe', 'ignore'], cwd }).toString().trim();
|
|
7
|
+
} catch {
|
|
8
|
+
return '';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getGitRepoUrl(cwd = process.cwd()) {
|
|
13
|
+
return getGitOutput('git remote get-url origin', cwd);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getGitRef(cwd = process.cwd()) {
|
|
17
|
+
const branch = getGitOutput('git rev-parse --abbrev-ref HEAD', cwd);
|
|
18
|
+
if (branch && branch !== 'HEAD') {
|
|
19
|
+
return branch;
|
|
20
|
+
}
|
|
21
|
+
return getGitOutput('git rev-parse HEAD', cwd);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function openUrl(url) {
|
|
25
|
+
const escaped = url.replace(/"/g, '\\"');
|
|
26
|
+
try {
|
|
27
|
+
if (process.platform === 'darwin') {
|
|
28
|
+
execSync(`open "${escaped}"`, { stdio: 'ignore' });
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
if (process.platform === 'win32') {
|
|
32
|
+
execSync(`start "" "${escaped}"`, { stdio: 'ignore' });
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
execSync(`xdg-open "${escaped}"`, { stdio: 'ignore' });
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeGrazieEnvironment(env) {
|
|
43
|
+
if (!env) {
|
|
44
|
+
return config.getEnvironment() === 'production' ? 'PRODUCTION' : 'STAGING';
|
|
45
|
+
}
|
|
46
|
+
return env.toString().toUpperCase();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getDefaultModel() {
|
|
50
|
+
return config.MODELS.claude.default;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseBool(value, fallback) {
|
|
54
|
+
if (value === undefined || value === null) {
|
|
55
|
+
return fallback;
|
|
56
|
+
}
|
|
57
|
+
const normalized = value.toString().toLowerCase();
|
|
58
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
|
|
59
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function createHandoff({
|
|
64
|
+
task,
|
|
65
|
+
repoUrl,
|
|
66
|
+
ref,
|
|
67
|
+
branchName,
|
|
68
|
+
grazieToken,
|
|
69
|
+
grazieEnvironment,
|
|
70
|
+
grazieModel,
|
|
71
|
+
gitToken,
|
|
72
|
+
facadeToken,
|
|
73
|
+
orcaUrl,
|
|
74
|
+
source,
|
|
75
|
+
autoStart,
|
|
76
|
+
shouldOpen,
|
|
77
|
+
cwd,
|
|
78
|
+
}) {
|
|
79
|
+
const finalTask = task && task.trim()
|
|
80
|
+
? task.trim()
|
|
81
|
+
: 'Continue the current task from the CLI session.';
|
|
82
|
+
|
|
83
|
+
const finalRepoUrl = repoUrl && repoUrl.trim() ? repoUrl.trim() : getGitRepoUrl(cwd);
|
|
84
|
+
if (!finalRepoUrl) {
|
|
85
|
+
throw new Error('Could not determine git repo. Use --repo or set JBAI_HANDOFF_REPO.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const finalGrazieToken = grazieToken || config.getToken();
|
|
89
|
+
if (!finalGrazieToken) {
|
|
90
|
+
throw new Error('No Grazie token found. Run: jbai token set');
|
|
91
|
+
}
|
|
92
|
+
if (config.isTokenExpired(finalGrazieToken)) {
|
|
93
|
+
throw new Error('Grazie token expired. Run: jbai token refresh');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const finalRef = ref || getGitRef(cwd);
|
|
97
|
+
const finalOrcaUrl = (orcaUrl || process.env.ORCA_LAB_URL || 'http://localhost:3000')
|
|
98
|
+
.replace(/\/$/, '');
|
|
99
|
+
const finalFacadeToken = facadeToken || process.env.FACADE_JWT_TOKEN || '';
|
|
100
|
+
const finalGitToken = gitToken || process.env.GITHUB_TOKEN || process.env.GH_TOKEN || '';
|
|
101
|
+
const finalGrazieEnv = normalizeGrazieEnvironment(
|
|
102
|
+
grazieEnvironment || process.env.JBAI_HANDOFF_ENV
|
|
103
|
+
);
|
|
104
|
+
const finalModel = grazieModel || process.env.JBAI_HANDOFF_MODEL || getDefaultModel();
|
|
105
|
+
const finalAutoStart = parseBool(autoStart ?? process.env.JBAI_HANDOFF_AUTO_START, true);
|
|
106
|
+
const openBrowser = parseBool(shouldOpen ?? process.env.JBAI_HANDOFF_OPEN, true);
|
|
107
|
+
|
|
108
|
+
const payload = {
|
|
109
|
+
task: finalTask,
|
|
110
|
+
repoUrl: finalRepoUrl,
|
|
111
|
+
ref: finalRef || undefined,
|
|
112
|
+
branchName: branchName || process.env.JBAI_HANDOFF_BRANCH || undefined,
|
|
113
|
+
gitToken: finalGitToken || undefined,
|
|
114
|
+
grazieToken: finalGrazieToken,
|
|
115
|
+
grazieEnvironment: finalGrazieEnv,
|
|
116
|
+
grazieModel: finalModel,
|
|
117
|
+
source: source || 'jbai-cli',
|
|
118
|
+
autoStart: finalAutoStart,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const headers = {
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
...(finalFacadeToken ? { Authorization: `Bearer ${finalFacadeToken}` } : {}),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const response = await fetch(`${finalOrcaUrl}/api/handoff`, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers,
|
|
129
|
+
body: JSON.stringify(payload),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
const errorText = await response.text().catch(() => '');
|
|
134
|
+
const detail = errorText ? ` ${errorText}` : '';
|
|
135
|
+
throw new Error(`Orca Lab handoff failed (${response.status}).${detail}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result = await response.json();
|
|
139
|
+
|
|
140
|
+
if (openBrowser && result.environmentUrl) {
|
|
141
|
+
openUrl(result.environmentUrl);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
createHandoff,
|
|
149
|
+
getGitRepoUrl,
|
|
150
|
+
getGitRef,
|
|
151
|
+
openUrl,
|
|
152
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const { spawn } = require('child_process');
|
|
3
|
+
const pty = require('node-pty');
|
|
4
|
+
const { createHandoff } = require('./handoff');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TRIGGER = '\x1d'; // Ctrl+]
|
|
7
|
+
const DEFAULT_TRIGGER_LABEL = 'Ctrl+]';
|
|
8
|
+
|
|
9
|
+
function stripHandoffFlag(args) {
|
|
10
|
+
const cleaned = [];
|
|
11
|
+
let disabled = false;
|
|
12
|
+
for (const arg of args) {
|
|
13
|
+
if (arg === '--no-handoff') {
|
|
14
|
+
disabled = true;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
cleaned.push(arg);
|
|
18
|
+
}
|
|
19
|
+
return { args: cleaned, disabled };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isPrintableChar(char) {
|
|
23
|
+
const code = char.charCodeAt(0);
|
|
24
|
+
return code >= 32 && code !== 127;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shouldIgnoreLine(line) {
|
|
28
|
+
const trimmed = line.trim();
|
|
29
|
+
if (!trimmed) return true;
|
|
30
|
+
if (trimmed.startsWith('/')) return true;
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildTrigger() {
|
|
35
|
+
const trigger = process.env.JBAI_HANDOFF_TRIGGER || DEFAULT_TRIGGER;
|
|
36
|
+
const label = process.env.JBAI_HANDOFF_TRIGGER_LABEL || DEFAULT_TRIGGER_LABEL;
|
|
37
|
+
return { trigger, label };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function runWithHandoff({
|
|
41
|
+
command,
|
|
42
|
+
args,
|
|
43
|
+
env,
|
|
44
|
+
toolName,
|
|
45
|
+
handoffDefaults = {},
|
|
46
|
+
}) {
|
|
47
|
+
const { trigger, label } = buildTrigger();
|
|
48
|
+
const canUsePty = process.stdin.isTTY && process.stdout.isTTY;
|
|
49
|
+
|
|
50
|
+
if (!canUsePty || handoffDefaults.enabled === false) {
|
|
51
|
+
const child = spawn(command, args, { stdio: 'inherit', env });
|
|
52
|
+
return child;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
process.stderr.write(`ℹ️ Handoff trigger: ${label}\n`);
|
|
56
|
+
|
|
57
|
+
const ptyProcess = pty.spawn(command, args, {
|
|
58
|
+
name: 'xterm-256color',
|
|
59
|
+
cols: process.stdout.columns || 80,
|
|
60
|
+
rows: process.stdout.rows || 24,
|
|
61
|
+
cwd: process.cwd(),
|
|
62
|
+
env,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
let lineBuffer = '';
|
|
66
|
+
let lastPrompt = '';
|
|
67
|
+
let inEscape = false;
|
|
68
|
+
let handoffInProgress = false;
|
|
69
|
+
|
|
70
|
+
const updateLineBuffer = (text) => {
|
|
71
|
+
for (const char of text) {
|
|
72
|
+
if (char === '\u001b') {
|
|
73
|
+
inEscape = true;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (inEscape) {
|
|
77
|
+
if (/[a-zA-Z~]/.test(char)) {
|
|
78
|
+
inEscape = false;
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (char === '\r' || char === '\n') {
|
|
83
|
+
if (!shouldIgnoreLine(lineBuffer)) {
|
|
84
|
+
lastPrompt = lineBuffer.trim();
|
|
85
|
+
}
|
|
86
|
+
lineBuffer = '';
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (char === '\u007f' || char === '\b') {
|
|
90
|
+
if (lineBuffer.length > 0) {
|
|
91
|
+
lineBuffer = lineBuffer.slice(0, -1);
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!isPrintableChar(char)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
lineBuffer += char;
|
|
99
|
+
if (lineBuffer.length > 4000) {
|
|
100
|
+
lineBuffer = lineBuffer.slice(-4000);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const triggerHandoff = async () => {
|
|
106
|
+
if (handoffInProgress) return;
|
|
107
|
+
handoffInProgress = true;
|
|
108
|
+
try {
|
|
109
|
+
const fallbackTask = process.env.JBAI_HANDOFF_TASK;
|
|
110
|
+
const task = (lastPrompt && lastPrompt.trim()) || fallbackTask || '';
|
|
111
|
+
if (!task) {
|
|
112
|
+
process.stderr.write('⚠️ No recent prompt detected; set JBAI_HANDOFF_TASK.\n');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
process.stderr.write('🚀 Creating Orca Lab handoff...\n');
|
|
117
|
+
const result = await createHandoff({
|
|
118
|
+
task,
|
|
119
|
+
repoUrl: process.env.JBAI_HANDOFF_REPO || handoffDefaults.repoUrl,
|
|
120
|
+
ref: process.env.JBAI_HANDOFF_REF || handoffDefaults.ref,
|
|
121
|
+
branchName: process.env.JBAI_HANDOFF_BRANCH || handoffDefaults.branchName,
|
|
122
|
+
gitToken: process.env.GITHUB_TOKEN || process.env.GH_TOKEN || handoffDefaults.gitToken,
|
|
123
|
+
facadeToken: process.env.FACADE_JWT_TOKEN || handoffDefaults.facadeToken,
|
|
124
|
+
orcaUrl: process.env.ORCA_LAB_URL || handoffDefaults.orcaUrl,
|
|
125
|
+
grazieToken: handoffDefaults.grazieToken,
|
|
126
|
+
grazieEnvironment: handoffDefaults.grazieEnvironment,
|
|
127
|
+
grazieModel: handoffDefaults.grazieModel,
|
|
128
|
+
source: toolName || 'jbai-cli',
|
|
129
|
+
autoStart: true,
|
|
130
|
+
shouldOpen: true,
|
|
131
|
+
cwd: handoffDefaults.cwd,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
process.stderr.write(`✅ Handoff ready: ${result.environmentUrl}\n`);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const message = error instanceof Error ? error.message : 'Handoff failed';
|
|
137
|
+
process.stderr.write(`❌ ${message}\n`);
|
|
138
|
+
} finally {
|
|
139
|
+
handoffInProgress = false;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
ptyProcess.onData((data) => process.stdout.write(data));
|
|
144
|
+
|
|
145
|
+
const onStdinData = (data) => {
|
|
146
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
147
|
+
const hasTrigger = buffer.includes(Buffer.from(trigger));
|
|
148
|
+
|
|
149
|
+
const sanitized = hasTrigger
|
|
150
|
+
? Buffer.from(buffer.toString('utf8').split(trigger).join(''), 'utf8')
|
|
151
|
+
: buffer;
|
|
152
|
+
|
|
153
|
+
if (hasTrigger) {
|
|
154
|
+
triggerHandoff();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (sanitized.length > 0) {
|
|
158
|
+
updateLineBuffer(sanitized.toString('utf8'));
|
|
159
|
+
ptyProcess.write(sanitized);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const onResize = () => {
|
|
164
|
+
try {
|
|
165
|
+
ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
166
|
+
} catch {
|
|
167
|
+
// Ignore resize errors
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
process.stdin.setRawMode(true);
|
|
172
|
+
process.stdin.resume();
|
|
173
|
+
process.stdin.on('data', onStdinData);
|
|
174
|
+
|
|
175
|
+
if (process.stdout.isTTY) {
|
|
176
|
+
process.stdout.on('resize', onResize);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const cleanup = () => {
|
|
180
|
+
if (process.stdin.isTTY) {
|
|
181
|
+
process.stdin.setRawMode(false);
|
|
182
|
+
}
|
|
183
|
+
process.stdin.off('data', onStdinData);
|
|
184
|
+
if (process.stdout.isTTY) {
|
|
185
|
+
process.stdout.off('resize', onResize);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
190
|
+
cleanup();
|
|
191
|
+
process.exit(exitCode || 0);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return ptyProcess;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
runWithHandoff,
|
|
199
|
+
stripHandoffFlag,
|
|
200
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jbai-cli",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
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",
|
|
@@ -18,18 +18,18 @@
|
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/JetBrains/jbai-cli"
|
|
21
|
+
"url": "git+https://github.com/JetBrains/jbai-cli.git"
|
|
22
22
|
},
|
|
23
23
|
"bugs": {
|
|
24
24
|
"url": "https://github.com/JetBrains/jbai-cli/issues"
|
|
25
25
|
},
|
|
26
26
|
"homepage": "https://github.com/JetBrains/jbai-cli#readme",
|
|
27
27
|
"bin": {
|
|
28
|
-
"jbai": "
|
|
29
|
-
"jbai-claude": "
|
|
30
|
-
"jbai-codex": "
|
|
31
|
-
"jbai-gemini": "
|
|
32
|
-
"jbai-opencode": "
|
|
28
|
+
"jbai": "bin/jbai.js",
|
|
29
|
+
"jbai-claude": "bin/jbai-claude.js",
|
|
30
|
+
"jbai-codex": "bin/jbai-codex.js",
|
|
31
|
+
"jbai-gemini": "bin/jbai-gemini.js",
|
|
32
|
+
"jbai-opencode": "bin/jbai-opencode.js"
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"bin/",
|
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=18.0.0"
|
|
41
41
|
},
|
|
42
|
-
"dependencies": {
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"node-pty": "^1.1.0"
|
|
44
|
+
},
|
|
43
45
|
"scripts": {
|
|
44
46
|
"postinstall": "node lib/postinstall.js",
|
|
45
47
|
"test": "node bin/jbai.js test"
|