sprag-cli 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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +637 -0
  3. package/README.md +758 -0
  4. package/bin/cli.js +801 -0
  5. package/examples/statusline-command.ps1 +43 -0
  6. package/examples/statusline-command.sh +36 -0
  7. package/package.json +62 -0
  8. package/presets/cohesion/cohesion-en.md +26 -0
  9. package/presets/doc2md/convert.py +363 -0
  10. package/presets/korean-style/LICENSE-fluent-korean +21 -0
  11. package/presets/korean-style/fluent-korean.md +52 -0
  12. package/presets/korean-style/supplement.md +93 -0
  13. package/presets/model-rules.json +115 -0
  14. package/presets/ratchet-rules.json +38 -0
  15. package/src/advice.js +564 -0
  16. package/src/agents.js +52 -0
  17. package/src/brief.js +264 -0
  18. package/src/caps-cache.js +84 -0
  19. package/src/cli-args.js +51 -0
  20. package/src/cohesion.js +70 -0
  21. package/src/commands/brief.js +31 -0
  22. package/src/commands/cohesion.js +59 -0
  23. package/src/commands/compact-window.js +93 -0
  24. package/src/commands/doc2md.js +166 -0
  25. package/src/commands/feedback.js +132 -0
  26. package/src/commands/handoff.js +33 -0
  27. package/src/commands/harness.js +459 -0
  28. package/src/commands/history.js +46 -0
  29. package/src/commands/install.js +358 -0
  30. package/src/commands/korean.js +220 -0
  31. package/src/commands/last.js +151 -0
  32. package/src/commands/mode.js +46 -0
  33. package/src/commands/route-scan.js +454 -0
  34. package/src/commands/seed.js +105 -0
  35. package/src/commands/uninstall.js +42 -0
  36. package/src/commands/update-check.js +77 -0
  37. package/src/commands/upgrade.js +68 -0
  38. package/src/compact-window.js +205 -0
  39. package/src/config.js +232 -0
  40. package/src/cost.js +253 -0
  41. package/src/debug.js +29 -0
  42. package/src/demo.js +331 -0
  43. package/src/doc2md-ledger.cjs +227 -0
  44. package/src/doc2md.cjs +997 -0
  45. package/src/fig2md-runner.cjs +21 -0
  46. package/src/fig2md.cjs +191 -0
  47. package/src/first-run-note.js +63 -0
  48. package/src/format-time.js +44 -0
  49. package/src/formatters/csv.js +8 -0
  50. package/src/formatters/json.js +3 -0
  51. package/src/formatters/statusline.js +750 -0
  52. package/src/formatters/table.js +299 -0
  53. package/src/handoff.js +161 -0
  54. package/src/harness-analyzer.cjs +264 -0
  55. package/src/harness-templates.js +153 -0
  56. package/src/harness.js +613 -0
  57. package/src/history.js +383 -0
  58. package/src/hook-manager.js +96 -0
  59. package/src/hook.cjs +196 -0
  60. package/src/installer.js +614 -0
  61. package/src/korean-lint.cjs +303 -0
  62. package/src/korean-style.js +187 -0
  63. package/src/litellm-budget.js +223 -0
  64. package/src/model-alias.js +484 -0
  65. package/src/model-rules.js +527 -0
  66. package/src/month-spend.js +47 -0
  67. package/src/parser.js +330 -0
  68. package/src/paths.js +41 -0
  69. package/src/prompt.js +52 -0
  70. package/src/route-scan.js +832 -0
  71. package/src/savings-ledger.js +137 -0
  72. package/src/seed-rules.js +280 -0
  73. package/src/session-cache.js +160 -0
  74. package/src/session-records.js +188 -0
  75. package/src/stats.js +380 -0
  76. package/src/stdin-payload.js +122 -0
  77. package/src/subagent-records.js +214 -0
  78. package/src/update-check.js +201 -0
  79. package/src/window-labels.js +64 -0
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Subcommand: install — write the Claude Code auto-trigger skill so the
3
+ * user can just mention chip wording and Claude responds. v2.6.0 dropped
4
+ * the redundant /token-monitor slash command in favor of the skill alone;
5
+ * a legacy command file is removed automatically. Cross-platform.
6
+ * claude-token-saver install # install/update the skill
7
+ * claude-token-saver install --force # overwrite existing skill file
8
+ * claude-token-saver install --yes # take the defaults instead of asking
9
+ * claude-token-saver install --no-input # same as --yes; also implied by CTS_NO_INPUT=1
10
+ */
11
+
12
+ import { debug } from '../debug.js';
13
+
14
+ /** Thrown when the user declines an optional step, so it is not reported as a failure. */
15
+ class SkipStep extends Error {}
16
+
17
+ export async function run({ hasFlag }) {
18
+ const { installAll } = await import('../installer.js');
19
+ const { userLanguage, languageDecided, setUserLanguage } = await import('../config.js');
20
+ const { canPrompt, confirm } = await import('../prompt.js');
21
+ const force = hasFlag('--force');
22
+ // Two features below write to ~/.claude and cost tokens in every session,
23
+ // so the install shows what they contain and asks before turning them on.
24
+ // Asking is only possible with a human attached: postinstall, CI and pipes
25
+ // fall through to the previous automatic defaults so an unattended upgrade
26
+ // behaves exactly as it did before.
27
+ const interactive = canPrompt() && !hasFlag('--yes') && !hasFlag('--no-input');
28
+
29
+ // Output language, decided first because every line below it — and every
30
+ // briefing the hooks inject from here on — is written in it. Until now it
31
+ // fell back to English with no question asked, so a Korean user read English
32
+ // reports until they happened to find `mode ko`.
33
+ //
34
+ // The locale is the default, not the answer: a terminal user gets to
35
+ // overrule it either way. An unattended install records the detected locale
36
+ // rather than leaving the setting blank, because "blank" silently means
37
+ // English — the one outcome a Korean-locale machine should not get by
38
+ // default. Already decided means never asked again.
39
+ try {
40
+ if (!languageDecided() && !process.env.CTS_LANG) {
41
+ const { koreanLocaleDetected } = await import('../korean-style.js');
42
+ const detected = koreanLocaleDetected() ? 'ko' : 'en';
43
+ console.log('');
44
+ console.log(' language: reports, warnings and session briefings are written in this language.');
45
+ console.log(` detected locale: ${detected === 'ko' ? 'Korean' : 'not Korean (English)'}`);
46
+ let chosen = detected;
47
+ if (interactive) {
48
+ const ko = await confirm(' Use Korean? (no = English)', { defaultValue: detected === 'ko' });
49
+ chosen = ko ? 'ko' : 'en';
50
+ }
51
+ setUserLanguage(chosen);
52
+ console.log(` language: set to ${chosen === 'ko' ? '한국어' : 'English'} — change it any time with \`claude-token-saver mode lang=${chosen === 'ko' ? 'en' : 'ko'}\``);
53
+ } else if (process.env.CTS_LANG) {
54
+ // Escape hatch for scripted installs, which cannot answer a prompt but
55
+ // do know which language the machine's user reads.
56
+ const forced = setUserLanguage(process.env.CTS_LANG);
57
+ console.log('');
58
+ console.log(forced
59
+ ? ` language: set to ${forced} (CTS_LANG)`
60
+ : ` language: ignored CTS_LANG=${process.env.CTS_LANG} — use 'ko' or 'en'`);
61
+ }
62
+ } catch (e) {
63
+ debug('install:language', e); // the English fallback still works
64
+ }
65
+ const lang = userLanguage();
66
+
67
+ const print = (kind, r) => {
68
+ const verb = r.action === 'exists' ? 'already exists' : r.action;
69
+ console.log(` ${kind}: ${r.path} (${verb})`);
70
+ };
71
+ const r = installAll({ force });
72
+ print('skill', r.skill);
73
+ print('SessionStart hook (route-scan)', r.sessionStartHook);
74
+ print('UserPromptSubmit hook (brief)', r.briefHook);
75
+ {
76
+ let s = r.statusline;
77
+ // A different statusline is already installed. Replacing it silently
78
+ // would be rude and leaving it silently loses the tool's main surface,
79
+ // so with a human attached the install asks. Default is "keep yours":
80
+ // an accidental Enter must not clobber someone's custom statusline.
81
+ if (s.action === 'skipped' && s.conflict && interactive) {
82
+ console.log('');
83
+ console.log(lang === 'ko'
84
+ ? ` statusline: 기존 statusline이 이미 설정되어 있습니다: ${s.existingCommand}`
85
+ : ` statusline: an existing statusline is already configured: ${s.existingCommand}`);
86
+ console.log(lang === 'ko'
87
+ ? ' 교체하면 토큰·캐시·상한 경고가 statusline에 표시됩니다. 기존 설정은 사라집니다.'
88
+ : ' replacing it shows token/cache/cap warnings in the statusline; the current one is removed.');
89
+ const replace = await confirm(lang === 'ko'
90
+ ? ' claude-token-saver statusline으로 교체할까요?'
91
+ : ' Replace it with the claude-token-saver statusline?', { defaultValue: false });
92
+ if (replace) {
93
+ const { installStatusline } = await import('../installer.js');
94
+ s = installStatusline({ force: true });
95
+ } else {
96
+ s = { ...s, reason: lang === 'ko'
97
+ ? '기존 statusline을 유지했습니다. 교체하려면 `claude-token-saver install --force`'
98
+ : 'kept your statusline — replace later with `claude-token-saver install --force`' };
99
+ }
100
+ }
101
+ const verb = s.action === 'exists' ? 'already configured (refreshInterval=5)'
102
+ : s.action === 'skipped' ? `skipped — ${s.reason}`
103
+ : s.reason ? `${s.action} — ${s.reason}`
104
+ : s.action;
105
+ console.log(` statusline: ${s.path} (${verb})`);
106
+ }
107
+ if (r.legacy.action === 'removed') {
108
+ print('legacy /token-monitor', r.legacy);
109
+ console.log(' (consolidated into the skill — same workflow, triggered by intent)');
110
+ }
111
+ // First-time setup: analyze existing session logs right away so the
112
+ // very first session already sees delegation candidates — without this,
113
+ // the initial scan would only start from the first session's hook and
114
+ // its results would surface one session late. Runs inline (a few
115
+ // seconds on a typical 14-day history): a detached child can be reaped
116
+ // by sandboxed installers before it finishes, and postinstall carries
117
+ // `|| true` so a failure here never breaks the install.
118
+ {
119
+ const rs = await import('../route-scan.js');
120
+ if (!rs.readRouteScan()) {
121
+ try {
122
+ console.log('');
123
+ console.log(lang === 'ko'
124
+ ? ' route-scan: 기존 세션 로그의 사용 패턴을 분석하는 중...'
125
+ : ' route-scan: analyzing usage patterns in your existing session logs...');
126
+ const cache = await rs.runRouteScan({ days: 14 });
127
+ console.log(lang === 'ko'
128
+ ? ` route-scan: 에피소드 ${cache.totalEpisodes}건 분석 완료 — 위임 후보 ${cache.candidates.length}건.`
129
+ : ` route-scan: analyzed ${cache.totalEpisodes} episodes — ${cache.candidates.length} delegation candidate(s).`);
130
+ console.log(lang === 'ko'
131
+ ? ' (다음 Claude Code 세션에서 티어 위임 후보가 표시됩니다)'
132
+ : ' (candidates surface in your next Claude Code session)');
133
+ } catch (e) { debug('install:route-scan-seed', e); /* hook-triggered scan covers it on the first session instead */ }
134
+ }
135
+ }
136
+ // Harness, set up as part of the install rather than left as a manual
137
+ // follow-up step. The 🅷 statusline segment and the ratchet rules that
138
+ // route-scan promotes both depend on the block existing in CLAUDE.md, so
139
+ // an install without it ships a tool that is half wired up.
140
+ //
141
+ // Scope is global (~/.claude/CLAUDE.md): a global install is not tied to
142
+ // any one project, and the harness principles are project-independent.
143
+ // Only ever ADDS — harnessInit appends its own marked block, backs up the
144
+ // previous file, and leaves surrounding content untouched. Skipped when a
145
+ // block is already there (nothing to do) and when CTS_NO_HARNESS=1 is set,
146
+ // for anyone who wants the statusline without the CLAUDE.md rules.
147
+ try {
148
+ const { harnessInit, harnessStatus } = await import('../harness.js');
149
+ const before = harnessStatus(undefined, { scope: 'global' });
150
+ if (process.env.CTS_NO_HARNESS === '1') {
151
+ console.log('');
152
+ console.log(lang === 'ko'
153
+ ? ' harness: CTS_NO_HARNESS=1 이므로 건너뜁니다 (나중에 `harness init --global`).'
154
+ : ' harness: skipped (CTS_NO_HARNESS=1) — run `harness init --global` later.');
155
+ } else if (before.hasBlock) {
156
+ console.log('');
157
+ console.log(lang === 'ko'
158
+ ? ` harness: 이미 설정됨 — 🅷 ${before.configured}/${before.total} (${before.file})`
159
+ : ` harness: already set up — 🅷 ${before.configured}/${before.total} (${before.file})`);
160
+ } else {
161
+ // Show the five principle headings before writing them. The block goes
162
+ // into the file the model reads at the start of every session, so the
163
+ // user should see its contents before agreeing, not after.
164
+ const { HARNESS_SECTIONS } = await import('../harness-templates.js');
165
+ console.log('');
166
+ console.log(lang === 'ko'
167
+ ? ' harness: 다음 5원칙을 ~/.claude/CLAUDE.md 에 추가합니다 (모든 프로젝트에 적용).'
168
+ : ' harness: the following 5 principles would be added to ~/.claude/CLAUDE.md (all projects).');
169
+ for (const s of HARNESS_SECTIONS) {
170
+ console.log(` ${s.heading.replace(/^###\s*/, '')}`);
171
+ }
172
+ console.log(lang === 'ko'
173
+ ? ' 기존 내용은 지우지 않고 뒤에 덧붙이며, 원본은 .bak 으로 백업됩니다.'
174
+ : ' existing content is kept — the block is appended and the original is backed up as .bak.');
175
+ let proceed = true;
176
+ if (interactive) {
177
+ proceed = await confirm(lang === 'ko' ? ' 지금 설정할까요?' : ' Set this up now?', { defaultValue: true });
178
+ }
179
+ if (!proceed) {
180
+ console.log(lang === 'ko'
181
+ ? ' harness: 건너뛰었습니다 — 나중에 `claude-token-saver harness init --global` 로 설정할 수 있습니다.'
182
+ : ' harness: skipped — set it up later with `claude-token-saver harness init --global`.');
183
+ throw new SkipStep();
184
+ }
185
+ const h = harnessInit({ scope: 'global' });
186
+ console.log('');
187
+ for (const p of h.backedUp) {
188
+ console.log(lang === 'ko' ? ` harness: 백업 ${p}` : ` harness: backed up ${p}`);
189
+ }
190
+ for (const p of h.wrote) {
191
+ console.log(lang === 'ko' ? ` harness: 작성 ${p}` : ` harness: wrote ${p}`);
192
+ }
193
+ console.log(lang === 'ko'
194
+ ? ' harness: 5원칙을 ~/.claude/CLAUDE.md 에 설정했습니다 — statusline에 🅷 5/5 가 표시됩니다.'
195
+ : ' harness: 5 principles installed in ~/.claude/CLAUDE.md — the statusline now shows 🅷 5/5.');
196
+ console.log(lang === 'ko'
197
+ ? ' 되돌리려면: claude-token-saver harness uninit --global'
198
+ : ' to undo: claude-token-saver harness uninit --global');
199
+ }
200
+ } catch (e) {
201
+ // A declined prompt already printed its own line; only real failures fall
202
+ // through to the advice below.
203
+ if (e instanceof SkipStep) { /* user said no — nothing to report */ }
204
+ else {
205
+ // Never fail an install over this — the statusline and Skill are already
206
+ // in place, and `harness init` remains available as a manual step.
207
+ debug('install:harness-init', e);
208
+ console.log(lang === 'ko'
209
+ ? ' harness: 자동 설정을 건너뛰었습니다 — `claude-token-saver harness init --global` 로 직접 설정하세요.'
210
+ : ' harness: auto-setup skipped — run `claude-token-saver harness init --global` yourself.');
211
+ }
212
+ }
213
+
214
+ // Korean writing guidance. Decided here rather than left to a command the
215
+ // user has to find, because the people who need it are exactly the ones
216
+ // who would not know to look for it. Enabled when the machine's locale
217
+ // says Korean; left alone once the user has answered either way, so an
218
+ // upgrade never re-enables something they turned off.
219
+ try {
220
+ const ks = await import('../korean-style.js');
221
+ if (process.env.CTS_NO_KOREAN === '1') {
222
+ console.log('');
223
+ console.log(lang === 'ko'
224
+ ? ' korean: CTS_NO_KOREAN=1 이므로 건너뜁니다 (나중에 `korean on`).'
225
+ : ' korean: skipped (CTS_NO_KOREAN=1) — run `korean on` later.');
226
+ } else if (ks.koreanStyleDecided()) {
227
+ console.log('');
228
+ console.log(lang === 'ko'
229
+ ? ` korean: 기존 설정 유지 — 한국어 문체 지침 ${ks.koreanStyleEnabled() ? '켜짐' : '꺼짐'}`
230
+ : ` korean: keeping your setting — Korean writing guidance is ${ks.koreanStyleEnabled() ? 'on' : 'off'}`);
231
+ } else {
232
+ // The locale only decides what the question defaults to. It is a good
233
+ // guess, not an answer, so a user at a terminal gets to overrule it
234
+ // either way — including turning the guidance on where the locale is
235
+ // English but the person writing is not.
236
+ const detected = ks.koreanLocaleDetected();
237
+ console.log('');
238
+ console.log(lang === 'ko'
239
+ ? ' korean: 한국어 문체 지침(fluent-korean)을 세션 시작 시 주입할 수 있습니다.'
240
+ : ' korean: Korean writing guidance (fluent-korean) can be injected at session start.');
241
+ console.log(lang === 'ko'
242
+ ? ' 내용: 조사·어미를 생략하지 않고, 명사구가 아니라 서술어로 문장을 끝맺으며,'
243
+ : ' what it does: keeps particles and endings, ends sentences with a predicate,');
244
+ console.log(lang === 'ko'
245
+ ? ' 번역체 대신 자연스러운 한국어를 쓰도록 지시합니다. 코드와 주석에는 적용되지 않습니다.'
246
+ : ' and asks for idiomatic Korean over translationese. Code and comments are exempt.');
247
+ console.log(lang === 'ko'
248
+ ? ' 비용: 세션당 약 1.5k 토큰(턴마다가 아니라 세션 시작 시 1회, 이후 프롬프트 캐시에 포함).'
249
+ : ' cost: ~1.5k tokens per session (once at session start, cached from the second request on).');
250
+ console.log(lang === 'ko'
251
+ ? ` 출처: ${ks.KOREAN_STYLE_SOURCE}`
252
+ : ` source: ${ks.KOREAN_STYLE_SOURCE}`);
253
+ console.log(lang === 'ko'
254
+ ? ` 감지된 환경: ${detected ? '한국어 (기본값 켬)' : '한국어 아님 (기본값 끔)'}`
255
+ : ` detected locale: ${detected ? 'Korean (default on)' : 'not Korean (default off)'}`);
256
+ let enable = detected;
257
+ if (interactive) {
258
+ enable = await confirm(lang === 'ko' ? ' 켤까요?' : ' Enable it?', { defaultValue: detected });
259
+ }
260
+ // Record the choice only when it is really a choice. An unattended
261
+ // install on a non-Korean machine leaves the setting undecided so that
262
+ // a later run at a terminal still gets to ask, instead of silently
263
+ // inheriting a default the user never saw.
264
+ if (interactive || enable) ks.setKoreanStyleEnabled(enable);
265
+ if (enable) {
266
+ console.log(lang === 'ko'
267
+ ? ' korean: 켰습니다 — 모든 프로젝트의 세션 시작 시 주입됩니다. 끄려면 `claude-token-saver korean off`.'
268
+ : ' korean: enabled — injected at session start in every project. Turn off with `claude-token-saver korean off`.');
269
+ } else {
270
+ console.log(lang === 'ko'
271
+ ? ' korean: 꺼 두었습니다 — 나중에 `claude-token-saver korean on` 으로 켤 수 있습니다.'
272
+ : ' korean: left off — enable later with `claude-token-saver korean on`.');
273
+ }
274
+ }
275
+ } catch (e) {
276
+ debug('install:korean-style', e); // optional feature; never fail install
277
+ }
278
+
279
+ // doc2md. The hook itself is two entries in settings.json and costs
280
+ // nothing until a document shows up, so it goes on with the rest of the
281
+ // install instead of waiting for the user to discover `doc2md on` — the
282
+ // people who would save the most tokens are the ones who never find it.
283
+ // The converter is the expensive half (a venv plus a pip install), so it
284
+ // is only built when a human is attached; an unattended postinstall just
285
+ // prints the command.
286
+ try {
287
+ const { createRequire } = await import('node:module');
288
+ const require = createRequire(import.meta.url);
289
+ const doc2md = require('../doc2md.cjs');
290
+ if (process.env.CTS_NO_DOC2MD === '1') {
291
+ console.log('');
292
+ console.log(lang === 'ko'
293
+ ? ' doc2md: CTS_NO_DOC2MD=1 이므로 건너뜁니다 (나중에 `doc2md on`).'
294
+ : ' doc2md: skipped (CTS_NO_DOC2MD=1) — run `doc2md on` later.');
295
+ } else {
296
+ const { installDoc2mdHook } = await import('../installer.js');
297
+ const res = installDoc2mdHook();
298
+ console.log('');
299
+ console.log(res.action === 'skipped'
300
+ ? ` doc2md: ${res.reason}`
301
+ : ` doc2md: Read/Write hook ${res.action} (${res.path})`);
302
+ console.log(lang === 'ko'
303
+ ? ` 대상 형식: ${doc2md.TARGET_EXTENSIONS.join(' ')} — 모델이 읽기 전에 Markdown으로 변환합니다.`
304
+ : ` formats: ${doc2md.TARGET_EXTENSIONS.join(' ')} — converted to Markdown before the model reads them.`);
305
+ let python = doc2md.findInterpreter();
306
+ if (!python && interactive) {
307
+ const build = await confirm(lang === 'ko'
308
+ ? ' 변환기(markitdown)를 지금 설치할까요? 몇 분 걸립니다.'
309
+ : ' Install the converter (markitdown) now? Takes a few minutes.', { defaultValue: true });
310
+ if (build) {
311
+ const cres = doc2md.installConverter({ onProgress: (m) => console.log(` ${m}`) });
312
+ if (cres.ok) python = cres.python;
313
+ else console.log(` ${cres.reason}: ${cres.detail || ''}`);
314
+ }
315
+ }
316
+ console.log(python
317
+ ? (lang === 'ko' ? ` 변환기: ${python}` : ` converter: ${python}`)
318
+ : (lang === 'ko'
319
+ ? ` 변환기가 없습니다 — \`${doc2md.INSTALL_HINT}\` 를 실행해야 훅이 동작합니다.`
320
+ : ` converter missing — run \`${doc2md.INSTALL_HINT}\` or the hook does nothing.`));
321
+ }
322
+ } catch (e) {
323
+ debug('install:doc2md', e); // optional feature; never fail install
324
+ }
325
+
326
+ // Starter rules. The asking itself belongs to the first session — a
327
+ // postinstall cannot hold a conversation, and a prompt here would be
328
+ // answered by whoever happens to be at the terminal for a set of rules
329
+ // they have not read. All the install does is say what is waiting.
330
+ try {
331
+ const { pendingSeeds } = await import('../seed-rules.js');
332
+ const pending = pendingSeeds();
333
+ if (pending.length > 0) {
334
+ console.log('');
335
+ console.log(lang === 'ko'
336
+ ? ` seed: 추천 룰 ${pending.length}건이 대기 중입니다 (모델 피팅 + 랫쳇 프리셋).`
337
+ : ` seed: ${pending.length} recommended rule(s) are waiting (model-fitting + ratchet presets).`);
338
+ console.log(lang === 'ko'
339
+ ? ' 다음 Claude Code 세션에서 한 건씩 등록할지 물어봅니다. 지금 보려면: claude-token-saver seed'
340
+ : ' the next Claude Code session asks about them one at a time. See them now: claude-token-saver seed');
341
+ }
342
+ } catch (e) {
343
+ debug('install:seed-offer', e); // optional feature; never fail install
344
+ }
345
+
346
+ console.log('');
347
+ console.log('Open Claude Code in any directory and just mention:');
348
+ console.log(' "cache hit rate" / "1M context" / "5H cap" — the skill auto-activates.');
349
+ if (!force) {
350
+ console.log('');
351
+ console.log('Tip: re-run with --force to overwrite the existing skill file.');
352
+ }
353
+ console.log('');
354
+ console.log(lang === 'ko'
355
+ ? '버그 제보·기능 제안: https://github.com/rootstudioyaml/claude-token-saver/issues'
356
+ : 'Bug reports & feature requests: https://github.com/rootstudioyaml/claude-token-saver/issues');
357
+ return;
358
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Subcommand: korean — Korean writing guidance for every session.
3
+ * claude-token-saver korean on # inject at session start, all projects
4
+ * claude-token-saver korean off # stop injecting
5
+ * claude-token-saver korean status # current state, cost, and provenance
6
+ * claude-token-saver korean show # print the guidance itself
7
+ *
8
+ * Why this exists rather than pointing users at Claude Code's output styles:
9
+ * an output style is one global slot, so turning it on takes the slot away
10
+ * from whatever else the user had there, and it has to be configured on every
11
+ * machine. This ships the guidance with the package and delivers it through
12
+ * the SessionStart hook that is already installed, so it applies everywhere
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
23
+ */
24
+
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 }) {
43
+ const sub = args[1] || 'status';
44
+ const ks = await import('../korean-style.js');
45
+ const { userLanguage, loadConfig, saveConfig } = await import('../config.js');
46
+ const lang = userLanguage();
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
+ if (lint.isHtmlFile(file)) text = lint.stripHtml(text);
90
+ const findings = lint.lintKoreanText(text, { code: !lint.isProseFile(file) });
91
+ total += findings.length;
92
+ if (findings.length) console.log(lint.formatFindings(file, findings));
93
+ }
94
+ if (total === 0) {
95
+ console.log(lang === 'ko' ? '문체 규약 위반이 없습니다.' : 'No findings.');
96
+ }
97
+ process.exit(total > 0 ? 1 : 0);
98
+ }
99
+
100
+ // Scope switch: `korean lint scope all|prose`.
101
+ if (sub === 'lint' && args[2] === 'scope') {
102
+ const cfg = loadConfig();
103
+ const next = args[3];
104
+ if (!next) {
105
+ console.log(lang === 'ko'
106
+ ? `검사 범위: ${koreanLintScope(cfg)}`
107
+ : `Check scope: ${koreanLintScope(cfg)}`);
108
+ return;
109
+ }
110
+ if (!['all', 'prose'].includes(next)) {
111
+ console.error('Usage: claude-token-saver korean lint scope [all|prose]');
112
+ process.exit(1);
113
+ }
114
+ cfg.koreanStyle = cfg.koreanStyle || {};
115
+ cfg.koreanStyle.lintScope = next;
116
+ saveConfig(cfg);
117
+ console.log(lang === 'ko'
118
+ ? (next === 'all'
119
+ ? '검사 범위: all — 문서와 코드 주석, UI 문자열까지 세션이 쓴 모든 텍스트 파일을 검사합니다.'
120
+ : '검사 범위: prose — 마크다운과 텍스트 문서만 검사합니다.')
121
+ : (next === 'all'
122
+ ? 'Check scope: all — every text file the session writes, comments and UI strings included.'
123
+ : 'Check scope: prose — documents only.'));
124
+ return;
125
+ }
126
+
127
+ // Enforcement mode: `korean lint block|warn|off`.
128
+ if (sub === 'lint') {
129
+ const mode = args[2];
130
+ const cfg = loadConfig();
131
+ if (!mode) {
132
+ console.log(lang === 'ko'
133
+ ? `쓰기 시점 검사: ${koreanLintMode(cfg)} (범위 ${koreanLintScope(cfg)})`
134
+ : `Write-time check: ${koreanLintMode(cfg)} (scope ${koreanLintScope(cfg)})`);
135
+ return;
136
+ }
137
+ if (!['block', 'warn', 'off'].includes(mode)) {
138
+ console.error('Usage: claude-token-saver korean lint [block|warn|off] | korean lint <file...>');
139
+ process.exit(1);
140
+ }
141
+ cfg.koreanStyle = cfg.koreanStyle || {};
142
+ cfg.koreanStyle.lint = mode;
143
+ saveConfig(cfg);
144
+ const note = {
145
+ block: lang === 'ko' ? '위반을 발견하면 모델에게 되돌려 보내 고치게 합니다.' : 'Findings are handed back to the model as blocking feedback.',
146
+ warn: lang === 'ko' ? '위반을 알리기만 하고 진행을 막지 않습니다.' : 'Findings are printed as a note and do not block.',
147
+ off: lang === 'ko' ? '쓰기 시점 검사를 하지 않습니다.' : 'The write-time check is disabled.',
148
+ }[mode];
149
+ console.log(`${lang === 'ko' ? '쓰기 시점 검사' : 'Write-time check'}: ${mode} — ${note}`);
150
+ return;
151
+ }
152
+
153
+ if (sub === 'show') {
154
+ const text = ks.koreanStyleText();
155
+ if (!text) {
156
+ console.error('Korean style guidance file is missing from the package.');
157
+ process.exit(1);
158
+ }
159
+ console.log(text);
160
+ return;
161
+ }
162
+
163
+ if (sub === 'on' || sub === 'off') {
164
+ const enabled = sub === 'on';
165
+ ks.setKoreanStyleEnabled(enabled);
166
+ const { installKoreanLintHook, removeKoreanLintHook } = await import('../installer.js');
167
+ const hook = enabled ? installKoreanLintHook() : removeKoreanLintHook();
168
+ if (enabled) {
169
+ console.log(lang === 'ko'
170
+ ? '한국어 문체 지침을 켰습니다. 다음 세션부터 모든 프로젝트에 적용됩니다.'
171
+ : 'Korean writing guidance is on. It applies in every project from the next session.');
172
+ console.log(lang === 'ko'
173
+ ? ' 주입 시점: 세션 시작 1회 (매 턴이 아니므로 두 번째 요청부터는 캐시에 올라갑니다)'
174
+ : ' Injected once per session (not per turn), so it rides the prompt cache from the second request on.');
175
+ console.log(lang === 'ko'
176
+ ? ` 출처: ${ks.KOREAN_STYLE_SOURCE}`
177
+ : ` Source: ${ks.KOREAN_STYLE_SOURCE}`);
178
+ if (hook.action === 'skipped') {
179
+ console.log(lang === 'ko'
180
+ ? ` ⚠ 쓰기 시점 검사 훅을 못 걸었습니다: ${hook.reason}`
181
+ : ` ⚠ Could not register the write-time hook: ${hook.reason}`);
182
+ } else {
183
+ console.log(lang === 'ko'
184
+ ? ` 쓰기 시점 검사: ${koreanLintMode(loadConfig())}, 범위 ${koreanLintScope(loadConfig())} (Write·Edit 로 쓴 한국어를 검사합니다)`
185
+ : ` Write-time check: ${koreanLintMode(loadConfig())}, scope ${koreanLintScope(loadConfig())} (runs on Korean written via Write/Edit)`);
186
+ }
187
+ } else {
188
+ console.log(lang === 'ko'
189
+ ? '한국어 문체 지침을 껐습니다. 다음 세션부터 주입하지 않습니다.'
190
+ : 'Korean writing guidance is off. Nothing is injected from the next session.');
191
+ }
192
+ return;
193
+ }
194
+
195
+ // status (default)
196
+ const on = ks.koreanStyleEnabled();
197
+ const text = ks.koreanStyleText();
198
+ // 4 bytes/token is the usual mixed ko/en approximation, same as the ratchet
199
+ // size report — this is the number the user is trading for the style.
200
+ const tokens = text ? Math.round(Buffer.byteLength(text, 'utf8') / 4) : 0;
201
+ if (lang === 'ko') {
202
+ console.log(`한국어 문체 지침: ${on ? '켜짐' : '꺼짐'}`);
203
+ console.log(` 비용: 세션당 약 ${tokens} 토큰 (세션 시작 1회 주입)`);
204
+ console.log(` 쓰기 시점 검사: ${koreanLintMode(loadConfig())}, 범위 ${koreanLintScope(loadConfig())} (바꾸려면 claude-token-saver korean lint block|warn|off, korean lint scope all|prose)`);
205
+ console.log(` 출처: ${ks.KOREAN_STYLE_SOURCE}`);
206
+ console.log(` 라이선스 전문: ${ks.KOREAN_STYLE_LICENSE_PATH}`);
207
+ console.log(on
208
+ ? ' 끄려면: claude-token-saver korean off'
209
+ : ' 켜려면: claude-token-saver korean on');
210
+ } else {
211
+ console.log(`Korean writing guidance: ${on ? 'on' : 'off'}`);
212
+ console.log(` Cost: ~${tokens} tokens per session (injected once at session start)`);
213
+ 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)`);
214
+ console.log(` Source: ${ks.KOREAN_STYLE_SOURCE}`);
215
+ console.log(` License text: ${ks.KOREAN_STYLE_LICENSE_PATH}`);
216
+ console.log(on
217
+ ? ' Turn off with: claude-token-saver korean off'
218
+ : ' Turn on with: claude-token-saver korean on');
219
+ }
220
+ }