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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/bin/bortex.js +146 -16
  3. 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) => { stdout += String(c || ''); });
4000
- child.stderr.on('data', (c) => { stderr += String(c || ''); });
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 lastCount = -1;
4381
+ let lastSignature = '';
4367
4382
  function renderLog(items) {
4368
- if (!Array.isArray(items) || items.length === lastCount) return;
4369
- lastCount = items.length;
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
- stdout += Buffer.isBuffer(chunk) ? chunk.toString(typeof encoding === 'string' ? encoding : 'utf8') : String(chunk ?? '');
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
- stderr += Buffer.isBuffer(chunk) ? chunk.toString(typeof encoding === 'string' ? encoding : 'utf8') : String(chunk ?? '');
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 = opts._remoteControlExecutionState || null;
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 result = await runRemoteControlPrompt(opts, text);
4566
- appendLog('assistant', result.output || '(no output)');
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bortexcode",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Bortex Code CLI - AI coding assistant powered by bortex.site",
5
5
  "homepage": "https://bortex.site",
6
6
  "license": "UNLICENSED",