@c4t4/heyamigo 0.10.1 → 0.10.3
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 +17 -8
- package/config/access.example.json +12 -2
- package/config/config.example.json +17 -1
- package/config/memory-instructions.md +1 -1
- package/config/personalities/casual.md +1 -1
- package/config/personalities/professional.md +1 -1
- package/config/personalities/sharp.md +2 -2
- package/dist/ai/claude.js +1 -0
- package/dist/ai/codex.js +1 -0
- package/dist/ai/grok.js +310 -0
- package/dist/ai/provider.js +5 -5
- package/dist/ai/providers.js +2 -0
- package/dist/ai/sessions.js +5 -1
- package/dist/boot.js +15 -6
- package/dist/channels/index.js +2 -1
- package/dist/channels/runtime.js +1 -0
- package/dist/channels/telegram.js +393 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/setup.js +168 -70
- package/dist/cli/start.js +25 -4
- package/dist/config.js +34 -1
- package/dist/db/address.js +13 -0
- package/dist/db/identity-sync.js +8 -0
- package/dist/gateway/bootstrap.js +15 -22
- package/dist/gateway/commands.js +13 -15
- package/dist/gateway/incoming.js +107 -254
- package/dist/gateway/ingest.js +240 -0
- package/dist/gateway/outgoing.js +3 -5
- package/dist/gateway/triggers.js +7 -40
- package/dist/memory/digest.js +5 -5
- package/dist/queue/async-tasks.js +11 -4
- package/dist/queue/browser-worker.js +5 -3
- package/dist/queue/cron-dispatch.js +6 -7
- package/dist/queue/job-address.js +4 -0
- package/dist/queue/outbound-postsend.js +4 -7
- package/dist/queue/worker.js +11 -5
- package/dist/wa/whitelist.js +40 -5
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/heyamigo-premium-clean.jpg" alt="Heyamigo" width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# heyamigo
|
|
2
6
|
|
|
3
|
-
A
|
|
7
|
+
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
8
|
|
|
5
9
|
```
|
|
6
|
-
WhatsApp ─► inbound ─► chat workers ─► outbound ─► WhatsApp
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
WhatsApp / Telegram ─► inbound ─► chat workers ─► outbound ─► WhatsApp / Telegram
|
|
11
|
+
│ ▲
|
|
12
|
+
├──────► async / browser ─┤
|
|
13
|
+
└──────► memory_writes ───┘
|
|
10
14
|
```
|
|
11
15
|
|
|
12
16
|
## What it does
|
|
@@ -14,7 +18,7 @@ WhatsApp ─► inbound ─► chat workers ─► outbound ─► WhatsApp
|
|
|
14
18
|
- **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
19
|
- **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
20
|
- **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
|
|
21
|
+
- **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
22
|
- **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
23
|
- **Default-deny proactive messaging.** Groups stay silent unless explicitly opted in. Per-role token quotas, file-size caps, tool restrictions.
|
|
20
24
|
|
|
@@ -31,7 +35,12 @@ npx @c4t4/heyamigo start # background, auto-restart
|
|
|
31
35
|
npx @c4t4/heyamigo logs # tail
|
|
32
36
|
```
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
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`.
|
|
39
|
+
|
|
40
|
+
Other providers:
|
|
41
|
+
|
|
42
|
+
- Codex: install `@openai/codex` and set `ai.provider: "codex"` in `config/config.json`.
|
|
43
|
+
- Grok Build: install with `curl -fsSL https://x.ai/cli/install.sh | bash`, run `grok login`, and set `ai.provider: "grok"`.
|
|
35
44
|
|
|
36
45
|
## In-chat commands
|
|
37
46
|
|
|
@@ -60,7 +69,7 @@ Codex instead of Claude: install `@openai/codex` and set `ai.provider: "codex"`
|
|
|
60
69
|
|
|
61
70
|
## Where to run it
|
|
62
71
|
|
|
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
|
|
72
|
+
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
73
|
|
|
65
74
|
## Tracking memory with git
|
|
66
75
|
|
|
@@ -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,
|
|
@@ -11,7 +18,7 @@
|
|
|
11
18
|
},
|
|
12
19
|
|
|
13
20
|
"triggers": {
|
|
14
|
-
"aliases": ["heyamigo", "amigo", "claude", "clawd"],
|
|
21
|
+
"aliases": ["heyamigo", "amigo", "claude", "clawd", "grok", "codex", "xai"],
|
|
15
22
|
"groupMode": "mention",
|
|
16
23
|
"dmMode": "mention",
|
|
17
24
|
"replyToBotCounts": 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: Sharp (default)
|
|
2
2
|
|
|
3
|
-
You answer
|
|
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
|
-
##
|
|
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
|
package/dist/ai/grok.js
ADDED
|
@@ -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
|
+
};
|
package/dist/ai/provider.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Provider abstraction for the user-facing chat ask path. Lets the worker
|
|
2
|
-
// route conversation turns to
|
|
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
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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 {};
|
package/dist/ai/providers.js
CHANGED
|
@@ -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
|
package/dist/ai/sessions.js
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
package/dist/channels/index.js
CHANGED
|
@@ -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
|
-
|
|
7
|
+
tg: telegramAdapter,
|
|
7
8
|
};
|
|
8
9
|
export function getChannelAdapter(channel) {
|
|
9
10
|
const adapter = REGISTRY[channel];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|