jbai-cli 2.1.0 → 2.1.1
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 +27 -5
- package/bin/jbai.js +315 -121
- package/lib/completions.js +5 -8
- package/lib/config.js +15 -92
- 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
|
@@ -31,131 +31,50 @@ const ENDPOINTS = {
|
|
|
31
31
|
// Run 'node bin/test-models.js' to verify model availability
|
|
32
32
|
const MODELS = {
|
|
33
33
|
claude: {
|
|
34
|
-
default: 'claude-sonnet-4-
|
|
34
|
+
default: 'claude-sonnet-4-6',
|
|
35
35
|
available: [
|
|
36
|
-
// Claude 4.6 series (latest)
|
|
37
36
|
'claude-opus-4-6',
|
|
38
37
|
'claude-sonnet-4-6',
|
|
39
|
-
// Claude 4.5 series
|
|
40
|
-
'claude-opus-4-5-20251101',
|
|
41
|
-
'claude-sonnet-4-5-20250929',
|
|
42
|
-
'claude-haiku-4-5-20251001',
|
|
43
|
-
// Claude 4.x series
|
|
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'
|
|
49
38
|
]
|
|
50
39
|
},
|
|
51
40
|
openai: {
|
|
52
41
|
// Chat/Completions models (used by OpenCode)
|
|
53
|
-
//
|
|
54
|
-
default: 'gpt-5.
|
|
42
|
+
// NOTE: codex-only models (gpt-5.x-codex) do NOT work on chat/completions
|
|
43
|
+
default: 'gpt-5.4',
|
|
55
44
|
available: [
|
|
56
|
-
|
|
57
|
-
'gpt-5.3-codex',
|
|
58
|
-
'gpt-5.2-2025-12-11',
|
|
45
|
+
'gpt-5.4',
|
|
59
46
|
'gpt-5.2',
|
|
60
|
-
'gpt-5.1-2025-11-13',
|
|
61
|
-
'gpt-5-2025-08-07',
|
|
62
|
-
'gpt-5-mini-2025-08-07',
|
|
63
|
-
'gpt-5-nano-2025-08-07',
|
|
64
|
-
// GPT-4.1 series
|
|
65
|
-
'gpt-4.1-2025-04-14',
|
|
66
|
-
'gpt-4.1-mini-2025-04-14',
|
|
67
|
-
'gpt-4.1-nano-2025-04-14',
|
|
68
|
-
// GPT-4o/4-turbo
|
|
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
47
|
'o3-2025-04-16',
|
|
76
|
-
'o3-mini-2025-01-31',
|
|
77
|
-
'o1-2024-12-17',
|
|
78
|
-
// Legacy
|
|
79
|
-
'gpt-3.5-turbo-0125'
|
|
80
48
|
]
|
|
81
49
|
},
|
|
82
50
|
// Codex CLI uses OpenAI models via the "responses" API (wire_api = "responses")
|
|
83
|
-
// Includes chat-capable models PLUS codex-only models (responses API only)
|
|
84
51
|
codex: {
|
|
85
|
-
default: 'gpt-5.
|
|
52
|
+
default: 'gpt-5.4',
|
|
86
53
|
available: [
|
|
87
|
-
|
|
88
|
-
'rockhopper-alpha',
|
|
89
|
-
'gpt-5.3-codex-api-preview',
|
|
90
|
-
// Codex-specific models (responses API only, NOT available via chat/completions)
|
|
54
|
+
'gpt-5.4',
|
|
91
55
|
'gpt-5.3-codex',
|
|
92
|
-
// GPT-5.x chat models (also work via responses API)
|
|
93
|
-
'gpt-5.2-2025-12-11',
|
|
94
|
-
'gpt-5.2',
|
|
95
|
-
'gpt-5.1-2025-11-13',
|
|
96
|
-
'gpt-5-2025-08-07',
|
|
97
56
|
'gpt-5.2-codex',
|
|
98
|
-
'
|
|
99
|
-
'gpt-5.1-codex-max',
|
|
100
|
-
'gpt-5.1-codex',
|
|
101
|
-
'gpt-5.1-codex-mini',
|
|
102
|
-
'gpt-5-codex',
|
|
103
|
-
// O-series (also work via responses API)
|
|
104
|
-
'o4-mini-2025-04-16',
|
|
105
|
-
'o3-2025-04-16'
|
|
57
|
+
'o3-2025-04-16',
|
|
106
58
|
]
|
|
107
59
|
},
|
|
108
60
|
gemini: {
|
|
109
|
-
default: 'gemini-2.5-
|
|
61
|
+
default: 'gemini-2.5-pro',
|
|
110
62
|
available: [
|
|
111
|
-
// Hidden/EAP models (JB internal NDA)
|
|
112
|
-
'supernova',
|
|
113
63
|
'gemini-3.1-pro-preview',
|
|
114
|
-
// Gemini 3.x (preview)
|
|
115
64
|
'gemini-3-pro-preview',
|
|
116
|
-
'gemini-3-flash-preview',
|
|
117
|
-
// Gemini 2.5
|
|
118
65
|
'gemini-2.5-pro',
|
|
119
66
|
'gemini-2.5-flash',
|
|
120
|
-
'gemini-2.5-flash-lite',
|
|
121
|
-
// Gemini 2.0
|
|
122
|
-
'gemini-2.0-flash-001',
|
|
123
|
-
'gemini-2.0-flash-lite-001'
|
|
124
67
|
]
|
|
125
68
|
},
|
|
126
69
|
// Grazie native Chat API models — accessible via /grazie-openai/v1 translation layer.
|
|
127
70
|
// Full list is dynamic (fetched from /user/v5/llm/profiles), this is a static fallback.
|
|
71
|
+
// Only coding-capable models with tool use support.
|
|
128
72
|
grazie: {
|
|
129
|
-
default: '
|
|
73
|
+
default: 'xai-grok-4',
|
|
130
74
|
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
75
|
'xai-grok-4',
|
|
146
|
-
'xai-grok-4-1-fast',
|
|
147
76
|
'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',
|
|
77
|
+
'deepseek-r1',
|
|
159
78
|
]
|
|
160
79
|
}
|
|
161
80
|
};
|
|
@@ -163,6 +82,10 @@ const MODELS = {
|
|
|
163
82
|
// Model aliases: some CLI tools send short names that Grazie doesn't recognise yet.
|
|
164
83
|
// Map them to the Grazie-accepted equivalents so the proxy can rewrite on the fly.
|
|
165
84
|
const MODEL_ALIASES = {
|
|
85
|
+
// Temporary compatibility mapping: staging currently exposes GPT-5.4 as spark preview.
|
|
86
|
+
// Keep user-facing defaults on gpt-5.4 and rewrite at the proxy edge.
|
|
87
|
+
'gpt-5.4': 'gpt-5.3-codex-spark-preview',
|
|
88
|
+
'openai-gpt-5-4': 'gpt-5.3-codex-spark-preview',
|
|
166
89
|
};
|
|
167
90
|
|
|
168
91
|
// All models for tools that support multiple providers (OpenCode, Codex)
|
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.1.
|
|
3
|
+
"version": "2.1.1",
|
|
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
|
-
};
|