aiexecode 1.0.94 → 1.0.127

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.

Potentially problematic release.


This version of aiexecode might be problematic. Click here for more details.

Files changed (80) hide show
  1. package/README.md +198 -88
  2. package/index.js +310 -86
  3. package/mcp-agent-lib/src/mcp_message_logger.js +17 -16
  4. package/package.json +4 -4
  5. package/payload_viewer/out/404/index.html +1 -1
  6. package/payload_viewer/out/404.html +1 -1
  7. package/payload_viewer/out/_next/static/chunks/{37d0cd2587a38f79.js → b6c0459f3789d25c.js} +1 -1
  8. package/payload_viewer/out/_next/static/chunks/b75131b58f8ca46a.css +3 -0
  9. package/payload_viewer/out/index.html +1 -1
  10. package/payload_viewer/out/index.txt +3 -3
  11. package/payload_viewer/web_server.js +361 -0
  12. package/prompts/completion_judge.txt +4 -0
  13. package/prompts/orchestrator.txt +116 -3
  14. package/src/LLMClient/client.js +401 -18
  15. package/src/LLMClient/converters/responses-to-claude.js +67 -18
  16. package/src/LLMClient/converters/responses-to-zai.js +667 -0
  17. package/src/LLMClient/errors.js +30 -4
  18. package/src/LLMClient/index.js +5 -0
  19. package/src/ai_based/completion_judge.js +263 -186
  20. package/src/ai_based/orchestrator.js +171 -35
  21. package/src/commands/agents.js +70 -0
  22. package/src/commands/apikey.js +1 -1
  23. package/src/commands/bg.js +129 -0
  24. package/src/commands/commands.js +51 -0
  25. package/src/commands/debug.js +52 -0
  26. package/src/commands/help.js +11 -1
  27. package/src/commands/model.js +42 -7
  28. package/src/commands/reasoning_effort.js +2 -2
  29. package/src/commands/skills.js +46 -0
  30. package/src/config/ai_models.js +106 -6
  31. package/src/config/constants.js +71 -0
  32. package/src/config/feature_flags.js +6 -7
  33. package/src/frontend/App.js +108 -1
  34. package/src/frontend/components/AutocompleteMenu.js +7 -1
  35. package/src/frontend/components/BackgroundProcessList.js +175 -0
  36. package/src/frontend/components/ConversationItem.js +26 -10
  37. package/src/frontend/components/CurrentModelView.js +2 -2
  38. package/src/frontend/components/HelpView.js +106 -2
  39. package/src/frontend/components/Input.js +33 -11
  40. package/src/frontend/components/ModelListView.js +1 -1
  41. package/src/frontend/components/SetupWizard.js +51 -8
  42. package/src/frontend/hooks/useFileCompletion.js +467 -0
  43. package/src/frontend/utils/toolUIFormatter.js +261 -0
  44. package/src/system/agents_loader.js +289 -0
  45. package/src/system/ai_request.js +156 -12
  46. package/src/system/background_process.js +317 -0
  47. package/src/system/code_executer.js +496 -56
  48. package/src/system/command_parser.js +33 -3
  49. package/src/system/conversation_state.js +265 -0
  50. package/src/system/conversation_trimmer.js +132 -0
  51. package/src/system/custom_command_loader.js +386 -0
  52. package/src/system/file_integrity.js +73 -10
  53. package/src/system/log.js +10 -2
  54. package/src/system/output_helper.js +52 -9
  55. package/src/system/session.js +213 -58
  56. package/src/system/session_memory.js +30 -2
  57. package/src/system/skill_loader.js +318 -0
  58. package/src/system/system_info.js +254 -40
  59. package/src/system/tool_approval.js +10 -0
  60. package/src/system/tool_registry.js +15 -1
  61. package/src/system/ui_events.js +11 -0
  62. package/src/tools/code_editor.js +16 -10
  63. package/src/tools/file_reader.js +66 -9
  64. package/src/tools/glob.js +0 -3
  65. package/src/tools/ripgrep.js +5 -7
  66. package/src/tools/skill_tool.js +122 -0
  67. package/src/tools/web_downloader.js +0 -3
  68. package/src/util/clone.js +174 -0
  69. package/src/util/config.js +55 -2
  70. package/src/util/config_migration.js +174 -0
  71. package/src/util/debug_log.js +8 -2
  72. package/src/util/exit_handler.js +8 -0
  73. package/src/util/file_reference_parser.js +132 -0
  74. package/src/util/path_validator.js +178 -0
  75. package/src/util/prompt_loader.js +91 -1
  76. package/src/util/safe_fs.js +66 -3
  77. package/payload_viewer/out/_next/static/chunks/ecd2072ebf41611f.css +0 -3
  78. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_buildManifest.js +0 -0
  79. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_clientMiddlewareManifest.json +0 -0
  80. /package/payload_viewer/out/_next/static/{wkEKh6i9XPSyP6rjDRvHn → 42iEoi-1o5MxNIZ1SWSvV}/_ssgManifest.js +0 -0
