claude-token-saver 3.40.1 → 3.42.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/bin/cli.js +29 -29
- package/package.json +8 -4
- package/src/brief.js +3 -3
- package/src/cohesion.js +2 -2
- package/src/commands/brief.js +7 -7
- package/src/commands/cohesion.js +6 -6
- package/src/commands/doc2md.js +6 -6
- package/src/commands/feedback.js +3 -3
- package/src/commands/harness.js +22 -22
- package/src/commands/install.js +16 -16
- package/src/commands/korean.js +14 -14
- package/src/commands/route-scan.js +21 -21
- package/src/commands/update-check.js +6 -6
- package/src/doc2md.cjs +15 -15
- package/src/harness-templates.js +7 -7
- package/src/korean-style.js +2 -2
- package/src/model-rules.js +4 -4
- package/src/update-check.js +4 -1
package/bin/cli.js
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* npx claude-token-saver --format json # JSON output
|
|
10
10
|
* npx claude-token-saver --format csv # CSV output
|
|
11
11
|
* npx claude-token-saver --project myproj # filter by project
|
|
12
|
-
* npx
|
|
13
|
-
* npx
|
|
14
|
-
* npx
|
|
12
|
+
* npx sprag route-scan # detect recurring easy work → haiku-delegation candidates
|
|
13
|
+
* npx sprag install # set up skill/hooks/statusline; asks about harness + Korean guidance
|
|
14
|
+
* npx sprag install --yes # take the defaults without asking (same as --no-input)
|
|
15
15
|
* npx claude-token-saver --install-hook # install PostToolUse hook
|
|
16
16
|
* npx claude-token-saver --uninstall-hook # remove hook
|
|
17
17
|
* npx claude-token-saver --hook-run # internal: called by hook
|
|
@@ -98,16 +98,16 @@ Usage:
|
|
|
98
98
|
claude-token-saver --format json JSON output
|
|
99
99
|
claude-token-saver --format csv CSV output
|
|
100
100
|
claude-token-saver --project myproj filter by project
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
sprag route-scan detect recurring easy work → delegation candidates
|
|
102
|
+
sprag install set up skill/hooks/statusline
|
|
103
|
+
sprag install --yes take the defaults without asking
|
|
104
104
|
claude-token-saver uninstall remove everything install added
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
sprag harness check score the harness setup in CLAUDE.md
|
|
106
|
+
sprag harness analyze run the harness transcript analysis manually
|
|
107
107
|
claude-token-saver last most recent warning + how to handle it
|
|
108
108
|
claude-token-saver history recent warning transitions
|
|
109
109
|
claude-token-saver handoff write a session handoff file
|
|
110
|
-
|
|
110
|
+
sprag feedback "<msg>" file a bug report / feature request (no browser needed)
|
|
111
111
|
claude-token-saver upgrade install the latest release
|
|
112
112
|
claude-token-saver --install-hook install cache-monitor PostToolUse hook
|
|
113
113
|
claude-token-saver --uninstall-hook remove that hook
|
|
@@ -179,8 +179,8 @@ async function main() {
|
|
|
179
179
|
// user can just mention chip wording and Claude responds. v2.6.0 dropped
|
|
180
180
|
// the redundant /token-monitor slash command in favor of the skill alone;
|
|
181
181
|
// a legacy command file is removed automatically. Cross-platform.
|
|
182
|
-
//
|
|
183
|
-
//
|
|
182
|
+
// sprag install # install/update the skill
|
|
183
|
+
// sprag install --force # overwrite existing skill file
|
|
184
184
|
if (args[0] === 'install') {
|
|
185
185
|
return (await import('../src/commands/install.js')).run({ hasFlag });
|
|
186
186
|
}
|
|
@@ -203,13 +203,13 @@ async function main() {
|
|
|
203
203
|
|
|
204
204
|
// Subcommand: route-scan — detect recurring easy work on expensive models
|
|
205
205
|
// and propose model-delegation ratchet rules. Zero token cost, fully local.
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
206
|
+
// sprag route-scan # scan (24h cache) + print candidates
|
|
207
|
+
// sprag route-scan --refresh # force rescan
|
|
208
|
+
// sprag route-scan --days 30 # wider lookback
|
|
209
|
+
// sprag route-scan --hook # SessionStart hook mode (context injection)
|
|
210
|
+
// sprag route-scan dismiss <N> # mute candidate R<N>
|
|
211
211
|
// Promote a candidate to a ratchet rule (scope is always explicit):
|
|
212
|
-
//
|
|
212
|
+
// sprag harness promote R<N> --project|--global
|
|
213
213
|
// brief --hook — UserPromptSubmit hook mode: per-session, change-triggered
|
|
214
214
|
// briefing of state the statusline can only chip (ctx tier crossings,
|
|
215
215
|
// mid-session route/rule-health changes). Silent when nothing changed.
|
|
@@ -223,21 +223,21 @@ async function main() {
|
|
|
223
223
|
|
|
224
224
|
// Subcommand: korean — Korean writing guidance injected at session start,
|
|
225
225
|
// so the rules apply in every project without an output-style switch.
|
|
226
|
-
//
|
|
226
|
+
// sprag korean on | off | status | show
|
|
227
227
|
if (args[0] === 'korean') {
|
|
228
228
|
return (await import('../src/commands/korean.js')).run({ args, hasFlag });
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
// Subcommand: cohesion — English sentence-connection guidance injected at
|
|
232
232
|
// session start; the language-neutral half of the Korean supplement.
|
|
233
|
-
//
|
|
233
|
+
// sprag cohesion on | off | status | show
|
|
234
234
|
if (args[0] === 'cohesion') {
|
|
235
235
|
return (await import('../src/commands/cohesion.js')).run({ args, hasFlag });
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
// Subcommand: doc2md — convert pptx/xlsx/pdf/docx to Markdown before the
|
|
239
239
|
// model reads them, so an unreadable binary never enters the context window.
|
|
240
|
-
//
|
|
240
|
+
// sprag doc2md on | off | <file> | --clean
|
|
241
241
|
if (args[0] === 'doc2md') {
|
|
242
242
|
return (await import('../src/commands/doc2md.js')).run({ args, hasFlag });
|
|
243
243
|
}
|
|
@@ -250,12 +250,12 @@ async function main() {
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
// Subcommand: harness — manage the project's CLAUDE.md harness rules.
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
253
|
+
// sprag harness init # write CLAUDE.md (5 sections) + ratchet.md
|
|
254
|
+
// sprag harness uninit # remove harness block from CLAUDE.md (backup kept)
|
|
255
|
+
// sprag harness check # show 🅷 N/5 + which sections are missing
|
|
256
|
+
// sprag harness promote "<rule>" # append a rule to ratchet.md
|
|
257
|
+
// sprag harness pull [--global|--project] # register the package's curated preset rules (default global)
|
|
258
|
+
// sprag harness off | on # toggle the statusline 🅷 segment
|
|
259
259
|
if (args[0] === 'harness') {
|
|
260
260
|
return (await import('../src/commands/harness.js')).run({ args, hasFlag });
|
|
261
261
|
}
|
|
@@ -281,9 +281,9 @@ async function main() {
|
|
|
281
281
|
|
|
282
282
|
// Subcommand: update-check — the registry lookup behind the ⬆ statusline
|
|
283
283
|
// chip and the session-start upgrade offer.
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
284
|
+
// sprag update-check # print cached status
|
|
285
|
+
// sprag update-check --refresh # hit the registry now (detached child uses this)
|
|
286
|
+
// sprag update-check --dismiss # stop offering THIS version at session start
|
|
287
287
|
if (args[0] === 'update-check') {
|
|
288
288
|
return (await import('../src/commands/update-check.js')).run({
|
|
289
289
|
hasFlag,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-token-saver",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.42.0",
|
|
4
|
+
"description": "Legacy name of sprag-cli (Sprag) - same tool, kept publishing so existing installs stay current. Prefer: npm i -g sprag-cli",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sprag": "bin/cli.js",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "node --test",
|
|
12
12
|
"docs:statusline": "node scripts/docs-statusline.mjs",
|
|
13
|
-
"postinstall": "node bin/cli.js install || true"
|
|
13
|
+
"postinstall": "node bin/cli.js install || true",
|
|
14
|
+
"publish:legacy": "node scripts/publish-legacy.mjs"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"bin/",
|
|
@@ -25,6 +26,10 @@
|
|
|
25
26
|
"node": ">=18"
|
|
26
27
|
},
|
|
27
28
|
"keywords": [
|
|
29
|
+
"sprag",
|
|
30
|
+
"ratchet",
|
|
31
|
+
"harness",
|
|
32
|
+
"ai-agent",
|
|
28
33
|
"claude",
|
|
29
34
|
"claude-code",
|
|
30
35
|
"anthropic",
|
|
@@ -43,7 +48,6 @@
|
|
|
43
48
|
"claude-code-statusline",
|
|
44
49
|
"claude-code-cost",
|
|
45
50
|
"model-fitting",
|
|
46
|
-
"ratchet",
|
|
47
51
|
"agent-harness"
|
|
48
52
|
],
|
|
49
53
|
"license": "MIT",
|
package/src/brief.js
CHANGED
|
@@ -226,8 +226,8 @@ export async function runBrief({ sessionId, transcriptPath, now = Date.now() })
|
|
|
226
226
|
}
|
|
227
227
|
for (const [kind, x] of fresh) {
|
|
228
228
|
items.push(kind === 'route'
|
|
229
|
-
? `새 위임 후보가 감지되었습니다 — "${x.label}" 유형 ${x.count}회 반복(statusline의 route? R${x.id} 칩). 등록:
|
|
230
|
-
: `승인된 위임 룰의 최근 에러율이 기준(20%)을 넘었습니다 — "${x.label}" (${x.healthSource === 'delegated' ? `위임 실행 ${x.delegatedRuns}건 중 에러율 ${Math.round((x.delegatedErrRate || 0) * 100)}%` : `유사 에피소드 기준 에러율 ${Math.round((x.errRate || 0) * 100)}%`}, statusline의 rule-health 칩). 조건 좁히기/제거를 사용자와 상의하세요:
|
|
229
|
+
? `새 위임 후보가 감지되었습니다 — "${x.label}" 유형 ${x.count}회 반복(statusline의 route? R${x.id} 칩). 등록: sprag harness promote R${x.id} --project|--global (적용 범위는 사용자에게 확인) / 무시: route-scan dismiss ${x.id}`
|
|
230
|
+
: `승인된 위임 룰의 최근 에러율이 기준(20%)을 넘었습니다 — "${x.label}" (${x.healthSource === 'delegated' ? `위임 실행 ${x.delegatedRuns}건 중 에러율 ${Math.round((x.delegatedErrRate || 0) * 100)}%` : `유사 에피소드 기준 에러율 ${Math.round((x.errRate || 0) * 100)}%`}, statusline의 rule-health 칩). 조건 좁히기/제거를 사용자와 상의하세요: sprag route-scan rules`);
|
|
231
231
|
}
|
|
232
232
|
s.briefed = [...briefed];
|
|
233
233
|
} catch { /* caches unreadable — ctx briefing above still applies */ }
|
|
@@ -257,7 +257,7 @@ export async function runBrief({ sessionId, transcriptPath, now = Date.now() })
|
|
|
257
257
|
|
|
258
258
|
if (items.length === 0) return null;
|
|
259
259
|
const lines = [
|
|
260
|
-
'[claude-token-saver 브리핑] 아래 상태 변화를 사용자에게 알려주세요. 진행 중인 답변 흐름을 끊지 말고, 답변 말미에 `※ [
|
|
260
|
+
'[claude-token-saver 브리핑] 아래 상태 변화를 사용자에게 알려주세요. 진행 중인 답변 흐름을 끊지 말고, 답변 말미에 `※ [sprag]` 라벨을 달아 각 항목을 1~2줄로 요약해 전달하면 됩니다 (이 브리핑은 항목당 한 번만 주입됩니다):',
|
|
261
261
|
];
|
|
262
262
|
for (const it of items) lines.push(`- ${it}`);
|
|
263
263
|
return lines.join('\n');
|
package/src/cohesion.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Off by default for the same reason the Korean guidance is opt-in: a
|
|
11
11
|
* token-saving tool has no business silently billing ~0.5k tokens a session.
|
|
12
|
-
* Enable with `
|
|
12
|
+
* Enable with `sprag cohesion on`.
|
|
13
13
|
*
|
|
14
14
|
* When the Korean guidance is enabled, this block is NOT injected even if
|
|
15
15
|
* enabled: the Korean supplement already carries the cohesion rules, and
|
|
@@ -62,7 +62,7 @@ export async function cohesionInjection({ cfg = loadConfig() } = {}) {
|
|
|
62
62
|
const text = cohesionText();
|
|
63
63
|
if (!text) return null;
|
|
64
64
|
return [
|
|
65
|
-
'[
|
|
65
|
+
'[sprag cohesion] Follow this guidance for English prose in this session.',
|
|
66
66
|
'The user enabled it in claude-token-saver.',
|
|
67
67
|
'',
|
|
68
68
|
text,
|
package/src/commands/brief.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subcommand: route-scan — detect recurring easy work on expensive models
|
|
3
3
|
* and propose model-delegation ratchet rules. Zero token cost, fully local.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* sprag route-scan # scan (24h cache) + print candidates
|
|
5
|
+
* sprag route-scan --refresh # force rescan
|
|
6
|
+
* sprag route-scan --days 30 # wider lookback
|
|
7
|
+
* sprag route-scan --hook # SessionStart hook mode (context injection)
|
|
8
|
+
* sprag route-scan dismiss <N> # mute candidate R<N>
|
|
9
9
|
* Promote a candidate to a ratchet rule (scope is always explicit):
|
|
10
|
-
*
|
|
10
|
+
* sprag harness promote R<N> --project|--global
|
|
11
11
|
* brief --hook — UserPromptSubmit hook mode: per-session, change-triggered
|
|
12
12
|
* briefing of state the statusline can only chip (ctx tier crossings,
|
|
13
13
|
* mid-session route/rule-health changes). Silent when nothing changed.
|
|
@@ -18,7 +18,7 @@ import { debug } from '../debug.js';
|
|
|
18
18
|
|
|
19
19
|
export async function run({ hasFlag }) {
|
|
20
20
|
if (!hasFlag('--hook')) {
|
|
21
|
-
console.error('Usage:
|
|
21
|
+
console.error('Usage: sprag brief --hook (UserPromptSubmit hook mode)');
|
|
22
22
|
process.exit(1);
|
|
23
23
|
}
|
|
24
24
|
const ctx = readStdinJson() || {};
|
package/src/commands/cohesion.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subcommand: cohesion — English sentence-connection guidance for every session.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* sprag cohesion on # inject at session start, all projects
|
|
4
|
+
* sprag cohesion off # stop injecting
|
|
5
|
+
* sprag cohesion status # current state and cost
|
|
6
|
+
* sprag cohesion show # print the guidance itself
|
|
7
7
|
*
|
|
8
8
|
* The English sibling of `korean on`, carrying only the language-neutral
|
|
9
9
|
* cohesion rules (given-before-new, one referent per pronoun, subject
|
|
@@ -51,9 +51,9 @@ export async function run({ args }) {
|
|
|
51
51
|
const suppressed = enabled && koreanStyleEnabled();
|
|
52
52
|
if (lang === 'ko') {
|
|
53
53
|
console.log(`cohesion: ${enabled ? 'on' : 'off'}${suppressed ? ' (korean 지침이 켜져 있어 주입은 생략됨)' : ''}`);
|
|
54
|
-
console.log('영어 산문의 문장 연결 지침을 세션 시작에 주입합니다. 켜기:
|
|
54
|
+
console.log('영어 산문의 문장 연결 지침을 세션 시작에 주입합니다. 켜기: sprag cohesion on');
|
|
55
55
|
} else {
|
|
56
56
|
console.log(`cohesion: ${enabled ? 'on' : 'off'}${suppressed ? ' (suppressed while the Korean guidance is on — it already carries these rules)' : ''}`);
|
|
57
|
-
console.log('Injects English cohesion guidance at session start. Enable with:
|
|
57
|
+
console.log('Injects English cohesion guidance at session start. Enable with: sprag cohesion on');
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/commands/doc2md.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Subcommand: doc2md — convert attached documents to Markdown before the
|
|
3
3
|
* model reads them.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* sprag doc2md # status
|
|
6
|
+
* sprag doc2md on|off # register / remove the Read hook
|
|
7
|
+
* sprag doc2md <file> # convert one file by hand
|
|
8
|
+
* sprag doc2md --clean # drop every cached conversion
|
|
9
|
+
* sprag doc2md --hook # PreToolUse entry point
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createRequire } from 'node:module';
|
|
@@ -162,5 +162,5 @@ export async function run({ args, hasFlag }) {
|
|
|
162
162
|
console.log(` hook: ${hookRegistered() ? 'registered on Read' : 'not registered'}`);
|
|
163
163
|
console.log('');
|
|
164
164
|
if (!python) console.log(`Install the converter: ${doc2md.INSTALL_HINT}`);
|
|
165
|
-
console.log('Enable with:
|
|
165
|
+
console.log('Enable with: sprag doc2md on');
|
|
166
166
|
}
|
package/src/commands/feedback.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Subcommand: feedback — submit a bug report or feature request from the
|
|
3
3
|
* terminal (or from a Claude Code session) without opening a browser.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* sprag feedback "statusline이 IntelliJ에서 깨져요"
|
|
6
|
+
* sprag feedback --title "cache chip" "5m TTL 칩이 안 사라짐"
|
|
7
7
|
*
|
|
8
8
|
* Why this exists: GitHub issues require a logged-in browser session, and
|
|
9
9
|
* corporate networks often block github.com entirely. This command tries
|
|
@@ -102,7 +102,7 @@ export async function run({ args, getArg, version, dataDir }) {
|
|
|
102
102
|
const explicitTitle = getArg('--title');
|
|
103
103
|
|
|
104
104
|
if (!message) {
|
|
105
|
-
console.log('usage:
|
|
105
|
+
console.log('usage: sprag feedback [--title "<제목>"] "<내용>"');
|
|
106
106
|
console.log(` (GitHub에서 직접 제보: ${ISSUES_URL})`);
|
|
107
107
|
process.exitCode = 1;
|
|
108
108
|
return;
|
package/src/commands/harness.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subcommand: harness — manage the project's CLAUDE.md harness rules.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* sprag harness init # write CLAUDE.md (5 sections) + ratchet.md
|
|
4
|
+
* sprag harness uninit # remove harness block from CLAUDE.md (backup kept)
|
|
5
|
+
* sprag harness check # show 🅷 N/5 + which sections are missing
|
|
6
|
+
* sprag harness promote "<rule>" # append a rule to ratchet.md
|
|
7
|
+
* sprag harness pull [--global|--project] # register the package's curated preset rules (default global)
|
|
8
|
+
* sprag harness off | on # toggle the statusline 🅷 segment
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { debug } from '../debug.js';
|
|
@@ -49,8 +49,8 @@ export async function run({ args, hasFlag }) {
|
|
|
49
49
|
const sec = HARNESS_SECTIONS.find((x) => x.id === id);
|
|
50
50
|
console.log(` - ${id}: ${sec ? sec.heading.replace(/^#+\s*/, '') : ''}`);
|
|
51
51
|
}
|
|
52
|
-
console.log('\nRun:
|
|
53
|
-
console.log(' or:
|
|
52
|
+
console.log('\nRun: sprag harness init (this project)');
|
|
53
|
+
console.log(' or: sprag harness init --global (all projects, ~/.claude/CLAUDE.md)');
|
|
54
54
|
} else {
|
|
55
55
|
console.log('All 5 harness sections present. ✅');
|
|
56
56
|
}
|
|
@@ -60,7 +60,7 @@ export async function run({ args, hasFlag }) {
|
|
|
60
60
|
const dead = [!s.hasRatchetImport && 'ratchet.md', !s.hasModelRatchetImport && 'ratchet-model.md'].filter(Boolean).join(' + ');
|
|
61
61
|
console.log(`\n⚠ ${dead} is NOT loaded into sessions — the harness block has no \`@\` import line for it.`);
|
|
62
62
|
console.log(' Those rules are being written to a file nothing reads.');
|
|
63
|
-
console.log(` Fix:
|
|
63
|
+
console.log(` Fix: sprag harness init${s.source === 'global' ? ' --global' : ''} (updates the block in place)`);
|
|
64
64
|
}
|
|
65
65
|
// Imported ratchets are charged on every request, so their size matters.
|
|
66
66
|
// Static `@` imports cannot be filtered at load time — the only lever is
|
|
@@ -71,7 +71,7 @@ export async function run({ args, hasFlag }) {
|
|
|
71
71
|
const line = `ratchet.md [${sc}]: ${size.count} rules, ~${size.tokens} tok/request`;
|
|
72
72
|
if (size.overBudget) {
|
|
73
73
|
console.log(`\n⚠ ${line} — over the ~${RATCHET_TOKEN_BUDGET} token budget.`);
|
|
74
|
-
console.log(` Trim:
|
|
74
|
+
console.log(` Trim: sprag harness prune${sc === 'global' ? ' --global' : ''} --older-than 6 --dry-run`);
|
|
75
75
|
console.log(' (project-specific rules belong in --project scope, not global.)');
|
|
76
76
|
} else {
|
|
77
77
|
console.log(`${line}`);
|
|
@@ -138,8 +138,8 @@ export async function run({ args, hasFlag }) {
|
|
|
138
138
|
}
|
|
139
139
|
const raw = promoteArgs.filter((_, i) => !scopeFlags.has(i)).join(' ').trim();
|
|
140
140
|
if (!raw) {
|
|
141
|
-
console.error('Usage:
|
|
142
|
-
console.error(' or:
|
|
141
|
+
console.error('Usage: sprag harness promote [--global|--project] <N> # from statusline 🅷⚠ ratchet? #N');
|
|
142
|
+
console.error(' or: sprag harness promote [--global|--project] "<rule text>"');
|
|
143
143
|
process.exit(1);
|
|
144
144
|
}
|
|
145
145
|
let rule = raw;
|
|
@@ -173,7 +173,7 @@ export async function run({ args, hasFlag }) {
|
|
|
173
173
|
const rs = await import('../route-scan.js');
|
|
174
174
|
const cand = (rs.openCandidates(rs.readRouteScan()) || []).find((c) => c.id === n);
|
|
175
175
|
if (!cand) {
|
|
176
|
-
console.error(`No open route candidate R${n}. Run:
|
|
176
|
+
console.error(`No open route candidate R${n}. Run: sprag route-scan`);
|
|
177
177
|
process.exit(1);
|
|
178
178
|
}
|
|
179
179
|
rule = lang === 'ko' ? cand.rule : (cand.ruleEn || cand.rule);
|
|
@@ -229,7 +229,7 @@ export async function run({ args, hasFlag }) {
|
|
|
229
229
|
console.error(`Route candidate R${routeCandidateId} was detected in another project (${routeCandidate.project}),`);
|
|
230
230
|
console.error('but this cached scan predates project-path tracking.');
|
|
231
231
|
console.error('Re-scan to capture it, then promote again:');
|
|
232
|
-
console.error('
|
|
232
|
+
console.error(' sprag route-scan --refresh');
|
|
233
233
|
process.exit(1);
|
|
234
234
|
}
|
|
235
235
|
}
|
|
@@ -261,8 +261,8 @@ export async function run({ args, hasFlag }) {
|
|
|
261
261
|
? `(route candidate R${routeCandidateId} resolved — 다음 세션부터 자동 위임, 이후 스캔마다 로그 기반 갱신됩니다)`
|
|
262
262
|
: `(route candidate R${routeCandidateId} resolved — delegation applies from the next session, refreshed from logs on every rescan)`);
|
|
263
263
|
console.log(lang === 'ko'
|
|
264
|
-
? '룰 목록/제거:
|
|
265
|
-
: 'List / remove:
|
|
264
|
+
? '룰 목록/제거: sprag route-scan rules [rm <N>]'
|
|
265
|
+
: 'List / remove: sprag route-scan rules [rm <N>]');
|
|
266
266
|
// Event-triggered refresh: establish the new rule's stat baseline
|
|
267
267
|
// right away instead of waiting for the next data-gated rescan.
|
|
268
268
|
try {
|
|
@@ -345,8 +345,8 @@ export async function run({ args, hasFlag }) {
|
|
|
345
345
|
}
|
|
346
346
|
if (r.skippedRules && r.added.length) console.log(` (${r.skippedRules} already present — skipped)`);
|
|
347
347
|
console.log(lang === 'ko'
|
|
348
|
-
? '\n필요 없는 룰은 언제든:
|
|
349
|
-
: '\nDrop any rule you do not want:
|
|
348
|
+
? '\n필요 없는 룰은 언제든: sprag harness list / rm <N>'
|
|
349
|
+
: '\nDrop any rule you do not want: sprag harness list / rm <N>');
|
|
350
350
|
return;
|
|
351
351
|
}
|
|
352
352
|
|
|
@@ -365,8 +365,8 @@ export async function run({ args, hasFlag }) {
|
|
|
365
365
|
};
|
|
366
366
|
if (wantProject) print('project');
|
|
367
367
|
if (wantGlobal) print('global');
|
|
368
|
-
console.log('Remove with:
|
|
369
|
-
console.log('Archive in bulk:
|
|
368
|
+
console.log('Remove with: sprag harness rm [--global|--project] <N>');
|
|
369
|
+
console.log('Archive in bulk: sprag harness prune [--global] [--tag <t>] [--older-than <months>] [--dry-run]');
|
|
370
370
|
return;
|
|
371
371
|
}
|
|
372
372
|
|
|
@@ -406,7 +406,7 @@ export async function run({ args, hasFlag }) {
|
|
|
406
406
|
const rmArgs = args.slice(2).filter((a) => a !== '--global' && a !== '--project');
|
|
407
407
|
const raw = (rmArgs[0] || '').trim();
|
|
408
408
|
if (!/^\d+$/.test(raw)) {
|
|
409
|
-
console.error('Usage:
|
|
409
|
+
console.error('Usage: sprag harness rm [--global|--project] <N> # N from `harness list`');
|
|
410
410
|
process.exit(1);
|
|
411
411
|
}
|
|
412
412
|
const n = parseInt(raw, 10);
|
|
@@ -454,6 +454,6 @@ export async function run({ args, hasFlag }) {
|
|
|
454
454
|
}
|
|
455
455
|
|
|
456
456
|
console.error(`Unknown harness subcommand: ${sub}`);
|
|
457
|
-
console.error('Usage:
|
|
457
|
+
console.error('Usage: sprag harness [check|init|uninit [--purge-ratchet]|promote "<rule>"|pull [--global|--project]|list|rm <N>|prune [--tag <t>] [--older-than <months>] [--dry-run]|off|on]');
|
|
458
458
|
process.exit(1);
|
|
459
459
|
}
|
package/src/commands/install.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* user can just mention chip wording and Claude responds. v2.6.0 dropped
|
|
4
4
|
* the redundant /token-monitor slash command in favor of the skill alone;
|
|
5
5
|
* a legacy command file is removed automatically. Cross-platform.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* sprag install # install/update the skill
|
|
7
|
+
* sprag install --force # overwrite existing skill file
|
|
8
|
+
* sprag install --yes # take the defaults instead of asking
|
|
9
|
+
* sprag install --no-input # same as --yes; also implied by CTS_NO_INPUT=1
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { debug } from '../debug.js';
|
|
@@ -94,8 +94,8 @@ export async function run({ hasFlag }) {
|
|
|
94
94
|
s = installStatusline({ force: true });
|
|
95
95
|
} else {
|
|
96
96
|
s = { ...s, reason: lang === 'ko'
|
|
97
|
-
? '기존 statusline을 유지했습니다. 교체하려면 `
|
|
98
|
-
: 'kept your statusline — replace later with `
|
|
97
|
+
? '기존 statusline을 유지했습니다. 교체하려면 `sprag install --force`'
|
|
98
|
+
: 'kept your statusline — replace later with `sprag install --force`' };
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
const verb = s.action === 'exists' ? 'already configured (refreshInterval=5)'
|
|
@@ -178,8 +178,8 @@ export async function run({ hasFlag }) {
|
|
|
178
178
|
}
|
|
179
179
|
if (!proceed) {
|
|
180
180
|
console.log(lang === 'ko'
|
|
181
|
-
? ' harness: 건너뛰었습니다 — 나중에 `
|
|
182
|
-
: ' harness: skipped — set it up later with `
|
|
181
|
+
? ' harness: 건너뛰었습니다 — 나중에 `sprag harness init --global` 로 설정할 수 있습니다.'
|
|
182
|
+
: ' harness: skipped — set it up later with `sprag harness init --global`.');
|
|
183
183
|
throw new SkipStep();
|
|
184
184
|
}
|
|
185
185
|
const h = harnessInit({ scope: 'global' });
|
|
@@ -194,8 +194,8 @@ export async function run({ hasFlag }) {
|
|
|
194
194
|
? ' harness: 5원칙을 ~/.claude/CLAUDE.md 에 설정했습니다 — statusline에 🅷 5/5 가 표시됩니다.'
|
|
195
195
|
: ' harness: 5 principles installed in ~/.claude/CLAUDE.md — the statusline now shows 🅷 5/5.');
|
|
196
196
|
console.log(lang === 'ko'
|
|
197
|
-
? ' 되돌리려면:
|
|
198
|
-
: ' to undo:
|
|
197
|
+
? ' 되돌리려면: sprag harness uninit --global'
|
|
198
|
+
: ' to undo: sprag harness uninit --global');
|
|
199
199
|
}
|
|
200
200
|
} catch (e) {
|
|
201
201
|
// A declined prompt already printed its own line; only real failures fall
|
|
@@ -206,8 +206,8 @@ export async function run({ hasFlag }) {
|
|
|
206
206
|
// in place, and `harness init` remains available as a manual step.
|
|
207
207
|
debug('install:harness-init', e);
|
|
208
208
|
console.log(lang === 'ko'
|
|
209
|
-
? ' harness: 자동 설정을 건너뛰었습니다 — `
|
|
210
|
-
: ' harness: auto-setup skipped — run `
|
|
209
|
+
? ' harness: 자동 설정을 건너뛰었습니다 — `sprag harness init --global` 로 직접 설정하세요.'
|
|
210
|
+
: ' harness: auto-setup skipped — run `sprag harness init --global` yourself.');
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
@@ -264,12 +264,12 @@ export async function run({ hasFlag }) {
|
|
|
264
264
|
if (interactive || enable) ks.setKoreanStyleEnabled(enable);
|
|
265
265
|
if (enable) {
|
|
266
266
|
console.log(lang === 'ko'
|
|
267
|
-
? ' korean: 켰습니다 — 모든 프로젝트의 세션 시작 시 주입됩니다. 끄려면 `
|
|
268
|
-
: ' korean: enabled — injected at session start in every project. Turn off with `
|
|
267
|
+
? ' korean: 켰습니다 — 모든 프로젝트의 세션 시작 시 주입됩니다. 끄려면 `sprag korean off`.'
|
|
268
|
+
: ' korean: enabled — injected at session start in every project. Turn off with `sprag korean off`.');
|
|
269
269
|
} else {
|
|
270
270
|
console.log(lang === 'ko'
|
|
271
|
-
? ' korean: 꺼 두었습니다 — 나중에 `
|
|
272
|
-
: ' korean: left off — enable later with `
|
|
271
|
+
? ' korean: 꺼 두었습니다 — 나중에 `sprag korean on` 으로 켤 수 있습니다.'
|
|
272
|
+
: ' korean: left off — enable later with `sprag korean on`.');
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
} catch (e) {
|
package/src/commands/korean.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subcommand: korean — Korean writing guidance for every session.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* sprag korean on # inject at session start, all projects
|
|
4
|
+
* sprag korean off # stop injecting
|
|
5
|
+
* sprag korean status # current state, cost, and provenance
|
|
6
|
+
* sprag korean show # print the guidance itself
|
|
7
7
|
*
|
|
8
8
|
* Why this exists rather than pointing users at Claude Code's output styles:
|
|
9
9
|
* an output style is one global slot, so turning it on takes the slot away
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
* reads the finished file. `korean on` therefore also installs a PostToolUse
|
|
19
19
|
* hook that runs the machine-checkable clauses over the prose the model just
|
|
20
20
|
* wrote:
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* sprag korean lint block|warn|off # how findings are handled
|
|
22
|
+
* sprag korean lint <file...> # check files on disk
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
// Enforcement is only useful if it is on by default — a check the user has to
|
|
@@ -108,7 +108,7 @@ export async function run({ args, hasFlag }) {
|
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
if (!['all', 'prose'].includes(next)) {
|
|
111
|
-
console.error('Usage:
|
|
111
|
+
console.error('Usage: sprag korean lint scope [all|prose]');
|
|
112
112
|
process.exit(1);
|
|
113
113
|
}
|
|
114
114
|
cfg.koreanStyle = cfg.koreanStyle || {};
|
|
@@ -135,7 +135,7 @@ export async function run({ args, hasFlag }) {
|
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
137
|
if (!['block', 'warn', 'off'].includes(mode)) {
|
|
138
|
-
console.error('Usage:
|
|
138
|
+
console.error('Usage: sprag korean lint [block|warn|off] | korean lint <file...>');
|
|
139
139
|
process.exit(1);
|
|
140
140
|
}
|
|
141
141
|
cfg.koreanStyle = cfg.koreanStyle || {};
|
|
@@ -201,20 +201,20 @@ export async function run({ args, hasFlag }) {
|
|
|
201
201
|
if (lang === 'ko') {
|
|
202
202
|
console.log(`한국어 문체 지침: ${on ? '켜짐' : '꺼짐'}`);
|
|
203
203
|
console.log(` 비용: 세션당 약 ${tokens} 토큰 (세션 시작 1회 주입)`);
|
|
204
|
-
console.log(` 쓰기 시점 검사: ${koreanLintMode(loadConfig())}, 범위 ${koreanLintScope(loadConfig())} (바꾸려면
|
|
204
|
+
console.log(` 쓰기 시점 검사: ${koreanLintMode(loadConfig())}, 범위 ${koreanLintScope(loadConfig())} (바꾸려면 sprag korean lint block|warn|off, korean lint scope all|prose)`);
|
|
205
205
|
console.log(` 출처: ${ks.KOREAN_STYLE_SOURCE}`);
|
|
206
206
|
console.log(` 라이선스 전문: ${ks.KOREAN_STYLE_LICENSE_PATH}`);
|
|
207
207
|
console.log(on
|
|
208
|
-
? ' 끄려면:
|
|
209
|
-
: ' 켜려면:
|
|
208
|
+
? ' 끄려면: sprag korean off'
|
|
209
|
+
: ' 켜려면: sprag korean on');
|
|
210
210
|
} else {
|
|
211
211
|
console.log(`Korean writing guidance: ${on ? 'on' : 'off'}`);
|
|
212
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:
|
|
213
|
+
console.log(` Write-time check: ${koreanLintMode(loadConfig())}, scope ${koreanLintScope(loadConfig())} (change with: sprag korean lint block|warn|off, korean lint scope all|prose)`);
|
|
214
214
|
console.log(` Source: ${ks.KOREAN_STYLE_SOURCE}`);
|
|
215
215
|
console.log(` License text: ${ks.KOREAN_STYLE_LICENSE_PATH}`);
|
|
216
216
|
console.log(on
|
|
217
|
-
? ' Turn off with:
|
|
218
|
-
: ' Turn on with:
|
|
217
|
+
? ' Turn off with: sprag korean off'
|
|
218
|
+
: ' Turn on with: sprag korean on');
|
|
219
219
|
}
|
|
220
220
|
}
|
|
@@ -111,7 +111,7 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
111
111
|
const n = parseInt(args[3], 10);
|
|
112
112
|
const removed = Number.isFinite(n) ? mr.removeModelRule(n) : null;
|
|
113
113
|
if (!removed) {
|
|
114
|
-
console.error('Usage:
|
|
114
|
+
console.error('Usage: sprag route-scan rules rm <N> # N from `route-scan rules`');
|
|
115
115
|
process.exit(1);
|
|
116
116
|
}
|
|
117
117
|
// A target whose last rule was removed gets its (tool-owned) file deleted.
|
|
@@ -181,20 +181,20 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
181
181
|
+ '\n Map it under modelAliases in profile-map.json, then run route-scan --refresh.');
|
|
182
182
|
}
|
|
183
183
|
console.log(lang === 'ko'
|
|
184
|
-
? '\n제거:
|
|
185
|
-
: '\nRemove with:
|
|
184
|
+
? '\n제거: sprag route-scan rules rm <N>'
|
|
185
|
+
: '\nRemove with: sprag route-scan rules rm <N>');
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
if (args[1] === 'dismiss') {
|
|
190
190
|
const n = parseInt(args[2], 10);
|
|
191
191
|
if (!Number.isFinite(n)) {
|
|
192
|
-
console.error('Usage:
|
|
192
|
+
console.error('Usage: sprag route-scan dismiss <N> # N from `route? R<N>`');
|
|
193
193
|
process.exit(1);
|
|
194
194
|
}
|
|
195
195
|
const cand = rs.resolveCandidate(n);
|
|
196
196
|
if (!cand) {
|
|
197
|
-
console.error(`No route candidate R${n}. Run:
|
|
197
|
+
console.error(`No route candidate R${n}. Run: sprag route-scan`);
|
|
198
198
|
process.exit(1);
|
|
199
199
|
}
|
|
200
200
|
console.log(lang === 'ko'
|
|
@@ -301,16 +301,16 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
301
301
|
if (u && u.available && !u.dismissed) {
|
|
302
302
|
updateBlock = lang === 'ko'
|
|
303
303
|
? [
|
|
304
|
-
`[
|
|
304
|
+
`[sprag update] 새 버전이 나와 있습니다: v${u.current} → ${u.latest}.`,
|
|
305
305
|
'사용자에게 지금 업그레이드할지 물어보고, 승낙하면 아래 명령을 실행하십시오. 묻지 않고 설치하지는 마십시오.',
|
|
306
306
|
' claude-token-saver upgrade # 업그레이드 실행',
|
|
307
|
-
'
|
|
307
|
+
' sprag update-check --dismiss # 사용자가 원치 않으면 (다음 버전이 나올 때까지 다시 묻지 않습니다)',
|
|
308
308
|
].join('\n')
|
|
309
309
|
: [
|
|
310
|
-
`[
|
|
310
|
+
`[sprag update] A newer version is available: v${u.current} → ${u.latest}.`,
|
|
311
311
|
'Ask the user whether to upgrade now, and run the command below only if they agree. Do not install without asking.',
|
|
312
312
|
' claude-token-saver upgrade # perform the upgrade',
|
|
313
|
-
'
|
|
313
|
+
' sprag update-check --dismiss # if they decline (stays quiet until a newer release)',
|
|
314
314
|
].join('\n');
|
|
315
315
|
}
|
|
316
316
|
} catch (e) { debug('route-scan:update-check', e); /* the offer is optional */ }
|
|
@@ -332,10 +332,10 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
332
332
|
const lines = [];
|
|
333
333
|
if (open.length > 0) {
|
|
334
334
|
if (lang === 'ko') {
|
|
335
|
-
lines.push(`[
|
|
335
|
+
lines.push(`[sprag route-scan] 최근 ${cache.days}일 세션에서 비싼 모델(opus/fable)이 반복 처리해 온, 더 싼 모델로 넘겨도 되는 작업이 감지되었습니다.`);
|
|
336
336
|
lines.push('(R<N>은 후보 번호, T2/T1은 난이도 등급입니다 — 사용자에게 전달할 때는 코드가 아니라 아래 풀어쓴 설명으로 브리핑하세요)');
|
|
337
337
|
} else {
|
|
338
|
-
lines.push(`[
|
|
338
|
+
lines.push(`[sprag route-scan] Over the last ${cache.days} days, expensive models (opus/fable) repeatedly handled work that a cheaper model could take.`);
|
|
339
339
|
lines.push('(R<N> is the candidate id, T2/T1 the difficulty tier — brief the user with the spelled-out wording below, not the codes.)');
|
|
340
340
|
}
|
|
341
341
|
for (const c of open) {
|
|
@@ -356,12 +356,12 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
356
356
|
}
|
|
357
357
|
if (lang === 'ko') {
|
|
358
358
|
lines.push('등록하면 다음 세션부터 자동 위임됩니다. 사용자에게 등록 여부를 물을 때 위 룰 원문을 그대로 보여주고, 적용 범위까지 확인한 뒤 실행하세요:');
|
|
359
|
-
lines.push('
|
|
360
|
-
lines.push('
|
|
359
|
+
lines.push(' sprag harness promote R<N> --project|--global # 적용 범위는 반드시 사용자에게 확인');
|
|
360
|
+
lines.push(' sprag route-scan dismiss <N> # 사용자가 원치 않으면');
|
|
361
361
|
} else {
|
|
362
362
|
lines.push('Once registered, delegation happens automatically from the next session. Show the user the rule text verbatim, confirm the scope with them, then run:');
|
|
363
|
-
lines.push('
|
|
364
|
-
lines.push('
|
|
363
|
+
lines.push(' sprag harness promote R<N> --project|--global # ALWAYS confirm the scope with the user first');
|
|
364
|
+
lines.push(' sprag route-scan dismiss <N> # if they do not want it');
|
|
365
365
|
}
|
|
366
366
|
}
|
|
367
367
|
if (reviewRules.length > 0) {
|
|
@@ -383,12 +383,12 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
383
383
|
lines.push(` "${r.rule}"`);
|
|
384
384
|
}
|
|
385
385
|
lines.push(lang === 'ko'
|
|
386
|
-
? ' 제거:
|
|
387
|
-
: ' remove with:
|
|
386
|
+
? ' 제거: sprag route-scan rules rm <N>'
|
|
387
|
+
: ' remove with: sprag route-scan rules rm <N>');
|
|
388
388
|
}
|
|
389
389
|
lines.push(lang === 'ko'
|
|
390
|
-
? '위 내용은 사용자의 첫 메시지 주제와 무관하더라도(단순 인사여도) 첫 응답 말미에 `※ [
|
|
391
|
-
: 'Summarize the above at the end of your first response under a `※ [
|
|
390
|
+
? '위 내용은 사용자의 첫 메시지 주제와 무관하더라도(단순 인사여도) 첫 응답 말미에 `※ [sprag]` 라벨을 달아 요약 브리핑하세요 — 사용자가 물어볼 때까지 기다리지 말 것.'
|
|
391
|
+
: 'Summarize the above at the end of your first response under a `※ [sprag]` label, even if the user\'s first message is unrelated (a bare greeting counts) — do not wait to be asked.');
|
|
392
392
|
// Style first, briefing second: the briefing is Korean prose too, so the
|
|
393
393
|
// guidance has to be in context before the model reads it.
|
|
394
394
|
if (koreanBlock) console.log(koreanBlock + '\n');
|
|
@@ -446,8 +446,8 @@ export async function run({ args, hasFlag, numArg }) {
|
|
|
446
446
|
}
|
|
447
447
|
console.log('');
|
|
448
448
|
console.log(lang === 'ko' ? '등록 / 무시:' : 'Promote / dismiss:');
|
|
449
|
-
console.log('
|
|
450
|
-
console.log('
|
|
449
|
+
console.log(' sprag harness promote R<N> --project|--global');
|
|
450
|
+
console.log(' sprag route-scan dismiss <N>');
|
|
451
451
|
// 최초 1회만 — 매번 찍으면 도구가 광고판이 된다 (CTS_NO_NOTE=1 로 끔)
|
|
452
452
|
(await import('../first-run-note.js')).printOnce('route-scan', lang);
|
|
453
453
|
return;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subcommand: update-check — everything around "is a newer version out".
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* sprag update-check # print the cached answer
|
|
5
|
+
* sprag update-check --refresh # ask the registry now
|
|
6
|
+
* sprag update-check --refresh --quiet
|
|
7
7
|
* # what the detached background child runs
|
|
8
|
-
*
|
|
8
|
+
* sprag update-check --dismiss # stop offering this version at session start
|
|
9
9
|
*
|
|
10
10
|
* The statusline never calls the --refresh path directly; it spawns this
|
|
11
11
|
* command detached so the render itself stays offline and instant.
|
|
@@ -71,7 +71,7 @@ export async function run({ hasFlag, version }) {
|
|
|
71
71
|
: `Up to date (v${version}).`);
|
|
72
72
|
if (!s.latest) {
|
|
73
73
|
console.log(lang === 'ko'
|
|
74
|
-
? `아직 확인된 기록이 없습니다. 지금 확인하려면:
|
|
75
|
-
: `No check has completed yet. Run:
|
|
74
|
+
? `아직 확인된 기록이 없습니다. 지금 확인하려면: sprag update-check --refresh (기록 파일: ${updateStatePath()})`
|
|
75
|
+
: `No check has completed yet. Run: sprag update-check --refresh (state: ${updateStatePath()})`);
|
|
76
76
|
}
|
|
77
77
|
}
|
package/src/doc2md.cjs
CHANGED
|
@@ -158,7 +158,7 @@ function banner(sourcePath, saving) {
|
|
|
158
158
|
const src = fs.statSync(sourcePath);
|
|
159
159
|
const lines = [
|
|
160
160
|
'<!--',
|
|
161
|
-
'
|
|
161
|
+
'sprag doc2md 가 변환한 파일입니다. 직접 편집하지 마십시오.',
|
|
162
162
|
`원본: ${path.resolve(sourcePath)} (${humanBytes(src.size)})`,
|
|
163
163
|
`변환: ${new Date().toISOString()} · Markdown ${humanBytes(Buffer.byteLength(saving.markdown, 'utf8'))} · 약 ${saving.tokens.toLocaleString('en-US')} 토큰`,
|
|
164
164
|
];
|
|
@@ -642,7 +642,7 @@ function markNoticeShown() {
|
|
|
642
642
|
} catch { /* an unwritable state dir just means the notice repeats */ }
|
|
643
643
|
}
|
|
644
644
|
|
|
645
|
-
const INSTALL_HINT = '
|
|
645
|
+
const INSTALL_HINT = 'sprag doc2md install-converter';
|
|
646
646
|
|
|
647
647
|
/**
|
|
648
648
|
* Decide what to tell Claude Code about one PreToolUse(Read) payload.
|
|
@@ -847,8 +847,8 @@ function contextForPrompt(payload, opts = {}) {
|
|
|
847
847
|
: ` ${name}: the converter needs Python 3.10+, and none is on PATH (macOS ships 3.9). Tell the user to install a newer Python, e.g. \`brew install python\`.`);
|
|
848
848
|
} else if (result.reason === 'no-figparser') {
|
|
849
849
|
lines.push(lang === 'ko'
|
|
850
|
-
? ` ${name}: .fig 파서가 없어 변환하지 못했습니다. 설치:
|
|
851
|
-
: ` ${name}: no .fig parser installed. Install it with:
|
|
850
|
+
? ` ${name}: .fig 파서가 없어 변환하지 못했습니다. 설치: sprag doc2md install-converter`
|
|
851
|
+
: ` ${name}: no .fig parser installed. Install it with: sprag doc2md install-converter`);
|
|
852
852
|
} else if (result.reason === 'sensitive') {
|
|
853
853
|
lines.push(lang === 'ko'
|
|
854
854
|
? ` ${name}: 파일명이 민감 문서 패턴에 걸려 변환하지 않았습니다.`
|
|
@@ -861,8 +861,8 @@ function contextForPrompt(payload, opts = {}) {
|
|
|
861
861
|
}
|
|
862
862
|
if (paths.length > targets.length) {
|
|
863
863
|
lines.push(lang === 'ko'
|
|
864
|
-
? ` (문서 ${paths.length}개 가운데 앞 ${targets.length}개만 변환했습니다. 나머지는 \`
|
|
865
|
-
: ` (converted the first ${targets.length} of ${paths.length} documents; run \`
|
|
864
|
+
? ` (문서 ${paths.length}개 가운데 앞 ${targets.length}개만 변환했습니다. 나머지는 \`sprag doc2md <경로>\` 로 변환하십시오.)`
|
|
865
|
+
: ` (converted the first ${targets.length} of ${paths.length} documents; run \`sprag doc2md <path>\` for the rest.)`);
|
|
866
866
|
}
|
|
867
867
|
|
|
868
868
|
return lang === 'ko'
|
|
@@ -870,15 +870,15 @@ function contextForPrompt(payload, opts = {}) {
|
|
|
870
870
|
'[doc2md] 이 프롬프트에 문서 경로가 있어 Markdown 으로 변환해 두었습니다.',
|
|
871
871
|
...lines,
|
|
872
872
|
'원본 대신 위 변환본을 Read 하십시오. pptx·xlsx·docx 는 Read 도구가 이진 파일이라며 거부하므로 원본을 직접 열 수 없습니다.',
|
|
873
|
-
'변환이 일어났다는 사실은 사용자에게도 보여야 하므로, 답변 첫 줄에 변환한 문서마다 다음 형식으로 한 줄씩 출력하십시오: `📄 [
|
|
874
|
-
'경로가 주어지지 않은 문서를 열어야 할 때는 `
|
|
873
|
+
'변환이 일어났다는 사실은 사용자에게도 보여야 하므로, 답변 첫 줄에 변환한 문서마다 다음 형식으로 한 줄씩 출력하십시오: `📄 [sprag] doc2md: <파일명> (<원본 크기> → <Markdown 크기>, 약 <토큰> 토큰)`. 절감액이 함께 안내됐다면 그 금액도 같은 줄 끝에 덧붙이십시오.',
|
|
874
|
+
'경로가 주어지지 않은 문서를 열어야 할 때는 `sprag doc2md <경로>` 를 실행해 변환본 경로를 얻으십시오.',
|
|
875
875
|
].join('\n')
|
|
876
876
|
: [
|
|
877
877
|
'[doc2md] This prompt names document paths, which have been converted to Markdown:',
|
|
878
878
|
...lines,
|
|
879
879
|
'Read the conversions above rather than the originals: Read refuses pptx/xlsx/docx as binary files, so the originals cannot be opened directly.',
|
|
880
|
-
'The user cannot see that any of this happened, so open your reply with one line per converted document: `📄 [
|
|
881
|
-
'For a document whose path was not given here, run `
|
|
880
|
+
'The user cannot see that any of this happened, so open your reply with one line per converted document: `📄 [sprag] doc2md: <name> (<source size> → <markdown size>, ~<tokens> tokens)`, adding the saving above if one was reported.',
|
|
881
|
+
'For a document whose path was not given here, run `sprag doc2md <path>` to get its conversion.',
|
|
882
882
|
].join('\n');
|
|
883
883
|
}
|
|
884
884
|
|
|
@@ -893,14 +893,14 @@ function sessionNote(lang = 'en') {
|
|
|
893
893
|
if (lang === 'ko') {
|
|
894
894
|
return [
|
|
895
895
|
'[doc2md] 문서(pptx·xlsx·pdf·docx)를 다룰 때의 규칙입니다.',
|
|
896
|
-
' 문서를 열어야 하는데 Read 가 이진 파일이라며 거부하면, `
|
|
896
|
+
' 문서를 열어야 하는데 Read 가 이진 파일이라며 거부하면, `sprag doc2md <경로>` 를 실행하고 그 결과로 나온 .md 를 Read 하십시오.',
|
|
897
897
|
' 사용자가 문서를 메시지에 직접 첨부했다면, 첨부는 내용 전체가 컨텍스트에 실려 토큰을 크게 씁니다. 다음부터는 첨부 대신 파일 경로를 적어 달라고 안내하십시오. 경로로 주면 변환본만 읽습니다.',
|
|
898
|
-
' 변환은 단방향입니다. 변환본 .md 를 고쳐도 원본에는 반영되지 않습니다. 문서 수정 요청을 받으면: ① 원본을 복사하고 ② 복사본을 스크립트로 수정하십시오. pptx·docx·xlsx 는 도구 venv 파이썬에 python-pptx·python-docx·openpyxl 이 준비되어 있고(`
|
|
898
|
+
' 변환은 단방향입니다. 변환본 .md 를 고쳐도 원본에는 반영되지 않습니다. 문서 수정 요청을 받으면: ① 원본을 복사하고 ② 복사본을 스크립트로 수정하십시오. pptx·docx·xlsx 는 도구 venv 파이썬에 python-pptx·python-docx·openpyxl 이 준비되어 있고(`sprag doc2md` 상태 출력에 경로가 나옵니다), .fig 는 openfig-core 로 편집·재인코드합니다. ③ 수정한 복사본을 doc2md 로 재변환해 변경이 들어갔는지 검증하십시오. 차트·이미지 같은 시각 요소는 변환본에 안 잡히므로 텍스트 검증만으로 완료를 단정하지 마십시오.',
|
|
899
899
|
].join('\n');
|
|
900
900
|
}
|
|
901
901
|
return [
|
|
902
902
|
'[doc2md] Handling documents (pptx/xlsx/pdf/docx):',
|
|
903
|
-
' If Read refuses a document as a binary file, run `
|
|
903
|
+
' If Read refuses a document as a binary file, run `sprag doc2md <path>` and Read the .md it prints.',
|
|
904
904
|
' If the user attached a document to their message, its full contents were billed into the context. Tell them that naming the file path instead is far cheaper, since only the converted Markdown gets read.',
|
|
905
905
|
' Conversions are one-way: editing the cached .md changes nothing in the source. When asked to modify a document: ① copy the original, ② edit the COPY with a script — the managed venv python has python-pptx/python-docx/openpyxl, and .fig edits go through openfig-core — then ③ re-convert the copy with doc2md to verify the change landed. Charts and images do not appear in conversions, so text verification alone does not prove visual edits.',
|
|
906
906
|
].join('\n');
|
|
@@ -939,7 +939,7 @@ function decideForWrite(context) {
|
|
|
939
939
|
+ ' 문서 수정이 목적이라면: 원본을 복사한 뒤(cp) 복사본을 스크립트로 수정하십시오. '
|
|
940
940
|
+ `pptx·docx·xlsx 는 ${managedPython()} 에 python-pptx·python-docx·openpyxl 이 설치되어 있고, `
|
|
941
941
|
+ '.fig 는 doc2md-fig 의 openfig-core 로 편집·재인코드할 수 있습니다. '
|
|
942
|
-
+ '수정 후 복사본을 `
|
|
942
|
+
+ '수정 후 복사본을 `sprag doc2md <복사본>` 으로 재변환해 의도한 변경이 들어갔는지 확인하십시오. 원본은 절대 직접 수정하지 마십시오.',
|
|
943
943
|
};
|
|
944
944
|
}
|
|
945
945
|
|
|
@@ -949,7 +949,7 @@ function decideForWrite(context) {
|
|
|
949
949
|
reason: `[doc2md] ${path.basename(abs)} 는 이진 문서입니다. Edit/Write 는 텍스트를 쓰므로 이 파일을 파괴합니다. `
|
|
950
950
|
+ '수정하려면 원본을 복사한 뒤(cp) 복사본을 스크립트로 고치십시오. '
|
|
951
951
|
+ `pptx·docx·xlsx 는 ${managedPython()} 의 python-pptx·python-docx·openpyxl, .fig 는 openfig-core 를 쓰고, `
|
|
952
|
-
+ '수정 후 `
|
|
952
|
+
+ '수정 후 `sprag doc2md <복사본>` 재변환으로 결과를 검증하십시오.',
|
|
953
953
|
};
|
|
954
954
|
}
|
|
955
955
|
return null;
|
package/src/harness-templates.js
CHANGED
|
@@ -51,7 +51,7 @@ export function harnessClaudeMdBlock(scope = 'project') {
|
|
|
51
51
|
return `${HARNESS_BLOCK_BEGIN}
|
|
52
52
|
## 🅷 Harness Rules (claude-token-saver)
|
|
53
53
|
|
|
54
|
-
이 섹션은 \`
|
|
54
|
+
이 섹션은 \`sprag harness init\`이 생성합니다. 5가지 원칙 모두를
|
|
55
55
|
지키면 statusline에 \`🅷 5/5\`로 표시되고, 빠진 게 있으면 \`🅷 3/5\` 식으로
|
|
56
56
|
경고합니다. 수정해도 무방하지만, 섹션 헤더(### 1. ~ ### 5.)는 검출용이므로
|
|
57
57
|
지우지 마세요.
|
|
@@ -60,7 +60,7 @@ ${HARNESS_SECTIONS[0].heading}
|
|
|
60
60
|
- 같은 에러·오해·반복 작업이 한 번 더 발생하면 즉시 \`.claude/ratchet.md\`에
|
|
61
61
|
"조건 → 행동" 한 줄로 룰 추가.
|
|
62
62
|
- claude-token-saver가 후보를 감지하면 statusline에 \`🅷⚠ ratchet?\`로 알림.
|
|
63
|
-
\`
|
|
63
|
+
\`sprag harness promote "<rule>" --project|--global\`로 승인.
|
|
64
64
|
- **scope는 항상 사용자에게 먼저 물어볼 것** — 프로젝트 한정이면 \`--project\`,
|
|
65
65
|
도구·환경 일반 룰이면 \`--global\`(\`~/.claude/ratchet.md\`). Bash 환경은
|
|
66
66
|
non-TTY라 CLI의 readline 프롬프트가 안 뜨므로, 호출자(LLM)가 직접 묻고
|
|
@@ -108,9 +108,9 @@ ${HARNESS_SECTIONS[4].heading}
|
|
|
108
108
|
---
|
|
109
109
|
|
|
110
110
|
📌 운영:
|
|
111
|
-
- \`
|
|
112
|
-
- \`
|
|
113
|
-
- \`
|
|
111
|
+
- \`sprag harness check\` — 현재 셋업 점수
|
|
112
|
+
- \`sprag harness promote "<룰>" --project|--global\` — ratchet에 룰 추가 (scope는 사용자에게 먼저 물어볼 것)
|
|
113
|
+
- \`sprag harness off\` — statusline 표시 끄기
|
|
114
114
|
|
|
115
115
|
---
|
|
116
116
|
|
|
@@ -127,13 +127,13 @@ export function harnessRatchetMdInitial() {
|
|
|
127
127
|
|
|
128
128
|
같은 실수가 두 번 발생하면 여기에 한 줄 추가됩니다. 형식: "YYYY-MM-DD: <조건> → <행동>".
|
|
129
129
|
|
|
130
|
-
\`
|
|
130
|
+
\`sprag harness promote "<rule>"\`로 룰을 추가하면 자동으로
|
|
131
131
|
이 파일에 append 됩니다.
|
|
132
132
|
|
|
133
133
|
이 파일은 CLAUDE.md의 \`@\` import로 매 세션 로드됩니다 — 즉 여기 있는 모든 줄이
|
|
134
134
|
매 요청마다 토큰을 씁니다. 날짜 뒤에 \`[태그]\`를 붙여두면 나중에 묶어서 정리할 수
|
|
135
135
|
있습니다: \`- 2026-05-08: [video,tts] ...\` →
|
|
136
|
-
\`
|
|
136
|
+
\`sprag harness prune --tag video\` (삭제 아니라 ratchet-archive.md로 이동).
|
|
137
137
|
|
|
138
138
|
## Rules
|
|
139
139
|
|
package/src/korean-style.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Cost: the text is ~1.3k tokens, injected once per session (not per turn) and
|
|
16
16
|
* covered by the prompt cache from the second request on. A token-saving tool
|
|
17
17
|
* has no business spending that silently, so the feature is opt-in via
|
|
18
|
-
* `
|
|
18
|
+
* `sprag korean on`.
|
|
19
19
|
*
|
|
20
20
|
* The guidance itself is vendored from fluent-korean
|
|
21
21
|
* (https://github.com/snflkd/fluent-korean), Copyright (c) 2026 snflkd, MIT
|
|
@@ -176,7 +176,7 @@ export function koreanStyleInjection({ cfg = loadConfig() } = {}) {
|
|
|
176
176
|
const text = koreanStyleText();
|
|
177
177
|
if (!text) return null;
|
|
178
178
|
return [
|
|
179
|
-
'[
|
|
179
|
+
'[sprag korean-style] 이 세션에서 한국어를 출력할 때는 아래 지침을 따르십시오.',
|
|
180
180
|
'이 지침은 사용자가 claude-token-saver에 설정한 것입니다.',
|
|
181
181
|
...koreanScopeLines(cfg),
|
|
182
182
|
'표기 규칙(예외 없이 적용): 도구 호출 인자에 한국어를 비롯한 비ASCII 문자열을 담을 때에는 반드시 리터럴 UTF-8로 작성하고, \\uXXXX 유니코드 이스케이프로는 절대 작성하지 마십시오. 이스케이프로 작성하면 글자가 깨진 채 파일에 기록되는 사례가 자주 발생합니다.',
|
package/src/model-rules.js
CHANGED
|
@@ -201,7 +201,7 @@ export function renderModelRatchet(rules, lang = userLanguage()) {
|
|
|
201
201
|
'# Model-Fitting Ratchet (claude-token-saver 자동 관리)',
|
|
202
202
|
'',
|
|
203
203
|
'로그 기반 티어 위임 룰. 이 파일은 route-scan이 매 스캔마다 통째로 재생성하므로',
|
|
204
|
-
'직접 수정하지 마세요 — 목록/제거: `
|
|
204
|
+
'직접 수정하지 마세요 — 목록/제거: `sprag route-scan rules [rm <N>]`.',
|
|
205
205
|
'',
|
|
206
206
|
'**사용자는 아래 룰에 해당하는 서브에이전트 위임을 이미 승인했습니다.**',
|
|
207
207
|
'룰에 들어맞는 요청이면 되묻지 말고 바로 위임하십시오. "사용자가 명시적으로',
|
|
@@ -212,7 +212,7 @@ export function renderModelRatchet(rules, lang = userLanguage()) {
|
|
|
212
212
|
'',
|
|
213
213
|
'위임을 실행할 때는 사용자에게 다음 한 줄을 먼저 표시할 것',
|
|
214
214
|
'(어떤 도구가 토큰을 아끼고 있는지 가시화):',
|
|
215
|
-
'`🔀 [
|
|
215
|
+
'`🔀 [sprag] 모델 피팅: "<유형>" → <agent> 위임`',
|
|
216
216
|
'',
|
|
217
217
|
'## 티어 판별 기준 (모든 룰에 공통 적용)',
|
|
218
218
|
'',
|
|
@@ -249,7 +249,7 @@ export function renderModelRatchet(rules, lang = userLanguage()) {
|
|
|
249
249
|
'',
|
|
250
250
|
'Log-derived tier delegation rules. route-scan regenerates this file in full',
|
|
251
251
|
'on every scan — do not edit it by hand. List / remove with:',
|
|
252
|
-
'`
|
|
252
|
+
'`sprag route-scan rules [rm <N>]`.',
|
|
253
253
|
'',
|
|
254
254
|
'**The user has already approved the subagent delegations described below.**',
|
|
255
255
|
'When a request matches a rule, delegate right away instead of asking. If a',
|
|
@@ -260,7 +260,7 @@ export function renderModelRatchet(rules, lang = userLanguage()) {
|
|
|
260
260
|
'',
|
|
261
261
|
'When delegating under a rule below, show the user this line first so it is',
|
|
262
262
|
'visible which tool is saving tokens:',
|
|
263
|
-
'`🔀 [
|
|
263
|
+
'`🔀 [sprag] model fitting: "<category>" → delegated to <agent>`',
|
|
264
264
|
'',
|
|
265
265
|
'## Tier criteria (apply to every rule below)',
|
|
266
266
|
'',
|
package/src/update-check.js
CHANGED
|
@@ -23,7 +23,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
23
23
|
import { userDataDir } from './paths.js';
|
|
24
24
|
import { debug } from './debug.js';
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// Whichever name this copy was installed under (sprag-cli or the legacy
|
|
27
|
+
// claude-token-saver) is the one whose registry entry must be consulted.
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
const PKG_NAME = createRequire(import.meta.url)('../package.json').name;
|
|
27
30
|
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h — the registry is not a health endpoint
|
|
28
31
|
const FETCH_TIMEOUT_MS = 5000;
|
|
29
32
|
|