claude-distill 0.3.0 → 0.4.1
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.md +88 -1
- package/bin/distill.js +6 -1
- package/lib/analyze.js +14 -4
- package/lib/locale.js +141 -0
- package/lib/store.js +18 -25
- package/lib/where.js +6 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# claude-distill
|
|
2
2
|
|
|
3
|
+
> **한 줄 요약** — Claude를 매일 새로 출근하는 알바생이라고 생각해보세요. 어제 가르친 요령을 오늘도 똑같이 다시 설명해야 합니다. `claude-distill`은 어제 배운 걸 **자동으로 인수인계 노트에 적어두는 도구**예요. 다음 날의 Claude가 출근하자마자 그 노트를 읽고 시작하니까, 같은 설명을 두 번 할 필요가 없습니다.
|
|
4
|
+
|
|
3
5
|
> Claude한테 같은 함정을 세 번째 설명하고 계신가요?
|
|
4
6
|
|
|
5
7
|
세션마다 트레이드오프 결정 (`A 대신 B 선택, 이유는…`), 환경 함정 (`Cursor webview는 confirm() 차단됨`), 같은 실수 (`Claude Code JSONL의 promptId는 항상 null`)가 쌓입니다. 그런데 세션이 끝나면 다 잊혀집니다. CLAUDE.md를 매번 갱신하면 좋겠지만 — 솔직히 안 하죠.
|
|
@@ -12,6 +14,47 @@
|
|
|
12
14
|
|
|
13
15
|
사용자가 하는 일: `claude-distill init` **한 번. 끝.**
|
|
14
16
|
|
|
17
|
+
### 한눈에 보기 — 설치 전 vs 설치 후
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
설치 전:
|
|
21
|
+
1번째 대화 → 주의할 점 발견 ❌ → "Claude야, 이거 주의해"
|
|
22
|
+
2번째 대화 → 같은 주의할 점 ❌ → "Claude야, 이거 주의해" (또?)
|
|
23
|
+
3번째 대화 → 같은 주의할 점 ❌ → "Claude야, 이거 주의해" (세 번째…)
|
|
24
|
+
└─ 대화마다 기억이 리셋, 영원히 반복
|
|
25
|
+
|
|
26
|
+
설치 후 (claude-distill init 한 번):
|
|
27
|
+
1번째 대화 → 주의할 점 발견 ❌ → 자동으로 인수인계 노트에 기록
|
|
28
|
+
2번째 대화 → Claude가 이미 알고 시작 ✓ (출근하자마자 노트 읽음)
|
|
29
|
+
3번째 대화 → Claude가 이미 알고 시작 ✓
|
|
30
|
+
└─ 한 번 배우면 모든 미래 대화가 공유
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```mermaid
|
|
34
|
+
flowchart LR
|
|
35
|
+
A[당신이 Claude와 대화] -->|대화 끝| B[자동으로 분석 시작]
|
|
36
|
+
B --> C{쓸 만한 배움이<br/>있었나?}
|
|
37
|
+
C -->|없음| X[조용히 종료<br/>비용 0원]
|
|
38
|
+
C -->|있음| D[AI가 핵심만 추출]
|
|
39
|
+
D --> E[인수인계 노트에 추가<br/>판례 / 사고 기록]
|
|
40
|
+
E -.다음 대화 시작 시<br/>Claude가 자동으로 읽음.-> F[다음 대화는<br/>'이미 아는 상태'로 시작]
|
|
41
|
+
F -.대화 끝나면 또.-> A
|
|
42
|
+
style F fill:#d4edda
|
|
43
|
+
style E fill:#fff3cd
|
|
44
|
+
style X fill:#f8f9fa
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> **용어**:
|
|
48
|
+
> - "인수인계 노트" = `~/.claude/knowledge.md` (판례) / `~/.claude/gotchas.md` (사고). 그냥 markdown 파일이라 직접 열어보고 편집 가능합니다.
|
|
49
|
+
> - "AI가 핵심만 추출" = Claude(Sonnet 또는 Haiku)가 대화 내용을 요약. 거의 모든 대화는 "쓸 만한 배움 없음"으로 분류돼 비용 0원으로 종료.
|
|
50
|
+
|
|
51
|
+
부담 없는 이유:
|
|
52
|
+
- **별도 서버 / 계정 없음** — 본인 머신 → Anthropic API 직통. distill 운영자한테도 transcript 안 감
|
|
53
|
+
- **의존성 0개, 50KB 미만** — [GitHub](https://github.com/parksubeom/claude-distill)에서 코드 그대로 검수
|
|
54
|
+
- **세션당 비용 ~$0** — 4단 게이트로 본 추출 호출 ~10× 컷 (휴리스틱 → Haiku → dedup hash → 재귀 가드)
|
|
55
|
+
- **Plain markdown 결과** — 마음에 안 드는 entry는 그 줄 삭제. UI / DB / 락인 없음
|
|
56
|
+
- **한국어 / 영어 자동 누적** — transcript 언어 자동 감지, 별도 설정 불필요
|
|
57
|
+
|
|
15
58
|
---
|
|
16
59
|
|
|
17
60
|
## 어떤 게 자동으로 누적되나
|
|
@@ -43,6 +86,27 @@
|
|
|
43
86
|
|
|
44
87
|
법률은 사람이 쓰지만, 판례와 사고 보고서는 매일 쌓이는 거니까 — 그건 자동화될 수 있습니다.
|
|
45
88
|
|
|
89
|
+
```
|
|
90
|
+
┌──────────────────────────────────────────┐
|
|
91
|
+
│ ~/.claude/CLAUDE.md (법률 — 사람이 작성)│
|
|
92
|
+
│ │
|
|
93
|
+
│ @~/.claude/knowledge.md ─────────────┼──┐
|
|
94
|
+
│ @~/.claude/gotchas.md ─────────────┼──┤
|
|
95
|
+
└──────────────────────────────────────────┘ │
|
|
96
|
+
▲ │
|
|
97
|
+
│ Claude가 매 대화 시작 시 │ 자동 로드
|
|
98
|
+
│ 자동으로 위 파일들을 읽음 │
|
|
99
|
+
│ │
|
|
100
|
+
│ ┌────────────────────────┘
|
|
101
|
+
│ ▼
|
|
102
|
+
┌─────┴───────────────────┐
|
|
103
|
+
│ knowledge.md (판례) │ ← claude-distill이
|
|
104
|
+
│ gotchas.md (사고) │ 대화 끝마다 자동 추가
|
|
105
|
+
└─────────────────────────┘
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`claude-distill init` 한 번이 위 그림의 **`@reference` 두 줄**을 `CLAUDE.md`에 자동 등록 → 그 후 매 대화 끝마다 distill이 노트에 자동 추가 → 다음 대화가 시작될 때 Claude가 자동으로 읽음.
|
|
109
|
+
|
|
46
110
|
---
|
|
47
111
|
|
|
48
112
|
## 설치
|
|
@@ -85,7 +149,9 @@ source ~/.zshrc
|
|
|
85
149
|
|
|
86
150
|
---
|
|
87
151
|
|
|
88
|
-
## 어떻게 동작
|
|
152
|
+
## 어떻게 동작 (내부 파이프라인 — 개발자용)
|
|
153
|
+
|
|
154
|
+
> 비개발자라면 위의 플로우차트만 보셔도 충분합니다. 아래는 내부를 검수하고 싶은 분을 위한 상세 단계.
|
|
89
155
|
|
|
90
156
|
```
|
|
91
157
|
세션 끝
|
|
@@ -152,10 +218,30 @@ source ~/.zshrc
|
|
|
152
218
|
- `ANTHROPIC_API_KEY` — 설정 시 Haiku 게이트 자동 활성 (~$0 가까이 게이트 비용)
|
|
153
219
|
- `CLAUDE_DISTILL_GATE_MODEL` — 게이트 모델 (기본 `claude-haiku-4-5-20251001`)
|
|
154
220
|
- `CLAUDE_DISTILL_MODEL` — 본 추출 모델 (기본 `claude-sonnet-4-6`)
|
|
221
|
+
- `CLAUDE_DISTILL_LANG` — 누적 언어 강제 (`ko` / `en`). 미설정 시 transcript 한글 비율로 자동 감지
|
|
155
222
|
- `CLAUDE_DISTILL_CHILD` — distill이 spawn한 자식 claude 표식. 수동 설정 불필요 (hook 무한 재귀 차단용 내부 플래그)
|
|
156
223
|
|
|
157
224
|
---
|
|
158
225
|
|
|
226
|
+
## 언어 (i18n) — 한국어 / 영어
|
|
227
|
+
|
|
228
|
+
`knowledge.md` / `gotchas.md`는 한국어 또는 영어로 누적할 수 있습니다. 헤더 / 필드 라벨 (`상황` / `함정` / `근거` … vs `Context` / `Trap` / `Basis` …) 과 entry 본문이 모두 해당 언어로 작성됩니다. 카테고리 키 (`api_quirk`, `trade_off_decision` 등) 는 머신 식별자라 영어 enum 그대로 유지.
|
|
229
|
+
|
|
230
|
+
**우선순위**:
|
|
231
|
+
1. `claude-distill analyze --lang=ko|en` (명시)
|
|
232
|
+
2. `CLAUDE_DISTILL_LANG=ko|en` 환경변수
|
|
233
|
+
3. **transcript 자동 감지** — 세션 turn에서 한글 음절 비율이 5% 넘으면 `ko`, 아니면 `en`
|
|
234
|
+
4. `process.env.LANG` (예: `ko_KR.UTF-8` → `ko`)
|
|
235
|
+
5. fallback: `en`
|
|
236
|
+
|
|
237
|
+
대부분의 사용자는 **3번 자동 감지로 충분** — 한국어로 코딩하는 세션은 한국어로, 영어 세션은 영어로 자연스럽게 누적됩니다. 별도 설정 없이.
|
|
238
|
+
|
|
239
|
+
영어 위주로 쓰다 한국어 entry가 섞이는 게 싫다면 `~/.zshrc`에 `export CLAUDE_DISTILL_LANG=en` 으로 고정.
|
|
240
|
+
|
|
241
|
+
> **혼재 주의**: 기존 markdown이 영어로 쌓여있을 때 locale을 `ko`로 바꾸면, 같은 파일에 영/한 entry가 섞입니다. plain markdown이라 사용자가 직접 정리 가능 (헤더 / 라벨 일괄 치환). v0.4 첫 cut은 단일 파일 정책 — 언어별 파일 분리 (`knowledge.ko.md`)는 추후 검토.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
159
245
|
## 프라이버시 / 보안
|
|
160
246
|
|
|
161
247
|
**별도 서버 없음.** distill은 본인 머신 → Anthropic API 직통입니다. 중간에 어떤 third-party 서버도 없음 — 코드도 50KB 미만, [GitHub](https://github.com/parksubeom/claude-distill)에서 그대로 검수 가능.
|
|
@@ -210,6 +296,7 @@ A. macOS / Linux / Windows (Node 18+ 설치돼있으면). 경로는 전부 `os.h
|
|
|
210
296
|
|
|
211
297
|
## 상태
|
|
212
298
|
|
|
299
|
+
v0.4 — i18n. `knowledge.md` / `gotchas.md`를 한국어 / 영어로 자동 누적 (transcript 언어 감지 또는 `CLAUDE_DISTILL_LANG`).
|
|
213
300
|
v0.3 — 게이트 도입 (휴리스틱 + Haiku) + Stop 훅 무한 재귀 가드. 본 추출 호출 ~10× 감소.
|
|
214
301
|
v0.2 — 자동 누적 모델로 재정비.
|
|
215
302
|
|
package/bin/distill.js
CHANGED
|
@@ -21,7 +21,12 @@ const help = `claude-distill — Knowledge + Gotchas auto-accumulator for Claude
|
|
|
21
21
|
claude-distill where 모든 파일 경로 / 존재 여부
|
|
22
22
|
|
|
23
23
|
수동 분석 (보통 Hook이 자동으로 함):
|
|
24
|
-
claude-distill analyze [--no-auto] [--mock] [--quiet]
|
|
24
|
+
claude-distill analyze [--no-auto] [--mock] [--quiet] [--lang=ko|en]
|
|
25
|
+
|
|
26
|
+
언어 (knowledge.md / gotchas.md 누적 언어):
|
|
27
|
+
--lang=ko|en 명시 (가장 우선)
|
|
28
|
+
CLAUDE_DISTILL_LANG=... 환경변수
|
|
29
|
+
(없으면) transcript 한글 비율로 자동 감지 → process.env.LANG → 'en'
|
|
25
30
|
|
|
26
31
|
결과는 plain markdown:
|
|
27
32
|
~/.claude/knowledge.md 판례 (전역)
|
package/lib/analyze.js
CHANGED
|
@@ -22,6 +22,7 @@ const cfg = require('./config');
|
|
|
22
22
|
const transcript = require('./transcript');
|
|
23
23
|
const store = require('./store');
|
|
24
24
|
const gate = require('./gate');
|
|
25
|
+
const localeMod = require('./locale');
|
|
25
26
|
|
|
26
27
|
// 자식 claude 프로세스 표식. distill→claude→Stop 훅→distill 무한 재귀 차단용.
|
|
27
28
|
const CHILD_ENV_KEY = 'CLAUDE_DISTILL_CHILD';
|
|
@@ -35,6 +36,7 @@ function parseFlags(args) {
|
|
|
35
36
|
auto: true, // 기본값 — 자동 누적 (CLI 핵심)
|
|
36
37
|
backend: 'auto', // 'auto' | 'cli' | 'api' — auto는 ANTHROPIC_API_KEY 있으면 api, 없으면 cli
|
|
37
38
|
noGate: false, // 게이트 우회 (디버깅 / 강제 분석)
|
|
39
|
+
lang: null, // 'ko' | 'en' | null — null이면 env / transcript 자동 감지
|
|
38
40
|
};
|
|
39
41
|
for (let i = 0; i < args.length; i++) {
|
|
40
42
|
const a = args[i];
|
|
@@ -47,6 +49,8 @@ function parseFlags(args) {
|
|
|
47
49
|
else if (a === '--backend') flags.backend = args[++i];
|
|
48
50
|
else if (a.startsWith('--session=')) flags.session = a.slice('--session='.length);
|
|
49
51
|
else if (a === '--session') flags.session = args[++i];
|
|
52
|
+
else if (a.startsWith('--lang=')) flags.lang = a.slice('--lang='.length);
|
|
53
|
+
else if (a === '--lang') flags.lang = args[++i];
|
|
50
54
|
}
|
|
51
55
|
return flags;
|
|
52
56
|
}
|
|
@@ -62,10 +66,11 @@ function existingForDedup() {
|
|
|
62
66
|
return out;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
function buildAnalyzerPrompt(promptText, existing, t) {
|
|
69
|
+
function buildAnalyzerPrompt(promptText, existing, t, locale) {
|
|
66
70
|
const exBlock = `<existing>\n# knowledge.md\n${existing.knowledge}\n\n# gotchas.md\n${existing.gotchas}\n</existing>`;
|
|
67
71
|
const txBlock = `<transcript file="${t.source}" total_lines="${t.totalLines}" slice_from="${t.sliceFrom}">\n${JSON.stringify(t.turns, null, 2)}\n</transcript>`;
|
|
68
|
-
|
|
72
|
+
const langDirective = localeMod.promptDirective(locale);
|
|
73
|
+
return `${promptText}\n\n---\n\n${exBlock}\n\n${txBlock}${langDirective}\n\nReturn the JSON array now. No prose, no markdown fences.`;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
function mockExtract() {
|
|
@@ -227,6 +232,11 @@ async function run(args) {
|
|
|
227
232
|
return;
|
|
228
233
|
}
|
|
229
234
|
|
|
235
|
+
// Locale 결정 — transcript가 로드된 뒤(자동 감지에 필요). flag → env →
|
|
236
|
+
// transcript Hangul 비율 → process.env.LANG → 'en'.
|
|
237
|
+
const locale = localeMod.resolveLocale({ flag: flags.lang, transcript: t });
|
|
238
|
+
log(flags, 'locale: ' + locale + (flags.lang ? ' (--lang)' : process.env.CLAUDE_DISTILL_LANG ? ' (env)' : ' (auto)'));
|
|
239
|
+
|
|
230
240
|
// ── 게이트 1: 휴리스틱 (로컬, 무료) ───────────────────────
|
|
231
241
|
// 짧은 세션 / 도구 사용 0 / 에러 키워드 0 → 인사이트 후보 거의 없음.
|
|
232
242
|
// mock 모드와 --no-gate 는 우회.
|
|
@@ -261,7 +271,7 @@ async function run(args) {
|
|
|
261
271
|
} else {
|
|
262
272
|
const prompt = fs.readFileSync(cfg.PROMPT_FILE, 'utf8');
|
|
263
273
|
const existing = existingForDedup();
|
|
264
|
-
const fullPrompt = buildAnalyzerPrompt(prompt, existing, t);
|
|
274
|
+
const fullPrompt = buildAnalyzerPrompt(prompt, existing, t, locale);
|
|
265
275
|
if (flags.dryRun) {
|
|
266
276
|
log(flags, '[--dry-run] prompt 준비됨 (' + fullPrompt.length + ' chars). LLM 호출 생략.');
|
|
267
277
|
return;
|
|
@@ -299,7 +309,7 @@ async function run(args) {
|
|
|
299
309
|
},
|
|
300
310
|
};
|
|
301
311
|
try {
|
|
302
|
-
const dest = store.appendEntry(annotated);
|
|
312
|
+
const dest = store.appendEntry(annotated, locale);
|
|
303
313
|
appended++;
|
|
304
314
|
log(flags, ' ✓ ' + (c.title || c.id) + ' → ' + dest);
|
|
305
315
|
} catch (err) {
|
package/lib/locale.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Locale resolution + label tables for ko / en.
|
|
2
|
+
//
|
|
3
|
+
// 우선순위:
|
|
4
|
+
// 1. --lang=ko|en 플래그
|
|
5
|
+
// 2. CLAUDE_DISTILL_LANG 환경변수
|
|
6
|
+
// 3. transcript 자동 감지 (Hangul 비율)
|
|
7
|
+
// 4. process.env.LANG
|
|
8
|
+
// 5. fallback 'en'
|
|
9
|
+
//
|
|
10
|
+
// 지원 범위는 의도적으로 ko / en 둘로만 제한. 추가 언어는 LABELS 테이블 + detector
|
|
11
|
+
// 분기만 늘리면 되지만 v0.4 첫 cut에선 두 언어로 시작.
|
|
12
|
+
|
|
13
|
+
const SUPPORTED = ['ko', 'en'];
|
|
14
|
+
const DEFAULT = 'en';
|
|
15
|
+
|
|
16
|
+
const LABELS = {
|
|
17
|
+
en: {
|
|
18
|
+
knowledgeHeader:
|
|
19
|
+
'# Knowledge — case law\n\n' +
|
|
20
|
+
'> Judgment calls worth remembering. Maintained by [claude-distill](https://github.com/parksubeom/claude-distill).\n\n',
|
|
21
|
+
gotchasHeader:
|
|
22
|
+
'# Gotchas — incident reports\n\n' +
|
|
23
|
+
'> Mistakes worth not repeating. Maintained by [claude-distill](https://github.com/parksubeom/claude-distill).\n\n',
|
|
24
|
+
category: 'Category',
|
|
25
|
+
confidence: 'Confidence',
|
|
26
|
+
date: 'Date',
|
|
27
|
+
source: 'Source',
|
|
28
|
+
sessionWord: 'session',
|
|
29
|
+
projectWord: 'project',
|
|
30
|
+
cmdsWord: 'cmds',
|
|
31
|
+
contextLabel: 'Context',
|
|
32
|
+
symptomLabel: 'Symptom',
|
|
33
|
+
insightLabel: 'Insight',
|
|
34
|
+
trapLabel: 'Trap',
|
|
35
|
+
basisLabel: 'Basis',
|
|
36
|
+
applicationLabel: 'Application',
|
|
37
|
+
tagsLabel: 'Tags',
|
|
38
|
+
untitled: '(untitled)',
|
|
39
|
+
},
|
|
40
|
+
ko: {
|
|
41
|
+
knowledgeHeader:
|
|
42
|
+
'# 판례 — Knowledge\n\n' +
|
|
43
|
+
'> 다음에도 기억할 만한 결정. [claude-distill](https://github.com/parksubeom/claude-distill)이 자동 누적합니다.\n\n',
|
|
44
|
+
gotchasHeader:
|
|
45
|
+
'# 사고 보고서 — Gotchas\n\n' +
|
|
46
|
+
'> 다시 빠지지 말아야 할 함정. [claude-distill](https://github.com/parksubeom/claude-distill)이 자동 누적합니다.\n\n',
|
|
47
|
+
category: '카테고리',
|
|
48
|
+
confidence: '신뢰도',
|
|
49
|
+
date: '날짜',
|
|
50
|
+
source: '출처',
|
|
51
|
+
sessionWord: '세션',
|
|
52
|
+
projectWord: '프로젝트',
|
|
53
|
+
cmdsWord: '명령',
|
|
54
|
+
contextLabel: '상황',
|
|
55
|
+
symptomLabel: '증상',
|
|
56
|
+
insightLabel: '인사이트',
|
|
57
|
+
trapLabel: '함정',
|
|
58
|
+
basisLabel: '근거',
|
|
59
|
+
applicationLabel: '적용',
|
|
60
|
+
tagsLabel: '태그',
|
|
61
|
+
untitled: '(제목 없음)',
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function normalize(v) {
|
|
66
|
+
if (!v) return null;
|
|
67
|
+
const s = String(v).toLowerCase().trim();
|
|
68
|
+
if (s.startsWith('ko')) return 'ko';
|
|
69
|
+
if (s.startsWith('en')) return 'en';
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// transcript의 turns를 stringify해서 한글 음절 (U+AC00–U+D7A3) 비율을 계산.
|
|
74
|
+
// 시그널이 약하면 (글자 자체가 적으면) null 반환 — 호출자가 다음 우선순위로.
|
|
75
|
+
function detectFromTranscript(t) {
|
|
76
|
+
if (!t || !Array.isArray(t.turns) || t.turns.length === 0) return null;
|
|
77
|
+
let kor = 0;
|
|
78
|
+
let letters = 0;
|
|
79
|
+
for (const turn of t.turns) {
|
|
80
|
+
const s = JSON.stringify(turn);
|
|
81
|
+
for (let i = 0; i < s.length; i++) {
|
|
82
|
+
const code = s.charCodeAt(i);
|
|
83
|
+
const isHangul = code >= 0xAC00 && code <= 0xD7A3;
|
|
84
|
+
const isLatin = (code >= 0x41 && code <= 0x5A) || (code >= 0x61 && code <= 0x7A);
|
|
85
|
+
if (isHangul) kor++;
|
|
86
|
+
if (isHangul || isLatin) letters++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (letters < 200) return null;
|
|
90
|
+
// 한글 음절은 글자 하나당 정보량이 라틴 알파벳보다 크므로 5%만 넘어도
|
|
91
|
+
// 실제로는 한국어 위주의 세션. 임계값 보수적으로 0.05.
|
|
92
|
+
return kor / letters > 0.05 ? 'ko' : 'en';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveLocale({ flag, transcript } = {}) {
|
|
96
|
+
const fromFlag = normalize(flag);
|
|
97
|
+
if (fromFlag) return fromFlag;
|
|
98
|
+
const fromEnv = normalize(process.env.CLAUDE_DISTILL_LANG);
|
|
99
|
+
if (fromEnv) return fromEnv;
|
|
100
|
+
const fromTranscript = detectFromTranscript(transcript);
|
|
101
|
+
if (fromTranscript) return fromTranscript;
|
|
102
|
+
const fromSysLang = normalize(process.env.LANG);
|
|
103
|
+
if (fromSysLang) return fromSysLang;
|
|
104
|
+
return DEFAULT;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function labels(locale) {
|
|
108
|
+
return LABELS[SUPPORTED.includes(locale) ? locale : DEFAULT];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// LLM에 출력 언어를 강제하는 directive. JSON 식별자 필드는 enum 그대로
|
|
112
|
+
// 유지하라고 명시 — 카테고리 키가 번역되면 store.js의 isGotcha 분기 등이 깨짐.
|
|
113
|
+
function promptDirective(locale) {
|
|
114
|
+
if (locale === 'ko') {
|
|
115
|
+
return [
|
|
116
|
+
'',
|
|
117
|
+
'---',
|
|
118
|
+
'',
|
|
119
|
+
'OUTPUT LANGUAGE: 모든 entry의 자연어 필드(title, context, insight, basis, application)와 tags 배열은 **한국어**로 작성하세요.',
|
|
120
|
+
'단, 식별자 필드(type, category, confidence)는 명세된 영어 enum 값 그대로 유지 — 번역 금지.',
|
|
121
|
+
'JSON 키 이름과 구조도 그대로. 한국어로 표현하기 어려운 기술 용어는 영어 그대로 두어도 됩니다.',
|
|
122
|
+
].join('\n');
|
|
123
|
+
}
|
|
124
|
+
return [
|
|
125
|
+
'',
|
|
126
|
+
'---',
|
|
127
|
+
'',
|
|
128
|
+
'OUTPUT LANGUAGE: Write the natural-language fields (title, context, insight, basis, application) and tags array in **English**.',
|
|
129
|
+
'Keep identifier fields (type, category, confidence) as their specified English enum values — do not translate.',
|
|
130
|
+
'Do not change JSON key names or structure.',
|
|
131
|
+
].join('\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
SUPPORTED,
|
|
136
|
+
DEFAULT,
|
|
137
|
+
resolveLocale,
|
|
138
|
+
detectFromTranscript,
|
|
139
|
+
labels,
|
|
140
|
+
promptDirective,
|
|
141
|
+
};
|
package/lib/store.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const cfg = require('./config');
|
|
8
|
+
const locale = require('./locale');
|
|
8
9
|
|
|
9
10
|
function resolveTarget(entry) {
|
|
10
11
|
const isGotcha = entry.type === 'gotcha';
|
|
@@ -16,61 +17,53 @@ function resolveTarget(entry) {
|
|
|
16
17
|
return isGotcha ? cfg.GLOBAL_GOTCHAS : cfg.GLOBAL_KNOWLEDGE;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function ensureHeader(file, isGotcha) {
|
|
20
|
+
function ensureHeader(file, isGotcha, lang) {
|
|
20
21
|
if (fs.existsSync(file)) return;
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
> Mistakes worth not repeating. Maintained by [claude-distill](https://github.com/parksubeom/claude-distill).
|
|
25
|
-
|
|
26
|
-
`
|
|
27
|
-
: `# Knowledge — case law
|
|
28
|
-
|
|
29
|
-
> Judgment calls worth remembering. Maintained by [claude-distill](https://github.com/parksubeom/claude-distill).
|
|
30
|
-
|
|
31
|
-
`;
|
|
22
|
+
const L = locale.labels(lang);
|
|
23
|
+
const header = isGotcha ? L.gotchasHeader : L.knowledgeHeader;
|
|
32
24
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
33
25
|
fs.writeFileSync(file, header);
|
|
34
26
|
}
|
|
35
27
|
|
|
36
|
-
function formatEntry(e) {
|
|
37
|
-
const
|
|
28
|
+
function formatEntry(e, lang) {
|
|
29
|
+
const L = locale.labels(lang);
|
|
30
|
+
const title = e.title || L.untitled;
|
|
38
31
|
const date = (e.source && e.source.timestamp) ? e.source.timestamp.slice(0, 10) : new Date().toISOString().slice(0, 10);
|
|
39
32
|
const sessionId = (e.source && e.source.sessionId) || 'unknown';
|
|
40
33
|
const project = (e.source && e.source.project) || 'unknown';
|
|
41
34
|
const cmds = (e.source && e.source.relatedCommands && e.source.relatedCommands.length)
|
|
42
|
-
? ' ·
|
|
35
|
+
? ' · ' + L.cmdsWord + ': ' + e.source.relatedCommands.join(', ')
|
|
43
36
|
: '';
|
|
44
37
|
const tags = (e.tags || []).map((t) => '`' + t + '`').join(' · ');
|
|
45
38
|
const conf = e.confidence || 'medium';
|
|
46
|
-
const symptomLabel = e.type === 'gotcha' ?
|
|
47
|
-
const insightLabel = e.type === 'gotcha' ?
|
|
39
|
+
const symptomLabel = e.type === 'gotcha' ? L.symptomLabel : L.contextLabel;
|
|
40
|
+
const insightLabel = e.type === 'gotcha' ? L.trapLabel : L.insightLabel;
|
|
48
41
|
|
|
49
42
|
return [
|
|
50
43
|
`## ${title}`,
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
`**${L.category}**: \`${e.category}\` · **${L.confidence}**: ${conf} · **${L.date}**: ${date}`,
|
|
45
|
+
`**${L.source}**: ${L.sessionWord} \`${sessionId.slice(0, 8)}\` · ${L.projectWord} \`${project}\`${cmds}`,
|
|
53
46
|
'',
|
|
54
47
|
`**${symptomLabel}**: ${e.context || ''}`,
|
|
55
48
|
'',
|
|
56
49
|
`**${insightLabel}**: ${e.insight || ''}`,
|
|
57
50
|
'',
|
|
58
|
-
|
|
51
|
+
`**${L.basisLabel}**: ${e.basis || ''}`,
|
|
59
52
|
'',
|
|
60
|
-
|
|
53
|
+
`**${L.applicationLabel}**: ${e.application || ''}`,
|
|
61
54
|
'',
|
|
62
|
-
|
|
55
|
+
`**${L.tagsLabel}**: ${tags}`,
|
|
63
56
|
'',
|
|
64
57
|
'---',
|
|
65
58
|
'',
|
|
66
59
|
].join('\n');
|
|
67
60
|
}
|
|
68
61
|
|
|
69
|
-
function appendEntry(entry) {
|
|
62
|
+
function appendEntry(entry, lang) {
|
|
70
63
|
const file = resolveTarget(entry);
|
|
71
64
|
const isGotcha = entry.type === 'gotcha';
|
|
72
|
-
ensureHeader(file, isGotcha);
|
|
73
|
-
fs.appendFileSync(file, formatEntry(entry));
|
|
65
|
+
ensureHeader(file, isGotcha, lang);
|
|
66
|
+
fs.appendFileSync(file, formatEntry(entry, lang));
|
|
74
67
|
return file;
|
|
75
68
|
}
|
|
76
69
|
|
package/lib/where.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const cfg = require('./config');
|
|
6
|
+
const localeMod = require('./locale');
|
|
6
7
|
|
|
7
8
|
const CLAUDE_MD = path.join(cfg.CLAUDE_DIR, 'CLAUDE.md');
|
|
8
9
|
|
|
@@ -21,6 +22,11 @@ function run() {
|
|
|
21
22
|
console.log(' analyzed.json ' + exists(path.join(cfg.STATE_DIR, 'analyzed.json')) + ' 중복 분석 방지 로그');
|
|
22
23
|
console.log('');
|
|
23
24
|
console.log('extract prompt ' + exists(cfg.PROMPT_FILE) + ' ' + cfg.PROMPT_FILE);
|
|
25
|
+
console.log('');
|
|
26
|
+
const envLang = process.env.CLAUDE_DISTILL_LANG || '(unset)';
|
|
27
|
+
const resolved = localeMod.resolveLocale();
|
|
28
|
+
console.log('locale (env) · CLAUDE_DISTILL_LANG=' + envLang);
|
|
29
|
+
console.log('locale (resolved) · ' + resolved + ' (transcript 자동 감지가 우선 — 세션 분석 시점에 결정)');
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
module.exports = { run };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-distill",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Distill knowledge and gotchas from Claude Code session transcripts. Hook-based feedback loop: session → extract → review → accumulate.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claude-distill": "bin/distill.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
|
42
|
-
"url": "https://github.com/parksubeom/claude-distill.git"
|
|
42
|
+
"url": "git+https://github.com/parksubeom/claude-distill.git"
|
|
43
43
|
},
|
|
44
44
|
"files": [
|
|
45
45
|
"bin",
|