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,531 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Automated tests for the User Learning hook system.
5
+ *
6
+ * Tests the full lifecycle:
7
+ * prompt-submit nudge → stop hook enforcement → post-remember clearing
8
+ *
9
+ * Run: node hooks/claude-code/test-user-learning.mjs
10
+ */
11
+
12
+ import { spawn } from 'node:child_process';
13
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
14
+ import { dirname, join } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const DATA_DIR = join(__dirname, '..', '..', 'data');
19
+ const PENDING_REMEMBER_DIR = join(DATA_DIR, 'pending-remember');
20
+ const HOOK_FEATURES_PATH = join(DATA_DIR, 'hook-features.json');
21
+
22
+ const PROMPT_SUBMIT = join(__dirname, 'prompt-submit.mjs');
23
+ const STOP_HOOK = join(__dirname, 'stop.mjs');
24
+ const POST_REMEMBER = join(__dirname, 'post-remember.mjs');
25
+
26
+ const TEST_SESSION = 'test-user-learning-001';
27
+ const FLAG_PATH = join(PENDING_REMEMBER_DIR, `${TEST_SESSION}.json`);
28
+
29
+ // Colors
30
+ const G = '\x1b[32m', R = '\x1b[31m', Y = '\x1b[33m', B = '\x1b[1m', X = '\x1b[0m';
31
+
32
+ let passed = 0, failed = 0;
33
+
34
+ function assert(condition, name) {
35
+ if (condition) { console.log(` ${G}✓${X} ${name}`); passed++; }
36
+ else { console.log(` ${R}✗${X} ${name}`); failed++; }
37
+ }
38
+
39
+ function runHook(scriptPath, stdinData) {
40
+ return new Promise((resolve) => {
41
+ const proc = spawn('node', [scriptPath], { stdio: ['pipe', 'pipe', 'pipe'] });
42
+ let stdout = '', stderr = '';
43
+ proc.stdout.on('data', d => stdout += d);
44
+ proc.stderr.on('data', d => stderr += d);
45
+ proc.on('close', code => {
46
+ let parsed = {};
47
+ try { parsed = stdout ? JSON.parse(stdout) : {}; } catch { /* ok */ }
48
+ resolve({ code, stdout, stderr, parsed });
49
+ });
50
+ proc.stdin.write(JSON.stringify(stdinData));
51
+ proc.stdin.end();
52
+ });
53
+ }
54
+
55
+ function readFlag() {
56
+ try { return JSON.parse(readFileSync(FLAG_PATH, 'utf-8')); }
57
+ catch { return null; }
58
+ }
59
+
60
+ function writeFlag(flag) {
61
+ writeFileSync(FLAG_PATH, JSON.stringify(flag));
62
+ }
63
+
64
+ function cleanup() {
65
+ try { unlinkSync(FLAG_PATH); } catch { /* ok */ }
66
+ }
67
+
68
+ function getContext(result) {
69
+ return result.parsed?.hookSpecificOutput?.additionalContext || '';
70
+ }
71
+
72
+ // Save original features to restore later
73
+ let originalFeatures;
74
+ try { originalFeatures = readFileSync(HOOK_FEATURES_PATH, 'utf-8'); }
75
+ catch { originalFeatures = '{}'; }
76
+
77
+ async function main() {
78
+ console.log(`\n${B}User Learning Hook System — Automated Tests${X}\n`);
79
+
80
+ if (!existsSync(PENDING_REMEMBER_DIR)) mkdirSync(PENDING_REMEMBER_DIR, { recursive: true });
81
+
82
+ const testFeatures = {
83
+ conversationMemory: true,
84
+ greeting: false,
85
+ userLearning: true,
86
+ userLearningThreshold: 3,
87
+ autoStoreOnEnd: false,
88
+ };
89
+ writeFileSync(HOOK_FEATURES_PATH, JSON.stringify(testFeatures));
90
+
91
+ try {
92
+ // ═══════════════════════════════════════════════════════════
93
+ // TEST 1: No nudge below threshold
94
+ // ═══════════════════════════════════════════════════════════
95
+ console.log(`${Y}Test 1: No nudge below threshold${X}`);
96
+ cleanup();
97
+
98
+ // Message 1
99
+ await runHook(PROMPT_SUBMIT, { prompt: 'Describe the color scheme in detail please', session_id: TEST_SESSION });
100
+ let flag = readFlag();
101
+ assert(flag !== null, 'Flag file created after message 1');
102
+ assert(flag.messageCount === 1, `messageCount = 1 (got ${flag?.messageCount})`);
103
+ assert(flag.totalSessionMessages === 1, `totalSessionMessages = 1 (got ${flag?.totalSessionMessages})`);
104
+
105
+ // Message 2
106
+ const r2 = await runHook(PROMPT_SUBMIT, { prompt: 'Now describe the typography choices', session_id: TEST_SESSION });
107
+ flag = readFlag();
108
+ assert(flag.messageCount === 2, `messageCount = 2 (got ${flag?.messageCount})`);
109
+ assert(!flag.userLearningPending, 'userLearningPending not set at message 2');
110
+ const ctx2 = getContext(r2);
111
+ assert(!ctx2.includes('User Learning'), 'No user learning nudge at message 2');
112
+
113
+ // ═══════════════════════════════════════════════════════════
114
+ // TEST 2: Nudge fires at threshold
115
+ // ═══════════════════════════════════════════════════════════
116
+ console.log(`\n${Y}Test 2: Nudge fires at threshold (message 3)${X}`);
117
+
118
+ const r3 = await runHook(PROMPT_SUBMIT, { prompt: 'What font sizes are being used here', session_id: TEST_SESSION });
119
+ flag = readFlag();
120
+ assert(flag.messageCount === 3, `messageCount = 3 (got ${flag?.messageCount})`);
121
+ assert(flag.totalSessionMessages === 3, `totalSessionMessages = 3 (got ${flag?.totalSessionMessages})`);
122
+ assert(flag.userLearningPending === true, 'userLearningPending = true');
123
+ assert(flag.userLearningNudgeCount === 1, `nudgeCount = 1 (got ${flag?.userLearningNudgeCount})`);
124
+
125
+ const ctx3 = getContext(r3);
126
+ assert(ctx3.includes('User Learning'), 'Nudge text contains "User Learning"');
127
+ assert(ctx3.includes('communication-style'), 'Nudge mentions communication-style');
128
+
129
+ // ═══════════════════════════════════════════════════════════
130
+ // TEST 2b: Nudge content has anti-misuse guardrails
131
+ // ═══════════════════════════════════════════════════════════
132
+ console.log(`\n${Y}Test 2b: Nudge contains anti-misuse guardrails${X}`);
133
+
134
+ assert(ctx3.includes('never `conversations`'), 'Nudge explicitly forbids conversations category');
135
+ assert(ctx3.includes('NOT a session summary'), 'Nudge says "NOT a session summary"');
136
+ assert(ctx3.includes('NOT what was worked on'), 'Nudge says content is NOT about what was worked on');
137
+ assert(ctx3.includes('GOOD example'), 'Nudge has GOOD example');
138
+ assert(ctx3.includes('BAD example'), 'Nudge has BAD example');
139
+ assert(ctx3.includes('instruction patterns') && ctx3.includes('correction style'), 'Nudge lists specific observation targets');
140
+ assert(ctx3.includes('AVOID DUPLICATES'), 'Nudge warns about duplicates');
141
+ assert(ctx3.includes('reflect'), 'Nudge mentions reflect as an option');
142
+
143
+ // ═══════════════════════════════════════════════════════════
144
+ // TEST 3: Stop hook blocks when userLearningPending
145
+ // ═══════════════════════════════════════════════════════════
146
+ console.log(`\n${Y}Test 3: Stop hook blocks for user learning${X}`);
147
+
148
+ const stop1 = await runHook(STOP_HOOK, { session_id: TEST_SESSION });
149
+ assert(stop1.parsed.decision === 'block', 'Stop hook blocks (retry 1)');
150
+ const stopReason = stop1.parsed.reason || '';
151
+ assert(stopReason.includes('communication-style'), 'Block reason mentions communication-style');
152
+
153
+ // ─── TEST 3b: Stop block has anti-misuse guardrails ───
154
+ console.log(`\n${Y}Test 3b: Stop block contains anti-misuse guardrails${X}`);
155
+
156
+ assert(stopReason.includes('NOT') && stopReason.includes('conversations'), 'Stop block forbids conversations category');
157
+ assert(stopReason.includes('NOT a session summary'), 'Stop block says NOT a session summary');
158
+ assert(stopReason.includes('GOOD'), 'Stop block has GOOD example');
159
+ assert(stopReason.includes('BAD'), 'Stop block has BAD example');
160
+ assert(stopReason.includes('AVOID DUPLICATES'), 'Stop block warns about duplicates');
161
+ assert(stopReason.includes('reflect'), 'Stop block mentions reflect as an option');
162
+
163
+ // ─── TEST 3c: After 1 retry, stop allows (lightweight enforcement) ───
164
+ console.log(`\n${Y}Test 3c: Stop allows after 1 retry (lightweight enforcement)${X}`);
165
+
166
+ flag = readFlag();
167
+ assert(flag.userLearningRetries === 1, `userLearningRetries = 1 (got ${flag?.userLearningRetries})`);
168
+
169
+ // 2nd stop call — should allow (1-retry max for user learning)
170
+ const stop1b = await runHook(STOP_HOOK, { session_id: TEST_SESSION });
171
+ assert(!stop1b.parsed.decision, 'Stop allows after 1 retry (user learning max reached)');
172
+
173
+ // Reset for Test 4
174
+ flag = readFlag();
175
+ flag.userLearningPending = true;
176
+ flag.userLearningRetries = 0;
177
+ writeFlag(flag);
178
+
179
+ // ═══════════════════════════════════════════════════════════
180
+ // TEST 4: Stop hook blocks once then gives up (1-retry max)
181
+ // ═══════════════════════════════════════════════════════════
182
+ console.log(`\n${Y}Test 4: Stop hook blocks once then gives up (lightweight)${X}`);
183
+
184
+ const stop2 = await runHook(STOP_HOOK, { session_id: TEST_SESSION });
185
+ assert(stop2.parsed.decision === 'block', 'Block on retry 1');
186
+
187
+ // After 1 retry, should soft-cleanup and allow
188
+ const stop3 = await runHook(STOP_HOOK, { session_id: TEST_SESSION });
189
+ assert(!stop3.parsed.decision, 'No block after 1 retry (allows stop)');
190
+
191
+ flag = readFlag();
192
+ assert(flag.userLearningPending === false, 'userLearningPending cleared after soft cleanup');
193
+ assert(flag.userLearningRetries === 0, 'userLearningRetries reset to 0');
194
+
195
+ // ═══════════════════════════════════════════════════════════
196
+ // TEST 5: post-remember clears pending on communication-style
197
+ // ═══════════════════════════════════════════════════════════
198
+ console.log(`\n${Y}Test 5: post-remember clears on communication-style remember${X}`);
199
+ cleanup();
200
+
201
+ writeFlag({
202
+ editCount: 0, retries: 0, files: [],
203
+ messageCount: 5, totalSessionMessages: 5,
204
+ userLearningPending: true, userLearningNudgeCount: 1, userLearningRetries: 0,
205
+ });
206
+
207
+ await runHook(POST_REMEMBER, {
208
+ session_id: TEST_SESSION,
209
+ tool_name: 'mcp__SynaBun__remember',
210
+ tool_input: { category: 'communication-style', text: 'User is direct and concise', project: 'global', importance: 6 },
211
+ tool_response: { success: true },
212
+ });
213
+
214
+ flag = readFlag();
215
+ assert(flag.userLearningPending === false, 'userLearningPending cleared');
216
+ assert(flag.userLearningObserved === true, 'userLearningObserved set after remember');
217
+
218
+ // Verify stop hook now allows stop
219
+ const stop5 = await runHook(STOP_HOOK, { session_id: TEST_SESSION });
220
+ assert(!stop5.parsed.decision, 'Stop allows after communication-style remember');
221
+
222
+ // ═══════════════════════════════════════════════════════════
223
+ // TEST 5b: userLearningObserved skips subsequent nudges
224
+ // ═══════════════════════════════════════════════════════════
225
+ console.log(`\n${Y}Test 5b: userLearningObserved skips subsequent nudges${X}`);
226
+ cleanup();
227
+
228
+ writeFlag({
229
+ editCount: 0, retries: 0, files: [],
230
+ messageCount: 5, totalSessionMessages: 5,
231
+ userLearningNudgeCount: 1, userLearningPending: false,
232
+ userLearningObserved: true,
233
+ greetingDelivered: true,
234
+ });
235
+
236
+ // Message 6 → totalSessionMessages = 6 = 2x threshold(3), but observed=true
237
+ const rObserved = await runHook(PROMPT_SUBMIT, { prompt: 'Show me the sidebar component details please', session_id: TEST_SESSION });
238
+ flag = readFlag();
239
+ assert(flag.userLearningNudgeCount === 1, `nudgeCount stays at 1 (got ${flag?.userLearningNudgeCount})`);
240
+ assert(!flag.userLearningPending, 'No pending set when already observed');
241
+ const ctxObserved = getContext(rObserved);
242
+ assert(!ctxObserved.includes('User Learning'), 'No user learning nudge when already observed');
243
+
244
+ // ═══════════════════════════════════════════════════════════
245
+ // TEST 6: post-remember preserves pending on unrelated category
246
+ // ═══════════════════════════════════════════════════════════
247
+ console.log(`\n${Y}Test 6: post-remember preserves pending on unrelated remember${X}`);
248
+ cleanup();
249
+
250
+ writeFlag({
251
+ editCount: 3, retries: 0, files: ['test.js'],
252
+ messageCount: 5, totalSessionMessages: 5,
253
+ userLearningPending: true, userLearningNudgeCount: 1, userLearningRetries: 0,
254
+ });
255
+
256
+ await runHook(POST_REMEMBER, {
257
+ session_id: TEST_SESSION,
258
+ tool_name: 'mcp__SynaBun__remember',
259
+ tool_input: { category: 'bug-fixes', text: 'Fixed a rendering bug' },
260
+ tool_response: { success: true },
261
+ });
262
+
263
+ flag = readFlag();
264
+ assert(flag.userLearningPending === true, 'userLearningPending preserved after bug-fixes remember');
265
+
266
+ // ═══════════════════════════════════════════════════════════
267
+ // TEST 7: Reflect clears pending when userLearningPending is true
268
+ // ═══════════════════════════════════════════════════════════
269
+ console.log(`\n${Y}Test 7: Reflect clears pending when userLearningPending is true${X}`);
270
+ cleanup();
271
+
272
+ // 7a: reflect when pending is true → should clear (Claude is acting on the UL nudge)
273
+ writeFlag({
274
+ editCount: 0, retries: 0, files: [],
275
+ messageCount: 5, totalSessionMessages: 5,
276
+ userLearningPending: true, userLearningNudgeCount: 1, userLearningRetries: 0,
277
+ });
278
+
279
+ await runHook(POST_REMEMBER, {
280
+ session_id: TEST_SESSION,
281
+ tool_name: 'mcp__SynaBun__reflect',
282
+ tool_input: { memory_id: 'some-comm-style-uuid', content: 'Updated: user is direct, terse, no greetings' },
283
+ tool_response: { success: true },
284
+ });
285
+
286
+ flag = readFlag();
287
+ assert(flag.userLearningPending === false, 'Reflect clears pending when userLearningPending is true');
288
+ assert(flag.userLearningObserved === true, 'userLearningObserved set after reflect');
289
+
290
+ // ═══════════════════════════════════════════════════════════
291
+ // TEST 7b: Reflect does NOT set observed when pending is false
292
+ // ═══════════════════════════════════════════════════════════
293
+ console.log(`\n${Y}Test 7b: Reflect does NOT set observed when pending is false${X}`);
294
+ cleanup();
295
+
296
+ writeFlag({
297
+ editCount: 0, retries: 0, files: [],
298
+ messageCount: 5, totalSessionMessages: 5,
299
+ userLearningPending: false, userLearningNudgeCount: 0, userLearningRetries: 0,
300
+ });
301
+
302
+ await runHook(POST_REMEMBER, {
303
+ session_id: TEST_SESSION,
304
+ tool_name: 'mcp__SynaBun__reflect',
305
+ tool_input: { memory_id: 'some-unrelated-memory-id', content: 'Refined architecture observation' },
306
+ tool_response: { success: true },
307
+ });
308
+
309
+ flag = readFlag();
310
+ assert(!flag.userLearningObserved, 'userLearningObserved not set when pending was false');
311
+
312
+ // ═══════════════════════════════════════════════════════════
313
+ // TEST 8: personality category also clears pending
314
+ // ═══════════════════════════════════════════════════════════
315
+ console.log(`\n${Y}Test 8: personality category also clears pending${X}`);
316
+ cleanup();
317
+
318
+ writeFlag({
319
+ editCount: 0, retries: 0, files: [],
320
+ messageCount: 5, totalSessionMessages: 5,
321
+ userLearningPending: true, userLearningNudgeCount: 1, userLearningRetries: 0,
322
+ });
323
+
324
+ await runHook(POST_REMEMBER, {
325
+ session_id: TEST_SESSION,
326
+ tool_name: 'mcp__SynaBun__remember',
327
+ tool_input: { category: 'personality', text: 'User prefers dark themes', project: 'global', importance: 5 },
328
+ tool_response: { success: true },
329
+ });
330
+
331
+ flag = readFlag();
332
+ assert(flag.userLearningPending === false, 'userLearningPending cleared by personality remember');
333
+
334
+ // ═══════════════════════════════════════════════════════════
335
+ // TEST 8b: conversations category does NOT clear pending
336
+ // ═══════════════════════════════════════════════════════════
337
+ console.log(`\n${Y}Test 8b: conversations category does NOT clear pending (the original bug)${X}`);
338
+ cleanup();
339
+
340
+ writeFlag({
341
+ editCount: 0, retries: 0, files: [],
342
+ messageCount: 5, totalSessionMessages: 5,
343
+ userLearningPending: true, userLearningNudgeCount: 1, userLearningRetries: 0,
344
+ });
345
+
346
+ await runHook(POST_REMEMBER, {
347
+ session_id: TEST_SESSION,
348
+ tool_name: 'mcp__SynaBun__remember',
349
+ tool_input: { category: 'conversations', text: 'Session summary: user asked about hooks' },
350
+ tool_response: { success: true },
351
+ });
352
+
353
+ flag = readFlag();
354
+ assert(flag.userLearningPending === true, 'userLearningPending NOT cleared by conversations remember');
355
+
356
+ // ═══════════════════════════════════════════════════════════
357
+ // TEST 9: Second nudge fires at next threshold multiple
358
+ // ═══════════════════════════════════════════════════════════
359
+ console.log(`\n${Y}Test 9: Second nudge at 2x threshold${X}`);
360
+ cleanup();
361
+
362
+ writeFlag({
363
+ editCount: 0, retries: 0, files: [],
364
+ messageCount: 5, totalSessionMessages: 5,
365
+ userLearningNudgeCount: 1, userLearningPending: false,
366
+ greetingDelivered: true,
367
+ });
368
+
369
+ // Message 6 → totalSessionMessages = 6 = 2x threshold(3)
370
+ const r6 = await runHook(PROMPT_SUBMIT, { prompt: 'Show me how the layout grid works please', session_id: TEST_SESSION });
371
+ flag = readFlag();
372
+ assert(flag.totalSessionMessages === 6, `totalSessionMessages = 6 (got ${flag?.totalSessionMessages})`);
373
+ assert(flag.userLearningNudgeCount === 2, `nudgeCount = 2 (got ${flag?.userLearningNudgeCount})`);
374
+ assert(flag.userLearningPending === true, 'userLearningPending set for 2nd nudge');
375
+
376
+ const ctx6 = getContext(r6);
377
+ assert(ctx6.includes('User Learning') || ctx6.includes('communication'), 'Second nudge present in context');
378
+
379
+ // ═══════════════════════════════════════════════════════════
380
+ // TEST 10: Max nudges cap (3) prevents further nudges
381
+ // ═══════════════════════════════════════════════════════════
382
+ console.log(`\n${Y}Test 10: Max nudges cap prevents further nudges${X}`);
383
+ cleanup();
384
+
385
+ writeFlag({
386
+ editCount: 0, retries: 0, files: [],
387
+ messageCount: 11, totalSessionMessages: 11,
388
+ userLearningNudgeCount: 3, userLearningPending: false,
389
+ greetingDelivered: true,
390
+ });
391
+
392
+ // Message 12 → totalSessionMessages = 12 = 4x threshold(3), but nudgeCount=3 = max
393
+ const r12 = await runHook(PROMPT_SUBMIT, { prompt: 'Explain the animation system in the project', session_id: TEST_SESSION });
394
+ flag = readFlag();
395
+ assert(flag.userLearningNudgeCount === 3, `nudgeCount stays 3 (got ${flag?.userLearningNudgeCount})`);
396
+ assert(!flag.userLearningPending, 'No pending set when max nudges reached');
397
+
398
+ // ═══════════════════════════════════════════════════════════
399
+ // TEST 11: Feature disabled → no nudge
400
+ // ═══════════════════════════════════════════════════════════
401
+ console.log(`\n${Y}Test 11: Feature disabled skips nudge entirely${X}`);
402
+ cleanup();
403
+
404
+ writeFileSync(HOOK_FEATURES_PATH, JSON.stringify({ ...testFeatures, userLearning: false }));
405
+
406
+ writeFlag({
407
+ editCount: 0, retries: 0, files: [],
408
+ messageCount: 2, totalSessionMessages: 2,
409
+ greetingDelivered: true,
410
+ });
411
+
412
+ // Message 3 → would normally fire, but feature is off
413
+ const rOff = await runHook(PROMPT_SUBMIT, { prompt: 'Describe the button styles currently used', session_id: TEST_SESSION });
414
+ flag = readFlag();
415
+ assert(!flag.userLearningPending, 'No nudge when feature disabled');
416
+ assert(!flag.userLearningNudgeCount, 'nudgeCount stays 0 when disabled');
417
+
418
+ const ctxOff = getContext(rOff);
419
+ assert(!ctxOff.includes('User Learning'), 'No user learning text in output when disabled');
420
+
421
+ // Restore features
422
+ writeFileSync(HOOK_FEATURES_PATH, JSON.stringify(testFeatures));
423
+
424
+ // ═══════════════════════════════════════════════════════════
425
+ // TEST 12: softCleanupFlag preserves tracking fields
426
+ // ═══════════════════════════════════════════════════════════
427
+ console.log(`\n${Y}Test 12: softCleanupFlag preserves session tracking${X}`);
428
+ cleanup();
429
+
430
+ writeFlag({
431
+ editCount: 0, retries: 0, files: [],
432
+ messageCount: 2, totalSessionMessages: 7,
433
+ totalEdits: 5, greetingDelivered: true, rememberCount: 1,
434
+ userLearningNudgeCount: 2, userLearningPending: false, userLearningRetries: 0,
435
+ userLearningObserved: true,
436
+ });
437
+
438
+ // Stop hook with editCount=0 and no blocking conditions → softCleanup runs
439
+ await runHook(STOP_HOOK, { session_id: TEST_SESSION });
440
+ flag = readFlag();
441
+ assert(flag.userLearningNudgeCount === 2, `nudgeCount preserved (got ${flag?.userLearningNudgeCount})`);
442
+ assert(flag.greetingDelivered === true, 'greetingDelivered preserved');
443
+ assert(flag.totalSessionMessages === 7, `totalSessionMessages preserved (got ${flag?.totalSessionMessages})`);
444
+ assert(flag.totalEdits === 5, `totalEdits preserved (got ${flag?.totalEdits})`);
445
+ assert(flag.editCount === 0, `editCount reset to 0 (got ${flag?.editCount})`);
446
+ assert(flag.files.length === 0, 'files array cleared');
447
+ assert(flag.userLearningObserved === true, 'userLearningObserved preserved through softCleanup');
448
+
449
+ // ═══════════════════════════════════════════════════════════
450
+ // TEST 13: Full happy path lifecycle
451
+ // ═══════════════════════════════════════════════════════════
452
+ console.log(`\n${Y}Test 13: Full lifecycle — nudge → stop block → remember → clear → allow stop${X}`);
453
+ cleanup();
454
+
455
+ // Simulate 3 messages to trigger nudge
456
+ await runHook(PROMPT_SUBMIT, { prompt: 'First message about the project layout', session_id: TEST_SESSION });
457
+ await runHook(PROMPT_SUBMIT, { prompt: 'Second message about the styling approach', session_id: TEST_SESSION });
458
+ const r3full = await runHook(PROMPT_SUBMIT, { prompt: 'Third message explaining the component tree', session_id: TEST_SESSION });
459
+
460
+ flag = readFlag();
461
+ assert(flag.userLearningPending === true, 'Lifecycle: nudge fired at message 3');
462
+
463
+ // Stop blocks
464
+ const stopBlock = await runHook(STOP_HOOK, { session_id: TEST_SESSION });
465
+ assert(stopBlock.parsed.decision === 'block', 'Lifecycle: stop blocks for user learning');
466
+
467
+ // Claude calls remember with communication-style
468
+ await runHook(POST_REMEMBER, {
469
+ session_id: TEST_SESSION,
470
+ tool_name: 'mcp__SynaBun__remember',
471
+ tool_input: { category: 'communication-style', text: 'User gives detailed, multi-sentence instructions', project: 'global', importance: 6 },
472
+ tool_response: { success: true },
473
+ });
474
+
475
+ flag = readFlag();
476
+ assert(flag.userLearningPending === false, 'Lifecycle: pending cleared after remember');
477
+
478
+ // Stop now allows
479
+ const stopAllow = await runHook(STOP_HOOK, { session_id: TEST_SESSION });
480
+ assert(!stopAllow.parsed.decision, 'Lifecycle: stop allows after user learning complete');
481
+
482
+ // ═══════════════════════════════════════════════════════════
483
+ // TEST 14: Edit-heavy session — user learning bundled with task remember
484
+ // ═══════════════════════════════════════════════════════════
485
+ console.log(`\n${Y}Test 14: User learning bundled with task-remember in edit-heavy session${X}`);
486
+ cleanup();
487
+
488
+ // Simulate: 3 messages + 3 edits + userLearningPending
489
+ writeFlag({
490
+ editCount: 3, retries: 0, files: ['a.js', 'b.js', 'c.js'],
491
+ messageCount: 3, totalSessionMessages: 3,
492
+ userLearningPending: true, userLearningNudgeCount: 1, userLearningRetries: 0,
493
+ greetingDelivered: true,
494
+ });
495
+
496
+ // Stop should block with BOTH obligations bundled in one block
497
+ const stopBundled = await runHook(STOP_HOOK, { session_id: TEST_SESSION });
498
+ assert(stopBundled.parsed.decision === 'block', 'Bundled: blocks with combined obligations');
499
+ const bundledReason = stopBundled.parsed.reason || '';
500
+ assert(bundledReason.includes('Task memory'), 'Bundled: reason includes task memory');
501
+ assert(bundledReason.includes('User learning'), 'Bundled: reason includes user learning');
502
+ assert(bundledReason.includes('communication-style'), 'Bundled: reason mentions communication-style');
503
+
504
+ // Claude does task remember → editCount resets, userLearningPending preserved
505
+ await runHook(POST_REMEMBER, {
506
+ session_id: TEST_SESSION,
507
+ tool_name: 'mcp__SynaBun__remember',
508
+ tool_input: { category: 'bug-fixes', text: 'Fixed rendering issues' },
509
+ tool_response: { success: true },
510
+ });
511
+
512
+ flag = readFlag();
513
+ assert(flag.editCount === 0, 'Bundled: editCount reset after task remember');
514
+ assert(flag.userLearningPending === true, 'Bundled: userLearningPending still true (needs comm-style remember)');
515
+
516
+ } finally {
517
+ cleanup();
518
+ writeFileSync(HOOK_FEATURES_PATH, originalFeatures);
519
+ }
520
+
521
+ // Summary
522
+ console.log(`\n${B}Results: ${G}${passed} passed${X}${failed > 0 ? `, ${R}${failed} failed${X}` : ''}${X}\n`);
523
+ process.exit(failed > 0 ? 1 : 0);
524
+ }
525
+
526
+ main().catch(e => {
527
+ console.error(e);
528
+ try { writeFileSync(HOOK_FEATURES_PATH, originalFeatures); } catch { /* ok */ }
529
+ cleanup();
530
+ process.exit(1);
531
+ });
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "claude-memory-mcp",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=22.5.0"
7
+ },
8
+ "main": "dist/index.js",
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "start": "node dist/index.js",
14
+ "start:http": "node dist/preload-http.js",
15
+ "dev": "tsx src/index.ts",
16
+ "dev:http": "tsx src/preload-http.ts",
17
+ "tui": "node dist/tui.js",
18
+ "tui:dev": "tsx src/tui.ts",
19
+ "migrate": "tsx src/scripts/migrate-qdrant-to-sqlite.ts"
20
+ },
21
+ "dependencies": {
22
+ "@huggingface/transformers": "^3.0.0",
23
+ "@inquirer/prompts": "^8.2.0",
24
+ "@modelcontextprotocol/sdk": "^1.12.1",
25
+ "chalk": "^5.6.2",
26
+ "express": "^4.21.0",
27
+ "uuid": "^11.1.0",
28
+ "zod": "^3.24.2"
29
+ },
30
+ "devDependencies": {
31
+ "@types/express": "^5.0.0",
32
+ "@types/node": "^22.12.0",
33
+ "@types/uuid": "^10.0.0",
34
+ "tsx": "^4.19.0",
35
+ "typescript": "^5.7.0",
36
+ "vitest": "^4.1.0"
37
+ }
38
+ }
@@ -0,0 +1,10 @@
1
+ import { register } from 'node:module';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { resolve, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ process.chdir(__dirname);
8
+
9
+ const entry = pathToFileURL(resolve(__dirname, 'dist', 'preload.js')).href;
10
+ await import(entry);
@@ -0,0 +1,70 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+
4
+ // --- Static config ---
5
+
6
+ export const config = {
7
+ dataDir: process.env.MEMORY_DATA_DIR || path.resolve(import.meta.dirname || process.cwd(), '..', 'data'),
8
+ sqlite: {
9
+ dbPath: process.env.SQLITE_DB_PATH || '', // resolved lazily from dataDir
10
+ },
11
+ embedding: {
12
+ model: 'Xenova/all-MiniLM-L6-v2',
13
+ dimensions: 384,
14
+ },
15
+ } as const;
16
+
17
+ // --- .env path resolution (for file watching) ---
18
+
19
+ export function getEnvPath(): string {
20
+ return process.env.DOTENV_PATH || path.resolve(import.meta.dirname || process.cwd(), '..', '..', '.env');
21
+ }
22
+
23
+ // --- Project Detection (reads from claude-code-projects.json) ---
24
+
25
+ const PROJECTS_PATH = path.join(config.dataDir, 'claude-code-projects.json');
26
+
27
+ interface RegisteredProject {
28
+ path: string;
29
+ label: string;
30
+ }
31
+
32
+ function loadRegisteredProjects(): RegisteredProject[] {
33
+ try {
34
+ if (!fs.existsSync(PROJECTS_PATH)) return [];
35
+ return JSON.parse(fs.readFileSync(PROJECTS_PATH, 'utf-8'));
36
+ } catch { return []; }
37
+ }
38
+
39
+ function normalizeLabel(label: string): string {
40
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
41
+ }
42
+
43
+ export function detectProject(cwd?: string): string {
44
+ const dir = cwd || process.cwd();
45
+ const lower = dir.toLowerCase().replace(/\\/g, '/');
46
+ const projects = loadRegisteredProjects();
47
+
48
+ // Sort by path length descending — most specific match wins
49
+ const sorted = projects
50
+ .map((p) => ({
51
+ path: p.path.toLowerCase().replace(/\\/g, '/'),
52
+ label: normalizeLabel(p.label),
53
+ }))
54
+ .sort((a, b) => b.path.length - a.path.length);
55
+
56
+ // 1. Exact path prefix match (cwd is inside a registered project)
57
+ for (const p of sorted) {
58
+ if (lower.startsWith(p.path + '/') || lower === p.path) return p.label;
59
+ }
60
+
61
+ // 2. Substring match — cwd folder name contains a registered project's folder name
62
+ for (const p of sorted) {
63
+ const projFolder = path.basename(p.path).toLowerCase();
64
+ if (lower.includes(projFolder)) return p.label;
65
+ }
66
+
67
+ // 3. Fallback to directory basename
68
+ const base = path.basename(dir).toLowerCase().replace(/[^a-z0-9-]/g, '-');
69
+ return base || 'global';
70
+ }