oh-my-adhd 0.2.12 → 0.2.13

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.
@@ -286,8 +286,9 @@ export async function getThreads() {
286
286
  });
287
287
  return sorted;
288
288
  }
289
+ const UUID_RE_STRICT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
289
290
  export async function getThread(threadId) {
290
- if (!threadId || !/^[a-zA-Z0-9_-]+$/.test(threadId))
291
+ if (!threadId || !UUID_RE_STRICT.test(threadId))
291
292
  return null;
292
293
  try {
293
294
  return await fs.readFile(path.join(THREADS_DIR, `${threadId}.md`), "utf-8");
@@ -23,13 +23,22 @@ export function registerWikiExport(server) {
23
23
  const date = new Date().toISOString().slice(0, 10);
24
24
  const defaultPath = path.join(os.homedir(), `oh-my-adhd-export-${date}.json`);
25
25
  const resolved = outputPath ? path.resolve(outputPath) : defaultPath;
26
- // Require .json extension — prevents LLM-controlled path from clobbering arbitrary config files
26
+ // Require .json extension — prevents LLM-controlled path from clobbering non-JSON config files
27
27
  if (!resolved.endsWith(".json")) {
28
28
  return {
29
29
  content: [{ type: "text", text: "오류: outputPath는 .json 확장자로 끝나야 합니다." }],
30
30
  isError: true,
31
31
  };
32
32
  }
33
+ // Block writes into known sensitive dirs (~/.ssh, ~/.aws, ~/.gnupg)
34
+ const sensitivePatterns = [".ssh/", ".aws/", ".gnupg/", ".config/git/"];
35
+ const homeDir = os.homedir();
36
+ if (sensitivePatterns.some(p => resolved.startsWith(path.join(homeDir, p)))) {
37
+ return {
38
+ content: [{ type: "text", text: "오류: 보안상 해당 경로에는 내보낼 수 없습니다." }],
39
+ isError: true,
40
+ };
41
+ }
33
42
  const filePath = resolved;
34
43
  const tmp = filePath + ".tmp";
35
44
  await fs.writeFile(tmp, JSON.stringify(exportData, null, 2), "utf-8");
@@ -122,24 +122,24 @@ export function registerWikiImport(server) {
122
122
  const tmp = manifestFile + ".tmp";
123
123
  await fs.writeFile(tmp, JSON.stringify(manifest, null, 2), "utf-8");
124
124
  await fs.rename(tmp, manifestFile);
125
- });
126
- // Import pages (not manifest-managed, no lock needed)
127
- if (exportData.pages && Array.isArray(exportData.pages)) {
128
- for (const rawPage of exportData.pages) {
129
- if (typeof rawPage !== "object" || rawPage === null)
130
- continue;
131
- const page = rawPage;
132
- const slug = typeof page.slug === "string" ? page.slug : "";
133
- const content = typeof page.content === "string" ? page.content : "";
134
- if (!SLUG_RE.test(slug) || !content)
135
- continue;
136
- const pageFile = path.join(pagesDir, `${slug}.md`);
137
- const tmp = pageFile + ".tmp";
138
- await fs.writeFile(tmp, content, "utf-8");
139
- await fs.rename(tmp, pageFile);
140
- importedPages++;
125
+ // Import pages inside lock for consistency with concurrent dump+import
126
+ if (exportData.pages && Array.isArray(exportData.pages)) {
127
+ for (const rawPage of exportData.pages) {
128
+ if (typeof rawPage !== "object" || rawPage === null)
129
+ continue;
130
+ const page = rawPage;
131
+ const slug = typeof page.slug === "string" ? page.slug : "";
132
+ const content = typeof page.content === "string" ? page.content : "";
133
+ if (!SLUG_RE.test(slug) || !content)
134
+ continue;
135
+ const pageFile = path.join(pagesDir, `${slug}.md`);
136
+ const pageTmp = pageFile + ".tmp";
137
+ await fs.writeFile(pageTmp, content, "utf-8");
138
+ await fs.rename(pageTmp, pageFile);
139
+ importedPages++;
140
+ }
141
141
  }
142
- }
142
+ });
143
143
  return {
144
144
  content: [{
145
145
  type: "text",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-adhd",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "description": "ADHD second brain — zero-friction capture, auto context restore, unstick. MCP-native Claude Code plugin.",
5
5
  "author": "Yeachan Heo",
6
6
  "repository": {
@@ -8,13 +8,15 @@ const BRAIN_DIR = process.env.OH_MY_ADHD_DIR ?? join(homedir(), ".oh-my-adhd");
8
8
  const MANIFEST = join(BRAIN_DIR, "threads", ".manifest.json");
9
9
 
10
10
  // Use parent PID (= Claude Code instance) as session discriminator — no singleton file needed
11
- const ppid = process.ppid ?? 0;
11
+ const ppid = process.ppid;
12
12
 
13
- // Write per-session start marker
14
- try {
15
- mkdirSync(BRAIN_DIR, { recursive: true });
16
- writeFileSync(join(BRAIN_DIR, `.session-start-${ppid}`), String(Date.now()));
17
- } catch { /* non-fatal */ }
13
+ // Write per-session start marker (skip if ppid is unavailable — avoids shared .session-start-0)
14
+ if (ppid) {
15
+ try {
16
+ mkdirSync(BRAIN_DIR, { recursive: true });
17
+ writeFileSync(join(BRAIN_DIR, `.session-start-${ppid}`), String(Date.now()));
18
+ } catch { /* non-fatal */ }
19
+ }
18
20
 
19
21
  // GC stale session files older than 24h (runs on every new session)
20
22
  try {
@@ -11,7 +11,9 @@ const BRAIN_DIR = process.env.OH_MY_ADHD_DIR ?? join(homedir(), ".oh-my-adhd");
11
11
  const MANIFEST = join(BRAIN_DIR, "threads", ".manifest.json");
12
12
 
13
13
  // Use parent PID (= Claude Code instance) as session discriminator
14
- const ppid = process.ppid ?? 0;
14
+ const ppid = process.ppid;
15
+ // If ppid is unavailable, skip protection rather than risk a shared-file collision
16
+ if (!ppid) process.exit(0);
15
17
  const SESSION_START_FILE = join(BRAIN_DIR, `.session-start-${ppid}`);
16
18
  const LAST_DUMP_FILE = join(BRAIN_DIR, `.last-dump-${ppid}`);
17
19