loadtoagent 1.3.4 → 1.3.6

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 (35) hide show
  1. package/docs/PROVIDER-CONTRACTS.md +3 -1
  2. package/package.json +4 -2
  3. package/renderer/app-dashboard.js +75 -13
  4. package/renderer/app-drawer-content.js +189 -8
  5. package/renderer/app-drawer.js +4 -3
  6. package/renderer/app-events-dialogs.js +6 -1
  7. package/renderer/app-events-filters.js +80 -2
  8. package/renderer/app-events-sessions.js +110 -17
  9. package/renderer/app-graph-model.js +8 -5
  10. package/renderer/app-graph-orchestration.js +43 -11
  11. package/renderer/app-graph-view.js +75 -14
  12. package/renderer/app-quality.js +2 -0
  13. package/renderer/app-session-render.js +7 -7
  14. package/renderer/app.js +104 -0
  15. package/renderer/i18n-messages.js +46 -9
  16. package/renderer/index.html +33 -7
  17. package/renderer/styles-components.css +218 -1
  18. package/renderer/styles-control-room.css +820 -29
  19. package/renderer/styles-readability.css +1 -9
  20. package/renderer/styles-responsive-shell.css +22 -1
  21. package/renderer/styles-terminal.css +21 -31
  22. package/renderer/styles-workflow-map.css +8 -0
  23. package/renderer/terminal-events.js +0 -13
  24. package/renderer/terminal-workbench.js +1 -4
  25. package/src/agentMonitor/claudeParser.js +189 -4
  26. package/src/agentMonitor/codexCollaboration.js +1 -0
  27. package/src/agentMonitor/codexParser.js +6 -5
  28. package/src/agentMonitor/executionActivity.js +26 -1
  29. package/src/agentMonitor/hierarchy.js +6 -3
  30. package/src/agentMonitor.js +10 -3
  31. package/src/contracts.js +1 -1
  32. package/src/monitorWorker.js +2 -0
  33. package/src/nodePtyRuntime.js +58 -0
  34. package/src/terminalHost.js +104 -5
  35. package/src/terminalManager.js +5 -1
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function unpackedAsarPath(file) {
7
+ return String(file || '')
8
+ .replace(/([\\/])app\.asar(?=[\\/])/g, '$1app.asar.unpacked')
9
+ .replace(/([\\/])node_modules\.asar(?=[\\/])/g, '$1node_modules.asar.unpacked');
10
+ }
11
+
12
+ function macNodePtyRuntimeFiles(options = {}) {
13
+ const platform = options.platform || process.platform;
14
+ if (platform !== 'darwin') return null;
15
+ const arch = String(options.arch || process.arch);
16
+ if (!/^(?:arm64|x64)$/.test(arch)) throw new Error(`지원하지 않는 macOS 아키텍처입니다: ${arch}`);
17
+ const resolvePackage = options.resolvePackage || (() => require.resolve('node-pty/package.json'));
18
+ const packageRoot = path.dirname(unpackedAsarPath(resolvePackage()));
19
+ const prebuild = path.join(packageRoot, 'prebuilds', `darwin-${arch}`);
20
+ return {
21
+ packageRoot,
22
+ helper: path.join(prebuild, 'spawn-helper'),
23
+ addon: path.join(prebuild, 'pty.node'),
24
+ };
25
+ }
26
+
27
+ function ensureMacNodePtyRuntime(options = {}) {
28
+ const files = macNodePtyRuntimeFiles(options);
29
+ if (!files) return { repaired: false, files: null };
30
+ const fileSystem = options.fileSystem || fs;
31
+ const executableMode = fileSystem.constants?.X_OK ?? fs.constants.X_OK;
32
+ for (const file of [files.addon, files.helper]) {
33
+ let stat;
34
+ try {
35
+ stat = fileSystem.statSync(file);
36
+ } catch (error) {
37
+ throw new Error(`macOS 터미널 구성 요소를 찾을 수 없습니다: ${file}`, { cause: error });
38
+ }
39
+ if (!stat.isFile()) throw new Error(`macOS 터미널 구성 요소가 파일이 아닙니다: ${file}`);
40
+ }
41
+
42
+ let repaired = false;
43
+ try {
44
+ fileSystem.accessSync(files.helper, executableMode);
45
+ } catch (_notExecutable) {
46
+ const mode = fileSystem.statSync(files.helper).mode;
47
+ try {
48
+ fileSystem.chmodSync(files.helper, mode | 0o111);
49
+ fileSystem.accessSync(files.helper, executableMode);
50
+ repaired = true;
51
+ } catch (error) {
52
+ throw new Error(`macOS 터미널 실행 권한을 복구할 수 없습니다: ${files.helper}`, { cause: error });
53
+ }
54
+ }
55
+ return { repaired, files };
56
+ }
57
+
58
+ module.exports = { ensureMacNodePtyRuntime, macNodePtyRuntimeFiles, unpackedAsarPath };
@@ -10,7 +10,8 @@ const { spawn } = require('child_process');
10
10
  const { endpointFor, safeWriteJson } = require('./bridgeServer');
