@meechi-ai/core 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/LICENSE +624 -0
- package/README.md +59 -0
- package/dist/components/CalendarView.d.ts +3 -0
- package/dist/components/CalendarView.js +72 -0
- package/dist/components/ChatInterface.d.ts +6 -0
- package/dist/components/ChatInterface.js +105 -0
- package/dist/components/FileExplorer.d.ts +9 -0
- package/dist/components/FileExplorer.js +757 -0
- package/dist/components/Icon.d.ts +9 -0
- package/dist/components/Icon.js +44 -0
- package/dist/components/SourceEditor.d.ts +13 -0
- package/dist/components/SourceEditor.js +50 -0
- package/dist/components/ThemeProvider.d.ts +5 -0
- package/dist/components/ThemeProvider.js +105 -0
- package/dist/components/ThemeSwitcher.d.ts +1 -0
- package/dist/components/ThemeSwitcher.js +16 -0
- package/dist/components/voice/VoiceInputArea.d.ts +14 -0
- package/dist/components/voice/VoiceInputArea.js +190 -0
- package/dist/components/voice/VoiceOverlay.d.ts +7 -0
- package/dist/components/voice/VoiceOverlay.js +71 -0
- package/dist/hooks/useMeechi.d.ts +16 -0
- package/dist/hooks/useMeechi.js +461 -0
- package/dist/hooks/useSync.d.ts +8 -0
- package/dist/hooks/useSync.js +87 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +22 -0
- package/dist/lib/ai/embeddings.d.ts +15 -0
- package/dist/lib/ai/embeddings.js +128 -0
- package/dist/lib/ai/gpu-lock.d.ts +19 -0
- package/dist/lib/ai/gpu-lock.js +43 -0
- package/dist/lib/ai/llm.worker.d.ts +1 -0
- package/dist/lib/ai/llm.worker.js +7 -0
- package/dist/lib/ai/local-llm.d.ts +30 -0
- package/dist/lib/ai/local-llm.js +211 -0
- package/dist/lib/ai/manager.d.ts +20 -0
- package/dist/lib/ai/manager.js +51 -0
- package/dist/lib/ai/parsing.d.ts +12 -0
- package/dist/lib/ai/parsing.js +56 -0
- package/dist/lib/ai/prompts.d.ts +2 -0
- package/dist/lib/ai/prompts.js +2 -0
- package/dist/lib/ai/providers/gemini.d.ts +6 -0
- package/dist/lib/ai/providers/gemini.js +88 -0
- package/dist/lib/ai/providers/groq.d.ts +6 -0
- package/dist/lib/ai/providers/groq.js +42 -0
- package/dist/lib/ai/registry.d.ts +29 -0
- package/dist/lib/ai/registry.js +52 -0
- package/dist/lib/ai/tools.d.ts +2 -0
- package/dist/lib/ai/tools.js +106 -0
- package/dist/lib/ai/types.d.ts +22 -0
- package/dist/lib/ai/types.js +1 -0
- package/dist/lib/ai/worker.d.ts +1 -0
- package/dist/lib/ai/worker.js +60 -0
- package/dist/lib/audio/input.d.ts +13 -0
- package/dist/lib/audio/input.js +121 -0
- package/dist/lib/audio/stt.d.ts +13 -0
- package/dist/lib/audio/stt.js +119 -0
- package/dist/lib/audio/tts.d.ts +12 -0
- package/dist/lib/audio/tts.js +128 -0
- package/dist/lib/audio/vad.d.ts +18 -0
- package/dist/lib/audio/vad.js +117 -0
- package/dist/lib/colors.d.ts +16 -0
- package/dist/lib/colors.js +67 -0
- package/dist/lib/extensions.d.ts +35 -0
- package/dist/lib/extensions.js +24 -0
- package/dist/lib/hooks/use-voice-loop.d.ts +13 -0
- package/dist/lib/hooks/use-voice-loop.js +313 -0
- package/dist/lib/mcp/McpClient.d.ts +19 -0
- package/dist/lib/mcp/McpClient.js +42 -0
- package/dist/lib/mcp/McpRegistry.d.ts +47 -0
- package/dist/lib/mcp/McpRegistry.js +117 -0
- package/dist/lib/mcp/native/GroqVoiceNative.d.ts +21 -0
- package/dist/lib/mcp/native/GroqVoiceNative.js +29 -0
- package/dist/lib/mcp/native/LocalSyncNative.d.ts +19 -0
- package/dist/lib/mcp/native/LocalSyncNative.js +26 -0
- package/dist/lib/mcp/native/LocalVoiceNative.d.ts +19 -0
- package/dist/lib/mcp/native/LocalVoiceNative.js +27 -0
- package/dist/lib/mcp/native/MeechiNativeCore.d.ts +25 -0
- package/dist/lib/mcp/native/MeechiNativeCore.js +209 -0
- package/dist/lib/mcp/native/index.d.ts +10 -0
- package/dist/lib/mcp/native/index.js +10 -0
- package/dist/lib/mcp/types.d.ts +35 -0
- package/dist/lib/mcp/types.js +1 -0
- package/dist/lib/pdf.d.ts +10 -0
- package/dist/lib/pdf.js +142 -0
- package/dist/lib/settings.d.ts +48 -0
- package/dist/lib/settings.js +87 -0
- package/dist/lib/storage/db.d.ts +57 -0
- package/dist/lib/storage/db.js +45 -0
- package/dist/lib/storage/local.d.ts +28 -0
- package/dist/lib/storage/local.js +534 -0
- package/dist/lib/storage/migrate.d.ts +3 -0
- package/dist/lib/storage/migrate.js +122 -0
- package/dist/lib/storage/types.d.ts +66 -0
- package/dist/lib/storage/types.js +1 -0
- package/dist/lib/sync/client-drive.d.ts +9 -0
- package/dist/lib/sync/client-drive.js +69 -0
- package/dist/lib/sync/engine.d.ts +18 -0
- package/dist/lib/sync/engine.js +517 -0
- package/dist/lib/sync/google-drive.d.ts +52 -0
- package/dist/lib/sync/google-drive.js +183 -0
- package/dist/lib/sync/merge.d.ts +1 -0
- package/dist/lib/sync/merge.js +68 -0
- package/dist/lib/yjs/YjsProvider.d.ts +11 -0
- package/dist/lib/yjs/YjsProvider.js +33 -0
- package/dist/lib/yjs/graph.d.ts +11 -0
- package/dist/lib/yjs/graph.js +7 -0
- package/dist/lib/yjs/hooks.d.ts +7 -0
- package/dist/lib/yjs/hooks.js +37 -0
- package/dist/lib/yjs/store.d.ts +4 -0
- package/dist/lib/yjs/store.js +19 -0
- package/dist/lib/yjs/syncGraph.d.ts +1 -0
- package/dist/lib/yjs/syncGraph.js +38 -0
- package/dist/providers/theme-provider.d.ts +3 -0
- package/dist/providers/theme-provider.js +18 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import { AudioInputService } from '../audio/input';
|
|
3
|
+
import { VADService } from '../audio/vad';
|
|
4
|
+
import { TranscriberService } from '../audio/stt';
|
|
5
|
+
import { SynthesizerService } from '../audio/tts';
|
|
6
|
+
export function useVoiceLoop(sendMessage) {
|
|
7
|
+
const [state, setState] = useState('idle');
|
|
8
|
+
const stateRef = useRef('idle'); // Fix for stale closure in callbacks
|
|
9
|
+
const [transcript, setTranscript] = useState("");
|
|
10
|
+
const [vadProb, setVadProb] = useState(0);
|
|
11
|
+
// Sync ref with state
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
stateRef.current = state;
|
|
14
|
+
}, [state]);
|
|
15
|
+
const audioInput = useRef(new AudioInputService());
|
|
16
|
+
const vad = useRef(new VADService());
|
|
17
|
+
const audioBuffer = useRef([]);
|
|
18
|
+
const audioChunksRef = useRef([]);
|
|
19
|
+
const isSpeechPrev = useRef(false);
|
|
20
|
+
// Audio Context for playback
|
|
21
|
+
const playbackCtx = useRef(null);
|
|
22
|
+
const playbackSource = useRef(null);
|
|
23
|
+
const chunksProcessedRef = useRef(0);
|
|
24
|
+
// STREAMING TTS STATE
|
|
25
|
+
const audioQueue = useRef([]);
|
|
26
|
+
const isPlayingRef = useRef(false);
|
|
27
|
+
const sentenceBuffer = useRef("");
|
|
28
|
+
const stopPlayback = useCallback(() => {
|
|
29
|
+
if (playbackSource.current) {
|
|
30
|
+
playbackSource.current.stop();
|
|
31
|
+
playbackSource.current = null;
|
|
32
|
+
}
|
|
33
|
+
// Clear Queue
|
|
34
|
+
audioQueue.current = [];
|
|
35
|
+
isPlayingRef.current = false;
|
|
36
|
+
sentenceBuffer.current = "";
|
|
37
|
+
setState('idle');
|
|
38
|
+
}, []);
|
|
39
|
+
const processTTSChunk = async (text) => {
|
|
40
|
+
if (!text.trim())
|
|
41
|
+
return;
|
|
42
|
+
// Strip Markdown for TTS (e.g. *italic*, **bold**, `code`)
|
|
43
|
+
// Also remove excessive punctuation runs
|
|
44
|
+
const cleanText = text
|
|
45
|
+
.replace(/[*_`~]/g, '') // Remove markdown symbols
|
|
46
|
+
.replace(/\[.*?\]/g, '') // Remove [instructions] if any leak
|
|
47
|
+
.replace(/\s+/g, ' ')
|
|
48
|
+
.trim();
|
|
49
|
+
if (!cleanText)
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
52
|
+
const rawAudio = await SynthesizerService.speak(cleanText);
|
|
53
|
+
if (rawAudio) {
|
|
54
|
+
audioQueue.current.push({ audio: rawAudio.audio, sampleRate: rawAudio.sampling_rate });
|
|
55
|
+
playNextInQueue();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
console.error("TTS Gen Error:", e);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const processAudio = async (text) => {
|
|
63
|
+
if (!text.trim())
|
|
64
|
+
return;
|
|
65
|
+
// PRE-FILTER: Ignore known hallucination strings from STT (Case-insensitive)
|
|
66
|
+
const lower = text.trim().toLowerCase();
|
|
67
|
+
if (lower === 'you' || // "You" is very common hallucination
|
|
68
|
+
/^\[.*\]$/.test(lower) || // [Music], [Silence]
|
|
69
|
+
/^\(.*\)$/.test(lower) || // (Music)
|
|
70
|
+
lower.includes('[music]') ||
|
|
71
|
+
lower.includes('[silence]') ||
|
|
72
|
+
// Common Whisper Hallucinations:
|
|
73
|
+
lower.includes('thank you for watching') ||
|
|
74
|
+
lower.includes('thanks for watching')) {
|
|
75
|
+
console.log("[VoiceLoop] Ignoring hallucination:", text);
|
|
76
|
+
setState('idle'); // Ensure we return to idle
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
console.log("[VoiceLoop] Processing User Audio:", text);
|
|
80
|
+
setState('processing');
|
|
81
|
+
sentenceBuffer.current = "";
|
|
82
|
+
try {
|
|
83
|
+
// 1. Send to AI with STREAMING callback
|
|
84
|
+
const response = await sendMessage(text, (token) => {
|
|
85
|
+
// Check for interruption
|
|
86
|
+
if (stateRef.current === 'listening')
|
|
87
|
+
return; // Abort if user started talking again?
|
|
88
|
+
sentenceBuffer.current += token;
|
|
89
|
+
// Split by sentence endings (. ! ? \n)
|
|
90
|
+
// Regex lookbehind is not supported in all browsers, so use simple split
|
|
91
|
+
// Capture delimiters
|
|
92
|
+
// Note: This matches "Dr." as a split. Improved regex needed for "Dr." etc, but for now simple is ok.
|
|
93
|
+
// Heuristic: Ends with [.!?] AND (followed by space or newline) - logic applied to buffer
|
|
94
|
+
// Check if we have a full sentence in buffer
|
|
95
|
+
// Regex: Match anything ending in [.!?] followed by space or EOS
|
|
96
|
+
// We only split if we have enough content
|
|
97
|
+
// HALLUCINATION/NOISE FILTER
|
|
98
|
+
// If the *entire* buffer is just these common patterns, we suppress it.
|
|
99
|
+
// We only check this if probability is low, but better safe.
|
|
100
|
+
if (/^[\(\[\{]?(music|silence|sound|background|noise)[\)\]\}]?$/i.test(sentenceBuffer.current.trim())) {
|
|
101
|
+
console.log("[VoiceLoop] Filtered Hallucination:", sentenceBuffer.current);
|
|
102
|
+
sentenceBuffer.current = "";
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// FIRST CHUNK STRATEGY:
|
|
106
|
+
// To lower latency, we split the FIRST sentence on commas or small pauses (length)
|
|
107
|
+
const isFirstChunk = !isPlayingRef.current && audioQueue.current.length === 0;
|
|
108
|
+
const splitRegex = isFirstChunk
|
|
109
|
+
? /([,;:.!?\n])\s+/ // Split on commas too for the first burst
|
|
110
|
+
: /([.!?\n])\s+/; // Standard sentence split
|
|
111
|
+
const minChars = isFirstChunk ? 30 : 100; // Allow smaller first chunk
|
|
112
|
+
if (splitRegex.test(sentenceBuffer.current) && sentenceBuffer.current.length > minChars) {
|
|
113
|
+
// Attempt to split
|
|
114
|
+
const match = sentenceBuffer.current.match(new RegExp(`^(.*?${splitRegex.source})`));
|
|
115
|
+
if (match) {
|
|
116
|
+
const chunk = match[0];
|
|
117
|
+
const remaining = sentenceBuffer.current.slice(chunk.length);
|
|
118
|
+
if (chunk.trim()) {
|
|
119
|
+
processTTSChunk(chunk.trim());
|
|
120
|
+
sentenceBuffer.current = remaining;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// 2. Flush remaining buffer
|
|
126
|
+
if (sentenceBuffer.current.trim()) {
|
|
127
|
+
await processTTSChunk(sentenceBuffer.current.trim());
|
|
128
|
+
}
|
|
129
|
+
console.log("[VoiceLoop] Generation Complete.");
|
|
130
|
+
// Wait for queue to drain?
|
|
131
|
+
// We need a loop to check if playing is done
|
|
132
|
+
const checkDone = setInterval(() => {
|
|
133
|
+
if (stateRef.current !== 'speaking' && stateRef.current !== 'processing') {
|
|
134
|
+
clearInterval(checkDone);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!isPlayingRef.current && audioQueue.current.length === 0) {
|
|
138
|
+
clearInterval(checkDone);
|
|
139
|
+
setState('idle');
|
|
140
|
+
}
|
|
141
|
+
}, 200);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
console.error("Voice Loop Error:", e);
|
|
145
|
+
setState('idle');
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const silenceChunksRef = useRef(0);
|
|
149
|
+
// 30 chunks * 512 samples / 16000 Hz ~= 1.0 second roughly.
|
|
150
|
+
// Let's use 1.2s to be safe for slow speakers.
|
|
151
|
+
// 1.2 / 0.032 = 37.5 ~ 40 chunks
|
|
152
|
+
const MAX_SILENCE_CHUNKS = 40;
|
|
153
|
+
// Track Playing State for UI
|
|
154
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
155
|
+
// Sync Ref with State for internal logic
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
setIsPlaying(isPlayingRef.current);
|
|
158
|
+
}, [isPlayingRef.current]); // This dependency might not trigger if ref mutates, we need to set state where we set ref.
|
|
159
|
+
// Helper to update playing state
|
|
160
|
+
const setPlayingState = (val) => {
|
|
161
|
+
isPlayingRef.current = val;
|
|
162
|
+
setIsPlaying(val);
|
|
163
|
+
};
|
|
164
|
+
// ... (stopPlayback updates)
|
|
165
|
+
// We need to update stopPlayback to use setPlayingState
|
|
166
|
+
// But since stopPlayback is memoized and we are inside the hook function body (recreated?), let's just update the internal logic below
|
|
167
|
+
// Process and Play next chunk in queue
|
|
168
|
+
const playNextInQueue = async () => {
|
|
169
|
+
if (isPlayingRef.current || audioQueue.current.length === 0) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const next = audioQueue.current.shift();
|
|
173
|
+
if (!next || !playbackCtx.current)
|
|
174
|
+
return;
|
|
175
|
+
setPlayingState(true);
|
|
176
|
+
setState('speaking');
|
|
177
|
+
try {
|
|
178
|
+
if (playbackCtx.current.state === 'suspended') {
|
|
179
|
+
await playbackCtx.current.resume();
|
|
180
|
+
}
|
|
181
|
+
const buffer = playbackCtx.current.createBuffer(1, next.audio.length, next.sampleRate);
|
|
182
|
+
buffer.copyToChannel(new Float32Array(next.audio), 0);
|
|
183
|
+
const source = playbackCtx.current.createBufferSource();
|
|
184
|
+
source.buffer = buffer;
|
|
185
|
+
source.connect(playbackCtx.current.destination);
|
|
186
|
+
source.start();
|
|
187
|
+
playbackSource.current = source;
|
|
188
|
+
source.onended = () => {
|
|
189
|
+
setPlayingState(false);
|
|
190
|
+
if (audioQueue.current.length > 0) {
|
|
191
|
+
playNextInQueue();
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
// Queue empty.
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
console.error("Playback Error:", e);
|
|
200
|
+
setPlayingState(false);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
// Update stopPlayback
|
|
204
|
+
// We can't easily overwrite the previous useCallback in this 'replace' block without context.
|
|
205
|
+
// But we can just ensuring setPlayingState(false) is called where isPlayingRef.current = false was.
|
|
206
|
+
// I will replace start() logic mostly here.
|
|
207
|
+
const start = async () => {
|
|
208
|
+
if (!playbackCtx.current) {
|
|
209
|
+
playbackCtx.current = new AudioContext();
|
|
210
|
+
}
|
|
211
|
+
if (playbackCtx.current.state === 'suspended') {
|
|
212
|
+
await playbackCtx.current.resume();
|
|
213
|
+
}
|
|
214
|
+
await vad.current.init();
|
|
215
|
+
await audioInput.current.start(async (data) => {
|
|
216
|
+
// 1. VAD Check
|
|
217
|
+
let { isSpeech, probability } = await vad.current.process(data);
|
|
218
|
+
const maxAmp = Math.max(...data);
|
|
219
|
+
if (!isSpeech && maxAmp > 0.05) {
|
|
220
|
+
isSpeech = true;
|
|
221
|
+
probability = 0.8;
|
|
222
|
+
}
|
|
223
|
+
setVadProb(probability);
|
|
224
|
+
isSpeechPrev.current = isSpeech;
|
|
225
|
+
const currentState = stateRef.current;
|
|
226
|
+
if (isSpeech) {
|
|
227
|
+
// SPEECH DETECTED
|
|
228
|
+
silenceChunksRef.current = 0; // Reset silence counter
|
|
229
|
+
if (currentState === 'speaking') {
|
|
230
|
+
stopPlayback(); // Interruption
|
|
231
|
+
}
|
|
232
|
+
if (currentState !== 'listening' && currentState !== 'processing') {
|
|
233
|
+
setState('listening');
|
|
234
|
+
}
|
|
235
|
+
audioBuffer.current.push(data);
|
|
236
|
+
}
|
|
237
|
+
else if (currentState === 'listening') {
|
|
238
|
+
// SILENCE (DETECTED)
|
|
239
|
+
// We are in listening mode, but VAD says silence.
|
|
240
|
+
// We do NOT stop immediately. We accumulate silence.
|
|
241
|
+
silenceChunksRef.current++;
|
|
242
|
+
audioBuffer.current.push(data); // Keep recording the "silence" (sentence pause)
|
|
243
|
+
if (silenceChunksRef.current > MAX_SILENCE_CHUNKS) {
|
|
244
|
+
// TIMEOUT: Speech Actually Ended
|
|
245
|
+
silenceChunksRef.current = 0;
|
|
246
|
+
const length = audioBuffer.current.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
247
|
+
// IGNORE SHORT CLICKS (Under 0.5s of effective content?)
|
|
248
|
+
// Since we included silence, the total length is huge now.
|
|
249
|
+
// We should check the length MINUS the silence tail?
|
|
250
|
+
// Or just rely on the fact that if we hit the timeout, it was probably speech + silence.
|
|
251
|
+
// But what if it was JUST silence (false trigger)?
|
|
252
|
+
// Check total length: 40 chunks * 512 = 20k samples.
|
|
253
|
+
// If we have < 30k total samples, it means we ONLY recorded the silence tail + tiny blip.
|
|
254
|
+
if (length < 24000) { // < 1.5s total (mostly silence)
|
|
255
|
+
console.log("[VoiceLoop] Speech too short (mostly silence), ignoring. Length:", length);
|
|
256
|
+
audioBuffer.current = [];
|
|
257
|
+
setState('idle');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
console.log(`[VoiceLoop] Speech Ended (Silence Timeout). Processing...`);
|
|
261
|
+
const fullAudio = new Float32Array(length);
|
|
262
|
+
let offset = 0;
|
|
263
|
+
for (const chunk of audioBuffer.current) {
|
|
264
|
+
fullAudio.set(chunk, offset);
|
|
265
|
+
offset += chunk.length;
|
|
266
|
+
}
|
|
267
|
+
audioBuffer.current = [];
|
|
268
|
+
stateRef.current = 'processing';
|
|
269
|
+
setState('processing');
|
|
270
|
+
console.log("[VoiceLoop] Transcribing...");
|
|
271
|
+
let text = "";
|
|
272
|
+
try {
|
|
273
|
+
text = await TranscriberService.transcribe(fullAudio);
|
|
274
|
+
console.log(`[VoiceLoop] Transcribed Text: "${text}"`);
|
|
275
|
+
setTranscript(text);
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
console.error("[VoiceLoop] Transcription Error:", err);
|
|
279
|
+
setTranscript("");
|
|
280
|
+
}
|
|
281
|
+
if (text.trim()) {
|
|
282
|
+
await processAudio(text);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
stateRef.current = 'idle';
|
|
286
|
+
setState('idle');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
};
|
|
292
|
+
const stop = () => {
|
|
293
|
+
audioInput.current.stop();
|
|
294
|
+
stopPlayback();
|
|
295
|
+
setState('idle');
|
|
296
|
+
};
|
|
297
|
+
// Kept for backward compatibility if manual playback is needed
|
|
298
|
+
const playResponse = async (text) => {
|
|
299
|
+
stopPlayback();
|
|
300
|
+
await processTTSChunk(text);
|
|
301
|
+
};
|
|
302
|
+
return {
|
|
303
|
+
start,
|
|
304
|
+
stop,
|
|
305
|
+
stopPlayback,
|
|
306
|
+
state,
|
|
307
|
+
vadProb,
|
|
308
|
+
transcript,
|
|
309
|
+
playResponse,
|
|
310
|
+
isPlaying,
|
|
311
|
+
getAnalyser: () => audioInput.current.getAnalyser()
|
|
312
|
+
};
|
|
313
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* McpClient - The MCP Host/Client for Meechi
|
|
3
|
+
*
|
|
4
|
+
* This class initializes the MCP registry with native (built-in) servers.
|
|
5
|
+
* It acts as the "host" that manages tool providers.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - Native servers: In-process TypeScript classes (MeechiNativeCore, etc.)
|
|
9
|
+
* - External servers: Real MCP protocol via SDK (coming in future releases)
|
|
10
|
+
*/
|
|
11
|
+
export declare class McpClient {
|
|
12
|
+
constructor();
|
|
13
|
+
getTools(): Promise<any[]>;
|
|
14
|
+
executeTool(name: string, args: any): Promise<any>;
|
|
15
|
+
}
|
|
16
|
+
export declare const mcpClient: McpClient;
|
|
17
|
+
export { mcpRegistry } from './McpRegistry';
|
|
18
|
+
export * from './types';
|
|
19
|
+
export * from './native';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { mcpRegistry } from './McpRegistry';
|
|
2
|
+
import { MeechiNativeCore, GroqVoiceNative, LocalVoiceNative, LocalSyncNative } from './native';
|
|
3
|
+
/**
|
|
4
|
+
* McpClient - The MCP Host/Client for Meechi
|
|
5
|
+
*
|
|
6
|
+
* This class initializes the MCP registry with native (built-in) servers.
|
|
7
|
+
* It acts as the "host" that manages tool providers.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* - Native servers: In-process TypeScript classes (MeechiNativeCore, etc.)
|
|
11
|
+
* - External servers: Real MCP protocol via SDK (coming in future releases)
|
|
12
|
+
*/
|
|
13
|
+
export class McpClient {
|
|
14
|
+
constructor() {
|
|
15
|
+
// 1. Mandatory Core System (permanent)
|
|
16
|
+
mcpRegistry.registerServer(new MeechiNativeCore());
|
|
17
|
+
// 2. Pluggable Native Servers (can be activated/deactivated)
|
|
18
|
+
mcpRegistry.registerServer(new GroqVoiceNative());
|
|
19
|
+
mcpRegistry.registerServer(new LocalVoiceNative());
|
|
20
|
+
mcpRegistry.registerServer(new LocalSyncNative());
|
|
21
|
+
}
|
|
22
|
+
async getTools() {
|
|
23
|
+
const tools = await mcpRegistry.getAllTools();
|
|
24
|
+
return tools.map(t => ({
|
|
25
|
+
type: 'function',
|
|
26
|
+
function: {
|
|
27
|
+
name: t.name,
|
|
28
|
+
description: t.description,
|
|
29
|
+
parameters: t.inputSchema
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
async executeTool(name, args) {
|
|
34
|
+
return await mcpRegistry.executeTool(name, args);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Global instance for the app
|
|
38
|
+
export const mcpClient = new McpClient();
|
|
39
|
+
// Re-exports
|
|
40
|
+
export { mcpRegistry } from './McpRegistry';
|
|
41
|
+
export * from './types';
|
|
42
|
+
export * from './native';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { McpTool, McpResource } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Basic interface for ANY MCP Server (Internal or External)
|
|
4
|
+
*/
|
|
5
|
+
export interface McpConnector {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
isPermanent: boolean;
|
|
10
|
+
isAgenticMemory?: boolean;
|
|
11
|
+
getTools(): Promise<McpTool[]>;
|
|
12
|
+
getResources?(): Promise<McpResource[]>;
|
|
13
|
+
executeTool(name: string, args: any): Promise<any>;
|
|
14
|
+
getSystemInstructions?(): Promise<string>;
|
|
15
|
+
}
|
|
16
|
+
export type MeechiTier = 'tier1' | 'tier2' | 'tier3';
|
|
17
|
+
/**
|
|
18
|
+
* The Central Registry that manages "Slotted" MCP Servers.
|
|
19
|
+
*/
|
|
20
|
+
export declare class McpRegistry {
|
|
21
|
+
private servers;
|
|
22
|
+
private activeSlots;
|
|
23
|
+
private activeMemories;
|
|
24
|
+
private tier;
|
|
25
|
+
private TIER_CONSTRAINTS;
|
|
26
|
+
constructor(tier?: MeechiTier);
|
|
27
|
+
setTier(tier: MeechiTier): void;
|
|
28
|
+
getMaxSlots(): number;
|
|
29
|
+
registerServer(server: McpConnector): void;
|
|
30
|
+
activateSlot(serverId: string): boolean;
|
|
31
|
+
deactivateSlot(serverId: string): void;
|
|
32
|
+
getAllTools(): Promise<McpTool[]>;
|
|
33
|
+
executeTool(name: string, args: any): Promise<any>;
|
|
34
|
+
getCombinedInstructions(): Promise<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Gets instructions for a SPECIFIC agent (e.g. for Mode switching)
|
|
37
|
+
*/
|
|
38
|
+
getAgentInstructions(agentId: string): Promise<string | null>;
|
|
39
|
+
getMarketplace(): {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
description: string;
|
|
43
|
+
isActive: boolean;
|
|
44
|
+
isPermanent: boolean;
|
|
45
|
+
}[];
|
|
46
|
+
}
|
|
47
|
+
export declare const mcpRegistry: McpRegistry;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Central Registry that manages "Slotted" MCP Servers.
|
|
3
|
+
*/
|
|
4
|
+
export class McpRegistry {
|
|
5
|
+
constructor(tier = 'tier1') {
|
|
6
|
+
this.servers = new Map();
|
|
7
|
+
this.activeSlots = new Set();
|
|
8
|
+
this.activeMemories = new Set();
|
|
9
|
+
this.tier = 'tier1';
|
|
10
|
+
this.TIER_CONSTRAINTS = {
|
|
11
|
+
tier1: { maxSlots: 2, maxMemories: 5 },
|
|
12
|
+
tier2: { maxSlots: 10, maxMemories: 5 },
|
|
13
|
+
tier3: { maxSlots: -1, maxMemories: 5 } // Unlimited slots, 5 agents
|
|
14
|
+
};
|
|
15
|
+
this.tier = tier;
|
|
16
|
+
}
|
|
17
|
+
setTier(tier) {
|
|
18
|
+
this.tier = tier;
|
|
19
|
+
}
|
|
20
|
+
getMaxSlots() {
|
|
21
|
+
return this.TIER_CONSTRAINTS[this.tier].maxSlots;
|
|
22
|
+
}
|
|
23
|
+
registerServer(server) {
|
|
24
|
+
this.servers.set(server.id, server);
|
|
25
|
+
if (server.isPermanent) {
|
|
26
|
+
this.activeSlots.add(server.id);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
activateSlot(serverId) {
|
|
30
|
+
const server = this.servers.get(serverId);
|
|
31
|
+
if (!server)
|
|
32
|
+
return false;
|
|
33
|
+
if (this.activeSlots.has(serverId))
|
|
34
|
+
return true;
|
|
35
|
+
const constraints = this.TIER_CONSTRAINTS[this.tier];
|
|
36
|
+
// Agentic Memory Specific Logic (Uses interface flag, not hardcoded IDs)
|
|
37
|
+
const isAgentic = server.isAgenticMemory === true;
|
|
38
|
+
if (isAgentic) {
|
|
39
|
+
if (this.activeMemories.size >= constraints.maxMemories) {
|
|
40
|
+
// Return false or throw error for UI to catch
|
|
41
|
+
throw new Error(`Memory limit reached. You can have up to ${constraints.maxMemories} active memories. Please deactivate one first.`);
|
|
42
|
+
}
|
|
43
|
+
this.activeMemories.add(serverId);
|
|
44
|
+
this.activeSlots.add(serverId);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
// Generic Marketplace Logic
|
|
48
|
+
const nonPermanentCount = Array.from(this.activeSlots).filter(id => {
|
|
49
|
+
const s = this.servers.get(id);
|
|
50
|
+
return !(s === null || s === void 0 ? void 0 : s.isPermanent) && !(s === null || s === void 0 ? void 0 : s.isAgenticMemory);
|
|
51
|
+
}).length;
|
|
52
|
+
if (constraints.maxSlots !== -1 && nonPermanentCount >= constraints.maxSlots) {
|
|
53
|
+
throw new Error(`Slot limit reached (${constraints.maxSlots}). Please upgrade your plan for more slots.`);
|
|
54
|
+
}
|
|
55
|
+
this.activeSlots.add(serverId);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
deactivateSlot(serverId) {
|
|
59
|
+
const server = this.servers.get(serverId);
|
|
60
|
+
if (server === null || server === void 0 ? void 0 : server.isPermanent)
|
|
61
|
+
return;
|
|
62
|
+
this.activeSlots.delete(serverId);
|
|
63
|
+
this.activeMemories.delete(serverId);
|
|
64
|
+
}
|
|
65
|
+
async getAllTools() {
|
|
66
|
+
let allTools = [];
|
|
67
|
+
for (const serverId of this.activeSlots) {
|
|
68
|
+
const server = this.servers.get(serverId);
|
|
69
|
+
if (server) {
|
|
70
|
+
const tools = await server.getTools();
|
|
71
|
+
allTools = [...allTools, ...tools];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return allTools;
|
|
75
|
+
}
|
|
76
|
+
async executeTool(name, args) {
|
|
77
|
+
// Find which server has this tool
|
|
78
|
+
for (const serverId of this.activeSlots) {
|
|
79
|
+
const server = this.servers.get(serverId);
|
|
80
|
+
if (server) {
|
|
81
|
+
const tools = await server.getTools();
|
|
82
|
+
if (tools.find(t => t.name === name)) {
|
|
83
|
+
return await server.executeTool(name, args);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`Tool ${name} not found in any active MCP slots.`);
|
|
88
|
+
}
|
|
89
|
+
async getCombinedInstructions() {
|
|
90
|
+
let instructions = "";
|
|
91
|
+
for (const serverId of this.activeMemories) {
|
|
92
|
+
const server = this.servers.get(serverId);
|
|
93
|
+
if (server === null || server === void 0 ? void 0 : server.getSystemInstructions) {
|
|
94
|
+
const instructionsText = await server.getSystemInstructions();
|
|
95
|
+
instructions += `\n\n--- PERSONA: ${server.name} ---\n${instructionsText}\n`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return instructions;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Gets instructions for a SPECIFIC agent (e.g. for Mode switching)
|
|
102
|
+
*/
|
|
103
|
+
async getAgentInstructions(agentId) {
|
|
104
|
+
const server = this.servers.get(agentId);
|
|
105
|
+
return (server === null || server === void 0 ? void 0 : server.getSystemInstructions) ? await server.getSystemInstructions() : null;
|
|
106
|
+
}
|
|
107
|
+
getMarketplace() {
|
|
108
|
+
return Array.from(this.servers.values()).map(s => ({
|
|
109
|
+
id: s.id,
|
|
110
|
+
name: s.name,
|
|
111
|
+
description: s.description,
|
|
112
|
+
isActive: this.activeSlots.has(s.id),
|
|
113
|
+
isPermanent: s.isPermanent
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export const mcpRegistry = new McpRegistry();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { McpTool } from '../types';
|
|
2
|
+
import { McpConnector } from '../McpRegistry';
|
|
3
|
+
/**
|
|
4
|
+
* NATIVE SERVER: GroqVoiceNative
|
|
5
|
+
*
|
|
6
|
+
* Cloud Voice capability for Groq.
|
|
7
|
+
* Enables high-speed voice interaction when a Groq key is available.
|
|
8
|
+
*
|
|
9
|
+
* This is a "capability" native - it exposes UI features rather than AI tools.
|
|
10
|
+
*/
|
|
11
|
+
export declare class GroqVoiceNative implements McpConnector {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
isPermanent: boolean;
|
|
16
|
+
private apiKey?;
|
|
17
|
+
constructor(apiKey?: string);
|
|
18
|
+
getTools(): Promise<McpTool[]>;
|
|
19
|
+
executeTool(name: string, args: any): Promise<any>;
|
|
20
|
+
isVoiceReady(): boolean;
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NATIVE SERVER: GroqVoiceNative
|
|
3
|
+
*
|
|
4
|
+
* Cloud Voice capability for Groq.
|
|
5
|
+
* Enables high-speed voice interaction when a Groq key is available.
|
|
6
|
+
*
|
|
7
|
+
* This is a "capability" native - it exposes UI features rather than AI tools.
|
|
8
|
+
*/
|
|
9
|
+
export class GroqVoiceNative {
|
|
10
|
+
constructor(apiKey) {
|
|
11
|
+
this.id = "native-groq-voice";
|
|
12
|
+
this.name = "Native Cloud Voice (Groq)";
|
|
13
|
+
this.description = "High-speed cloud voice interaction via Groq. Requires a valid API key.";
|
|
14
|
+
this.isPermanent = false;
|
|
15
|
+
this.apiKey = apiKey;
|
|
16
|
+
}
|
|
17
|
+
async getTools() {
|
|
18
|
+
// Voice doesn't necessarily expose tools to the AI,
|
|
19
|
+
// it exposes capabilities to the UI.
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
async executeTool(name, args) {
|
|
23
|
+
throw new Error(`Tool ${name} not implemented in GroqVoiceNative`);
|
|
24
|
+
}
|
|
25
|
+
// Custom capability for the UI to check
|
|
26
|
+
isVoiceReady() {
|
|
27
|
+
return !!this.apiKey;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { McpTool } from '../types';
|
|
2
|
+
import { McpConnector } from '../McpRegistry';
|
|
3
|
+
/**
|
|
4
|
+
* NATIVE SERVER: LocalSyncNative
|
|
5
|
+
*
|
|
6
|
+
* Local Folder Synchronization capability.
|
|
7
|
+
* Allows the browser-based IndexedDB to be mirrored to a local file system folder.
|
|
8
|
+
*
|
|
9
|
+
* This is a "capability" native - it exposes sync features rather than AI tools.
|
|
10
|
+
*/
|
|
11
|
+
export declare class LocalSyncNative implements McpConnector {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
isPermanent: boolean;
|
|
16
|
+
getTools(): Promise<McpTool[]>;
|
|
17
|
+
executeTool(name: string, args: any): Promise<any>;
|
|
18
|
+
isSyncCapable(): boolean;
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NATIVE SERVER: LocalSyncNative
|
|
3
|
+
*
|
|
4
|
+
* Local Folder Synchronization capability.
|
|
5
|
+
* Allows the browser-based IndexedDB to be mirrored to a local file system folder.
|
|
6
|
+
*
|
|
7
|
+
* This is a "capability" native - it exposes sync features rather than AI tools.
|
|
8
|
+
*/
|
|
9
|
+
export class LocalSyncNative {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.id = "native-local-sync";
|
|
12
|
+
this.name = "Native Local Folder Sync";
|
|
13
|
+
this.description = "Mirrors your database to a local folder for backup and cross-browser sync.";
|
|
14
|
+
this.isPermanent = false;
|
|
15
|
+
}
|
|
16
|
+
async getTools() {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
async executeTool(name, args) {
|
|
20
|
+
throw new Error(`Tool ${name} not implemented in LocalSyncNative`);
|
|
21
|
+
}
|
|
22
|
+
// Capability check for Storage UI
|
|
23
|
+
isSyncCapable() {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { McpTool } from '../types';
|
|
2
|
+
import { McpConnector } from '../McpRegistry';
|
|
3
|
+
/**
|
|
4
|
+
* NATIVE SERVER: LocalVoiceNative
|
|
5
|
+
*
|
|
6
|
+
* Experimental Local Voice capability.
|
|
7
|
+
* Uses client-side ONNX models (Whisper/Kokoro) for private, offline interaction.
|
|
8
|
+
*
|
|
9
|
+
* This is a "capability" native - it exposes UI features rather than AI tools.
|
|
10
|
+
*/
|
|
11
|
+
export declare class LocalVoiceNative implements McpConnector {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
isPermanent: boolean;
|
|
16
|
+
getTools(): Promise<McpTool[]>;
|
|
17
|
+
executeTool(name: string, args: any): Promise<any>;
|
|
18
|
+
isVoiceCapable(): boolean;
|
|
19
|
+
}
|