oh-my-adhd 0.2.6 → 0.2.8
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/bin/oh-my-adhd.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFileSync } from "fs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFileSync, renameSync } from "fs";
|
|
4
4
|
import { join, dirname } from "path";
|
|
5
5
|
import { homedir } from "os";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
@@ -145,7 +145,9 @@ switch (cmd) {
|
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
const settingsTmp = `${settingsPath}.tmp.${randomUUID()}`;
|
|
149
|
+
writeFileSync(settingsTmp, JSON.stringify(settings, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
150
|
+
renameSync(settingsTmp, settingsPath);
|
|
149
151
|
console.log(`✓ Hooks registered in ${settingsPath}`);
|
|
150
152
|
|
|
151
153
|
console.log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
package/dist/mcp/lib/brain.js
CHANGED
|
@@ -202,8 +202,8 @@ export async function saveCapture(content, threadId) {
|
|
|
202
202
|
const idx = manifest.findIndex((m) => m.id === tid);
|
|
203
203
|
// Compute signal cache fields from new content
|
|
204
204
|
const stripped = stripGitSuffix(content).trim();
|
|
205
|
-
const is_open = OPEN_SIGNAL.test(stripped);
|
|
206
|
-
const is_done = DONE_SIGNAL.test(stripped)
|
|
205
|
+
const is_open = OPEN_SIGNAL.test(stripped) && !DONE_SIGNAL.test(stripped);
|
|
206
|
+
const is_done = DONE_SIGNAL.test(stripped);
|
|
207
207
|
const last_action = stripped.replace(/\n+/g, " ").slice(0, 160);
|
|
208
208
|
const next_action = extractFieldBrain(stripped, "다음할것").slice(0, 120);
|
|
209
209
|
const blocker = extractFieldBrain(stripped, "막힌것").slice(0, 120);
|
|
@@ -217,6 +217,8 @@ export async function saveCapture(content, threadId) {
|
|
|
217
217
|
else
|
|
218
218
|
manifest.push(meta);
|
|
219
219
|
await writeManifest(manifest.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()));
|
|
220
|
+
// Session-scoped dump marker for stop-hook
|
|
221
|
+
await fs.writeFile(path.join(BRAIN_DIR, ".last-dump"), String(Date.now()), "utf-8").catch(() => { });
|
|
220
222
|
});
|
|
221
223
|
return { capture, threadId: tid, title, skipped };
|
|
222
224
|
}
|
|
@@ -240,7 +242,7 @@ export async function getThreads() {
|
|
|
240
242
|
const scanCaptures = content.split(/\n---\n/).slice(1).filter((p) => p.trim());
|
|
241
243
|
const lastCapture = scanCaptures.at(-1) ?? "";
|
|
242
244
|
const fullText = lastCapture.replace(/^(?:_[^_\n]+_|\*\*[^*\n]+\*\*)\s*/m, "").trim();
|
|
243
|
-
const is_open = OPEN_SIGNAL.test(fullText);
|
|
245
|
+
const is_open = OPEN_SIGNAL.test(fullText) && !DONE_SIGNAL.test(fullText);
|
|
244
246
|
return {
|
|
245
247
|
id: tid,
|
|
246
248
|
title,
|
|
@@ -251,7 +253,7 @@ export async function getThreads() {
|
|
|
251
253
|
next_action: extractFieldBrain(fullText, "다음할것").slice(0, 120),
|
|
252
254
|
blocker: extractFieldBrain(fullText, "막힌것").slice(0, 120),
|
|
253
255
|
capture_count: scanCaptures.length,
|
|
254
|
-
is_done: DONE_SIGNAL.test(fullText)
|
|
256
|
+
is_done: DONE_SIGNAL.test(fullText),
|
|
255
257
|
};
|
|
256
258
|
}));
|
|
257
259
|
results
|
|
@@ -63,8 +63,15 @@ export function registerWikiDump(server) {
|
|
|
63
63
|
}
|
|
64
64
|
catch { /* dead-end check is best-effort, never crash */ }
|
|
65
65
|
}
|
|
66
|
+
// Dopamine streak: show today's save count when ≥2
|
|
67
|
+
if (!result.skipped && allThreads.length > 0) {
|
|
68
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
69
|
+
const todayCount = allThreads.filter(t => t.updatedAt?.startsWith(today)).length;
|
|
70
|
+
if (todayCount >= 2)
|
|
71
|
+
respLines[0] = `저장됨 ✓ (오늘 ${todayCount}번째 🔥)`;
|
|
72
|
+
}
|
|
66
73
|
// Only nag if user has NEVER successfully used structured schema — avoids repeated noise
|
|
67
|
-
const hasEverStructured = allThreads.some(t => t.next_action || t.blocker);
|
|
74
|
+
const hasEverStructured = allThreads.some(t => t.id === result.threadId && (t.next_action || t.blocker));
|
|
68
75
|
if (!isStructured && !result.skipped && !hasEverStructured) {
|
|
69
76
|
respLines.push("");
|
|
70
77
|
respLines.push("💡 다음번엔 이 형식으로 쓰면 다음 세션에서 더 잘 복원돼:");
|
|
@@ -38,10 +38,10 @@ export function registerWikiRecall(server) {
|
|
|
38
38
|
// Use stored next_action/blocker from manifest if available
|
|
39
39
|
next_action = t.next_action ?? "";
|
|
40
40
|
blocker = t.blocker ?? "";
|
|
41
|
-
const is_done = t.is_done !== undefined ? t.is_done :
|
|
41
|
+
const is_done = t.is_done !== undefined ? t.is_done : DONE_SIGNAL.test(last_action);
|
|
42
42
|
const gapHours = isNaN(new Date(t.updatedAt).getTime())
|
|
43
43
|
? null
|
|
44
|
-
: Math.round((Date.now() - new Date(t.updatedAt).getTime()) / 3600000);
|
|
44
|
+
: Math.max(0, Math.round((Date.now() - new Date(t.updatedAt).getTime()) / 3600000));
|
|
45
45
|
return {
|
|
46
46
|
threadId: t.id,
|
|
47
47
|
title: t.title,
|
|
@@ -63,15 +63,15 @@ export function registerWikiRecall(server) {
|
|
|
63
63
|
const captures = content.split(/\n---\n/).slice(1).filter((p) => p.trim());
|
|
64
64
|
const lastCapture = captures.at(-1) ?? "";
|
|
65
65
|
const fullText = lastCapture.replace(/^(?:_[^_\n]+_|\*\*[^*\n]+\*\*)\s*/m, "").trim();
|
|
66
|
-
is_open = OPEN_SIGNAL.test(fullText);
|
|
67
|
-
const isDone = DONE_SIGNAL.test(fullText)
|
|
66
|
+
is_open = OPEN_SIGNAL.test(fullText) && !DONE_SIGNAL.test(fullText);
|
|
67
|
+
const isDone = DONE_SIGNAL.test(fullText);
|
|
68
68
|
last_action = fullText.replace(/\n+/g, " ").slice(0, 160);
|
|
69
69
|
next_action = extractFieldBrain(fullText, "다음할것").slice(0, 120);
|
|
70
70
|
blocker = extractFieldBrain(fullText, "막힌것").slice(0, 120);
|
|
71
71
|
capture_count = captures.length;
|
|
72
72
|
const gapHours = isNaN(new Date(t.updatedAt).getTime())
|
|
73
73
|
? null
|
|
74
|
-
: Math.round((Date.now() - new Date(t.updatedAt).getTime()) / 3600000);
|
|
74
|
+
: Math.max(0, Math.round((Date.now() - new Date(t.updatedAt).getTime()) / 3600000));
|
|
75
75
|
// Back-fill manifest cache so stop hook sees correct is_open next time
|
|
76
76
|
updateManifestEntry(t.id, {
|
|
77
77
|
is_open,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getThreads, getThread, OPEN_SIGNAL, extractFieldBrain } from "../../lib/brain.js";
|
|
3
3
|
export function registerWikiUnstick(server) {
|
|
4
|
-
server.tool("wiki_unstick", "막혀서 무력감 느낄 때 호출.
|
|
4
|
+
server.tool("wiki_unstick", "막혀서 무력감 느낄 때 호출. 막힌 컨텍스트와 dead-end 목록을 수집해서 반환한다. **이 툴을 호출한 후 Claude는 반드시** 반환된 컨텍스트를 보고 에너지 레벨에 맞는 구체적인 스텝 하나를 한 줄로 직접 제안해야 한다. 제안은 행동 동사로 시작하고, ⛔ 목록에 있는 것은 절대 포함하지 않는다.", {
|
|
5
5
|
task: z.string().max(2000).optional().describe("막힌 태스크 직접 설명 (없으면 최근 미완료 스레드 자동 감지)"),
|
|
6
6
|
energy: z.enum(["low", "medium", "high"]).optional().default("medium").describe("현재 집중력/에너지 수준 (low=2분짜리, medium=5분짜리, high=15분짜리)"),
|
|
7
7
|
}, async ({ task, energy }) => {
|
|
@@ -51,10 +51,11 @@ export function registerWikiUnstick(server) {
|
|
|
51
51
|
}
|
|
52
52
|
// Collect dead-ends from all open threads (not just the chosen one)
|
|
53
53
|
const allOpenThreads = threads.filter(t => t.is_open && t.id !== chosenThread.id);
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
const otContents = await Promise.all(allOpenThreads.slice(0, 10).map(ot => getThread(ot.id)));
|
|
55
|
+
allOpenThreads.slice(0, 10).forEach((ot, i) => {
|
|
56
|
+
const otContent = otContents[i];
|
|
56
57
|
if (!otContent)
|
|
57
|
-
|
|
58
|
+
return;
|
|
58
59
|
const otCaptures = otContent.split(/\n---\n/).slice(1).filter(p => p.trim());
|
|
59
60
|
const otLast = otCaptures.at(-1) ?? "";
|
|
60
61
|
const otText = otLast.replace(/^(?:_[^_\n]+_|\*\*[^*\n]+\*\*)\s*/m, "").trim();
|
|
@@ -63,7 +64,7 @@ export function registerWikiUnstick(server) {
|
|
|
63
64
|
if (otBlocker && !deadEnds.some(d => d.toLowerCase().replace(/\s+/g, " ").includes(otBlockerKey))) {
|
|
64
65
|
crossThreadBlockers.push(`[${ot.title?.slice(0, 20) ?? "다른 스레드"}] ${otBlocker}`);
|
|
65
66
|
}
|
|
66
|
-
}
|
|
67
|
+
});
|
|
67
68
|
}
|
|
68
69
|
if (!task && !targetTitle && !targetContext) {
|
|
69
70
|
return { content: [{ type: "text", text: "막힌 대상을 찾지 못했어. 둘 중 하나로 다시 호출해줘:\n" +
|
|
@@ -100,7 +101,7 @@ export function registerWikiUnstick(server) {
|
|
|
100
101
|
lines.push("", "### 블로커");
|
|
101
102
|
blockers.forEach((b) => lines.push(`- ${b}`));
|
|
102
103
|
}
|
|
103
|
-
lines.push("", "---",
|
|
104
|
+
lines.push("", "---", `_제안 기준: ${taskSize}짜리 / ${taskDetail}_`);
|
|
104
105
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
105
106
|
}
|
|
106
107
|
catch (e) {
|
package/package.json
CHANGED
package/scripts/stop-hook.mjs
CHANGED
|
@@ -7,11 +7,18 @@ import { homedir } from "os";
|
|
|
7
7
|
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
|
const SESSION_START_FILE = join(BRAIN_DIR, ".session-start");
|
|
10
|
+
const LAST_DUMP_FILE = join(BRAIN_DIR, ".last-dump");
|
|
10
11
|
|
|
11
12
|
try {
|
|
12
13
|
const sessionStartMs = parseInt(readFileSync(SESSION_START_FILE, "utf-8").trim(), 10);
|
|
13
14
|
if (isNaN(sessionStartMs)) process.exit(0); // no marker — don't block
|
|
14
15
|
|
|
16
|
+
// Session-scoped check: did wiki_dump run after this session started?
|
|
17
|
+
try {
|
|
18
|
+
const lastDumpMs = parseInt(readFileSync(LAST_DUMP_FILE, "utf-8").trim(), 10);
|
|
19
|
+
if (!isNaN(lastDumpMs) && lastDumpMs > sessionStartMs) process.exit(0);
|
|
20
|
+
} catch { /* .last-dump missing = no dump this session */ }
|
|
21
|
+
|
|
15
22
|
let manifest;
|
|
16
23
|
try {
|
|
17
24
|
manifest = JSON.parse(readFileSync(MANIFEST, "utf-8"));
|
|
@@ -19,19 +26,18 @@ try {
|
|
|
19
26
|
process.exit(0); // no manifest — nothing to protect
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
// Allow stop if any dump happened after session start
|
|
23
|
-
const latestDump = manifest.reduce((max, t) => {
|
|
24
|
-
const ts = new Date(t.updatedAt).getTime();
|
|
25
|
-
return ts > max ? ts : max;
|
|
26
|
-
}, 0);
|
|
27
|
-
if (latestDump > sessionStartMs) process.exit(0);
|
|
28
|
-
|
|
29
29
|
// Block only if there are open threads worth saving
|
|
30
30
|
const openThreads = manifest.filter(t => t.is_open);
|
|
31
31
|
if (openThreads.length > 0) {
|
|
32
32
|
const top = openThreads[0];
|
|
33
|
-
|
|
34
|
-
const
|
|
33
|
+
// Sanitize: strip control chars and injection-shaped strings before embedding in model-facing text
|
|
34
|
+
const sanitize = (s, max) => String(s ?? "")
|
|
35
|
+
.replace(/[\x00-\x1F\x7F]/g, " ")
|
|
36
|
+
.replace(/[`$<>]/g, "")
|
|
37
|
+
.replace(/\bignore (all|previous)\b/gi, "[redacted]")
|
|
38
|
+
.slice(0, max);
|
|
39
|
+
const title = sanitize(top.title ?? "진행중인 작업", 40);
|
|
40
|
+
const nextHint = top.next_action ? `\n→ 다음할것: ${sanitize(top.next_action, 60)}` : "";
|
|
35
41
|
process.stdout.write(JSON.stringify({
|
|
36
42
|
decision: "block",
|
|
37
43
|
reason: `저장 없이 끝내려고? "${title}" 스레드가 열려있어.${nextHint}\nwiki_dump로 결정/막힌것/다음할것 저장하고 끝내자.`,
|