@giwonn/claude-daily-review 0.3.13 → 0.3.14

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.
@@ -13,7 +13,7 @@
13
13
  "source": {
14
14
  "source": "url",
15
15
  "url": "https://github.com/giwonn/claude-daily-review.git",
16
- "ref": "v0.3.13"
16
+ "ref": "v0.3.14"
17
17
  },
18
18
  "description": "Auto-capture conversations for daily review and career documentation",
19
19
  "author": {
@@ -23,19 +23,25 @@ function acquireLock() {
23
23
 
24
24
  mkdirSync(dirname(lockPath), { recursive: true });
25
25
 
26
- // Check for stale lock
27
- if (existsSync(lockPath)) {
26
+ const lockData = JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() });
27
+
28
+ try {
29
+ writeFileSync(lockPath, lockData, { flag: 'wx' });
30
+ return true;
31
+ } catch {
32
+ // File exists — check if stale
28
33
  try {
29
34
  const lock = JSON.parse(readFileSync(lockPath, 'utf-8'));
30
35
  const age = Date.now() - new Date(lock.timestamp).getTime();
31
- if (age < LOCK_STALE_MS) return false; // Another session is recovering
32
- } catch { /* corrupt lock, take over */ }
36
+ if (age < LOCK_STALE_MS) return false;
37
+ unlinkSync(lockPath);
38
+ // Retry once after removing stale lock
39
+ try {
40
+ writeFileSync(lockPath, lockData, { flag: 'wx' });
41
+ return true;
42
+ } catch { return false; }
43
+ } catch { return false; }
33
44
  }
34
-
35
- try {
36
- writeFileSync(lockPath, JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() }));
37
- return true;
38
- } catch { return false; }
39
45
  }
40
46
 
41
47
  function releaseLock() {
@@ -15,21 +15,35 @@ export class GitHubStorageAdapter {
15
15
  }
16
16
 
17
17
  /** @private @param {string} path @returns {string} */
18
- getUrl(path) { return this.basePath ? `${this.baseUrl}/${this.basePath}/${path}` : `${this.baseUrl}/${path}`; }
18
+ getUrl(path) {
19
+ if (path.split('/').includes('..')) throw new Error('Invalid path: traversal not allowed');
20
+ return this.basePath ? `${this.baseUrl}/${this.basePath}/${path}` : `${this.baseUrl}/${path}`;
21
+ }
22
+
23
+ /**
24
+ * @private
25
+ * @param {string} url
26
+ * @param {RequestInit} [options]
27
+ * @returns {Promise<Record<string, unknown> | null>}
28
+ */
29
+ async fetchOrNull(url, options) {
30
+ const res = await fetch(url, { ...options, headers: this.headers });
31
+ if (res.status === 404) return null;
32
+ if (!res.ok) throw new Error(`GitHub API error: ${res.status}`);
33
+ return /** @type {Record<string, unknown>} */ (await res.json());
34
+ }
19
35
 
20
36
  /** @private @param {string} path @returns {Promise<string | null>} */
21
37
  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());
38
+ const data = await this.fetchOrNull(this.getUrl(path), { method: 'GET' });
39
+ if (!data) return null;
25
40
  return /** @type {string | null} */ (data.sha || null);
26
41
  }
27
42
 
28
43
  /** @param {string} path @returns {Promise<string | null>} */
29
44
  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());
45
+ const data = await this.fetchOrNull(this.getUrl(path), { method: 'GET' });
46
+ if (!data) return null;
33
47
  return Buffer.from(/** @type {string} */ (data.content), 'base64').toString('utf-8');
34
48
  }
35
49
 
@@ -40,10 +54,13 @@ export class GitHubStorageAdapter {
40
54
  const body = { message: `update ${path}`, content: Buffer.from(content).toString('base64') };
41
55
  if (sha) body.sha = sha;
42
56
  const res = await fetch(this.getUrl(path), { method: 'PUT', headers: this.headers, body: JSON.stringify(body) });
43
- if (!res.ok && res.status === 409) {
57
+ if (res.status === 409) {
44
58
  const freshSha = await this.getSha(path);
45
59
  if (freshSha) body.sha = freshSha;
46
- await fetch(this.getUrl(path), { method: 'PUT', headers: this.headers, body: JSON.stringify(body) });
60
+ const retry = await fetch(this.getUrl(path), { method: 'PUT', headers: this.headers, body: JSON.stringify(body) });
61
+ if (!retry.ok) throw new Error(`GitHub API error: ${retry.status}`);
62
+ } else if (!res.ok) {
63
+ throw new Error(`GitHub API error: ${res.status}`);
47
64
  }
48
65
  }
49
66
 
@@ -55,16 +72,14 @@ export class GitHubStorageAdapter {
55
72
 
56
73
  /** @param {string} path @returns {Promise<boolean>} */
57
74
  async exists(path) {
58
- const res = await fetch(this.getUrl(path), { method: 'GET', headers: this.headers });
59
- return res.status !== 404;
75
+ const data = await this.fetchOrNull(this.getUrl(path), { method: 'GET' });
76
+ return data !== null;
60
77
  }
61
78
 
62
79
  /** @param {string} dir @returns {Promise<string[]>} */
63
80
  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 [];
81
+ const data = await this.fetchOrNull(this.getUrl(dir), { method: 'GET' });
82
+ if (!data || !Array.isArray(data)) return [];
68
83
  return data.map((/** @type {{ name: string }} */ entry) => entry.name);
69
84
  }
70
85
 
@@ -73,9 +88,8 @@ export class GitHubStorageAdapter {
73
88
 
74
89
  /** @param {string} path @returns {Promise<boolean>} */
75
90
  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();
91
+ const data = await this.fetchOrNull(this.getUrl(path), { method: 'GET' });
92
+ if (!data) return false;
79
93
  return Array.isArray(data);
80
94
  }
81
95
  }
package/lib/storage.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  /** @typedef {import('./types.d.ts').StorageAdapter} StorageAdapter */
3
3
 
4
4
  import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
5
- import { dirname, join } from 'path';
5
+ import { dirname, join, resolve as pathResolve } from 'path';
6
6
 
7
7
  /** @implements {StorageAdapter} */
8
8
  export class LocalStorageAdapter {
@@ -14,7 +14,11 @@ export class LocalStorageAdapter {
14
14
 
15
15
  /** @private @param {string} path @returns {string} */
16
16
  resolve(path) {
17
- return join(this.basePath, path);
17
+ const full = pathResolve(this.basePath, path);
18
+ if (full !== this.basePath && !full.startsWith(this.basePath + '/')) {
19
+ throw new Error('Invalid path: traversal outside base directory');
20
+ }
21
+ return full;
18
22
  }
19
23
 
20
24
  /** @param {string} path @returns {Promise<string | null>} */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giwonn/claude-daily-review",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "type": "module",
5
5
  "description": "Claude Code plugin that auto-captures conversations for daily review and career documentation",
6
6
  "repository": {