oh-my-adhd 0.2.8 → 0.2.10

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.
@@ -14,6 +14,7 @@ import { registerWikiGraph } from "./tools/wiki-graph.js";
14
14
  import { registerWikiStructure } from "./tools/wiki-structure.js";
15
15
  import { registerWikiSave } from "./tools/wiki-save.js";
16
16
  import { registerWikiDelete } from "./tools/wiki-delete.js";
17
+ import { registerWikiExport } from "./tools/wiki-export.js";
17
18
  const server = new McpServer({
18
19
  name: "oh-my-adhd",
19
20
  version: "0.2.0",
@@ -29,6 +30,7 @@ registerWikiGraph(server);
29
30
  registerWikiStructure(server);
30
31
  registerWikiSave(server);
31
32
  registerWikiDelete(server);
33
+ registerWikiExport(server);
32
34
  async function main() {
33
35
  const transport = new StdioServerTransport();
34
36
  await server.connect(transport);
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+ import { getThreads, getThread, getPages } from "../../lib/brain.js";
3
+ import fs from "fs/promises";
4
+ import path from "path";
5
+ import os from "os";
6
+ export function registerWikiExport(server) {
7
+ server.tool("wiki_export", "모든 스레드와 페이지를 JSON 파일로 내보낸다. 백업/이전 용도. 파일 경로를 반환하며 언제든 재실행 가능.", {
8
+ outputPath: z.string().optional().describe("내보낼 파일 경로 (기본값: ~/oh-my-adhd-export-YYYY-MM-DD.json)"),
9
+ }, async ({ outputPath }) => {
10
+ try {
11
+ const [threads, pages] = await Promise.all([getThreads(), getPages()]);
12
+ const threadContents = await Promise.all(threads.map(async (t) => ({
13
+ ...t,
14
+ content: await getThread(t.id),
15
+ })));
16
+ const exportData = {
17
+ exportedAt: new Date().toISOString(),
18
+ schemaVersion: 1,
19
+ stats: { threads: threads.length, pages: pages.length },
20
+ threads: threadContents,
21
+ pages,
22
+ };
23
+ const date = new Date().toISOString().slice(0, 10);
24
+ const defaultPath = path.join(os.homedir(), `oh-my-adhd-export-${date}.json`);
25
+ const resolved = outputPath ? path.resolve(outputPath) : defaultPath;
26
+ // Require .json extension — prevents LLM-controlled path from clobbering arbitrary config files
27
+ if (!resolved.endsWith(".json")) {
28
+ return {
29
+ content: [{ type: "text", text: "오류: outputPath는 .json 확장자로 끝나야 합니다." }],
30
+ isError: true,
31
+ };
32
+ }
33
+ const filePath = resolved;
34
+ const tmp = filePath + ".tmp";
35
+ await fs.writeFile(tmp, JSON.stringify(exportData, null, 2), "utf-8");
36
+ await fs.rename(tmp, filePath);
37
+ const sizeKB = Math.round((await fs.stat(filePath)).size / 1024);
38
+ return {
39
+ content: [{
40
+ type: "text",
41
+ text: [
42
+ "내보내기 완료 ✓",
43
+ `경로: ${filePath}`,
44
+ `스레드: ${threads.length}개 | 페이지: ${pages.length}개 | ${sizeKB}KB`,
45
+ "",
46
+ "복원하려면: wiki_export 결과 JSON을 ~/.oh-my-adhd/로 수동 복사",
47
+ ].join("\n"),
48
+ }],
49
+ };
50
+ }
51
+ catch (e) {
52
+ return {
53
+ content: [{ type: "text", text: `오류: ${e.message ?? String(e)}` }],
54
+ isError: true,
55
+ };
56
+ }
57
+ });
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-adhd",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
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": {
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ // SessionStart hook — writes .session-start timestamp + injects recall context as additionalContext
3
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
4
+ import { join } from "path";
5
+ import { homedir } from "os";
6
+
7
+ const BRAIN_DIR = process.env.OH_MY_ADHD_DIR ?? join(homedir(), ".oh-my-adhd");
8
+ const SESSION_START_FILE = join(BRAIN_DIR, ".session-start");
9
+ const MANIFEST = join(BRAIN_DIR, "threads", ".manifest.json");
10
+
11
+ // Always write session start timestamp first
12
+ try {
13
+ mkdirSync(BRAIN_DIR, { recursive: true });
14
+ writeFileSync(SESSION_START_FILE, String(Date.now()));
15
+ } catch { /* non-fatal */ }
16
+
17
+ // Build recall context from manifest
18
+ try {
19
+ const manifest = JSON.parse(readFileSync(MANIFEST, "utf-8"));
20
+ if (!Array.isArray(manifest) || manifest.length === 0) process.exit(0);
21
+
22
+ const now = Date.now();
23
+ const gapLabel = (updatedAt) => {
24
+ const h = Math.max(0, Math.round((now - new Date(updatedAt).getTime()) / 3600000));
25
+ if (h < 1) return "방금 전";
26
+ if (h < 18) return `${h}시간 전`;
27
+ if (h < 36) return "어제";
28
+ return `${Math.floor(h / 24)}일 전`;
29
+ };
30
+
31
+ const openThreads = manifest.filter(t => t.is_open).slice(0, 4);
32
+ if (openThreads.length === 0) process.exit(0);
33
+
34
+ // Sanitize stored content before injecting into model context (prompt injection prevention)
35
+ const sanitize = (s, max) => String(s ?? "")
36
+ .replace(/[\x00-\x1F\x7F]/g, " ")
37
+ .replace(/[`$<>]/g, "")
38
+ .replace(/\bignore (all|previous)\b/gi, "[redacted]")
39
+ .slice(0, max);
40
+
41
+ const lines = ["[Second Brain 복원]", ""];
42
+ const top = openThreads[0];
43
+ lines.push(`🔴 **${sanitize(top.title, 40)}** (${gapLabel(top.updatedAt)})`);
44
+ if (top.next_action) lines.push(`→ 다음: ${sanitize(top.next_action, 100)}`);
45
+ if (top.blocker) lines.push(`⛔ 막힌것: ${sanitize(top.blocker, 80)}`);
46
+
47
+ if (openThreads.length > 1) {
48
+ lines.push("");
49
+ for (const t of openThreads.slice(1)) {
50
+ const hint = t.next_action ? ` → ${sanitize(t.next_action, 60)}` : "";
51
+ lines.push(`• ${sanitize(t.title, 30)} (${gapLabel(t.updatedAt)})${hint}`);
52
+ }
53
+ }
54
+
55
+ lines.push("");
56
+ lines.push(`이어서 갈까? thread: \`${top.id}\``);
57
+
58
+ process.stdout.write(JSON.stringify({ additionalContext: lines.join("\n") }));
59
+ } catch {
60
+ // No manifest / parse error — graceful degradation, never block session start
61
+ process.exit(0);
62
+ }