oh-my-adhd 0.2.0
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/LICENSE +21 -0
- package/README.md +182 -0
- package/bin/oh-my-adhd.mjs +382 -0
- package/dist/mcp/lib/brain.js +396 -0
- package/dist/mcp/lib/consolidate.js +190 -0
- package/dist/mcp/lib/linker.js +98 -0
- package/dist/mcp/lib/search.js +99 -0
- package/dist/mcp/mcp/server.js +36 -0
- package/dist/mcp/mcp/tools/wiki-delete.js +22 -0
- package/dist/mcp/mcp/tools/wiki-dump.js +85 -0
- package/dist/mcp/mcp/tools/wiki-graph.js +19 -0
- package/dist/mcp/mcp/tools/wiki-link.js +33 -0
- package/dist/mcp/mcp/tools/wiki-pages.js +20 -0
- package/dist/mcp/mcp/tools/wiki-query.js +67 -0
- package/dist/mcp/mcp/tools/wiki-recall.js +205 -0
- package/dist/mcp/mcp/tools/wiki-save.js +22 -0
- package/dist/mcp/mcp/tools/wiki-setup.js +25 -0
- package/dist/mcp/mcp/tools/wiki-structure.js +28 -0
- package/dist/mcp/mcp/tools/wiki-unstick.js +110 -0
- package/dist/mcp/mcp/utils.js +31 -0
- package/package.json +54 -0
- package/scripts/capture.sh +31 -0
- package/scripts/com.oh-my-adhd.server.plist +28 -0
- package/scripts/demo.sh +43 -0
- package/scripts/install-launchagent.sh +35 -0
- package/scripts/stop-hook.mjs +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Yeachan Heo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# oh-my-adhd
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/oh-my-adhd)
|
|
4
|
+
[](https://www.npmjs.com/package/oh-my-adhd)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
> **Stop re-explaining your project to Claude every morning.**
|
|
8
|
+
|
|
9
|
+
Claude Code MCP plugin that remembers where you left off — across sessions, across days. Built for ADHD brains, useful for everyone.
|
|
10
|
+
|
|
11
|
+
> 어제 뭐 했는지 기억 안 나도 괜찮아요.
|
|
12
|
+
|
|
13
|
+
## Demo
|
|
14
|
+
|
|
15
|
+
<!-- Record with: asciinema rec demo.cast -->
|
|
16
|
+
<!-- Then: svg-term --in demo.cast --out demo.svg -->
|
|
17
|
+
<!-- Or: termtosvg demo.svg -->
|
|
18
|
+
|
|
19
|
+
**What you'll see after install:**
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
[oh-my-adhd] 어제 어디까지 했더라...
|
|
23
|
+
|
|
24
|
+
## 어제 멈춘 곳
|
|
25
|
+
|
|
26
|
+
> 🔴 **React 폼 validation 리팩터링** — 15시간 전
|
|
27
|
+
> → 다음: useFormState 마이그레이션 + 에러 메시지 i18n
|
|
28
|
+
> ⛔ 막힌것: zod refine() 비동기 검증이 submit 중복 발생
|
|
29
|
+
>
|
|
30
|
+
> 이어서 갈까? (thread: `abc123...`)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
*Every new Claude Code session opens like this — automatically.*
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 왜 만들었냐 / Why
|
|
38
|
+
|
|
39
|
+
ADHD가 있으면 컨텍스트 스위칭이 치명적이다. Claude와 한 시간 작업하다 탭 닫으면 다음 날 "어디까지 했더라"를 복구하는 데 30분이 날아간다. 그게 싫어서 만들었다.
|
|
40
|
+
|
|
41
|
+
이건 메모 앱이 아니다. **막힌 곳에서 다시 시작하는 도구**다.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 설치 / Install
|
|
46
|
+
|
|
47
|
+
**Prerequisites**: Node.js 18+, npm, Claude Code
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx oh-my-adhd init
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
끝이다. Claude Code 재시작하면 바로 쓸 수 있다.
|
|
54
|
+
|
|
55
|
+
내부적으로:
|
|
56
|
+
- `~/.claude.json`에 MCP 서버 등록
|
|
57
|
+
- `~/.claude/settings.json`에 SessionStart / Stop 훅 추가
|
|
58
|
+
- `~/.oh-my-adhd/` 브레인 디렉터리 생성
|
|
59
|
+
|
|
60
|
+
> **재설치 안전**: `init`을 여러 번 실행해도 기존 브레인 데이터(`~/.oh-my-adhd/`)는 건드리지 않는다. 설정 파일은 덮어쓰기 전에 자동 백업된다.
|
|
61
|
+
|
|
62
|
+
> **설치 확인**: `npx oh-my-adhd doctor` 로 MCP 등록, 훅, 데이터 무결성을 한번에 확인할 수 있다.
|
|
63
|
+
|
|
64
|
+
> **브레인 디렉터리 변경**: `OH_MY_ADHD_DIR=/path/to/brain npx oh-my-adhd mcp` 로 기본 경로(`~/.oh-my-adhd`)를 덮어쓸 수 있다.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 어떻게 동작하냐 / How it works
|
|
69
|
+
|
|
70
|
+
### 세션 시작하면 자동으로
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
[oh-my-adhd] Second Brain 복원 중...
|
|
74
|
+
→ 어제 oh-my-adhd MCP 전환 작업 중이었어. tools/ 분할 구현까지 완료.
|
|
75
|
+
다음할것: README 재작성 + init 스크립트 테스트
|
|
76
|
+
막힌것: CLAUDE.md 지침만으론 자동 dump 신뢰 불가 (Stop hook으로 해결됨)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
새 세션을 열면 `wiki_recall`이 자동으로 최근 컨텍스트를 불러온다. "어디까지 했더라"를 Claude가 먼저 꺼내준다.
|
|
80
|
+
|
|
81
|
+
### 막혔을 때
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
wiki_unstick(energy: "low")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
지금 에너지 레벨(low/medium/high)에 맞는 다음 액션을 제안한다. 이미 시도해서 실패한 것은 다시 제안하지 않는다.
|
|
88
|
+
|
|
89
|
+
### 세션 끝나기 전에
|
|
90
|
+
|
|
91
|
+
Stop 훅이 발동해서 "wiki_dump 호출했나?" 리마인더를 출력한다. Claude가 이를 보고 wiki_dump를 호출해 결정/막힌것/다음할것을 저장한다. (Stop hook은 Claude에게 보내는 알림이지 강제 저장 메커니즘이 아니다 — Claude가 이를 인지하고 wiki_dump를 호출해야 저장된다.)
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## MCP Tools
|
|
96
|
+
|
|
97
|
+
### 자동 호출 (Daily use — auto-invoked)
|
|
98
|
+
|
|
99
|
+
| 툴 | 설명 |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `wiki_recall` | 세션 시작 시 자동 호출 — 최근 컨텍스트 복원 |
|
|
102
|
+
| `wiki_dump` | 컨텍스트 저장. 결정/막힌것/다음할것 구조 |
|
|
103
|
+
| `wiki_unstick` | 에너지 레벨별 다음 액션 제안. dead-end 자동 회피 |
|
|
104
|
+
|
|
105
|
+
### 수동 호출 (Advanced — call when needed)
|
|
106
|
+
|
|
107
|
+
| 툴 | 설명 |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `wiki_setup` | 첫 설치 시 초기 컨텍스트 등록 |
|
|
110
|
+
| `wiki_query` | 과거 스레드 검색 (BM25) |
|
|
111
|
+
| `wiki_pages` | 위키 페이지 목록 조회 |
|
|
112
|
+
| `wiki_link` | 페이지 간 링크 생성 |
|
|
113
|
+
| `wiki_graph` | 지식 그래프 시각화 |
|
|
114
|
+
| `wiki_structure` | 날것 캡처를 구조화 |
|
|
115
|
+
| `wiki_save` | 구조화된 위키 페이지 저장 |
|
|
116
|
+
| `wiki_delete` | 스레드 또는 페이지 삭제 (.trash 백업) |
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Storage Format
|
|
121
|
+
|
|
122
|
+
wiki_dump는 자유 텍스트가 아니라 구조화된 형식으로 저장된다:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
결정: [이번 세션에서 확정된 것]
|
|
126
|
+
가설: [현재 시도 중인 방향]
|
|
127
|
+
막힌것: [이미 시도해서 안 된 것 — 다음 세션에서 반복 금지]
|
|
128
|
+
다음할것: [지금 당장 멈춘 시점의 다음 액션. 구체적으로]
|
|
129
|
+
블로커: [해결 안 된 장애물]
|
|
130
|
+
요약: [한 줄 컨텍스트]
|
|
131
|
+
[git: branch@sha | dirty: modified-files]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`막힌것` 필드가 핵심이다. 이게 없으면 다음 세션에서 같은 실수를 반복한다.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Auto-consolidation
|
|
139
|
+
|
|
140
|
+
스레드가 50개 이상이고 마지막 압축으로부터 24시간 이상 지났을 때, `wiki_recall` 호출 시 백그라운드에서 자동으로 실행된다 (별도 호출 불필요):
|
|
141
|
+
- 30일 이상 미접근 스레드를 키워드 요약으로 압축
|
|
142
|
+
- 삭제 대신 `.trash/`로 백업
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Code Anchoring
|
|
147
|
+
|
|
148
|
+
wiki_dump 시 현재 git 컨텍스트가 자동으로 붙는다:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
[git: main@a3f2c1b | dirty: src/mcp/server.ts, README.md]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
나중에 "그때 어떤 파일 고치던 중이었지?" 를 추적할 수 있다.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 문제 해결 / Troubleshooting
|
|
159
|
+
|
|
160
|
+
**wiki_recall이 아무것도 안 보여줘**
|
|
161
|
+
→ `npx oh-my-adhd doctor` 로 설치 상태 확인
|
|
162
|
+
|
|
163
|
+
**Claude가 컨텍스트를 기억 못해**
|
|
164
|
+
→ Claude Code를 완전히 재시작했는지 확인 (창 닫고 다시 열기)
|
|
165
|
+
→ `~/.claude/settings.json`에 SessionStart hook이 있는지 확인
|
|
166
|
+
|
|
167
|
+
**설치 취소하고 싶어**
|
|
168
|
+
→ `~/.claude.json`에서 `mcpServers["oh-my-adhd"]` 제거
|
|
169
|
+
→ `~/.claude/settings.json`에서 SessionStart/Stop hook 제거
|
|
170
|
+
→ `~/.oh-my-adhd/` 디렉터리는 그대로 유지됨 (데이터 보존)
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## oh-my 시리즈
|
|
175
|
+
|
|
176
|
+
[oh-my-claudecode](https://github.com/Yeachan-Heo/oh-my-claudecode)의 생태계 안에서 동작하도록 설계됐다. MCP 서버로 독립 실행도 가능하고, omc 플러그인으로도 쓸 수 있다.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 라이선스
|
|
181
|
+
|
|
182
|
+
MIT
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFileSync } from "fs";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const PROJECT_DIR = join(__dirname, "..");
|
|
11
|
+
const BRAIN_DIR = join(homedir(), ".oh-my-adhd");
|
|
12
|
+
const cmd = process.argv[2];
|
|
13
|
+
|
|
14
|
+
function ensureBrainDir() {
|
|
15
|
+
if (!existsSync(BRAIN_DIR)) {
|
|
16
|
+
mkdirSync(BRAIN_DIR, { recursive: true });
|
|
17
|
+
mkdirSync(join(BRAIN_DIR, "threads"), { recursive: true });
|
|
18
|
+
mkdirSync(join(BRAIN_DIR, "pages"), { recursive: true });
|
|
19
|
+
console.log(`✓ Brain directory created at ${BRAIN_DIR}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printHelp() {
|
|
24
|
+
console.log(`
|
|
25
|
+
oh-my-adhd — ADHD second brain for Claude Code
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
npx oh-my-adhd <command>
|
|
29
|
+
|
|
30
|
+
Commands:
|
|
31
|
+
init One-line setup: MCP server + hooks + brain directory
|
|
32
|
+
doctor 설치 상태 자가진단 (MCP 등록, 훅, 데이터 무결성)
|
|
33
|
+
mcp Start the MCP server manually (for Claude Desktop)
|
|
34
|
+
help Show this help
|
|
35
|
+
|
|
36
|
+
Quick start:
|
|
37
|
+
npx oh-my-adhd init
|
|
38
|
+
# Restart Claude Code — done.
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
switch (cmd) {
|
|
43
|
+
case "init": {
|
|
44
|
+
ensureBrainDir();
|
|
45
|
+
|
|
46
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
47
|
+
|
|
48
|
+
// ── 1. ~/.claude.json — MCP 서버 등록 ──────────────────────────────
|
|
49
|
+
const claudeJsonPath = join(homedir(), ".claude.json");
|
|
50
|
+
let claudeJson = {};
|
|
51
|
+
if (existsSync(claudeJsonPath)) {
|
|
52
|
+
copyFileSync(claudeJsonPath, `${claudeJsonPath}.bak.${timestamp}`);
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(readFileSync(claudeJsonPath, "utf8"));
|
|
55
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
56
|
+
claudeJson = parsed;
|
|
57
|
+
} else {
|
|
58
|
+
console.error(`오류: ${claudeJsonPath} 형식이 올바르지 않습니다. 백업(${claudeJsonPath}.bak.${timestamp})을 확인하세요.`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error(`오류: ${claudeJsonPath} JSON 파싱 실패 — ${e}\n백업: ${claudeJsonPath}.bak.${timestamp}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
claudeJson.mcpServers = claudeJson.mcpServers || {};
|
|
67
|
+
claudeJson.mcpServers["oh-my-adhd"] = {
|
|
68
|
+
command: "npx",
|
|
69
|
+
args: ["oh-my-adhd", "mcp"],
|
|
70
|
+
};
|
|
71
|
+
writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n", "utf8");
|
|
72
|
+
console.log(`✓ MCP server registered in ${claudeJsonPath}`);
|
|
73
|
+
|
|
74
|
+
// ── 2. ~/.claude/settings.json — hooks 추가 ────────────────────────
|
|
75
|
+
const claudeDir = join(homedir(), ".claude");
|
|
76
|
+
if (!existsSync(claudeDir)) mkdirSync(claudeDir, { recursive: true });
|
|
77
|
+
|
|
78
|
+
const settingsPath = join(claudeDir, "settings.json");
|
|
79
|
+
let settings = {};
|
|
80
|
+
if (existsSync(settingsPath)) {
|
|
81
|
+
copyFileSync(settingsPath, `${settingsPath}.bak.${timestamp}`);
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
84
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
85
|
+
settings = parsed;
|
|
86
|
+
} else {
|
|
87
|
+
console.error(`오류: ${settingsPath} 형식이 올바르지 않습니다. 백업을 확인하세요.`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error(`오류: ${settingsPath} JSON 파싱 실패 — ${e}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
settings.hooks = settings.hooks || {};
|
|
97
|
+
|
|
98
|
+
// SessionStart: wiki_recall (mcp_tool — actually fires the tool, not just echo)
|
|
99
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart || [];
|
|
100
|
+
const alreadyHasRecall = settings.hooks.SessionStart.some((entry) =>
|
|
101
|
+
Array.isArray(entry.hooks) && entry.hooks.some(
|
|
102
|
+
(h) => h.type === "mcp_tool" && h.server === "oh-my-adhd" && h.tool === "wiki_recall"
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
if (!alreadyHasRecall) {
|
|
106
|
+
settings.hooks.SessionStart.push({
|
|
107
|
+
hooks: [
|
|
108
|
+
{
|
|
109
|
+
type: "mcp_tool",
|
|
110
|
+
server: "oh-my-adhd",
|
|
111
|
+
tool: "wiki_recall",
|
|
112
|
+
input: { limit: 5 },
|
|
113
|
+
statusMessage: "어제 어디까지 했더라...",
|
|
114
|
+
timeout: 15,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
type: "command",
|
|
118
|
+
// Write session-start timestamp so Stop hook knows if a dump happened this session
|
|
119
|
+
command: `node -e "const{writeFileSync,mkdirSync}=require('fs'),{join}=require('path'),{homedir}=require('os');const d=process.env.OH_MY_ADHD_DIR||join(homedir(),'.oh-my-adhd');try{mkdirSync(d,{recursive:true})}catch{}writeFileSync(join(d,'.session-start'),String(Date.now()))"`,
|
|
120
|
+
timeout: 5,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Stop: enforce wiki_dump via blocking hook (outputs {"decision":"block",...} if no dump happened)
|
|
127
|
+
settings.hooks.Stop = settings.hooks.Stop || [];
|
|
128
|
+
const alreadyHasStop = settings.hooks.Stop.some((entry) =>
|
|
129
|
+
Array.isArray(entry.hooks) && entry.hooks.some(
|
|
130
|
+
(h) => typeof h.command === "string" && h.command.includes("stop-hook.mjs")
|
|
131
|
+
)
|
|
132
|
+
);
|
|
133
|
+
if (!alreadyHasStop) {
|
|
134
|
+
// Remove legacy echo-only Stop hooks added by older versions of oh-my-adhd
|
|
135
|
+
settings.hooks.Stop = settings.hooks.Stop.filter((entry) =>
|
|
136
|
+
!Array.isArray(entry.hooks) || !entry.hooks.some(
|
|
137
|
+
(h) => typeof h.command === "string" && h.command.includes("wiki_dump") && h.command.startsWith("echo")
|
|
138
|
+
)
|
|
139
|
+
);
|
|
140
|
+
settings.hooks.Stop.push({
|
|
141
|
+
hooks: [
|
|
142
|
+
{
|
|
143
|
+
type: "command",
|
|
144
|
+
command: `node "${join(PROJECT_DIR, "scripts/stop-hook.mjs")}"`,
|
|
145
|
+
timeout: 10,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
152
|
+
console.log(`✓ Hooks registered in ${settingsPath}`);
|
|
153
|
+
|
|
154
|
+
console.log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
155
|
+
console.log(" ✓ Brain " + BRAIN_DIR);
|
|
156
|
+
console.log(" ✓ MCP oh-my-adhd 등록됨");
|
|
157
|
+
console.log(" ✓ Hooks SessionStart + Stop");
|
|
158
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
|
159
|
+
|
|
160
|
+
// ── 3. Interactive seed — user's own first memory ─────────────────────
|
|
161
|
+
const threadsDir = join(BRAIN_DIR, "threads");
|
|
162
|
+
const manifestPath = join(threadsDir, ".manifest.json");
|
|
163
|
+
const existingManifest = existsSync(manifestPath)
|
|
164
|
+
? JSON.parse(readFileSync(manifestPath, "utf8"))
|
|
165
|
+
: [];
|
|
166
|
+
|
|
167
|
+
if (existingManifest.length === 0) {
|
|
168
|
+
const readline = await import("readline");
|
|
169
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
170
|
+
const question = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
171
|
+
|
|
172
|
+
let userTask = "", userBlocker = "", userNextStep = "";
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
if (!process.stdin.isTTY) {
|
|
176
|
+
rl.close();
|
|
177
|
+
} else {
|
|
178
|
+
console.log("지금 작업 중인 게 뭐야? (한 줄, 엔터로 건너뛰기)");
|
|
179
|
+
userTask = await question("> ");
|
|
180
|
+
if (userTask.trim()) {
|
|
181
|
+
console.log("막힌 거 있어? (없으면 엔터)");
|
|
182
|
+
userBlocker = await question("> ");
|
|
183
|
+
console.log("다음 한 발자국은? (없으면 엔터)");
|
|
184
|
+
userNextStep = await question("> ");
|
|
185
|
+
}
|
|
186
|
+
rl.close();
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
rl.close();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const seedId = randomUUID();
|
|
193
|
+
const seedTs = new Date().toISOString();
|
|
194
|
+
|
|
195
|
+
if (userTask.trim()) {
|
|
196
|
+
const contentLines = [`요약: ${userTask.trim()}`];
|
|
197
|
+
if (userNextStep.trim()) contentLines.push(`다음할것: ${userNextStep.trim()}`);
|
|
198
|
+
if (userBlocker.trim()) contentLines.push(`막힌것: ${userBlocker.trim()}`);
|
|
199
|
+
|
|
200
|
+
const seedContent = [
|
|
201
|
+
`# ${userTask.trim().slice(0, 60)}`,
|
|
202
|
+
``,
|
|
203
|
+
`_created: ${seedTs}_`,
|
|
204
|
+
``,
|
|
205
|
+
`---`,
|
|
206
|
+
`**${seedTs}**`,
|
|
207
|
+
``,
|
|
208
|
+
...contentLines,
|
|
209
|
+
].join("\n");
|
|
210
|
+
|
|
211
|
+
writeFileSync(join(threadsDir, `${seedId}.md`), seedContent, "utf8");
|
|
212
|
+
writeFileSync(manifestPath, JSON.stringify(
|
|
213
|
+
[{
|
|
214
|
+
id: seedId,
|
|
215
|
+
title: userTask.trim().slice(0, 40),
|
|
216
|
+
updatedAt: seedTs,
|
|
217
|
+
is_open: true,
|
|
218
|
+
last_action: contentLines.join(" "),
|
|
219
|
+
next_action: userNextStep.trim().slice(0, 120),
|
|
220
|
+
blocker: userBlocker.trim().slice(0, 120),
|
|
221
|
+
capture_count: 1,
|
|
222
|
+
}],
|
|
223
|
+
null, 2
|
|
224
|
+
) + "\n", "utf8");
|
|
225
|
+
|
|
226
|
+
console.log(`\n✓ 첫 기억 심었어.\n`);
|
|
227
|
+
|
|
228
|
+
// Preview: show exactly what wiki_recall will output next session
|
|
229
|
+
console.log("━━━ 다음번 Claude Code 열면 이게 첫 화면이야 ━━━\n");
|
|
230
|
+
console.log("## 어제 멈춘 곳\n");
|
|
231
|
+
console.log(`> 🔴 **${userTask.trim().slice(0, 50)}** — 방금 전`);
|
|
232
|
+
if (userNextStep.trim()) console.log(`> → 다음: ${userNextStep.trim().slice(0, 80)}`);
|
|
233
|
+
if (userBlocker.trim()) console.log(`> ⛔ 막힌것: ${userBlocker.trim().slice(0, 80)}`);
|
|
234
|
+
console.log(`>`);
|
|
235
|
+
console.log(`> 이어서 갈까? (thread: \`${seedId.slice(0, 8)}...\`)`);
|
|
236
|
+
console.log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
237
|
+
console.log("↑ 이게 매일 아침 Claude Code 열 때마다 자동으로 뜬다. 약속.\n");
|
|
238
|
+
} else {
|
|
239
|
+
const seedContent = [
|
|
240
|
+
`# 첫 기억 (예시 — 지워도 됨)`,
|
|
241
|
+
``,
|
|
242
|
+
`_created: ${seedTs}_`,
|
|
243
|
+
``,
|
|
244
|
+
`---`,
|
|
245
|
+
`**${seedTs}**`,
|
|
246
|
+
``,
|
|
247
|
+
`결정: [이번 대화에서 확정된 것]`,
|
|
248
|
+
`막힌것: [이미 시도해서 안 된 것 — 다음 세션 반복 방지]`,
|
|
249
|
+
`다음할것: [지금 멈춘 시점의 다음 액션]`,
|
|
250
|
+
`요약: wiki_dump 형식 예시 — 이 스레드에 덮어써도 됨`,
|
|
251
|
+
].join("\n");
|
|
252
|
+
|
|
253
|
+
writeFileSync(join(threadsDir, `${seedId}.md`), seedContent, "utf8");
|
|
254
|
+
writeFileSync(manifestPath, JSON.stringify(
|
|
255
|
+
[{ id: seedId, title: "첫 기억 (예시)", updatedAt: seedTs, is_open: true, last_action: "다음할것: wiki_dump 형식으로 첫 작업 등록", capture_count: 1 }],
|
|
256
|
+
null, 2
|
|
257
|
+
) + "\n", "utf8");
|
|
258
|
+
|
|
259
|
+
console.log(`✓ 예시 기억 심어뒀어. wiki_dump로 진짜 작업을 덮어써.`);
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
console.log(`✓ 기존 기억 보존됨 (${existingManifest.length}개 스레드)`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
266
|
+
console.log(" 이제 Claude Code 재시작 한 번만 하면 돼.");
|
|
267
|
+
console.log("");
|
|
268
|
+
console.log(" 까먹어도 괜찮아. 그게 이 도구의 일이야.");
|
|
269
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
case "doctor": {
|
|
274
|
+
const results = [];
|
|
275
|
+
|
|
276
|
+
// 1. Brain directory
|
|
277
|
+
if (existsSync(BRAIN_DIR)) {
|
|
278
|
+
results.push(`✓ Brain dir: ${BRAIN_DIR}`);
|
|
279
|
+
const versionFile = join(BRAIN_DIR, "VERSION");
|
|
280
|
+
const version = existsSync(versionFile) ? readFileSync(versionFile, "utf8").trim() : "unversioned";
|
|
281
|
+
results.push(` Schema version: ${version}`);
|
|
282
|
+
} else {
|
|
283
|
+
results.push(`✗ Brain dir 없음: ${BRAIN_DIR} — npx oh-my-adhd init 실행`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 2. Manifest
|
|
287
|
+
const manifestPath = join(BRAIN_DIR, "threads", ".manifest.json");
|
|
288
|
+
if (existsSync(manifestPath)) {
|
|
289
|
+
try {
|
|
290
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
291
|
+
results.push(`✓ Manifest: ${Array.isArray(manifest) ? manifest.length : "?"}개 스레드`);
|
|
292
|
+
} catch (e) {
|
|
293
|
+
results.push(`✗ Manifest 손상: ${e.message} — ${manifestPath}.corrupt.* 백업 확인`);
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
results.push("⚠ Manifest 없음 (첫 wiki_dump 시 자동 생성)");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 3. MCP registration
|
|
300
|
+
const claudeJsonPath = join(homedir(), ".claude.json");
|
|
301
|
+
if (existsSync(claudeJsonPath)) {
|
|
302
|
+
try {
|
|
303
|
+
const cfg = JSON.parse(readFileSync(claudeJsonPath, "utf8"));
|
|
304
|
+
const server = cfg?.mcpServers?.["oh-my-adhd"];
|
|
305
|
+
if (server) {
|
|
306
|
+
results.push(`✓ MCP 등록됨: command="${server.command}" args=${JSON.stringify(server.args)}`);
|
|
307
|
+
} else {
|
|
308
|
+
results.push("✗ MCP 미등록 — npx oh-my-adhd init 실행");
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
results.push("⚠ ~/.claude.json 파싱 불가");
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
results.push("✗ ~/.claude.json 없음 — npx oh-my-adhd init 실행");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 4. Hooks
|
|
318
|
+
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
319
|
+
if (existsSync(settingsPath)) {
|
|
320
|
+
try {
|
|
321
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
322
|
+
const hasRecall = (settings?.hooks?.SessionStart ?? []).some((entry) =>
|
|
323
|
+
Array.isArray(entry.hooks) && entry.hooks.some(
|
|
324
|
+
(h) => h.type === "mcp_tool" && h.server === "oh-my-adhd" && h.tool === "wiki_recall"
|
|
325
|
+
)
|
|
326
|
+
);
|
|
327
|
+
const hasStop = (settings?.hooks?.Stop ?? []).some((entry) =>
|
|
328
|
+
Array.isArray(entry.hooks) && entry.hooks.some(
|
|
329
|
+
(h) => typeof h.command === "string" && h.command.includes("stop-hook.mjs")
|
|
330
|
+
)
|
|
331
|
+
);
|
|
332
|
+
results.push(hasRecall ? "✓ SessionStart 훅 (wiki_recall)" : "✗ SessionStart 훅 없음 — npx oh-my-adhd init 실행");
|
|
333
|
+
results.push(hasStop ? "✓ Stop 훅 (블로킹 강제 저장)" : "✗ Stop 훅 없음 — npx oh-my-adhd init 실행");
|
|
334
|
+
} catch {
|
|
335
|
+
results.push("⚠ settings.json 파싱 불가");
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
results.push("✗ ~/.claude/settings.json 없음 — npx oh-my-adhd init 실행");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 5. Trash size
|
|
342
|
+
const trashPath = join(BRAIN_DIR, ".trash");
|
|
343
|
+
if (existsSync(trashPath)) {
|
|
344
|
+
const trashFiles = readdirSync(trashPath);
|
|
345
|
+
results.push(`ℹ Trash: ${trashFiles.length}개 파일 (수동 정리 가능: rm -rf ${trashPath})`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 6. Recent logs
|
|
349
|
+
const logPath = join(BRAIN_DIR, "logs", "brain.log");
|
|
350
|
+
if (existsSync(logPath)) {
|
|
351
|
+
const lines = readFileSync(logPath, "utf8").trim().split("\n");
|
|
352
|
+
const errors = lines.filter(l => l.includes("[ERROR]") || l.includes("[WARN]")).slice(-5);
|
|
353
|
+
if (errors.length > 0) {
|
|
354
|
+
results.push(`\n⚠ 최근 경고/에러 (최대 5개):`);
|
|
355
|
+
errors.forEach(l => results.push(" " + l));
|
|
356
|
+
} else {
|
|
357
|
+
results.push("✓ 로그: 에러 없음");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
console.log("oh-my-adhd doctor\n");
|
|
362
|
+
results.forEach(r => console.log(r));
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
case "mcp": {
|
|
367
|
+
const serverDist = join(PROJECT_DIR, "dist/mcp/mcp/server.js");
|
|
368
|
+
const serverSrc = join(PROJECT_DIR, "src/mcp/server.ts");
|
|
369
|
+
const [execCmd, execArgs] = existsSync(serverDist)
|
|
370
|
+
? [process.execPath, [serverDist]]
|
|
371
|
+
: ["npx", ["tsx", serverSrc]]; // fallback for dev (no build)
|
|
372
|
+
const proc = spawn(execCmd, execArgs, { cwd: PROJECT_DIR, stdio: "inherit" });
|
|
373
|
+
proc.on("error", (e) => { console.error("Failed to start MCP:", e.message); process.exit(1); });
|
|
374
|
+
proc.on("exit", (code) => process.exit(code ?? 0));
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
default: {
|
|
379
|
+
printHelp();
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|