claude-token-saver 3.39.0 → 3.40.1
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.ko.md +22 -9
- package/README.md +19 -6
- package/package.json +5 -4
- package/src/commands/korean.js +1 -0
- package/src/korean-lint.cjs +51 -1
package/README.ko.md
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/site/assets/logo/sprag-lockup.svg">
|
|
5
|
+
<img alt="Sprag" src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/site/assets/logo/sprag-lockup-light.svg" width="220">
|
|
6
|
+
</picture>
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
**AI 코딩 에이전트용 품질 래칫. 같은 실수는 두 번 없습니다.**
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
[](https://www.npmjs.com/package/sprag-cli)
|
|
11
|
+
[](https://www.npmjs.com/package/claude-token-saver)
|
|
12
|
+
[](./LICENSE)
|
|
13
|
+
|
|
14
|
+
[sprag.io](https://sprag.io) · [벤치마크](./docs/BENCHMARK.md) · [English](./README.md)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Sprag (claude-token-saver)
|
|
8
21
|
|
|
9
22
|
**아낀 돈을 두 줄로 보여 줍니다.** 비싼 모델이 반복하던 쉬운 작업을 싼 모델로 내려보내고, 모델이 읽지 못하는 문서를 Markdown 으로 바꿉니다. 두 절감액 모두 추정이 아니라 원장 기록입니다. 의존성 0, 설치 한 줄.
|
|
10
23
|
|
|
11
24
|

|
|
12
25
|
|
|
13
26
|
```bash
|
|
14
|
-
npm i -g claude-token-saver
|
|
27
|
+
npm i -g sprag-cli # or: npm i -g claude-token-saver (same package, old name)
|
|
15
28
|
```
|
|
16
29
|
|
|
17
30
|
숫자 네 개가 이 도구의 전부입니다.
|
|
@@ -395,7 +408,7 @@ Claude Code의 output style로도 같은 일을 할 수 있지만, output style
|
|
|
395
408
|
|
|
396
409
|
**이전** (지침 꺼짐)
|
|
397
410
|
|
|
398
|
-
> 렌더 실패 원인 좁히기 3단계: 먼저 `hn:still --check`로 스틸이 명세보다 최신인지(FRESH/STALE)
|
|
411
|
+
> 렌더 실패 원인 좁히기 3단계: 먼저 `hn:still --check`로 스틸이 명세보다 최신인지(FRESH/STALE) 확인합니다. STALE이면 재생성 안 해서 옛 자산으로 렌더 시도한 게 원인. 다음 caption-blocks가 최신 나레이션과 단어열 일치하는지 직접 대조합니다. TTS 재생성 후 blocks 재생성 빠지면 자막·음성 어긋나 렌더 깨짐.
|
|
399
412
|
|
|
400
413
|
**이후** (지침 켜짐)
|
|
401
414
|
|
|
@@ -413,7 +426,7 @@ Claude Code의 output style로도 같은 일을 할 수 있지만, output style
|
|
|
413
426
|
|
|
414
427
|
세 층이 더해집니다.
|
|
415
428
|
|
|
416
|
-
- **번역투**: 이중 피동, 일본어에서 온 굳은 표현,
|
|
429
|
+
- **번역투**: 이중 피동, 일본어에서 온 굳은 표현, `~에 있어서`, 영어 have를 직역한 '가지다' 남용. 기계가 판정할 수 있는 것은 쓰기 시점 검사(아래)에도 함께 들어갑니다.
|
|
417
430
|
- **상투 패턴**: 자동으로 붙는 수식어("다양한", "핵심적인"), 표지판 문장, 수사적 질문 뒤 즉답, 근거 없는 긍정 마무리.
|
|
418
431
|
- **응집성**: 문장이 이어지는 방식이라 정규식으로는 검사하지 못합니다. 이 절의 방향을 정한 연구 결과가 있습니다. 접속어·지시어 같은 표층 연결 장치는 글의 품질과 상관이 없거나 오히려 부적 상관이고, 앞 문장이 내놓은 정보를 다음 문장이 받아 풀어 주는 상술형 연결만 정적 상관을 보였습니다. 그래서 지침은 연결이 어색할 때 접속어를 더하지 말고 정보의 배열(아는 것 먼저, 새 것 나중)을 고치라고 말합니다.
|
|
419
432
|
|
|
@@ -503,7 +516,7 @@ claude-token-saver doc2md install-converter # 설치를 미리 끝내 두고
|
|
|
503
516
|
몇 가지는 의도적으로 하지 않습니다.
|
|
504
517
|
|
|
505
518
|
- **이미지는 변환하지 않습니다.** markitdown 이 빈 결과를 돌려주고, OCR 은 실측에서 리소스 이름을 틀리게 읽었습니다(`c5.xlarge` 를 `c.xlarge` 로). 이름 자체가 내용인 문서에서는 텍스트가 없느니만 못합니다. 모델이 이미지는 직접 읽습니다.
|
|
506
|
-
- **변환기가
|
|
519
|
+
- **변환기가 없어도 알리지 않고 지나가지 않습니다.** 설치 명령을 한 번 안내한 뒤 원본 `Read` 를 그대로 통과시킵니다. 매번 알리면 그것대로 방해가 되고, 아무 말도 하지 않으면 고장을 숨기게 됩니다. `doc2md` 를 인자 없이 실행하면 변환기와 훅 등록 상태를 한 번에 확인할 수 있습니다.
|
|
507
520
|
- **변환본은 프로젝트 안에 남기지 않습니다.** 도구의 상태 디렉터리 아래 권한 `0700` 으로 저장하므로 `.gitignore` 에 무엇을 추가할 필요가 없습니다. 파일명이 급여·계약·개인정보 같은 패턴에 걸리면 아예 변환하지 않습니다.
|
|
508
521
|
- **압축 폭탄은 막습니다.** pptx·xlsx·docx 는 zip 컨테이너입니다. 선언된 크기를 먼저 걸러 내고, 선언은 조작될 수 있으므로 실제 해제 바이트도 상한과 대조합니다.
|
|
509
522
|
- **엑셀은 행 수로 자릅니다.** 변환 시간은 파일 크기가 아니라 행 수를 따릅니다(실측: PDF 6.3MB 0.9초, 엑셀 5.8MB 47.75초). 5만 행을 넘으면 앞부분만 변환하고, **잘랐다는 사실과 전체 행 수를 안내에 함께 적습니다.**
|
|
@@ -610,7 +623,7 @@ npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
|
|
|
610
623
|
|
|
611
624
|
## 피드백
|
|
612
625
|
|
|
613
|
-
버그 제보와 기능 제안은 GitHub 이슈로 받습니다: https://github.com/rootstudioyaml/
|
|
626
|
+
버그 제보와 기능 제안은 GitHub 이슈로 받습니다: https://github.com/rootstudioyaml/sprag/issues
|
|
614
627
|
|
|
615
628
|
브라우저나 GitHub 로그인이 어려운 환경(사내망, 세션 도중)이라면 터미널에서 바로 제출할 수 있고, Claude에게 대신 제출해 달라고 요청해도 됩니다.
|
|
616
629
|
|
package/README.md
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/site/assets/logo/sprag-lockup.svg">
|
|
5
|
+
<img alt="Sprag" src="https://raw.githubusercontent.com/rootstudioyaml/sprag/main/site/assets/logo/sprag-lockup-light.svg" width="220">
|
|
6
|
+
</picture>
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
**A quality ratchet for AI coding agents. Mistakes never repeat.**
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
[](https://www.npmjs.com/package/sprag-cli)
|
|
11
|
+
[](https://www.npmjs.com/package/claude-token-saver)
|
|
12
|
+
[](./LICENSE)
|
|
13
|
+
|
|
14
|
+
[sprag.io](https://sprag.io) · [Benchmark](./docs/BENCHMARK.md) · [한국어](./README.ko.md)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Sprag (claude-token-saver)
|
|
8
21
|
|
|
9
22
|
**Shows what it saved, on two lines.** It moves the easy work your expensive model keeps repeating onto cheaper ones, and turns documents the model cannot read into Markdown. Both figures are ledger entries rather than estimates, and whichever saved more takes the top line. Zero dependencies, one-line install.
|
|
10
23
|
|
|
11
24
|

|
|
12
25
|
|
|
13
26
|
```bash
|
|
14
|
-
npm i -g claude-token-saver
|
|
27
|
+
npm i -g sprag-cli # or: npm i -g claude-token-saver (same package, old name)
|
|
15
28
|
```
|
|
16
29
|
|
|
17
30
|
Four numbers are the whole pitch.
|
|
@@ -731,7 +744,7 @@ The full history moved to [CHANGELOG.md](./CHANGELOG.md) (Korean; version headin
|
|
|
731
744
|
|
|
732
745
|
## Feedback
|
|
733
746
|
|
|
734
|
-
Found a bug, or want a feature? Open an issue: https://github.com/rootstudioyaml/
|
|
747
|
+
Found a bug, or want a feature? Open an issue: https://github.com/rootstudioyaml/sprag/issues
|
|
735
748
|
|
|
736
749
|
No browser or GitHub login handy (corporate network, mid-session)? Submit straight from the terminal — or ask Claude to do it for you:
|
|
737
750
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-token-saver",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.40.1",
|
|
4
4
|
"description": "Route the easy work your expensive Claude model keeps repeating down to haiku/sonnet — post-hoc session analysis, no realtime router, no extra LLM calls.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
+
"sprag": "bin/cli.js",
|
|
7
8
|
"claude-token-saver": "bin/cli.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
@@ -48,11 +49,11 @@
|
|
|
48
49
|
"license": "MIT",
|
|
49
50
|
"repository": {
|
|
50
51
|
"type": "git",
|
|
51
|
-
"url": "git+https://github.com/rootstudioyaml/
|
|
52
|
+
"url": "git+https://github.com/rootstudioyaml/sprag.git"
|
|
52
53
|
},
|
|
53
|
-
"homepage": "https://
|
|
54
|
+
"homepage": "https://sprag.io",
|
|
54
55
|
"bugs": {
|
|
55
|
-
"url": "https://github.com/rootstudioyaml/
|
|
56
|
+
"url": "https://github.com/rootstudioyaml/sprag/issues"
|
|
56
57
|
},
|
|
57
58
|
"author": "DeepPulse (https://www.youtube.com/@DeepPulseKR)"
|
|
58
59
|
}
|
package/src/commands/korean.js
CHANGED
|
@@ -86,6 +86,7 @@ export async function run({ args, hasFlag }) {
|
|
|
86
86
|
console.error(`${file}: ${e.message}`);
|
|
87
87
|
continue;
|
|
88
88
|
}
|
|
89
|
+
if (lint.isHtmlFile(file)) text = lint.stripHtml(text);
|
|
89
90
|
const findings = lint.lintKoreanText(text, { code: !lint.isProseFile(file) });
|
|
90
91
|
total += findings.length;
|
|
91
92
|
if (findings.length) console.log(lint.formatFindings(file, findings));
|
package/src/korean-lint.cjs
CHANGED
|
@@ -35,12 +35,21 @@
|
|
|
35
35
|
// Documents. Under the `prose` scope these are the only files checked.
|
|
36
36
|
const PROSE_EXTENSIONS = new Set(['.md', '.mdx', '.markdown', '.txt', '.rst', '.adoc']);
|
|
37
37
|
|
|
38
|
+
// HTML is a document a reader receives, so it is checked under BOTH scopes,
|
|
39
|
+
// like prose — but through `stripHtml` first, so tags, comments, and CSS never
|
|
40
|
+
// trip the rules. Script bodies are kept: on a static page the Korean UI copy
|
|
41
|
+
// (i18n dictionaries, template strings) lives exactly there, and that gap is
|
|
42
|
+
// how an em dash shipped to a real landing page unchecked (2026-09-13).
|
|
43
|
+
const HTML_EXTENSIONS = new Set(['.html', '.htm', '.xhtml', '.vue', '.svelte']);
|
|
44
|
+
|
|
38
45
|
// Only two kinds of path are skipped: installed dependencies and VCS
|
|
39
46
|
// internals, neither of which anyone in this session wrote. Build output is
|
|
40
47
|
// deliberately NOT on this list. Generated artifacts are the files a reader
|
|
41
48
|
// actually receives, so exempting `dist/` or `build/` would exempt the very
|
|
42
49
|
// documents the check exists for.
|
|
43
50
|
const SKIP_PATH = /(^|[\\/])(node_modules|\.git|.*\.min\.[a-z]+|.*-lock\.json|.*\.lock)([\\/]|$)/i;
|
|
51
|
+
// This module and the hook copy that embeds it quote every banned form.
|
|
52
|
+
const SELF_PATH = /(^|[\\/])(korean-lint\.cjs|cache-monitor-hook\.cjs)$/i;
|
|
44
53
|
const BINARY_EXTENSIONS = new Set([
|
|
45
54
|
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico', '.pdf', '.zip', '.gz', '.tar',
|
|
46
55
|
'.mp3', '.mp4', '.wav', '.mov', '.woff', '.woff2', '.ttf', '.otf', '.map', '.bin',
|
|
@@ -71,6 +80,18 @@ const METAPHOR_LEXICON = [
|
|
|
71
80
|
{ re: /벽에 부딪|길을 열|문을 열/, fix: '무엇이 막히고 무엇이 가능해지는지 그대로 적습니다' },
|
|
72
81
|
{ re: /갈림길|갈리는 지점/, fix: "'결정이 나뉘는 조건'으로 바꿉니다" },
|
|
73
82
|
{ re: /깨어나|잠들어 있/, fix: "'동작을 시작하다'·'실행되지 않고 있다'로 바꿉니다" },
|
|
83
|
+
// Added 2026-09-13. "fails loudly / fails silently" reads as idiom in English
|
|
84
|
+
// but lands as figurative vocabulary in Korean: a failure has no volume.
|
|
85
|
+
// 0 hits across 400 Korean files in yaml-sns-agent, so the confirm-request is
|
|
86
|
+
// not noise.
|
|
87
|
+
{
|
|
88
|
+
re: /(?:시끄럽게|시끄러운|조용히|조용한)\s*[가-힣]*(?:실패|틀리|드러나|어긋)|(?:실패|오류|버그)[가은는이]?\s*(?:시끄럽|시끄러운|조용하|조용한)/,
|
|
89
|
+
fix: "무엇으로 실패를 확인하는지 적습니다. '종료 코드로 바로 드러나는가'·'검증 없이 지나가는가'",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
re: /안전망/,
|
|
93
|
+
fix: "'검증 수단'·'확인 장치'처럼 무엇이 실패를 잡아내는지 그대로 적습니다",
|
|
94
|
+
},
|
|
74
95
|
];
|
|
75
96
|
|
|
76
97
|
// Guidance 3.2/3.3: literal renderings of English noun phrases.
|
|
@@ -111,9 +132,13 @@ const NOMINAL_ENDING = /(?:음|함|됨|임|점|론|양|성|화)\.$/;
|
|
|
111
132
|
function isLintTarget(filePath, scope = 'all') {
|
|
112
133
|
if (!filePath) return false;
|
|
113
134
|
if (SKIP_PATH.test(filePath)) return false;
|
|
135
|
+
// The rule table has to spell every banned form out, so this file always
|
|
136
|
+
// matches its own lexicon. Checking it reports the rules, not a breach.
|
|
137
|
+
if (SELF_PATH.test(filePath)) return false;
|
|
114
138
|
const dot = filePath.lastIndexOf('.');
|
|
115
139
|
const ext = dot === -1 ? '' : filePath.slice(dot).toLowerCase();
|
|
116
140
|
if (PROSE_EXTENSIONS.has(ext)) return true;
|
|
141
|
+
if (HTML_EXTENSIONS.has(ext)) return true;
|
|
117
142
|
if (scope !== 'all') return false;
|
|
118
143
|
if (!ext) return false;
|
|
119
144
|
return !BINARY_EXTENSIONS.has(ext);
|
|
@@ -124,6 +149,26 @@ function isProseFile(filePath) {
|
|
|
124
149
|
return isLintTarget(filePath, 'prose');
|
|
125
150
|
}
|
|
126
151
|
|
|
152
|
+
function isHtmlFile(filePath) {
|
|
153
|
+
const dot = String(filePath).lastIndexOf('.');
|
|
154
|
+
const ext = dot === -1 ? '' : String(filePath).slice(dot).toLowerCase();
|
|
155
|
+
return HTML_EXTENSIONS.has(ext);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Blank out the parts of an HTML document no reader sees — comments, CSS, and
|
|
160
|
+
* the tags themselves — while keeping every newline, so finding line numbers
|
|
161
|
+
* still point into the original file. Script bodies stay: that is where the
|
|
162
|
+
* page's Korean copy lives on a static site.
|
|
163
|
+
*/
|
|
164
|
+
function stripHtml(text) {
|
|
165
|
+
const blank = (m) => m.replace(/[^\n]/g, ' ');
|
|
166
|
+
return String(text)
|
|
167
|
+
.replace(/<!--[\s\S]*?-->/g, blank)
|
|
168
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, blank)
|
|
169
|
+
.replace(/<[^>]+>/g, blank);
|
|
170
|
+
}
|
|
171
|
+
|
|
127
172
|
/** Drop fenced code blocks and inline code so snippets never trip the rules. */
|
|
128
173
|
function stripCode(text) {
|
|
129
174
|
const out = [];
|
|
@@ -226,9 +271,12 @@ function lintToolUse(context, { scope = 'all' } = {}) {
|
|
|
226
271
|
const filePath = toolInput && typeof toolInput.file_path === 'string' ? toolInput.file_path : '';
|
|
227
272
|
if (!isLintTarget(filePath, scope)) return null;
|
|
228
273
|
|
|
229
|
-
|
|
274
|
+
let text = writtenTextOf(toolName, toolInput);
|
|
230
275
|
if (!text || !hasKorean(text)) return null;
|
|
231
276
|
|
|
277
|
+
// Edits arrive as fragments, so tag-stripping only applies when the payload
|
|
278
|
+
// is a whole document; a fragment is still linted, just without stripping.
|
|
279
|
+
if (isHtmlFile(filePath) && toolName === 'Write') text = stripHtml(text);
|
|
232
280
|
const findings = lintKoreanText(text, { code: !isProseFile(filePath) });
|
|
233
281
|
if (findings.length === 0) return null;
|
|
234
282
|
return { filePath, findings };
|
|
@@ -246,6 +294,8 @@ module.exports = {
|
|
|
246
294
|
METAPHOR_LEXICON,
|
|
247
295
|
isLintTarget,
|
|
248
296
|
isProseFile,
|
|
297
|
+
isHtmlFile,
|
|
298
|
+
stripHtml,
|
|
249
299
|
lintKoreanText,
|
|
250
300
|
writtenTextOf,
|
|
251
301
|
lintToolUse,
|