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.
package/dist/mcp/lib/brain.js
CHANGED
|
@@ -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 (
|
|
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 || !
|
|
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
|
|
34
|
-
const
|
|
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
|
-
|
|
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
|
-
"복원하려면:
|
|
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
|
-
|
|
21
|
+
// Check file size before reading into memory
|
|
22
22
|
try {
|
|
23
|
-
|
|
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
|
-
|
|
37
|
+
let raw;
|
|
38
|
+
try {
|
|
39
|
+
raw = await fs.readFile(resolved, "utf-8");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
32
42
|
return {
|
|
33
|
-
content: [{ type: "text", text:
|
|
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
|
@@ -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(/[
|
|
53
|
+
.replace(/[\x00-\x1F\x7F]/g, " ")
|
|
53
54
|
.replace(/[`$<>]/g, "")
|
|
54
|
-
.replace(/\
|
|
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 = [
|
package/scripts/stop-hook.mjs
CHANGED
|
@@ -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(/[
|
|
43
|
+
.replace(/[\x00-\x1F\x7F]/g, " ")
|
|
43
44
|
.replace(/[`$<>]/g, "")
|
|
44
|
-
.replace(/\
|
|
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)}` : "";
|