autodev-cli 1.4.48 → 1.4.51

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.
@@ -39,14 +39,14 @@ const tls = __importStar(require("tls"));
39
39
  const crypto = __importStar(require("crypto"));
40
40
  const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
- const vnc_1 = require("./vnc");
43
- const rdp_1 = require("./rdp");
42
+ const manager_1 = require("./vnc/manager");
43
+ const manager_2 = require("./rdp/manager");
44
44
  const messageBuilder_1 = require("./messageBuilder");
45
45
  const todo_1 = require("./todo");
46
46
  const todoWriteManager_1 = require("./todoWriteManager");
47
47
  const commands_1 = require("./core/commands");
48
- const pathSafe_1 = require("./core/pathSafe");
49
- const gitService = __importStar(require("./git/gitService"));
48
+ const fileBrowser_1 = require("./fileBrowser");
49
+ const gitRequest_1 = require("./git/gitRequest");
50
50
  const wsHandshake_1 = require("./wsHandshake");
51
51
  // ---------------------------------------------------------------------------
52
52
  // WebSocketPoller — persistent WS connection for ws:// / wss:// endpoints
@@ -64,14 +64,12 @@ class WebSocketPoller {
64
64
  _reconnectTimer = null;
65
65
  _log = () => { };
66
66
  static RECONNECT_DELAY_MS = 5_000;
67
- _vncPassword;
68
- _vncSessions = new Map();
69
- _rdpSessions = new Map();
70
- _rdpSettings = {};
67
+ // VNC / RDP remote-desktop sessions are managed by shared session managers so
68
+ // the MCP-only bridge (mcp-operate) can reuse the exact same machinery.
69
+ _vncManager = new manager_1.VncSessionManager((frame) => { this.sendFrame(frame); }, (m) => this._log(m));
70
+ _rdpManager = new manager_2.RdpSessionManager((frame) => { this.sendFrame(frame); }, (m) => this._log(m));
71
71
  _gitEnabled = false;
72
72
  _fileBrowserEnabled = false;
73
- _vncEnabled = false;
74
- _rdpEnabled = false;
75
73
  // WebSocket message reassembly (fragmented frames: FIN=0 start + 0x0 continuations).
76
74
  _fragOpcode = 0;
77
75
  _fragChunks = [];
@@ -178,8 +176,8 @@ class WebSocketPoller {
178
176
  destroy() {
179
177
  this._destroyed = true;
180
178
  this._stopHeartbeat();
181
- this._stopAllVncSessions();
182
- this._stopAllRdpSessions();
179
+ this._vncManager.stopAll();
180
+ this._rdpManager.stopAll();
183
181
  if (this._reconnectTimer) {
184
182
  clearTimeout(this._reconnectTimer);
185
183
  this._reconnectTimer = null;
@@ -303,8 +301,8 @@ class WebSocketPoller {
303
301
  }
304
302
  _scheduleReconnect() {
305
303
  this._stopHeartbeat();
306
- this._stopAllVncSessions();
307
- this._stopAllRdpSessions();
304
+ this._vncManager.stopAll();
305
+ this._rdpManager.stopAll();
308
306
  if (this._destroyed) {
309
307
  return;
310
308
  }
@@ -511,133 +509,13 @@ class WebSocketPoller {
511
509
  }
512
510
  return;
513
511
  }
514
- if (msgType === 'vnc_session') {
515
- if (!this._vncEnabled) {
516
- this._log('vnc_session ignored VNC not enabled');
517
- return;
518
- }
519
- const action = msg['action'];
520
- if (action === 'start') {
521
- const sessionId = msg['sessionId'];
522
- const port = Number(msg['port'] ?? 5900);
523
- // Prefer password from server frame; fall back to locally-configured password
524
- const password = msg['password'] || this._vncPassword;
525
- this._log(`VNC session start: ${sessionId} → port ${port}`);
526
- const session = new vnc_1.VncSession(sessionId, (frame) => this.sendFrame(frame));
527
- this._vncSessions.set(sessionId, session);
528
- session.start(port, password).catch((err) => {
529
- this._log(`VNC session ${sessionId} failed to start: ${err.message}`);
530
- this._vncSessions.delete(sessionId);
531
- this.sendFrame({ type: 'vnc_close', sessionId, reason: err.message });
532
- });
533
- }
534
- return;
535
- }
536
- if (msgType === 'vnc_input') {
537
- const sessionId = msg['sessionId'];
538
- const event = msg['event'];
539
- if (sessionId && event) {
540
- this._vncSessions.get(sessionId)?.handleInput(event);
541
- }
542
- return;
543
- }
544
- if (msgType === 'vnc_close') {
545
- const sessionId = msg['sessionId'];
546
- if (sessionId) {
547
- this._log(`VNC session closed: ${sessionId}`);
548
- this._vncSessions.get(sessionId)?.stop();
549
- this._vncSessions.delete(sessionId);
550
- }
551
- return;
552
- }
553
- // ── RDP frames from pixel-office ─────────────────────────────────────────
554
- if (msgType === 'rdp_session') {
555
- if (!this._rdpEnabled) {
556
- this._log('rdp_session ignored — RDP not enabled');
557
- return;
558
- }
559
- const action = msg['action'];
560
- if (action === 'start') {
561
- const sessionId = msg['sessionId'];
562
- const opts = {
563
- // Host/port come ONLY from local settings — never from the WS frame.
564
- // A frame-supplied host/port would let a remote party open an outbound
565
- // RDP bridge to an arbitrary target (SSRF / internal-network pivot).
566
- // Default to loopback (xrdp runs on the same machine as the extension).
567
- host: this._rdpSettings.host || '127.0.0.1',
568
- port: this._rdpSettings.port ?? 3389,
569
- // credentials never sent from server — always use local settings
570
- username: this._rdpSettings.username || msg['username'],
571
- password: this._rdpSettings.password || msg['password'],
572
- domain: this._rdpSettings.domain || msg['domain'],
573
- width: msg['width'] ? Number(msg['width']) : undefined,
574
- height: msg['height'] ? Number(msg['height']) : undefined,
575
- colorDepth: msg['colorDepth'] ? Number(msg['colorDepth']) : undefined,
576
- };
577
- this._log(`RDP session start: ${sessionId} → ${opts.host}:${opts.port ?? 3389}`);
578
- // Send Guacamole token to browser so it can connect via guacamole-lite
579
- // (guacd + guacamole-lite must be running on the same host as xrdp, port 4567)
580
- if (opts.username || opts.password) {
581
- const guacSettings = {
582
- hostname: opts.host,
583
- port: String(opts.port ?? 3389),
584
- 'ignore-cert': true,
585
- };
586
- if (opts.username) {
587
- guacSettings['username'] = opts.username;
588
- }
589
- if (opts.password) {
590
- guacSettings['password'] = opts.password;
591
- }
592
- if (opts.domain) {
593
- guacSettings['domain'] = opts.domain;
594
- }
595
- if (opts.width) {
596
- guacSettings['width'] = opts.width;
597
- }
598
- if (opts.height) {
599
- guacSettings['height'] = opts.height;
600
- }
601
- guacSettings['color-depth'] = opts.colorDepth ?? 24;
602
- const tokenPayload = JSON.stringify({ connection: { type: 'rdp', settings: guacSettings } });
603
- const token = Buffer.from(tokenPayload).toString('base64');
604
- // Use configured WSS URL (for HTTPS frontends), else fall back to plain WS on port 4567
605
- const guacWsUrl = this._rdpSettings.guacWsUrl || `ws://${opts.host}:4567`;
606
- this.sendFrame({
607
- type: 'rdp_guac_token',
608
- sessionId,
609
- wsUrl: guacWsUrl,
610
- token,
611
- width: opts.width ?? 1280,
612
- height: opts.height ?? 800,
613
- });
614
- this._log(`RDP guac token sent for session ${sessionId} → ${guacWsUrl}`);
615
- }
616
- const session = new rdp_1.RdpSession(sessionId, (frame) => this.sendFrame(frame), (msg) => this._log(msg));
617
- this._rdpSessions.set(sessionId, session);
618
- session.start(opts).catch((err) => {
619
- this._log(`RDP session ${sessionId} failed to start: ${err.message}`);
620
- this._rdpSessions.delete(sessionId);
621
- this.sendFrame({ type: 'rdp_close', sessionId, reason: err.message });
622
- });
623
- }
624
- return;
625
- }
626
- if (msgType === 'rdp_input') {
627
- const sessionId = msg['sessionId'];
628
- const event = msg['event'];
629
- if (sessionId && event) {
630
- this._rdpSessions.get(sessionId)?.handleInput(event);
631
- }
512
+ // ── VNC / RDP remote-desktop frames from pixel-office ────────────────────
513
+ // Delegated to shared session managers (same machinery the mcp-operate
514
+ // bridge uses). Each returns true if it consumed the frame.
515
+ if (msgType && this._vncManager.handleFrame(msgType, msg)) {
632
516
  return;
633
517
  }
634
- if (msgType === 'rdp_close') {
635
- const sessionId = msg['sessionId'];
636
- if (sessionId) {
637
- this._log(`RDP session closed: ${sessionId}`);
638
- this._rdpSessions.get(sessionId)?.stop();
639
- this._rdpSessions.delete(sessionId);
640
- }
518
+ if (msgType && this._rdpManager.handleFrame(msgType, msg)) {
641
519
  return;
642
520
  }
643
521
  // ── A2A task frame ────────────────────────────────────────────────────────
@@ -773,286 +651,38 @@ class WebSocketPoller {
773
651
  }
774
652
  /** Handle a file-browser request from the server (originated by the browser UI). */
775
653
  _handleFbRequest(requestId, action, relPath, content, newPath, query) {
776
- const respond = (ok, extra) => {
777
- this.sendFrame({ type: 'fb_response', requestId, ok, ...extra });
778
- };
779
- if (!this._fileBrowserEnabled) {
780
- respond(false, { error: 'File browser not enabled' });
781
- return;
782
- }
783
- const root = this._workspaceRoot;
784
- if (!root) {
785
- respond(false, { error: 'No workspace root configured' });
786
- return;
787
- }
788
- // Resolve and validate path is within workspace root. The root itself is
789
- // permitted for read-only actions (list/search) but never for mutations.
790
- // Containment is lexical AND canonical (realpath) — a workspace symlink
791
- // pointing outside must not let a remote fb_request read/write host files.
792
- const resolveSafe = (rel, allowRoot) => (0, pathSafe_1.resolveWithinRoot)(root, rel, allowRoot);
793
- const MUTATING = new Set(['write', 'delete', 'rename', 'mkdir']);
794
- const allowRoot = !MUTATING.has(action);
795
- const absPath = resolveSafe(relPath, allowRoot);
796
- if (!absPath) {
797
- respond(false, {
798
- error: allowRoot ? 'Path outside workspace' : 'Refusing to modify workspace root',
799
- });
800
- return;
801
- }
802
- try {
803
- switch (action) {
804
- case 'list': {
805
- const entries = fs.readdirSync(absPath, { withFileTypes: true }).map(e => {
806
- const stat = (() => { try {
807
- return fs.statSync(path.join(absPath, e.name));
808
- }
809
- catch {
810
- return null;
811
- } })();
812
- return {
813
- name: e.name,
814
- type: e.isDirectory() ? 'dir' : 'file',
815
- size: stat?.size ?? 0,
816
- mtime: stat?.mtimeMs ?? 0,
817
- };
818
- });
819
- // Dirs first, then files; both alphabetical
820
- entries.sort((a, b) => {
821
- if (a.type !== b.type) {
822
- return a.type === 'dir' ? -1 : 1;
823
- }
824
- return a.name.localeCompare(b.name);
825
- });
826
- respond(true, { entries });
827
- break;
828
- }
829
- case 'read': {
830
- const stat = fs.statSync(absPath);
831
- const MAX_BYTES = 1_048_576; // 1 MB
832
- if (stat.size > MAX_BYTES) {
833
- respond(false, { error: `File too large (${stat.size} bytes, limit 1 MB)` });
834
- break;
835
- }
836
- // Binary detection: read first 512 bytes and check for null bytes
837
- const sample = Buffer.allocUnsafe(Math.min(512, stat.size));
838
- const fd = fs.openSync(absPath, 'r');
839
- fs.readSync(fd, sample, 0, sample.length, 0);
840
- fs.closeSync(fd);
841
- const isBinary = sample.includes(0x00);
842
- if (isBinary) {
843
- respond(false, { error: 'Binary file — cannot display' });
844
- break;
845
- }
846
- const fileContent = fs.readFileSync(absPath, 'utf8');
847
- respond(true, { content: fileContent });
848
- break;
849
- }
850
- case 'write': {
851
- if (content === undefined) {
852
- respond(false, { error: 'No content provided' });
853
- break;
854
- }
855
- fs.writeFileSync(absPath, content, 'utf8');
856
- respond(true);
857
- break;
858
- }
859
- case 'delete': {
860
- fs.rmSync(absPath, { recursive: true, force: true });
861
- respond(true);
862
- break;
863
- }
864
- case 'rename': {
865
- if (!newPath) {
866
- respond(false, { error: 'No newPath provided' });
867
- break;
868
- }
869
- const absNewPath = resolveSafe(newPath, false);
870
- if (!absNewPath) {
871
- respond(false, { error: 'newPath outside workspace' });
872
- break;
873
- }
874
- fs.renameSync(absPath, absNewPath);
875
- respond(true);
876
- break;
877
- }
878
- case 'download': {
879
- const stat = fs.statSync(absPath);
880
- const MAX_DOWNLOAD_BYTES = 25 * 1_048_576; // 25 MB — base64 ~1.33x in heap
881
- if (stat.size > MAX_DOWNLOAD_BYTES) {
882
- respond(false, { error: `File too large (${stat.size} bytes, limit 25 MB)` });
883
- break;
884
- }
885
- const buf = fs.readFileSync(absPath);
886
- respond(true, { base64: buf.toString('base64') });
887
- break;
888
- }
889
- case 'mkdir': {
890
- fs.mkdirSync(absPath, { recursive: true });
891
- respond(true);
892
- break;
893
- }
894
- case 'search': {
895
- const rawQuery = (query ?? '').toLowerCase().trim();
896
- if (!rawQuery) {
897
- respond(true, { results: [] });
898
- break;
899
- }
900
- const results = [];
901
- const walk = (dir, relDir, depth) => {
902
- if (depth > 8 || results.length >= 300)
903
- return;
904
- let dirents;
905
- try {
906
- dirents = fs.readdirSync(dir, { withFileTypes: true });
907
- }
908
- catch {
909
- return;
910
- }
911
- for (const e of dirents) {
912
- if (results.length >= 300)
913
- break;
914
- const rel = relDir ? `${relDir}/${e.name}` : e.name;
915
- if (e.name.toLowerCase().includes(rawQuery)) {
916
- results.push({ path: rel, name: e.name, type: e.isDirectory() ? 'dir' : 'file' });
917
- }
918
- if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== 'vendor') {
919
- walk(path.join(dir, e.name), rel, depth + 1);
920
- }
921
- }
922
- };
923
- walk(absPath, '', 0);
924
- respond(true, { results });
925
- break;
926
- }
927
- default:
928
- respond(false, { error: `Unknown action: ${action}` });
929
- }
930
- }
931
- catch (err) {
932
- respond(false, { error: String(err) });
933
- }
654
+ (0, fileBrowser_1.handleFbRequest)({
655
+ root: this._workspaceRoot ?? null,
656
+ enabled: this._fileBrowserEnabled,
657
+ requestId,
658
+ action,
659
+ relPath,
660
+ content,
661
+ newPath,
662
+ query,
663
+ sendFrame: (frame) => { this.sendFrame(frame); },
664
+ log: (m) => this._log(m),
665
+ });
934
666
  }
667
+ /** Handle a git-panel request from the server (originated by the browser UI). */
935
668
  _handleGitRequest(requestId, action, filePath, staged, message, branch, hash) {
936
- const respond = (ok, data, error) => {
937
- this.sendFrame({ type: 'git_response', requestId, ok, ...(data ?? {}), ...(error ? { error } : {}) });
938
- };
939
- const root = this._workspaceRoot;
940
- if (!root) {
941
- respond(false, undefined, 'No workspace root');
942
- return;
943
- }
944
- if (!this._gitEnabled) {
945
- respond(false, undefined, 'Git not enabled');
946
- return;
947
- }
948
- // Containment guard — mirror _handleFbRequest. Every path-bearing arg
949
- // (filePath) must resolve inside the workspace root both lexically AND after
950
- // resolving symlinks; otherwise a git_request could read arbitrary host
951
- // files (e.g. path '../../.claude/.credentials.json' via the readFileSync
952
- // fallback in getDiff, or leak them through `git diff -- <path>`). An empty
953
- // filePath means "whole repo" and is permitted (allowRoot).
954
- if (filePath !== undefined && filePath !== '') {
955
- if ((0, pathSafe_1.resolveWithinRoot)(root, filePath, true) === null) {
956
- respond(false, undefined, 'Path outside workspace');
957
- return;
958
- }
959
- }
960
- (async () => {
961
- try {
962
- switch (action) {
963
- case 'status': {
964
- const status = await gitService.getStatus(root);
965
- respond(true, { status });
966
- break;
967
- }
968
- case 'log': {
969
- const commits = await gitService.getLog(root);
970
- respond(true, { commits });
971
- break;
972
- }
973
- case 'diff': {
974
- const diff = await gitService.getDiff(root, filePath ?? '', staged ?? false);
975
- respond(true, { diff });
976
- break;
977
- }
978
- case 'commit_diff': {
979
- const diff = await gitService.getCommitDiff(root, hash ?? '', filePath);
980
- respond(true, { diff });
981
- break;
982
- }
983
- case 'stage': {
984
- if (filePath)
985
- await gitService.stageFile(root, filePath);
986
- else
987
- await gitService.stageAll(root);
988
- respond(true);
989
- break;
990
- }
991
- case 'unstage': {
992
- if (!filePath) {
993
- respond(false, undefined, 'path required');
994
- break;
995
- }
996
- await gitService.unstageFile(root, filePath);
997
- respond(true);
998
- break;
999
- }
1000
- case 'commit': {
1001
- if (!message) {
1002
- respond(false, undefined, 'message required');
1003
- break;
1004
- }
1005
- const commitHash = await gitService.commit(root, message);
1006
- respond(true, { hash: commitHash });
1007
- break;
1008
- }
1009
- case 'fetch': {
1010
- await gitService.fetchOrigin(root);
1011
- respond(true);
1012
- break;
1013
- }
1014
- case 'branches': {
1015
- const branches = await gitService.getBranches(root);
1016
- respond(true, { branches });
1017
- break;
1018
- }
1019
- case 'checkout': {
1020
- if (!branch) {
1021
- respond(false, undefined, 'branch required');
1022
- break;
1023
- }
1024
- await gitService.checkoutBranch(root, branch);
1025
- respond(true);
1026
- break;
1027
- }
1028
- default:
1029
- respond(false, undefined, `Unknown git action: ${action}`);
1030
- }
1031
- }
1032
- catch (err) {
1033
- respond(false, undefined, String(err));
1034
- }
1035
- })();
1036
- }
1037
- /** Stop all active VNC sessions (called on destroy/reconnect). */
1038
- _stopAllVncSessions() {
1039
- for (const [id, session] of this._vncSessions) {
1040
- this._log(`VNC session terminated (disconnect): ${id}`);
1041
- session.stop();
1042
- }
1043
- this._vncSessions.clear();
1044
- }
1045
- /** Stop all active RDP sessions (called on destroy/reconnect). */
1046
- _stopAllRdpSessions() {
1047
- for (const [id, session] of this._rdpSessions) {
1048
- this._log(`RDP session terminated (disconnect): ${id}`);
1049
- session.stop();
1050
- }
1051
- this._rdpSessions.clear();
669
+ (0, gitRequest_1.handleGitRequest)({
670
+ root: this._workspaceRoot ?? null,
671
+ enabled: this._gitEnabled,
672
+ requestId,
673
+ action,
674
+ filePath,
675
+ staged,
676
+ message,
677
+ branch,
678
+ hash,
679
+ sendFrame: (frame) => { this.sendFrame(frame); },
680
+ log: (m) => this._log(m),
681
+ });
1052
682
  }
1053
683
  /** Update the VNC password used for incoming vnc_session requests. */
1054
684
  setVncPassword(password) {
1055
- this._vncPassword = password;
685
+ this._vncManager.setPassword(password);
1056
686
  }
1057
687
  setGitEnabled(enabled) {
1058
688
  this._gitEnabled = enabled;
@@ -1061,13 +691,13 @@ class WebSocketPoller {
1061
691
  this._fileBrowserEnabled = enabled;
1062
692
  }
1063
693
  setVncEnabled(enabled) {
1064
- this._vncEnabled = enabled;
694
+ this._vncManager.setEnabled(enabled);
1065
695
  }
1066
696
  setRdpEnabled(enabled) {
1067
- this._rdpEnabled = enabled;
697
+ this._rdpManager.setEnabled(enabled);
1068
698
  }
1069
699
  setRdpSettings(s) {
1070
- this._rdpSettings = s;
700
+ this._rdpManager.setSettings(s);
1071
701
  }
1072
702
  /**
1073
703
  * Send a JSON payload to the server over the WebSocket connection.