sincenety 0.1.1 → 0.2.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
@@ -2,20 +2,27 @@
2
2
 
3
3
  > **[English Documentation (README.md)](./README.md)**
4
4
 
5
- **Claude Code 작업 갈무리 도구** — `sincenety` 한 번 실행으로 마지막 갈무리 이후의 모든 Claude Code 작업을 자동 분석하여 구조화된 기록을 생성합니다.
5
+ **Claude Code 작업 갈무리 도구** — `sincenety` 한 번 실행으로 오늘 하루의 모든 Claude Code 작업을 자동 분석하여 구조화된 기록을 생성합니다.
6
6
 
7
- start/stop 없이, 실행 시점 기준으로 소급하여 모든 작업을 정리합니다.
7
+ start/stop 없이, 오늘 00:00부터 현재까지의 모든 작업을 소급 정리합니다 (upsert로 중복 방지).
8
8
 
9
9
  ```
10
10
  $ sincenety
11
11
 
12
- 📋 2026년 4월 7일 화요일 작업 갈무리 (12:00 ~ 18:05)
13
- 총 4개 세션, 1605개 메시지 | 토큰: 9.0Kin / 212.7Kout
14
- ────────────────────────────────────────────────────────
15
- [claudflare_web] 11:22 ~ 14:49 (3시간 28분, 445msg, 66.4Ktok)
16
- pathcosmos.com 보안 강화 + 웹 분석 구축
17
- 모델: claude-opus-4-6
18
- ...
12
+ 📋 2026년 4월 7일 화요일 작업 갈무리 (00:00 ~ 18:05)
13
+
14
+ ┌─────────────────────────────────────────────────┐
15
+ 세션: 4개 메시지: 1,605개 │ 토큰: 221.7K │
16
+ └─────────────────────────────────────────────────┘
17
+
18
+ ┌──────────────┬───────────────┬────────┬─────────┬───────────────────────────────┐
19
+ │ 프로젝트 │ 시간 │ 메시지 │ 토큰 │ 작업 내용 │
20
+ ├──────────────┼───────────────┼────────┼─────────┼───────────────────────────────┤
21
+ │ claudflare │ 11:22 ~ 14:49│ 445 │ 66.4K │ 보안 강화 + 웹 분석 구축 │
22
+ │ sincenety │ 15:01 ~ 18:05│ 312 │ 48.2K │ AI 요약 일일보고 시스템 │
23
+ │ ... │ │ │ │ │
24
+ └──────────────┴───────────────┴────────┴─────────┴───────────────────────────────┘
25
+
19
26
  ✅ 갈무리 완료. 기록이 저장되었습니다.
20
27
  ```
21
28
 
@@ -25,9 +32,10 @@ $ sincenety
25
32
 
26
33
  ### 소급 갈무리
27
34
 
28
- 별도 기록 행위 없이, `sincenety` 실행 시 마지막 갈무리 시점 이후의 `~/.claude/` 데이터를 분석하여 프로젝트별/세션별 작업 내용을 자동 재구성합니다.
35
+ 별도 기록 행위 없이, `sincenety` 실행 시 오늘 00:00부터 현재까지의 `~/.claude/` 데이터를 분석하여 프로젝트별/세션별 작업 내용을 자동 재구성합니다. 하루 전체를 수집하되 upsert로 중복을 방지합니다.
29
36
 
30
- - **세션 JSONL 파싱**`~/.claude/projects/[project]/[sessionId].jsonl`에서 토큰 사용량, 모델명, 정밀 타임스탬프 추출
37
+ - **대화 분석**사용자 입력 + 어시스턴트 응답 쌍(`conversationTurns`)을 함께 수집하여 정밀한 작업 내용 파악
38
+ - **세션 JSONL 파싱** — `~/.claude/projects/[project]/[sessionId].jsonl`에서 토큰 사용량, 모델명, 정밀 타임스탬프, 대화 턴 추출
31
39
  - **history.jsonl 보조 인덱스** — 빠른 세션 목록 조회용
32
40
  - **갈무리 포인트** — 매 실행 시 "여기까지 정리했음" 마커를 저장하여 중복 없이 이어서 정리
33
41
 
@@ -42,9 +50,16 @@ $ sincenety
42
50
  | 사용 모델 | assistant 응답에서 모델명 추출 |
43
51
  | 카테고리 | 프로젝트 경로 기반 자동 분류 |
44
52
 
