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,241 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SynaBun PreCompact Hook for Claude Code
5
+ *
6
+ * Fires BEFORE context compaction (manual or auto). Reads the session
7
+ * transcript and caches a lightweight session summary so the post-compact
8
+ * SessionStart hook can inject it as context for automatic conversation
9
+ * indexing in SynaBun.
10
+ *
11
+ * Input (stdin JSON):
12
+ * { session_id, transcript_path, cwd, trigger: "manual"|"auto", custom_instructions? }
13
+ *
14
+ * Output: exit code 0 (PreCompact hooks cannot inject context or block)
15
+ *
16
+ * Enable in .claude/settings.json:
17
+ * {
18
+ * "hooks": {
19
+ * "PreCompact": [{
20
+ * "matcher": "",
21
+ * "hooks": [{
22
+ * "type": "command",
23
+ * "command": "node \"<path-to-synabun>/hooks/claude-code/pre-compact.mjs\"",
24
+ * "timeout": 10
25
+ * }]
26
+ * }]
27
+ * }
28
+ * }
29
+ */
30
+
31
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, statSync, appendFileSync } from 'node:fs';
32
+ import { dirname, join, basename } from 'node:path';
33
+ import { fileURLToPath } from 'node:url';
34
+
35
+ const __dirname = dirname(fileURLToPath(import.meta.url));
36
+ const CACHE_DIR = join(__dirname, '..', '..', 'data', 'precompact');
37
+ const PENDING_DIR = join(__dirname, '..', '..', 'data', 'pending-compact');
38
+ const DEBUG_LOG = join(__dirname, '..', '..', 'data', 'compact-debug.log');
39
+
40
+ // --- Stdin ---
41
+
42
+ function readStdin() {
43
+ return new Promise((resolve) => {
44
+ if (process.stdin.isTTY) return resolve('{}');
45
+ let data = '';
46
+ process.stdin.setEncoding('utf8');
47
+ process.stdin.on('data', (chunk) => { data += chunk; });
48
+ process.stdin.on('end', () => resolve(data));
49
+ setTimeout(() => resolve(data || '{}'), 3000);
50
+ });
51
+ }
52
+
53
+ // --- Transcript parsing ---
54
+
55
+ function parseTranscript(transcriptPath) {
56
+ const userMessages = [];
57
+ const assistantSnippets = [];
58
+ const toolsUsed = new Set();
59
+ const filesModified = new Set();
60
+ const filesRead = new Set();
61
+ let totalTurns = 0;
62
+
63
+ try {
64
+ const content = readFileSync(transcriptPath, 'utf-8');
65
+ const lines = content.split('\n').filter(l => l.trim());
66
+
67
+ for (const line of lines) {
68
+ try {
69
+ const entry = JSON.parse(line);
70
+
71
+ if (entry.role === 'user') {
72
+ totalTurns++;
73
+ // Extract text from user messages (string or content array)
74
+ if (typeof entry.content === 'string') {
75
+ const text = entry.content.trim();
76
+ if (text && !text.startsWith('{') && text.length > 5) {
77
+ userMessages.push(text.slice(0, 300));
78
+ }
79
+ } else if (Array.isArray(entry.content)) {
80
+ for (const block of entry.content) {
81
+ if (block.type === 'text' && block.text) {
82
+ const text = block.text.trim();
83
+ if (text && !text.startsWith('{') && text.length > 5) {
84
+ userMessages.push(text.slice(0, 300));
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ if (entry.role === 'assistant' && Array.isArray(entry.content)) {
92
+ for (const block of entry.content) {
93
+ // Capture tool usage
94
+ if (block.type === 'tool_use') {
95
+ toolsUsed.add(block.name);
96
+ const input = block.input || {};
97
+
98
+ // Track file modifications
99
+ if (['Edit', 'Write', 'NotebookEdit'].includes(block.name) && input.file_path) {
100
+ filesModified.add(input.file_path);
101
+ }
102
+ if (block.name === 'Read' && input.file_path) {
103
+ filesRead.add(input.file_path);
104
+ }
105
+ // Track bash commands that modify files
106
+ if (block.name === 'Bash' && input.command) {
107
+ const cmd = input.command;
108
+ if (/\bgit\s+(add|commit|push|merge|rebase|checkout)/.test(cmd)) {
109
+ toolsUsed.add('git');
110
+ }
111
+ if (/\bnpm\s+(install|run|test|build)/.test(cmd)) {
112
+ toolsUsed.add('npm');
113
+ }
114
+ }
115
+ }
116
+
117
+ // Capture assistant text snippets (first few)
118
+ if (block.type === 'text' && block.text && assistantSnippets.length < 5) {
119
+ const text = block.text.trim();
120
+ if (text.length > 20) {
121
+ assistantSnippets.push(text.slice(0, 200));
122
+ }
123
+ }
124
+ }
125
+ }
126
+ } catch { /* skip malformed lines */ }
127
+ }
128
+ } catch { /* transcript unreadable */ }
129
+
130
+ return {
131
+ userMessages: userMessages.slice(0, 15),
132
+ assistantSnippets,
133
+ toolsUsed: [...toolsUsed],
134
+ filesModified: [...filesModified],
135
+ filesRead: [...filesRead].slice(0, 20),
136
+ totalTurns,
137
+ };
138
+ }
139
+
140
+ // --- Cleanup old cache files (older than 2 hours) ---
141
+
142
+ function cleanupOldCache() {
143
+ try {
144
+ if (!existsSync(CACHE_DIR)) return;
145
+ const cutoff = Date.now() - 2 * 60 * 60 * 1000;
146
+ for (const file of readdirSync(CACHE_DIR)) {
147
+ const filePath = join(CACHE_DIR, file);
148
+ try {
149
+ const stat = statSync(filePath);
150
+ if (stat.mtimeMs < cutoff) unlinkSync(filePath);
151
+ } catch { /* skip */ }
152
+ }
153
+ } catch { /* ignore cleanup errors */ }
154
+ }
155
+
156
+ // --- Main ---
157
+
158
+ async function main() {
159
+ let input = {};
160
+ try {
161
+ const raw = await readStdin();
162
+ input = JSON.parse(raw);
163
+ } catch { /* proceed with defaults */ }
164
+
165
+ const sessionId = input.session_id || '';
166
+ const transcriptPath = input.transcript_path || '';
167
+ const trigger = input.trigger || 'unknown';
168
+ const cwd = input.cwd || '';
169
+
170
+ if (!sessionId || !transcriptPath) {
171
+ process.exit(0);
172
+ return;
173
+ }
174
+
175
+ // Parse transcript for key data
176
+ const parsed = parseTranscript(transcriptPath);
177
+
178
+ // Build cache object
179
+ const cache = {
180
+ session_id: sessionId,
181
+ transcript_path: transcriptPath,
182
+ trigger,
183
+ cwd,
184
+ cached_at: new Date().toISOString(),
185
+ user_message_count: parsed.userMessages.length,
186
+ total_turns: parsed.totalTurns,
187
+ user_messages: parsed.userMessages,
188
+ assistant_snippets: parsed.assistantSnippets,
189
+ tools_used: parsed.toolsUsed,
190
+ files_modified: parsed.filesModified,
191
+ files_read: parsed.filesRead,
192
+ };
193
+
194
+ // Capture active loop state for compaction recovery
195
+ const LOOP_DIR = join(__dirname, '..', '..', 'data', 'loop');
196
+ const loopPath = join(LOOP_DIR, `${sessionId}.json`);
197
+ if (existsSync(loopPath)) {
198
+ try {
199
+ const loopState = JSON.parse(readFileSync(loopPath, 'utf-8'));
200
+ if (loopState.active) {
201
+ cache.loop = {
202
+ active: true,
203
+ task: loopState.task,
204
+ context: loopState.context,
205
+ currentIteration: loopState.currentIteration,
206
+ totalIterations: loopState.totalIterations,
207
+ maxMinutes: loopState.maxMinutes,
208
+ startedAt: loopState.startedAt,
209
+ progressSummary: loopState.progressSummary || null,
210
+ journal: (loopState.journal || []).slice(-5),
211
+ lastMemoryAt: loopState.lastMemoryAt || 0,
212
+ usesBrowser: loopState.usesBrowser || false,
213
+ };
214
+ }
215
+ } catch { /* skip */ }
216
+ }
217
+
218
+ // Write cache
219
+ if (!existsSync(CACHE_DIR)) mkdirSync(CACHE_DIR, { recursive: true });
220
+ writeFileSync(join(CACHE_DIR, `${sessionId}.json`), JSON.stringify(cache, null, 2));
221
+
222
+ // Write pending flag — Stop hook will block until this is cleared by PostToolUse
223
+ if (!existsSync(PENDING_DIR)) mkdirSync(PENDING_DIR, { recursive: true });
224
+ writeFileSync(join(PENDING_DIR, `${sessionId}.json`), JSON.stringify({
225
+ session_id: sessionId,
226
+ created_at: new Date().toISOString(),
227
+ retries: 0,
228
+ }));
229
+
230
+ // Debug logging
231
+ try {
232
+ appendFileSync(DEBUG_LOG, `[${new Date().toISOString()}] PRE-COMPACT session_id=${sessionId} trigger=${trigger} cwd=${cwd}\n`);
233
+ } catch { /* ok */ }
234
+
235
+ // Cleanup old files
236
+ cleanupOldCache();
237
+
238
+ process.exit(0);
239
+ }
240
+
241
+ main().catch(() => process.exit(0));
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SynaBun PreToolUse Hook — Block WebSearch/WebFetch when browser is active
5
+ *
6
+ * Matches: ^WebSearch$|^WebFetch$
7
+ *
8
+ * Blocks these tools when SynaBun's Neural Interface is reachable AND:
9
+ * 1. An active loop with usesBrowser=true exists, OR
10
+ * 2. Any browser session is currently open
11
+ *
12
+ * If the Neural Interface is unreachable (SynaBun offline), all checks are
13
+ * skipped — loop files on disk may be stale and cannot be trusted.
14
+ *
15
+ * Input (stdin JSON):
16
+ * { session_id, tool_name, tool_input: { ... } }
17
+ *
18
+ * Output (stdout JSON):
19
+ * - If blocked: { decision: "block", reason: "..." }
20
+ * - If allowed: {}
21
+ */
22
+
23
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
24
+ import { join, dirname } from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ const SYNABUN_ROOT = join(__dirname, '..', '..');
29
+ const LOOP_DIR = join(SYNABUN_ROOT, 'data', 'loop');
30
+ const NI_BASE = process.env.SYNABUN_NI_URL || 'http://localhost:3344';
31
+
32
+ const BLOCK_REASON = (toolName) =>
33
+ `BLOCKED: ${toolName} is not allowed while a SynaBun browser session is open. Use the SynaBun browser tools (browser_navigate, browser_click, browser_content, etc.) instead. If the page requires login or CAPTCHA, STOP and tell the user what they need to do in the browser panel. Do NOT fall back to web search.`;
34
+
35
+ let input = '';
36
+ process.stdin.setEncoding('utf-8');
37
+ process.stdin.on('data', chunk => { input += chunk; });
38
+ process.stdin.on('end', async () => {
39
+ try {
40
+ const { session_id, tool_name } = JSON.parse(input);
41
+
42
+ // First, check if the Neural Interface is reachable.
43
+ // If it's down, skip all checks — loop files may be stale.
44
+ let niReachable = false;
45
+ let activeBrowserSessions = [];
46
+ try {
47
+ const controller = new AbortController();
48
+ const timeout = setTimeout(() => controller.abort(), 2000);
49
+ const res = await fetch(`${NI_BASE}/api/browser/sessions`, { signal: controller.signal });
50
+ clearTimeout(timeout);
51
+ if (res.ok) {
52
+ niReachable = true;
53
+ const data = await res.json();
54
+ activeBrowserSessions = data.sessions || [];
55
+ }
56
+ } catch {
57
+ // Neural Interface not reachable — allow all tools
58
+ }
59
+
60
+ if (!niReachable) {
61
+ process.stdout.write(JSON.stringify({}));
62
+ return;
63
+ }
64
+
65
+ // Check 1: Active loop with usesBrowser (only trusted when NI is reachable)
66
+ if (existsSync(LOOP_DIR)) {
67
+ const files = readdirSync(LOOP_DIR).filter(f => f.endsWith('.json') && !f.startsWith('pending-'));
68
+ for (const file of files) {
69
+ try {
70
+ const loop = JSON.parse(readFileSync(join(LOOP_DIR, file), 'utf-8'));
71
+ if (loop.active && loop.usesBrowser) {
72
+ process.stdout.write(JSON.stringify({ decision: 'block', reason: BLOCK_REASON(tool_name) }));
73
+ return;
74
+ }
75
+ } catch { /* skip corrupt files */ }
76
+ }
77
+ }
78
+
79
+ // Check 2: Any active browser session via the NI response we already fetched
80
+ if (activeBrowserSessions.length > 0) {
81
+ process.stdout.write(JSON.stringify({ decision: 'block', reason: BLOCK_REASON(tool_name) }));
82
+ return;
83
+ }
84
+
85
+ // No browser activity — allow the tool
86
+ process.stdout.write(JSON.stringify({}));
87
+ } catch {
88
+ process.stdout.write(JSON.stringify({}));
89
+ }
90
+ });