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.
Files changed (50) hide show
  1. package/.env.example +13 -0
  2. package/CHANGELOG.md +106 -0
  3. package/README.md +20 -5
  4. package/docs/GROUP.md +39 -4
  5. package/docs/INSTALL.md +2 -0
  6. package/package.json +4 -4
  7. package/scripts/setup.mjs +20 -3
  8. package/src/app/instance.ts +223 -0
  9. package/src/app/types.ts +34 -1
  10. package/src/bot/ask-user-service.ts +226 -0
  11. package/src/bot/auth.ts +5 -1
  12. package/src/bot/bot.ts +105 -4
  13. package/src/bot/chat-controller.ts +129 -0
  14. package/src/bot/commands.ts +22 -2
  15. package/src/bot/group-memory.ts +192 -12
  16. package/src/bot/handlers/forum.ts +16 -6
  17. package/src/bot/handlers/grok-slash.ts +336 -0
  18. package/src/bot/handlers/message.ts +165 -25
  19. package/src/bot/handlers/photo.ts +4 -1
  20. package/src/bot/handlers/system.ts +63 -1
  21. package/src/bot/image-return.ts +4 -1
  22. package/src/bot/manager-context.ts +208 -0
  23. package/src/bot/manager-jobs.ts +142 -0
  24. package/src/bot/menu/ephemeral.ts +4 -1
  25. package/src/bot/plan-exit-service.ts +169 -0
  26. package/src/bot/prompt-anchor.ts +2 -3
  27. package/src/bot/prompt-content.ts +5 -0
  28. package/src/bot/registry.ts +11 -2
  29. package/src/bot/scope.ts +9 -8
  30. package/src/bot/session-runtime.ts +665 -55
  31. package/src/bot/telegram-actions.ts +728 -38
  32. package/src/bot/telegram-bots.ts +2 -1
  33. package/src/bot/telegram-io.ts +4 -1
  34. package/src/cli.ts +43 -7
  35. package/src/config.ts +35 -25
  36. package/src/forum/manager.ts +2 -1
  37. package/src/forum/thread.ts +33 -0
  38. package/src/grok/client.ts +29 -5
  39. package/src/grok/plan-approval.ts +8 -0
  40. package/src/index.ts +4 -0
  41. package/src/render/manager-directive.ts +137 -0
  42. package/src/render/session-comment.ts +10 -0
  43. package/src/render/telegram-bridge.ts +118 -14
  44. package/src/service/linux.ts +21 -15
  45. package/src/service/macos.ts +20 -15
  46. package/src/service/platform.ts +12 -3
  47. package/src/service/types.ts +6 -0
  48. package/src/service/windows.ts +31 -22
  49. package/src/sessions/history.ts +18 -0
  50. 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: one topic per project, **AI Chat** workspace, exact-name bind, `/forum_setup`. See **[docs/GROUP.md](./docs/GROUP.md)**. |
31
- | 🌉 **Telegram bridge** | Agent can `create_topic`, `set_path`, `send_prompt` across topics, `search_memory`, and call allowlisted sibling bots via JSON actions. |
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
- | **Cross-topic agent bridge** (create / bind / send_prompt) | ✅ | ❌ |
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
- Startup options: `grok-tg setup [--path] | run | install | status | logs [n] |
122
- stop | restart | uninstall`. Or try it without installing: `npx
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** / **AI Chat** | `GROK_WORKSPACE` | Orchestration, cross-project planning, agent bridge actions |
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
- Messages you send **inside a topic** are prompts for a session whose `cwd` is
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
- | `search_memory` | Search topic + session indexes |
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.5.0",
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.30.0",
64
- "tsx": "^4.19.2"
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.10.0",
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>] [<TELEGRAM_BOT_TOKEN> [ALLOWED_USER_ID]]
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 envDir = (process.env.GROK_TG_DIR || process.env.GROK_TG_CWD || "").trim();
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?: { skipSelfRecheck?: boolean; promptId?: string },
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
  }