ai-cli-mcp 2.14.0 → 2.14.1

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [2.14.1](https://github.com/mkXultra/ai-cli-mcp/compare/v2.14.0...v2.14.1) (2026-04-07)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * 削除済み作業フォルダでのプロセス操作クラッシュを修正 ([02d765f](https://github.com/mkXultra/ai-cli-mcp/commit/02d765ff76ebd295118e16a112ea3e7ac6fab111))
7
+
1
8
  # [2.14.0](https://github.com/mkXultra/ai-cli-mcp/compare/v2.13.0...v2.14.0) (2026-04-07)
2
9
 
3
10
 
@@ -294,6 +294,90 @@ printf '%s\n' '{"type":"system","session_id":"session-cli-1"}'
294
294
  expect(stored.status).toBe('running');
295
295
  killSpy.mockRestore();
296
296
  });
297
+ it('lists processes without crashing when a tracked work folder has been deleted', async () => {
298
+ const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
299
+ tempDirs.push(root);
300
+ const stateDir = join(root, 'state');
301
+ const workFolder = join(root, 'deleted-project');
302
+ mkdirSync(workFolder, { recursive: true });
303
+ const pid = 45678;
304
+ const processDir = join(stateDir, 'cwds', encodeCwd(realpathSync(workFolder)), String(pid));
305
+ mkdirSync(processDir, { recursive: true });
306
+ writeFileSync(join(processDir, 'meta.json'), JSON.stringify({
307
+ pid,
308
+ prompt: 'deleted cwd',
309
+ workFolder,
310
+ toolType: 'claude',
311
+ startTime: new Date().toISOString(),
312
+ stdoutPath: join(processDir, 'stdout.log'),
313
+ stderrPath: join(processDir, 'stderr.log'),
314
+ status: 'running',
315
+ }));
316
+ rmSync(workFolder, { recursive: true, force: true });
317
+ const service = new CliProcessService({
318
+ stateDir,
319
+ cliPaths: {
320
+ claude: '/bin/sh',
321
+ codex: '/bin/sh',
322
+ gemini: '/bin/sh',
323
+ forge: '/bin/sh',
324
+ },
325
+ });
326
+ const killSpy = vi.spyOn(globalThis.process, 'kill').mockImplementation((target, signal) => {
327
+ if (signal === 0 && target === pid) {
328
+ throw Object.assign(new Error('not running'), { code: 'ESRCH' });
329
+ }
330
+ return true;
331
+ });
332
+ const listed = await service.listProcesses();
333
+ expect(listed).toEqual([
334
+ {
335
+ pid,
336
+ agent: 'claude',
337
+ status: 'completed',
338
+ },
339
+ ]);
340
+ expect(JSON.parse(readFileSync(join(processDir, 'meta.json'), 'utf-8')).status).toBe('completed');
341
+ killSpy.mockRestore();
342
+ });
343
+ it('cleans up finished process directories even when their work folder has been deleted', async () => {
344
+ const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
345
+ tempDirs.push(root);
346
+ const stateDir = join(root, 'state');
347
+ const workFolder = join(root, 'deleted-finished-project');
348
+ mkdirSync(workFolder, { recursive: true });
349
+ const pid = 56789;
350
+ const cwdDir = join(stateDir, 'cwds', encodeCwd(realpathSync(workFolder)));
351
+ const processDir = join(cwdDir, String(pid));
352
+ mkdirSync(processDir, { recursive: true });
353
+ writeFileSync(join(processDir, 'meta.json'), JSON.stringify({
354
+ pid,
355
+ prompt: 'done',
356
+ workFolder,
357
+ toolType: 'claude',
358
+ startTime: new Date().toISOString(),
359
+ stdoutPath: join(processDir, 'stdout.log'),
360
+ stderrPath: join(processDir, 'stderr.log'),
361
+ status: 'completed',
362
+ }));
363
+ rmSync(workFolder, { recursive: true, force: true });
364
+ const service = new CliProcessService({
365
+ stateDir,
366
+ cliPaths: {
367
+ claude: '/bin/sh',
368
+ codex: '/bin/sh',
369
+ gemini: '/bin/sh',
370
+ forge: '/bin/sh',
371
+ },
372
+ });
373
+ const result = await service.cleanupProcesses();
374
+ expect(result).toEqual({
375
+ removed: 1,
376
+ message: 'Removed 1 processes',
377
+ });
378
+ expect(existsSync(processDir)).toBe(false);
379
+ expect(existsSync(cwdDir)).toBe(false);
380
+ });
297
381
  it('cleans up completed and failed process directories but preserves running ones', async () => {
298
382
  const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
299
383
  tempDirs.push(root);
@@ -1,6 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync, } from 'node:fs';
3
- import { join } from 'node:path';
3
+ import { join, basename, dirname } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { buildCliCommand } from './cli-builder.js';
6
6
  import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from './cli-utils.js';
@@ -77,6 +77,7 @@ export class CliProcessService {
77
77
  pid,
78
78
  prompt: cmd.prompt,
79
79
  workFolder: cmd.cwd,
80
+ cwdKey: this.resolveCwdKey(cmd.cwd),
80
81
  model: options.model,
81
82
  toolType: cmd.agent,
82
83
  startTime: new Date().toISOString(),
@@ -196,7 +197,7 @@ export class CliProcessService {
196
197
  if (refreshed.status === 'running') {
197
198
  continue;
198
199
  }
199
- const processDir = this.resolveProcessDir(refreshed.workFolder, refreshed.pid);
200
+ const processDir = this.resolveStoredProcessDir(refreshed);
200
201
  if (existsSync(processDir)) {
201
202
  rmSync(processDir, { recursive: true, force: true });
202
203
  removed++;
@@ -233,10 +234,14 @@ export class CliProcessService {
233
234
  return process;
234
235
  }
235
236
  parseProcessFile(metaPath) {
236
- return JSON.parse(readFileSync(metaPath, 'utf-8'));
237
+ const process = JSON.parse(readFileSync(metaPath, 'utf-8'));
238
+ if (!process.cwdKey) {
239
+ process.cwdKey = basename(dirname(dirname(metaPath)));
240
+ }
241
+ return process;
237
242
  }
238
243
  writeProcess(process) {
239
- const processDir = this.resolveProcessDir(process.workFolder, process.pid);
244
+ const processDir = this.resolveStoredProcessDir(process);
240
245
  mkdirSync(processDir, { recursive: true });
241
246
  writeFileSync(this.resolveMetaPath(processDir), JSON.stringify(process, null, 2));
242
247
  }
@@ -257,7 +262,16 @@ export class CliProcessService {
257
262
  return join(this.stateDir, 'cwds');
258
263
  }
259
264
  resolveProcessDir(cwd, pid) {
260
- return join(this.resolveCwdsDir(), normalizeCwdForStorage(realpathSync(cwd)), String(pid));
265
+ return join(this.resolveCwdsDir(), this.resolveCwdKey(cwd), String(pid));
266
+ }
267
+ resolveStoredProcessDir(process) {
268
+ if (!process.cwdKey) {
269
+ process.cwdKey = this.resolveCwdKey(process.workFolder);
270
+ }
271
+ return join(this.resolveCwdsDir(), process.cwdKey, String(process.pid));
272
+ }
273
+ resolveCwdKey(cwd) {
274
+ return normalizeCwdForStorage(realpathSync(cwd));
261
275
  }
262
276
  resolveMetaPath(processDir) {
263
277
  return join(processDir, 'meta.json');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-cli-mcp",
3
- "version": "2.14.0",
3
+ "version": "2.14.1",
4
4
  "mcpName": "io.github.mkXultra/ai-cli-mcp",
5
5
  "description": "MCP server for AI CLI tools (Claude, Codex, Gemini, and Forge) with background process management",
6
6
  "author": "mkXultra",
@@ -334,6 +334,111 @@ printf '%s\n' '{"type":"system","session_id":"session-cli-1"}'
334
334
  killSpy.mockRestore();
335
335
  });
336
336
 
337
+ it('lists processes without crashing when a tracked work folder has been deleted', async () => {
338
+ const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
339
+ tempDirs.push(root);
340
+ const stateDir = join(root, 'state');
341
+ const workFolder = join(root, 'deleted-project');
342
+ mkdirSync(workFolder, { recursive: true });
343
+
344
+ const pid = 45678;
345
+ const processDir = join(stateDir, 'cwds', encodeCwd(realpathSync(workFolder)), String(pid));
346
+ mkdirSync(processDir, { recursive: true });
347
+
348
+ writeFileSync(
349
+ join(processDir, 'meta.json'),
350
+ JSON.stringify({
351
+ pid,
352
+ prompt: 'deleted cwd',
353
+ workFolder,
354
+ toolType: 'claude',
355
+ startTime: new Date().toISOString(),
356
+ stdoutPath: join(processDir, 'stdout.log'),
357
+ stderrPath: join(processDir, 'stderr.log'),
358
+ status: 'running',
359
+ })
360
+ );
361
+
362
+ rmSync(workFolder, { recursive: true, force: true });
363
+
364
+ const service = new CliProcessService({
365
+ stateDir,
366
+ cliPaths: {
367
+ claude: '/bin/sh',
368
+ codex: '/bin/sh',
369
+ gemini: '/bin/sh',
370
+ forge: '/bin/sh',
371
+ },
372
+ });
373
+
374
+ const killSpy = vi.spyOn(globalThis.process, 'kill').mockImplementation((target: number, signal?: string | number) => {
375
+ if (signal === 0 && target === pid) {
376
+ throw Object.assign(new Error('not running'), { code: 'ESRCH' });
377
+ }
378
+ return true;
379
+ });
380
+
381
+ const listed = await service.listProcesses();
382
+
383
+ expect(listed).toEqual([
384
+ {
385
+ pid,
386
+ agent: 'claude',
387
+ status: 'completed',
388
+ },
389
+ ]);
390
+ expect(JSON.parse(readFileSync(join(processDir, 'meta.json'), 'utf-8')).status).toBe('completed');
391
+ killSpy.mockRestore();
392
+ });
393
+
394
+ it('cleans up finished process directories even when their work folder has been deleted', async () => {
395
+ const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
396
+ tempDirs.push(root);
397
+ const stateDir = join(root, 'state');
398
+ const workFolder = join(root, 'deleted-finished-project');
399
+ mkdirSync(workFolder, { recursive: true });
400
+
401
+ const pid = 56789;
402
+ const cwdDir = join(stateDir, 'cwds', encodeCwd(realpathSync(workFolder)));
403
+ const processDir = join(cwdDir, String(pid));
404
+ mkdirSync(processDir, { recursive: true });
405
+
406
+ writeFileSync(
407
+ join(processDir, 'meta.json'),
408
+ JSON.stringify({
409
+ pid,
410
+ prompt: 'done',
411
+ workFolder,
412
+ toolType: 'claude',
413
+ startTime: new Date().toISOString(),
414
+ stdoutPath: join(processDir, 'stdout.log'),
415
+ stderrPath: join(processDir, 'stderr.log'),
416
+ status: 'completed',
417
+ })
418
+ );
419
+
420
+ rmSync(workFolder, { recursive: true, force: true });
421
+
422
+ const service = new CliProcessService({
423
+ stateDir,
424
+ cliPaths: {
425
+ claude: '/bin/sh',
426
+ codex: '/bin/sh',
427
+ gemini: '/bin/sh',
428
+ forge: '/bin/sh',
429
+ },
430
+ });
431
+
432
+ const result = await service.cleanupProcesses();
433
+
434
+ expect(result).toEqual({
435
+ removed: 1,
436
+ message: 'Removed 1 processes',
437
+ });
438
+ expect(existsSync(processDir)).toBe(false);
439
+ expect(existsSync(cwdDir)).toBe(false);
440
+ });
441
+
337
442
  it('cleans up completed and failed process directories but preserves running ones', async () => {
338
443
  const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
339
444
  tempDirs.push(root);
@@ -12,7 +12,7 @@ import {
12
12
  unlinkSync,
13
13
  writeFileSync,
14
14
  } from 'node:fs';
15
- import { join } from 'node:path';
15
+ import { join, basename, dirname } from 'node:path';
16
16
  import { homedir } from 'node:os';
17
17
  import { buildCliCommand, type BuildCliCommandOptions } from './cli-builder.js';
18
18
  import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from './cli-utils.js';
@@ -24,6 +24,7 @@ interface StoredProcess {
24
24
  pid: number;
25
25
  prompt: string;
26
26
  workFolder: string;
27
+ cwdKey?: string;
27
28
  model?: string;
28
29
  toolType: AgentType;
29
30
  startTime: string;
@@ -128,6 +129,7 @@ export class CliProcessService {
128
129
  pid,
129
130
  prompt: cmd.prompt,
130
131
  workFolder: cmd.cwd,
132
+ cwdKey: this.resolveCwdKey(cmd.cwd),
131
133
  model: options.model,
132
134
  toolType: cmd.agent,
133
135
  startTime: new Date().toISOString(),
@@ -260,7 +262,7 @@ export class CliProcessService {
260
262
  continue;
261
263
  }
262
264
 
263
- const processDir = this.resolveProcessDir(refreshed.workFolder, refreshed.pid);
265
+ const processDir = this.resolveStoredProcessDir(refreshed);
264
266
  if (existsSync(processDir)) {
265
267
  rmSync(processDir, { recursive: true, force: true });
266
268
  removed++;
@@ -304,11 +306,15 @@ export class CliProcessService {
304
306
  }
305
307
 
306
308
  private parseProcessFile(metaPath: string): StoredProcess {
307
- return JSON.parse(readFileSync(metaPath, 'utf-8')) as StoredProcess;
309
+ const process = JSON.parse(readFileSync(metaPath, 'utf-8')) as StoredProcess;
310
+ if (!process.cwdKey) {
311
+ process.cwdKey = basename(dirname(dirname(metaPath)));
312
+ }
313
+ return process;
308
314
  }
309
315
 
310
316
  private writeProcess(process: StoredProcess): void {
311
- const processDir = this.resolveProcessDir(process.workFolder, process.pid);
317
+ const processDir = this.resolveStoredProcessDir(process);
312
318
  mkdirSync(processDir, { recursive: true });
313
319
  writeFileSync(this.resolveMetaPath(processDir), JSON.stringify(process, null, 2));
314
320
  }
@@ -333,7 +339,18 @@ export class CliProcessService {
333
339
  }
334
340
 
335
341
  private resolveProcessDir(cwd: string, pid: number): string {
336
- return join(this.resolveCwdsDir(), normalizeCwdForStorage(realpathSync(cwd)), String(pid));
342
+ return join(this.resolveCwdsDir(), this.resolveCwdKey(cwd), String(pid));
343
+ }
344
+
345
+ private resolveStoredProcessDir(process: StoredProcess): string {
346
+ if (!process.cwdKey) {
347
+ process.cwdKey = this.resolveCwdKey(process.workFolder);
348
+ }
349
+ return join(this.resolveCwdsDir(), process.cwdKey, String(process.pid));
350
+ }
351
+
352
+ private resolveCwdKey(cwd: string): string {
353
+ return normalizeCwdForStorage(realpathSync(cwd));
337
354
  }
338
355
 
339
356
  private resolveMetaPath(processDir: string): string {