@@ -10,6 +10,26 @@ import { UnifiedLLMClient } from "../LLMClient/index.js";
10
10
 
11
11
  const debugLog = createDebugLogger('ai_request.log', 'ai_request');
12
12
 
13
+ /**
14
+ * 안전한 JSON 파싱 헬퍼 함수
15
+ * 파싱 실패 시 에러를 던지지 않고 기본값 반환
16
+ * @param {string} jsonString - 파싱할 JSON 문자열
17
+ * @param {*} defaultValue - 파싱 실패 시 반환할 기본값
18
+ * @returns {*} 파싱된 객체 또는 기본값
19
+ */
20
+ function safeJsonParse(jsonString, defaultValue = null) {
21
+ if (typeof jsonString !== 'string') {
22
+ return defaultValue;
23
+ }
24
+ try {
25
+ return JSON.parse(jsonString);
26
+ } catch (error) {
27
+ debugLog(`[safeJsonParse] JSON parsing failed: ${error.message}`);
28
+ debugLog(`[safeJsonParse] Input (first 200 chars): ${jsonString.substring(0, 200)}`);
29
+ return defaultValue;
30
+ }
31
+ }
32
+
13
33
  // OpenAI SDK를 사용하기 위한 환경변수를 불러옵니다.
14
34
  dotenv.config({ quiet: true });
15
35
 
