@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.
@@ -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 | - |