cckeep 0.2.3 → 0.2.4

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
@@ -72,7 +72,7 @@ asdf reshim nodejs # asdf
72
72
 
73
73
  | 画面の状態 | 意味 | cckeep の動作 |
74
74
  |---|---|---|
75
- | `/rc active` | 接続できている | ペインを覚えるだけ |
75
+ | `/rc active`、または切り詰められた `/rc` | 接続できている | ペインを覚えるだけ |
76
76
  | `/rc reconnecting` | 31秒の再試行中 | 待つ(たいていはこれで戻る) |
77
77
  | `/rc reconnecting` が2分以上 | 固まっている([#34255](https://github.com/anthropics/claude-code/issues/34255)) | パネルを開いて切断し、繋ぎ直す |
78
78
  | `Remote Control disconnected` | 諦めた | すぐに繋ぎ直す |
@@ -199,6 +199,8 @@ Claude Code はフッターにリモートコントロールの状態を表示
199
199
 
200
200
  ペインを探すところに一手間あります。Claude Code は自身のプロセスタイトルを書き換えるため、tmux はそのペインのコマンド名を `claude` ではなく `2.1.220` のように報告します。tmux の報告する名前だけで判定すると、実機では1つも見つかりません。そこで cckeep はプロセステーブルも参照し、ペインのプロセス(またはそこから起動されたプロセス)が実際に `claude` であればそのペインを対象とします。
201
201
 
202
+ インジケータは右寄せで描画されるため、カスタムステータスラインを使っていたりペインが狭かったりすると、`active` が削られて `/rc` だけになります。cckeep はこれを接続中として扱います。インジケータ自体は接続がある間しか描画されず、接続中と読んでもペインを記録して待つだけなので、안全な側に倒れるからです。
203
+
202
204
  「どこを見るか」も同じくらい重要です。状態の判定に使う文字列は画面末尾の十数行からしか読みません。これらの言葉は普通の会話にも出てくるので、たとえば `/rc active` について話しているだけのセッションが「接続中」に見えてしまうからです。逆にダイアログとステータスパネルの検出は画面全体を対象にしています。こちらは誤検知しても1回見送るだけで済みますが、見落とすと誤ったキー入力に直結します。
203
205
 
204
206
  判定層(`src/detect.js`)は、画面テキストと直前の状態だけを受け取る純粋関数です。だからこそ、安全ルールをターミナル無しで網羅的にテストできます。静止判定・直前の再確認・キー送信といった I/O は、実行層(`src/run.js`)が担当します。
package/README.md CHANGED
@@ -74,7 +74,7 @@ This is the whole design problem. A watchdog that types into your terminal on a
74
74
 
75
75
  | State on screen | What it means | What cckeep does |
76
76
  |---|---|---|
77
- | `/rc active` | connected | remembers the pane, nothing else |
77
+ | `/rc active`, or a truncated `/rc` | connected | remembers the pane, nothing else |
78
78
  | `/rc reconnecting` | inside the 31-second budget | waits — this usually resolves |
79
79
  | `/rc reconnecting`, 2 minutes on | wedged ([#34255](https://github.com/anthropics/claude-code/issues/34255)) | cycles the bridge: opens the panel, disconnects, reconnects |
80
80
  | `Remote Control disconnected` | gave up | re-arms immediately |
@@ -203,6 +203,8 @@ Claude Code paints a Remote Control indicator in its footer: `/rc active` when c
203
203
 
204
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
205
 
206
+ The indicator is right-aligned, so a custom status line or a narrow pane squeezes it down to a bare `/rc` with the word cut off. cckeep treats that as connected: the indicator only renders while a link exists, and reading it as connected merely records the pane and waits.
207
+
206
208
  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.
207
209
 
208
210
  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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cckeep",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
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
@@ -3,10 +3,25 @@
3
3
  // so every rule below is directly testable, which matters a lot for a tool whose
4
4
  // failure mode is typing into someone's terminal at the wrong moment.
5
5
 
6
- /** Indicators Claude Code paints in the footer for the Remote Control link. */
7
- const CONNECTED = '/rc active';
8
- const RETRYING = '/rc reconnecting';
9
- const FAILED = /Remote Control disconnected|Remote Control failed|\/rc failed/;
6
+ /**
7
+ * The Remote Control indicator Claude Code right-aligns in its footer.
8
+ *
9
+ * It reads `/rc active`, `/rc reconnecting` or `/rc failed` when there is room
10
+ * — but it is right-aligned on the status line, so a custom statusline or a
11
+ * narrow pane truncates it to a bare `/rc`. Matching the full phrase therefore
12
+ * never fires for anyone with a status line of their own, which is common.
13
+ *
14
+ * Anchored to the end of the line because the indicator is right-aligned. A
15
+ * separator and link may follow it (`/rc active · claude.ai/code`), but prose
16
+ * may not — that is what keeps `/rc` written mid-sentence from matching.
17
+ */
18
+ const RC_INDICATOR = /(?:^|\s)\/rc(?:\s+(active|reconnecting|failed))?(?:\s*[\u00b7|].*)?\s*$/;
19
+
20
+ /** Lines a user types into; they may well contain `/rc` themselves. */
21
+ const INPUT_LINE = /^[❯>]/;
22
+
23
+ /** The notification Claude Code prints when it has given up for good. */
24
+ const FAILED_NOTICE = /Remote Control disconnected|Remote Control failed/;
10
25
 
11
26
  /** The /remote-control status panel — its first entry is "Disconnect this session". */
12
27
  const PANEL = /Disconnect this session|(?:Show|Hide) QR code/;
@@ -28,6 +43,13 @@ const MODAL = /(?:❯|›)\s*(?:\d+\.|Yes|No)|Do you want|\(y\/n\)/;
28
43
  */
29
44
  const FOOTER_LINES = 12;
30
45
 
46
+ /**
47
+ * The indicator sits on the status line, one or two rows from the bottom — a
48
+ * tighter window than the notifications above it, so that `/rc` typed into the
49
+ * input box cannot be mistaken for it.
50
+ */
51
+ const INDICATOR_LINES = 4;
52
+
31
53
  export const DEFAULTS = {
32
54
  /** Consecutive checks stuck in "reconnecting" before we treat the bridge as wedged. */
33
55
  stuckLimit: 8,
@@ -45,16 +67,34 @@ export function emptyState() {
45
67
  * Reduce a captured pane to the handful of signals the rules care about.
46
68
  * @param {string} screen raw `tmux capture-pane -p` output
47
69
  */
70
+ /**
71
+ * Read the indicator out of the last few lines.
72
+ *
73
+ * Returns 'active' | 'reconnecting' | 'failed', or null when no indicator is on
74
+ * screen. A truncated `/rc` counts as 'active': the indicator only renders while
75
+ * a link exists, and reading it as connected merely records the pane and waits,
76
+ * which is the safe direction.
77
+ */
78
+ function readIndicator(lines, window = INDICATOR_LINES) {
79
+ for (const line of lines.slice(Math.max(0, lines.length - window))) {
80
+ if (INPUT_LINE.test(line.trim())) continue;
81
+ const m = line.match(RC_INDICATOR);
82
+ if (m) return m[1] ?? 'active';
83
+ }
84
+ return null;
85
+ }
86
+
48
87
  export function readScreen(screen, footerLines = FOOTER_LINES) {
49
88
  const lines = String(screen).split('\n');
50
89
  const footer = lines.slice(Math.max(0, lines.length - footerLines)).join('\n');
90
+ const indicator = readIndicator(lines);
51
91
 
52
92
  return {
53
93
  // Signals that make cckeep act are read from the footer only, so text in
54
94
  // the conversation cannot trigger anything.
55
- connected: footer.includes(CONNECTED),
56
- retrying: footer.includes(RETRYING),
57
- failed: FAILED.test(footer),
95
+ connected: indicator === 'active',
96
+ retrying: indicator === 'reconnecting',
97
+ failed: indicator === 'failed' || FAILED_NOTICE.test(footer),
58
98
 
59
99
  // Signals that make cckeep hold off are read from the whole pane. A false
60
100
  // positive here costs a skipped pass; missing one costs a keystroke in the