claude-token-saver 2.10.1 → 2.11.0
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 +28 -0
- package/bin/cli.js +58 -2
- package/package.json +1 -1
- package/src/harness.js +94 -0
package/README.md
CHANGED
|
@@ -134,6 +134,31 @@ claude-token-saver mode # 현재 설정 확인
|
|
|
134
134
|
|
|
135
135
|
전체 옵션은 `--help` 또는 [영문 README](./README.en.md#options).
|
|
136
136
|
|
|
137
|
+
## 🅷 Harness 모드
|
|
138
|
+
|
|
139
|
+
다섯 가지 원칙(Ratchet, Evidence, PEV, Structured Task, Default Safe Path)을 한 줄 명령으로 `CLAUDE.md`에 셋업하고, statusline에 `🅷 5/5`로 점수화합니다. 같은 에러가 반복되면 `🅷⚠ ratchet?`로 알림이 뜹니다.
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
claude-token-saver harness init # CLAUDE.md(5섹션) + .claude/ratchet.md
|
|
143
|
+
claude-token-saver harness check # 현재 점수
|
|
144
|
+
claude-token-saver harness promote <N> # statusline 경고 #N → ratchet에 한 줄 등록
|
|
145
|
+
claude-token-saver harness list # 등록된 ratchet 룰 번호 매겨 보기
|
|
146
|
+
claude-token-saver harness rm <N> # 룰 삭제 (자동 .bak 백업)
|
|
147
|
+
claude-token-saver harness uninit # harness 블록 제거 (CLAUDE.md 다른 내용은 보존)
|
|
148
|
+
claude-token-saver harness off | on # statusline 🅷 표시 토글
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### ⚠️ 주의 — `harness rm`은 신중하게
|
|
152
|
+
|
|
153
|
+
ratchet의 가치는 **"한 방향 누적"**에 있습니다. 룰을 가볍게 지우기 시작하면 같은 실수가 다시 새기 시작합니다. **지우기 전에 다음을 확인하세요**:
|
|
154
|
+
|
|
155
|
+
- **룰이 너무 광범위해서 정상 케이스도 막나?** → ❌ 삭제 ✅ **조건을 좁혀서 다듬기**
|
|
156
|
+
- 예: `"하드코딩 금지"` → `"테스트 외 코드에서 하드코딩 금지"`
|
|
157
|
+
- **룰이 너무 좁아 거의 발동 안 되나?** → ❌ 삭제 ✅ **그냥 두기** (비용 0)
|
|
158
|
+
- **정말 잘못된 룰이라 확신?** → ✅ **그때만 삭제**
|
|
159
|
+
|
|
160
|
+
대부분의 "과도한 ratchet" 문제는 **룰의 표현이 좁지 못해서** 생깁니다. 삭제는 마지막 수단으로 두고, 먼저 `.claude/ratchet.md`를 직접 열어 조건을 다듬는 쪽을 우선하세요. 삭제 시 자동 `.bak`이 남지만, **세션 컨텍스트(왜 그 룰이 박혔는지)는 백업으로 복원되지 않습니다**.
|
|
161
|
+
|
|
137
162
|
## 토큰 급증 원인 코드
|
|
138
163
|
|
|
139
164
|
| 코드 | 의미 |
|
|
@@ -169,6 +194,9 @@ Node.js ≥ 18 · macOS / Linux / Windows / WSL · 의존성 0.
|
|
|
169
194
|
|
|
170
195
|
## 릴리스 노트
|
|
171
196
|
|
|
197
|
+
### v2.11.0 (2026-05-02)
|
|
198
|
+
- `harness list` / `harness rm <N>` 추가. 등록된 ratchet 룰을 번호로 보고 개별 삭제 가능 (자동 `.bak` 백업). 삭제 전 "조건을 좁혀서 다듬기" 우선 검토 안내가 CLI에 표시됩니다. README의 [⚠️ 주의 — `harness rm`은 신중하게](#️-주의--harness-rm은-신중하게) 항목 참고.
|
|
199
|
+
|
|
172
200
|
### v2.9.4 (2026-04-27)
|
|
173
201
|
- README에 Node.js 사전 설치 안내 추가 (macOS/Windows/Linux별). GitHub에서 처음 본 사용자가 npm 명령부터 막히는 일을 방지. sudo 글로벌 설치 시 postinstall이 root 홈에 SKILL을 만드는 함정도 함께 안내.
|
|
174
202
|
|
package/bin/cli.js
CHANGED
|
@@ -446,12 +446,13 @@ async function main() {
|
|
|
446
446
|
|
|
447
447
|
// Subcommand: harness — manage the project's CLAUDE.md harness rules.
|
|
448
448
|
// claude-token-saver harness init # write CLAUDE.md (5 sections) + ratchet.md
|
|
449
|
+
// claude-token-saver harness uninit # remove harness block from CLAUDE.md (backup kept)
|
|
449
450
|
// claude-token-saver harness check # show 🅷 N/5 + which sections are missing
|
|
450
451
|
// claude-token-saver harness promote "<rule>" # append a rule to ratchet.md
|
|
451
452
|
// claude-token-saver harness off | on # toggle the statusline 🅷 segment
|
|
452
453
|
if (args[0] === 'harness') {
|
|
453
454
|
const sub = args[1];
|
|
454
|
-
const { harnessInit, harnessStatus, harnessPromote, findProjectRoot } =
|
|
455
|
+
const { harnessInit, harnessUninit, harnessStatus, harnessPromote, harnessListRules, harnessRmRule, findProjectRoot } =
|
|
455
456
|
await import('../src/harness.js');
|
|
456
457
|
const { HARNESS_SECTIONS } = await import('../src/harness-templates.js');
|
|
457
458
|
const { loadConfig, saveConfig } = await import('../src/config.js');
|
|
@@ -557,6 +558,61 @@ async function main() {
|
|
|
557
558
|
return;
|
|
558
559
|
}
|
|
559
560
|
|
|
561
|
+
if (sub === 'uninit' || sub === 'remove') {
|
|
562
|
+
const purgeRatchet = args.includes('--purge-ratchet');
|
|
563
|
+
const r = harnessUninit({ purgeRatchet });
|
|
564
|
+
console.log(`Project root: ${r.root}`);
|
|
565
|
+
r.removed.forEach((f) => console.log(` removed: ${f}`));
|
|
566
|
+
r.backedUp.forEach((f) => console.log(` backup: ${f}`));
|
|
567
|
+
r.skipped.forEach((f) => console.log(` skip: ${f}`));
|
|
568
|
+
if (r.removed.length === 0) console.log('Nothing to remove.');
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (sub === 'list' || sub === 'ls') {
|
|
573
|
+
const { path, rules } = harnessListRules();
|
|
574
|
+
if (!rules.length) {
|
|
575
|
+
console.log(`No ratchet rules in ${path}`);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
console.log(`📋 Ratchet rules — ${path}\n`);
|
|
579
|
+
for (const r of rules) console.log(` #${r.index} ${r.text}`);
|
|
580
|
+
console.log('\nRemove with: claude-token-saver harness rm <N>');
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (sub === 'rm') {
|
|
585
|
+
const raw = (args[2] || '').trim();
|
|
586
|
+
if (!/^\d+$/.test(raw)) {
|
|
587
|
+
console.error('Usage: claude-token-saver harness rm <N> # N from `harness list`');
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
const n = parseInt(raw, 10);
|
|
591
|
+
// ⚠️ Heads-up before deletion. Ratchet's value is one-way accumulation —
|
|
592
|
+
// dropping a rule is sometimes right, but more often the rule is just
|
|
593
|
+
// too narrow. Surface the alternative loudly here.
|
|
594
|
+
console.log('⚠️ 주의: ratchet 룰 삭제는 신중하게.');
|
|
595
|
+
console.log(' 같은 실수가 또 발생할 가능성이 큽니다. 보통은 "조건이 너무 좁아서"');
|
|
596
|
+
console.log(' 문제가 되는 경우가 많아요. 지우기 전에 한 번 더 검토하세요:');
|
|
597
|
+
console.log(' - 룰이 너무 광범위해서 정상 케이스도 막나? → 조건을 좁혀서 다듬기');
|
|
598
|
+
console.log(' - 룰이 너무 좁아서 거의 발동 안 되나? → 그냥 두기 (비용 0)');
|
|
599
|
+
console.log(' - 정말 잘못된 룰이라 확신? → 그때만 삭제');
|
|
600
|
+
console.log('');
|
|
601
|
+
const r = harnessRmRule(n);
|
|
602
|
+
if (!r.ok) {
|
|
603
|
+
console.error(`❌ ${r.error}`);
|
|
604
|
+
if (r.rules) {
|
|
605
|
+
console.error('Available:');
|
|
606
|
+
for (const x of r.rules) console.error(` #${x.index} ${x.text}`);
|
|
607
|
+
}
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
console.log(`✅ Removed #${n}: ${r.removed.text}`);
|
|
611
|
+
console.log(` Backup: ${r.backup}`);
|
|
612
|
+
console.log(` 복구: cp "${r.backup}" "${r.path}"`);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
560
616
|
if (sub === 'off' || sub === 'on') {
|
|
561
617
|
const cfg = loadConfig();
|
|
562
618
|
cfg.harness = cfg.harness || {};
|
|
@@ -567,7 +623,7 @@ async function main() {
|
|
|
567
623
|
}
|
|
568
624
|
|
|
569
625
|
console.error(`Unknown harness subcommand: ${sub}`);
|
|
570
|
-
console.error('Usage: claude-token-saver harness [check|init|promote "<rule>"|off|on]');
|
|
626
|
+
console.error('Usage: claude-token-saver harness [check|init|uninit [--purge-ratchet]|promote "<rule>"|list|rm <N>|off|on]');
|
|
571
627
|
process.exit(1);
|
|
572
628
|
}
|
|
573
629
|
|
package/package.json
CHANGED
package/src/harness.js
CHANGED
|
@@ -154,6 +154,54 @@ export function harnessInit({ root = findProjectRoot(), force = false } = {}) {
|
|
|
154
154
|
return result;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* harness uninit — remove the harness block from CLAUDE.md (preserves the
|
|
159
|
+
* user's other content). A safety backup is written first. ratchet.md is
|
|
160
|
+
* left intact (user-grown rules) unless `purgeRatchet` is true.
|
|
161
|
+
*
|
|
162
|
+
* Returns { removed: [], backedUp: [], skipped: [] }.
|
|
163
|
+
*/
|
|
164
|
+
export function harnessUninit({ root = findProjectRoot(), purgeRatchet = false } = {}) {
|
|
165
|
+
const cmPath = claudeMdPath(root);
|
|
166
|
+
const rmPath = ratchetMdPath(root);
|
|
167
|
+
const result = { removed: [], backedUp: [], skipped: [], root };
|
|
168
|
+
|
|
169
|
+
if (existsSync(cmPath)) {
|
|
170
|
+
const existing = readFileSync(cmPath, 'utf8');
|
|
171
|
+
if (existing.includes(HARNESS_BLOCK_BEGIN)) {
|
|
172
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
173
|
+
const bak = `${cmPath}.bak-${stamp}`;
|
|
174
|
+
writeFileSync(bak, existing);
|
|
175
|
+
const re = new RegExp(
|
|
176
|
+
`\\n*${escapeRe(HARNESS_BLOCK_BEGIN)}[\\s\\S]*?${escapeRe(HARNESS_BLOCK_END)}\\n?`,
|
|
177
|
+
'm',
|
|
178
|
+
);
|
|
179
|
+
const next = existing.replace(re, '').replace(/\n{3,}$/, '\n\n');
|
|
180
|
+
writeFileSync(cmPath, next);
|
|
181
|
+
result.backedUp.push(bak);
|
|
182
|
+
result.removed.push(cmPath + ' (harness block removed)');
|
|
183
|
+
} else {
|
|
184
|
+
result.skipped.push(cmPath + ' (no harness block found)');
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
result.skipped.push(cmPath + ' (does not exist)');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (purgeRatchet && existsSync(rmPath)) {
|
|
191
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
192
|
+
const bak = `${rmPath}.bak-${stamp}`;
|
|
193
|
+
writeFileSync(bak, readFileSync(rmPath, 'utf8'));
|
|
194
|
+
result.backedUp.push(bak);
|
|
195
|
+
// Replace with empty initial template rather than delete (preserves dir).
|
|
196
|
+
writeFileSync(rmPath, harnessRatchetMdInitial());
|
|
197
|
+
result.removed.push(rmPath + ' (reset to initial)');
|
|
198
|
+
} else if (existsSync(rmPath)) {
|
|
199
|
+
result.skipped.push(rmPath + ' (kept; pass --purge-ratchet to reset)');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
157
205
|
/**
|
|
158
206
|
* harness promote — append a one-line rule to .claude/ratchet.md.
|
|
159
207
|
* Creates the file from the initial template if missing.
|
|
@@ -172,6 +220,52 @@ export function harnessPromote(ruleText, { root = findProjectRoot() } = {}) {
|
|
|
172
220
|
return { path: rmPath, root };
|
|
173
221
|
}
|
|
174
222
|
|
|
223
|
+
/**
|
|
224
|
+
* harness list — return numbered ratchet rules from .claude/ratchet.md.
|
|
225
|
+
* Numbering is 1-based and matches `harness rm <N>`.
|
|
226
|
+
*/
|
|
227
|
+
export function harnessListRules({ root = findProjectRoot() } = {}) {
|
|
228
|
+
const rmPath = ratchetMdPath(root);
|
|
229
|
+
if (!existsSync(rmPath)) return { path: rmPath, rules: [] };
|
|
230
|
+
const lines = readFileSync(rmPath, 'utf8').split('\n');
|
|
231
|
+
const rules = [];
|
|
232
|
+
for (let i = 0; i < lines.length; i++) {
|
|
233
|
+
const line = lines[i];
|
|
234
|
+
// A "rule line" starts with "- " (markdown bullet). Header lines, blanks,
|
|
235
|
+
// and the "## Rules" anchor are ignored.
|
|
236
|
+
if (/^\s*-\s+/.test(line)) {
|
|
237
|
+
rules.push({ index: rules.length + 1, lineNo: i, text: line.replace(/^\s*-\s+/, '') });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return { path: rmPath, rules };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* harness rm — remove a ratchet rule by its 1-based index. Writes a `.bak`
|
|
245
|
+
* before mutating so the user can recover. Returns the removed rule for the
|
|
246
|
+
* CLI to echo back.
|
|
247
|
+
*
|
|
248
|
+
* NOTE: Removal is intentionally a separate verb from `promote`. Ratchet's
|
|
249
|
+
* value is one-way accumulation; deleting should feel deliberate. The CLI
|
|
250
|
+
* surfaces a "narrow the condition instead" reminder around this call.
|
|
251
|
+
*/
|
|
252
|
+
export function harnessRmRule(n, { root = findProjectRoot() } = {}) {
|
|
253
|
+
const { path: rmPath, rules } = harnessListRules({ root });
|
|
254
|
+
if (!existsSync(rmPath)) {
|
|
255
|
+
return { ok: false, error: `ratchet.md not found at ${rmPath}` };
|
|
256
|
+
}
|
|
257
|
+
const target = rules.find((r) => r.index === n);
|
|
258
|
+
if (!target) {
|
|
259
|
+
return { ok: false, error: `No rule #${n} (have ${rules.length})`, rules };
|
|
260
|
+
}
|
|
261
|
+
const content = readFileSync(rmPath, 'utf8');
|
|
262
|
+
writeFileSync(rmPath + '.bak', content);
|
|
263
|
+
const lines = content.split('\n');
|
|
264
|
+
lines.splice(target.lineNo, 1);
|
|
265
|
+
writeFileSync(rmPath, lines.join('\n'));
|
|
266
|
+
return { ok: true, path: rmPath, backup: rmPath + '.bak', removed: target };
|
|
267
|
+
}
|
|
268
|
+
|
|
175
269
|
/**
|
|
176
270
|
* Statusline segment shape for the 🅷 indicator. Returns null when the user
|
|
177
271
|
* has explicitly disabled harness display, or when there's no CLAUDE.md and
|