jbai-cli 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -38
- package/bin/jbai-claude.js +3 -17
- package/bin/jbai-codex-5.4.js +6 -0
- package/bin/jbai-codex.js +13 -17
- package/bin/jbai-continue.js +3 -17
- package/bin/jbai-council.js +1 -1
- package/bin/jbai-gemini.js +3 -17
- package/bin/jbai-goose.js +3 -18
- package/bin/jbai-opencode.js +6 -51
- package/bin/jbai-proxy.js +42 -6
- package/bin/jbai.js +315 -121
- package/lib/completions.js +5 -8
- package/lib/config.js +51 -94
- package/lib/model-list.js +2 -2
- package/lib/postinstall.js +0 -23
- package/package.json +3 -4
- package/lib/handoff.js +0 -152
- package/lib/interactive-handoff.js +0 -220
package/lib/config.js
CHANGED
|
@@ -28,142 +28,99 @@ const ENDPOINTS = {
|
|
|
28
28
|
|
|
29
29
|
// All models available from JetBrains AI Platform (Grazie)
|
|
30
30
|
// Model names must match EXACTLY what the Grazie API accepts
|
|
31
|
-
//
|
|
31
|
+
// Verified against Grazie staging profiles 2026-04-10
|
|
32
32
|
const MODELS = {
|
|
33
|
+
// Claude Code: uses Anthropic /messages endpoint (Chat feature required)
|
|
33
34
|
claude: {
|
|
34
|
-
default: 'claude-sonnet-4-
|
|
35
|
+
default: 'claude-sonnet-4-6',
|
|
35
36
|
available: [
|
|
36
|
-
// Claude 4.6 series (latest)
|
|
37
37
|
'claude-opus-4-6',
|
|
38
38
|
'claude-sonnet-4-6',
|
|
39
|
-
|
|
40
|
-
'claude-
|
|
41
|
-
'claude-
|
|
42
|
-
'claude-
|
|
43
|
-
|
|
44
|
-
'claude-opus-4-1-20250805',
|
|
45
|
-
'claude-sonnet-4-20250514',
|
|
46
|
-
// Claude 3.x series
|
|
47
|
-
'claude-3-7-sonnet-20250219',
|
|
48
|
-
'claude-3-5-haiku-20241022'
|
|
39
|
+
'claude-opus-4-5',
|
|
40
|
+
'claude-sonnet-4-5',
|
|
41
|
+
'claude-haiku-4-5',
|
|
42
|
+
'claude-opus-4-1',
|
|
43
|
+
'claude-sonnet-4-0',
|
|
49
44
|
]
|
|
50
45
|
},
|
|
46
|
+
// OpenCode: uses OpenAI /chat/completions endpoint (Chat feature required)
|
|
51
47
|
openai: {
|
|
52
|
-
|
|
53
|
-
// Keep in sync with the OpenAI proxy's advertised list.
|
|
54
|
-
default: 'gpt-5.2-2025-12-11',
|
|
48
|
+
default: 'gpt-5.4',
|
|
55
49
|
available: [
|
|
56
|
-
|
|
57
|
-
'gpt-5.
|
|
58
|
-
'gpt-5.
|
|
50
|
+
'gpt-5.4',
|
|
51
|
+
'gpt-5.4-mini',
|
|
52
|
+
'gpt-5.4-nano',
|
|
59
53
|
'gpt-5.2',
|
|
60
|
-
'gpt-5.1
|
|
61
|
-
'gpt-5
|
|
62
|
-
'gpt-5-mini
|
|
63
|
-
'gpt-5-nano
|
|
64
|
-
|
|
65
|
-
'
|
|
66
|
-
'gpt-4.1
|
|
67
|
-
'gpt-4.1-
|
|
68
|
-
|
|
69
|
-
'gpt-4o-2024-11-20',
|
|
70
|
-
'gpt-4o-mini-2024-07-18',
|
|
71
|
-
'gpt-4-turbo-2024-04-09',
|
|
72
|
-
'gpt-4-0613',
|
|
73
|
-
// O-series (reasoning) - use max_completion_tokens instead of max_tokens
|
|
74
|
-
'o4-mini-2025-04-16',
|
|
75
|
-
'o3-2025-04-16',
|
|
76
|
-
'o3-mini-2025-01-31',
|
|
77
|
-
'o1-2024-12-17',
|
|
78
|
-
// Legacy
|
|
79
|
-
'gpt-3.5-turbo-0125'
|
|
54
|
+
'gpt-5.1',
|
|
55
|
+
'gpt-5',
|
|
56
|
+
'gpt-5-mini',
|
|
57
|
+
'gpt-5-nano',
|
|
58
|
+
'o3',
|
|
59
|
+
'o4-mini',
|
|
60
|
+
'gpt-4.1',
|
|
61
|
+
'gpt-4.1-mini',
|
|
62
|
+
'gpt-4.1-nano',
|
|
80
63
|
]
|
|
81
64
|
},
|
|
82
|
-
// Codex CLI uses OpenAI
|
|
83
|
-
// Includes chat
|
|
65
|
+
// Codex CLI: uses OpenAI /responses endpoint (Responses feature required)
|
|
66
|
+
// Includes both chat+responses models and codex-only models
|
|
84
67
|
codex: {
|
|
85
|
-
default: 'gpt-5.
|
|
68
|
+
default: 'gpt-5.4',
|
|
86
69
|
available: [
|
|
87
|
-
|
|
88
|
-
'
|
|
89
|
-
'gpt-5.
|
|
90
|
-
// Codex-specific models (responses API only, NOT available via chat/completions)
|
|
70
|
+
'gpt-5.4',
|
|
71
|
+
'gpt-5.4-mini',
|
|
72
|
+
'gpt-5.4-nano',
|
|
91
73
|
'gpt-5.3-codex',
|
|
92
|
-
|
|
93
|
-
'gpt-5.2-2025-12-11',
|
|
74
|
+
'gpt-5.3-codex-spark-preview',
|
|
94
75
|
'gpt-5.2',
|
|
95
|
-
'gpt-5.1-2025-11-13',
|
|
96
|
-
'gpt-5-2025-08-07',
|
|
97
76
|
'gpt-5.2-codex',
|
|
98
|
-
'gpt-5.2-pro
|
|
99
|
-
'gpt-5.1
|
|
77
|
+
'gpt-5.2-pro',
|
|
78
|
+
'gpt-5.1',
|
|
100
79
|
'gpt-5.1-codex',
|
|
101
80
|
'gpt-5.1-codex-mini',
|
|
81
|
+
'gpt-5.1-codex-max',
|
|
82
|
+
'gpt-5',
|
|
102
83
|
'gpt-5-codex',
|
|
103
|
-
|
|
104
|
-
'
|
|
105
|
-
'o3
|
|
84
|
+
'gpt-5-mini',
|
|
85
|
+
'gpt-5-nano',
|
|
86
|
+
'o3',
|
|
87
|
+
'o4-mini',
|
|
88
|
+
'gpt-4.1',
|
|
89
|
+
'gpt-4.1-mini',
|
|
90
|
+
'gpt-4.1-nano',
|
|
106
91
|
]
|
|
107
92
|
},
|
|
108
93
|
gemini: {
|
|
109
|
-
default: 'gemini-2.5-
|
|
94
|
+
default: 'gemini-2.5-pro',
|
|
110
95
|
available: [
|
|
111
|
-
// Hidden/EAP models (JB internal NDA)
|
|
112
|
-
'supernova',
|
|
113
96
|
'gemini-3.1-pro-preview',
|
|
114
|
-
// Gemini 3.x (preview)
|
|
115
|
-
'gemini-3-pro-preview',
|
|
116
97
|
'gemini-3-flash-preview',
|
|
117
|
-
|
|
98
|
+
'gemini-3.1-flash-lite-preview',
|
|
118
99
|
'gemini-2.5-pro',
|
|
119
100
|
'gemini-2.5-flash',
|
|
120
101
|
'gemini-2.5-flash-lite',
|
|
121
|
-
|
|
122
|
-
'gemini-2.0-flash-
|
|
123
|
-
'gemini-2.0-flash-lite-001'
|
|
102
|
+
'gemini-2.0-flash',
|
|
103
|
+
'gemini-2.0-flash-lite',
|
|
124
104
|
]
|
|
125
105
|
},
|
|
126
106
|
// Grazie native Chat API models — accessible via /grazie-openai/v1 translation layer.
|
|
127
107
|
// Full list is dynamic (fetched from /user/v5/llm/profiles), this is a static fallback.
|
|
128
108
|
grazie: {
|
|
129
|
-
default: '
|
|
109
|
+
default: 'xai-grok-4',
|
|
130
110
|
available: [
|
|
131
|
-
// Google
|
|
132
|
-
'google-gemini-3-1-pro',
|
|
133
|
-
'google-gemini-3-0-flash',
|
|
134
|
-
'google-gemini-3-0-pro-snowball',
|
|
135
|
-
'google-chat-gemini-pro-2.5',
|
|
136
|
-
'google-gemini-2.5-flash',
|
|
137
|
-
// DeepSeek
|
|
138
|
-
'deepseek-r1',
|
|
139
|
-
'openrouter-deepseek-v3-2',
|
|
140
|
-
// Mistral
|
|
141
|
-
'mistral-large',
|
|
142
|
-
'mistral-small',
|
|
143
|
-
'openrouter-mistral-large-2512',
|
|
144
|
-
// xAI / Grok
|
|
145
111
|
'xai-grok-4',
|
|
112
|
+
'xai-grok-4-fast',
|
|
146
113
|
'xai-grok-4-1-fast',
|
|
114
|
+
'xai-grok-4-1-fast-non-reasoning',
|
|
147
115
|
'xai-grok-code-fast-1',
|
|
148
|
-
|
|
149
|
-
'qwen-max',
|
|
150
|
-
'qwen-plus',
|
|
151
|
-
// Kimi
|
|
152
|
-
'openrouter-kimi-k2-5',
|
|
153
|
-
'openrouter-kimi-k2-thinking',
|
|
154
|
-
// MiniMax
|
|
155
|
-
'openrouter-minimax-m2-1',
|
|
156
|
-
// ZhipuAI
|
|
157
|
-
'openrouter-zhipuai-glm-4-7',
|
|
158
|
-
'openrouter-zhipuai-glm-4-7-flash',
|
|
116
|
+
'deepseek-r1',
|
|
159
117
|
]
|
|
160
118
|
}
|
|
161
119
|
};
|
|
162
120
|
|
|
163
|
-
// Model aliases:
|
|
164
|
-
//
|
|
165
|
-
const MODEL_ALIASES = {
|
|
166
|
-
};
|
|
121
|
+
// Model aliases: map short names to Grazie-accepted equivalents.
|
|
122
|
+
// gpt-5.4 now works natively on staging — no alias needed.
|
|
123
|
+
const MODEL_ALIASES = {};
|
|
167
124
|
|
|
168
125
|
// All models for tools that support multiple providers (OpenCode, Codex)
|
|
169
126
|
const ALL_MODELS = {
|
package/lib/model-list.js
CHANGED
|
@@ -56,7 +56,7 @@ function getGroupsForTool(tool) {
|
|
|
56
56
|
defaultModel: config.MODELS.openai.default,
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
|
-
title: 'Grazie Native
|
|
59
|
+
title: 'Grazie Native (xAI, DeepSeek)',
|
|
60
60
|
models: config.MODELS.grazie.available,
|
|
61
61
|
defaultModel: config.MODELS.grazie.default,
|
|
62
62
|
},
|
|
@@ -85,7 +85,7 @@ function getGroupsForTool(tool) {
|
|
|
85
85
|
defaultModel: config.MODELS.gemini.default,
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
|
-
title: 'Grazie Native
|
|
88
|
+
title: 'Grazie Native (xAI, DeepSeek)',
|
|
89
89
|
models: config.MODELS.grazie.available,
|
|
90
90
|
defaultModel: config.MODELS.grazie.default,
|
|
91
91
|
},
|
package/lib/postinstall.js
CHANGED
|
@@ -1,30 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
3
|
const config = require('./config');
|
|
6
4
|
|
|
7
|
-
// Fix node-pty spawn-helper permissions (macOS/Linux)
|
|
8
|
-
// The prebuilt binary sometimes loses execute permissions during npm install
|
|
9
|
-
try {
|
|
10
|
-
const platform = process.platform === 'darwin' ? 'darwin' : process.platform;
|
|
11
|
-
const arch = process.arch;
|
|
12
|
-
const spawnHelperPath = path.join(
|
|
13
|
-
__dirname,
|
|
14
|
-
'..',
|
|
15
|
-
'node_modules',
|
|
16
|
-
'node-pty',
|
|
17
|
-
'prebuilds',
|
|
18
|
-
`${platform}-${arch}`,
|
|
19
|
-
'spawn-helper'
|
|
20
|
-
);
|
|
21
|
-
if (fs.existsSync(spawnHelperPath)) {
|
|
22
|
-
fs.chmodSync(spawnHelperPath, 0o755);
|
|
23
|
-
}
|
|
24
|
-
} catch {
|
|
25
|
-
// Ignore errors - this is a best-effort fix
|
|
26
|
-
}
|
|
27
|
-
|
|
28
5
|
console.log(`
|
|
29
6
|
╔══════════════════════════════════════════════════════════════╗
|
|
30
7
|
║ jbai-cli installed! ║
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jbai-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "CLI wrappers to use AI coding tools (Claude Code, Codex, Gemini CLI, OpenCode, Goose, Continue) with JetBrains AI Platform",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jetbrains",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"jbai-codex": "bin/jbai-codex.js",
|
|
36
36
|
"jbai-codex-5.2": "bin/jbai-codex-5.2.js",
|
|
37
37
|
"jbai-codex-5.3": "bin/jbai-codex-5.3.js",
|
|
38
|
+
"jbai-codex-5.4": "bin/jbai-codex-5.4.js",
|
|
38
39
|
"jbai-codex-rockhopper": "bin/jbai-codex-rockhopper.js",
|
|
39
40
|
"jbai-gemini": "bin/jbai-gemini.js",
|
|
40
41
|
"jbai-gemini-3.1": "bin/jbai-gemini-3.1.js",
|
|
@@ -55,9 +56,7 @@
|
|
|
55
56
|
"engines": {
|
|
56
57
|
"node": ">=18.0.0"
|
|
57
58
|
},
|
|
58
|
-
"dependencies": {
|
|
59
|
-
"node-pty": "^1.1.0"
|
|
60
|
-
},
|
|
59
|
+
"dependencies": {},
|
|
61
60
|
"scripts": {
|
|
62
61
|
"postinstall": "node lib/postinstall.js",
|
|
63
62
|
"test": "node bin/jbai.js test"
|
package/lib/handoff.js
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,220 +0,0 @@
|
|
|
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
|
-
let ptyProcess;
|
|
58
|
-
try {
|
|
59
|
-
ptyProcess = pty.spawn(command, args, {
|
|
60
|
-
name: 'xterm-256color',
|
|
61
|
-
cols: process.stdout.columns || 80,
|
|
62
|
-
rows: process.stdout.rows || 24,
|
|
63
|
-
cwd: process.cwd(),
|
|
64
|
-
env,
|
|
65
|
-
});
|
|
66
|
-
} catch (err) {
|
|
67
|
-
// node-pty throws synchronous errors (e.g., posix_spawnp failed) when executable not found
|
|
68
|
-
const error = new Error(err.message || 'Failed to spawn process');
|
|
69
|
-
// Check if this is likely a "command not found" error
|
|
70
|
-
const isNotFound = err.message && (
|
|
71
|
-
err.message.includes('posix_spawnp failed') ||
|
|
72
|
-
err.message.includes('ENOENT') ||
|
|
73
|
-
err.message.includes('not found')
|
|
74
|
-
);
|
|
75
|
-
error.code = isNotFound ? 'ENOENT' : 'SPAWN_ERROR';
|
|
76
|
-
error.command = command;
|
|
77
|
-
error.originalError = err;
|
|
78
|
-
// Return a minimal event emitter that immediately emits the error
|
|
79
|
-
const { EventEmitter } = require('events');
|
|
80
|
-
const fakeChild = new EventEmitter();
|
|
81
|
-
setImmediate(() => fakeChild.emit('error', error));
|
|
82
|
-
return fakeChild;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
let lineBuffer = '';
|
|
86
|
-
let lastPrompt = '';
|
|
87
|
-
let inEscape = false;
|
|
88
|
-
let handoffInProgress = false;
|
|
89
|
-
|
|
90
|
-
const updateLineBuffer = (text) => {
|
|
91
|
-
for (const char of text) {
|
|
92
|
-
if (char === '\u001b') {
|
|
93
|
-
inEscape = true;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (inEscape) {
|
|
97
|
-
if (/[a-zA-Z~]/.test(char)) {
|
|
98
|
-
inEscape = false;
|
|
99
|
-
}
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (char === '\r' || char === '\n') {
|
|
103
|
-
if (!shouldIgnoreLine(lineBuffer)) {
|
|
104
|
-
lastPrompt = lineBuffer.trim();
|
|
105
|
-
}
|
|
106
|
-
lineBuffer = '';
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (char === '\u007f' || char === '\b') {
|
|
110
|
-
if (lineBuffer.length > 0) {
|
|
111
|
-
lineBuffer = lineBuffer.slice(0, -1);
|
|
112
|
-
}
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (!isPrintableChar(char)) {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
lineBuffer += char;
|
|
119
|
-
if (lineBuffer.length > 4000) {
|
|
120
|
-
lineBuffer = lineBuffer.slice(-4000);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const triggerHandoff = async () => {
|
|
126
|
-
if (handoffInProgress) return;
|
|
127
|
-
handoffInProgress = true;
|
|
128
|
-
try {
|
|
129
|
-
const fallbackTask = process.env.JBAI_HANDOFF_TASK;
|
|
130
|
-
const task = (lastPrompt && lastPrompt.trim()) || fallbackTask || '';
|
|
131
|
-
if (!task) {
|
|
132
|
-
process.stderr.write('⚠️ No recent prompt detected; set JBAI_HANDOFF_TASK.\n');
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
process.stderr.write('🚀 Creating Orca Lab handoff...\n');
|
|
137
|
-
const result = await createHandoff({
|
|
138
|
-
task,
|
|
139
|
-
repoUrl: process.env.JBAI_HANDOFF_REPO || handoffDefaults.repoUrl,
|
|
140
|
-
ref: process.env.JBAI_HANDOFF_REF || handoffDefaults.ref,
|
|
141
|
-
branchName: process.env.JBAI_HANDOFF_BRANCH || handoffDefaults.branchName,
|
|
142
|
-
gitToken: process.env.GITHUB_TOKEN || process.env.GH_TOKEN || handoffDefaults.gitToken,
|
|
143
|
-
facadeToken: process.env.FACADE_JWT_TOKEN || handoffDefaults.facadeToken,
|
|
144
|
-
orcaUrl: process.env.ORCA_LAB_URL || handoffDefaults.orcaUrl,
|
|
145
|
-
grazieToken: handoffDefaults.grazieToken,
|
|
146
|
-
grazieEnvironment: handoffDefaults.grazieEnvironment,
|
|
147
|
-
grazieModel: handoffDefaults.grazieModel,
|
|
148
|
-
source: toolName || 'jbai-cli',
|
|
149
|
-
autoStart: true,
|
|
150
|
-
shouldOpen: true,
|
|
151
|
-
cwd: handoffDefaults.cwd,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
process.stderr.write(`✅ Handoff ready: ${result.environmentUrl}\n`);
|
|
155
|
-
} catch (error) {
|
|
156
|
-
const message = error instanceof Error ? error.message : 'Handoff failed';
|
|
157
|
-
process.stderr.write(`❌ ${message}\n`);
|
|
158
|
-
} finally {
|
|
159
|
-
handoffInProgress = false;
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
ptyProcess.onData((data) => process.stdout.write(data));
|
|
164
|
-
|
|
165
|
-
const onStdinData = (data) => {
|
|
166
|
-
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
167
|
-
const hasTrigger = buffer.includes(Buffer.from(trigger));
|
|
168
|
-
|
|
169
|
-
const sanitized = hasTrigger
|
|
170
|
-
? Buffer.from(buffer.toString('utf8').split(trigger).join(''), 'utf8')
|
|
171
|
-
: buffer;
|
|
172
|
-
|
|
173
|
-
if (hasTrigger) {
|
|
174
|
-
triggerHandoff();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (sanitized.length > 0) {
|
|
178
|
-
updateLineBuffer(sanitized.toString('utf8'));
|
|
179
|
-
ptyProcess.write(sanitized);
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const onResize = () => {
|
|
184
|
-
try {
|
|
185
|
-
ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
186
|
-
} catch {
|
|
187
|
-
// Ignore resize errors
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
process.stdin.setRawMode(true);
|
|
192
|
-
process.stdin.resume();
|
|
193
|
-
process.stdin.on('data', onStdinData);
|
|
194
|
-
|
|
195
|
-
if (process.stdout.isTTY) {
|
|
196
|
-
process.stdout.on('resize', onResize);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const cleanup = () => {
|
|
200
|
-
if (process.stdin.isTTY) {
|
|
201
|
-
process.stdin.setRawMode(false);
|
|
202
|
-
}
|
|
203
|
-
process.stdin.off('data', onStdinData);
|
|
204
|
-
if (process.stdout.isTTY) {
|
|
205
|
-
process.stdout.off('resize', onResize);
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
ptyProcess.onExit(({ exitCode }) => {
|
|
210
|
-
cleanup();
|
|
211
|
-
process.exit(exitCode || 0);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
return ptyProcess;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
module.exports = {
|
|
218
|
-
runWithHandoff,
|
|
219
|
-
stripHandoffFlag,
|
|
220
|
-
};
|