@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/hooks/hooks.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "hooks": [
6
6
  {
7
7
  "type": "command",
8
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/dist/on-stop.js\"",
8
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/on-stop.mjs\"",
9
9
  "async": true,
10
10
  "timeout": 10
11
11
  }
@@ -25,10 +25,11 @@
25
25
  ],
26
26
  "SessionStart": [
27
27
  {
28
+ "matcher": "startup",
28
29
  "hooks": [
29
30
  {
30
31
  "type": "command",
31
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/dist/on-session-start-check.js\"",
32
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start-check",
32
33
  "timeout": 5
33
34
  }
34
35
  ]
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ import { loadConfig, createStorageAdapter } from '../lib/config.mjs';
4
+ import { parseHookInput, appendRawLog } from '../lib/raw-logger.mjs';
5
+ import { getRawDir } from '../lib/vault.mjs';
6
+ import { formatDate } from '../lib/periods.mjs';
7
+
8
+ async function main() {
9
+ try {
10
+ const config = loadConfig();
11
+ if (!config) return;
12
+ const storage = await createStorageAdapter(config);
13
+ let data = '';
14
+ process.stdin.setEncoding('utf-8');
15
+ for await (const chunk of process.stdin) { data += chunk; }
16
+ const input = parseHookInput(data);
17
+ const sessionDir = getRawDir(input.session_id);
18
+ const date = formatDate(new Date());
19
+ await appendRawLog(storage, sessionDir, date, input);
20
+ } catch {
21
+ // async hook — fail silently
22
+ }
23
+ }
24
+ main();
@@ -0,0 +1,27 @@
1
+ : << 'CMDBLOCK'
2
+ @echo off
3
+ if "%~1"=="" (
4
+ echo run-hook.cmd: missing script name >&2
5
+ exit /b 1
6
+ )
7
+ set "HOOK_DIR=%~dp0"
8
+ if exist "C:\Program Files\Git\bin\bash.exe" (
9
+ "C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
10
+ exit /b %ERRORLEVEL%
11
+ )
12
+ if exist "C:\Program Files (x86)\Git\bin\bash.exe" (
13
+ "C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
14
+ exit /b %ERRORLEVEL%
15
+ )
16
+ where bash >nul 2>nul
17
+ if %ERRORLEVEL% equ 0 (
18
+ bash "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
19
+ exit /b %ERRORLEVEL%
20
+ )
21
+ exit /b 0
22
+ CMDBLOCK
23
+
24
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
25
+ SCRIPT_NAME="$1"
26
+ shift
27
+ exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
6
+
7
+ result=$(node -e "
8
+ import { loadConfig } from '${PLUGIN_ROOT}/lib/config.mjs';
9
+ try {
10
+ const config = loadConfig();
11
+ if (!config) process.stdout.write('NEEDS_SETUP');
12
+ } catch {
13
+ process.stdout.write('NEEDS_SETUP');
14
+ }
15
+ " 2>/dev/null || echo "NEEDS_SETUP")
16
+
17
+ if [ "$result" = "NEEDS_SETUP" ]; then
18
+ msg='<important-reminder>IN YOUR FIRST REPLY YOU MUST TELL THE USER: daily-review 플러그인이 아직 설정되지 않았습니다. /daily-review-setup 을 실행해주세요.</important-reminder>'
19
+
20
+ if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
21
+ printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\n' "$msg"
22
+ else
23
+ printf '{"additional_context":"%s"}\n' "$msg"
24
+ fi
25
+ fi
26
+
27
+ exit 0
package/lib/config.mjs ADDED
@@ -0,0 +1,122 @@
1
+ // @ts-check
2
+ /** @typedef {import('./types.d.ts').Config} Config */
3
+ /** @typedef {import('./types.d.ts').StorageAdapter} StorageAdapter */
4
+
5
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
6
+ import { dirname, join } from 'path';
7
+ import { LocalStorageAdapter } from './storage.mjs';
8
+
9
+ /** @returns {string} */
10
+ export function getConfigPath() {
11
+ const dataDir = process.env.CLAUDE_PLUGIN_DATA;
12
+ if (!dataDir) {
13
+ throw new Error('CLAUDE_PLUGIN_DATA environment variable is not set');
14
+ }
15
+ return join(dataDir, 'config.json');
16
+ }
17
+
18
+ /**
19
+ * @param {unknown} raw
20
+ * @returns {raw is { vaultPath: string; reviewFolder: string; language: string; periods: any; profile: any }}
21
+ */
22
+ function isOldConfig(raw) {
23
+ if (!raw || typeof raw !== 'object') return false;
24
+ return 'vaultPath' in raw && 'reviewFolder' in raw;
25
+ }
26
+
27
+ /**
28
+ * @param {{ vaultPath: string; reviewFolder: string; language: string; periods: any; profile: any }} old
29
+ * @returns {Config}
30
+ */
31
+ function migrateOldConfig(old) {
32
+ return {
33
+ storage: {
34
+ type: 'local',
35
+ local: { basePath: join(old.vaultPath, old.reviewFolder) },
36
+ },
37
+ language: old.language,
38
+ periods: old.periods,
39
+ profile: old.profile,
40
+ };
41
+ }
42
+
43
+ /** @returns {Config | null} */
44
+ export function loadConfig() {
45
+ const configPath = getConfigPath();
46
+ if (!existsSync(configPath)) return null;
47
+ const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
48
+ if (isOldConfig(raw)) {
49
+ const migrated = migrateOldConfig(raw);
50
+ saveConfig(migrated);
51
+ return migrated;
52
+ }
53
+ return /** @type {Config} */ (raw);
54
+ }
55
+
56
+ /** @param {Config} config */
57
+ export function saveConfig(config) {
58
+ const configPath = getConfigPath();
59
+ mkdirSync(dirname(configPath), { recursive: true });
60
+ writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
61
+ }
62
+
63
+ /**
64
+ * @param {unknown} config
65
+ * @returns {config is Config}
66
+ */
67
+ export function validateConfig(config) {
68
+ if (!config || typeof config !== 'object') return false;
69
+ const c = /** @type {Record<string, unknown>} */ (config);
70
+ if (!c.storage || typeof c.storage !== 'object') return false;
71
+ const s = /** @type {Record<string, unknown>} */ (c.storage);
72
+ if (s.type !== 'local' && s.type !== 'github') return false;
73
+ if (s.type === 'local') {
74
+ if (!s.local || typeof s.local !== 'object') return false;
75
+ const l = /** @type {Record<string, unknown>} */ (s.local);
76
+ if (typeof l.basePath !== 'string' || l.basePath === '') return false;
77
+ }
78
+ if (s.type === 'github') {
79
+ if (!s.github || typeof s.github !== 'object') return false;
80
+ const g = /** @type {Record<string, unknown>} */ (s.github);
81
+ if (typeof g.owner !== 'string' || !g.owner) return false;
82
+ if (typeof g.repo !== 'string' || !g.repo) return false;
83
+ if (typeof g.token !== 'string' || !g.token) return false;
84
+ }
85
+ return true;
86
+ }
87
+
88
+ /** @param {string} basePath @returns {Config} */
89
+ export function createDefaultLocalConfig(basePath) {
90
+ return {
91
+ storage: { type: 'local', local: { basePath } },
92
+ language: 'ko',
93
+ periods: { daily: true, weekly: true, monthly: true, quarterly: true, yearly: false },
94
+ profile: { company: '', role: '', team: '', context: '' },
95
+ };
96
+ }
97
+
98
+ /** @param {string} owner @param {string} repo @param {string} token @returns {Config} */
99
+ export function createDefaultGitHubConfig(owner, repo, token) {
100
+ return {
101
+ storage: { type: 'github', github: { owner, repo, token, basePath: 'daily-review' } },
102
+ language: 'ko',
103
+ periods: { daily: true, weekly: true, monthly: true, quarterly: true, yearly: false },
104
+ profile: { company: '', role: '', team: '', context: '' },
105
+ };
106
+ }
107
+
108
+ /**
109
+ * @param {Config} config
110
+ * @returns {Promise<StorageAdapter>}
111
+ */
112
+ export async function createStorageAdapter(config) {
113
+ if (config.storage.type === 'local') {
114
+ return new LocalStorageAdapter(config.storage.local.basePath);
115
+ }
116
+ if (config.storage.type === 'github') {
117
+ const { GitHubStorageAdapter } = await import('./github-storage.mjs');
118
+ const g = config.storage.github;
119
+ return new GitHubStorageAdapter(g.owner, g.repo, g.token, g.basePath);
120
+ }
121
+ throw new Error(`Unknown storage type: ${config.storage.type}`);
122
+ }
@@ -0,0 +1,44 @@
1
+ // @ts-check
2
+ /** @typedef {import('./types.d.ts').DeviceCodeResponse} DeviceCodeResponse */
3
+
4
+ const GITHUB_CLIENT_ID = 'Ov23lijFU2NkxD93Q2f2';
5
+
6
+ /** @returns {Promise<DeviceCodeResponse>} */
7
+ export async function requestDeviceCode() {
8
+ const res = await fetch('https://github.com/login/device/code', {
9
+ method: 'POST',
10
+ headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
11
+ body: JSON.stringify({ client_id: GITHUB_CLIENT_ID, scope: 'repo' }),
12
+ });
13
+ if (!res.ok) throw new Error(`GitHub device code request failed: ${res.status}`);
14
+ return /** @type {Promise<DeviceCodeResponse>} */ (res.json());
15
+ }
16
+
17
+ /** @param {number} ms @returns {Promise<void>} */
18
+ function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
19
+
20
+ /** @param {DeviceCodeResponse} deviceCode @param {number} [maxAttempts=180] @returns {Promise<string>} */
21
+ export async function pollForToken(deviceCode, maxAttempts = 180) {
22
+ let interval = deviceCode.interval * 1000;
23
+ for (let i = 0; i < maxAttempts; i++) {
24
+ if (interval > 0) await sleep(interval);
25
+ const res = await fetch('https://github.com/login/oauth/access_token', {
26
+ method: 'POST',
27
+ headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
28
+ body: JSON.stringify({
29
+ client_id: GITHUB_CLIENT_ID,
30
+ device_code: deviceCode.device_code,
31
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
32
+ }),
33
+ });
34
+ /** @type {Record<string, unknown>} */
35
+ let data;
36
+ try { data = /** @type {Record<string, unknown>} */ (await res.json()); }
37
+ catch { continue; }
38
+ if (data.access_token) return /** @type {string} */ (data.access_token);
39
+ if (data.error === 'slow_down') { interval += 5000; continue; }
40
+ if (data.error === 'authorization_pending') continue;
41
+ throw new Error(`GitHub auth error: ${data.error}`);
42
+ }
43
+ throw new Error('GitHub auth timed out waiting for authorization');
44
+ }
@@ -0,0 +1,81 @@
1
+ // @ts-check
2
+ /** @typedef {import('./types.d.ts').StorageAdapter} StorageAdapter */
3
+
4
+ /** @implements {StorageAdapter} */
5
+ export class GitHubStorageAdapter {
6
+ /** @param {string} owner @param {string} repo @param {string} token @param {string} basePath */
7
+ constructor(owner, repo, token, basePath) {
8
+ /** @private */ this.baseUrl = `https://api.github.com/repos/${owner}/${repo}/contents`;
9
+ /** @private */ this.basePath = basePath;
10
+ /** @private */ this.headers = {
11
+ Authorization: `Bearer ${token}`,
12
+ Accept: 'application/vnd.github.v3+json',
13
+ 'Content-Type': 'application/json',
14
+ };
15
+ }
16
+
17
+ /** @private @param {string} path @returns {string} */
18
+ getUrl(path) { return `${this.baseUrl}/${this.basePath}/${path}`; }
19
+
20
+ /** @private @param {string} path @returns {Promise<string | null>} */
21
+ async getSha(path) {
22
+ const res = await fetch(this.getUrl(path), { method: 'GET', headers: this.headers });
23
+ if (res.status === 404) return null;
24
+ const data = /** @type {Record<string, unknown>} */ (await res.json());
25
+ return /** @type {string | null} */ (data.sha || null);
26
+ }
27
+
28
+ /** @param {string} path @returns {Promise<string | null>} */
29
+ async read(path) {
30
+ const res = await fetch(this.getUrl(path), { method: 'GET', headers: this.headers });
31
+ if (res.status === 404) return null;
32
+ const data = /** @type {Record<string, unknown>} */ (await res.json());
33
+ return Buffer.from(/** @type {string} */ (data.content), 'base64').toString('utf-8');
34
+ }
35
+
36
+ /** @param {string} path @param {string} content @returns {Promise<void>} */
37
+ async write(path, content) {
38
+ const sha = await this.getSha(path);
39
+ /** @type {Record<string, unknown>} */
40
+ const body = { message: `update ${path}`, content: Buffer.from(content).toString('base64') };
41
+ if (sha) body.sha = sha;
42
+ const res = await fetch(this.getUrl(path), { method: 'PUT', headers: this.headers, body: JSON.stringify(body) });
43
+ if (!res.ok && res.status === 409) {
44
+ const freshSha = await this.getSha(path);
45
+ if (freshSha) body.sha = freshSha;
46
+ await fetch(this.getUrl(path), { method: 'PUT', headers: this.headers, body: JSON.stringify(body) });
47
+ }
48
+ }
49
+
50
+ /** @param {string} path @param {string} content @returns {Promise<void>} */
51
+ async append(path, content) {
52
+ const existing = await this.read(path);
53
+ await this.write(path, existing ? existing + content : content);
54
+ }
55
+
56
+ /** @param {string} path @returns {Promise<boolean>} */
57
+ async exists(path) {
58
+ const res = await fetch(this.getUrl(path), { method: 'GET', headers: this.headers });
59
+ return res.status !== 404;
60
+ }
61
+
62
+ /** @param {string} dir @returns {Promise<string[]>} */
63
+ async list(dir) {
64
+ const res = await fetch(this.getUrl(dir), { method: 'GET', headers: this.headers });
65
+ if (res.status === 404) return [];
66
+ const data = await res.json();
67
+ if (!Array.isArray(data)) return [];
68
+ return data.map((/** @type {{ name: string }} */ entry) => entry.name);
69
+ }
70
+
71
+ /** @param {string} _dir @returns {Promise<void>} */
72
+ async mkdir(_dir) { /* GitHub creates directories implicitly */ }
73
+
74
+ /** @param {string} path @returns {Promise<boolean>} */
75
+ async isDirectory(path) {
76
+ const res = await fetch(this.getUrl(path), { method: 'GET', headers: this.headers });
77
+ if (res.status === 404) return false;
78
+ const data = await res.json();
79
+ return Array.isArray(data);
80
+ }
81
+ }
package/lib/merge.mjs ADDED
@@ -0,0 +1,51 @@
1
+ // @ts-check
2
+ /** @typedef {import('./types.d.ts').StorageAdapter} StorageAdapter */
3
+
4
+ /** @param {StorageAdapter} storage @param {string} rawDir @returns {Promise<string[]>} */
5
+ export async function findUnprocessedSessions(storage, rawDir) {
6
+ if (!(await storage.exists(rawDir))) return [];
7
+ const entries = await storage.list(rawDir);
8
+ const results = [];
9
+ for (const entry of entries) {
10
+ const entryPath = `${rawDir}/${entry}`;
11
+ if (!(await storage.isDirectory(entryPath))) continue;
12
+ if (await storage.exists(`${entryPath}/.completed`)) continue;
13
+ results.push(entry);
14
+ }
15
+ return results;
16
+ }
17
+
18
+ /** @param {StorageAdapter} storage @param {string} reviewsDir @returns {Promise<string[]>} */
19
+ export async function findPendingReviews(storage, reviewsDir) {
20
+ if (!(await storage.exists(reviewsDir))) return [];
21
+ const entries = await storage.list(reviewsDir);
22
+ return entries.filter((f) => f.endsWith('.md'));
23
+ }
24
+
25
+ /** @param {StorageAdapter} storage @param {string} sessionDir @returns {Promise<void>} */
26
+ export async function markSessionCompleted(storage, sessionDir) {
27
+ await storage.write(`${sessionDir}/.completed`, new Date().toISOString());
28
+ }
29
+
30
+ /** @param {StorageAdapter} storage @param {string} sessionDir @returns {Promise<boolean>} */
31
+ export async function isSessionCompleted(storage, sessionDir) {
32
+ return storage.exists(`${sessionDir}/.completed`);
33
+ }
34
+
35
+ /** @param {StorageAdapter} storage @param {string[]} reviewPaths @param {string} dailyPath @returns {Promise<void>} */
36
+ export async function mergeReviewsIntoDaily(storage, reviewPaths, dailyPath) {
37
+ const reviewContents = [];
38
+ for (const p of reviewPaths) {
39
+ const content = await storage.read(p);
40
+ if (content && content.trim().length > 0) reviewContents.push(content.trim());
41
+ }
42
+ if (reviewContents.length === 0) {
43
+ if (!(await storage.exists(dailyPath))) await storage.write(dailyPath, '');
44
+ return;
45
+ }
46
+ const existing = await storage.read(dailyPath);
47
+ const merged = existing
48
+ ? existing.trimEnd() + '\n\n' + reviewContents.join('\n\n') + '\n'
49
+ : reviewContents.join('\n\n') + '\n';
50
+ await storage.write(dailyPath, merged);
51
+ }
@@ -0,0 +1,82 @@
1
+ // @ts-check
2
+
3
+ /** @param {Date} date @returns {number} */
4
+ export function getISOWeek(date) {
5
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
6
+ const dayNum = d.getUTCDay() || 7;
7
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum);
8
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
9
+ return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
10
+ }
11
+
12
+ /** @param {Date} date @returns {number} */
13
+ export function getISOWeekYear(date) {
14
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
15
+ const dayNum = d.getUTCDay() || 7;
16
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum);
17
+ return d.getUTCFullYear();
18
+ }
19
+
20
+ /** @param {Date} date @returns {number} */
21
+ export function getQuarter(date) {
22
+ return Math.ceil((date.getMonth() + 1) / 3);
23
+ }
24
+
25
+ /** @param {Date} date @returns {string} */
26
+ export function formatDate(date) {
27
+ const y = date.getFullYear();
28
+ const m = String(date.getMonth() + 1).padStart(2, '0');
29
+ const d = String(date.getDate()).padStart(2, '0');
30
+ return `${y}-${m}-${d}`;
31
+ }
32
+
33
+ /** @param {Date} date @returns {string} */
34
+ export function formatWeek(date) {
35
+ return `${getISOWeekYear(date)}-W${String(getISOWeek(date)).padStart(2, '0')}`;
36
+ }
37
+
38
+ /** @param {Date} date @returns {string} */
39
+ export function formatMonth(date) {
40
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
41
+ }
42
+
43
+ /** @param {Date} date @returns {string} */
44
+ export function formatQuarter(date) {
45
+ return `${date.getFullYear()}-Q${getQuarter(date)}`;
46
+ }
47
+
48
+ /** @param {Date} date @returns {string} */
49
+ export function formatYear(date) {
50
+ return `${date.getFullYear()}`;
51
+ }
52
+
53
+ /**
54
+ * @param {Date} today
55
+ * @param {Date | null} lastRun
56
+ * @returns {{ needsWeekly: boolean, needsMonthly: boolean, needsQuarterly: boolean, needsYearly: boolean, previousWeek: string, previousMonth: string, previousQuarter: string, previousYear: string }}
57
+ */
58
+ export function checkPeriodsNeeded(today, lastRun) {
59
+ if (!lastRun) {
60
+ return {
61
+ needsWeekly: false, needsMonthly: false, needsQuarterly: false, needsYearly: false,
62
+ previousWeek: '', previousMonth: '', previousQuarter: '', previousYear: '',
63
+ };
64
+ }
65
+ const todayWeek = formatWeek(today);
66
+ const lastWeek = formatWeek(lastRun);
67
+ const todayMonth = formatMonth(today);
68
+ const lastMonth = formatMonth(lastRun);
69
+ const todayQuarter = formatQuarter(today);
70
+ const lastQuarter = formatQuarter(lastRun);
71
+ const todayYear = formatYear(today);
72
+ const lastYear = formatYear(lastRun);
73
+
74
+ return {
75
+ needsWeekly: todayWeek !== lastWeek,
76
+ needsMonthly: todayMonth !== lastMonth,
77
+ needsQuarterly: todayQuarter !== lastQuarter,
78
+ needsYearly: todayYear !== lastYear,
79
+ previousWeek: lastWeek, previousMonth: lastMonth,
80
+ previousQuarter: lastQuarter, previousYear: lastYear,
81
+ };
82
+ }
@@ -0,0 +1,19 @@
1
+ // @ts-check
2
+ /** @typedef {import('./types.d.ts').StorageAdapter} StorageAdapter */
3
+ /** @typedef {import('./types.d.ts').HookInput} HookInput */
4
+
5
+ /** @param {string} raw @returns {HookInput} */
6
+ export function parseHookInput(raw) {
7
+ const parsed = JSON.parse(raw);
8
+ if (!parsed || typeof parsed !== 'object') throw new Error('Invalid hook input: expected object');
9
+ if (typeof parsed.session_id !== 'string' || !parsed.session_id) throw new Error('Invalid hook input: missing session_id');
10
+ return /** @type {HookInput} */ (parsed);
11
+ }
12
+
13
+ /** @param {StorageAdapter} storage @param {string} sessionDir @param {string} date @param {HookInput} entry @returns {Promise<void>} */
14
+ export async function appendRawLog(storage, sessionDir, date, entry) {
15
+ await storage.mkdir(sessionDir);
16
+ const logPath = `${sessionDir}/${date}.jsonl`;
17
+ const record = { ...entry, timestamp: new Date().toISOString() };
18
+ await storage.append(logPath, JSON.stringify(record) + '\n');
19
+ }
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ import { loadConfig, createStorageAdapter } from './config.mjs';
4
+
5
+ async function main() {
6
+ const [command, ...args] = process.argv.slice(2);
7
+ const config = loadConfig();
8
+ if (!config) { process.stderr.write('config not found\n'); process.exit(1); }
9
+
10
+ const storage = await createStorageAdapter(config);
11
+
12
+ switch (command) {
13
+ case 'read': {
14
+ const content = await storage.read(args[0]);
15
+ if (content !== null) process.stdout.write(content);
16
+ break;
17
+ }
18
+ case 'write': {
19
+ let data = '';
20
+ process.stdin.setEncoding('utf-8');
21
+ for await (const chunk of process.stdin) { data += chunk; }
22
+ await storage.write(args[0], data);
23
+ break;
24
+ }
25
+ case 'append': {
26
+ let data = '';
27
+ process.stdin.setEncoding('utf-8');
28
+ for await (const chunk of process.stdin) { data += chunk; }
29
+ await storage.append(args[0], data);
30
+ break;
31
+ }
32
+ case 'list': {
33
+ const entries = await storage.list(args[0]);
34
+ process.stdout.write(entries.join('\n') + '\n');
35
+ break;
36
+ }
37
+ case 'exists': {
38
+ const exists = await storage.exists(args[0]);
39
+ process.stdout.write(exists ? 'true\n' : 'false\n');
40
+ process.exit(exists ? 0 : 1);
41
+ break;
42
+ }
43
+ default:
44
+ process.stderr.write(`Unknown command: ${command}\nUsage: storage-cli <read|write|append|list|exists> <path>\n`);
45
+ process.exit(1);
46
+ }
47
+ }
48
+ main().catch((err) => { process.stderr.write(`Error: ${err.message}\n`); process.exit(1); });
@@ -0,0 +1,63 @@
1
+ // @ts-check
2
+ /** @typedef {import('./types.d.ts').StorageAdapter} StorageAdapter */
3
+
4
+ import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
5
+ import { dirname, join } from 'path';
6
+
7
+ /** @implements {StorageAdapter} */
8
+ export class LocalStorageAdapter {
9
+ /** @param {string} basePath */
10
+ constructor(basePath) {
11
+ /** @private */
12
+ this.basePath = basePath;
13
+ }
14
+
15
+ /** @private @param {string} path @returns {string} */
16
+ resolve(path) {
17
+ return join(this.basePath, path);
18
+ }
19
+
20
+ /** @param {string} path @returns {Promise<string | null>} */
21
+ async read(path) {
22
+ const full = this.resolve(path);
23
+ if (!existsSync(full)) return null;
24
+ return readFileSync(full, 'utf-8');
25
+ }
26
+
27
+ /** @param {string} path @param {string} content @returns {Promise<void>} */
28
+ async write(path, content) {
29
+ const full = this.resolve(path);
30
+ mkdirSync(dirname(full), { recursive: true });
31
+ writeFileSync(full, content, 'utf-8');
32
+ }
33
+
34
+ /** @param {string} path @param {string} content @returns {Promise<void>} */
35
+ async append(path, content) {
36
+ const full = this.resolve(path);
37
+ mkdirSync(dirname(full), { recursive: true });
38
+ appendFileSync(full, content, 'utf-8');
39
+ }
40
+
41
+ /** @param {string} path @returns {Promise<boolean>} */
42
+ async exists(path) {
43
+ return existsSync(this.resolve(path));
44
+ }
45
+
46
+ /** @param {string} dir @returns {Promise<string[]>} */
47
+ async list(dir) {
48
+ const full = this.resolve(dir);
49
+ if (!existsSync(full)) return [];
50
+ return readdirSync(full);
51
+ }
52
+
53
+ /** @param {string} dir @returns {Promise<void>} */
54
+ async mkdir(dir) {
55
+ mkdirSync(this.resolve(dir), { recursive: true });
56
+ }
57
+
58
+ /** @param {string} path @returns {Promise<boolean>} */
59
+ async isDirectory(path) {
60
+ try { return statSync(this.resolve(path)).isDirectory(); }
61
+ catch { return false; }
62
+ }
63
+ }
package/lib/types.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ export interface Profile {
2
+ company: string;
3
+ role: string;
4
+ team: string;
5
+ context: string;
6
+ }
7
+
8
+ export interface Periods {
9
+ daily: true;
10
+ weekly: boolean;
11
+ monthly: boolean;
12
+ quarterly: boolean;
13
+ yearly: boolean;
14
+ }
15
+
16
+ export interface LocalStorageConfig {
17
+ basePath: string;
18
+ }
19
+
20
+ export interface GitHubStorageConfig {
21
+ owner: string;
22
+ repo: string;
23
+ token: string;
24
+ basePath: string;
25
+ }
26
+
27
+ export interface StorageConfig {
28
+ type: "local" | "github";
29
+ local?: LocalStorageConfig;
30
+ github?: GitHubStorageConfig;
31
+ }
32
+
33
+ export interface Config {
34
+ storage: StorageConfig;
35
+ language: string;
36
+ periods: Periods;
37
+ profile: Profile;
38
+ }
39
+
40
+ export interface StorageAdapter {
41
+ read(path: string): Promise<string | null>;
42
+ write(path: string, content: string): Promise<void>;
43
+ append(path: string, content: string): Promise<void>;
44
+ exists(path: string): Promise<boolean>;
45
+ list(dir: string): Promise<string[]>;
46
+ mkdir(dir: string): Promise<void>;
47
+ isDirectory(path: string): Promise<boolean>;
48
+ }
49
+
50
+ export interface HookInput {
51
+ session_id: string;
52
+ transcript_path: string;
53
+ cwd: string;
54
+ hook_event_name: string;
55
+ [key: string]: unknown;
56
+ }
57
+
58
+ export interface DeviceCodeResponse {
59
+ device_code: string;
60
+ user_code: string;
61
+ verification_uri: string;
62
+ expires_in: number;
63
+ interval: number;
64
+ }