bortexcode 1.2.9 → 1.4.0
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 +30 -0
- package/bin/bortex.js +594 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ bortexcode
|
|
|
48
48
|
bortexcode "explain this function"
|
|
49
49
|
bortexcode --agent "refactor src/utils.js"
|
|
50
50
|
bortexcode remote-control
|
|
51
|
+
bortexcode remote-control --cloud
|
|
51
52
|
bortexcode --remote-control
|
|
52
53
|
```
|
|
53
54
|
|
|
@@ -67,6 +68,7 @@ Common commands:
|
|
|
67
68
|
/llm-config sync
|
|
68
69
|
/remote-control [name]
|
|
69
70
|
/remote-control --lan
|
|
71
|
+
/remote-control --cloud
|
|
70
72
|
/rc
|
|
71
73
|
/exit
|
|
72
74
|
```
|
|
@@ -87,11 +89,31 @@ For phone access on the same LAN:
|
|
|
87
89
|
bortexcode remote-control --remote-lan
|
|
88
90
|
```
|
|
89
91
|
|
|
92
|
+
For remote access without opening an inbound port, use the bortex.site relay:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
bortexcode remote-control --cloud --name "My Project"
|
|
96
|
+
bortexcode --remote-cloud "My Project"
|
|
97
|
+
bortexcode remote-control --cloud --remote-permission full
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Cloud Remote Control defaults to `balanced` permissions. It allows inspection,
|
|
101
|
+
status commands, and common test/lint/build shell commands while blocking
|
|
102
|
+
mutating commands such as file writes, patch apply, git stage/commit/discard,
|
|
103
|
+
and arbitrary shell mutations. Use `--remote-permission read-only` for
|
|
104
|
+
inspection-only sessions or `--remote-permission full` only for trusted sessions.
|
|
105
|
+
|
|
106
|
+
The browser UI includes Cancel and Revoke controls. Cancel requests termination
|
|
107
|
+
of the running command. Revoke invalidates the tokenized URL and stops the
|
|
108
|
+
server-mode cloud session.
|
|
109
|
+
|
|
90
110
|
Inside the REPL:
|
|
91
111
|
|
|
92
112
|
```text
|
|
93
113
|
/remote-control My Project
|
|
94
114
|
/remote-control --lan
|
|
115
|
+
/remote-control --cloud
|
|
116
|
+
/remote-control --cloud --permission full
|
|
95
117
|
/remote-control stop
|
|
96
118
|
```
|
|
97
119
|
|
|
@@ -115,6 +137,12 @@ Inside the REPL:
|
|
|
115
137
|
Disable startup update check
|
|
116
138
|
--remote-control, --rc [name]
|
|
117
139
|
Enable browser remote control
|
|
140
|
+
--remote-cloud, --cloud
|
|
141
|
+
Use bortex.site relay instead of opening a local port
|
|
142
|
+
--remote-relay <url>
|
|
143
|
+
Remote relay base URL
|
|
144
|
+
--remote-permission <mode>
|
|
145
|
+
Remote permissions: read-only, balanced, full
|
|
118
146
|
--remote-lan Bind remote control to 0.0.0.0 for LAN/mobile access
|
|
119
147
|
--remote-host <host>, --remote-port <port>
|
|
120
148
|
Remote control bind address
|
|
@@ -141,4 +169,6 @@ BORTEX_CLI_ICONS=1
|
|
|
141
169
|
BORTEX_NO_UPDATE_CHECK=1
|
|
142
170
|
BORTEX_REMOTE_HOST
|
|
143
171
|
BORTEX_REMOTE_PORT
|
|
172
|
+
BORTEX_REMOTE_RELAY_URL
|
|
173
|
+
BORTEX_REMOTE_PERMISSION
|
|
144
174
|
```
|
package/bin/bortex.js
CHANGED
|
@@ -33,6 +33,9 @@ function parseArgs(argv) {
|
|
|
33
33
|
_apiKeyExplicit: false,
|
|
34
34
|
remoteControl: false,
|
|
35
35
|
remoteControlServerMode: false,
|
|
36
|
+
remoteControlCloud: false,
|
|
37
|
+
remoteControlRelayUrl: process.env.BORTEX_REMOTE_RELAY_URL || '',
|
|
38
|
+
remoteControlPermission: process.env.BORTEX_REMOTE_PERMISSION || 'balanced',
|
|
36
39
|
remoteControlName: '',
|
|
37
40
|
remoteControlHost: process.env.BORTEX_REMOTE_HOST || '127.0.0.1',
|
|
38
41
|
remoteControlPort: Number(process.env.BORTEX_REMOTE_PORT || 0) || 0,
|
|
@@ -51,6 +54,10 @@ function parseArgs(argv) {
|
|
|
51
54
|
opts.remoteControlServerMode = true;
|
|
52
55
|
continue;
|
|
53
56
|
}
|
|
57
|
+
if ((a === 'cloud' || a === 'relay') && opts.remoteControlServerMode) {
|
|
58
|
+
opts.remoteControlCloud = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
54
61
|
if (a === '--agent' || a === '-a') {
|
|
55
62
|
opts.agent = true;
|
|
56
63
|
continue;
|
|
@@ -150,6 +157,41 @@ function parseArgs(argv) {
|
|
|
150
157
|
opts.remoteControlName = a.slice('--remote-control='.length);
|
|
151
158
|
continue;
|
|
152
159
|
}
|
|
160
|
+
if (a === '--remote-cloud' || a === '--cloud' || a === '--relay') {
|
|
161
|
+
opts.remoteControl = true;
|
|
162
|
+
opts.remoteControlCloud = true;
|
|
163
|
+
if (argv[i + 1] && !String(argv[i + 1]).startsWith('-')) {
|
|
164
|
+
opts.remoteControlName = argv[i + 1];
|
|
165
|
+
i += 1;
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if ((a === '--remote-relay' || a === '--relay-url') && argv[i + 1]) {
|
|
170
|
+
opts.remoteControlRelayUrl = argv[i + 1];
|
|
171
|
+
i += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (a.startsWith('--remote-relay=')) {
|
|
175
|
+
opts.remoteControlRelayUrl = a.slice('--remote-relay='.length);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (a.startsWith('--relay-url=')) {
|
|
179
|
+
opts.remoteControlRelayUrl = a.slice('--relay-url='.length);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if ((a === '--remote-permission' || a === '--permission') && argv[i + 1]) {
|
|
183
|
+
opts.remoteControlPermission = argv[i + 1];
|
|
184
|
+
i += 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (a.startsWith('--remote-permission=')) {
|
|
188
|
+
opts.remoteControlPermission = a.slice('--remote-permission='.length);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (a.startsWith('--permission=')) {
|
|
192
|
+
opts.remoteControlPermission = a.slice('--permission='.length);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
153
195
|
if (a === '--remote-lan') {
|
|
154
196
|
opts.remoteControlHost = '0.0.0.0';
|
|
155
197
|
continue;
|
|
@@ -197,6 +239,7 @@ function usage() {
|
|
|
197
239
|
console.log('Usage:');
|
|
198
240
|
console.log(` ${cliName} [options] [prompt]`);
|
|
199
241
|
console.log(` ${cliName} remote-control [--name <title>] [--remote-lan]`);
|
|
242
|
+
console.log(` ${cliName} remote-control --cloud [--name <title>]`);
|
|
200
243
|
console.log(` ${cliName} --api-key <apikey>`);
|
|
201
244
|
console.log(` ${cliName} "write a python function"`);
|
|
202
245
|
console.log('');
|
|
@@ -218,6 +261,12 @@ function usage() {
|
|
|
218
261
|
console.log(' Disable startup update check');
|
|
219
262
|
console.log(' --remote-control, --rc [name]');
|
|
220
263
|
console.log(' Enable browser remote control for this session');
|
|
264
|
+
console.log(' --remote-cloud, --cloud');
|
|
265
|
+
console.log(' Use bortex.site relay instead of opening a local port');
|
|
266
|
+
console.log(' --remote-relay <url>');
|
|
267
|
+
console.log(' Remote relay base URL (default: current --url)');
|
|
268
|
+
console.log(' --remote-permission <mode>');
|
|
269
|
+
console.log(' Remote permissions: read-only, balanced, full');
|
|
221
270
|
console.log(' --remote-lan Bind remote control to 0.0.0.0 for LAN/mobile access');
|
|
222
271
|
console.log(' --remote-host <host>, --remote-port <port>');
|
|
223
272
|
console.log(' Remote control bind address');
|
|
@@ -230,6 +279,8 @@ function usage() {
|
|
|
230
279
|
console.log('Environment:');
|
|
231
280
|
console.log(' BORTEX_URL Server URL');
|
|
232
281
|
console.log(' BORTEX_API_KEY API key');
|
|
282
|
+
console.log(' BORTEX_REMOTE_RELAY_URL Remote Control cloud relay URL');
|
|
283
|
+
console.log(' BORTEX_REMOTE_PERMISSION Remote permissions: read-only, balanced, full');
|
|
233
284
|
}
|
|
234
285
|
|
|
235
286
|
function formatMs(ms) {
|
|
@@ -3897,8 +3948,31 @@ async function runToolRunCommand(opts, line) {
|
|
|
3897
3948
|
});
|
|
3898
3949
|
}
|
|
3899
3950
|
|
|
3951
|
+
let ACTIVE_REMOTE_CONTROL_EXECUTION_STATE = null;
|
|
3952
|
+
|
|
3953
|
+
function terminateChildProcessTree(child) {
|
|
3954
|
+
if (!child || !child.pid) return;
|
|
3955
|
+
if (process.platform === 'win32') {
|
|
3956
|
+
try {
|
|
3957
|
+
spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], {
|
|
3958
|
+
stdio: 'ignore',
|
|
3959
|
+
windowsHide: true
|
|
3960
|
+
});
|
|
3961
|
+
return;
|
|
3962
|
+
} catch (_err) {}
|
|
3963
|
+
}
|
|
3964
|
+
try { child.kill('SIGTERM'); } catch (_err) {}
|
|
3965
|
+
setTimeout(() => {
|
|
3966
|
+
try { child.kill('SIGKILL'); } catch (_err) {}
|
|
3967
|
+
}, 1500).unref?.();
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3900
3970
|
function runChild(command, args, { cwd, shell = false } = {}) {
|
|
3901
3971
|
return new Promise((resolve) => {
|
|
3972
|
+
const cancelState = ACTIVE_REMOTE_CONTROL_EXECUTION_STATE;
|
|
3973
|
+
if (cancelState?.cancelRequested || cancelState?.revoked || cancelState?.active === false) {
|
|
3974
|
+
return resolve({ ok: false, code: null, stdout: '', stderr: 'Canceled by remote request.' });
|
|
3975
|
+
}
|
|
3902
3976
|
const child = spawn(command, args, {
|
|
3903
3977
|
cwd: cwd || process.cwd(),
|
|
3904
3978
|
shell,
|
|
@@ -3907,10 +3981,30 @@ function runChild(command, args, { cwd, shell = false } = {}) {
|
|
|
3907
3981
|
});
|
|
3908
3982
|
let stdout = '';
|
|
3909
3983
|
let stderr = '';
|
|
3984
|
+
let settled = false;
|
|
3985
|
+
let canceled = false;
|
|
3986
|
+
const finish = (result) => {
|
|
3987
|
+
if (settled) return;
|
|
3988
|
+
settled = true;
|
|
3989
|
+
if (cancelTimer) clearInterval(cancelTimer);
|
|
3990
|
+
resolve(result);
|
|
3991
|
+
};
|
|
3992
|
+
const cancelTimer = cancelState ? setInterval(() => {
|
|
3993
|
+
if (cancelState.cancelRequested || cancelState.revoked || cancelState.active === false) {
|
|
3994
|
+
canceled = true;
|
|
3995
|
+
terminateChildProcessTree(child);
|
|
3996
|
+
}
|
|
3997
|
+
}, 250) : null;
|
|
3998
|
+
if (cancelTimer && typeof cancelTimer.unref === 'function') cancelTimer.unref();
|
|
3910
3999
|
child.stdout.on('data', (c) => { stdout += String(c || ''); });
|
|
3911
4000
|
child.stderr.on('data', (c) => { stderr += String(c || ''); });
|
|
3912
|
-
child.on('error', (err) =>
|
|
3913
|
-
child.on('close', (code) =>
|
|
4001
|
+
child.on('error', (err) => finish({ ok: false, code: null, stdout, stderr: err.message }));
|
|
4002
|
+
child.on('close', (code) => {
|
|
4003
|
+
const finalStderr = canceled
|
|
4004
|
+
? `${stderr || ''}${stderr && !stderr.endsWith('\n') ? '\n' : ''}Canceled by remote request.`
|
|
4005
|
+
: stderr;
|
|
4006
|
+
finish({ ok: !canceled && code === 0, code, stdout, stderr: finalStderr });
|
|
4007
|
+
});
|
|
3914
4008
|
});
|
|
3915
4009
|
}
|
|
3916
4010
|
|
|
@@ -3930,6 +4024,7 @@ function printLocalHelp() {
|
|
|
3930
4024
|
console.log(' /process-status check running processes');
|
|
3931
4025
|
console.log(' /port-status check listening ports');
|
|
3932
4026
|
console.log(' /remote-control [name] [--lan|--host <host>|--port <port>]');
|
|
4027
|
+
console.log(' /remote-control --cloud [name]');
|
|
3933
4028
|
console.log(' /remote-control stop');
|
|
3934
4029
|
console.log(' /diff [unstaged|staged|all]');
|
|
3935
4030
|
console.log(' /stage <file>|--all');
|
|
@@ -4342,29 +4437,50 @@ async function captureRemoteControlOutput(fn) {
|
|
|
4342
4437
|
async function runRemoteControlPrompt(opts, text) {
|
|
4343
4438
|
const line = String(text || '').trim();
|
|
4344
4439
|
if (!line) return { ok: false, output: 'Empty prompt.' };
|
|
4440
|
+
const permission = assessRemoteControlPromptPermission(opts, line);
|
|
4441
|
+
if (!permission.ok) {
|
|
4442
|
+
return {
|
|
4443
|
+
ok: false,
|
|
4444
|
+
output: `Remote command blocked (${permission.mode}): ${permission.reason}.\nRestart with --remote-permission full only for a trusted session.`
|
|
4445
|
+
};
|
|
4446
|
+
}
|
|
4345
4447
|
pushCliHistory(opts, `[remote] ${line}`);
|
|
4346
4448
|
try { saveCliWorkspaceState(opts); } catch (_err) {}
|
|
4347
|
-
const
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
const
|
|
4357
|
-
if (
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4449
|
+
const previousRemoteExecution = ACTIVE_REMOTE_CONTROL_EXECUTION_STATE;
|
|
4450
|
+
ACTIVE_REMOTE_CONTROL_EXECUTION_STATE = opts._remoteControlExecutionState || null;
|
|
4451
|
+
let captured;
|
|
4452
|
+
try {
|
|
4453
|
+
captured = await captureRemoteControlOutput(async () => {
|
|
4454
|
+
if (line === '/exit' || line === '/quit') {
|
|
4455
|
+
console.log('Remote clients cannot exit the local terminal process. Stop it locally with Ctrl+C.');
|
|
4456
|
+
return;
|
|
4457
|
+
}
|
|
4458
|
+
const localResult = await handleLocalCommand(opts, line);
|
|
4459
|
+
if (localResult.handled) return;
|
|
4460
|
+
const localIntent = classifyLocalPromptIntent(line);
|
|
4461
|
+
if (localIntent?.command) {
|
|
4462
|
+
const intentPermission = assessRemoteControlPromptPermission(opts, localIntent.command);
|
|
4463
|
+
if (!intentPermission.ok) {
|
|
4464
|
+
console.log(`Remote inferred command blocked (${intentPermission.mode}): ${intentPermission.reason}.`);
|
|
4465
|
+
return;
|
|
4466
|
+
}
|
|
4467
|
+
const intentResult = await handleLocalCommand(opts, localIntent.command);
|
|
4468
|
+
if (intentResult.handled) return;
|
|
4469
|
+
}
|
|
4470
|
+
if (!permission.skipNaturalActions) {
|
|
4471
|
+
const naturalFileAction = await runNaturalLocalFileAction(opts, line);
|
|
4472
|
+
if (naturalFileAction.handled) return;
|
|
4473
|
+
}
|
|
4474
|
+
if (opts.offline) {
|
|
4475
|
+
console.log('Offline mode: use slash commands and local tools from this remote session.');
|
|
4476
|
+
return;
|
|
4477
|
+
}
|
|
4478
|
+
const data = await askServer(opts, line);
|
|
4479
|
+
printResponse(opts, data);
|
|
4480
|
+
});
|
|
4481
|
+
} finally {
|
|
4482
|
+
ACTIVE_REMOTE_CONTROL_EXECUTION_STATE = previousRemoteExecution;
|
|
4483
|
+
}
|
|
4368
4484
|
const output = `${captured.stdout || ''}${captured.stderr || ''}`.trim();
|
|
4369
4485
|
return { ok: true, output: output || '(no output)' };
|
|
4370
4486
|
}
|
|
@@ -4377,12 +4493,13 @@ async function startRemoteControlServer(opts, overrides = {}) {
|
|
|
4377
4493
|
|
|
4378
4494
|
const http = require('http');
|
|
4379
4495
|
const crypto = require('crypto');
|
|
4496
|
+
opts.remoteControlPermission = getRemoteControlPermissionMode(opts, overrides);
|
|
4380
4497
|
const host = String(overrides.host || opts.remoteControlHost || '127.0.0.1').trim() || '127.0.0.1';
|
|
4381
4498
|
const port = Math.max(0, Math.min(65535, Number(overrides.port ?? opts.remoteControlPort ?? 0) || 0));
|
|
4382
4499
|
const token = crypto.randomBytes(24).toString('hex');
|
|
4383
4500
|
const state = {
|
|
4384
4501
|
id: crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
4385
|
-
name:
|
|
4502
|
+
name: getRemoteControlDisplayName(opts, overrides),
|
|
4386
4503
|
host,
|
|
4387
4504
|
port,
|
|
4388
4505
|
token,
|
|
@@ -4496,11 +4613,377 @@ async function stopRemoteControlServer(opts) {
|
|
|
4496
4613
|
return true;
|
|
4497
4614
|
}
|
|
4498
4615
|
|
|
4616
|
+
function getRemoteControlDisplayName(opts, overrides = {}) {
|
|
4617
|
+
return String(overrides.name || opts.remoteControlName || path.basename(opts.cwd || process.cwd()) || os.hostname() || 'Bortex Code').trim();
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4620
|
+
function getRemoteControlStatus(opts) {
|
|
4621
|
+
if (opts.remoteControlCloudSession?.active) return 'cloud';
|
|
4622
|
+
if (opts.remoteControlSession?.server) return 'local';
|
|
4623
|
+
return 'off';
|
|
4624
|
+
}
|
|
4625
|
+
|
|
4626
|
+
function getRemoteControlRelayBase(opts, overrides = {}) {
|
|
4627
|
+
return String(overrides.relayUrl || opts.remoteControlRelayUrl || opts.url || 'https://bortex.site').replace(/\/+$/, '');
|
|
4628
|
+
}
|
|
4629
|
+
|
|
4630
|
+
function normalizeRemoteControlPermissionMode(value) {
|
|
4631
|
+
const v = String(value || 'balanced').trim().toLowerCase();
|
|
4632
|
+
if (['full', 'trusted', 'unsafe', 'yolo'].includes(v)) return 'full';
|
|
4633
|
+
if (['read-only', 'readonly', 'read', 'safe', 'ro'].includes(v)) return 'read-only';
|
|
4634
|
+
return 'balanced';
|
|
4635
|
+
}
|
|
4636
|
+
|
|
4637
|
+
function getRemoteControlPermissionMode(opts, overrides = {}) {
|
|
4638
|
+
return normalizeRemoteControlPermissionMode(overrides.permission || opts.remoteControlPermission || 'balanced');
|
|
4639
|
+
}
|
|
4640
|
+
|
|
4641
|
+
function isReadOnlyRemoteSlashCommand(line) {
|
|
4642
|
+
const [cmd, ...rest] = parseWords(line);
|
|
4643
|
+
const lc = String(cmd || '').toLowerCase();
|
|
4644
|
+
if (!lc.startsWith('/')) return false;
|
|
4645
|
+
if ([
|
|
4646
|
+
'/help', '/commands', '/menu', '/status', '/pwd', '/ls', '/tree', '/read',
|
|
4647
|
+
'/diff', '/hunks', '/show-hunk', '/ssh-status', '/sys-status',
|
|
4648
|
+
'/process-status', '/port-status', '/history'
|
|
4649
|
+
].includes(lc)) return true;
|
|
4650
|
+
if (lc === '/llm-config') {
|
|
4651
|
+
const sub = String(rest[0] || 'show').toLowerCase();
|
|
4652
|
+
return !sub || ['show', 'status'].includes(sub);
|
|
4653
|
+
}
|
|
4654
|
+
if (lc === '/git') {
|
|
4655
|
+
const sub = String(rest[0] || '').toLowerCase();
|
|
4656
|
+
return ['status', 'diff', 'log', 'show', 'branch', 'rev-parse', 'remote'].includes(sub);
|
|
4657
|
+
}
|
|
4658
|
+
return false;
|
|
4659
|
+
}
|
|
4660
|
+
|
|
4661
|
+
function isBalancedRemoteShellCommand(cmdText) {
|
|
4662
|
+
const text = String(cmdText || '').trim();
|
|
4663
|
+
const lower = text.toLowerCase();
|
|
4664
|
+
if (!text) return false;
|
|
4665
|
+
if (/[;&|`$<>]/.test(text)) return false;
|
|
4666
|
+
if (/\b(rm|del|erase|rmdir|rd|move|mv|copy|cp|curl|wget|scp|ssh|chmod|chown|sudo|su|kill|pkill|taskkill|format|shutdown|reboot)\b/i.test(text)) return false;
|
|
4667
|
+
if (/\b(git\s+(add|commit|reset|restore|checkout|switch|clean|stash|push|pull|merge|rebase|apply|am|cherry-pick))\b/i.test(text)) return false;
|
|
4668
|
+
if (/^(npm|pnpm|yarn)\s+(test|run\s+(test|lint|build|check|typecheck)|exec\s+tsc)\b/i.test(text)) return true;
|
|
4669
|
+
if (/^node\s+--check\s+\S+/i.test(text)) return true;
|
|
4670
|
+
if (/^(git\s+(status|diff|log|show|branch|rev-parse)|rg|grep|findstr|ls|dir|pwd|cat|type)\b/i.test(text)) return true;
|
|
4671
|
+
return false;
|
|
4672
|
+
}
|
|
4673
|
+
|
|
4674
|
+
function isBalancedRemoteSlashCommand(line) {
|
|
4675
|
+
if (isReadOnlyRemoteSlashCommand(line)) return true;
|
|
4676
|
+
const [cmd, ...rest] = parseWords(line);
|
|
4677
|
+
const lc = String(cmd || '').toLowerCase();
|
|
4678
|
+
if (lc === '/sh') {
|
|
4679
|
+
const cmdText = line.slice(line.indexOf('/sh') + 3).trim();
|
|
4680
|
+
return isBalancedRemoteShellCommand(cmdText);
|
|
4681
|
+
}
|
|
4682
|
+
if (lc === '/agent') return true;
|
|
4683
|
+
if (lc === '/ux') return true;
|
|
4684
|
+
if (lc === '/remote-control' || lc === '/remote' || lc === '/rc') return true;
|
|
4685
|
+
if (lc === '/agent-run') {
|
|
4686
|
+
return rest.some((x) => String(x).toLowerCase() === '--dry');
|
|
4687
|
+
}
|
|
4688
|
+
return false;
|
|
4689
|
+
}
|
|
4690
|
+
|
|
4691
|
+
function assessRemoteControlPromptPermission(opts, line) {
|
|
4692
|
+
const mode = getRemoteControlPermissionMode(opts);
|
|
4693
|
+
const text = String(line || '').trim();
|
|
4694
|
+
if (!text) return { ok: false, mode, reason: 'empty prompt' };
|
|
4695
|
+
if (mode === 'full') return { ok: true, mode };
|
|
4696
|
+
if (!text.startsWith('/')) return { ok: true, mode, skipNaturalActions: true };
|
|
4697
|
+
if (mode === 'read-only') {
|
|
4698
|
+
if (isReadOnlyRemoteSlashCommand(text)) return { ok: true, mode, skipNaturalActions: true };
|
|
4699
|
+
return { ok: false, mode, reason: 'read-only mode allows inspection commands only' };
|
|
4700
|
+
}
|
|
4701
|
+
if (isBalancedRemoteSlashCommand(text)) return { ok: true, mode, skipNaturalActions: true };
|
|
4702
|
+
return { ok: false, mode, reason: 'balanced mode blocks mutating or unsafe remote commands' };
|
|
4703
|
+
}
|
|
4704
|
+
|
|
4705
|
+
function remoteControlSleep(ms) {
|
|
4706
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4707
|
+
}
|
|
4708
|
+
|
|
4709
|
+
function withRemoteControlQuery(rawUrl, baseUrl, params = {}) {
|
|
4710
|
+
const urlObj = new URL(rawUrl, baseUrl);
|
|
4711
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
4712
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
4713
|
+
urlObj.searchParams.set(key, String(value));
|
|
4714
|
+
}
|
|
4715
|
+
});
|
|
4716
|
+
return urlObj.toString();
|
|
4717
|
+
}
|
|
4718
|
+
|
|
4719
|
+
async function fetchRemoteControlJson(url, options = {}, timeoutMs = 30000) {
|
|
4720
|
+
const controller = new AbortController();
|
|
4721
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
4722
|
+
if (typeof timeoutId.unref === 'function') timeoutId.unref();
|
|
4723
|
+
try {
|
|
4724
|
+
const res = await fetch(url, {
|
|
4725
|
+
...options,
|
|
4726
|
+
headers: {
|
|
4727
|
+
Accept: 'application/json',
|
|
4728
|
+
...(options.headers || {})
|
|
4729
|
+
},
|
|
4730
|
+
signal: controller.signal
|
|
4731
|
+
});
|
|
4732
|
+
const text = await res.text();
|
|
4733
|
+
let data = null;
|
|
4734
|
+
if (text) {
|
|
4735
|
+
try { data = JSON.parse(text); } catch (_err) { data = null; }
|
|
4736
|
+
}
|
|
4737
|
+
if (!res.ok) {
|
|
4738
|
+
const detail = data?.error || data?.message || text || res.statusText || 'request failed';
|
|
4739
|
+
throw new Error(`HTTP ${res.status}: ${detail}`);
|
|
4740
|
+
}
|
|
4741
|
+
if (data?.ok === false) {
|
|
4742
|
+
throw new Error(data.error || data.message || 'request failed');
|
|
4743
|
+
}
|
|
4744
|
+
return data || {};
|
|
4745
|
+
} finally {
|
|
4746
|
+
clearTimeout(timeoutId);
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
|
|
4750
|
+
async function postCloudRemoteHeartbeat(state, extra = {}) {
|
|
4751
|
+
const heartbeatUrl = withRemoteControlQuery(
|
|
4752
|
+
state.heartbeatUrl,
|
|
4753
|
+
state.baseUrl,
|
|
4754
|
+
{ agentToken: state.agentToken }
|
|
4755
|
+
);
|
|
4756
|
+
await fetchRemoteControlJson(heartbeatUrl, {
|
|
4757
|
+
method: 'POST',
|
|
4758
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4759
|
+
body: JSON.stringify({ cwd: state.cwd, version: CLI_VERSION, ...extra })
|
|
4760
|
+
}, 12000);
|
|
4761
|
+
}
|
|
4762
|
+
|
|
4763
|
+
async function postCloudRemoteResult(state, commandId, result) {
|
|
4764
|
+
const resultUrl = withRemoteControlQuery(
|
|
4765
|
+
state.resultUrl,
|
|
4766
|
+
state.baseUrl,
|
|
4767
|
+
{ agentToken: state.agentToken }
|
|
4768
|
+
);
|
|
4769
|
+
await fetchRemoteControlJson(resultUrl, {
|
|
4770
|
+
method: 'POST',
|
|
4771
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4772
|
+
body: JSON.stringify({
|
|
4773
|
+
commandId,
|
|
4774
|
+
ok: !!result.ok,
|
|
4775
|
+
output: String(result.output || '')
|
|
4776
|
+
})
|
|
4777
|
+
}, 35000);
|
|
4778
|
+
}
|
|
4779
|
+
|
|
4780
|
+
async function pollCloudRemoteControlControlLoop(state) {
|
|
4781
|
+
while (state.active) {
|
|
4782
|
+
try {
|
|
4783
|
+
const controlUrl = withRemoteControlQuery(
|
|
4784
|
+
state.controlUrl,
|
|
4785
|
+
state.baseUrl,
|
|
4786
|
+
{ agentToken: state.agentToken }
|
|
4787
|
+
);
|
|
4788
|
+
const data = await fetchRemoteControlJson(controlUrl, { method: 'GET' }, 12000);
|
|
4789
|
+
if (data.revoked) {
|
|
4790
|
+
state.revoked = true;
|
|
4791
|
+
state.cancelRequested = true;
|
|
4792
|
+
state.active = false;
|
|
4793
|
+
console.log('Cloud Remote Control revoked by remote client.');
|
|
4794
|
+
break;
|
|
4795
|
+
}
|
|
4796
|
+
if (data.cancelRequested) {
|
|
4797
|
+
state.cancelRequested = true;
|
|
4798
|
+
state.cancelCommandId = data.cancelCommandId || null;
|
|
4799
|
+
}
|
|
4800
|
+
state.controlErrorCount = 0;
|
|
4801
|
+
} catch (err) {
|
|
4802
|
+
state.controlErrorCount = (state.controlErrorCount || 0) + 1;
|
|
4803
|
+
if (/revoked|410/i.test(String(err.message || err))) {
|
|
4804
|
+
state.revoked = true;
|
|
4805
|
+
state.cancelRequested = true;
|
|
4806
|
+
state.active = false;
|
|
4807
|
+
break;
|
|
4808
|
+
}
|
|
4809
|
+
}
|
|
4810
|
+
await remoteControlSleep(Math.min(3000, 1000 + (state.controlErrorCount || 0) * 250));
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
|
|
4814
|
+
async function pollCloudRemoteControlLoop(opts, state) {
|
|
4815
|
+
let lastHeartbeatAt = 0;
|
|
4816
|
+
while (state.active) {
|
|
4817
|
+
let delayMs = 1200;
|
|
4818
|
+
try {
|
|
4819
|
+
const pollUrl = withRemoteControlQuery(
|
|
4820
|
+
state.pollUrl,
|
|
4821
|
+
state.baseUrl,
|
|
4822
|
+
{ agentToken: state.agentToken, after: state.lastCommandId || 0 }
|
|
4823
|
+
);
|
|
4824
|
+
const data = await fetchRemoteControlJson(pollUrl, { method: 'GET' }, 35000);
|
|
4825
|
+
if (data.revoked) {
|
|
4826
|
+
state.revoked = true;
|
|
4827
|
+
state.cancelRequested = true;
|
|
4828
|
+
state.active = false;
|
|
4829
|
+
console.log('Cloud Remote Control revoked by remote client.');
|
|
4830
|
+
break;
|
|
4831
|
+
}
|
|
4832
|
+
if (data.cancelRequested) {
|
|
4833
|
+
state.cancelRequested = true;
|
|
4834
|
+
state.cancelCommandId = data.cancelCommandId || null;
|
|
4835
|
+
}
|
|
4836
|
+
const commands = Array.isArray(data.commands) ? data.commands : [];
|
|
4837
|
+
state.lastSeenAt = Date.now();
|
|
4838
|
+
state.errorCount = 0;
|
|
4839
|
+
|
|
4840
|
+
for (const command of commands) {
|
|
4841
|
+
if (!state.active) break;
|
|
4842
|
+
const commandId = Number(command?.id || 0);
|
|
4843
|
+
if (commandId > (state.lastCommandId || 0)) state.lastCommandId = commandId;
|
|
4844
|
+
const text = String(command?.text || command?.prompt || '').trim();
|
|
4845
|
+
if (!text) continue;
|
|
4846
|
+
state.cancelRequested = false;
|
|
4847
|
+
state.cancelCommandId = null;
|
|
4848
|
+
state.busy = true;
|
|
4849
|
+
opts._remoteControlExecutionState = state;
|
|
4850
|
+
let result;
|
|
4851
|
+
try {
|
|
4852
|
+
result = await runRemoteControlPrompt(opts, text);
|
|
4853
|
+
} catch (err) {
|
|
4854
|
+
result = { ok: false, output: err.stack || err.message || String(err) };
|
|
4855
|
+
} finally {
|
|
4856
|
+
state.busy = false;
|
|
4857
|
+
delete opts._remoteControlExecutionState;
|
|
4858
|
+
}
|
|
4859
|
+
if (state.cancelRequested && result.ok) {
|
|
4860
|
+
result = { ok: false, output: 'Canceled by remote request.' };
|
|
4861
|
+
}
|
|
4862
|
+
await postCloudRemoteResult(state, commandId, result);
|
|
4863
|
+
if (state.cancelRequested) {
|
|
4864
|
+
state.cancelRequested = false;
|
|
4865
|
+
state.cancelCommandId = null;
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
|
|
4869
|
+
if (!commands.length && state.cancelRequested && !state.busy) {
|
|
4870
|
+
await postCloudRemoteHeartbeat(state, { cancelAck: true });
|
|
4871
|
+
state.cancelRequested = false;
|
|
4872
|
+
state.cancelCommandId = null;
|
|
4873
|
+
}
|
|
4874
|
+
if (!commands.length && Date.now() - lastHeartbeatAt > 10000) {
|
|
4875
|
+
lastHeartbeatAt = Date.now();
|
|
4876
|
+
await postCloudRemoteHeartbeat(state);
|
|
4877
|
+
}
|
|
4878
|
+
if (commands.length) delayMs = 250;
|
|
4879
|
+
} catch (err) {
|
|
4880
|
+
if (state.active) {
|
|
4881
|
+
state.errorCount = (state.errorCount || 0) + 1;
|
|
4882
|
+
const now = Date.now();
|
|
4883
|
+
if (!state.lastErrorAt || now - state.lastErrorAt > 15000) {
|
|
4884
|
+
console.error(`Cloud Remote Control error: ${err.message || String(err)}`);
|
|
4885
|
+
state.lastErrorAt = now;
|
|
4886
|
+
}
|
|
4887
|
+
delayMs = Math.min(5000, 1000 + state.errorCount * 500);
|
|
4888
|
+
}
|
|
4889
|
+
} finally {
|
|
4890
|
+
state.busy = false;
|
|
4891
|
+
}
|
|
4892
|
+
await remoteControlSleep(delayMs);
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
|
|
4896
|
+
async function startCloudRemoteControlSession(opts, overrides = {}) {
|
|
4897
|
+
if (opts.remoteControlCloudSession?.active) {
|
|
4898
|
+
console.log(`Cloud Remote Control already active: ${opts.remoteControlCloudSession.url}`);
|
|
4899
|
+
return opts.remoteControlCloudSession;
|
|
4900
|
+
}
|
|
4901
|
+
|
|
4902
|
+
const baseUrl = getRemoteControlRelayBase(opts, overrides);
|
|
4903
|
+
const name = getRemoteControlDisplayName(opts, overrides);
|
|
4904
|
+
const permission = getRemoteControlPermissionMode(opts, overrides);
|
|
4905
|
+
opts.remoteControlPermission = permission;
|
|
4906
|
+
const payload = {
|
|
4907
|
+
name,
|
|
4908
|
+
cwd: opts.cwd,
|
|
4909
|
+
version: CLI_VERSION,
|
|
4910
|
+
machine: os.hostname(),
|
|
4911
|
+
platform: process.platform,
|
|
4912
|
+
node: process.version,
|
|
4913
|
+
mode: opts.agent ? 'agent' : 'chat',
|
|
4914
|
+
permission
|
|
4915
|
+
};
|
|
4916
|
+
const data = await fetchRemoteControlJson(`${baseUrl}/api/bortex-code/remote/register`, {
|
|
4917
|
+
method: 'POST',
|
|
4918
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4919
|
+
body: JSON.stringify(payload)
|
|
4920
|
+
}, 30000);
|
|
4921
|
+
|
|
4922
|
+
const sessionId = String(data.sessionId || '').trim();
|
|
4923
|
+
const agentToken = String(data.agentToken || '').trim();
|
|
4924
|
+
if (!sessionId || !agentToken) {
|
|
4925
|
+
throw new Error('Remote relay registration did not return a session token.');
|
|
4926
|
+
}
|
|
4927
|
+
|
|
4928
|
+
const state = {
|
|
4929
|
+
active: true,
|
|
4930
|
+
cloud: true,
|
|
4931
|
+
baseUrl,
|
|
4932
|
+
sessionId,
|
|
4933
|
+
agentToken,
|
|
4934
|
+
clientToken: data.clientToken || '',
|
|
4935
|
+
name,
|
|
4936
|
+
permission,
|
|
4937
|
+
cwd: opts.cwd,
|
|
4938
|
+
url: data.url || `${baseUrl}/bortex-code/remote?session=${encodeURIComponent(sessionId)}`,
|
|
4939
|
+
pollUrl: data.pollUrl || `${baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(sessionId)}/poll`,
|
|
4940
|
+
resultUrl: data.resultUrl || `${baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(sessionId)}/result`,
|
|
4941
|
+
heartbeatUrl: data.heartbeatUrl || `${baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(sessionId)}/heartbeat`,
|
|
4942
|
+
controlUrl: data.controlUrl || `${baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(sessionId)}/control`,
|
|
4943
|
+
lastCommandId: 0,
|
|
4944
|
+
startedAt: Date.now(),
|
|
4945
|
+
busy: false,
|
|
4946
|
+
errorCount: 0
|
|
4947
|
+
};
|
|
4948
|
+
state.stop = async () => {
|
|
4949
|
+
state.active = false;
|
|
4950
|
+
};
|
|
4951
|
+
opts.remoteControlCloudSession = state;
|
|
4952
|
+
state.pollPromise = pollCloudRemoteControlLoop(opts, state).catch((err) => {
|
|
4953
|
+
if (state.active) console.error(`Cloud Remote Control stopped unexpectedly: ${err.message || String(err)}`);
|
|
4954
|
+
});
|
|
4955
|
+
state.controlPromise = pollCloudRemoteControlControlLoop(state).catch((err) => {
|
|
4956
|
+
if (state.active) console.error(`Cloud Remote Control control loop stopped unexpectedly: ${err.message || String(err)}`);
|
|
4957
|
+
});
|
|
4958
|
+
|
|
4959
|
+
console.log(`Cloud Remote Control active: ${state.name}`);
|
|
4960
|
+
console.log(`URL: ${state.url}`);
|
|
4961
|
+
console.log(`Permission: ${state.permission}`);
|
|
4962
|
+
console.log('No inbound port required. Keep this process running.');
|
|
4963
|
+
console.log('Security: keep this tokenized URL private. Anyone with the URL can control this Bortex Code session.');
|
|
4964
|
+
return state;
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
async function stopCloudRemoteControlSession(opts) {
|
|
4968
|
+
const state = opts.remoteControlCloudSession;
|
|
4969
|
+
if (!state?.active) {
|
|
4970
|
+
console.log('Cloud Remote Control is not active.');
|
|
4971
|
+
return false;
|
|
4972
|
+
}
|
|
4973
|
+
await state.stop();
|
|
4974
|
+
opts.remoteControlCloudSession = null;
|
|
4975
|
+
console.log('Cloud Remote Control stopped.');
|
|
4976
|
+
return true;
|
|
4977
|
+
}
|
|
4978
|
+
|
|
4499
4979
|
function parseRemoteControlSlashArgs(rest = [], opts = {}) {
|
|
4500
4980
|
const out = {
|
|
4501
4981
|
name: '',
|
|
4502
4982
|
host: opts.remoteControlHost || '127.0.0.1',
|
|
4503
|
-
port: Number(opts.remoteControlPort || 0) || 0
|
|
4983
|
+
port: Number(opts.remoteControlPort || 0) || 0,
|
|
4984
|
+
relayUrl: opts.remoteControlRelayUrl || '',
|
|
4985
|
+
permission: opts.remoteControlPermission || 'balanced',
|
|
4986
|
+
cloud: false
|
|
4504
4987
|
};
|
|
4505
4988
|
const nameParts = [];
|
|
4506
4989
|
for (let i = 0; i < rest.length; i += 1) {
|
|
@@ -4513,6 +4996,36 @@ function parseRemoteControlSlashArgs(rest = [], opts = {}) {
|
|
|
4513
4996
|
out.host = '0.0.0.0';
|
|
4514
4997
|
continue;
|
|
4515
4998
|
}
|
|
4999
|
+
if (a === 'cloud' || a === '--cloud' || a === '--remote-cloud' || a === '--relay') {
|
|
5000
|
+
out.cloud = true;
|
|
5001
|
+
continue;
|
|
5002
|
+
}
|
|
5003
|
+
if ((a === '--relay-url' || a === '--remote-relay') && rest[i + 1]) {
|
|
5004
|
+
out.relayUrl = String(rest[i + 1]);
|
|
5005
|
+
i += 1;
|
|
5006
|
+
continue;
|
|
5007
|
+
}
|
|
5008
|
+
if (a.startsWith('--relay-url=')) {
|
|
5009
|
+
out.relayUrl = a.slice('--relay-url='.length);
|
|
5010
|
+
continue;
|
|
5011
|
+
}
|
|
5012
|
+
if (a.startsWith('--remote-relay=')) {
|
|
5013
|
+
out.relayUrl = a.slice('--remote-relay='.length);
|
|
5014
|
+
continue;
|
|
5015
|
+
}
|
|
5016
|
+
if ((a === '--permission' || a === '--remote-permission') && rest[i + 1]) {
|
|
5017
|
+
out.permission = String(rest[i + 1]);
|
|
5018
|
+
i += 1;
|
|
5019
|
+
continue;
|
|
5020
|
+
}
|
|
5021
|
+
if (a.startsWith('--permission=')) {
|
|
5022
|
+
out.permission = a.slice('--permission='.length);
|
|
5023
|
+
continue;
|
|
5024
|
+
}
|
|
5025
|
+
if (a.startsWith('--remote-permission=')) {
|
|
5026
|
+
out.permission = a.slice('--remote-permission='.length);
|
|
5027
|
+
continue;
|
|
5028
|
+
}
|
|
4516
5029
|
if ((a === '--host' || a === '--remote-host') && rest[i + 1]) {
|
|
4517
5030
|
out.host = String(rest[i + 1]);
|
|
4518
5031
|
i += 1;
|
|
@@ -5835,13 +6348,18 @@ async function handleLocalCommand(opts, line) {
|
|
|
5835
6348
|
const done = todoItems.filter((t) => t.done).length;
|
|
5836
6349
|
const runPending = Array.isArray(opts.runState?.steps) ? opts.runState.steps.filter((s) => s.status === 'pending').length : 0;
|
|
5837
6350
|
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
|
|
6351
|
+
console.log(`Todo: ${done}/${todoItems.length} | Plan: ${opts.plan?.goal ? 'active' : 'none'} | Run: ${opts.runState?.goal ? `active (${runPending} pending)` : 'none'} | Remote: ${getRemoteControlStatus(opts)}`);
|
|
5839
6352
|
return { handled: true };
|
|
5840
6353
|
}
|
|
5841
6354
|
if (lc === '/remote-control' || lc === '/remote' || lc === '/rc') {
|
|
5842
6355
|
const parsed = parseRemoteControlSlashArgs(rest, opts);
|
|
5843
|
-
if (parsed.stop || opts.remoteControlSession?.server) {
|
|
5844
|
-
await stopRemoteControlServer(opts);
|
|
6356
|
+
if (parsed.stop || opts.remoteControlSession?.server || opts.remoteControlCloudSession?.active) {
|
|
6357
|
+
if (opts.remoteControlSession?.server) await stopRemoteControlServer(opts);
|
|
6358
|
+
if (opts.remoteControlCloudSession?.active) await stopCloudRemoteControlSession(opts);
|
|
6359
|
+
return { handled: true };
|
|
6360
|
+
}
|
|
6361
|
+
if (parsed.cloud) {
|
|
6362
|
+
await startCloudRemoteControlSession(opts, parsed);
|
|
5845
6363
|
return { handled: true };
|
|
5846
6364
|
}
|
|
5847
6365
|
await startRemoteControlServer(opts, parsed);
|
|
@@ -7010,6 +7528,7 @@ const SLASH_COMMANDS = [
|
|
|
7010
7528
|
['/help', 'Show command help'],
|
|
7011
7529
|
['/remote-control [name]', 'Control this local session from a browser'],
|
|
7012
7530
|
['/remote-control --lan', 'Expose Remote Control on the local network'],
|
|
7531
|
+
['/remote-control --cloud', 'Control this session through bortex.site relay'],
|
|
7013
7532
|
['/rc', 'Toggle Remote Control'],
|
|
7014
7533
|
['/llm-config show', 'Show cached LLM configuration'],
|
|
7015
7534
|
['/llm-config sync', 'Sync LLM configuration from Bortex'],
|
|
@@ -7186,11 +7705,18 @@ async function runRepl(opts) {
|
|
|
7186
7705
|
};
|
|
7187
7706
|
opts._askInput = async (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
7188
7707
|
if (opts.remoteControl) {
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7708
|
+
if (opts.remoteControlCloud) {
|
|
7709
|
+
await startCloudRemoteControlSession(opts, {
|
|
7710
|
+
name: opts.remoteControlName,
|
|
7711
|
+
relayUrl: opts.remoteControlRelayUrl
|
|
7712
|
+
});
|
|
7713
|
+
} else {
|
|
7714
|
+
await startRemoteControlServer(opts, {
|
|
7715
|
+
name: opts.remoteControlName,
|
|
7716
|
+
host: opts.remoteControlHost,
|
|
7717
|
+
port: opts.remoteControlPort
|
|
7718
|
+
});
|
|
7719
|
+
}
|
|
7194
7720
|
}
|
|
7195
7721
|
|
|
7196
7722
|
const question = () => new Promise((resolve) => {
|
|
@@ -7216,7 +7742,7 @@ async function runRepl(opts) {
|
|
|
7216
7742
|
const done = todoItems.filter((t) => t.done).length;
|
|
7217
7743
|
console.log(formatModeLine(opts));
|
|
7218
7744
|
const runPending = Array.isArray(opts.runState?.steps) ? opts.runState.steps.filter((s) => s.status === 'pending').length : 0;
|
|
7219
|
-
console.log(`Todo: ${done}/${todoItems.length} | Plan: ${opts.plan?.goal ? 'active' : 'none'} | Run: ${opts.runState?.goal ? `active (${runPending} pending)` : 'none'}`);
|
|
7745
|
+
console.log(`Todo: ${done}/${todoItems.length} | Plan: ${opts.plan?.goal ? 'active' : 'none'} | Run: ${opts.runState?.goal ? `active (${runPending} pending)` : 'none'} | Remote: ${getRemoteControlStatus(opts)}`);
|
|
7220
7746
|
continue;
|
|
7221
7747
|
}
|
|
7222
7748
|
if (line === '/help') {
|
|
@@ -7266,6 +7792,9 @@ async function runRepl(opts) {
|
|
|
7266
7792
|
if (opts.remoteControlSession?.server) {
|
|
7267
7793
|
await stopRemoteControlServer(opts);
|
|
7268
7794
|
}
|
|
7795
|
+
if (opts.remoteControlCloudSession?.active) {
|
|
7796
|
+
await stopCloudRemoteControlSession(opts);
|
|
7797
|
+
}
|
|
7269
7798
|
try { saveCliWorkspaceState(opts); } catch (_err) { }
|
|
7270
7799
|
delete opts._readMultiline;
|
|
7271
7800
|
delete opts._askInput;
|
|
@@ -7317,23 +7846,47 @@ async function main() {
|
|
|
7317
7846
|
}
|
|
7318
7847
|
|
|
7319
7848
|
if (opts.remoteControlServerMode) {
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7849
|
+
let remoteState = null;
|
|
7850
|
+
if (opts.remoteControlCloud) {
|
|
7851
|
+
remoteState = await startCloudRemoteControlSession(opts, {
|
|
7852
|
+
name: opts.remoteControlName,
|
|
7853
|
+
relayUrl: opts.remoteControlRelayUrl
|
|
7854
|
+
});
|
|
7855
|
+
} else {
|
|
7856
|
+
remoteState = await startRemoteControlServer(opts, {
|
|
7857
|
+
name: opts.remoteControlName,
|
|
7858
|
+
host: opts.remoteControlHost,
|
|
7859
|
+
port: opts.remoteControlPort
|
|
7860
|
+
});
|
|
7861
|
+
}
|
|
7325
7862
|
console.log('Server mode: keep this process running. Press Ctrl+C to stop Remote Control.');
|
|
7326
|
-
|
|
7863
|
+
const signalPromise = new Promise((resolve) => {
|
|
7327
7864
|
let stopping = false;
|
|
7328
7865
|
const stop = async () => {
|
|
7329
7866
|
if (stopping) return;
|
|
7330
7867
|
stopping = true;
|
|
7331
|
-
|
|
7868
|
+
if (opts.remoteControlSession?.server) {
|
|
7869
|
+
try { await stopRemoteControlServer(opts); } catch (_err) {}
|
|
7870
|
+
}
|
|
7871
|
+
if (opts.remoteControlCloudSession?.active) {
|
|
7872
|
+
try { await stopCloudRemoteControlSession(opts); } catch (_err) {}
|
|
7873
|
+
}
|
|
7332
7874
|
resolve();
|
|
7333
7875
|
};
|
|
7334
7876
|
process.once('SIGINT', stop);
|
|
7335
7877
|
process.once('SIGTERM', stop);
|
|
7336
7878
|
});
|
|
7879
|
+
const remoteDonePromises = [];
|
|
7880
|
+
if (remoteState?.pollPromise) remoteDonePromises.push(remoteState.pollPromise);
|
|
7881
|
+
if (remoteState?.controlPromise) remoteDonePromises.push(remoteState.controlPromise);
|
|
7882
|
+
if (remoteDonePromises.length) {
|
|
7883
|
+
await Promise.race([signalPromise, ...remoteDonePromises]);
|
|
7884
|
+
if (opts.remoteControlCloudSession?.active) {
|
|
7885
|
+
await stopCloudRemoteControlSession(opts);
|
|
7886
|
+
}
|
|
7887
|
+
} else {
|
|
7888
|
+
await signalPromise;
|
|
7889
|
+
}
|
|
7337
7890
|
return;
|
|
7338
7891
|
}
|
|
7339
7892
|
|