@c4t4/heyamigo 0.10.1 → 0.10.2

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/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # heyamigo
2
2
 
3
- A WhatsApp-resident assistant. Claude or Codex under the hood, durable SQLite queues, per-sender timezone scheduling, two-track architecture so browser work never blocks the chat.
3
+ A chat-resident assistant for WhatsApp and Telegram. Claude, Codex, or Grok under the hood, durable SQLite queues, per-sender timezone scheduling, two-track architecture so browser work never blocks the chat.
4
4
 
5
5
  ```
6
- WhatsApp ─► inbound ─► chat workers ─► outbound ─► WhatsApp
7
- │ ▲
8
- ├──────► async / browser ─┤
9
- └──────► memory_writes ───┘
6
+ WhatsApp / Telegram ─► inbound ─► chat workers ─► outbound ─► WhatsApp / Telegram
7
+ │ ▲
8
+ ├──────► async / browser ─┤
9
+ └──────► memory_writes ───┘
10
10
  ```
11
11
 
12
12
  ## What it does
@@ -14,7 +14,7 @@ WhatsApp ─► inbound ─► chat workers ─► outbound ─► WhatsApp
14
14
  - **Long-term memory per person, per chat, per topic.** Files on disk. The agent decides what's worth keeping; background workers consolidate while you're not chatting.
15
15
  - **A relevance watchlist.** Open loops the agent tracks on your behalf — questions you'd forget, things you're waiting on — surfaced naturally when the moment matches. Built like external working memory for the user.
16
16
  - **Scheduling in the sender's timezone.** Natural language → `[REMIND: 2026-05-26 09:00 — ...]` or `[CRON: 0 9 * * 1 PROMPT — ...]`. Fires at the user's wall-clock 9am, not the server's. Cron variants: deliver text, run AI, kick off async work, or drive a browser.
17
- - **A real Chrome.** Browser delegation via `[ASYNC-BROWSER: ...]` to a parallel Claude session on a shared logged-in Chrome over CDP. TikTok, Instagram, anywhere the owner is logged in. SSH-tunneled noVNC for setup.
17
+ - **A real Chrome.** Browser delegation via `[ASYNC-BROWSER: ...]` to a parallel provider session on a shared logged-in Chrome over CDP. TikTok, Instagram, anywhere the owner is logged in. SSH-tunneled noVNC for setup.
18
18
  - **Per-reply footer with confirmation tags.** Every side effect from the turn is visible: `_9.9s · 465k↑ 169↓ · +remind · +thread-new · +digest_`. No guessing whether a schedule actually got created.
19
19
  - **Default-deny proactive messaging.** Groups stay silent unless explicitly opted in. Per-role token quotas, file-size caps, tool restrictions.
20
20
 
@@ -31,7 +31,12 @@ npx @c4t4/heyamigo start # background, auto-restart
31
31
  npx @c4t4/heyamigo logs # tail
32
32
  ```
33
33
 
34
- Codex instead of Claude: install `@openai/codex` and set `ai.provider: "codex"` in `config/config.json`.
34
+ Telegram is optional. Create a bot with BotFather, set `telegram.enabled: true` and `telegram.botToken` in `config/config.json`, then allow users/groups in `config/access.json`. Telegram user keys use `tg_<user_id>`; Telegram group entries use addresses like `tg:group:-1001234567890`.
35
+
36
+ Other providers:
37
+
38
+ - Codex: install `@openai/codex` and set `ai.provider: "codex"` in `config/config.json`.
39
+ - Grok Build: install with `curl -fsSL https://x.ai/cli/install.sh | bash`, run `grok login`, and set `ai.provider: "grok"`.
35
40
 
36
41
  ## In-chat commands
37
42
 
@@ -60,7 +65,7 @@ Codex instead of Claude: install `@openai/codex` and set `ai.provider: "codex"`
60
65
 
61
66
  ## Where to run it
62
67
 
