claude-token-saver 3.20.0 → 3.21.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/package.json +1 -1
- package/src/commands/install.js +76 -11
- package/src/formatters/statusline.js +4 -3
- package/src/prompt.js +52 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-token-saver",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.21.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/install.js
CHANGED
|
@@ -9,11 +9,21 @@
|
|
|
9
9
|
|
|
10
10
|
import { debug } from '../debug.js';
|
|
11
11
|
|
|
12
|
+
/** Thrown when the user declines an optional step, so it is not reported as a failure. */
|
|
13
|
+
class SkipStep extends Error {}
|
|
14
|
+
|
|
12
15
|
export async function run({ hasFlag }) {
|
|
13
16
|
const { installAll } = await import('../installer.js');
|
|
14
17
|
const { userLanguage } = await import('../config.js');
|
|
18
|
+
const { canPrompt, confirm } = await import('../prompt.js');
|
|
15
19
|
const lang = userLanguage();
|
|
16
20
|
const force = hasFlag('--force');
|
|
21
|
+
// Two features below write to ~/.claude and cost tokens in every session,
|
|
22
|
+
// so the install shows what they contain and asks before turning them on.
|
|
23
|
+
// Asking is only possible with a human attached: postinstall, CI and pipes
|
|
24
|
+
// fall through to the previous automatic defaults so an unattended upgrade
|
|
25
|
+
// behaves exactly as it did before.
|
|
26
|
+
const interactive = canPrompt() && !hasFlag('--yes') && !hasFlag('--no-input');
|
|
17
27
|
const print = (kind, r) => {
|
|
18
28
|
const verb = r.action === 'exists' ? 'already exists' : r.action;
|
|
19
29
|
console.log(` ${kind}: ${r.path} (${verb})`);
|
|
@@ -84,6 +94,30 @@ export async function run({ hasFlag }) {
|
|
|
84
94
|
? ` harness: 이미 설정됨 — 🅷 ${before.configured}/${before.total} (${before.file})`
|
|
85
95
|
: ` harness: already set up — 🅷 ${before.configured}/${before.total} (${before.file})`);
|
|
86
96
|
} else {
|
|
97
|
+
// Show the five principle headings before writing them. The block goes
|
|
98
|
+
// into the file the model reads at the start of every session, so the
|
|
99
|
+
// user should see its contents before agreeing, not after.
|
|
100
|
+
const { HARNESS_SECTIONS } = await import('../harness-templates.js');
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(lang === 'ko'
|
|
103
|
+
? ' harness: 다음 5원칙을 ~/.claude/CLAUDE.md 에 추가합니다 (모든 프로젝트에 적용).'
|
|
104
|
+
: ' harness: the following 5 principles would be added to ~/.claude/CLAUDE.md (all projects).');
|
|
105
|
+
for (const s of HARNESS_SECTIONS) {
|
|
106
|
+
console.log(` ${s.heading.replace(/^###\s*/, '')}`);
|
|
107
|
+
}
|
|
108
|
+
console.log(lang === 'ko'
|
|
109
|
+
? ' 기존 내용은 지우지 않고 뒤에 덧붙이며, 원본은 .bak 으로 백업됩니다.'
|
|
110
|
+
: ' existing content is kept — the block is appended and the original is backed up as .bak.');
|
|
111
|
+
let proceed = true;
|
|
112
|
+
if (interactive) {
|
|
113
|
+
proceed = await confirm(lang === 'ko' ? ' 지금 설정할까요?' : ' Set this up now?', { defaultValue: true });
|
|
114
|
+
}
|
|
115
|
+
if (!proceed) {
|
|
116
|
+
console.log(lang === 'ko'
|
|
117
|
+
? ' harness: 건너뛰었습니다 — 나중에 `claude-token-saver harness init --global` 로 설정할 수 있습니다.'
|
|
118
|
+
: ' harness: skipped — set it up later with `claude-token-saver harness init --global`.');
|
|
119
|
+
throw new SkipStep();
|
|
120
|
+
}
|
|
87
121
|
const h = harnessInit({ scope: 'global' });
|
|
88
122
|
console.log('');
|
|
89
123
|
for (const p of h.backedUp) {
|
|
@@ -100,12 +134,17 @@ export async function run({ hasFlag }) {
|
|
|
100
134
|
: ' to undo: claude-token-saver harness uninit --global');
|
|
101
135
|
}
|
|
102
136
|
} catch (e) {
|
|
137
|
+
// A declined prompt already printed its own line; only real failures fall
|
|
138
|
+
// through to the advice below.
|
|
139
|
+
if (e instanceof SkipStep) { /* user said no — nothing to report */ }
|
|
140
|
+
else {
|
|
103
141
|
// Never fail an install over this — the statusline and Skill are already
|
|
104
142
|
// in place, and `harness init` remains available as a manual step.
|
|
105
143
|
debug('install:harness-init', e);
|
|
106
144
|
console.log(lang === 'ko'
|
|
107
145
|
? ' harness: 자동 설정을 건너뛰었습니다 — `claude-token-saver harness init --global` 로 직접 설정하세요.'
|
|
108
146
|
: ' harness: auto-setup skipped — run `claude-token-saver harness init --global` yourself.');
|
|
147
|
+
}
|
|
109
148
|
}
|
|
110
149
|
|
|
111
150
|
// Korean writing guidance. Decided here rather than left to a command the
|
|
@@ -125,23 +164,49 @@ export async function run({ hasFlag }) {
|
|
|
125
164
|
console.log(lang === 'ko'
|
|
126
165
|
? ` korean: 기존 설정 유지 — 한국어 문체 지침 ${ks.koreanStyleEnabled() ? '켜짐' : '꺼짐'}`
|
|
127
166
|
: ` korean: keeping your setting — Korean writing guidance is ${ks.koreanStyleEnabled() ? 'on' : 'off'}`);
|
|
128
|
-
} else
|
|
129
|
-
|
|
167
|
+
} else {
|
|
168
|
+
// The locale only decides what the question defaults to. It is a good
|
|
169
|
+
// guess, not an answer, so a user at a terminal gets to overrule it
|
|
170
|
+
// either way — including turning the guidance on where the locale is
|
|
171
|
+
// English but the person writing is not.
|
|
172
|
+
const detected = ks.koreanLocaleDetected();
|
|
130
173
|
console.log('');
|
|
131
174
|
console.log(lang === 'ko'
|
|
132
|
-
? ' korean: 한국어
|
|
133
|
-
: ' korean: Korean
|
|
175
|
+
? ' korean: 한국어 문체 지침(fluent-korean)을 세션 시작 시 주입할 수 있습니다.'
|
|
176
|
+
: ' korean: Korean writing guidance (fluent-korean) can be injected at session start.');
|
|
177
|
+
console.log(lang === 'ko'
|
|
178
|
+
? ' 내용: 조사·어미를 생략하지 않고, 명사구가 아니라 서술어로 문장을 끝맺으며,'
|
|
179
|
+
: ' what it does: keeps particles and endings, ends sentences with a predicate,');
|
|
180
|
+
console.log(lang === 'ko'
|
|
181
|
+
? ' 번역체 대신 자연스러운 한국어를 쓰도록 지시합니다. 코드와 주석에는 적용되지 않습니다.'
|
|
182
|
+
: ' and asks for idiomatic Korean over translationese. Code and comments are exempt.');
|
|
183
|
+
console.log(lang === 'ko'
|
|
184
|
+
? ' 비용: 세션당 약 1.5k 토큰(턴마다가 아니라 세션 시작 시 1회, 이후 프롬프트 캐시에 포함).'
|
|
185
|
+
: ' cost: ~1.5k tokens per session (once at session start, cached from the second request on).');
|
|
134
186
|
console.log(lang === 'ko'
|
|
135
187
|
? ` 출처: ${ks.KOREAN_STYLE_SOURCE}`
|
|
136
188
|
: ` source: ${ks.KOREAN_STYLE_SOURCE}`);
|
|
137
189
|
console.log(lang === 'ko'
|
|
138
|
-
?
|
|
139
|
-
:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
190
|
+
? ` 감지된 환경: ${detected ? '한국어 (기본값 켬)' : '한국어 아님 (기본값 끔)'}`
|
|
191
|
+
: ` detected locale: ${detected ? 'Korean (default on)' : 'not Korean (default off)'}`);
|
|
192
|
+
let enable = detected;
|
|
193
|
+
if (interactive) {
|
|
194
|
+
enable = await confirm(lang === 'ko' ? ' 켤까요?' : ' Enable it?', { defaultValue: detected });
|
|
195
|
+
}
|
|
196
|
+
// Record the choice only when it is really a choice. An unattended
|
|
197
|
+
// install on a non-Korean machine leaves the setting undecided so that
|
|
198
|
+
// a later run at a terminal still gets to ask, instead of silently
|
|
199
|
+
// inheriting a default the user never saw.
|
|
200
|
+
if (interactive || enable) ks.setKoreanStyleEnabled(enable);
|
|
201
|
+
if (enable) {
|
|
202
|
+
console.log(lang === 'ko'
|
|
203
|
+
? ' korean: 켰습니다 — 모든 프로젝트의 세션 시작 시 주입됩니다. 끄려면 `claude-token-saver korean off`.'
|
|
204
|
+
: ' korean: enabled — injected at session start in every project. Turn off with `claude-token-saver korean off`.');
|
|
205
|
+
} else {
|
|
206
|
+
console.log(lang === 'ko'
|
|
207
|
+
? ' korean: 꺼 두었습니다 — 나중에 `claude-token-saver korean on` 으로 켤 수 있습니다.'
|
|
208
|
+
: ' korean: left off — enable later with `claude-token-saver korean on`.');
|
|
209
|
+
}
|
|
145
210
|
}
|
|
146
211
|
} catch (e) {
|
|
147
212
|
debug('install:korean-style', e); // optional feature; never fail install
|
|
@@ -145,11 +145,12 @@ export function pickCapWarn(caps) {
|
|
|
145
145
|
function buildKoreanSeg(c, isIcon, verbose) {
|
|
146
146
|
try {
|
|
147
147
|
if (!koreanStyleEnabled()) return null;
|
|
148
|
-
// Deliberately quiet (gray, one
|
|
148
|
+
// Deliberately quiet (gray, one glyph): this is a "yes, it is on"
|
|
149
149
|
// confirmation, not a warning. Without it a silently-failed hook looks
|
|
150
150
|
// exactly like a working one, because the style only shows up when the
|
|
151
|
-
// model happens to write Korean.
|
|
152
|
-
|
|
151
|
+
// model happens to write Korean. The icon says "writing guidance", not
|
|
152
|
+
// "Korean" — the verbose label already carries the language.
|
|
153
|
+
if (isIcon) return `${c(GRAY)}${verbose ? '✍️ Korean style' : '✍️'}${c(RESET)}`;
|
|
153
154
|
return `${c(GRAY)}Korean style${c(RESET)}`;
|
|
154
155
|
} catch {
|
|
155
156
|
return null;
|
package/src/prompt.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt — minimal yes/no prompting for the install flow.
|
|
3
|
+
*
|
|
4
|
+
* Everything here exists to answer one question: may we stop and ask, or must
|
|
5
|
+
* we fall back to a default? Most installs of this package run as npm's
|
|
6
|
+
* postinstall, where stdin is not a terminal and a readline prompt would either
|
|
7
|
+
* hang the install or read garbage. So the rule is: ask only when a human is
|
|
8
|
+
* demonstrably on the other end, and otherwise keep the previous
|
|
9
|
+
* decide-it-for-them behavior untouched.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createInterface } from 'node:readline';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether it is safe to block on a question.
|
|
16
|
+
*
|
|
17
|
+
* `npm_lifecycle_event` is checked in addition to the TTY test because npm can
|
|
18
|
+
* leave a TTY attached while still running the script unattended; CI is
|
|
19
|
+
* checked because build agents deadlock rather than answer.
|
|
20
|
+
*/
|
|
21
|
+
export function canPrompt({ env = process.env, stdin = process.stdin, stdout = process.stdout } = {}) {
|
|
22
|
+
if (env.CTS_NO_INPUT === '1') return false;
|
|
23
|
+
if (env.CI && env.CI !== 'false') return false;
|
|
24
|
+
if (env.npm_lifecycle_event === 'postinstall') return false;
|
|
25
|
+
return Boolean(stdin.isTTY && stdout.isTTY);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Ask a yes/no question and resolve to a boolean.
|
|
30
|
+
*
|
|
31
|
+
* An empty answer takes `defaultValue`, which is also what a closed stream
|
|
32
|
+
* resolves to, so a prompt that somehow runs unattended still terminates with
|
|
33
|
+
* the same choice the non-interactive path would have made.
|
|
34
|
+
*/
|
|
35
|
+
export function confirm(question, { defaultValue = true, input = process.stdin, output = process.stdout } = {}) {
|
|
36
|
+
const hint = defaultValue ? '[Y/n]' : '[y/N]';
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const rl = createInterface({ input, output });
|
|
39
|
+
let answered = false;
|
|
40
|
+
rl.question(`${question} ${hint} `, (answer) => {
|
|
41
|
+
answered = true;
|
|
42
|
+
rl.close();
|
|
43
|
+
const a = String(answer).trim().toLowerCase();
|
|
44
|
+
if (a === 'y' || a === 'yes') return resolve(true);
|
|
45
|
+
if (a === 'n' || a === 'no') return resolve(false);
|
|
46
|
+
resolve(defaultValue); // empty line, or anything we do not recognize
|
|
47
|
+
});
|
|
48
|
+
// A stream that ends without a line — a closed pipe, Ctrl-D — must still
|
|
49
|
+
// settle the promise, or the install would wait forever.
|
|
50
|
+
rl.on('close', () => { if (!answered) resolve(defaultValue); });
|
|
51
|
+
});
|
|
52
|
+
}
|