@shykaruu/jarvis-brain 0.4.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 +428 -0
- package/bin/jarvis.ts +449 -0
- package/package.json +79 -0
- package/roles/activity-observer.yaml +60 -0
- package/roles/ceo-founder.yaml +144 -0
- package/roles/chief-of-staff.yaml +158 -0
- package/roles/dev-lead.yaml +182 -0
- package/roles/executive-assistant.yaml +77 -0
- package/roles/marketing-director.yaml +168 -0
- package/roles/personal-assistant.yaml +266 -0
- package/roles/research-specialist.yaml +60 -0
- package/roles/specialists/content-writer.yaml +53 -0
- package/roles/specialists/customer-support.yaml +57 -0
- package/roles/specialists/data-analyst.yaml +57 -0
- package/roles/specialists/financial-analyst.yaml +56 -0
- package/roles/specialists/hr-specialist.yaml +55 -0
- package/roles/specialists/legal-advisor.yaml +58 -0
- package/roles/specialists/marketing-strategist.yaml +56 -0
- package/roles/specialists/project-coordinator.yaml +55 -0
- package/roles/specialists/research-analyst.yaml +58 -0
- package/roles/specialists/software-engineer.yaml +57 -0
- package/roles/specialists/system-administrator.yaml +57 -0
- package/roles/system-admin.yaml +76 -0
- package/scripts/ensure-bun.cjs +16 -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 +261 -0
- package/src/actions/browser/session.ts +506 -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 +363 -0
- package/src/actions/tools/builtin.ts +950 -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/documents.ts +169 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +31 -0
- package/src/actions/tools/registry.ts +173 -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 +592 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +309 -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 +301 -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 +417 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +238 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +827 -0
- package/src/cli/uninstall.test.ts +37 -0
- package/src/cli/uninstall.ts +202 -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/dashboard-auth.ts +75 -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 +149 -0
- package/src/comms/voice.test.ts +504 -0
- package/src/comms/voice.ts +341 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +669 -0
- package/src/config/README.md +389 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +183 -0
- package/src/config/loader.ts +148 -0
- package/src/config/types.ts +293 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +667 -0
- package/src/daemon/api-routes.ts +3067 -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/dashboard-auth.test.ts +170 -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/flock.c +7 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1070 -0
- package/src/daemon/llm-settings.test.ts +78 -0
- package/src/daemon/llm-settings.ts +450 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.test.ts +283 -0
- package/src/daemon/pid.ts +224 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +926 -0
- package/src/global.d.ts +4 -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 +407 -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 +400 -0
- package/src/llm/gemini.ts +380 -0
- package/src/llm/groq.ts +406 -0
- package/src/llm/history.ts +147 -0
- package/src/llm/index.ts +21 -0
- package/src/llm/manager.ts +226 -0
- package/src/llm/ollama.ts +316 -0
- package/src/llm/openai.ts +411 -0
- package/src/llm/openrouter.ts +390 -0
- package/src/llm/provider.test.ts +487 -0
- package/src/llm/provider.ts +61 -0
- package/src/llm/test.ts +88 -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 +120 -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 +218 -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 +195 -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/sites/builder-tools.ts +215 -0
- package/src/sites/dev-server-manager.ts +286 -0
- package/src/sites/fixtures/security-test-site/.jarvis-project.json +6 -0
- package/src/sites/fixtures/security-test-site/Makefile +15 -0
- package/src/sites/fixtures/security-test-site/README.md +18 -0
- package/src/sites/fixtures/security-test-site/index.html +12 -0
- package/src/sites/fixtures/security-test-site/index.ts +16 -0
- package/src/sites/fixtures/security-test-site/package.json +13 -0
- package/src/sites/fixtures/security-test-site/src/app.tsx +780 -0
- package/src/sites/fixtures/security-test-site/tsconfig.json +10 -0
- package/src/sites/git-manager.ts +240 -0
- package/src/sites/github-manager.ts +355 -0
- package/src/sites/index.ts +25 -0
- package/src/sites/project-manager.ts +389 -0
- package/src/sites/proxy.ts +133 -0
- package/src/sites/service.ts +136 -0
- package/src/sites/templates.ts +169 -0
- package/src/sites/types.ts +89 -0
- package/src/user/profile-followup.test.ts +84 -0
- package/src/user/profile-followup.ts +185 -0
- package/src/user/profile.ts +224 -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 +270 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/dashboard-sessions.ts +44 -0
- package/src/vault/documents.ts +130 -0
- package/src/vault/entities.ts +185 -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 +139 -0
- package/src/vault/retrieval.ts +258 -0
- package/src/vault/schema.ts +709 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/user-profile.test.ts +113 -0
- package/src/vault/user-profile.ts +176 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/webapp-template-seeds.ts +116 -0
- package/src/vault/webapp-templates.ts +244 -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-3gr23jt9.js +112614 -0
- package/ui/dist/index-9vmj8127.css +14239 -0
- package/ui/dist/index-hy9pc1gm.js +112873 -0
- package/ui/dist/index-j2ep5d1w.js +112374 -0
- package/ui/dist/index-jt00vjqs.js +112858 -0
- package/ui/dist/index-k9ymx5qb.js +112374 -0
- package/ui/dist/index.html +16 -0
- package/ui/public/audio/pcm-capture-processor.js +11 -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,173 @@
|
|
|
1
|
+
import { test, expect, describe } from 'bun:test';
|
|
2
|
+
import { ChannelManager } from './index.ts';
|
|
3
|
+
import type { ChannelAdapter, ChannelMessage } from './channels/telegram.ts';
|
|
4
|
+
import { splitMessage } from './channels/discord.ts';
|
|
5
|
+
|
|
6
|
+
// Mock channel adapter for testing
|
|
7
|
+
class MockChannel implements ChannelAdapter {
|
|
8
|
+
name = 'mock';
|
|
9
|
+
private _connected = false;
|
|
10
|
+
private _handler: ((msg: ChannelMessage) => Promise<string>) | null = null;
|
|
11
|
+
|
|
12
|
+
async connect(): Promise<void> {
|
|
13
|
+
this._connected = true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async disconnect(): Promise<void> {
|
|
17
|
+
this._connected = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async sendMessage(to: string, text: string): Promise<void> {
|
|
21
|
+
// Mock implementation
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
onMessage(handler: (msg: ChannelMessage) => Promise<string>): void {
|
|
25
|
+
this._handler = handler;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
isConnected(): boolean {
|
|
29
|
+
return this._connected;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Test helper to simulate receiving a message
|
|
33
|
+
async simulateMessage(text: string): Promise<string | null> {
|
|
34
|
+
if (!this._handler) return null;
|
|
35
|
+
|
|
36
|
+
const msg: ChannelMessage = {
|
|
37
|
+
id: '1',
|
|
38
|
+
channel: 'mock',
|
|
39
|
+
from: 'testuser',
|
|
40
|
+
text,
|
|
41
|
+
timestamp: Date.now(),
|
|
42
|
+
metadata: {},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return this._handler(msg);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test('ChannelManager - register channel', () => {
|
|
50
|
+
const manager = new ChannelManager();
|
|
51
|
+
const channel = new MockChannel();
|
|
52
|
+
|
|
53
|
+
manager.register(channel);
|
|
54
|
+
expect(manager.listChannels()).toEqual(['mock']);
|
|
55
|
+
expect(manager.getChannel('mock')).toBe(channel);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('ChannelManager - set handler', async () => {
|
|
59
|
+
const manager = new ChannelManager();
|
|
60
|
+
const channel = new MockChannel();
|
|
61
|
+
|
|
62
|
+
manager.register(channel);
|
|
63
|
+
|
|
64
|
+
let handlerCalled = false;
|
|
65
|
+
manager.setHandler(async (msg) => {
|
|
66
|
+
handlerCalled = true;
|
|
67
|
+
return `Echo: ${msg.text}`;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const response = await channel.simulateMessage('test');
|
|
71
|
+
expect(handlerCalled).toBe(true);
|
|
72
|
+
expect(response).toBe('Echo: test');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('ChannelManager - connect all channels', async () => {
|
|
76
|
+
const manager = new ChannelManager();
|
|
77
|
+
const channel1 = new MockChannel();
|
|
78
|
+
const channel2 = new MockChannel();
|
|
79
|
+
channel2.name = 'mock2';
|
|
80
|
+
|
|
81
|
+
manager.register(channel1);
|
|
82
|
+
manager.register(channel2);
|
|
83
|
+
|
|
84
|
+
await manager.connectAll();
|
|
85
|
+
|
|
86
|
+
const status = manager.getStatus();
|
|
87
|
+
expect(status.mock).toBe(true);
|
|
88
|
+
expect(status.mock2).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('ChannelManager - disconnect all channels', async () => {
|
|
92
|
+
const manager = new ChannelManager();
|
|
93
|
+
const channel = new MockChannel();
|
|
94
|
+
|
|
95
|
+
manager.register(channel);
|
|
96
|
+
await manager.connectAll();
|
|
97
|
+
|
|
98
|
+
expect(manager.getStatus().mock).toBe(true);
|
|
99
|
+
|
|
100
|
+
await manager.disconnectAll();
|
|
101
|
+
expect(manager.getStatus().mock).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('ChannelManager - list channels', () => {
|
|
105
|
+
const manager = new ChannelManager();
|
|
106
|
+
|
|
107
|
+
expect(manager.listChannels()).toEqual([]);
|
|
108
|
+
|
|
109
|
+
manager.register(new MockChannel());
|
|
110
|
+
expect(manager.listChannels()).toEqual(['mock']);
|
|
111
|
+
|
|
112
|
+
const channel2 = new MockChannel();
|
|
113
|
+
channel2.name = 'mock2';
|
|
114
|
+
manager.register(channel2);
|
|
115
|
+
|
|
116
|
+
expect(manager.listChannels()).toContain('mock');
|
|
117
|
+
expect(manager.listChannels()).toContain('mock2');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Discord splitMessage tests
|
|
121
|
+
describe('Discord splitMessage', () => {
|
|
122
|
+
test('returns single chunk for short message', () => {
|
|
123
|
+
const result = splitMessage('Hello world', 2000);
|
|
124
|
+
expect(result).toEqual(['Hello world']);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('returns single chunk for message exactly at limit', () => {
|
|
128
|
+
const text = 'a'.repeat(2000);
|
|
129
|
+
const result = splitMessage(text, 2000);
|
|
130
|
+
expect(result).toEqual([text]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('splits long message at newline boundary', () => {
|
|
134
|
+
const line = 'This is a line of text\n';
|
|
135
|
+
// Create text that exceeds 100 chars, with newlines
|
|
136
|
+
const text = line.repeat(10); // 220 chars
|
|
137
|
+
const result = splitMessage(text, 100);
|
|
138
|
+
expect(result.length).toBeGreaterThan(1);
|
|
139
|
+
// Each chunk should be <= 100 chars
|
|
140
|
+
for (const chunk of result) {
|
|
141
|
+
expect(chunk.length).toBeLessThanOrEqual(100);
|
|
142
|
+
}
|
|
143
|
+
// Recombined should equal original (modulo trimmed whitespace)
|
|
144
|
+
const recombined = result.join('');
|
|
145
|
+
// The original text is preserved (whitespace trimming might remove leading spaces between chunks)
|
|
146
|
+
expect(recombined.replace(/\s+/g, '')).toBe(text.replace(/\s+/g, ''));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('hard splits when no good break point', () => {
|
|
150
|
+
// No spaces or newlines — must hard split
|
|
151
|
+
const text = 'a'.repeat(5000);
|
|
152
|
+
const result = splitMessage(text, 2000);
|
|
153
|
+
expect(result.length).toBe(3); // 2000 + 2000 + 1000
|
|
154
|
+
expect(result[0]!.length).toBe(2000);
|
|
155
|
+
expect(result[1]!.length).toBe(2000);
|
|
156
|
+
expect(result[2]!.length).toBe(1000);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('handles empty string', () => {
|
|
160
|
+
const result = splitMessage('', 2000);
|
|
161
|
+
expect(result).toEqual(['']);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('splits at space when no newlines available', () => {
|
|
165
|
+
// Words separated by spaces, no newlines
|
|
166
|
+
const words = Array(200).fill('word').join(' '); // "word word word..." ~1000 chars
|
|
167
|
+
const result = splitMessage(words, 100);
|
|
168
|
+
expect(result.length).toBeGreaterThan(1);
|
|
169
|
+
for (const chunk of result) {
|
|
170
|
+
expect(chunk.length).toBeLessThanOrEqual(100);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import type { JarvisConfig } from '../config/types.ts';
|
|
3
|
+
import {
|
|
4
|
+
createDashboardSession,
|
|
5
|
+
deleteDashboardSession,
|
|
6
|
+
validateDashboardSession,
|
|
7
|
+
type DashboardSession,
|
|
8
|
+
} from '../vault/dashboard-sessions.ts';
|
|
9
|
+
|
|
10
|
+
export const DASHBOARD_SESSION_COOKIE = 'jarvis_dashboard_session';
|
|
11
|
+
export const DASHBOARD_SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
12
|
+
export const DASHBOARD_SESSION_MAX_AGE_SECONDS = Math.floor(DASHBOARD_SESSION_MAX_AGE_MS / 1000);
|
|
13
|
+
|
|
14
|
+
export function getCookie(req: Request, name: string): string | null {
|
|
15
|
+
const cookies = req.headers.get('Cookie');
|
|
16
|
+
if (!cookies) return null;
|
|
17
|
+
const match = cookies.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
|
|
18
|
+
return match ? decodeURIComponent(match[1]!) : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function safeCompare(a: string, b: string): boolean {
|
|
22
|
+
if (a.length !== b.length) return false;
|
|
23
|
+
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isDashboardPasswordEnabled(config: Pick<JarvisConfig, 'dashboard'> | { dashboard?: { password_hash?: string } } | null | undefined): boolean {
|
|
27
|
+
return Boolean(config?.dashboard?.password_hash?.trim());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function shouldUseSecureCookies(req: Request): boolean {
|
|
31
|
+
const forwardedProto = req.headers.get('x-forwarded-proto');
|
|
32
|
+
if (forwardedProto) {
|
|
33
|
+
return forwardedProto.split(',')[0]!.trim().toLowerCase() === 'https';
|
|
34
|
+
}
|
|
35
|
+
return new URL(req.url).protocol === 'https:';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildCookieAttributes(req: Request, expiresAt?: number): string {
|
|
39
|
+
const parts = ['Path=/', 'HttpOnly', 'SameSite=Lax'];
|
|
40
|
+
if (shouldUseSecureCookies(req)) parts.push('Secure');
|
|
41
|
+
if (expiresAt) {
|
|
42
|
+
parts.push(`Max-Age=${DASHBOARD_SESSION_MAX_AGE_SECONDS}`);
|
|
43
|
+
parts.push(`Expires=${new Date(expiresAt).toUTCString()}`);
|
|
44
|
+
}
|
|
45
|
+
return parts.join('; ');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildDashboardSessionCookie(req: Request, sessionId: string, expiresAt: number): string {
|
|
49
|
+
return `${DASHBOARD_SESSION_COOKIE}=${encodeURIComponent(sessionId)}; ${buildCookieAttributes(req, expiresAt)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildClearedDashboardSessionCookie(req: Request): string {
|
|
53
|
+
const epoch = new Date(0).toUTCString();
|
|
54
|
+
const parts = ['Path=/', 'HttpOnly', 'SameSite=Lax', 'Max-Age=0', `Expires=${epoch}`];
|
|
55
|
+
if (shouldUseSecureCookies(req)) parts.push('Secure');
|
|
56
|
+
return `${DASHBOARD_SESSION_COOKIE}=; ${parts.join('; ')}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createAuthenticatedDashboardSession(): DashboardSession {
|
|
60
|
+
const sessionId = crypto.randomUUID();
|
|
61
|
+
const expiresAt = Date.now() + DASHBOARD_SESSION_MAX_AGE_MS;
|
|
62
|
+
return createDashboardSession(sessionId, expiresAt);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getDashboardSessionFromRequest(req: Request): DashboardSession | null {
|
|
66
|
+
const sessionId = getCookie(req, DASHBOARD_SESSION_COOKIE);
|
|
67
|
+
if (!sessionId) return null;
|
|
68
|
+
return validateDashboardSession(sessionId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function revokeDashboardSessionFromRequest(req: Request): void {
|
|
72
|
+
const sessionId = getCookie(req, DASHBOARD_SESSION_COOKIE);
|
|
73
|
+
if (!sessionId) return;
|
|
74
|
+
deleteDashboardSession(sessionId);
|
|
75
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desktop Notification Sender
|
|
3
|
+
*
|
|
4
|
+
* Sends native desktop notifications.
|
|
5
|
+
* Tries in order:
|
|
6
|
+
* 1. notify-send (Linux/WSLg)
|
|
7
|
+
* 2. PowerShell toast (WSL2 → Windows)
|
|
8
|
+
* Gracefully degrades if neither is available.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
type NotifyMethod = 'notify-send' | 'powershell' | null;
|
|
12
|
+
|
|
13
|
+
let method: NotifyMethod | undefined;
|
|
14
|
+
|
|
15
|
+
function detectMethod(): NotifyMethod {
|
|
16
|
+
if (method !== undefined) return method;
|
|
17
|
+
|
|
18
|
+
// Try notify-send first (native Linux/WSLg)
|
|
19
|
+
try {
|
|
20
|
+
const result = Bun.spawnSync(['which', 'notify-send']);
|
|
21
|
+
if (result.exitCode === 0) {
|
|
22
|
+
method = 'notify-send';
|
|
23
|
+
console.log('[DesktopNotify] Using notify-send');
|
|
24
|
+
return method;
|
|
25
|
+
}
|
|
26
|
+
} catch { /* continue */ }
|
|
27
|
+
|
|
28
|
+
// Try PowerShell (WSL2 → Windows toast)
|
|
29
|
+
try {
|
|
30
|
+
const result = Bun.spawnSync(['which', 'powershell.exe']);
|
|
31
|
+
if (result.exitCode === 0) {
|
|
32
|
+
method = 'powershell';
|
|
33
|
+
console.log('[DesktopNotify] Using PowerShell toasts');
|
|
34
|
+
return method;
|
|
35
|
+
}
|
|
36
|
+
} catch { /* continue */ }
|
|
37
|
+
|
|
38
|
+
method = null;
|
|
39
|
+
console.log('[DesktopNotify] No notification method available');
|
|
40
|
+
return method;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Send a native desktop notification.
|
|
45
|
+
* Returns true if sent, false if unavailable.
|
|
46
|
+
*/
|
|
47
|
+
export function sendDesktopNotification(
|
|
48
|
+
title: string,
|
|
49
|
+
body: string,
|
|
50
|
+
options?: {
|
|
51
|
+
urgency?: 'low' | 'normal' | 'critical';
|
|
52
|
+
expireMs?: number;
|
|
53
|
+
}
|
|
54
|
+
): boolean {
|
|
55
|
+
const m = detectMethod();
|
|
56
|
+
if (!m) return false;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
if (m === 'notify-send') {
|
|
60
|
+
return sendViaNotifySend(title, body, options);
|
|
61
|
+
} else {
|
|
62
|
+
return sendViaPowerShell(title, body);
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sendViaNotifySend(
|
|
70
|
+
title: string,
|
|
71
|
+
body: string,
|
|
72
|
+
options?: { urgency?: string; expireMs?: number }
|
|
73
|
+
): boolean {
|
|
74
|
+
const urgency = options?.urgency ?? 'normal';
|
|
75
|
+
const expireMs = options?.expireMs ?? (urgency === 'critical' ? 10000 : 5000);
|
|
76
|
+
|
|
77
|
+
Bun.spawn([
|
|
78
|
+
'notify-send',
|
|
79
|
+
`--urgency=${urgency}`,
|
|
80
|
+
`--expire-time=${expireMs}`,
|
|
81
|
+
'--app-name=JARVIS',
|
|
82
|
+
title,
|
|
83
|
+
body,
|
|
84
|
+
], { stdout: 'ignore', stderr: 'ignore' });
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sendViaPowerShell(title: string, body: string): boolean {
|
|
89
|
+
// Escape single quotes for PowerShell
|
|
90
|
+
const safeTitle = title.replace(/'/g, "''").slice(0, 100);
|
|
91
|
+
const safeBody = body.replace(/'/g, "''").slice(0, 200);
|
|
92
|
+
|
|
93
|
+
const script = `
|
|
94
|
+
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
|
95
|
+
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime] | Out-Null
|
|
96
|
+
$xml = [Windows.Data.Xml.Dom.XmlDocument]::new()
|
|
97
|
+
$xml.LoadXml('<toast><visual><binding template="ToastText02"><text id="1">${safeTitle}</text><text id="2">${safeBody}</text></binding></visual></toast>')
|
|
98
|
+
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
|
99
|
+
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('JARVIS').Show($toast)
|
|
100
|
+
`.trim();
|
|
101
|
+
|
|
102
|
+
Bun.spawn(['powershell.exe', '-NoProfile', '-NonInteractive', '-Command', script], {
|
|
103
|
+
stdout: 'ignore',
|
|
104
|
+
stderr: 'ignore',
|
|
105
|
+
});
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if desktop notifications are available.
|
|
111
|
+
*/
|
|
112
|
+
export function isDesktopNotifyAvailable(): boolean {
|
|
113
|
+
return detectMethod() !== null;
|
|
114
|
+
}
|
|
@@ -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
|
+
}
|