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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/bin/bortex.js +201 -17
  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}`);
@@ -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: { 'Content-Type': 'application/json' },
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.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bortexcode",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Bortex Code CLI - AI coding assistant powered by bortex.site",
5
5
  "homepage": "https://bortex.site",
6
6
  "license": "UNLICENSED",