53
+ ### AI 요약 일일보고
54
+
55
+ Claude Code 세션 자체를 요약 엔진으로 활용합니다. `--json` 플래그로 대화 턴 포함 구조화 JSON을 출력하면, SKILL.md가 Claude Code에게 직접 요약 생성을 지시합니다. 외부 API 키 없이도 동작하며, `ANTHROPIC_API_KEY`가 있으면 Claude API 요약도 가능합니다.
56
+
57
+ - **`save-daily`** — AI가 생성한 일일보고를 DB에 저장
58
+ - **`report`** — 일일/주간/월간 보고 조회 (주간/월간은 일일보고를 종합)
59
+
45
60
  ### 이메일 리포트
46
61
 
47
- Gmail SMTP를 통해 갈무리 리포트를 이메일로 발송합니다. 세션별 컬러 코딩, 토큰 대시보드, 갈무리 요약이 포함된 HTML 이메일입니다.
62
+ Gmail SMTP를 통해 갈무리 리포트를 이메일로 발송합니다. 세션별 컬러 코딩, 토큰 대시보드, 갈무리 요약이 포함된 HTML 이메일입니다. wrapUp 데이터(outcome, flow, significance)를 반영하며, XML/시스템 태그가 정리된 깔끔한 이메일을 생성합니다.
48
63
 
49
64
  ### 자동 스케줄링
50
65
 
@@ -75,13 +90,16 @@ npm link # 글로벌 등록
75
90
  ### 기본 사용
76
91
 
77
92
  ```bash
78
- # 갈무리 (마지막 포인트 이후)
93
+ # 갈무리 (오늘 00:00부터 현재까지)
79
94
  sincenety
80
95
 
81
96
  # 특정 시점부터 갈무리
82
97
  sincenety --since "09:00"
83
98
  sincenety --since "2026-04-07 09:00"
84
99
 
100
+ # 구조화 JSON 출력 (대화 턴 포함, AI 요약 파이프라인용)
101
+ sincenety --json
102
+
85
103
  # 빠른 모드 (토큰 추출 없이 history.jsonl만 사용)
86
104
  sincenety --no-detail
87
105
 
@@ -89,6 +107,15 @@ sincenety --no-detail
89
107
  sincenety log
90
108
  sincenety log --date 2026-04-06
91
109
  sincenety log --week
110
+
111
+ # AI 요약 일일보고 저장 (stdin으로 JSON 입력)
112
+ sincenety save-daily < daily_report.json
113
+
114
+ # 보고 조회
115
+ sincenety report # 오늘 일일보고
116
+ sincenety report --week # 주간 보고
117
+ sincenety report --month # 월간 보고
118
+ sincenety report --date 2026-04-06 # 특정일 보고
92
119
  ```
93
120
 
94
121
  ### 이메일 설정
@@ -133,12 +160,13 @@ Claude Code 안에서 `/sincenety`로 직접 호출 가능합니다. `~/.claude/
133
160
  ```
134
161
  sincenety/
135
162
  ├── src/
136
- │ ├── cli.ts # CLI 진입점 (commander, 5개 서브커맨드)
163
+ │ ├── cli.ts # CLI 진입점 (commander, 7개 서브커맨드)
137
164
  │ ├── core/
138
- │ │ └── gatherer.ts # 갈무리 핵심 로직 (파싱→그룹핑→저장→리포트)
165
+ │ │ ├── gatherer.ts # 갈무리 핵심 로직 (파싱→그룹핑→저장→리포트)
166
+ │ │ └── summarizer.ts # AI 요약 (Claude API + 휴리스틱 fallback)
139
167
  │ ├── parser/
140
168
  │ │ ├── history.ts # ~/.claude/history.jsonl 스트리밍 파서
141
- │ │ └── session-jsonl.ts # 세션 JSONL 파서 (토큰/모델/타이밍 추출)
169
+ │ │ └── session-jsonl.ts # 세션 JSONL 파서 (토큰/모델/타이밍/대화턴 추출)
142
170
  │ ├── grouper/
143
171
  │ │ └── session.ts # sessionId+project 기준 그룹핑
144
172
  │ ├── storage/
@@ -148,7 +176,7 @@ sincenety/
148
176
  │ │ ├── key.ts # PBKDF2 키 파생 (머신 바운드 + passphrase)
149
177
  │ │ └── crypto.ts # AES-256-GCM encrypt/decrypt
150
178
  │ ├── report/
