claude-token-saver 3.37.0 → 3.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -214,6 +214,7 @@ Claude Code 안에서 `/claude-token-saver` Skill을 실행하거나, 칩에 적
214
214
  | `claude-token-saver route-scan savings` | 라우팅 절감 원장입니다. 모델 이동별 합계와 실행별 내역을 함께 보여 주며, 표시되는 금액의 근거가 됩니다 |
215
215
  | `claude-token-saver compact-window` | 1M 컨텍스트를 쓰면서 자동 압축 창이 설정되지 않았으면 경고하고, `set`으로 40만에 고정합니다 (아래 참고) |
216
216
  | `claude-token-saver korean on\|off\|status` | 한국어 문체 지침을 세션 시작 시 주입하고, 쓰기 시점 검사를 함께 설치합니다 (아래 참고) |
217
+ | `claude-token-saver cohesion on\|off\|status\|show` | 영어 문장 연결(응집성) 지침을 세션 시작 시 주입합니다 |
217
218
  | `claude-token-saver korean lint block\|warn\|off` | 쓰기 시점 검사가 위반을 어떻게 처리할지 정합니다 |
218
219
  | `claude-token-saver korean lint scope all\|prose` | 검사 범위를 모든 텍스트 파일과 문서 전용 사이에서 고릅니다 |
219
220
  | `claude-token-saver doc2md on\|off` | 첨부 문서를 모델이 읽기 전에 Markdown 으로 변환합니다 (아래 참고) |
@@ -416,7 +417,7 @@ Claude Code의 output style로도 같은 일을 할 수 있지만, output style
416
417
  - **상투 패턴**: 자동으로 붙는 수식어("다양한", "핵심적인"), 표지판 문장, 수사적 질문 뒤 즉답, 근거 없는 긍정 마무리.
417
418
  - **응집성**: 문장이 이어지는 방식이라 정규식으로는 검사하지 못합니다. 이 절의 방향을 정한 연구 결과가 있습니다. 접속어·지시어 같은 표층 연결 장치는 글의 품질과 상관이 없거나 오히려 부적 상관이고, 앞 문장이 내놓은 정보를 다음 문장이 받아 풀어 주는 상술형 연결만 정적 상관을 보였습니다. 그래서 지침은 연결이 어색할 때 접속어를 더하지 말고 정보의 배열(아는 것 먼저, 새 것 나중)을 고치라고 말합니다.
418
419
 
419
- 응집성 층의 원칙 대부분은 한국어에만 해당하지 않습니다. 구정보 우선 배열, 대명사의 단일 지시, 문단 안 주어 유지, 비약을 잇는 다리 문장, 짧은 반복 문장 병합은 영어 산문에도 그대로 적용됩니다. 논문이 한국어 학습자를 다뤘을 뿐, 검증된 원칙은 텍스트언어학의 표준 응집성 모형입니다.
420
+ 응집성 층의 원칙 대부분은 한국어에만 해당하지 않습니다. 구정보 우선 배열, 대명사의 단일 지시, 문단 안 주어 유지, 비약을 잇는 다리 문장, 짧은 반복 문장 병합은 영어 산문에도 그대로 적용됩니다. 논문이 한국어 학습자를 다뤘을 뿐, 검증된 원칙은 텍스트언어학의 표준 응집성 모형입니다. 영어 산출물에는 `claude-token-saver cohesion on`이 같은 다섯 원칙을 영어 블록으로 주입합니다 (korean 지침이 켜져 있으면 중복이라 생략됩니다).
420
421
 
421
422
  마지막 절은 고치면 안 되는 것을 명시합니다. 정착된 전문 용어, 문어체, 원문 인용이 그것이고, 검사가 내는 지적은 판정이 아니라 확인 요청입니다.
422
423
 
@@ -601,10 +602,26 @@ npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
601
602
 
602
603
  전체 내역은 [CHANGELOG.md](./CHANGELOG.md)로 옮겼습니다. 최근 변경은 다음과 같습니다.
603
604
 
