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,26 @@
1
+ /**
2
+ * MCP response helpers — eliminates verbose response object boilerplate.
3
+ * Inspired by FastMCP's approach of letting tools return simple values.
4
+ *
5
+ * Before: return { content: [{ type: 'text' as const, text: msg }] };
6
+ * After: return text(msg);
7
+ */
8
+
9
+ /**
10
+ * Strip unpaired Unicode surrogates (U+D800–U+DFFF) that produce
11
+ * invalid JSON ("no low surrogate in string" API errors).
12
+ * Common in browser-extracted content from Facebook, WhatsApp, etc.
13
+ */
14
+ function sanitizeSurrogates(s: string): string {
15
+ return s.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, '\uFFFD');
16
+ }
17
+
18
+ /** Wrap a string in the MCP text response format. */
19
+ export function text(msg: string) {
20
+ return { content: [{ type: 'text' as const, text: sanitizeSurrogates(msg) }] };
21
+ }
22
+
23
+ /** Wrap base64 image data in the MCP image response format. */
24
+ export function image(data: string, mimeType: string) {
25
+ return { content: [{ type: 'image' as const, data, mimeType }] };
26
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ import { restoreMemory, getMemory } from '../services/sqlite.js';
3
+ import type { MemoryPayload } from '../types.js';
4
+ import { text } from './response.js';
5
+
6
+ export const restoreSchema = {
7
+ memory_id: z
8
+ .string()
9
+ .describe(
10
+ 'The memory ID to restore from trash. Use the full UUID.'
11
+ ),
12
+ };
13
+
14
+ export const restoreDescription =
15
+ 'Restore a trashed memory by ID. Use this to undo a forget operation.';
16
+
17
+ export async function handleRestore(args: { memory_id: string }) {
18
+ const memoryId = args.memory_id;
19
+
20
+ const existing = await getMemory(memoryId);
21
+ if (!existing) {
22
+ return text(`Memory "${memoryId}" not found.`);
23
+ }
24
+
25
+ const payload = existing.payload as unknown as MemoryPayload;
26
+
27
+ if (!payload.trashed_at) {
28
+ return text(`Memory [${memoryId.slice(0, 8)}] is not in trash.`);
29
+ }
30
+
31
+ await restoreMemory(memoryId);
32
+
33
+ return text(`Restored [${memoryId.slice(0, 8)}]: "${payload.content.slice(0, 80)}..."`);
34
+ }
@@ -0,0 +1,114 @@
1
+ import { z } from 'zod';
2
+ import { scrollMemories } from '../services/sqlite.js';
3
+ import { hashFile } from '../services/file-checksums.js';
4
+ import type { MemoryPayload } from '../types.js';
5
+ import { coerceStringArray } from './utils.js';
6
+ import { text } from './response.js';
7
+
8
+ export const syncSchema = {
9
+ project: z
10
+ .string()
11
+ .optional()
12
+ .describe('Optional: only check memories for this project.'),
13
+ categories: coerceStringArray()
14
+ .optional()
15
+ .describe('Optional: filter results to memories in these categories only.'),
16
+ limit: z
17
+ .coerce.number()
18
+ .min(1)
19
+ .max(1000)
20
+ .optional()
21
+ .describe('Max stale memories to return (default 50). Keep low to avoid output overflow.'),
22
+ };
23
+
24
+ export const syncDescription =
25
+ 'Check for stale memories whose related files have changed. Compares stored file checksums against current hashes. Returns compact output (ID + changed files only, no content). IMPORTANT: Always pass "categories" to scope the scan — calling without categories scans ALL memories and may produce output too large to return inline. For large memory sets, call iteratively per category rather than globally. Default limit is 50; increase only when using a narrow category filter.';
26
+
27
+ export async function handleSync(args: { project?: string; categories?: string[]; limit?: number }) {
28
+ // Scroll all memories (paginated)
29
+ const allPoints: Array<{ id: string | number; payload: unknown }> = [];
30
+ let offset: string | undefined;
31
+
32
+ do {
33
+ const filter = args.project
34
+ ? { must: [{ key: 'project', match: { value: args.project } }] }
35
+ : undefined;
36
+ const result = await scrollMemories(filter, 100, offset);
37
+ for (const p of result.points) {
38
+ if (p.payload) allPoints.push({ id: p.id, payload: p.payload });
39
+ }
40
+ offset = (result.next_page_offset as string) ?? undefined;
41
+ } while (offset);
42
+
43
+ // Only evaluate memories with stored file_checksums — we need a baseline to compare against.
44
+ // Memories with related_files but no checksums are legacy; we can't determine staleness without a baseline.
45
+ const withChecksums = allPoints.filter(p => {
46
+ const payload = p.payload as unknown as MemoryPayload;
47
+ return payload.file_checksums && Object.keys(payload.file_checksums).length > 0;
48
+ });
49
+
50
+ const stale: Array<{
51
+ id: string | number;
52
+ category: string;
53
+ importance: number;
54
+ stale_files: string[];
55
+ }> = [];
56
+
57
+ for (const point of withChecksums) {
58
+ const payload = point.payload as unknown as MemoryPayload;
59
+ const storedChecksums = payload.file_checksums!;
60
+ const staleFiles: string[] = [];
61
+
62
+ for (const filePath of Object.keys(storedChecksums)) {
63
+ const currentHash = hashFile(filePath);
64
+ if (!currentHash) continue; // File not found — can't compare, skip
65
+
66
+ const storedHash = storedChecksums[filePath];
67
+ if (currentHash !== storedHash) {
68
+ staleFiles.push(filePath);
69
+ }
70
+ }
71
+
72
+ if (staleFiles.length > 0) {
73
+ stale.push({
74
+ id: point.id,
75
+ category: payload.category,
76
+ importance: payload.importance,
77
+ stale_files: staleFiles,
78
+ });
79
+ }
80
+ }
81
+
82
+ // Apply category filter if provided
83
+ const filtered = args.categories && args.categories.length > 0
84
+ ? stale.filter(m => args.categories!.includes(m.category))
85
+ : stale;
86
+
87
+ if (filtered.length === 0) {
88
+ const scopeMsg = args.categories ? ` in categories [${args.categories.join(', ')}]` : '';
89
+ return text(`All clear — checked ${withChecksums.length} memories with stored checksums${scopeMsg}, none are stale.`);
90
+ }
91
+
92
+ // Sort by importance descending
93
+ filtered.sort((a, b) => b.importance - a.importance);
94
+
95
+ // Apply limit
96
+ const maxResults = args.limit ?? 50;
97
+ const limited = filtered.slice(0, maxResults);
98
+ const truncated = filtered.length > maxResults;
99
+
100
+ // Compact output — IDs and changed files only, no full content
101
+ const scopeMsg = args.categories ? ` in [${args.categories.join(', ')}]` : '';
102
+ let msg = `Found ${filtered.length} stale memories${scopeMsg} (out of ${withChecksums.length} with checksums)`;
103
+ if (truncated) msg += ` — showing first ${maxResults}`;
104
+ msg += `:\n\n`;
105
+
106
+ for (const mem of limited) {
107
+ msg += `${mem.id} | ${mem.category} | imp:${mem.importance}\n`;
108
+ msg += ` Changed: ${mem.stale_files.join(', ')}\n`;
109
+ }
110
+
111
+ msg += `\nTo get full content: use recall with the memory ID, or memories with action "by-category".`;
112
+
113
+ return text(msg);
114
+ }
@@ -0,0 +1,98 @@
1
+ import { z } from 'zod';
2
+ import * as ni from '../services/neural-interface.js';
3
+ import { text } from './response.js';
4
+
5
+ // ═══════════════════════════════════════════
6
+ // tictactoe — Dedicated TicTacToe MCP tool
7
+ // ═══════════════════════════════════════════
8
+
9
+ export const tictactoeSchema = {
10
+ action: z.enum(['start', 'move', 'state', 'end']).describe(
11
+ 'Game action. "start" = set up board (optional piece: X or O). "move" = place piece (requires cell 1-9). "state" = get current board. "end" = tear down game.'
12
+ ),
13
+ cell: z.number().min(1).max(9).optional().describe(
14
+ 'Cell number 1-9 (required for "move" action). Layout:\n 1 | 2 | 3\n ---------\n 4 | 5 | 6\n ---------\n 7 | 8 | 9'
15
+ ),
16
+ piece: z.enum(['X', 'O']).optional().describe(
17
+ 'Which piece you play as (for "start" action). Default: X. X always goes first.'
18
+ ),
19
+ };
20
+
21
+ export const tictactoeDescription =
22
+ 'Play Tic Tac Toe on the whiteboard. Actions: "start" sets up the board (piece defaults to X), "move" places the current turn\'s piece in cell 1-9, "state" shows the current board, "end" closes the game. The board renders visually on the whiteboard. X always goes first. Returns ASCII board + game status after each action.';
23
+
24
+ export async function handleTictactoe(args: { action: string; cell?: number; piece?: string }) {
25
+ switch (args.action) {
26
+ case 'start':
27
+ return handleStart(args.piece);
28
+ case 'move':
29
+ return handleMove(args.cell);
30
+ case 'state':
31
+ return handleState();
32
+ case 'end':
33
+ return handleEnd();
34
+ default:
35
+ return text(`Unknown action: ${args.action}`);
36
+ }
37
+ }
38
+
39
+ async function handleStart(piece?: string) {
40
+ const result = await ni.tictactoeStart(piece);
41
+ if (result.error) {
42
+ return text(`Failed to start game: ${result.error}`);
43
+ }
44
+
45
+ const msg = `Game started! You are ${result.piece}.\n\n${result.ascii}\n\nTurn: ${result.turn} | Status: ${result.status}`;
46
+ return text(msg);
47
+ }
48
+
49
+ async function handleMove(cell?: number) {
50
+ if (cell === undefined) {
51
+ return text('cell is required for move action (1-9)');
52
+ }
53
+
54
+ const result = await ni.tictactoeMove(cell);
55
+ if (result.error) {
56
+ return text(`Move failed: ${result.error}`);
57
+ }
58
+
59
+ let statusLine = `Turn: ${result.turn} | Status: ${result.status}`;
60
+ if (result.winner) {
61
+ statusLine = `Status: ${result.winner} wins!`;
62
+ } else if (result.status === 'draw') {
63
+ statusLine = 'Status: draw';
64
+ }
65
+
66
+ const msg = `${result.ascii}\n\n${statusLine}`;
67
+ return text(msg);
68
+ }
69
+
70
+ async function handleState() {
71
+ const result = await ni.tictactoeState();
72
+ if (result.error) {
73
+ return text(`Failed to get state: ${result.error}`);
74
+ }
75
+
76
+ if (!result.active) {
77
+ return text('No active TicTacToe game. Use action "start" to begin.');
78
+ }
79
+
80
+ let statusLine = `Turn: ${result.turn} | Status: ${result.status} | You are: ${result.piece}`;
81
+ if (result.winner) {
82
+ statusLine = `Status: ${result.winner} wins! | You were: ${result.piece}`;
83
+ } else if (result.status === 'draw') {
84
+ statusLine = `Status: draw | You were: ${result.piece}`;
85
+ }
86
+
87
+ const msg = `${result.ascii}\n\n${statusLine}`;
88
+ return text(msg);
89
+ }
90
+
91
+ async function handleEnd() {
92
+ const result = await ni.tictactoeEnd();
93
+ if (result.error) {
94
+ return text(`Failed to end game: ${result.error}`);
95
+ }
96
+
97
+ return text('Game ended. Board cleared.');
98
+ }
@@ -0,0 +1,13 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+
3
+ import {
4
+ tictactoeSchema, tictactoeDescription, handleTictactoe,
5
+ } from './tictactoe-tools.js';
6
+
7
+ /**
8
+ * Register the TicTacToe MCP tool on the given server instance.
9
+ * Single tool with action-based dispatch: start, move, state, end.
10
+ */
11
+ export function registerTicTacToeTools(server: McpServer) {
12
+ server.tool('tictactoe', tictactoeDescription, tictactoeSchema, handleTictactoe);
13
+ }
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Zod schema that accepts an array of strings OR a string representation of one.
5
+ *
6
+ * Claude Code's MCP client sometimes serializes array arguments as strings
7
+ * (e.g. `"[\"a\",\"b\"]"` or `"a, b"`) instead of real JSON arrays.
8
+ * This preprocessor coerces both forms into a proper string array.
9
+ */
10
+ export function coerceStringArray() {
11
+ return z.preprocess((val) => {
12
+ if (Array.isArray(val)) return val;
13
+ if (typeof val === 'string') {
14
+ const trimmed = val.trim();
15
+ if (!trimmed) return [];
16
+ // Try JSON array first: ["a", "b"]
17
+ if (trimmed.startsWith('[')) {
18
+ try { return JSON.parse(trimmed); } catch { /* fall through */ }
19
+ }
20
+ // Comma-separated fallback: "a, b, c"
21
+ return trimmed.split(',').map(s => s.trim()).filter(Boolean);
22
+ }
23
+ return val;
24
+ }, z.array(z.string()));
25
+ }
@@ -0,0 +1,260 @@
1
+ import { z } from 'zod';
2
+ import * as ni from '../services/neural-interface.js';
3
+ import { text, image } from './response.js';
4
+
5
+ // ═══════════════════════════════════════════
6
+ // whiteboard_read
7
+ // ═══════════════════════════════════════════
8
+
9
+ export const whiteboardReadSchema = {};
10
+
11
+ export const whiteboardReadDescription =
12
+ 'Read the current whiteboard state. Returns element descriptions with IDs, positions, and properties, plus the usable viewport dimensions (width x height in pixels, excluding navbar and terminal). ALWAYS call this before placing elements — the viewport tells you the exact canvas boundaries. All elements must fit within these dimensions.';
13
+
14
+ interface WhiteboardElement {
15
+ id: string;
16
+ type: string;
17
+ x: number;
18
+ y: number;
19
+ width?: number;
20
+ height?: number;
21
+ content?: string;
22
+ items?: string[];
23
+ ordered?: boolean;
24
+ shape?: string;
25
+ color?: string;
26
+ fontSize?: number;
27
+ bold?: boolean;
28
+ italic?: boolean;
29
+ points?: number[][];
30
+ startAnchor?: string | null;
31
+ endAnchor?: string | null;
32
+ strokeWidth?: number;
33
+ rotation?: number;
34
+ zIndex?: number;
35
+ dataUrl?: string;
36
+ sectionType?: string;
37
+ label?: string;
38
+ }
39
+
40
+ function describeElement(el: WhiteboardElement): string {
41
+ const pos = `at (${Math.round(el.x)}, ${Math.round(el.y)})`;
42
+
43
+ switch (el.type) {
44
+ case 'text': {
45
+ const style = [
46
+ el.bold && 'bold',
47
+ el.italic && 'italic',
48
+ el.fontSize && `${el.fontSize}px`,
49
+ el.color && `color:${el.color}`,
50
+ ].filter(Boolean).join(', ');
51
+ const content = el.content || '(empty)';
52
+ const preview = content.length > 80 ? content.slice(0, 80) + '...' : content;
53
+ return `[${el.id}] TEXT ${pos}: "${preview}"${style ? ` (${style})` : ''}`;
54
+ }
55
+ case 'shape': {
56
+ const dims = `${Math.round(el.width || 160)}x${Math.round(el.height || 100)}`;
57
+ return `[${el.id}] SHAPE:${el.shape || 'rect'} ${pos} ${dims}${el.color ? ` color:${el.color}` : ''}`;
58
+ }
59
+ case 'arrow': {
60
+ const pts = (el.points || []).map(p => `(${Math.round(p[0])},${Math.round(p[1])})`).join(' → ');
61
+ const anchors = [];
62
+ if (el.startAnchor) anchors.push(`from:${el.startAnchor}`);
63
+ if (el.endAnchor) anchors.push(`to:${el.endAnchor}`);
64
+ return `[${el.id}] ARROW: ${pts}${anchors.length ? ` anchored ${anchors.join(', ')}` : ''}`;
65
+ }
66
+ case 'pen': {
67
+ const ptCount = el.points?.length || 0;
68
+ return `[${el.id}] PEN ${pos} (${ptCount} points, stroke:${el.strokeWidth || 3})${el.color ? ` color:${el.color}` : ''}`;
69
+ }
70
+ case 'list': {
71
+ const items = el.items || [];
72
+ const preview = items.slice(0, 5).map((it, i) => ` ${i + 1}. ${it}`).join('\n');
73
+ const more = items.length > 5 ? `\n ... +${items.length - 5} more` : '';
74
+ return `[${el.id}] LIST ${pos} (${items.length} item${items.length !== 1 ? 's' : ''}):\n${preview}${more}`;
75
+ }
76
+ case 'image': {
77
+ const dims = `${Math.round(el.width || 0)}x${Math.round(el.height || 0)}`;
78
+ return `[${el.id}] IMAGE ${pos} ${dims}`;
79
+ }
80
+ case 'section': {
81
+ const dims = `${Math.round(el.width || 0)}x${Math.round(el.height || 0)}`;
82
+ const sType = el.sectionType || 'unknown';
83
+ const label = el.label || sType;
84
+ return `[${el.id}] SECTION:${sType} ${pos} ${dims} "${label}"`;
85
+ }
86
+ default:
87
+ return `[${el.id}] ${el.type.toUpperCase()} ${pos}`;
88
+ }
89
+ }
90
+
91
+ export async function handleWhiteboardRead() {
92
+ const result = await ni.getWhiteboard();
93
+ if (result.error) {
94
+ return text(`Failed to read whiteboard: ${result.error}`);
95
+ }
96
+
97
+ const elements = (result.elements || []) as WhiteboardElement[];
98
+ const viewport = result.viewport as { width: number; height: number; yOffset?: number; xOffset?: number } | null;
99
+
100
+ if (elements.length === 0) {
101
+ let emptyText = 'Whiteboard is empty. No elements present.';
102
+ if (viewport) emptyText += `\nUsable viewport: ${viewport.width}x${viewport.height} (origin offset: x=${viewport.xOffset || 0}, y=${viewport.yOffset || 0})\n⚠️ Design compact layouts centered in the viewport. Do NOT fill the entire width — use ~70-80% of viewport width, centered. All sections must fit vertically within the viewport height. Scale section heights proportionally to fit.`;
103
+ return text(emptyText);
104
+ }
105
+
106
+ let msg = `Whiteboard: ${elements.length} element(s)`;
107
+ if (viewport) msg += ` | Usable viewport: ${viewport.width}x${viewport.height} (origin: x=${viewport.xOffset || 0}, y=${viewport.yOffset || 0})`;
108
+ msg += '\n\n';
109
+ msg += elements.map(describeElement).join('\n');
110
+
111
+ // Spatial bounds summary
112
+ const positioned = elements.filter(e => e.x != null && e.y != null);
113
+ if (positioned.length > 0) {
114
+ const xs = positioned.map(e => e.x);
115
+ const ys = positioned.map(e => e.y);
116
+ msg += `\n\nBounds: x=[${Math.round(Math.min(...xs))}, ${Math.round(Math.max(...xs))}], y=[${Math.round(Math.min(...ys))}, ${Math.round(Math.max(...ys))}]`;
117
+ }
118
+
119
+ return text(msg);
120
+ }
121
+
122
+
123
+ // ═══════════════════════════════════════════
124
+ // whiteboard_add
125
+ // ═══════════════════════════════════════════
126
+
127
+ export const whiteboardAddSchema = {
128
+ coordMode: z.enum(['px', 'pct']).optional().describe('Coordinate mode. "px" (default) = absolute pixels. "pct" = percentage of viewport (0-100). x:50 y:50 = exact center.'),
129
+ layout: z.enum(['row', 'column', 'grid', 'center']).optional().describe('Auto-layout (overrides x/y). "row" = horizontal centered. "column" = vertical centered. "grid" = auto columns. "center" = stacked center.'),
130
+ elements: z.array(z.object({
131
+ type: z.enum(['text', 'list', 'shape', 'arrow', 'pen', 'image', 'section']).describe('Element type'),
132
+ x: z.number().optional().describe('X position (pixels from left). Required for text, list, shape.'),
133
+ y: z.number().optional().describe('Y position (pixels from top). Required for text, list, shape.'),
134
+ width: z.number().optional().describe('Width in pixels (shapes). Default: 160'),
135
+ height: z.number().optional().describe('Height in pixels (shapes). Default: 100'),
136
+ content: z.string().optional().describe('Text content (for text elements). Use \\n for line breaks.'),
137
+ items: z.array(z.string()).optional().describe('List items array (for list elements). Each string is one bullet point.'),
138
+ ordered: z.boolean().optional().describe('Use numbered list instead of bullets (list elements). Default: false'),
139
+ fontSize: z.number().optional().describe('Font size in px (text: default 22, list: default 18)'),
140
+ color: z.string().optional().describe('Color as CSS value. Default: rgba(255,255,255,0.85). Options: #ef4444 (red), #f97316 (orange), #eab308 (yellow), #22c55e (green), #3b82f6 (blue), #a855f7 (purple), #ec4899 (pink), #06b6d4 (cyan)'),
141
+ bold: z.boolean().optional().describe('Bold text (text elements)'),
142
+ italic: z.boolean().optional().describe('Italic text (text elements)'),
143
+ shape: z.enum(['rect', 'pill', 'circle', 'drawn-circle']).optional().describe('Shape subtype (for shape elements). Default: rect'),
144
+ points: z.array(z.array(z.number()).length(2)).optional().describe('Array of [x,y] waypoints (for arrow and pen elements). Arrows need 2+ points.'),
145
+ startAnchor: z.string().optional().describe('Element ID to anchor arrow start to'),
146
+ endAnchor: z.string().optional().describe('Element ID to anchor arrow end to'),
147
+ strokeWidth: z.number().optional().describe('Stroke width for pen elements (default 3)'),
148
+ rotation: z.number().optional().describe('Rotation in degrees'),
149
+ url: z.string().optional().describe('URL path for image elements (e.g., "/games/TicTacToe/Cross.svg"). Server resolves to base64 dataUrl.'),
150
+ sectionType: z.enum(['navbar', 'hero', 'sidebar', 'content', 'footer', 'card', 'form', 'image-placeholder', 'button', 'text-block', 'grid', 'modal']).optional().describe('Section type for wireframe elements (type=section). Determines default size, color, and semantic meaning.'),
151
+ label: z.string().optional().describe('Display label for section elements. Defaults to section type name.'),
152
+ })).describe('Array of elements to add to the whiteboard'),
153
+ };
154
+
155
+ export const whiteboardAddDescription =
156
+ 'Add elements to the whiteboard. ALWAYS call whiteboard_read first to get the usable viewport size.\n\n⚠️ LAYOUT RULES (MANDATORY):\n1. COMPACT DESIGN — Do NOT fill the entire viewport. Use ~70-80% of viewport width, centered horizontally. This creates a clean, contained wireframe.\n2. VERTICAL FIT — ALL elements MUST fit within the viewport height. Scale section heights proportionally if the total exceeds the available space. Leave ~20px padding from top and bottom edges.\n3. NO OVERFLOW — No element should extend beyond viewport edges. The viewport already excludes the navbar, toolbar, and terminal. Coordinates are auto-offset to the usable area.\n4. PROPORTIONAL SCALING — Section default sizes are for manual use. When building full-page wireframes via MCP, calculate heights as fractions of viewport height (e.g. navbar=6%, hero=30%, content=40%, footer=10%).\n\nCoordinate modes (coordMode): "px" (default) = absolute pixels. "pct" = percentage of usable viewport (0-100), e.g. x:10 y:5 = near top-left of usable area. Coordinates are automatically offset past the navbar and toolbar.\n\nAuto-layout (layout): overrides individual x/y. "row" = horizontal row centered vertically. "column" = vertical stack centered horizontally. "grid" = auto-grid (2-4 columns based on count). "center" = stacked in center. Layouts auto-fit within viewport.\n\nSupports: text (\\n for line breaks), list (items array), shape (rect/pill/circle/drawn-circle), arrow (points + optional anchoring), pen, image (server URL), section (wireframe blocks). Returns assigned IDs. Good defaults: text fontSize 22, list fontSize 18, shapes 160x100.\n\nWireframe sections (type "section"): Semantic website layout blocks. Set sectionType (navbar/hero/sidebar/content/footer/card/form/image-placeholder/button/text-block/grid/modal) and optional label. When using "pct" coordMode, set widths as % of viewport (e.g. 70 for 70%) and heights as % too. Center horizontally with x: 15 for a 70%-width element.';
157
+
158
+ export async function handleWhiteboardAdd(args: { elements: Record<string, unknown>[]; coordMode?: string; layout?: string }) {
159
+ const result = await ni.addWhiteboardElements(args.elements, args.coordMode, args.layout);
160
+ if (result.error) {
161
+ return text(`Failed to add elements: ${result.error}`);
162
+ }
163
+
164
+ const added = result.added as { id: string; type: string; zIndex: number }[];
165
+ const summary = added.map(a => ` ${a.id} (${a.type}, z:${a.zIndex})`).join('\n');
166
+ return text(`Added ${added.length} element(s):\n${summary}`);
167
+ }
168
+
169
+
170
+ // ═══════════════════════════════════════════
171
+ // whiteboard_update
172
+ // ═══════════════════════════════════════════
173
+
174
+ export const whiteboardUpdateSchema = {
175
+ coordMode: z.enum(['px', 'pct']).optional().describe('Coordinate mode for position/size values. "pct" = percentage of viewport.'),
176
+ id: z.string().describe('The element ID to update (from whiteboard_read results)'),
177
+ updates: z.object({
178
+ x: z.number().optional().describe('New X position'),
179
+ y: z.number().optional().describe('New Y position'),
180
+ width: z.number().optional().describe('New width'),
181
+ height: z.number().optional().describe('New height'),
182
+ content: z.string().optional().describe('New text content (text elements). Use \\n for line breaks.'),
183
+ items: z.array(z.string()).optional().describe('New list items array (list elements)'),
184
+ ordered: z.boolean().optional().describe('Switch between bullet/numbered list (list elements)'),
185
+ fontSize: z.number().optional().describe('New font size'),
186
+ color: z.string().optional().describe('New color'),
187
+ bold: z.boolean().optional().describe('Bold text'),
188
+ italic: z.boolean().optional().describe('Italic text'),
189
+ shape: z.enum(['rect', 'pill', 'circle', 'drawn-circle']).optional().describe('Change shape type'),
190
+ points: z.array(z.array(z.number()).length(2)).optional().describe('New points array'),
191
+ startAnchor: z.string().nullable().optional().describe('New start anchor ID (null to detach)'),
192
+ endAnchor: z.string().nullable().optional().describe('New end anchor ID (null to detach)'),
193
+ rotation: z.number().optional().describe('New rotation in degrees'),
194
+ strokeWidth: z.number().optional().describe('New stroke width'),
195
+ sectionType: z.enum(['navbar', 'hero', 'sidebar', 'content', 'footer', 'card', 'form', 'image-placeholder', 'button', 'text-block', 'grid', 'modal']).optional().describe('Change section type'),
196
+ label: z.string().optional().describe('Change section label'),
197
+ }).describe('Fields to update. Only specified fields are changed; others remain untouched.'),
198
+ };
199
+
200
+ export const whiteboardUpdateDescription =
201
+ 'Update properties of an existing whiteboard element. Use whiteboard_read first to get element IDs. Only the specified fields are changed; others remain untouched. Set coordMode to "pct" to use percentage-based positioning. Changes appear in real-time.';
202
+
203
+ export async function handleWhiteboardUpdate(args: { id: string; updates: Record<string, unknown>; coordMode?: string }) {
204
+ const result = await ni.updateWhiteboardElement(args.id, args.updates, args.coordMode);
205
+ if (result.error) {
206
+ return text(`Failed to update element: ${result.error}`);
207
+ }
208
+
209
+ const el = result.element as WhiteboardElement;
210
+ return text(`Updated: ${describeElement(el)}`);
211
+ }
212
+
213
+
214
+ // ═══════════════════════════════════════════
215
+ // whiteboard_remove
216
+ // ═══════════════════════════════════════════
217
+
218
+ export const whiteboardRemoveSchema = {
219
+ id: z.string().optional().describe('Element ID to remove. Omit to clear the entire whiteboard.'),
220
+ };
221
+
222
+ export const whiteboardRemoveDescription =
223
+ 'Remove a specific element from the whiteboard by ID, or clear the entire whiteboard if no ID is provided. Use whiteboard_read first to see element IDs. Arrows anchored to a removed element will be detached.';
224
+
225
+ export async function handleWhiteboardRemove(args: { id?: string }) {
226
+ if (!args.id) {
227
+ const result = await ni.clearWhiteboard();
228
+ if (result.error) {
229
+ return text(`Failed to clear whiteboard: ${result.error}`);
230
+ }
231
+ return text('Whiteboard cleared.');
232
+ }
233
+
234
+ const result = await ni.removeWhiteboardElement(args.id);
235
+ if (result.error) {
236
+ return text(`Failed to remove element: ${result.error}`);
237
+ }
238
+
239
+ const removed = result.removed as { id: string; type: string };
240
+ return text(`Removed ${removed.type} element ${removed.id}`);
241
+ }
242
+
243
+
244
+ // ═══════════════════════════════════════════
245
+ // whiteboard_screenshot
246
+ // ═══════════════════════════════════════════
247
+
248
+ export const whiteboardScreenshotSchema = {};
249
+
250
+ export const whiteboardScreenshotDescription =
251
+ 'Take a visual screenshot of the whiteboard. Returns a JPEG image showing all current elements. Requires the Neural Interface to be open in a browser with Focus mode active. After receiving the screenshot, spawn a Haiku Task agent to interpret the visual contents if needed.';
252
+
253
+ export async function handleWhiteboardScreenshot() {
254
+ const result = await ni.whiteboardScreenshot();
255
+ if (result.error) {
256
+ return text(`Screenshot failed: ${result.error}`);
257
+ }
258
+
259
+ return image(result.data as string, 'image/jpeg');
260
+ }
@@ -0,0 +1,22 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+
3
+ import {
4
+ whiteboardReadSchema, whiteboardReadDescription, handleWhiteboardRead,
5
+ whiteboardAddSchema, whiteboardAddDescription, handleWhiteboardAdd,
6
+ whiteboardUpdateSchema, whiteboardUpdateDescription, handleWhiteboardUpdate,
7
+ whiteboardRemoveSchema, whiteboardRemoveDescription, handleWhiteboardRemove,
8
+ whiteboardScreenshotSchema, whiteboardScreenshotDescription, handleWhiteboardScreenshot,
9
+ } from './whiteboard-tools.js';
10
+
11
+ /**
12
+ * Register all 5 whiteboard MCP tools on the given server instance.
13
+ * These tools let Claude read, create, modify, remove, and screenshot
14
+ * the whiteboard in the Neural Interface Focus mode.
15
+ */
16
+ export function registerWhiteboardTools(server: McpServer) {
17
+ server.tool('whiteboard_read', whiteboardReadDescription, whiteboardReadSchema, handleWhiteboardRead);
18
+ server.tool('whiteboard_add', whiteboardAddDescription, whiteboardAddSchema, handleWhiteboardAdd);
19
+ server.tool('whiteboard_update', whiteboardUpdateDescription, whiteboardUpdateSchema, handleWhiteboardUpdate);
20
+ server.tool('whiteboard_remove', whiteboardRemoveDescription, whiteboardRemoveSchema, handleWhiteboardRemove);
21
+ server.tool('whiteboard_screenshot', whiteboardScreenshotDescription, whiteboardScreenshotSchema, handleWhiteboardScreenshot);
22
+ }