63
- A VPS (Hetzner, DO) at ~$5/mo is the path of least resistance. Home server or Raspberry Pi also fine. Needs Node 18+, a persistent filesystem, and one outbound WebSocket to WhatsApp. Not serverless-compatible.
68
+ A VPS (Hetzner, DO) at ~$5/mo is the path of least resistance. Home server or Raspberry Pi also fine. Needs Node 18+, a persistent filesystem, and outbound access to the enabled chat channels. Not serverless-compatible.
64
69
 
65
70
  ## Tracking memory with git
66
71
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "_readme": "Access rules. Copy this to access.json and customize. Groups auto-discover with mode=off when the bot sees them.",
2
+ "_readme": "Access rules. Copy this to access.json and customize. Groups auto-discover with mode=off when the bot sees them. WhatsApp users are phone numbers; Telegram users are tg_<user_id>.",
3
3
 
4
4
  "_limits_readme": "maxFileBytes caps the size of media/documents sent to Claude (null = unlimited). dailyTokenLimit caps Claude tokens (input+output) per user per day in the owner's timezone (null = unlimited). The bot owner is always unlimited regardless of role.",
5
5
 
@@ -45,6 +45,7 @@
45
45
  "users": {
46
46
  "17861234567": { "role": "admin", "name": "Alice" },
47
47
  "14155559999": { "role": "admin", "name": "Bob" },
48
+ "tg_123456789": { "role": "admin", "name": "Alice on Telegram" },
48
49
  "491701234567": { "role": "user", "name": "Carlos" },
49
50
  "5511987654321": { "role": "user", "name": "Davi" }
50
51
  },
@@ -71,6 +72,14 @@
71
72
  "allowedSenders": ["17861234567", "491701234567"],
72
73
  "proactive": true
73
74
  },
75
+ {
76
+ "_note": "Telegram group. Chat id is the address external id; senders use tg_<user_id>.",
77
+ "jid": "tg:group:-1001234567890",
78
+ "name": "Telegram Team",
79
+ "mode": "active",
80
+ "allowedSenders": ["tg_123456789"],
81
+ "proactive": false
82
+ },
74
83
  {
75
84
  "_note": "Silent group: stores messages but never responds",
76
85
  "jid": "120363zzzzz@g.us",
@@ -88,10 +97,11 @@
88
97
  ],
89
98
 
90
99
  "dms": {
91
- "_readme": "Matched by chat partner number, not sender. Owner messages in DMs are always silent.",
100
+ "_readme": "Matched by chat partner number/key, not sender. Telegram DMs use tg_<user_id>. Owner messages in WhatsApp DMs are always silent.",
92
101
  "defaultMode": "off",
93
102
  "allowed": [
94
103
  { "_note": "friend can DM the bot, and bot can proactively message them", "number": "491701234567", "mode": "active", "proactive": true },
104
+ { "_note": "Telegram friend can DM the bot", "number": "tg_123456789", "mode": "active", "proactive": true },
95
105
  { "_note": "colleague, store messages but don't respond", "number": "5511987654321", "mode": "silent", "proactive": false }
96
106
  ]
97
107
  }
