bortexcode 1.4.0 → 1.6.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 +4 -0
- package/bin/bortex.js +201 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,6 +107,10 @@ The browser UI includes Cancel and Revoke controls. Cancel requests termination
|
|
|
107
107
|
of the running command. Revoke invalidates the tokenized URL and stops the
|
|
108
108
|
server-mode cloud session.
|
|
109
109
|
|
|
110
|
+
Remote output streams live for shell commands and CLI output: stdout/stderr
|
|
111
|
+
chunks appear in the browser while the command is still running, then the final
|
|
112
|
+
result replaces the streaming buffer when the command completes.
|
|
113
|
+
|
|
110
114
|
Inside the REPL:
|
|
111
115
|
|
|
112
116
|
```text
|
package/bin/bortex.js
CHANGED
|
@@ -3983,6 +3983,13 @@ function runChild(command, args, { cwd, shell = false } = {}) {
|
|
|
3983
3983
|
let stderr = '';
|
|
3984
3984
|
let settled = false;
|
|
3985
3985
|
let canceled = false;
|
|
3986
|
+
const emitChunk = (stream, text) => {
|
|
3987
|
+
try {
|
|
3988
|
+
if (cancelState && typeof cancelState.onChunk === 'function') {
|
|
3989
|
+
cancelState.onChunk(stream, text);
|
|
3990
|
+
}
|
|
3991
|
+
} catch (_err) {}
|
|
3992
|
+
};
|
|
3986
3993
|
const finish = (result) => {
|
|
3987
3994
|
if (settled) return;
|
|
3988
3995
|
settled = true;
|
|
@@ -3996,8 +4003,16 @@ function runChild(command, args, { cwd, shell = false } = {}) {
|
|
|
3996
4003
|
}
|
|
3997
4004
|
}, 250) : null;
|
|
3998
4005
|
if (cancelTimer && typeof cancelTimer.unref === 'function') cancelTimer.unref();
|
|
3999
|
-
child.stdout.on('data', (c) => {
|
|
4000
|
-
|
|
4006
|
+
child.stdout.on('data', (c) => {
|
|
4007
|
+
const text = String(c || '');
|
|
4008
|
+
stdout += text;
|
|
4009
|
+
emitChunk('stdout', text);
|
|
4010
|
+
});
|
|
4011
|
+
child.stderr.on('data', (c) => {
|
|
4012
|
+
const text = String(c || '');
|
|
4013
|
+
stderr += text;
|
|
4014
|
+
emitChunk('stderr', text);
|
|
4015
|
+
});
|
|
4001
4016
|
child.on('error', (err) => finish({ ok: false, code: null, stdout, stderr: err.message }));
|
|
4002
4017
|
child.on('close', (code) => {
|
|
4003
4018
|
const finalStderr = canceled
|
|
@@ -4363,10 +4378,12 @@ function buildRemoteControlHtml(state) {
|
|
|
4363
4378
|
const statusEl = document.getElementById('status');
|
|
4364
4379
|
const promptEl = document.getElementById('prompt');
|
|
4365
4380
|
const sendEl = document.getElementById('send');
|
|
4366
|
-
let
|
|
4381
|
+
let lastSignature = '';
|
|
4367
4382
|
function renderLog(items) {
|
|
4368
|
-
if (!Array.isArray(items)
|
|
4369
|
-
|
|
4383
|
+
if (!Array.isArray(items)) return;
|
|
4384
|
+
const signature = items.map((item, index) => String(item.id || index) + ':' + String(item.ts || '') + ':' + String(item.text || '').length).join('|');
|
|
4385
|
+
if (signature === lastSignature) return;
|
|
4386
|
+
lastSignature = signature;
|
|
4370
4387
|
logEl.innerHTML = items.map((item) => {
|
|
4371
4388
|
const role = String(item.role || 'system');
|
|
4372
4389
|
const text = String(item.text || '');
|
|
@@ -4412,17 +4429,21 @@ function buildRemoteControlHtml(state) {
|
|
|
4412
4429
|
</html>`;
|
|
4413
4430
|
}
|
|
4414
4431
|
|
|
4415
|
-
async function captureRemoteControlOutput(fn) {
|
|
4432
|
+
async function captureRemoteControlOutput(fn, onChunk) {
|
|
4416
4433
|
let stdout = '';
|
|
4417
4434
|
let stderr = '';
|
|
4418
4435
|
const originalStdoutWrite = process.stdout.write;
|
|
4419
4436
|
const originalStderrWrite = process.stderr.write;
|
|
4420
4437
|
process.stdout.write = function patchedStdoutWrite(chunk, encoding, cb) {
|
|
4421
|
-
|
|
4438
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString(typeof encoding === 'string' ? encoding : 'utf8') : String(chunk ?? '');
|
|
4439
|
+
stdout += text;
|
|
4440
|
+
try { if (typeof onChunk === 'function') onChunk('stdout', text); } catch (_err) {}
|
|
4422
4441
|
return originalStdoutWrite.apply(process.stdout, arguments);
|
|
4423
4442
|
};
|
|
4424
4443
|
process.stderr.write = function patchedStderrWrite(chunk, encoding, cb) {
|
|
4425
|
-
|
|
4444
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString(typeof encoding === 'string' ? encoding : 'utf8') : String(chunk ?? '');
|
|
4445
|
+
stderr += text;
|
|
4446
|
+
try { if (typeof onChunk === 'function') onChunk('stderr', text); } catch (_err) {}
|
|
4426
4447
|
return originalStderrWrite.apply(process.stderr, arguments);
|
|
4427
4448
|
};
|
|
4428
4449
|
try {
|
|
@@ -4434,7 +4455,7 @@ async function captureRemoteControlOutput(fn) {
|
|
|
4434
4455
|
return { stdout, stderr };
|
|
4435
4456
|
}
|
|
4436
4457
|
|
|
4437
|
-
async function runRemoteControlPrompt(opts, text) {
|
|
4458
|
+
async function runRemoteControlPrompt(opts, text, onChunk) {
|
|
4438
4459
|
const line = String(text || '').trim();
|
|
4439
4460
|
if (!line) return { ok: false, output: 'Empty prompt.' };
|
|
4440
4461
|
const permission = assessRemoteControlPromptPermission(opts, line);
|
|
@@ -4447,7 +4468,10 @@ async function runRemoteControlPrompt(opts, text) {
|
|
|
4447
4468
|
pushCliHistory(opts, `[remote] ${line}`);
|
|
4448
4469
|
try { saveCliWorkspaceState(opts); } catch (_err) {}
|
|
4449
4470
|
const previousRemoteExecution = ACTIVE_REMOTE_CONTROL_EXECUTION_STATE;
|
|
4450
|
-
ACTIVE_REMOTE_CONTROL_EXECUTION_STATE =
|
|
4471
|
+
ACTIVE_REMOTE_CONTROL_EXECUTION_STATE = {
|
|
4472
|
+
...(opts._remoteControlExecutionState || {}),
|
|
4473
|
+
onChunk
|
|
4474
|
+
};
|
|
4451
4475
|
let captured;
|
|
4452
4476
|
try {
|
|
4453
4477
|
captured = await captureRemoteControlOutput(async () => {
|
|
@@ -4477,7 +4501,7 @@ async function runRemoteControlPrompt(opts, text) {
|
|
|
4477
4501
|
}
|
|
4478
4502
|
const data = await askServer(opts, line);
|
|
4479
4503
|
printResponse(opts, data);
|
|
4480
|
-
});
|
|
4504
|
+
}, onChunk);
|
|
4481
4505
|
} finally {
|
|
4482
4506
|
ACTIVE_REMOTE_CONTROL_EXECUTION_STATE = previousRemoteExecution;
|
|
4483
4507
|
}
|
|
@@ -4506,10 +4530,37 @@ async function startRemoteControlServer(opts, overrides = {}) {
|
|
|
4506
4530
|
cwd: opts.cwd,
|
|
4507
4531
|
startedAt: Date.now(),
|
|
4508
4532
|
busy: false,
|
|
4509
|
-
log: []
|
|
4533
|
+
log: [],
|
|
4534
|
+
nextLogId: 1
|
|
4510
4535
|
};
|
|
4511
4536
|
const appendLog = (role, text) => {
|
|
4512
|
-
state.log.push({ role, text: String(text || ''), ts: Date.now() });
|
|
4537
|
+
state.log.push({ id: state.nextLogId++, role, text: String(text || ''), ts: Date.now() });
|
|
4538
|
+
if (state.log.length > 200) state.log.splice(0, state.log.length - 200);
|
|
4539
|
+
};
|
|
4540
|
+
const appendStreamChunk = (streamKey, text, commandId) => {
|
|
4541
|
+
const chunk = String(text || '');
|
|
4542
|
+
if (!chunk) return;
|
|
4543
|
+
let item = null;
|
|
4544
|
+
for (let i = state.log.length - 1; i >= 0; i -= 1) {
|
|
4545
|
+
const candidate = state.log[i];
|
|
4546
|
+
if (candidate?.streaming && candidate.commandId === commandId) {
|
|
4547
|
+
item = candidate;
|
|
4548
|
+
break;
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
if (!item) {
|
|
4552
|
+
item = {
|
|
4553
|
+
id: state.nextLogId++,
|
|
4554
|
+
role: streamKey === 'stderr' ? 'system' : 'assistant',
|
|
4555
|
+
text: '',
|
|
4556
|
+
ts: Date.now(),
|
|
4557
|
+
streaming: true,
|
|
4558
|
+
commandId
|
|
4559
|
+
};
|
|
4560
|
+
state.log.push(item);
|
|
4561
|
+
}
|
|
4562
|
+
item.text = `${item.text || ''}${chunk}`.slice(-20000);
|
|
4563
|
+
item.ts = Date.now();
|
|
4513
4564
|
if (state.log.length > 200) state.log.splice(0, state.log.length - 200);
|
|
4514
4565
|
};
|
|
4515
4566
|
appendLog('system', `Remote Control started for ${state.cwd}`);
|
|
@@ -4562,8 +4613,17 @@ async function startRemoteControlServer(opts, overrides = {}) {
|
|
|
4562
4613
|
const text = String(payload.text || payload.prompt || '').trim();
|
|
4563
4614
|
if (!text) return sendJson(res, 400, { ok: false, error: 'empty prompt' });
|
|
4564
4615
|
appendLog('user', text);
|
|
4565
|
-
const
|
|
4566
|
-
|
|
4616
|
+
const commandId = state.nextLogId;
|
|
4617
|
+
const result = await runRemoteControlPrompt(opts, text, (stream, chunk) => appendStreamChunk(stream, chunk, commandId));
|
|
4618
|
+
const streamLog = state.log.find((item) => item?.streaming && item.commandId === commandId);
|
|
4619
|
+
if (streamLog) {
|
|
4620
|
+
streamLog.streaming = false;
|
|
4621
|
+
streamLog.role = result.ok ? 'assistant' : 'system';
|
|
4622
|
+
streamLog.text = result.output || streamLog.text || '(no output)';
|
|
4623
|
+
streamLog.ts = Date.now();
|
|
4624
|
+
} else {
|
|
4625
|
+
appendLog('assistant', result.output || '(no output)');
|
|
4626
|
+
}
|
|
4567
4627
|
return sendJson(res, result.ok ? 200 : 500, { ok: !!result.ok, output: result.output || '' });
|
|
4568
4628
|
} catch (err) {
|
|
4569
4629
|
appendLog('system', `Error: ${err.message}`);
|
|
@@ -4692,6 +4752,7 @@ function assessRemoteControlPromptPermission(opts, line) {
|
|
|
4692
4752
|
const mode = getRemoteControlPermissionMode(opts);
|
|
4693
4753
|
const text = String(line || '').trim();
|
|
4694
4754
|
if (!text) return { ok: false, mode, reason: 'empty prompt' };
|
|
4755
|
+
if (opts._remoteControlApprovalBypass) return { ok: true, mode, approved: true };
|
|
4695
4756
|
if (mode === 'full') return { ok: true, mode };
|
|
4696
4757
|
if (!text.startsWith('/')) return { ok: true, mode, skipNaturalActions: true };
|
|
4697
4758
|
if (mode === 'read-only') {
|
|
@@ -4777,6 +4838,91 @@ async function postCloudRemoteResult(state, commandId, result) {
|
|
|
4777
4838
|
}, 35000);
|
|
4778
4839
|
}
|
|
4779
4840
|
|
|
4841
|
+
async function postCloudRemoteChunk(state, commandId, stream, text) {
|
|
4842
|
+
if (!state?.chunkUrl || !text) return;
|
|
4843
|
+
const chunkUrl = withRemoteControlQuery(
|
|
4844
|
+
state.chunkUrl,
|
|
4845
|
+
state.baseUrl,
|
|
4846
|
+
{ agentToken: state.agentToken }
|
|
4847
|
+
);
|
|
4848
|
+
await fetchRemoteControlJson(chunkUrl, {
|
|
4849
|
+
method: 'POST',
|
|
4850
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4851
|
+
body: JSON.stringify({
|
|
4852
|
+
commandId,
|
|
4853
|
+
stream: stream === 'stderr' ? 'stderr' : 'stdout',
|
|
4854
|
+
text: String(text || '')
|
|
4855
|
+
})
|
|
4856
|
+
}, 12000);
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4859
|
+
async function postCloudRemoteApprovalRequest(state, commandId, permission, text) {
|
|
4860
|
+
const approvalUrl = withRemoteControlQuery(
|
|
4861
|
+
state.approvalUrl || `${state.baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(state.sessionId)}/approval-request`,
|
|
4862
|
+
state.baseUrl,
|
|
4863
|
+
{ agentToken: state.agentToken }
|
|
4864
|
+
);
|
|
4865
|
+
await fetchRemoteControlJson(approvalUrl, {
|
|
4866
|
+
method: 'POST',
|
|
4867
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4868
|
+
body: JSON.stringify({
|
|
4869
|
+
commandId,
|
|
4870
|
+
text: String(text || ''),
|
|
4871
|
+
mode: permission?.mode || state.permission || 'balanced',
|
|
4872
|
+
reason: permission?.reason || 'This command requires explicit approval.'
|
|
4873
|
+
})
|
|
4874
|
+
}, 12000);
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4877
|
+
function createCloudRemoteChunkStreamer(state, commandId) {
|
|
4878
|
+
const queue = [];
|
|
4879
|
+
let timer = null;
|
|
4880
|
+
let flushing = false;
|
|
4881
|
+
const schedule = (ms = 250) => {
|
|
4882
|
+
if (timer) return;
|
|
4883
|
+
timer = setTimeout(() => {
|
|
4884
|
+
timer = null;
|
|
4885
|
+
flush().catch(() => {});
|
|
4886
|
+
}, ms);
|
|
4887
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
4888
|
+
};
|
|
4889
|
+
const flush = async () => {
|
|
4890
|
+
if (flushing) return;
|
|
4891
|
+
flushing = true;
|
|
4892
|
+
if (timer) {
|
|
4893
|
+
clearTimeout(timer);
|
|
4894
|
+
timer = null;
|
|
4895
|
+
}
|
|
4896
|
+
try {
|
|
4897
|
+
while (queue.length) {
|
|
4898
|
+
const pending = queue.splice(0, queue.length);
|
|
4899
|
+
const grouped = new Map();
|
|
4900
|
+
for (const item of pending) {
|
|
4901
|
+
const stream = item.stream === 'stderr' ? 'stderr' : 'stdout';
|
|
4902
|
+
grouped.set(stream, `${grouped.get(stream) || ''}${item.text || ''}`);
|
|
4903
|
+
}
|
|
4904
|
+
for (const [stream, text] of grouped.entries()) {
|
|
4905
|
+
if (!text) continue;
|
|
4906
|
+
await postCloudRemoteChunk(state, commandId, stream, text).catch((err) => {
|
|
4907
|
+
state.lastChunkError = err.message || String(err);
|
|
4908
|
+
});
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4911
|
+
} finally {
|
|
4912
|
+
flushing = false;
|
|
4913
|
+
}
|
|
4914
|
+
};
|
|
4915
|
+
return {
|
|
4916
|
+
push(stream, text) {
|
|
4917
|
+
if (!text || !state.active) return;
|
|
4918
|
+
queue.push({ stream, text: String(text || '') });
|
|
4919
|
+
const queuedSize = queue.reduce((n, item) => n + String(item.text || '').length, 0);
|
|
4920
|
+
schedule(queuedSize > 3000 ? 25 : 250);
|
|
4921
|
+
},
|
|
4922
|
+
flush
|
|
4923
|
+
};
|
|
4924
|
+
}
|
|
4925
|
+
|
|
4780
4926
|
async function pollCloudRemoteControlControlLoop(state) {
|
|
4781
4927
|
while (state.active) {
|
|
4782
4928
|
try {
|
|
@@ -4845,16 +4991,42 @@ async function pollCloudRemoteControlLoop(opts, state) {
|
|
|
4845
4991
|
if (!text) continue;
|
|
4846
4992
|
state.cancelRequested = false;
|
|
4847
4993
|
state.cancelCommandId = null;
|
|
4994
|
+
const approved = !!command.approved;
|
|
4995
|
+
const permission = assessRemoteControlPromptPermission(opts, text);
|
|
4996
|
+
if (!permission.ok && !approved) {
|
|
4997
|
+
state.busy = true;
|
|
4998
|
+
opts._remoteControlExecutionState = state;
|
|
4999
|
+
try {
|
|
5000
|
+
await postCloudRemoteApprovalRequest(state, commandId, permission, text);
|
|
5001
|
+
console.log(`Cloud Remote Control approval requested for command #${commandId}: ${permission.reason}.`);
|
|
5002
|
+
} catch (err) {
|
|
5003
|
+
const message = err.message || String(err);
|
|
5004
|
+
await postCloudRemoteResult(state, commandId, {
|
|
5005
|
+
ok: false,
|
|
5006
|
+
output: `Approval request failed: ${message}`
|
|
5007
|
+
}).catch(() => {});
|
|
5008
|
+
} finally {
|
|
5009
|
+
state.busy = false;
|
|
5010
|
+
delete opts._remoteControlExecutionState;
|
|
5011
|
+
}
|
|
5012
|
+
continue;
|
|
5013
|
+
}
|
|
4848
5014
|
state.busy = true;
|
|
4849
5015
|
opts._remoteControlExecutionState = state;
|
|
5016
|
+
const streamer = createCloudRemoteChunkStreamer(state, commandId);
|
|
4850
5017
|
let result;
|
|
5018
|
+
const previousApprovalBypass = opts._remoteControlApprovalBypass;
|
|
5019
|
+
if (approved) opts._remoteControlApprovalBypass = true;
|
|
4851
5020
|
try {
|
|
4852
|
-
result = await runRemoteControlPrompt(opts, text);
|
|
5021
|
+
result = await runRemoteControlPrompt(opts, text, (stream, chunk) => streamer.push(stream, chunk));
|
|
4853
5022
|
} catch (err) {
|
|
4854
5023
|
result = { ok: false, output: err.stack || err.message || String(err) };
|
|
4855
5024
|
} finally {
|
|
5025
|
+
await streamer.flush().catch(() => {});
|
|
4856
5026
|
state.busy = false;
|
|
4857
5027
|
delete opts._remoteControlExecutionState;
|
|
5028
|
+
if (previousApprovalBypass === undefined) delete opts._remoteControlApprovalBypass;
|
|
5029
|
+
else opts._remoteControlApprovalBypass = previousApprovalBypass;
|
|
4858
5030
|
}
|
|
4859
5031
|
if (state.cancelRequested && result.ok) {
|
|
4860
5032
|
result = { ok: false, output: 'Canceled by remote request.' };
|
|
@@ -4903,6 +5075,11 @@ async function startCloudRemoteControlSession(opts, overrides = {}) {
|
|
|
4903
5075
|
const name = getRemoteControlDisplayName(opts, overrides);
|
|
4904
5076
|
const permission = getRemoteControlPermissionMode(opts, overrides);
|
|
4905
5077
|
opts.remoteControlPermission = permission;
|
|
5078
|
+
const sharedSettings = loadSharedSettings();
|
|
5079
|
+
const registerCookie = getCookieHeaderFromSharedSettings();
|
|
5080
|
+
const registerApiKey = String(
|
|
5081
|
+
opts.apiKey || process.env.BORTEX_API_KEY || sharedSettings?.llmSettings?.apiKey || ''
|
|
5082
|
+
).trim();
|
|
4906
5083
|
const payload = {
|
|
4907
5084
|
name,
|
|
4908
5085
|
cwd: opts.cwd,
|
|
@@ -4913,9 +5090,12 @@ async function startCloudRemoteControlSession(opts, overrides = {}) {
|
|
|
4913
5090
|
mode: opts.agent ? 'agent' : 'chat',
|
|
4914
5091
|
permission
|
|
4915
5092
|
};
|
|
5093
|
+
const registerHeaders = { 'Content-Type': 'application/json' };
|
|
5094
|
+
if (registerCookie) registerHeaders.Cookie = registerCookie;
|
|
5095
|
+
if (registerApiKey) registerHeaders['x-api-key'] = registerApiKey;
|
|
4916
5096
|
const data = await fetchRemoteControlJson(`${baseUrl}/api/bortex-code/remote/register`, {
|
|
4917
5097
|
method: 'POST',
|
|
4918
|
-
headers:
|
|
5098
|
+
headers: registerHeaders,
|
|
4919
5099
|
body: JSON.stringify(payload)
|
|
4920
5100
|
}, 30000);
|
|
4921
5101
|
|
|
@@ -4938,6 +5118,8 @@ async function startCloudRemoteControlSession(opts, overrides = {}) {
|
|
|
4938
5118
|
url: data.url || `${baseUrl}/bortex-code/remote?session=${encodeURIComponent(sessionId)}`,
|
|
4939
5119
|
pollUrl: data.pollUrl || `${baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(sessionId)}/poll`,
|
|
4940
5120
|
resultUrl: data.resultUrl || `${baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(sessionId)}/result`,
|
|
5121
|
+
chunkUrl: data.chunkUrl || `${baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(sessionId)}/chunk`,
|
|
5122
|
+
approvalUrl: data.approvalUrl || `${baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(sessionId)}/approval-request`,
|
|
4941
5123
|
heartbeatUrl: data.heartbeatUrl || `${baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(sessionId)}/heartbeat`,
|
|
4942
5124
|
controlUrl: data.controlUrl || `${baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(sessionId)}/control`,
|
|
4943
5125
|
lastCommandId: 0,
|
|
@@ -4958,6 +5140,8 @@ async function startCloudRemoteControlSession(opts, overrides = {}) {
|
|
|
4958
5140
|
|
|
4959
5141
|
console.log(`Cloud Remote Control active: ${state.name}`);
|
|
4960
5142
|
console.log(`URL: ${state.url}`);
|
|
5143
|
+
if (data.accountUrl) console.log(`Account URL: ${data.accountUrl}`);
|
|
5144
|
+
if (data.sessionsUrl) console.log(`Sessions: ${data.sessionsUrl}`);
|
|
4961
5145
|
console.log(`Permission: ${state.permission}`);
|
|
4962
5146
|
console.log('No inbound port required. Keep this process running.');
|
|
4963
5147
|
console.log('Security: keep this tokenized URL private. Anyone with the URL can control this Bortex Code session.');
|