@usejarvis/brain 0.1.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 +153 -0
- package/README.md +278 -0
- package/bin/jarvis.ts +413 -0
- package/package.json +74 -0
- package/scripts/ensure-bun.cjs +8 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +252 -0
- package/src/actions/browser/session.ts +437 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +321 -0
- package/src/actions/tools/builtin.ts +846 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +20 -0
- package/src/actions/tools/registry.ts +171 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +576 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +307 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +297 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +241 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +230 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +580 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +142 -0
- package/src/comms/voice.test.ts +152 -0
- package/src/comms/voice.ts +291 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +473 -0
- package/src/config/README.md +387 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +137 -0
- package/src/config/loader.ts +142 -0
- package/src/config/types.ts +260 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +600 -0
- package/src/daemon/api-routes.ts +2119 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1004 -0
- package/src/daemon/llm-settings.ts +316 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.ts +98 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +788 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +348 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +386 -0
- package/src/llm/gemini.ts +371 -0
- package/src/llm/index.ts +19 -0
- package/src/llm/manager.ts +153 -0
- package/src/llm/ollama.ts +307 -0
- package/src/llm/openai.ts +350 -0
- package/src/llm/provider.test.ts +231 -0
- package/src/llm/provider.ts +60 -0
- package/src/llm/test.ts +87 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +119 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +194 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +190 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +260 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/entities.ts +180 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +126 -0
- package/src/vault/retrieval.ts +227 -0
- package/src/vault/schema.ts +658 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-j75njzc1.css +1199 -0
- package/ui/dist/index-p2zh407q.js +80603 -0
- package/ui/dist/index.html +13 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example usage of J.A.R.V.I.S. Communication Layer
|
|
3
|
+
*
|
|
4
|
+
* This demonstrates how to:
|
|
5
|
+
* 1. Start the WebSocket server
|
|
6
|
+
* 2. Set up Telegram bot integration
|
|
7
|
+
* 3. Handle messages from multiple channels
|
|
8
|
+
* 4. Relay LLM streaming responses
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
WebSocketServer,
|
|
13
|
+
ChannelManager,
|
|
14
|
+
TelegramAdapter,
|
|
15
|
+
StreamRelay,
|
|
16
|
+
type WSMessage,
|
|
17
|
+
type ChannelMessage,
|
|
18
|
+
} from './index.ts';
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
console.log('🤖 Starting J.A.R.V.I.S. Communication Layer...\n');
|
|
22
|
+
|
|
23
|
+
// 1. Initialize WebSocket server
|
|
24
|
+
const wsServer = new WebSocketServer(3142);
|
|
25
|
+
|
|
26
|
+
wsServer.setHandler({
|
|
27
|
+
async onMessage(msg: WSMessage) {
|
|
28
|
+
console.log('[WS] Received message:', msg.type);
|
|
29
|
+
|
|
30
|
+
if (msg.type === 'chat') {
|
|
31
|
+
// Echo back for demo
|
|
32
|
+
return {
|
|
33
|
+
type: 'chat' as const,
|
|
34
|
+
payload: {
|
|
35
|
+
reply: `You said: ${msg.payload}`,
|
|
36
|
+
},
|
|
37
|
+
id: msg.id,
|
|
38
|
+
timestamp: Date.now(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
onConnect() {
|
|
43
|
+
console.log('[WS] Client connected');
|
|
44
|
+
},
|
|
45
|
+
onDisconnect() {
|
|
46
|
+
console.log('[WS] Client disconnected');
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
wsServer.start();
|
|
51
|
+
|
|
52
|
+
// 2. Initialize Channel Manager
|
|
53
|
+
const channelManager = new ChannelManager();
|
|
54
|
+
|
|
55
|
+
// Set up unified message handler
|
|
56
|
+
channelManager.setHandler(async (message: ChannelMessage) => {
|
|
57
|
+
console.log(`\n[${message.channel.toUpperCase()}] Message from ${message.from}:`);
|
|
58
|
+
console.log(` "${message.text}"`);
|
|
59
|
+
|
|
60
|
+
// Broadcast to WebSocket clients
|
|
61
|
+
wsServer.broadcast({
|
|
62
|
+
type: 'chat',
|
|
63
|
+
payload: {
|
|
64
|
+
channel: message.channel,
|
|
65
|
+
from: message.from,
|
|
66
|
+
text: message.text,
|
|
67
|
+
},
|
|
68
|
+
id: message.id,
|
|
69
|
+
timestamp: message.timestamp,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Simple echo response for demo
|
|
73
|
+
return `Received your message: "${message.text}"`;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// 3. Register channels (Telegram only for demo)
|
|
77
|
+
const telegramToken = process.env.TELEGRAM_BOT_TOKEN;
|
|
78
|
+
|
|
79
|
+
if (telegramToken) {
|
|
80
|
+
const telegram = new TelegramAdapter(telegramToken);
|
|
81
|
+
channelManager.register(telegram);
|
|
82
|
+
console.log('✓ Telegram adapter registered');
|
|
83
|
+
} else {
|
|
84
|
+
console.log('⚠️ TELEGRAM_BOT_TOKEN not set, skipping Telegram');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Connect all channels
|
|
88
|
+
try {
|
|
89
|
+
await channelManager.connectAll();
|
|
90
|
+
console.log('\n✓ All channels connected');
|
|
91
|
+
console.log('Status:', channelManager.getStatus());
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('Error connecting channels:', error);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 4. Demo: Stream relay (requires LLM provider setup)
|
|
97
|
+
const streamRelay = new StreamRelay(wsServer);
|
|
98
|
+
|
|
99
|
+
// Example stream simulation
|
|
100
|
+
console.log('\n📡 Streaming demo (simulated)...');
|
|
101
|
+
const simulatedStream = (async function* () {
|
|
102
|
+
yield { type: 'text' as const, text: 'Hello ' };
|
|
103
|
+
await new Promise(r => setTimeout(r, 100));
|
|
104
|
+
yield { type: 'text' as const, text: 'from ' };
|
|
105
|
+
await new Promise(r => setTimeout(r, 100));
|
|
106
|
+
yield { type: 'text' as const, text: 'J.A.R.V.I.S.!' };
|
|
107
|
+
await new Promise(r => setTimeout(r, 100));
|
|
108
|
+
yield { type: 'done' as const, response: { content: 'Hello from J.A.R.V.I.S.!', tool_calls: [], usage: { input_tokens: 0, output_tokens: 10 }, model: 'demo', finish_reason: 'stop' as const } };
|
|
109
|
+
})();
|
|
110
|
+
|
|
111
|
+
const fullResponse = await streamRelay.relayStream(simulatedStream, 'demo-123');
|
|
112
|
+
console.log('Full response:', fullResponse);
|
|
113
|
+
|
|
114
|
+
console.log('\n✓ Communication layer is running');
|
|
115
|
+
console.log(` WebSocket: ws://localhost:${wsServer.getPort()}/ws`);
|
|
116
|
+
console.log(` Health: http://localhost:${wsServer.getPort()}/health`);
|
|
117
|
+
console.log(` Channels: ${channelManager.listChannels().join(', ')}`);
|
|
118
|
+
console.log('\nPress Ctrl+C to stop...');
|
|
119
|
+
|
|
120
|
+
// Graceful shutdown
|
|
121
|
+
process.on('SIGINT', async () => {
|
|
122
|
+
console.log('\n\n🛑 Shutting down...');
|
|
123
|
+
await channelManager.disconnectAll();
|
|
124
|
+
wsServer.stop();
|
|
125
|
+
process.exit(0);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Core WebSocket and Streaming
|
|
2
|
+
export { WebSocketServer, type WSMessage, type WSClientHandler } from './websocket.ts';
|
|
3
|
+
export { StreamRelay } from './streaming.ts';
|
|
4
|
+
|
|
5
|
+
// Voice I/O
|
|
6
|
+
export {
|
|
7
|
+
OpenAIWhisperSTT,
|
|
8
|
+
GroqWhisperSTT,
|
|
9
|
+
LocalWhisperSTT,
|
|
10
|
+
EdgeTTSProvider,
|
|
11
|
+
ElevenLabsTTSProvider,
|
|
12
|
+
createSTTProvider,
|
|
13
|
+
createTTSProvider,
|
|
14
|
+
listElevenLabsVoices,
|
|
15
|
+
splitIntoSentences,
|
|
16
|
+
type STTProvider,
|
|
17
|
+
type TTSProvider,
|
|
18
|
+
} from './voice.ts';
|
|
19
|
+
|
|
20
|
+
// Channel adapters
|
|
21
|
+
export {
|
|
22
|
+
TelegramAdapter,
|
|
23
|
+
type ChannelMessage,
|
|
24
|
+
type ChannelHandler,
|
|
25
|
+
type ChannelAdapter,
|
|
26
|
+
} from './channels/telegram.ts';
|
|
27
|
+
export { WhatsAppAdapter } from './channels/whatsapp.ts';
|
|
28
|
+
export { DiscordAdapter } from './channels/discord.ts';
|
|
29
|
+
export { SignalAdapter } from './channels/signal.ts';
|
|
30
|
+
|
|
31
|
+
// Channel Manager
|
|
32
|
+
export class ChannelManager {
|
|
33
|
+
private channels: Map<string, import('./channels/telegram.ts').ChannelAdapter> = new Map();
|
|
34
|
+
private handler: import('./channels/telegram.ts').ChannelHandler | null = null;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Register a channel adapter
|
|
38
|
+
*/
|
|
39
|
+
register(adapter: import('./channels/telegram.ts').ChannelAdapter): void {
|
|
40
|
+
if (this.channels.has(adapter.name)) {
|
|
41
|
+
console.warn(`[ChannelManager] Channel "${adapter.name}" already registered, overwriting`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.channels.set(adapter.name, adapter);
|
|
45
|
+
console.log(`[ChannelManager] Registered channel: ${adapter.name}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Set the message handler for all channels
|
|
50
|
+
*/
|
|
51
|
+
setHandler(handler: import('./channels/telegram.ts').ChannelHandler): void {
|
|
52
|
+
this.handler = handler;
|
|
53
|
+
|
|
54
|
+
// Apply handler to all registered channels
|
|
55
|
+
for (const adapter of this.channels.values()) {
|
|
56
|
+
adapter.onMessage(handler);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log('[ChannelManager] Handler set for all channels');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Connect all registered channels
|
|
64
|
+
*/
|
|
65
|
+
async connectAll(): Promise<void> {
|
|
66
|
+
const results = await Promise.allSettled(
|
|
67
|
+
Array.from(this.channels.values()).map(async (adapter) => {
|
|
68
|
+
try {
|
|
69
|
+
await adapter.connect();
|
|
70
|
+
console.log(`[ChannelManager] Connected: ${adapter.name}`);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(`[ChannelManager] Failed to connect ${adapter.name}:`, error);
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const failures = results.filter((r) => r.status === 'rejected');
|
|
79
|
+
if (failures.length > 0) {
|
|
80
|
+
console.warn(`[ChannelManager] ${failures.length} channel(s) failed to connect`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const successes = results.filter((r) => r.status === 'fulfilled').length;
|
|
84
|
+
console.log(`[ChannelManager] ${successes}/${this.channels.size} channels connected`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Disconnect all registered channels
|
|
89
|
+
*/
|
|
90
|
+
async disconnectAll(): Promise<void> {
|
|
91
|
+
await Promise.allSettled(
|
|
92
|
+
Array.from(this.channels.values()).map(async (adapter) => {
|
|
93
|
+
try {
|
|
94
|
+
await adapter.disconnect();
|
|
95
|
+
console.log(`[ChannelManager] Disconnected: ${adapter.name}`);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(`[ChannelManager] Error disconnecting ${adapter.name}:`, error);
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
console.log('[ChannelManager] All channels disconnected');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get a specific channel adapter by name
|
|
107
|
+
*/
|
|
108
|
+
getChannel(name: string): import('./channels/telegram.ts').ChannelAdapter | undefined {
|
|
109
|
+
return this.channels.get(name);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* List all registered channel names
|
|
114
|
+
*/
|
|
115
|
+
listChannels(): string[] {
|
|
116
|
+
return Array.from(this.channels.keys());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get status of all channels
|
|
121
|
+
*/
|
|
122
|
+
getStatus(): Record<string, boolean> {
|
|
123
|
+
const status: Record<string, boolean> = {};
|
|
124
|
+
for (const [name, adapter] of this.channels) {
|
|
125
|
+
status[name] = adapter.isConnected();
|
|
126
|
+
}
|
|
127
|
+
return status;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { LLMStreamEvent } from '../llm/provider.ts';
|
|
2
|
+
import type { WebSocketServer, WSMessage } from './websocket.ts';
|
|
3
|
+
|
|
4
|
+
export type RelayOptions = {
|
|
5
|
+
/** Called each time a complete sentence is available during streaming. */
|
|
6
|
+
onSentence?: (sentence: string) => void;
|
|
7
|
+
/** Called when all text is done streaming. */
|
|
8
|
+
onTextDone?: () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Sentence boundary: period, exclamation, question mark, colon followed by whitespace or end
|
|
12
|
+
const SENTENCE_END_RE = /[.!?:]\s/;
|
|
13
|
+
|
|
14
|
+
export class StreamRelay {
|
|
15
|
+
private wsServer: WebSocketServer;
|
|
16
|
+
|
|
17
|
+
constructor(wsServer: WebSocketServer) {
|
|
18
|
+
this.wsServer = wsServer;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Relay LLM stream events to all connected WebSocket clients.
|
|
23
|
+
* Accumulates and returns the complete response text.
|
|
24
|
+
* Optionally fires onSentence callback as complete sentences arrive.
|
|
25
|
+
*/
|
|
26
|
+
async relayStream(
|
|
27
|
+
stream: AsyncIterable<LLMStreamEvent>,
|
|
28
|
+
requestId: string,
|
|
29
|
+
options?: RelayOptions,
|
|
30
|
+
): Promise<string> {
|
|
31
|
+
let fullText = '';
|
|
32
|
+
let sentenceBuffer = '';
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
for await (const event of stream) {
|
|
36
|
+
if (event.type === 'text') {
|
|
37
|
+
fullText += event.text;
|
|
38
|
+
|
|
39
|
+
// Sentence-level TTS callback
|
|
40
|
+
if (options?.onSentence) {
|
|
41
|
+
sentenceBuffer += event.text;
|
|
42
|
+
// Flush complete sentences from the buffer
|
|
43
|
+
let match: RegExpExecArray | null;
|
|
44
|
+
while ((match = SENTENCE_END_RE.exec(sentenceBuffer)) !== null) {
|
|
45
|
+
const end = match.index + match[0].length;
|
|
46
|
+
const sentence = sentenceBuffer.slice(0, end).trim();
|
|
47
|
+
if (sentence) {
|
|
48
|
+
options.onSentence(sentence);
|
|
49
|
+
}
|
|
50
|
+
sentenceBuffer = sentenceBuffer.slice(end);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Broadcast chunk to all connected clients
|
|
55
|
+
const message: WSMessage = {
|
|
56
|
+
type: 'stream',
|
|
57
|
+
payload: {
|
|
58
|
+
text: event.text,
|
|
59
|
+
requestId,
|
|
60
|
+
accumulated: fullText,
|
|
61
|
+
},
|
|
62
|
+
id: requestId,
|
|
63
|
+
timestamp: Date.now(),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
this.wsServer.broadcast(message);
|
|
67
|
+
} else if (event.type === 'tool_call') {
|
|
68
|
+
// Broadcast tool call notification to clients
|
|
69
|
+
const toolMessage: WSMessage = {
|
|
70
|
+
type: 'stream',
|
|
71
|
+
payload: {
|
|
72
|
+
tool_call: {
|
|
73
|
+
name: event.tool_call.name,
|
|
74
|
+
arguments: event.tool_call.arguments,
|
|
75
|
+
},
|
|
76
|
+
requestId,
|
|
77
|
+
},
|
|
78
|
+
id: requestId,
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
this.wsServer.broadcast(toolMessage);
|
|
83
|
+
} else if (event.type === 'error') {
|
|
84
|
+
console.error('[StreamRelay] Stream error:', event.error);
|
|
85
|
+
|
|
86
|
+
const errorMessage: WSMessage = {
|
|
87
|
+
type: 'error',
|
|
88
|
+
payload: {
|
|
89
|
+
message: event.error,
|
|
90
|
+
requestId,
|
|
91
|
+
},
|
|
92
|
+
id: requestId,
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
this.wsServer.broadcast(errorMessage);
|
|
97
|
+
} else if (event.type === 'done') {
|
|
98
|
+
// Flush remaining sentence buffer
|
|
99
|
+
if (options?.onSentence && sentenceBuffer.trim()) {
|
|
100
|
+
options.onSentence(sentenceBuffer.trim());
|
|
101
|
+
sentenceBuffer = '';
|
|
102
|
+
}
|
|
103
|
+
options?.onTextDone?.();
|
|
104
|
+
|
|
105
|
+
console.log('[StreamRelay] Stream complete for request:', requestId);
|
|
106
|
+
|
|
107
|
+
const doneMessage: WSMessage = {
|
|
108
|
+
type: 'status',
|
|
109
|
+
payload: {
|
|
110
|
+
status: 'done',
|
|
111
|
+
requestId,
|
|
112
|
+
fullText,
|
|
113
|
+
usage: event.response.usage,
|
|
114
|
+
},
|
|
115
|
+
id: requestId,
|
|
116
|
+
timestamp: Date.now(),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
this.wsServer.broadcast(doneMessage);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('[StreamRelay] Error relaying stream:', error);
|
|
124
|
+
|
|
125
|
+
const errorMessage: WSMessage = {
|
|
126
|
+
type: 'error',
|
|
127
|
+
payload: {
|
|
128
|
+
message: error instanceof Error ? error.message : 'Stream relay error',
|
|
129
|
+
requestId,
|
|
130
|
+
},
|
|
131
|
+
id: requestId,
|
|
132
|
+
timestamp: Date.now(),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
this.wsServer.broadcast(errorMessage);
|
|
136
|
+
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return fullText;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { test, expect, describe } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
createSTTProvider,
|
|
4
|
+
createTTSProvider,
|
|
5
|
+
OpenAIWhisperSTT,
|
|
6
|
+
GroqWhisperSTT,
|
|
7
|
+
LocalWhisperSTT,
|
|
8
|
+
EdgeTTSProvider,
|
|
9
|
+
splitIntoSentences,
|
|
10
|
+
} from './voice.ts';
|
|
11
|
+
import type { STTConfig, TTSConfig } from '../config/types.ts';
|
|
12
|
+
|
|
13
|
+
describe('createSTTProvider factory', () => {
|
|
14
|
+
test('returns OpenAIWhisperSTT when provider=openai and key present', () => {
|
|
15
|
+
const config: STTConfig = {
|
|
16
|
+
provider: 'openai',
|
|
17
|
+
openai: { api_key: 'test-openai-key-not-real' },
|
|
18
|
+
};
|
|
19
|
+
const provider = createSTTProvider(config);
|
|
20
|
+
expect(provider).toBeInstanceOf(OpenAIWhisperSTT);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('returns null when provider=openai and no key', () => {
|
|
24
|
+
const config: STTConfig = { provider: 'openai' };
|
|
25
|
+
const provider = createSTTProvider(config);
|
|
26
|
+
expect(provider).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('returns GroqWhisperSTT when provider=groq and key present', () => {
|
|
30
|
+
const config: STTConfig = {
|
|
31
|
+
provider: 'groq',
|
|
32
|
+
groq: { api_key: 'gtest-openai-key-not-real' },
|
|
33
|
+
};
|
|
34
|
+
const provider = createSTTProvider(config);
|
|
35
|
+
expect(provider).toBeInstanceOf(GroqWhisperSTT);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('returns null when provider=groq and no key', () => {
|
|
39
|
+
const config: STTConfig = { provider: 'groq' };
|
|
40
|
+
const provider = createSTTProvider(config);
|
|
41
|
+
expect(provider).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('returns LocalWhisperSTT when provider=local (no key needed)', () => {
|
|
45
|
+
const config: STTConfig = { provider: 'local' };
|
|
46
|
+
const provider = createSTTProvider(config);
|
|
47
|
+
expect(provider).toBeInstanceOf(LocalWhisperSTT);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('returns LocalWhisperSTT with custom endpoint', () => {
|
|
51
|
+
const config: STTConfig = {
|
|
52
|
+
provider: 'local',
|
|
53
|
+
local: { endpoint: 'http://my-server:9000' },
|
|
54
|
+
};
|
|
55
|
+
const provider = createSTTProvider(config);
|
|
56
|
+
expect(provider).toBeInstanceOf(LocalWhisperSTT);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('returns null for unknown provider', () => {
|
|
60
|
+
const config = { provider: 'unknown' } as any;
|
|
61
|
+
const provider = createSTTProvider(config);
|
|
62
|
+
expect(provider).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('returns OpenAI with custom model', () => {
|
|
66
|
+
const config: STTConfig = {
|
|
67
|
+
provider: 'openai',
|
|
68
|
+
openai: { api_key: 'test-key-not-real', model: 'whisper-large-v3' },
|
|
69
|
+
};
|
|
70
|
+
const provider = createSTTProvider(config);
|
|
71
|
+
expect(provider).toBeInstanceOf(OpenAIWhisperSTT);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('createTTSProvider factory', () => {
|
|
76
|
+
test('returns null when tts disabled', () => {
|
|
77
|
+
const config: TTSConfig = { enabled: false };
|
|
78
|
+
expect(createTTSProvider(config)).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('returns EdgeTTSProvider when enabled', () => {
|
|
82
|
+
const config: TTSConfig = { enabled: true };
|
|
83
|
+
const provider = createTTSProvider(config);
|
|
84
|
+
expect(provider).toBeInstanceOf(EdgeTTSProvider);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('passes voice config to provider', () => {
|
|
88
|
+
const config: TTSConfig = { enabled: true, voice: 'en-GB-SoniaNeural' };
|
|
89
|
+
const provider = createTTSProvider(config);
|
|
90
|
+
expect(provider).toBeInstanceOf(EdgeTTSProvider);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('passes rate and volume config', () => {
|
|
94
|
+
const config: TTSConfig = { enabled: true, rate: '+20%', volume: '-10%' };
|
|
95
|
+
const provider = createTTSProvider(config);
|
|
96
|
+
expect(provider).not.toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('EdgeTTSProvider', () => {
|
|
101
|
+
test('implements TTSProvider interface', () => {
|
|
102
|
+
const provider = new EdgeTTSProvider();
|
|
103
|
+
expect(typeof provider.synthesize).toBe('function');
|
|
104
|
+
expect(typeof provider.synthesizeStream).toBe('function');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('constructor accepts custom voice/rate/volume', () => {
|
|
108
|
+
const provider = new EdgeTTSProvider('en-GB-SoniaNeural', '+10%', '-5%');
|
|
109
|
+
expect(provider).toBeInstanceOf(EdgeTTSProvider);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('splitIntoSentences', () => {
|
|
114
|
+
test('splits on period + capital letter', () => {
|
|
115
|
+
const result = splitIntoSentences('Hello there. World is great. This works.');
|
|
116
|
+
expect(result.length).toBe(3);
|
|
117
|
+
expect(result[0]).toBe('Hello there.');
|
|
118
|
+
expect(result[1]).toBe('World is great.');
|
|
119
|
+
expect(result[2]).toBe('This works.');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('splits on exclamation and question marks', () => {
|
|
123
|
+
const result = splitIntoSentences('Wait! Are you sure? Yes I am.');
|
|
124
|
+
expect(result.length).toBe(3);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('handles single sentence', () => {
|
|
128
|
+
const result = splitIntoSentences('Just one sentence.');
|
|
129
|
+
expect(result).toEqual(['Just one sentence.']);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('handles empty string', () => {
|
|
133
|
+
const result = splitIntoSentences('');
|
|
134
|
+
expect(result).toEqual(['']);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('collapses code blocks', () => {
|
|
138
|
+
const result = splitIntoSentences('Here is code:\n```\nconst x = 1;\n```\nDone.');
|
|
139
|
+
// Should not split inside code block
|
|
140
|
+
expect(result.length).toBeLessThanOrEqual(3);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('splits on double newlines (paragraph breaks)', () => {
|
|
144
|
+
const result = splitIntoSentences('First paragraph\n\nSecond paragraph');
|
|
145
|
+
expect(result.length).toBe(2);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('handles text with no sentence-ending punctuation', () => {
|
|
149
|
+
const result = splitIntoSentences('just some words without punctuation');
|
|
150
|
+
expect(result).toEqual(['just some words without punctuation']);
|
|
151
|
+
});
|
|
152
|
+
});
|