claude-cache-keepalive 0.1.6 → 0.1.8

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.md CHANGED
@@ -23,8 +23,8 @@ This puts a `cwarm` command on your PATH (npm creates both the Unix and Windows
23
23
  ## Usage
24
24
 
25
25
  ```sh
26
- cwarm # = claude --continue, inside the keepalive host
27
- cwarm new # any claude args are passed straight through
26
+ cwarm # = plain `claude`, inside the keepalive host (no implicit --continue)
27
+ cwarm --continue # resume your last session; any claude args pass straight through
28
28
  cwarm resume
29
29
  cwarm -p "..."
30
30
  cwarm --version # (passed through → prints claude's version)
@@ -101,13 +101,50 @@ Environment variables (mostly for testing / advanced use):
101
101
  > ⚠️ **誠實說明**:保溫=閒置時送一則小訊息(`hi`),會消耗你的方案用量、並在對話留下 `hi` 紀錄。只在長時間閒置後才觸發(1h cache 約 58 分、5m cache 約 4 分)且有冷卻,屬保守設計、明確 opt‑in。不接受這個取捨就別用。
102
102
 
103
103
  - **安裝**:`npm install -g claude-cache-keepalive`
104
- - **使用**:`cwarm`(=`claude --continue` 跑在保溫 host 裡;其餘參數原樣轉給 claude)
104
+ - **使用**:`cwarm`(=乾淨的 `claude` 跑在保溫 host 裡,不再隱含 `--continue`;要接續上次打 `cwarm --continue`,其餘參數原樣轉給 claude)
105
105
  - **暫停**:`touch ~/.claude/cwarm.disabled`;**紀錄**:`~/.claude/cwarm-keepalive.log`
106
106
  - **選配 statusline**(顯示 `♻️ cache 58m12s` 倒數;會先備份、包裝既有 statusline、可一鍵還原):`cwarm setup` / `cwarm setup --remove`
107
107
  - **限制**:不能 detach(關視窗=結束,但縮小/背景照常保溫)。
108
108
 
109
+ ### 運作原理
110
+
111
+ - **PTY host**:`cwarm` 把 `claude` spawn 在一個它自己擁有的 pseudo-terminal 裡,透明地把你的鍵盤 ↔ claude ↔ 畫面(含視窗 resize)接起來。這跟 tmux/expect/VS Code 終端的做法相同,也是唯一穩健、能把輸入注入終端程式的方式。
112
+ - **閒置偵測**:閒置=距你上次**訊息**多久,量自 `~/.claude/projects/` 底下最新的 transcript 檔。這才是決定 cache 年齡的訊號——捲動、用方向鍵讀、打到一半沒送出,都是終端輸入但不會刷新 cache,所以不該算成活動。(早期版本計時鍵盤輸入,會讓你在閱讀時 cache 冷掉。)
113
+ - **TTL 感知(實測,非猜測)**:cache TTL 直接讀自 transcript 的 `message.usage.cache_creation`,不從訂閱方案推斷——最近有寫 `ephemeral_1h_input_tokens` → 1h cache(閒置約 58 分才注入、冷卻 1h);只有 `ephemeral_5m_input_tokens`(或還沒證據)→ 5m cache(保守,約 4 分注入、冷卻 5m)。這能撐過 client 版本、環境變數、伺服器旗標的變動(例如 Pro 帳號也可能拿到 1h cache)。
114
+ - **提示安全注入**:keepalive 只在 PTY **靜止一小段時間後**才觸發(`CWARM_QUIET_MS`,預設 2.5 秒)。必答提示(工具權限、`AskUserQuestion`、計畫批准)的 spinner 會一直動,忙著跑工具時也持續輸出,兩者都「不安靜」,所以 keepalive 不會送進去(不會誤選選單預設項、也不會打斷長工具執行)。而且注入時會**先送 `Esc`** 退回輸入框,那個 Enter 永遠落不到提示上。(提示真的卡住時 cache 本來就無法保溫,等你回答後會自動恢復。)
115
+ - **與焦點/縮小無關**:注入是行程內部的 `pty.write`,跟視窗狀態無關。只有關閉視窗(結束 host 行程)才會停。
116
+
117
+ ### 設定
118
+
119
+ 環境變數(多為測試/進階用途):
120
+
121
+ | 變數 | 意義 |
122
+ |-----|------|
123
+ | `CWARM_MSG` | keepalive 訊息(預設 `hi`) |
124
+ | `CWARM_TICK_MS` | 檢查間隔(預設 `20000`) |
125
+ | `CWARM_QUIET_MS` | 畫面需靜止多久才注入(預設 `2500`) |
126
+ | `CWARM_ESC_DELAY_MS` | `Esc` 與訊息之間的間隔(預設 `250`) |
127
+ | `CWARM_THRESHOLD_S` | 覆寫閒置門檻(秒) |
128
+ | `CWARM_TTL_S` | 覆寫冷卻(秒) |
129
+ | `CWARM_CLAUDE` | `claude` 執行檔路徑(否則用 `which`/`where` 自動偵測) |
130
+ | `CLAUDE_CONFIG_DIR` | Claude 設定目錄(預設 `~/.claude`) |
131
+
132
+ ### 平台支援
133
+
134
+ - **Windows**(Git Bash/PowerShell/cmd/Windows Terminal):已驗證,含非 ASCII(中日韓)輸入。
135
+ - **Linux arm64/aarch64**:已在 Raspberry Pi 4(Debian、Node 22)驗證——全域安裝(node-pty 乾淨編譯)、`cwarm` 啟動、即時 keepalive 注入都確認可用。x64 預期相同。
136
+ - **macOS**:同樣的跨平台機制(node-pty + 你 shell 裡的 `claude`);預期可用,尚未實測,歡迎回報。
137
+
109
138
  ## Changelog
110
139
 
140
+ ### 0.1.8
141
+ - **Fix:** the keepalive's `Esc`‑prefix (added in 0.1.5) could dismiss Claude Code's **folder‑trust dialog** ("Do you trust the files in this folder?"). Since 0.1.7 dropped the implicit `--continue`, bare `cwarm` starts a *fresh* session, so an untrusted directory shows the trust dialog on launch; if you stepped away past the idle threshold, the keepalive's `Esc` cancelled it — which writes `hasTrustDialogAccepted: false` into `~/.claude.json` and makes that folder's `.claude/settings.local.json` permissions silently ignored (the "Ignoring N permissions.allow entries: this workspace has not been trusted" warning you only see after `/exit` restores the normal screen). The keepalive now detects the trust dialog on screen and **skips the whole tick** (no `Esc`, no message), leaving it for you to answer; all other mandatory prompts keep the 0.1.5 `Esc` behaviour. Adds `looksLikeTrustPrompt`.
142
+ - **修正:** 0.1.5 加入的 keepalive **`Esc` 先行**可能會把 Claude Code 的**資料夾信任對話框**(「Do you trust the files in this folder?」)給收掉。自 0.1.7 拿掉隱含的 `--continue` 後,單獨打 `cwarm` 會開*全新* session,所以進入未信任的資料夾時啟動就會跳信任框;若你人走開、閒置過門檻,keepalive 的 `Esc` 就把它取消掉——這會在 `~/.claude.json` 寫下 `hasTrustDialogAccepted: false`,使該資料夾的 `.claude/settings.local.json` 權限被靜默忽略(就是你 `/exit` 還原一般畫面後才看到的「Ignoring N permissions.allow entries: this workspace has not been trusted」警告)。keepalive 現在會偵測畫面上的信任框並**整輪跳過**(不送 `Esc`、不送訊息),交給你本人回答;其他必答提示維持 0.1.5 的 `Esc` 行為。新增 `looksLikeTrustPrompt`。
143
+
144
+ ### 0.1.7
145
+ - **Change:** `cwarm` no longer implicitly adds `--continue`. It is now a fully transparent pass‑through — `cwarm [args]` is exactly `claude [args]`, so bare `cwarm` starts a clean session. To resume your last session, run `cwarm --continue`. (Previously bare `cwarm` auto‑resumed.)
146
+ - **變更:** `cwarm` 不再隱含補上 `--continue`,改為完全透傳——`cwarm [參數]` 就等於 `claude [參數]`,所以單獨打 `cwarm` 會開全新 session。要接續上次請打 `cwarm --continue`。(先前單獨打 `cwarm` 會自動接續。)
147
+
111
148
  ### 0.1.6
112
149
  - **Docs:** every changelog entry now carries a Traditional Chinese version alongside the English. No code change.
113
150
  - **文件:** 每條 changelog 現在都在英文旁附上繁體中文。無程式碼變動。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-cache-keepalive",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Keep Claude Code's prompt cache warm while idle, by running claude inside a PTY host and injecting a tiny keepalive when you step away. Cross-platform, no tmux required.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -12,14 +12,16 @@ if (first === 'setup') {
12
12
  printHelp();
13
13
  process.exit(0);
14
14
  } else {
15
- startHost({ args: argv }); // 無參數時 host 內部預設 --continue
15
+ startHost({ args: argv }); // 完全透傳給 claude;不帶參數就是乾淨的 claude(不再隱含 --continue
16
16
  }
17
17
 
18
18
  function printHelp() {
19
19
  process.stdout.write(`cwarm — keep Claude Code's prompt cache warm while idle.
20
20
 
21
21
  Usage:
22
- cwarm [claude args...] Launch claude (default: --continue) inside the keepalive host.
22
+ cwarm [claude args...] Launch claude inside the keepalive host. Args pass straight
23
+ through, so \`cwarm\` === plain \`claude\`; use \`cwarm --continue\`
24
+ to resume your last session.
23
25
  cwarm setup Optionally install the cache-countdown statusline (opt-in).
24
26
  cwarm setup --remove Remove the statusline this tool installed.
25
27
  cwarm help Show this help.
package/src/host.mjs CHANGED
@@ -5,7 +5,7 @@ import { createRequire } from 'node:module';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { spawnSync } from 'node:child_process';
8
- import { defaultClaudeDir, regimeParams, detectTtlRegime, decideInject, transcriptIdleMs } from './keepalive.mjs';
8
+ import { defaultClaudeDir, regimeParams, detectTtlRegime, decideInject, transcriptIdleMs, looksLikeTrustPrompt } from './keepalive.mjs';
9
9
 
10
10
  const require = createRequire(import.meta.url);
11
11
  const isWin = process.platform === 'win32';
@@ -35,7 +35,9 @@ export function startHost(opts = {}) {
35
35
  const LOG = path.join(claudeDir, 'cwarm-keepalive.log');
36
36
  const DISABLE = path.join(claudeDir, 'cwarm.disabled');
37
37
 
38
- const args = opts.args && opts.args.length ? opts.args : ['--continue'];
38
+ // 完全透傳:cwarm [args] === claude [args]。不帶參數就是乾淨的 `claude`(全新 session);
39
+ // 要接續上次請自己打 `cwarm --continue`。不再隱含補 --continue。
40
+ const args = opts.args || [];
39
41
  const { file, args: spawnArgs } = spawnSpec(resolveClaude(), args);
40
42
 
41
43
  const ptyProc = pty.spawn(file, spawnArgs, {
@@ -56,7 +58,14 @@ export function startHost(opts = {}) {
56
58
  // 追蹤 claude 最近一次有輸出到畫面的時刻:閒置在輸入框時畫面靜止;提示等待回答時 spinner 在動、
57
59
  // 生成/跑工具時持續輸出。注入前要求畫面已靜止一段時間(見下方 quietMs),就能避開「忙/卡」狀態。
58
60
  let lastOutputMs = Date.now();
59
- ptyProc.onData((d) => { lastOutputMs = Date.now(); process.stdout.write(d); });
61
+ // 保留最近一小段螢幕輸出(含 ANSI),供偵測「資料夾信任」對話框用:那個框被保溫 Esc 掉會留下
62
+ // hasTrustDialogAccepted:false,害該資料夾 settings.local.json 權限整批失效,故它在畫面上時整輪不注入。
63
+ let screenBuf = '';
64
+ ptyProc.onData((d) => {
65
+ lastOutputMs = Date.now();
66
+ process.stdout.write(d);
67
+ screenBuf = (screenBuf + d.toString('utf8')).slice(-8192);
68
+ });
60
69
  process.stdout.on('resize', () => {
61
70
  try { ptyProc.resize(process.stdout.columns || 80, process.stdout.rows || 24); } catch {}
62
71
  });
@@ -73,7 +82,18 @@ export function startHost(opts = {}) {
73
82
  if (ttlO != null) overrides.ttl = Number(ttlO);
74
83
 
75
84
  let lastFire = 0;
85
+ let trustGuardLogged = false;
76
86
  const timer = setInterval(() => {
87
+ // 信任對話框在畫面上時,這輪完全不動作(連 Esc 都不送)——那是使用者本人該回答的框,被保溫
88
+ // Esc 掉會留下 hasTrustDialogAccepted:false,害該資料夾 settings.local.json 權限失效、之後不再跳框。
89
+ if (looksLikeTrustPrompt(screenBuf)) {
90
+ if (!trustGuardLogged) {
91
+ trustGuardLogged = true;
92
+ try { fs.appendFileSync(LOG, `${new Date().toISOString()} skip: trust dialog on screen — left for the user to answer\n`); } catch {}
93
+ }
94
+ return;
95
+ }
96
+ trustGuardLogged = false;
77
97
  const cwd = process.cwd();
78
98
  const regime = detectTtlRegime(claudeDir, cwd); // 從 transcript 實測 1h/5m,不再猜方案
79
99
  const { ttl, idleThreshold } = regimeParams(regime, overrides);
package/src/keepalive.mjs CHANGED
@@ -141,3 +141,19 @@ export function decideInject({ now, idleMs, lastFire, idleThreshold, ttl, disabl
141
141
  if (quietMs != null && screenIdleMs != null && screenIdleMs < quietMs) return false; // 畫面還在動(提示/生成/打字中)
142
142
  return true;
143
143
  }
144
+
145
+ // 畫面上是否正顯示 Claude Code 的「資料夾信任」對話框("Do you trust the files in this folder?")。
146
+ // 保溫的「Esc 先行」是用來收掉一般必答 modal(權限/選單/計畫批准),但信任框特殊:被 Esc 掉會在
147
+ // ~/.claude.json 寫下 hasTrustDialogAccepted:false,使該資料夾的 .claude/settings.local.json 權限
148
+ // 整批失效、且之後不再自動跳框。這種框只能由使用者本人回答——偵測到就整輪跳過注入(連 Esc 都不送)。
149
+ // 傳入的是原始終端輸出(含 ANSI),先剝掉控制序列再比對,避免顏色/游標碼把字拆開。
150
+ const TRUST_PROMPT_RE = /trust\s+the\s+files\s+in\s+this\s+(?:folder|workspace)/i;
151
+ export function looksLikeTrustPrompt(screenText) {
152
+ if (!screenText) return false;
153
+ const plain = String(screenText)
154
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, ' ') // OSC …(BEL 或 ST 結尾)
155
+ .replace(/\x1b[@-Z\\-_]/g, ' ') // 兩位元組跳脫
156
+ .replace(/\x1b\[[0-9;?]*[ -\/]*[@-~]/g, ' ') // CSI(顏色/游標)
157
+ .replace(/[\x00-\x08\x0b-\x1f\x7f]/g, ' '); // 其餘控制碼 → 空白
158
+ return TRUST_PROMPT_RE.test(plain);
159
+ }