grok-telegram-bot 2.0.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/.env.example +135 -0
- package/CHANGELOG.md +598 -0
- package/LICENSE +21 -0
- package/README.md +644 -0
- package/bin/grok-tg.mjs +21 -0
- package/docs/INSTALL.md +153 -0
- package/docs/UPGRADE.md +253 -0
- package/docs/ops/RELEASE_CHECKLIST.md +39 -0
- package/package.json +74 -0
- package/scripts/setup.mjs +116 -0
- package/src/agents/catalog.ts +58 -0
- package/src/app/accounts.ts +162 -0
- package/src/app/auth-service.ts +136 -0
- package/src/app/grok-credentials.ts +103 -0
- package/src/app/instance-lock.ts +139 -0
- package/src/app/json-store.ts +54 -0
- package/src/app/reasoning.ts +30 -0
- package/src/app/settings-store.ts +38 -0
- package/src/app/stt.ts +53 -0
- package/src/app/types.ts +56 -0
- package/src/app/updater.ts +234 -0
- package/src/app/usage.ts +38 -0
- package/src/app/version.ts +41 -0
- package/src/bot/account-rotator.ts +52 -0
- package/src/bot/auth.ts +38 -0
- package/src/bot/bot.ts +225 -0
- package/src/bot/chat-controller.ts +317 -0
- package/src/bot/commands.ts +52 -0
- package/src/bot/deps.ts +67 -0
- package/src/bot/file-ingest.ts +190 -0
- package/src/bot/handlers/accounts.ts +220 -0
- package/src/bot/handlers/auth.ts +64 -0
- package/src/bot/handlers/control.ts +103 -0
- package/src/bot/handlers/document.ts +112 -0
- package/src/bot/handlers/history.ts +63 -0
- package/src/bot/handlers/kill.ts +54 -0
- package/src/bot/handlers/mcp.ts +206 -0
- package/src/bot/handlers/menu.ts +220 -0
- package/src/bot/handlers/message.ts +103 -0
- package/src/bot/handlers/photo.ts +123 -0
- package/src/bot/handlers/projects.ts +183 -0
- package/src/bot/handlers/running.ts +181 -0
- package/src/bot/handlers/session-card.ts +81 -0
- package/src/bot/handlers/session-kill.ts +95 -0
- package/src/bot/handlers/sessions.ts +148 -0
- package/src/bot/handlers/system.ts +51 -0
- package/src/bot/handlers/tasks.ts +224 -0
- package/src/bot/handlers/usage.ts +38 -0
- package/src/bot/handlers/voice.ts +55 -0
- package/src/bot/image-return.ts +69 -0
- package/src/bot/menu/ephemeral.ts +117 -0
- package/src/bot/menu/keyboard.ts +49 -0
- package/src/bot/menu/refresh.ts +13 -0
- package/src/bot/menu/status-panel.ts +173 -0
- package/src/bot/permission-service.ts +149 -0
- package/src/bot/prompt-content.ts +64 -0
- package/src/bot/prompt-retry.ts +70 -0
- package/src/bot/reauth-controller.ts +297 -0
- package/src/bot/registry.ts +186 -0
- package/src/bot/reply-context.ts +77 -0
- package/src/bot/session-fork.ts +35 -0
- package/src/bot/session-runtime.ts +1048 -0
- package/src/bot/telegram-io.ts +109 -0
- package/src/bot/typing.ts +35 -0
- package/src/bot/wizard/task-wizard.ts +214 -0
- package/src/cli.ts +126 -0
- package/src/config.ts +248 -0
- package/src/grok/client.ts +617 -0
- package/src/grok/models.ts +50 -0
- package/src/grok/session-log.ts +148 -0
- package/src/grok/transport.ts +51 -0
- package/src/grok/types.ts +136 -0
- package/src/index.ts +84 -0
- package/src/logger.ts +78 -0
- package/src/mcp/config.ts +120 -0
- package/src/mcp/probe.ts +218 -0
- package/src/mcp/types.ts +68 -0
- package/src/projects/manager.ts +99 -0
- package/src/render/chunk.ts +57 -0
- package/src/render/diff.ts +48 -0
- package/src/render/escape.ts +22 -0
- package/src/render/file-summary.ts +111 -0
- package/src/render/hashtags.ts +34 -0
- package/src/render/markdown.ts +130 -0
- package/src/render/progress-estimate.ts +63 -0
- package/src/render/progress.ts +80 -0
- package/src/render/subagent.ts +75 -0
- package/src/render/tool-call.ts +196 -0
- package/src/service/index.ts +24 -0
- package/src/service/linux.ts +85 -0
- package/src/service/macos.ts +101 -0
- package/src/service/platform.ts +64 -0
- package/src/service/types.ts +36 -0
- package/src/service/windows.ts +198 -0
- package/src/sessions/history.ts +225 -0
- package/src/sessions/process.ts +30 -0
- package/src/sessions/store.ts +133 -0
- package/src/sessions/tail.ts +86 -0
- package/src/sessions/types.ts +26 -0
- package/src/stream/streamer.ts +261 -0
- package/src/tasks/runner.ts +82 -0
- package/src/tasks/schedule.ts +142 -0
- package/src/tasks/scheduler.ts +53 -0
- package/src/tasks/store.ts +80 -0
- package/src/tasks/types.ts +33 -0
- package/tsconfig.json +19 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration: loads .env, validates required values, resolves paths.
|
|
3
|
+
*
|
|
4
|
+
* The bot drives the official Grok Build CLI over ACP (`grok agent stdio`) and
|
|
5
|
+
* authenticates with your xAI account sign-in (`grok login`, `~/.grok/auth.json`),
|
|
6
|
+
* or an optional `XAI_API_KEY` on headless hosts.
|
|
7
|
+
*/
|
|
8
|
+
import { config as loadDotenv } from "dotenv";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
/** Absolute path to the installed bot code (one level above src/). For a global
|
|
15
|
+
* npm install this lives inside node_modules — code lives here, never user data. */
|
|
16
|
+
export const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
|
|
18
|
+
/** Canonical, path-independent home for this bot's `.env`, `logs/`, `data/` and
|
|
19
|
+
* the single-instance locks: `~/.grok/tg`. Used whenever the bot is started
|
|
20
|
+
* without an explicit instance dir and there's no `.env` in the current folder,
|
|
21
|
+
* so the SAME configuration is found no matter which directory you launch from. */
|
|
22
|
+
export const CANONICAL_DIR = join(homedir(), ".grok", "tg");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Directory holding THIS instance's `.env`, `logs/` and `data/`. Resolution
|
|
26
|
+
* (first match wins):
|
|
27
|
+
* 1. `--instance <dir>` argv — set by the installed background service,
|
|
28
|
+
* 2. `GROK_TG_DIR` env — an explicit override,
|
|
29
|
+
* 3. `GROK_TG_CWD` env — the legacy launcher variable,
|
|
30
|
+
* 4. the current folder, IF it already contains a `.env`,
|
|
31
|
+
* 5. the canonical `~/.grok/tg` home — the path-independent default.
|
|
32
|
+
*/
|
|
33
|
+
export const INSTANCE_DIR = resolveInstanceDir();
|
|
34
|
+
|
|
35
|
+
/** Absolute path to the `.env` this instance loads (and that `setup` writes). */
|
|
36
|
+
export const ENV_PATH = join(INSTANCE_DIR, ".env");
|
|
37
|
+
|
|
38
|
+
function resolveInstanceDir(): string {
|
|
39
|
+
const flag = process.argv.indexOf("--instance");
|
|
40
|
+
if (flag !== -1 && process.argv[flag + 1]) return resolve(process.argv[flag + 1]!);
|
|
41
|
+
const envDir = process.env.GROK_TG_DIR?.trim() || process.env.GROK_TG_CWD?.trim();
|
|
42
|
+
if (envDir) return resolve(expandHome(envDir));
|
|
43
|
+
if (existsSync(join(process.cwd(), ".env"))) return process.cwd();
|
|
44
|
+
return CANONICAL_DIR;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Load .env from the resolved instance directory. dotenv does NOT override
|
|
48
|
+
// variables already present in the environment (the launcher/service env wins).
|
|
49
|
+
loadDotenv({ path: ENV_PATH });
|
|
50
|
+
|
|
51
|
+
function expandHome(p: string): string {
|
|
52
|
+
if (p === "~") return homedir();
|
|
53
|
+
if (p.startsWith("~/") || p.startsWith("~\\")) return join(homedir(), p.slice(2));
|
|
54
|
+
return p;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function bool(v: string | undefined, def: boolean): boolean {
|
|
58
|
+
if (v === undefined || v === "") return def;
|
|
59
|
+
return ["1", "true", "yes", "on"].includes(v.toLowerCase());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function num(v: string | undefined, def: number): number {
|
|
63
|
+
const n = Number(v);
|
|
64
|
+
return Number.isFinite(n) && n > 0 ? n : def;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Like num() but allows 0 (e.g. to disable retries). Rejects negatives. */
|
|
68
|
+
function nonNegNum(v: string | undefined, def: number): number {
|
|
69
|
+
if (v === undefined || v === "") return def;
|
|
70
|
+
const n = Number(v);
|
|
71
|
+
return Number.isFinite(n) && n >= 0 ? n : def;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function list(v: string | undefined): string[] {
|
|
75
|
+
return (v || "")
|
|
76
|
+
.split(",")
|
|
77
|
+
.map((s) => s.trim())
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface AppConfig {
|
|
82
|
+
token: string;
|
|
83
|
+
allowedUsers: Set<string>;
|
|
84
|
+
grokCliPath: string;
|
|
85
|
+
workspace: string;
|
|
86
|
+
/** Optional xAI API key for headless hosts. When set, exported to the agent
|
|
87
|
+
* as XAI_API_KEY. Otherwise the agent uses the `grok login` token in
|
|
88
|
+
* ~/.grok/auth.json. */
|
|
89
|
+
grokApiKey?: string;
|
|
90
|
+
/** Optional Grok API base URL (default https://api.x.ai/v1). */
|
|
91
|
+
grokBaseUrl?: string;
|
|
92
|
+
/** Default model for new sessions (e.g. grok-4-1-fast). */
|
|
93
|
+
grokModel: string;
|
|
94
|
+
/** Optional max output tokens (exported as GROK_MAX_TOKENS). */
|
|
95
|
+
grokMaxTokens?: number;
|
|
96
|
+
/** Cap on tool-execution rounds per headless turn (grok --max-tool-rounds). */
|
|
97
|
+
maxToolRounds: number;
|
|
98
|
+
/** Custom sub-agent name to hint in prompts (informational; Grok has no
|
|
99
|
+
* --agent flag headlessly). */
|
|
100
|
+
agent?: string;
|
|
101
|
+
trustAllTools: boolean;
|
|
102
|
+
projectRoots: string[];
|
|
103
|
+
streamThrottleMs: number;
|
|
104
|
+
messageBatchMs: number;
|
|
105
|
+
showToolCalls: boolean;
|
|
106
|
+
showEditDiffs: boolean;
|
|
107
|
+
diffMaxLines: number;
|
|
108
|
+
sendAgentImages: boolean;
|
|
109
|
+
agentImagesMax: number;
|
|
110
|
+
docMaxChars: number;
|
|
111
|
+
logLevel: string;
|
|
112
|
+
sessionsDir: string;
|
|
113
|
+
projectRoot: string;
|
|
114
|
+
logsDir: string;
|
|
115
|
+
logFile: string;
|
|
116
|
+
/** Emit a `restarted` event / clear running turns when asked (kept for the
|
|
117
|
+
* self-healing + reauth flows; there is no persistent daemon with Grok). */
|
|
118
|
+
grokAutoRestart: boolean;
|
|
119
|
+
dataDir: string;
|
|
120
|
+
promptIdleMs: number;
|
|
121
|
+
quietNotifications: boolean;
|
|
122
|
+
promptRetryAttempts: number;
|
|
123
|
+
autoForkOnError: boolean;
|
|
124
|
+
autoForkContextPct: number;
|
|
125
|
+
resumeOnStreamError: boolean;
|
|
126
|
+
sttApiUrl?: string;
|
|
127
|
+
sttApiKey?: string;
|
|
128
|
+
sttModel: string;
|
|
129
|
+
sttLanguage?: string;
|
|
130
|
+
mcpProbeTimeoutMs: number;
|
|
131
|
+
mcpProbeConcurrency: number;
|
|
132
|
+
showSubagents: boolean;
|
|
133
|
+
showProgress: boolean;
|
|
134
|
+
progressFallback: boolean;
|
|
135
|
+
notifyOtherSessions: boolean;
|
|
136
|
+
autoUpdate: boolean;
|
|
137
|
+
updateCheckMs: number;
|
|
138
|
+
singleInstance: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function loadConfig(): AppConfig {
|
|
142
|
+
const token = (process.env.TELEGRAM_BOT_TOKEN || "").trim();
|
|
143
|
+
if (!token) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
"TELEGRAM_BOT_TOKEN is missing. Copy .env.example to .env and set it (run `npm run setup`).",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const workspaceRaw = process.env.GROK_WORKSPACE?.trim() || process.cwd();
|
|
150
|
+
const workspace = resolve(expandHome(workspaceRaw));
|
|
151
|
+
|
|
152
|
+
// Default project roots: the workspace parent + home directory.
|
|
153
|
+
const roots = list(process.env.PROJECT_ROOTS).map((p) => resolve(expandHome(p)));
|
|
154
|
+
if (roots.length === 0) {
|
|
155
|
+
roots.push(dirname(workspace), homedir());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const dataDir = process.env.DATA_DIR?.trim()
|
|
159
|
+
? resolve(expandHome(process.env.DATA_DIR.trim()))
|
|
160
|
+
: join(INSTANCE_DIR, "data");
|
|
161
|
+
// The bot owns its sessions on disk (Grok itself keeps them in SQLite): one
|
|
162
|
+
// `<id>.json` + `<id>.jsonl` + `<id>.lock` per session, mirroring the layout
|
|
163
|
+
// the session store / history parser / tail watcher already understand.
|
|
164
|
+
const sessionsDir = process.env.SESSIONS_DIR?.trim()
|
|
165
|
+
? resolve(expandHome(process.env.SESSIONS_DIR.trim()))
|
|
166
|
+
: join(dataDir, "sessions");
|
|
167
|
+
const logsDir = process.env.LOG_DIR?.trim()
|
|
168
|
+
? resolve(expandHome(process.env.LOG_DIR.trim()))
|
|
169
|
+
: join(INSTANCE_DIR, "logs");
|
|
170
|
+
const logFile = process.env.LOG_FILE?.trim()
|
|
171
|
+
? resolve(expandHome(process.env.LOG_FILE.trim()))
|
|
172
|
+
: join(logsDir, "grok-telegram-bot.log");
|
|
173
|
+
|
|
174
|
+
const cfg: AppConfig = {
|
|
175
|
+
token,
|
|
176
|
+
allowedUsers: new Set(list(process.env.ALLOWED_USERS)),
|
|
177
|
+
grokCliPath: resolveGrokPath(process.env.GROK_CLI_PATH?.trim()),
|
|
178
|
+
workspace,
|
|
179
|
+
grokApiKey: process.env.XAI_API_KEY?.trim() || process.env.GROK_API_KEY?.trim() || undefined,
|
|
180
|
+
grokBaseUrl: process.env.GROK_BASE_URL?.trim() || undefined,
|
|
181
|
+
grokModel: process.env.GROK_MODEL?.trim() || "grok-4.5",
|
|
182
|
+
grokMaxTokens: process.env.GROK_MAX_TOKENS ? num(process.env.GROK_MAX_TOKENS, 0) || undefined : undefined,
|
|
183
|
+
maxToolRounds: num(process.env.GROK_MAX_TOOL_ROUNDS, 400),
|
|
184
|
+
agent: process.env.GROK_AGENT?.trim() || undefined,
|
|
185
|
+
trustAllTools: bool(process.env.GROK_TRUST_ALL_TOOLS, true),
|
|
186
|
+
projectRoots: [...new Set(roots)],
|
|
187
|
+
streamThrottleMs: num(process.env.STREAM_THROTTLE_MS, 1500),
|
|
188
|
+
messageBatchMs: nonNegNum(process.env.MESSAGE_BATCH_MS, 800),
|
|
189
|
+
showToolCalls: bool(process.env.SHOW_TOOL_CALLS, true),
|
|
190
|
+
showEditDiffs: bool(process.env.SHOW_EDIT_DIFFS, true),
|
|
191
|
+
diffMaxLines: num(process.env.DIFF_MAX_LINES, 120),
|
|
192
|
+
sendAgentImages: bool(process.env.SEND_AGENT_IMAGES, true),
|
|
193
|
+
agentImagesMax: num(process.env.AGENT_IMAGES_MAX, 8),
|
|
194
|
+
docMaxChars: nonNegNum(process.env.DOC_MAX_CHARS, 100_000),
|
|
195
|
+
logLevel: process.env.LOG_LEVEL?.trim() || "info",
|
|
196
|
+
sessionsDir,
|
|
197
|
+
projectRoot: PROJECT_ROOT,
|
|
198
|
+
logsDir,
|
|
199
|
+
logFile,
|
|
200
|
+
grokAutoRestart: bool(process.env.GROK_AUTO_RESTART, true),
|
|
201
|
+
promptIdleMs: num(process.env.PROMPT_IDLE_TIMEOUT_MS, 900_000),
|
|
202
|
+
quietNotifications: bool(process.env.QUIET_NOTIFICATIONS, true),
|
|
203
|
+
promptRetryAttempts: nonNegNum(process.env.PROMPT_RETRY_ATTEMPTS, 5),
|
|
204
|
+
autoForkOnError: bool(process.env.AUTO_FORK_ON_ERROR, true),
|
|
205
|
+
autoForkContextPct: nonNegNum(process.env.AUTO_FORK_CONTEXT_PCT, 85),
|
|
206
|
+
resumeOnStreamError: bool(process.env.RESUME_ON_STREAM_ERROR, true),
|
|
207
|
+
dataDir,
|
|
208
|
+
sttApiUrl: process.env.STT_API_URL?.trim() || undefined,
|
|
209
|
+
sttApiKey: process.env.STT_API_KEY?.trim() || undefined,
|
|
210
|
+
sttModel: process.env.STT_MODEL?.trim() || "whisper-1",
|
|
211
|
+
sttLanguage: process.env.STT_LANGUAGE?.trim() || undefined,
|
|
212
|
+
mcpProbeTimeoutMs: num(process.env.MCP_PROBE_TIMEOUT_MS, 8000),
|
|
213
|
+
mcpProbeConcurrency: num(process.env.MCP_PROBE_CONCURRENCY, 6),
|
|
214
|
+
showSubagents: bool(process.env.SHOW_SUBAGENTS, true),
|
|
215
|
+
showProgress: bool(process.env.SHOW_PROGRESS, true),
|
|
216
|
+
progressFallback: bool(process.env.PROGRESS_FALLBACK, true),
|
|
217
|
+
notifyOtherSessions: bool(process.env.NOTIFY_OTHER_SESSIONS, true),
|
|
218
|
+
autoUpdate: bool(process.env.AUTO_UPDATE, true),
|
|
219
|
+
updateCheckMs: num(process.env.UPDATE_CHECK_MS, 3_600_000),
|
|
220
|
+
singleInstance: bool(process.env.GROK_TG_SINGLE_INSTANCE, true),
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return cfg;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Resolve the `grok` binary path. The official installer puts it in
|
|
227
|
+
* ~/.grok/bin; also try common PATH locations before a bare `grok`. */
|
|
228
|
+
function resolveGrokPath(explicit?: string): string {
|
|
229
|
+
if (explicit) return expandHome(explicit);
|
|
230
|
+
|
|
231
|
+
const home = homedir();
|
|
232
|
+
const exe = process.platform === "win32" ? "grok.exe" : "grok";
|
|
233
|
+
const candidates = [
|
|
234
|
+
join(home, ".grok", "bin", exe),
|
|
235
|
+
join(home, ".local", "bin", "grok"),
|
|
236
|
+
"/usr/local/bin/grok",
|
|
237
|
+
"/opt/homebrew/bin/grok",
|
|
238
|
+
];
|
|
239
|
+
for (const c of candidates) {
|
|
240
|
+
if (existsSync(c)) return c;
|
|
241
|
+
}
|
|
242
|
+
// Fall back to PATH lookup.
|
|
243
|
+
return "grok";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function isAbsolutePath(p: string): boolean {
|
|
247
|
+
return isAbsolute(p);
|
|
248
|
+
}
|