aiexecode 1.0.111 → 1.0.112

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.

@@ -1,6 +1,7 @@
1
1
  import { exec } from "child_process";
2
2
  import { promisify } from "util";
3
3
  import { platform } from "os";
4
+ import { readFileSync, existsSync } from "fs";
4
5
 
5
6
  const execAsync = promisify(exec);
6
7
 
@@ -23,6 +24,95 @@ function getOSType() {
23
24
  }
24
25
  }
25
26
 
27
+ /**
28
+ * Linux 배포판 정보를 감지합니다.
29
+ * @returns {Promise<Object>} { id, name, version, packageManager }
30
+ */
31
+ async function detectLinuxDistro() {
32
+ const result = {
33
+ id: 'unknown',
34
+ name: 'Linux',
35
+ version: '',
36
+ packageManager: null,
37
+ packageManagerName: ''
38
+ };
39
+
40
+ // /etc/os-release 파일에서 배포판 정보 읽기
41
+ try {
42
+ if (existsSync('/etc/os-release')) {
43
+ const content = readFileSync('/etc/os-release', 'utf8');
44
+ const lines = content.split('\n');
45
+
46
+ for (const line of lines) {
47
+ const [key, ...valueParts] = line.split('=');
48
+ const value = valueParts.join('=').replace(/^["']|["']$/g, '');
49
+
50
+ if (key === 'ID') result.id = value.toLowerCase();
51
+ if (key === 'NAME') result.name = value;
52
+ if (key === 'VERSION_ID') result.version = value;
53
+ if (key === 'ID_LIKE') result.idLike = value.toLowerCase();
54
+ }
55
+ }
56
+ } catch (e) {
57
+ // 파일 읽기 실패 시 무시
58
+ }
59
+
60
+ // 패키지 매니저 감지
61
+ const packageManagers = [
62
+ { cmd: 'apt', id: ['ubuntu', 'debian', 'linuxmint', 'pop', 'elementary', 'zorin', 'kali'], name: 'APT' },
63
+ { cmd: 'dnf', id: ['fedora', 'rhel', 'centos', 'rocky', 'alma', 'nobara'], name: 'DNF' },
64
+ { cmd: 'yum', id: ['centos', 'rhel', 'amazon'], name: 'YUM' },
65
+ { cmd: 'pacman', id: ['arch', 'manjaro', 'endeavouros', 'garuda'], name: 'Pacman' },
66
+ { cmd: 'zypper', id: ['opensuse', 'suse'], name: 'Zypper' },
67
+ { cmd: 'apk', id: ['alpine'], name: 'APK' },
68
+ { cmd: 'emerge', id: ['gentoo'], name: 'Portage' },
69
+ { cmd: 'brew', id: [], name: 'Homebrew' } // fallback for any Linux with brew
70
+ ];
71
+
72
+ // 배포판 ID로 먼저 매칭
73
+ for (const pm of packageManagers) {
74
+ if (pm.id.includes(result.id) || (result.idLike && pm.id.some(id => result.idLike.includes(id)))) {
75
+ const hasCmd = await getCommandPath(pm.cmd);
76
+ if (hasCmd) {
77
+ result.packageManager = pm.cmd;
78
+ result.packageManagerName = pm.name;
79
+ break;
80
+ }
81
+ }
82
+ }
83
+
84
+ // ID 매칭 실패 시 설치된 패키지 매니저로 감지
85
+ if (!result.packageManager) {
86
+ for (const pm of packageManagers) {
87
+ const hasCmd = await getCommandPath(pm.cmd);
88
+ if (hasCmd) {
89
+ result.packageManager = pm.cmd;
90
+ result.packageManagerName = pm.name;
91
+ break;
92
+ }
93
+ }
94
+ }
95
+
96
+ return result;
97
+ }
98
+
99
+ /**
100
+ * macOS 버전 정보를 가져옵니다.
101
+ * @returns {Promise<Object>} { name, version }
102
+ */
103
+ async function getMacOSInfo() {
104
+ const result = { name: 'macOS', version: '' };
105
+
106
+ try {
107
+ const { stdout } = await execAsync('sw_vers -productVersion', { encoding: 'utf8' });
108
+ result.version = stdout.trim();
109
+ } catch (e) {
110
+ // 실패 시 무시
111
+ }
112
+
113
+ return result;
114
+ }
115
+
26
116
  /**
27
117
  * 특정 명령어의 실행 파일 경로를 찾습니다.
28
118
  * @param {string} command - 찾을 명령어 이름
@@ -166,26 +256,161 @@ export async function getSystemInfoString(options = {}) {
166
256
  return lines.join('\n');
167
257
  }
168
258
 
259
+ /**
260
+ * 명령어별 설치 방법을 반환합니다.
261
+ * @param {string} command - 명령어 이름
262
+ * @param {string} os - OS 타입
263
+ * @param {Object} linuxDistro - Linux 배포판 정보
264
+ * @returns {Object} { primary, alternatives, url }
265
+ */
266
+ function getInstallInstructions(command, os, linuxDistro = null) {
267
+ const instructions = {
268
+ ripgrep: {
269
+ macos: {
270
+ primary: 'brew install ripgrep',
271
+ alternatives: ['cargo install ripgrep'],
272
+ url: 'https://github.com/BurntSushi/ripgrep#installation'
273
+ },
274
+ linux: {
275
+ apt: { primary: 'sudo apt install ripgrep', alternatives: [] },
276
+ dnf: { primary: 'sudo dnf install ripgrep', alternatives: [] },
277
+ yum: { primary: 'sudo yum install ripgrep', alternatives: [] },
278
+ pacman: { primary: 'sudo pacman -S ripgrep', alternatives: [] },
279
+ zypper: { primary: 'sudo zypper install ripgrep', alternatives: [] },
280
+ apk: { primary: 'sudo apk add ripgrep', alternatives: [] },
281
+ brew: { primary: 'brew install ripgrep', alternatives: [] },
282
+ default: { primary: 'cargo install ripgrep', alternatives: ['brew install ripgrep'] },
283
+ url: 'https://github.com/BurntSushi/ripgrep#installation'
284
+ }
285
+ },
286
+ node: {
287
+ macos: {
288
+ primary: 'brew install node',
289
+ alternatives: ['Use nvm: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash'],
290
+ url: 'https://nodejs.org/'
291
+ },
292
+ linux: {
293
+ apt: { primary: 'sudo apt install nodejs npm', alternatives: ['curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt install -y nodejs'] },
294
+ dnf: { primary: 'sudo dnf install nodejs npm', alternatives: [] },
295
+ yum: { primary: 'sudo yum install nodejs npm', alternatives: [] },
296
+ pacman: { primary: 'sudo pacman -S nodejs npm', alternatives: [] },
297
+ zypper: { primary: 'sudo zypper install nodejs npm', alternatives: [] },
298
+ apk: { primary: 'sudo apk add nodejs npm', alternatives: [] },
299
+ brew: { primary: 'brew install node', alternatives: [] },
300
+ default: { primary: 'Use nvm: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash', alternatives: [] },
301
+ url: 'https://nodejs.org/'
302
+ }
303
+ },
304
+ bash: {
305
+ macos: {
306
+ primary: 'bash is built-in on macOS',
307
+ alternatives: [],
308
+ url: null
309
+ },
310
+ linux: {
311
+ apt: { primary: 'sudo apt install bash', alternatives: [] },
312
+ dnf: { primary: 'sudo dnf install bash', alternatives: [] },
313
+ yum: { primary: 'sudo yum install bash', alternatives: [] },
314
+ pacman: { primary: 'sudo pacman -S bash', alternatives: [] },
315
+ zypper: { primary: 'sudo zypper install bash', alternatives: [] },
316
+ apk: { primary: 'sudo apk add bash', alternatives: [] },
317
+ default: { primary: 'Install bash via your package manager', alternatives: [] },
318
+ url: null
319
+ }
320
+ },
321
+ python: {
322
+ macos: {
323
+ primary: 'brew install python3',
324
+ alternatives: [],
325
+ url: 'https://www.python.org/downloads/'
326
+ },
327
+ linux: {
328
+ apt: { primary: 'sudo apt install python3', alternatives: [] },
329
+ dnf: { primary: 'sudo dnf install python3', alternatives: [] },
330
+ yum: { primary: 'sudo yum install python3', alternatives: [] },
331
+ pacman: { primary: 'sudo pacman -S python', alternatives: [] },
332
+ zypper: { primary: 'sudo zypper install python3', alternatives: [] },
333
+ apk: { primary: 'sudo apk add python3', alternatives: [] },
334
+ brew: { primary: 'brew install python3', alternatives: [] },
335
+ default: { primary: 'Install python3 via your package manager', alternatives: [] },
336
+ url: 'https://www.python.org/downloads/'
337
+ }
338
+ }
339
+ };
340
+
341
+ const cmdInstructions = instructions[command];
342
+ if (!cmdInstructions) {
343
+ return { primary: `Install ${command}`, alternatives: [], url: null };
344
+ }
345
+
346
+ if (os === 'macos') {
347
+ return cmdInstructions.macos;
348
+ }
349
+
350
+ if (os === 'linux' && linuxDistro) {
351
+ const pm = linuxDistro.packageManager;
352
+ const linuxInst = cmdInstructions.linux;
353
+
354
+ if (pm && linuxInst[pm]) {
355
+ return {
356
+ ...linuxInst[pm],
357
+ url: linuxInst.url
358
+ };
359
+ }
360
+
361
+ return {
362
+ ...linuxInst.default,
363
+ url: linuxInst.url
364
+ };
365
+ }
366
+
367
+ return { primary: `Install ${command}`, alternatives: [], url: null };
368
+ }
369
+
169
370
  /**
170
371
  * 필수 의존성을 체크하고 문제가 있으면 설치 방법을 안내합니다.
171
372
  * @param {Object} options - 옵션
172
373
  * @param {boolean} options.skipPython - Python 체크를 생략할지 여부
173
- * @returns {Promise<Object>} { success: boolean, issues: Array, os: string }
374
+ * @returns {Promise<Object>} { success: boolean, issues: Array, os: string, osInfo: Object }
174
375
  */
175
376
  export async function checkDependencies(options = {}) {
176
377
  const { skipPython = false } = options;
177
378
  const info = await getSystemInfo({ skipPython });
178
379
  const issues = [];
179
380
 
381
+ // OS 상세 정보 수집
382
+ let osInfo = { name: info.os, version: '' };
383
+ let linuxDistro = null;
384
+
385
+ if (info.os === 'linux') {
386
+ linuxDistro = await detectLinuxDistro();
387
+ osInfo = {
388
+ name: linuxDistro.name,
389
+ version: linuxDistro.version,
390
+ id: linuxDistro.id,
391
+ packageManager: linuxDistro.packageManager,
392
+ packageManagerName: linuxDistro.packageManagerName
393
+ };
394
+ } else if (info.os === 'macos') {
395
+ const macInfo = await getMacOSInfo();
396
+ osInfo = macInfo;
397
+ }
398
+
180
399
  // Windows 체크
181
400
  if (info.os === 'windows') {
182
401
  return {
183
402
  success: false,
184
403
  os: 'windows',
404
+ osInfo: { name: 'Windows', version: '' },
185
405
  issues: [{
186
406
  type: 'unsupported_os',
187
407
  message: 'Windows is not supported',
188
- details: 'This application only supports macOS and Linux operating systems.'
408
+ details: 'This application requires macOS or Linux.',
409
+ suggestions: [
410
+ 'Use Windows Subsystem for Linux (WSL2)',
411
+ 'Use a Linux virtual machine',
412
+ 'Use Docker with a Linux container'
413
+ ]
189
414
  }],
190
415
  warnings: []
191
416
  };
@@ -193,33 +418,27 @@ export async function checkDependencies(options = {}) {
193
418
 
194
419
  // ripgrep 체크 (필수)
195
420
  if (!info.commands.hasRipgrep) {
196
- const installInstructions = info.os === 'macos'
197
- ? 'brew install ripgrep'
198
- : info.os === 'linux'
199
- ? 'apt install ripgrep # or brew install ripgrep # or cargo install ripgrep'
200
- : 'Visit https://github.com/BurntSushi/ripgrep#installation';
201
-
421
+ const inst = getInstallInstructions('ripgrep', info.os, linuxDistro);
202
422
  issues.push({
203
423
  type: 'missing_command',
204
- command: 'ripgrep (rg)',
205
- message: 'ripgrep is not installed',
206
- install: installInstructions
424
+ command: 'ripgrep',
425
+ displayName: 'ripgrep (rg)',
426
+ description: 'Fast regex-based code search tool',
427
+ message: 'ripgrep is required for code search functionality',
428
+ install: inst
207
429
  });
208
430
  }
209
431
 
210
- // node 체크 (필수)
432
+ // node 체크 (필수) - 이 메시지를 보려면 node가 있어야 하지만 완전성을 위해 유지
211
433
  if (!info.commands.hasNode) {
212
- const installInstructions = info.os === 'macos'
213
- ? 'brew install node'
214
- : info.os === 'linux'
215
- ? 'Visit https://nodejs.org/ or use nvm: https://github.com/nvm-sh/nvm'
216
- : 'Visit https://nodejs.org/';
217
-
434
+ const inst = getInstallInstructions('node', info.os, linuxDistro);
218
435
  issues.push({
219
436
  type: 'missing_command',
220
437
  command: 'node',
221
- message: 'Node.js is not installed',
222
- install: installInstructions
438
+ displayName: 'Node.js',
439
+ description: 'JavaScript runtime environment',
440
+ message: 'Node.js is required to run this application',
441
+ install: inst
223
442
  });
224
443
  }
225
444
 
@@ -227,42 +446,37 @@ export async function checkDependencies(options = {}) {
227
446
  if (!info.commands.hasBash) {
228
447
  const shellPath = await getCommandPath('sh');
229
448
  if (!shellPath) {
230
- const installInstructions = info.os === 'macos'
231
- ? 'bash is built-in on macOS. Please check your system.'
232
- : info.os === 'linux'
233
- ? 'apt install bash # or check your package manager'
234
- : 'bash or sh shell is required';
235
-
449
+ const inst = getInstallInstructions('bash', info.os, linuxDistro);
236
450
  issues.push({
237
451
  type: 'missing_command',
238
- command: 'bash or sh',
239
- message: 'No compatible shell found',
240
- install: installInstructions
452
+ command: 'bash',
453
+ displayName: 'Bash Shell',
454
+ description: 'Unix shell for command execution',
455
+ message: 'A compatible shell (bash or sh) is required',
456
+ install: inst
241
457
  });
242
458
  }
243
459
  }
244
460
 
245
- // python 체크 (선택사항 - 경고만 표시, skipPython이 true면 생략)
461
+ // python 체크 (선택사항)
246
462
  const warnings = [];
247
463
  if (!skipPython && !info.commands.hasPython) {
248
- const installInstructions = info.os === 'macos'
249
- ? 'brew install python3'
250
- : info.os === 'linux'
251
- ? 'apt install python3 # or yum install python3'
252
- : 'Visit https://www.python.org/downloads/';
253
-
464
+ const inst = getInstallInstructions('python', info.os, linuxDistro);
254
465
  warnings.push({
255
466
  type: 'optional_command',
256
- command: 'python or python3',
257
- message: 'Python is not installed',
258
- install: installInstructions,
259
- impact: 'The following tools will be disabled: fetch_web_page, run_python_code'
467
+ command: 'python',
468
+ displayName: 'Python 3',
469
+ description: 'Programming language for web scraping and scripting',
470
+ message: 'Python is optional but enables additional features',
471
+ install: inst,
472
+ disabledFeatures: ['fetch_web_page', 'run_python_code']
260
473
  });
261
474
  }
262
475
 
263
476
  return {
264
477
  success: issues.length === 0,
265
478
  os: info.os,
479
+ osInfo,
266
480
  issues,
267
481
  warnings
268
482
  };
@@ -2,6 +2,7 @@ import { safeReadFile, safeStat } from '../util/safe_fs.js';
2
2
  import { resolve } from 'path';
3
3
  import { createHash } from 'crypto';
4
4
  import { trackFileRead, saveFileSnapshot } from '../system/file_integrity.js';
5
+ import { markFileAsReRead } from '../system/conversation_trimmer.js';
5
6
  import { createDebugLogger } from '../util/debug_log.js';
6
7
  import { toDisplayPath } from '../util/path_helper.js';
7
8
  import { theme } from '../frontend/design/themeColors.js';
@@ -56,6 +57,8 @@ export async function read_file({ filePath }) {
56
57
  if (fileSizeBytes > MAX_FILE_SIZE_BYTES) {
57
58
  debugLog(`ERROR: File exceeds ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB size limit`);
58
59
  debugLog('========== read_file ERROR END ==========');
60
+ // trim된 파일 목록에서 제거 (재읽기 시도했으므로 알림 불필요)
61
+ markFileAsReRead(absolutePath);
59
62
  return {
60
63
  operation_successful: false,
61
64
  error_message: `File exceeds 10MB size limit (actual: ${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB). Large files cannot be read for security reasons.`,
@@ -83,6 +86,8 @@ export async function read_file({ filePath }) {
83
86
  if (totalLines > MAX_LINES) {
84
87
  debugLog(`ERROR: File exceeds ${MAX_LINES} lines limit`);
85
88
  debugLog('========== read_file ERROR END ==========');
89
+ // trim된 파일 목록에서 제거 (재읽기 시도했으므로 알림 불필요)
90
+ markFileAsReRead(absolutePath);
86
91
  return {
87
92
  operation_successful: false,
88
93
  error_message: `File exceeds ${MAX_LINES} lines (actual: ${totalLines} lines). Use read_file_range to read specific sections of this large file.`,
@@ -98,6 +103,9 @@ export async function read_file({ filePath }) {
98
103
  await trackFileRead(absolutePath, content);
99
104
  debugLog(`File read tracked`);
100
105
 
106
+ // trim된 파일 목록에서 제거 (다시 읽었으므로 알림 불필요)
107
+ markFileAsReRead(absolutePath);
108
+
101
109
  // 스냅샷 저장 (UI 미리보기용)
102
110
  debugLog(`Saving file snapshot...`);
103
111
  saveFileSnapshot(absolutePath, content);
@@ -125,6 +133,8 @@ export async function read_file({ filePath }) {
125
133
 
126
134
  // 에러 시에도 절대경로로 반환
127
135
  const absolutePath = resolve(filePath);
136
+ // trim된 파일 목록에서 제거 (재읽기 시도했으므로 알림 불필요)
137
+ markFileAsReRead(absolutePath);
128
138
  return {
129
139
  operation_successful: false,
130
140
  error_message: error.message,
@@ -214,6 +224,8 @@ export async function read_file_range({ filePath, startLine, endLine }) {
214
224
  if (fileSizeBytes > MAX_FILE_SIZE_BYTES) {
215
225
  debugLog(`ERROR: File exceeds ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB size limit`);
216
226
  debugLog('========== read_file_range ERROR END ==========');
227
+ // trim된 파일 목록에서 제거 (재읽기 시도했으므로 알림 불필요)
228
+ markFileAsReRead(absolutePath);
217
229
  return {
218
230
  operation_successful: false,
219
231
  error_message: `File exceeds 10MB size limit (actual: ${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB). Large files cannot be read for security reasons.`,
@@ -246,6 +258,9 @@ export async function read_file_range({ filePath, startLine, endLine }) {
246
258
  await trackFileRead(absolutePath, content);
247
259
  debugLog(`File read tracked`);
248
260
 
261
+ // trim된 파일 목록에서 제거 (다시 읽었으므로 알림 불필요)
262
+ markFileAsReRead(absolutePath);
263
+
249
264
  // 스냅샷 저장 (UI 미리보기용)
250
265
  debugLog(`Saving file snapshot...`);
251
266
  saveFileSnapshot(absolutePath, content);
@@ -303,6 +318,8 @@ export async function read_file_range({ filePath, startLine, endLine }) {
303
318
 
304
319
  // 에러 시에도 절대경로로 반환
305
320
  const absolutePath = resolve(filePath);
321
+ // trim된 파일 목록에서 제거 (재읽기 시도했으므로 알림 불필요)
322
+ markFileAsReRead(absolutePath);
306
323
  return {
307
324
  operation_successful: false,
308
325
  error_message: error.message,
@@ -158,4 +158,27 @@ export function createTodoReminder(todos) {
158
158
  `현재 할 일 목록:\n${todoList}\n\n완료된 작업은 즉시 완료 처리하세요. 한 번에 하나의 작업만 in_progress로 유지하세요.`,
159
159
  { hideFromUser: false }
160
160
  );
161
+ }
162
+
163
+ /**
164
+ * Trim된 파일 읽기 알림을 생성합니다.
165
+ * Claude Code 스타일: 대화가 trim되어 파일 내용이 삭제되었지만 경로는 기억하도록 안내합니다.
166
+ *
167
+ * @param {Array<string>} filePaths - trim된 파일 경로 배열
168
+ * @returns {string|null} system-reminder 형식의 알림 또는 null
169
+ */
170
+ export function createTrimmedFileReminder(filePaths) {
171
+ if (!filePaths || filePaths.length === 0) {
172
+ return null;
173
+ }
174
+
175
+ const fileList = filePaths.map(path => ` - ${path}`).join('\n');
176
+
177
+ const reminderText = `The following files were read before the conversation was trimmed, but their contents are too large to include in context. Use read_file or read_file_range if you need to access them again:
178
+
179
+ ${fileList}
180
+
181
+ Note: These files were previously read in this session. The file integrity system still tracks them, so you can edit them after re-reading.`;
182
+
183
+ return createSystemReminder(reminderText, { hideFromUser: true });
161
184
  }