605
+ - **v3.39.0**: `feedback` 서브커맨드가 터미널이나 Claude 세션에서 버그 제보·기능 제안을 바로 제출합니다. gh CLI가 있으면 이슈 직접 등록, 없으면 로그인 없는 익명 제출(릴레이가 이슈로 자동 등록), 오프라인이면 로컬 저장. `install`은 기존 statusline이 있으면 조용히 건너뛰지 않고 교체 여부를 묻습니다.
606
+ - **v3.38.0**: `cohesion on`이 응집성 다섯 원칙을 영어 블록으로 주입합니다. 옵트인이고, korean 지침이 켜져 있으면 중복 주입을 막습니다.
604
607
  - **v3.37.0**: 한국어 지침에 보강 지침(supplement)이 붙습니다. 번역투·상투 패턴·응집성(문장 이어짐) 조항이며, 쓰기 시점 검사에도 번역투 5종이 추가됐습니다. 실파일 255개 실측에서 오탐 1건으로 검증했습니다.
605
608
  - **v3.35.0**: 이번 달 1일 00시 이후 지출 추정치를 `💵 Sep $42` 세그먼트로 상시 표시합니다. LiteLLM 게이트웨이 사용자는 키의 max_budget/spend 를 `🔑 budget` 게이지로 봅니다 (5h/7d cap 이 없는 Bedrock·LiteLLM 환경 대응).
606
609
  - **v3.34.0**: seed 프리셋 제안, 설치 시 출력 언어 선택, 컨텍스트 경고 500k 상향. 상세는 CHANGELOG 참고.
607
610
 
611
+ ## 피드백
612
+
613
+ 버그 제보와 기능 제안은 GitHub 이슈로 받습니다: https://github.com/rootstudioyaml/claude-token-saver/issues
614
+
615
+ 브라우저나 GitHub 로그인이 어려운 환경(사내망, 세션 도중)이라면 터미널에서 바로 제출할 수 있고, Claude에게 대신 제출해 달라고 요청해도 됩니다.
616
+
617
+ ```bash
618
+ claude-token-saver feedback "Bedrock에서 5m TTL 칩이 사라지지 않아요"
619
+ ```
620
+
621
+ `gh` CLI가 인증되어 있으면 GitHub 이슈로 바로 등록하고, 없으면 로그인이 필요 없는 익명 경로로 제출합니다(github.com이 막힌 사내망에서도 동작합니다). `gh` 경로를 건너뛰려면 `--anonymous`를 붙입니다. 도구 버전과 OS 정보는 자동으로 첨부됩니다.
622
+
623
+ 버그를 제보하실 때는 도구 버전(`claude-token-saver --version`)과 OS를 함께 적어 주시고, statusline이나 경고 관련 문제라면 statusline 출력이나 `claude-token-saver last` 결과를 덧붙여 주시면 원인을 빨리 찾을 수 있습니다.
624
+
608
625
  ## 라이선스
609
626
 
610
627
  MIT
