claude-token-saver 3.23.3 → 3.24.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.en.md +26 -1
- package/README.md +26 -1
- package/package.json +1 -1
- package/src/commands/korean.js +145 -2
- package/src/installer.js +62 -0
- package/src/korean-lint.cjs +244 -0
- package/src/korean-style.js +22 -2
package/README.en.md
CHANGED
|
@@ -158,7 +158,9 @@ Run these in your shell (inside Claude Code, the `/claude-token-saver` Skill is
|
|
|
158
158
|
| `claude-token-saver route-scan` | Detect recurring easy work on expensive models → propose haiku-delegation ratchet rules (below) |
|
|
159
159
|
| `claude-token-saver route-scan savings` | The routing-savings ledger — per-model-change rollup + per-run log (the evidence behind the figure) |
|
|
160
160
|
| `claude-token-saver compact-window` | Warn when a 1M-context session has no auto-compact cap → pin 400k with `set` (below) |
|
|
161
|
-
| `claude-token-saver korean on\|off\|status` | Inject Korean writing guidance at session start (below) |
|
|
161
|
+
| `claude-token-saver korean on\|off\|status` | Inject Korean writing guidance at session start and install the write-time check (below) |
|
|
162
|
+
| `claude-token-saver korean lint block\|warn\|off` | How the write-time check handles findings |
|
|
163
|
+
| `claude-token-saver korean lint scope all\|prose` | Check every text file, or documents only |
|
|
162
164
|
| `claude-token-saver install` | Manually register Skill + statusline |
|
|
163
165
|
|
|
164
166
|
Switch output language with `mode ko` / `mode en` (English default; statusline chips stay symbolic).
|
|
@@ -313,6 +315,29 @@ Three things change. Clauses chained with em dashes become separate sentences, s
|
|
|
313
315
|
|
|
314
316
|
The technical content is identical in both. The guidance touches sentence construction only, not judgement or accuracy: the answer does not change, it just stops needing a second read. In a channel people scroll through, that difference cuts follow-up questions — and the tokens those follow-ups would have cost.
|
|
315
317
|
|
|
318
|
+
### The write-time check (v3.24.0)
|
|
319
|
+
|
|
320
|
+
Injecting the guidance once at session start turned out to be half the job. The model reads it, then writes dozens of files over the next hours with nothing re-reading the output. Sessions with the guidance active still shipped violations into documents, and it surfaced only when a human read the finished artifact. An August 2026 fix reworded the scope sentence to address this; it recurred, because rewording an instruction does not add a checkpoint.
|
|
321
|
+
|
|
322
|
+
From v3.24.0 `korean on` also installs a PostToolUse hook. It opens the file the model just wrote, runs the clauses a machine can decide, and hands any findings back. The file is already saved, so nothing is lost — the model fixes it on the spot.
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
claude-token-saver korean lint block # default: findings are handed back as blocking feedback
|
|
326
|
+
claude-token-saver korean lint warn # print findings, do not block
|
|
327
|
+
claude-token-saver korean lint off # disable the check
|
|
328
|
+
|
|
329
|
+
claude-token-saver korean lint scope all # default: every text file the session writes
|
|
330
|
+
claude-token-saver korean lint scope prose # documents only
|
|
331
|
+
|
|
332
|
+
claude-token-saver korean lint docs/*.md # check files already on disk
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Checked: 15 figurative phrases, translationese markers, separators (`—`·`ㅡ`·`|`), three or more `의` particles in one phrase, and a period after a nominal ending. Clauses that need judgement stay with the guidance text.
|
|
336
|
+
|
|
337
|
+
The default `all` scope covers code comments, UI strings, subtitles, templates, and build output, not just documents. The vendored guidance exempts comments, but comments are read by people and generated artifacts (PDF, HTML) are assembled from those strings, so exempting them reopens the exact gap that was reported. Only installed dependencies, VCS internals, lockfiles, and binary or image files are skipped; `dist/` and `build/` are checked. `korean lint scope prose` restores the narrow reading.
|
|
338
|
+
|
|
339
|
+
The scope sentence in the injected guidance is generated from the same setting, so the model is never told one rule while being corrected against another.
|
|
340
|
+
|
|
316
341
|
### The encoding rule that ships with it (v3.23.2)
|
|
317
342
|
|
|
318
343
|
Alongside the writing guidance, one more line is injected: **non-ASCII strings in tool-call parameters must be written as literal UTF-8, never as `\uXXXX` unicode escapes.**
|
package/README.md
CHANGED
|
@@ -157,7 +157,9 @@ Claude Code 안에서 `/claude-token-saver` Skill을 실행하거나, 칩에 적
|
|
|
157
157
|
| `claude-token-saver route-scan` | 상위 모델이 반복 처리한 쉬운 작업을 감지해 haiku 위임 랫쳇 룰을 제안합니다 (아래 참고) |
|
|
158
158
|
| `claude-token-saver route-scan savings` | 라우팅 절감 원장입니다. 모델 이동별 합계와 실행별 내역을 함께 보여 주며, 표시되는 금액의 근거가 됩니다 |
|
|
159
159
|
| `claude-token-saver compact-window` | 1M 컨텍스트를 쓰면서 자동 압축 창이 설정되지 않았으면 경고하고, `set`으로 40만에 고정합니다 (아래 참고) |
|
|
160
|
-
| `claude-token-saver korean on\|off\|status` | 한국어 문체 지침을 세션 시작 시
|
|
160
|
+
| `claude-token-saver korean on\|off\|status` | 한국어 문체 지침을 세션 시작 시 주입하고, 쓰기 시점 검사를 함께 설치합니다 (아래 참고) |
|
|
161
|
+
| `claude-token-saver korean lint block\|warn\|off` | 쓰기 시점 검사가 위반을 어떻게 처리할지 정합니다 |
|
|
162
|
+
| `claude-token-saver korean lint scope all\|prose` | 검사 범위를 모든 텍스트 파일과 문서 전용 사이에서 고릅니다 |
|
|
161
163
|
| `claude-token-saver install` | Skill·statusline 수동 등록 |
|
|
162
164
|
|
|
163
165
|
출력 언어는 `mode ko`와 `mode en`으로 전환합니다. 기본값은 영어이며, statusline의 칩은 언제나 기호로 표시합니다. 전체 옵션은 [영문 README](./README.en.md#options)를 참고하십시오.
|
|
@@ -303,6 +305,29 @@ Claude Code의 output style로도 같은 일을 할 수 있지만, output style
|
|
|
303
305
|
|
|
304
306
|
기술적 내용은 양쪽이 동일합니다. 지침은 판단이나 정확도가 아니라 문장의 완성도에만 관여하므로, 답이 달라지는 것이 아니라 같은 답을 다시 읽지 않아도 되는 형태로 만들어 줍니다. 슬랙처럼 사람이 스크롤하며 읽는 채널에서는 이 차이가 되묻는 횟수를 줄이고, 되묻지 않는 만큼 토큰도 아낍니다.
|
|
305
307
|
|
|
308
|
+
### 쓰기 시점 검사 (v3.24.0)
|
|
309
|
+
|
|
310
|
+
지침을 세션 시작에 한 번 넣는 것만으로는 부족했습니다. 모델은 지침을 한 번 읽고 그 뒤로 파일 수십 개를 쓰는데, 그동안 결과물을 다시 읽어 보는 단계가 없었습니다. 그래서 지침이 켜진 세션이 규약에 어긋나는 문장을 문서에 그대로 실어 보냈고, 사람이 완성본을 읽을 때에야 드러났습니다. 2026년 8월에 적용 범위 문장을 고쳐서 같은 문제를 잡으려 했지만, 문장을 고쳐도 검사 단계가 없다는 조건은 그대로였기 때문에 재발했습니다.
|
|
311
|
+
|
|
312
|
+
v3.24.0부터 `korean on`이 PostToolUse 훅을 함께 설치합니다. 모델이 방금 쓴 파일을 열어서 기계로 판정할 수 있는 조항을 검사하고, 위반이 있으면 모델에게 되돌려 보냅니다. 파일은 이미 저장된 뒤이므로 잃는 것은 없고, 모델이 그 자리에서 고칩니다.
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
claude-token-saver korean lint block # 기본값. 위반을 되돌려 보내 고치게 합니다
|
|
316
|
+
claude-token-saver korean lint warn # 알리기만 하고 진행을 막지 않습니다
|
|
317
|
+
claude-token-saver korean lint off # 검사하지 않습니다
|
|
318
|
+
|
|
319
|
+
claude-token-saver korean lint scope all # 기본값. 세션이 쓴 모든 텍스트 파일
|
|
320
|
+
claude-token-saver korean lint scope prose # 마크다운·텍스트 문서만
|
|
321
|
+
|
|
322
|
+
claude-token-saver korean lint docs/*.md # 이미 저장된 파일을 직접 검사
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
검사 항목은 사람이 판정할 필요가 없는 것들입니다. 비유 어휘 15종(`~는 자리`, `~의 흐름`, `닿는다`, `걷어내다`, `발목을 잡다` 등), 번역체 표지(`~에 대한`, `~를 위한`, `~되어지`), 구분자(`—`·`ㅡ`·`|`), 한 구에 세 번 이상 이어지는 조사 `의`, 명사형 종결 뒤의 마침표입니다. 판단이 필요한 조항(성분 생략, 한자어 선택)은 그대로 지침이 담당합니다.
|
|
326
|
+
|
|
327
|
+
기본 범위 `all`은 문서뿐 아니라 **코드 주석과 화면에 나가는 문자열, 자막·템플릿, 생성 결과물까지** 검사합니다. 벤더링한 지침 원문은 코드 주석을 예외로 두지만, 주석도 사람이 읽고 PDF·HTML 같은 산출물은 그 문자열들로 조립되기 때문에 예외로 두면 정확히 문제가 됐던 경로가 다시 열립니다. 검사에서 빠지는 것은 설치된 의존성(`node_modules`), VCS 내부, 락 파일, 그리고 이진·이미지 파일뿐입니다. `dist`나 `build` 같은 산출물 디렉터리는 검사합니다. 원문 규약대로 문서만 보고 싶으면 `korean lint scope prose`로 되돌립니다.
|
|
328
|
+
|
|
329
|
+
주입문의 적용 범위 안내도 이 설정에서 생성합니다. 모델에게 알리는 범위와 검사하는 범위가 갈라지면 8월과 같은 상태로 돌아가기 때문입니다.
|
|
330
|
+
|
|
306
331
|
### 함께 주입되는 인코딩 규칙 (v3.23.2)
|
|
307
332
|
|
|
308
333
|
문체 지침과 별도로, **도구 호출 인자에 담는 비ASCII 문자열은 리터럴 UTF-8로 쓰고 `\uXXXX` 유니코드 이스케이프로는 쓰지 말라**는 한 줄이 같이 주입됩니다.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-token-saver",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.24.0",
|
|
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": {
|
package/src/commands/korean.js
CHANGED
|
@@ -11,14 +11,144 @@
|
|
|
11
11
|
* machine. This ships the guidance with the package and delivers it through
|
|
12
12
|
* the SessionStart hook that is already installed, so it applies everywhere
|
|
13
13
|
* the CLI is installed and leaves the output-style slot free.
|
|
14
|
+
*
|
|
15
|
+
* Injection alone turned out to be half the job. The guidance is read once at
|
|
16
|
+
* session start and never again, so a long session writes documents that drift
|
|
17
|
+
* back to the patterns it forbids, and the drift is caught only when a human
|
|
18
|
+
* reads the finished file. `korean on` therefore also installs a PostToolUse
|
|
19
|
+
* hook that runs the machine-checkable clauses over the prose the model just
|
|
20
|
+
* wrote:
|
|
21
|
+
* claude-token-saver korean lint block|warn|off # how findings are handled
|
|
22
|
+
* claude-token-saver korean lint <file...> # check files on disk
|
|
14
23
|
*/
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
// Enforcement is only useful if it is on by default — a check the user has to
|
|
26
|
+
// discover is the same hole in a different shape.
|
|
27
|
+
function koreanLintMode(cfg) {
|
|
28
|
+
const mode = cfg?.koreanStyle?.lint;
|
|
29
|
+
return mode === 'off' || mode === 'warn' ? mode : 'block';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// `all` checks every text file the session writes, including Korean sitting in
|
|
33
|
+
// comments and UI strings. That is wider than the vendored guidance's own
|
|
34
|
+
// exemption list, and deliberately so: a comment is read by a person, and
|
|
35
|
+
// generated artifacts (PDF, HTML, captions) are assembled from those strings,
|
|
36
|
+
// so exempting them reopens the gap for exactly the outputs users complained
|
|
37
|
+
// about. `prose` restores the narrow reading.
|
|
38
|
+
function koreanLintScope(cfg) {
|
|
39
|
+
return cfg?.koreanStyle?.lintScope === 'prose' ? 'prose' : 'all';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function run({ args, hasFlag }) {
|
|
17
43
|
const sub = args[1] || 'status';
|
|
18
44
|
const ks = await import('../korean-style.js');
|
|
19
|
-
const { userLanguage } = await import('../config.js');
|
|
45
|
+
const { userLanguage, loadConfig, saveConfig } = await import('../config.js');
|
|
20
46
|
const lang = userLanguage();
|
|
21
47
|
|
|
48
|
+
// PostToolUse hook. Claude Code feeds the tool-call payload on stdin; we
|
|
49
|
+
// check the prose the model just wrote and hand any findings straight back
|
|
50
|
+
// to it. Silent and cheap when the file is clean, which is the common case.
|
|
51
|
+
if (hasFlag?.('--hook') || sub === '--hook') {
|
|
52
|
+
const { readStdinJson } = await import('../stdin-payload.js');
|
|
53
|
+
const payload = readStdinJson();
|
|
54
|
+
if (!payload || !ks.koreanStyleEnabled()) return;
|
|
55
|
+
const mode = koreanLintMode(loadConfig());
|
|
56
|
+
if (mode === 'off') return;
|
|
57
|
+
const { createRequire } = await import('node:module');
|
|
58
|
+
const req = createRequire(import.meta.url);
|
|
59
|
+
const lint = req('../korean-lint.cjs');
|
|
60
|
+
const result = lint.lintToolUse(payload, { scope: koreanLintScope(loadConfig()) });
|
|
61
|
+
if (!result) return;
|
|
62
|
+
const message = lint.formatFindings(result.filePath, result.findings);
|
|
63
|
+
if (mode === 'block') {
|
|
64
|
+
// Exit 2 is Claude Code's blocking-feedback channel: the file is already
|
|
65
|
+
// written, so nothing is lost, but the model must address this before it
|
|
66
|
+
// moves on. That is the whole point — a warning it can scroll past is
|
|
67
|
+
// what failed in the first place.
|
|
68
|
+
process.stderr.write(message + '\n');
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
console.log(message);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Manual run over files already on disk: `korean lint docs/*.md`.
|
|
76
|
+
if (sub === 'lint' && args.length > 2 && !['on', 'off', 'warn', 'block', 'scope'].includes(args[2])) {
|
|
77
|
+
const { readFileSync } = await import('node:fs');
|
|
78
|
+
const { createRequire } = await import('node:module');
|
|
79
|
+
const lint = createRequire(import.meta.url)('../korean-lint.cjs');
|
|
80
|
+
let total = 0;
|
|
81
|
+
for (const file of args.slice(2)) {
|
|
82
|
+
let text;
|
|
83
|
+
try {
|
|
84
|
+
text = readFileSync(file, 'utf8');
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.error(`${file}: ${e.message}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const findings = lint.lintKoreanText(text, { code: !lint.isProseFile(file) });
|
|
90
|
+
total += findings.length;
|
|
91
|
+
if (findings.length) console.log(lint.formatFindings(file, findings));
|
|
92
|
+
}
|
|
93
|
+
if (total === 0) {
|
|
94
|
+
console.log(lang === 'ko' ? '문체 규약 위반이 없습니다.' : 'No findings.');
|
|
95
|
+
}
|
|
96
|
+
process.exit(total > 0 ? 1 : 0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Scope switch: `korean lint scope all|prose`.
|
|
100
|
+
if (sub === 'lint' && args[2] === 'scope') {
|
|
101
|
+
const cfg = loadConfig();
|
|
102
|
+
const next = args[3];
|
|
103
|
+
if (!next) {
|
|
104
|
+
console.log(lang === 'ko'
|
|
105
|
+
? `검사 범위: ${koreanLintScope(cfg)}`
|
|
106
|
+
: `Check scope: ${koreanLintScope(cfg)}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!['all', 'prose'].includes(next)) {
|
|
110
|
+
console.error('Usage: claude-token-saver korean lint scope [all|prose]');
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
cfg.koreanStyle = cfg.koreanStyle || {};
|
|
114
|
+
cfg.koreanStyle.lintScope = next;
|
|
115
|
+
saveConfig(cfg);
|
|
116
|
+
console.log(lang === 'ko'
|
|
117
|
+
? (next === 'all'
|
|
118
|
+
? '검사 범위: all — 문서와 코드 주석, UI 문자열까지 세션이 쓴 모든 텍스트 파일을 검사합니다.'
|
|
119
|
+
: '검사 범위: prose — 마크다운과 텍스트 문서만 검사합니다.')
|
|
120
|
+
: (next === 'all'
|
|
121
|
+
? 'Check scope: all — every text file the session writes, comments and UI strings included.'
|
|
122
|
+
: 'Check scope: prose — documents only.'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Enforcement mode: `korean lint block|warn|off`.
|
|
127
|
+
if (sub === 'lint') {
|
|
128
|
+
const mode = args[2];
|
|
129
|
+
const cfg = loadConfig();
|
|
130
|
+
if (!mode) {
|
|
131
|
+
console.log(lang === 'ko'
|
|
132
|
+
? `쓰기 시점 검사: ${koreanLintMode(cfg)} (범위 ${koreanLintScope(cfg)})`
|
|
133
|
+
: `Write-time check: ${koreanLintMode(cfg)} (scope ${koreanLintScope(cfg)})`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!['block', 'warn', 'off'].includes(mode)) {
|
|
137
|
+
console.error('Usage: claude-token-saver korean lint [block|warn|off] | korean lint <file...>');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
cfg.koreanStyle = cfg.koreanStyle || {};
|
|
141
|
+
cfg.koreanStyle.lint = mode;
|
|
142
|
+
saveConfig(cfg);
|
|
143
|
+
const note = {
|
|
144
|
+
block: lang === 'ko' ? '위반을 발견하면 모델에게 되돌려 보내 고치게 합니다.' : 'Findings are handed back to the model as blocking feedback.',
|
|
145
|
+
warn: lang === 'ko' ? '위반을 알리기만 하고 진행을 막지 않습니다.' : 'Findings are printed as a note and do not block.',
|
|
146
|
+
off: lang === 'ko' ? '쓰기 시점 검사를 하지 않습니다.' : 'The write-time check is disabled.',
|
|
147
|
+
}[mode];
|
|
148
|
+
console.log(`${lang === 'ko' ? '쓰기 시점 검사' : 'Write-time check'}: ${mode} — ${note}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
22
152
|
if (sub === 'show') {
|
|
23
153
|
const text = ks.koreanStyleText();
|
|
24
154
|
if (!text) {
|
|
@@ -32,6 +162,8 @@ export async function run({ args }) {
|
|
|
32
162
|
if (sub === 'on' || sub === 'off') {
|
|
33
163
|
const enabled = sub === 'on';
|
|
34
164
|
ks.setKoreanStyleEnabled(enabled);
|
|
165
|
+
const { installKoreanLintHook, removeKoreanLintHook } = await import('../installer.js');
|
|
166
|
+
const hook = enabled ? installKoreanLintHook() : removeKoreanLintHook();
|
|
35
167
|
if (enabled) {
|
|
36
168
|
console.log(lang === 'ko'
|
|
37
169
|
? '한국어 문체 지침을 켰습니다. 다음 세션부터 모든 프로젝트에 적용됩니다.'
|
|
@@ -42,6 +174,15 @@ export async function run({ args }) {
|
|
|
42
174
|
console.log(lang === 'ko'
|
|
43
175
|
? ` 출처: ${ks.KOREAN_STYLE_SOURCE}`
|
|
44
176
|
: ` Source: ${ks.KOREAN_STYLE_SOURCE}`);
|
|
177
|
+
if (hook.action === 'skipped') {
|
|
178
|
+
console.log(lang === 'ko'
|
|
179
|
+
? ` ⚠ 쓰기 시점 검사 훅을 못 걸었습니다: ${hook.reason}`
|
|
180
|
+
: ` ⚠ Could not register the write-time hook: ${hook.reason}`);
|
|
181
|
+
} else {
|
|
182
|
+
console.log(lang === 'ko'
|
|
183
|
+
? ` 쓰기 시점 검사: ${koreanLintMode(loadConfig())}, 범위 ${koreanLintScope(loadConfig())} (Write·Edit 로 쓴 한국어를 검사합니다)`
|
|
184
|
+
: ` Write-time check: ${koreanLintMode(loadConfig())}, scope ${koreanLintScope(loadConfig())} (runs on Korean written via Write/Edit)`);
|
|
185
|
+
}
|
|
45
186
|
} else {
|
|
46
187
|
console.log(lang === 'ko'
|
|
47
188
|
? '한국어 문체 지침을 껐습니다. 다음 세션부터 주입하지 않습니다.'
|
|
@@ -59,6 +200,7 @@ export async function run({ args }) {
|
|
|
59
200
|
if (lang === 'ko') {
|
|
60
201
|
console.log(`한국어 문체 지침: ${on ? '켜짐' : '꺼짐'}`);
|
|
61
202
|
console.log(` 비용: 세션당 약 ${tokens} 토큰 (세션 시작 1회 주입)`);
|
|
203
|
+
console.log(` 쓰기 시점 검사: ${koreanLintMode(loadConfig())}, 범위 ${koreanLintScope(loadConfig())} (바꾸려면 claude-token-saver korean lint block|warn|off, korean lint scope all|prose)`);
|
|
62
204
|
console.log(` 출처: ${ks.KOREAN_STYLE_SOURCE}`);
|
|
63
205
|
console.log(` 라이선스 전문: ${ks.KOREAN_STYLE_LICENSE_PATH}`);
|
|
64
206
|
console.log(on
|
|
@@ -67,6 +209,7 @@ export async function run({ args }) {
|
|
|
67
209
|
} else {
|
|
68
210
|
console.log(`Korean writing guidance: ${on ? 'on' : 'off'}`);
|
|
69
211
|
console.log(` Cost: ~${tokens} tokens per session (injected once at session start)`);
|
|
212
|
+
console.log(` Write-time check: ${koreanLintMode(loadConfig())}, scope ${koreanLintScope(loadConfig())} (change with: claude-token-saver korean lint block|warn|off, korean lint scope all|prose)`);
|
|
70
213
|
console.log(` Source: ${ks.KOREAN_STYLE_SOURCE}`);
|
|
71
214
|
console.log(` License text: ${ks.KOREAN_STYLE_LICENSE_PATH}`);
|
|
72
215
|
console.log(on
|
package/src/installer.js
CHANGED
|
@@ -284,6 +284,68 @@ export function installBriefHook() {
|
|
|
284
284
|
return { path: file, action: 'created' };
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
// Registers the PostToolUse hook that checks Korean prose the session just
|
|
288
|
+
// wrote. Session-start guidance teaches the model but is never re-read, so
|
|
289
|
+
// files written later drift back to the patterns the guidance forbids and the
|
|
290
|
+
// drift surfaces only when a human reads the artifact. This hook closes that
|
|
291
|
+
// gap: it runs on the file the model just wrote, while it can still fix it.
|
|
292
|
+
// Installed by `korean on`, removed by `korean off`. Idempotent.
|
|
293
|
+
const KOREAN_LINT_HOOK_COMMAND = 'claude-token-saver korean --hook';
|
|
294
|
+
|
|
295
|
+
export function installKoreanLintHook() {
|
|
296
|
+
const dir = claudeUserDir();
|
|
297
|
+
const file = join(dir, 'settings.json');
|
|
298
|
+
mkdirSync(dir, { recursive: true });
|
|
299
|
+
|
|
300
|
+
let settings = {};
|
|
301
|
+
if (existsSync(file)) {
|
|
302
|
+
try {
|
|
303
|
+
settings = JSON.parse(readFileSync(file, 'utf8'));
|
|
304
|
+
} catch (e) {
|
|
305
|
+
return { path: file, action: 'skipped', reason: `unreadable JSON (${e.message})` };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
settings.hooks = settings.hooks || {};
|
|
310
|
+
if (settings.hooks.PostToolUse !== undefined && !Array.isArray(settings.hooks.PostToolUse)) {
|
|
311
|
+
return { path: file, action: 'skipped', reason: 'hooks.PostToolUse is not an array — fix settings.json manually' };
|
|
312
|
+
}
|
|
313
|
+
const list = Array.isArray(settings.hooks.PostToolUse) ? settings.hooks.PostToolUse : [];
|
|
314
|
+
const already = list.some((m) =>
|
|
315
|
+
Array.isArray(m?.hooks) && m.hooks.some((h) => typeof h?.command === 'string' && h.command.includes('korean --hook')),
|
|
316
|
+
);
|
|
317
|
+
if (already) return { path: file, action: 'exists' };
|
|
318
|
+
|
|
319
|
+
list.push({
|
|
320
|
+
matcher: 'Write|Edit|MultiEdit',
|
|
321
|
+
hooks: [{ type: 'command', command: KOREAN_LINT_HOOK_COMMAND, timeout: 10 }],
|
|
322
|
+
});
|
|
323
|
+
settings.hooks.PostToolUse = list;
|
|
324
|
+
writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
|
|
325
|
+
return { path: file, action: 'created' };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function removeKoreanLintHook() {
|
|
329
|
+
const file = join(claudeUserDir(), 'settings.json');
|
|
330
|
+
if (!existsSync(file)) return { path: file, action: 'absent' };
|
|
331
|
+
let settings;
|
|
332
|
+
try {
|
|
333
|
+
settings = JSON.parse(readFileSync(file, 'utf8'));
|
|
334
|
+
} catch (e) {
|
|
335
|
+
return { path: file, action: 'skipped', reason: `unreadable JSON (${e.message})` };
|
|
336
|
+
}
|
|
337
|
+
const list = settings?.hooks?.PostToolUse;
|
|
338
|
+
if (!Array.isArray(list)) return { path: file, action: 'absent' };
|
|
339
|
+
const kept = list.filter((m) =>
|
|
340
|
+
!(Array.isArray(m?.hooks) && m.hooks.some((h) => typeof h?.command === 'string' && h.command.includes('korean --hook'))),
|
|
341
|
+
);
|
|
342
|
+
if (kept.length === list.length) return { path: file, action: 'absent' };
|
|
343
|
+
if (kept.length === 0) delete settings.hooks.PostToolUse;
|
|
344
|
+
else settings.hooks.PostToolUse = kept;
|
|
345
|
+
writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
|
|
346
|
+
return { path: file, action: 'removed' };
|
|
347
|
+
}
|
|
348
|
+
|
|
287
349
|
export function installAll({ force = false } = {}) {
|
|
288
350
|
return {
|
|
289
351
|
skill: installSkill({ force }),
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* korean-lint — machine-checkable half of the fluent-korean guidance.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists next to korean-style.js:
|
|
6
|
+
*
|
|
7
|
+
* korean-style.js hands the model ~1.3k tokens of prose at session start. That
|
|
8
|
+
* is the right way to teach judgement, but prose has no enforcement point. The
|
|
9
|
+
* model reads it once, writes forty files over the next two hours, and nothing
|
|
10
|
+
* ever re-checks the output. Users reported exactly that hole: the guidance was
|
|
11
|
+
* loaded, the session still shipped `~는 자리` and `~의 흐름` into documents,
|
|
12
|
+
* and it surfaced only when a human read the finished artifact.
|
|
13
|
+
*
|
|
14
|
+
* So the clauses a machine can decide are checked at write time instead. The
|
|
15
|
+
* PostToolUse hook already runs on Write/Edit, so every file the session
|
|
16
|
+
* produces passes through here and findings are handed back to the model while
|
|
17
|
+
* it can still fix them.
|
|
18
|
+
*
|
|
19
|
+
* Scope: every text file the session writes, documents and source alike. The
|
|
20
|
+
* vendored guidance exempts code comments, but a comment is read by a person
|
|
21
|
+
* and generated artifacts are assembled from the strings sitting in source, so
|
|
22
|
+
* exempting them reopens the gap for exactly the outputs that were reported.
|
|
23
|
+
* `isLintTarget(path, 'prose')` restores the narrow reading. Only dependencies,
|
|
24
|
+
* VCS internals, lockfiles, and binary or image files are skipped; build output
|
|
25
|
+
* is checked like anything else, and fenced code blocks are dropped from
|
|
26
|
+
* documents. Findings are requests to confirm, not
|
|
27
|
+
* verdicts — a settled idiom or a verbatim quotation is allowed to stay.
|
|
28
|
+
*
|
|
29
|
+
* Zero dependencies, CommonJS, so ~/.claude/cache-monitor-hook.cjs can require
|
|
30
|
+
* it standalone.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
// Documents. Under the `prose` scope these are the only files checked.
|
|
36
|
+
const PROSE_EXTENSIONS = new Set(['.md', '.mdx', '.markdown', '.txt', '.rst', '.adoc']);
|
|
37
|
+
|
|
38
|
+
// Only two kinds of path are skipped: installed dependencies and VCS
|
|
39
|
+
// internals, neither of which anyone in this session wrote. Build output is
|
|
40
|
+
// deliberately NOT on this list. Generated artifacts are the files a reader
|
|
41
|
+
// actually receives, so exempting `dist/` or `build/` would exempt the very
|
|
42
|
+
// documents the check exists for.
|
|
43
|
+
const SKIP_PATH = /(^|[\\/])(node_modules|\.git|.*\.min\.[a-z]+|.*-lock\.json|.*\.lock)([\\/]|$)/i;
|
|
44
|
+
const BINARY_EXTENSIONS = new Set([
|
|
45
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico', '.pdf', '.zip', '.gz', '.tar',
|
|
46
|
+
'.mp3', '.mp4', '.wav', '.mov', '.woff', '.woff2', '.ttf', '.otf', '.map', '.bin',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Figurative vocabulary (guidance 3.4). This is the clause human review misses
|
|
51
|
+
* most often, because each individual phrase reads fine in isolation — it is
|
|
52
|
+
* only against the rule that it is standing where a plain noun or verb should.
|
|
53
|
+
* Add a line here when a new one shows up; that is the whole maintenance story.
|
|
54
|
+
*/
|
|
55
|
+
const METAPHOR_LEXICON = [
|
|
56
|
+
// `자리` is the single most common substitution for a plain noun. Physical
|
|
57
|
+
// seating is the rare literal use, so those modifiers are excluded and
|
|
58
|
+
// everything else is raised for confirmation.
|
|
59
|
+
{ re: /(?<!빈 |좌석 |앞 |뒤 |옆 )(?:^|(?<=\s))자리(?:에서|에서는|에|로|가|는|다|입니다|였습니다)?(?=\s|$|[.,·)])/, fix: "'지점'·'상황'·'부분'처럼 뜻을 그대로 담은 명사로 바꿉니다" },
|
|
60
|
+
{ re: /[가-힣]+의\s*흐름/, fix: "'방향'·'순서'·'경과'로 바꿉니다" },
|
|
61
|
+
{ re: /닿(?:는다|습니다|아|는 지점)/, fix: "'겨냥하다'·'해당하다'처럼 동작을 그대로 서술합니다" },
|
|
62
|
+
{ re: /박(?:아 두|혀 있|아 넣)/, fix: "'명시하다'·'기록하다'로 바꿉니다" },
|
|
63
|
+
{ re: /걷어내/, fix: "'없애다'·'제거하다'로 바꿉니다" },
|
|
64
|
+
{ re: /손대(?:는|지|어)/, fix: "'수정하다'·'변경하다'로 바꿉니다" },
|
|
65
|
+
{ re: /발목을 잡/, fix: '무엇이 어떻게 막는지 그대로 서술합니다' },
|
|
66
|
+
{ re: /민낯|속살/, fix: "'실제 상태'·'내부 구조'로 바꿉니다" },
|
|
67
|
+
{ re: /열쇠(?:다|입니다|가 된다)/, fix: "'핵심 조건'·'결정 요인'으로 바꿉니다" },
|
|
68
|
+
{ re: /물꼬|신호탄|분수령|기폭제/, fix: '무엇이 시작되고 무엇이 바뀌는지 그대로 적습니다' },
|
|
69
|
+
{ re: /판(?:을|도가|이)\s*(?:흔들|바뀌|뒤집)/, fix: '무엇이 어떻게 달라지는지 그대로 적습니다' },
|
|
70
|
+
{ re: /몸집|심장부|두뇌 역할/, fix: "'규모'·'중심 구성 요소'로 바꿉니다" },
|
|
71
|
+
{ re: /벽에 부딪|길을 열|문을 열/, fix: '무엇이 막히고 무엇이 가능해지는지 그대로 적습니다' },
|
|
72
|
+
{ re: /갈림길|갈리는 지점/, fix: "'결정이 나뉘는 조건'으로 바꿉니다" },
|
|
73
|
+
{ re: /깨어나|잠들어 있/, fix: "'동작을 시작하다'·'실행되지 않고 있다'로 바꿉니다" },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
// Guidance 3.2/3.3: literal renderings of English noun phrases.
|
|
77
|
+
const TRANSLATIONESE = [
|
|
78
|
+
{ re: /에 대한/, fix: '서술어로 풀어 씁니다' },
|
|
79
|
+
{ re: /[을를] 위한/, fix: '서술어로 풀어 씁니다' },
|
|
80
|
+
{ re: /되어지/, fix: '이중 피동을 없애고 능동이나 단일 피동으로 씁니다' },
|
|
81
|
+
{ re: /하는 것을 통해/, fix: "'~해서'·'~함으로써'로 줄입니다" },
|
|
82
|
+
{ re: /라고 할 수 있다/, fix: '단정하거나 근거를 붙여 서술합니다' },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// Guidance 3.7: a period belongs after a 종결어미, not after a nominal ending.
|
|
86
|
+
const NOMINAL_ENDING = /(?:음|함|됨|임|점|론|양|성|화)\.$/;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Which files the checker opens.
|
|
90
|
+
*
|
|
91
|
+
* `prose` follows the guidance's own exemption list: documents only, because
|
|
92
|
+
* code and comments are excluded there. `all` is the scope users asked for
|
|
93
|
+
* once they saw what the narrow reading costs — Korean written into a comment,
|
|
94
|
+
* a UI string, or a template ends up in front of a reader exactly like a
|
|
95
|
+
* document does, and a generated PDF is assembled from those strings. Under
|
|
96
|
+
* `all` the only things skipped are installed dependencies, lockfiles, and
|
|
97
|
+
* files that are not text.
|
|
98
|
+
*
|
|
99
|
+
* When the scope is `all` the injected guidance says so too (see
|
|
100
|
+
* korean-style.js), so the model is told the same rule the checker enforces.
|
|
101
|
+
*/
|
|
102
|
+
function isLintTarget(filePath, scope = 'all') {
|
|
103
|
+
if (!filePath) return false;
|
|
104
|
+
if (SKIP_PATH.test(filePath)) return false;
|
|
105
|
+
const dot = filePath.lastIndexOf('.');
|
|
106
|
+
const ext = dot === -1 ? '' : filePath.slice(dot).toLowerCase();
|
|
107
|
+
if (PROSE_EXTENSIONS.has(ext)) return true;
|
|
108
|
+
if (scope !== 'all') return false;
|
|
109
|
+
if (!ext) return false;
|
|
110
|
+
return !BINARY_EXTENSIONS.has(ext);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Kept for callers that only ever meant documents.
|
|
114
|
+
function isProseFile(filePath) {
|
|
115
|
+
return isLintTarget(filePath, 'prose');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Drop fenced code blocks and inline code so snippets never trip the rules. */
|
|
119
|
+
function stripCode(text) {
|
|
120
|
+
const out = [];
|
|
121
|
+
let fenced = false;
|
|
122
|
+
for (const line of String(text).split('\n')) {
|
|
123
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
124
|
+
fenced = !fenced;
|
|
125
|
+
out.push('');
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
out.push(fenced ? '' : line.replace(/`[^`]*`/g, ' '));
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function hasKorean(s) {
|
|
134
|
+
return /[가-힣]/.test(s);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Lint one document. Returns `[{ line, rule, hit, fix }]`, empty when clean.
|
|
139
|
+
* `lines` are 1-indexed against the original text so the model can jump
|
|
140
|
+
* straight to the offending line.
|
|
141
|
+
*/
|
|
142
|
+
function lintKoreanText(text, { maxFindings = 20, code = false } = {}) {
|
|
143
|
+
const findings = [];
|
|
144
|
+
const lines = stripCode(text);
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < lines.length; i++) {
|
|
147
|
+
const line = lines[i];
|
|
148
|
+
if (!hasKorean(line)) continue;
|
|
149
|
+
// Reference lists and link lines are citations, not authored prose.
|
|
150
|
+
if (/^\s*\[\d+\]:/.test(line)) continue;
|
|
151
|
+
// A document that teaches the rules has to spell the banned form out. Its
|
|
152
|
+
// headings and its ✗/○ example pairs are quotations of the rule, not
|
|
153
|
+
// breaches of it, so they are left alone.
|
|
154
|
+
if (/^\s*#/.test(line)) continue;
|
|
155
|
+
if (/[✗○✘❌⭕]/.test(line)) continue;
|
|
156
|
+
|
|
157
|
+
const at = i + 1;
|
|
158
|
+
const push = (rule, hit, fix) => {
|
|
159
|
+
if (findings.length < maxFindings) findings.push({ line: at, rule, hit, fix });
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// In a source file `|` is an operator, so only the dashes are checked
|
|
163
|
+
// there; in a document a leading `|` is a table row rather than a sentence.
|
|
164
|
+
const sep = line.match(code ? /[—ㅡ]/ : /[—ㅡ|]/);
|
|
165
|
+
if (sep && !(sep[0] === '|' && /^\s*\|/.test(line))) {
|
|
166
|
+
push('구분자', sep[0], '접속사나 쉼표, 가운뎃점(·)으로 바꿉니다');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const { re, fix } of TRANSLATIONESE) {
|
|
170
|
+
const m = line.match(re);
|
|
171
|
+
if (m) push('번역체', m[0], fix);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const { re, fix } of METAPHOR_LEXICON) {
|
|
175
|
+
const m = line.match(re);
|
|
176
|
+
if (m) push('비유 어휘', m[0], fix);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const chunk of line.split(/[.,·\n]/)) {
|
|
180
|
+
const count = (chunk.match(/[가-힣]의(?=\s|[가-힣])/g) || []).length;
|
|
181
|
+
if (count >= 3) {
|
|
182
|
+
push("'의' 반복", chunk.trim().slice(0, 30), '생략된 문장 성분이 없는지 확인합니다');
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const trimmed = line.trim();
|
|
188
|
+
if (NOMINAL_ENDING.test(trimmed) && !/^[#>\-*\d]/.test(trimmed)) {
|
|
189
|
+
push('명사형 종결', trimmed.slice(-8), "마침표를 빼거나 '~습니다'로 끝맺습니다");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return findings;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Pull the text a Write/Edit/MultiEdit call just put on disk. */
|
|
197
|
+
function writtenTextOf(toolName, toolInput) {
|
|
198
|
+
if (!toolInput || typeof toolInput !== 'object') return null;
|
|
199
|
+
if (toolName === 'Write') return typeof toolInput.content === 'string' ? toolInput.content : null;
|
|
200
|
+
if (toolName === 'Edit') return typeof toolInput.new_string === 'string' ? toolInput.new_string : null;
|
|
201
|
+
if (toolName === 'MultiEdit' || toolName === 'NotebookEdit') {
|
|
202
|
+
const edits = Array.isArray(toolInput.edits) ? toolInput.edits : [];
|
|
203
|
+
const joined = edits.map((e) => (e && typeof e.new_string === 'string' ? e.new_string : '')).join('\n');
|
|
204
|
+
return joined || null;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Full check for one PostToolUse payload. Returns null when there is nothing to
|
|
211
|
+
* say, which is the common case and must stay cheap.
|
|
212
|
+
*/
|
|
213
|
+
function lintToolUse(context, { scope = 'all' } = {}) {
|
|
214
|
+
if (!context) return null;
|
|
215
|
+
const toolName = context.tool_name;
|
|
216
|
+
const toolInput = context.tool_input;
|
|
217
|
+
const filePath = toolInput && typeof toolInput.file_path === 'string' ? toolInput.file_path : '';
|
|
218
|
+
if (!isLintTarget(filePath, scope)) return null;
|
|
219
|
+
|
|
220
|
+
const text = writtenTextOf(toolName, toolInput);
|
|
221
|
+
if (!text || !hasKorean(text)) return null;
|
|
222
|
+
|
|
223
|
+
const findings = lintKoreanText(text, { code: !isProseFile(filePath) });
|
|
224
|
+
if (findings.length === 0) return null;
|
|
225
|
+
return { filePath, findings };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Render findings as the message handed back to the model. */
|
|
229
|
+
function formatFindings(filePath, findings) {
|
|
230
|
+
const head = `[korean-style] ${filePath} 에 문체 규약 위반 ${findings.length}건이 있습니다. 파일을 고친 뒤 계속하십시오.`;
|
|
231
|
+
const body = findings.map((f) => ` ${f.line}행 ${f.rule}: "${f.hit}" 이 걸렸습니다. ${f.fix}`);
|
|
232
|
+
const tail = ' (원문 인용이거나 이미 굳은 표현이면 그대로 두고, 그 이유를 한 줄로 밝히십시오.)';
|
|
233
|
+
return [head, ...body, tail].join('\n');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = {
|
|
237
|
+
METAPHOR_LEXICON,
|
|
238
|
+
isLintTarget,
|
|
239
|
+
isProseFile,
|
|
240
|
+
lintKoreanText,
|
|
241
|
+
writtenTextOf,
|
|
242
|
+
lintToolUse,
|
|
243
|
+
formatFindings,
|
|
244
|
+
};
|
package/src/korean-style.js
CHANGED
|
@@ -117,6 +117,27 @@ export function koreanStyleText() {
|
|
|
117
117
|
* "the user is asking about Korean writing rules" instead of "these rules
|
|
118
118
|
* govern how I write from now on".
|
|
119
119
|
*/
|
|
120
|
+
// The injected scope sentence and the write-time checker have to agree. When
|
|
121
|
+
// they disagree the model is told one thing and corrected for another, which
|
|
122
|
+
// is how the August 2026 report started: the scope line read as excluding
|
|
123
|
+
// documents, so a session shipped 18 em dashes into a markdown deliverable
|
|
124
|
+
// while the guidance was active. These lines are therefore derived from the
|
|
125
|
+
// same `lintScope` setting the checker reads.
|
|
126
|
+
export function koreanScopeLines(cfg = loadConfig()) {
|
|
127
|
+
const scope = cfg?.koreanStyle?.lintScope === 'prose' ? 'prose' : 'all';
|
|
128
|
+
if (scope === 'prose') {
|
|
129
|
+
return [
|
|
130
|
+
'적용 대상: 대화 답변, 그리고 새로 작성하거나 수정하는 마크다운·문서·보고서의 한국어 산문을 모두 포함합니다.',
|
|
131
|
+
'적용 예외: 원문을 그대로 옮기는 인용, 코드, 코드 주석, 그리고 프로젝트의 기존 표기 관례를 따라야 하는 커밋 메시지와 로그 문자열입니다.',
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
return [
|
|
135
|
+
'적용 대상: 대화 답변, 그리고 세션이 쓰거나 고치는 모든 파일의 한국어를 포함합니다. 문서와 보고서는 물론이고 코드 주석, 화면에 나가는 문자열, 자막과 템플릿, 스크립트가 읽어 산출물을 만드는 데이터 파일까지 모두 해당합니다.',
|
|
136
|
+
'적용 예외: 원문을 그대로 옮기는 인용과, 프로젝트의 기존 표기 관례를 따라야 하는 커밋 메시지와 로그 문자열입니다.',
|
|
137
|
+
'쓰기 시점에 기계 검사가 함께 돌아갑니다. 위반을 알리면 그 파일을 고친 뒤에 다음 작업으로 넘어가십시오.',
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
|
|
120
141
|
export function koreanStyleInjection({ cfg = loadConfig() } = {}) {
|
|
121
142
|
if (!koreanStyleEnabled(cfg)) return null;
|
|
122
143
|
const text = koreanStyleText();
|
|
@@ -124,8 +145,7 @@ export function koreanStyleInjection({ cfg = loadConfig() } = {}) {
|
|
|
124
145
|
return [
|
|
125
146
|
'[claude-token-saver korean-style] 이 세션에서 한국어를 출력할 때는 아래 지침을 따르십시오.',
|
|
126
147
|
'이 지침은 사용자가 claude-token-saver에 설정한 것입니다.',
|
|
127
|
-
|
|
128
|
-
'적용 예외: 원문을 그대로 옮기는 인용, 코드, 코드 주석, 그리고 프로젝트의 기존 표기 관례를 따라야 하는 커밋 메시지와 로그 문자열입니다.',
|
|
148
|
+
...koreanScopeLines(cfg),
|
|
129
149
|
'표기 규칙(예외 없이 적용): 도구 호출 인자에 한국어를 비롯한 비ASCII 문자열을 담을 때에는 반드시 리터럴 UTF-8로 작성하고, \\uXXXX 유니코드 이스케이프로는 절대 작성하지 마십시오. 이스케이프로 작성하면 글자가 깨진 채 파일에 기록되는 사례가 자주 발생합니다.',
|
|
130
150
|
`(출처: ${KOREAN_STYLE_SOURCE})`,
|
|
131
151
|
'',
|