@@ -55,15 +75,24 @@ async function getLLMClient() {
55
75
  const currentModel = process.env.MODEL || settings?.MODEL || DEFAULT_MODEL;
56
76
  const modelInfo = getModelInfo(currentModel);
57
77
  const provider = modelInfo?.provider || 'openai';
78
+ const baseUrl = process.env.BASE_URL || settings?.BASE_URL || null;
58
79
 
59
- debugLog('[getLLMClient] Initializing UnifiedLLMClient with API key (first 10 chars): ' + process.env.API_KEY.substring(0, 10) + '...');
60
- debugLog(`[getLLMClient] Model: ${currentModel}, Provider: ${provider}`);
61
- llmClient = new UnifiedLLMClient({
80
+ debugLog('[getLLMClient] Initializing UnifiedLLMClient with API key: [PRESENT]');
81
+ debugLog(`[getLLMClient] Model: ${currentModel}, Provider: ${provider}, BaseURL: ${baseUrl || '(default)'}`);
82
+
83
+ const clientConfig = {
62
84
  apiKey: process.env.API_KEY,
63
85
  model: currentModel,
64
86
  provider: provider,
65
87
  logDir: PAYLOAD_LLM_LOG_DIR
66
- });
88
+ };
89
+
90
+ // 커스텀 BASE_URL이 설정되어 있으면 추가
91
+ if (baseUrl) {
92
+ clientConfig.baseUrl = baseUrl;
93
+ }
94
+
95
+ llmClient = new UnifiedLLMClient(clientConfig);
67
96
 
68
97
  debugLog('[getLLMClient] Client created successfully');
69
98
  return llmClient;
@@ -181,6 +210,7 @@ function toAbsolutePath(anyPath) {
181
210
  // 모든 AI 요청 과정에서 요청/응답 로그를 남기고 결과를 돌려줍니다.
182
211
  export async function request(taskName, requestPayload) {
183
212
  debugLog(`[request] ========== START: ${taskName} ==========`);
213
+
184
214
  const provider = await getCurrentProvider();
185
215
  debugLog(`[request] Provider: ${provider}`);
186
216
 
@@ -218,8 +248,8 @@ export async function request(taskName, requestPayload) {
218
248
  const msg = payloadCopy.input[i];
219
249
  const { type, call_id, output } = msg;
220
250
  if (type !== 'function_call_output') continue;
221
- const parsedOutput = JSON.parse(output);
222
- if (!isValidJSON(parsedOutput.stdout)) {
251
+ const parsedOutput = safeJsonParse(output, { stdout: output });
252
+ if (!parsedOutput || !isValidJSON(parsedOutput.stdout)) {
223
253
  // stdout와 stderr를 모두 포함하는 형식으로 변환
224
254
  const hasStdout = parsedOutput.stdout;
225
255
  const hasStderr = parsedOutput.stderr;
@@ -235,7 +265,10 @@ export async function request(taskName, requestPayload) {
235
265
  msg.output = parsedOutput.stdout;
236
266
  }
237
267
  } else {
238
- parsedOutput.stdout = JSON.parse(parsedOutput.stdout);
268
+ const parsedStdout = safeJsonParse(parsedOutput.stdout, null);
269
+ if (parsedStdout !== null) {
270
+ parsedOutput.stdout = parsedStdout;
271
+ }
239
272
  if (parsedOutput.original_result) {
240
273
  parsedOutput.stdout = ({ ...parsedOutput.original_result, ...(parsedOutput.stdout) });
241
274
  delete parsedOutput.original_result;
@@ -496,16 +529,24 @@ export async function request(taskName, requestPayload) {
496
529
  debugLog(`[request] Request prepared - logging to file`);
497
530
 
498
531
  // 로그는 원본 포맷으로 저장 (API 호출 전)
532
+ const logStartTime = Date.now();
499
533
  await logger(`${taskName}_REQ`, originalRequest, provider);
500
- debugLog(`[request] Request logged - calling LLM API`);
534
+ debugLog(`[request] Request logged (${Date.now() - logStartTime}ms) - calling LLM API`);
501
535
 
502
536
  // AbortController의 signal을 options로 전달
503
537
  const requestOptions = {
504
538
  signal: currentAbortController.signal
505
539
  };
506
- debugLog(`[request] Calling client.response with abort signal`);
540
+
541
+ // ===== API 호출 시작 =====
542
+ const apiCallStartTime = Date.now();
543
+ debugLog(`[request] >>>>> API CALL START: ${new Date(apiCallStartTime).toISOString()}`);
507
544
 
508
545
  response = await client.response(originalRequest, requestOptions);
546
+
547
+ const apiCallEndTime = Date.now();
548
+ const apiCallDuration = apiCallEndTime - apiCallStartTime;
549
+ debugLog(`[request] <<<<< API CALL END: ${apiCallDuration}ms (${new Date(apiCallEndTime).toISOString()})`);
509
550
  debugLog(`[request] Response received - id: ${response?.id}, status: ${response?.status}, output items: ${response?.output?.length || 0}`);
510
551
 
511
552
  // 원본 응답을 깊은 복사로 보존 (이후 수정으로부터 보호)
@@ -549,6 +590,84 @@ export async function request(taskName, requestPayload) {
549
590
  // 로그는 원본 포맷으로 저장
550
591
  debugLog(`[request] Logging response to file`);
551
592
  await logger(`${taskName}_RES`, originalResponse, provider);
593
+
594
+ // 화면에 응답 결과 표시 (raw JSON)
595
+ // 캐시 토큰 정보 추출
596
+ const cacheTokens = response.usage?.cache_read_input_tokens || response.usage?.input_tokens_details?.cached_tokens || 0;
597
+ const inputTokens = response.usage?.input_tokens || 0;
598
+ const cacheRatio = inputTokens > 0 ? Math.round(cacheTokens / inputTokens * 100) : 0;
599
+
600
+ const rawOutput = {
601
+ status: response.status,
602
+ output: (response.output || []).map(o => {
603
+ if (o.type === 'reasoning') {
604
+ // thinking/reasoning 블록 표시
605
+ const thinking = o.content?.[0]?.thinking || '';
606
+ return {
607
+ type: 'thinking',
608
+ content: thinking.length > 150 ? thinking.substring(0, 150) + `... (${thinking.length} chars)` : thinking
609
+ };
610
+ } else if (o.type === 'function_call') {
611
+ return {
612
+ type: o.type,
613
+ name: o.name,
614
+ arguments: (() => {
615
+ try {
616
+ const args = JSON.parse(o.arguments || '{}');
617
+ // 긴 인자값은 축약
618
+ const truncated = {};
619
+ for (const [k, v] of Object.entries(args)) {
620
+ if (typeof v === 'string' && v.length > 100) {
621
+ truncated[k] = v.substring(0, 100) + `... (${v.length} chars)`;
622
+ } else {
623
+ truncated[k] = v;
624
+ }
625
+ }
626
+ return truncated;
627
+ } catch {
628
+ return o.arguments;
629
+ }
630
+ })()
631
+ };
632
+ } else if (o.type === 'message') {
633
+ const text = response.output_text || '';
634
+ return {
635
+ type: o.type,
636
+ text: text.length > 200 ? text.substring(0, 200) + `... (${text.length} chars)` : text
637
+ };
638
+ }
639
+ return { type: o.type };
640
+ }),
641
+ // 토큰 사용량 정보 추가
642
+ tokens: {
643
+ input: inputTokens,
644
+ output: response.usage?.output_tokens || 0,
645
+ cached: cacheTokens > 0 ? `${cacheTokens} (${cacheRatio}%)` : 0
646
+ },
647
+ output_text: (() => {
648
+ const text = response.output_text || '';
649
+ if (taskName === 'completion_judge') {
650
+ // GLM 모델 대응: 텍스트 끝에 붙은 JSON 추출
651
+ try {
652
+ return JSON.parse(text);
653
+ } catch {
654
+ // 텍스트에서 JSON 추출 시도
655
+ const jsonMatch = text.match(/\{[^{}]*"should_complete"\s*:\s*(true|false)[^{}]*\}/);
656
+ if (jsonMatch) {
657
+ try {
658
+ return JSON.parse(jsonMatch[0]);
659
+ } catch {
660
+ return text.length > 200 ? text.substring(0, 200) + `... (${text.length} chars)` : text;
661
+ }
662
+ }
663
+ return text.length > 200 ? text.substring(0, 200) + `... (${text.length} chars)` : text;
664
+ }
665
+ }
666
+ return text.length > 200 ? text.substring(0, 200) + `... (${text.length} chars)` : text;
667
+ })()
668
+ };
669
+
670
+
552
671
  debugLog(`[request] ========== END: ${taskName} ==========`);
553
672
 
554
673
  return response;
@@ -566,21 +685,46 @@ export async function request(taskName, requestPayload) {
566
685
  *
567
686
  * @see https://platform.openai.com/docs/guides/error-codes
568
687
  */
688
+ /*
689
+ * Z.AI/GLM 컨텍스트 초과 에러 샘플:
690
+ * {
691
+ * "error": {
692
+ * "code": "1210",
693
+ * "message": "Invalid API parameter, please check the documentation.Request 320006 input tokens exceeds the model's maximum context length 202750"
694
+ * },
695
+ * "request_id": "20260124000100026f0914d2e744f5"
696
+ * }
697
+ * HTTP_STATUS: 400
698
+ */
569
699
  export function shouldRetryWithTrim(error) {
570
700
  if (!error) return false;
571
701
 
572
- // OpenAI SDK의 공식 에러 코드 확인
702
+ // 에러 코드 메시지 추출
573
703
  const errorCode = error?.code || error?.error?.code;
704
+ const errorMessage = error?.message || error?.error?.message || '';
574
705
 
706
+ // OpenAI: context_length_exceeded
575
707
  if (errorCode === 'context_length_exceeded') {
576
- debugLog('[shouldRetryWithTrim] Detected: context_length_exceeded');
708
+ debugLog('[shouldRetryWithTrim] Detected: context_length_exceeded (OpenAI)');
709
+ return true;
710
+ }
711
+
712
+ // Z.AI/GLM: code "1210" (context length exceeded)
713
+ if (errorCode === '1210' || errorCode === 1210) {
714
+ debugLog('[shouldRetryWithTrim] Detected: code 1210 (GLM context exceeded)');
715
+ return true;
716
+ }
717
+
718
+ // 메시지에서 컨텍스트 초과 패턴 감지 (다양한 API 대응)
719
+ const contextExceededPattern = /exceeds.*(?:context|token).*(?:length|limit|maximum)/i;
720
+ if (contextExceededPattern.test(errorMessage)) {
721
+ debugLog(`[shouldRetryWithTrim] Detected context exceeded in message: ${errorMessage.substring(0, 100)}`);
577
722
  return true;
578
723
  }
579
724
 
580
725
  // 400 에러도 trim 후 재시도 대상
581
726
  if (error?.status === 400 || error?.response?.status === 400) {
582
727
  const errorType = error?.type || error?.error?.type;
583
- const errorMessage = error?.message || error?.error?.message || '';
584
728
  debugLog(
585
729
  `[shouldRetryWithTrim] Detected 400 error - ` +
586
730
  `Type: ${errorType}, Code: ${errorCode}, ` +
@@ -0,0 +1,317 @@
1
+ /**
2
+ * 백그라운드 프로세스 관리 모듈
3
+ *
4
+ * AI Agent가 백그라운드로 명령어를 실행하고 관리할 수 있도록 지원
5
+ */
6
+
7
+ import { spawn } from 'child_process';
8
+ import { EventEmitter } from 'events';
9
+ import { createDebugLogger } from '../util/debug_log.js';
10
+ import { whichCommand } from './code_executer.js';
11
+
12
+ const debugLog = createDebugLogger('background_process.log', 'background_process');
13
+
14
+ /**
15
+ * 백그라운드 프로세스 정보
16
+ * @typedef {Object} BackgroundProcess
17
+ * @property {string} id - 고유 ID
18
+ * @property {string} command - 실행된 명령어
19
+ * @property {number} pid - 프로세스 ID
20
+ * @property {string} status - 상태 (running, completed, failed, killed)
21
+ * @property {Date} startedAt - 시작 시간
22
+ * @property {Date|null} endedAt - 종료 시간
23
+ * @property {string} stdout - 표준 출력
24
+ * @property {string} stderr - 표준 에러
25
+ * @property {number|null} exitCode - 종료 코드
26
+ */
27
+
28
+ class BackgroundProcessManager extends EventEmitter {
29
+ constructor() {
30
+ super();
31
+ /** @type {Map<string, BackgroundProcess>} */
32
+ this.processes = new Map();
33
+ this.nextId = 1;
34
+ }
35
+
36
+ /**
37
+ * 고유 ID 생성
38
+ */
39
+ _generateId() {
40
+ return `bg_${this.nextId++}`;
41
+ }
42
+
43
+ /**
44
+ * 백그라운드로 명령어 실행
45
+ * @param {string} script - 실행할 스크립트
46
+ * @param {Object} options - 옵션
47
+ * @returns {Promise<{id: string, pid: number}>}
48
+ */
49
+ async run(script, options = {}) {
50
+ const shellPath = await whichCommand("bash") || await whichCommand("sh");
51
+ if (!shellPath) {
52
+ throw new Error('No shell found');
53
+ }
54
+
55
+ const id = this._generateId();
56
+ const startedAt = new Date();
57
+
58
+ debugLog(`[BackgroundProcess] Starting: id=${id}, script="${script.substring(0, 50)}..."`);
59
+
60
+ const child = spawn(shellPath, ['-c', script], {
61
+ stdio: ['ignore', 'pipe', 'pipe'],
62
+ env: {
63
+ ...process.env,
64
+ DEBIAN_FRONTEND: 'noninteractive',
65
+ CI: 'true',
66
+ BATCH: '1',
67
+ },
68
+ cwd: options.cwd || process.cwd(),
69
+ detached: true,
70
+ });
71
+
72
+ const processInfo = {
73
+ id,
74
+ command: script,
75
+ pid: child.pid,
76
+ status: 'running',
77
+ startedAt,
78
+ endedAt: null,
79
+ stdout: '',
80
+ stderr: '',
81
+ exitCode: null,
82
+ _process: child, // 내부용
83
+ };
84
+
85
+ this.processes.set(id, processInfo);
86
+
87
+ // stdout 수집
88
+ child.stdout.on('data', (data) => {
89
+ processInfo.stdout += data.toString();
90
+ this.emit('output', { id, type: 'stdout', data: data.toString() });
91
+ });
92
+
93
+ // stderr 수집
94
+ child.stderr.on('data', (data) => {
95
+ processInfo.stderr += data.toString();
96
+ this.emit('output', { id, type: 'stderr', data: data.toString() });
97
+ });
98
+
99
+ // 프로세스 종료 처리
100
+ child.on('close', (code) => {
101
+ processInfo.status = code === 0 ? 'completed' : 'failed';
102
+ processInfo.exitCode = code;
103
+ processInfo.endedAt = new Date();
104
+ delete processInfo._process;
105
+
106
+ debugLog(`[BackgroundProcess] Closed: id=${id}, code=${code}`);
107
+ this.emit('close', { id, code, status: processInfo.status });
108
+ });
109
+
110
+ child.on('error', (err) => {
111
+ processInfo.status = 'failed';
112
+ processInfo.stderr += `\nError: ${err.message}`;
113
+ processInfo.endedAt = new Date();
114
+ delete processInfo._process;
115
+
116
+ debugLog(`[BackgroundProcess] Error: id=${id}, error=${err.message}`);
117
+ this.emit('error', { id, error: err });
118
+ });
119
+
120
+ this.emit('started', { id, pid: child.pid, command: script });
121
+
122
+ return { id, pid: child.pid };
123
+ }
124
+
125
+ /**
126
+ * 프로세스 종료
127
+ * @param {string} id - 프로세스 ID
128
+ * @param {string} signal - 시그널 (기본: SIGTERM)
129
+ * @returns {boolean} 성공 여부
130
+ */
131
+ kill(id, signal = 'SIGTERM') {
132
+ const processInfo = this.processes.get(id);
133
+ if (!processInfo) {
134
+ debugLog(`[BackgroundProcess] Kill failed: id=${id} not found`);
135
+ return false;
136
+ }
137
+
138
+ if (processInfo.status !== 'running') {
139
+ debugLog(`[BackgroundProcess] Kill skipped: id=${id} already ${processInfo.status}`);
140
+ return false;
141
+ }
142
+
143
+ const child = processInfo._process;
144
+ if (!child) {
145
+ return false;
146
+ }
147
+
148
+ try {
149
+ // 프로세스 그룹 전체 종료
150
+ process.kill(-child.pid, signal);
151
+ processInfo.status = 'killed';
152
+ processInfo.endedAt = new Date();
153
+ debugLog(`[BackgroundProcess] Killed: id=${id}, signal=${signal}`);
154
+ this.emit('killed', { id, signal });
155
+ return true;
156
+ } catch (err) {
157
+ try {
158
+ child.kill(signal);
159
+ processInfo.status = 'killed';
160
+ processInfo.endedAt = new Date();
161
+ debugLog(`[BackgroundProcess] Killed (fallback): id=${id}`);
162
+ this.emit('killed', { id, signal });
163
+ return true;
164
+ } catch (err2) {
165
+ debugLog(`[BackgroundProcess] Kill error: id=${id}, error=${err2.message}`);
166
+ return false;
167
+ }
168
+ }
169
+ }
170
+
171
+ /**
172
+ * 프로세스 정보 조회
173
+ * @param {string} id - 프로세스 ID
174
+ * @returns {BackgroundProcess|null}
175
+ */
176
+ get(id) {
177
+ const info = this.processes.get(id);
178
+ if (!info) return null;
179
+
180
+ // _process 필드 제외하고 반환
181
+ const { _process, ...publicInfo } = info;
182
+ return publicInfo;
183
+ }
184
+
185
+ /**
186
+ * 모든 프로세스 목록 조회
187
+ * @param {Object} filter - 필터 옵션
188
+ * @returns {BackgroundProcess[]}
189
+ */
190
+ list(filter = {}) {
191
+ let result = [];
192
+
193
+ for (const [id, info] of this.processes) {
194
+ const { _process, ...publicInfo } = info;
195
+
196
+ // 필터 적용
197
+ if (filter.status && publicInfo.status !== filter.status) continue;
198
+ if (filter.running && publicInfo.status !== 'running') continue;
199
+
200
+ result.push(publicInfo);
201
+ }
202
+
203
+ // 시작 시간순 정렬
204
+ result.sort((a, b) => b.startedAt - a.startedAt);
205
+
206
+ return result;
207
+ }
208
+
209
+ /**
210
+ * 실행 중인 프로세스 수
211
+ */
212
+ get runningCount() {
213
+ let count = 0;
214
+ for (const info of this.processes.values()) {
215
+ if (info.status === 'running') count++;
216
+ }
217
+ return count;
218
+ }
219
+
220
+ /**
221
+ * 완료된 프로세스 정리
222
+ * @param {number} maxAge - 최대 보관 시간 (ms)
223
+ */
224
+ cleanup(maxAge = 3600000) {
225
+ const now = Date.now();
226
+ const toDelete = [];
227
+
228
+ for (const [id, info] of this.processes) {
229
+ if (info.status !== 'running' && info.endedAt) {
230
+ if (now - info.endedAt.getTime() > maxAge) {
231
+ toDelete.push(id);
232
+ }
233
+ }
234
+ }
235
+
236
+ for (const id of toDelete) {
237
+ this.processes.delete(id);
238
+ debugLog(`[BackgroundProcess] Cleaned up: id=${id}`);
239
+ }
240
+
241
+ return toDelete.length;
242
+ }
243
+
244
+ /**
245
+ * 모든 실행 중인 프로세스 종료
246
+ */
247
+ async killAll() {
248
+ const killed = [];
249
+ for (const [id, info] of this.processes) {
250
+ if (info.status === 'running') {
251
+ if (this.kill(id)) {
252
+ killed.push(id);
253
+ }
254
+ }
255
+ }
256
+ return killed;
257
+ }
258
+ }
259
+
260
+ // 싱글톤 인스턴스
261
+ let instance = null;
262
+
263
+ /**
264
+ * BackgroundProcessManager 싱글톤 인스턴스 가져오기
265
+ */
266
+ export function getBackgroundProcessManager() {
267
+ if (!instance) {
268
+ instance = new BackgroundProcessManager();
269
+ }
270
+ return instance;
271
+ }
272
+
273
+ /**
274
+ * 백그라운드로 명령어 실행
275
+ * @param {string} script - 실행할 스크립트
276
+ * @param {Object} options - 옵션
277
+ * @returns {Promise<{id: string, pid: number}>}
278
+ */
279
+ export async function runInBackground(script, options = {}) {
280
+ const manager = getBackgroundProcessManager();
281
+ return await manager.run(script, options);
282
+ }
283
+
284
+ /**
285
+ * 백그라운드 프로세스 종료
286
+ * @param {string} id - 프로세스 ID
287
+ */
288
+ export function killBackgroundProcess(id) {
289
+ const manager = getBackgroundProcessManager();
290
+ return manager.kill(id);
291
+ }
292
+
293
+ /**
294
+ * 백그라운드 프로세스 목록 조회
295
+ */
296
+ export function listBackgroundProcesses(filter = {}) {
297
+ const manager = getBackgroundProcessManager();
298
+ return manager.list(filter);
299
+ }
300
+
301
+ /**
302
+ * 백그라운드 프로세스 정보 조회
303
+ */
304
+ export function getBackgroundProcess(id) {
305
+ const manager = getBackgroundProcessManager();
306
+ return manager.get(id);
307
+ }
308
+
309
+ /**
310
+ * 모든 백그라운드 프로세스 종료 (앱 종료 시)
311
+ */
312
+ export async function killAllBackgroundProcesses() {
313
+ const manager = getBackgroundProcessManager();
314
+ return await manager.killAll();
315
+ }
316
+
317
+ export { BackgroundProcessManager };