@giwonn/claude-daily-review 0.2.3 → 0.3.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/.claude-plugin/marketplace.json +29 -0
- package/.github/workflows/publish.yml +24 -0
- package/.github/workflows/update-marketplace.yml +28 -0
- package/docs/superpowers/plans/2026-03-28-claude-daily-review-plan.md +2130 -0
- package/docs/superpowers/plans/2026-03-28-no-build-refactor-plan.md +1158 -0
- package/docs/superpowers/plans/2026-03-28-storage-adapter-refactor-plan.md +2207 -0
- package/docs/superpowers/specs/2026-03-28-claude-daily-review-design.md +582 -0
- package/docs/superpowers/specs/2026-03-28-no-build-refactor-design.md +333 -0
- package/docs/superpowers/specs/2026-03-28-storage-adapter-refactor-design.md +365 -0
- package/hooks/hooks.json +3 -2
- package/hooks/on-stop.mjs +24 -0
- package/hooks/run-hook.cmd +27 -0
- package/hooks/session-start-check +27 -0
- package/lib/config.mjs +122 -0
- package/lib/github-auth.mjs +44 -0
- package/lib/github-storage.mjs +81 -0
- package/lib/merge.mjs +51 -0
- package/lib/periods.mjs +82 -0
- package/lib/raw-logger.mjs +19 -0
- package/lib/storage-cli.mjs +48 -0
- package/lib/storage.mjs +63 -0
- package/lib/types.d.ts +64 -0
- package/lib/vault.mjs +43 -0
- package/package.json +3 -23
- package/prompts/session-end.md +5 -5
- package/prompts/session-start.md +5 -5
- package/dist/on-session-start-check.js +0 -56
- package/dist/on-session-start-check.js.map +0 -1
- package/dist/on-stop.js +0 -274
- package/dist/on-stop.js.map +0 -1
- package/dist/storage-cli.js +0 -267
- package/dist/storage-cli.js.map +0 -1
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# No-Build 플러그인 구조 전환 설계 문서
|
|
2
|
+
|
|
3
|
+
## 1. 개요
|
|
4
|
+
|
|
5
|
+
TypeScript + tsup 빌드 구조를 제거하고, 순수 `.mjs` + JSDoc 기반으로 전환한다. Claude Code 플러그인 생태계 표준 패턴(빌드 없음, 직접 실행)을 따른다. marketplace.json의 SHA 업데이트를 CI에서 자동화한다.
|
|
6
|
+
|
|
7
|
+
## 2. 목표
|
|
8
|
+
|
|
9
|
+
- 빌드 단계 제거 (tsup, typescript 의존성 제거)
|
|
10
|
+
- `.mjs` + JSDoc으로 타입 안전성 유지
|
|
11
|
+
- dist/ 제거, 소스 파일을 직접 실행
|
|
12
|
+
- superpowers 플러그인 패턴 준수 (run-hook.cmd polyglot wrapper)
|
|
13
|
+
- marketplace.json SHA 자동 업데이트 CI 워크플로우
|
|
14
|
+
|
|
15
|
+
## 3. 파일 구조
|
|
16
|
+
|
|
17
|
+
### 제거 대상
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
src/ ← 전체 제거
|
|
21
|
+
dist/ ← 전체 제거
|
|
22
|
+
tests/ ← 전체 제거
|
|
23
|
+
tsconfig.json ← 제거
|
|
24
|
+
tsup.config.ts ← 제거
|
|
25
|
+
vitest.config.ts ← 제거
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 새 구조
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
claude-daily-review/
|
|
32
|
+
├── .claude-plugin/
|
|
33
|
+
│ └── marketplace.json
|
|
34
|
+
├── .github/
|
|
35
|
+
│ └── workflows/
|
|
36
|
+
│ ├── publish.yml ← npm publish (기존)
|
|
37
|
+
│ └── update-marketplace.yml ← SHA 자동 업데이트 (신규)
|
|
38
|
+
├── hooks/
|
|
39
|
+
│ ├── hooks.json
|
|
40
|
+
│ ├── run-hook.cmd ← Windows+Unix polyglot wrapper
|
|
41
|
+
│ ├── session-start-check ← bash 스크립트 (config 체크 + additionalContext)
|
|
42
|
+
│ └── on-stop.mjs ← Node.js (raw log append)
|
|
43
|
+
├── lib/
|
|
44
|
+
│ ├── types.d.ts ← TypeScript 타입 정의 (JSDoc용)
|
|
45
|
+
│ ├── config.mjs ← 설정 관리
|
|
46
|
+
│ ├── storage.mjs ← StorageAdapter 인터페이스 구현 (local)
|
|
47
|
+
│ ├── github-storage.mjs ← GitHubStorageAdapter
|
|
48
|
+
│ ├── github-auth.mjs ← OAuth Device Flow
|
|
49
|
+
│ └── periods.mjs ← 날짜/주기 유틸리티
|
|
50
|
+
├── prompts/
|
|
51
|
+
│ ├── session-end.md
|
|
52
|
+
│ └── session-start.md
|
|
53
|
+
├── skills/
|
|
54
|
+
│ └── daily-review-setup.md
|
|
55
|
+
├── package.json ← 최소화 (이름, 버전, type: module)
|
|
56
|
+
├── README.md
|
|
57
|
+
└── README.ko.md
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 4. 모듈별 변환
|
|
61
|
+
|
|
62
|
+
### 4.1 lib/types.d.ts
|
|
63
|
+
|
|
64
|
+
JSDoc에서 참조할 타입 정의. 런타임에는 사용되지 않음.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
export interface Profile {
|
|
68
|
+
company: string;
|
|
69
|
+
role: string;
|
|
70
|
+
team: string;
|
|
71
|
+
context: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface Periods {
|
|
75
|
+
daily: true;
|
|
76
|
+
weekly: boolean;
|
|
77
|
+
monthly: boolean;
|
|
78
|
+
quarterly: boolean;
|
|
79
|
+
yearly: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface LocalStorageConfig {
|
|
83
|
+
basePath: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface GitHubStorageConfig {
|
|
87
|
+
owner: string;
|
|
88
|
+
repo: string;
|
|
89
|
+
token: string;
|
|
90
|
+
basePath: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface StorageConfig {
|
|
94
|
+
type: "local" | "github";
|
|
95
|
+
local?: LocalStorageConfig;
|
|
96
|
+
github?: GitHubStorageConfig;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface Config {
|
|
100
|
+
storage: StorageConfig;
|
|
101
|
+
language: string;
|
|
102
|
+
periods: Periods;
|
|
103
|
+
profile: Profile;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface StorageAdapter {
|
|
107
|
+
read(path: string): Promise<string | null>;
|
|
108
|
+
write(path: string, content: string): Promise<void>;
|
|
109
|
+
append(path: string, content: string): Promise<void>;
|
|
110
|
+
exists(path: string): Promise<boolean>;
|
|
111
|
+
list(dir: string): Promise<string[]>;
|
|
112
|
+
mkdir(dir: string): Promise<void>;
|
|
113
|
+
isDirectory(path: string): Promise<boolean>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface HookInput {
|
|
117
|
+
session_id: string;
|
|
118
|
+
transcript_path: string;
|
|
119
|
+
cwd: string;
|
|
120
|
+
hook_event_name: string;
|
|
121
|
+
[key: string]: unknown;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 4.2 lib/config.mjs
|
|
126
|
+
|
|
127
|
+
기존 `src/core/config.ts`를 `.mjs` + JSDoc으로 변환.
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
// @ts-check
|
|
131
|
+
/** @typedef {import('./types.d.ts').Config} Config */
|
|
132
|
+
|
|
133
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
134
|
+
import { dirname, join } from 'path';
|
|
135
|
+
|
|
136
|
+
/** @returns {string} */
|
|
137
|
+
export function getConfigPath() { ... }
|
|
138
|
+
|
|
139
|
+
/** @returns {Config | null} */
|
|
140
|
+
export function loadConfig() { ... }
|
|
141
|
+
// ... 동일한 로직, JSDoc 타입 어노테이션만 추가
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 4.3 lib/storage.mjs
|
|
145
|
+
|
|
146
|
+
`LocalStorageAdapter`를 클래스로 구현.
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
// @ts-check
|
|
150
|
+
/** @typedef {import('./types.d.ts').StorageAdapter} StorageAdapter */
|
|
151
|
+
|
|
152
|
+
/** @implements {StorageAdapter} */
|
|
153
|
+
export class LocalStorageAdapter { ... }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 4.4 lib/github-storage.mjs
|
|
157
|
+
|
|
158
|
+
`GitHubStorageAdapter`를 클래스로 구현. `fetch` 사용 (Node.js 내장).
|
|
159
|
+
|
|
160
|
+
### 4.5 lib/github-auth.mjs
|
|
161
|
+
|
|
162
|
+
OAuth Device Flow. `fetch` 사용.
|
|
163
|
+
|
|
164
|
+
### 4.6 lib/periods.mjs
|
|
165
|
+
|
|
166
|
+
날짜/주기 유틸리티. 순수 함수.
|
|
167
|
+
|
|
168
|
+
### 4.7 hooks/on-stop.mjs
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
#!/usr/bin/env node
|
|
172
|
+
// stdin → raw log append
|
|
173
|
+
import { loadConfig, createStorageAdapter } from '../lib/config.mjs';
|
|
174
|
+
// ...
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 4.8 hooks/session-start-check
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
#!/usr/bin/env bash
|
|
181
|
+
# superpowers 패턴: config 체크 → additionalContext 주입
|
|
182
|
+
set -euo pipefail
|
|
183
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
184
|
+
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
185
|
+
|
|
186
|
+
# Node.js로 config 체크
|
|
187
|
+
result=$(node -e "
|
|
188
|
+
import { loadConfig } from '${PLUGIN_ROOT}/lib/config.mjs';
|
|
189
|
+
const config = loadConfig();
|
|
190
|
+
if (!config) process.stdout.write('NEEDS_SETUP');
|
|
191
|
+
" 2>/dev/null || echo "NEEDS_SETUP")
|
|
192
|
+
|
|
193
|
+
if [ "$result" = "NEEDS_SETUP" ]; then
|
|
194
|
+
msg="<important-reminder>IN YOUR FIRST REPLY YOU MUST TELL THE USER: daily-review 플러그인이 아직 설정되지 않았습니다. /daily-review-setup 을 실행해주세요.</important-reminder>"
|
|
195
|
+
escaped=$(echo "$msg" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
196
|
+
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\n' "$escaped"
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
exit 0
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### 4.9 hooks/run-hook.cmd
|
|
203
|
+
|
|
204
|
+
superpowers의 polyglot wrapper를 그대로 복사하여 Windows + Unix 모두 지원.
|
|
205
|
+
|
|
206
|
+
## 5. hooks/hooks.json
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"hooks": {
|
|
211
|
+
"Stop": [
|
|
212
|
+
{
|
|
213
|
+
"hooks": [
|
|
214
|
+
{
|
|
215
|
+
"type": "command",
|
|
216
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/on-stop.mjs\"",
|
|
217
|
+
"async": true,
|
|
218
|
+
"timeout": 10
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
],
|
|
223
|
+
"SessionEnd": [
|
|
224
|
+
{
|
|
225
|
+
"hooks": [
|
|
226
|
+
{
|
|
227
|
+
"type": "agent",
|
|
228
|
+
"prompt": "Follow the instructions in the file at ${CLAUDE_PLUGIN_ROOT}/prompts/session-end.md exactly. The CLAUDE_PLUGIN_DATA directory is: ${CLAUDE_PLUGIN_DATA}. The plugin root is: ${CLAUDE_PLUGIN_ROOT}",
|
|
229
|
+
"timeout": 120
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
],
|
|
234
|
+
"SessionStart": [
|
|
235
|
+
{
|
|
236
|
+
"matcher": "startup",
|
|
237
|
+
"hooks": [
|
|
238
|
+
{
|
|
239
|
+
"type": "command",
|
|
240
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start-check",
|
|
241
|
+
"timeout": 5
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## 6. package.json
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"name": "@giwonn/claude-daily-review",
|
|
255
|
+
"version": "0.3.0",
|
|
256
|
+
"type": "module",
|
|
257
|
+
"description": "Claude Code plugin that auto-captures conversations for daily review and career documentation",
|
|
258
|
+
"repository": {
|
|
259
|
+
"type": "git",
|
|
260
|
+
"url": "https://github.com/giwonn/claude-daily-review"
|
|
261
|
+
},
|
|
262
|
+
"license": "MIT"
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
의존성 없음. devDependencies도 없음.
|
|
267
|
+
|
|
268
|
+
## 7. marketplace.json SHA 자동 업데이트
|
|
269
|
+
|
|
270
|
+
### 문제
|
|
271
|
+
|
|
272
|
+
릴리즈마다 marketplace.json의 SHA를 수동으로 업데이트해야 함.
|
|
273
|
+
|
|
274
|
+
### 해결: GitHub Actions 워크플로우
|
|
275
|
+
|
|
276
|
+
release 생성 시:
|
|
277
|
+
1. npm publish
|
|
278
|
+
2. marketplace.json의 SHA를 릴리즈 태그의 커밋 SHA로 업데이트
|
|
279
|
+
3. 자동 커밋 + push
|
|
280
|
+
|
|
281
|
+
```yaml
|
|
282
|
+
# .github/workflows/update-marketplace.yml
|
|
283
|
+
name: Update Marketplace SHA
|
|
284
|
+
|
|
285
|
+
on:
|
|
286
|
+
release:
|
|
287
|
+
types: [published]
|
|
288
|
+
|
|
289
|
+
jobs:
|
|
290
|
+
update-sha:
|
|
291
|
+
runs-on: ubuntu-latest
|
|
292
|
+
permissions:
|
|
293
|
+
contents: write
|
|
294
|
+
steps:
|
|
295
|
+
- uses: actions/checkout@v4
|
|
296
|
+
with:
|
|
297
|
+
ref: master
|
|
298
|
+
|
|
299
|
+
- name: Update SHA in marketplace.json
|
|
300
|
+
run: |
|
|
301
|
+
SHA=$(git rev-parse HEAD)
|
|
302
|
+
sed -i "s/\"sha\": \"[a-f0-9]*\"/\"sha\": \"$SHA\"/" .claude-plugin/marketplace.json
|
|
303
|
+
|
|
304
|
+
- name: Commit and push
|
|
305
|
+
run: |
|
|
306
|
+
git config user.name "github-actions[bot]"
|
|
307
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
308
|
+
git add .claude-plugin/marketplace.json
|
|
309
|
+
git commit -m "chore: update marketplace SHA to $SHA" || exit 0
|
|
310
|
+
git push
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### 주의: 순환 방지
|
|
314
|
+
|
|
315
|
+
marketplace.json 변경 커밋은 릴리즈 태그에 포함되지 않으므로, 다음 릴리즈에서 반영됨. 이는 의도된 동작 — 릴리즈 시점의 코드가 설치되고, SHA는 그 직후 업데이트됨.
|
|
316
|
+
|
|
317
|
+
## 8. 스코프
|
|
318
|
+
|
|
319
|
+
### 포함
|
|
320
|
+
|
|
321
|
+
- TypeScript → .mjs + JSDoc 변환 (config, storage, github-storage, github-auth, periods)
|
|
322
|
+
- Hook 스크립트 변환 (on-stop.mjs, session-start-check bash)
|
|
323
|
+
- run-hook.cmd polyglot wrapper 추가
|
|
324
|
+
- hooks.json 업데이트
|
|
325
|
+
- package.json 최소화 (빌드 의존성 제거)
|
|
326
|
+
- src/, dist/, tests/, tsconfig, tsup, vitest 제거
|
|
327
|
+
- marketplace SHA 자동 업데이트 워크플로우
|
|
328
|
+
- .gitignore 정리
|
|
329
|
+
|
|
330
|
+
### 제외
|
|
331
|
+
|
|
332
|
+
- 기능 변경 없음 — 동일한 동작, 다른 구조
|
|
333
|
+
- 테스트 프레임워크 재구축 (플러그인은 수동 테스트가 표준)
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# StorageAdapter 추상화 + GitHub 연동 설계 문서
|
|
2
|
+
|
|
3
|
+
## 1. 개요
|
|
4
|
+
|
|
5
|
+
기존 claude-daily-review 플러그인의 외부 저장소 의존성(fs)을 `StorageAdapter` 인터페이스로 추상화하고, GitHub를 저장소 백엔드로 추가한다. 사용자는 셋업 시 로컬 파일시스템 또는 GitHub 중 선택할 수 있다.
|
|
6
|
+
|
|
7
|
+
## 2. 목표
|
|
8
|
+
|
|
9
|
+
- 외부 저장소 의존성을 인터페이스로 추상화 (DIP)
|
|
10
|
+
- GitHub Contents API 기반 저장소 어댑터 추가
|
|
11
|
+
- GitHub OAuth Device Flow로 사용자 인증 (우리 OAuth App의 client_id 사용)
|
|
12
|
+
- GitHub repo 자동 생성 또는 기존 repo 지정 지원
|
|
13
|
+
- 기존 로컬 저장소 기능 유지
|
|
14
|
+
- 기존 테스트 async 전환 및 유지
|
|
15
|
+
|
|
16
|
+
## 3. StorageAdapter 인터페이스
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
export interface StorageAdapter {
|
|
20
|
+
read(path: string): Promise<string | null>;
|
|
21
|
+
write(path: string, content: string): Promise<void>;
|
|
22
|
+
append(path: string, content: string): Promise<void>;
|
|
23
|
+
exists(path: string): Promise<boolean>;
|
|
24
|
+
list(dir: string): Promise<string[]>;
|
|
25
|
+
mkdir(dir: string): Promise<void>;
|
|
26
|
+
isDirectory(path: string): Promise<boolean>;
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
모든 경로는 storage root 기준 상대 경로. 예: `daily/2026-03-28.md`, `.raw/sess-1/2026-03-28.jsonl`
|
|
31
|
+
|
|
32
|
+
## 4. 구현체
|
|
33
|
+
|
|
34
|
+
### 4.1 LocalStorageAdapter
|
|
35
|
+
|
|
36
|
+
기존 fs 코드를 StorageAdapter 인터페이스로 감싼다.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
export class LocalStorageAdapter implements StorageAdapter {
|
|
40
|
+
constructor(private basePath: string) {}
|
|
41
|
+
// fs 함수들을 async wrapper로 구현
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| 메서드 | 구현 |
|
|
46
|
+
|--------|------|
|
|
47
|
+
| `read(path)` | `readFileSync(resolve(basePath, path))` — 파일 없으면 `null` 반환 |
|
|
48
|
+
| `write(path, content)` | `mkdirSync` + `writeFileSync` |
|
|
49
|
+
| `append(path, content)` | `mkdirSync` + `appendFileSync` |
|
|
50
|
+
| `exists(path)` | `existsSync` |
|
|
51
|
+
| `list(dir)` | `readdirSync` — 디렉토리 없으면 `[]` |
|
|
52
|
+
| `mkdir(dir)` | `mkdirSync({ recursive: true })` |
|
|
53
|
+
| `isDirectory(path)` | `statSync().isDirectory()` — 에러 시 `false` |
|
|
54
|
+
|
|
55
|
+
### 4.2 GitHubStorageAdapter
|
|
56
|
+
|
|
57
|
+
GitHub Contents API를 사용한다.
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
export class GitHubStorageAdapter implements StorageAdapter {
|
|
61
|
+
constructor(private owner: string, private repo: string, private token: string, private basePath: string) {}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
| 메서드 | GitHub API |
|
|
66
|
+
|--------|------------|
|
|
67
|
+
| `read(path)` | `GET /repos/{owner}/{repo}/contents/{basePath}/{path}` — base64 디코드, 404면 `null` |
|
|
68
|
+
| `write(path, content)` | `PUT /repos/{owner}/{repo}/contents/{basePath}/{path}` — 기존 파일이면 SHA 포함 |
|
|
69
|
+
| `append(path, content)` | `read` → 기존 내용 + content → `write` (SHA 기반 업데이트) |
|
|
70
|
+
| `exists(path)` | `GET` 후 200이면 `true`, 404면 `false` |
|
|
71
|
+
| `list(dir)` | `GET /repos/{owner}/{repo}/contents/{basePath}/{dir}` — 배열 응답에서 name 추출 |
|
|
72
|
+
| `mkdir(dir)` | no-op (GitHub는 디렉토리 개념 없음, 파일 생성 시 자동 생성) |
|
|
73
|
+
| `isDirectory(path)` | `GET` 후 응답이 배열이면 `true` |
|
|
74
|
+
|
|
75
|
+
### SHA 관리
|
|
76
|
+
|
|
77
|
+
GitHub Contents API는 파일 업데이트 시 현재 SHA가 필요하다. GitHubStorageAdapter 내부에서:
|
|
78
|
+
1. `write`/`append` 호출 시 먼저 `GET`으로 현재 SHA 취득
|
|
79
|
+
2. SHA와 함께 `PUT` 요청
|
|
80
|
+
3. 409 Conflict 발생 시 SHA 재취득 후 재시도 (최대 3회)
|
|
81
|
+
|
|
82
|
+
### Rate Limit 고려
|
|
83
|
+
|
|
84
|
+
- GitHub API: 인증된 요청 시간당 5,000회
|
|
85
|
+
- Stop 훅은 async라 응답마다 1회 호출 — 하루 수백 회 수준으로 충분
|
|
86
|
+
- 주기별 요약은 SessionStart에서 몇 회 호출 — 무시할 수준
|
|
87
|
+
|
|
88
|
+
## 5. GitHub OAuth Device Flow
|
|
89
|
+
|
|
90
|
+
### 흐름
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
1. POST https://github.com/login/device/code
|
|
94
|
+
Body: { client_id: "OUR_CLIENT_ID", scope: "repo" }
|
|
95
|
+
Response: { device_code, user_code, verification_uri, interval }
|
|
96
|
+
|
|
97
|
+
2. 사용자에게 표시:
|
|
98
|
+
"https://github.com/login/device 에 접속해서 코드 ABCD-1234 를 입력하세요"
|
|
99
|
+
|
|
100
|
+
3. Polling (interval 간격):
|
|
101
|
+
POST https://github.com/login/oauth/access_token
|
|
102
|
+
Body: { client_id, device_code, grant_type: "urn:ietf:params:oauth:grant-type:device_code" }
|
|
103
|
+
|
|
104
|
+
- "authorization_pending" → 계속 polling
|
|
105
|
+
- "slow_down" → interval 증가 후 계속
|
|
106
|
+
- 200 + access_token → 완료
|
|
107
|
+
|
|
108
|
+
4. 토큰을 config에 저장
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### client_id
|
|
112
|
+
|
|
113
|
+
코드에 상수로 포함. OAuth App은 GitHub에 "claude-daily-review"로 등록.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const GITHUB_CLIENT_ID = "Ov23li..."; // 실제 등록 후 값
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
참고: Device Flow는 public client이므로 client_secret 불필요.
|
|
120
|
+
|
|
121
|
+
### Scope
|
|
122
|
+
|
|
123
|
+
`repo` — private repo 접근 필요. public repo만 지원하려면 `public_repo`로 축소 가능하나, 사용자 편의를 위해 `repo` 사용.
|
|
124
|
+
|
|
125
|
+
## 6. GitHub Repo 설정
|
|
126
|
+
|
|
127
|
+
### 셋업 플로우
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
/daily-review-setup
|
|
131
|
+
→ storage 선택: "local" / "github"
|
|
132
|
+
|
|
133
|
+
[github 선택 시]
|
|
134
|
+
→ OAuth Device Flow 인증
|
|
135
|
+
→ "기존 repo를 사용할까요, 새로 만들까요?"
|
|
136
|
+
→ 기존: repo 이름 입력 (owner/repo)
|
|
137
|
+
→ 새로: repo 이름 입력 → POST /user/repos로 생성 (private)
|
|
138
|
+
→ basePath 설정 (기본: "daily-review")
|
|
139
|
+
→ 완료
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Repo 생성 API
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
POST /user/repos
|
|
146
|
+
Body: {
|
|
147
|
+
name: "daily-review",
|
|
148
|
+
private: true,
|
|
149
|
+
description: "Auto-generated daily review by claude-daily-review"
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 7. 설정 스키마 변경
|
|
154
|
+
|
|
155
|
+
### 기존
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"vaultPath": "/path/to/vault",
|
|
160
|
+
"reviewFolder": "daily-review",
|
|
161
|
+
...
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 변경 후
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"storage": {
|
|
170
|
+
"type": "local",
|
|
171
|
+
"local": {
|
|
172
|
+
"basePath": "/path/to/vault/daily-review"
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
"language": "ko",
|
|
176
|
+
"periods": {
|
|
177
|
+
"daily": true,
|
|
178
|
+
"weekly": true,
|
|
179
|
+
"monthly": true,
|
|
180
|
+
"quarterly": true,
|
|
181
|
+
"yearly": false
|
|
182
|
+
},
|
|
183
|
+
"profile": {
|
|
184
|
+
"company": "ABC Corp",
|
|
185
|
+
"role": "프론트엔드 개발자",
|
|
186
|
+
"team": "결제플랫폼팀",
|
|
187
|
+
"context": "B2B SaaS 결제 시스템 개발 및 운영"
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
또는 GitHub:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"storage": {
|
|
197
|
+
"type": "github",
|
|
198
|
+
"github": {
|
|
199
|
+
"owner": "username",
|
|
200
|
+
"repo": "daily-review",
|
|
201
|
+
"token": "gho_xxx",
|
|
202
|
+
"basePath": "daily-review"
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
"language": "ko",
|
|
206
|
+
"periods": { ... },
|
|
207
|
+
"profile": { ... }
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`vaultPath`와 `reviewFolder`는 `storage.local.basePath`로 통합. 기존 config가 있으면 마이그레이션 지원.
|
|
212
|
+
|
|
213
|
+
## 8. 변경 대상 모듈
|
|
214
|
+
|
|
215
|
+
### 변경 필요
|
|
216
|
+
|
|
217
|
+
| 모듈 | 변경 내용 |
|
|
218
|
+
|------|-----------|
|
|
219
|
+
| `src/core/storage.ts` | **신규.** StorageAdapter 인터페이스 정의 |
|
|
220
|
+
| `src/core/local-storage.ts` | **신규.** LocalStorageAdapter 구현 |
|
|
221
|
+
| `src/core/github-storage.ts` | **신규.** GitHubStorageAdapter 구현 |
|
|
222
|
+
| `src/core/github-auth.ts` | **신규.** OAuth Device Flow 구현 |
|
|
223
|
+
| `src/core/config.ts` | 설정 스키마 변경 + 어댑터 팩토리 함수 |
|
|
224
|
+
| `src/core/vault.ts` | fs 직접 사용 → StorageAdapter 주입, async 전환 |
|
|
225
|
+
| `src/core/raw-logger.ts` | fs 직접 사용 → StorageAdapter 주입, async 전환 |
|
|
226
|
+
| `src/core/merge.ts` | fs 직접 사용 → StorageAdapter 주입, async 전환 |
|
|
227
|
+
| `src/hooks/on-stop.ts` | adapter 생성 후 주입, async 전환 |
|
|
228
|
+
| `prompts/session-end.md` | 에이전트가 storage type에 따라 다르게 동작하도록 안내 |
|
|
229
|
+
| `prompts/session-start.md` | 동일 |
|
|
230
|
+
| `skills/daily-review-setup.md` | storage 선택 + GitHub 인증 플로우 추가 |
|
|
231
|
+
|
|
232
|
+
### 변경 없음
|
|
233
|
+
|
|
234
|
+
| 모듈 | 이유 |
|
|
235
|
+
|------|------|
|
|
236
|
+
| `src/core/periods.ts` | 순수 계산, 외부 의존 없음 |
|
|
237
|
+
| `hooks/hooks.json` | 훅 설정 자체는 동일 |
|
|
238
|
+
|
|
239
|
+
## 9. async 전환
|
|
240
|
+
|
|
241
|
+
StorageAdapter의 모든 메서드가 `Promise`를 반환하므로, 이를 사용하는 모든 함수가 async로 전환된다.
|
|
242
|
+
|
|
243
|
+
### 영향 범위
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// Before (sync)
|
|
247
|
+
export function appendRawLog(sessionDir: string, date: string, entry: HookInput): void
|
|
248
|
+
|
|
249
|
+
// After (async)
|
|
250
|
+
export async function appendRawLog(storage: StorageAdapter, sessionDir: string, date: string, entry: HookInput): Promise<void>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
| 함수 | 변경 |
|
|
254
|
+
|------|------|
|
|
255
|
+
| `vault.ensureVaultDirectories(config)` | `ensureVaultDirectories(storage, config)` async |
|
|
256
|
+
| `rawLogger.appendRawLog(dir, date, entry)` | `appendRawLog(storage, dir, date, entry)` async |
|
|
257
|
+
| `merge.findUnprocessedSessions(dir)` | `findUnprocessedSessions(storage, dir)` async |
|
|
258
|
+
| `merge.findPendingReviews(dir)` | `findPendingReviews(storage, dir)` async |
|
|
259
|
+
| `merge.markSessionCompleted(dir)` | `markSessionCompleted(storage, dir)` async |
|
|
260
|
+
| `merge.isSessionCompleted(dir)` | `isSessionCompleted(storage, dir)` async |
|
|
261
|
+
| `merge.mergeReviewsIntoDaily(paths, daily)` | `mergeReviewsIntoDaily(storage, paths, daily)` async |
|
|
262
|
+
| `onStop.handleStopHook(stdin)` | `handleStopHook(stdin)` async |
|
|
263
|
+
|
|
264
|
+
vault의 경로 생성 함수들(getDailyPath 등)은 순수 문자열 연산이므로 변경 없음.
|
|
265
|
+
|
|
266
|
+
## 10. 테스트 전략
|
|
267
|
+
|
|
268
|
+
### LocalStorageAdapter 테스트
|
|
269
|
+
|
|
270
|
+
기존 테스트를 async로 전환 + StorageAdapter를 통해 호출하도록 수정. 실질적 동작은 동일.
|
|
271
|
+
|
|
272
|
+
### GitHubStorageAdapter 테스트
|
|
273
|
+
|
|
274
|
+
GitHub API를 직접 호출하지 않고, HTTP 요청을 mock하여 테스트.
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// fetch를 mock하여 GitHub API 응답 시뮬레이션
|
|
278
|
+
vi.spyOn(globalThis, "fetch").mockImplementation(...)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
테스트 케이스:
|
|
282
|
+
- read: 정상 응답 (base64 디코드), 404 → null
|
|
283
|
+
- write: 새 파일 생성 (SHA 없음), 기존 파일 업데이트 (SHA 포함)
|
|
284
|
+
- append: read + write 조합
|
|
285
|
+
- exists: 200 → true, 404 → false
|
|
286
|
+
- list: 배열 응답에서 name 추출
|
|
287
|
+
- write 409 conflict → SHA 재취득 후 재시도
|
|
288
|
+
|
|
289
|
+
### GitHub Auth 테스트
|
|
290
|
+
|
|
291
|
+
Device Flow의 각 단계를 fetch mock으로 테스트:
|
|
292
|
+
- device code 요청 성공
|
|
293
|
+
- polling: authorization_pending → 재시도
|
|
294
|
+
- polling: slow_down → interval 증가
|
|
295
|
+
- polling: 성공 → 토큰 반환
|
|
296
|
+
- polling: 타임아웃
|
|
297
|
+
|
|
298
|
+
### 코어 모듈 테스트
|
|
299
|
+
|
|
300
|
+
vault, raw-logger, merge 테스트는 StorageAdapter mock을 주입하여 테스트. 실제 fs 의존 없이 순수 로직만 검증.
|
|
301
|
+
|
|
302
|
+
추가로 LocalStorageAdapter를 사용한 통합 테스트도 유지.
|
|
303
|
+
|
|
304
|
+
## 11. 에이전트 프롬프트 변경
|
|
305
|
+
|
|
306
|
+
SessionEnd/SessionStart 에이전트는 파일을 직접 Read/Write 도구로 조작한다. GitHub storage인 경우:
|
|
307
|
+
- 에이전트가 config에서 storage type 확인
|
|
308
|
+
- `type: "local"` → 기존대로 Read/Write 도구 사용
|
|
309
|
+
- `type: "github"` → Bash 도구로 `node` 스크립트 호출하여 GitHub API 사용
|
|
310
|
+
|
|
311
|
+
이를 위해 에이전트가 호출할 수 있는 CLI 유틸리티 스크립트를 추가:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
# 파일 읽기
|
|
315
|
+
node dist/cli/storage-read.js <path>
|
|
316
|
+
|
|
317
|
+
# 파일 쓰기 (stdin으로 content)
|
|
318
|
+
echo "content" | node dist/cli/storage-write.js <path>
|
|
319
|
+
|
|
320
|
+
# 파일 목록
|
|
321
|
+
node dist/cli/storage-list.js <dir>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
이 스크립트들은 config에서 storage type을 읽고 적절한 어댑터를 사용한다.
|
|
325
|
+
|
|
326
|
+
## 12. Config 마이그레이션
|
|
327
|
+
|
|
328
|
+
기존 config 형식(`vaultPath` + `reviewFolder`)이 감지되면 자동 마이그레이션:
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
function migrateConfig(old: OldConfig): Config {
|
|
332
|
+
return {
|
|
333
|
+
storage: {
|
|
334
|
+
type: "local",
|
|
335
|
+
local: {
|
|
336
|
+
basePath: join(old.vaultPath, old.reviewFolder),
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
language: old.language,
|
|
340
|
+
periods: old.periods,
|
|
341
|
+
profile: old.profile,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## 13. 스코프
|
|
347
|
+
|
|
348
|
+
### 이번 스코프
|
|
349
|
+
|
|
350
|
+
- StorageAdapter 인터페이스 정의
|
|
351
|
+
- LocalStorageAdapter 구현
|
|
352
|
+
- GitHubStorageAdapter 구현
|
|
353
|
+
- GitHub OAuth Device Flow 구현
|
|
354
|
+
- 코어 모듈 리팩토링 (StorageAdapter 주입, async 전환)
|
|
355
|
+
- 에이전트용 CLI 스크립트 (storage-read, storage-write, storage-list)
|
|
356
|
+
- 설정 스키마 변경 + 마이그레이션
|
|
357
|
+
- 셋업 스킬 업데이트 (storage 선택 + GitHub 인증)
|
|
358
|
+
- 에이전트 프롬프트 업데이트
|
|
359
|
+
- 테스트 전면 수정
|
|
360
|
+
|
|
361
|
+
### 제외
|
|
362
|
+
|
|
363
|
+
- GitHub App 서버 사이드 (불필요 — Device Flow는 serverless)
|
|
364
|
+
- 실제 OAuth App 등록 (구현 후 별도 진행)
|
|
365
|
+
- GitHub Pages 연동
|