151
- │ │ ├── terminal.ts # 터미널 출력 포매터
179
+ │ │ ├── terminal.ts # 터미널 테이블 출력 (유니코드 박스 드로잉, 한글 폭 계산)
152
180
  │ │ └── markdown.ts # 마크다운 리포트 생성
153
181
  │ ├── email/
154
182
  │ │ ├── sender.ts # nodemailer 이메일 발송
@@ -171,24 +199,33 @@ sincenety/
171
199
  ~/.claude/history.jsonl ──→ 세션 목록 추출 (sessionId + project)
172
200
 
173
201
 
174
- ~/.claude/projects/[project]/[sessionId].jsonl ──→ 토큰/모델/타이밍 추출
202
+ ~/.claude/projects/[project]/[sessionId].jsonl ──→ 토큰/모델/타이밍/대화턴 추출
175
203
 
176
204
 
177
205
  그룹핑 + 요약 생성
178
206
 
179
- ┌──────────────┼──────────────┐
180
- ▼ ▼
181
- 터미널 출력 DB 저장 (암호화) 이메일 발송
207
+ ┌──────────┬───┼───────┬──────────────┐
208
+ ▼ ▼
209
+ 터미널 출력 --json DB 저장 이메일 발송 Claude Code
210
+ (테이블) (구조화) (암호화) AI 요약
211
+
212
+
213
+ save-daily
214
+ (일일보고 DB 저장)
215
+
216
+
217
+ report (일일/주간/월간)
182
218
  ```
183
219
 
184
220
  ### DB 스키마
185
221
 
186
- **4개 테이블:**
222
+ **5개 테이블:**
187
223
 
188
224
  | 테이블 | 설명 |
189
225
  |--------|------|
190
226
  | `sessions` | 세션별 작업 기록 (22개 컬럼 — 토큰, 시간, 타이틀, 설명, 모델 등) |
191
227
  | `gather_reports` | 갈무리 실행마다 리포트 저장 (마크다운 + JSON) |
228
+ | `daily_reports` | AI 요약 일일/주간/월간 보고 (UNIQUE(report_date, report_type)) |
192
229
  | `checkpoints` | 갈무리 포인트 (마지막 처리 timestamp) |
193
230
  | `config` | 설정 (이메일, SMTP 등) |
194
231
 
@@ -303,13 +340,24 @@ npx . # 현재 디렉토리를 npx로 실행
303
340
  - **자동 스케줄링**: launchd (macOS) / crontab (Linux) 자동 설치
304
341
  - **`--auto` 플래그**: 갈무리 + 이메일 자동 발송 (스케줄러용)
305
342
 
343
+ ### v0.3 (2026-04-07) — AI 요약 일일보고 + 주간/월간 보고
344
+
345
+ - **기본 갈무리 범위 변경**: 항상 오늘 00:00부터 (upsert로 중복 방지)
346
+ - **대화 턴 수집**: `session-jsonl.ts`에서 사용자 입력 + 어시스턴트 응답 쌍(`conversationTurns`) 추출
347
+ - **터미널 테이블 출력**: 요약 테이블 + 세션 상세 테이블 (유니코드 박스 드로잉, 한글 fullwidth 폭 계산)
348
+ - **AI 요약 아키텍처**: `--json` 플래그로 대화 턴 포함 구조화 JSON 출력, SKILL.md가 Claude Code에게 직접 요약 지시
349
+ - **summarizer.ts**: Claude API 요약 (`ANTHROPIC_API_KEY` 있을 때) + 휴리스틱 fallback
350
+ - **일일보고 시스템**: `save-daily` (stdin JSON → DB 저장), `report` (일일/주간/월간 조회)
351
+ - **DB 스키마 v3**: `daily_reports` 테이블 추가 (UNIQUE(report_date, report_type))
352
+ - **이메일 개선**: XML/시스템 태그 정리 (cleanText, esc 함수 강화), wrapUp 데이터 반영, Section 04 작업 흐름 표시
353
+
306
354
  ### 향후 계획
307
355
 
308
356
  - [ ] npm publish → `npx sincenety@latest` 배포
309
357
  - [ ] passphrase 설정 기능 완성
310
358
  - [ ] 유사 작업 매칭 (TF-IDF 기반)
311
359
  - [ ] MariaDB/PostgreSQL 외부 DB 연결 (현재 비활성화)
312
- - [ ] 주간/월간 요약 리포트
360
+ - [x] 주간/월간 요약 리포트
313
361
  - [ ] ccusage 연동 (토큰 비용 자동 계산)
314
362
 
315
363
  ---
@@ -321,8 +369,8 @@ npx . # 현재 디렉토리를 npx로 실행
321
369
  | TypeScript 소스 파일 | 15개 |
322
370
  | 총 코드 라인 | 3,013줄 |
323
371
  | 암호화 테스트 | 26/26 통과 |
324
- | CLI 명령어 | 5개 (갈무리, log, config, email, schedule) |
325
- | DB 테이블 | 4개 |
372
+ | CLI 명령어 | 7개 (갈무리, log, config, email, schedule, save-daily, report) |
373
+ | DB 테이블 | 5개 |
326
374
  | 의존성 (production) | 3개 (commander, nodemailer, sql.js) |
327
375
  | 보안 이슈 발견/수정 | 8/8 |
328
376
 
package/README.md CHANGED
@@ -9,13 +9,17 @@ No start/stop needed. Just run it when you're done.
9
9
  ```
