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.
- package/LICENSE +21 -0
- package/README.ko.md +637 -0
- package/README.md +758 -0
- package/bin/cli.js +801 -0
- package/examples/statusline-command.ps1 +43 -0
- package/examples/statusline-command.sh +36 -0
- package/package.json +62 -0
- package/presets/cohesion/cohesion-en.md +26 -0
- package/presets/doc2md/convert.py +363 -0
- package/presets/korean-style/LICENSE-fluent-korean +21 -0
- package/presets/korean-style/fluent-korean.md +52 -0
- package/presets/korean-style/supplement.md +93 -0
- package/presets/model-rules.json +115 -0
- package/presets/ratchet-rules.json +38 -0
- package/src/advice.js +564 -0
- package/src/agents.js +52 -0
- package/src/brief.js +264 -0
- package/src/caps-cache.js +84 -0
- package/src/cli-args.js +51 -0
- package/src/cohesion.js +70 -0
- package/src/commands/brief.js +31 -0
- package/src/commands/cohesion.js +59 -0
- package/src/commands/compact-window.js +93 -0
- package/src/commands/doc2md.js +166 -0
- package/src/commands/feedback.js +132 -0
- package/src/commands/handoff.js +33 -0
- package/src/commands/harness.js +459 -0
- package/src/commands/history.js +46 -0
- package/src/commands/install.js +358 -0
- package/src/commands/korean.js +220 -0
- package/src/commands/last.js +151 -0
- package/src/commands/mode.js +46 -0
- package/src/commands/route-scan.js +454 -0
- package/src/commands/seed.js +105 -0
- package/src/commands/uninstall.js +42 -0
- package/src/commands/update-check.js +77 -0
- package/src/commands/upgrade.js +68 -0
- package/src/compact-window.js +205 -0
- package/src/config.js +232 -0
- package/src/cost.js +253 -0
- package/src/debug.js +29 -0
- package/src/demo.js +331 -0
- package/src/doc2md-ledger.cjs +227 -0
- package/src/doc2md.cjs +997 -0
- package/src/fig2md-runner.cjs +21 -0
- package/src/fig2md.cjs +191 -0
- package/src/first-run-note.js +63 -0
- package/src/format-time.js +44 -0
- package/src/formatters/csv.js +8 -0
- package/src/formatters/json.js +3 -0
- package/src/formatters/statusline.js +750 -0
- package/src/formatters/table.js +299 -0
- package/src/handoff.js +161 -0
- package/src/harness-analyzer.cjs +264 -0
- package/src/harness-templates.js +153 -0
- package/src/harness.js +613 -0
- package/src/history.js +383 -0
- package/src/hook-manager.js +96 -0
- package/src/hook.cjs +196 -0
- package/src/installer.js +614 -0
- package/src/korean-lint.cjs +303 -0
- package/src/korean-style.js +187 -0
- package/src/litellm-budget.js +223 -0
- package/src/model-alias.js +484 -0
- package/src/model-rules.js +527 -0
- package/src/month-spend.js +47 -0
- package/src/parser.js +330 -0
- package/src/paths.js +41 -0
- package/src/prompt.js +52 -0
- package/src/route-scan.js +832 -0
- package/src/savings-ledger.js +137 -0
- package/src/seed-rules.js +280 -0
- package/src/session-cache.js +160 -0
- package/src/session-records.js +188 -0
- package/src/stats.js +380 -0
- package/src/stdin-payload.js +122 -0
- package/src/subagent-records.js +214 -0
- package/src/update-check.js +201 -0
- 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
|
+
}
|