synabun 1.3.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.
Files changed (187) hide show
  1. package/CHANGELOG.md +665 -0
  2. package/LICENSE +191 -0
  3. package/README.md +853 -0
  4. package/hooks/claude-code/post-plan.mjs +391 -0
  5. package/hooks/claude-code/post-remember.mjs +239 -0
  6. package/hooks/claude-code/pre-compact.mjs +241 -0
  7. package/hooks/claude-code/pre-websearch.mjs +90 -0
  8. package/hooks/claude-code/prompt-submit.mjs +861 -0
  9. package/hooks/claude-code/session-start.mjs +494 -0
  10. package/hooks/claude-code/shared.mjs +368 -0
  11. package/hooks/claude-code/stop.mjs +646 -0
  12. package/hooks/claude-code/test-user-learning.mjs +531 -0
  13. package/mcp-server/package.json +38 -0
  14. package/mcp-server/run.mjs +10 -0
  15. package/mcp-server/src/config.ts +70 -0
  16. package/mcp-server/src/http.ts +93 -0
  17. package/mcp-server/src/index.ts +203 -0
  18. package/mcp-server/src/preload-http.ts +25 -0
  19. package/mcp-server/src/preload.ts +29 -0
  20. package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
  21. package/mcp-server/src/services/categories.ts +429 -0
  22. package/mcp-server/src/services/discord.ts +702 -0
  23. package/mcp-server/src/services/file-checksums.ts +33 -0
  24. package/mcp-server/src/services/local-embeddings.ts +77 -0
  25. package/mcp-server/src/services/neural-interface.ts +344 -0
  26. package/mcp-server/src/services/sqlite.ts +823 -0
  27. package/mcp-server/src/tools/browser-advanced.ts +123 -0
  28. package/mcp-server/src/tools/browser-interact.ts +255 -0
  29. package/mcp-server/src/tools/browser-navigate.ts +77 -0
  30. package/mcp-server/src/tools/browser-observe.ts +1330 -0
  31. package/mcp-server/src/tools/browser.ts +107 -0
  32. package/mcp-server/src/tools/card-tools.ts +193 -0
  33. package/mcp-server/src/tools/card.ts +22 -0
  34. package/mcp-server/src/tools/category.ts +379 -0
  35. package/mcp-server/src/tools/discord-channel.ts +274 -0
  36. package/mcp-server/src/tools/discord-guild.ts +188 -0
  37. package/mcp-server/src/tools/discord-member.ts +158 -0
  38. package/mcp-server/src/tools/discord-message.ts +231 -0
  39. package/mcp-server/src/tools/discord-onboarding.ts +228 -0
  40. package/mcp-server/src/tools/discord-role.ts +209 -0
  41. package/mcp-server/src/tools/discord-thread.ts +110 -0
  42. package/mcp-server/src/tools/discord-webhook.ts +176 -0
  43. package/mcp-server/src/tools/discord.ts +40 -0
  44. package/mcp-server/src/tools/forget.ts +38 -0
  45. package/mcp-server/src/tools/git-tools.ts +141 -0
  46. package/mcp-server/src/tools/git.ts +13 -0
  47. package/mcp-server/src/tools/image-tools.ts +98 -0
  48. package/mcp-server/src/tools/image.ts +17 -0
  49. package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
  50. package/mcp-server/src/tools/leonardo.ts +28 -0
  51. package/mcp-server/src/tools/loop.ts +348 -0
  52. package/mcp-server/src/tools/memories.ts +118 -0
  53. package/mcp-server/src/tools/recall.ts +323 -0
  54. package/mcp-server/src/tools/reflect.ts +145 -0
  55. package/mcp-server/src/tools/remember.ts +115 -0
  56. package/mcp-server/src/tools/response.ts +26 -0
  57. package/mcp-server/src/tools/restore.ts +34 -0
  58. package/mcp-server/src/tools/sync.ts +114 -0
  59. package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
  60. package/mcp-server/src/tools/tictactoe.ts +13 -0
  61. package/mcp-server/src/tools/utils.ts +25 -0
  62. package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
  63. package/mcp-server/src/tools/whiteboard.ts +22 -0
  64. package/mcp-server/src/tui.ts +940 -0
  65. package/mcp-server/src/types.ts +85 -0
  66. package/mcp-server/tsconfig.json +19 -0
  67. package/neural-interface/package.json +27 -0
  68. package/neural-interface/public/apple-touch-icon.png +0 -0
  69. package/neural-interface/public/category-logos/claude.svg +1 -0
  70. package/neural-interface/public/category-logos/gemini-color.svg +1 -0
  71. package/neural-interface/public/category-logos/openai.svg +1 -0
  72. package/neural-interface/public/category-logos/synabun.png +0 -0
  73. package/neural-interface/public/claude-chat.css +878 -0
  74. package/neural-interface/public/claude-chat.html +92 -0
  75. package/neural-interface/public/claude-chat.js +1168 -0
  76. package/neural-interface/public/favicon-16x16.png +0 -0
  77. package/neural-interface/public/favicon-32x32.png +0 -0
  78. package/neural-interface/public/gallery.svg +6 -0
  79. package/neural-interface/public/icon-192.png +0 -0
  80. package/neural-interface/public/icon-512.png +0 -0
  81. package/neural-interface/public/index-legacy.html +14677 -0
  82. package/neural-interface/public/index.html +212 -0
  83. package/neural-interface/public/index2d-legacy.html +8193 -0
  84. package/neural-interface/public/index2d.html +127 -0
  85. package/neural-interface/public/invite.html +351 -0
  86. package/neural-interface/public/leonardoai.svg +1 -0
  87. package/neural-interface/public/logo-synabun.png +0 -0
  88. package/neural-interface/public/logoHD.png +0 -0
  89. package/neural-interface/public/manifest.json +14 -0
  90. package/neural-interface/public/offline.html +299 -0
  91. package/neural-interface/public/onboarding.html +2335 -0
  92. package/neural-interface/public/openclaw-logo-text.png +0 -0
  93. package/neural-interface/public/pin-svgrepo-com.svg +25 -0
  94. package/neural-interface/public/sessionicon.svg +8 -0
  95. package/neural-interface/public/shared/ansi-parser.js +305 -0
  96. package/neural-interface/public/shared/api.js +1043 -0
  97. package/neural-interface/public/shared/colors.js +153 -0
  98. package/neural-interface/public/shared/constants.js +141 -0
  99. package/neural-interface/public/shared/html-shell.js +777 -0
  100. package/neural-interface/public/shared/html-term-renderer.js +1022 -0
  101. package/neural-interface/public/shared/i18n.js +147 -0
  102. package/neural-interface/public/shared/registry.js +82 -0
  103. package/neural-interface/public/shared/state.js +86 -0
  104. package/neural-interface/public/shared/storage.js +150 -0
  105. package/neural-interface/public/shared/styles.css +21212 -0
  106. package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
  107. package/neural-interface/public/shared/ui-bookmarks.js +231 -0
  108. package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
  109. package/neural-interface/public/shared/ui-command-runner.js +534 -0
  110. package/neural-interface/public/shared/ui-cost-widget.js +602 -0
  111. package/neural-interface/public/shared/ui-detail.js +1454 -0
  112. package/neural-interface/public/shared/ui-explorer.js +1356 -0
  113. package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
  114. package/neural-interface/public/shared/ui-games.js +411 -0
  115. package/neural-interface/public/shared/ui-help.js +107 -0
  116. package/neural-interface/public/shared/ui-image-gallery.js +461 -0
  117. package/neural-interface/public/shared/ui-invite.js +519 -0
  118. package/neural-interface/public/shared/ui-keybinds.js +452 -0
  119. package/neural-interface/public/shared/ui-layouts.js +297 -0
  120. package/neural-interface/public/shared/ui-link.js +731 -0
  121. package/neural-interface/public/shared/ui-loading.js +255 -0
  122. package/neural-interface/public/shared/ui-menubar.js +499 -0
  123. package/neural-interface/public/shared/ui-multiselect.js +93 -0
  124. package/neural-interface/public/shared/ui-navbar.js +222 -0
  125. package/neural-interface/public/shared/ui-notifications.js +287 -0
  126. package/neural-interface/public/shared/ui-panels.js +309 -0
  127. package/neural-interface/public/shared/ui-resume.js +653 -0
  128. package/neural-interface/public/shared/ui-search.js +145 -0
  129. package/neural-interface/public/shared/ui-sessions.js +531 -0
  130. package/neural-interface/public/shared/ui-settings.js +5987 -0
  131. package/neural-interface/public/shared/ui-sidebar.js +1905 -0
  132. package/neural-interface/public/shared/ui-skills.js +2805 -0
  133. package/neural-interface/public/shared/ui-stats.js +38 -0
  134. package/neural-interface/public/shared/ui-sync.js +201 -0
  135. package/neural-interface/public/shared/ui-terminal.js +6385 -0
  136. package/neural-interface/public/shared/ui-tooltip.js +123 -0
  137. package/neural-interface/public/shared/ui-trash.js +439 -0
  138. package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
  139. package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
  140. package/neural-interface/public/shared/ui-tutorial.js +1117 -0
  141. package/neural-interface/public/shared/ui-update.js +244 -0
  142. package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
  143. package/neural-interface/public/shared/ui-workspaces.js +722 -0
  144. package/neural-interface/public/shared/utils.js +360 -0
  145. package/neural-interface/public/shared/vterm-buffer.js +1102 -0
  146. package/neural-interface/public/sw.js +52 -0
  147. package/neural-interface/public/synabun.png +0 -0
  148. package/neural-interface/public/toolbar-album.svg +8 -0
  149. package/neural-interface/public/toolbar-question.svg +6 -0
  150. package/neural-interface/public/toolbar-sessions.svg +6 -0
  151. package/neural-interface/public/variant/2d/background.js +113 -0
  152. package/neural-interface/public/variant/2d/context-menu.js +133 -0
  153. package/neural-interface/public/variant/2d/gfx.js +45 -0
  154. package/neural-interface/public/variant/2d/graph.js +1635 -0
  155. package/neural-interface/public/variant/2d/hulls.js +207 -0
  156. package/neural-interface/public/variant/2d/lasso.js +135 -0
  157. package/neural-interface/public/variant/2d/layout.js +594 -0
  158. package/neural-interface/public/variant/2d/main.js +521 -0
  159. package/neural-interface/public/variant/2d/minimap.js +312 -0
  160. package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
  161. package/neural-interface/public/variant/3d/background.js +884 -0
  162. package/neural-interface/public/variant/3d/camera.js +661 -0
  163. package/neural-interface/public/variant/3d/gfx.js +116 -0
  164. package/neural-interface/public/variant/3d/graph.js +2399 -0
  165. package/neural-interface/public/variant/3d/main.js +500 -0
  166. package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
  167. package/neural-interface/scripts/rebuild-pty.js +100 -0
  168. package/neural-interface/server.js +15000 -0
  169. package/neural-interface/templates/CLAUDE-template.md +211 -0
  170. package/package.json +53 -0
  171. package/postinstall.js +135 -0
  172. package/setup.js +260 -0
  173. package/skills/leonardo/SKILL.md +159 -0
  174. package/skills/leonardo/icon.svg +1 -0
  175. package/skills/leonardo/modules/browser-guide.md +378 -0
  176. package/skills/leonardo/modules/image-prompter.md +277 -0
  177. package/skills/leonardo/modules/model-advisor.md +100 -0
  178. package/skills/leonardo/modules/prompt-library.md +110 -0
  179. package/skills/leonardo/modules/style-guide.md +186 -0
  180. package/skills/leonardo/modules/video-prompter.md +311 -0
  181. package/skills/synabun/SKILL.md +185 -0
  182. package/skills/synabun/icon.png +0 -0
  183. package/skills/synabun/modules/audit.md +374 -0
  184. package/skills/synabun/modules/changelog.md +194 -0
  185. package/skills/synabun/modules/idea.md +144 -0
  186. package/skills/synabun/modules/memorize.md +188 -0
  187. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,391 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SynaBun PostToolUse Hook — Plan Storage