10
10
  $ sincenety
11
11
 
12
- 📋 2026-04-07 (Tue) Work Gather (12:00 ~ 18:05)
13
- 4 sessions, 1605 messages | Tokens: 9.0Kin / 212.7Kout
14
- ────────────────────────────────────────────────────────
15
- [claudflare_web] 11:22 ~ 14:49 (3h 28m, 445msg, 66.4Ktok)
16
- pathcosmos.com security + web analytics setup
17
- Model: claude-opus-4-6
18
- ...
12
+ 📋 2026-04-07 (Mon) Work Gather (00:00 ~ 18:05)
13
+ ┌─────────────────────────────────────────────────────────┐
14
+ │ Sessions: 4 │ Messages: 1605 │ Tokens: 9.0Ki / 212.7Ko │
15
+ └─────────────────────────────────────────────────────────┘
16
+ ┌──────────────┬───────────────┬────────┬────────┬────────┐
17
+ │ Project │ Time │ Msgs │ Tokens │ Model
18
+ ├──────────────┼───────────────┼────────┼────────┼────────┤
19
+ │ claudflare │ 11:22 ~ 14:49 │ 445 │ 66.4K │ opus │
20
+ │ sincenety │ 14:55 ~ 18:05 │ 860 │ 98.2K │ opus │
21
+ │ ... │ │ │ │ │
22
+ └──────────────┴───────────────┴────────┴────────┴────────┘
19
23
  ✅ Gather complete. Records saved.
20
24
  ```
21
25
 
@@ -27,8 +31,8 @@ $ sincenety
27
31
 
28
32
  No need to remember to start/stop tracking. `sincenety` parses `~/.claude/` data at runtime and reconstructs everything:
29
33
 
30
- - **Session JSONL parsing** — Extracts token usage, model names, and millisecond-precision timestamps from `~/.claude/projects/[project]/[sessionId].jsonl`
31
- - **Checkpoint system** — Each run saves a "gathered up to here" marker, so the next run picks up where you left off
34
+ - **Session JSONL parsing** — Extracts token usage, model names, millisecond-precision timestamps, and conversation turns (user input + assistant output pairs) from `~/.claude/projects/[project]/[sessionId].jsonl`
35
+ - **Full-day default** — Always gathers from today 00:00 by default; upsert logic prevents duplicates across runs
32
36
 
33
37
  ### Rich Work Records
34
38
 
@@ -41,9 +45,15 @@ No need to remember to start/stop tracking. `sincenety` parses `~/.claude/` data
41
45
  | Model | Extracted from assistant responses |
42
46
  | Category | Auto-classified from project path |
43
47
 
48
+ ### AI-Powered Daily Reports
49
+
50
+ Generate summaries powered by Claude Code itself — no external API key needed. The CLI outputs structured JSON with conversation turns, and the Claude Code skill (SKILL.md) instructs the session to generate summaries directly. Summaries are saved to the `daily_reports` table and can be viewed as daily, weekly, or monthly reports.
51
+
52
+ When `ANTHROPIC_API_KEY` is set, the `summarizer.ts` module can also call the Claude API directly for turn-based analysis with heuristic fallback.
53
+
44
54
  ### Email Reports
45
55
 
46
- Send beautiful HTML email reports via Gmail SMTP. Color-coded sessions, token dashboard, and session summaries included.
56
+ Send beautiful HTML email reports via Gmail SMTP. Color-coded sessions, token dashboard, work flow, and outcome/significance data included. XML/system tag cleanup ensures clean output.
47
57
 
48
58
  ### Auto-Scheduling
49
59
 
@@ -76,13 +86,16 @@ npm link
76
86
  ### Basic Gathering
77
87
 
78
88
  ```bash
