@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,316 @@
|
|
|
1
|
+
import type { STTProvider } from '../voice.ts';
|
|
2
|
+
|
|
3
|
+
export type ChannelMessage = {
|
|
4
|
+
id: string;
|
|
5
|
+
channel: string;
|
|
6
|
+
from: string;
|
|
7
|
+
text: string;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
metadata: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ChannelHandler = (message: ChannelMessage) => Promise<string>;
|
|
13
|
+
|
|
14
|
+
export interface ChannelAdapter {
|
|
15
|
+
name: string;
|
|
16
|
+
connect(): Promise<void>;
|
|
17
|
+
disconnect(): Promise<void>;
|
|
18
|
+
sendMessage(to: string, text: string): Promise<void>;
|
|
19
|
+
onMessage(handler: ChannelHandler): void;
|
|
20
|
+
isConnected(): boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type TelegramUpdate = {
|
|
24
|
+
update_id: number;
|
|
25
|
+
message?: {
|
|
26
|
+
message_id: number;
|
|
27
|
+
from: {
|
|
28
|
+
id: number;
|
|
29
|
+
first_name: string;
|
|
30
|
+
last_name?: string;
|
|
31
|
+
username?: string;
|
|
32
|
+
};
|
|
33
|
+
chat: {
|
|
34
|
+
id: number;
|
|
35
|
+
type: string;
|
|
36
|
+
};
|
|
37
|
+
date: number;
|
|
38
|
+
text?: string;
|
|
39
|
+
voice?: {
|
|
40
|
+
duration: number;
|
|
41
|
+
mime_type: string;
|
|
42
|
+
file_id: string;
|
|
43
|
+
file_unique_id: string;
|
|
44
|
+
file_size?: number;
|
|
45
|
+
};
|
|
46
|
+
audio?: {
|
|
47
|
+
duration: number;
|
|
48
|
+
mime_type: string;
|
|
49
|
+
file_id: string;
|
|
50
|
+
file_unique_id: string;
|
|
51
|
+
file_size?: number;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type TelegramGetUpdatesResponse = {
|
|
57
|
+
ok: boolean;
|
|
58
|
+
result: TelegramUpdate[];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export class TelegramAdapter implements ChannelAdapter {
|
|
62
|
+
name = 'telegram';
|
|
63
|
+
private token: string;
|
|
64
|
+
private handler: ChannelHandler | null = null;
|
|
65
|
+
private polling: boolean = false;
|
|
66
|
+
private offset: number = 0;
|
|
67
|
+
private baseUrl: string;
|
|
68
|
+
private pollingInterval: number = 1000;
|
|
69
|
+
private sttProvider: STTProvider | null = null;
|
|
70
|
+
private allowedUsers: number[];
|
|
71
|
+
|
|
72
|
+
constructor(token: string, opts?: { sttProvider?: STTProvider; allowedUsers?: number[] }) {
|
|
73
|
+
this.token = token;
|
|
74
|
+
this.baseUrl = `https://api.telegram.org/bot${token}`;
|
|
75
|
+
this.sttProvider = opts?.sttProvider ?? null;
|
|
76
|
+
this.allowedUsers = opts?.allowedUsers ?? [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setSTTProvider(provider: STTProvider): void {
|
|
80
|
+
this.sttProvider = provider;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async connect(): Promise<void> {
|
|
84
|
+
if (this.polling) {
|
|
85
|
+
console.warn('[TelegramAdapter] Already connected');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Verify bot token by calling getMe
|
|
90
|
+
try {
|
|
91
|
+
const response = await fetch(`${this.baseUrl}/getMe`);
|
|
92
|
+
const data = await response.json() as any;
|
|
93
|
+
|
|
94
|
+
if (!data.ok) {
|
|
95
|
+
throw new Error(`Invalid bot token: ${data.description}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log('[TelegramAdapter] Connected as:', data.result.username);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Failed to connect to Telegram: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.polling = true;
|
|
106
|
+
this.startPolling();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async disconnect(): Promise<void> {
|
|
110
|
+
this.polling = false;
|
|
111
|
+
console.log('[TelegramAdapter] Disconnected');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async sendMessage(chatId: string, text: string): Promise<void> {
|
|
115
|
+
// Telegram has a 4096 char limit per message
|
|
116
|
+
const chunks = splitText(text, 4096);
|
|
117
|
+
for (const chunk of chunks) {
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch(`${this.baseUrl}/sendMessage`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'Content-Type': 'application/json' },
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
chat_id: chatId,
|
|
124
|
+
text: chunk,
|
|
125
|
+
parse_mode: 'Markdown',
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const data = await response.json() as any;
|
|
130
|
+
|
|
131
|
+
if (!data.ok) {
|
|
132
|
+
// Retry without Markdown if parsing failed
|
|
133
|
+
if (data.description?.includes('parse')) {
|
|
134
|
+
await fetch(`${this.baseUrl}/sendMessage`, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: { 'Content-Type': 'application/json' },
|
|
137
|
+
body: JSON.stringify({ chat_id: chatId, text: chunk }),
|
|
138
|
+
});
|
|
139
|
+
} else {
|
|
140
|
+
throw new Error(`Telegram API error: ${data.description}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('[TelegramAdapter] Error sending message:', error);
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
onMessage(handler: ChannelHandler): void {
|
|
151
|
+
this.handler = handler;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
isConnected(): boolean {
|
|
155
|
+
return this.polling;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async startPolling(): Promise<void> {
|
|
159
|
+
console.log('[TelegramAdapter] Starting polling...');
|
|
160
|
+
|
|
161
|
+
while (this.polling) {
|
|
162
|
+
try {
|
|
163
|
+
const updates = await this.getUpdates();
|
|
164
|
+
|
|
165
|
+
for (const update of updates) {
|
|
166
|
+
await this.processUpdate(update);
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error('[TelegramAdapter] Polling error:', error);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await new Promise(resolve => setTimeout(resolve, this.pollingInterval));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log('[TelegramAdapter] Polling stopped');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async getUpdates(): Promise<TelegramUpdate[]> {
|
|
179
|
+
const response = await fetch(`${this.baseUrl}/getUpdates`, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
offset: this.offset,
|
|
184
|
+
timeout: 30,
|
|
185
|
+
allowed_updates: ['message'],
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const data: TelegramGetUpdatesResponse = await response.json() as TelegramGetUpdatesResponse;
|
|
190
|
+
|
|
191
|
+
if (!data.ok) {
|
|
192
|
+
throw new Error('Failed to get updates');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (data.result.length > 0) {
|
|
196
|
+
this.offset = data.result[data.result.length - 1]!.update_id + 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return data.result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private async processUpdate(update: TelegramUpdate): Promise<void> {
|
|
203
|
+
if (!update.message || !this.handler) return;
|
|
204
|
+
|
|
205
|
+
const { message } = update;
|
|
206
|
+
|
|
207
|
+
// Security: check allowed users
|
|
208
|
+
if (this.allowedUsers.length > 0 && !this.allowedUsers.includes(message.from.id)) {
|
|
209
|
+
console.log(`[TelegramAdapter] Ignoring message from unauthorized user: ${message.from.id} (${message.from.username ?? message.from.first_name})`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let text = message.text ?? '';
|
|
214
|
+
|
|
215
|
+
// Handle voice/audio messages via STT
|
|
216
|
+
const voiceFile = message.voice ?? message.audio;
|
|
217
|
+
if (voiceFile && !text) {
|
|
218
|
+
if (!this.sttProvider) {
|
|
219
|
+
await this.sendMessage(
|
|
220
|
+
message.chat.id.toString(),
|
|
221
|
+
'Voice messages require STT configuration. Set up an STT provider in the Dashboard Settings.'
|
|
222
|
+
);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const audioBuffer = await this.downloadFile(voiceFile.file_id);
|
|
227
|
+
text = await this.sttProvider.transcribe(audioBuffer);
|
|
228
|
+
console.log('[TelegramAdapter] Transcribed voice:', text.slice(0, 80));
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.error('[TelegramAdapter] STT error:', err);
|
|
231
|
+
await this.sendMessage(
|
|
232
|
+
message.chat.id.toString(),
|
|
233
|
+
'Failed to transcribe voice message. Please try sending text.'
|
|
234
|
+
);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!text) return;
|
|
240
|
+
|
|
241
|
+
const channelMessage: ChannelMessage = {
|
|
242
|
+
id: message.message_id.toString(),
|
|
243
|
+
channel: 'telegram',
|
|
244
|
+
from: message.from.username || message.from.first_name,
|
|
245
|
+
text,
|
|
246
|
+
timestamp: message.date * 1000,
|
|
247
|
+
metadata: {
|
|
248
|
+
chatId: message.chat.id,
|
|
249
|
+
userId: message.from.id,
|
|
250
|
+
chatType: message.chat.type,
|
|
251
|
+
firstName: message.from.first_name,
|
|
252
|
+
lastName: message.from.last_name,
|
|
253
|
+
isVoice: !!voiceFile,
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
console.log('[TelegramAdapter] Message from', channelMessage.from, ':', channelMessage.text.slice(0, 80));
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const response = await this.handler(channelMessage);
|
|
261
|
+
|
|
262
|
+
if (response) {
|
|
263
|
+
await this.sendMessage(message.chat.id.toString(), response);
|
|
264
|
+
}
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error('[TelegramAdapter] Error handling message:', error);
|
|
267
|
+
|
|
268
|
+
await this.sendMessage(
|
|
269
|
+
message.chat.id.toString(),
|
|
270
|
+
'Sorry, I encountered an error processing your message.'
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private async downloadFile(fileId: string): Promise<Buffer> {
|
|
276
|
+
// Step 1: Get file path from Telegram
|
|
277
|
+
const fileResp = await fetch(`${this.baseUrl}/getFile`, {
|
|
278
|
+
method: 'POST',
|
|
279
|
+
headers: { 'Content-Type': 'application/json' },
|
|
280
|
+
body: JSON.stringify({ file_id: fileId }),
|
|
281
|
+
});
|
|
282
|
+
const fileData = await fileResp.json() as any;
|
|
283
|
+
|
|
284
|
+
if (!fileData.ok) {
|
|
285
|
+
throw new Error(`Failed to get file info: ${fileData.description}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Step 2: Download the actual file
|
|
289
|
+
const filePath = fileData.result.file_path;
|
|
290
|
+
const downloadUrl = `https://api.telegram.org/file/bot${this.token}/${filePath}`;
|
|
291
|
+
const downloadResp = await fetch(downloadUrl);
|
|
292
|
+
|
|
293
|
+
if (!downloadResp.ok) {
|
|
294
|
+
throw new Error(`Failed to download file: ${downloadResp.status}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return Buffer.from(await downloadResp.arrayBuffer());
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function splitText(text: string, maxLength: number): string[] {
|
|
302
|
+
if (text.length <= maxLength) return [text];
|
|
303
|
+
const chunks: string[] = [];
|
|
304
|
+
let remaining = text;
|
|
305
|
+
while (remaining.length > 0) {
|
|
306
|
+
if (remaining.length <= maxLength) {
|
|
307
|
+
chunks.push(remaining);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
let splitIdx = remaining.lastIndexOf('\n', maxLength);
|
|
311
|
+
if (splitIdx < maxLength / 2) splitIdx = maxLength;
|
|
312
|
+
chunks.push(remaining.slice(0, splitIdx));
|
|
313
|
+
remaining = remaining.slice(splitIdx);
|
|
314
|
+
}
|
|
315
|
+
return chunks;
|
|
316
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ChannelAdapter, ChannelHandler, ChannelMessage } from './telegram.ts';
|
|
2
|
+
|
|
3
|
+
export class WhatsAppAdapter implements ChannelAdapter {
|
|
4
|
+
name = 'whatsapp';
|
|
5
|
+
private phoneNumberId: string;
|
|
6
|
+
private accessToken: string;
|
|
7
|
+
private handler: ChannelHandler | null = null;
|
|
8
|
+
private connected: boolean = false;
|
|
9
|
+
|
|
10
|
+
constructor(config: { phoneNumberId: string; accessToken: string }) {
|
|
11
|
+
this.phoneNumberId = config.phoneNumberId;
|
|
12
|
+
this.accessToken = config.accessToken;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async connect(): Promise<void> {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'WhatsApp adapter not yet implemented. Requires WhatsApp Business API setup. ' +
|
|
18
|
+
'Visit https://developers.facebook.com/docs/whatsapp/cloud-api/get-started'
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// Future implementation would:
|
|
22
|
+
// 1. Set up webhook endpoint for incoming messages
|
|
23
|
+
// 2. Verify webhook with Facebook/Meta
|
|
24
|
+
// 3. Start listening for webhook events
|
|
25
|
+
// 4. Set this.connected = true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async disconnect(): Promise<void> {
|
|
29
|
+
this.connected = false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async sendMessage(to: string, text: string): Promise<void> {
|
|
33
|
+
throw new Error('WhatsApp adapter not yet implemented.');
|
|
34
|
+
|
|
35
|
+
// Future implementation:
|
|
36
|
+
// const response = await fetch(
|
|
37
|
+
// `https://graph.facebook.com/v18.0/${this.phoneNumberId}/messages`,
|
|
38
|
+
// {
|
|
39
|
+
// method: 'POST',
|
|
40
|
+
// headers: {
|
|
41
|
+
// 'Authorization': `Bearer ${this.accessToken}`,
|
|
42
|
+
// 'Content-Type': 'application/json',
|
|
43
|
+
// },
|
|
44
|
+
// body: JSON.stringify({
|
|
45
|
+
// messaging_product: 'whatsapp',
|
|
46
|
+
// to,
|
|
47
|
+
// text: { body: text },
|
|
48
|
+
// }),
|
|
49
|
+
// }
|
|
50
|
+
// );
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
onMessage(handler: ChannelHandler): void {
|
|
54
|
+
this.handler = handler;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
isConnected(): boolean {
|
|
58
|
+
return this.connected;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -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,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
|
+
}
|