@@ -1,9 +1,16 @@
1
1
  {
2
2
  "whatsapp": {
3
+ "enabled": true,
3
4
  "authDir": "./storage/auth",
4
5
  "browserName": "WhatsApp Bot"
5
6
  },
6
7
 
8
+ "telegram": {
9
+ "enabled": false,
10
+ "botToken": "",
11
+ "pollIntervalMs": 1000
12
+ },
13
+
7
14
  "owner": {
8
15
  "number": "",
9
16
  "treatAsAllowedEverywhere": true,
@@ -37,11 +44,20 @@
37
44
  },
38
45
 
39
46
  "codex": {
47
+ "contextWindow": 200000,
40
48
  "yolo": true,
41
49
  "skipGitRepoCheck": true,
42
50
  "extraArgs": []
43
51
  },
44
52
 
53
+ "grok": {
54
+ "bin": "grok",
55
+ "contextWindow": 1000000,
56
+ "alwaysApprove": true,
57
+ "memory": false,
58
+ "extraArgs": []
59
+ },
60
+
45
61
  "bootstrap": {
46
62
  "historyDepth": 50,
47
63
  "includeHistory": true,
@@ -205,7 +205,7 @@ Cost tracked per cron — `/crons` shows fire count + tokens. Omitting variant d
205
205
 
206
206
  ### Cross-chat: `[SEND-TEXT: address=wa:dm:<n>@s.whatsapp.net body="..."]`
207
207
 
208
- Rare; usually owner-only.
208
+ Rare; usually owner-only. Telegram targets use the same address grammar, e.g. `tg:dm:123456789` or `tg:group:-1001234567890`.
209
209
 
210
210
  ### Rules
211
211
 
@@ -1,6 +1,6 @@
1
1
  # Personality: Casual
2
2
 
3
- You answer WhatsApp messages for the account owner. Act like a smart friend chatting over coffee.
3
+ You answer chat messages for the account owner. Act like a smart friend chatting over coffee.
4
4
 
5
5
  ## Voice
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Personality: Professional
2
2
 
3
- You answer WhatsApp messages for the account owner in business and professional contexts.
3
+ You answer chat messages for the account owner in business and professional contexts.
4
4
 
5
5
  ## Voice
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Personality: Sharp (default)
2
2
 
3
- You answer WhatsApp messages for the account owner. Not customer service, not marketing copy. A conversational peer: sharp, direct, useful.
3
+ You answer chat messages for the account owner. Not customer service, not marketing copy. A conversational peer: sharp, direct, useful.
4
4
 
5
5
  ## Voice
6
6
 
@@ -32,6 +32,6 @@ See the layers. Most questions have a surface answer and a real answer — give
32
32
 
33
33
  The default chatbot failure mode. Skip validation openers ("great question", "good point", "absolutely", "that makes sense") — just answer. Disagree directly when you disagree; softening bad ideas wastes their time. Don't reflexively offer more help. Don't apologize for nothing. Don't flatter — engage with substance instead of complimenting it.
34
34
 
35
- ## WhatsApp
35
+ ## Chat
36
36
 
37
37
  Short replies, plain text. No markdown headers, bold, or bullet lists (renders poorly). Don't dominate groups. Never break frame with "As an AI assistant..." or similar.
package/dist/ai/claude.js CHANGED
@@ -202,6 +202,7 @@ export async function runClaudeTask(params) {
202
202
  }
203
203
  export const claudeProvider = {
204
204
  name: 'claude',
205
+ contextWindow: config.claude.contextWindow,
205
206
  // Claude CLI's `result` event reports per-turn usage (just the
206
207
  // tokens consumed by this single resume invocation).
207
208
  usageReportingMode: 'per-turn',
package/dist/ai/codex.js CHANGED
@@ -267,6 +267,7 @@ async function askCodex(params) {
267
267
  }
268
268
  export const codexProvider = {
269
269
  name: 'codex',
270
+ contextWindow: config.codex.contextWindow,
270
271
  // Codex CLI's `turn.completed.usage` reports cumulative totals for
271
272
  // the entire resume thread, not just this one turn. Worker uses
272
273
  // this flag to delta-math each turn before display so the context
@@ -0,0 +1,310 @@
1
+ // Grok Build CLI provider. Maps the neutral AiProvider contract onto
2
+ // `grok` headless mode (`--prompt-file` + `--output-format json`).
3
+ //
4
+ // Grok Build is a local coding-agent CLI, not a plain API model. It already
5
+ // knows how to inspect repo config, use MCP, run shell tools, and resume
6
+ // sessions. This adapter keeps the same heyamigo contract Claude/Codex use:
7
+ // one prompt in, one reply out, opaque provider-native session ids.
8
+ import { mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync, } from 'fs';
9
+ import { tmpdir } from 'os';
10
+ import { join, resolve } from 'path';
11
+ import { config } from '../config.js';
12
+ import { logger } from '../logger.js';
13
+ import { logPrompt } from '../promptlog.js';
14
+ import { runClaude, TIMEOUT_MS } from './spawn.js';
15
+ let cachedSystemPrompt = null;
16
+ function systemPrompt() {
17
+ if (cachedSystemPrompt !== null)
18
+ return cachedSystemPrompt;
19
+ const personality = readFileSync(resolve(process.cwd(), config.claude.personalityFile), 'utf-8');
20
+ let memoryInstructions = '';
21
+ try {
22
+ memoryInstructions = readFileSync(resolve(process.cwd(), config.memory.instructionsFile), 'utf-8');
23
+ }
24
+ catch {
25
+ // memory instructions optional
26
+ }
27
+ cachedSystemPrompt = memoryInstructions
28
+ ? `${personality}\n\n---\n\n${memoryInstructions}`
29
+ : personality;
30
+ return cachedSystemPrompt;
31
+ }
32
+ function reloadSystemPrompt() {
33
+ cachedSystemPrompt = null;
34
+ }
35
+ function permissionModeFor(mode) {
36
+ switch (mode) {
37
+ case 'read-only':
38
+ return 'plan';
39
+ case 'auto':
40
+ return 'acceptEdits';
41
+ case 'full':
42
+ return 'bypassPermissions';
43
+ }
44
+ }
45
+ function laneTimeoutMs(lane) {
46
+ return TIMEOUT_MS[lane];
47
+ }
48
+ function hasWebTool(tools) {
49
+ return tools.some((tool) => /web(fetch|search)?/i.test(tool));
50
+ }
51
+ function buildArgs(params) {
52
+ const cfg = config.grok;
53
+ let prompt = params.prompt;
54
+ const args = [
55
+ '--cwd',
56
+ process.cwd(),
57
+ '--output-format',
58
+ 'json',
59
+ '--permission-mode',
60
+ permissionModeFor(params.mode),
61
+ '--verbatim',
62
+ ];
63
+ if (cfg.model)
64
+ args.push('-m', cfg.model);
65
+ if (params.mode === 'read-only') {
66
+ args.push('--sandbox', 'read-only');
67
+ }
68
+ else if (cfg.alwaysApprove) {
69
+ args.push('--always-approve');
70
+ }
71
+ if (cfg.memory) {
72
+ args.push('--experimental-memory');
73
+ }
74
+ else {
75
+ args.push('--no-memory');
76
+ }
77
+ if (params.allowedTools && params.allowedTools !== 'all') {
78
+ if (params.allowedTools.length > 0) {
79
+ args.push('--allow', params.allowedTools.join(','));
80
+ }
81
+ if (!hasWebTool(params.allowedTools)) {
82
+ args.push('--disable-web-search');
83
+ }
84
+ }
85
+ for (const extra of cfg.extraArgs)
86
+ args.push(extra);
87
+ if (params.sessionId) {
88
+ args.push('--resume', params.sessionId);
89
+ }
90
+ else if (params.includeSystemPrompt) {
91
+ // Keep this in the prompt file instead of argv so large personalities and
92
+ // memory instructions don't hit ARG_MAX.
93
+ prompt = `${systemPrompt()}\n\n---\n\n${prompt}`;
94
+ }
95
+ args.push('--prompt-file', params.promptFile);
96
+ return { args, prompt };
97
+ }
98
+ function usageFrom(raw) {
99
+ const usage = raw.usage;
100
+ return {
101
+ inputTokens: usage?.inputTokens ?? usage?.input_tokens ?? usage?.prompt_tokens ?? 0,
102
+ cacheReadTokens: usage?.cacheReadTokens ?? usage?.cached_input_tokens ?? 0,
103
+ cacheCreationTokens: usage?.cacheCreationTokens ?? 0,
104
+ outputTokens: usage?.outputTokens ?? usage?.output_tokens ?? usage?.completion_tokens ?? 0,
105
+ numTurns: 0,
106
+ };
107
+ }
108
+ function textFrom(raw) {
109
+ for (const value of [
110
+ raw.text,
111
+ raw.output_text,
112
+ raw.result,
113
+ raw.reply,
114
+ raw.message,
115
+ ]) {
116
+ if (typeof value === 'string')
117
+ return value;
118
+ }
119
+ return null;
120
+ }
121
+ function parseJsonObject(stdout) {
122
+ const trimmed = stdout.trim();
123
+ if (!trimmed)
124
+ return null;
125
+ try {
126
+ return JSON.parse(trimmed);
127
+ }
128
+ catch {
129
+ // Grok may emit log lines before/after JSON on some failures. Try the
130
+ // broadest JSON-looking slice before giving up.
131
+ const first = trimmed.indexOf('{');
132
+ const last = trimmed.lastIndexOf('}');
133
+ if (first >= 0 && last > first) {
134
+ try {
135
+ return JSON.parse(trimmed.slice(first, last + 1));
136
+ }
137
+ catch {
138
+ return null;
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+ }
144
+ function parseStreamingJson(stdout) {
145
+ let reply = '';
146
+ let sessionId;
147
+ let error = null;
148
+ for (const line of stdout.split(/\r?\n/)) {
149
+ const trimmed = line.trim();
150
+ if (!trimmed)
151
+ continue;
152
+ let ev;
153
+ try {
154
+ ev = JSON.parse(trimmed);
155
+ }
156
+ catch {
157
+ continue;
158
+ }
159
+ if (ev.type === 'text' && typeof ev.data === 'string') {
160
+ reply += ev.data;
161
+ }
162
+ else if (ev.type === 'end') {
163
+ const id = ev.sessionId ?? ev.session_id;
164
+ if (typeof id === 'string')
165
+ sessionId = id;
166
+ }
167
+ else if (ev.type === 'error') {
168
+ error = textFrom(ev) ?? (typeof ev.data === 'string' ? ev.data : null);
169
+ }
170
+ }
171
+ if (error)
172
+ throw new Error(`grok returned error: ${error}`);
173
+ if (!reply)
174
+ return null;
175
+ return {
176
+ reply: reply.trim(),
177
+ sessionId,
178
+ usage: {
179
+ inputTokens: 0,
180
+ cacheReadTokens: 0,
181
+ cacheCreationTokens: 0,
182
+ outputTokens: 0,
183
+ numTurns: 0,
184
+ },
185
+ };
186
+ }
187
+ function parseGrokOutput(stdout) {
188
+ const raw = parseJsonObject(stdout);
189
+ if (raw) {
190
+ if (raw.type === 'error') {
191
+ throw new Error(`grok returned error: ${textFrom(raw) ?? stdout.slice(0, 500)}`);
192
+ }
193
+ const reply = textFrom(raw);
194
+ if (reply !== null) {
195
+ const id = raw.sessionId ?? raw.session_id;
196
+ return {
197
+ reply: reply.trim(),
198
+ sessionId: typeof id === 'string' ? id : undefined,
199
+ usage: usageFrom(raw),
200
+ };
201
+ }
202
+ }
203
+ return parseStreamingJson(stdout);
204
+ }
205
+ function createPromptFile(prompt) {
206
+ const dir = mkdtempSync(join(tmpdir(), 'heyamigo-grok-'));
207
+ const path = join(dir, 'prompt.txt');
208
+ writeFileSync(path, prompt, 'utf-8');
209
+ return { dir, path };
210
+ }
211
+ function removePromptFile(tmp) {
212
+ try {
213
+ unlinkSync(tmp.path);
214
+ }
215
+ catch { }
216
+ try {
217
+ rmSync(tmp.dir, { recursive: true, force: true });
218
+ }
219
+ catch { }
220
+ }
221
+ async function runGrokTask(params) {
222
+ const tmp = createPromptFile(params.input);
223
+ let args = [];
224
+ let promptForFile = params.input;
225
+ try {
226
+ const built = buildArgs({
227
+ mode: params.mode,
228
+ sessionId: params.sessionId,
229
+ includeSystemPrompt: params.includeSystemPrompt,
230
+ prompt: params.input,
231
+ allowedTools: params.allowedTools,
232
+ promptFile: tmp.path,
233
+ });
234
+ args = built.args;
235
+ promptForFile = built.prompt;
236
+ writeFileSync(tmp.path, promptForFile, 'utf-8');
237
+ logger.info({
238
+ caller: params.caller,
239
+ resume: !!params.sessionId,
240
+ argv: args,
241
+ promptChars: promptForFile.length,
242
+ }, 'spawning grok');
243
+ const { stdout, stderr, durationMs } = await runClaude({
244
+ args,
245
+ input: '',
246
+ timeoutMs: laneTimeoutMs(params.lane),
247
+ caller: params.caller,
248
+ bin: config.grok.bin,
249
+ });
250
+ const startedAt = Date.now() - durationMs;
251
+ const parsed = parseGrokOutput(stdout);
252
+ if (!parsed) {
253
+ throw new Error(`grok produced no parseable result; stdout: ${stdout.slice(0, 500)}`);
254
+ }
255
+ void logPrompt({
256
+ ts: Math.floor(startedAt / 1000),
257
+ caller: params.caller,
258
+ args,
259
+ input: params.input,
260
+ output: parsed.reply,
261
+ sessionId: parsed.sessionId,
262
+ usage: parsed.usage,
263
+ durationMs,
264
+ stderr,
265
+ });
266
+ return parsed;
267
+ }
268
+ finally {
269
+ removePromptFile(tmp);
270
+ }
271
+ }
272
+ async function askGrok(params) {
273
+ const result = await runGrokTask({
274
+ input: params.input,
275
+ caller: 'worker',
276
+ mode: 'auto',
277
+ lane: 'main',
278
+ sessionId: params.sessionId,
279
+ includeSystemPrompt: true,
280
+ allowedTools: params.allowedTools,
281
+ addDirs: [
282
+ config.memory.dir,
283
+ config.storage.mediaDir,
284
+ ],
285
+ });
286
+ if (!result.sessionId) {
287
+ throw new Error('grok ask: response missing session id');
288
+ }
289
+ return {
290
+ reply: result.reply,
291
+ sessionId: result.sessionId,
292
+ usage: result.usage ?? {
293
+ inputTokens: 0,
294
+ cacheReadTokens: 0,
295
+ cacheCreationTokens: 0,
296
+ outputTokens: 0,
297
+ numTurns: 0,
298
+ },
299
+ };
300
+ }
301
+ export const grokProvider = {
302
+ name: 'grok',
303
+ contextWindow: config.grok.contextWindow,
304
+ // The current Grok Build headless JSON output does not expose reliable
305
+ // per-turn token usage, so treat any reported counts as this invocation only.
306
+ usageReportingMode: 'per-turn',
307
+ ask: askGrok,
308
+ runTask: runGrokTask,
309
+ reloadSystemPrompt,
310
+ };
@@ -1,9 +1,9 @@
1
1
  // Provider abstraction for the user-facing chat ask path. Lets the worker
2
- // route conversation turns to either Claude or Codex (or any future CLI)
2
+ // route conversation turns to Claude, Codex, Grok, or any future CLI
3
3
  // without knowing the wire details.
4
4
  //
5
- // Scope: covers the interactive worker call (one turn in, one turn out, with
6
- // resumable session ids and per-role tool gating). Memory digests, async
7
- // tasks, and the journal observers stay on `runClaude` directly they're
8
- // Claude-specific batch pipelines and not in this interface.
5
+ // Scope: covers the interactive worker call and general provider-backed agent
6
+ // tasks (memory digests, async/background work, browser tasks). A few legacy
7
+ // utilities may still call a specific CLI directly, but runtime work should
8
+ // flow through this interface.
9
9
  export {};
@@ -1,9 +1,11 @@
1
1
  import { config } from '../config.js';
2
2
  import { claudeProvider } from './claude.js';
3
3
  import { codexProvider } from './codex.js';
4
+ import { grokProvider } from './grok.js';
4
5
  const REGISTRY = {
5
6
  claude: claudeProvider,
6
7
  codex: codexProvider,
8
+ grok: grokProvider,
7
9
  };
8
10
  // Resolve the active provider. Defaults to claude if no override is set in
9
11
  // config; pass an explicit name to force one (useful for per-role routing
@@ -34,7 +34,11 @@ function load() {
34
34
  ('codex' in obj &&
35
35
  typeof obj.codex === 'object' &&
36
36
  obj.codex !== null &&
37
- 'sessionId' in obj.codex);
37
+ 'sessionId' in obj.codex) ||
38
+ ('grok' in obj &&
39
+ typeof obj.grok === 'object' &&
40
+ obj.grok !== null &&
41
+ 'sessionId' in obj.grok);
38
42
  if (isNamespaced) {
39
43
  out[jid] = obj;
40
44
  }
package/dist/boot.js CHANGED
@@ -3,9 +3,12 @@
3
3
  // startup order — there used to be two parallel main() functions that
4
4
  // drifted; this prevents that.
5
5
  import { setBaileysSocket } from './channels/index.js';
6
+ import { telegramRuntime } from './channels/telegram.js';
7
+ import { config } from './config.js';
6
8
  import { closeDb, initDb } from './db/index.js';
7
9
  import { syncIdentitiesFromAccess } from './db/identity-sync.js';
8
10
  import { attachIncoming } from './gateway/incoming.js';
11
+ import { processIncomingMessage } from './gateway/ingest.js';
9
12
  import { logger } from './logger.js';
10
13
  import { startScheduler } from './memory/scheduler.js';
11
14
  import { startBrowserWorkers, stopBrowserWorkers } from './queue/browser-worker.js';
@@ -35,6 +38,7 @@ export async function bootBot() {
35
38
  stopBrowserWorkers();
36
39
  stopSenderWorker();
37
40
  stopMemoryWorker();
41
+ void telegramRuntime.stop();
38
42
  stopOrchestrator();
39
43
  closeDb();
40
44
  },
@@ -47,12 +51,17 @@ export async function bootBot() {
47
51
  startBrowserWorkers();
48
52
  startChatWorkers();
49
53
  startScheduler();
50
- await startSocket((sock) => {
51
- attachIncoming(sock);
52
- // Point the Baileys adapter at the live socket. Called on each
53
- // reconnect with a fresh sock; the adapter just keeps the latest.
54
- setBaileysSocket(sock);
55
- });
54
+ if (config.telegram.enabled) {
55
+ await telegramRuntime.start(processIncomingMessage);
56
+ }
57
+ if (config.whatsapp.enabled !== false) {
58
+ await startSocket((sock) => {
59
+ attachIncoming(sock);
60
+ // Point the Baileys adapter at the live socket. Called on each
61
+ // reconnect with a fresh sock; the adapter just keeps the latest.
62
+ setBaileysSocket(sock);
63
+ });
64
+ }
56
65
  }
57
66
  // Install once. Both signals trigger the same graceful drain:
58
67
  // orchestrator picks up the shutdown control row, waits for busy
@@ -1,9 +1,10 @@
1
1
  // Channel adapter registry. Sender worker calls getChannelAdapter(name)
2
2
  // keyed off the parsed address.channel.
3
3
  import { baileysAdapter } from './baileys.js';
4
+ import { telegramAdapter } from './telegram.js';
4
5
  const REGISTRY = {
5
6
  wa: baileysAdapter,
6
- // tg: telegramAdapter, // Phase 8
7
+ tg: telegramAdapter,
7
8
  };
8
9
  export function getChannelAdapter(channel) {
9
10
  const adapter = REGISTRY[channel];
@@ -0,0 +1 @@
1
+ export {};