79
- # Gather since last checkpoint
89
+ # Gather today's work (default: from 00:00, upsert prevents duplicates)
80
90
  sincenety
81
91
 
82
92
  # Gather from specific time
83
93
  sincenety --since "09:00"
84
94
  sincenety --since "2026-04-07 09:00"
85
95
 
96
+ # JSON output with conversation turns (for AI summary pipeline)
97
+ sincenety --json
98
+
86
99
  # Fast mode (history.jsonl only, no token extraction)
87
100
  sincenety --no-detail
88
101
 
@@ -92,6 +105,19 @@ sincenety log --date 2026-04-06
92
105
  sincenety log --week
93
106
  ```
94
107
 
108
+ ### Daily Reports
109
+
110
+ ```bash
111
+ # Save an AI-generated summary to the DB (accepts JSON from stdin)
112
+ sincenety save-daily < summary.json
113
+
114
+ # View daily/weekly/monthly reports
115
+ sincenety report # Today's report
116
+ sincenety report --date 2026-04-06
117
+ sincenety report --week # Weekly aggregate
118
+ sincenety report --month # Monthly aggregate
119
+ ```
120
+
95
121
  ### Email Setup
96
122
 
97
123
  ```bash
@@ -128,11 +154,13 @@ Use `/sincenety` directly inside Claude Code sessions.
128
154
  ```
129
155
  sincenety/
130
156
  ├── src/
131
- │ ├── cli.ts # CLI entry (commander, 5 subcommands)
132
- │ ├── core/gatherer.ts # Core logic (parse → group → store → report)
157
+ │ ├── cli.ts # CLI entry (commander, 7 subcommands)
158
+ │ ├── core/
159
+ │ │ ├── gatherer.ts # Core logic (parse → group → store → report)
160
+ │ │ └── summarizer.ts # Claude API summarization + heuristic fallback
133
161
  │ ├── parser/
134
162
  │ │ ├── history.ts # ~/.claude/history.jsonl streaming parser
135
- │ │ └── session-jsonl.ts # Session JSONL parser (tokens/model/timing)
163
+ │ │ └── session-jsonl.ts # Session JSONL parser (tokens/model/timing/conversationTurns)
136
164
  │ ├── grouper/session.ts # Session grouping by sessionId + project
137
165
  │ ├── storage/
138
166
  │ │ ├── adapter.ts # StorageAdapter interface
@@ -158,14 +186,17 @@ sincenety/
158
186
  ~/.claude/history.jsonl ──→ Extract session list (sessionId + project)
159
187
 
160
188
 
161
- ~/.claude/projects/[project]/[sessionId].jsonl ──→ Extract tokens/model/timing
189
+ ~/.claude/projects/[project]/[sessionId].jsonl ──→ Extract tokens/model/timing/turns
162
190
 
163
191
 
164
192
  Group + summarize
165
193
 
166
- ┌──────────────┼──────────────┐
167
- ▼ ▼
168
- Terminal output DB save (encrypted) Email send
194
+ ┌──────────┬───┼───────┬──────────────┐
195
+ ▼ ▼
196
+ Terminal table DB save Email --json output AI summary
197
+ (box-drawing) (encrypted) │ (Claude Code
198
+ + CJK-aware ▼ or API)
199
+ save-daily ──→ daily_reports
169
200
  ```
170
201
 
171
202
  ### Encryption
@@ -182,6 +213,7 @@ sincenety/
182
213
  |-------|-------------|
183
214
  | `sessions` | Per-session work records (22 columns — tokens, timing, title, description, model, etc.) |
184
215
  | `gather_reports` | Report per gather run (markdown + JSON) |
216
+ | `daily_reports` | AI-generated daily/weekly/monthly summaries (UNIQUE(report_date, report_type)) |
185
217
  | `checkpoints` | Last processed timestamp |
186
218
  | `config` | Settings (email, SMTP, etc.) |
187
219
 
@@ -215,11 +247,13 @@ node dist/cli.js # Direct execution
215
247
 
216
248
  ## Roadmap
217
249
 
250
+ - [x] Weekly/monthly summary reports
218
251
  - [ ] Passphrase encryption option
219
252
  - [ ] Similar task matching (TF-IDF)
220
253
  - [ ] External DB connectors (MariaDB/PostgreSQL)
