grok-telegram-bot 2.5.0 → 2.7.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 +13 -0
- package/CHANGELOG.md +106 -0
- package/README.md +20 -5
- package/docs/GROUP.md +39 -4
- package/docs/INSTALL.md +2 -0
- package/package.json +4 -4
- package/scripts/setup.mjs +20 -3
- package/src/app/instance.ts +223 -0
- package/src/app/types.ts +34 -1
- package/src/bot/ask-user-service.ts +226 -0
- package/src/bot/auth.ts +5 -1
- package/src/bot/bot.ts +105 -4
- package/src/bot/chat-controller.ts +129 -0
- package/src/bot/commands.ts +22 -2
- package/src/bot/group-memory.ts +192 -12
- package/src/bot/handlers/forum.ts +16 -6
- package/src/bot/handlers/grok-slash.ts +336 -0
- package/src/bot/handlers/message.ts +165 -25
- package/src/bot/handlers/photo.ts +4 -1
- package/src/bot/handlers/system.ts +63 -1
- package/src/bot/image-return.ts +4 -1
- package/src/bot/manager-context.ts +208 -0
- package/src/bot/manager-jobs.ts +142 -0
- package/src/bot/menu/ephemeral.ts +4 -1
- package/src/bot/plan-exit-service.ts +169 -0
- package/src/bot/prompt-anchor.ts +2 -3
- package/src/bot/prompt-content.ts +5 -0
- package/src/bot/registry.ts +11 -2
- package/src/bot/scope.ts +9 -8
- package/src/bot/session-runtime.ts +665 -55
- package/src/bot/telegram-actions.ts +728 -38
- package/src/bot/telegram-bots.ts +2 -1
- package/src/bot/telegram-io.ts +4 -1
- package/src/cli.ts +43 -7
- package/src/config.ts +35 -25
- package/src/forum/manager.ts +2 -1
- package/src/forum/thread.ts +33 -0
- package/src/grok/client.ts +29 -5
- package/src/grok/plan-approval.ts +8 -0
- package/src/index.ts +4 -0
- package/src/render/manager-directive.ts +137 -0
- package/src/render/session-comment.ts +10 -0
- package/src/render/telegram-bridge.ts +118 -14
- package/src/service/linux.ts +21 -15
- package/src/service/macos.ts +20 -15
- package/src/service/platform.ts +12 -3
- package/src/service/types.ts +6 -0
- package/src/service/windows.ts +31 -22
- package/src/sessions/history.ts +18 -0
- package/src/stream/streamer.ts +46 -10
package/.env.example
CHANGED
|
@@ -40,6 +40,19 @@ GROK_TRUST_ALL_TOOLS=true
|
|
|
40
40
|
# Approve/Deny buttons in Telegram.
|
|
41
41
|
AUTO_APPROVE_PERMISSIONS=true
|
|
42
42
|
|
|
43
|
+
# Auto-approve Grok plan-mode exit (no Approve / Request changes / Abandon).
|
|
44
|
+
# Default true so 24/7 unattended bots never wait. Set false for Telegram review.
|
|
45
|
+
# AUTO_APPROVE_PLAN=true
|
|
46
|
+
|
|
47
|
+
# Optional Grok agent env (applied on process start / after /restart).
|
|
48
|
+
# GROK_SANDBOX=workspace-safe
|
|
49
|
+
# GROK_MEMORY=
|
|
50
|
+
# GROK_AGENT_PROFILE=
|
|
51
|
+
# GROK_PLUGIN_DIR=
|
|
52
|
+
|
|
53
|
+
# Named instance slug (same as grok-tg --name). Optional.
|
|
54
|
+
# GROK_TG_NAME=
|
|
55
|
+
|
|
43
56
|
# Comma-separated roots the /projects browser is allowed to list.
|
|
44
57
|
# Supports ~ for home directory. Defaults to GROK_WORKSPACE's parent + home.
|
|
45
58
|
# Example: H:\Lucru\Domains,C:\Lucru\Domains
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,110 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
The latest section is published verbatim as the GitHub Release notes by
|
|
8
8
|
`.github/workflows/release.yml` when a `vX.Y.Z` tag is pushed.
|
|
9
9
|
|
|
10
|
+
## [2.7.0] - 2026-08-30
|
|
11
|
+
|
|
12
|
+
Contributor batch from [@vincenthehna](https://github.com/vincenthehna) (PRs
|
|
13
|
+
#6–#8), ported onto current main and credited here. PR #9 (one process / many
|
|
14
|
+
tokens) was **not** merged: it conflicts with forum General + `TOPIC_GROUP_ID`
|
|
15
|
+
(which bot owns the group?) and with the named-instance model.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Grok Build slash catalog on Telegram** (PR #6 / #7). `/goal`, `/plan`,
|
|
20
|
+
`/compact`, `/workflow(s)`, `/deep_research`, memory/imagine commands, and
|
|
21
|
+
other ACP-useful shell builtins are advertised (menu ≤100) and forwarded into
|
|
22
|
+
the active session (`executeCommand` then prompt fallback). Underscores map
|
|
23
|
+
to Grok hyphens; collision aliases (`/grok_new`, `/memory_flush`, …) keep
|
|
24
|
+
bot-owned bare names. `/goal` is refused in **General** (use a project topic
|
|
25
|
+
or AI Chat). Catch-all forwards unknown Grok/skills slashes.
|
|
26
|
+
- **Named instances** (PR #8). `grok-tg --name work setup|install|status`
|
|
27
|
+
gives each BotFather token its own `~/.grok/tg/instances/<slug>/` and unique
|
|
28
|
+
OS service (`grok-telegram-bot-work`). `grok-tg instances` lists them. Default
|
|
29
|
+
bot and `TELEGRAM_BOT_TOKEN` are unchanged.
|
|
30
|
+
- **Optional interactive plan review** (PR #7). `AUTO_APPROVE_PLAN=false`
|
|
31
|
+
shows Approve / Request changes / Abandon (timeout still unblocks). Default
|
|
32
|
+
remains auto-approve so 24/7 unattended bots never wait. `ask_user_question`
|
|
33
|
+
gets Telegram buttons unless auto-skip is on.
|
|
34
|
+
- **`/sandbox`** plus spawn env: `GROK_SANDBOX`, `GROK_MEMORY`,
|
|
35
|
+
`--agent-profile`, `--plugin-dir`. `session/new` sets `_meta.yoloMode` when
|
|
36
|
+
tools are trusted.
|
|
37
|
+
- **CI workflow** (`typecheck` job) so `main` can require status checks.
|
|
38
|
+
|
|
39
|
+
### Docs
|
|
40
|
+
|
|
41
|
+
- README / INSTALL: several bots on one host via `--name`.
|
|
42
|
+
- AGENTS.md: CLI instances.
|
|
43
|
+
|
|
44
|
+
## [2.6.0] - 2026-08-30
|
|
45
|
+
|
|
46
|
+
General becomes a chat-like **manager** for the forum group: memory-first
|
|
47
|
+
routing, `send_prompt` into project topics with job report-back, and **visible
|
|
48
|
+
short replies** (the previous notify-only quiet path looked like the bot was
|
|
49
|
+
dead). Also bumps runtime deps (`tsx`, `grammy`).
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **General manager mode.** The forum **General** topic (`thread id 1`) is an
|
|
54
|
+
OpenClaw-style orchestrator, not a coding workspace. It keeps user messages,
|
|
55
|
+
starts a **new parallel session** per message (reply-to continues the same
|
|
56
|
+
session), streams **prose only** (no tools / progress / Done spam), and
|
|
57
|
+
dispatches real work into project topics.
|
|
58
|
+
- **Manager directive + auto context.** First General prompt injects how to
|
|
59
|
+
behave (delegate, memory-first, no git-first). Every user turn also gets a
|
|
60
|
+
capped **MANAGER CONTEXT** block: topic catalog, recent General chat, ranked
|
|
61
|
+
memory hits, and in-process manager jobs.
|
|
62
|
+
- **Manager jobs + work reports.** `send_prompt` from General registers a job
|
|
63
|
+
(`list_jobs`). When the child topic turn finishes, General is woken with a
|
|
64
|
+
structured `MANAGER WORK REPORT` so the manager can tell you done / failed.
|
|
65
|
+
- **Bridge: `notify`, `list_topics`, `list_jobs`, `session_id`.**
|
|
66
|
+
- `notify` — optional extra ping (not required in General; prose is shown).
|
|
67
|
+
- `list_topics` / `list_jobs` — catalog and dispatch status for the agent.
|
|
68
|
+
- `send_prompt.session_id` — resume a **specific** Grok session (full UUID or
|
|
69
|
+
short prefix from memory). Topic may be omitted; path → forum topic is
|
|
70
|
+
inferred. Placeholder topics (`…`) are rejected / inferred from memory.
|
|
71
|
+
- **Group memory ranking.** `search_memory` scores **relevance + recency**
|
|
72
|
+
(newest session per project path wins) so “last modifications” does not
|
|
73
|
+
surface stale Done notes. Hits include `[Xm/h/d ago]` stamps.
|
|
74
|
+
- **Outbound General thread helper.** Telegram rejects `message_thread_id: 1`.
|
|
75
|
+
Sends/edits to General omit that field (`outboundThreadExtra`); inbound
|
|
76
|
+
routing still treats missing thread id as General.
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- **General UX.** User messages are **kept** (not replaced by prompt anchors).
|
|
81
|
+
Replies thread to the user message. A `Starting…` / `Thinking…` bubble is
|
|
82
|
+
edited in place into the short answer. Independent General messages no longer
|
|
83
|
+
coalesce into one queued turn.
|
|
84
|
+
- **Prose is the user-facing channel.** The manager is told the user **sees**
|
|
85
|
+
free-form chat text. `notify` is optional. Bridge-result follow-ups (after
|
|
86
|
+
`search_memory` / `list_topics`) stream the real answer instead of staying
|
|
87
|
+
silent. If the model emits no prose, the Thinking bubble is replaced with a
|
|
88
|
+
short fallback — never deleted leaving an empty chat.
|
|
89
|
+
- **Streamer prose-only mode.** General streams agent text only (no thoughts,
|
|
90
|
+
tools, plan board, or progress bar), seeded on the Thinking placeholder.
|
|
91
|
+
- **Forum send path.** `sendMessage` / stream edits / auth deny / prompt
|
|
92
|
+
anchors / photos / docs use `outboundThreadExtra` so General posts succeed.
|
|
93
|
+
- **Dependencies.** `tsx` `^4.19.2` → `^4.23.13`; `grammy` and `@types/node`
|
|
94
|
+
updated to current 1.x / 22.x wanted versions.
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- **Bot not responding in General after manager-mode work.** Notify-only +
|
|
99
|
+
deleting `Thinking…` when `search_memory` chained made every answer vanish.
|
|
100
|
+
General now streams short prose, keeps the status bubble across bridge
|
|
101
|
+
follow-ups, and never drops the placeholder without a visible line.
|
|
102
|
+
- **`message_thread_id: 1` rejected by Telegram** (“message thread not found”)
|
|
103
|
+
on General sends — omit the field for General; keep it for real topics.
|
|
104
|
+
- **Manager context path trap.** Recent General history no longer treats every
|
|
105
|
+
session under the workspace root as General chat (title + exact cwd only).
|
|
106
|
+
|
|
107
|
+
### Docs
|
|
108
|
+
|
|
109
|
+
- **[docs/GROUP.md](./docs/GROUP.md)** — General = manager, memory-first
|
|
110
|
+
dispatch, `session_id`, `list_jobs`, prose vs `notify`.
|
|
111
|
+
- **AGENTS.md** — General manager conventions for future changes.
|
|
112
|
+
- **README** — General manager + expanded bridge actions.
|
|
113
|
+
|
|
10
114
|
## [2.5.0] - 2026-08-05
|
|
11
115
|
|
|
12
116
|
Forum project topics, cross-topic Telegram bridge, prompt anchors, and a more
|
|
@@ -931,6 +1035,8 @@ from a single chat and switch between them, on a redesigned, compact menu.
|
|
|
931
1035
|
diffs, MarkdownV2 rendering, scheduled tasks, multi-image prompts, and a
|
|
932
1036
|
cross-platform 24/7 background service.
|
|
933
1037
|
|
|
1038
|
+
[2.7.0]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.7.0
|
|
1039
|
+
[2.6.0]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.6.0
|
|
934
1040
|
[2.5.0]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.5.0
|
|
935
1041
|
[2.4.0]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.4.0
|
|
936
1042
|
[2.3.1]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.3.1
|
package/README.md
CHANGED
|
@@ -27,8 +27,9 @@ re-architected for the Grok Build CLI and extended into a full multi-session cli
|
|
|
27
27
|
| Capability | What it does |
|
|
28
28
|
|---|---|
|
|
29
29
|
| 🗂 **Projects** | `/projects` browses your folders and runs Grok in the one you pick. |
|
|
30
|
-
| 🏷 **Forum project topics** | Optional forum supergroup:
|
|
31
|
-
|
|
|
30
|
+
| 🏷 **Forum project topics** | Optional forum supergroup: **General** manager, **AI Chat** workspace, one topic per project, exact-name bind, `/forum_setup`. See **[docs/GROUP.md](./docs/GROUP.md)**. |
|
|
31
|
+
| 🎛 **General manager** | Forum General is a chat-like orchestrator: memory-first, short replies, `send_prompt` into project topics, report-back when child work finishes. |
|
|
32
|
+
| 🌉 **Telegram bridge** | Agent can `create_topic`, `set_path`, `send_prompt` (optional `session_id`), `notify`, `search_memory`, `list_topics` / `list_jobs`, and call allowlisted sibling bots via JSON actions. |
|
|
32
33
|
| 🔖 **Prompt anchors** | Each user prompt is re-posted with a searchable `#prompt_…` tag; stream/Done messages reply to that anchor (media re-attached). |
|
|
33
34
|
| 🆕 **New session** | `/new` or the bar **New** button starts a fresh session; forum topics keep New on the topic menu. |
|
|
34
35
|
| 💡 **Post-turn suggestions** | After Done, 1–3 scored follow-ups as buttons; high-score items can auto-queue as one multi-step prompt. |
|
|
@@ -66,7 +67,8 @@ re-architected for the Grok Build CLI and extended into a full multi-session cli
|
|
|
66
67
|
| Connect Grok CLI to Telegram (ACP) | ✅ | ✅ |
|
|
67
68
|
| Switch between projects | ✅ | ❌ |
|
|
68
69
|
| **Forum topics = projects** + AI Chat workspace | ✅ | ❌ |
|
|
69
|
-
| **
|
|
70
|
+
| **General manager** (memory-first, dispatch + report-back) | ✅ | ❌ |
|
|
71
|
+
| **Cross-topic agent bridge** (create / bind / send_prompt / session_id) | ✅ | ❌ |
|
|
70
72
|
| Resume saved sessions | ✅ | ❌ |
|
|
71
73
|
| Attach to **live** PC sessions (watch / fork) | ✅ | ❌ |
|
|
72
74
|
| **Kill a session by PID** (or all at once) | ✅ | ❌ |
|
|
@@ -118,8 +120,21 @@ ghost/duplicate that was still polling Telegram (the usual cause of a stale
|
|
|
118
120
|
"⛔ Not authorized"), so the fresh process with your current `.env` wins. A
|
|
119
121
|
plain `grok-tg run` yields to an already-running background service instead.
|
|
120
122
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
Want a **second Telegram bot** (one private chat per project) without switching
|
|
124
|
+
sessions? Create another bot in @BotFather and:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
grok-tg --name work setup <NEW_BOT_TOKEN> <YOUR_USER_ID>
|
|
128
|
+
grok-tg --name work install
|
|
129
|
+
grok-tg instances
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Each named instance has its own `~/.grok/tg/instances/<slug>/` and unique
|
|
133
|
+
service (`grok-telegram-bot-work`). The default bot is unchanged.
|
|
134
|
+
|
|
135
|
+
Startup options: `grok-tg [--name <slug>] setup [--path] | run | install |
|
|
136
|
+
status | logs [n] | stop | restart | uninstall | instances`. Or try it without
|
|
137
|
+
installing: `npx
|
|
123
138
|
grok-telegram-bot setup`. See **[docs/INSTALL.md](./docs/INSTALL.md)** for the
|
|
124
139
|
full guide.
|
|
125
140
|
|
package/docs/GROUP.md
CHANGED
|
@@ -67,11 +67,24 @@ When the bot is later promoted to admin, it re-probes automatically (`my_chat_me
|
|
|
67
67
|
|
|
68
68
|
| Topic | Working directory | Typical use |
|
|
69
69
|
|---|---|---|
|
|
70
|
-
| **General**
|
|
70
|
+
| **General** | `GROK_WORKSPACE` | **Manager chat** (OpenClaw-style): routes work, memory-first, no coding spam |
|
|
71
|
+
| **AI Chat** | `GROK_WORKSPACE` | Normal coding/conversation in the workspace |
|
|
71
72
|
| **Project topic** | Bound project path | All coding work for that folder |
|
|
72
73
|
| **User-created topic** | Bound after name/path match | Ad-hoc projects or new folders |
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
### General = manager
|
|
76
|
+
|
|
77
|
+
Messages in **General** drive a chat-like orchestrator, not a coding agent:
|
|
78
|
+
|
|
79
|
+
1. User asks in General (e.g. “fix login in MyApp”).
|
|
80
|
+
2. Bot uses **memory + topic catalog** (auto-injected) and may call `search_memory` / `list_topics`.
|
|
81
|
+
3. It replies briefly (“OK — I’ll start … in **MyApp**”) with **no progress bars / tool dumps**.
|
|
82
|
+
4. It **dispatches** via `create_topic` / `set_path` / `send_prompt` into the right project topic.
|
|
83
|
+
5. When that child turn finishes, the bridge **wakes General** with a `MANAGER WORK REPORT` so the manager summarizes success/fail for you.
|
|
84
|
+
|
|
85
|
+
Real implementation stays in **project topics**. General should not edit app code.
|
|
86
|
+
|
|
87
|
+
Messages you send **inside a project topic** are prompts for a session whose `cwd` is
|
|
75
88
|
that topic’s path. Menus, model/reasoning picks, `/sessions`, `/running`, and
|
|
76
89
|
Stop are **topic-scoped** so one project does not steal another’s session.
|
|
77
90
|
|
|
@@ -148,8 +161,11 @@ may feed results back as a quiet system turn (not a second Done).
|
|
|
148
161
|
|---|---|
|
|
149
162
|
| `create_topic` | New forum topic; optional `path` binds immediately |
|
|
150
163
|
| `set_path` | Bind/rebind topic by title or `#threadId` |
|
|
151
|
-
| `send_prompt` | Inject a prompt into another topic (`ran` / `queued`; optional `new_session
|
|
152
|
-
| `
|
|
164
|
+
| `send_prompt` | Inject a prompt into another topic (`ran` / `queued`; optional `new_session`, **`session_id`** to resume a specific session) |
|
|
165
|
+
| `notify` | Optional extra ping. In **General** the user already sees short chat prose; `notify` is not required. |
|
|
166
|
+
| `search_memory` | Search topic + session indexes (memory-first) |
|
|
167
|
+
| `list_topics` | List mapped forum topics (name, `#id`, path) |
|
|
168
|
+
| `list_jobs` | List recent General → project dispatches |
|
|
153
169
|
| `list_bots` | List allowlisted sibling bots |
|
|
154
170
|
| `bot_command` | Call `/command@bot` and wait for that bot to settle |
|
|
155
171
|
|
|
@@ -174,6 +190,25 @@ Example (from General / AI Chat):
|
|
|
174
190
|
|
|
175
191
|
`topic` may be the exact title, `#threadId`, `general`, or `ai chat`.
|
|
176
192
|
|
|
193
|
+
To **resume a related session** found via memory (not the topic's currently open session):
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"telegram": [
|
|
198
|
+
{
|
|
199
|
+
"action": "send_prompt",
|
|
200
|
+
"topic": "MyApp",
|
|
201
|
+
"session_id": "019fc9ec",
|
|
202
|
+
"prompt": "Continue: apply the follow-up fix."
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`session_id` may be a full UUID or a short prefix from memory hits. Without it, the bridge uses the topic's foreground session.
|
|
209
|
+
|
|
210
|
+
`topic` must be an **exact** mapped title or `#threadId` (never placeholders like `…`). If only the session is known, omit `topic` — the bridge maps `session.cwd` → forum topic.
|
|
211
|
+
|
|
177
212
|
---
|
|
178
213
|
|
|
179
214
|
## Sibling bots (optional)
|
package/docs/INSTALL.md
CHANGED
|
@@ -56,6 +56,8 @@ grok-tg run # run in the foreground (Ctrl-C to stop)
|
|
|
56
56
|
| `grok-tg logs [n]` | Tail the last `n` log lines (default 100). |
|
|
57
57
|
| `grok-tg stop` / `restart` / `start` | Control the running service. |
|
|
58
58
|
| `grok-tg uninstall` | Stop + remove the background service. |
|
|
59
|
+
| `grok-tg --name <slug> …` | Same commands for a **second Telegram bot** (own token, own chat, own service). |
|
|
60
|
+
| `grok-tg instances` | List the default bot and every named instance. |
|
|
59
61
|
| `grok-tg help` | Show all commands. |
|
|
60
62
|
|
|
61
63
|
The background service is **user-level** and auto-detected per platform — a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grok-telegram-bot",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "Control the official Grok Build CLI from Telegram over the Agent Client Protocol (ACP). Sign in with your xAI account, switch projects, resume sessions, stream responses with diffs, queue follow-ups, manage multiple sign-ins, and run 24/7 as a cross-platform background service.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -60,15 +60,15 @@
|
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"diff": "^7.0.0",
|
|
62
62
|
"dotenv": "^16.4.7",
|
|
63
|
-
"grammy": "^1.
|
|
64
|
-
"tsx": "^4.
|
|
63
|
+
"grammy": "^1.46.0",
|
|
64
|
+
"tsx": "^4.23.13"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
67
67
|
"@homebridge/node-pty-prebuilt-multiarch": "0.13.1"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@types/diff": "^7.0.0",
|
|
71
|
-
"@types/node": "^22.
|
|
71
|
+
"@types/node": "^22.20.1",
|
|
72
72
|
"typescript": "^5.7.2"
|
|
73
73
|
}
|
|
74
74
|
}
|
package/scripts/setup.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Easy setup: creates/updates the bot's .env, auto-detects the `grok` binary
|
|
4
4
|
* and sensible PROJECT_ROOTS, and optionally writes the bot token / user id:
|
|
5
5
|
*
|
|
6
|
-
* node scripts/setup.mjs [--path] [--instance <dir>] [<
|
|
6
|
+
* node scripts/setup.mjs [--path] [--instance <dir>] [--name <slug>]
|
|
7
|
+
* [<TELEGRAM_BOT_TOKEN> [ALLOWED_USER_ID]]
|
|
7
8
|
*
|
|
8
9
|
* By default the .env lives in the canonical, path-independent home
|
|
9
10
|
* `~/.grok/tg/.env`, so the bot loads the SAME config no matter where it's
|
|
@@ -29,8 +30,23 @@ function expandHome(p) {
|
|
|
29
30
|
function resolveInstanceDir() {
|
|
30
31
|
const flag = process.argv.indexOf("--instance");
|
|
31
32
|
if (flag !== -1 && process.argv[flag + 1]) return resolve(process.argv[flag + 1]);
|
|
32
|
-
const
|
|
33
|
+
const nameEq = process.argv.find((a) => a.startsWith("--name="));
|
|
34
|
+
const nameFlag = process.argv.indexOf("--name");
|
|
35
|
+
const name = nameEq
|
|
36
|
+
? nameEq.slice("--name=".length)
|
|
37
|
+
: nameFlag !== -1
|
|
38
|
+
? process.argv[nameFlag + 1]
|
|
39
|
+
: process.env.GROK_TG_NAME;
|
|
40
|
+
if (name && String(name).trim()) {
|
|
41
|
+
return join(CANONICAL_DIR, "instances", String(name).trim().toLowerCase());
|
|
42
|
+
}
|
|
43
|
+
const envDir = (process.env.GROK_TG_DIR || "").trim();
|
|
33
44
|
if (envDir) return resolve(expandHome(envDir));
|
|
45
|
+
const cwdHint = (process.env.GROK_TG_CWD || "").trim();
|
|
46
|
+
if (cwdHint) {
|
|
47
|
+
const hint = resolve(expandHome(cwdHint));
|
|
48
|
+
if (existsSync(join(hint, ".env"))) return hint;
|
|
49
|
+
}
|
|
34
50
|
if (existsSync(join(process.cwd(), ".env"))) return process.cwd();
|
|
35
51
|
return CANONICAL_DIR;
|
|
36
52
|
}
|
|
@@ -41,7 +57,8 @@ const positionals = [];
|
|
|
41
57
|
for (let i = 0; i < argv.length; i++) {
|
|
42
58
|
const a = argv[i];
|
|
43
59
|
if (a === "--path") pathOnly = true;
|
|
44
|
-
else if (a === "--instance") i++;
|
|
60
|
+
else if (a === "--instance" || a === "--name") i++;
|
|
61
|
+
else if (a.startsWith("--instance=") || a.startsWith("--name=")) continue;
|
|
45
62
|
else positionals.push(a);
|
|
46
63
|
}
|
|
47
64
|
const [tokenArg, userArg] = positionals;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named bot instances: each Telegram bot token gets its own directory
|
|
3
|
+
* (`.env`, `logs/`, `data/`) and a unique OS service name so several bots
|
|
4
|
+
* can run 24/7 on one host without overwriting each other.
|
|
5
|
+
*
|
|
6
|
+
* The unnamed default stays at `~/.grok/tg` with the original service id.
|
|
7
|
+
* `grok-tg --name work …` uses `~/.grok/tg/instances/work` and
|
|
8
|
+
* `grok-telegram-bot-work`.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { basename, join, resolve } from "node:path";
|
|
13
|
+
|
|
14
|
+
/** Canonical home for the default (unnamed) instance. */
|
|
15
|
+
export const CANONICAL_DIR = join(homedir(), ".grok", "tg");
|
|
16
|
+
export const INSTANCES_SUBDIR = "instances";
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_SERVICE_ID = "grok-telegram-bot";
|
|
19
|
+
export const DEFAULT_WINDOWS_TASK = "GrokTelegramBot";
|
|
20
|
+
export const DEFAULT_MACOS_LABEL = "com.grok.telegrambot";
|
|
21
|
+
|
|
22
|
+
const RESERVED = new Set(["default", "instances", "locks", "logs", "data"]);
|
|
23
|
+
const SLUG_RE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
24
|
+
|
|
25
|
+
export interface InstanceFlags {
|
|
26
|
+
instanceDir?: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ServiceIdentity {
|
|
31
|
+
id: string;
|
|
32
|
+
displayName: string;
|
|
33
|
+
windowsTaskName: string;
|
|
34
|
+
macosLabel: string;
|
|
35
|
+
slug?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface KnownInstance {
|
|
39
|
+
/** `(default)` or the instance slug. */
|
|
40
|
+
name: string;
|
|
41
|
+
dir: string;
|
|
42
|
+
slug?: string;
|
|
43
|
+
identity: ServiceIdentity;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function expandHome(p: string): string {
|
|
47
|
+
if (p === "~") return homedir();
|
|
48
|
+
if (p.startsWith("~/") || p.startsWith("~\\")) return join(homedir(), p.slice(2));
|
|
49
|
+
return p;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseInstanceFlags(argv: string[]): InstanceFlags {
|
|
53
|
+
const out: InstanceFlags = {};
|
|
54
|
+
for (let i = 0; i < argv.length; i++) {
|
|
55
|
+
const a = argv[i];
|
|
56
|
+
if (!a) continue;
|
|
57
|
+
if ((a === "--instance" || a === "--name") && argv[i + 1]) {
|
|
58
|
+
const value = argv[++i]!;
|
|
59
|
+
if (a === "--instance") out.instanceDir = value;
|
|
60
|
+
else out.name = value;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (a.startsWith("--instance=")) out.instanceDir = a.slice("--instance=".length);
|
|
64
|
+
else if (a.startsWith("--name=")) out.name = a.slice("--name=".length);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Drop `--instance` / `--name` (and their values) so the CLI command remains first. */
|
|
70
|
+
export function stripInstanceFlags(argv: string[]): string[] {
|
|
71
|
+
const out: string[] = [];
|
|
72
|
+
for (let i = 0; i < argv.length; i++) {
|
|
73
|
+
const a = argv[i];
|
|
74
|
+
if (!a) continue;
|
|
75
|
+
if (a === "--instance" || a === "--name") {
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (a.startsWith("--instance=") || a.startsWith("--name=")) continue;
|
|
80
|
+
out.push(a);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function parseInstanceSlug(raw: string): string {
|
|
86
|
+
const slug = raw.trim().toLowerCase();
|
|
87
|
+
if (!SLUG_RE.test(slug)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Invalid instance name "${raw}". Use 1–32 characters: start with a letter, then letters, digits or hyphens (e.g. work, home, coding).`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (RESERVED.has(slug)) {
|
|
93
|
+
throw new Error(`Instance name "${slug}" is reserved. Pick another name.`);
|
|
94
|
+
}
|
|
95
|
+
return slug;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function namedInstanceDir(name: string, canonicalDir = CANONICAL_DIR): string {
|
|
99
|
+
return join(canonicalDir, INSTANCES_SUBDIR, parseInstanceSlug(name));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve this process's instance directory.
|
|
104
|
+
*
|
|
105
|
+
* Order: `--instance` → `--name` / `GROK_TG_NAME` → `GROK_TG_DIR` → a
|
|
106
|
+
* `GROK_TG_CWD` or cwd that already contains `.env` → `~/.grok/tg`.
|
|
107
|
+
*
|
|
108
|
+
* `GROK_TG_CWD` is only a "user launched from here" hint (the `grok-tg`
|
|
109
|
+
* launcher always sets it). It must not override `--name`, otherwise a
|
|
110
|
+
* named instance could never be selected.
|
|
111
|
+
*/
|
|
112
|
+
export function resolveInstanceDir(opts: {
|
|
113
|
+
argv?: string[];
|
|
114
|
+
envDir?: string;
|
|
115
|
+
nameEnv?: string;
|
|
116
|
+
cwdHint?: string;
|
|
117
|
+
cwd?: string;
|
|
118
|
+
canonicalDir?: string;
|
|
119
|
+
} = {}): string {
|
|
120
|
+
const canonicalDir = opts.canonicalDir ?? CANONICAL_DIR;
|
|
121
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
122
|
+
const flags = parseInstanceFlags(opts.argv ?? process.argv);
|
|
123
|
+
if (flags.instanceDir?.trim()) return resolve(expandHome(flags.instanceDir.trim()));
|
|
124
|
+
const name = flags.name?.trim() || opts.nameEnv?.trim();
|
|
125
|
+
if (name) return namedInstanceDir(name, canonicalDir);
|
|
126
|
+
if (opts.envDir?.trim()) return resolve(expandHome(opts.envDir.trim()));
|
|
127
|
+
const hinted = opts.cwdHint?.trim();
|
|
128
|
+
if (hinted) {
|
|
129
|
+
const hint = resolve(expandHome(hinted));
|
|
130
|
+
if (existsSync(join(hint, ".env"))) return hint;
|
|
131
|
+
}
|
|
132
|
+
if (existsSync(join(cwd, ".env"))) return cwd;
|
|
133
|
+
return canonicalDir;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* OS service names for this instance.
|
|
138
|
+
*
|
|
139
|
+
* Unique names are used only for `--name` / `~/.grok/tg/instances/<slug>` so
|
|
140
|
+
* an existing default install (`grok-telegram-bot.service`) is unchanged.
|
|
141
|
+
* A random folder with a `.env` keeps the default service id.
|
|
142
|
+
*/
|
|
143
|
+
export function serviceIdentity(
|
|
144
|
+
instanceDir: string,
|
|
145
|
+
name?: string,
|
|
146
|
+
canonicalDir = CANONICAL_DIR,
|
|
147
|
+
): ServiceIdentity {
|
|
148
|
+
const slug = name?.trim()
|
|
149
|
+
? parseInstanceSlug(name)
|
|
150
|
+
: inferNamedInstanceSlug(instanceDir, canonicalDir);
|
|
151
|
+
if (!slug) {
|
|
152
|
+
return {
|
|
153
|
+
id: DEFAULT_SERVICE_ID,
|
|
154
|
+
displayName: "Grok Telegram Bot",
|
|
155
|
+
windowsTaskName: DEFAULT_WINDOWS_TASK,
|
|
156
|
+
macosLabel: DEFAULT_MACOS_LABEL,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
id: `${DEFAULT_SERVICE_ID}-${slug}`,
|
|
161
|
+
displayName: `Grok Telegram Bot (${slug})`,
|
|
162
|
+
windowsTaskName: `${DEFAULT_WINDOWS_TASK}-${slug}`,
|
|
163
|
+
macosLabel: `${DEFAULT_MACOS_LABEL}.${slug}`,
|
|
164
|
+
slug,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function listKnownInstances(canonicalDir = CANONICAL_DIR): KnownInstance[] {
|
|
169
|
+
const out: KnownInstance[] = [];
|
|
170
|
+
if (existsSync(join(canonicalDir, ".env"))) {
|
|
171
|
+
out.push({
|
|
172
|
+
name: "(default)",
|
|
173
|
+
dir: canonicalDir,
|
|
174
|
+
identity: serviceIdentity(canonicalDir, undefined, canonicalDir),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const root = join(canonicalDir, INSTANCES_SUBDIR);
|
|
178
|
+
if (!existsSync(root)) return out;
|
|
179
|
+
let names: string[] = [];
|
|
180
|
+
try {
|
|
181
|
+
names = readdirSync(root, { withFileTypes: true })
|
|
182
|
+
.filter((d) => d.isDirectory())
|
|
183
|
+
.map((d) => d.name)
|
|
184
|
+
.sort();
|
|
185
|
+
} catch {
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
for (const name of names) {
|
|
189
|
+
const dir = join(root, name);
|
|
190
|
+
if (!existsSync(join(dir, ".env"))) continue;
|
|
191
|
+
out.push({
|
|
192
|
+
name,
|
|
193
|
+
dir,
|
|
194
|
+
slug: name,
|
|
195
|
+
identity: serviceIdentity(dir, undefined, canonicalDir),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Basename when `instanceDir` is a direct child of `<canonical>/instances`. */
|
|
202
|
+
function namedChildName(instanceDir: string, canonicalDir: string): string | undefined {
|
|
203
|
+
const resolved = resolve(instanceDir);
|
|
204
|
+
const parent = resolve(join(resolved, ".."));
|
|
205
|
+
const root = resolve(join(canonicalDir, INSTANCES_SUBDIR));
|
|
206
|
+
if (parent !== root) return undefined;
|
|
207
|
+
return basename(resolved);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function inferNamedInstanceSlug(instanceDir: string, canonicalDir: string): string | undefined {
|
|
211
|
+
const child = namedChildName(instanceDir, canonicalDir);
|
|
212
|
+
if (!child) return undefined;
|
|
213
|
+
try {
|
|
214
|
+
return parseInstanceSlug(child);
|
|
215
|
+
} catch {
|
|
216
|
+
const fallback = child
|
|
217
|
+
.toLowerCase()
|
|
218
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
219
|
+
.replace(/^-+|-+$/g, "")
|
|
220
|
+
.slice(0, 32);
|
|
221
|
+
return fallback && SLUG_RE.test(fallback) && !RESERVED.has(fallback) ? fallback : "extra";
|
|
222
|
+
}
|
|
223
|
+
}
|
package/src/app/types.ts
CHANGED
|
@@ -78,13 +78,43 @@ export interface PromptInput {
|
|
|
78
78
|
* trigger another self-recheck — only real user prompts do (once each).
|
|
79
79
|
*/
|
|
80
80
|
skipSelfRecheck?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Manager dispatch metadata for this prompt only (General → project).
|
|
83
|
+
* Carried through the queue so concurrent send_prompt jobs do not steal
|
|
84
|
+
* each other's report-back. Shape matches bot/manager-jobs ReportBackMeta.
|
|
85
|
+
*/
|
|
86
|
+
reportBack?: {
|
|
87
|
+
jobId: string;
|
|
88
|
+
originChatId: number;
|
|
89
|
+
originThreadId: number;
|
|
90
|
+
userAskPreview: string;
|
|
91
|
+
targetName: string;
|
|
92
|
+
targetPath: string;
|
|
93
|
+
dispatchPrompt: string;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Pre-posted status bubble (General: "Starting…") that the turn edits to
|
|
97
|
+
* "Thinking…" then streams the agent reply into.
|
|
98
|
+
*/
|
|
99
|
+
seedMessageId?: number;
|
|
100
|
+
/**
|
|
101
|
+
* Grok Build slash command (`/goal`, `/compact`, …). Must stay the first
|
|
102
|
+
* line of the prompt — skip manager/complexity wrappers.
|
|
103
|
+
*/
|
|
104
|
+
rawSlashCommand?: boolean;
|
|
81
105
|
}
|
|
82
106
|
|
|
83
107
|
export function textPrompt(
|
|
84
108
|
text: string,
|
|
85
109
|
replyTo?: number,
|
|
86
110
|
quotedText?: string,
|
|
87
|
-
opts?: {
|
|
111
|
+
opts?: {
|
|
112
|
+
skipSelfRecheck?: boolean;
|
|
113
|
+
promptId?: string;
|
|
114
|
+
reportBack?: PromptInput["reportBack"];
|
|
115
|
+
seedMessageId?: number;
|
|
116
|
+
rawSlashCommand?: boolean;
|
|
117
|
+
},
|
|
88
118
|
): PromptInput {
|
|
89
119
|
return {
|
|
90
120
|
text,
|
|
@@ -94,5 +124,8 @@ export function textPrompt(
|
|
|
94
124
|
promptId: opts?.promptId,
|
|
95
125
|
quotedText,
|
|
96
126
|
skipSelfRecheck: opts?.skipSelfRecheck,
|
|
127
|
+
reportBack: opts?.reportBack,
|
|
128
|
+
seedMessageId: opts?.seedMessageId,
|
|
129
|
+
rawSlashCommand: opts?.rawSlashCommand,
|
|
97
130
|
};
|
|
98
131
|
}
|