claude-cache-keepalive 0.1.4 → 0.1.6
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 +17 -1
- package/package.json +1 -1
- package/src/host.mjs +13 -4
- package/src/keepalive.mjs +8 -1
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@ It's transparent — type and use claude exactly as normal (no `Ctrl-b` prefix,
|
|
|
44
44
|
- any recent turn wrote `ephemeral_1h_input_tokens` → **1 h cache** → inject after ~58 min idle, cooldown 1 h.
|
|
45
45
|
- only `ephemeral_5m_input_tokens` (or no evidence yet) → **5 min cache** (conservative) → inject after ~4 min idle, cooldown 5 min.
|
|
46
46
|
- This survives client‑version, env‑var and server‑flag changes that the plan string can't see (e.g. a Pro account can still get a 1 h cache).
|
|
47
|
+
- **Prompt‑safe injection** — the keepalive only fires once the PTY has been **silent for a moment** (`CWARM_QUIET_MS`, default 2.5 s). A mandatory prompt (tool‑permission, `AskUserQuestion`, plan approval) keeps animating its spinner, and a busy tool‑run keeps streaming output — both are "not silent", so the keepalive won't fire into them (no accidental menu‑default selection, no interrupting a long tool‑run). And when it does fire it's **`Esc`‑prefixed**: it backs out to the input box first, so the keepalive's Enter can never land on a prompt and auto‑select. (While a prompt is genuinely blocking, the cache can't be kept warm regardless — no API turn can happen until you answer — so it simply resumes once you do.)
|
|
47
48
|
- **Focus/minimize independent** — injection is an in‑process `pty.write`, unrelated to window state. Only closing the window (ending the host process) stops it.
|
|
48
49
|
|
|
49
50
|
## Optional: cache‑countdown statusline
|
|
@@ -71,6 +72,8 @@ Environment variables (mostly for testing / advanced use):
|
|
|
71
72
|
|-----|---------|
|
|
72
73
|
| `CWARM_MSG` | keepalive message (default `hi`) |
|
|
73
74
|
| `CWARM_TICK_MS` | check interval (default `20000`) |
|
|
75
|
+
| `CWARM_QUIET_MS` | screen must be silent this long before injecting (default `2500`) |
|
|
76
|
+
| `CWARM_ESC_DELAY_MS` | gap between the `Esc` and the keepalive message (default `250`) |
|
|
74
77
|
| `CWARM_THRESHOLD_S` | override idle threshold (seconds) |
|
|
75
78
|
| `CWARM_TTL_S` | override cooldown (seconds) |
|
|
76
79
|
| `CWARM_CLAUDE` | path to the `claude` executable (otherwise auto‑detected via `which`/`where`) |
|
|
@@ -80,7 +83,8 @@ Environment variables (mostly for testing / advanced use):
|
|
|
80
83
|
|
|
81
84
|
- **No detach.** Closing the window ends the session — there's no tmux‑style detach/reattach (that would mean reimplementing a terminal multiplexer; out of scope). But minimize / background / unfocused all keep working.
|
|
82
85
|
- **Single session.** Designed for one `cwarm` session at a time.
|
|
83
|
-
- If you walk away
|
|
86
|
+
- If you walk away with a half‑typed draft and stay idle past the threshold, the keepalive's `Esc` clears the draft before sending `hi`. Rare.
|
|
87
|
+
- **A genuinely blocking prompt can't be kept warm.** While Claude Code waits on a mandatory answer, no API turn can happen, so the cache may cool during that window; warming resumes automatically once you answer.
|
|
84
88
|
|
|
85
89
|
## Platform support
|
|
86
90
|
|
|
@@ -104,17 +108,29 @@ Environment variables (mostly for testing / advanced use):
|
|
|
104
108
|
|
|
105
109
|
## Changelog
|
|
106
110
|
|
|
111
|
+
### 0.1.6
|
|
112
|
+
- **Docs:** every changelog entry now carries a Traditional Chinese version alongside the English. No code change.
|
|
113
|
+
- **文件:** 每條 changelog 現在都在英文旁附上繁體中文。無程式碼變動。
|
|
114
|
+
|
|
115
|
+
### 0.1.5
|
|
116
|
+
- **Fix:** the keepalive could fire while Claude Code was showing a **mandatory prompt** (tool‑permission, `AskUserQuestion`, plan approval). Because the injected `hi␍` ends in Enter, that Enter landed on the prompt and selected its highlighted default — e.g. **auto‑approving a tool** — instead of sending a message (the reported "can't send `hi`"). Two layers fix it: **(1)** injection now waits for the PTY to be **quiet** (`CWARM_QUIET_MS`, default 2.5 s) — an animating prompt and a busy tool‑run both keep emitting output, so the keepalive no longer fires into either (this also stops it interrupting a long tool‑run, which the transcript‑mtime idle timer can't see); **(2)** the keepalive is now **`Esc`‑prefixed** (`CWARM_ESC_DELAY_MS` gap, default 250 ms) — it backs out of any prompt to the input box before sending `hi`, so the Enter can never select a menu default. Investigated empirically: a pending tool turn isn't written to the transcript while blocked (so transcript inspection can't detect this state), but the screen reliably distinguishes idle (silent) from prompt/busy (animating). While a prompt is genuinely blocking the cache can't be kept warm regardless; warming resumes once you answer.
|
|
117
|
+
- **修正:** keepalive 可能在 Claude Code 跳出**必答提示**(工具權限、`AskUserQuestion`、計畫批准)時觸發。因為注入的 `hi` 訊息以 Enter 結尾,那個 Enter 會落在提示上、選中反白的預設項——例如**自動核准某個工具**——而不是送出訊息(就是你回報的「送不出 `hi`」)。兩層修正:**(1)** 注入前先等 PTY **靜止**(`CWARM_QUIET_MS`,預設 2.5 秒)——提示在動、忙著跑工具/生成時都會持續輸出,所以 keepalive 不會再送進這兩種狀態(也順帶不會打斷長時間的工具執行,那是 transcript mtime 閒置計時看不到的);**(2)** keepalive 現在會**先送 `Esc`**(`CWARM_ESC_DELAY_MS` 間隔,預設 250 毫秒)——先退出任何提示、回到輸入框再送 `hi`,那個 Enter 就絕不會選到選單預設項。實測發現:卡住時那個 pending 的工具回合還沒被寫進 transcript(所以查 transcript 偵測不到這個狀態),但畫面能可靠分辨閒置(靜止)與提示/忙碌(在動)。提示真的卡住時 cache 本來就無法保溫;你回答後會自動恢復保溫。
|
|
118
|
+
|
|
107
119
|
### 0.1.4
|
|
108
120
|
- **Fix:** the terminal could be left unusable after `/exit` or Ctrl‑C (keystrokes garbled / no usable input). The PTY host now restores the terminal on every exit path: it emits an explicit reset (disabling alt‑screen, bracketed‑paste, mouse, cursor‑hide, and — critically on Windows — `win32‑input‑mode` `?9001` and focus‑reporting `?1004`, which otherwise make the shell receive keystrokes as unparseable `ESC[…_` packets) and flushes stdout before exiting. Adds a `SIGINT` handler that forwards `0x03` to claude instead of letting the host be killed before cleanup, plus `SIGHUP`/`exit` safety restores.
|
|
121
|
+
- **修正:** `/exit` 或 Ctrl-C 之後終端可能變得不能用(鍵盤輸入亂碼/打不了字)。PTY host 現在會在每條退出路徑都還原終端:主動送出一段明確的重置序列(關掉 alt-screen、bracketed-paste、滑鼠、隱藏游標,以及——在 Windows 上最關鍵的——`win32-input-mode` `?9001` 與 focus-reporting `?1004`,否則 shell 會把每個鍵碼當成無法解析的 `ESC[…_` 封包收下),並在退出前把 stdout flush 掉。新增 `SIGINT` handler 把 `0x03` 轉送給 claude,而不是讓 host 在清理前就被殺掉;另加 `SIGHUP`/`exit` 的保險還原。
|
|
109
122
|
|
|
110
123
|
### 0.1.3
|
|
111
124
|
- **Change:** the cache TTL is now **measured from the transcript** (`message.usage.cache_creation`'s `ephemeral_1h` / `ephemeral_5m` tokens) instead of being guessed from your subscription plan. A recent 1h write → 1h regime; only 5m writes (or no evidence) → 5m regime (conservative). This drops the `~/.claude/.credentials.json` read entirely and is correct even when a Pro account gets a 1h cache. Adds `transcriptPath` / `readTtlRegime` / `detectTtlRegime` / `regimeParams`; removes `detectPlan` / `planParams`.
|
|
125
|
+
- **變更:** cache TTL 現在**直接從 transcript 實測**(`message.usage.cache_creation` 裡的 `ephemeral_1h`/`ephemeral_5m` token),不再用你的訂閱方案去猜。最近有任一回合寫過 1h → 1h 檔位;只有 5m 寫入(或還沒有證據)→ 5m 檔位(保守)。這完全拿掉了對 `~/.claude/.credentials.json` 的讀取,連 Pro 帳號拿到 1h cache 的情況也判得對。新增 `transcriptPath`/`readTtlRegime`/`detectTtlRegime`/`regimeParams`;移除 `detectPlan`/`planParams`。
|
|
112
126
|
|
|
113
127
|
### 0.1.2
|
|
114
128
|
- **Fix:** idle is now measured from the newest transcript file's mtime — i.e. time since your last *message* — instead of keystrokes. Scrolling, arrow‑key reading, or a half‑typed prompt no longer reset the idle timer, so the keepalive actually fires while you're reading and the cache stops going cold. Adds `encodeProjectDir` / `transcriptMtimeMs` / `transcriptIdleMs`.
|
|
129
|
+
- **修正:** 閒置現在改用最新 transcript 檔的 mtime 來計算——也就是距你上次*發訊息*多久——而不是看鍵盤輸入。捲動、用方向鍵讀回覆、或打到一半還沒送出,都不會再重置閒置計時,所以 keepalive 會在你閱讀時照常觸發、cache 不再冷掉。新增 `encodeProjectDir`/`transcriptMtimeMs`/`transcriptIdleMs`。
|
|
115
130
|
|
|
116
131
|
### 0.1.0
|
|
117
132
|
- Initial release. (0.1.1 was a version‑only bump and was never published to npm.)
|
|
133
|
+
- **首次發佈。**(0.1.1 只是純版本號 bump,從未發佈到 npm。)
|
|
118
134
|
|
|
119
135
|
## License
|
|
120
136
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-cache-keepalive",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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/host.mjs
CHANGED
|
@@ -53,7 +53,10 @@ export function startHost(opts = {}) {
|
|
|
53
53
|
if (process.stdin.isTTY) { try { process.stdin.setRawMode(true); } catch {} }
|
|
54
54
|
process.stdin.resume();
|
|
55
55
|
process.stdin.on('data', (d) => { ptyProc.write(d); });
|
|
56
|
-
|
|
56
|
+
// 追蹤 claude 最近一次有輸出到畫面的時刻:閒置在輸入框時畫面靜止;提示等待回答時 spinner 在動、
|
|
57
|
+
// 生成/跑工具時持續輸出。注入前要求畫面已靜止一段時間(見下方 quietMs),就能避開「忙/卡」狀態。
|
|
58
|
+
let lastOutputMs = Date.now();
|
|
59
|
+
ptyProc.onData((d) => { lastOutputMs = Date.now(); process.stdout.write(d); });
|
|
57
60
|
process.stdout.on('resize', () => {
|
|
58
61
|
try { ptyProc.resize(process.stdout.columns || 80, process.stdout.rows || 24); } catch {}
|
|
59
62
|
});
|
|
@@ -61,6 +64,8 @@ export function startHost(opts = {}) {
|
|
|
61
64
|
// ---- 內建保溫 ----
|
|
62
65
|
const tickMs = Number(process.env.CWARM_TICK_MS) || 20_000;
|
|
63
66
|
const msg = process.env.CWARM_MSG || opts.msg || 'hi';
|
|
67
|
+
const quietMs = Number(process.env.CWARM_QUIET_MS) || 2500; // 畫面需靜止這麼久才注入
|
|
68
|
+
const escDelayMs = Number(process.env.CWARM_ESC_DELAY_MS) || 250; // Esc 與訊息之間的間隔
|
|
64
69
|
const overrides = {};
|
|
65
70
|
const thr = process.env.CWARM_THRESHOLD_S ?? opts.thresholdS;
|
|
66
71
|
const ttlO = process.env.CWARM_TTL_S ?? opts.ttlS;
|
|
@@ -74,11 +79,15 @@ export function startHost(opts = {}) {
|
|
|
74
79
|
const { ttl, idleThreshold } = regimeParams(regime, overrides);
|
|
75
80
|
const now = Date.now();
|
|
76
81
|
const idleMs = transcriptIdleMs(claudeDir, cwd, now);
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
const screenIdleMs = now - lastOutputMs;
|
|
83
|
+
if (decideInject({ now, idleMs, lastFire, idleThreshold, ttl, disabled: fs.existsSync(DISABLE), screenIdleMs, quietMs })) {
|
|
84
|
+
// 先送 Esc:把任何「必答」modal(權限/選單/計畫批准)收掉、退回輸入框,後面那個 Enter 才不會誤選預設項;
|
|
85
|
+
// 空輸入框時 Esc 等同 no-op。隔一小段再送訊息——讓 claude 先把 modal 收乾淨,也避免 ESC 與字元被併成 Meta 鍵。
|
|
86
|
+
ptyProc.write('\x1b');
|
|
79
87
|
lastFire = now;
|
|
88
|
+
setTimeout(() => { if (!exiting) { try { ptyProc.write(msg + '\r'); } catch {} } }, escDelayMs);
|
|
80
89
|
const idle = idleMs == null ? -1 : Math.round(idleMs / 1000);
|
|
81
|
-
try { fs.appendFileSync(LOG, `${new Date().toISOString()} inject "${msg}" regime=${regime ?? 'unknown'} idle=${idle}s\n`); } catch {}
|
|
90
|
+
try { fs.appendFileSync(LOG, `${new Date().toISOString()} inject "${msg}" regime=${regime ?? 'unknown'} idle=${idle}s screenIdle=${Math.round(screenIdleMs / 1000)}s\n`); } catch {}
|
|
82
91
|
}
|
|
83
92
|
}, tickMs);
|
|
84
93
|
|
package/src/keepalive.mjs
CHANGED
|
@@ -127,10 +127,17 @@ export function detectTtlRegime(claudeDir, cwd, opts) {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// 純決策:現在該不該注入 keepalive?idleMs = 距上次訊息多久(由 transcriptIdleMs 算)。
|
|
130
|
-
|
|
130
|
+
// screenIdleMs = 距 claude 最近一次「畫面輸出」多久;quietMs = 需靜止多久才放行。
|
|
131
|
+
// 為什麼要這個畫面靜默門檻:transcript 在「等你回答必答提示(權限/選單/計畫批准)」與
|
|
132
|
+
// 「跑長工具/生成中」時都不會更新,光看 idleMs 無法分辨這兩種「忙/卡」狀態與「真的閒置在輸入框」。
|
|
133
|
+
// 但畫面活動可以:閒置在輸入框時畫面靜止;提示等待時 spinner 在動、生成中持續輸出。
|
|
134
|
+
// 故只有畫面靜止夠久才注入——避免把 hi 的 Enter 送進 modal 誤選預設項,也避免打斷長工具執行。
|
|
135
|
+
// quietMs 省略(null)時不套此門檻(保持純 idle 決策,供既有測試/呼叫者使用)。
|
|
136
|
+
export function decideInject({ now, idleMs, lastFire, idleThreshold, ttl, disabled, screenIdleMs, quietMs }) {
|
|
131
137
|
if (disabled) return false; // 暫停開關
|
|
132
138
|
if (idleMs == null) return false; // 找不到 transcript → 保守不發
|
|
133
139
|
if (idleMs < idleThreshold * 1000) return false; // 距上次訊息還不夠久
|
|
134
140
|
if (now - lastFire < ttl * 1000) return false; // 冷卻未滿一個 TTL
|
|
141
|
+
if (quietMs != null && screenIdleMs != null && screenIdleMs < quietMs) return false; // 畫面還在動(提示/生成/打字中)
|
|
135
142
|
return true;
|
|
136
143
|
}
|