claude-token-saver 3.39.0 β 3.40.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.ko.md +5 -5
- package/README.md +2 -2
- package/package.json +6 -5
- package/src/commands/korean.js +1 -0
- package/src/korean-lint.cjs +51 -1
package/README.ko.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/claude-token-saver)
|
|
4
4
|
|
|
5
|
-
π **[
|
|
5
|
+
π **[sprag.io](https://sprag.io)** Β· νλ‘μ νΈ μ¬μ΄νΈ (claude-token-saverλ **Sprag**λ‘ μ΄λ¦μ λ°κΎΈλ μ€μ
λλ€)
|
|
6
6
|
|
|
7
7
|
# claude-token-saver
|
|
8
8
|
|
|
@@ -395,7 +395,7 @@ Claude Codeμ output styleλ‘λ κ°μ μΌμ ν μ μμ§λ§, output style
|
|
|
395
395
|
|
|
396
396
|
**μ΄μ ** (μ§μΉ¨ κΊΌμ§)
|
|
397
397
|
|
|
398
|
-
> λ λ μ€ν¨ μμΈ μ’νκΈ° 3λ¨κ³: λ¨Όμ `hn:still --check`λ‘ μ€νΈμ΄ λͺ
μΈλ³΄λ€ μ΅μ μΈμ§(FRESH/STALE)
|
|
398
|
+
> λ λ μ€ν¨ μμΈ μ’νκΈ° 3λ¨κ³: λ¨Όμ `hn:still --check`λ‘ μ€νΈμ΄ λͺ
μΈλ³΄λ€ μ΅μ μΈμ§(FRESH/STALE) νμΈν©λλ€. STALEμ΄λ©΄ μ¬μμ± μ ν΄μ μ μμ°μΌλ‘ λ λ μλν κ² μμΈ. λ€μ caption-blocksκ° μ΅μ λλ μ΄μ
κ³Ό λ¨μ΄μ΄ μΌμΉνλμ§ μ§μ λμ‘°ν©λλ€. TTS μ¬μμ± ν blocks μ¬μμ± λΉ μ§λ©΄ μλ§Β·μμ± μ΄κΈλ λ λ κΉ¨μ§.
|
|
399
399
|
|
|
400
400
|
**μ΄ν** (μ§μΉ¨ μΌμ§)
|
|
401
401
|
|
|
@@ -413,7 +413,7 @@ Claude Codeμ output styleλ‘λ κ°μ μΌμ ν μ μμ§λ§, output style
|
|
|
413
413
|
|
|
414
414
|
μΈ μΈ΅μ΄ λν΄μ§λλ€.
|
|
415
415
|
|
|
416
|
-
- **λ²μν¬**: μ΄μ€ νΌλ, μΌλ³Έμ΄μμ μ¨ κ΅³μ νν,
|
|
416
|
+
- **λ²μν¬**: μ΄μ€ νΌλ, μΌλ³Έμ΄μμ μ¨ κ΅³μ νν, `~μ μμ΄μ`, μμ΄ haveλ₯Ό μ§μν 'κ°μ§λ€' λ¨μ©. κΈ°κ³κ° νμ ν μ μλ κ²μ μ°κΈ° μμ κ²μ¬(μλ)μλ ν¨κ» λ€μ΄κ°λλ€.
|
|
417
417
|
- **μν¬ ν¨ν΄**: μλμΌλ‘ λΆλ μμμ΄("λ€μν", "ν΅μ¬μ μΈ"), νμ§ν λ¬Έμ₯, μμ¬μ μ§λ¬Έ λ€ μ¦λ΅, κ·Όκ±° μλ κΈμ λ§λ¬΄λ¦¬.
|
|
418
418
|
- **μμ§μ±**: λ¬Έμ₯μ΄ μ΄μ΄μ§λ λ°©μμ΄λΌ μ κ·μμΌλ‘λ κ²μ¬νμ§ λͺ»ν©λλ€. μ΄ μ μ λ°©ν₯μ μ ν μ°κ΅¬ κ²°κ³Όκ° μμ΅λλ€. μ μμ΄Β·μ§μμ΄ κ°μ νμΈ΅ μ°κ²° μ₯μΉλ κΈμ νμ§κ³Ό μκ΄μ΄ μκ±°λ μ€νλ € λΆμ μκ΄μ΄κ³ , μ λ¬Έμ₯μ΄ λ΄λμ μ 보λ₯Ό λ€μ λ¬Έμ₯μ΄ λ°μ νμ΄ μ£Όλ μμ ν μ°κ²°λ§ μ μ μκ΄μ 보μμ΅λλ€. κ·Έλμ μ§μΉ¨μ μ°κ²°μ΄ μ΄μν λ μ μμ΄λ₯Ό λνμ§ λ§κ³ μ 보μ λ°°μ΄(μλ κ² λ¨Όμ , μ κ² λμ€)μ κ³ μΉλΌκ³ λ§ν©λλ€.
|
|
419
419
|
|
|
@@ -503,7 +503,7 @@ claude-token-saver doc2md install-converter # μ€μΉλ₯Ό 미리 λλ΄ λκ³
|
|
|
503
503
|
λͺ κ°μ§λ μλμ μΌλ‘ νμ§ μμ΅λλ€.
|
|
504
504
|
|
|
505
505
|
- **μ΄λ―Έμ§λ λ³ννμ§ μμ΅λλ€.** markitdown μ΄ λΉ κ²°κ³Όλ₯Ό λλ €μ£Όκ³ , OCR μ μ€μΈ‘μμ 리μμ€ μ΄λ¦μ νλ¦¬κ² μ½μμ΅λλ€(`c5.xlarge` λ₯Ό `c.xlarge` λ‘). μ΄λ¦ μμ²΄κ° λ΄μ©μΈ λ¬Έμμμλ ν
μ€νΈκ° μλλλ§ λͺ»ν©λλ€. λͺ¨λΈμ΄ μ΄λ―Έμ§λ μ§μ μ½μ΅λλ€.
|
|
506
|
-
- **λ³νκΈ°κ°
|
|
506
|
+
- **λ³νκΈ°κ° μμ΄λ μλ¦¬μ§ μκ³ μ§λκ°μ§ μμ΅λλ€.** μ€μΉ λͺ
λ Ήμ ν λ² μλ΄ν λ€ μλ³Έ `Read` λ₯Ό κ·Έλλ‘ ν΅κ³Όμν΅λλ€. λ§€λ² μ리면 κ·Έκ²λλ‘ λ°©ν΄κ° λκ³ , μ무 λ§λ νμ§ μμΌλ©΄ κ³ μ₯μ μ¨κΈ°κ² λ©λλ€. `doc2md` λ₯Ό μΈμ μμ΄ μ€ννλ©΄ λ³νκΈ°μ ν
λ±λ‘ μνλ₯Ό ν λ²μ νμΈν μ μμ΅λλ€.
|
|
507
507
|
- **λ³νλ³Έμ νλ‘μ νΈ μμ λ¨κΈ°μ§ μμ΅λλ€.** λꡬμ μν λλ ν°λ¦¬ μλ κΆν `0700` μΌλ‘ μ μ₯νλ―λ‘ `.gitignore` μ 무μμ μΆκ°ν νμκ° μμ΅λλ€. νμΌλͺ
μ΄ κΈμ¬Β·κ³μ½Β·κ°μΈμ 보 κ°μ ν¨ν΄μ 걸리면 μμ λ³ννμ§ μμ΅λλ€.
|
|
508
508
|
- **μμΆ ννμ λ§μ΅λλ€.** pptxΒ·xlsxΒ·docx λ zip 컨ν
μ΄λμ
λλ€. μ μΈλ ν¬κΈ°λ₯Ό λ¨Όμ κ±Έλ¬ λ΄κ³ , μ μΈμ μ‘°μλ μ μμΌλ―λ‘ μ€μ ν΄μ λ°μ΄νΈλ μνκ³Ό λμ‘°ν©λλ€.
|
|
509
509
|
- **μμ
μ ν μλ‘ μλ¦
λλ€.** λ³ν μκ°μ νμΌ ν¬κΈ°κ° μλλΌ ν μλ₯Ό λ°λ¦
λλ€(μ€μΈ‘: PDF 6.3MB 0.9μ΄, μμ
5.8MB 47.75μ΄). 5λ§ νμ λμΌλ©΄ μλΆλΆλ§ λ³ννκ³ , **μλλ€λ μ¬μ€κ³Ό μ 체 ν μλ₯Ό μλ΄μ ν¨κ» μ μ΅λλ€.**
|
|
@@ -610,7 +610,7 @@ npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
|
|
|
610
610
|
|
|
611
611
|
## νΌλλ°±
|
|
612
612
|
|
|
613
|
-
λ²κ·Έ μ 보μ κΈ°λ₯ μ μμ GitHub μ΄μλ‘ λ°μ΅λλ€: https://github.com/rootstudioyaml/
|
|
613
|
+
λ²κ·Έ μ 보μ κΈ°λ₯ μ μμ GitHub μ΄μλ‘ λ°μ΅λλ€: https://github.com/rootstudioyaml/sprag/issues
|
|
614
614
|
|
|
615
615
|
λΈλΌμ°μ λ GitHub λ‘κ·ΈμΈμ΄ μ΄λ €μ΄ νκ²½(μ¬λ΄λ§, μΈμ
λμ€)μ΄λΌλ©΄ ν°λ―Έλμμ λ°λ‘ μ μΆν μ μκ³ , Claudeμκ² λμ μ μΆν΄ λ¬λΌκ³ μμ²ν΄λ λ©λλ€.
|
|
616
616
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/claude-token-saver)
|
|
4
4
|
|
|
5
|
-
π **[
|
|
5
|
+
π **[sprag.io](https://sprag.io)** Β· project site (claude-token-saver is being renamed to **Sprag**)
|
|
6
6
|
|
|
7
7
|
# claude-token-saver
|
|
8
8
|
|
|
@@ -731,7 +731,7 @@ The full history moved to [CHANGELOG.md](./CHANGELOG.md) (Korean; version headin
|
|
|
731
731
|
|
|
732
732
|
## Feedback
|
|
733
733
|
|
|
734
|
-
Found a bug, or want a feature? Open an issue: https://github.com/rootstudioyaml/
|
|
734
|
+
Found a bug, or want a feature? Open an issue: https://github.com/rootstudioyaml/sprag/issues
|
|
735
735
|
|
|
736
736
|
No browser or GitHub login handy (corporate network, mid-session)? Submit straight from the terminal β or ask Claude to do it for you:
|
|
737
737
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-token-saver",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Route the easy work your expensive Claude model keeps repeating down to haiku/sonnet
|
|
3
|
+
"version": "3.40.0",
|
|
4
|
+
"description": "Route the easy work your expensive Claude model keeps repeating down to haiku/sonnet \u2014 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,
|