11
11
  const { runBestEffort } = require('./diagnostics');
12
12
 
13
- const TERMINAL_HOST_PROTOCOL = 1;
13
+ const TERMINAL_HOST_PROTOCOL = 2;
14
+ const TERMINAL_HOST_RUNTIME = `node-pty-${require('node-pty/package.json').version}`;
14
15
  const MAX_FRAME_CHARS = 4 * 1024 * 1024;
15
16
  const AUTH_TIMEOUT_MS = 5_000;
16
17
  const REQUEST_TIMEOUT_MS = 15_000;
@@ -21,14 +22,94 @@ function sendFrame(socket, payload) {
21
22
  socket.write(`${JSON.stringify(payload)}\n`, 'utf8');
22
23
  }
23
24
 
24
- function readHostDiscovery(file, fileSystem = fs) {
25
+ function incompatibleHostError(message, discovery) {
26
+ const error = new Error(message);
27
+ error.code = 'LOADTOAGENT_INCOMPATIBLE_TERMINAL_HOST';
28
+ error.discovery = discovery;
29
+ return error;
30
+ }
31
+
32
+ function readHostDiscovery(file, fileSystem = fs, expectedRuntime = TERMINAL_HOST_RUNTIME) {
25
33
  const parsed = JSON.parse(fileSystem.readFileSync(file, 'utf8'));
26
- if (parsed?.protocol !== TERMINAL_HOST_PROTOCOL || !parsed.endpoint || !parsed.token) {
34
+ if (!parsed?.endpoint || !parsed.token || !Number.isSafeInteger(Number(parsed.pid)) || Number(parsed.pid) <= 0) {
27
35
  throw new Error('터미널 호스트 연결 정보가 올바르지 않습니다.');
28
36
  }
37
+ if (parsed.protocol !== TERMINAL_HOST_PROTOCOL || parsed.runtime !== expectedRuntime) {
38
+ throw incompatibleHostError('현재 앱과 호환되지 않는 터미널 호스트입니다.', parsed);
39
+ }
29
40
  return parsed;
30
41
  }
31
42
 
43
+ function verifyHostDiscovery(discovery, timeoutMs = 1_500) {
44
+ return new Promise((resolve, reject) => {
45
+ const socket = net.createConnection(discovery.endpoint);
46
+ let buffer = '';
47
+ let settled = false;
48
+ const finish = error => {
49
+ if (settled) return;
50
+ settled = true;
51
+ clearTimeout(timer);
52
+ socket.destroy();
53
+ if (error) reject(error);
54
+ else resolve();
55
+ };
56
+ const timer = setTimeout(() => finish(new Error('이전 터미널 호스트 인증 시간이 초과되었습니다.')), timeoutMs);
57
+ socket.setNoDelay(true);
58
+ socket.on('connect', () => sendFrame(socket, { type: 'authenticate', token: discovery.token }));
59
+ socket.on('data', chunk => {
60
+ buffer += chunk.toString('utf8');
61
+ if (buffer.length > MAX_FRAME_CHARS) {
62
+ finish(new Error('이전 터미널 호스트 응답이 너무 큽니다.'));
63
+ return;
64
+ }
65
+ let newline;
66
+ while ((newline = buffer.indexOf('\n')) >= 0) {
67
+ const line = buffer.slice(0, newline).trim();
68
+ buffer = buffer.slice(newline + 1);
69
+ if (!line) continue;
70
+ try {
71
+ if (JSON.parse(line).type === 'ready') {
72
+ finish();
73
+ return;
74
+ }
75
+ } catch (_invalidFrame) {}
76
+ }
77
+ });
78
+ socket.on('error', finish);
79
+ socket.on('close', () => {
80
+ if (!settled) finish(new Error('이전 터미널 호스트 인증 연결이 닫혔습니다.'));
81
+ });
82
+ });
83
+ }
84
+
85
+ function processExists(pid) {
86
+ try {
87
+ process.kill(pid, 0);
88
+ return true;
89
+ } catch (error) {
90
+ if (error?.code === 'ESRCH') return false;
91
+ if (error?.code === 'EPERM') return true;
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ async function terminateHostProcess(discovery) {
97
+ const pid = Number(discovery?.pid);
98
+ if (!Number.isSafeInteger(pid) || pid <= 0 || pid === process.pid) {
99
+ throw new Error('교체할 터미널 호스트 프로세스 정보가 올바르지 않습니다.');
100
+ }
101
+ try {
102
+ process.kill(pid, 'SIGTERM');
103
+ } catch (error) {
104
+ if (error?.code !== 'ESRCH') throw error;
105
+ }
106
+ const deadline = Date.now() + 3_000;
107
+ while (processExists(pid)) {
108
+ if (Date.now() >= deadline) throw new Error(`이전 터미널 호스트가 종료되지 않았습니다: PID ${pid}`);
109
+ await new Promise(resolve => setTimeout(resolve, 50));
110
+ }
111
+ }
112
+
32
113
  function activeSessions(manager) {
33
114
  return manager.list().filter(session => session.status === 'running' || session.status === 'starting');
34
115
  }
@@ -40,6 +121,7 @@ class TerminalHostServer {
40
121
  this.discoveryFile = path.resolve(options.discoveryFile || path.join(os.tmpdir(), 'loadtoagent-terminal-host.json'));
41
122
  this.endpoint = options.endpoint || endpointFor(this.platform, `${path.dirname(this.discoveryFile)}:terminal-host`);
42
123
  this.token = options.token || crypto.randomBytes(32).toString('hex');
124
+ this.runtime = String(options.runtime || TERMINAL_HOST_RUNTIME);
43
125
  this.server = null;
44
126
  this.clients = new Set();
45
127
  this.shutdownTimer = null;
@@ -57,6 +139,7 @@ class TerminalHostServer {
57
139
  info() {
58
140
  return {
59
141
  protocol: TERMINAL_HOST_PROTOCOL,
142
+ runtime: this.runtime,
60
143
  endpoint: this.endpoint,
61
144
  token: this.token,
62
145
  pid: process.pid,
@@ -207,7 +290,7 @@ class TerminalHostServer {
207
290
  if (this.server) runBestEffort('terminal-host-server-close', () => this.server.close());
208
291
  this.server = null;
209
292
  try {
210
- const current = readHostDiscovery(this.discoveryFile);
293
+ const current = readHostDiscovery(this.discoveryFile, fs, this.runtime);
211
294
  if (current.pid === process.pid && current.token === this.token) fs.unlinkSync(this.discoveryFile);
212
295
  } catch (_missingOrReplacedDiscovery) {}
213
296
  if (this.platform !== 'win32' && fs.existsSync(this.endpoint)) {
@@ -263,6 +346,9 @@ class TerminalHostClient extends EventEmitter {
263
346
  this.discoveryFile = path.resolve(options.discoveryFile);
264
347
  this.spawnHost = typeof options.spawnHost === 'function' ? options.spawnHost : null;
265
348
  this.connectTimeoutMs = Number(options.connectTimeoutMs || 12_000);
349
+ this.expectedRuntime = String(options.expectedRuntime || TERMINAL_HOST_RUNTIME);
350
+ this.verifyHost = typeof options.verifyHost === 'function' ? options.verifyHost : verifyHostDiscovery;
351
+ this.terminateHost = typeof options.terminateHost === 'function' ? options.terminateHost : terminateHostProcess;
266
352
  this.socket = null;
267
353
  this.buffer = '';
268
354
  this.connected = false;
@@ -300,6 +386,16 @@ class TerminalHostClient extends EventEmitter {
300
386
  } catch (error) {
301
387
  lastError = error;
302
388
  this.resetSocket();
389
+ if (error?.code === 'LOADTOAGENT_INCOMPATIBLE_TERMINAL_HOST') {
390
+ let verified = false;
391
+ try {
392
+ await Promise.resolve(this.verifyHost(error.discovery));
393
+ verified = true;
394
+ } catch (verificationError) {
395
+ lastError = verificationError;
396
+ }
397
+ if (verified) await Promise.resolve(this.terminateHost(error.discovery));
398
+ }
303
399
  }
304
400
  if (!launched) {
305
401
  if (!this.spawnHost) throw lastError;
@@ -313,7 +409,7 @@ class TerminalHostClient extends EventEmitter {
313
409
  }
314
410
 
315
411
  connectExisting() {
316
- const discovery = readHostDiscovery(this.discoveryFile);
412
+ const discovery = readHostDiscovery(this.discoveryFile, fs, this.expectedRuntime);
317
413
  return new Promise((resolve, reject) => {
318
414
  const socket = net.createConnection(discovery.endpoint);
319
415
  const timer = setTimeout(() => {
@@ -449,7 +545,10 @@ module.exports = {
449
545
  TerminalHostServer,
450
546
  TerminalHostClient,
451
547
  TERMINAL_HOST_PROTOCOL,
548
+ TERMINAL_HOST_RUNTIME,
452
549
  readHostDiscovery,
550
+ verifyHostDiscovery,
551
+ terminateHostProcess,
453
552
  launchTerminalHost,
454
553
  resolveTerminalHostExecutable,
455
554
  };
@@ -7,6 +7,7 @@ const crypto = require('crypto');
7
7
  const { EventEmitter } = require('events');
8
8
  const { spawn: spawnChild } = require('child_process');
9
9
  const { runBestEffort } = require('./diagnostics');
10
+ const { ensureMacNodePtyRuntime } = require('./nodePtyRuntime');
10
11
 
11
12
  const MAX_SESSIONS = 24;
12
13
  const MAX_INPUT_CHARS = 128 * 1024;
@@ -370,7 +371,10 @@ class TerminalManager extends EventEmitter {
370
371
  }
371
372
 
372
373
  pty() {
373
- if (!this.ptyModule) this.ptyModule = require('node-pty');
374
+ if (!this.ptyModule) {
375
+ ensureMacNodePtyRuntime({ platform: this.platform });
376
+ this.ptyModule = require('node-pty');
377
+ }
374
378
  return this.ptyModule;
375
379
  }
376
380