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.
- package/LICENSE +21 -0
- package/README.md +364 -0
- package/dist/assets/characters/Textures/colormap.png +0 -0
- package/dist/assets/characters/character-female-a.glb +0 -0
- package/dist/assets/characters/character-female-b.glb +0 -0
- package/dist/assets/characters/character-female-c.glb +0 -0
- package/dist/assets/characters/character-female-d.glb +0 -0
- package/dist/assets/characters/character-female-e.glb +0 -0
- package/dist/assets/characters/character-female-f.glb +0 -0
- package/dist/assets/characters/character-male-a-processed.gltf +11862 -0
- package/dist/assets/characters/character-male-a.glb +0 -0
- package/dist/assets/characters/character-male-b.glb +0 -0
- package/dist/assets/characters/character-male-c.glb +0 -0
- package/dist/assets/characters/character-male-d.glb +0 -0
- package/dist/assets/characters/character-male-e.glb +0 -0
- package/dist/assets/characters/character-male-f.glb +0 -0
- package/dist/assets/icons/icon-192.png +0 -0
- package/dist/assets/icons/icon-512.png +0 -0
- package/dist/assets/landing-Cc0MDBAK.css +1 -0
- package/dist/assets/main-BIpLsrUu.css +1 -0
- package/dist/assets/main-DMTRw3br.js +276 -0
- package/dist/assets/textures/concrete_floor_worn_001_diff_1k.jpg +0 -0
- package/dist/assets/textures/logo-blanco.png +0 -0
- package/dist/assets/vendor-react-uS-d4TUT.js +17 -0
- package/dist/assets/vendor-three-4iQNXcoo.js +3828 -0
- package/dist/assets/web-BZdi2lG9.js +1 -0
- package/dist/assets/web-yHsOO1Qb.js +1 -0
- package/dist/index.html +38 -0
- package/dist/manifest.json +39 -0
- package/dist/src/packages/landing/index.html +463 -0
- package/dist/src/packages/server/app.js +87 -0
- package/dist/src/packages/server/auth/index.js +121 -0
- package/dist/src/packages/server/claude/backend.js +578 -0
- package/dist/src/packages/server/claude/index.js +8 -0
- package/dist/src/packages/server/claude/runner/internal-events.js +22 -0
- package/dist/src/packages/server/claude/runner/process-lifecycle.js +208 -0
- package/dist/src/packages/server/claude/runner/recovery-store.js +72 -0
- package/dist/src/packages/server/claude/runner/resource-monitor.js +51 -0
- package/dist/src/packages/server/claude/runner/restart-policy.js +69 -0
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +153 -0
- package/dist/src/packages/server/claude/runner/watchdog.js +114 -0
- package/dist/src/packages/server/claude/runner.js +310 -0
- package/dist/src/packages/server/claude/session-loader.js +898 -0
- package/dist/src/packages/server/claude/types.js +5 -0
- package/dist/src/packages/server/cli.js +113 -0
- package/dist/src/packages/server/codex/backend.js +119 -0
- package/dist/src/packages/server/codex/index.js +2 -0
- package/dist/src/packages/server/codex/json-event-parser.js +612 -0
- package/dist/src/packages/server/data/builtin-skills/bitbucket-pr.js +298 -0
- package/dist/src/packages/server/data/builtin-skills/full-notifications.js +49 -0
- package/dist/src/packages/server/data/builtin-skills/git-captain.js +304 -0
- package/dist/src/packages/server/data/builtin-skills/index.js +61 -0
- package/dist/src/packages/server/data/builtin-skills/pm2-logs.js +354 -0
- package/dist/src/packages/server/data/builtin-skills/send-message-to-agent.js +51 -0
- package/dist/src/packages/server/data/builtin-skills/server-logs.js +124 -0
- package/dist/src/packages/server/data/builtin-skills/streaming-exec.js +94 -0
- package/dist/src/packages/server/data/builtin-skills/types.js +4 -0
- package/dist/src/packages/server/data/builtin-skills.js +6 -0
- package/dist/src/packages/server/data/index.js +890 -0
- package/dist/src/packages/server/data/snapshots.js +371 -0
- package/dist/src/packages/server/index.js +96 -0
- package/dist/src/packages/server/prompts/tide-commander.js +13 -0
- package/dist/src/packages/server/routes/agents.js +406 -0
- package/dist/src/packages/server/routes/config.js +347 -0
- package/dist/src/packages/server/routes/custom-models.js +170 -0
- package/dist/src/packages/server/routes/exec.js +269 -0
- package/dist/src/packages/server/routes/files.js +995 -0
- package/dist/src/packages/server/routes/index.js +38 -0
- package/dist/src/packages/server/routes/notifications.js +81 -0
- package/dist/src/packages/server/routes/permissions.js +115 -0
- package/dist/src/packages/server/routes/snapshots.js +224 -0
- package/dist/src/packages/server/routes/stt.js +99 -0
- package/dist/src/packages/server/routes/tts.js +166 -0
- package/dist/src/packages/server/routes/voice-assistant.js +310 -0
- package/dist/src/packages/server/runtime/claude-runtime-provider.js +10 -0
- package/dist/src/packages/server/runtime/codex-runtime-provider.js +11 -0
- package/dist/src/packages/server/runtime/index.js +2 -0
- package/dist/src/packages/server/runtime/types.js +6 -0
- package/dist/src/packages/server/services/agent-lifecycle-service.js +82 -0
- package/dist/src/packages/server/services/agent-service.js +410 -0
- package/dist/src/packages/server/services/boss-message-service.js +430 -0
- package/dist/src/packages/server/services/boss-service.js +553 -0
- package/dist/src/packages/server/services/building-service.js +867 -0
- package/dist/src/packages/server/services/claude-service.js +5 -0
- package/dist/src/packages/server/services/custom-class-service.js +323 -0
- package/dist/src/packages/server/services/database-service.js +914 -0
- package/dist/src/packages/server/services/docker-service.js +865 -0
- package/dist/src/packages/server/services/fileTracker.js +242 -0
- package/dist/src/packages/server/services/index.js +21 -0
- package/dist/src/packages/server/services/permission-service.js +258 -0
- package/dist/src/packages/server/services/pm2-service.js +435 -0
- package/dist/src/packages/server/services/runtime-command-execution.js +168 -0
- package/dist/src/packages/server/services/runtime-events.js +357 -0
- package/dist/src/packages/server/services/runtime-service.js +308 -0
- package/dist/src/packages/server/services/runtime-status-sync.js +104 -0
- package/dist/src/packages/server/services/runtime-subagents.js +50 -0
- package/dist/src/packages/server/services/runtime-watchdog.js +74 -0
- package/dist/src/packages/server/services/secrets-service.js +206 -0
- package/dist/src/packages/server/services/skill-service.js +508 -0
- package/dist/src/packages/server/services/subordinate-context-service.js +223 -0
- package/dist/src/packages/server/services/supervisor-claude.js +132 -0
- package/dist/src/packages/server/services/supervisor-prompts.js +80 -0
- package/dist/src/packages/server/services/supervisor-service.js +659 -0
- package/dist/src/packages/server/services/work-plan-service.js +476 -0
- package/dist/src/packages/server/setup.js +86 -0
- package/dist/src/packages/server/utils/index.js +4 -0
- package/dist/src/packages/server/utils/logger.js +302 -0
- package/dist/src/packages/server/utils/string.js +39 -0
- package/dist/src/packages/server/utils/tool-formatting.js +139 -0
- package/dist/src/packages/server/utils/unicode.js +46 -0
- package/dist/src/packages/server/websocket/handler.js +290 -0
- package/dist/src/packages/server/websocket/handlers/agent-handler.js +515 -0
- package/dist/src/packages/server/websocket/handlers/boss-handler.js +116 -0
- package/dist/src/packages/server/websocket/handlers/boss-response-handler.js +250 -0
- package/dist/src/packages/server/websocket/handlers/building-handler.js +298 -0
- package/dist/src/packages/server/websocket/handlers/command-handler.js +217 -0
- package/dist/src/packages/server/websocket/handlers/custom-class-handler.js +68 -0
- package/dist/src/packages/server/websocket/handlers/database-handler.js +223 -0
- package/dist/src/packages/server/websocket/handlers/notification-handler.js +25 -0
- package/dist/src/packages/server/websocket/handlers/permission-handler.js +21 -0
- package/dist/src/packages/server/websocket/handlers/secrets-handler.js +61 -0
- package/dist/src/packages/server/websocket/handlers/skill-handler.js +148 -0
- package/dist/src/packages/server/websocket/handlers/supervisor-handler.js +44 -0
- package/dist/src/packages/server/websocket/handlers/sync-handler.js +19 -0
- package/dist/src/packages/server/websocket/handlers/types.js +4 -0
- package/dist/src/packages/server/websocket/listeners/boss-listeners.js +21 -0
- package/dist/src/packages/server/websocket/listeners/index.js +32 -0
- package/dist/src/packages/server/websocket/listeners/permission-listeners.js +19 -0
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +196 -0
- package/dist/src/packages/server/websocket/listeners/skill-listeners.js +51 -0
- package/dist/src/packages/server/websocket/listeners/supervisor-listeners.js +37 -0
- package/dist/src/packages/shared/agent-types.js +54 -0
- package/dist/src/packages/shared/building-types.js +43 -0
- package/dist/src/packages/shared/common-types.js +1 -0
- package/dist/src/packages/shared/database-types.js +8 -0
- package/dist/src/packages/shared/types/snapshot.js +7 -0
- package/dist/src/packages/shared/types.js +12 -0
- package/dist/src/packages/shared/websocket-messages.js +1 -0
- package/dist/sw.js +37 -0
- 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,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
|
+
}
|