5
+ *
6
+ * Matches: ^(Enter|Exit)PlanMode$
7
+ *
8
+ * EnterPlanMode: Injects a reminder to use AskUserQuestion for
9
+ * clarifications instead of plain-text questions.
10
+ *
11
+ * ExitPlanMode: When Claude exits plan mode (plan approved), this hook:
12
+ * 1. Finds the most recently modified plan file in ~/.claude/plans/
13
+ * 2. Auto-creates a child category under "plans" for the project if needed
14
+ * 3. Generates a local embedding and stores the plan in SQLite
15
+ * 4. Returns additionalContext confirming storage
16
+ *
17
+ * Input (stdin JSON):
18
+ * { session_id, tool_name, tool_input, tool_response, cwd }
19
+ *
20
+ * Output (stdout JSON):
21
+ * { additionalContext: "..." } on success, or {} on skip/error
22
+ */
23
+
24
+ import { readFileSync, writeFileSync, appendFileSync, readdirSync, statSync, existsSync, mkdirSync } from 'node:fs';
25
+ import { DatabaseSync } from 'node:sqlite';
26
+ import { dirname, join } from 'node:path';
27
+ import { fileURLToPath, pathToFileURL } from 'node:url';
28
+ import { randomUUID } from 'node:crypto';
29
+ import { detectProject, getMcpCategoriesPath, MCP_DATA_DIR, DATA_DIR } from './shared.mjs';
30
+
31
+ const __dirname = dirname(fileURLToPath(import.meta.url));
32
+
33
+ // Debug log (append-only, survives across invocations)
34
+ const DEBUG_LOG_PATH = join(DATA_DIR, 'plan-debug.log');
35
+ function debugLog(msg) {
36
+ try {
37
+ const ts = new Date().toISOString();
38
+ appendFileSync(DEBUG_LOG_PATH, `[${ts}] ${msg}\n`, 'utf-8');
39
+ } catch { /* best-effort */ }
40
+ }
41
+
42
+ // Plans directory — project-local to avoid ~/.claude/ sensitive-file permission prompts
43
+ const PLANS_DIR = join(DATA_DIR, 'plans');
44
+ // Fallback: Claude Code's ExitPlanMode may still write to ~/.claude/plans/ internally
45
+ const PLANS_DIR_FALLBACK = join(process.env.USERPROFILE || process.env.HOME || '', '.claude', 'plans');
46
+
47
+ // Dedup tracker — records which plan files have already been stored
48
+ const STORED_PLANS_PATH = join(DATA_DIR, 'stored-plans.json');
49
+
50
+ // SQLite database path
51
+ const DB_PATH = process.env.SQLITE_DB_PATH || join(MCP_DATA_DIR, 'memory.db');
52
+
53
+ // ─── Stdin ───
54
+
55
+ function readStdin() {
56
+ return new Promise((resolve) => {
57
+ if (process.stdin.isTTY) return resolve('{}');
58
+ let data = '';
59
+ process.stdin.setEncoding('utf8');
60
+ process.stdin.on('data', (chunk) => { data += chunk; });
61
+ process.stdin.on('end', () => resolve(data));
62
+ setTimeout(() => resolve(data || '{}'), 3000);
63
+ });
64
+ }
65
+
66
+ // ─── Local embedding generation ───
67
+
68
+ async function generateEmbedding(text) {
69
+ const embPath = join(__dirname, '..', '..', 'mcp-server', 'dist', 'services', 'local-embeddings.js');
70
+ if (!existsSync(embPath)) {
71
+ throw new Error('MCP server not built. Run: cd mcp-server && npm run build');
72
+ }
73
+ const { generateEmbedding: embed } = await import(pathToFileURL(embPath).href);
74
+ return embed(text);
75
+ }
76
+
77
+ // ─── Vector encoding ───
78
+
79
+ function encodeVector(vector) {
80
+ const f32 = new Float32Array(vector);
81
+ return new Uint8Array(f32.buffer);
82
+ }
83
+
84
+ // ─── Database storage ───
85
+
86
+ function storeInSQLite(id, vector, payload) {
87
+ const db = new DatabaseSync(DB_PATH);
88
+ db.exec('PRAGMA journal_mode = WAL');
89
+ db.exec('PRAGMA busy_timeout = 5000');
90
+
91
+ try {
92
+ db.prepare(`
93
+ INSERT OR REPLACE INTO memories
94
+ (id, vector, content, category, subcategory, project, tags, importance, source,
95
+ created_at, updated_at, accessed_at, access_count, related_files,
96
+ related_memory_ids, file_checksums, trashed_at, source_session_chunks)
97
+ VALUES
98
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
99
+ `).run(
100
+ id,
101
+ encodeVector(vector),
102
+ payload.content,
103
+ payload.category,
104
+ payload.subcategory || null,
105
+ payload.project,
106
+ JSON.stringify(payload.tags || []),
107
+ payload.importance || 5,
108
+ payload.source || 'auto-saved',
109
+ payload.created_at,
110
+ payload.updated_at,
111
+ payload.accessed_at,
112
+ payload.access_count || 0,
113
+ null, // related_files
114
+ null, // related_memory_ids
115
+ null, // file_checksums
116
+ null, // trashed_at
117
+ null, // source_session_chunks
118
+ );
119
+ } finally {
120
+ db.close();
121
+ }
122
+ }
123
+
124
+ // ─── Dedup tracker ───
125
+
126
+ function loadStoredPlans() {
127
+ try {
128
+ if (!existsSync(STORED_PLANS_PATH)) return {};
129
+ return JSON.parse(readFileSync(STORED_PLANS_PATH, 'utf-8'));
130
+ } catch { return {}; }
131
+ }
132
+
133
+ function markPlanStored(fileName, memoryId) {
134
+ const stored = loadStoredPlans();
135
+ stored[fileName] = { memoryId, storedAt: new Date().toISOString() };
136
+ try {
137
+ if (!existsSync(dirname(STORED_PLANS_PATH))) mkdirSync(dirname(STORED_PLANS_PATH), { recursive: true });
138
+ writeFileSync(STORED_PLANS_PATH, JSON.stringify(stored, null, 2), 'utf-8');
139
+ } catch { /* best-effort */ }
140
+ }
141
+
142
+ // ─── Plan file discovery ───
143
+
144
+ /**
145
+ * List all plan .md files, sorted by mtime descending.
146
+ * Each entry: { name, path, mtime }
147
+ */
148
+ function listPlanFiles() {
149
+ const results = [];
150
+ for (const dir of [PLANS_DIR, PLANS_DIR_FALLBACK]) {
151
+ if (!existsSync(dir)) continue;
152
+ for (const f of readdirSync(dir).filter((f) => f.endsWith('.md'))) {
153
+ // Skip duplicates (same filename in both dirs — prefer primary)
154
+ if (results.some((r) => r.name === f)) continue;
155
+ const fullPath = join(dir, f);
156
+ const stat = statSync(fullPath);
157
+ results.push({ name: f, path: fullPath, mtime: stat.mtimeMs });
158
+ }
159
+ }
160
+ return results.sort((a, b) => b.mtime - a.mtime);
161
+ }
162
+
163
+ /**
164
+ * Match the correct plan file using tool_response content from ExitPlanMode.
165
+ * Strategies (in order):
166
+ * 1. Filename extraction — tool_response may reference the plan filename
167
+ * 2. Content matching — tool_response may contain the plan text; find the file whose content matches
168
+ * Returns null if no match or if the matched file is already stored.
169
+ */
170
+ function findPlanByContent(toolResponse) {
171
+ if (!toolResponse || typeof toolResponse !== 'string') return null;
172
+
173
+ const stored = loadStoredPlans();
174
+ const files = listPlanFiles();
175
+ if (files.length === 0) return null;
176
+
177
+ // Strategy 1: Extract filename pattern (word-word-word.md) from response
178
+ const fnameMatch = toolResponse.match(/([a-z]+-[a-z]+-[a-z]+\.md)/);
179
+ if (fnameMatch) {
180
+ const matched = files.find((f) => f.name === fnameMatch[1]);
181
+ if (matched && !stored[matched.name]) return { ...matched, method: 'filename' };
182
+ }
183
+
184
+ // Strategy 2: Content matching — compare response text against each unstored plan file
185
+ // tool_response from ExitPlanMode likely contains the plan content itself
186
+ const responseTrimmed = toolResponse.trim();
187
+ if (responseTrimmed.length > 50) {
188
+ // Extract first meaningful line (H1 heading or first paragraph) for fast pre-filter
189
+ const responseFirstLine = responseTrimmed.split('\n').find((l) => l.trim())?.trim() || '';
190
+
191
+ for (const file of files) {
192
+ if (stored[file.name]) continue;
193
+ try {
194
+ const content = readFileSync(file.path, 'utf-8').trim();
195
+ // Exact match
196
+ if (content === responseTrimmed) return { ...file, method: 'exact-content' };
197
+ // Response contains the file content (response may have extra wrapper text)
198
+ if (responseTrimmed.includes(content)) return { ...file, method: 'content-in-response' };
199
+ // File content contains the response (file is the full plan, response is a subset)
200
+ if (content.includes(responseTrimmed)) return { ...file, method: 'response-in-content' };
201
+ // First-line match as fallback (H1 heading match)
202
+ if (responseFirstLine.length > 10) {
203
+ const fileFirstLine = content.split('\n').find((l) => l.trim())?.trim() || '';
204
+ if (fileFirstLine === responseFirstLine) return { ...file, method: 'heading-match' };
205
+ }
206
+ } catch { /* skip unreadable files */ }
207
+ }
208
+ }
209
+
210
+ return null;
211
+ }
212
+
213
+ /**
214
+ * Fallback: return the most recently modified unstored plan file.
215
+ * Used only when content matching fails (no tool_response or no match).
216
+ */
217
+ function findLatestPlan() {
218
+ const stored = loadStoredPlans();
219
+ const files = listPlanFiles();
220
+ if (files.length === 0) return null;
221
+ const unstored = files.find((f) => !stored[f.name]);
222
+ return unstored ? { ...unstored, method: 'mtime-fallback' } : null;
223
+ }
224
+
225
+ // ─── Category management ───
226
+
227
+ function ensureProjectCategory(project) {
228
+ const categoryName = `plans-${project}`;
229
+ const catPath = getMcpCategoriesPath();
230
+
231
+ try {
232
+ const data = JSON.parse(readFileSync(catPath, 'utf-8'));
233
+ const categories = data.categories || [];
234
+
235
+ // Check if child category already exists
236
+ if (categories.some((c) => c.name === categoryName)) {
237
+ return categoryName;
238
+ }
239
+
240
+ // Check if "plans" parent exists
241
+ if (!categories.some((c) => c.name === 'plans')) {
242
+ // Create parent if missing (shouldn't happen, but safety net)
243
+ categories.push({
244
+ name: 'plans',
245
+ description: 'Implementation plans stored after plan mode approval. Sub-categorized by project name.',
246
+ is_parent: true,
247
+ color: '#06d6a0',
248
+ created_at: new Date().toISOString(),
249
+ });
250
+ }
251
+
252
+ // Create child category for this project
253
+ categories.push({
254
+ name: categoryName,
255
+ description: `Implementation plans for ${project} project`,
256
+ parent: 'plans',
257
+ created_at: new Date().toISOString(),
258
+ });
259
+
260
+ data.categories = categories;
261
+ writeFileSync(catPath, JSON.stringify(data, null, 2), 'utf-8');
262
+
263
+ return categoryName;
264
+ } catch {
265
+ // If categories file can't be read/written, fall back to parent
266
+ return 'plans';
267
+ }
268
+ }
269
+
270
+ // ─── Neural Interface cache invalidation ───
271
+
272
+ async function invalidateNeuralInterface() {
273
+ try {
274
+ // Trigger sync broadcast so the Neural Interface graph refreshes
275
+ const niUrl = process.env.SYNABUN_NI_URL || 'http://localhost:3344';
276
+ await fetch(`${niUrl}/api/memories?invalidate=true`, {
277
+ method: 'GET',
278
+ signal: AbortSignal.timeout(2000),
279
+ });
280
+ } catch { /* fire-and-forget */ }
281
+ }
282
+
283
+ // ─── Extract plan title from markdown ───
284
+
285
+ function extractPlanTitle(content) {
286
+ // Look for first H1 heading
287
+ const h1Match = content.match(/^#\s+(.+)$/m);
288
+ if (h1Match) return h1Match[1].trim();
289
+
290
+ // Fall back to first non-empty line
291
+ const firstLine = content.split('\n').find((l) => l.trim());
292
+ return firstLine?.trim().slice(0, 100) || 'Untitled plan';
293
+ }
294
+
295
+ // ─── Main ───
296
+
297
+ async function main() {
298
+ let input = {};
299
+ try {
300
+ const raw = await readStdin();
301
+ input = JSON.parse(raw);
302
+ } catch { /* proceed */ }
303
+
304
+ const toolName = input.tool_name || '';
305
+
306
+ debugLog(`Hook invoked — tool_name: "${toolName}", session: ${input.session_id || 'unknown'}, cwd: ${input.cwd || 'unknown'}`);
307
+
308
+ // Handle EnterPlanMode — remind Claude to use AskUserQuestion
309
+ if (toolName === 'EnterPlanMode') {
310
+ debugLog('EnterPlanMode fired — injecting AskUserQuestion reminder');
311
+ process.stdout.write(JSON.stringify({
312
+ additionalContext: `SynaBun: You are now in plan mode. When you have questions or need clarification from the user, you MUST use the \`AskUserQuestion\` tool — do NOT write questions as plain text. Load it via \`ToolSearch\` if its schema is not yet available. Use \`ExitPlanMode\` for final plan approval.\n\n**Plan file location**: Write plan files to \`${PLANS_DIR}/\` — NOT \`~/.claude/plans/\`. This avoids sensitive-file permission prompts that stall the session.`,
313
+ }));
314
+ return;
315
+ }
316
+
317
+ // Only handle ExitPlanMode beyond this point
318
+ if (toolName !== 'ExitPlanMode') {
319
+ process.stdout.write(JSON.stringify({}));
320
+ return;
321
+ }
322
+
323
+ // Match plan file: content-match first (session-accurate), mtime fallback
324
+ const toolResponse = typeof input.tool_response === 'string'
325
+ ? input.tool_response
326
+ : (input.tool_response != null ? JSON.stringify(input.tool_response) : '');
327
+
328
+ debugLog(`ExitPlanMode fired — tool_response length: ${toolResponse.length}, first 200 chars: ${toolResponse.slice(0, 200).replace(/\n/g, '\\n')}`);
329
+
330
+ const planFile = findPlanByContent(toolResponse) || findLatestPlan();
331
+ if (!planFile) {
332
+ process.stdout.write(JSON.stringify({
333
+ additionalContext: 'SynaBun: No unstored plan file found (all plans already in memory).',
334
+ }));
335
+ return;
336
+ }
337
+
338
+ const planContent = readFileSync(planFile.path, 'utf-8');
339
+ if (!planContent.trim()) {
340
+ process.stdout.write(JSON.stringify({}));
341
+ return;
342
+ }
343
+
344
+ const project = detectProject(input.cwd || '');
345
+ const planTitle = extractPlanTitle(planContent);
346
+
347
+ // Ensure child category exists under "plans"
348
+ const categoryName = ensureProjectCategory(project);
349
+
350
+ // Generate local embedding (Transformers.js, no API key needed)
351
+ const embedding = await generateEmbedding(planContent);
352
+
353
+ // Store in SQLite
354
+ const id = randomUUID();
355
+ const now = new Date().toISOString();
356
+
357
+ storeInSQLite(id, embedding, {
358
+ content: planContent,
359
+ category: categoryName,
360
+ project,
361
+ importance: 7,
362
+ tags: ['plan', 'implementation', project],
363
+ source: 'auto-saved',
364
+ subcategory: 'plan',
365
+ created_at: now,
366
+ updated_at: now,
367
+ accessed_at: now,
368
+ access_count: 0,
369
+ });
370
+
371
+ // Mark this plan as stored (dedup for future ExitPlanMode calls)
372
+ markPlanStored(planFile.name, id);
373
+
374
+ // Invalidate Neural Interface cache (fire-and-forget)
375
+ invalidateNeuralInterface();
376
+
377
+ // Confirm storage — include match method for diagnostics
378
+ const shortTitle = planTitle.length > 60 ? planTitle.slice(0, 60) + '...' : planTitle;
379
+ const matchInfo = planFile.method || 'unknown';
380
+ debugLog(`Stored plan: ${planFile.name} [${matchInfo}] → memory ${id} (project: ${project})`);
381
+ process.stdout.write(JSON.stringify({
382
+ additionalContext: `SynaBun: Plan stored in memory [${id}] — "${shortTitle}" (category: ${categoryName}, project: ${project}). Source file: ${planFile.name} [matched: ${matchInfo}]`,
383
+ }));
384
+ }
385
+
386
+ main().catch((err) => {
387
+ // On error, still output valid JSON so Claude Code doesn't break
388
+ process.stdout.write(JSON.stringify({
389
+ additionalContext: `SynaBun: Plan storage failed — ${err.message}. The plan file is still saved at ${PLANS_DIR}.`,
390
+ }));
391
+ });
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SynaBun PostToolUse Hook for Claude Code (unified handler)
5
+ *
6
+ * Matches: ^Edit$|^Write$|^NotebookEdit$|mcp__SynaBun__remember
7
+ *
8
+ * Two responsibilities:
9
+ *
10
+ * 1. EDIT TRACKING — When Claude uses Edit, Write, or NotebookEdit,
11
+ * increments a pending-remember counter. If Claude finishes responding
12
+ * with 3+ unremembered edits, the Stop hook will block it.
13
+ *
14
+ * 2. FLAG MANAGEMENT — When Claude calls `remember`:
15
+ * - category "conversations" → clears pending-compact flag (compaction enforcement)
16
+ * - any other category → resets pending-remember flag (editCount→0,
17
+ * keeps rememberCount/totalEdits so subsequent edits start a fresh segment)
18
+ *
19
+ * Input (stdin JSON):
20
+ * { session_id, tool_name, tool_input: { ... }, tool_response: { ... } }
21
+ *
22
+ * Output (stdout JSON):
23
+ * - On edit at threshold multiples (3, 6, 9...): { additionalContext: "..." }
24
+ * - Otherwise: {} (side effects only)
25
+ */
26
+
27
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, readdirSync } from 'node:fs';
28
+ import { dirname, join } from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+ import { loadCategories, buildCategoryReference, detectProject } from './shared.mjs';
31
+
32
+ const __dirname = dirname(fileURLToPath(import.meta.url));
33
+ const DATA_DIR = join(__dirname, '..', '..', 'data');
34
+ const PENDING_COMPACT_DIR = join(DATA_DIR, 'pending-compact');
35
+ const PENDING_REMEMBER_DIR = join(DATA_DIR, 'pending-remember');
36
+
37
+ // Ensure directories exist
38
+ for (const dir of [PENDING_COMPACT_DIR, PENDING_REMEMBER_DIR]) {
39
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
40
+ }
41
+
42
+ const EDIT_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit']);
43
+ const EDIT_THRESHOLD = 1;
44
+
45
+ function readStdin() {
46
+ return new Promise((resolve) => {
47
+ if (process.stdin.isTTY) return resolve('{}');
48
+ let data = '';
49
+ process.stdin.setEncoding('utf8');
50
+ process.stdin.on('data', (chunk) => { data += chunk; });
51
+ process.stdin.on('end', () => resolve(data));
52
+ setTimeout(() => resolve(data || '{}'), 2000);
53
+ });
54
+ }
55
+
56
+ async function main() {
57
+ let input = {};
58
+ try {
59
+ const raw = await readStdin();
60
+ input = JSON.parse(raw);
61
+ } catch { /* proceed */ }
62
+
63
+ const sessionId = input.session_id || '';
64
+ const toolName = input.tool_name || '';
65
+ const toolInput = input.tool_input || {};
66
+ const cwd = input.cwd || '';
67
+
68
+ if (!sessionId) {
69
+ process.stdout.write(JSON.stringify({}));
70
+ return;
71
+ }
72
+
73
+ // ─── EDIT TRACKING ───
74
+ if (EDIT_TOOLS.has(toolName)) {
75
+ const flagPath = join(PENDING_REMEMBER_DIR, `${sessionId}.json`);
76
+ let flag = { editCount: 0, retries: 0, files: [], firstEditAt: null };
77
+
78
+ // Read existing flag if present
79
+ if (existsSync(flagPath)) {
80
+ try {
81
+ flag = JSON.parse(readFileSync(flagPath, 'utf-8'));
82
+ } catch { /* start fresh */ }
83
+ }
84
+
85
+ // Increment edit counters
86
+ flag.editCount = (flag.editCount || 0) + 1;
87
+ flag.totalEdits = (flag.totalEdits || 0) + 1;
88
+ if (!flag.firstEditAt) flag.firstEditAt = new Date().toISOString();
89
+ flag.lastEditAt = new Date().toISOString();
90
+
91
+ // Track file path if available
92
+ const filePath = toolInput.file_path || toolInput.notebook_path || '';
93
+ if (filePath && Array.isArray(flag.files) && !flag.files.includes(filePath)) {
94
+ flag.files.push(filePath);
95
+ }
96
+
97
+ try {
98
+ writeFileSync(flagPath, JSON.stringify(flag));
99
+ } catch { /* ok */ }
100
+
101
+ // Proactive nudge at every multiple of threshold (3, 6, 9, ...)
102
+ if (flag.editCount > 0 && flag.editCount % EDIT_THRESHOLD === 0) {
103
+ let nudgeText;
104
+ if (flag.editCount <= EDIT_THRESHOLD) {
105
+ nudgeText = `SynaBun: ${flag.editCount} file edits so far — remember to store this work in memory before wrapping up.`;
106
+ } else if (flag.editCount <= EDIT_THRESHOLD * 2) {
107
+ nudgeText = `SynaBun: ${flag.editCount} file edits without storing to memory. Call \`remember\` soon to avoid losing context.`;
108
+ } else {
109
+ nudgeText = `SynaBun: ${flag.editCount} file edits — this is significant work. You MUST call \`remember\` before continuing.`;
110
+ }
111
+
112
+ // On first threshold hit, append the full category reference (lazy injection)
113
+ if (!flag.categoryTreeInjected) {
114
+ try {
115
+ const categories = loadCategories();
116
+ const project = detectProject(cwd);
117
+ const categoryRef = buildCategoryReference(categories, project);
118
+ nudgeText += `\n\n${categoryRef}`;
119
+ flag.categoryTreeInjected = true;
120
+ writeFileSync(flagPath, JSON.stringify(flag));
121
+ } catch { /* category tree optional — nudge still works without it */ }
122
+ }
123
+
124
+ process.stdout.write(JSON.stringify({ additionalContext: nudgeText }));
125
+ } else {
126
+ process.stdout.write(JSON.stringify({}));
127
+ }
128
+ return;
129
+ }
130
+
131
+ // ─── USER LEARNING CLEARING (remember or reflect for communication-style/personality) ───
132
+ // Since user learning is non-blocking (prompt-submit nudge only), any remember or
133
+ // reflect targeting communication-style clears the flag immediately.
134
+ const UL_CATEGORIES = ['communication-style', 'personality'];
135
+ const isRememberUL = toolName.includes('remember') && UL_CATEGORIES.includes(toolInput.category || '');
136
+ const isReflectUL = toolName.includes('reflect');
137
+
138
+ if (isRememberUL || isReflectUL) {
139
+ const ulFlagPath = join(PENDING_REMEMBER_DIR, `${sessionId}.json`);
140
+ if (existsSync(ulFlagPath)) {
141
+ try {
142
+ const ulFlag = JSON.parse(readFileSync(ulFlagPath, 'utf-8'));
143
+ if (ulFlag.userLearningPending && (isRememberUL || isReflectUL)) {
144
+ ulFlag.userLearningPending = false;
145
+ ulFlag.userLearningObserved = true;
146
+ writeFileSync(ulFlagPath, JSON.stringify(ulFlag));
147
+ }
148
+ } catch { /* ok */ }
149
+ }
150
+ }
151
+
152
+ // ─── REMEMBER FLAG CLEARING ───
153
+ // Both remember and reflect count as storing work — clear edit tracking for either.
154
+ if (toolName.includes('remember') || toolName.includes('reflect')) {
155
+ const category = toolInput.category || '';
156
+
157
+ if (category === 'conversations') {
158
+ // Clear ALL pending-compact flags (session ID may differ after compaction)
159
+ try {
160
+ if (existsSync(PENDING_COMPACT_DIR)) {
161
+ for (const f of readdirSync(PENDING_COMPACT_DIR).filter(f => f.endsWith('.json'))) {
162
+ try { unlinkSync(join(PENDING_COMPACT_DIR, f)); } catch { /* ok */ }
163
+ }
164
+ }
165
+ } catch { /* ok */ }
166
+ // Also reset pending-remember flag (conversations remember counts too)
167
+ const rememberFlagPath = join(PENDING_REMEMBER_DIR, `${sessionId}.json`);
168
+ if (existsSync(rememberFlagPath)) {
169
+ try {
170
+ let flag = {};
171
+ try { flag = JSON.parse(readFileSync(rememberFlagPath, 'utf-8')); } catch { /* fresh */ }
172
+ flag.editCount = 0;
173
+ flag.messageCount = 0;
174
+ flag.retries = 0;
175
+ flag.files = [];
176
+ flag.firstEditAt = null;
177
+ flag.lastEditAt = null;
178
+ flag.rememberCount = (flag.rememberCount || 0) + 1;
179
+ flag.lastRememberedAt = new Date().toISOString();
180
+ writeFileSync(rememberFlagPath, JSON.stringify(flag));
181
+ } catch { /* ok */ }
182
+ }
183
+ } else if (category) {
184
+ // Reset pending-remember flag (keep file, zero counters for next task segment)
185
+ const rememberFlagPath = join(PENDING_REMEMBER_DIR, `${sessionId}.json`);
186
+ if (existsSync(rememberFlagPath)) {
187
+ try {
188
+ let flag = {};
189
+ try { flag = JSON.parse(readFileSync(rememberFlagPath, 'utf-8')); } catch { /* start fresh */ }
190
+
191
+ const prevTotal = flag.totalEdits || flag.editCount || 0;
192
+ const prevRememberCount = flag.rememberCount || 0;
193
+
194
+ // Reset edit + message tracking but keep session-level stats
195
+ flag.editCount = 0;
196
+ flag.messageCount = 0;
197
+ flag.retries = 0;
198
+ flag.files = [];
199
+ flag.firstEditAt = null;
200
+ flag.lastEditAt = null;
201
+ flag.firstMessageAt = null;
202
+ flag.lastMessageAt = null;
203
+ flag.lastRememberedAt = new Date().toISOString();
204
+ flag.rememberCount = prevRememberCount + 1;
205
+ flag.totalEdits = prevTotal;
206
+
207
+ writeFileSync(rememberFlagPath, JSON.stringify(flag));
208
+ } catch {
209
+ // If reset fails, fall back to deleting
210
+ try { unlinkSync(rememberFlagPath); } catch { /* ok */ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ // Update loop memory tracking when remember is called during an active loop
217
+ if (toolName.includes('remember')) {
218
+ const LOOP_DIR = join(DATA_DIR, 'loop');
219
+ const loopPath = join(LOOP_DIR, `${sessionId}.json`);
220
+ if (existsSync(loopPath)) {
221
+ try {
222
+ const loop = JSON.parse(readFileSync(loopPath, 'utf-8'));
223
+ if (loop.active) {
224
+ loop.lastMemoryAt = loop.currentIteration || 0;
225
+ loop.memoryPending = false;
226
+ loop.memoryRetries = 0;
227
+ writeFileSync(loopPath, JSON.stringify(loop, null, 2));
228
+ }
229
+ } catch { /* ok */ }
230
+ }
231
+ }
232
+
233
+ // Always output empty (no additional context needed)
234
+ process.stdout.write(JSON.stringify({}));
235
+ }
236
+
237
+ main().catch(() => {
238
+ process.stdout.write(JSON.stringify({}));
239
+ });