tide-commander 0.52.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 (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +364 -0
  3. package/dist/assets/characters/Textures/colormap.png +0 -0
  4. package/dist/assets/characters/character-female-a.glb +0 -0
  5. package/dist/assets/characters/character-female-b.glb +0 -0
  6. package/dist/assets/characters/character-female-c.glb +0 -0
  7. package/dist/assets/characters/character-female-d.glb +0 -0
  8. package/dist/assets/characters/character-female-e.glb +0 -0
  9. package/dist/assets/characters/character-female-f.glb +0 -0
  10. package/dist/assets/characters/character-male-a-processed.gltf +11862 -0
  11. package/dist/assets/characters/character-male-a.glb +0 -0
  12. package/dist/assets/characters/character-male-b.glb +0 -0
  13. package/dist/assets/characters/character-male-c.glb +0 -0
  14. package/dist/assets/characters/character-male-d.glb +0 -0
  15. package/dist/assets/characters/character-male-e.glb +0 -0
  16. package/dist/assets/characters/character-male-f.glb +0 -0
  17. package/dist/assets/icons/icon-192.png +0 -0
  18. package/dist/assets/icons/icon-512.png +0 -0
  19. package/dist/assets/landing-Cc0MDBAK.css +1 -0
  20. package/dist/assets/main-BIpLsrUu.css +1 -0
  21. package/dist/assets/main-DMTRw3br.js +276 -0
  22. package/dist/assets/textures/concrete_floor_worn_001_diff_1k.jpg +0 -0
  23. package/dist/assets/textures/logo-blanco.png +0 -0
  24. package/dist/assets/vendor-react-uS-d4TUT.js +17 -0
  25. package/dist/assets/vendor-three-4iQNXcoo.js +3828 -0
  26. package/dist/assets/web-BZdi2lG9.js +1 -0
  27. package/dist/assets/web-yHsOO1Qb.js +1 -0
  28. package/dist/index.html +38 -0
  29. package/dist/manifest.json +39 -0
  30. package/dist/src/packages/landing/index.html +463 -0
  31. package/dist/src/packages/server/app.js +87 -0
  32. package/dist/src/packages/server/auth/index.js +121 -0
  33. package/dist/src/packages/server/claude/backend.js +578 -0
  34. package/dist/src/packages/server/claude/index.js +8 -0
  35. package/dist/src/packages/server/claude/runner/internal-events.js +22 -0
  36. package/dist/src/packages/server/claude/runner/process-lifecycle.js +208 -0
  37. package/dist/src/packages/server/claude/runner/recovery-store.js +72 -0
  38. package/dist/src/packages/server/claude/runner/resource-monitor.js +51 -0
  39. package/dist/src/packages/server/claude/runner/restart-policy.js +69 -0
  40. package/dist/src/packages/server/claude/runner/stdout-pipeline.js +153 -0
  41. package/dist/src/packages/server/claude/runner/watchdog.js +114 -0
  42. package/dist/src/packages/server/claude/runner.js +310 -0
  43. package/dist/src/packages/server/claude/session-loader.js +898 -0
  44. package/dist/src/packages/server/claude/types.js +5 -0
  45. package/dist/src/packages/server/cli.js +113 -0
  46. package/dist/src/packages/server/codex/backend.js +119 -0
  47. package/dist/src/packages/server/codex/index.js +2 -0
  48. package/dist/src/packages/server/codex/json-event-parser.js +612 -0
  49. package/dist/src/packages/server/data/builtin-skills/bitbucket-pr.js +298 -0
  50. package/dist/src/packages/server/data/builtin-skills/full-notifications.js +49 -0
  51. package/dist/src/packages/server/data/builtin-skills/git-captain.js +304 -0
  52. package/dist/src/packages/server/data/builtin-skills/index.js +61 -0
  53. package/dist/src/packages/server/data/builtin-skills/pm2-logs.js +354 -0
  54. package/dist/src/packages/server/data/builtin-skills/send-message-to-agent.js +51 -0
  55. package/dist/src/packages/server/data/builtin-skills/server-logs.js +124 -0
  56. package/dist/src/packages/server/data/builtin-skills/streaming-exec.js +94 -0
  57. package/dist/src/packages/server/data/builtin-skills/types.js +4 -0
  58. package/dist/src/packages/server/data/builtin-skills.js +6 -0
  59. package/dist/src/packages/server/data/index.js +890 -0
  60. package/dist/src/packages/server/data/snapshots.js +371 -0
  61. package/dist/src/packages/server/index.js +96 -0
  62. package/dist/src/packages/server/prompts/tide-commander.js +13 -0
  63. package/dist/src/packages/server/routes/agents.js +406 -0
  64. package/dist/src/packages/server/routes/config.js +347 -0
  65. package/dist/src/packages/server/routes/custom-models.js +170 -0
  66. package/dist/src/packages/server/routes/exec.js +269 -0
  67. package/dist/src/packages/server/routes/files.js +995 -0
  68. package/dist/src/packages/server/routes/index.js +38 -0
  69. package/dist/src/packages/server/routes/notifications.js +81 -0
  70. package/dist/src/packages/server/routes/permissions.js +115 -0
  71. package/dist/src/packages/server/routes/snapshots.js +224 -0
  72. package/dist/src/packages/server/routes/stt.js +99 -0
  73. package/dist/src/packages/server/routes/tts.js +166 -0
  74. package/dist/src/packages/server/routes/voice-assistant.js +310 -0
  75. package/dist/src/packages/server/runtime/claude-runtime-provider.js +10 -0
  76. package/dist/src/packages/server/runtime/codex-runtime-provider.js +11 -0
  77. package/dist/src/packages/server/runtime/index.js +2 -0
  78. package/dist/src/packages/server/runtime/types.js +6 -0
  79. package/dist/src/packages/server/services/agent-lifecycle-service.js +82 -0
  80. package/dist/src/packages/server/services/agent-service.js +410 -0
  81. package/dist/src/packages/server/services/boss-message-service.js +430 -0
  82. package/dist/src/packages/server/services/boss-service.js +553 -0
  83. package/dist/src/packages/server/services/building-service.js +867 -0
  84. package/dist/src/packages/server/services/claude-service.js +5 -0
  85. package/dist/src/packages/server/services/custom-class-service.js +323 -0
  86. package/dist/src/packages/server/services/database-service.js +914 -0
  87. package/dist/src/packages/server/services/docker-service.js +865 -0
  88. package/dist/src/packages/server/services/fileTracker.js +242 -0
  89. package/dist/src/packages/server/services/index.js +21 -0
  90. package/dist/src/packages/server/services/permission-service.js +258 -0
  91. package/dist/src/packages/server/services/pm2-service.js +435 -0
  92. package/dist/src/packages/server/services/runtime-command-execution.js +168 -0
  93. package/dist/src/packages/server/services/runtime-events.js +357 -0
  94. package/dist/src/packages/server/services/runtime-service.js +308 -0
  95. package/dist/src/packages/server/services/runtime-status-sync.js +104 -0
  96. package/dist/src/packages/server/services/runtime-subagents.js +50 -0
  97. package/dist/src/packages/server/services/runtime-watchdog.js +74 -0
  98. package/dist/src/packages/server/services/secrets-service.js +206 -0
  99. package/dist/src/packages/server/services/skill-service.js +508 -0
  100. package/dist/src/packages/server/services/subordinate-context-service.js +223 -0
  101. package/dist/src/packages/server/services/supervisor-claude.js +132 -0
  102. package/dist/src/packages/server/services/supervisor-prompts.js +80 -0
  103. package/dist/src/packages/server/services/supervisor-service.js +659 -0
  104. package/dist/src/packages/server/services/work-plan-service.js +476 -0
  105. package/dist/src/packages/server/setup.js +86 -0
  106. package/dist/src/packages/server/utils/index.js +4 -0
  107. package/dist/src/packages/server/utils/logger.js +302 -0
  108. package/dist/src/packages/server/utils/string.js +39 -0
  109. package/dist/src/packages/server/utils/tool-formatting.js +139 -0
  110. package/dist/src/packages/server/utils/unicode.js +46 -0
  111. package/dist/src/packages/server/websocket/handler.js +290 -0
  112. package/dist/src/packages/server/websocket/handlers/agent-handler.js +515 -0
  113. package/dist/src/packages/server/websocket/handlers/boss-handler.js +116 -0
  114. package/dist/src/packages/server/websocket/handlers/boss-response-handler.js +250 -0
  115. package/dist/src/packages/server/websocket/handlers/building-handler.js +298 -0
  116. package/dist/src/packages/server/websocket/handlers/command-handler.js +217 -0
  117. package/dist/src/packages/server/websocket/handlers/custom-class-handler.js +68 -0
  118. package/dist/src/packages/server/websocket/handlers/database-handler.js +223 -0
  119. package/dist/src/packages/server/websocket/handlers/notification-handler.js +25 -0
  120. package/dist/src/packages/server/websocket/handlers/permission-handler.js +21 -0
  121. package/dist/src/packages/server/websocket/handlers/secrets-handler.js +61 -0
  122. package/dist/src/packages/server/websocket/handlers/skill-handler.js +148 -0
  123. package/dist/src/packages/server/websocket/handlers/supervisor-handler.js +44 -0
  124. package/dist/src/packages/server/websocket/handlers/sync-handler.js +19 -0
  125. package/dist/src/packages/server/websocket/handlers/types.js +4 -0
  126. package/dist/src/packages/server/websocket/listeners/boss-listeners.js +21 -0
  127. package/dist/src/packages/server/websocket/listeners/index.js +32 -0
  128. package/dist/src/packages/server/websocket/listeners/permission-listeners.js +19 -0
  129. package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +196 -0
  130. package/dist/src/packages/server/websocket/listeners/skill-listeners.js +51 -0
  131. package/dist/src/packages/server/websocket/listeners/supervisor-listeners.js +37 -0
  132. package/dist/src/packages/shared/agent-types.js +54 -0
  133. package/dist/src/packages/shared/building-types.js +43 -0
  134. package/dist/src/packages/shared/common-types.js +1 -0
  135. package/dist/src/packages/shared/database-types.js +8 -0
  136. package/dist/src/packages/shared/types/snapshot.js +7 -0
  137. package/dist/src/packages/shared/types.js +12 -0
  138. package/dist/src/packages/shared/websocket-messages.js +1 -0
  139. package/dist/sw.js +37 -0
  140. package/package.json +90 -0
@@ -0,0 +1,166 @@
1
+ /**
2
+ * TTS (Text-to-Speech) Route
3
+ * Uses Piper for high-quality speech synthesis, returns audio to browser
4
+ * Auto-detects language (Spanish/English) based on text content
5
+ */
6
+ import { Router } from 'express';
7
+ import { spawn } from 'child_process';
8
+ import os from 'os';
9
+ import path from 'path';
10
+ import { logger } from '../utils/logger.js';
11
+ const router = Router();
12
+ // Piper voice model paths
13
+ const PIPER_VOICES_DIR = path.join(os.homedir(), '.local/share/piper-voices');
14
+ // Voice models by language
15
+ const VOICES = {
16
+ es: 'es_MX-claude-high', // Latin American Spanish (Mexican female - Claude voice)
17
+ en: 'en_US-amy-medium', // American English (female)
18
+ };
19
+ // Common Spanish words for language detection
20
+ const SPANISH_INDICATORS = [
21
+ // Common words
22
+ 'el', 'la', 'los', 'las', 'un', 'una', 'unos', 'unas',
23
+ 'de', 'del', 'en', 'con', 'por', 'para', 'sin', 'sobre',
24
+ 'que', 'qué', 'como', 'cómo', 'cuando', 'cuándo', 'donde', 'dónde',
25
+ 'es', 'está', 'son', 'están', 'ser', 'estar', 'hay',
26
+ 'yo', 'tú', 'él', 'ella', 'nosotros', 'ellos', 'ellas',
27
+ 'mi', 'tu', 'su', 'nuestro', 'vuestro',
28
+ 'este', 'esta', 'estos', 'estas', 'ese', 'esa', 'esos', 'esas',
29
+ 'pero', 'porque', 'aunque', 'también', 'además', 'entonces',
30
+ 'puede', 'pueden', 'puedo', 'podemos', 'hacer', 'hecho',
31
+ 'muy', 'más', 'menos', 'bien', 'mal', 'sí', 'no',
32
+ 'ahora', 'aquí', 'allí', 'hoy', 'ayer', 'mañana',
33
+ // Verbs
34
+ 'tiene', 'tienen', 'tengo', 'tenemos', 'quiero', 'quiere',
35
+ 'necesito', 'necesita', 'busco', 'busca', 'encuentro', 'encuentra',
36
+ // Tech terms often used in Spanish
37
+ 'archivo', 'archivos', 'código', 'función', 'método', 'clase',
38
+ 'ejecutar', 'compilar', 'instalar', 'configurar',
39
+ ];
40
+ // Common English words for language detection
41
+ const ENGLISH_INDICATORS = [
42
+ // Common words
43
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',
44
+ 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should',
45
+ 'this', 'that', 'these', 'those', 'it', 'its',
46
+ 'i', 'you', 'he', 'she', 'we', 'they', 'my', 'your', 'his', 'her', 'our', 'their',
47
+ 'and', 'or', 'but', 'if', 'then', 'else', 'when', 'where', 'how', 'what', 'which',
48
+ 'to', 'of', 'in', 'on', 'at', 'by', 'for', 'with', 'about', 'from',
49
+ 'not', 'no', 'yes', 'can', 'cannot', "can't", "don't", "doesn't", "won't",
50
+ 'here', 'there', 'now', 'today', 'tomorrow', 'yesterday',
51
+ // Tech terms
52
+ 'file', 'files', 'code', 'function', 'method', 'class',
53
+ 'run', 'execute', 'compile', 'install', 'configure', 'build',
54
+ 'error', 'warning', 'success', 'failed', 'complete',
55
+ ];
56
+ /**
57
+ * Detect language based on word frequency
58
+ * Returns 'es' for Spanish, 'en' for English
59
+ */
60
+ function detectLanguage(text) {
61
+ const words = text.toLowerCase().split(/\s+/);
62
+ let spanishScore = 0;
63
+ let englishScore = 0;
64
+ for (const word of words) {
65
+ // Clean punctuation from word
66
+ const cleanWord = word.replace(/[.,!?;:'"()[\]{}]/g, '');
67
+ if (SPANISH_INDICATORS.includes(cleanWord)) {
68
+ spanishScore++;
69
+ }
70
+ if (ENGLISH_INDICATORS.includes(cleanWord)) {
71
+ englishScore++;
72
+ }
73
+ }
74
+ // Check for Spanish-specific characters (accents, ñ, ¿, ¡)
75
+ if (/[áéíóúüñ¿¡]/.test(text)) {
76
+ spanishScore += 3;
77
+ }
78
+ const detected = spanishScore > englishScore ? 'es' : 'en';
79
+ logger.server.log(`[TTS] Language detection: Spanish=${spanishScore}, English=${englishScore} -> ${detected}`);
80
+ return detected;
81
+ }
82
+ /**
83
+ * POST /api/tts/speak
84
+ * Generates audio using Piper TTS and returns WAV audio data
85
+ * Auto-detects language if not specified
86
+ */
87
+ router.post('/speak', async (req, res) => {
88
+ const { text, voice, lang } = req.body;
89
+ if (!text || typeof text !== 'string') {
90
+ return res.status(400).json({ error: 'Text is required' });
91
+ }
92
+ // Clean text for speech (remove markdown, etc.)
93
+ const cleanedText = text
94
+ .replace(/```[\s\S]*?```/g, ' code block ')
95
+ .replace(/`[^`]+`/g, ' code ')
96
+ .replace(/^#{1,6}\s+/gm, '')
97
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
98
+ .replace(/\*([^*]+)\*/g, '$1')
99
+ .replace(/__([^_]+)__/g, '$1')
100
+ .replace(/_([^_]+)_/g, '$1')
101
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
102
+ .replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1')
103
+ .replace(/^[-*_]{3,}$/gm, '')
104
+ .replace(/<[^>]+>/g, '')
105
+ .replace(/\s+/g, ' ')
106
+ .trim();
107
+ if (!cleanedText) {
108
+ return res.status(400).json({ error: 'No text to speak after cleaning' });
109
+ }
110
+ // Determine voice: explicit voice > explicit lang > auto-detect
111
+ let selectedVoice;
112
+ if (voice) {
113
+ selectedVoice = voice;
114
+ }
115
+ else {
116
+ const detectedLang = lang || detectLanguage(cleanedText);
117
+ selectedVoice = VOICES[detectedLang] || VOICES.en;
118
+ }
119
+ logger.server.log(`[TTS] Generating audio with voice ${selectedVoice}: ${cleanedText.substring(0, 50)}...`);
120
+ const modelPath = path.join(PIPER_VOICES_DIR, `${selectedVoice}.onnx`);
121
+ try {
122
+ // Generate WAV audio using piper
123
+ const audioData = await new Promise((resolve, reject) => {
124
+ const chunks = [];
125
+ const piper = spawn('piper', [
126
+ '--model', modelPath,
127
+ '--output_file', '-' // Output to stdout as WAV
128
+ ]);
129
+ piper.stdout.on('data', (chunk) => {
130
+ chunks.push(chunk);
131
+ });
132
+ piper.stderr.on('data', (data) => {
133
+ // Piper outputs progress to stderr, ignore it
134
+ const msg = data.toString();
135
+ if (msg.includes('Error') || msg.includes('error')) {
136
+ logger.server.error(`[TTS] Piper stderr: ${msg}`);
137
+ }
138
+ });
139
+ piper.on('error', (err) => {
140
+ reject(new Error(`Piper spawn error: ${err.message}`));
141
+ });
142
+ piper.on('close', (code) => {
143
+ if (code === 0) {
144
+ resolve(Buffer.concat(chunks));
145
+ }
146
+ else {
147
+ reject(new Error(`Piper exited with code ${code}`));
148
+ }
149
+ });
150
+ // Send text to piper
151
+ if (piper.stdin) {
152
+ piper.stdin.write(cleanedText);
153
+ piper.stdin.end();
154
+ }
155
+ });
156
+ // Send WAV audio back to browser
157
+ res.setHeader('Content-Type', 'audio/wav');
158
+ res.setHeader('Content-Length', audioData.length);
159
+ res.send(audioData);
160
+ }
161
+ catch (err) {
162
+ logger.server.error(`[TTS] Error generating audio: ${err}`);
163
+ res.status(500).json({ error: 'Failed to generate audio' });
164
+ }
165
+ });
166
+ export default router;
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Voice Assistant Route
3
+ * Processes voice commands using Claude Code (haiku) with persistent session
4
+ */
5
+ import { Router } from 'express';
6
+ import { spawn } from 'child_process';
7
+ import { StringDecoder } from 'string_decoder';
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import os from 'os';
11
+ import { agentService } from '../services/index.js';
12
+ import { loadAreas } from '../data/index.js';
13
+ import { ClaudeBackend } from '../claude/index.js';
14
+ import { logger, sanitizeUnicode } from '../utils/index.js';
15
+ const router = Router();
16
+ const claudeBackend = new ClaudeBackend();
17
+ // Voice assistant state file
18
+ const VOICE_STATE_FILE = path.join(os.homedir(), '.local/share/tide-commander/voice-assistant.json');
19
+ // Persistent session ID for voice assistant
20
+ let voiceSessionId = null;
21
+ /**
22
+ * Load voice assistant state from disk
23
+ */
24
+ function loadVoiceState() {
25
+ try {
26
+ if (fs.existsSync(VOICE_STATE_FILE)) {
27
+ const data = JSON.parse(fs.readFileSync(VOICE_STATE_FILE, 'utf-8'));
28
+ voiceSessionId = data.sessionId || null;
29
+ logger.server.log(`[VoiceAssistant] Loaded session: ${voiceSessionId}`);
30
+ }
31
+ }
32
+ catch (err) {
33
+ logger.server.error(`[VoiceAssistant] Failed to load state:`, err);
34
+ }
35
+ }
36
+ /**
37
+ * Save voice assistant state to disk
38
+ */
39
+ function saveVoiceState() {
40
+ try {
41
+ const dir = path.dirname(VOICE_STATE_FILE);
42
+ if (!fs.existsSync(dir)) {
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ }
45
+ fs.writeFileSync(VOICE_STATE_FILE, JSON.stringify({
46
+ sessionId: voiceSessionId,
47
+ updatedAt: Date.now(),
48
+ }, null, 2));
49
+ }
50
+ catch (err) {
51
+ logger.server.error(`[VoiceAssistant] Failed to save state:`, err);
52
+ }
53
+ }
54
+ // Load state on module init
55
+ loadVoiceState();
56
+ /**
57
+ * Build the system prompt with current agent list and their areas
58
+ */
59
+ function buildSystemPrompt() {
60
+ const agents = agentService.getAllAgents();
61
+ const areas = loadAreas();
62
+ // Build agent list with their assigned areas and boss status
63
+ const agentList = agents.map(a => {
64
+ const agentAreas = areas.filter(area => area.assignedAgentIds.includes(a.id));
65
+ const areaNames = agentAreas.map(area => area.name).join(', ');
66
+ const parts = [`${a.name} (id: ${a.id}, ${a.status})`];
67
+ if (a.isBoss)
68
+ parts.push('[BOSS]');
69
+ if (areaNames)
70
+ parts.push(`[areas: ${areaNames}]`);
71
+ return `- ${parts.join(' ')}`;
72
+ }).join('\n');
73
+ return `YOU ARE A JSON API. YOU MUST ONLY OUTPUT VALID JSON. NEVER OUTPUT TEXT OR EXPLANATIONS.
74
+
75
+ You route voice commands to AI agents. You do NOT write code. You do NOT help directly. You ONLY parse and route.
76
+
77
+ Agents:
78
+ ${agentList}
79
+
80
+ REQUIRED OUTPUT FORMAT (nothing else):
81
+ {"targetAgentId":"id|null","targetAgentName":"name|null","messageToAgent":"the user's request to forward","responseToUser":"brief confirmation under 15 words","action":"send_to_agent|list_agents|general_response"}
82
+
83
+ ACTIONS:
84
+ - send_to_agent: User wants an agent to do something. Put their request in messageToAgent.
85
+ - list_agents: User asks about agents
86
+ - general_response: Greetings only
87
+
88
+ IMPORTANT: responseToUser MUST be in the SAME LANGUAGE as the user's message. If user speaks Spanish, respond in Spanish. If English, respond in English.
89
+
90
+ CRITICAL: Your response must be ONLY the JSON object. No markdown. No explanations. No code.`;
91
+ }
92
+ /**
93
+ * Call Claude Code with haiku model, maintaining session
94
+ */
95
+ async function callClaudeHaiku(userMessage, isFirstMessage) {
96
+ return new Promise((resolve, reject) => {
97
+ const executable = claudeBackend.getExecutablePath();
98
+ const args = [
99
+ '--print',
100
+ '--verbose',
101
+ '--model', 'haiku',
102
+ '--output-format', 'stream-json',
103
+ '--input-format', 'stream-json',
104
+ ];
105
+ // Always write system prompt to temp file - Claude Code needs it on every call
106
+ const systemPromptFile = path.join(os.tmpdir(), `voice-assistant-prompt-${Date.now()}.txt`);
107
+ fs.writeFileSync(systemPromptFile, buildSystemPrompt());
108
+ args.push('--system-prompt-file', systemPromptFile);
109
+ // Resume existing session if available
110
+ if (voiceSessionId && !isFirstMessage) {
111
+ args.push('--resume', voiceSessionId);
112
+ }
113
+ const childProcess = spawn(executable, args, {
114
+ env: { ...process.env, LANG: 'en_US.UTF-8', LC_ALL: 'en_US.UTF-8' },
115
+ shell: false, // Don't use shell to avoid escaping issues
116
+ });
117
+ // Clean up temp file when done
118
+ const cleanup = () => {
119
+ if (fs.existsSync(systemPromptFile)) {
120
+ try {
121
+ fs.unlinkSync(systemPromptFile);
122
+ }
123
+ catch { /* ignore */ }
124
+ }
125
+ };
126
+ const decoder = new StringDecoder('utf8');
127
+ let buffer = '';
128
+ let textOutput = '';
129
+ let newSessionId = null;
130
+ childProcess.stdout?.on('data', (data) => {
131
+ buffer += decoder.write(data);
132
+ const lines = buffer.split('\n');
133
+ buffer = lines.pop() || '';
134
+ for (const line of lines) {
135
+ if (!line.trim())
136
+ continue;
137
+ try {
138
+ const event = JSON.parse(line);
139
+ // Capture session ID from init event
140
+ if (event.type === 'system' && event.session_id) {
141
+ newSessionId = event.session_id;
142
+ }
143
+ if (event.type === 'assistant' && event.message?.content) {
144
+ for (const block of event.message.content) {
145
+ if (block.type === 'text' && block.text)
146
+ textOutput += block.text;
147
+ }
148
+ }
149
+ if (event.type === 'stream_event' && event.event?.type === 'content_block_delta') {
150
+ if (event.event.delta?.type === 'text_delta' && event.event.delta.text) {
151
+ textOutput += event.event.delta.text;
152
+ }
153
+ }
154
+ }
155
+ catch { /* ignore non-JSON */ }
156
+ }
157
+ });
158
+ childProcess.stderr?.on('data', (data) => {
159
+ const msg = decoder.write(data);
160
+ // Only log actual errors, not verbose output
161
+ if (msg.includes('Error') || msg.includes('error')) {
162
+ logger.server.error(`[VoiceAssistant] Claude stderr: ${msg}`);
163
+ }
164
+ });
165
+ childProcess.on('close', (code) => {
166
+ cleanup();
167
+ const remaining = buffer + decoder.end();
168
+ if (remaining.trim()) {
169
+ try {
170
+ const event = JSON.parse(remaining);
171
+ if (event.type === 'system' && event.session_id) {
172
+ newSessionId = event.session_id;
173
+ }
174
+ if (event.type === 'assistant' && event.message?.content) {
175
+ for (const block of event.message.content) {
176
+ if (block.type === 'text' && block.text)
177
+ textOutput += block.text;
178
+ }
179
+ }
180
+ }
181
+ catch { /* ignore */ }
182
+ }
183
+ if (code !== 0 && !textOutput)
184
+ reject(new Error(`Claude exited with code ${code}`));
185
+ else if (!textOutput)
186
+ reject(new Error('No response from Claude'));
187
+ else
188
+ resolve({ text: textOutput, sessionId: newSessionId });
189
+ });
190
+ childProcess.on('error', (err) => {
191
+ cleanup();
192
+ reject(err);
193
+ });
194
+ childProcess.on('spawn', () => {
195
+ // Include routing instruction with every message to ensure compliance
196
+ const routingInstruction = isFirstMessage ? '' : '\n\n[REMINDER: Output ONLY JSON. No explanations.]';
197
+ const stdinMessage = JSON.stringify({
198
+ type: 'user',
199
+ message: { role: 'user', content: sanitizeUnicode(userMessage) + routingInstruction },
200
+ });
201
+ childProcess.stdin?.write(stdinMessage + '\n');
202
+ childProcess.stdin?.end();
203
+ });
204
+ setTimeout(() => {
205
+ if (!childProcess.killed) {
206
+ childProcess.kill('SIGTERM');
207
+ cleanup();
208
+ reject(new Error('Claude timed out'));
209
+ }
210
+ }, 30000);
211
+ });
212
+ }
213
+ function stripCodeFences(s) {
214
+ s = s.trim();
215
+ if (s.startsWith('```json'))
216
+ s = s.slice(7);
217
+ else if (s.startsWith('```'))
218
+ s = s.slice(3);
219
+ if (s.endsWith('```'))
220
+ s = s.slice(0, -3);
221
+ return s.trim();
222
+ }
223
+ /**
224
+ * POST /api/voice-assistant/process
225
+ */
226
+ router.post('/process', async (req, res) => {
227
+ const { text } = req.body;
228
+ if (!text || typeof text !== 'string') {
229
+ return res.status(400).json({ error: 'Text is required' });
230
+ }
231
+ logger.server.log(`[VoiceAssistant] Processing: "${text}" (session: ${voiceSessionId || 'new'})`);
232
+ try {
233
+ const isFirstMessage = !voiceSessionId;
234
+ const { text: response, sessionId } = await callClaudeHaiku(text, isFirstMessage);
235
+ // Save session ID for future messages
236
+ if (sessionId && sessionId !== voiceSessionId) {
237
+ voiceSessionId = sessionId;
238
+ saveVoiceState();
239
+ logger.server.log(`[VoiceAssistant] Session saved: ${voiceSessionId}`);
240
+ }
241
+ const jsonStr = stripCodeFences(response);
242
+ let parsed;
243
+ try {
244
+ parsed = JSON.parse(jsonStr);
245
+ }
246
+ catch {
247
+ logger.server.error(`[VoiceAssistant] Failed to parse: ${jsonStr}`);
248
+ throw new Error('Failed to parse response');
249
+ }
250
+ logger.server.log(`[VoiceAssistant] Parsed:`, parsed);
251
+ if (parsed.action === 'send_to_agent' && parsed.targetAgentId) {
252
+ const targetAgent = agentService.getAgent(parsed.targetAgentId);
253
+ if (!targetAgent) {
254
+ return res.json({
255
+ success: true,
256
+ response: `No encuentro al agente ${parsed.targetAgentName}`,
257
+ });
258
+ }
259
+ const { runtimeService, bossMessageService } = await import('../services/index.js');
260
+ const { buildCustomAgentConfig } = await import('../websocket/handlers/command-handler.js');
261
+ if (targetAgent.isBoss || targetAgent.class === 'boss') {
262
+ const { message: bossMessage, systemPrompt } = await bossMessageService.buildBossMessage(parsed.targetAgentId, parsed.messageToAgent);
263
+ await runtimeService.sendCommand(parsed.targetAgentId, bossMessage, systemPrompt);
264
+ }
265
+ else {
266
+ const customAgentConfig = buildCustomAgentConfig(parsed.targetAgentId, targetAgent.class);
267
+ await runtimeService.sendCommand(parsed.targetAgentId, parsed.messageToAgent, undefined, undefined, customAgentConfig);
268
+ }
269
+ logger.server.log(`[VoiceAssistant] Sent to ${targetAgent.name}: "${parsed.messageToAgent}"`);
270
+ return res.json({
271
+ success: true,
272
+ response: parsed.responseToUser,
273
+ targetAgent: { id: targetAgent.id, name: targetAgent.name },
274
+ messageSent: parsed.messageToAgent,
275
+ });
276
+ }
277
+ return res.json({
278
+ success: true,
279
+ response: parsed.responseToUser,
280
+ });
281
+ }
282
+ catch (err) {
283
+ const msg = err?.message || 'Unknown error';
284
+ logger.server.error(`[VoiceAssistant] Error: ${msg}`);
285
+ // Reset session on error so next message starts fresh
286
+ if (msg.includes('exited with code') || msg.includes('timed out')) {
287
+ voiceSessionId = null;
288
+ }
289
+ return res.status(500).json({ error: 'Failed to process', details: msg });
290
+ }
291
+ });
292
+ /**
293
+ * POST /api/voice-assistant/reset
294
+ * Reset the voice assistant session
295
+ */
296
+ router.post('/reset', (_req, res) => {
297
+ voiceSessionId = null;
298
+ saveVoiceState();
299
+ logger.server.log('[VoiceAssistant] Session reset');
300
+ res.json({ success: true, message: 'Session reset' });
301
+ });
302
+ router.get('/status', (_req, res) => {
303
+ res.json({
304
+ available: true,
305
+ hasSession: !!voiceSessionId,
306
+ sessionId: voiceSessionId,
307
+ agentCount: agentService.getAllAgents().length,
308
+ });
309
+ });
310
+ export default router;
@@ -0,0 +1,10 @@
1
+ import { ClaudeRunner } from '../claude/runner.js';
2
+ class ClaudeRuntimeProvider {
3
+ name = 'claude';
4
+ createRunner(callbacks) {
5
+ return new ClaudeRunner(callbacks);
6
+ }
7
+ }
8
+ export function createClaudeRuntimeProvider() {
9
+ return new ClaudeRuntimeProvider();
10
+ }
@@ -0,0 +1,11 @@
1
+ import { ClaudeRunner } from '../claude/runner.js';
2
+ import { CodexBackend } from '../codex/backend.js';
3
+ class CodexRuntimeProvider {
4
+ name = 'codex';
5
+ createRunner(callbacks) {
6
+ return new ClaudeRunner(callbacks, new CodexBackend());
7
+ }
8
+ }
9
+ export function createCodexRuntimeProvider() {
10
+ return new CodexRuntimeProvider();
11
+ }
@@ -0,0 +1,2 @@
1
+ export { createClaudeRuntimeProvider } from './claude-runtime-provider.js';
2
+ export { createCodexRuntimeProvider } from './codex-runtime-provider.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Runtime abstraction types for agent CLI providers.
3
+ * Phase 1 keeps Claude as the only implementation but routes through these
4
+ * contracts so additional providers can be introduced safely.
5
+ */
6
+ export {};
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Agent Lifecycle Service
3
+ * Handles agent restart operations when skills or class instructions change
4
+ */
5
+ import * as agentService from './agent-service.js';
6
+ import * as runtimeService from './runtime-service.js';
7
+ import * as customClassService from './custom-class-service.js';
8
+ import { createLogger } from '../utils/index.js';
9
+ const log = createLogger('AgentLifecycle');
10
+ /**
11
+ * Restart all agents using a given skill when the skill is updated.
12
+ * An agent uses a skill if:
13
+ * 1. The skill is directly assigned to the agent
14
+ * 2. The skill is assigned to the agent's class
15
+ * 3. The agent's custom class has the skill as a default
16
+ */
17
+ export async function restartAgentsWithSkill(skill, sendActivity) {
18
+ const allAgents = agentService.getAllAgents();
19
+ const affectedAgents = allAgents.filter(agent => {
20
+ // Check direct assignment
21
+ if (skill.assignedAgentIds.includes(agent.id))
22
+ return true;
23
+ // Check class assignment (skill assigned to agent's class)
24
+ if (skill.assignedAgentClasses.includes(agent.class))
25
+ return true;
26
+ // Check custom class default skills
27
+ const customClass = customClassService.getCustomClass(agent.class);
28
+ if (customClass?.defaultSkillIds?.includes(skill.id))
29
+ return true;
30
+ return false;
31
+ });
32
+ if (affectedAgents.length === 0) {
33
+ log.log(`🔄 No agents using skill "${skill.name}" to restart`);
34
+ return;
35
+ }
36
+ log.log(`🔄 Restarting ${affectedAgents.length} agent(s) using skill "${skill.name}" due to skill update`);
37
+ for (const agent of affectedAgents) {
38
+ await restartAgent(agent, `skill "${skill.name}" updated`, sendActivity);
39
+ }
40
+ }
41
+ /**
42
+ * Restart all agents with a given class when the class instructions are updated.
43
+ * This stops the agent's current session and clears the sessionId so the next
44
+ * command will start a fresh session with the new instructions.
45
+ */
46
+ export async function restartAgentsWithClass(classId, sendActivity) {
47
+ const allAgents = agentService.getAllAgents();
48
+ const affectedAgents = allAgents.filter(agent => agent.class === classId);
49
+ if (affectedAgents.length === 0) {
50
+ log.log(`🔄 No agents with class "${classId}" to restart`);
51
+ return;
52
+ }
53
+ log.log(`🔄 Restarting ${affectedAgents.length} agent(s) with class "${classId}" due to instructions update`);
54
+ for (const agent of affectedAgents) {
55
+ await restartAgent(agent, 'class instructions updated', sendActivity);
56
+ }
57
+ }
58
+ /**
59
+ * Internal helper to restart a single agent
60
+ */
61
+ async function restartAgent(agent, reason, sendActivity) {
62
+ try {
63
+ log.log(`🔄 Restarting agent ${agent.name} (${agent.id}) due to ${reason}`);
64
+ // Stop the current process if running
65
+ await runtimeService.stopAgent(agent.id);
66
+ // Reset status but preserve sessionId - the session will resume with new instructions
67
+ agentService.updateAgent(agent.id, {
68
+ status: 'idle',
69
+ currentTask: undefined,
70
+ currentTool: undefined,
71
+ // Note: sessionId is preserved - Claude will resume the existing session
72
+ // with the new instructions/skills on the next command
73
+ });
74
+ // Notify the user
75
+ sendActivity?.(agent.id, `Agent restarted - ${reason}`);
76
+ log.log(`🔄 Agent ${agent.name} restarted successfully`);
77
+ }
78
+ catch (err) {
79
+ log.error(`🔄 Failed to restart agent ${agent.name}:`, err);
80
+ sendActivity?.(agent.id, `Failed to restart after ${reason}`);
81
+ }
82
+ }