bortexcode 1.4.0 → 1.5.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 +146 -16
- 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}`);
|
|
@@ -4777,6 +4837,73 @@ async function postCloudRemoteResult(state, commandId, result) {
|
|
|
4777
4837
|
}, 35000);
|
|
4778
4838
|
}
|
|
4779
4839
|
|
|
4840
|
+
async function postCloudRemoteChunk(state, commandId, stream, text) {
|
|
4841
|
+
if (!state?.chunkUrl || !text) return;
|
|
4842
|
+
const chunkUrl = withRemoteControlQuery(
|
|
4843
|
+
state.chunkUrl,
|
|
4844
|
+
state.baseUrl,
|
|
4845
|
+
{ agentToken: state.agentToken }
|
|
4846
|
+
);
|
|
4847
|
+
await fetchRemoteControlJson(chunkUrl, {
|
|
4848
|
+
method: 'POST',
|
|
4849
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4850
|
+
body: JSON.stringify({
|
|
4851
|
+
commandId,
|
|
4852
|
+
stream: stream === 'stderr' ? 'stderr' : 'stdout',
|
|
4853
|
+
text: String(text || '')
|
|
4854
|
+
})
|
|
4855
|
+
}, 12000);
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4858
|
+
function createCloudRemoteChunkStreamer(state, commandId) {
|
|
4859
|
+
const queue = [];
|
|
4860
|
+
let timer = null;
|
|
4861
|
+
let flushing = false;
|
|
4862
|
+
const schedule = (ms = 250) => {
|
|
4863
|
+
if (timer) return;
|
|
4864
|
+
timer = setTimeout(() => {
|
|
4865
|
+
timer = null;
|
|
4866
|
+
flush().catch(() => {});
|
|
4867
|
+
}, ms);
|
|
4868
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
4869
|
+
};
|
|
4870
|
+
const flush = async () => {
|
|
4871
|
+
if (flushing) return;
|
|
4872
|
+
flushing = true;
|
|
4873
|
+
if (timer) {
|
|
4874
|
+
clearTimeout(timer);
|
|
4875
|
+
timer = null;
|
|
4876
|
+
}
|
|
4877
|
+
try {
|
|
4878
|
+
while (queue.length) {
|
|
4879
|
+
const pending = queue.splice(0, queue.length);
|
|
4880
|
+
const grouped = new Map();
|
|
4881
|
+
for (const item of pending) {
|
|
4882
|
+
const stream = item.stream === 'stderr' ? 'stderr' : 'stdout';
|
|
4883
|
+
grouped.set(stream, `${grouped.get(stream) || ''}${item.text || ''}`);
|
|
4884
|
+
}
|
|
4885
|
+
for (const [stream, text] of grouped.entries()) {
|
|
4886
|
+
if (!text) continue;
|
|
4887
|
+
await postCloudRemoteChunk(state, commandId, stream, text).catch((err) => {
|
|
4888
|
+
state.lastChunkError = err.message || String(err);
|
|
4889
|
+
});
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
} finally {
|
|
4893
|
+
flushing = false;
|
|
4894
|
+
}
|
|
4895
|
+
};
|
|
4896
|
+
return {
|
|
4897
|
+
push(stream, text) {
|
|
4898
|
+
if (!text || !state.active) return;
|
|
4899
|
+
queue.push({ stream, text: String(text || '') });
|
|
4900
|
+
const queuedSize = queue.reduce((n, item) => n + String(item.text || '').length, 0);
|
|
4901
|
+
schedule(queuedSize > 3000 ? 25 : 250);
|
|
4902
|
+
},
|
|
4903
|
+
flush
|
|
4904
|
+
};
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4780
4907
|
async function pollCloudRemoteControlControlLoop(state) {
|
|
4781
4908
|
while (state.active) {
|
|
4782
4909
|
try {
|
|
@@ -4847,12 +4974,14 @@ async function pollCloudRemoteControlLoop(opts, state) {
|
|
|
4847
4974
|
state.cancelCommandId = null;
|
|
4848
4975
|
state.busy = true;
|
|
4849
4976
|
opts._remoteControlExecutionState = state;
|
|
4977
|
+
const streamer = createCloudRemoteChunkStreamer(state, commandId);
|
|
4850
4978
|
let result;
|
|
4851
4979
|
try {
|
|
4852
|
-
result = await runRemoteControlPrompt(opts, text);
|
|
4980
|
+
result = await runRemoteControlPrompt(opts, text, (stream, chunk) => streamer.push(stream, chunk));
|
|
4853
4981
|
} catch (err) {
|
|
4854
4982
|
result = { ok: false, output: err.stack || err.message || String(err) };
|
|
4855
4983
|
} finally {
|
|
4984
|
+
await streamer.flush().catch(() => {});
|
|
4856
4985
|
state.busy = false;
|
|
4857
4986
|
delete opts._remoteControlExecutionState;
|
|
4858
4987
|
}
|
|
@@ -4938,6 +5067,7 @@ async function startCloudRemoteControlSession(opts, overrides = {}) {
|
|
|
4938
5067
|
url: data.url || `${baseUrl}/bortex-code/remote?session=${encodeURIComponent(sessionId)}`,
|
|
4939
5068
|
pollUrl: data.pollUrl || `${baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(sessionId)}/poll`,
|
|
4940
5069
|
resultUrl: data.resultUrl || `${baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(sessionId)}/result`,
|
|
5070
|
+
chunkUrl: data.chunkUrl || `${baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(sessionId)}/chunk`,
|
|
4941
5071
|
heartbeatUrl: data.heartbeatUrl || `${baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(sessionId)}/heartbeat`,
|
|
4942
5072
|
controlUrl: data.controlUrl || `${baseUrl}/api/bortex-code/remote/session/${encodeURIComponent(sessionId)}/control`,
|
|
4943
5073
|
lastCommandId: 0,
|