bortexcode 1.2.8 → 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.
Files changed (3) hide show
  1. package/README.md +36 -0
  2. package/bin/bortex.js +492 -1
  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:');
@@ -3874,6 +3929,8 @@ function printLocalHelp() {
3874
3929
  console.log(' /sys-status check system / runtime status');
3875
3930
  console.log(' /process-status check running processes');
3876
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');
3877
3934
  console.log(' /diff [unstaged|staged|all]');
3878
3935
  console.log(' /stage <file>|--all');
3879
3936
  console.log(' /unstage <file>|--all');
@@ -4107,6 +4164,379 @@ async function runGitApplyInline(opts, mode) {
4107
4164
  }
4108
4165
  }
4109
4166
 
4167
+ function escapeHtml(value) {
4168
+ return String(value ?? '')
4169
+ .replace(/&/g, '&amp;')
4170
+ .replace(/</g, '&lt;')
4171
+ .replace(/>/g, '&gt;')
4172
+ .replace(/"/g, '&quot;')
4173
+ .replace(/'/g, '&#39;');
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) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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
+
4110
4540
  function printAgentLocalHelp() {
4111
4541
  console.log('Agent-local (planner CLI):');
4112
4542
  console.log(' /agent-local <goal> genera un piano operativo CLI');
@@ -5390,6 +5820,33 @@ async function handleLocalCommand(opts, line) {
5390
5820
  });
5391
5821
  return { handled: true };
5392
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
+ }
5393
5850
  if (lc === '/pwd') {
5394
5851
  console.log(opts.cwd);
5395
5852
  return { handled: true };
@@ -6551,6 +7008,9 @@ const SLASH_COMMANDS = [
6551
7008
  ['/status', 'Show session and workspace state'],
6552
7009
  ['/commands', 'Show this command menu'],
6553
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'],
6554
7014
  ['/llm-config show', 'Show cached LLM configuration'],
6555
7015
  ['/llm-config sync', 'Sync LLM configuration from Bortex'],
6556
7016
  ['/pwd', 'Show current working directory'],
@@ -6725,6 +7185,13 @@ async function runRepl(opts) {
6725
7185
  return lines.join('\n');
6726
7186
  };
6727
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
+ }
6728
7195
 
6729
7196
  const question = () => new Promise((resolve) => {
6730
7197
  const prompt = `bortex:${opts.agent ? 'agent' : 'chat'}> `;
@@ -6796,6 +7263,9 @@ async function runRepl(opts) {
6796
7263
 
6797
7264
  rl.close();
6798
7265
  uninstallSlashMenu();
7266
+ if (opts.remoteControlSession?.server) {
7267
+ await stopRemoteControlServer(opts);
7268
+ }
6799
7269
  try { saveCliWorkspaceState(opts); } catch (_err) { }
6800
7270
  delete opts._readMultiline;
6801
7271
  delete opts._askInput;
@@ -6846,6 +7316,27 @@ async function main() {
6846
7316
  opts.sessionReused = false;
6847
7317
  }
6848
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
+
6849
7340
  if (opts.prompt) {
6850
7341
  await runSinglePrompt(opts);
6851
7342
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bortexcode",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
4
4
  "description": "Bortex Code CLI - AI coding assistant powered by bortex.site",
5
5
  "homepage": "https://bortex.site",
6
6
  "license": "UNLICENSED",