aiterm-mcp 0.58.3 → 0.59.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.
@@ -0,0 +1,138 @@
1
+ // 利用者の設定file(harnessが既定を保存するfile)を、席の切り替えの前後で元の値へ戻すための部品(ADR 0114)。
2
+ // harnessの切り替えの命令は、席だけでなく利用者の既定も書き換える(Claude Codeの /fast、Codexの /fast、Grokの --model と /model)。
3
+ // 席の切り替えを席だけに留めるため、「読む → 切り替える → 戻す」を、processをまたぐ鍵で1つずつにする。
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { isDeepStrictEqual } from "node:util";
7
+ import { AitermError } from "./errors.js";
8
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9
+ // 鍵を握ったままprocessが落ちた時に、後の人が引き取るまでの時間。1回の切り替え(画面の待ちと公式の口の呼び出し)より十分長く取る。
10
+ const LOCK_STALE_MS = 120_000;
11
+ const LOCK_WAIT_MS = 150_000;
12
+ const LOCK_POLL_MS = 120;
13
+ /**
14
+ * 設定fileを触る切り替えを、同じ端末の全てのAitermの間で1つずつにする。鍵は設定fileの隣に作る
15
+ * (Aitermの置き場はprocessごとに変えられるが、設定fileの場所は全員が同じに見る)。
16
+ * 設定fileの置き場が無い・書けない時は、harnessも書けないので鍵なしで通す。
17
+ */
18
+ export async function withUserConfigLock(configPath, action) {
19
+ const lock = `${configPath}.aiterm-lock`;
20
+ if (!fs.existsSync(path.dirname(configPath)))
21
+ return action();
22
+ const deadline = Date.now() + LOCK_WAIT_MS;
23
+ for (;;) {
24
+ try {
25
+ fs.mkdirSync(lock);
26
+ break;
27
+ }
28
+ catch (error) {
29
+ const code = error.code;
30
+ if (code !== "EEXIST")
31
+ return action();
32
+ }
33
+ let age = 0;
34
+ try {
35
+ age = Date.now() - fs.statSync(lock).mtimeMs;
36
+ }
37
+ catch {
38
+ continue;
39
+ }
40
+ if (age > LOCK_STALE_MS) {
41
+ try {
42
+ fs.rmSync(lock, { recursive: true, force: true });
43
+ }
44
+ catch { /* 先に誰かが片付けた */ }
45
+ continue;
46
+ }
47
+ if (Date.now() > deadline) {
48
+ throw new AitermError(`USER_CONFIG_LOCK_TIMEOUT: ${configPath} を触る切り替えが、ほかの席で続いています。何も変えていません。`, 2);
49
+ }
50
+ await sleep(LOCK_POLL_MS + Math.floor(Math.random() * LOCK_POLL_MS));
51
+ }
52
+ try {
53
+ try {
54
+ fs.writeFileSync(path.join(lock, "owner"), `${process.pid}\n`);
55
+ }
56
+ catch { /* 持ち主の控えは調べる時のため。無くても動く */ }
57
+ return await action();
58
+ }
59
+ finally {
60
+ try {
61
+ fs.rmSync(lock, { recursive: true, force: true });
62
+ }
63
+ catch { /* 残っても、古くなれば次の人が引き取る */ }
64
+ }
65
+ }
66
+ /** fileの今の中身。無い・読めない時はnull。 */
67
+ export function readFileOrNull(file) {
68
+ try {
69
+ return fs.readFileSync(file);
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ function writeInPlace(file, body) {
76
+ let mode = 0o600;
77
+ try {
78
+ mode = fs.statSync(file).mode & 0o777;
79
+ }
80
+ catch { /* 無ければ0600で作る */ }
81
+ const temp = `${file}.aiterm-restore-${process.pid}`;
82
+ fs.writeFileSync(temp, body, { mode });
83
+ try {
84
+ fs.renameSync(temp, file);
85
+ }
86
+ catch (error) {
87
+ // Windowsでは、ほかのprocessがfileを開いている間、置き換えに失敗する事がある。途中のfileを残さず、呼び手に次の回で試させる。
88
+ try {
89
+ fs.unlinkSync(temp);
90
+ }
91
+ catch { /* 残っても中身は前の値 */ }
92
+ throw error;
93
+ }
94
+ }
95
+ /**
96
+ * JSONの設定fileの、最上位の項目(1つ、または複数)を、切り替えの前の値へ戻す。
97
+ * - 項目が前と同じで、ほかの項目の値も同じ(harnessが書き直して並びが変わっただけ)なら、前の中身をそのまま書き戻す。
98
+ * - ほかの項目が変わっていたら(ほかの人の書き込み、harnessの控えの更新)、その項目だけを直して、ほかは今のまま残す。
99
+ * - 今のfileが無い・JSONとして読めない時、前の中身が読めない時は触らない。
100
+ * 戻り値は、書き戻したかどうか。
101
+ */
102
+ export function restoreJsonSetting(file, before, key) {
103
+ const keys = typeof key === "string" ? [key] : key;
104
+ const now = readFileOrNull(file);
105
+ if (now === null)
106
+ return false;
107
+ if (before !== null && now.equals(before))
108
+ return false;
109
+ let current;
110
+ let previous = null;
111
+ try {
112
+ current = JSON.parse(now.toString("utf8"));
113
+ if (before !== null)
114
+ previous = JSON.parse(before.toString("utf8"));
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ if (!current || typeof current !== "object" || Array.isArray(current))
120
+ return false;
121
+ if (previous !== null && (typeof previous !== "object" || Array.isArray(previous)))
122
+ return false;
123
+ const edited = { ...current };
124
+ for (const name of keys) {
125
+ if (previous !== null && Object.hasOwn(previous, name))
126
+ edited[name] = previous[name];
127
+ else
128
+ delete edited[name];
129
+ }
130
+ if (before !== null && isDeepStrictEqual(edited, previous)) {
131
+ writeInPlace(file, before);
132
+ return true;
133
+ }
134
+ if (isDeepStrictEqual(edited, current))
135
+ return false;
136
+ writeInPlace(file, `${JSON.stringify(edited, null, 2)}\n`);
137
+ return true;
138
+ }
package/docs/DESIGN.md CHANGED
@@ -63,6 +63,16 @@ agentの席への`pty_send`は、任意の`preface`(本文の前に置く1行
63
63
  空行と本文を1回の貼り付けで入れる。貼り付けた長い文をClaude Codeが`<pasted_content>`で包んでも、前置きは包みの外に出る。
64
64
  ほかのharnessの席へは1つにつないで貼る。`preface`は入力欄がそのまま読むので、1行・200字以内・制御文字なし・行頭が記号や空白でない・
65
65
  `@`を含まない文だけを通し、外れた時は打鍵前に断る。
66
+ 呼び手が`preface`を渡さない時も、Claude Codeの席へ送る文が包まれる長さ(改行3つ以上か800字以上。`claudeWrapsPaste`)なら、Aitermが既定の1行
67
+ (`AGENT_DEFAULT_PREFACE`。出所だけを言う)を同じ形で入れる。新しいturn・差し込み・起動時の文で同じ。起動時に席へ渡す文で、この1行の見分け方を教える(ADR 0116)。
68
+
69
+ Claude Codeの席へ送る前に、今の画面が選ぶ画面(最後の行が`Enter to confirm · Esc to cancel`で、上に番号の行がある。`claudeLiveSelectMenu`)で
70
+ ないかを読む。利用枠に当たったClaude Codeが自分で開く`/rate-limit-options`の画面は、Escで閉じてから送る。見覚えの無い選ぶ画面には何も打たずに
71
+ 断る。選ぶ画面は貼り付けを捨ててEnterだけを読み、打鍵の数字と「j」「k」を選択の操作として読むので、そのまま送ると文が消える(ADR 0117)。
72
+
73
+ 席のprocessの環境には、起動の印(`AITERM_AGENT_LAUNCH_ID`ほか`AITERM_AGENT_*`)を入れる。印は席が起こした子にも継がれる。起動の時点で会話IDが
74
+ 決まるClaude CodeとGrokの席には、席の会話ID(`AITERM_AGENT_VENDOR_SESSION_ID`)も入れ、席そのものの会話かを値で見分けられるようにする(ADR 0118)。
75
+ 素の端末(`pty_open`)には`AITERM_AGENT_*`を入れない。
66
76
 
67
77
  Codexは回答を流している間、画面に動作中の行を出さない。`pty_send`の振り分けは、Codexの席に限り、画面が入力待ちでも、この起動の記録の
68
78
  最後の境界がturnの開始で、processが生きていて、0.4秒おいて見直しても記録が同じturnのままで画面が動いている時を、動いていると数える
@@ -171,6 +181,7 @@ hookコマンドはCodexが使うshellで評価される。Windowsでは引用
171
181
  承認省略flagを恒久設定へ書かない。利用者の別hookを承認しない。
172
182
  選択と配送の所有記録は`~/.config/aiterm-mcp/codex-parent-hooks/`に置く。
173
183
  Nodeは`src/setup-node.ts`でHomebrewの同じformulaの`opt`へ正規化し、更新で消えるCellar実体を保存しない。
184
+ 動いているprocessが自分のNodeを起こす時・呼び手へ渡す時も、`src/live-node.ts`で今ある場所を引く(`process.execPath`を直接使わない。ADR 0113)。
174
185
 
175
186
  新規の起動差し替えは作らない。旧中継の設定は新しいhookの確認後に従来の解除処理で復元する。
176
187
  macOSの専用LaunchAgent、Windowsのユーザー環境変数、所有外の値の保持は旧adapterが担う。
package/docs/RELEASE.md CHANGED
@@ -11,6 +11,9 @@ Aitermのreleaseはこのrepositoryが所有する。`.github/workflows/product-
11
11
  - 版番号だけの変更はJSONの実差分で識別し、Linuxでbuildと配布metadata・pack・文書確認を行う。依存や実行設定の変更は省略しない。
12
12
  文書だけなら文書検査、実装と文書の混在なら関連試験と文書検査を行う。
13
13
  - 週1回の定期実行(月曜 03:00 JST)と手動実行は、指定された環境で全テストを回す。定期実行の対象は3環境である。
14
+ - pushは、同じcommitの全テスト(手動実行か定期実行)がもう通っている環境を流さない。枝で手動実行を通してからmainへ早送りすると、
15
+ mainのpushは変更分類と関門だけで終わる。通っている環境が一部の時は、残りの環境だけを流す。GitHubへ聞けない時は、選んだ環境を全部流す。
16
+ 同じcommitのpush・pull requestの成功は、試験を絞っている事があるので数えない(ADR 0115)。
14
17
  - tag push: 所有確認と、tagged commitが`origin/main`の祖先であることの確認だけを行い、npmへprovenance付きで
15
18
  publishする。同じcommitのmain CIの結果は待たない。
16
19
 
@@ -19,6 +22,8 @@ Aitermのreleaseはこのrepositoryが所有する。`.github/workflows/product-
19
22
  1. 変更に直結するfocused testを手元で通す。full regressionは手元で回さず、CIに任せる。
20
23
  個別試験も`node --import ./test/seat-env.mjs --test ...`で起動する。稼働中の席の保存場所を試験へ継承しない。
21
24
  2. `CHANGELOG.md`の`## [Unreleased]`へ内容を書き、mainへcommitしてpushする。
25
+ 3環境の全テストを先に通しておく時は、枝をpushして手動実行(`gh workflow run ci.yml --ref <枝> -f environment=all -f comparison_base=<前の公開commit>`)を
26
+ 1回流し、通ったcommitをそのままmainへ早送りする。Windowsの全テストは1回で済む。
22
27
  3. 一回で公開する。
23
28
 
24
29
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.58.3",
3
+ "version": "0.59.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": [
@@ -72,7 +72,7 @@
72
72
  },
73
73
  "dependencies": {
74
74
  "@modelcontextprotocol/sdk": "^1.29.0",
75
- "aiterm-steer-delivery": "^0.4.3",
75
+ "aiterm-steer-delivery": "^0.4.6",
76
76
  "ws": "8.21.3",
77
77
  "zod": "^4.4.3"
78
78
  },