oh-my-adhd 0.2.5 → 0.2.7

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.
@@ -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
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
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━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
@@ -64,7 +64,7 @@ export function registerWikiDump(server) {
64
64
  catch { /* dead-end check is best-effort, never crash */ }
65
65
  }
66
66
  // Only nag if user has NEVER successfully used structured schema — avoids repeated noise
67
- const hasEverStructured = allThreads.some(t => t.next_action || t.blocker);
67
+ const hasEverStructured = allThreads.some(t => t.id === result.threadId && (t.next_action || t.blocker));
68
68
  if (!isStructured && !result.skipped && !hasEverStructured) {
69
69
  respLines.push("");
70
70
  respLines.push("💡 다음번엔 이 형식으로 쓰면 다음 세션에서 더 잘 복원돼:");
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { getThreads, getThread, OPEN_SIGNAL, DONE_SIGNAL, extractFieldBrain } from "../../lib/brain.js";
2
+ import { getThreads, getThread, OPEN_SIGNAL, DONE_SIGNAL, extractFieldBrain, updateManifestEntry } from "../../lib/brain.js";
3
3
  import { runConsolidationIfDue } from "../../lib/consolidate.js";
4
4
  import { git } from "../utils.js";
5
5
  export function registerWikiRecall(server) {
@@ -41,7 +41,7 @@ export function registerWikiRecall(server) {
41
41
  const is_done = t.is_done !== undefined ? t.is_done : (DONE_SIGNAL.test(last_action) && !is_open);
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,
@@ -71,7 +71,17 @@ export function registerWikiRecall(server) {
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
+ // Back-fill manifest cache so stop hook sees correct is_open next time
76
+ updateManifestEntry(t.id, {
77
+ is_open,
78
+ is_done: isDone,
79
+ capture_count,
80
+ last_action: last_action.slice(0, 160),
81
+ next_action: next_action || undefined,
82
+ blocker: blocker || undefined,
83
+ title,
84
+ }).catch(() => { });
75
85
  return {
76
86
  threadId: t.id,
77
87
  title,
@@ -136,8 +146,16 @@ export function registerWikiRecall(server) {
136
146
  // Lead entry — the most important thread, formatted as a direct question
137
147
  const top = sorted[0];
138
148
  const others = sorted.slice(1);
149
+ // Stale-open threads: split by depth of abandonment
150
+ const GHOST_THRESHOLD_H = 336; // 2 weeks
151
+ const topIsGhost = top.is_open && top.status === "stale" && (top.gap_hours ?? 0) >= GHOST_THRESHOLD_H;
139
152
  // Lead section header
140
- if (top.is_open) {
153
+ if (topIsGhost) {
154
+ lines.push("## 💀 아직도 할 거야?\n");
155
+ const weeks = top.gap_hours !== null ? Math.floor(top.gap_hours / 168) : null;
156
+ lines.push(`_${weeks ? `${weeks}주` : "오랫동안"} 못 봤어. 계속할 건지, 정리할 건지 결정해줘._\n`);
157
+ }
158
+ else if (top.is_open) {
141
159
  lines.push("## 어제 멈춘 곳\n");
142
160
  }
143
161
  else {
@@ -154,16 +172,18 @@ export function registerWikiRecall(server) {
154
172
  if (top.blocker)
155
173
  lines.push(`> ⛔ 막힌것: ${top.blocker}`);
156
174
  // Call to action
157
- if (top.is_open) {
158
- lines.push(`>`);
175
+ lines.push(`>`);
176
+ if (topIsGhost) {
177
+ lines.push(`> → 이어서? \`wiki_dump({ threadId: "${top.threadId}", content: "계속" })\``);
178
+ lines.push(`> → 정리? \`wiki_dump({ threadId: "${top.threadId}", content: "결정: 이 프로젝트 종료" })\``);
179
+ }
180
+ else if (top.is_open) {
159
181
  lines.push(`> 이어서 갈까? (thread: \`${top.threadId}\`)`);
160
182
  }
161
183
  else {
162
- lines.push(`>`);
163
184
  lines.push(`> thread: \`${top.threadId}\``);
164
185
  }
165
186
  // Stale-open threads: split by depth of abandonment
166
- const GHOST_THRESHOLD_H = 336; // 2 weeks — "진짜 잊혀진 것"
167
187
  const ghostThreads = others.filter(t => t.is_open && t.status === "stale" && (t.gap_hours ?? 0) >= GHOST_THRESHOLD_H);
168
188
  const forgottenThreads = others.filter(t => t.is_open && t.status === "stale" && (t.gap_hours ?? 0) < GHOST_THRESHOLD_H);
169
189
  const activeOthers = others.filter(t => !(t.is_open && t.status === "stale"));
@@ -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", "막혀서 무력감 느낄 때 호출. 이미 시도해서 것은 제외하고, 에너지 레벨에 맞는 가장 작은발자국만 알려준다. low=2분, medium=5분, high=15분.", {
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
- for (const ot of allOpenThreads.slice(0, 10)) {
55
- const otContent = await getThread(ot.id);
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
- continue;
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("", "---", "", `위 내용을 보고 **지금 당장 할 수 있는 ${taskSize}짜리 스텝 하나**만 알려줘.`, `조건: 행동 동사로 시작 / ${taskDetail} / ⛔ 목록에 있는 것 절대 제안 금지 / 한 줄`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-adhd",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
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": {
@@ -30,8 +30,14 @@ try {
30
30
  const openThreads = manifest.filter(t => t.is_open);
31
31
  if (openThreads.length > 0) {
32
32
  const top = openThreads[0];
33
- const title = (top.title ?? "진행중인 작업").slice(0, 40);
34
- const nextHint = top.next_action ? `\n→ 다음할것: ${top.next_action.slice(0, 60)}` : "";
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로 결정/막힌것/다음할것 저장하고 끝내자.`,