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.
|
|
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
|
-
|
|
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.
|
|
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(),
|
|
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
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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(),
|
|
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 {
|