claude-cache-keepalive 0.1.7 → 0.1.10
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 +14 -0
- package/package.json +1 -1
- package/src/cli.mjs +3 -0
- package/src/host.mjs +36 -2
- package/src/keepalive.mjs +53 -0
- package/src/statusline/segment.mjs +3 -1
package/README.md
CHANGED
|
@@ -76,6 +76,7 @@ Environment variables (mostly for testing / advanced use):
|
|
|
76
76
|
| `CWARM_ESC_DELAY_MS` | gap between the `Esc` and the keepalive message (default `250`) |
|
|
77
77
|
| `CWARM_THRESHOLD_S` | override idle threshold (seconds) |
|
|
78
78
|
| `CWARM_TTL_S` | override cooldown (seconds) |
|
|
79
|
+
| `CWARM_BILLING` | force billing mode: `subscription` (keep warming) or `credits` (suspend); otherwise auto‑detected |
|
|
79
80
|
| `CWARM_CLAUDE` | path to the `claude` executable (otherwise auto‑detected via `which`/`where`) |
|
|
80
81
|
| `CLAUDE_CONFIG_DIR` | Claude config dir (default `~/.claude`) |
|
|
81
82
|
|
|
@@ -126,6 +127,7 @@ Environment variables (mostly for testing / advanced use):
|
|
|
126
127
|
| `CWARM_ESC_DELAY_MS` | `Esc` 與訊息之間的間隔(預設 `250`) |
|
|
127
128
|
| `CWARM_THRESHOLD_S` | 覆寫閒置門檻(秒) |
|
|
128
129
|
| `CWARM_TTL_S` | 覆寫冷卻(秒) |
|
|
130
|
+
| `CWARM_BILLING` | 強制指定計費模式:`subscription`(照常保溫)或 `credits`(暫停);否則自動偵測 |
|
|
129
131
|
| `CWARM_CLAUDE` | `claude` 執行檔路徑(否則用 `which`/`where` 自動偵測) |
|
|
130
132
|
| `CLAUDE_CONFIG_DIR` | Claude 設定目錄(預設 `~/.claude`) |
|
|
131
133
|
|
|
@@ -137,6 +139,18 @@ Environment variables (mostly for testing / advanced use):
|
|
|
137
139
|
|
|
138
140
|
## Changelog
|
|
139
141
|
|
|
142
|
+
### 0.1.10
|
|
143
|
+
- **Fix:** the optional statusline segment now reflects the 0.1.9 billing guard. On credits/API billing the keepalive is suspended, but the `♻️ cache …` countdown kept ticking as if warming were still active — misleading. The segment now shows `⏸️ cwarm off (API)` while `detectBillingMode` reports credits, and returns to the normal countdown once you're back on a subscription account.
|
|
144
|
+
- **修正:** 選配的 statusline 區段現在會反映 0.1.9 的計費防護。credits/API 計費時 keepalive 已暫停,但 `♻️ cache …` 倒數仍照跑,彷彿還在保溫——會造成誤導。現在 `detectBillingMode` 回報 credits 期間會改顯示 `⏸️ cwarm off (API)`,切回訂閱帳號後自動恢復正常倒數。
|
|
145
|
+
|
|
146
|
+
### 0.1.9
|
|
147
|
+
- **Fix:** keepalive now **auto-suspends on credits/API billing**. If you `/login` into an Anthropic Console account (credits usage) — or run purely on `ANTHROPIC_API_KEY` — every injected `hi` and every cache refresh costs real money, so warming the cache no longer makes sense (on a subscription it only spends rate-limit quota). cwarm now detects the billing mode each tick (from `~/.claude/.credentials.json`'s `claudeAiOauth.subscriptionType`, falling back to `~/.claude.json`'s `oauthAccount.billingType`, then the `ANTHROPIC_API_KEY` env var) and skips injection while on credits, logging `skip: credits/API billing detected` once; switching back to a subscription account mid-session resumes warming automatically. Override with `CWARM_BILLING=subscription|credits` if detection guesses wrong. Adds `billingModeFromSources` / `detectBillingMode`.
|
|
148
|
+
- **修正:** keepalive 現在會在 **credits/API 計費時自動暫停**。若你用 `/login` 切到 Anthropic Console 帳號(credits usage),或純靠 `ANTHROPIC_API_KEY` 執行,每次注入的 `hi` 與每次 cache 續寫都是實際花錢,保溫就失去意義(訂閱制下花的只是額度)。cwarm 現在每個 tick 偵測計費模式(先看 `~/.claude/.credentials.json` 的 `claudeAiOauth.subscriptionType`,再退回 `~/.claude.json` 的 `oauthAccount.billingType`,最後看 `ANTHROPIC_API_KEY` 環境變數),credits 期間跳過注入並記錄一次 `skip: credits/API billing detected`;session 中切回訂閱帳號會自動恢復保溫。偵測誤判可用 `CWARM_BILLING=subscription|credits` 強制指定。新增 `billingModeFromSources`/`detectBillingMode`。
|
|
149
|
+
|
|
150
|
+
### 0.1.8
|
|
151
|
+
- **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`.
|
|
152
|
+
- **修正:** 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`。
|
|
153
|
+
|
|
140
154
|
### 0.1.7
|
|
141
155
|
- **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.)
|
|
142
156
|
- **變更:** `cwarm` 不再隱含補上 `--continue`,改為完全透傳——`cwarm [參數]` 就等於 `claude [參數]`,所以單獨打 `cwarm` 會開全新 session。要接續上次請打 `cwarm --continue`。(先前單獨打 `cwarm` 會自動接續。)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-cache-keepalive",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
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
|
@@ -32,6 +32,9 @@ Everything except the 'setup' subcommand is passed straight to claude
|
|
|
32
32
|
Keepalive only fires after you've been idle past the cache-TTL threshold.
|
|
33
33
|
The TTL is auto-detected from the transcript's cache_creation (1h cache ->
|
|
34
34
|
fire after ~58min idle; 5m cache -> ~4min), not guessed from your plan.
|
|
35
|
+
Keepalive auto-suspends when you're on credits/API billing (Console account
|
|
36
|
+
via /login, or ANTHROPIC_API_KEY) — injections would cost real money there.
|
|
37
|
+
Override with CWARM_BILLING=subscription|credits if detection is wrong.
|
|
35
38
|
Pause anytime: touch ~/.claude/cwarm.disabled
|
|
36
39
|
Log: ~/.claude/cwarm-keepalive.log
|
|
37
40
|
`);
|
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, detectBillingMode } from './keepalive.mjs';
|
|
9
9
|
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
11
|
const isWin = process.platform === 'win32';
|
|
@@ -58,7 +58,14 @@ export function startHost(opts = {}) {
|
|
|
58
58
|
// 追蹤 claude 最近一次有輸出到畫面的時刻:閒置在輸入框時畫面靜止;提示等待回答時 spinner 在動、
|
|
59
59
|
// 生成/跑工具時持續輸出。注入前要求畫面已靜止一段時間(見下方 quietMs),就能避開「忙/卡」狀態。
|
|
60
60
|
let lastOutputMs = Date.now();
|
|
61
|
-
|
|
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
|
+
});
|
|
62
69
|
process.stdout.on('resize', () => {
|
|
63
70
|
try { ptyProc.resize(process.stdout.columns || 80, process.stdout.rows || 24); } catch {}
|
|
64
71
|
});
|
|
@@ -75,7 +82,34 @@ export function startHost(opts = {}) {
|
|
|
75
82
|
if (ttlO != null) overrides.ttl = Number(ttlO);
|
|
76
83
|
|
|
77
84
|
let lastFire = 0;
|
|
85
|
+
let trustGuardLogged = false;
|
|
86
|
+
let creditsGuardLogged = false;
|
|
78
87
|
const timer = setInterval(() => {
|
|
88
|
+
// credits / API 計費(/login 切到 Console 帳號、或只用 ANTHROPIC_API_KEY)時,每次注入
|
|
89
|
+
// 都是實際花錢,保溫沒有意義 → 自動暫停。每個 tick 重新偵測,session 中 /login 切回
|
|
90
|
+
// 訂閱帳號會自動恢復保溫。要強制指定:CWARM_BILLING=subscription|credits。
|
|
91
|
+
const billing = detectBillingMode(claudeDir);
|
|
92
|
+
if (billing === 'credits') {
|
|
93
|
+
if (!creditsGuardLogged) {
|
|
94
|
+
creditsGuardLogged = true;
|
|
95
|
+
try { fs.appendFileSync(LOG, `${new Date().toISOString()} skip: credits/API billing detected — keepalive suspended (CWARM_BILLING=subscription to override)\n`); } catch {}
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (creditsGuardLogged) {
|
|
100
|
+
creditsGuardLogged = false;
|
|
101
|
+
try { fs.appendFileSync(LOG, `${new Date().toISOString()} resume: subscription billing detected — keepalive re-enabled\n`); } catch {}
|
|
102
|
+
}
|
|
103
|
+
// 信任對話框在畫面上時,這輪完全不動作(連 Esc 都不送)——那是使用者本人該回答的框,被保溫
|
|
104
|
+
// Esc 掉會留下 hasTrustDialogAccepted:false,害該資料夾 settings.local.json 權限失效、之後不再跳框。
|
|
105
|
+
if (looksLikeTrustPrompt(screenBuf)) {
|
|
106
|
+
if (!trustGuardLogged) {
|
|
107
|
+
trustGuardLogged = true;
|
|
108
|
+
try { fs.appendFileSync(LOG, `${new Date().toISOString()} skip: trust dialog on screen — left for the user to answer\n`); } catch {}
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
trustGuardLogged = false;
|
|
79
113
|
const cwd = process.cwd();
|
|
80
114
|
const regime = detectTtlRegime(claudeDir, cwd); // 從 transcript 實測 1h/5m,不再猜方案
|
|
81
115
|
const { ttl, idleThreshold } = regimeParams(regime, overrides);
|
package/src/keepalive.mjs
CHANGED
|
@@ -126,6 +126,43 @@ export function detectTtlRegime(claudeDir, cwd, opts) {
|
|
|
126
126
|
return readTtlRegime(transcriptPath(claudeDir, cwd), opts);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// ---- 計費模式偵測(subscription vs credits/API)----
|
|
130
|
+
// 保溫只在「訂閱制」下划算:額度是 rate limit,cache 保溫省的是額度。若 /login 切到
|
|
131
|
+
// Console 帳號(credits / API 計費),每次注入的 "hi" 與 cache 續寫都是實際花錢,
|
|
132
|
+
// 保溫反而燒錢 → 偵測到 credits 就自動暫停注入。
|
|
133
|
+
// 判斷順序:CWARM_BILLING 強制指定 → .credentials.json 的 claudeAiOauth.subscriptionType
|
|
134
|
+
// (/login 當下就會改寫,最即時)→ .claude.json 的 oauthAccount.billingType →
|
|
135
|
+
// ANTHROPIC_API_KEY 環境變數 → null(無法判斷,維持現行行為照常保溫)。
|
|
136
|
+
export function billingModeFromSources({ env = {}, credentials = null, config = null } = {}) {
|
|
137
|
+
const forced = String(env.CWARM_BILLING || '').toLowerCase();
|
|
138
|
+
if (forced === 'subscription' || forced === 'credits') return forced;
|
|
139
|
+
const oauth = credentials && credentials.claudeAiOauth;
|
|
140
|
+
if (oauth && typeof oauth === 'object') {
|
|
141
|
+
const sub = oauth.subscriptionType;
|
|
142
|
+
if (typeof sub === 'string' && /^(pro|max|team|enterprise)/i.test(sub)) return 'subscription';
|
|
143
|
+
return 'credits'; // 有 OAuth 登入但沒有訂閱層級 → Console 帳號(credits 計費)
|
|
144
|
+
}
|
|
145
|
+
const billing = config && config.oauthAccount && config.oauthAccount.billingType;
|
|
146
|
+
if (billing === 'stripe_subscription') return 'subscription';
|
|
147
|
+
if (typeof billing === 'string' && billing) return 'credits';
|
|
148
|
+
if (env.ANTHROPIC_API_KEY) return 'credits'; // 沒有任何登入痕跡、只有 API key → 純 API 計費
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readJsonSafe(p) {
|
|
153
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// IO 版:讀 claudeDir 下的 .credentials.json 與 .claude.json(CLAUDE_CONFIG_DIR 佈局),
|
|
157
|
+
// 後者找不到再退回 homedir 的 ~/.claude.json(預設佈局)。macOS 憑證在 Keychain、
|
|
158
|
+
// 沒有 .credentials.json 檔,會自然落到 config 判斷。
|
|
159
|
+
export function detectBillingMode(claudeDir, { env = process.env, homedir = os.homedir() } = {}) {
|
|
160
|
+
const credentials = readJsonSafe(path.join(claudeDir, '.credentials.json'));
|
|
161
|
+
const config = readJsonSafe(path.join(claudeDir, '.claude.json'))
|
|
162
|
+
?? readJsonSafe(path.join(homedir, '.claude.json'));
|
|
163
|
+
return billingModeFromSources({ env, credentials, config });
|
|
164
|
+
}
|
|
165
|
+
|
|
129
166
|
// 純決策:現在該不該注入 keepalive?idleMs = 距上次訊息多久(由 transcriptIdleMs 算)。
|
|
130
167
|
// screenIdleMs = 距 claude 最近一次「畫面輸出」多久;quietMs = 需靜止多久才放行。
|
|
131
168
|
// 為什麼要這個畫面靜默門檻:transcript 在「等你回答必答提示(權限/選單/計畫批准)」與
|
|
@@ -141,3 +178,19 @@ export function decideInject({ now, idleMs, lastFire, idleThreshold, ttl, disabl
|
|
|
141
178
|
if (quietMs != null && screenIdleMs != null && screenIdleMs < quietMs) return false; // 畫面還在動(提示/生成/打字中)
|
|
142
179
|
return true;
|
|
143
180
|
}
|
|
181
|
+
|
|
182
|
+
// 畫面上是否正顯示 Claude Code 的「資料夾信任」對話框("Do you trust the files in this folder?")。
|
|
183
|
+
// 保溫的「Esc 先行」是用來收掉一般必答 modal(權限/選單/計畫批准),但信任框特殊:被 Esc 掉會在
|
|
184
|
+
// ~/.claude.json 寫下 hasTrustDialogAccepted:false,使該資料夾的 .claude/settings.local.json 權限
|
|
185
|
+
// 整批失效、且之後不再自動跳框。這種框只能由使用者本人回答——偵測到就整輪跳過注入(連 Esc 都不送)。
|
|
186
|
+
// 傳入的是原始終端輸出(含 ANSI),先剝掉控制序列再比對,避免顏色/游標碼把字拆開。
|
|
187
|
+
const TRUST_PROMPT_RE = /trust\s+the\s+files\s+in\s+this\s+(?:folder|workspace)/i;
|
|
188
|
+
export function looksLikeTrustPrompt(screenText) {
|
|
189
|
+
if (!screenText) return false;
|
|
190
|
+
const plain = String(screenText)
|
|
191
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, ' ') // OSC …(BEL 或 ST 結尾)
|
|
192
|
+
.replace(/\x1b[@-Z\\-_]/g, ' ') // 兩位元組跳脫
|
|
193
|
+
.replace(/\x1b\[[0-9;?]*[ -\/]*[@-~]/g, ' ') // CSI(顏色/游標)
|
|
194
|
+
.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, ' '); // 其餘控制碼 → 空白
|
|
195
|
+
return TRUST_PROMPT_RE.test(plain);
|
|
196
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
|
-
import { defaultClaudeDir, readTtlRegime, regimeParams } from '../keepalive.mjs';
|
|
7
|
+
import { defaultClaudeDir, readTtlRegime, regimeParams, detectBillingMode } from '../keepalive.mjs';
|
|
8
8
|
|
|
9
9
|
const claudeDir = defaultClaudeDir();
|
|
10
10
|
const ORIG = path.join(claudeDir, 'cwarm-statusline-orig.json');
|
|
@@ -15,6 +15,8 @@ function readStdin() {
|
|
|
15
15
|
|
|
16
16
|
// cache 倒數段:用 transcript mtime 當 idle、用 transcript 實測的 cache_creation 判 TTL(1h / 5m)。
|
|
17
17
|
function cacheSegment(payload) {
|
|
18
|
+
// credits/API 計費時 keepalive 已暫停,倒數沒有意義且會誤導 → 顯示暫停標記
|
|
19
|
+
if (detectBillingMode(claudeDir) === 'credits') return '⏸️ cwarm off (API)';
|
|
18
20
|
const tp = payload?.transcript_path;
|
|
19
21
|
if (!tp) return '';
|
|
20
22
|
let mtimeMs;
|