cckeep 0.2.0 → 0.2.3
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/README.ja.md +57 -53
- package/README.md +5 -1
- package/bin/cckeep.js +6 -2
- package/package.json +1 -1
- package/src/detect.js +24 -4
- package/src/i18n.js +23 -23
- package/src/procs.js +64 -0
- package/src/run.js +3 -1
- package/src/tmux.js +27 -3
package/README.ja.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/kamihork/cckeep/main/assets/logo.png" width="140" height="140" alt="cckeep ロゴ
|
|
2
|
+
<img src="https://raw.githubusercontent.com/kamihork/cckeep/main/assets/logo.png" width="140" height="140" alt="cckeep ロゴ(繋がったままのリンク)">
|
|
3
3
|
|
|
4
4
|
<h1>cckeep</h1>
|
|
5
5
|
|
|
6
6
|
<p><strong>Claude Code のリモートコントロールが黙って死ぬのを防ぎます。</strong><br>
|
|
7
|
-
リモートコントロールは約31
|
|
8
|
-
<code>cckeep</code>
|
|
7
|
+
リモートコントロールは約31秒で再接続を諦め、二度と戻りません。<br>
|
|
8
|
+
<code>cckeep</code> がそれを検知して繋ぎ直します。作業中のセッションには手を出しません。</p>
|
|
9
9
|
|
|
10
10
|
<p>
|
|
11
11
|
<a href="https://www.npmjs.com/package/cckeep"><img src="https://img.shields.io/npm/v/cckeep?color=1f9d8f&label=npm" alt="npm version"></a>
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
|
|
20
20
|
## 何が問題か
|
|
21
21
|
|
|
22
|
-
[リモートコントロール](https://code.claude.com/docs/en/remote-control)は、手元で動いている Claude Code のセッションをスマホや claude.ai
|
|
22
|
+
[リモートコントロール](https://code.claude.com/docs/en/remote-control)は、手元で動いている Claude Code のセッションをスマホや claude.ai から操作する機能です。接続が切れても自動で再接続してくれますが、その回数は **1/2/4/8/16秒の間隔で5回まで**、合計 **31秒**しかありません。ノートを閉じる、Wi-Fi を切り替える、エレベーターに乗る。それだけで再試行は尽き、接続は閉じられて二度と戻りません。
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
壊れ方はもう一つあります。`/rc reconnecting` の表示のまま、いつまでも動かなくなるパターンです。これが [anthropics/claude-code#34255](https://github.com/anthropics/claude-code/issues/34255) で、2026年3月に報告されてから 👍 99件を集めたまま、まだ直っていません。
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
どちらの場合も、気づき方は同じです。スマホを開いたらセッションが消えている。そして公式に案内されている復旧方法は、机に戻って `/remote-control` と打つことだけです。
|
|
27
27
|
|
|
28
28
|
## クイックスタート
|
|
29
29
|
|
|
@@ -32,18 +32,18 @@ npm install -g cckeep
|
|
|
32
32
|
cckeep enable
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
`npm install` は CLI を `PATH`
|
|
35
|
+
`npm install` は CLI を `PATH` に置くだけで、この時点ではまだ何も動いていません。`cckeep enable` がバックグラウンドジョブを登録し(macOS は launchd、Linux は systemd user timer)、そこから15秒ごとに状態を確認して、切れているものを繋ぎ直すようになります。
|
|
36
36
|
|
|
37
|
-
`npx cckeep enable`
|
|
37
|
+
`npx cckeep enable` ではなく、グローバルに入れてから実行してください。登録されたジョブはインストール先の cckeep をそのまま実行しますが、npx のキャッシュは使い捨てです。そこを指したジョブは、キャッシュが消された瞬間に何も言わず動かなくなります。監視ツールにとって、これがいちばん避けたい壊れ方です。そのため `cckeep enable` は npx のパスからの登録を拒否します。
|
|
38
38
|
|
|
39
|
-
入れる前に様子を見るだけなら `npx`
|
|
39
|
+
入れる前に様子を見るだけなら `npx` で構いません。次の2つは何も変更しません。
|
|
40
40
|
|
|
41
41
|
```sh
|
|
42
42
|
npx cckeep # いま何が見えているか
|
|
43
43
|
npx cckeep doctor # tmux・ペイン・スケジューラの確認
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
**インストール直後に `command not found: cckeep`
|
|
46
|
+
**インストール直後に `command not found: cckeep` と出た場合**、shim 方式のバージョン管理ツールを使っているはずです。`nodenv` や `asdf` では、新しく入った実行ファイルが `PATH` に現れるまでに rehash が必要です。`nvm` の場合はシェルを開き直してください。
|
|
47
47
|
|
|
48
48
|
```sh
|
|
49
49
|
nodenv rehash # nodenv
|
|
@@ -51,33 +51,33 @@ asdf reshim nodejs # asdf
|
|
|
51
51
|
# nvm: 新しいシェルを開くだけ
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
条件が一つあります。**Claude Code
|
|
54
|
+
条件が一つあります。**Claude Code を tmux の中で動かすこと**です。素のターミナルで起動したセッションには、外のプロセスから入力を送る手段がありません。届く経路がない以上、どんなツールでも手が出せません。→ [tmux で Claude Code を動かす](#tmux-で-claude-code-を動かす)
|
|
55
55
|
|
|
56
|
-
> 机に戻る手間が省けたなら、⭐
|
|
56
|
+
> 机に戻る手間が省けたなら、⭐ を付けてもらえると同じ問題を抱えている人に届きやすくなります。
|
|
57
57
|
|
|
58
|
-
##
|
|
58
|
+
## 作業中のセッションには手を出しません
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
ここがこのツールの設計上いちばん難しいところです。タイマーでターミナルに文字を打ち込むツールは、「いま打っても安全か」を確実に判断できない限り、ただ危険なだけになります。以下はすべて実装済みで、[テスト](test/)で守られています。
|
|
61
61
|
|
|
62
|
-
-
|
|
63
|
-
- **ダイアログには打たない。**
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
- **連打しない。**
|
|
67
|
-
-
|
|
62
|
+
- **ターンの実行中は打たない。** ペインを2秒あけて2回キャプチャします。実行中はスピナーとトークン数が動くので、2回が同じなら何もしていない証拠です。違っていれば手を出しません
|
|
63
|
+
- **ダイアログには打たない。** 権限プロンプトや選択メニューでは、Enter が「選択」になってしまいます。選択マーカーが画面にあれば、その回はまるごと見送ります
|
|
64
|
+
- **自分で開いたパネルには触らない。** `/remote-control` は QR コード付きのステータスパネルを開きます。cckeep が Enter を押すのは、自分でパネルを開いたときだけです
|
|
65
|
+
- **自分で切った接続は戻さない。** 一度でも接続できていたペインだけを対象にします。意図して切ったものは切れたままです
|
|
66
|
+
- **連打しない。** 同じペインに手を出すのは5分に1回までです
|
|
67
|
+
- **送る直前にもう一度確かめる。** 判断は1回のキャプチャで下しますが、待機したあとにもう一度確認します。その間に復帰していたり、ダイアログが出ていれば何も送りません
|
|
68
68
|
|
|
69
|
-
`--dry-run`
|
|
69
|
+
`--dry-run` を付けると、送る内容を表示するだけで実際には何も送りません。
|
|
70
70
|
|
|
71
|
-
##
|
|
71
|
+
## 何を見て判断しているか
|
|
72
72
|
|
|
73
73
|
| 画面の状態 | 意味 | cckeep の動作 |
|
|
74
74
|
|---|---|---|
|
|
75
|
-
| `/rc active` |
|
|
76
|
-
| `/rc reconnecting` | 31
|
|
77
|
-
| `/rc reconnecting` が2
|
|
78
|
-
| `Remote Control disconnected` | 諦めた |
|
|
79
|
-
| 表示なし(
|
|
80
|
-
| 表示なし(
|
|
75
|
+
| `/rc active` | 接続できている | ペインを覚えるだけ |
|
|
76
|
+
| `/rc reconnecting` | 31秒の再試行中 | 待つ(たいていはこれで戻る) |
|
|
77
|
+
| `/rc reconnecting` が2分以上 | 固まっている([#34255](https://github.com/anthropics/claude-code/issues/34255)) | パネルを開いて切断し、繋ぎ直す |
|
|
78
|
+
| `Remote Control disconnected` | 諦めた | すぐに繋ぎ直す |
|
|
79
|
+
| 表示なし(以前は出ていた) | 通知が流れて消えた | 4回続けて静かなら繋ぎ直す |
|
|
80
|
+
| 表示なし(一度も出ていない) | そもそも使っていない | 何もしない |
|
|
81
81
|
|
|
82
82
|
## コマンド
|
|
83
83
|
|
|
@@ -97,7 +97,7 @@ cckeep logs # これまでの動作
|
|
|
97
97
|
|
|
98
98
|
## tmux で Claude Code を動かす
|
|
99
99
|
|
|
100
|
-
cckeep は tmux
|
|
100
|
+
cckeep は tmux のペインを読み、そこに入力します。動いている Claude Code セッションに外のプロセスから届く経路はこれしかなく、だからこそ tmux の中で起動しておく必要があります。プロセスを再起動すればいい、という話でもありません。それでは会話そのものが終わってしまい、守りたかったものを自分で失うことになります。
|
|
101
101
|
|
|
102
102
|
最小の変更は、対話起動だけを包むシェル関数です。`claude update` や `claude doctor`、`claude -p` はそのまま素通しします。
|
|
103
103
|
|
|
@@ -137,9 +137,9 @@ cc() {
|
|
|
137
137
|
}
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
`cc -c`
|
|
140
|
+
`cc -c` や `cc --continue` はそのまま使えます。フラグは Claude Code にそのまま渡ります。上のブロックが埋めているのは、フラグでは救えないケース、つまり tmux セッションだけが残って中の Claude Code が終了している状態です。
|
|
141
141
|
|
|
142
|
-
あわせて `~/.tmux.conf`
|
|
142
|
+
あわせて `~/.tmux.conf` に次の設定が必要です。これが無いと、tmux の中で Shift+Enter とデスクトップ通知が動かなくなります([公式の案内](https://code.claude.com/docs/en/terminal-config#configure-tmux))。
|
|
143
143
|
|
|
144
144
|
```sh
|
|
145
145
|
set -g allow-passthrough on
|
|
@@ -147,11 +147,11 @@ set -s extended-keys on
|
|
|
147
147
|
set -as terminal-features 'xterm*:extkeys'
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
-
Ctrl+B
|
|
150
|
+
Ctrl+B の衝突については何もしなくて構いません。Claude Code が tmux を検出して、自分のショートカットを `Ctrl+B Ctrl+B` に読み替えてくれます。
|
|
151
151
|
|
|
152
152
|
## 設定
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
デフォルト値は、動いていることに気づかない程度を狙って調整してあります。設定は `~/.cckeep/config.json`、環境変数、実行時フラグの順に上書きされます。設定ファイルが壊れている場合は、黙って一部だけ読み込むのではなくエラーで停止します。
|
|
155
155
|
|
|
156
156
|
```json
|
|
157
157
|
{
|
|
@@ -164,52 +164,56 @@ Ctrl+B の衝突は対処不要です。Claude Code は tmux を検出して、
|
|
|
164
164
|
}
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
-
- `interval
|
|
168
|
-
- `cooldown
|
|
169
|
-
- `stuckLimit
|
|
170
|
-
- `missLimit
|
|
171
|
-
- `settle
|
|
172
|
-
- `paneCommand
|
|
167
|
+
- `interval`: 巡回する間隔(秒)。`enable` が登録する間隔でもあります
|
|
168
|
+
- `cooldown`: 同じペインに再び手を出せるようになるまでの秒数
|
|
169
|
+
- `stuckLimit`: `reconnecting` の表示が何回続いたら固まったとみなすか
|
|
170
|
+
- `missLimit`: 以前は表示があったペインが、何回続けて無表示なら繋ぎ直すか
|
|
171
|
+
- `settle`: 静止判定に使う2回のキャプチャの間隔(ミリ秒)。遅いマシンでは増やしてください
|
|
172
|
+
- `paneCommand`: Claude Code のペインだと判定するフォアグラウンドプロセス名
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
どの項目にも環境変数版があります。`CCKEEP_INTERVAL`、`CCKEEP_COOLDOWN`、`CCKEEP_STUCK_LIMIT`、`CCKEEP_MISS_LIMIT`、`CCKEEP_SETTLE`、`CCKEEP_PANE_COMMAND` です。`CCKEEP_HOME` を指定すれば、状態・設定・ログの置き場所を `~/.cckeep` 以外に移せます。
|
|
175
175
|
|
|
176
176
|
## 適用範囲
|
|
177
177
|
|
|
178
|
-
cckeep がするのは接続の張り直しだけです。Claude Code
|
|
178
|
+
cckeep がするのは接続の張り直しだけです。Claude Code 側の再試行回数は**変えられません**。クローズドソースのバイナリに埋め込まれた定数なので、変えられるのは Anthropic だけです。[#34255](https://github.com/anthropics/claude-code/issues/34255) が修正されれば、このツールは要らなくなります。それがいちばん良い結末だと思っています。それまでは、ここに ⭐ を付けるより向こうに 👍 を付けるほうが役に立ちます。
|
|
179
179
|
|
|
180
180
|
設計上、以下は対象外です。
|
|
181
181
|
|
|
182
|
-
- **tmux
|
|
183
|
-
- **VS Code
|
|
184
|
-
- **サーバーモード**(`claude remote-control`)
|
|
185
|
-
- **10
|
|
182
|
+
- **tmux の外のセッション**: 入力を送る経路がありません
|
|
183
|
+
- **VS Code 拡張**: ターミナル TUI ではないので、tmux で包めません
|
|
184
|
+
- **サーバーモード**(`claude remote-control`): 自分で起動したプロセスなので、launchd や systemd、あるいは `while true` ループで直接監督すれば済みます
|
|
185
|
+
- **10分を超える通信断**: Claude Code 自身がセッションを終了してしまうため、張り直す相手が残りません
|
|
186
186
|
|
|
187
187
|
## プライバシー
|
|
188
188
|
|
|
189
|
-
cckeep
|
|
189
|
+
cckeep は接続状態を判定するために、tmux ペインに表示されているテキストを読みます。それはあなたの会話そのものです。そのため、次のようにしています。
|
|
190
190
|
|
|
191
191
|
- すべてローカルで完結し、このパッケージにネットワークコードはありません
|
|
192
192
|
- テレメトリなし、アカウントなし、外部送信なし
|
|
193
|
-
-
|
|
194
|
-
-
|
|
193
|
+
- ペインのテキストは数個の判定文字列と照合したあとすぐ破棄します。ログに残るのはペイン名と判定結果だけです
|
|
194
|
+
- ログの場所は `~/.cckeep/cckeep.log` です。`cckeep logs` で表示できます
|
|
195
195
|
|
|
196
196
|
## 仕組み
|
|
197
197
|
|
|
198
|
-
Claude Code
|
|
198
|
+
Claude Code はフッターにリモートコントロールの状態を表示します。接続中は `/rc active`、再試行中は `/rc reconnecting`、諦めたときは `Remote Control disconnected` という通知です。cckeep は Claude Code が動いているペインを探し、`tmux capture-pane` の出力からこれらを読み取ります。ペインごとの小さなカウンタは `~/.cckeep/state.json` に保存します。
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
ペインを探すところに一手間あります。Claude Code は自身のプロセスタイトルを書き換えるため、tmux はそのペインのコマンド名を `claude` ではなく `2.1.220` のように報告します。tmux の報告する名前だけで判定すると、実機では1つも見つかりません。そこで cckeep はプロセステーブルも参照し、ペインのプロセス(またはそこから起動されたプロセス)が実際に `claude` であればそのペインを対象とします。
|
|
201
201
|
|
|
202
|
-
|
|
202
|
+
「どこを見るか」も同じくらい重要です。状態の判定に使う文字列は画面末尾の十数行からしか読みません。これらの言葉は普通の会話にも出てくるので、たとえば `/rc active` について話しているだけのセッションが「接続中」に見えてしまうからです。逆にダイアログとステータスパネルの検出は画面全体を対象にしています。こちらは誤検知しても1回見送るだけで済みますが、見落とすと誤ったキー入力に直結します。
|
|
203
|
+
|
|
204
|
+
判定層(`src/detect.js`)は、画面テキストと直前の状態だけを受け取る純粋関数です。だからこそ、安全ルールをターミナル無しで網羅的にテストできます。静止判定・直前の再確認・キー送信といった I/O は、実行層(`src/run.js`)が担当します。
|
|
205
|
+
|
|
206
|
+
これらは公開 API ではありません。判定に使っている文字列は UI のテキストなので、いつ変わってもおかしくありません。変わったとき cckeep は誤動作ではなく沈黙します。読めなくなったペインは「一度も接続していない」ように見え、接続を見たことがないペインには決して手を出さないからです。
|
|
203
207
|
|
|
204
208
|
## 開発
|
|
205
209
|
|
|
206
210
|
```sh
|
|
207
211
|
git clone https://github.com/kamihork/cckeep.git && cd cckeep
|
|
208
|
-
npm test #
|
|
212
|
+
npm test # ネットワークも tmux も不要
|
|
209
213
|
node bin/cckeep.js doctor
|
|
210
214
|
```
|
|
211
215
|
|
|
212
|
-
テストは tmux
|
|
216
|
+
テストは tmux をフェイクするので、どんな環境でも走ります。コントリビューションは歓迎です。特に、検出が漏れてしまう Claude Code のバージョンやターミナルでの実際の表示文字列を教えてもらえると助かります。詳しくは [CONTRIBUTING.md](CONTRIBUTING.md) を参照してください。
|
|
213
217
|
|
|
214
218
|
## ライセンス
|
|
215
219
|
|
package/README.md
CHANGED
|
@@ -199,7 +199,11 @@ cckeep reads the visible text of your tmux panes to decide whether a pane is con
|
|
|
199
199
|
|
|
200
200
|
## How it works
|
|
201
201
|
|
|
202
|
-
Claude Code paints a Remote Control indicator in its footer: `/rc active` when connected, `/rc reconnecting` while retrying, and a `Remote Control disconnected` notification when it gives up. cckeep
|
|
202
|
+
Claude Code paints a Remote Control indicator in its footer: `/rc active` when connected, `/rc reconnecting` while retrying, and a `Remote Control disconnected` notification when it gives up. cckeep finds the panes running Claude Code, reads those indicators out of `tmux capture-pane`, and keeps a small per-pane counter in `~/.cckeep/state.json`.
|
|
203
|
+
|
|
204
|
+
Finding those panes takes one extra step: Claude Code rewrites its own process title, so tmux reports such a pane as `2.1.220` rather than `claude`. Matching the name tmux reports therefore finds nothing on a real machine. cckeep checks the process table as well, and treats a pane as Claude Code's when the pane's process — or anything it spawned — is actually `claude`.
|
|
205
|
+
|
|
206
|
+
Where it looks matters as much as what it looks for. The state indicators are read from the last dozen lines only, because the words themselves turn up in ordinary conversation — a session where you happen to discuss `/rc active` would otherwise read as connected. Dialog and status-panel detection deliberately scans the whole pane instead: a false positive there costs one skipped pass, while a miss costs a keystroke in the wrong place.
|
|
203
207
|
|
|
204
208
|
The decision layer (`src/detect.js`) is a pure function of screen text plus prior state, which is why the safety rules can be tested exhaustively without a terminal. The runner (`src/run.js`) does the I/O: the idle check, the last-moment re-check, and the keystrokes.
|
|
205
209
|
|
package/bin/cckeep.js
CHANGED
|
@@ -8,6 +8,7 @@ import * as tmux from '../src/tmux.js';
|
|
|
8
8
|
import * as scheduler from '../src/scheduler.js';
|
|
9
9
|
import { pickLang, strings } from '../src/i18n.js';
|
|
10
10
|
import { resolveCommand } from '../src/commands.js';
|
|
11
|
+
import { parsePsTable, isTargetPane } from '../src/procs.js';
|
|
11
12
|
|
|
12
13
|
const HELP = `cckeep — keep Claude Code Remote Control from silently going dead
|
|
13
14
|
|
|
@@ -90,8 +91,10 @@ function render(out, t, opts) {
|
|
|
90
91
|
console.log(t.hintOutside);
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
94
|
+
// Session names carry a path hash, so they easily outrun a fixed column.
|
|
95
|
+
const width = Math.max(...out.results.map((r) => r.pane.length)) + 2;
|
|
93
96
|
for (const r of out.results) {
|
|
94
|
-
console.log(`${r.pane.padEnd(
|
|
97
|
+
console.log(`${r.pane.padEnd(width)}${describe(r, t)}`);
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
|
|
@@ -155,7 +158,8 @@ async function main() {
|
|
|
155
158
|
case 'doctor': {
|
|
156
159
|
const bin = tmux.tmuxPath();
|
|
157
160
|
const panes = bin && tmux.hasServer() ? tmux.listPanes() : [];
|
|
158
|
-
const
|
|
161
|
+
const procs = parsePsTable(tmux.processTable());
|
|
162
|
+
const claudePanes = panes.filter((p) => isTargetPane(p, procs, config.paneCommand));
|
|
159
163
|
const rows = [
|
|
160
164
|
['platform', platform()],
|
|
161
165
|
['node', process.version],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cckeep",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Keeps Claude Code Remote Control from silently going dead. Re-arms the sessions its 31-second retry budget gives up on — local, tmux-native, zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/detect.js
CHANGED
|
@@ -17,6 +17,17 @@ const PANEL = /Disconnect this session|(?:Show|Hide) QR code/;
|
|
|
17
17
|
*/
|
|
18
18
|
const MODAL = /(?:❯|›)\s*(?:\d+\.|Yes|No)|Do you want|\(y\/n\)/;
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* How many lines from the bottom count as "the footer".
|
|
22
|
+
*
|
|
23
|
+
* The indicators live just below the input box, but the words themselves can
|
|
24
|
+
* appear anywhere in a transcript — a session where you discuss cckeep, or run
|
|
25
|
+
* it, will have "/rc active" sitting in the conversation body. Matching the
|
|
26
|
+
* whole pane made such a session look connected, which then armed the
|
|
27
|
+
* missing-indicator fallback and pointed it at a perfectly healthy session.
|
|
28
|
+
*/
|
|
29
|
+
const FOOTER_LINES = 12;
|
|
30
|
+
|
|
20
31
|
export const DEFAULTS = {
|
|
21
32
|
/** Consecutive checks stuck in "reconnecting" before we treat the bridge as wedged. */
|
|
22
33
|
stuckLimit: 8,
|
|
@@ -34,11 +45,20 @@ export function emptyState() {
|
|
|
34
45
|
* Reduce a captured pane to the handful of signals the rules care about.
|
|
35
46
|
* @param {string} screen raw `tmux capture-pane -p` output
|
|
36
47
|
*/
|
|
37
|
-
export function readScreen(screen) {
|
|
48
|
+
export function readScreen(screen, footerLines = FOOTER_LINES) {
|
|
49
|
+
const lines = String(screen).split('\n');
|
|
50
|
+
const footer = lines.slice(Math.max(0, lines.length - footerLines)).join('\n');
|
|
51
|
+
|
|
38
52
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
// Signals that make cckeep act are read from the footer only, so text in
|
|
54
|
+
// the conversation cannot trigger anything.
|
|
55
|
+
connected: footer.includes(CONNECTED),
|
|
56
|
+
retrying: footer.includes(RETRYING),
|
|
57
|
+
failed: FAILED.test(footer),
|
|
58
|
+
|
|
59
|
+
// Signals that make cckeep hold off are read from the whole pane. A false
|
|
60
|
+
// positive here costs a skipped pass; missing one costs a keystroke in the
|
|
61
|
+
// wrong place.
|
|
42
62
|
panel: PANEL.test(screen),
|
|
43
63
|
modal: MODAL.test(screen),
|
|
44
64
|
};
|
package/src/i18n.js
CHANGED
|
@@ -7,7 +7,7 @@ const en = {
|
|
|
7
7
|
connected: 'connected',
|
|
8
8
|
retrying: 'retrying',
|
|
9
9
|
waiting: 'no indicator',
|
|
10
|
-
neverConnected: '
|
|
10
|
+
neverConnected: 'no indicator seen yet',
|
|
11
11
|
dialog: 'dialog open',
|
|
12
12
|
cooldown: 'cooling down',
|
|
13
13
|
busy: 'busy',
|
|
@@ -30,34 +30,34 @@ const en = {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
const ja = {
|
|
33
|
-
noTmux: 'tmux が見つかりません。cckeep は tmux
|
|
34
|
-
noServer: 'tmux
|
|
35
|
-
noPanes: 'Claude Code が動いている tmux
|
|
36
|
-
hintOutside: 'tmux の外で起動した Claude Code
|
|
33
|
+
noTmux: 'tmux が見つかりません。cckeep は tmux のペインを読んで入力するので、tmux が必要です。',
|
|
34
|
+
noServer: 'tmux が起動していないので、監視するものがありません。',
|
|
35
|
+
noPanes: 'Claude Code が動いている tmux ペインが見つかりません。',
|
|
36
|
+
hintOutside: 'tmux の外で起動した Claude Code には手が出せません。外のプロセスから入力を送る経路がないためです。',
|
|
37
37
|
header: 'ペイン 状態 詳細',
|
|
38
38
|
connected: '接続中',
|
|
39
39
|
retrying: '再試行中',
|
|
40
40
|
waiting: '表示なし',
|
|
41
|
-
neverConnected: '
|
|
42
|
-
dialog: '
|
|
43
|
-
cooldown: '
|
|
44
|
-
busy: '
|
|
45
|
-
recovered: '
|
|
46
|
-
rearmed: '
|
|
47
|
-
confirmed: '
|
|
48
|
-
wouldRearm: '
|
|
49
|
-
wouldConfirm: '
|
|
50
|
-
watching: (n) => `${n}
|
|
51
|
-
enabled: (kind, path) =>
|
|
52
|
-
enableFailed: (kind) => `${kind}
|
|
53
|
-
disabled: (kind) =>
|
|
54
|
-
unsupported: '
|
|
55
|
-
ephemeral: (p) =>
|
|
41
|
+
neverConnected: '接続表示は未確認',
|
|
42
|
+
dialog: 'ダイアログが開いている',
|
|
43
|
+
cooldown: '待機中(連打防止)',
|
|
44
|
+
busy: '実行中のためスキップ',
|
|
45
|
+
recovered: '自力で復帰',
|
|
46
|
+
rearmed: '繋ぎ直しました',
|
|
47
|
+
confirmed: '接続を張り直しました',
|
|
48
|
+
wouldRearm: '繋ぎ直します',
|
|
49
|
+
wouldConfirm: '接続を張り直します',
|
|
50
|
+
watching: (n) => `${n} 秒ごとに確認します。止めるときは Ctrl+C。`,
|
|
51
|
+
enabled: (kind, path) => `有効にしました(${kind})。定義ファイル: ${path}`,
|
|
52
|
+
enableFailed: (kind) => `${kind} の定義ファイルは作成しましたが、読み込みに失敗しました。手動で読み込むか、ターミナルで \`cckeep watch\` を動かしてください。`,
|
|
53
|
+
disabled: (kind) => `無効にしました。${kind} のジョブを削除しています。`,
|
|
54
|
+
unsupported: '自動登録に対応しているのは macOS(launchd)と Linux(systemd user timer)です。それ以外の環境では、`cckeep watch` をお使いの常駐手段で動かしてください。',
|
|
55
|
+
ephemeral: (p) => `登録を中止しました。この cckeep は npm の使い捨てキャッシュ(npx)の中にあります。\n ${p}\nここを指すジョブは、キャッシュが消えた瞬間に何も言わず動かなくなります。先にグローバルへインストールしてください。\n\n npm install -g cckeep && cckeep enable\n`,
|
|
56
56
|
scheduledPath: '登録先',
|
|
57
|
-
scheduledMissing: '見つかりません
|
|
58
|
-
noLog: '
|
|
57
|
+
scheduledMissing: '見つかりません(`cckeep enable` を実行し直してください)',
|
|
58
|
+
noLog: 'まだログはありません。',
|
|
59
59
|
doctorOk: 'ok',
|
|
60
|
-
doctorFail: '
|
|
60
|
+
doctorFail: '見つかりません',
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
export function pickLang(explicit) {
|
package/src/procs.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code rewrites its own process title, so tmux reports a pane running it
|
|
3
|
+
* as e.g. "2.1.220" rather than "claude". Matching `pane_current_command`
|
|
4
|
+
* against a name therefore finds nothing on a real machine — which is exactly
|
|
5
|
+
* the bug this module exists to fix.
|
|
6
|
+
*
|
|
7
|
+
* The executable name is still correct in the process table, so that is what we
|
|
8
|
+
* check. Pure functions here; the one `ps` call lives in tmux.js.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Parse `ps -Ao pid=,ppid=,comm=` into a pid -> {ppid, comm} map. */
|
|
12
|
+
export function parsePsTable(output) {
|
|
13
|
+
const table = new Map();
|
|
14
|
+
for (const line of String(output).split('\n')) {
|
|
15
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/);
|
|
16
|
+
if (!m) continue;
|
|
17
|
+
const [, pid, ppid, rawComm] = m;
|
|
18
|
+
// macOS prints the full path here for some processes; Linux prints a name.
|
|
19
|
+
const comm = rawComm.trim().split('/').pop();
|
|
20
|
+
table.set(Number(pid), { ppid: Number(ppid), comm });
|
|
21
|
+
}
|
|
22
|
+
return table;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Does `rootPid` — or anything it spawned — run `name`?
|
|
27
|
+
*
|
|
28
|
+
* A pane usually runs Claude Code directly, but someone may have it under a
|
|
29
|
+
* shell, a wrapper, or a version manager's shim, so descendants count too.
|
|
30
|
+
*/
|
|
31
|
+
export function hasDescendantNamed(table, rootPid, name, maxDepth = 4) {
|
|
32
|
+
if (!table || !rootPid) return false;
|
|
33
|
+
|
|
34
|
+
const children = new Map();
|
|
35
|
+
for (const [pid, info] of table) {
|
|
36
|
+
if (!children.has(info.ppid)) children.set(info.ppid, []);
|
|
37
|
+
children.get(info.ppid).push(pid);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
let frontier = [Number(rootPid)];
|
|
42
|
+
for (let depth = 0; depth <= maxDepth && frontier.length; depth++) {
|
|
43
|
+
const next = [];
|
|
44
|
+
for (const pid of frontier) {
|
|
45
|
+
if (seen.has(pid)) continue;
|
|
46
|
+
seen.add(pid);
|
|
47
|
+
if (table.get(pid)?.comm === name) return true;
|
|
48
|
+
next.push(...(children.get(pid) ?? []));
|
|
49
|
+
}
|
|
50
|
+
frontier = next;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Is this pane running Claude Code?
|
|
57
|
+
*
|
|
58
|
+
* The name tmux reports is the fast path and still works wherever the title is
|
|
59
|
+
* left alone; the process table is the fallback that makes it work at all.
|
|
60
|
+
*/
|
|
61
|
+
export function isTargetPane(pane, table, name) {
|
|
62
|
+
if (pane.command === name) return true;
|
|
63
|
+
return hasDescendantNamed(table, pane.pid, name);
|
|
64
|
+
}
|
package/src/run.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { decide, readScreen } from './detect.js';
|
|
2
|
+
import { parsePsTable, isTargetPane } from './procs.js';
|
|
2
3
|
import * as realTmux from './tmux.js';
|
|
3
4
|
import { loadState, saveState, forPane, prune, appendLog } from './state.js';
|
|
4
5
|
|
|
@@ -31,11 +32,12 @@ export async function runPass({ tmux = realTmux, config, dryRun = false, now = M
|
|
|
31
32
|
if (!tmux.hasServer()) return { results, acted: 0, error: 'no-server' };
|
|
32
33
|
|
|
33
34
|
const panes = tmux.listPanes();
|
|
35
|
+
const procs = parsePsTable(tmux.processTable ? tmux.processTable() : '');
|
|
34
36
|
const state = loadState();
|
|
35
37
|
let acted = 0;
|
|
36
38
|
|
|
37
39
|
for (const pane of panes) {
|
|
38
|
-
if (pane
|
|
40
|
+
if (!isTargetPane(pane, procs, config.paneCommand)) continue;
|
|
39
41
|
|
|
40
42
|
const screen = tmux.capture(pane.id);
|
|
41
43
|
if (!screen) continue;
|
package/src/tmux.js
CHANGED
|
@@ -44,17 +44,41 @@ export function hasServer() {
|
|
|
44
44
|
|
|
45
45
|
/** Every pane on the server, with the process currently in the foreground. */
|
|
46
46
|
export function listPanes() {
|
|
47
|
-
const out = tmux(['list-panes', '-a', '-F', '#{pane_id}\t#{pane_current_command}\t#{session_name}\t#{window_index}\t#{pane_index}']);
|
|
47
|
+
const out = tmux(['list-panes', '-a', '-F', '#{pane_id}\t#{pane_current_command}\t#{session_name}\t#{window_index}\t#{pane_index}\t#{pane_pid}']);
|
|
48
48
|
if (!out) return [];
|
|
49
49
|
return out
|
|
50
50
|
.split('\n')
|
|
51
51
|
.filter(Boolean)
|
|
52
52
|
.map((line) => {
|
|
53
|
-
const [id, command, session, windowIndex, paneIndex] = line.split('\t');
|
|
54
|
-
return {
|
|
53
|
+
const [id, command, session, windowIndex, paneIndex, pid] = line.split('\t');
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
command,
|
|
57
|
+
session,
|
|
58
|
+
windowIndex,
|
|
59
|
+
paneIndex,
|
|
60
|
+
pid: Number(pid),
|
|
61
|
+
label: `${session}:${windowIndex}.${paneIndex}`,
|
|
62
|
+
};
|
|
55
63
|
});
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
/**
|
|
67
|
+
* One snapshot of the process table per pass. Claude Code rewrites its process
|
|
68
|
+
* title, so the name tmux reports is not enough to recognise a pane running it.
|
|
69
|
+
*/
|
|
70
|
+
export function processTable() {
|
|
71
|
+
try {
|
|
72
|
+
return execFileSync('ps', ['-Ao', 'pid=,ppid=,comm='], {
|
|
73
|
+
encoding: 'utf8',
|
|
74
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
75
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
export function capture(paneId) {
|
|
59
83
|
return tmux(['capture-pane', '-p', '-t', paneId]) ?? '';
|
|
60
84
|
}
|