bortexcode 1.2.6 → 1.2.9
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 +36 -0
- package/bin/bortex.js +644 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,8 @@ export BORTEX_API_KEY=<YOUR_API_KEY>
|
|
|
47
47
|
bortexcode
|
|
48
48
|
bortexcode "explain this function"
|
|
49
49
|
bortexcode --agent "refactor src/utils.js"
|
|
50
|
+
bortexcode remote-control
|
|
51
|
+
bortexcode --remote-control
|
|
50
52
|
```
|
|
51
53
|
|
|
52
54
|
## REPL Commands
|
|
@@ -63,9 +65,36 @@ Common commands:
|
|
|
63
65
|
/help
|
|
64
66
|
/llm-config show
|
|
65
67
|
/llm-config sync
|
|
68
|
+
/remote-control [name]
|
|
69
|
+
/remote-control --lan
|
|
70
|
+
/rc
|
|
66
71
|
/exit
|
|
67
72
|
```
|
|
68
73
|
|
|
74
|
+
## Remote Control
|
|
75
|
+
|
|
76
|
+
Remote Control exposes the current Bortex Code process through a token-protected
|
|
77
|
+
browser UI. By default it binds to `127.0.0.1`.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
bortexcode remote-control --name "My Project"
|
|
81
|
+
bortexcode --remote-control "My Project"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For phone access on the same LAN:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
bortexcode remote-control --remote-lan
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Inside the REPL:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
/remote-control My Project
|
|
94
|
+
/remote-control --lan
|
|
95
|
+
/remote-control stop
|
|
96
|
+
```
|
|
97
|
+
|
|
69
98
|
## Options
|
|
70
99
|
|
|
71
100
|
```text
|
|
@@ -84,6 +113,11 @@ Common commands:
|
|
|
84
113
|
--check-update Check for updates now
|
|
85
114
|
--no-update-check
|
|
86
115
|
Disable startup update check
|
|
116
|
+
--remote-control, --rc [name]
|
|
117
|
+
Enable browser remote control
|
|
118
|
+
--remote-lan Bind remote control to 0.0.0.0 for LAN/mobile access
|
|
119
|
+
--remote-host <host>, --remote-port <port>
|
|
120
|
+
Remote control bind address
|
|
87
121
|
-v, --version Show version
|
|
88
122
|
-h, --help Show help
|
|
89
123
|
```
|
|
@@ -105,4 +139,6 @@ BORTEX_URL
|
|
|
105
139
|
BORTEX_API_KEY
|
|
106
140
|
BORTEX_CLI_ICONS=1
|
|
107
141
|
BORTEX_NO_UPDATE_CHECK=1
|
|
142
|
+
BORTEX_REMOTE_HOST
|
|
143
|
+
BORTEX_REMOTE_PORT
|
|
108
144
|
```
|
package/bin/bortex.js
CHANGED
|
@@ -31,6 +31,11 @@ function parseArgs(argv) {
|
|
|
31
31
|
_modelExplicit: false,
|
|
32
32
|
_apiUrlExplicit: false,
|
|
33
33
|
_apiKeyExplicit: false,
|
|
34
|
+
remoteControl: false,
|
|
35
|
+
remoteControlServerMode: false,
|
|
36
|
+
remoteControlName: '',
|
|
37
|
+
remoteControlHost: process.env.BORTEX_REMOTE_HOST || '127.0.0.1',
|
|
38
|
+
remoteControlPort: Number(process.env.BORTEX_REMOTE_PORT || 0) || 0,
|
|
34
39
|
ux: {
|
|
35
40
|
verbose: false,
|
|
36
41
|
spinner: true,
|
|
@@ -42,6 +47,10 @@ function parseArgs(argv) {
|
|
|
42
47
|
const rest = [];
|
|
43
48
|
for (let i = 0; i < argv.length; i += 1) {
|
|
44
49
|
const a = argv[i];
|
|
50
|
+
if (a === 'remote-control' || a === 'remote') {
|
|
51
|
+
opts.remoteControlServerMode = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
45
54
|
if (a === '--agent' || a === '-a') {
|
|
46
55
|
opts.agent = true;
|
|
47
56
|
continue;
|
|
@@ -128,6 +137,46 @@ function parseArgs(argv) {
|
|
|
128
137
|
opts.forceUpdateCheck = true;
|
|
129
138
|
continue;
|
|
130
139
|
}
|
|
140
|
+
if (a === '--remote-control' || a === '--rc') {
|
|
141
|
+
opts.remoteControl = true;
|
|
142
|
+
if (argv[i + 1] && !String(argv[i + 1]).startsWith('-')) {
|
|
143
|
+
opts.remoteControlName = argv[i + 1];
|
|
144
|
+
i += 1;
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (a.startsWith('--remote-control=')) {
|
|
149
|
+
opts.remoteControl = true;
|
|
150
|
+
opts.remoteControlName = a.slice('--remote-control='.length);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (a === '--remote-lan') {
|
|
154
|
+
opts.remoteControlHost = '0.0.0.0';
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if ((a === '--remote-host' || a === '--rc-host') && argv[i + 1]) {
|
|
158
|
+
opts.remoteControlHost = argv[i + 1];
|
|
159
|
+
i += 1;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (a.startsWith('--remote-host=')) {
|
|
163
|
+
opts.remoteControlHost = a.slice('--remote-host='.length);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if ((a === '--remote-port' || a === '--rc-port') && argv[i + 1]) {
|
|
167
|
+
opts.remoteControlPort = Number(argv[i + 1]) || 0;
|
|
168
|
+
i += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (a.startsWith('--remote-port=')) {
|
|
172
|
+
opts.remoteControlPort = Number(a.slice('--remote-port='.length)) || 0;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if ((a === '--name' || a === '--remote-name') && argv[i + 1]) {
|
|
176
|
+
opts.remoteControlName = argv[i + 1];
|
|
177
|
+
i += 1;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
131
180
|
if (a === '--login' || a === '--signin') {
|
|
132
181
|
opts.login = true;
|
|
133
182
|
continue;
|
|
@@ -147,6 +196,7 @@ function usage() {
|
|
|
147
196
|
console.log('');
|
|
148
197
|
console.log('Usage:');
|
|
149
198
|
console.log(` ${cliName} [options] [prompt]`);
|
|
199
|
+
console.log(` ${cliName} remote-control [--name <title>] [--remote-lan]`);
|
|
150
200
|
console.log(` ${cliName} --api-key <apikey>`);
|
|
151
201
|
console.log(` ${cliName} "write a python function"`);
|
|
152
202
|
console.log('');
|
|
@@ -166,10 +216,15 @@ function usage() {
|
|
|
166
216
|
console.log(' --check-update Check for updates now');
|
|
167
217
|
console.log(' --no-update-check');
|
|
168
218
|
console.log(' Disable startup update check');
|
|
219
|
+
console.log(' --remote-control, --rc [name]');
|
|
220
|
+
console.log(' Enable browser remote control for this session');
|
|
221
|
+
console.log(' --remote-lan Bind remote control to 0.0.0.0 for LAN/mobile access');
|
|
222
|
+
console.log(' --remote-host <host>, --remote-port <port>');
|
|
223
|
+
console.log(' Remote control bind address');
|
|
169
224
|
console.log(' -v, --version Show version');
|
|
170
225
|
console.log(' -h, --help Show help');
|
|
171
226
|
console.log('');
|
|
172
|
-
console.log('REPL commands: /agent on|off, /status, /exit, /help, /llm-config');
|
|
227
|
+
console.log('REPL commands: /agent on|off, /status, /remote-control, /exit, /help, /llm-config');
|
|
173
228
|
console.log('Local tools: /pwd, /cd, /ls, /tree, /read, /write, /append, /mkdir, /git, /ssh-status, /sys-status, /process-status, /port-status, /tool, /sh');
|
|
174
229
|
console.log('');
|
|
175
230
|
console.log('Environment:');
|
|
@@ -1209,6 +1264,20 @@ function extractNaturalFileContent(text, filePath) {
|
|
|
1209
1264
|
return stripMatchingQuotes(content);
|
|
1210
1265
|
}
|
|
1211
1266
|
|
|
1267
|
+
function extractNaturalReplaceParts(text) {
|
|
1268
|
+
const raw = String(text || '').trim();
|
|
1269
|
+
const quotedPair = raw.match(/(?:sostituisci|rimpiazza|replace)\s+(["'`])([\s\S]*?)\1\s+(?:con|with)\s+(["'`])([\s\S]*?)\3/i);
|
|
1270
|
+
if (quotedPair) {
|
|
1271
|
+
return { search: quotedPair[2], replacement: quotedPair[4] };
|
|
1272
|
+
}
|
|
1273
|
+
const plainPair = raw.match(/(?:sostituisci|rimpiazza|replace)\s+([\s\S]+?)\s+(?:con|with)\s+([\s\S]+?)(?:\s+(?:nel|nella|in|su)\s+(?:file|path|percorso)?\s|$)/i);
|
|
1274
|
+
if (!plainPair) return null;
|
|
1275
|
+
const search = stripMatchingQuotes(plainPair[1]);
|
|
1276
|
+
const replacement = stripMatchingQuotes(plainPair[2]);
|
|
1277
|
+
if (!search) return null;
|
|
1278
|
+
return { search, replacement };
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1212
1281
|
function buildNaturalLocalFileActionCalls(text) {
|
|
1213
1282
|
const raw = String(text || '').trim();
|
|
1214
1283
|
const lower = raw.toLowerCase();
|
|
@@ -1216,6 +1285,18 @@ function buildNaturalLocalFileActionCalls(text) {
|
|
|
1216
1285
|
const filePath = extractNaturalFilePath(raw);
|
|
1217
1286
|
if (!filePath) return null;
|
|
1218
1287
|
|
|
1288
|
+
const replaceParts = extractNaturalReplaceParts(raw);
|
|
1289
|
+
if (replaceParts) {
|
|
1290
|
+
return [{
|
|
1291
|
+
tool: 'replace',
|
|
1292
|
+
path: filePath,
|
|
1293
|
+
search: replaceParts.search,
|
|
1294
|
+
replacement: replaceParts.replacement,
|
|
1295
|
+
all: true,
|
|
1296
|
+
confirm: true
|
|
1297
|
+
}];
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1219
1300
|
if (/(?:^|\b)(leggi|mostra|apri|read|show|cat)(?:\b|$)/i.test(raw) && /(?:file|path|percorso|\/|\\|\.\.?[\\/])/.test(raw)) {
|
|
1220
1301
|
return [{ tool: 'read', path: filePath }];
|
|
1221
1302
|
}
|
|
@@ -2723,6 +2804,9 @@ async function executeStructuredBuiltinTool(opts, call) {
|
|
|
2723
2804
|
error: res.ok ? null : `${label} exit ${res.code == null ? 'null' : res.code}`
|
|
2724
2805
|
};
|
|
2725
2806
|
};
|
|
2807
|
+
if (tool === 'applyPatch') {
|
|
2808
|
+
return runUnifiedPatchTool(opts, call);
|
|
2809
|
+
}
|
|
2726
2810
|
if (tool === 'read') {
|
|
2727
2811
|
const filePath = resolveCliPath(opts, call.path);
|
|
2728
2812
|
try {
|
|
@@ -2767,6 +2851,39 @@ async function executeStructuredBuiltinTool(opts, call) {
|
|
|
2767
2851
|
return { ok: false, stdout: null, stderr: err.message, data: { path: filePath, mode: tool }, error: err.message };
|
|
2768
2852
|
}
|
|
2769
2853
|
}
|
|
2854
|
+
if (tool === 'replace') {
|
|
2855
|
+
const filePath = resolveCliPath(opts, call.path);
|
|
2856
|
+
const search = String(call.search ?? '');
|
|
2857
|
+
const replacement = String(call.replacement ?? '');
|
|
2858
|
+
const replaceAll = call.all !== false;
|
|
2859
|
+
try {
|
|
2860
|
+
if (!search) {
|
|
2861
|
+
return { ok: false, stdout: null, stderr: 'empty search string', data: { path: filePath }, error: 'empty search string' };
|
|
2862
|
+
}
|
|
2863
|
+
const before = fs.readFileSync(filePath, 'utf8');
|
|
2864
|
+
const count = before.split(search).length - 1;
|
|
2865
|
+
if (count <= 0) {
|
|
2866
|
+
const msg = `Replace no match -> ${filePath}`;
|
|
2867
|
+
console.log(msg);
|
|
2868
|
+
return { ok: false, stdout: `${msg}\n`, stderr: 'no matches', data: { path: filePath, search, count: 0 }, error: 'no matches' };
|
|
2869
|
+
}
|
|
2870
|
+
const after = replaceAll
|
|
2871
|
+
? before.split(search).join(replacement)
|
|
2872
|
+
: before.replace(search, replacement);
|
|
2873
|
+
fs.writeFileSync(filePath, after, 'utf8');
|
|
2874
|
+
const applied = replaceAll ? count : 1;
|
|
2875
|
+
const msg = `Replace ok -> ${filePath} (${applied} occurrence${applied === 1 ? '' : 's'})`;
|
|
2876
|
+
console.log(msg);
|
|
2877
|
+
return {
|
|
2878
|
+
ok: true,
|
|
2879
|
+
stdout: `${msg}\n`,
|
|
2880
|
+
stderr: null,
|
|
2881
|
+
data: { path: filePath, search, replacement, count: applied, all: replaceAll, bytes: Buffer.byteLength(after, 'utf8') }
|
|
2882
|
+
};
|
|
2883
|
+
} catch (err) {
|
|
2884
|
+
return { ok: false, stdout: null, stderr: err.message, data: { path: filePath, search }, error: err.message };
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2770
2887
|
if (tool === 'gitStatus') {
|
|
2771
2888
|
const res = await runChild('git', ['status', '--short', ...(call.branch ? ['-b'] : [])], { cwd: opts.cwd, shell: false });
|
|
2772
2889
|
const entries = parseGitStatusShortEntries(res.stdout || '');
|
|
@@ -3023,6 +3140,19 @@ function validateStructuredToolCall(raw) {
|
|
|
3023
3140
|
if (typeof call.text !== 'string') return { ok: false, error: 'Campo `text` mancante o non valido.' };
|
|
3024
3141
|
break;
|
|
3025
3142
|
}
|
|
3143
|
+
case 'replace': {
|
|
3144
|
+
const e1 = requireString('path');
|
|
3145
|
+
if (e1) return { ok: false, error: e1 };
|
|
3146
|
+
const e2 = requireString('search');
|
|
3147
|
+
if (e2) return { ok: false, error: e2 };
|
|
3148
|
+
if (typeof call.replacement !== 'string') return { ok: false, error: 'Campo `replacement` mancante o non valido.' };
|
|
3149
|
+
if (call.all != null && typeof call.all !== 'boolean') return { ok: false, error: '`all` deve essere boolean.' };
|
|
3150
|
+
break;
|
|
3151
|
+
}
|
|
3152
|
+
case 'applyPatch':
|
|
3153
|
+
if (typeof call.patch !== 'string' || !call.patch.trim()) return { ok: false, error: 'Campo `patch` mancante o non valido.' };
|
|
3154
|
+
if (call.checkOnly != null && typeof call.checkOnly !== 'boolean') return { ok: false, error: '`checkOnly` deve essere boolean.' };
|
|
3155
|
+
break;
|
|
3026
3156
|
case 'diff':
|
|
3027
3157
|
case 'gitDiff':
|
|
3028
3158
|
if (call.mode && !['unstaged', 'staged', 'all'].includes(String(call.mode))) {
|
|
@@ -3084,6 +3214,8 @@ function structuredToolCallToLocalCommand(call) {
|
|
|
3084
3214
|
case 'mkdir': return `/mkdir ${call.path || '.'}`;
|
|
3085
3215
|
case 'write': return `/write ${call.path} ${call.text}`;
|
|
3086
3216
|
case 'append': return `/append ${call.path} ${call.text}`;
|
|
3217
|
+
case 'replace': return `replace(${call.path}: ${call.search} -> ${call.replacement}${call.all === false ? ', first' : ', all'})`;
|
|
3218
|
+
case 'applyPatch': return `applyPatch(${call.checkOnly ? 'check' : 'apply'}, ${String(call.patch || '').length} chars)`;
|
|
3087
3219
|
case 'diff': return `/diff ${call.mode || 'unstaged'}`;
|
|
3088
3220
|
case 'gitDiff': return `gitDiff(${call.mode || 'all'}${call.statOnly === false ? ',patch' : ',stat'})`;
|
|
3089
3221
|
case 'search': return `search(${call.pattern}${call.path ? ` in ${call.path}` : ''}${call.glob ? ` glob=${call.glob}` : ''})`;
|
|
@@ -3107,7 +3239,8 @@ function structuredToolCallToLocalCommand(call) {
|
|
|
3107
3239
|
|
|
3108
3240
|
function isRiskyStructuredToolCall(call) {
|
|
3109
3241
|
const t = String(call.tool || '');
|
|
3110
|
-
if (['write', 'append', 'mkdir', 'sh', 'runTest', 'runBuild', 'runLint'].includes(t)) return true;
|
|
3242
|
+
if (['write', 'append', 'replace', 'mkdir', 'sh', 'runTest', 'runBuild', 'runLint'].includes(t)) return true;
|
|
3243
|
+
if (t === 'applyPatch') return call.checkOnly !== true;
|
|
3111
3244
|
if (t === 'git') return !isReadonlyGitArgs(call.args);
|
|
3112
3245
|
return false;
|
|
3113
3246
|
}
|
|
@@ -3484,7 +3617,7 @@ async function buildStructuredToolPlanWithLlm(opts, goal, llmOptions = {}) {
|
|
|
3484
3617
|
|
|
3485
3618
|
const profile = detectWorkspaceProfile(opts);
|
|
3486
3619
|
const supportedTools = [
|
|
3487
|
-
'project', 'pwd', 'ls', 'tree', 'glob', 'read', 'readMany', 'mkdir', 'write', 'append',
|
|
3620
|
+
'project', 'pwd', 'ls', 'tree', 'glob', 'read', 'readMany', 'mkdir', 'write', 'append', 'replace', 'applyPatch',
|
|
3488
3621
|
'diff', 'git', 'gitStatus', 'gitDiff', 'search', 'runTest', 'runBuild', 'runLint', 'sshStatus', 'systemStatus', 'processStatus', 'portStatus', 'sh', 'review', 'commitSuggest'
|
|
3489
3622
|
];
|
|
3490
3623
|
const testCmd = profile.suggested.test?.[0] || '';
|
|
@@ -3577,6 +3710,8 @@ async function buildStructuredToolPlanWithLlm(opts, goal, llmOptions = {}) {
|
|
|
3577
3710
|
'Regole:',
|
|
3578
3711
|
'- Preferisci tool read-only prima dei tool rischiosi',
|
|
3579
3712
|
'- Per controlli di stato locale usa sshStatus/systemStatus/processStatus/portStatus invece di sh quando possibile',
|
|
3713
|
+
'- Per modifiche testuali puntuali usa replace invece di sh',
|
|
3714
|
+
'- Per modifiche multi-file o blocchi ampi usa applyPatch con una unified diff valida; usa checkOnly:true se devi solo validare',
|
|
3580
3715
|
'- Includi review e commitSuggest verso la fine',
|
|
3581
3716
|
'- Se proponi `runTest`/`runBuild`/`runLint` preferiscili a `sh` quando possibile',
|
|
3582
3717
|
'- Se proponi `sh`, usa comandi test/build/lint concreti se disponibili',
|
|
@@ -3794,6 +3929,8 @@ function printLocalHelp() {
|
|
|
3794
3929
|
console.log(' /sys-status check system / runtime status');
|
|
3795
3930
|
console.log(' /process-status check running processes');
|
|
3796
3931
|
console.log(' /port-status check listening ports');
|
|
3932
|
+
console.log(' /remote-control [name] [--lan|--host <host>|--port <port>]');
|
|
3933
|
+
console.log(' /remote-control stop');
|
|
3797
3934
|
console.log(' /diff [unstaged|staged|all]');
|
|
3798
3935
|
console.log(' /stage <file>|--all');
|
|
3799
3936
|
console.log(' /unstage <file>|--all');
|
|
@@ -3803,6 +3940,7 @@ function printLocalHelp() {
|
|
|
3803
3940
|
console.log(' /patch-apply <patch-file>');
|
|
3804
3941
|
console.log(' /patch-check-inline incolla patch e valida (.end per terminare)');
|
|
3805
3942
|
console.log(' /patch-apply-inline incolla patch e applica (.end per terminare)');
|
|
3943
|
+
console.log(' /tool {"tool":"applyPatch","patch":"...","checkOnly":true|false}');
|
|
3806
3944
|
console.log(' /todo add|list|done|rm|clear ...');
|
|
3807
3945
|
console.log(' /plan [goal]');
|
|
3808
3946
|
console.log(' /agent-local <goal> planner locale rapido');
|
|
@@ -3934,6 +4072,75 @@ function printPlan(opts) {
|
|
|
3934
4072
|
(opts.plan.steps || []).forEach((s, i) => console.log(` ${i + 1}. ${s}`));
|
|
3935
4073
|
}
|
|
3936
4074
|
|
|
4075
|
+
async function runUnifiedPatchTool(opts, call) {
|
|
4076
|
+
const patch = String(call.patch || '');
|
|
4077
|
+
const checkOnly = call.checkOnly === true;
|
|
4078
|
+
const data = {
|
|
4079
|
+
cwd: opts.cwd,
|
|
4080
|
+
patchChars: patch.length,
|
|
4081
|
+
checkOnly,
|
|
4082
|
+
checkOk: false,
|
|
4083
|
+
applied: false
|
|
4084
|
+
};
|
|
4085
|
+
if (!patch.trim()) {
|
|
4086
|
+
return { ok: false, stdout: null, stderr: 'empty patch', data, error: 'empty patch' };
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
const tmp = path.join(os.tmpdir(), `bortex-tool-${Date.now()}-${Math.random().toString(16).slice(2)}.patch`);
|
|
4090
|
+
fs.writeFileSync(tmp, patch, 'utf8');
|
|
4091
|
+
try {
|
|
4092
|
+
const check = await runChild('git', ['apply', '--check', '--verbose', tmp], { cwd: opts.cwd, shell: false });
|
|
4093
|
+
const checkStdout = String(check.stdout || '');
|
|
4094
|
+
const checkStderr = String(check.stderr || '');
|
|
4095
|
+
if (checkStdout) process.stdout.write(checkStdout.endsWith('\n') ? checkStdout : `${checkStdout}\n`);
|
|
4096
|
+
if (checkStderr) process.stderr.write(checkStderr.endsWith('\n') ? checkStderr : `${checkStderr}\n`);
|
|
4097
|
+
data.checkOk = !!check.ok;
|
|
4098
|
+
data.checkExitCode = check.code == null ? null : check.code;
|
|
4099
|
+
|
|
4100
|
+
if (!check.ok) {
|
|
4101
|
+
const msg = 'Patch check failed';
|
|
4102
|
+
console.log(msg);
|
|
4103
|
+
return {
|
|
4104
|
+
ok: false,
|
|
4105
|
+
stdout: checkStdout || null,
|
|
4106
|
+
stderr: checkStderr || msg,
|
|
4107
|
+
data,
|
|
4108
|
+
error: msg
|
|
4109
|
+
};
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
if (checkOnly) {
|
|
4113
|
+
const msg = 'Patch check ok';
|
|
4114
|
+
console.log(msg);
|
|
4115
|
+
return {
|
|
4116
|
+
ok: true,
|
|
4117
|
+
stdout: [checkStdout, `${msg}\n`].filter(Boolean).join(''),
|
|
4118
|
+
stderr: checkStderr || null,
|
|
4119
|
+
data
|
|
4120
|
+
};
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
const applied = await runChild('git', ['apply', '--verbose', '--whitespace=nowarn', tmp], { cwd: opts.cwd, shell: false });
|
|
4124
|
+
const applyStdout = String(applied.stdout || '');
|
|
4125
|
+
const applyStderr = String(applied.stderr || '');
|
|
4126
|
+
if (applyStdout) process.stdout.write(applyStdout.endsWith('\n') ? applyStdout : `${applyStdout}\n`);
|
|
4127
|
+
if (applyStderr) process.stderr.write(applyStderr.endsWith('\n') ? applyStderr : `${applyStderr}\n`);
|
|
4128
|
+
data.applied = !!applied.ok;
|
|
4129
|
+
data.applyExitCode = applied.code == null ? null : applied.code;
|
|
4130
|
+
const msg = applied.ok ? 'Patch apply ok' : 'Patch apply failed';
|
|
4131
|
+
console.log(msg);
|
|
4132
|
+
return {
|
|
4133
|
+
ok: !!applied.ok,
|
|
4134
|
+
stdout: [checkStdout, applyStdout, `${msg}\n`].filter(Boolean).join(''),
|
|
4135
|
+
stderr: [checkStderr, applyStderr].filter(Boolean).join('\n') || null,
|
|
4136
|
+
data,
|
|
4137
|
+
error: applied.ok ? null : msg
|
|
4138
|
+
};
|
|
4139
|
+
} finally {
|
|
4140
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
|
|
3937
4144
|
async function runGitApplyInline(opts, mode) {
|
|
3938
4145
|
if (typeof opts._readMultiline !== 'function') {
|
|
3939
4146
|
throw new Error('Patch inline disponibile solo in REPL interattiva.');
|
|
@@ -3957,6 +4164,379 @@ async function runGitApplyInline(opts, mode) {
|
|
|
3957
4164
|
}
|
|
3958
4165
|
}
|
|
3959
4166
|
|
|
4167
|
+
function escapeHtml(value) {
|
|
4168
|
+
return String(value ?? '')
|
|
4169
|
+
.replace(/&/g, '&')
|
|
4170
|
+
.replace(/</g, '<')
|
|
4171
|
+
.replace(/>/g, '>')
|
|
4172
|
+
.replace(/"/g, '"')
|
|
4173
|
+
.replace(/'/g, ''');
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
function getRemoteLanAddresses(port, token) {
|
|
4177
|
+
const out = [];
|
|
4178
|
+
const nets = os.networkInterfaces();
|
|
4179
|
+
Object.values(nets).forEach((entries) => {
|
|
4180
|
+
(entries || []).forEach((entry) => {
|
|
4181
|
+
if (!entry || entry.internal || entry.family !== 'IPv4') return;
|
|
4182
|
+
out.push(`http://${entry.address}:${port}/?token=${encodeURIComponent(token)}`);
|
|
4183
|
+
});
|
|
4184
|
+
});
|
|
4185
|
+
return out;
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
function sendJson(res, statusCode, payload) {
|
|
4189
|
+
const body = JSON.stringify(payload);
|
|
4190
|
+
res.writeHead(statusCode, {
|
|
4191
|
+
'content-type': 'application/json; charset=utf-8',
|
|
4192
|
+
'cache-control': 'no-store',
|
|
4193
|
+
'content-length': Buffer.byteLength(body)
|
|
4194
|
+
});
|
|
4195
|
+
res.end(body);
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
function readHttpBody(req, maxBytes = 1024 * 1024) {
|
|
4199
|
+
return new Promise((resolve, reject) => {
|
|
4200
|
+
let size = 0;
|
|
4201
|
+
const chunks = [];
|
|
4202
|
+
req.on('data', (chunk) => {
|
|
4203
|
+
size += chunk.length;
|
|
4204
|
+
if (size > maxBytes) {
|
|
4205
|
+
reject(new Error('request body too large'));
|
|
4206
|
+
req.destroy();
|
|
4207
|
+
return;
|
|
4208
|
+
}
|
|
4209
|
+
chunks.push(chunk);
|
|
4210
|
+
});
|
|
4211
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
4212
|
+
req.on('error', reject);
|
|
4213
|
+
});
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
function buildRemoteControlHtml(state) {
|
|
4217
|
+
const title = escapeHtml(state.name || 'Bortex Remote Control');
|
|
4218
|
+
const token = escapeHtml(state.token);
|
|
4219
|
+
const cwd = escapeHtml(state.cwd || process.cwd());
|
|
4220
|
+
return `<!doctype html>
|
|
4221
|
+
<html lang="en">
|
|
4222
|
+
<head>
|
|
4223
|
+
<meta charset="utf-8">
|
|
4224
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
4225
|
+
<title>${title}</title>
|
|
4226
|
+
<style>
|
|
4227
|
+
:root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
4228
|
+
body { margin: 0; background: #0f1115; color: #f4f7fb; }
|
|
4229
|
+
main { max-width: 980px; margin: 0 auto; padding: 20px; }
|
|
4230
|
+
header { display: flex; justify-content: space-between; gap: 16px; align-items: flex-start; border-bottom: 1px solid #2a2f3a; padding-bottom: 14px; }
|
|
4231
|
+
h1 { font-size: 20px; margin: 0 0 6px; font-weight: 700; }
|
|
4232
|
+
.meta { color: #9aa4b2; font-size: 13px; overflow-wrap: anywhere; }
|
|
4233
|
+
.pill { border: 1px solid #334155; border-radius: 999px; padding: 6px 10px; color: #cbd5e1; font-size: 12px; white-space: nowrap; }
|
|
4234
|
+
#log { margin: 18px 0; background: #080a0f; border: 1px solid #242a36; border-radius: 8px; min-height: 45vh; max-height: 62vh; overflow: auto; padding: 12px; }
|
|
4235
|
+
.msg { border-bottom: 1px solid #151923; padding: 10px 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
4236
|
+
.role { color: #8ab4ff; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; margin-bottom: 4px; }
|
|
4237
|
+
.assistant .role { color: #78d7a4; }
|
|
4238
|
+
.system .role { color: #fbbf24; }
|
|
4239
|
+
textarea { width: 100%; min-height: 92px; resize: vertical; border-radius: 8px; border: 1px solid #334155; background: #10141d; color: #f8fafc; padding: 12px; font: 14px/1.45 ui-monospace, SFMono-Regular, Consolas, monospace; box-sizing: border-box; }
|
|
4240
|
+
.bar { display: flex; gap: 10px; margin-top: 10px; align-items: center; }
|
|
4241
|
+
button { border: 0; border-radius: 8px; background: #2563eb; color: white; font-weight: 650; padding: 10px 14px; cursor: pointer; }
|
|
4242
|
+
button:disabled { opacity: .55; cursor: not-allowed; }
|
|
4243
|
+
.hint { color: #94a3b8; font-size: 12px; }
|
|
4244
|
+
code { color: #bfdbfe; }
|
|
4245
|
+
</style>
|
|
4246
|
+
</head>
|
|
4247
|
+
<body>
|
|
4248
|
+
<main>
|
|
4249
|
+
<header>
|
|
4250
|
+
<div>
|
|
4251
|
+
<h1>${title}</h1>
|
|
4252
|
+
<div class="meta">cwd: <code>${cwd}</code></div>
|
|
4253
|
+
</div>
|
|
4254
|
+
<div id="status" class="pill">connecting</div>
|
|
4255
|
+
</header>
|
|
4256
|
+
<section id="log"></section>
|
|
4257
|
+
<form id="form">
|
|
4258
|
+
<textarea id="prompt" placeholder="Ask Bortex Code or run a slash command, e.g. /status, /tool-plan --run fix lint"></textarea>
|
|
4259
|
+
<div class="bar">
|
|
4260
|
+
<button id="send" type="submit">Send</button>
|
|
4261
|
+
<span class="hint">Token-protected local session. Keep this URL private.</span>
|
|
4262
|
+
</div>
|
|
4263
|
+
</form>
|
|
4264
|
+
</main>
|
|
4265
|
+
<script>
|
|
4266
|
+
const token = ${JSON.stringify(state.token)};
|
|
4267
|
+
const logEl = document.getElementById('log');
|
|
4268
|
+
const statusEl = document.getElementById('status');
|
|
4269
|
+
const promptEl = document.getElementById('prompt');
|
|
4270
|
+
const sendEl = document.getElementById('send');
|
|
4271
|
+
let lastCount = -1;
|
|
4272
|
+
function renderLog(items) {
|
|
4273
|
+
if (!Array.isArray(items) || items.length === lastCount) return;
|
|
4274
|
+
lastCount = items.length;
|
|
4275
|
+
logEl.innerHTML = items.map((item) => {
|
|
4276
|
+
const role = String(item.role || 'system');
|
|
4277
|
+
const text = String(item.text || '');
|
|
4278
|
+
return '<div class="msg ' + role.replace(/[^a-z0-9_-]/gi, '') + '"><div class="role">' + role + '</div><div>' +
|
|
4279
|
+
text.replace(/[&<>"']/g, (c) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])) +
|
|
4280
|
+
'</div></div>';
|
|
4281
|
+
}).join('');
|
|
4282
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
4283
|
+
}
|
|
4284
|
+
async function refresh() {
|
|
4285
|
+
try {
|
|
4286
|
+
const r = await fetch('/api/status?token=' + encodeURIComponent(token), { cache: 'no-store' });
|
|
4287
|
+
const data = await r.json();
|
|
4288
|
+
statusEl.textContent = data.busy ? 'running' : 'connected';
|
|
4289
|
+
sendEl.disabled = !!data.busy;
|
|
4290
|
+
renderLog(data.log || []);
|
|
4291
|
+
} catch (err) {
|
|
4292
|
+
statusEl.textContent = 'disconnected';
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
document.getElementById('form').addEventListener('submit', async (event) => {
|
|
4296
|
+
event.preventDefault();
|
|
4297
|
+
const text = promptEl.value.trim();
|
|
4298
|
+
if (!text) return;
|
|
4299
|
+
sendEl.disabled = true;
|
|
4300
|
+
await fetch('/api/prompt?token=' + encodeURIComponent(token), {
|
|
4301
|
+
method: 'POST',
|
|
4302
|
+
headers: { 'content-type': 'application/json' },
|
|
4303
|
+
body: JSON.stringify({ text })
|
|
4304
|
+
}).catch(() => {});
|
|
4305
|
+
promptEl.value = '';
|
|
4306
|
+
await refresh();
|
|
4307
|
+
});
|
|
4308
|
+
promptEl.addEventListener('keydown', (event) => {
|
|
4309
|
+
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
|
4310
|
+
document.getElementById('form').requestSubmit();
|
|
4311
|
+
}
|
|
4312
|
+
});
|
|
4313
|
+
refresh();
|
|
4314
|
+
setInterval(refresh, 1200);
|
|
4315
|
+
</script>
|
|
4316
|
+
</body>
|
|
4317
|
+
</html>`;
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4320
|
+
async function captureRemoteControlOutput(fn) {
|
|
4321
|
+
let stdout = '';
|
|
4322
|
+
let stderr = '';
|
|
4323
|
+
const originalStdoutWrite = process.stdout.write;
|
|
4324
|
+
const originalStderrWrite = process.stderr.write;
|
|
4325
|
+
process.stdout.write = function patchedStdoutWrite(chunk, encoding, cb) {
|
|
4326
|
+
stdout += Buffer.isBuffer(chunk) ? chunk.toString(typeof encoding === 'string' ? encoding : 'utf8') : String(chunk ?? '');
|
|
4327
|
+
return originalStdoutWrite.apply(process.stdout, arguments);
|
|
4328
|
+
};
|
|
4329
|
+
process.stderr.write = function patchedStderrWrite(chunk, encoding, cb) {
|
|
4330
|
+
stderr += Buffer.isBuffer(chunk) ? chunk.toString(typeof encoding === 'string' ? encoding : 'utf8') : String(chunk ?? '');
|
|
4331
|
+
return originalStderrWrite.apply(process.stderr, arguments);
|
|
4332
|
+
};
|
|
4333
|
+
try {
|
|
4334
|
+
await fn();
|
|
4335
|
+
} finally {
|
|
4336
|
+
process.stdout.write = originalStdoutWrite;
|
|
4337
|
+
process.stderr.write = originalStderrWrite;
|
|
4338
|
+
}
|
|
4339
|
+
return { stdout, stderr };
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
async function runRemoteControlPrompt(opts, text) {
|
|
4343
|
+
const line = String(text || '').trim();
|
|
4344
|
+
if (!line) return { ok: false, output: 'Empty prompt.' };
|
|
4345
|
+
pushCliHistory(opts, `[remote] ${line}`);
|
|
4346
|
+
try { saveCliWorkspaceState(opts); } catch (_err) {}
|
|
4347
|
+
const captured = await captureRemoteControlOutput(async () => {
|
|
4348
|
+
if (line === '/exit' || line === '/quit') {
|
|
4349
|
+
console.log('Remote clients cannot exit the local terminal process. Stop it locally with Ctrl+C.');
|
|
4350
|
+
return;
|
|
4351
|
+
}
|
|
4352
|
+
const localResult = await handleLocalCommand(opts, line);
|
|
4353
|
+
if (localResult.handled) return;
|
|
4354
|
+
const localIntent = classifyLocalPromptIntent(line);
|
|
4355
|
+
if (localIntent?.command) {
|
|
4356
|
+
const intentResult = await handleLocalCommand(opts, localIntent.command);
|
|
4357
|
+
if (intentResult.handled) return;
|
|
4358
|
+
}
|
|
4359
|
+
const naturalFileAction = await runNaturalLocalFileAction(opts, line);
|
|
4360
|
+
if (naturalFileAction.handled) return;
|
|
4361
|
+
if (opts.offline) {
|
|
4362
|
+
console.log('Offline mode: use slash commands and local tools from this remote session.');
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
const data = await askServer(opts, line);
|
|
4366
|
+
printResponse(opts, data);
|
|
4367
|
+
});
|
|
4368
|
+
const output = `${captured.stdout || ''}${captured.stderr || ''}`.trim();
|
|
4369
|
+
return { ok: true, output: output || '(no output)' };
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4372
|
+
async function startRemoteControlServer(opts, overrides = {}) {
|
|
4373
|
+
if (opts.remoteControlSession?.server) {
|
|
4374
|
+
console.log(`Remote Control already active: ${opts.remoteControlSession.localUrl}`);
|
|
4375
|
+
return opts.remoteControlSession;
|
|
4376
|
+
}
|
|
4377
|
+
|
|
4378
|
+
const http = require('http');
|
|
4379
|
+
const crypto = require('crypto');
|
|
4380
|
+
const host = String(overrides.host || opts.remoteControlHost || '127.0.0.1').trim() || '127.0.0.1';
|
|
4381
|
+
const port = Math.max(0, Math.min(65535, Number(overrides.port ?? opts.remoteControlPort ?? 0) || 0));
|
|
4382
|
+
const token = crypto.randomBytes(24).toString('hex');
|
|
4383
|
+
const state = {
|
|
4384
|
+
id: crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
4385
|
+
name: String(overrides.name || opts.remoteControlName || path.basename(opts.cwd || process.cwd()) || os.hostname() || 'Bortex Code').trim(),
|
|
4386
|
+
host,
|
|
4387
|
+
port,
|
|
4388
|
+
token,
|
|
4389
|
+
cwd: opts.cwd,
|
|
4390
|
+
startedAt: Date.now(),
|
|
4391
|
+
busy: false,
|
|
4392
|
+
log: []
|
|
4393
|
+
};
|
|
4394
|
+
const appendLog = (role, text) => {
|
|
4395
|
+
state.log.push({ role, text: String(text || ''), ts: Date.now() });
|
|
4396
|
+
if (state.log.length > 200) state.log.splice(0, state.log.length - 200);
|
|
4397
|
+
};
|
|
4398
|
+
appendLog('system', `Remote Control started for ${state.cwd}`);
|
|
4399
|
+
|
|
4400
|
+
const isAuthorized = (req, urlObj) => {
|
|
4401
|
+
const header = String(req.headers['x-bortex-remote-token'] || '');
|
|
4402
|
+
const query = String(urlObj.searchParams.get('token') || '');
|
|
4403
|
+
return header === token || query === token;
|
|
4404
|
+
};
|
|
4405
|
+
|
|
4406
|
+
const server = http.createServer(async (req, res) => {
|
|
4407
|
+
let urlObj;
|
|
4408
|
+
try { urlObj = new URL(req.url, `http://${host === '0.0.0.0' ? '127.0.0.1' : host}`); } catch (_err) {
|
|
4409
|
+
return sendJson(res, 400, { ok: false, error: 'bad url' });
|
|
4410
|
+
}
|
|
4411
|
+
if (req.method === 'GET' && urlObj.pathname === '/') {
|
|
4412
|
+
if (!isAuthorized(req, urlObj)) {
|
|
4413
|
+
res.writeHead(401, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' });
|
|
4414
|
+
return res.end('Unauthorized: missing or invalid token.');
|
|
4415
|
+
}
|
|
4416
|
+
const body = buildRemoteControlHtml(state);
|
|
4417
|
+
res.writeHead(200, {
|
|
4418
|
+
'content-type': 'text/html; charset=utf-8',
|
|
4419
|
+
'cache-control': 'no-store',
|
|
4420
|
+
'content-length': Buffer.byteLength(body)
|
|
4421
|
+
});
|
|
4422
|
+
return res.end(body);
|
|
4423
|
+
}
|
|
4424
|
+
if (!isAuthorized(req, urlObj)) return sendJson(res, 401, { ok: false, error: 'unauthorized' });
|
|
4425
|
+
if (req.method === 'GET' && urlObj.pathname === '/api/status') {
|
|
4426
|
+
return sendJson(res, 200, {
|
|
4427
|
+
ok: true,
|
|
4428
|
+
id: state.id,
|
|
4429
|
+
name: state.name,
|
|
4430
|
+
version: CLI_VERSION,
|
|
4431
|
+
cwd: opts.cwd,
|
|
4432
|
+
agent: !!opts.agent,
|
|
4433
|
+
busy: !!state.busy,
|
|
4434
|
+
startedAt: state.startedAt,
|
|
4435
|
+
log: state.log
|
|
4436
|
+
});
|
|
4437
|
+
}
|
|
4438
|
+
if (req.method === 'POST' && urlObj.pathname === '/api/prompt') {
|
|
4439
|
+
if (state.busy) return sendJson(res, 409, { ok: false, error: 'remote session busy' });
|
|
4440
|
+
state.busy = true;
|
|
4441
|
+
try {
|
|
4442
|
+
const raw = await readHttpBody(req);
|
|
4443
|
+
let payload;
|
|
4444
|
+
try { payload = JSON.parse(raw || '{}'); } catch (_err) { payload = { text: raw }; }
|
|
4445
|
+
const text = String(payload.text || payload.prompt || '').trim();
|
|
4446
|
+
if (!text) return sendJson(res, 400, { ok: false, error: 'empty prompt' });
|
|
4447
|
+
appendLog('user', text);
|
|
4448
|
+
const result = await runRemoteControlPrompt(opts, text);
|
|
4449
|
+
appendLog('assistant', result.output || '(no output)');
|
|
4450
|
+
return sendJson(res, result.ok ? 200 : 500, { ok: !!result.ok, output: result.output || '' });
|
|
4451
|
+
} catch (err) {
|
|
4452
|
+
appendLog('system', `Error: ${err.message}`);
|
|
4453
|
+
return sendJson(res, 500, { ok: false, error: err.message });
|
|
4454
|
+
} finally {
|
|
4455
|
+
state.busy = false;
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
return sendJson(res, 404, { ok: false, error: 'not found' });
|
|
4459
|
+
});
|
|
4460
|
+
|
|
4461
|
+
await new Promise((resolve, reject) => {
|
|
4462
|
+
server.once('error', reject);
|
|
4463
|
+
server.listen(port, host, resolve);
|
|
4464
|
+
});
|
|
4465
|
+
const actualPort = server.address().port;
|
|
4466
|
+
state.port = actualPort;
|
|
4467
|
+
state.server = server;
|
|
4468
|
+
state.localUrl = `http://127.0.0.1:${actualPort}/?token=${encodeURIComponent(token)}`;
|
|
4469
|
+
state.lanUrls = getRemoteLanAddresses(actualPort, token);
|
|
4470
|
+
state.stop = () => new Promise((resolve) => server.close(() => resolve()));
|
|
4471
|
+
opts.remoteControlSession = state;
|
|
4472
|
+
|
|
4473
|
+
console.log(`Remote Control active: ${state.name}`);
|
|
4474
|
+
console.log(`Local URL: ${state.localUrl}`);
|
|
4475
|
+
if (host === '0.0.0.0' || host === '::') {
|
|
4476
|
+
if (state.lanUrls.length) {
|
|
4477
|
+
console.log('LAN URLs:');
|
|
4478
|
+
state.lanUrls.forEach((u) => console.log(` ${u}`));
|
|
4479
|
+
}
|
|
4480
|
+
console.log('Security: keep this tokenized URL private. Anyone with the URL can control this Bortex Code session.');
|
|
4481
|
+
} else {
|
|
4482
|
+
console.log('Mobile/LAN access: restart with --remote-lan or /remote-control --lan to bind on 0.0.0.0.');
|
|
4483
|
+
}
|
|
4484
|
+
return state;
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4487
|
+
async function stopRemoteControlServer(opts) {
|
|
4488
|
+
const state = opts.remoteControlSession;
|
|
4489
|
+
if (!state?.server) {
|
|
4490
|
+
console.log('Remote Control is not active.');
|
|
4491
|
+
return false;
|
|
4492
|
+
}
|
|
4493
|
+
await state.stop();
|
|
4494
|
+
opts.remoteControlSession = null;
|
|
4495
|
+
console.log('Remote Control stopped.');
|
|
4496
|
+
return true;
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
function parseRemoteControlSlashArgs(rest = [], opts = {}) {
|
|
4500
|
+
const out = {
|
|
4501
|
+
name: '',
|
|
4502
|
+
host: opts.remoteControlHost || '127.0.0.1',
|
|
4503
|
+
port: Number(opts.remoteControlPort || 0) || 0
|
|
4504
|
+
};
|
|
4505
|
+
const nameParts = [];
|
|
4506
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
4507
|
+
const a = String(rest[i] || '');
|
|
4508
|
+
if (a === 'stop' || a === 'off' || a === 'disconnect') {
|
|
4509
|
+
out.stop = true;
|
|
4510
|
+
continue;
|
|
4511
|
+
}
|
|
4512
|
+
if (a === '--lan' || a === '--remote-lan') {
|
|
4513
|
+
out.host = '0.0.0.0';
|
|
4514
|
+
continue;
|
|
4515
|
+
}
|
|
4516
|
+
if ((a === '--host' || a === '--remote-host') && rest[i + 1]) {
|
|
4517
|
+
out.host = String(rest[i + 1]);
|
|
4518
|
+
i += 1;
|
|
4519
|
+
continue;
|
|
4520
|
+
}
|
|
4521
|
+
if (a.startsWith('--host=')) {
|
|
4522
|
+
out.host = a.slice('--host='.length);
|
|
4523
|
+
continue;
|
|
4524
|
+
}
|
|
4525
|
+
if ((a === '--port' || a === '--remote-port') && rest[i + 1]) {
|
|
4526
|
+
out.port = Number(rest[i + 1]) || 0;
|
|
4527
|
+
i += 1;
|
|
4528
|
+
continue;
|
|
4529
|
+
}
|
|
4530
|
+
if (a.startsWith('--port=')) {
|
|
4531
|
+
out.port = Number(a.slice('--port='.length)) || 0;
|
|
4532
|
+
continue;
|
|
4533
|
+
}
|
|
4534
|
+
nameParts.push(a);
|
|
4535
|
+
}
|
|
4536
|
+
out.name = nameParts.join(' ').trim();
|
|
4537
|
+
return out;
|
|
4538
|
+
}
|
|
4539
|
+
|
|
3960
4540
|
function printAgentLocalHelp() {
|
|
3961
4541
|
console.log('Agent-local (planner CLI):');
|
|
3962
4542
|
console.log(' /agent-local <goal> genera un piano operativo CLI');
|
|
@@ -5240,6 +5820,33 @@ async function handleLocalCommand(opts, line) {
|
|
|
5240
5820
|
});
|
|
5241
5821
|
return { handled: true };
|
|
5242
5822
|
}
|
|
5823
|
+
if (lc === '/agent') {
|
|
5824
|
+
const v = String(rest[0] || '').toLowerCase();
|
|
5825
|
+
if (!['on', 'off', 'true', 'false', '1', '0'].includes(v)) {
|
|
5826
|
+
console.log('Uso: /agent on|off');
|
|
5827
|
+
return { handled: true };
|
|
5828
|
+
}
|
|
5829
|
+
opts.agent = v === 'on' || v === 'true' || v === '1';
|
|
5830
|
+
console.log(`Mode: ${opts.agent ? 'agent' : 'chat'}`);
|
|
5831
|
+
return { handled: true };
|
|
5832
|
+
}
|
|
5833
|
+
if (lc === '/status') {
|
|
5834
|
+
const todoItems = Array.isArray(opts.todo) ? opts.todo : [];
|
|
5835
|
+
const done = todoItems.filter((t) => t.done).length;
|
|
5836
|
+
const runPending = Array.isArray(opts.runState?.steps) ? opts.runState.steps.filter((s) => s.status === 'pending').length : 0;
|
|
5837
|
+
console.log(formatModeLine(opts));
|
|
5838
|
+
console.log(`Todo: ${done}/${todoItems.length} | Plan: ${opts.plan?.goal ? 'active' : 'none'} | Run: ${opts.runState?.goal ? `active (${runPending} pending)` : 'none'} | Remote: ${opts.remoteControlSession?.server ? 'active' : 'off'}`);
|
|
5839
|
+
return { handled: true };
|
|
5840
|
+
}
|
|
5841
|
+
if (lc === '/remote-control' || lc === '/remote' || lc === '/rc') {
|
|
5842
|
+
const parsed = parseRemoteControlSlashArgs(rest, opts);
|
|
5843
|
+
if (parsed.stop || opts.remoteControlSession?.server) {
|
|
5844
|
+
await stopRemoteControlServer(opts);
|
|
5845
|
+
return { handled: true };
|
|
5846
|
+
}
|
|
5847
|
+
await startRemoteControlServer(opts, parsed);
|
|
5848
|
+
return { handled: true };
|
|
5849
|
+
}
|
|
5243
5850
|
if (lc === '/pwd') {
|
|
5244
5851
|
console.log(opts.cwd);
|
|
5245
5852
|
return { handled: true };
|
|
@@ -6401,6 +7008,9 @@ const SLASH_COMMANDS = [
|
|
|
6401
7008
|
['/status', 'Show session and workspace state'],
|
|
6402
7009
|
['/commands', 'Show this command menu'],
|
|
6403
7010
|
['/help', 'Show command help'],
|
|
7011
|
+
['/remote-control [name]', 'Control this local session from a browser'],
|
|
7012
|
+
['/remote-control --lan', 'Expose Remote Control on the local network'],
|
|
7013
|
+
['/rc', 'Toggle Remote Control'],
|
|
6404
7014
|
['/llm-config show', 'Show cached LLM configuration'],
|
|
6405
7015
|
['/llm-config sync', 'Sync LLM configuration from Bortex'],
|
|
6406
7016
|
['/pwd', 'Show current working directory'],
|
|
@@ -6575,6 +7185,13 @@ async function runRepl(opts) {
|
|
|
6575
7185
|
return lines.join('\n');
|
|
6576
7186
|
};
|
|
6577
7187
|
opts._askInput = async (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
7188
|
+
if (opts.remoteControl) {
|
|
7189
|
+
await startRemoteControlServer(opts, {
|
|
7190
|
+
name: opts.remoteControlName,
|
|
7191
|
+
host: opts.remoteControlHost,
|
|
7192
|
+
port: opts.remoteControlPort
|
|
7193
|
+
});
|
|
7194
|
+
}
|
|
6578
7195
|
|
|
6579
7196
|
const question = () => new Promise((resolve) => {
|
|
6580
7197
|
const prompt = `bortex:${opts.agent ? 'agent' : 'chat'}> `;
|
|
@@ -6646,6 +7263,9 @@ async function runRepl(opts) {
|
|
|
6646
7263
|
|
|
6647
7264
|
rl.close();
|
|
6648
7265
|
uninstallSlashMenu();
|
|
7266
|
+
if (opts.remoteControlSession?.server) {
|
|
7267
|
+
await stopRemoteControlServer(opts);
|
|
7268
|
+
}
|
|
6649
7269
|
try { saveCliWorkspaceState(opts); } catch (_err) { }
|
|
6650
7270
|
delete opts._readMultiline;
|
|
6651
7271
|
delete opts._askInput;
|
|
@@ -6696,6 +7316,27 @@ async function main() {
|
|
|
6696
7316
|
opts.sessionReused = false;
|
|
6697
7317
|
}
|
|
6698
7318
|
|
|
7319
|
+
if (opts.remoteControlServerMode) {
|
|
7320
|
+
await startRemoteControlServer(opts, {
|
|
7321
|
+
name: opts.remoteControlName,
|
|
7322
|
+
host: opts.remoteControlHost,
|
|
7323
|
+
port: opts.remoteControlPort
|
|
7324
|
+
});
|
|
7325
|
+
console.log('Server mode: keep this process running. Press Ctrl+C to stop Remote Control.');
|
|
7326
|
+
await new Promise((resolve) => {
|
|
7327
|
+
let stopping = false;
|
|
7328
|
+
const stop = async () => {
|
|
7329
|
+
if (stopping) return;
|
|
7330
|
+
stopping = true;
|
|
7331
|
+
try { await stopRemoteControlServer(opts); } catch (_err) {}
|
|
7332
|
+
resolve();
|
|
7333
|
+
};
|
|
7334
|
+
process.once('SIGINT', stop);
|
|
7335
|
+
process.once('SIGTERM', stop);
|
|
7336
|
+
});
|
|
7337
|
+
return;
|
|
7338
|
+
}
|
|
7339
|
+
|
|
6699
7340
|
if (opts.prompt) {
|
|
6700
7341
|
await runSinglePrompt(opts);
|
|
6701
7342
|
return;
|