package/README.md CHANGED
@@ -217,6 +217,7 @@ Run these in your shell (inside Claude Code, the `/claude-token-saver` Skill is
217
217
  | `claude-token-saver route-scan savings` | The routing-savings ledger — per-model-change rollup + per-run log (the evidence behind the figure) |
218
218
  | `claude-token-saver compact-window` | Warn when a 1M-context session has no auto-compact cap → pin 400k with `set` (below) |
219
219
  | `claude-token-saver korean on\|off\|status` | Inject Korean writing guidance at session start and install the write-time check (below) |
220
+ | `claude-token-saver cohesion on\|off\|status\|show` | Inject English cohesion guidance (sentence-connection rules) at session start |
220
221
  | `claude-token-saver korean lint block\|warn\|off` | How the write-time check handles findings |
221
222
  | `claude-token-saver korean lint scope all\|prose` | Check every text file, or documents only |
222
223
  | `claude-token-saver doc2md on\|off` | Convert attached documents to Markdown before the model reads them (below) |
@@ -428,7 +429,7 @@ It adds three layers:
428
429
  - **AI-writing tics**: automatic intensifiers ("다양한", "핵심적인"), signpost sentences, rhetorical question-then-answer, unconditionally upbeat endings.
429
430
  - **Cohesion** — how sentences connect, which no regex can check. The research finding that shapes this section: surface connectives (conjunctions, demonstratives) correlate *negatively or not at all* with judged text quality, while elaboration — the next sentence picking up and unpacking what the previous one introduced — is the only connection type with a positive correlation. So the guidance says: when a transition feels rough, fix the information order (given before new), don't add a connective.
430
431
 
431
- **Most of the cohesion layer is not Korean-specific.** Given-before-new ordering (the "given-new contract"), one clear referent per pronoun, keeping one subject per paragraph, bridging sentences instead of leaping, and merging choppy repetitive sentences into a modifier-plus-core structure apply to English prose the same way — the studies happen to be about Korean learners, but the principles they validate are the standard cohesion model from text linguistics. If you write English deliverables with Claude, those five rules are worth pinning in your own CLAUDE.md even with this feature off.
432
+ **Most of the cohesion layer is not Korean-specific.** Given-before-new ordering (the "given-new contract"), one clear referent per pronoun, keeping one subject per paragraph, bridging sentences instead of leaping, and merging choppy repetitive sentences into a modifier-plus-core structure apply to English prose the same way — the studies happen to be about Korean learners, but the principles they validate are the standard cohesion model from text linguistics. If you write English deliverables with Claude, run `claude-token-saver cohesion on` — it injects exactly those five rules as a standalone English block (~0.5k tokens per session), no Korean feature required. While `korean on` is active the block is suppressed, because the Korean supplement already carries the same rules.
432
433
 
433
434
  A final subsection lists what must **not** be "corrected": settled domain terms, formal register, and verbatim quotations — every lint finding is a request to confirm, not a verdict.
434
435
 
@@ -722,10 +723,26 @@ Also update `statusLine.command` in `~/.claude/settings.json` to `claude-token-s
722
723
 
723
724
  The full history moved to [CHANGELOG.md](./CHANGELOG.md) (Korean; version headings and command names are language-neutral). Recent changes:
724
725
 
726
+ - **v3.39.0**: `feedback` subcommand — file bug reports and feature requests straight from the terminal or a Claude session, via the gh CLI, an anonymous no-login form (auto-filed as a GitHub issue by an Apps Script relay), or a local fallback. `install` now asks before replacing an existing statusline instead of silently skipping.
727
+ - **v3.38.0**: `cohesion on` — the language-neutral cohesion rules from the Korean supplement become a standalone English injection (given-before-new, one referent per pronoun, subject consistency, bridging, merging choppy sentences). Opt-in, ~0.5k tokens per session, suppressed while `korean on` already carries them.
725
728
  - **v3.37.0**: Korean guidance grows a conservative supplement (translationese, AI-writing tics, a research-backed cohesion section whose principles apply to English prose too) and the write-time lint gains 5 translationese patterns, validated at 1 false positive across 255 real files.
726
729
  - **v3.35.0**: A `💵 Sep $42` segment now shows estimated spend since 00:00 on the 1st of the current month, always on — including gateway setups with no 5h/7d caps. LiteLLM gateway users get a `🔑 budget ▰▱ 34% $34/$100` gauge built from the key's budget (`GET /key/info` + `GET /user/info`, team-membership budget first, then key, then internal user — verified against a Dockerized LiteLLM).
727
730
  - **v3.34.0**: seed presets offered one at a time, output-language choice at install, context warning raised to 500k.
728
731
 
732
+ ## Feedback
733
+
734
+ Found a bug, or want a feature? Open an issue: https://github.com/rootstudioyaml/claude-token-saver/issues
735
+
736
+ No browser or GitHub login handy (corporate network, mid-session)? Submit straight from the terminal — or ask Claude to do it for you:
737
+
738
+ ```bash
739
+ claude-token-saver feedback "the 5m TTL chip never clears on Bedrock"
740
+ ```
741
+
742
+ It files a GitHub issue via the `gh` CLI when one is authenticated; otherwise it submits anonymously (no login, works where github.com is blocked). Pass `--anonymous` to skip the `gh` path. Version and OS metadata are attached automatically.
743
+
744
+ When reporting a bug, please include the tool version (`claude-token-saver --version`), your OS, and — if it is a statusline or warning issue — the statusline output or the `claude-token-saver last` result.
745
+
729
746
  ## License
730
747
 
731
748
  MIT
package/bin/cli.js CHANGED
@@ -85,9 +85,9 @@ function readUpdateChip() {
85
85
 
86
86
  // Subcommands this build knows how to run. Used only by the guard below.
87
87
  const KNOWN_SUBCOMMANDS = new Set([
88
- 'last', 'brief', 'history', 'handoff', 'install', 'uninstall', 'mode', 'korean',
88
+ 'last', 'brief', 'history', 'handoff', 'install', 'uninstall', 'mode', 'korean', 'cohesion',
89
89
  'doc2md', 'harness', 'route-scan', 'compact-window', 'update-check', 'upgrade',
90
- 'seed', 'litellm-budget',
90
+ 'seed', 'litellm-budget', 'feedback',
91
91
  ]);
92
92
 
93
93
  const USAGE = `claude-token-saver — Claude Code token usage, cache health, and model routing
@@ -107,6 +107,7 @@ Usage:
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
+ claude-token-saver feedback "<msg>" file a bug report / feature request (no browser needed)
110
111
  claude-token-saver upgrade install the latest release
111
112
  claude-token-saver --install-hook install cache-monitor PostToolUse hook
112
113
  claude-token-saver --uninstall-hook remove that hook
@@ -114,6 +115,7 @@ Usage:
114
115
  --verbose / --no-color / --icon / --no-timer / --single-line
115
116
 
116
117
  Run any subcommand with --help for its own options where available.
118
+ Bug reports & feature requests: https://github.com/rootstudioyaml/claude-token-saver/issues
117
119
  `;
118
120
 
119
121
  async function main() {
@@ -161,6 +163,14 @@ async function main() {
161
163
  // hits.
162
164
  // claude-token-saver handoff # write to cwd
163
165
  // claude-token-saver handoff --cwd PATH # custom directory
166
+ // Subcommand: feedback — file a bug report / feature request without a
167
+ // browser. Tries gh CLI, then an anonymous form POST, then a local save
168
+ // with a prefilled GitHub issue URL. See src/commands/feedback.js.
169
+ if (args[0] === 'feedback') {
170
+ const { userDataDir } = await import('../src/paths.js');
171
+ return (await import('../src/commands/feedback.js')).run({ args, getArg, version: PKG_VERSION, dataDir: userDataDir() });
172
+ }
173
+
164
174
  if (args[0] === 'handoff') {
165
175
  return (await import('../src/commands/handoff.js')).run({ getArg });
166
176
  }
@@ -218,6 +228,13 @@ async function main() {
218
228
  return (await import('../src/commands/korean.js')).run({ args, hasFlag });
219
229
  }
220
230
 
231
+ // Subcommand: cohesion — English sentence-connection guidance injected at
232
+ // session start; the language-neutral half of the Korean supplement.
233
+ // claude-token-saver cohesion on | off | status | show
234
+ if (args[0] === 'cohesion') {
235
+ return (await import('../src/commands/cohesion.js')).run({ args, hasFlag });
236
+ }
237
+
221
238
  // Subcommand: doc2md — convert pptx/xlsx/pdf/docx to Markdown before the
222
239
  // model reads them, so an unreadable binary never enters the context window.
223
240
  // claude-token-saver doc2md on | off | <file> | --clean
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-token-saver",
3
- "version": "3.37.0",
3
+ "version": "3.39.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": {
@@ -0,0 +1,26 @@
1
+ <!--
2
+ English cohesion guidance — claude-token-saver's own text, distilled from the
3
+ same sources as the Korean supplement's cohesion section: the given-new
4
+ contract from text linguistics, and three studies on Korean learner writing
5
+ whose validated principles are language-neutral (surface connectives
6
+ correlate negatively or not at all with judged text quality; elaboration is
7
+ the only connection type with a positive correlation).
8
+
9
+ Injected at session start when `claude-token-saver cohesion on` is set and
10
+ the Korean guidance is off (the Korean supplement already carries these
11
+ rules, so injecting both would bill the same principles twice).
12
+ -->
13
+
14
+ Follow these rules whenever you write English prose the user will read — answers, documents, reports, comments, UI copy. They govern how sentences connect, which is where generated text most often reads as stilted even when every sentence is individually fine.
15
+
16
+ 1. **Move from known to new.** Start each sentence with information the reader already has; put the new information at the end. Let the next sentence pick up that new information and unpack it. When a transition feels rough, fix this ordering first — do not reach for a connective. Research on text quality found that surface connectives (however, moreover, additionally) correlate negatively or not at all with judged quality; elaboration — the next sentence developing what the previous one introduced — is the only connection type that correlates positively.
17
+
18
+ 2. **One clear referent per pronoun.** If "it", "this", or "they" could point at more than one thing in the previous sentence, repeat the noun instead. Introduce people and organizations with a role tag on first mention ("the maintainer, Alice Park") rather than dropping a bare name.
19
+
20
+ 3. **Keep one subject per paragraph.** Changing the grammatical subject every sentence forces the reader to reorient each time. Stay with one subject unless the topic actually shifts.
21
+
22
+ 4. **No leaps.** If a sentence presupposes a condition or a cause the text has not established, add the bridging sentence rather than trusting the reader to reconstruct it. The most common failure is a new entity appearing with a definite article ("the report", "the agent") before anything has introduced it.
23
+
24
+ 5. **Merge choppy repetition.** Three short sentences circling the same subject read worse than one sentence with the minor facts folded into modifiers. Demote the less important sentence to a clause; keep the core claim as the main clause.
25
+
26
+ Do not overcorrect: settled domain terms, formal register, and verbatim quotations stay as they are.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * cohesion — English sentence-connection guidance injected at session start.
3
+ *
4
+ * The Korean supplement's cohesion section turned out to be language-neutral:
5
+ * given-before-new ordering, one referent per pronoun, subject consistency,
6
+ * bridging instead of leaping, merging choppy sentences. English-only users
7
+ * never run `korean on`, so those rules never reached them. This module ships
8
+ * the same principles as a standalone English block.
9
+ *
10
+ * Off by default for the same reason the Korean guidance is opt-in: a
11
+ * token-saving tool has no business silently billing ~0.5k tokens a session.
12
+ * Enable with `claude-token-saver cohesion on`.
13
+ *
14
+ * When the Korean guidance is enabled, this block is NOT injected even if
15
+ * enabled: the Korean supplement already carries the cohesion rules, and
16
+ * injecting the same principles twice bills them twice.
17
+ */
18
+
19
+ import { readFileSync, existsSync } from 'node:fs';
20
+ import { join, dirname } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { loadConfig, saveConfig } from './config.js';
23
+
24
+ const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
25
+
26
+ export const COHESION_PATH = join(packageRoot, 'presets', 'cohesion', 'cohesion-en.md');
27
+
28
+ /** Whether session-start injection is enabled. Off unless the user asked. */
29
+ export function cohesionEnabled(cfg = loadConfig()) {
30
+ return cfg?.cohesion?.enabled === true;
31
+ }
32
+
33
+ export function setCohesionEnabled(enabled) {
34
+ const cfg = loadConfig();
35
+ cfg.cohesion = { ...(cfg.cohesion || {}), enabled: !!enabled };
36
+ saveConfig(cfg);
37
+ return cfg.cohesion;
38
+ }
39
+
40
+ /** The guidance text with the provenance comment stripped, or null. */
41
+ export function cohesionText() {
42
+ try {
43
+ if (!existsSync(COHESION_PATH)) return null;
44
+ const raw = readFileSync(COHESION_PATH, 'utf8');
45
+ const body = raw.replace(/^<!--[\s\S]*?-->\s*/, '').trim();
46
+ return body || null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Block to inject at session start, or null when disabled, unavailable, or
54
+ * redundant (Korean guidance on — its supplement already carries these rules).
55
+ */
56
+ export async function cohesionInjection({ cfg = loadConfig() } = {}) {
57
+ if (!cohesionEnabled(cfg)) return null;
58
+ try {
59
+ const { koreanStyleEnabled } = await import('./korean-style.js');
60
+ if (koreanStyleEnabled(cfg)) return null;
61
+ } catch { /* korean module unavailable: inject normally */ }
62
+ const text = cohesionText();
63
+ if (!text) return null;
64
+ return [
65
+ '[claude-token-saver cohesion] Follow this guidance for English prose in this session.',
66
+ 'The user enabled it in claude-token-saver.',
67
+ '',
68
+ text,
69
+ ].join('\n');
70
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Subcommand: cohesion — English sentence-connection guidance for every session.
3
+ * claude-token-saver cohesion on # inject at session start, all projects
4
+ * claude-token-saver cohesion off # stop injecting
5
+ * claude-token-saver cohesion status # current state and cost
6
+ * claude-token-saver cohesion show # print the guidance itself
7
+ *
8
+ * The English sibling of `korean on`, carrying only the language-neutral
9
+ * cohesion rules (given-before-new, one referent per pronoun, subject
10
+ * consistency, bridging, merging choppy sentences). No lint: every clause
11
+ * needs judgement, so nothing here is machine-checkable.
12
+ */
13
+
14
+ export async function run({ args }) {
15
+ const sub = args[1] || 'status';
16
+ const co = await import('../cohesion.js');
17
+ const { userLanguage } = await import('../config.js');
18
+ const lang = userLanguage();
19
+
20
+ if (sub === 'on' || sub === 'off') {
21
+ co.setCohesionEnabled(sub === 'on');
22
+ if (sub === 'on') {
23
+ console.log(lang === 'ko'
24
+ ? '✍️ cohesion on: 다음 세션부터 영어 문장 연결 지침이 주입됩니다 (약 0.5k 토큰/세션, 세션 시작 1회).'
25
+ : '✍️ cohesion on — English cohesion guidance will be injected from the next session (~0.5k tokens per session, once at session start).');
26
+ const { koreanStyleEnabled } = await import('../korean-style.js');
27
+ if (koreanStyleEnabled()) {
28
+ console.log(lang === 'ko'
29
+ ? '참고: korean 지침이 켜져 있는 동안에는 같은 원칙이 이미 들어가므로 이 블록은 주입되지 않습니다.'
30
+ : 'Note: while the Korean guidance is on, its supplement already carries these rules, so this block is not injected.');
31
+ }
32
+ } else {
33
+ console.log(lang === 'ko' ? 'cohesion off: 더 이상 주입하지 않습니다.' : 'cohesion off — no longer injected.');
34
+ }
35
+ return;
36
+ }
37
+
38
+ if (sub === 'show') {
39
+ const text = co.cohesionText();
40
+ if (!text) {
41
+ console.error('cohesion guidance file missing: ' + co.COHESION_PATH);
42
+ process.exit(1);
43
+ }
44
+ console.log(text);
45
+ return;
46
+ }
47
+
48
+ // status (default)
49
+ const enabled = co.cohesionEnabled();
50
+ const { koreanStyleEnabled } = await import('../korean-style.js');
51
+ const suppressed = enabled && koreanStyleEnabled();
52
+ if (lang === 'ko') {
53
+ console.log(`cohesion: ${enabled ? 'on' : 'off'}${suppressed ? ' (korean 지침이 켜져 있어 주입은 생략됨)' : ''}`);
54
+ console.log('영어 산문의 문장 연결 지침을 세션 시작에 주입합니다. 켜기: claude-token-saver cohesion on');
55
+ } else {
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: claude-token-saver cohesion on');
58
+ }
59
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Subcommand: feedback — submit a bug report or feature request from the
3
+ * terminal (or from a Claude Code session) without opening a browser.
4
+ *
5
+ * claude-token-saver feedback "statusline이 IntelliJ에서 깨져요"
6
+ * claude-token-saver feedback --title "cache chip" "5m TTL 칩이 안 사라짐"
7
+ *
8
+ * Why this exists: GitHub issues require a logged-in browser session, and
9
+ * corporate networks often block github.com entirely. This command tries
10
+ * three transports in order and reports which one carried the message:
11
+ *
12
+ * 1. `gh` CLI, if installed and authenticated — files a real GitHub issue.
13
+ * 2. Anonymous Google Form POST — no login, no GitHub access needed.
14
+ * (Only when the form endpoint below is configured for this build.)
15
+ * 3. Local fallback — saves the report to a Markdown file and prints a
16
+ * prefilled GitHub new-issue URL to use from an unblocked machine.
17
+ *
18
+ * Metadata (tool version, OS, Node version) is attached automatically so a
19
+ * report is diagnosable without a follow-up round trip.
20
+ */
21
+
22
+ import { spawnSync } from 'node:child_process';
23
+ import { writeFileSync, mkdirSync } from 'node:fs';
24
+ import { join } from 'node:path';
25
+ import os from 'node:os';
26
+ import { debug } from '../debug.js';
27
+
28
+ const REPO = 'rootstudioyaml/claude-token-saver';
29
+ const ISSUES_URL = `https://github.com/${REPO}/issues`;
30
+
31
+ // Anonymous submission endpoint (Google Form). A Google Form's formResponse
32
+ // URL accepts unauthenticated POSTs, which is exactly the property a
33
+ // login-free, GitHub-blocked-network path needs. Responses land in the
34
+ // maintainer's "claude-token-saver 피드백 (Feedback)" form (published
35
+ // 2026-09-13, responder access: anyone with the link).
36
+ // id — the /d/e/<id>/ segment of the form URL
37
+ // message — entry.NNNN field id of the message question
38
+ // meta — entry.NNNN field id of the metadata question
39
+ const FORM = {
40
+ id: '1FAIpQLScISjU8_t9y8xfp-X3jvLV6_RcSjQIJ4aydATC8NazkxcJbgg',
41
+ message: 'entry.79517542',
42
+ meta: 'entry.112678638',
43
+ };
44
+
45
+ function metadata(version) {
46
+ return [
47
+ `version: ${version}`,
48
+ `os: ${process.platform} ${os.release()}`,
49
+ `node: ${process.version}`,
50
+ ].join('\n');
51
+ }
52
+
53
+ function tryGhCli(title, body) {
54
+ try {
55
+ const auth = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', timeout: 10_000 });
56
+ if (auth.status !== 0) return null;
57
+ const res = spawnSync('gh', ['issue', 'create', '-R', REPO, '--title', title, '--body', body],
58
+ { encoding: 'utf8', timeout: 30_000 });
59
+ if (res.status !== 0) { debug('feedback:gh', res.stderr); return null; }
60
+ const url = String(res.stdout).trim().split('\n').pop();
61
+ return { transport: 'gh', url };
62
+ } catch (e) { debug('feedback:gh', e); return null; }
63
+ }
64
+
65
+ async function tryForm(title, body, meta) {
66
+ if (!FORM) return null;
67
+ try {
68
+ const params = new URLSearchParams();
69
+ params.set(FORM.message, `${title}\n\n${body}`);
70
+ params.set(FORM.meta, meta);
71
+ const res = await fetch(`https://docs.google.com/forms/d/e/${FORM.id}/formResponse`, {
72
+ method: 'POST',
73
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
74
+ body: params.toString(),
75
+ signal: AbortSignal.timeout(15_000),
76
+ });
77
+ if (!res.ok) { debug('feedback:form', `HTTP ${res.status}`); return null; }
78
+ return { transport: 'form' };
79
+ } catch (e) { debug('feedback:form', e); return null; }
80
+ }
81
+
82
+ function saveLocal(title, body, dataDir) {
83
+ const dir = join(dataDir, 'feedback');
84
+ mkdirSync(dir, { recursive: true });
85
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
86
+ const file = join(dir, `${stamp}.md`);
87
+ writeFileSync(file, `# ${title}\n\n${body}\n`);
88
+ const prefilled = `${ISSUES_URL}/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
89
+ return { transport: 'local', file, prefilled };
90
+ }
91
+
92
+ export async function run({ args, getArg, version, dataDir }) {
93
+ // --anonymous skips the gh transport: for people whose gh CLI is signed in
94
+ // with an account they do not want attached to the report.
95
+ const anonymous = args.includes('--anonymous');
96
+ const rest = args.slice(1).filter((a, i, all) => {
97
+ if (a === '--title') return false;
98
+ if (all[i - 1] === '--title') return false;
99
+ return !a.startsWith('--');
100
+ });
101
+ const message = rest.join(' ').trim();
102
+ const explicitTitle = getArg('--title');
103
+
104
+ if (!message) {
105
+ console.log('usage: claude-token-saver feedback [--title "<제목>"] "<내용>"');
106
+ console.log(` (GitHub에서 직접 제보: ${ISSUES_URL})`);
107
+ process.exitCode = 1;
108
+ return;
109
+ }
110
+
111
+ const title = explicitTitle || (message.length > 60 ? `${message.slice(0, 57)}...` : message);
112
+ const meta = metadata(version);
113
+ const body = `${message}\n\n---\n${meta}`;
114
+
115
+ const viaGh = anonymous ? null : tryGhCli(title, body);
116
+ if (viaGh) {
117
+ console.log(`feedback: GitHub 이슈로 등록했습니다 — ${viaGh.url}`);
118
+ return;
119
+ }
120
+
121
+ const viaForm = await tryForm(title, message, meta);
122
+ if (viaForm) {
123
+ console.log('feedback: 제출했습니다. 감사합니다. (익명 제출이라 답변 추적은 GitHub 이슈에서만 가능합니다)');
124
+ console.log(` 공개 트래커: ${ISSUES_URL}`);
125
+ return;
126
+ }
127
+
128
+ const local = saveLocal(title, body, dataDir);
129
+ console.log(`feedback: 온라인 제출 경로가 없어 로컬에 저장했습니다: ${local.file}`);
130
+ console.log(' GitHub 접근이 가능한 환경에서 아래 주소를 열면 내용이 채워진 이슈 작성 화면이 나옵니다:');
131
+ console.log(` ${local.prefilled}`);
132
+ }
@@ -73,7 +73,31 @@ export async function run({ hasFlag }) {
73
73
  print('SessionStart hook (route-scan)', r.sessionStartHook);
74
74
  print('UserPromptSubmit hook (brief)', r.briefHook);
75
75
  {
76
- const s = r.statusline;
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
+ }
77
101
  const verb = s.action === 'exists' ? 'already configured (refreshInterval=5)'
78
102
  : s.action === 'skipped' ? `skipped — ${s.reason}`
79
103
  : s.reason ? `${s.action} — ${s.reason}`
@@ -326,5 +350,9 @@ export async function run({ hasFlag }) {
326
350
  console.log('');
327
351
  console.log('Tip: re-run with --force to overwrite the existing skill file.');
328
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');
329
357
  return;
330
358
  }
@@ -251,6 +251,14 @@ export async function run({ args, hasFlag, numArg }) {
251
251
  koreanBlock = koreanStyleInjection();
252
252
  } catch (e) { debug('route-scan:korean-style', e); /* style is optional */ }
253
253
 
254
+ // English cohesion guidance, when enabled. Same round-trip; skipped
255
+ // internally when the Korean guidance already carries the same rules.
256
+ let cohesionBlock = null;
257
+ try {
258
+ const { cohesionInjection } = await import('../cohesion.js');
259
+ cohesionBlock = await cohesionInjection();
260
+ } catch (e) { debug('route-scan:cohesion', e); /* the guidance is optional */ }
261
+
254
262
  // doc2md's standing note, only when the user turned the feature on.
255
263
  // Rides the same SessionStart round-trip as the style block for the same
256
264
  // reason: one injection, one cached prefix. It carries the two things the
@@ -312,6 +320,7 @@ export async function run({ args, hasFlag, numArg }) {
312
320
  if (seedBlock) console.log(seedBlock);
313
321
  if (doc2mdBlock) console.log(doc2mdBlock);
314
322
  if (koreanBlock) console.log(koreanBlock);
323
+ if (cohesionBlock) console.log(cohesionBlock);
315
324
  return; // nothing else to inject
316
325
  }
317
326
  if (updateBlock) console.log(updateBlock);
@@ -383,6 +392,7 @@ export async function run({ args, hasFlag, numArg }) {
383
392
  // Style first, briefing second: the briefing is Korean prose too, so the
384
393
  // guidance has to be in context before the model reads it.
385
394
  if (koreanBlock) console.log(koreanBlock + '\n');
395
+ if (cohesionBlock) console.log(cohesionBlock + '\n');
386
396
  console.log(lines.join('\n'));
387
397
  // Record what was actually briefed so the UserPromptSubmit brief hook
388
398
  // suppresses exactly these — a candidate landing after this read (e.g.
package/src/installer.js CHANGED
@@ -114,6 +114,12 @@ honor that for the rest of the turn without changing the saved setting.
114
114
  capturing git status + cap snapshot, so a fresh session can resume cleanly.
115
115
  - \`claude-token-saver mode\` — show statusline preferences.
116
116
  - \`claude-token-saver mode icon verbose 1d\` — change preferences.
117
+ - \`claude-token-saver feedback "<message>"\` — file a bug report or feature
118
+ request for this tool right from the session (tries the gh CLI, then an
119
+ anonymous no-login submission, then saves locally with a prefilled GitHub
120
+ issue URL; \`--anonymous\` skips the gh path).
121
+ Use it whenever the user says the tool itself misbehaves or wishes it did
122
+ something it does not — offer to submit the report for them.
117
123
 
118
124
  ## Storage layout (for reference)
119
125
 
@@ -197,7 +203,10 @@ export function installStatusline({ force = false } = {}) {
197
203
  }
198
204
 
199
205
  if (!force) {
200
- return { path: file, action: 'skipped', reason: `existing statusLine command (${cur.command}) re-run with --force to overwrite` };
206
+ // `conflict` lets the interactive install distinguish "someone else's
207
+ // statusline is here" (worth asking about) from other skip reasons
208
+ // (unreadable JSON), which a prompt cannot fix.
209
+ return { path: file, action: 'skipped', conflict: true, existingCommand: cur.command, reason: `existing statusLine command (${cur.command}) — re-run with --force to overwrite` };
201
210
  }
202
211
  settings.statusLine = {
203
212
  type: 'command',