@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,1158 @@
|
|
|
1
|
+
# No-Build Refactor Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Convert TypeScript + tsup build system to plain .mjs + JSDoc with no build step, matching Claude Code plugin ecosystem conventions.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Replace src/*.ts with lib/*.mjs (JSDoc typed), replace dist/ with direct execution, add run-hook.cmd polyglot wrapper for Windows support, add CI workflow for marketplace SHA auto-update.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js ESM (.mjs), JSDoc type annotations, bash scripts, GitHub Actions
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File Structure
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
claude-daily-review/
|
|
17
|
+
├── .claude-plugin/
|
|
18
|
+
│ └── marketplace.json ← MODIFY: SHA auto-update
|
|
19
|
+
├── .github/workflows/
|
|
20
|
+
│ ├── publish.yml ← KEEP
|
|
21
|
+
│ └── update-marketplace.yml ← CREATE: SHA auto-update
|
|
22
|
+
├── hooks/
|
|
23
|
+
│ ├── hooks.json ← MODIFY: new paths
|
|
24
|
+
│ ├── run-hook.cmd ← CREATE: polyglot wrapper
|
|
25
|
+
│ ├── session-start-check ← CREATE: bash script
|
|
26
|
+
│ └── on-stop.mjs ← CREATE: raw log append
|
|
27
|
+
├── lib/
|
|
28
|
+
│ ├── types.d.ts ← CREATE: type definitions for JSDoc
|
|
29
|
+
│ ├── config.mjs ← CREATE: from src/core/config.ts
|
|
30
|
+
│ ├── storage.mjs ← CREATE: from src/core/local-storage.ts
|
|
31
|
+
│ ├── github-storage.mjs ← CREATE: from src/core/github-storage.ts
|
|
32
|
+
│ ├── github-auth.mjs ← CREATE: from src/core/github-auth.ts
|
|
33
|
+
│ ├── periods.mjs ← CREATE: from src/core/periods.ts
|
|
34
|
+
│ ├── vault.mjs ← CREATE: from src/core/vault.ts
|
|
35
|
+
│ ├── raw-logger.mjs ← CREATE: from src/core/raw-logger.ts
|
|
36
|
+
│ ├── merge.mjs ← CREATE: from src/core/merge.ts
|
|
37
|
+
│ └── storage-cli.mjs ← CREATE: from src/cli/storage-cli.ts
|
|
38
|
+
├── prompts/ ← KEEP (update paths in content)
|
|
39
|
+
├── skills/ ← KEEP
|
|
40
|
+
├── package.json ← MODIFY: simplify
|
|
41
|
+
├── README.md ← KEEP
|
|
42
|
+
├── README.ko.md ← KEEP
|
|
43
|
+
│
|
|
44
|
+
├── src/ ← DELETE entire directory
|
|
45
|
+
├── dist/ ← DELETE entire directory
|
|
46
|
+
├── tests/ ← DELETE entire directory
|
|
47
|
+
├── tsconfig.json ← DELETE
|
|
48
|
+
├── tsup.config.ts ← DELETE
|
|
49
|
+
└── vitest.config.ts ← DELETE
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### Task 1: Create lib/ with types and core modules
|
|
55
|
+
|
|
56
|
+
**Files:**
|
|
57
|
+
- Create: `lib/types.d.ts`
|
|
58
|
+
- Create: `lib/config.mjs`
|
|
59
|
+
- Create: `lib/periods.mjs`
|
|
60
|
+
- Create: `lib/storage.mjs`
|
|
61
|
+
- Create: `lib/vault.mjs`
|
|
62
|
+
- Create: `lib/raw-logger.mjs`
|
|
63
|
+
- Create: `lib/merge.mjs`
|
|
64
|
+
|
|
65
|
+
- [ ] **Step 1: Create lib/types.d.ts**
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// lib/types.d.ts
|
|
69
|
+
export interface Profile {
|
|
70
|
+
company: string;
|
|
71
|
+
role: string;
|
|
72
|
+
team: string;
|
|
73
|
+
context: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface Periods {
|
|
77
|
+
daily: true;
|
|
78
|
+
weekly: boolean;
|
|
79
|
+
monthly: boolean;
|
|
80
|
+
quarterly: boolean;
|
|
81
|
+
yearly: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface LocalStorageConfig {
|
|
85
|
+
basePath: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface GitHubStorageConfig {
|
|
89
|
+
owner: string;
|
|
90
|
+
repo: string;
|
|
91
|
+
token: string;
|
|
92
|
+
basePath: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface StorageConfig {
|
|
96
|
+
type: "local" | "github";
|
|
97
|
+
local?: LocalStorageConfig;
|
|
98
|
+
github?: GitHubStorageConfig;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface Config {
|
|
102
|
+
storage: StorageConfig;
|
|
103
|
+
language: string;
|
|
104
|
+
periods: Periods;
|
|
105
|
+
profile: Profile;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface StorageAdapter {
|
|
109
|
+
read(path: string): Promise<string | null>;
|
|
110
|
+
write(path: string, content: string): Promise<void>;
|
|
111
|
+
append(path: string, content: string): Promise<void>;
|
|
112
|
+
exists(path: string): Promise<boolean>;
|
|
113
|
+
list(dir: string): Promise<string[]>;
|
|
114
|
+
mkdir(dir: string): Promise<void>;
|
|
115
|
+
isDirectory(path: string): Promise<boolean>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface HookInput {
|
|
119
|
+
session_id: string;
|
|
120
|
+
transcript_path: string;
|
|
121
|
+
cwd: string;
|
|
122
|
+
hook_event_name: string;
|
|
123
|
+
[key: string]: unknown;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface DeviceCodeResponse {
|
|
127
|
+
device_code: string;
|
|
128
|
+
user_code: string;
|
|
129
|
+
verification_uri: string;
|
|
130
|
+
expires_in: number;
|
|
131
|
+
interval: number;
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- [ ] **Step 2: Create lib/config.mjs**
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
// @ts-check
|
|
139
|
+
/** @typedef {import('./types.d.ts').Config} Config */
|
|
140
|
+
/** @typedef {import('./types.d.ts').StorageAdapter} StorageAdapter */
|
|
141
|
+
|
|
142
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
143
|
+
import { dirname, join } from 'path';
|
|
144
|
+
import { LocalStorageAdapter } from './storage.mjs';
|
|
145
|
+
|
|
146
|
+
/** @returns {string} */
|
|
147
|
+
export function getConfigPath() {
|
|
148
|
+
const dataDir = process.env.CLAUDE_PLUGIN_DATA;
|
|
149
|
+
if (!dataDir) {
|
|
150
|
+
throw new Error('CLAUDE_PLUGIN_DATA environment variable is not set');
|
|
151
|
+
}
|
|
152
|
+
return join(dataDir, 'config.json');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {unknown} raw
|
|
157
|
+
* @returns {raw is { vaultPath: string; reviewFolder: string; language: string; periods: any; profile: any }}
|
|
158
|
+
*/
|
|
159
|
+
function isOldConfig(raw) {
|
|
160
|
+
if (!raw || typeof raw !== 'object') return false;
|
|
161
|
+
return 'vaultPath' in raw && 'reviewFolder' in raw;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {{ vaultPath: string; reviewFolder: string; language: string; periods: any; profile: any }} old
|
|
166
|
+
* @returns {Config}
|
|
167
|
+
*/
|
|
168
|
+
function migrateOldConfig(old) {
|
|
169
|
+
return {
|
|
170
|
+
storage: {
|
|
171
|
+
type: 'local',
|
|
172
|
+
local: { basePath: join(old.vaultPath, old.reviewFolder) },
|
|
173
|
+
},
|
|
174
|
+
language: old.language,
|
|
175
|
+
periods: old.periods,
|
|
176
|
+
profile: old.profile,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** @returns {Config | null} */
|
|
181
|
+
export function loadConfig() {
|
|
182
|
+
const configPath = getConfigPath();
|
|
183
|
+
if (!existsSync(configPath)) return null;
|
|
184
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
185
|
+
if (isOldConfig(raw)) {
|
|
186
|
+
const migrated = migrateOldConfig(raw);
|
|
187
|
+
saveConfig(migrated);
|
|
188
|
+
return migrated;
|
|
189
|
+
}
|
|
190
|
+
return /** @type {Config} */ (raw);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @param {Config} config */
|
|
194
|
+
export function saveConfig(config) {
|
|
195
|
+
const configPath = getConfigPath();
|
|
196
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
197
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {unknown} config
|
|
202
|
+
* @returns {config is Config}
|
|
203
|
+
*/
|
|
204
|
+
export function validateConfig(config) {
|
|
205
|
+
if (!config || typeof config !== 'object') return false;
|
|
206
|
+
const c = /** @type {Record<string, unknown>} */ (config);
|
|
207
|
+
if (!c.storage || typeof c.storage !== 'object') return false;
|
|
208
|
+
const s = /** @type {Record<string, unknown>} */ (c.storage);
|
|
209
|
+
if (s.type !== 'local' && s.type !== 'github') return false;
|
|
210
|
+
if (s.type === 'local') {
|
|
211
|
+
if (!s.local || typeof s.local !== 'object') return false;
|
|
212
|
+
const l = /** @type {Record<string, unknown>} */ (s.local);
|
|
213
|
+
if (typeof l.basePath !== 'string' || l.basePath === '') return false;
|
|
214
|
+
}
|
|
215
|
+
if (s.type === 'github') {
|
|
216
|
+
if (!s.github || typeof s.github !== 'object') return false;
|
|
217
|
+
const g = /** @type {Record<string, unknown>} */ (s.github);
|
|
218
|
+
if (typeof g.owner !== 'string' || !g.owner) return false;
|
|
219
|
+
if (typeof g.repo !== 'string' || !g.repo) return false;
|
|
220
|
+
if (typeof g.token !== 'string' || !g.token) return false;
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** @param {string} basePath @returns {Config} */
|
|
226
|
+
export function createDefaultLocalConfig(basePath) {
|
|
227
|
+
return {
|
|
228
|
+
storage: { type: 'local', local: { basePath } },
|
|
229
|
+
language: 'ko',
|
|
230
|
+
periods: { daily: true, weekly: true, monthly: true, quarterly: true, yearly: false },
|
|
231
|
+
profile: { company: '', role: '', team: '', context: '' },
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** @param {string} owner @param {string} repo @param {string} token @returns {Config} */
|
|
236
|
+
export function createDefaultGitHubConfig(owner, repo, token) {
|
|
237
|
+
return {
|
|
238
|
+
storage: { type: 'github', github: { owner, repo, token, basePath: 'daily-review' } },
|
|
239
|
+
language: 'ko',
|
|
240
|
+
periods: { daily: true, weekly: true, monthly: true, quarterly: true, yearly: false },
|
|
241
|
+
profile: { company: '', role: '', team: '', context: '' },
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @param {Config} config
|
|
247
|
+
* @returns {Promise<StorageAdapter>}
|
|
248
|
+
*/
|
|
249
|
+
export async function createStorageAdapter(config) {
|
|
250
|
+
if (config.storage.type === 'local') {
|
|
251
|
+
return new LocalStorageAdapter(config.storage.local.basePath);
|
|
252
|
+
}
|
|
253
|
+
if (config.storage.type === 'github') {
|
|
254
|
+
const { GitHubStorageAdapter } = await import('./github-storage.mjs');
|
|
255
|
+
const g = config.storage.github;
|
|
256
|
+
return new GitHubStorageAdapter(g.owner, g.repo, g.token, g.basePath);
|
|
257
|
+
}
|
|
258
|
+
throw new Error(`Unknown storage type: ${config.storage.type}`);
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
- [ ] **Step 3: Create lib/periods.mjs**
|
|
263
|
+
|
|
264
|
+
Convert `src/core/periods.ts` to `.mjs` with JSDoc. Remove TypeScript syntax, add JSDoc annotations. Same logic exactly.
|
|
265
|
+
|
|
266
|
+
```javascript
|
|
267
|
+
// @ts-check
|
|
268
|
+
|
|
269
|
+
/** @param {Date} date @returns {number} */
|
|
270
|
+
export function getISOWeek(date) {
|
|
271
|
+
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
272
|
+
const dayNum = d.getUTCDay() || 7;
|
|
273
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
274
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
275
|
+
return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** @param {Date} date @returns {number} */
|
|
279
|
+
export function getISOWeekYear(date) {
|
|
280
|
+
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
281
|
+
const dayNum = d.getUTCDay() || 7;
|
|
282
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
283
|
+
return d.getUTCFullYear();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** @param {Date} date @returns {number} */
|
|
287
|
+
export function getQuarter(date) {
|
|
288
|
+
return Math.ceil((date.getMonth() + 1) / 3);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** @param {Date} date @returns {string} */
|
|
292
|
+
export function formatDate(date) {
|
|
293
|
+
const y = date.getFullYear();
|
|
294
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
295
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
296
|
+
return `${y}-${m}-${d}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** @param {Date} date @returns {string} */
|
|
300
|
+
export function formatWeek(date) {
|
|
301
|
+
return `${getISOWeekYear(date)}-W${String(getISOWeek(date)).padStart(2, '0')}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** @param {Date} date @returns {string} */
|
|
305
|
+
export function formatMonth(date) {
|
|
306
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** @param {Date} date @returns {string} */
|
|
310
|
+
export function formatQuarter(date) {
|
|
311
|
+
return `${date.getFullYear()}-Q${getQuarter(date)}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** @param {Date} date @returns {string} */
|
|
315
|
+
export function formatYear(date) {
|
|
316
|
+
return `${date.getFullYear()}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* @param {Date} today
|
|
321
|
+
* @param {Date | null} lastRun
|
|
322
|
+
* @returns {{ needsWeekly: boolean, needsMonthly: boolean, needsQuarterly: boolean, needsYearly: boolean, previousWeek: string, previousMonth: string, previousQuarter: string, previousYear: string }}
|
|
323
|
+
*/
|
|
324
|
+
export function checkPeriodsNeeded(today, lastRun) {
|
|
325
|
+
if (!lastRun) {
|
|
326
|
+
return {
|
|
327
|
+
needsWeekly: false, needsMonthly: false, needsQuarterly: false, needsYearly: false,
|
|
328
|
+
previousWeek: '', previousMonth: '', previousQuarter: '', previousYear: '',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
const todayWeek = formatWeek(today);
|
|
332
|
+
const lastWeek = formatWeek(lastRun);
|
|
333
|
+
const todayMonth = formatMonth(today);
|
|
334
|
+
const lastMonth = formatMonth(lastRun);
|
|
335
|
+
const todayQuarter = formatQuarter(today);
|
|
336
|
+
const lastQuarter = formatQuarter(lastRun);
|
|
337
|
+
const todayYear = formatYear(today);
|
|
338
|
+
const lastYear = formatYear(lastRun);
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
needsWeekly: todayWeek !== lastWeek,
|
|
342
|
+
needsMonthly: todayMonth !== lastMonth,
|
|
343
|
+
needsQuarterly: todayQuarter !== lastQuarter,
|
|
344
|
+
needsYearly: todayYear !== lastYear,
|
|
345
|
+
previousWeek: lastWeek, previousMonth: lastMonth,
|
|
346
|
+
previousQuarter: lastQuarter, previousYear: lastYear,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
- [ ] **Step 4: Create lib/storage.mjs (LocalStorageAdapter)**
|
|
352
|
+
|
|
353
|
+
```javascript
|
|
354
|
+
// @ts-check
|
|
355
|
+
/** @typedef {import('./types.d.ts').StorageAdapter} StorageAdapter */
|
|
356
|
+
|
|
357
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
358
|
+
import { dirname, join } from 'path';
|
|
359
|
+
|
|
360
|
+
/** @implements {StorageAdapter} */
|
|
361
|
+
export class LocalStorageAdapter {
|
|
362
|
+
/** @param {string} basePath */
|
|
363
|
+
constructor(basePath) {
|
|
364
|
+
/** @private */
|
|
365
|
+
this.basePath = basePath;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** @private @param {string} path @returns {string} */
|
|
369
|
+
resolve(path) {
|
|
370
|
+
return join(this.basePath, path);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** @param {string} path @returns {Promise<string | null>} */
|
|
374
|
+
async read(path) {
|
|
375
|
+
const full = this.resolve(path);
|
|
376
|
+
if (!existsSync(full)) return null;
|
|
377
|
+
return readFileSync(full, 'utf-8');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** @param {string} path @param {string} content @returns {Promise<void>} */
|
|
381
|
+
async write(path, content) {
|
|
382
|
+
const full = this.resolve(path);
|
|
383
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
384
|
+
writeFileSync(full, content, 'utf-8');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** @param {string} path @param {string} content @returns {Promise<void>} */
|
|
388
|
+
async append(path, content) {
|
|
389
|
+
const full = this.resolve(path);
|
|
390
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
391
|
+
appendFileSync(full, content, 'utf-8');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** @param {string} path @returns {Promise<boolean>} */
|
|
395
|
+
async exists(path) {
|
|
396
|
+
return existsSync(this.resolve(path));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** @param {string} dir @returns {Promise<string[]>} */
|
|
400
|
+
async list(dir) {
|
|
401
|
+
const full = this.resolve(dir);
|
|
402
|
+
if (!existsSync(full)) return [];
|
|
403
|
+
return readdirSync(full);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** @param {string} dir @returns {Promise<void>} */
|
|
407
|
+
async mkdir(dir) {
|
|
408
|
+
mkdirSync(this.resolve(dir), { recursive: true });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** @param {string} path @returns {Promise<boolean>} */
|
|
412
|
+
async isDirectory(path) {
|
|
413
|
+
try { return statSync(this.resolve(path)).isDirectory(); }
|
|
414
|
+
catch { return false; }
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
- [ ] **Step 5: Create lib/vault.mjs**
|
|
420
|
+
|
|
421
|
+
```javascript
|
|
422
|
+
// @ts-check
|
|
423
|
+
/** @typedef {import('./types.d.ts').StorageAdapter} StorageAdapter */
|
|
424
|
+
/** @typedef {import('./types.d.ts').Periods} Periods */
|
|
425
|
+
|
|
426
|
+
/** @param {string} sessionId @returns {string} */
|
|
427
|
+
export function getRawDir(sessionId) { return `.raw/${sessionId}`; }
|
|
428
|
+
|
|
429
|
+
/** @returns {string} */
|
|
430
|
+
export function getReviewsDir() { return '.reviews'; }
|
|
431
|
+
|
|
432
|
+
/** @param {string} date @returns {string} */
|
|
433
|
+
export function getDailyPath(date) { return `daily/${date}.md`; }
|
|
434
|
+
|
|
435
|
+
/** @param {string} week @returns {string} */
|
|
436
|
+
export function getWeeklyPath(week) { return `weekly/${week}.md`; }
|
|
437
|
+
|
|
438
|
+
/** @param {string} month @returns {string} */
|
|
439
|
+
export function getMonthlyPath(month) { return `monthly/${month}.md`; }
|
|
440
|
+
|
|
441
|
+
/** @param {string} quarter @returns {string} */
|
|
442
|
+
export function getQuarterlyPath(quarter) { return `quarterly/${quarter}.md`; }
|
|
443
|
+
|
|
444
|
+
/** @param {string} year @returns {string} */
|
|
445
|
+
export function getYearlyPath(year) { return `yearly/${year}.md`; }
|
|
446
|
+
|
|
447
|
+
/** @param {string} projectName @param {string} date @returns {string} */
|
|
448
|
+
export function getProjectDailyPath(projectName, date) { return `projects/${projectName}/${date}.md`; }
|
|
449
|
+
|
|
450
|
+
/** @param {string} projectName @returns {string} */
|
|
451
|
+
export function getProjectSummaryPath(projectName) { return `projects/${projectName}/summary.md`; }
|
|
452
|
+
|
|
453
|
+
/** @param {string} date @returns {string} */
|
|
454
|
+
export function getUncategorizedPath(date) { return `uncategorized/${date}.md`; }
|
|
455
|
+
|
|
456
|
+
/** @param {StorageAdapter} storage @param {Periods} periods @returns {Promise<void>} */
|
|
457
|
+
export async function ensureVaultDirectories(storage, periods) {
|
|
458
|
+
const dirs = ['daily', 'projects', 'uncategorized', '.raw', '.reviews'];
|
|
459
|
+
if (periods.weekly) dirs.push('weekly');
|
|
460
|
+
if (periods.monthly) dirs.push('monthly');
|
|
461
|
+
if (periods.quarterly) dirs.push('quarterly');
|
|
462
|
+
if (periods.yearly) dirs.push('yearly');
|
|
463
|
+
for (const dir of dirs) { await storage.mkdir(dir); }
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
- [ ] **Step 6: Create lib/raw-logger.mjs**
|
|
468
|
+
|
|
469
|
+
```javascript
|
|
470
|
+
// @ts-check
|
|
471
|
+
/** @typedef {import('./types.d.ts').StorageAdapter} StorageAdapter */
|
|
472
|
+
/** @typedef {import('./types.d.ts').HookInput} HookInput */
|
|
473
|
+
|
|
474
|
+
/** @param {string} raw @returns {HookInput} */
|
|
475
|
+
export function parseHookInput(raw) {
|
|
476
|
+
const parsed = JSON.parse(raw);
|
|
477
|
+
if (!parsed || typeof parsed !== 'object') throw new Error('Invalid hook input: expected object');
|
|
478
|
+
if (typeof parsed.session_id !== 'string' || !parsed.session_id) throw new Error('Invalid hook input: missing session_id');
|
|
479
|
+
return /** @type {HookInput} */ (parsed);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** @param {StorageAdapter} storage @param {string} sessionDir @param {string} date @param {HookInput} entry @returns {Promise<void>} */
|
|
483
|
+
export async function appendRawLog(storage, sessionDir, date, entry) {
|
|
484
|
+
await storage.mkdir(sessionDir);
|
|
485
|
+
const logPath = `${sessionDir}/${date}.jsonl`;
|
|
486
|
+
const record = { ...entry, timestamp: new Date().toISOString() };
|
|
487
|
+
await storage.append(logPath, JSON.stringify(record) + '\n');
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
- [ ] **Step 7: Create lib/merge.mjs**
|
|
492
|
+
|
|
493
|
+
```javascript
|
|
494
|
+
// @ts-check
|
|
495
|
+
/** @typedef {import('./types.d.ts').StorageAdapter} StorageAdapter */
|
|
496
|
+
|
|
497
|
+
/** @param {StorageAdapter} storage @param {string} rawDir @returns {Promise<string[]>} */
|
|
498
|
+
export async function findUnprocessedSessions(storage, rawDir) {
|
|
499
|
+
if (!(await storage.exists(rawDir))) return [];
|
|
500
|
+
const entries = await storage.list(rawDir);
|
|
501
|
+
const results = [];
|
|
502
|
+
for (const entry of entries) {
|
|
503
|
+
const entryPath = `${rawDir}/${entry}`;
|
|
504
|
+
if (!(await storage.isDirectory(entryPath))) continue;
|
|
505
|
+
if (await storage.exists(`${entryPath}/.completed`)) continue;
|
|
506
|
+
results.push(entry);
|
|
507
|
+
}
|
|
508
|
+
return results;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** @param {StorageAdapter} storage @param {string} reviewsDir @returns {Promise<string[]>} */
|
|
512
|
+
export async function findPendingReviews(storage, reviewsDir) {
|
|
513
|
+
if (!(await storage.exists(reviewsDir))) return [];
|
|
514
|
+
const entries = await storage.list(reviewsDir);
|
|
515
|
+
return entries.filter((f) => f.endsWith('.md'));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** @param {StorageAdapter} storage @param {string} sessionDir @returns {Promise<void>} */
|
|
519
|
+
export async function markSessionCompleted(storage, sessionDir) {
|
|
520
|
+
await storage.write(`${sessionDir}/.completed`, new Date().toISOString());
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** @param {StorageAdapter} storage @param {string} sessionDir @returns {Promise<boolean>} */
|
|
524
|
+
export async function isSessionCompleted(storage, sessionDir) {
|
|
525
|
+
return storage.exists(`${sessionDir}/.completed`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** @param {StorageAdapter} storage @param {string[]} reviewPaths @param {string} dailyPath @returns {Promise<void>} */
|
|
529
|
+
export async function mergeReviewsIntoDaily(storage, reviewPaths, dailyPath) {
|
|
530
|
+
const reviewContents = [];
|
|
531
|
+
for (const p of reviewPaths) {
|
|
532
|
+
const content = await storage.read(p);
|
|
533
|
+
if (content && content.trim().length > 0) reviewContents.push(content.trim());
|
|
534
|
+
}
|
|
535
|
+
if (reviewContents.length === 0) {
|
|
536
|
+
if (!(await storage.exists(dailyPath))) await storage.write(dailyPath, '');
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const existing = await storage.read(dailyPath);
|
|
540
|
+
const merged = existing
|
|
541
|
+
? existing.trimEnd() + '\n\n' + reviewContents.join('\n\n') + '\n'
|
|
542
|
+
: reviewContents.join('\n\n') + '\n';
|
|
543
|
+
await storage.write(dailyPath, merged);
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
- [ ] **Step 8: Verify lib/ modules load**
|
|
548
|
+
|
|
549
|
+
Run: `node -e "import('./lib/config.mjs').then(m => console.log('OK:', Object.keys(m)))"`
|
|
550
|
+
Expected: `OK: [ 'getConfigPath', 'loadConfig', 'saveConfig', ... ]`
|
|
551
|
+
|
|
552
|
+
- [ ] **Step 9: Commit**
|
|
553
|
+
|
|
554
|
+
```bash
|
|
555
|
+
git add lib/
|
|
556
|
+
git commit -m "feat: add lib/ with .mjs + JSDoc modules (no build required)"
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
### Task 2: Create GitHub modules (auth + storage)
|
|
562
|
+
|
|
563
|
+
**Files:**
|
|
564
|
+
- Create: `lib/github-auth.mjs`
|
|
565
|
+
- Create: `lib/github-storage.mjs`
|
|
566
|
+
|
|
567
|
+
- [ ] **Step 1: Create lib/github-auth.mjs**
|
|
568
|
+
|
|
569
|
+
```javascript
|
|
570
|
+
// @ts-check
|
|
571
|
+
/** @typedef {import('./types.d.ts').DeviceCodeResponse} DeviceCodeResponse */
|
|
572
|
+
|
|
573
|
+
const GITHUB_CLIENT_ID = 'Ov23lijFU2NkxD93Q2f2';
|
|
574
|
+
|
|
575
|
+
/** @returns {Promise<DeviceCodeResponse>} */
|
|
576
|
+
export async function requestDeviceCode() {
|
|
577
|
+
const res = await fetch('https://github.com/login/device/code', {
|
|
578
|
+
method: 'POST',
|
|
579
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
580
|
+
body: JSON.stringify({ client_id: GITHUB_CLIENT_ID, scope: 'repo' }),
|
|
581
|
+
});
|
|
582
|
+
if (!res.ok) throw new Error(`GitHub device code request failed: ${res.status}`);
|
|
583
|
+
return /** @type {Promise<DeviceCodeResponse>} */ (res.json());
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** @param {number} ms @returns {Promise<void>} */
|
|
587
|
+
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
588
|
+
|
|
589
|
+
/** @param {DeviceCodeResponse} deviceCode @param {number} [maxAttempts=180] @returns {Promise<string>} */
|
|
590
|
+
export async function pollForToken(deviceCode, maxAttempts = 180) {
|
|
591
|
+
let interval = deviceCode.interval * 1000;
|
|
592
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
593
|
+
if (interval > 0) await sleep(interval);
|
|
594
|
+
const res = await fetch('https://github.com/login/oauth/access_token', {
|
|
595
|
+
method: 'POST',
|
|
596
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
597
|
+
body: JSON.stringify({
|
|
598
|
+
client_id: GITHUB_CLIENT_ID,
|
|
599
|
+
device_code: deviceCode.device_code,
|
|
600
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
601
|
+
}),
|
|
602
|
+
});
|
|
603
|
+
/** @type {Record<string, unknown>} */
|
|
604
|
+
let data;
|
|
605
|
+
try { data = /** @type {Record<string, unknown>} */ (await res.json()); }
|
|
606
|
+
catch { continue; }
|
|
607
|
+
if (data.access_token) return /** @type {string} */ (data.access_token);
|
|
608
|
+
if (data.error === 'slow_down') { interval += 5000; continue; }
|
|
609
|
+
if (data.error === 'authorization_pending') continue;
|
|
610
|
+
throw new Error(`GitHub auth error: ${data.error}`);
|
|
611
|
+
}
|
|
612
|
+
throw new Error('GitHub auth timed out waiting for authorization');
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
- [ ] **Step 2: Create lib/github-storage.mjs**
|
|
617
|
+
|
|
618
|
+
```javascript
|
|
619
|
+
// @ts-check
|
|
620
|
+
/** @typedef {import('./types.d.ts').StorageAdapter} StorageAdapter */
|
|
621
|
+
|
|
622
|
+
/** @implements {StorageAdapter} */
|
|
623
|
+
export class GitHubStorageAdapter {
|
|
624
|
+
/** @param {string} owner @param {string} repo @param {string} token @param {string} basePath */
|
|
625
|
+
constructor(owner, repo, token, basePath) {
|
|
626
|
+
/** @private */ this.baseUrl = `https://api.github.com/repos/${owner}/${repo}/contents`;
|
|
627
|
+
/** @private */ this.basePath = basePath;
|
|
628
|
+
/** @private */ this.headers = {
|
|
629
|
+
Authorization: `Bearer ${token}`,
|
|
630
|
+
Accept: 'application/vnd.github.v3+json',
|
|
631
|
+
'Content-Type': 'application/json',
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** @private @param {string} path @returns {string} */
|
|
636
|
+
getUrl(path) { return `${this.baseUrl}/${this.basePath}/${path}`; }
|
|
637
|
+
|
|
638
|
+
/** @private @param {string} path @returns {Promise<string | null>} */
|
|
639
|
+
async getSha(path) {
|
|
640
|
+
const res = await fetch(this.getUrl(path), { method: 'GET', headers: this.headers });
|
|
641
|
+
if (res.status === 404) return null;
|
|
642
|
+
const data = /** @type {Record<string, unknown>} */ (await res.json());
|
|
643
|
+
return /** @type {string | null} */ (data.sha || null);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** @param {string} path @returns {Promise<string | null>} */
|
|
647
|
+
async read(path) {
|
|
648
|
+
const res = await fetch(this.getUrl(path), { method: 'GET', headers: this.headers });
|
|
649
|
+
if (res.status === 404) return null;
|
|
650
|
+
const data = /** @type {Record<string, unknown>} */ (await res.json());
|
|
651
|
+
return Buffer.from(/** @type {string} */ (data.content), 'base64').toString('utf-8');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** @param {string} path @param {string} content @returns {Promise<void>} */
|
|
655
|
+
async write(path, content) {
|
|
656
|
+
const sha = await this.getSha(path);
|
|
657
|
+
/** @type {Record<string, unknown>} */
|
|
658
|
+
const body = { message: `update ${path}`, content: Buffer.from(content).toString('base64') };
|
|
659
|
+
if (sha) body.sha = sha;
|
|
660
|
+
const res = await fetch(this.getUrl(path), { method: 'PUT', headers: this.headers, body: JSON.stringify(body) });
|
|
661
|
+
if (!res.ok && res.status === 409) {
|
|
662
|
+
const freshSha = await this.getSha(path);
|
|
663
|
+
if (freshSha) body.sha = freshSha;
|
|
664
|
+
await fetch(this.getUrl(path), { method: 'PUT', headers: this.headers, body: JSON.stringify(body) });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** @param {string} path @param {string} content @returns {Promise<void>} */
|
|
669
|
+
async append(path, content) {
|
|
670
|
+
const existing = await this.read(path);
|
|
671
|
+
await this.write(path, existing ? existing + content : content);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** @param {string} path @returns {Promise<boolean>} */
|
|
675
|
+
async exists(path) {
|
|
676
|
+
const res = await fetch(this.getUrl(path), { method: 'GET', headers: this.headers });
|
|
677
|
+
return res.status !== 404;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** @param {string} dir @returns {Promise<string[]>} */
|
|
681
|
+
async list(dir) {
|
|
682
|
+
const res = await fetch(this.getUrl(dir), { method: 'GET', headers: this.headers });
|
|
683
|
+
if (res.status === 404) return [];
|
|
684
|
+
const data = await res.json();
|
|
685
|
+
if (!Array.isArray(data)) return [];
|
|
686
|
+
return data.map((/** @type {{ name: string }} */ entry) => entry.name);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** @param {string} _dir @returns {Promise<void>} */
|
|
690
|
+
async mkdir(_dir) { /* GitHub creates directories implicitly */ }
|
|
691
|
+
|
|
692
|
+
/** @param {string} path @returns {Promise<boolean>} */
|
|
693
|
+
async isDirectory(path) {
|
|
694
|
+
const res = await fetch(this.getUrl(path), { method: 'GET', headers: this.headers });
|
|
695
|
+
if (res.status === 404) return false;
|
|
696
|
+
const data = await res.json();
|
|
697
|
+
return Array.isArray(data);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
- [ ] **Step 3: Verify imports work**
|
|
703
|
+
|
|
704
|
+
Run: `node -e "import('./lib/github-auth.mjs').then(m => console.log('OK:', Object.keys(m)))"`
|
|
705
|
+
Expected: `OK: [ 'requestDeviceCode', 'pollForToken' ]`
|
|
706
|
+
|
|
707
|
+
- [ ] **Step 4: Commit**
|
|
708
|
+
|
|
709
|
+
```bash
|
|
710
|
+
git add lib/github-auth.mjs lib/github-storage.mjs
|
|
711
|
+
git commit -m "feat: add GitHub auth and storage modules as .mjs"
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
### Task 3: Create hook scripts + storage CLI
|
|
717
|
+
|
|
718
|
+
**Files:**
|
|
719
|
+
- Create: `hooks/on-stop.mjs`
|
|
720
|
+
- Create: `hooks/session-start-check`
|
|
721
|
+
- Create: `hooks/run-hook.cmd`
|
|
722
|
+
- Create: `lib/storage-cli.mjs`
|
|
723
|
+
|
|
724
|
+
- [ ] **Step 1: Create hooks/on-stop.mjs**
|
|
725
|
+
|
|
726
|
+
```javascript
|
|
727
|
+
#!/usr/bin/env node
|
|
728
|
+
// @ts-check
|
|
729
|
+
import { loadConfig, createStorageAdapter } from '../lib/config.mjs';
|
|
730
|
+
import { parseHookInput, appendRawLog } from '../lib/raw-logger.mjs';
|
|
731
|
+
import { getRawDir } from '../lib/vault.mjs';
|
|
732
|
+
import { formatDate } from '../lib/periods.mjs';
|
|
733
|
+
|
|
734
|
+
async function main() {
|
|
735
|
+
try {
|
|
736
|
+
const config = loadConfig();
|
|
737
|
+
if (!config) return;
|
|
738
|
+
const storage = await createStorageAdapter(config);
|
|
739
|
+
let data = '';
|
|
740
|
+
process.stdin.setEncoding('utf-8');
|
|
741
|
+
for await (const chunk of process.stdin) { data += chunk; }
|
|
742
|
+
const input = parseHookInput(data);
|
|
743
|
+
const sessionDir = getRawDir(input.session_id);
|
|
744
|
+
const date = formatDate(new Date());
|
|
745
|
+
await appendRawLog(storage, sessionDir, date, input);
|
|
746
|
+
} catch {
|
|
747
|
+
// async hook — fail silently
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
main();
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
- [ ] **Step 2: Create hooks/session-start-check**
|
|
754
|
+
|
|
755
|
+
```bash
|
|
756
|
+
#!/usr/bin/env bash
|
|
757
|
+
set -euo pipefail
|
|
758
|
+
|
|
759
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
760
|
+
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
761
|
+
|
|
762
|
+
result=$(node -e "
|
|
763
|
+
import { loadConfig } from '${PLUGIN_ROOT}/lib/config.mjs';
|
|
764
|
+
try {
|
|
765
|
+
const config = loadConfig();
|
|
766
|
+
if (!config) process.stdout.write('NEEDS_SETUP');
|
|
767
|
+
} catch {
|
|
768
|
+
process.stdout.write('NEEDS_SETUP');
|
|
769
|
+
}
|
|
770
|
+
" 2>/dev/null || echo "NEEDS_SETUP")
|
|
771
|
+
|
|
772
|
+
if [ "$result" = "NEEDS_SETUP" ]; then
|
|
773
|
+
msg='<important-reminder>IN YOUR FIRST REPLY YOU MUST TELL THE USER: daily-review 플러그인이 아직 설정되지 않았습니다. /daily-review-setup 을 실행해주세요.</important-reminder>'
|
|
774
|
+
|
|
775
|
+
if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
|
|
776
|
+
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\n' "$msg"
|
|
777
|
+
else
|
|
778
|
+
printf '{"additional_context":"%s"}\n' "$msg"
|
|
779
|
+
fi
|
|
780
|
+
fi
|
|
781
|
+
|
|
782
|
+
exit 0
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
- [ ] **Step 3: Create hooks/run-hook.cmd**
|
|
786
|
+
|
|
787
|
+
Copy the superpowers polyglot wrapper exactly:
|
|
788
|
+
|
|
789
|
+
```cmd
|
|
790
|
+
: << 'CMDBLOCK'
|
|
791
|
+
@echo off
|
|
792
|
+
if "%~1"=="" (
|
|
793
|
+
echo run-hook.cmd: missing script name >&2
|
|
794
|
+
exit /b 1
|
|
795
|
+
)
|
|
796
|
+
set "HOOK_DIR=%~dp0"
|
|
797
|
+
if exist "C:\Program Files\Git\bin\bash.exe" (
|
|
798
|
+
"C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
|
|
799
|
+
exit /b %ERRORLEVEL%
|
|
800
|
+
)
|
|
801
|
+
if exist "C:\Program Files (x86)\Git\bin\bash.exe" (
|
|
802
|
+
"C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
|
|
803
|
+
exit /b %ERRORLEVEL%
|
|
804
|
+
)
|
|
805
|
+
where bash >nul 2>nul
|
|
806
|
+
if %ERRORLEVEL% equ 0 (
|
|
807
|
+
bash "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
|
|
808
|
+
exit /b %ERRORLEVEL%
|
|
809
|
+
)
|
|
810
|
+
exit /b 0
|
|
811
|
+
CMDBLOCK
|
|
812
|
+
|
|
813
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
814
|
+
SCRIPT_NAME="$1"
|
|
815
|
+
shift
|
|
816
|
+
exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
- [ ] **Step 4: Create lib/storage-cli.mjs**
|
|
820
|
+
|
|
821
|
+
```javascript
|
|
822
|
+
#!/usr/bin/env node
|
|
823
|
+
// @ts-check
|
|
824
|
+
import { loadConfig, createStorageAdapter } from './config.mjs';
|
|
825
|
+
|
|
826
|
+
async function main() {
|
|
827
|
+
const [command, ...args] = process.argv.slice(2);
|
|
828
|
+
const config = loadConfig();
|
|
829
|
+
if (!config) { process.stderr.write('config not found\n'); process.exit(1); }
|
|
830
|
+
|
|
831
|
+
const storage = await createStorageAdapter(config);
|
|
832
|
+
|
|
833
|
+
switch (command) {
|
|
834
|
+
case 'read': {
|
|
835
|
+
const content = await storage.read(args[0]);
|
|
836
|
+
if (content !== null) process.stdout.write(content);
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
case 'write': {
|
|
840
|
+
let data = '';
|
|
841
|
+
process.stdin.setEncoding('utf-8');
|
|
842
|
+
for await (const chunk of process.stdin) { data += chunk; }
|
|
843
|
+
await storage.write(args[0], data);
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
case 'append': {
|
|
847
|
+
let data = '';
|
|
848
|
+
process.stdin.setEncoding('utf-8');
|
|
849
|
+
for await (const chunk of process.stdin) { data += chunk; }
|
|
850
|
+
await storage.append(args[0], data);
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
case 'list': {
|
|
854
|
+
const entries = await storage.list(args[0]);
|
|
855
|
+
process.stdout.write(entries.join('\n') + '\n');
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
case 'exists': {
|
|
859
|
+
const exists = await storage.exists(args[0]);
|
|
860
|
+
process.stdout.write(exists ? 'true\n' : 'false\n');
|
|
861
|
+
process.exit(exists ? 0 : 1);
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
default:
|
|
865
|
+
process.stderr.write(`Unknown command: ${command}\nUsage: storage-cli <read|write|append|list|exists> <path>\n`);
|
|
866
|
+
process.exit(1);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
main().catch((err) => { process.stderr.write(`Error: ${err.message}\n`); process.exit(1); });
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
- [ ] **Step 5: Commit**
|
|
873
|
+
|
|
874
|
+
```bash
|
|
875
|
+
git add hooks/on-stop.mjs hooks/session-start-check hooks/run-hook.cmd lib/storage-cli.mjs
|
|
876
|
+
git commit -m "feat: add hook scripts and storage CLI as direct-run .mjs/bash"
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
---
|
|
880
|
+
|
|
881
|
+
### Task 4: Update hooks.json + prompts
|
|
882
|
+
|
|
883
|
+
**Files:**
|
|
884
|
+
- Modify: `hooks/hooks.json`
|
|
885
|
+
- Modify: `prompts/session-end.md`
|
|
886
|
+
- Modify: `prompts/session-start.md`
|
|
887
|
+
|
|
888
|
+
- [ ] **Step 1: Update hooks.json**
|
|
889
|
+
|
|
890
|
+
```json
|
|
891
|
+
{
|
|
892
|
+
"hooks": {
|
|
893
|
+
"Stop": [
|
|
894
|
+
{
|
|
895
|
+
"hooks": [
|
|
896
|
+
{
|
|
897
|
+
"type": "command",
|
|
898
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/on-stop.mjs\"",
|
|
899
|
+
"async": true,
|
|
900
|
+
"timeout": 10
|
|
901
|
+
}
|
|
902
|
+
]
|
|
903
|
+
}
|
|
904
|
+
],
|
|
905
|
+
"SessionEnd": [
|
|
906
|
+
{
|
|
907
|
+
"hooks": [
|
|
908
|
+
{
|
|
909
|
+
"type": "agent",
|
|
910
|
+
"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}",
|
|
911
|
+
"timeout": 120
|
|
912
|
+
}
|
|
913
|
+
]
|
|
914
|
+
}
|
|
915
|
+
],
|
|
916
|
+
"SessionStart": [
|
|
917
|
+
{
|
|
918
|
+
"matcher": "startup",
|
|
919
|
+
"hooks": [
|
|
920
|
+
{
|
|
921
|
+
"type": "command",
|
|
922
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start-check",
|
|
923
|
+
"timeout": 5
|
|
924
|
+
}
|
|
925
|
+
]
|
|
926
|
+
}
|
|
927
|
+
]
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
- [ ] **Step 2: Update prompts to reference lib/storage-cli.mjs**
|
|
933
|
+
|
|
934
|
+
In `prompts/session-end.md` and `prompts/session-start.md`, replace all references to:
|
|
935
|
+
- `${CLAUDE_PLUGIN_ROOT}/dist/storage-cli.js` → `${CLAUDE_PLUGIN_ROOT}/lib/storage-cli.mjs`
|
|
936
|
+
|
|
937
|
+
- [ ] **Step 3: Commit**
|
|
938
|
+
|
|
939
|
+
```bash
|
|
940
|
+
git add hooks/hooks.json prompts/
|
|
941
|
+
git commit -m "feat: update hooks.json and prompts for no-build structure"
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
---
|
|
945
|
+
|
|
946
|
+
### Task 5: Clean up old files + update package.json
|
|
947
|
+
|
|
948
|
+
**Files:**
|
|
949
|
+
- Delete: `src/` (entire directory)
|
|
950
|
+
- Delete: `dist/` (entire directory)
|
|
951
|
+
- Delete: `tests/` (entire directory)
|
|
952
|
+
- Delete: `tsconfig.json`
|
|
953
|
+
- Delete: `tsup.config.ts`
|
|
954
|
+
- Delete: `vitest.config.ts`
|
|
955
|
+
- Modify: `package.json`
|
|
956
|
+
- Modify: `.gitignore`
|
|
957
|
+
|
|
958
|
+
- [ ] **Step 1: Simplify package.json**
|
|
959
|
+
|
|
960
|
+
```json
|
|
961
|
+
{
|
|
962
|
+
"name": "@giwonn/claude-daily-review",
|
|
963
|
+
"version": "0.3.0",
|
|
964
|
+
"type": "module",
|
|
965
|
+
"description": "Claude Code plugin that auto-captures conversations for daily review and career documentation",
|
|
966
|
+
"repository": {
|
|
967
|
+
"type": "git",
|
|
968
|
+
"url": "https://github.com/giwonn/claude-daily-review"
|
|
969
|
+
},
|
|
970
|
+
"license": "MIT"
|
|
971
|
+
}
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
- [ ] **Step 2: Update .gitignore**
|
|
975
|
+
|
|
976
|
+
```
|
|
977
|
+
node_modules/
|
|
978
|
+
.idea/
|
|
979
|
+
.claude/
|
|
980
|
+
*.tgz
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
(Remove `dist/` line — no longer exists)
|
|
984
|
+
|
|
985
|
+
- [ ] **Step 3: Delete old files**
|
|
986
|
+
|
|
987
|
+
```bash
|
|
988
|
+
rm -rf src/ dist/ tests/ tsconfig.json tsup.config.ts vitest.config.ts package-lock.json node_modules/
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
- [ ] **Step 4: Verify the plugin works locally**
|
|
992
|
+
|
|
993
|
+
```bash
|
|
994
|
+
CLAUDE_PLUGIN_DATA=/tmp/cdr-test node hooks/on-stop.mjs <<< '{"session_id":"test","transcript_path":"/tmp/t","cwd":"/tmp","hook_event_name":"Stop"}'
|
|
995
|
+
```
|
|
996
|
+
Expected: No error, raw log created at `/tmp/cdr-test/...`
|
|
997
|
+
|
|
998
|
+
Actually, since on-stop reads config first:
|
|
999
|
+
```bash
|
|
1000
|
+
mkdir -p /tmp/cdr-test && echo '{"storage":{"type":"local","local":{"basePath":"/tmp/cdr-vault"}},"language":"ko","periods":{"daily":true,"weekly":false,"monthly":false,"quarterly":false,"yearly":false},"profile":{"company":"","role":"","team":"","context":""}}' > /tmp/cdr-test/config.json && CLAUDE_PLUGIN_DATA=/tmp/cdr-test node hooks/on-stop.mjs <<< '{"session_id":"test-sess","transcript_path":"/tmp/t","cwd":"/tmp","hook_event_name":"Stop"}'
|
|
1001
|
+
```
|
|
1002
|
+
Then verify: `ls /tmp/cdr-vault/.raw/test-sess/`
|
|
1003
|
+
Expected: A `.jsonl` file exists.
|
|
1004
|
+
|
|
1005
|
+
- [ ] **Step 5: Commit**
|
|
1006
|
+
|
|
1007
|
+
```bash
|
|
1008
|
+
git add -A
|
|
1009
|
+
git commit -m "refactor: remove TypeScript build system, use .mjs + JSDoc directly"
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
---
|
|
1013
|
+
|
|
1014
|
+
### Task 6: Update CI workflows + marketplace SHA
|
|
1015
|
+
|
|
1016
|
+
**Files:**
|
|
1017
|
+
- Modify: `.github/workflows/publish.yml`
|
|
1018
|
+
- Create: `.github/workflows/update-marketplace.yml`
|
|
1019
|
+
- Modify: `.claude-plugin/marketplace.json`
|
|
1020
|
+
|
|
1021
|
+
- [ ] **Step 1: Simplify publish.yml (no build step)**
|
|
1022
|
+
|
|
1023
|
+
```yaml
|
|
1024
|
+
name: Publish to npm
|
|
1025
|
+
|
|
1026
|
+
on:
|
|
1027
|
+
release:
|
|
1028
|
+
types: [published]
|
|
1029
|
+
|
|
1030
|
+
jobs:
|
|
1031
|
+
publish:
|
|
1032
|
+
runs-on: ubuntu-latest
|
|
1033
|
+
environment: npm
|
|
1034
|
+
permissions:
|
|
1035
|
+
contents: read
|
|
1036
|
+
id-token: write
|
|
1037
|
+
steps:
|
|
1038
|
+
- uses: actions/checkout@v4
|
|
1039
|
+
|
|
1040
|
+
- uses: actions/setup-node@v4
|
|
1041
|
+
with:
|
|
1042
|
+
node-version: "22"
|
|
1043
|
+
registry-url: "https://registry.npmjs.org"
|
|
1044
|
+
|
|
1045
|
+
- run: npm install -g npm@latest
|
|
1046
|
+
|
|
1047
|
+
- run: npm publish --access public --provenance
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
Note: No `npm ci`, `npm run build`, or `npm test` — there's nothing to build or test.
|
|
1051
|
+
|
|
1052
|
+
- [ ] **Step 2: Create update-marketplace.yml**
|
|
1053
|
+
|
|
1054
|
+
```yaml
|
|
1055
|
+
name: Update Marketplace SHA
|
|
1056
|
+
|
|
1057
|
+
on:
|
|
1058
|
+
release:
|
|
1059
|
+
types: [published]
|
|
1060
|
+
|
|
1061
|
+
jobs:
|
|
1062
|
+
update-sha:
|
|
1063
|
+
runs-on: ubuntu-latest
|
|
1064
|
+
permissions:
|
|
1065
|
+
contents: write
|
|
1066
|
+
steps:
|
|
1067
|
+
- uses: actions/checkout@v4
|
|
1068
|
+
with:
|
|
1069
|
+
ref: master
|
|
1070
|
+
|
|
1071
|
+
- name: Update SHA in marketplace.json
|
|
1072
|
+
run: |
|
|
1073
|
+
SHA=$(git rev-parse HEAD)
|
|
1074
|
+
sed -i "s/\"sha\": \"[a-f0-9]*\"/\"sha\": \"$SHA\"/" .claude-plugin/marketplace.json
|
|
1075
|
+
cat .claude-plugin/marketplace.json
|
|
1076
|
+
|
|
1077
|
+
- name: Commit and push
|
|
1078
|
+
run: |
|
|
1079
|
+
git config user.name "github-actions[bot]"
|
|
1080
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
1081
|
+
git add .claude-plugin/marketplace.json
|
|
1082
|
+
git diff --cached --quiet && echo "No changes" || (git commit -m "chore: update marketplace SHA [skip ci]" && git push)
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
- [ ] **Step 3: Update marketplace.json version**
|
|
1086
|
+
|
|
1087
|
+
Update the `version` field to `0.3.0` and SHA to current HEAD.
|
|
1088
|
+
|
|
1089
|
+
- [ ] **Step 4: Commit**
|
|
1090
|
+
|
|
1091
|
+
```bash
|
|
1092
|
+
git add .github/ .claude-plugin/marketplace.json
|
|
1093
|
+
git commit -m "ci: simplify publish workflow, add marketplace SHA auto-update"
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
---
|
|
1097
|
+
|
|
1098
|
+
### Task 7: Final verification + release
|
|
1099
|
+
|
|
1100
|
+
- [ ] **Step 1: Verify all files are correct**
|
|
1101
|
+
|
|
1102
|
+
```bash
|
|
1103
|
+
ls lib/ hooks/ prompts/ skills/ .claude-plugin/
|
|
1104
|
+
```
|
|
1105
|
+
Expected: All .mjs, .md, hooks.json, run-hook.cmd, session-start-check present.
|
|
1106
|
+
|
|
1107
|
+
- [ ] **Step 2: Verify no build artifacts remain**
|
|
1108
|
+
|
|
1109
|
+
```bash
|
|
1110
|
+
test ! -d src && test ! -d dist && test ! -d tests && test ! -f tsconfig.json && echo "CLEAN"
|
|
1111
|
+
```
|
|
1112
|
+
Expected: `CLEAN`
|
|
1113
|
+
|
|
1114
|
+
- [ ] **Step 3: Test on-stop hook**
|
|
1115
|
+
|
|
1116
|
+
```bash
|
|
1117
|
+
mkdir -p /tmp/cdr-test && echo '{"storage":{"type":"local","local":{"basePath":"/tmp/cdr-vault"}},"language":"ko","periods":{"daily":true,"weekly":false,"monthly":false,"quarterly":false,"yearly":false},"profile":{"company":"","role":"","team":"","context":""}}' > /tmp/cdr-test/config.json && CLAUDE_PLUGIN_DATA=/tmp/cdr-test node hooks/on-stop.mjs <<< '{"session_id":"final-test","transcript_path":"/tmp/t","cwd":"/tmp","hook_event_name":"Stop"}' && ls /tmp/cdr-vault/.raw/final-test/
|
|
1118
|
+
```
|
|
1119
|
+
Expected: `.jsonl` file listed.
|
|
1120
|
+
|
|
1121
|
+
- [ ] **Step 4: Test session-start-check**
|
|
1122
|
+
|
|
1123
|
+
```bash
|
|
1124
|
+
CLAUDE_PLUGIN_ROOT=$(pwd) CLAUDE_PLUGIN_DATA=/tmp/nonexistent bash hooks/session-start-check
|
|
1125
|
+
```
|
|
1126
|
+
Expected: JSON with `additionalContext` containing setup message.
|
|
1127
|
+
|
|
1128
|
+
- [ ] **Step 5: Push and create release**
|
|
1129
|
+
|
|
1130
|
+
```bash
|
|
1131
|
+
git push
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
Then create release v0.3.0 to trigger both npm publish and marketplace SHA update.
|
|
1135
|
+
|
|
1136
|
+
- [ ] **Step 6: Verify plugin installation**
|
|
1137
|
+
|
|
1138
|
+
```bash
|
|
1139
|
+
claude plugin marketplace update giwonn-plugins
|
|
1140
|
+
claude plugin uninstall claude-daily-review
|
|
1141
|
+
claude plugin install claude-daily-review@giwonn-plugins
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
Start new session and verify setup message appears.
|
|
1145
|
+
|
|
1146
|
+
---
|
|
1147
|
+
|
|
1148
|
+
## Summary
|
|
1149
|
+
|
|
1150
|
+
| Task | Description | Files |
|
|
1151
|
+
|------|-------------|-------|
|
|
1152
|
+
| 1 | Core lib/ modules (.mjs + JSDoc) | 7 files created |
|
|
1153
|
+
| 2 | GitHub modules (auth + storage) | 2 files created |
|
|
1154
|
+
| 3 | Hook scripts + storage CLI | 4 files created |
|
|
1155
|
+
| 4 | hooks.json + prompts update | 3 files modified |
|
|
1156
|
+
| 5 | Delete old TS/build files | ~15 files deleted |
|
|
1157
|
+
| 6 | CI workflows + marketplace SHA | 3 files |
|
|
1158
|
+
| 7 | Final verification + release | - |
|