aiterm-mcp 0.7.1 → 0.8.0

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/dist/rtk.js CHANGED
@@ -28,16 +28,23 @@ function truncate(s, n) {
28
28
  return "...";
29
29
  return cp.slice(0, n - 3).join("") + "...";
30
30
  }
31
- const PROMPT_TAIL = /[$#%>]\s*$/;
31
+ // プロンプト行の判定(C3): 行全体が「プロンプト前置文字(語/@ : ~ / \ [] . -)+プロンプト記号($#%>)+末尾空白」
32
+ // の形のときだけプロンプトとみなす。末尾記号だけを見ると `</div>` `>>>` `echo x >` 等の本文行を誤除去する。
33
+ const PROMPT_TAIL = /^[\w@.:~\/\\\[\]-]*[$#%>]\s*$/;
32
34
  /** 観測ログ片からエコー行と前後のプロンプト行を落とし、コマンド出力本体だけ残す(発見的)。 */
33
35
  export function stripShellFrame(text, command) {
34
36
  const lines = text.split("\n");
35
37
  const cmd = command.trim();
36
38
  let start = 0;
37
39
  if (cmd) {
38
- for (let i = 0; i < lines.length; i++)
39
- if (lines[i].includes(cmd))
40
+ // 最初の一致(=コマンドエコー行)だけを落とす。最後の一致まで進めると、出力本体に cmd 文字列が
41
+ // 再出現(cat したファイル内・commit メッセージ内 等)した場合に本体を丸ごと捨ててしまう(C2)。
42
+ for (let i = 0; i < lines.length; i++) {
43
+ if (lines[i].includes(cmd)) {
40
44
  start = i + 1;
45
+ break;
46
+ }
47
+ }
41
48
  }
42
49
  let end = lines.length;
43
50
  while (end > start) {
@@ -77,7 +84,10 @@ export function reducePytest(output) {
77
84
  flush();
78
85
  continue;
79
86
  }
80
- if (t.startsWith("===") && (t.includes("passed") || t.includes("failed") || t.includes("skipped"))) {
87
+ if (t.startsWith("===") &&
88
+ (t.includes("passed") || t.includes("failed") || t.includes("skipped") || t.includes("error"))) {
89
+ // 収集エラーのみ(`=== 1 error in Xs ===`)も要約行として拾う。拾わないと全ゼロ扱いで
90
+ // "No tests collected"(無害誤読)に潰れる。"ERRORS" セクション見出しは大文字ゆえ非該当。
81
91
  summaryLine = t;
82
92
  continue;
83
93
  }
@@ -85,7 +95,7 @@ export function reducePytest(output) {
85
95
  !t.startsWith("===") &&
86
96
  !t.startsWith("FAILED") &&
87
97
  !t.startsWith("ERROR") &&
88
- (t.includes(" passed") || t.includes(" failed") || t.includes(" skipped")) &&
98
+ (t.includes(" passed") || t.includes(" failed") || t.includes(" skipped") || t.includes(" error")) &&
89
99
  t.includes(" in ")) {
90
100
  summaryLine = t;
91
101
  continue;
@@ -114,13 +124,16 @@ export function reducePytest(output) {
114
124
  }
115
125
  }
116
126
  flush();
117
- const [p, f, s, xf, xp] = parsePytestCounts(summaryLine);
118
- if (p === 0 && f === 0 && s === 0 && xf === 0 && xp === 0)
127
+ const [p, f, s, xf, xp, e] = parsePytestCounts(summaryLine);
128
+ if (p === 0 && f === 0 && s === 0 && xf === 0 && xp === 0 && e === 0)
119
129
  return "Pytest: No tests collected";
120
- const extras = s > 0 || xf > 0 || xp > 0 || xfailLines.length > 0;
130
+ // error(収集/内部エラー)は失敗の一種=緑扱いにしない。extras に含め、"N passed" 早期 return を止める。
131
+ const extras = s > 0 || xf > 0 || xp > 0 || e > 0 || xfailLines.length > 0;
121
132
  if (f === 0 && p > 0 && !extras)
122
133
  return `Pytest: ${p} passed`;
123
134
  let head = `Pytest: ${p} passed, ${f} failed`;
135
+ if (e > 0)
136
+ head += `, ${e} error${e === 1 ? "" : "s"}`;
124
137
  if (s > 0)
125
138
  head += `, ${s} skipped`;
126
139
  if (xf > 0)
@@ -183,7 +196,7 @@ export function reducePytest(output) {
183
196
  return out.join("\n").trim();
184
197
  }
185
198
  function parsePytestCounts(summary) {
186
- let p = 0, f = 0, s = 0, xf = 0, xp = 0;
199
+ let p = 0, f = 0, s = 0, xf = 0, xp = 0, e = 0;
187
200
  for (const part of summary.split(",")) {
188
201
  const words = part.split(/\s+/).filter(Boolean);
189
202
  for (let i = 0; i < words.length; i++) {
@@ -193,6 +206,7 @@ function parsePytestCounts(summary) {
193
206
  if (Number.isNaN(n))
194
207
  continue;
195
208
  const w = words[i];
209
+ // "error"/"errors" は passed/failed/skipped/xfailed/xpassed のいずれの部分文字列でもないので順不同で安全。
196
210
  if (w.includes("xpassed"))
197
211
  xp = n;
198
212
  else if (w.includes("xfailed"))
@@ -203,9 +217,11 @@ function parsePytestCounts(summary) {
203
217
  f = n;
204
218
  else if (w.includes("skipped"))
205
219
  s = n;
220
+ else if (w.includes("error"))
221
+ e = n;
206
222
  }
207
223
  }
208
- return [p, f, s, xf, xp];
224
+ return [p, f, s, xf, xp, e];
209
225
  }
210
226
  // ---------------------------------------------------------------- grep
211
227
  const GREP_LINE = /^(.*?):(\d+):(.*)$/;
@@ -310,7 +326,8 @@ export function reduceGitLog(output) {
310
326
  else if (t && !subject && (ln.startsWith(" ") || ln.startsWith("\t")))
311
327
  subject = t;
312
328
  else if (t && (ln.startsWith(" ") || ln.startsWith("\t"))) {
313
- if (!t.startsWith("Signed-off-by:") && !t.startsWith("Co-authored-by:"))
329
+ // trailer は大小文字が実装依存(本リポ規約は Co-Authored-By:)。大小無視で除外する(C5)。
330
+ if (!/^(signed-off-by|co-authored-by):/i.test(t))
314
331
  body.push(t);
315
332
  }
316
333
  }
@@ -355,13 +372,25 @@ function applyFilter(rule, output) {
355
372
  return body;
356
373
  }
357
374
  // ---------------------------------------------------------------- ルーティング
375
+ const WRAPPERS = new Set(["sudo", "env", "command", "exec"]);
376
+ const RUNNERS = new Set(["uv", "poetry", "pdm", "rye", "hatch", "pipenv"]);
377
+ // [verb, sub, stripped]。stripped は前置(ラッパー/環境代入/フラグ/ランナー run)を除いた実コマンド。
378
+ // 誤分類は classify で null→generic に落ちるだけなので、読み飛ばしは積極的で安全(C4)。
358
379
  function basenameCmd(command) {
359
380
  const toks = command.trim().split(/\s+/).filter(Boolean);
360
381
  let i = 0;
361
- while (i < toks.length && (toks[i].includes("=") || ["sudo", "env", "command", "exec"].includes(toks[i])))
382
+ // 前置ラッパー(sudo/env/…)・環境代入(FOO=1)・そのフラグ(-E/-i)を読み飛ばす
383
+ while (i < toks.length && (toks[i].includes("=") || WRAPPERS.has(toks[i]) || toks[i].startsWith("-")))
362
384
  i++;
385
+ // ランナー( uv/poetry run <cmd> )は run と共に読み飛ばして実コマンドへ
386
+ if (i + 1 < toks.length && RUNNERS.has(toks[i]) && toks[i + 1] === "run") {
387
+ i += 2;
388
+ while (i < toks.length && (toks[i].includes("=") || toks[i].startsWith("-")))
389
+ i++;
390
+ }
391
+ const stripped = toks.slice(i).join(" ");
363
392
  if (i >= toks.length)
364
- return ["", ""];
393
+ return ["", "", ""];
365
394
  const verb = toks[i].split("/").pop();
366
395
  let sub = "";
367
396
  for (const t of toks.slice(i + 1)) {
@@ -370,10 +399,10 @@ function basenameCmd(command) {
370
399
  break;
371
400
  }
372
401
  }
373
- return [verb, sub];
402
+ return [verb, sub, stripped];
374
403
  }
375
404
  export function classify(command) {
376
- const [verb, sub] = basenameCmd(command);
405
+ const [verb, sub, stripped] = basenameCmd(command);
377
406
  if (verb === "git") {
378
407
  if (sub === "status")
379
408
  return "git-status";
@@ -385,10 +414,12 @@ export function classify(command) {
385
414
  return "grep";
386
415
  if (verb === "pytest" || verb === "py.test")
387
416
  return "pytest";
388
- if (verb === "python" && command.includes("pytest"))
417
+ // python3 / python3.11 等のバージョン付きも許容(C4: `python3 -m pytest` を拾う)
418
+ if (/^python[0-9.]*$/.test(verb) && command.includes("pytest"))
389
419
  return "pytest";
420
+ // FILTERS は basename 経由の stripped で判定(C6: `sudo df` `sudo make` も分類できる)
390
421
  for (const rule of FILTERS)
391
- if (rule.match.test(command.trim()))
422
+ if (rule.match.test(stripped))
392
423
  return rule.name;
393
424
  return null;
394
425
  }
@@ -398,7 +429,11 @@ const REDUCERS = {
398
429
  grep: reduceGrep,
399
430
  pytest: (o) => reducePytest(o),
400
431
  };
401
- /** コマンドに応じた reducer を観測出力へ適用。返り値 [reducedText|null, reducerName|null]。 */
432
+ /**
433
+ * コマンドに応じた reducer を観測出力へ適用。返り値 [reducedText|null, reducerName|null]。
434
+ * 前提(C13): `output` は制御文字除去済み(ANSI/CR 等)であること。色付き/生 PTY 出力を直接渡すと
435
+ * 誤パースし得る。呼び手 core.readOutput は `stripShellFrame(stripControl(text), cmd)` で前処理してから渡す。
436
+ */
402
437
  export function reduce(command, output) {
403
438
  const name = classify(command);
404
439
  if (name === null)
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "mcpName": "io.github.kitepon-rgb/aiterm-mcp",
5
- "description": "AI-driven persistent terminal as a local stdio MCP server (tmux-backed). Holds one local PTY; SSH and containers are just commands you send into it. Token-reducing reads.",
5
+ "description": "AI-driven persistent terminal as a local stdio MCP server (tmux-backed). Holds one local PTY; SSH and containers are just commands you send into it. Also launches interactive Codex/Grok/Composer agent TUIs in a persistent terminal. Token-reducing reads.",
6
6
  "keywords": [
7
7
  "mcp",
8
8
  "mcp-server",
@@ -15,6 +15,9 @@
15
15
  "claude-code",
16
16
  "cursor",
17
17
  "ai-agent",
18
+ "agent",
19
+ "codex",
20
+ "grok",
18
21
  "devtools"
19
22
  ],
20
23
  "license": "MIT",