thetacog-mcp 1.0.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.
package/server.js ADDED
@@ -0,0 +1,1031 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ThetaCog MCP Server v0.1.0 - Cognitive Room Detection & Switching
5
+ *
6
+ * MODE MANAGEMENT, NOT TASK MANAGEMENT.
7
+ *
8
+ * This MCP server enables Claude to:
9
+ * 1. DETECT which cognitive room you should be in based on conversation
10
+ * 2. STATUS check your current room context and input streams
11
+ * 3. SWITCH between rooms with context preservation
12
+ *
13
+ * ARCHITECTURE:
14
+ * - HTML files with embedded JSON = self-contained rooms (git-tracked)
15
+ * - SQLite = optional session state and switch history
16
+ * - Supabase = optional multi-tenant sync (same as CRM pattern)
17
+ *
18
+ * The HTML files ARE the rooms. JSON inside them. Claude Flow MCP talks
19
+ * to the same SQLite when available, but rooms work without it.
20
+ *
21
+ * ROOM ARCHETYPES:
22
+ * - Builder (๐Ÿ”จ Blue, Tactical) - Ship, don't theorize
23
+ * - Architect (๐Ÿ“ Indigo, Strategic) - See the whole war before fighting
24
+ * - Operator (๐ŸŽฉ Green, Strategic) - Close, don't explore
25
+ * - Vault (๐Ÿ”’ Red, Foundational) - Protect the irreversible
26
+ * - Voice (๐ŸŽค Purple, Tactical) - Test messaging variants
27
+ * - Laboratory (๐Ÿงช Cyan, Tactical) - Break things safely
28
+ *
29
+ * TERMINAL MAPPING (macOS default):
30
+ * - iTerm2 โ†’ Builder
31
+ * - VS Code โ†’ Architect
32
+ * - Kitty โ†’ Operator
33
+ * - WezTerm โ†’ Vault
34
+ * - Terminal โ†’ Voice
35
+ * - Cursor โ†’ Laboratory
36
+ *
37
+ * v0.1.0 Changes:
38
+ * - Initial release with 3 core tools: detect, status, switch
39
+ * - SQLite optional (works in memory-only mode)
40
+ * - Graceful shutdown handlers (SIGINT, SIGTERM, SIGHUP)
41
+ * - Install subcommand for easy registration
42
+ *
43
+ * For parallel founders who think in parallel.
44
+ */
45
+
46
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
47
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
48
+ import {
49
+ CallToolRequestSchema,
50
+ ListToolsRequestSchema,
51
+ } from '@modelcontextprotocol/sdk/types.js';
52
+ import fs from 'fs';
53
+ import path from 'path';
54
+ import { fileURLToPath } from 'url';
55
+ import { execSync } from 'child_process';
56
+
57
+ // ============================================================================
58
+ // INSTALL SUBCOMMAND (copied from CRM pattern)
59
+ // ============================================================================
60
+
61
+ if (process.argv[2] === 'install') {
62
+ console.error('๐Ÿง  Installing ThetaCog MCP Server...');
63
+ console.error('');
64
+ console.error('โ„น๏ธ Mode management, not task management.');
65
+ console.error(' - Detects which cognitive room you should be in');
66
+ console.error(' - Switches context with memory palace anchoring');
67
+ console.error(' - SQLite optional (works in memory-only mode)');
68
+ console.error('');
69
+
70
+ try {
71
+ console.error('๐Ÿ“ Registering MCP server with Claude Code...');
72
+ try {
73
+ execSync('claude mcp add thetacog thetacog-mcp', { stdio: 'pipe' });
74
+ console.error('โœ… MCP server registered!');
75
+ } catch (addError) {
76
+ const errorMsg = addError.message || '';
77
+ if (errorMsg.includes('already exists')) {
78
+ console.error('โœ… MCP server already registered!');
79
+ } else {
80
+ throw addError;
81
+ }
82
+ }
83
+ console.error('');
84
+ console.error('๐ŸŽ‰ Installation complete!');
85
+ console.error('');
86
+ console.error('๐Ÿ“‹ Next steps:');
87
+ console.error(' 1. Restart Claude Code (Cmd+Q โ†’ reopen)');
88
+ console.error(' 2. Test with: "What room should I be in?"');
89
+ console.error(' 3. Or: "Switch to architect mode"');
90
+ console.error('');
91
+ console.error('๐Ÿ“– Tools available:');
92
+ console.error(' - thetacog-detect: Analyze conversation for room signals');
93
+ console.error(' - thetacog-status: Get current room context');
94
+ console.error(' - thetacog-switch: Switch to a different room');
95
+ console.error('');
96
+ process.exit(0);
97
+ } catch (error) {
98
+ console.error('โŒ Failed to register MCP server:', error.message);
99
+ console.error(' Try running manually: claude mcp add thetacog thetacog-mcp');
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ // ============================================================================
105
+ // CONFIGURATION
106
+ // ============================================================================
107
+
108
+ const __filename = fileURLToPath(import.meta.url);
109
+ const __dirname = path.dirname(__filename);
110
+
111
+ // Room detection signals - add more patterns here to improve detection
112
+ // Claude Flow can edit this section to add domain-specific signals
113
+ const DETECTION_SIGNALS = {
114
+ builder: {
115
+ keywords: ['ship', 'deploy', 'build', 'fix', 'implement', 'code', 'done', 'finish', 'deadline', 'demo'],
116
+ confidence: 0.85,
117
+ emoji: '๐Ÿ”จ',
118
+ color: '#3b82f6',
119
+ tier: 'tactical'
120
+ },
121
+ architect: {
122
+ keywords: ['strategy', 'roadmap', 'bigger picture', 'sequence', 'prioritize', 'architecture', 'plan', 'vision', 'Q1', 'Q2', 'Q3', 'Q4'],
123
+ confidence: 0.90,
124
+ emoji: '๐Ÿ“',
125
+ color: '#4f46e5',
126
+ tier: 'strategic'
127
+ },
128
+ operator: {
129
+ keywords: ['close', 'deal', 'revenue', 'prospect', 'sales', 'meeting', 'pipeline', 'convert', 'customer'],
130
+ confidence: 0.88,
131
+ emoji: '๐ŸŽฉ',
132
+ color: '#22c55e',
133
+ tier: 'strategic'
134
+ },
135
+ vault: {
136
+ keywords: ['patent', 'IP', 'legal', 'protect', 'trademark', 'confidential', 'irreversible', 'prove', 'validate'],
137
+ confidence: 0.92,
138
+ emoji: '๐Ÿ”’',
139
+ color: '#ef4444',
140
+ tier: 'foundational'
141
+ },
142
+ voice: {
143
+ keywords: ['message', 'copy', 'blog', 'content', 'docs', 'documentation', 'explain', 'teach', 'write'],
144
+ confidence: 0.80,
145
+ emoji: '๐ŸŽค',
146
+ color: '#a855f7',
147
+ tier: 'tactical'
148
+ },
149
+ laboratory: {
150
+ keywords: ['experiment', 'prototype', 'test', 'try', 'explore', 'break', 'hack', 'spike', 'POC'],
151
+ confidence: 0.82,
152
+ emoji: '๐Ÿงช',
153
+ color: '#06b6d4',
154
+ tier: 'tactical'
155
+ }
156
+ };
157
+
158
+ // Terminal to room mapping (macOS)
159
+ // Claude Flow can edit this to add Windows/Linux terminals
160
+ const TERMINALS_MAC = {
161
+ 'iTerm': { app: 'iTerm.app', room: 'builder', html: 'iterm2-builder.html' },
162
+ 'VS Code': { app: 'Visual Studio Code.app', room: 'architect', html: 'vscode-architect.html' },
163
+ 'Kitty': { app: 'kitty.app', room: 'operator', html: 'kitty-operator.html' },
164
+ 'WezTerm': { app: 'WezTerm.app', room: 'vault', html: 'wezterm-vault.html' },
165
+ 'Terminal': { app: 'Terminal.app', room: 'voice', html: 'terminal-voice.html' },
166
+ 'Cursor': { app: 'Cursor.app', room: 'laboratory', html: 'cursor-laboratory.html' },
167
+ 'Alacritty': { app: 'Alacritty.app', room: 'performer', html: 'alacritty-performer.html' }
168
+ };
169
+
170
+ // Memory Palace anchors for each room
171
+ // Claude Flow can customize these per user
172
+ const MEMORY_PALACES = {
173
+ builder: "Walk to the workshop. Blue light. Tools on the wall. The smell of sawdust.",
174
+ architect: "Walk up the stairs to the drafting room. Indigo light. Unroll the blueprints. See the whole war before you fight it.",
175
+ operator: "Enter the trading floor. Green light. The deals are live. Every conversation ends with a next step.",
176
+ vault: "Descend to the vault. Red light. The heavy door closes behind you. What enters here is protected forever.",
177
+ voice: "Step onto the stage. Purple spotlight. The audience is listening. Test the message.",
178
+ laboratory: "Enter the lab. Cyan glow. Safety goggles on. Break things safely here."
179
+ };
180
+
181
+ // Identity rules for each room
182
+ // Claude Flow can expand these
183
+ const IDENTITY_RULES = {
184
+ builder: [
185
+ "You are shipping, not theorizing",
186
+ "Done beats right when the demo is Sunday",
187
+ "Make it work, not perfect"
188
+ ],
189
+ architect: [
190
+ "You are redrawing the entire territory",
191
+ "You see the whole war before you fight it",
192
+ "You do not execute battles. You sequence them."
193
+ ],
194
+ operator: [
195
+ "You are closing, not exploring",
196
+ "Revenue is the only metric that matters here",
197
+ "Every conversation ends with a next step"
198
+ ],
199
+ vault: [
200
+ "You are protecting the irreversible",
201
+ "You are validating before committing",
202
+ "You do not ship from here. You prove from here."
203
+ ],
204
+ voice: [
205
+ "You are testing messaging",
206
+ "Experiment with variants",
207
+ "Find what resonates"
208
+ ],
209
+ laboratory: [
210
+ "You are prototyping fast",
211
+ "Break things safely",
212
+ "Failure is data here"
213
+ ]
214
+ };
215
+
216
+ // ============================================================================
217
+ // TERMINAL DETECTION
218
+ // ============================================================================
219
+
220
+ /**
221
+ * Detect which terminal Claude is running in
222
+ * Uses TERM_PROGRAM env var (set by most terminals)
223
+ */
224
+ function detectTerminal() {
225
+ const termProgram = process.env.TERM_PROGRAM || '';
226
+ const termProgramVersion = process.env.TERM_PROGRAM_VERSION || '';
227
+
228
+ // Map env values to our terminal names
229
+ const termMap = {
230
+ 'iTerm.app': 'iTerm',
231
+ 'vscode': 'VS Code',
232
+ 'Apple_Terminal': 'Terminal',
233
+ 'kitty': 'Kitty',
234
+ 'WezTerm': 'WezTerm',
235
+ 'Cursor': 'Cursor',
236
+ 'Alacritty': 'Alacritty'
237
+ };
238
+
239
+ const detected = termMap[termProgram] || null;
240
+ const room = detected ? TERMINALS_MAC[detected]?.room : null;
241
+
242
+ return {
243
+ termProgram,
244
+ termProgramVersion,
245
+ terminal: detected,
246
+ room: room,
247
+ html: detected ? TERMINALS_MAC[detected]?.html : null
248
+ };
249
+ }
250
+
251
+ // ============================================================================
252
+ // STATE MANAGEMENT (SQLite primary, JSON export layer)
253
+ // ============================================================================
254
+
255
+ let currentRoom = null;
256
+ let roomHistory = [];
257
+ let db = null;
258
+ let thetacogDir = null;
259
+
260
+ // Try to load SQLite
261
+ async function initDatabase() {
262
+ try {
263
+ const Database = (await import('better-sqlite3')).default;
264
+ thetacogDir = path.join(process.env.HOME, '.thetacog');
265
+ const dbPath = path.join(thetacogDir, 'thetacog.db');
266
+
267
+ // Ensure directory exists
268
+ if (!fs.existsSync(thetacogDir)) {
269
+ fs.mkdirSync(thetacogDir, { recursive: true });
270
+ }
271
+
272
+ db = new Database(dbPath);
273
+
274
+ // Create tables if they don't exist
275
+ db.exec(`
276
+ CREATE TABLE IF NOT EXISTS room_state (
277
+ id INTEGER PRIMARY KEY,
278
+ current_room TEXT,
279
+ context_snapshot TEXT,
280
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
281
+ );
282
+
283
+ CREATE TABLE IF NOT EXISTS room_history (
284
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
285
+ from_room TEXT,
286
+ to_room TEXT,
287
+ signal TEXT,
288
+ confidence REAL,
289
+ switched_at DATETIME DEFAULT CURRENT_TIMESTAMP
290
+ );
291
+
292
+ -- Todos per room (the main work items)
293
+ CREATE TABLE IF NOT EXISTS room_todos (
294
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
295
+ room TEXT NOT NULL,
296
+ text TEXT NOT NULL,
297
+ done INTEGER DEFAULT 0,
298
+ priority INTEGER DEFAULT 5,
299
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
300
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
301
+ );
302
+
303
+ -- Input streams between rooms (flywheel)
304
+ CREATE TABLE IF NOT EXISTS room_streams (
305
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
306
+ from_room TEXT NOT NULL,
307
+ to_room TEXT NOT NULL,
308
+ message TEXT NOT NULL,
309
+ read INTEGER DEFAULT 0,
310
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
311
+ );
312
+
313
+ CREATE TABLE IF NOT EXISTS room_configs (
314
+ room_name TEXT PRIMARY KEY,
315
+ config_json TEXT,
316
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
317
+ );
318
+ `);
319
+
320
+ // Load current room from state
321
+ const state = db.prepare('SELECT current_room FROM room_state WHERE id = 1').get();
322
+ if (state) {
323
+ currentRoom = state.current_room;
324
+ }
325
+
326
+ console.error('[ThetaCog] SQLite initialized at', dbPath);
327
+ return true;
328
+ } catch (e) {
329
+ console.error('[ThetaCog] SQLite not available, running in memory-only mode');
330
+ return false;
331
+ }
332
+ }
333
+
334
+ // ============================================================================
335
+ // TODO CRUD (SQLite primary)
336
+ // ============================================================================
337
+
338
+ /**
339
+ * Add a todo to a room
340
+ */
341
+ function addTodo(room, text, priority = 5) {
342
+ if (!db) return { error: 'SQLite not available' };
343
+
344
+ const result = db.prepare(`
345
+ INSERT INTO room_todos (room, text, priority)
346
+ VALUES (?, ?, ?)
347
+ `).run(room, text, priority);
348
+
349
+ exportStateToJson(); // Sync to JSON
350
+ return { id: result.lastInsertRowid, room, text, priority, done: false };
351
+ }
352
+
353
+ /**
354
+ * List todos for a room (or all rooms)
355
+ */
356
+ function listTodos(room = null) {
357
+ if (!db) return [];
358
+
359
+ if (room) {
360
+ return db.prepare(`
361
+ SELECT * FROM room_todos WHERE room = ? ORDER BY priority ASC, created_at DESC
362
+ `).all(room);
363
+ } else {
364
+ return db.prepare(`
365
+ SELECT * FROM room_todos ORDER BY room, priority ASC, created_at DESC
366
+ `).all();
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Update a todo (toggle done, change priority, edit text)
372
+ */
373
+ function updateTodo(id, updates) {
374
+ if (!db) return { error: 'SQLite not available' };
375
+
376
+ const fields = [];
377
+ const values = [];
378
+
379
+ if (updates.done !== undefined) {
380
+ fields.push('done = ?');
381
+ values.push(updates.done ? 1 : 0);
382
+ }
383
+ if (updates.priority !== undefined) {
384
+ fields.push('priority = ?');
385
+ values.push(updates.priority);
386
+ }
387
+ if (updates.text !== undefined) {
388
+ fields.push('text = ?');
389
+ values.push(updates.text);
390
+ }
391
+
392
+ if (fields.length === 0) return { error: 'No updates provided' };
393
+
394
+ fields.push('updated_at = CURRENT_TIMESTAMP');
395
+ values.push(id);
396
+
397
+ db.prepare(`UPDATE room_todos SET ${fields.join(', ')} WHERE id = ?`).run(...values);
398
+ exportStateToJson(); // Sync to JSON
399
+ return { success: true, id };
400
+ }
401
+
402
+ /**
403
+ * Delete a todo
404
+ */
405
+ function deleteTodo(id) {
406
+ if (!db) return { error: 'SQLite not available' };
407
+
408
+ db.prepare('DELETE FROM room_todos WHERE id = ?').run(id);
409
+ exportStateToJson(); // Sync to JSON
410
+ return { success: true, id };
411
+ }
412
+
413
+ // ============================================================================
414
+ // INPUT STREAMS (Flywheel coordination)
415
+ // ============================================================================
416
+
417
+ /**
418
+ * Send a message from one room to another
419
+ */
420
+ function sendStream(fromRoom, toRoom, message) {
421
+ if (!db) return { error: 'SQLite not available' };
422
+
423
+ const result = db.prepare(`
424
+ INSERT INTO room_streams (from_room, to_room, message)
425
+ VALUES (?, ?, ?)
426
+ `).run(fromRoom, toRoom, message);
427
+
428
+ exportStateToJson();
429
+ return { id: result.lastInsertRowid, fromRoom, toRoom, message };
430
+ }
431
+
432
+ /**
433
+ * Get unread streams for a room
434
+ */
435
+ function getStreams(room) {
436
+ if (!db) return [];
437
+
438
+ return db.prepare(`
439
+ SELECT * FROM room_streams WHERE to_room = ? AND read = 0
440
+ ORDER BY created_at DESC
441
+ `).all(room);
442
+ }
443
+
444
+ /**
445
+ * Mark streams as read
446
+ */
447
+ function markStreamsRead(room) {
448
+ if (!db) return { error: 'SQLite not available' };
449
+
450
+ db.prepare('UPDATE room_streams SET read = 1 WHERE to_room = ?').run(room);
451
+ return { success: true };
452
+ }
453
+
454
+ // ============================================================================
455
+ // JSON EXPORT (for HTML to read on tab focus)
456
+ // ============================================================================
457
+
458
+ /**
459
+ * Export full state to JSON file
460
+ * HTML files read this on visibilitychange event
461
+ */
462
+ function exportStateToJson() {
463
+ if (!db || !thetacogDir) return;
464
+
465
+ const state = {
466
+ currentRoom: currentRoom || 'builder',
467
+ exportedAt: new Date().toISOString(),
468
+ rooms: {}
469
+ };
470
+
471
+ // Get all rooms
472
+ const rooms = ['builder', 'architect', 'operator', 'vault', 'voice', 'laboratory'];
473
+
474
+ for (const room of rooms) {
475
+ const todos = db.prepare(`
476
+ SELECT id, text, done, priority FROM room_todos WHERE room = ?
477
+ ORDER BY priority ASC
478
+ `).all(room);
479
+
480
+ const streams = db.prepare(`
481
+ SELECT id, from_room, message, created_at FROM room_streams
482
+ WHERE to_room = ? AND read = 0
483
+ `).all(room);
484
+
485
+ state.rooms[room] = {
486
+ emoji: DETECTION_SIGNALS[room]?.emoji || '๐Ÿง ',
487
+ color: DETECTION_SIGNALS[room]?.color || '#667eea',
488
+ tier: DETECTION_SIGNALS[room]?.tier || 'tactical',
489
+ memoryPalace: MEMORY_PALACES[room],
490
+ identityRules: IDENTITY_RULES[room],
491
+ todos: todos.map(t => ({ ...t, done: !!t.done })),
492
+ inputStreams: streams
493
+ };
494
+ }
495
+
496
+ // Write to JSON file
497
+ const jsonPath = path.join(thetacogDir, 'state.json');
498
+ fs.writeFileSync(jsonPath, JSON.stringify(state, null, 2));
499
+
500
+ return state;
501
+ }
502
+
503
+ /**
504
+ * Open the HTML for a room (or current room's HTML)
505
+ */
506
+ function openRoomHtml(room = null) {
507
+ const targetRoom = room || currentRoom || 'builder';
508
+ const terminal = Object.values(TERMINALS_MAC).find(t => t.room === targetRoom);
509
+
510
+ if (!terminal) {
511
+ return { error: `No HTML mapping for room: ${targetRoom}` };
512
+ }
513
+
514
+ // Find the HTML file in .workflow/rooms/ relative to cwd
515
+ const possiblePaths = [
516
+ path.join(process.cwd(), '.workflow', 'rooms', terminal.html),
517
+ path.join(process.env.HOME, '.thetacog', 'rooms', terminal.html)
518
+ ];
519
+
520
+ for (const htmlPath of possiblePaths) {
521
+ if (fs.existsSync(htmlPath)) {
522
+ // Use execSync to open (works on macOS)
523
+ try {
524
+ execSync(`open "${htmlPath}"`, { stdio: 'pipe' });
525
+ return { success: true, opened: htmlPath, room: targetRoom };
526
+ } catch (e) {
527
+ return { error: `Failed to open: ${e.message}` };
528
+ }
529
+ }
530
+ }
531
+
532
+ return { error: `HTML not found for room: ${targetRoom}`, searched: possiblePaths };
533
+ }
534
+
535
+ // ============================================================================
536
+ // ROOM DETECTION LOGIC
537
+ // ============================================================================
538
+
539
+ /**
540
+ * Detect which room the user should be in based on conversation text
541
+ * @param {string} text - The conversation text to analyze
542
+ * @returns {object} - { room, confidence, signals, switchRecommended }
543
+ */
544
+ function detectRoom(text) {
545
+ const lowerText = text.toLowerCase();
546
+ const matches = [];
547
+
548
+ // Check each room's keywords
549
+ for (const [roomName, config] of Object.entries(DETECTION_SIGNALS)) {
550
+ const foundKeywords = config.keywords.filter(kw => lowerText.includes(kw.toLowerCase()));
551
+ if (foundKeywords.length > 0) {
552
+ // Confidence scales with number of matching keywords
553
+ const keywordRatio = foundKeywords.length / config.keywords.length;
554
+ const adjustedConfidence = config.confidence * (0.5 + 0.5 * keywordRatio);
555
+
556
+ matches.push({
557
+ room: roomName,
558
+ confidence: adjustedConfidence,
559
+ signals: foundKeywords,
560
+ emoji: config.emoji,
561
+ color: config.color,
562
+ tier: config.tier
563
+ });
564
+ }
565
+ }
566
+
567
+ // Sort by confidence
568
+ matches.sort((a, b) => b.confidence - a.confidence);
569
+
570
+ if (matches.length === 0) {
571
+ return {
572
+ room: currentRoom || 'builder',
573
+ confidence: 0.3,
574
+ signals: [],
575
+ switchRecommended: false,
576
+ message: "No clear room signal detected. Staying in current room."
577
+ };
578
+ }
579
+
580
+ const best = matches[0];
581
+ const switchRecommended = best.room !== currentRoom && best.confidence > 0.7;
582
+
583
+ return {
584
+ room: best.room,
585
+ confidence: best.confidence,
586
+ signals: best.signals,
587
+ emoji: best.emoji,
588
+ color: best.color,
589
+ tier: best.tier,
590
+ currentRoom: currentRoom,
591
+ switchRecommended: switchRecommended,
592
+ memoryPalace: MEMORY_PALACES[best.room],
593
+ identityRules: IDENTITY_RULES[best.room],
594
+ alternatives: matches.slice(1, 3) // Top 2 alternatives
595
+ };
596
+ }
597
+
598
+ /**
599
+ * Switch to a new room, preserving context
600
+ * @param {string} newRoom - The room to switch to
601
+ * @param {string} contextSnapshot - Optional context to save from previous room
602
+ * @returns {object} - The new room configuration
603
+ */
604
+ function switchRoom(newRoom, contextSnapshot = null) {
605
+ const previousRoom = currentRoom;
606
+
607
+ // Save to history
608
+ if (db) {
609
+ db.prepare(`
610
+ INSERT INTO room_history (from_room, to_room, signal, confidence)
611
+ VALUES (?, ?, ?, ?)
612
+ `).run(previousRoom, newRoom, 'manual_switch', 1.0);
613
+
614
+ // Update current state
615
+ db.prepare(`
616
+ INSERT OR REPLACE INTO room_state (id, current_room, context_snapshot, updated_at)
617
+ VALUES (1, ?, ?, CURRENT_TIMESTAMP)
618
+ `).run(newRoom, contextSnapshot);
619
+ }
620
+
621
+ // Update in-memory state
622
+ roomHistory.push({
623
+ from: previousRoom,
624
+ to: newRoom,
625
+ at: new Date().toISOString()
626
+ });
627
+ currentRoom = newRoom;
628
+
629
+ return {
630
+ previousRoom: previousRoom,
631
+ currentRoom: newRoom,
632
+ memoryPalace: MEMORY_PALACES[newRoom],
633
+ identityRules: IDENTITY_RULES[newRoom],
634
+ emoji: DETECTION_SIGNALS[newRoom]?.emoji || '๐Ÿง ',
635
+ color: DETECTION_SIGNALS[newRoom]?.color || '#667eea',
636
+ tier: DETECTION_SIGNALS[newRoom]?.tier || 'tactical',
637
+ contextSaved: !!contextSnapshot,
638
+ terminal: Object.values(TERMINALS_MAC).find(t => t.room === newRoom)?.app || null,
639
+ html: Object.values(TERMINALS_MAC).find(t => t.room === newRoom)?.html || null
640
+ };
641
+ }
642
+
643
+ /**
644
+ * Get current room status
645
+ * @returns {object} - Current room configuration and state
646
+ */
647
+ function getStatus() {
648
+ const room = currentRoom || 'builder';
649
+
650
+ return {
651
+ currentRoom: room,
652
+ emoji: DETECTION_SIGNALS[room]?.emoji || '๐Ÿง ',
653
+ color: DETECTION_SIGNALS[room]?.color || '#667eea',
654
+ tier: DETECTION_SIGNALS[room]?.tier || 'tactical',
655
+ memoryPalace: MEMORY_PALACES[room],
656
+ identityRules: IDENTITY_RULES[room],
657
+ terminal: Object.values(TERMINALS_MAC).find(t => t.room === room)?.app || null,
658
+ html: Object.values(TERMINALS_MAC).find(t => t.room === room)?.html || null,
659
+ recentSwitches: roomHistory.slice(-5),
660
+ sqliteEnabled: !!db
661
+ };
662
+ }
663
+
664
+ // ============================================================================
665
+ // MCP SERVER
666
+ // ============================================================================
667
+
668
+ class ThetaCogServer {
669
+ constructor() {
670
+ this.server = new Server(
671
+ {
672
+ name: 'thetacog-mcp',
673
+ version: '0.1.0',
674
+ },
675
+ {
676
+ capabilities: {
677
+ tools: {},
678
+ },
679
+ }
680
+ );
681
+
682
+ this.setupToolHandlers();
683
+ }
684
+
685
+ setupToolHandlers() {
686
+ // List available tools
687
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
688
+ return {
689
+ tools: [
690
+ {
691
+ name: 'thetacog-detect',
692
+ description: 'Analyze conversation to detect which cognitive room you should be in. Returns room suggestion, confidence score, and switching signals. Use when user says things like "let\'s think bigger picture" or "time to ship".',
693
+ inputSchema: {
694
+ type: 'object',
695
+ properties: {
696
+ text: {
697
+ type: 'string',
698
+ description: 'The conversation text to analyze for room detection signals',
699
+ },
700
+ },
701
+ required: ['text'],
702
+ },
703
+ },
704
+ {
705
+ name: 'thetacog-status',
706
+ description: 'Get current cognitive room status. Returns: current room, identity rules, memory palace anchor, recent switches, and terminal mapping.',
707
+ inputSchema: {
708
+ type: 'object',
709
+ properties: {},
710
+ },
711
+ },
712
+ {
713
+ name: 'thetacog-switch',
714
+ description: 'Switch to a different cognitive room. Saves context from previous room, loads new room configuration. Returns memory palace anchor and identity rules for new room.',
715
+ inputSchema: {
716
+ type: 'object',
717
+ properties: {
718
+ room: {
719
+ type: 'string',
720
+ description: 'Room to switch to: builder, architect, operator, vault, voice, or laboratory',
721
+ enum: ['builder', 'architect', 'operator', 'vault', 'voice', 'laboratory']
722
+ },
723
+ context: {
724
+ type: 'string',
725
+ description: 'Optional context snapshot to save from current room (e.g., "Working on Stripe integration, 80% complete")',
726
+ },
727
+ },
728
+ required: ['room'],
729
+ },
730
+ },
731
+ {
732
+ name: 'thetacog-open',
733
+ description: 'Open the HTML dashboard for a room in the browser. If no room specified, opens current room. The HTML auto-refreshes from state.json on tab focus.',
734
+ inputSchema: {
735
+ type: 'object',
736
+ properties: {
737
+ room: {
738
+ type: 'string',
739
+ description: 'Room to open (optional, defaults to current room)',
740
+ enum: ['builder', 'architect', 'operator', 'vault', 'voice', 'laboratory']
741
+ },
742
+ },
743
+ },
744
+ },
745
+ {
746
+ name: 'thetacog-todo',
747
+ description: 'Manage todos for a room. Actions: add, list, update, delete. Todos are stored in SQLite and synced to state.json for HTML display.',
748
+ inputSchema: {
749
+ type: 'object',
750
+ properties: {
751
+ action: {
752
+ type: 'string',
753
+ description: 'Action to perform',
754
+ enum: ['add', 'list', 'update', 'delete']
755
+ },
756
+ room: {
757
+ type: 'string',
758
+ description: 'Room for the todo (required for add, optional for list)',
759
+ },
760
+ text: {
761
+ type: 'string',
762
+ description: 'Todo text (required for add)',
763
+ },
764
+ id: {
765
+ type: 'number',
766
+ description: 'Todo ID (required for update/delete)',
767
+ },
768
+ done: {
769
+ type: 'boolean',
770
+ description: 'Mark as done (for update)',
771
+ },
772
+ priority: {
773
+ type: 'number',
774
+ description: 'Priority 1-10 (1=highest). For add or update.',
775
+ },
776
+ },
777
+ required: ['action'],
778
+ },
779
+ },
780
+ {
781
+ name: 'thetacog-stream',
782
+ description: 'Send messages between rooms (flywheel coordination). Get unread input streams for a room.',
783
+ inputSchema: {
784
+ type: 'object',
785
+ properties: {
786
+ action: {
787
+ type: 'string',
788
+ description: 'Action: send, get, or mark-read',
789
+ enum: ['send', 'get', 'mark-read']
790
+ },
791
+ from: {
792
+ type: 'string',
793
+ description: 'Source room (for send)',
794
+ },
795
+ to: {
796
+ type: 'string',
797
+ description: 'Target room (for send, get, mark-read)',
798
+ },
799
+ message: {
800
+ type: 'string',
801
+ description: 'Message to send (for send)',
802
+ },
803
+ },
804
+ required: ['action'],
805
+ },
806
+ },
807
+ {
808
+ name: 'thetacog-export',
809
+ description: 'Export current state to JSON file. HTML files read this on tab focus to refresh.',
810
+ inputSchema: {
811
+ type: 'object',
812
+ properties: {},
813
+ },
814
+ },
815
+ {
816
+ name: 'thetacog-terminal',
817
+ description: 'Detect which terminal Claude is running in, and which room it maps to.',
818
+ inputSchema: {
819
+ type: 'object',
820
+ properties: {},
821
+ },
822
+ },
823
+ ],
824
+ };
825
+ });
826
+
827
+ // Handle tool calls
828
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
829
+ const { name, arguments: args } = request.params;
830
+
831
+ try {
832
+ switch (name) {
833
+ case 'thetacog-detect': {
834
+ const result = detectRoom(args.text || '');
835
+ return {
836
+ content: [
837
+ {
838
+ type: 'text',
839
+ text: JSON.stringify(result, null, 2),
840
+ },
841
+ ],
842
+ };
843
+ }
844
+
845
+ case 'thetacog-status': {
846
+ const result = getStatus();
847
+ return {
848
+ content: [
849
+ {
850
+ type: 'text',
851
+ text: JSON.stringify(result, null, 2),
852
+ },
853
+ ],
854
+ };
855
+ }
856
+
857
+ case 'thetacog-switch': {
858
+ const result = switchRoom(args.room, args.context || null);
859
+ return {
860
+ content: [
861
+ {
862
+ type: 'text',
863
+ text: JSON.stringify(result, null, 2),
864
+ },
865
+ ],
866
+ };
867
+ }
868
+
869
+ case 'thetacog-open': {
870
+ const result = openRoomHtml(args.room || null);
871
+ return {
872
+ content: [
873
+ {
874
+ type: 'text',
875
+ text: JSON.stringify(result, null, 2),
876
+ },
877
+ ],
878
+ };
879
+ }
880
+
881
+ case 'thetacog-todo': {
882
+ let result;
883
+ switch (args.action) {
884
+ case 'add':
885
+ result = addTodo(args.room, args.text, args.priority || 5);
886
+ break;
887
+ case 'list':
888
+ result = listTodos(args.room || null);
889
+ break;
890
+ case 'update':
891
+ result = updateTodo(args.id, {
892
+ done: args.done,
893
+ priority: args.priority,
894
+ text: args.text
895
+ });
896
+ break;
897
+ case 'delete':
898
+ result = deleteTodo(args.id);
899
+ break;
900
+ default:
901
+ result = { error: 'Unknown action. Use: add, list, update, delete' };
902
+ }
903
+ return {
904
+ content: [
905
+ {
906
+ type: 'text',
907
+ text: JSON.stringify(result, null, 2),
908
+ },
909
+ ],
910
+ };
911
+ }
912
+
913
+ case 'thetacog-stream': {
914
+ let result;
915
+ switch (args.action) {
916
+ case 'send':
917
+ result = sendStream(args.from, args.to, args.message);
918
+ break;
919
+ case 'get':
920
+ result = getStreams(args.to);
921
+ break;
922
+ case 'mark-read':
923
+ result = markStreamsRead(args.to);
924
+ break;
925
+ default:
926
+ result = { error: 'Unknown action. Use: send, get, mark-read' };
927
+ }
928
+ return {
929
+ content: [
930
+ {
931
+ type: 'text',
932
+ text: JSON.stringify(result, null, 2),
933
+ },
934
+ ],
935
+ };
936
+ }
937
+
938
+ case 'thetacog-export': {
939
+ const result = exportStateToJson();
940
+ return {
941
+ content: [
942
+ {
943
+ type: 'text',
944
+ text: JSON.stringify({
945
+ success: true,
946
+ exportedAt: result?.exportedAt,
947
+ path: path.join(thetacogDir || '~/.thetacog', 'state.json')
948
+ }, null, 2),
949
+ },
950
+ ],
951
+ };
952
+ }
953
+
954
+ case 'thetacog-terminal': {
955
+ const result = detectTerminal();
956
+ return {
957
+ content: [
958
+ {
959
+ type: 'text',
960
+ text: JSON.stringify(result, null, 2),
961
+ },
962
+ ],
963
+ };
964
+ }
965
+
966
+ default:
967
+ throw new Error(`Unknown tool: ${name}`);
968
+ }
969
+ } catch (error) {
970
+ return {
971
+ content: [
972
+ {
973
+ type: 'text',
974
+ text: JSON.stringify({ error: error.message }),
975
+ },
976
+ ],
977
+ isError: true,
978
+ };
979
+ }
980
+ });
981
+ }
982
+
983
+ async shutdown() {
984
+ console.error('๐Ÿ›‘ Shutting down ThetaCog MCP server...');
985
+
986
+ // Close SQLite connection
987
+ if (db) {
988
+ try {
989
+ db.close();
990
+ console.error(' โœ… Closed SQLite connection');
991
+ } catch (error) {
992
+ console.error(` โš ๏ธ Failed to close SQLite: ${error.message}`);
993
+ }
994
+ }
995
+
996
+ console.error(' โœ… Cleanup complete');
997
+ process.exit(0);
998
+ }
999
+
1000
+ async run() {
1001
+ // Initialize database (optional)
1002
+ await initDatabase();
1003
+
1004
+ // Start MCP server
1005
+ const transport = new StdioServerTransport();
1006
+ await this.server.connect(transport);
1007
+
1008
+ console.error('โœ… ThetaCog MCP server v0.1.0 started');
1009
+ console.error(' ๐Ÿง  Mode management, not task management');
1010
+ console.error(' ๐Ÿ“ฆ SQLite: ' + (db ? 'enabled' : 'memory-only mode'));
1011
+ console.error(' ๐Ÿ”จ Current room: ' + (currentRoom || 'builder (default)'));
1012
+
1013
+ // Register cleanup handlers for graceful shutdown (copied from CRM pattern)
1014
+ process.on('SIGINT', () => this.shutdown());
1015
+ process.on('SIGTERM', () => this.shutdown());
1016
+ process.on('SIGHUP', () => this.shutdown());
1017
+
1018
+ // Handle uncaught errors gracefully
1019
+ process.on('uncaughtException', (error) => {
1020
+ console.error('โŒ Uncaught exception:', error.message);
1021
+ this.shutdown();
1022
+ });
1023
+ }
1024
+ }
1025
+
1026
+ // ============================================================================
1027
+ // STARTUP
1028
+ // ============================================================================
1029
+
1030
+ const server = new ThetaCogServer();
1031
+ server.run().catch(console.error);