221
- - [ ] Weekly/monthly summary reports
222
254
  - [ ] ccusage integration (automatic cost calculation)
255
+ - [ ] Multi-language report output (EN/KO toggle)
256
+ - [ ] Report export (PDF/HTML standalone)
223
257
 
224
258
  ---
225
259
 
package/dist/cli.js CHANGED
@@ -21,6 +21,7 @@ program
21
21
  .option("--detail", "세션 JSONL 상세 모드 (토큰/시간 추출)", true)
22
22
  .option("--no-detail", "history.jsonl만 사용 (빠른 모드)")
23
23
  .option("--auto", "자동 갈무리 + 이메일 발송 (스케줄러용)")
24
+ .option("--json", "구조화 JSON 출력 (스킬 연동용, 대화 턴 포함)")
24
25
  .action(async (_, options) => {
25
26
  // "log" 서브커맨드가 아닌 경우에만 갈무리 실행
26
27
  if (program.args[0] === "log")
@@ -46,11 +47,45 @@ program
46
47
  historyPath,
47
48
  useSessionJsonl: options.detail !== false,
48
49
  });
50
+ // --json: 대화 턴 포함 구조화 JSON 출력 (Claude Code 스킬 연동용)
51
+ if (options.json) {
52
+ const jsonOutput = {
53
+ fromTimestamp: result.fromTimestamp,
54
+ toTimestamp: result.toTimestamp,
55
+ isFirstRun: result.isFirstRun,
56
+ sessions: result.sessions.map((s) => ({
57
+ sessionId: s.sessionId,
58
+ projectName: s.projectName,
59
+ startedAt: s.startedAt,
60
+ endedAt: s.endedAt,
61
+ durationMinutes: s.durationMinutes ?? 0,
62
+ messageCount: s.messageCount,
63
+ userMessageCount: s.userMessageCount ?? 0,
64
+ assistantMessageCount: s.assistantMessageCount ?? 0,
65
+ toolCallCount: s.toolCallCount ?? 0,
66
+ inputTokens: s.inputTokens ?? 0,
67
+ outputTokens: s.outputTokens ?? 0,
68
+ totalTokens: (s.inputTokens ?? 0) + (s.outputTokens ?? 0),
69
+ model: s.model ?? "",
70
+ title: s.title ?? s.summary,
71
+ description: s.description ?? "",
72
+ // 대화 턴: 사용자 입력 + 어시스턴트 응답 쌍
73
+ conversationTurns: (s.conversationTurns ?? []).map((t) => ({
74
+ timestamp: t.timestamp,
75
+ userInput: t.userInput,
76
+ assistantOutput: t.assistantOutput,
77
+ })),
78
+ })),
79
+ };
80
+ console.log(JSON.stringify(jsonOutput));
81
+ return;
82
+ }
49
83
  console.log(formatGatherReport(result));
50
- // --auto: 갈무리 후 이메일 발송 (설정된 경우)
51
- if (options.auto) {
52
- const configured = await isEmailConfigured(storage);
53
- if (configured) {
84
+ // 이메일 설정 상태 확인
85
+ const emailConfigured = await isEmailConfigured(storage);
86
+ if (emailConfigured) {
87
+ // --auto 또는 세션이 있을 때 자동 발송
88
+ if (options.auto && result.sessions.length > 0) {
54
89
  try {
55
90
  await sendGatherEmail(storage);
56
91
  }
@@ -59,6 +94,24 @@ program
59
94
  }
60
95
  }
61
96
  }
97
+ else if (result.isFirstRun) {
98
+ // 첫 실행 시 이메일 설정 안내
99
+ console.log(" ────────────────────────────────────────────────────────");
100
+ console.log(" 📧 이메일 발송을 설정하면 갈무리 리포트를 메일로 받을 수 있습니다.");
101
+ console.log("");
102
+ console.log(" 1. Google 앱 비밀번호 생성 (2단계 인증 필요):");
103
+ console.log(" https://myaccount.google.com/apppasswords");
104
+ console.log("");
105
+ console.log(" 2. sincenety에 이메일 설정:");
106
+ console.log(" sincenety config --email you@gmail.com");
107
+ console.log(" sincenety config --smtp-user you@gmail.com");
108
+ console.log(" sincenety config --smtp-pass");
109
+ console.log("");
110
+ console.log(" 설정 후 'sincenety email'로 발송하거나,");
111
+ console.log(" 'sincenety --auto'로 갈무리+발송을 한 번에 실행할 수 있습니다.");
112
+ console.log(" (이메일 없이도 터미널 출력은 항상 동작합니다)");
113
+ console.log("");
114
+ }
62
115
  }
