bortexcode 1.2.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 +108 -0
- package/bin/bortex-agent.js +472 -0
- package/bin/bortex-shared-paths.js +65 -0
- package/bin/bortex.js +6219 -0
- package/bin/bortexcode-agent.js +9 -0
- package/bin/bortexcode.js +9 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Bortex Code CLI
|
|
2
|
+
|
|
3
|
+
Terminal AI coding assistant powered by https://bortex.site.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
After publishing to npm:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g bortexcode
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Fallback installer:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
curl -fsSL https://bortex.site/install.sh | bash
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Windows PowerShell:
|
|
20
|
+
|
|
21
|
+
```powershell
|
|
22
|
+
irm https://bortex.site/install.ps1 | iex
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Direct tarball fallback:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g https://bortex.site/bortex-code/bortex-code-latest.tgz
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Login
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bortexcode --login
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or provide an API key:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bortexcode --api-key <YOUR_API_KEY>
|
|
41
|
+
export BORTEX_API_KEY=<YOUR_API_KEY>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bortexcode
|
|
48
|
+
bortexcode "explain this function"
|
|
49
|
+
bortexcode --agent "refactor src/utils.js"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## REPL Commands
|
|
53
|
+
|
|
54
|
+
Press `/` at the beginning of the prompt to open the command menu.
|
|
55
|
+
Press Tab to complete slash commands.
|
|
56
|
+
|
|
57
|
+
Common commands:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
/agent on|off
|
|
61
|
+
/status
|
|
62
|
+
/commands
|
|
63
|
+
/help
|
|
64
|
+
/llm-config show
|
|
65
|
+
/llm-config sync
|
|
66
|
+
/exit
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Options
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
-a, --agent Agent mode
|
|
73
|
+
--chat Chat mode (default)
|
|
74
|
+
-u, --url <url> Bortex server URL
|
|
75
|
+
--api-key <k> API key
|
|
76
|
+
--login Open browser login
|
|
77
|
+
-m, --model <name> Force a specific LLM model
|
|
78
|
+
--api-url <u> Custom LLM endpoint
|
|
79
|
+
--offline Local mode
|
|
80
|
+
--verbose Show routing details
|
|
81
|
+
--progress Show server progress events
|
|
82
|
+
--no-spinner Disable spinner
|
|
83
|
+
--icons Enable icon output
|
|
84
|
+
--check-update Check for updates now
|
|
85
|
+
--no-update-check
|
|
86
|
+
Disable startup update check
|
|
87
|
+
-v, --version Show version
|
|
88
|
+
-h, --help Show help
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Updates
|
|
92
|
+
|
|
93
|
+
Bortex Code checks for newer versions on startup. When an update is available,
|
|
94
|
+
it asks before installing it globally with npm.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
bortexcode --check-update
|
|
98
|
+
bortexcode --no-update-check
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Environment
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
BORTEX_URL
|
|
105
|
+
BORTEX_API_KEY
|
|
106
|
+
BORTEX_CLI_ICONS=1
|
|
107
|
+
BORTEX_NO_UPDATE_CHECK=1
|
|
108
|
+
```
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const packageMeta = require('../package.json');
|
|
8
|
+
const { getAgentsCatalogFallbackPath } = require('./bortex-shared-paths');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_URL = process.env.BORTEX_AGENT_URL || process.env.BORTEX_URL || 'https://bortex.site';
|
|
11
|
+
const DEFAULT_SERVER_WAIT_MS = 30000;
|
|
12
|
+
const AGENT_CATALOG_FALLBACK = getAgentsCatalogFallbackPath();
|
|
13
|
+
const CLI_VERSION = String(packageMeta.version || '0.0.0');
|
|
14
|
+
|
|
15
|
+
function getInvokedCliName() {
|
|
16
|
+
const scriptPath = String(process.argv[1] || '').trim();
|
|
17
|
+
const baseName = scriptPath ? path.basename(scriptPath, path.extname(scriptPath)) : '';
|
|
18
|
+
return baseName || 'bortexcode-agent';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function printUsage() {
|
|
22
|
+
const cliName = getInvokedCliName();
|
|
23
|
+
console.log('Bortex Agent CLI');
|
|
24
|
+
console.log('');
|
|
25
|
+
console.log('Uso:');
|
|
26
|
+
console.log(` ${cliName} list [--url http://127.0.0.1:3210]`);
|
|
27
|
+
console.log(` ${cliName} health [--url http://127.0.0.1:3210]`);
|
|
28
|
+
console.log(` ${cliName} run (--id <id> | --name <name> | --file <manifest>) [--input "testo"]`);
|
|
29
|
+
console.log(` ${cliName} stop [--run-id <id> | --all]`);
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log('Opzioni:');
|
|
32
|
+
console.log(' --url <url> URL del server locale Bortex');
|
|
33
|
+
console.log(' --id <id> Avvia un agente dal catalogo tramite id');
|
|
34
|
+
console.log(' --name <name> Avvia un agente dal catalogo tramite nome');
|
|
35
|
+
console.log(' --file <path> Avvia un agente esportato da manifest .bortex-agent.json');
|
|
36
|
+
console.log(' --input <text> Messaggio iniziale / prompt agente');
|
|
37
|
+
console.log(' --outputs <csv> Override outputs agente, es. inapp,push');
|
|
38
|
+
console.log(' --run-id <id> Run id esplicito');
|
|
39
|
+
console.log(' --all Ferma tutte le run locali');
|
|
40
|
+
console.log(' --json Output JSON dove possibile');
|
|
41
|
+
console.log(' --no-wait Non attendere l avvio del server locale');
|
|
42
|
+
console.log(' -v, --version Mostra versione');
|
|
43
|
+
console.log(' -h, --help Mostra aiuto');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseArgs(argv) {
|
|
47
|
+
const args = Array.isArray(argv) ? [...argv] : [];
|
|
48
|
+
const initial = String(args[0] || '').trim();
|
|
49
|
+
const command = !initial || initial.startsWith('-')
|
|
50
|
+
? 'help'
|
|
51
|
+
: String(args.shift() || 'help').trim().toLowerCase();
|
|
52
|
+
const options = {
|
|
53
|
+
command,
|
|
54
|
+
url: DEFAULT_URL,
|
|
55
|
+
id: '',
|
|
56
|
+
name: '',
|
|
57
|
+
file: '',
|
|
58
|
+
input: '',
|
|
59
|
+
outputs: [],
|
|
60
|
+
json: false,
|
|
61
|
+
waitForServer: true,
|
|
62
|
+
runId: '',
|
|
63
|
+
stopAll: false
|
|
64
|
+
};
|
|
65
|
+
const positional = [];
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
68
|
+
const arg = args[i];
|
|
69
|
+
if (arg === '--help' || arg === '-h') {
|
|
70
|
+
options.help = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (arg === '--version' || arg === '-v') {
|
|
74
|
+
options.version = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (arg === '--json') {
|
|
78
|
+
options.json = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (arg === '--no-wait') {
|
|
82
|
+
options.waitForServer = false;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (arg === '--all') {
|
|
86
|
+
options.stopAll = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if ((arg === '--url' || arg === '-u') && args[i + 1]) {
|
|
90
|
+
options.url = String(args[i + 1] || '').trim() || options.url;
|
|
91
|
+
i += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (arg.startsWith('--url=')) {
|
|
95
|
+
options.url = String(arg.slice('--url='.length) || '').trim() || options.url;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (arg === '--id' && args[i + 1]) {
|
|
99
|
+
options.id = String(args[i + 1] || '').trim();
|
|
100
|
+
i += 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (arg.startsWith('--id=')) {
|
|
104
|
+
options.id = String(arg.slice('--id='.length) || '').trim();
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (arg === '--name' && args[i + 1]) {
|
|
108
|
+
options.name = String(args[i + 1] || '').trim();
|
|
109
|
+
i += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (arg.startsWith('--name=')) {
|
|
113
|
+
options.name = String(arg.slice('--name='.length) || '').trim();
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (arg === '--file' && args[i + 1]) {
|
|
117
|
+
options.file = String(args[i + 1] || '').trim();
|
|
118
|
+
i += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (arg.startsWith('--file=')) {
|
|
122
|
+
options.file = String(arg.slice('--file='.length) || '').trim();
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (arg === '--input' && args[i + 1]) {
|
|
126
|
+
options.input = String(args[i + 1] || '');
|
|
127
|
+
i += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (arg.startsWith('--input=')) {
|
|
131
|
+
options.input = String(arg.slice('--input='.length) || '');
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (arg === '--outputs' && args[i + 1]) {
|
|
135
|
+
options.outputs = String(args[i + 1] || '')
|
|
136
|
+
.split(',')
|
|
137
|
+
.map((entry) => String(entry || '').trim())
|
|
138
|
+
.filter(Boolean);
|
|
139
|
+
i += 1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (arg.startsWith('--outputs=')) {
|
|
143
|
+
options.outputs = String(arg.slice('--outputs='.length) || '')
|
|
144
|
+
.split(',')
|
|
145
|
+
.map((entry) => String(entry || '').trim())
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (arg === '--run-id' && args[i + 1]) {
|
|
150
|
+
options.runId = String(args[i + 1] || '').trim();
|
|
151
|
+
i += 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (arg.startsWith('--run-id=')) {
|
|
155
|
+
options.runId = String(arg.slice('--run-id='.length) || '').trim();
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
positional.push(arg);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!options.input && positional.length && options.command === 'run') {
|
|
162
|
+
options.input = positional.join(' ').trim();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return options;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function normalizeBaseUrl(url) {
|
|
169
|
+
return String(url || DEFAULT_URL).trim().replace(/\/+$/, '') || DEFAULT_URL;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function sleep(ms) {
|
|
173
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function requestJson(method, url, payload = null, extraHeaders = {}) {
|
|
177
|
+
const headers = {
|
|
178
|
+
Accept: 'application/json',
|
|
179
|
+
...extraHeaders
|
|
180
|
+
};
|
|
181
|
+
const init = { method, headers };
|
|
182
|
+
if (payload !== null && payload !== undefined) {
|
|
183
|
+
headers['Content-Type'] = 'application/json';
|
|
184
|
+
init.body = JSON.stringify(payload);
|
|
185
|
+
}
|
|
186
|
+
const res = await fetch(url, init);
|
|
187
|
+
const rawText = await res.text();
|
|
188
|
+
let data = null;
|
|
189
|
+
if (rawText) {
|
|
190
|
+
try {
|
|
191
|
+
data = JSON.parse(rawText);
|
|
192
|
+
} catch (_) {
|
|
193
|
+
data = rawText;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
const detail = data && typeof data === 'object'
|
|
198
|
+
? (data.error || data.message || JSON.stringify(data))
|
|
199
|
+
: String(data || res.statusText || 'Request failed').trim();
|
|
200
|
+
throw new Error(`HTTP ${res.status}: ${detail}`);
|
|
201
|
+
}
|
|
202
|
+
return data;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function waitForServer(baseUrl, timeoutMs = DEFAULT_SERVER_WAIT_MS) {
|
|
206
|
+
const startedAt = Date.now();
|
|
207
|
+
let lastError = null;
|
|
208
|
+
while ((Date.now() - startedAt) < timeoutMs) {
|
|
209
|
+
try {
|
|
210
|
+
const health = await requestJson('GET', `${baseUrl}/health`);
|
|
211
|
+
if (health && (health.ok === true || health.status === 'ok')) {
|
|
212
|
+
return health;
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
lastError = err;
|
|
216
|
+
}
|
|
217
|
+
await sleep(1000);
|
|
218
|
+
}
|
|
219
|
+
if (lastError) throw lastError;
|
|
220
|
+
throw new Error('Server Bortex non raggiungibile.');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function loadManifestAgent(filePath) {
|
|
224
|
+
const resolved = path.resolve(filePath);
|
|
225
|
+
const raw = fs.readFileSync(resolved, 'utf8');
|
|
226
|
+
const parsed = JSON.parse(raw);
|
|
227
|
+
if (parsed?.agent && typeof parsed.agent === 'object') return parsed.agent;
|
|
228
|
+
if (Array.isArray(parsed?.agents) && parsed.agents.length) return parsed.agents[0];
|
|
229
|
+
if (parsed && typeof parsed === 'object') return parsed;
|
|
230
|
+
throw new Error('Manifest agente non valido.');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function loadCatalogFallback() {
|
|
234
|
+
if (!fs.existsSync(AGENT_CATALOG_FALLBACK)) return [];
|
|
235
|
+
try {
|
|
236
|
+
const raw = fs.readFileSync(AGENT_CATALOG_FALLBACK, 'utf8');
|
|
237
|
+
const parsed = JSON.parse(raw);
|
|
238
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
239
|
+
} catch (_) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function printAgentList(agents = [], asJson = false) {
|
|
245
|
+
if (asJson) {
|
|
246
|
+
console.log(JSON.stringify({ ok: true, agents }, null, 2));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (!agents.length) {
|
|
250
|
+
console.log('Nessun agente trovato.');
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
agents.forEach((agent) => {
|
|
254
|
+
const runtimeProfile = String(agent?.runtimeProfile || '').trim();
|
|
255
|
+
const mission = String(agent?.mission || '').trim();
|
|
256
|
+
console.log(`- ${agent.id || '(senza id)'} | ${agent.name || 'Agente'}`);
|
|
257
|
+
if (runtimeProfile) console.log(` runtime: ${runtimeProfile}`);
|
|
258
|
+
if (mission) console.log(` mission: ${mission}`);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function formatProgressLine(step = {}) {
|
|
263
|
+
const type = String(step?.type || 'step').trim();
|
|
264
|
+
const action = String(step?.action || '').trim();
|
|
265
|
+
const text = String(step?.text || step?.thought || '').trim();
|
|
266
|
+
if (type === 'tool_result') {
|
|
267
|
+
return `[${type}] ${action || 'tool'} ok`;
|
|
268
|
+
}
|
|
269
|
+
if (action && text) return `[${type}] ${action} - ${text}`;
|
|
270
|
+
if (action) return `[${type}] ${action}`;
|
|
271
|
+
if (text) return `[${type}] ${text}`;
|
|
272
|
+
return `[${type}]`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function streamAgentRun(baseUrl, payload, asJson = false) {
|
|
276
|
+
const res = await fetch(`${baseUrl}/agents/run`, {
|
|
277
|
+
method: 'POST',
|
|
278
|
+
headers: {
|
|
279
|
+
Accept: 'application/x-ndjson, application/json',
|
|
280
|
+
'Content-Type': 'application/json',
|
|
281
|
+
'x-bortex-progress': '1'
|
|
282
|
+
},
|
|
283
|
+
body: JSON.stringify({ ...payload, progress: true })
|
|
284
|
+
});
|
|
285
|
+
if (!res.ok) {
|
|
286
|
+
const text = await res.text();
|
|
287
|
+
throw new Error(`HTTP ${res.status}: ${text || res.statusText}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!res.body) {
|
|
291
|
+
throw new Error('Stream progress non disponibile.');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const reader = res.body.getReader();
|
|
295
|
+
const decoder = new TextDecoder();
|
|
296
|
+
let buffer = '';
|
|
297
|
+
let finalResult = null;
|
|
298
|
+
|
|
299
|
+
while (true) {
|
|
300
|
+
const { value, done } = await reader.read();
|
|
301
|
+
if (done) break;
|
|
302
|
+
buffer += decoder.decode(value, { stream: true });
|
|
303
|
+
let newlineIndex = buffer.indexOf('\n');
|
|
304
|
+
while (newlineIndex !== -1) {
|
|
305
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
306
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
307
|
+
if (line) {
|
|
308
|
+
const event = JSON.parse(line);
|
|
309
|
+
if (event.type === 'progress') {
|
|
310
|
+
const step = event?.data?.step || event?.data || {};
|
|
311
|
+
if (asJson) {
|
|
312
|
+
console.log(JSON.stringify(event));
|
|
313
|
+
} else {
|
|
314
|
+
console.log(formatProgressLine(step));
|
|
315
|
+
}
|
|
316
|
+
} else if (event.type === 'result') {
|
|
317
|
+
finalResult = event;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
newlineIndex = buffer.indexOf('\n');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!finalResult) {
|
|
325
|
+
throw new Error('Run agente terminata senza risultato finale.');
|
|
326
|
+
}
|
|
327
|
+
if (finalResult.status !== 'success') {
|
|
328
|
+
throw new Error(finalResult.error || 'Run agente fallita.');
|
|
329
|
+
}
|
|
330
|
+
return finalResult.data || null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function handleListCommand(options) {
|
|
334
|
+
const baseUrl = normalizeBaseUrl(options.url);
|
|
335
|
+
try {
|
|
336
|
+
if (options.waitForServer) await waitForServer(baseUrl);
|
|
337
|
+
const data = await requestJson('GET', `${baseUrl}/agents`);
|
|
338
|
+
printAgentList(Array.isArray(data?.agents) ? data.agents : [], options.json);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
const fallback = loadCatalogFallback().map((agent) => ({
|
|
341
|
+
id: agent?.id || '',
|
|
342
|
+
name: agent?.name || '',
|
|
343
|
+
mission: agent?.mission || '',
|
|
344
|
+
runtimeProfile: agent?.runtimeProfile || ''
|
|
345
|
+
}));
|
|
346
|
+
if (!fallback.length) {
|
|
347
|
+
throw new Error(`Catalogo agenti non disponibile da ${baseUrl} e nessun catalogo sincronizzato trovato in locale.`);
|
|
348
|
+
}
|
|
349
|
+
printAgentList(fallback, options.json);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function handleHealthCommand(options) {
|
|
354
|
+
const baseUrl = normalizeBaseUrl(options.url);
|
|
355
|
+
const data = await requestJson('GET', `${baseUrl}/health`);
|
|
356
|
+
if (options.json) {
|
|
357
|
+
console.log(JSON.stringify(data, null, 2));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
console.log(`Server Bortex online su ${baseUrl}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function handleRunCommand(options) {
|
|
364
|
+
const baseUrl = normalizeBaseUrl(options.url);
|
|
365
|
+
const runId = options.runId || `cli_${Date.now()}`;
|
|
366
|
+
if (options.waitForServer) {
|
|
367
|
+
await waitForServer(baseUrl);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const payload = { runId };
|
|
371
|
+
if (options.input) payload.userMessage = options.input;
|
|
372
|
+
if (options.outputs.length) payload.agentOutputs = options.outputs;
|
|
373
|
+
if (options.file) {
|
|
374
|
+
payload.agentConfig = loadManifestAgent(options.file);
|
|
375
|
+
} else if (options.id) {
|
|
376
|
+
payload.agentId = options.id;
|
|
377
|
+
} else if (options.name) {
|
|
378
|
+
payload.agentName = options.name;
|
|
379
|
+
} else {
|
|
380
|
+
throw new Error('Per run serve --id, --name oppure --file.');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let stopping = false;
|
|
384
|
+
const stopHandler = async () => {
|
|
385
|
+
if (stopping) return;
|
|
386
|
+
stopping = true;
|
|
387
|
+
try {
|
|
388
|
+
await requestJson('POST', `${baseUrl}/agents/stop`, { runId });
|
|
389
|
+
console.error(`Run ${runId} fermata.`);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.error(`Stop run ${runId} fallita: ${err.message}`);
|
|
392
|
+
} finally {
|
|
393
|
+
process.exit(130);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
process.once('SIGINT', stopHandler);
|
|
397
|
+
process.once('SIGTERM', stopHandler);
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const result = await streamAgentRun(baseUrl, payload, options.json);
|
|
401
|
+
if (options.json) {
|
|
402
|
+
console.log(JSON.stringify({ ok: true, runId, result }, null, 2));
|
|
403
|
+
} else {
|
|
404
|
+
console.log(`Run completata: ${runId}`);
|
|
405
|
+
}
|
|
406
|
+
} finally {
|
|
407
|
+
process.removeListener('SIGINT', stopHandler);
|
|
408
|
+
process.removeListener('SIGTERM', stopHandler);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function handleStopCommand(options) {
|
|
413
|
+
const baseUrl = normalizeBaseUrl(options.url);
|
|
414
|
+
if (options.waitForServer) {
|
|
415
|
+
await waitForServer(baseUrl);
|
|
416
|
+
}
|
|
417
|
+
const payload = options.stopAll ? {} : { runId: options.runId };
|
|
418
|
+
if (!options.stopAll && !payload.runId) {
|
|
419
|
+
throw new Error('Per stop serve --run-id oppure --all.');
|
|
420
|
+
}
|
|
421
|
+
const result = await requestJson('POST', `${baseUrl}/agents/stop`, payload);
|
|
422
|
+
if (options.json) {
|
|
423
|
+
console.log(JSON.stringify(result, null, 2));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (options.stopAll) {
|
|
427
|
+
console.log('Stop richiesto per tutte le run agent locali.');
|
|
428
|
+
} else {
|
|
429
|
+
console.log(`Stop richiesto per run ${options.runId}.`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function main() {
|
|
434
|
+
const options = parseArgs(process.argv.slice(2));
|
|
435
|
+
if (options.version) {
|
|
436
|
+
console.log(CLI_VERSION);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (options.help || !options.command || options.command === 'help') {
|
|
440
|
+
printUsage();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (options.command === 'list') {
|
|
444
|
+
await handleListCommand(options);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (options.command === 'health') {
|
|
448
|
+
await handleHealthCommand(options);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (options.command === 'run') {
|
|
452
|
+
await handleRunCommand(options);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (options.command === 'stop') {
|
|
456
|
+
await handleStopCommand(options);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
throw new Error(`Comando non supportato: ${options.command}`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (require.main === module) {
|
|
463
|
+
main().catch((err) => {
|
|
464
|
+
console.error(err?.message || err);
|
|
465
|
+
process.exit(1);
|
|
466
|
+
});
|
|
467
|
+
} else {
|
|
468
|
+
module.exports = {
|
|
469
|
+
parseArgs,
|
|
470
|
+
main
|
|
471
|
+
};
|
|
472
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const APPDATA_DIR_NAME = 'bortexcode';
|
|
8
|
+
const LEGACY_APPDATA_DIR_NAME = 'coding-agent-editor';
|
|
9
|
+
const SETTINGS_FILE_NAME = 'bortex-settings.json';
|
|
10
|
+
const AGENTS_CATALOG_FILE_NAME = 'agents-catalog.runtime-generic.v2.json';
|
|
11
|
+
|
|
12
|
+
// Cross-platform config root:
|
|
13
|
+
// Windows → %APPDATA%\bortexcode\
|
|
14
|
+
// macOS → ~/Library/Application Support/bortexcode/
|
|
15
|
+
// Linux → ~/.config/bortexcode/ (rispetta XDG_CONFIG_HOME)
|
|
16
|
+
function getAppDataRoot() {
|
|
17
|
+
if (process.platform === 'win32') {
|
|
18
|
+
return process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
19
|
+
}
|
|
20
|
+
if (process.platform === 'darwin') {
|
|
21
|
+
return path.join(os.homedir(), 'Library', 'Application Support');
|
|
22
|
+
}
|
|
23
|
+
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getLegacySharedDir() {
|
|
27
|
+
return path.join(getAppDataRoot(), LEGACY_APPDATA_DIR_NAME);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getPreferredSharedDir() {
|
|
31
|
+
return path.join(getAppDataRoot(), APPDATA_DIR_NAME);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveSharedDir() {
|
|
35
|
+
const preferredDir = getPreferredSharedDir();
|
|
36
|
+
const legacyDir = getLegacySharedDir();
|
|
37
|
+
try {
|
|
38
|
+
if (fs.existsSync(preferredDir)) return preferredDir;
|
|
39
|
+
} catch (_err) { }
|
|
40
|
+
try {
|
|
41
|
+
if (fs.existsSync(legacyDir)) return legacyDir;
|
|
42
|
+
} catch (_err) { }
|
|
43
|
+
return preferredDir;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getSharedSettingsPath() {
|
|
47
|
+
return path.join(resolveSharedDir(), SETTINGS_FILE_NAME);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getAgentsCatalogFallbackPath() {
|
|
51
|
+
return path.join(resolveSharedDir(), AGENTS_CATALOG_FILE_NAME);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
APPDATA_DIR_NAME,
|
|
56
|
+
LEGACY_APPDATA_DIR_NAME,
|
|
57
|
+
SETTINGS_FILE_NAME,
|
|
58
|
+
AGENTS_CATALOG_FILE_NAME,
|
|
59
|
+
getAppDataRoot,
|
|
60
|
+
getLegacySharedDir,
|
|
61
|
+
getPreferredSharedDir,
|
|
62
|
+
resolveSharedDir,
|
|
63
|
+
getSharedSettingsPath,
|
|
64
|
+
getAgentsCatalogFallbackPath
|
|
65
|
+
};
|