aiterm-mcp 0.44.1 → 0.45.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/CHANGELOG.md +16 -1
- package/README.ja.md +15 -11
- package/README.md +15 -11
- package/dist/rtk.js +267 -46
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.45.0] - 2026-09-30
|
|
11
|
+
|
|
12
|
+
### 変更
|
|
13
|
+
|
|
14
|
+
- `pty_read({ rtk: true })`のreducerの手本をrtk 0.42.0から0.50.0へ上げた。
|
|
15
|
+
- grep:rtkの上限(全体200行・1ファイル25行)に収まる間は、grepの出力をそのまま返す。これまでは件数の見出しを付け、長い行を80字で切っていた。上限を超えた時だけファイルごとにまとめ、長い行は検索語の周りを残して切り、省いた分を`+N more in <file>`/`+N more files`で書く。まとめた形が元より長ければ元を返す。
|
|
16
|
+
- pytest:件数の見出しの下の罫線をやめた。pytestが報告の前に落ちた時(`INTERNALERROR>`、`ERROR: `)は、`Pytest: No tests collected`にせず汎用削減へ戻す。
|
|
17
|
+
- 縮めた結果が元より多くのトークンを使う時は、reducerを適用しない(rtkの`never_worse`)。
|
|
18
|
+
- pytestとgrepの回帰見本は、公開版rtk 0.50.0の実行結果から取り直した(recallの案内行は除く)。`FAILED`要約行の理由を全文残す違いは従来どおり。
|
|
19
|
+
|
|
20
|
+
### 修正
|
|
21
|
+
|
|
22
|
+
- `git log`のreducerが、何も書かずに10件で打ち切っていた。件数(`-n N`/`-N`/`--max-count`)か範囲(`A..B`)を指定したlogは打ち切らない。件数を指定した時は、rtkと同じく1行の幅を120字にする。指定が無い時は10件にして、`[+N more commits]`で終える。
|
|
23
|
+
|
|
10
24
|
## [0.44.1] - 2026-09-30
|
|
11
25
|
|
|
12
26
|
### 修正
|
|
@@ -1924,7 +1938,8 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
1924
1938
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
1925
1939
|
provenance.
|
|
1926
1940
|
|
|
1927
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.
|
|
1941
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.45.0...HEAD
|
|
1942
|
+
[0.45.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.44.1...v0.45.0
|
|
1928
1943
|
[0.44.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.44.0...v0.44.1
|
|
1929
1944
|
[0.44.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.43.7...v0.44.0
|
|
1930
1945
|
[0.43.7]: https://github.com/kitepon/aiterm-mcp/compare/v0.43.6...v0.43.7
|
package/README.ja.md
CHANGED
|
@@ -203,7 +203,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
|
|
|
203
203
|
場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
|
|
204
204
|
Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
|
|
205
205
|
|
|
206
|
-
**状態:** 開発継続中 · 現行公開版 **v0.
|
|
206
|
+
**状態:** 開発継続中 · 現行公開版 **v0.45.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
207
207
|
|
|
208
208
|
### 更新と巻き戻し
|
|
209
209
|
|
|
@@ -338,16 +338,19 @@ Throughline自体が不要である。
|
|
|
338
338
|
[aiterm demo: 51 行 / ~56 tok (raw 152 行 / ~166 tok); 102 行 hidden] [is_complete=True via quiescent]
|
|
339
339
|
```
|
|
340
340
|
|
|
341
|
-
`grep`
|
|
341
|
+
`grep` は、rtk の上限(全体 200 行・1 ファイル 25 行)に収まる間は grep の出力をそのまま返す。上限を超えた時だけ、コマンド別 reducer がファイルごとにまとめ、省いた分を書き添える:
|
|
342
342
|
|
|
343
343
|
```text
|
|
344
|
-
→ pty_send("demo", "grep -rn
|
|
344
|
+
→ pty_send("demo", "grep -rn session src/")
|
|
345
345
|
→ pty_read("demo", { wait: true, rtk: true })
|
|
346
|
-
←
|
|
347
|
-
|
|
348
|
-
src/
|
|
349
|
-
|
|
350
|
-
|
|
346
|
+
← 550 matches in 23 files:
|
|
347
|
+
|
|
348
|
+
src/agent-resolver.ts:211:// …(長い行は検索語の周りを ~80 字だけ残す)
|
|
349
|
+
…
|
|
350
|
+
src/remote.ts:268:session_id: session, agent_transcript: true, raw: true,
|
|
351
|
+
+4 more in src/remote.ts
|
|
352
|
+
+7 more files
|
|
353
|
+
[aiterm demo: rtk:grep 適用 / ~4286 tok (raw ~12546 tok)] [is_complete=True via quiescent]
|
|
351
354
|
```
|
|
352
355
|
|
|
353
356
|
ネストは「中へ送るただのテキスト」——同じ PTY の*中で* Python REPL に入る(`ssh host`・`docker exec -it … bash`・起動したコーディングエージェントの TUI も、まったく同じ要領でネストする):
|
|
@@ -449,7 +452,7 @@ aiterm → cwd=/tmp var=hello123 # 1 本の永続PTYが両方を保
|
|
|
449
452
|
|
|
450
453
|
cd でディレクトリを移り、環境変数を立て、ビルドを走らせる。ssh で一度ログインして、その接続のまま 10 個コマンドを打つ。REPL や起動したエージェントの TUI を 1 ターンずつ操作する。こういう流れは、1 本の永続PTYが状態を握っていて初めて成り立つ。端末に何かを覚えておいてほしいときは、aiterm を使う。
|
|
451
454
|
|
|
452
|
-
<sub>¹ いまのハーネスは ~192 KB の出力をいったんファイルに逃がして、先頭 ~2 KB だけを見せる。そのためトークン数はほぼ並ぶ。aiterm は行数を正確に返すうえ、あとから `line_range="A:B"` で好きな範囲(先頭でも末尾でも)を取り出せる。² `rtk` の grep
|
|
455
|
+
<sub>¹ いまのハーネスは ~192 KB の出力をいったんファイルに逃がして、先頭 ~2 KB だけを見せる。そのためトークン数はほぼ並ぶ。aiterm は行数を正確に返すうえ、あとから `line_range="A:B"` で好きな範囲(先頭でも末尾でも)を取り出せる。² `rtk` の grep 縮約は、rtk の上限(全体 200 行・1 ファイル 25 行)に収まる間は grep の出力をそのまま返す。上限を超えるとファイルごとにまとめ、長い行は検索語の周りを ~80 字だけ残し、あふれを `+N more in <file>`/`+N more files` にまとめる。大きな検索の全行をそのまま読みたいときは組み込みツールを使う。</sub>
|
|
453
456
|
|
|
454
457
|
## 既存手段との比較
|
|
455
458
|
|
|
@@ -649,7 +652,8 @@ agent_launch({ "harness": "codex-cli", "remote": { "host": "rabbit" }, "cwd": "/
|
|
|
649
652
|
- **`is_complete=False` は失敗ではない。** 「timeout 内に完了を観測できなかった」という意味。長時間コマンドでは `timeout` を伸ばすか `until`/`mark` を使う。
|
|
650
653
|
- **agent harnessは実物TUIを起動し、model APIを代理しない。** model・認証・挙動は選んだharnessのもの。隠れたagent間protocolはなく、MCPクライアントがClaude/Codex/Grok/Cursor TUIへ入力を送り出力を読む。
|
|
651
654
|
- **`pty_send({ rtk: true })` は単行コマンドのみ+外部 `rtk` バイナリが必要**(無ければ素通し)。一方 `pty_read({ rtk: true })` の reducer は自前実装で rtk 非依存。
|
|
652
|
-
- **`pytest` reducer
|
|
655
|
+
- **`pytest` reducer は件数・`FAILURES` ブロック整形が rtk 0.50.0 と byte 一致**(回帰テストで固定)。ただし `-ra`/`-rf` 時の `FAILED` 要約行の理由は**全文を保持する**(rtk 0.50.0 は最初の `" - "` 区切りで切るが、本実装は可読性優先で情報を残すため、この行は意図的に rtk と完全一致させない)。rtk が大出力時に付ける `[full output: …]`(recall ポインタ)行は read 側では再現しない。
|
|
656
|
+
- **`grep` reducer は rtk 0.50.0 と一致**する。上限に収まる間は出力をそのまま返し、上限を超えた時のまとめ方は recall の案内を除いて `rtk grep` と byte 一致する。`git log` は件数(`-n`)か範囲(`A..B`)を指定すると全コミットを残し、指定が無ければ 10 件にして `[+N more commits]` で終える。rtk の `never_worse` と同じく、縮めた結果が元より多くのトークンを使う reducer は適用しない。
|
|
653
657
|
- **tmux は `-f /dev/null` 起動**なので `~/.tmux.conf` を読まない(環境差を排除するため)。
|
|
654
658
|
- **全セッションが単一multiplexer endpoint(POSIXは`claude.sock`、Windows nativeは1つのpsmux namespace)を共有する。** platformの`kill-server` commandは全セッションを消す。
|
|
655
659
|
|
|
@@ -668,7 +672,7 @@ Windows固有だけの変更はLinuxとWindowsを選ぶ。版番号だけの変
|
|
|
668
672
|
`npm run release -- <version>`がversion同期・commit・tag・GitHub Releaseを一回で行い、tag起点のnpm公開は
|
|
669
673
|
tagged commitが`origin/main`の祖先であることだけを確認して、他のCI結果を待ちません。
|
|
670
674
|
|
|
671
|
-
共通進行は`src/core.ts`、harness固有は`src/harnesses/`、OS差は`src/tmux-runtime.ts`/`src/agent-resolver.ts`、reducerは`src/rtk.ts`、公開面は`src/index.ts`が所有する。現行設計は[`docs/DESIGN.md`](docs/DESIGN.md)、release手順は[`docs/RELEASE.md`](docs/RELEASE.md)を正とする。`prototype/python/`はreducerの歴史的移植元であり、pytest
|
|
675
|
+
共通進行は`src/core.ts`、harness固有は`src/harnesses/`、OS差は`src/tmux-runtime.ts`/`src/agent-resolver.ts`、reducerは`src/rtk.ts`、公開面は`src/index.ts`が所有する。現行設計は[`docs/DESIGN.md`](docs/DESIGN.md)、release手順は[`docs/RELEASE.md`](docs/RELEASE.md)を正とする。`prototype/python/`はreducerの歴史的移植元であり、pytestとgrepのreducerは本家rtk 0.50.0と一致する(上記の`FAILED`行の差異だけは意図的・回帰テストで固定)。
|
|
672
676
|
|
|
673
677
|
## 試す
|
|
674
678
|
|
package/README.md
CHANGED
|
@@ -217,7 +217,7 @@ collection is off by default and performs no network I/O. It ships via
|
|
|
217
217
|
tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
|
|
218
218
|
Release re-registers the Official MCP Registry entry.
|
|
219
219
|
|
|
220
|
-
**Status:** actively maintained · current public release **v0.
|
|
220
|
+
**Status:** actively maintained · current public release **v0.45.0** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
|
|
221
221
|
|
|
222
222
|
### Update and rollback
|
|
223
223
|
|
|
@@ -367,16 +367,19 @@ A long output folded head+tail — the middle is elided by the reducer, not by m
|
|
|
367
367
|
[aiterm demo: 51 lines / ~56 tok (raw 152 lines / ~166 tok); 102 lines hidden] [is_complete=True via quiescent]
|
|
368
368
|
```
|
|
369
369
|
|
|
370
|
-
A `grep
|
|
370
|
+
A `grep` comes back exactly as grep printed it while it stays within rtk's caps (200 lines in all, 25 per file). Past a cap, the per-command reducer groups the hits by file and says what it left out:
|
|
371
371
|
|
|
372
372
|
```text
|
|
373
|
-
→ pty_send("demo", "grep -rn
|
|
373
|
+
→ pty_send("demo", "grep -rn session src/")
|
|
374
374
|
→ pty_read("demo", { wait: true, rtk: true })
|
|
375
|
-
←
|
|
376
|
-
|
|
377
|
-
src/
|
|
378
|
-
|
|
379
|
-
|
|
375
|
+
← 550 matches in 23 files:
|
|
376
|
+
|
|
377
|
+
src/agent-resolver.ts:211:// …(long lines keep ~80 chars around the pattern)
|
|
378
|
+
…
|
|
379
|
+
src/remote.ts:268:session_id: session, agent_transcript: true, raw: true,
|
|
380
|
+
+4 more in src/remote.ts
|
|
381
|
+
+7 more files
|
|
382
|
+
[aiterm demo: rtk:grep applied / ~4286 tok (raw ~12546 tok)] [is_complete=True via quiescent]
|
|
380
383
|
```
|
|
381
384
|
|
|
382
385
|
Nesting is just text you send in — here a Python REPL *inside* the same PTY (an `ssh host`, a `docker exec -it … bash`, or a launched coding-agent TUI nests exactly the same way):
|
|
@@ -480,7 +483,7 @@ aiterm → cwd=/tmp var=hello123 # one persistent PTY holds both
|
|
|
480
483
|
|
|
481
484
|
`cd` then set env then build, `ssh` once then run ten commands on the authenticated session, drive a live REPL or a launched agent's TUI turn by turn — one persistent PTY holds all of it. Reach for aiterm when the terminal has to remember something.
|
|
482
485
|
|
|
483
|
-
<sub>¹ Today's harness auto-offloads the ~192 KB dump to a file and previews only a ~2 KB head, so the token counts nearly tie; aiterm reports the accurate line count and lets `line_range="A:B"` pull any slice later, head or tail. ² The `rtk` grep reducer
|
|
486
|
+
<sub>¹ Today's harness auto-offloads the ~192 KB dump to a file and previews only a ~2 KB head, so the token counts nearly tie; aiterm reports the accurate line count and lets `line_range="A:B"` pull any slice later, head or tail. ² The `rtk` grep reducer returns grep's output unchanged while it fits rtk's caps (200 lines, 25 per file). Past a cap it groups by file, keeps ~80 chars of each long line around the pattern, and folds the rest into `+N more in <file>` / `+N more files`; use the built-in tool when you need every full line of a large search.</sub>
|
|
484
487
|
|
|
485
488
|
## vs. the alternatives
|
|
486
489
|
|
|
@@ -695,7 +698,8 @@ Sessions live on a shared tmux socket on POSIX or a shared psmux namespace on na
|
|
|
695
698
|
- **`is_complete=False` is not a failure.** It means "completion was not observed within `timeout`." For long commands, raise `timeout` or use `until`/`mark`.
|
|
696
699
|
- **Agent harnesses run their real TUI; aiterm doesn't proxy the model API.** The selected harness owns model choice, authentication, and behavior. There is no hidden inter-agent protocol; the MCP client drives the Claude/Codex/Grok/Cursor TUI with ordinary send/read operations.
|
|
697
700
|
- **`pty_send({ rtk: true })` is single-line only and needs the external `rtk` binary** (passthrough without it). The `pty_read({ rtk: true })` reducer, by contrast, is self-contained and rtk-independent.
|
|
698
|
-
- **The `pytest` reducer matches rtk 0.
|
|
701
|
+
- **The `pytest` reducer matches rtk 0.50.0** on test counts and `FAILURES`-block formatting (locked by regression tests). It **deliberately preserves the full failure reason** on the `FAILED` summary lines (emitted under `-ra`/`-rf`), whereas rtk 0.50.0 truncates the reason at the first `" - "` — a readability choice, so those lines are intentionally not byte-identical to rtk. The `[full output: …]` recall-pointer line rtk appends on large output is not reproduced on the read side.
|
|
702
|
+
- **The `grep` reducer matches rtk 0.50.0**: within the caps it returns the output unchanged, and past a cap its grouped form is byte-identical to `rtk grep` without the recall hints. `git log` keeps every commit when you set a count (`-n`) or a range (`A..B`); otherwise it shows 10 and ends with `[+N more commits]`. Like rtk's `never_worse`, a reducer whose result would cost more tokens than the output is skipped.
|
|
699
703
|
- **tmux is started with `-f /dev/null`**, so it does not read `~/.tmux.conf` (to keep behavior reproducible across machines).
|
|
700
704
|
- **All sessions share one multiplexer endpoint** (`claude.sock` on POSIX, one psmux namespace on native Windows). The platform's `kill-server` command removes them all.
|
|
701
705
|
|
|
@@ -718,7 +722,7 @@ Windows runner needs psmux ≥ 3.3.8 and Git for Windows on its PATH, and must r
|
|
|
718
722
|
interactive Windows user; `NETWORK SERVICE` lacks the per-user environment the pane shell and
|
|
719
723
|
harness CLIs rely on and is not a valid runner identity.
|
|
720
724
|
|
|
721
|
-
Logic lives in `src/core.ts` (tmux control, reduction, completion detection, safety, agent launch) and `src/rtk.ts` (per-command reducers); `src/index.ts` is the MCP surface. The current architecture is in [`docs/DESIGN.md`](docs/DESIGN.md), the release procedure is in [`docs/RELEASE.md`](docs/RELEASE.md), and `prototype/python/` remains the reducer's historical porting source (the pytest
|
|
725
|
+
Logic lives in `src/core.ts` (tmux control, reduction, completion detection, safety, agent launch) and `src/rtk.ts` (per-command reducers); `src/index.ts` is the MCP surface. The current architecture is in [`docs/DESIGN.md`](docs/DESIGN.md), the release procedure is in [`docs/RELEASE.md`](docs/RELEASE.md), and `prototype/python/` remains the reducer's historical porting source (the pytest and grep reducers are ported to match upstream rtk 0.50.0, except the deliberate `FAILED`-line difference noted above, and is locked by regression tests).
|
|
722
726
|
|
|
723
727
|
## Try it
|
|
724
728
|
|
package/dist/rtk.js
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 要件C: rtk のファイルは複製せず、アルゴリズムを参照に自分のコードとして書き起こす。
|
|
5
5
|
* rtk バイナリが無い場所でも縮約が効くよう、観測済み出力に対して動く。
|
|
6
|
-
* pytest
|
|
7
|
-
*
|
|
6
|
+
* 手本は rtk 0.50.0。pytest は同版の出力と厳密一致させ、grep は上限に届かなければ出力をそのまま返す。
|
|
7
|
+
* rtk の never_worse と同じく、縮めた結果が元より多くのトークンを使うなら reducer を適用しない。
|
|
8
8
|
*
|
|
9
9
|
* 公開 API: reduce(command, output) -> [reducedText|null, reducerName|null] / classify / stripShellFrame
|
|
10
10
|
*/
|
|
11
|
-
// rtk 由来のキャップ(truncate.rs / config.rs の値。数値のみ参照、コードは自作)
|
|
12
|
-
const CAP_GREP_TOTAL = 200; // grep:
|
|
13
|
-
const CAP_GREP_PER_FILE = 25; // grep:
|
|
14
|
-
const GREP_MAX_LEN = 80; // grep: 1
|
|
11
|
+
// rtk 由来のキャップ(truncate.rs / config.rs / main.rs の値。数値のみ参照、コードは自作)
|
|
12
|
+
const CAP_GREP_TOTAL = 200; // grep: まとめる時に表示する最大行(文脈行を含む)
|
|
13
|
+
const CAP_GREP_PER_FILE = 25; // grep: まとめる時のファイルあたり最大
|
|
14
|
+
const GREP_MAX_LEN = 80; // grep: まとめる時の 1 行の最大幅(UTF-8 バイトで判定)
|
|
15
15
|
const COMPACT_PATH_THRESH = 50; // grep: パス短縮の閾値
|
|
16
16
|
const MAX_PYTEST_FAILURES = 10; // pytest: 失敗ブロック最大
|
|
17
17
|
const MAX_XFAIL = 10; // pytest: xfail/xpass 行 最大
|
|
@@ -19,6 +19,7 @@ const PYTEST_RELEVANT_PER_FAIL = 3;
|
|
|
19
19
|
const LOG_LIMIT_DEFAULT = 10; // git log: 既定コミット数
|
|
20
20
|
const LOG_BODY_LINES = 3;
|
|
21
21
|
const LOG_WIDTH = 80;
|
|
22
|
+
const LOG_WIDTH_USER_LIMIT = 120; // git log: 利用者が件数を決めた時の幅
|
|
22
23
|
/** char(コードポイント)ベース切り詰め(rtk utils::truncate と同等)。 */
|
|
23
24
|
function truncate(s, n) {
|
|
24
25
|
const cp = Array.from(s);
|
|
@@ -134,8 +135,13 @@ export function reducePytest(output) {
|
|
|
134
135
|
pytestEvidence = true;
|
|
135
136
|
if (!pytestEvidence)
|
|
136
137
|
return null;
|
|
137
|
-
if (p === 0 && f === 0 && s === 0 && xf === 0 && xp === 0 && e === 0)
|
|
138
|
+
if (p === 0 && f === 0 && s === 0 && xf === 0 && xp === 0 && e === 0) {
|
|
139
|
+
// rtk 0.50.0 は報告前に落ちた実行(exit≠0,5)を "No tests collected" にせず元の出力を返す。
|
|
140
|
+
// read 側は終了コードを持たないので、pytest 自身の異常終了の印で見分ける。
|
|
141
|
+
if (/^(INTERNALERROR>|ERROR: )/m.test(output))
|
|
142
|
+
return null;
|
|
138
143
|
return "Pytest: No tests collected";
|
|
144
|
+
}
|
|
139
145
|
// error(収集/内部エラー)は失敗の一種=緑扱いにしない。extras に含め、"N passed" 早期 return を止める。
|
|
140
146
|
const extras = s > 0 || xf > 0 || xp > 0 || e > 0 || xfailLines.length > 0;
|
|
141
147
|
if (f === 0 && p > 0 && !extras)
|
|
@@ -149,7 +155,7 @@ export function reducePytest(output) {
|
|
|
149
155
|
head += `, ${xf} xfailed`;
|
|
150
156
|
if (xp > 0)
|
|
151
157
|
head += `, ${xp} xpassed`;
|
|
152
|
-
const out = [head
|
|
158
|
+
const out = [head];
|
|
153
159
|
if (xfailLines.length) {
|
|
154
160
|
out.push("", "Expected-failure outcomes:");
|
|
155
161
|
for (const ln of xfailLines.slice(0, MAX_XFAIL))
|
|
@@ -171,8 +177,8 @@ export function reducePytest(output) {
|
|
|
171
177
|
const parts = first.split(" - ");
|
|
172
178
|
const name = parts[0].slice("FAILED".length).trim();
|
|
173
179
|
out.push(`${i + 1}. [FAIL] ${name}`);
|
|
174
|
-
// 失敗理由は全文保持(可読性優先。rtk 0.
|
|
175
|
-
// 末尾セパレータは continue で入れない(rtk 0.
|
|
180
|
+
// 失敗理由は全文保持(可読性優先。rtk 0.50.0 は最初の " - " segment で切るが、本実装は情報を残す)。
|
|
181
|
+
// 末尾セパレータは continue で入れない(rtk 0.50.0 と同じ)。
|
|
176
182
|
if (parts.length > 1)
|
|
177
183
|
out.push(" " + truncate(parts.slice(1).join(" - "), 100));
|
|
178
184
|
continue;
|
|
@@ -195,7 +201,7 @@ export function reducePytest(output) {
|
|
|
195
201
|
break;
|
|
196
202
|
}
|
|
197
203
|
}
|
|
198
|
-
// rtk 0.
|
|
204
|
+
// rtk 0.50.0 互換: セパレータ判定は表示数(shown)でなく全失敗数(failures)基準(cap 超過時の空行数まで一致)。
|
|
199
205
|
if (i < failures.length - 1)
|
|
200
206
|
out.push("");
|
|
201
207
|
}
|
|
@@ -234,6 +240,7 @@ function parsePytestCounts(summary) {
|
|
|
234
240
|
}
|
|
235
241
|
// ---------------------------------------------------------------- grep
|
|
236
242
|
const GREP_LINE = /^(.*?):(\d+):(.*)$/;
|
|
243
|
+
const GREP_CONTEXT_TAIL = /^(\d+)-(.*)$/;
|
|
237
244
|
function compactPath(p) {
|
|
238
245
|
if (p.length <= COMPACT_PATH_THRESH)
|
|
239
246
|
return p;
|
|
@@ -242,42 +249,118 @@ function compactPath(p) {
|
|
|
242
249
|
return p;
|
|
243
250
|
return `${parts[0]}/.../${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
|
|
244
251
|
}
|
|
245
|
-
|
|
252
|
+
/** rtk search.rs の clean_line と同じ: 長い行はパターンの周りを残し、無ければ先頭を残す。 */
|
|
253
|
+
function cleanGrepLine(line, maxLen, pattern) {
|
|
254
|
+
const trimmed = line.trim();
|
|
255
|
+
if (Buffer.byteLength(trimmed, "utf8") <= maxLen)
|
|
256
|
+
return trimmed;
|
|
257
|
+
const chars = Array.from(trimmed);
|
|
258
|
+
const lower = trimmed.toLowerCase();
|
|
259
|
+
const pos = pattern ? lower.indexOf(pattern.toLowerCase()) : -1;
|
|
260
|
+
if (pos < 0)
|
|
261
|
+
return chars.slice(0, maxLen - 3).join("") + "...";
|
|
262
|
+
const charPos = Array.from(lower.slice(0, pos)).length;
|
|
263
|
+
const charLen = chars.length;
|
|
264
|
+
let start = Math.max(0, charPos - Math.floor(maxLen / 3));
|
|
265
|
+
const end = Math.min(start + maxLen, charLen);
|
|
266
|
+
if (end === charLen)
|
|
267
|
+
start = Math.max(0, end - maxLen);
|
|
268
|
+
const slice = chars.slice(start, end).join("");
|
|
269
|
+
if (start > 0 && end < charLen)
|
|
270
|
+
return `...${slice}...`;
|
|
271
|
+
if (start > 0)
|
|
272
|
+
return `...${slice}`;
|
|
273
|
+
return `${slice}...`;
|
|
274
|
+
}
|
|
275
|
+
/** `file:N:content`(一致)と、既知ファイルの `file-N-content`(文脈)を読む。 */
|
|
276
|
+
function parseGrepLine(line, knownFiles) {
|
|
277
|
+
for (const f of knownFiles) {
|
|
278
|
+
if (!line.startsWith(f + "-"))
|
|
279
|
+
continue;
|
|
280
|
+
const m = GREP_CONTEXT_TAIL.exec(line.slice(f.length + 1));
|
|
281
|
+
if (m)
|
|
282
|
+
return [f, parseInt(m[1], 10), false, m[2]];
|
|
283
|
+
}
|
|
284
|
+
const m = GREP_LINE.exec(line);
|
|
285
|
+
if (!m || !m[1])
|
|
286
|
+
return null;
|
|
287
|
+
return [m[1], parseInt(m[2], 10), true, m[3]];
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* rtk 0.50.0 の grep: 上限(全体 200 行・ファイルあたり 25 行)に届かなければ出力をそのまま返す。
|
|
291
|
+
* 上限を超えた時だけファイルごとにまとめ、それが元より短い時に限りまとめた形を返す。
|
|
292
|
+
* 読めるのはファイル名と行番号の付いた出力だけで、それ以外は null(→ 汎用削減)。
|
|
293
|
+
*/
|
|
294
|
+
export function reduceGrep(output, pattern = "") {
|
|
295
|
+
const lines = output.split("\n");
|
|
296
|
+
const candidates = new Set();
|
|
297
|
+
for (const line of lines) {
|
|
298
|
+
const m = GREP_LINE.exec(line);
|
|
299
|
+
if (m && m[1])
|
|
300
|
+
candidates.add(m[1]);
|
|
301
|
+
}
|
|
302
|
+
if (!candidates.size)
|
|
303
|
+
return null;
|
|
304
|
+
// 長いファイル名から照合し、`a-1-x` を `a` の文脈行として先に拾う
|
|
305
|
+
const knownFiles = [...candidates].sort((a, b) => b.length - a.length);
|
|
246
306
|
const byFile = new Map();
|
|
247
|
-
const order = [];
|
|
248
307
|
let total = 0;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (
|
|
308
|
+
let hasContext = false;
|
|
309
|
+
for (const line of lines) {
|
|
310
|
+
if (line === "--") {
|
|
311
|
+
hasContext = true;
|
|
252
312
|
continue;
|
|
253
|
-
|
|
254
|
-
|
|
313
|
+
}
|
|
314
|
+
const parsed = parseGrepLine(line, knownFiles);
|
|
315
|
+
if (!parsed)
|
|
255
316
|
continue;
|
|
256
|
-
|
|
257
|
-
if (
|
|
317
|
+
const [fname, lineno, isMatch, content] = parsed;
|
|
318
|
+
if (isMatch)
|
|
319
|
+
total++;
|
|
320
|
+
else
|
|
321
|
+
hasContext = true;
|
|
322
|
+
if (!byFile.has(fname))
|
|
258
323
|
byFile.set(fname, []);
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
byFile.get(fname).push([lineno, truncate(content.trim(), GREP_MAX_LEN)]);
|
|
324
|
+
byFile.get(fname).push([lineno, isMatch, cleanGrepLine(content, GREP_MAX_LEN, pattern)]);
|
|
262
325
|
}
|
|
263
326
|
if (total === 0)
|
|
264
327
|
return null;
|
|
265
|
-
const
|
|
328
|
+
const plain = output.replace(/\n+$/, "");
|
|
329
|
+
const body = [];
|
|
266
330
|
let shown = 0;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
331
|
+
let skippedFiles = 0;
|
|
332
|
+
for (const fname of [...byFile.keys()].sort()) {
|
|
333
|
+
const entries = byFile.get(fname);
|
|
334
|
+
if (shown >= CAP_GREP_TOTAL) {
|
|
335
|
+
skippedFiles++;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
270
338
|
const disp = compactPath(fname);
|
|
271
|
-
|
|
339
|
+
let fileShown = 0;
|
|
340
|
+
let prev = 0;
|
|
341
|
+
for (const [lineno, isMatch, content] of entries.slice(0, CAP_GREP_PER_FILE)) {
|
|
272
342
|
if (shown >= CAP_GREP_TOTAL)
|
|
273
343
|
break;
|
|
274
|
-
|
|
344
|
+
if (hasContext && prev > 0 && lineno > prev + 1)
|
|
345
|
+
body.push("--");
|
|
346
|
+
prev = lineno;
|
|
347
|
+
const sep = isMatch ? ":" : "-";
|
|
348
|
+
body.push(`${disp}${sep}${lineno}${sep}${content}`);
|
|
275
349
|
shown++;
|
|
350
|
+
fileShown++;
|
|
276
351
|
}
|
|
352
|
+
const remaining = entries.length - fileShown;
|
|
353
|
+
if (remaining > 0)
|
|
354
|
+
body.push(` +${remaining} more in ${disp}`);
|
|
277
355
|
}
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
|
|
356
|
+
if (skippedFiles > 0)
|
|
357
|
+
body.push(`+${skippedFiles} more files`);
|
|
358
|
+
// rtk と同じく、表示数(文脈行を含む)と一致数を比べて上限で削ったかを決める
|
|
359
|
+
const capped = shown < total || skippedFiles > 0;
|
|
360
|
+
if (!capped)
|
|
361
|
+
return plain;
|
|
362
|
+
const grouped = `${total} matches in ${byFile.size} files:\n\n${body.join("\n")}`;
|
|
363
|
+
return Buffer.byteLength(grouped, "utf8") < Buffer.byteLength(plain, "utf8") ? grouped : plain;
|
|
281
364
|
}
|
|
282
365
|
// ---------------------------------------------------------------- git status / log
|
|
283
366
|
const GIT_HINT = /^\(use "git |^\(create\/copy files/;
|
|
@@ -313,14 +396,40 @@ export function reduceGitStatus(output) {
|
|
|
313
396
|
}
|
|
314
397
|
return kept.length ? kept.join("\n") : "ok";
|
|
315
398
|
}
|
|
316
|
-
|
|
399
|
+
/**
|
|
400
|
+
* git log の引数から、利用者が件数(-n N / -N / --max-count)や範囲(A..B)を決めたかを読む。
|
|
401
|
+
* rtk は件数を決めたlogを打ち切らず幅を 120 にする。0.50.0 は範囲も利用者の決めた境界として打ち切らない。
|
|
402
|
+
*/
|
|
403
|
+
function gitLogBounds(args) {
|
|
404
|
+
let userLimit = false;
|
|
405
|
+
let range = false;
|
|
406
|
+
for (let i = 0; i < args.length; i++) {
|
|
407
|
+
const a = args[i];
|
|
408
|
+
if (a === "--")
|
|
409
|
+
break;
|
|
410
|
+
if (/^-\d+$/.test(a) || /^-n\d+$/.test(a) || /^--max-count=\d+$/.test(a))
|
|
411
|
+
userLimit = true;
|
|
412
|
+
else if ((a === "-n" || a === "--max-count") && /^\d+$/.test(args[i + 1] ?? "")) {
|
|
413
|
+
userLimit = true;
|
|
414
|
+
i++;
|
|
415
|
+
}
|
|
416
|
+
else if (!a.startsWith("-") && a.includes(".."))
|
|
417
|
+
range = true;
|
|
418
|
+
}
|
|
419
|
+
return { userLimit, range };
|
|
420
|
+
}
|
|
421
|
+
export function reduceGitLog(output, args = []) {
|
|
317
422
|
if (!output.includes("commit "))
|
|
318
423
|
return null;
|
|
319
424
|
const blocks = output.split(/(?=^commit [0-9a-f]{7,40})/m).filter((b) => b.trim().startsWith("commit "));
|
|
320
425
|
if (!blocks.length)
|
|
321
426
|
return null;
|
|
427
|
+
const { userLimit, range } = gitLogBounds(args);
|
|
428
|
+
const width = userLimit ? LOG_WIDTH_USER_LIMIT : LOG_WIDTH;
|
|
429
|
+
// 件数も範囲も決めていない時だけ既定の 10 件に絞り、絞ったことを書く(黙って捨てない)
|
|
430
|
+
const limit = userLimit || range ? blocks.length : LOG_LIMIT_DEFAULT;
|
|
322
431
|
const out = [];
|
|
323
|
-
for (const b of blocks.slice(0,
|
|
432
|
+
for (const b of blocks.slice(0, limit)) {
|
|
324
433
|
const bl = b.split("\n").map((x) => x.replace(/\s+$/, ""));
|
|
325
434
|
let commit = "", author = "", subject = "";
|
|
326
435
|
const body = [];
|
|
@@ -342,16 +451,18 @@ export function reduceGitLog(output) {
|
|
|
342
451
|
}
|
|
343
452
|
const am = /<([^>]+)>/.exec(author);
|
|
344
453
|
const who = am ? am[1] : author;
|
|
345
|
-
let head = truncate(`${commit} ${subject}`.trim(),
|
|
454
|
+
let head = truncate(`${commit} ${subject}`.trim(), width);
|
|
346
455
|
if (who)
|
|
347
456
|
head += ` <${who}>`;
|
|
348
457
|
const entry = [head];
|
|
349
458
|
for (const bln of body.slice(0, LOG_BODY_LINES))
|
|
350
|
-
entry.push(" " + truncate(bln,
|
|
459
|
+
entry.push(" " + truncate(bln, width));
|
|
351
460
|
if (body.length > LOG_BODY_LINES)
|
|
352
461
|
entry.push(` [+${body.length - LOG_BODY_LINES} lines omitted]`);
|
|
353
462
|
out.push(entry.join("\n"));
|
|
354
463
|
}
|
|
464
|
+
if (blocks.length > limit)
|
|
465
|
+
out.push(`[+${blocks.length - limit} more commits]`);
|
|
355
466
|
return out.join("\n").trim();
|
|
356
467
|
}
|
|
357
468
|
const FILTERS = [
|
|
@@ -432,12 +543,116 @@ export function classify(command) {
|
|
|
432
543
|
return rule.name;
|
|
433
544
|
return null;
|
|
434
545
|
}
|
|
546
|
+
/** 引用符('…' "…")と \\ を解いた語の列。読み取り専用の解析なので、展開や置換はしない。 */
|
|
547
|
+
function shellWords(command) {
|
|
548
|
+
const words = [];
|
|
549
|
+
let cur = "";
|
|
550
|
+
let has = false;
|
|
551
|
+
let quote = "";
|
|
552
|
+
for (let i = 0; i < command.length; i++) {
|
|
553
|
+
const c = command[i];
|
|
554
|
+
if (quote) {
|
|
555
|
+
if (c === quote)
|
|
556
|
+
quote = "";
|
|
557
|
+
else if (c === "\\" && quote === '"' && i + 1 < command.length)
|
|
558
|
+
cur += command[++i];
|
|
559
|
+
else
|
|
560
|
+
cur += c;
|
|
561
|
+
}
|
|
562
|
+
else if (c === "'" || c === '"') {
|
|
563
|
+
quote = c;
|
|
564
|
+
has = true;
|
|
565
|
+
}
|
|
566
|
+
else if (c === "\\" && i + 1 < command.length) {
|
|
567
|
+
cur += command[++i];
|
|
568
|
+
has = true;
|
|
569
|
+
}
|
|
570
|
+
else if (/\s/.test(c)) {
|
|
571
|
+
if (has)
|
|
572
|
+
words.push(cur);
|
|
573
|
+
cur = "";
|
|
574
|
+
has = false;
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
cur += c;
|
|
578
|
+
has = true;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (has)
|
|
582
|
+
words.push(cur);
|
|
583
|
+
return words;
|
|
584
|
+
}
|
|
585
|
+
/** verb(git/grep 等)より後ろの語。前置ラッパーは basenameCmd と同じく読み飛ばす。 */
|
|
586
|
+
function argsAfterVerb(command) {
|
|
587
|
+
const [verb, , stripped] = basenameCmd(command);
|
|
588
|
+
if (!verb)
|
|
589
|
+
return [];
|
|
590
|
+
const words = shellWords(stripped);
|
|
591
|
+
return words.slice(1);
|
|
592
|
+
}
|
|
593
|
+
// 値を取る grep / rg のフラグ(別の語で値を渡す形)。パターンを値と取り違えないために使う。
|
|
594
|
+
const GREP_VALUE_SHORT = new Set(["e", "f", "m", "A", "B", "C", "d", "D"]);
|
|
595
|
+
const RG_VALUE_SHORT = new Set(["e", "f", "m", "A", "B", "C", "d", "g", "t", "T", "M", "j", "E", "r"]);
|
|
596
|
+
const GREP_VALUE_LONG = new Set([
|
|
597
|
+
"regexp", "file", "max-count", "after-context", "before-context", "context", "devices", "directories",
|
|
598
|
+
"include", "exclude", "exclude-dir", "exclude-from", "label", "glob", "iglob", "type", "type-not",
|
|
599
|
+
"max-columns", "threads", "max-depth", "color", "colors", "sort", "sortr", "encoding", "pre", "replace",
|
|
600
|
+
]);
|
|
601
|
+
/** grep / rg の検索パターン(-e が複数なら `|` で結ぶ。rtk の pattern_display と同じ)。 */
|
|
602
|
+
export function grepPattern(command) {
|
|
603
|
+
const args = argsAfterVerb(command);
|
|
604
|
+
const valueShort = basenameCmd(command)[0] === "rg" ? RG_VALUE_SHORT : GREP_VALUE_SHORT;
|
|
605
|
+
const explicit = [];
|
|
606
|
+
const positional = [];
|
|
607
|
+
let endOfFlags = false;
|
|
608
|
+
for (let i = 0; i < args.length; i++) {
|
|
609
|
+
const a = args[i];
|
|
610
|
+
if (endOfFlags || !a.startsWith("-") || a === "-") {
|
|
611
|
+
positional.push(a);
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (a === "--") {
|
|
615
|
+
endOfFlags = true;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
if (a.startsWith("--")) {
|
|
619
|
+
const [name, attached] = a.slice(2).split(/=(.*)/s);
|
|
620
|
+
const value = attached !== undefined ? attached : GREP_VALUE_LONG.has(name) ? args[++i] : undefined;
|
|
621
|
+
if (name === "regexp" && value !== undefined)
|
|
622
|
+
explicit.push(value);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
const cluster = a.slice(1);
|
|
626
|
+
for (let j = 0; j < cluster.length; j++) {
|
|
627
|
+
if (!valueShort.has(cluster[j]))
|
|
628
|
+
continue;
|
|
629
|
+
const rest = cluster.slice(j + 1);
|
|
630
|
+
const value = rest || args[++i];
|
|
631
|
+
if (cluster[j] === "e" && value !== undefined)
|
|
632
|
+
explicit.push(value);
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (explicit.length)
|
|
637
|
+
return explicit.join("|");
|
|
638
|
+
return positional[0] ?? "";
|
|
639
|
+
}
|
|
640
|
+
/** `git [opts] log <args>` の log より後ろの語。 */
|
|
641
|
+
function gitLogArgs(command) {
|
|
642
|
+
const args = argsAfterVerb(command);
|
|
643
|
+
const i = args.indexOf("log");
|
|
644
|
+
return i >= 0 ? args.slice(i + 1) : [];
|
|
645
|
+
}
|
|
435
646
|
const REDUCERS = {
|
|
436
|
-
"git-status": reduceGitStatus,
|
|
437
|
-
"git-log": reduceGitLog,
|
|
438
|
-
grep: reduceGrep,
|
|
647
|
+
"git-status": (o) => reduceGitStatus(o),
|
|
648
|
+
"git-log": (o, c) => reduceGitLog(o, gitLogArgs(c)),
|
|
649
|
+
grep: (o, c) => reduceGrep(o, grepPattern(c)),
|
|
439
650
|
pytest: (o) => reducePytest(o),
|
|
440
651
|
};
|
|
652
|
+
/** rtk の estimate_tokens と同じ(UTF-8 バイト数 / 4 の切り上げ)。 */
|
|
653
|
+
function estimateTokens(s) {
|
|
654
|
+
return Math.ceil(Buffer.byteLength(s, "utf8") / 4);
|
|
655
|
+
}
|
|
441
656
|
/**
|
|
442
657
|
* コマンドに応じた reducer を観測出力へ適用。返り値 [reducedText|null, reducerName|null]。
|
|
443
658
|
* 前提(C13): `output` は制御文字除去済み(ANSI/CR 等)であること。色付き/生 PTY 出力を直接渡すと
|
|
@@ -447,12 +662,18 @@ export function reduce(command, output) {
|
|
|
447
662
|
const name = classify(command);
|
|
448
663
|
if (name === null)
|
|
449
664
|
return [null, null];
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
665
|
+
let red = null;
|
|
666
|
+
if (name in REDUCERS)
|
|
667
|
+
red = REDUCERS[name](output, command);
|
|
668
|
+
else {
|
|
669
|
+
const rule = FILTERS.find((r) => r.name === name);
|
|
670
|
+
if (rule)
|
|
671
|
+
red = applyFilter(rule, output);
|
|
453
672
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
673
|
+
if (red === null)
|
|
674
|
+
return [null, null];
|
|
675
|
+
// rtk の never_worse: 縮めた結果の方がトークンを使うなら適用しない(→ 呼び手の汎用削減へ)
|
|
676
|
+
if (estimateTokens(red) > estimateTokens(output))
|
|
677
|
+
return [null, null];
|
|
678
|
+
return [red, name];
|
|
458
679
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"mcpName": "io.github.kitepon/aiterm-mcp",
|
|
5
5
|
"description": "Persistent terminal MCP with one harness-based launcher for Claude Code, Codex CLI, Grok CLI, and Cursor Agent CLI, plus durable PTYs for SSH, containers, and REPLs.",
|
|
6
6
|
"keywords": [
|