cckeep 0.1.2 → 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 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秒で再試行を諦め、二度と戻りません。<br>
8
- <code>cckeep</code> はそれを検知して繋ぎ直します — 作業中のセッションには触れずに。</p>
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,31 +19,31 @@
19
19
 
20
20
  ## 何が問題か
21
21
 
22
- [リモートコントロール](https://code.claude.com/docs/en/remote-control)は、手元で動いている Claude Code のセッションをスマホや claude.ai から操作する機能です。接続が切れても自動で再接続してくれます — ただし **1/2/4/8/16秒のバックオフで5回まで**。合計 **31秒** の予算です。ノートを閉じる、Wi-Fi を切り替える、エレベーターに乗る。それだけで予算は尽き、接続は閉じられて二度と戻りません。
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
- もう一つの壊れ方もあります。`/rc reconnecting` の表示のまま永久に固まるパターンで、これが [anthropics/claude-code#34255](https://github.com/anthropics/claude-code/issues/34255) 2026年3月から open、👍 99件、未修正です。
24
+ 壊れ方はもう一つあります。`/rc reconnecting` の表示のまま、いつまでも動かなくなるパターンです。これが [anthropics/claude-code#34255](https://github.com/anthropics/claude-code/issues/34255) で、2026年3月に報告されてから 👍 99件を集めたまま、まだ直っていません。
25
25
 
26
- どちらにせよ気づき方は同じです。スマホを開いたらセッションが消えている。公式に案内されている復旧方法は、机に戻って `/remote-control` と打つことです。
26
+ どちらの場合も、気づき方は同じです。スマホを開いたらセッションが消えている。そして公式に案内されている復旧方法は、机に戻って `/remote-control` と打つことだけです。
27
27
 
28
28
  ## クイックスタート
29
29
 
30
30
  ```sh
31
31
  npm install -g cckeep
32
- cckeep install
32
+ cckeep enable
33
33
  ```
34
34
 
35
- バックグラウンドジョブを登録します(macOS は launchd、Linux は systemd user timer)15秒ごとに確認し、死んでいるものを繋ぎ直します。
35
+ `npm install` は CLI を `PATH` に置くだけで、この時点ではまだ何も動いていません。`cckeep enable` がバックグラウンドジョブを登録し(macOS は launchd、Linux は systemd user timer)、そこから15秒ごとに状態を確認して、切れているものを繋ぎ直すようになります。
36
36
 
37
- `npx cckeep install` ではなくグローバルに入れてください。登録されたジョブはインストール先の cckeep を実行しますが、npx のキャッシュは使い捨てです。そこを指すジョブはキャッシュが消された瞬間に、黙って動かなくなります — 監視ツールが持ってはいけない唯一の壊れ方です。そのため `cckeep install` は npx のパスからの登録を拒否します。
37
+ `npx cckeep enable` ではなく、グローバルに入れてから実行してください。登録されたジョブはインストール先の cckeep をそのまま実行しますが、npx のキャッシュは使い捨てです。そこを指したジョブは、キャッシュが消された瞬間に何も言わず動かなくなります。監視ツールにとって、これがいちばん避けたい壊れ方です。そのため `cckeep enable` は npx のパスからの登録を拒否します。
38
38
 
39
- 入れる前に様子を見るだけなら `npx` で構いません。以下の2つは何も変更しません。
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` と出たら**、shim 方式のバージョン管理ツールを使っています。`nodenv` や `asdf` は新しく入った実行ファイルを PATH に出すのに rehash が必要で、`nvm` はシェルを開き直す必要があります。
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 tmux の中で動いていること。** 素のターミナルで起動したセッションには別プロセスから入力を送る手段がなく、どんなツールでも手が出せません。→ [tmux で Claude Code を動かす](#tmux-で-claude-code-を動かす)
54
+ 条件が一つあります。**Claude Code tmux の中で動かすこと**です。素のターミナルで起動したセッションには、外のプロセスから入力を送る手段がありません。届く経路がない以上、どんなツールでも手が出せません。→ [tmux で Claude Code を動かす](#tmux-で-claude-code-を動かす)
55
55
 
56
- > 机に戻る手間が省けたなら、⭐ が同じ問題を抱えている人に届く助けになります。
56
+ > 机に戻る手間が省けたなら、⭐ を付けてもらえると同じ問題を抱えている人に届きやすくなります。
57
57
 
58
- ## 作業中のセッションには打ち込みません
58
+ ## 作業中のセッションには手を出しません
59
59
 
60
- これが設計上の核心です。タイマーでターミナルに文字を打ち込むツールは、「いま安全か」を確実に判断できない限り危険物でしかありません。以下はすべて実装され、[テスト](test/)されています。
60
+ ここがこのツールの設計上いちばん難しいところです。タイマーでターミナルに文字を打ち込むツールは、「いま打っても安全か」を確実に判断できない限り、ただ危険なだけになります。以下はすべて実装済みで、[テスト](test/)で守られています。
61
61
 
62
- - **ターン実行中は絶対に打たない。** ペインを2秒あけて2回キャプチャします。実行中はスピナーとトークンカウンタが動くので、2回が一致すれば何も起きていない証拠、違えば手を出さない
63
- - **ダイアログには打たない。** 権限プロンプトや選択メニューでは Enter が「選択」になります。選択マーカーが画面にあれば、その回はスキップ
64
- - **あなたが開いたパネルには打たない。** `/remote-control` は QR コード付きのステータスパネルを開きます。cckeep が Enter を押すのは、自分で開いたときだけ
65
- - **自分で切ったセッションは戻さない。** 一度でも接続済みだったペインだけを追跡します。意図的に切ったものは切れたまま
66
- - **連打しない。** 1ペインにつき5分に1回まで
67
- - **直前に再確認する。** 判断は1回のキャプチャで下し、待機後にもう一度確認します。その間に復帰したりダイアログが出ていれば、何も送りません
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分継続 | 固着([#34255](https://github.com/anthropics/claude-code/issues/34255)) | パネルを開いて切断し、張り直す |
78
- | `Remote Control disconnected` | 諦めた | 即座に繋ぎ直す |
79
- | 表示なし(以前はあった) | 通知が流れて消えた | 4回静かなら繋ぎ直す |
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
 
@@ -85,17 +85,19 @@ asdf reshim nodejs # asdf
85
85
  cckeep # 状態表示: Claude Code のペインごとに1行
86
86
  cckeep watch # スケジュール登録せずフォアグラウンドで実行
87
87
  cckeep once # 1回だけ実行(スケジューラが叩くもの)
88
- cckeep install # バックグラウンドジョブを登録
89
- cckeep uninstall # 解除
88
+ cckeep enable # バックグラウンドでの監視を開始
89
+ cckeep disable # 監視を停止
90
90
  cckeep doctor # tmux・ペイン・スケジューラ・各パス
91
91
  cckeep logs # これまでの動作
92
92
  ```
93
93
 
94
94
  オプション: `--dry-run`、`--json`、`--interval <秒>`、`--lang en|ja`(`LANG` から自動判定)。
95
95
 
96
+ `install` / `uninstall` は `enable` / `disable` のエイリアスとして引き続き使えます。
97
+
96
98
  ## tmux で Claude Code を動かす
97
99
 
98
- cckeep は tmux ペインを読み、そこに入力します。動いている Claude Code セッションに別プロセスから届く経路はこれだけで、だからこそ tmux 内での起動が必要です。プロセスの再起動は代替になりません — 会話が終わってしまい、守りたかったものそのものを失うからです。
100
+ cckeep は tmux のペインを読み、そこに入力します。動いている Claude Code セッションに外のプロセスから届く経路はこれしかなく、だからこそ tmux の中で起動しておく必要があります。プロセスを再起動すればいい、という話でもありません。それでは会話そのものが終わってしまい、守りたかったものを自分で失うことになります。
99
101
 
100
102
  最小の変更は、対話起動だけを包むシェル関数です。`claude update` や `claude doctor`、`claude -p` はそのまま素通しします。
101
103
 
@@ -135,9 +137,9 @@ cc() {
135
137
  }
136
138
  ```
137
139
 
138
- `cc -c` `cc --continue` はそのまま使えます — フラグは Claude Code にそのまま渡ります。上のブロックが埋めるのは、フラグでは救えないケース(tmux セッションだけ残って中の Claude Code が終了している状態)です。
140
+ `cc -c` `cc --continue` はそのまま使えます。フラグは Claude Code にそのまま渡ります。上のブロックが埋めているのは、フラグでは救えないケース、つまり tmux セッションだけが残って中の Claude Code が終了している状態です。
139
141
 
140
- あわせて `~/.tmux.conf` に2行必要です。これが無いと tmux 内で Shift+Enter とデスクトップ通知が壊れます([公式の案内](https://code.claude.com/docs/en/terminal-config#configure-tmux))。
142
+ あわせて `~/.tmux.conf` に次の設定が必要です。これが無いと、tmux の中で Shift+Enter とデスクトップ通知が動かなくなります([公式の案内](https://code.claude.com/docs/en/terminal-config#configure-tmux))。
141
143
 
142
144
  ```sh
143
145
  set -g allow-passthrough on
@@ -145,11 +147,11 @@ set -s extended-keys on
145
147
  set -as terminal-features 'xterm*:extkeys'
146
148
  ```
147
149
 
148
- Ctrl+B の衝突は対処不要です。Claude Code tmux を検出して、自身のショートカットを `Ctrl+B Ctrl+B` に読み替えます。
150
+ Ctrl+B の衝突については何もしなくて構いません。Claude Code tmux を検出して、自分のショートカットを `Ctrl+B Ctrl+B` に読み替えてくれます。
149
151
 
150
152
  ## 設定
151
153
 
152
- デフォルトは「存在に気づかない」ことを狙って調整してあります。`~/.cckeep/config.json`、環境変数、実行時フラグの順で上書きされます。壊れた設定ファイルは黙って一部だけ読むのではなく、エラーで停止します。
154
+ デフォルト値は、動いていることに気づかない程度を狙って調整してあります。設定は `~/.cckeep/config.json`、環境変数、実行時フラグの順に上書きされます。設定ファイルが壊れている場合は、黙って一部だけ読み込むのではなくエラーで停止します。
153
155
 
154
156
  ```json
155
157
  {
@@ -162,52 +164,56 @@ Ctrl+B の衝突は対処不要です。Claude Code は tmux を検出して、
162
164
  }
163
165
  ```
164
166
 
165
- - `interval` — 巡回間隔(秒)。`install` が登録する間隔でもある
166
- - `cooldown` — 同じペインに再度手を出せるまでの秒数
167
- - `stuckLimit` `reconnecting` が何回続いたら固着とみなすか
168
- - `missLimit` — 表示のあったペインが何回無表示なら繋ぎ直すか
169
- - `settle` — 静止判定の2回のキャプチャ間隔(ミリ秒)。遅いマシンでは増やす
170
- - `paneCommand` Claude Code のペインと判定するフォアグラウンドプロセス名
167
+ - `interval`: 巡回する間隔(秒)。`enable` が登録する間隔でもあります
168
+ - `cooldown`: 同じペインに再び手を出せるようになるまでの秒数
169
+ - `stuckLimit`: `reconnecting` の表示が何回続いたら固まったとみなすか
170
+ - `missLimit`: 以前は表示があったペインが、何回続けて無表示なら繋ぎ直すか
171
+ - `settle`: 静止判定に使う2回のキャプチャの間隔(ミリ秒)。遅いマシンでは増やしてください
172
+ - `paneCommand`: Claude Code のペインだと判定するフォアグラウンドプロセス名
171
173
 
172
- すべてに環境変数版があります: `CCKEEP_INTERVAL`、`CCKEEP_COOLDOWN`、`CCKEEP_STUCK_LIMIT`、`CCKEEP_MISS_LIMIT`、`CCKEEP_SETTLE`、`CCKEEP_PANE_COMMAND`。`CCKEEP_HOME` で状態・設定・ログの置き場所を `~/.cckeep` から移せます。
174
+ どの項目にも環境変数版があります。`CCKEEP_INTERVAL`、`CCKEEP_COOLDOWN`、`CCKEEP_STUCK_LIMIT`、`CCKEEP_MISS_LIMIT`、`CCKEEP_SETTLE`、`CCKEEP_PANE_COMMAND` です。`CCKEEP_HOME` を指定すれば、状態・設定・ログの置き場所を `~/.cckeep` 以外に移せます。
173
175
 
174
176
  ## 適用範囲
175
177
 
176
- cckeep がするのは接続の張り直しだけです。Claude Code の再試行予算そのものは**変えません** — クローズドソースのバイナリ内の定数であり、変えられるのは Anthropic だけです。[#34255](https://github.com/anthropics/claude-code/issues/34255) が修正されればこのツールは不要になります。それが正しい結末です。それまでは、ここに star を付けるより向こうに 👍 を付けるほうが価値があります。
178
+ cckeep がするのは接続の張り直しだけです。Claude Code 側の再試行回数は**変えられません**。クローズドソースのバイナリに埋め込まれた定数なので、変えられるのは Anthropic だけです。[#34255](https://github.com/anthropics/claude-code/issues/34255) が修正されれば、このツールは要らなくなります。それがいちばん良い結末だと思っています。それまでは、ここに を付けるより向こうに 👍 を付けるほうが役に立ちます。
177
179
 
178
180
  設計上、以下は対象外です。
179
181
 
180
- - **tmux 外のセッション** — 入力を送る経路がない
181
- - **VS Code 拡張** ターミナル TUI ではなく、tmux で包めない
182
- - **サーバーモード**(`claude remote-control`) 自分が起動したプロセスなので、launchd/systemd や `while true` ループで直接監督すればよい
183
- - **10分を超える通信断** Claude Code 自身がセッションを終了するため、張り直す対象が残らない
182
+ - **tmux の外のセッション**: 入力を送る経路がありません
183
+ - **VS Code 拡張**: ターミナル TUI ではないので、tmux で包めません
184
+ - **サーバーモード**(`claude remote-control`): 自分で起動したプロセスなので、launchd や systemd、あるいは `while true` ループで直接監督すれば済みます
185
+ - **10分を超える通信断**: Claude Code 自身がセッションを終了してしまうため、張り直す相手が残りません
184
186
 
185
187
  ## プライバシー
186
188
 
187
- cckeep は接続状態を判定するために tmux ペインの表示テキストを読みます。それはあなたの会話です。したがって:
189
+ cckeep は接続状態を判定するために、tmux ペインに表示されているテキストを読みます。それはあなたの会話そのものです。そのため、次のようにしています。
188
190
 
189
191
  - すべてローカルで完結し、このパッケージにネットワークコードはありません
190
192
  - テレメトリなし、アカウントなし、外部送信なし
191
- - ペインのテキストは数個の判定文字列と照合した後すぐ破棄され、ログに残るのはペイン名と判定結果だけです
192
- - ログは `~/.cckeep/cckeep.log`。`cckeep logs` で表示できます
193
+ - ペインのテキストは数個の判定文字列と照合したあとすぐ破棄します。ログに残るのはペイン名と判定結果だけです
194
+ - ログの場所は `~/.cckeep/cckeep.log` です。`cckeep logs` で表示できます
193
195
 
194
196
  ## 仕組み
195
197
 
196
- Claude Code はフッターにリモートコントロールの状態を描画します。接続中は `/rc active`、再試行中は `/rc reconnecting`、諦めたときは `Remote Control disconnected` の通知です。cckeep は tmux にフォアグラウンドプロセスが `claude` のペインを問い合わせ、`tmux capture-pane` からこれらを読み取り、ペインごとの小さなカウンタを `~/.cckeep/state.json` に保持します。
198
+ Claude Code はフッターにリモートコントロールの状態を表示します。接続中は `/rc active`、再試行中は `/rc reconnecting`、諦めたときは `Remote Control disconnected` という通知です。cckeep は Claude Code が動いているペインを探し、`tmux capture-pane` の出力からこれらを読み取ります。ペインごとの小さなカウンタは `~/.cckeep/state.json` に保存します。
199
+
200
+ ペインを探すところに一手間あります。Claude Code は自身のプロセスタイトルを書き換えるため、tmux はそのペインのコマンド名を `claude` ではなく `2.1.220` のように報告します。tmux の報告する名前だけで判定すると、実機では1つも見つかりません。そこで cckeep はプロセステーブルも参照し、ペインのプロセス(またはそこから起動されたプロセス)が実際に `claude` であればそのペインを対象とします。
201
+
202
+ 「どこを見るか」も同じくらい重要です。状態の判定に使う文字列は画面末尾の十数行からしか読みません。これらの言葉は普通の会話にも出てくるので、たとえば `/rc active` について話しているだけのセッションが「接続中」に見えてしまうからです。逆にダイアログとステータスパネルの検出は画面全体を対象にしています。こちらは誤検知しても1回見送るだけで済みますが、見落とすと誤ったキー入力に直結します。
197
203
 
198
- 判定層(`src/detect.js`)は画面テキストと直前の状態だけを引数に取る純粋関数です。だからこそ安全ルールをターミナル無しで網羅的にテストできます。I/O は実行層(`src/run.js`)が担当します — 静止判定、直前の再確認、キー送信です。
204
+ 判定層(`src/detect.js`)は、画面テキストと直前の状態だけを受け取る純粋関数です。だからこそ、安全ルールをターミナル無しで網羅的にテストできます。静止判定・直前の再確認・キー送信といった I/O は、実行層(`src/run.js`)が担当します。
199
205
 
200
- これらは公開 API ではありません。判定文字列は UI テキストであり変わり得ます。変わったとき cckeep は誤動作ではなく沈黙します — 読めないペインは「一度も接続していない」ように見え、接続を見たことがないペインには決して手を出さないためです。
206
+ これらは公開 API ではありません。判定に使っている文字列は UI のテキストなので、いつ変わってもおかしくありません。変わったとき cckeep は誤動作ではなく沈黙します。読めなくなったペインは「一度も接続していない」ように見え、接続を見たことがないペインには決して手を出さないからです。
201
207
 
202
208
  ## 開発
203
209
 
204
210
  ```sh
205
211
  git clone https://github.com/kamihork/cckeep.git && cd cckeep
206
- npm test # 36テスト。ネットワークも tmux も不要
212
+ npm test # ネットワークも tmux も不要
207
213
  node bin/cckeep.js doctor
208
214
  ```
209
215
 
210
- テストは tmux をフェイクするのでどこでも走ります。コントリビューション歓迎です — 特に、検出が漏れる Claude Code のバージョンやターミナルでの実際の表示文字列が助かります。[CONTRIBUTING.md](CONTRIBUTING.md) を参照してください。
216
+ テストは tmux をフェイクするので、どんな環境でも走ります。コントリビューションは歓迎です。特に、検出が漏れてしまう Claude Code のバージョンやターミナルでの実際の表示文字列を教えてもらえると助かります。詳しくは [CONTRIBUTING.md](CONTRIBUTING.md) を参照してください。
211
217
 
212
218
  ## ライセンス
213
219
 
package/README.md CHANGED
@@ -29,12 +29,12 @@ Either way you find out the same way: you reach for your phone, and the session
29
29
 
30
30
  ```sh
31
31
  npm install -g cckeep
32
- cckeep install
32
+ cckeep enable
33
33
  ```
34
34
 
35
- That registers a background job — launchd on macOS, a systemd user timer on Linux — that checks every 15 seconds and re-arms whatever went dead.
35
+ `npm install` only puts the CLI on your `PATH`; `cckeep enable` is the step that registers a background job — launchd on macOS, a systemd user timer on Linux — that checks every 15 seconds and re-arms whatever went dead.
36
36
 
37
- Install it globally rather than running `npx cckeep install`. The scheduled job runs cckeep from wherever it was installed, and npx's cache is throwaway: a job pointing into it keeps working until the cache is cleared and then stops, silently — the one failure a watchdog must not have. `cckeep install` refuses to schedule from an npx path for that reason.
37
+ Install it globally rather than running `npx cckeep enable`. The scheduled job runs cckeep from wherever it was installed, and npx's cache is throwaway: a job pointing into it keeps working until the cache is cleared and then stops, silently — the one failure a watchdog must not have. `cckeep enable` refuses to schedule from an npx path for that reason.
38
38
 
39
39
  To look before installing anything, `npx` is fine — neither of these changes a thing:
40
40
 
@@ -87,14 +87,16 @@ This is the whole design problem. A watchdog that types into your terminal on a
87
87
  cckeep # status: one line per Claude Code pane
88
88
  cckeep watch # run in the foreground instead of scheduling
89
89
  cckeep once # a single pass — what the scheduler runs
90
- cckeep install # register the background job
91
- cckeep uninstall # remove it
90
+ cckeep enable # start checking in the background
91
+ cckeep disable # stop checking
92
92
  cckeep doctor # tmux, panes, scheduler, paths
93
93
  cckeep logs # what it has done
94
94
  ```
95
95
 
96
96
  Options: `--dry-run`, `--json`, `--interval <s>`, `--lang en|ja` (auto-detected from `LANG`).
97
97
 
98
+ `install` and `uninstall` still work as aliases for `enable` and `disable`.
99
+
98
100
  ## Running Claude Code in tmux
99
101
 
100
102
  cckeep reads and types into tmux panes. That is the only channel a separate process has into a live Claude Code session — and it is why the session must be started inside tmux. Restarting the process is not an alternative: it would end the conversation, which is exactly what you are trying to save.
@@ -166,7 +168,7 @@ Defaults are tuned so you never notice it. Override in `~/.cckeep/config.json`,
166
168
  }
167
169
  ```
168
170
 
169
- - `interval` — seconds between passes (also what `install` schedules)
171
+ - `interval` — seconds between passes (also what `enable` schedules)
170
172
  - `cooldown` — seconds before the same pane may be acted on again
171
173
  - `stuckLimit` — checks in `reconnecting` before the bridge is treated as wedged
172
174
  - `missLimit` — checks with no indicator before re-arming a pane that had one
@@ -197,7 +199,11 @@ cckeep reads the visible text of your tmux panes to decide whether a pane is con
197
199
 
198
200
  ## How it works
199
201
 
200
- 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 asks tmux for every pane whose foreground process is `claude`, reads those indicators out of `tmux capture-pane`, and keeps a small per-pane counter in `~/.cckeep/state.json`.
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.
201
207
 
202
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.
203
209
 
package/bin/cckeep.js CHANGED
@@ -7,6 +7,8 @@ import { logPath, statePath, homeDir } from '../src/state.js';
7
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
+ import { resolveCommand } from '../src/commands.js';
11
+ import { parsePsTable, isTargetPane } from '../src/procs.js';
10
12
 
11
13
  const HELP = `cckeep — keep Claude Code Remote Control from silently going dead
12
14
 
@@ -17,11 +19,14 @@ COMMANDS
17
19
  status what cckeep sees right now (default)
18
20
  watch run in the foreground, one pass every --interval seconds
19
21
  once a single pass — what the scheduler runs
20
- install register a background job (launchd on macOS, systemd on Linux)
21
- uninstall remove it
22
+ enable start checking in the background (launchd / systemd user timer)
23
+ disable stop checking
22
24
  doctor check tmux, Claude Code panes, and the scheduler
23
25
  logs show recent actions
24
26
 
27
+ "npm install -g cckeep" only puts this CLI on your PATH. "cckeep enable" is
28
+ what registers the background job. (install / uninstall still work as aliases.)
29
+
25
30
  OPTIONS
26
31
  --interval <s> seconds between passes (default 15)
27
32
  --dry-run report what would be sent, send nothing
@@ -86,8 +91,10 @@ function render(out, t, opts) {
86
91
  console.log(t.hintOutside);
87
92
  return;
88
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;
89
96
  for (const r of out.results) {
90
- console.log(`${r.pane.padEnd(14)}${describe(r, t)}`);
97
+ console.log(`${r.pane.padEnd(width)}${describe(r, t)}`);
91
98
  }
92
99
  }
93
100
 
@@ -107,7 +114,7 @@ async function main() {
107
114
  process.exit(1);
108
115
  }
109
116
 
110
- switch (opts.command ?? 'status') {
117
+ switch (resolveCommand(opts.command)) {
111
118
  case 'status': {
112
119
  const out = await runPass({ config, dryRun: true });
113
120
  render(out, t, opts);
@@ -128,7 +135,7 @@ async function main() {
128
135
  await new Promise((r) => setTimeout(r, config.interval * 1000));
129
136
  }
130
137
  }
131
- case 'install': {
138
+ case 'enable': {
132
139
  const res = scheduler.install({ interval: config.interval });
133
140
  if (res.kind === 'ephemeral') {
134
141
  console.error(t.ephemeral(res.path));
@@ -138,20 +145,21 @@ async function main() {
138
145
  console.log(t.unsupported);
139
146
  process.exit(1);
140
147
  }
141
- console.log(res.ok ? t.installed(res.kind, res.path) : t.installFailed(res.kind));
148
+ console.log(res.ok ? t.enabled(res.kind, res.path) : t.enableFailed(res.kind));
142
149
  if (!res.ok) process.exit(1);
143
150
  break;
144
151
  }
145
- case 'uninstall': {
152
+ case 'disable': {
146
153
  const res = scheduler.uninstall();
147
154
  if (res.kind === 'unsupported') return console.log(t.unsupported);
148
- console.log(t.uninstalled(res.kind));
155
+ console.log(t.disabled(res.kind));
149
156
  break;
150
157
  }
151
158
  case 'doctor': {
152
159
  const bin = tmux.tmuxPath();
153
160
  const panes = bin && tmux.hasServer() ? tmux.listPanes() : [];
154
- const claudePanes = panes.filter((p) => p.command === config.paneCommand);
161
+ const procs = parsePsTable(tmux.processTable());
162
+ const claudePanes = panes.filter((p) => isTargetPane(p, procs, config.paneCommand));
155
163
  const rows = [
156
164
  ['platform', platform()],
157
165
  ['node', process.version],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cckeep",
3
- "version": "0.1.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": {
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `npm install -g cckeep` puts the CLI on your PATH; registering the background
3
+ * job is a separate, deliberate step. Naming that step `install` too made the
4
+ * quick start read as if you were installing twice, so the scheduler verbs are
5
+ * `enable` / `disable` — the same words systemctl uses for the same idea.
6
+ *
7
+ * The old names stay as aliases: they shipped in 0.1.x and in the plists people
8
+ * already generated, and breaking them buys nothing.
9
+ */
10
+ export const ALIASES = {
11
+ install: 'enable',
12
+ uninstall: 'disable',
13
+ };
14
+
15
+ export const COMMANDS = new Set([
16
+ 'status',
17
+ 'watch',
18
+ 'once',
19
+ 'enable',
20
+ 'disable',
21
+ 'doctor',
22
+ 'logs',
23
+ 'help',
24
+ 'version',
25
+ ]);
26
+
27
+ /** Map whatever the user typed onto a canonical command. */
28
+ export function resolveCommand(input) {
29
+ const name = input ?? 'status';
30
+ return ALIASES[name] ?? name;
31
+ }
32
+
33
+ export function isKnownCommand(input) {
34
+ return COMMANDS.has(resolveCommand(input));
35
+ }
package/src/config.js CHANGED
@@ -5,7 +5,7 @@ import { DEFAULTS } from './detect.js';
5
5
 
6
6
  export const BASE = {
7
7
  ...DEFAULTS,
8
- /** Seconds between passes in watch mode, and what `install` schedules. */
8
+ /** Seconds between passes in watch mode, and what `enable` schedules. */
9
9
  interval: 15,
10
10
  /** Milliseconds between the two captures of the idle check. */
11
11
  settle: 2000,
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
- connected: screen.includes(CONNECTED),
40
- retrying: screen.includes(RETRYING),
41
- failed: FAILED.test(screen),
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: 'not connected',
10
+ neverConnected: 'no indicator seen yet',
11
11
  dialog: 'dialog open',
12
12
  cooldown: 'cooling down',
13
13
  busy: 'busy',
@@ -17,11 +17,11 @@ const en = {
17
17
  wouldRearm: 'would re-arm',
18
18
  wouldConfirm: 'would cycle bridge',
19
19
  watching: (n) => `Watching every ${n}s. Ctrl+C to stop.`,
20
- installed: (kind, path) => `Installed (${kind}): ${path}`,
21
- installFailed: (kind) => `Wrote the ${kind} unit, but could not load it. Load it manually, or run \`cckeep watch\` in a terminal.`,
22
- uninstalled: (kind) => `Removed the ${kind} job.`,
20
+ enabled: (kind, path) => `Enabled (${kind}): ${path}`,
21
+ enableFailed: (kind) => `Wrote the ${kind} unit, but could not load it. Load it manually, or run \`cckeep watch\` in a terminal.`,
22
+ disabled: (kind) => `Disabled — removed the ${kind} job.`,
23
23
  unsupported: 'Automatic scheduling supports macOS (launchd) and Linux (systemd user timers). Elsewhere, run `cckeep watch` under your own supervisor.',
24
- ephemeral: (p) => `Refusing to schedule: this copy of cckeep lives in npm's throwaway npx cache\n ${p}\nA scheduled job pointing there stops working the moment the cache is cleared — silently. Install it properly first:\n\n npm install -g cckeep && cckeep install\n`,
24
+ ephemeral: (p) => `Refusing to schedule: this copy of cckeep lives in npm's throwaway npx cache\n ${p}\nA scheduled job pointing there stops working the moment the cache is cleared — silently. Install it properly first:\n\n npm install -g cckeep && cckeep enable\n`,
25
25
  scheduledPath: 'scheduled cmd',
26
26
  scheduledMissing: 'MISSING — run `cckeep install` again',
27
27
  noLog: 'No log yet.',
@@ -30,34 +30,34 @@ const en = {
30
30
  };
31
31
 
32
32
  const ja = {
33
- noTmux: 'tmux が見つかりません。cckeep は tmux のペインを読んで入力するため、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} 秒ごとに監視します。Ctrl+C で停止。`,
51
- installed: (kind, path) => `登録しました (${kind}): ${path}`,
52
- installFailed: (kind) => `${kind} の定義は書きましたが、読み込みに失敗しました。手動で読み込むか、ターミナルで \`cckeep watch\` を実行してください。`,
53
- uninstalled: (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 install\n`,
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: '見つかりません`cckeep install` を再実行してください',
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.command !== config.paneCommand) continue;
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 { id, command, session, windowIndex, paneIndex, label: `${session}:${windowIndex}.${paneIndex}` };
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
  }