63
116
  catch (err) {
64
117
  console.error(` ❌ ${err instanceof Error ? err.message : String(err)}`);
@@ -117,6 +170,7 @@ program
117
170
  .option("--smtp-port <port>", "SMTP 포트 (기본: 587)")
118
171
  .option("--smtp-user <user>", "SMTP 사용자 (발신 이메일)")
119
172
  .option("--smtp-pass", "SMTP 앱 비밀번호 설정 (프롬프트 입력)")
173
+ // .option("--api-key", "Anthropic API 키 설정 (프롬프트 입력, 세션 요약용)") // 비활성화: 내부 실행 전용
120
174
  .action(async (options) => {
121
175
  const storage = new SqlJsAdapter();
122
176
  try {
@@ -231,6 +285,185 @@ program
231
285
  process.exit(1);
232
286
  }
233
287
  });
288
+ // save-daily 서브커맨드: AI 요약 일일보고 저장
289
+ program
290
+ .command("save-daily")
291
+ .description("AI 요약 일일보고를 DB에 저장 (stdin으로 JSON 입력)")
292
+ .option("--type <type>", "보고 유형: daily | weekly | monthly", "daily")
293
+ .action(async (options) => {
294
+ // stdin 읽기
295
+ const chunks = [];
296
+ for await (const chunk of process.stdin) {
297
+ chunks.push(chunk);
298
+ }
299
+ const input = Buffer.concat(chunks).toString("utf-8").trim();
300
+ if (!input) {
301
+ console.error(" ❌ 입력 데이터가 없습니다. stdin으로 JSON을 전달해 주세요.");
302
+ process.exit(1);
303
+ }
304
+ let data;
305
+ try {
306
+ data = JSON.parse(input);
307
+ if (!data.date || !Array.isArray(data.sessions)) {
308
+ throw new Error("date와 sessions 필드가 필요합니다");
309
+ }
310
+ }
311
+ catch (err) {
312
+ console.error(` ❌ JSON 파싱 실패: ${err instanceof Error ? err.message : String(err)}`);
313
+ process.exit(1);
314
+ }
315
+ const storage = new SqlJsAdapter();
316
+ try {
317
+ await storage.initialize();
318
+ // 세션 통계 보완: DB에서 해당 날짜 세션 데이터 가져오기
319
+ const dbSessions = await storage.getSessionsByDate(data.date);
320
+ let totalMessages = 0;
321
+ let totalTokens = 0;
322
+ // 각 요약에 DB 통계 병합
323
+ for (const summary of data.sessions) {
324
+ const dbSession = dbSessions.find((s) => s.id === summary.sessionId);
325
+ if (dbSession) {
326
+ summary.startedAt ??= dbSession.startedAt;
327
+ summary.endedAt ??= dbSession.endedAt;
328
+ summary.durationMinutes ??= dbSession.durationMinutes;
329
+ summary.messageCount ??= dbSession.messageCount;
330
+ summary.totalTokens ??= dbSession.totalTokens;
331
+ summary.projectName ??= dbSession.projectName;
332
+ }
333
+ totalMessages += summary.messageCount ?? 0;
334
+ totalTokens += summary.totalTokens ?? 0;
335
+ }
336
+ // 기간 계산
337
+ const dateObj = new Date(data.date);
338
+ const periodFrom = dateObj.getTime();
339
+ const periodTo = periodFrom + 86400000; // +1일
340
+ await storage.saveDailyReport({
341
+ reportDate: data.date,
342
+ reportType: options.type,
343
+ periodFrom,
344
+ periodTo,
345
+ sessionCount: data.sessions.length,
346
+ totalMessages,
347
+ totalTokens,
348
+ summaryJson: JSON.stringify(data.sessions),
349
+ overview: data.overview ?? null,
350
+ reportMarkdown: null,
351
+ createdAt: Date.now(),
352
+ emailedAt: null,
353
+ emailTo: null,
354
+ });
355
+ console.log(` ✅ ${options.type === "daily" ? "일일" : options.type === "weekly" ? "주간" : "월간"}보고 저장 완료: ${data.date} (${data.sessions.length}세션)`);
356
+ }
357
+ catch (err) {
358
+ console.error(` ❌ ${err instanceof Error ? err.message : String(err)}`);
359
+ process.exit(1);
360
+ }
361
+ finally {
362
+ await storage.close();
363
+ }
364
+ });
365
+ // report 서브커맨드: 주간/월간 보고 조회
366
+ program
367
+ .command("report")
368
+ .description("일일보고 조회/집계")
369
+ .option("--date <date>", "특정 날짜 일일보고 조회")
370
+ .option("--week", "이번 주 일일보고 집계")
371
+ .option("--month", "이번 달 일일보고 집계")
372
+ .option("--from <date>", "시작 날짜 (YYYY-MM-DD)")
373
+ .option("--to <date>", "종료 날짜 (YYYY-MM-DD)")
374
+ .option("--json", "JSON 출력 (스킬 연동용)")
375
+ .action(async (options) => {
376
+ const storage = new SqlJsAdapter();
377
+ try {
378
+ await storage.initialize();
379
+ let from;
380
+ let to;
381
+ let label;
382
+ if (options.date) {
383
+ // 단일 날짜
384
+ const report = await storage.getDailyReport(options.date);
385
+ if (!report) {
386
+ console.log(` ${options.date}에 일일보고가 없습니다.`);
387
+ return;
388
+ }
389
+ if (options.json) {
390
+ console.log(JSON.stringify(report));
391
+ }
392
+ else {
393
+ console.log(`\n 📋 ${options.date} 일일보고 (${report.sessionCount}세션)`);
394
+ if (report.overview)
395
+ console.log(` ${report.overview}`);
396
+ const sessions = JSON.parse(report.summaryJson || "[]");
397
+ for (const s of sessions) {
398
+ console.log(`\n [${s.projectName}] ${s.topic ?? ""}`);
399
+ if (s.outcome)
400
+ console.log(` 결과: ${s.outcome}`);
401
+ if (s.flow)
402
+ console.log(` 흐름: ${s.flow}`);
403
+ }
404
+ console.log("");
405
+ }
406
+ return;
407
+ }
408
+ if (options.week) {
409
+ const now = new Date();
410
+ const day = now.getDay();
411
+ const monday = new Date(now);
412
+ monday.setDate(now.getDate() - (day === 0 ? 6 : day - 1));
413
+ from = monday.toISOString().slice(0, 10);
414
+ to = now.toISOString().slice(0, 10);
415
+ label = `이번 주 (${from} ~ ${to})`;
416
+ }
417
+ else if (options.month) {
418
+ const now = new Date();
419
+ from = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
420
+ to = now.toISOString().slice(0, 10);
421
+ label = `이번 달 (${from} ~ ${to})`;
422
+ }
423
+ else if (options.from && options.to) {
424
+ from = options.from;
425
+ to = options.to;
426
+ label = `${from} ~ ${to}`;
427
+ }
428
+ else {
429
+ // 기본: 오늘
430
+ from = new Date().toISOString().slice(0, 10);
431
+ to = from;
432
+ label = `오늘 (${from})`;
433
+ }
434
+ const reports = await storage.getDailyReportsByRange(from, to);
435
+ if (reports.length === 0) {
436
+ console.log(` ${label}에 일일보고가 없습니다.`);
437
+ return;
438
+ }
439
+ if (options.json) {
440
+ console.log(JSON.stringify(reports));
441
+ return;
442
+ }
443
+ // 터미널 출력
444
+ const totalSessions = reports.reduce((s, r) => s + r.sessionCount, 0);
445
+ const totalTokens = reports.reduce((s, r) => s + r.totalTokens, 0);
446
+ console.log(`\n 📋 ${label} — ${reports.length}일, ${totalSessions}세션`);
447
+ console.log(" " + "─".repeat(56));
448
+ for (const r of reports) {
449
+ console.log(`\n 📅 ${r.reportDate} (${r.sessionCount}세션)`);
450
+ if (r.overview)
451
+ console.log(` ${r.overview}`);
452
+ const sessions = JSON.parse(r.summaryJson || "[]");
453
+ for (const s of sessions) {
454
+ console.log(` • [${s.projectName}] ${s.topic ?? ""}`);
455
+ }
456
+ }
457
+ console.log("");
458
+ }
459
+ catch (err) {
460
+ console.error(` ❌ ${err instanceof Error ? err.message : String(err)}`);
461
+ process.exit(1);
462
+ }
463
+ finally {
464
+ await storage.close();
465
+ }
466
+ });
234
467
  function promptPassword(prompt) {
235
468
  return new Promise((resolve) => {
236
469
  const rl = createInterface({