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 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
+ };