oh-my-adhd 0.2.13 → 0.2.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.
@@ -11,6 +11,7 @@ const MANIFEST_LOCK_TTL_MS = 10000;
11
11
  const LOG_FILE = path.join(BRAIN_DIR, "logs", "brain.log");
12
12
  const VERSION_FILE = path.join(BRAIN_DIR, "VERSION");
13
13
  export const SCHEMA_VERSION = 1;
14
+ export const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14
15
  async function appendLog(level, msg) {
15
16
  try {
16
17
  const entry = `${new Date().toISOString()} [${level}] ${msg}\n`;
@@ -169,7 +170,7 @@ export async function saveCapture(content, threadId) {
169
170
  const captureId = randomUUID();
170
171
  const timestamp = new Date().toISOString();
171
172
  const tid = threadId ?? captureId;
172
- if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tid)) {
173
+ if (!UUID_RE.test(tid)) {
173
174
  throw new Error(`Invalid threadId: ${tid}`);
174
175
  }
175
176
  const threadFile = path.join(THREADS_DIR, `${tid}.md`);
@@ -286,9 +287,8 @@ export async function getThreads() {
286
287
  });
287
288
  return sorted;
288
289
  }
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;
290
290
  export async function getThread(threadId) {
291
- if (!threadId || !UUID_RE_STRICT.test(threadId))
291
+ if (!threadId || !UUID_RE.test(threadId))
292
292
  return null;
293
293
  try {
294
294
  return await fs.readFile(path.join(THREADS_DIR, `${threadId}.md`), "utf-8");
@@ -352,7 +352,6 @@ export async function savePage(slug, content) {
352
352
  await fs.writeFile(tmpPage, content, "utf-8");
353
353
  await fs.rename(tmpPage, pageFile);
354
354
  }
355
- const UUID_RE = /^[a-zA-Z0-9_-]+$/;
356
355
  const TRASH_DIR = path.join(BRAIN_DIR, ".trash");
357
356
  export async function deleteThread(threadId) {
358
357
  if (!UUID_RE.test(threadId)) {
@@ -30,10 +30,18 @@ export function registerWikiExport(server) {
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/"];
33
+ // Block writes into known sensitive dirs use realpath for symlink safety
34
+ const SENSITIVE_DIRS = [".ssh", ".aws", ".gnupg", ".kube", ".docker",
35
+ path.join(".config", "git"), path.join(".config", "gh")];
35
36
  const homeDir = os.homedir();
36
- if (sensitivePatterns.some(p => resolved.startsWith(path.join(homeDir, p)))) {
37
+ let realResolved = resolved;
38
+ try {
39
+ realResolved = await fs.realpath(path.dirname(resolved));
40
+ }
41
+ catch { /* dir may not exist yet */ }
42
+ const realHome = await fs.realpath(homeDir).catch(() => homeDir);
43
+ const relDir = path.relative(realHome, realResolved).toLowerCase();
44
+ if (SENSITIVE_DIRS.some(d => relDir === d.toLowerCase() || relDir.startsWith(d.toLowerCase() + path.sep))) {
37
45
  return {
38
46
  content: [{ type: "text", text: "오류: 보안상 해당 경로에는 내보낼 수 없습니다." }],
39
47
  isError: true,
@@ -52,7 +60,7 @@ export function registerWikiExport(server) {
52
60
  `경로: ${filePath}`,
53
61
  `스레드: ${threads.length}개 | 페이지: ${pages.length}개 | ${sizeKB}KB`,
54
62
  "",
55
- "복원하려면: wiki_export 결과 JSON을 ~/.oh-my-adhd/로 수동 복사",
63
+ "복원하려면: wiki_import({ inputPath: \"<이 경로>\" }) 호출",
56
64
  ].join("\n"),
57
65
  }],
58
66
  };
@@ -1,10 +1,10 @@
1
1
  import { z } from "zod";
2
- import { ensureBrainDirs, BRAIN_DIR, SCHEMA_VERSION, withBrainLock } from "../../lib/brain.js";
2
+ import { ensureBrainDirs, BRAIN_DIR, SCHEMA_VERSION, UUID_RE, withBrainLock } from "../../lib/brain.js";
3
3
  import fs from "fs/promises";
4
4
  import path from "path";
5
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
5
  const SLUG_RE = /^[a-z0-9가-힣][a-z0-9가-힣_-]{0,127}$/;
7
6
  const MAX_CONTENT_BYTES = 5 * 1024 * 1024; // 5MB per thread
7
+ const MAX_ITEMS = 10000; // max threads or pages per import
8
8
  export function registerWikiImport(server) {
9
9
  server.tool("wiki_import", "wiki_export로 내보낸 JSON 백업 파일을 가져온다. 기존 데이터와 병합(merge)하며 중복 thread ID는 덮어쓴다.", {
10
10
  inputPath: z.string().describe("가져올 .json 파일 경로 (wiki_export로 생성된 파일)"),
@@ -18,9 +18,15 @@ export function registerWikiImport(server) {
18
18
  isError: true,
19
19
  };
20
20
  }
21
- let raw;
21
+ // Check file size before reading into memory
22
22
  try {
23
- raw = await fs.readFile(resolved, "utf-8");
23
+ const stat = await fs.stat(resolved);
24
+ if (stat.size > 100 * 1024 * 1024) {
25
+ return {
26
+ content: [{ type: "text", text: "오류: 파일이 너무 큽니다 (100MB 초과)." }],
27
+ isError: true,
28
+ };
29
+ }
24
30
  }
25
31
  catch {
26
32
  return {
@@ -28,9 +34,13 @@ export function registerWikiImport(server) {
28
34
  isError: true,
29
35
  };
30
36
  }
31
- if (Buffer.byteLength(raw, "utf-8") > 100 * 1024 * 1024) {
37
+ let raw;
38
+ try {
39
+ raw = await fs.readFile(resolved, "utf-8");
40
+ }
41
+ catch {
32
42
  return {
33
- content: [{ type: "text", text: "오류: 파일이 너무 큽니다 (100MB 초과)." }],
43
+ content: [{ type: "text", text: `오류: 파일을 읽을 없습니다: ${resolved}` }],
34
44
  isError: true,
35
45
  };
36
46
  }
@@ -50,6 +60,12 @@ export function registerWikiImport(server) {
50
60
  isError: true,
51
61
  };
52
62
  }
63
+ if (exportData.threads.length > MAX_ITEMS || (exportData.pages?.length ?? 0) > MAX_ITEMS) {
64
+ return {
65
+ content: [{ type: "text", text: `오류: 항목이 너무 많습니다 (최대 ${MAX_ITEMS}개).` }],
66
+ isError: true,
67
+ };
68
+ }
53
69
  if (exportData.schemaVersion !== undefined && exportData.schemaVersion !== SCHEMA_VERSION) {
54
70
  return {
55
71
  content: [{ type: "text", text: `오류: 스키마 버전 불일치 (파일: ${exportData.schemaVersion}, 현재: ${SCHEMA_VERSION})` }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-adhd",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
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": {
@@ -48,10 +48,12 @@ try {
48
48
  const openThreads = manifest.filter(t => t.is_open).slice(0, 4);
49
49
  if (openThreads.length === 0) process.exit(0);
50
50
 
51
+ // Strip control chars only — preserves emoji, Korean, CJK, etc.
51
52
  const sanitize = (s, max) => String(s ?? "")
52
- .replace(/[^ -~가-힣㄰-㆏ᄀ-ᇿ]/g, " ")
53
+ .replace(/[\x00-\x1F\x7F]/g, " ")
53
54
  .replace(/[`$<>]/g, "")
54
- .replace(/\bignore (all|previous)\b/gi, "[redacted]")
55
+ .replace(/\b(ignore|disregard)\s+(all|previous|prior)\b/gi, "[redacted]")
56
+ .replace(/(이전|앞의|위의|모든)\s*(지시|명령|규칙)\s*(무시|잊어|버려)/g, "[redacted]")
55
57
  .slice(0, max);
56
58
 
57
59
  const lines = [
@@ -38,10 +38,12 @@ try {
38
38
  const openThreads = manifest.filter(t => t.is_open);
39
39
  if (openThreads.length > 0) {
40
40
  const top = openThreads[0];
41
+ // Strip control chars only — preserves emoji, Korean, CJK, etc.
41
42
  const sanitize = (s, max) => String(s ?? "")
42
- .replace(/[^ -~가-힣㄰-㆏ᄀ-ᇿ]/g, " ")
43
+ .replace(/[\x00-\x1F\x7F]/g, " ")
43
44
  .replace(/[`$<>]/g, "")
44
- .replace(/\bignore (all|previous)\b/gi, "[redacted]")
45
+ .replace(/\b(ignore|disregard)\s+(all|previous|prior)\b/gi, "[redacted]")
46
+ .replace(/(이전|앞의|위의|모든)\s*(지시|명령|규칙)\s*(무시|잊어|버려)/g, "[redacted]")
45
47
  .slice(0, max);
46
48
  const title = sanitize(top.title ?? "진행중인 작업", 40);
47
49
  const nextHint = top.next_action ? `\n→ 다음할것: ${sanitize(top.next_action, 60)}` : "";