grok-telegram-bot 2.3.1 → 2.5.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 +64 -2
- package/CHANGELOG.md +156 -1
- package/README.md +58 -15
- package/docs/GROUP.md +225 -0
- package/docs/INSTALL.md +3 -0
- package/package.json +1 -1
- package/src/app/accounts.ts +84 -0
- package/src/app/instance-lock.ts +6 -0
- package/src/app/lifetime-flag.ts +20 -0
- package/src/app/settings-store.ts +47 -8
- package/src/app/types.ts +30 -2
- package/src/app/updater.ts +38 -6
- package/src/app/usage.ts +204 -7
- package/src/bot/account-rotator.ts +10 -0
- package/src/bot/auth.ts +96 -15
- package/src/bot/bot.ts +154 -11
- package/src/bot/chat-controller.ts +82 -13
- package/src/bot/commands.ts +69 -27
- package/src/bot/complexity-gate.ts +69 -0
- package/src/bot/deps.ts +22 -0
- package/src/bot/group-memory.ts +159 -0
- package/src/bot/handlers/accounts.ts +58 -1
- package/src/bot/handlers/control.ts +85 -32
- package/src/bot/handlers/document.ts +31 -4
- package/src/bot/handlers/forum.ts +207 -0
- package/src/bot/handlers/import-session.ts +290 -0
- package/src/bot/handlers/menu.ts +102 -61
- package/src/bot/handlers/message.ts +102 -21
- package/src/bot/handlers/photo.ts +123 -16
- package/src/bot/handlers/running.ts +172 -16
- package/src/bot/handlers/session-card.ts +20 -0
- package/src/bot/handlers/sessions.ts +76 -15
- package/src/bot/handlers/usage.ts +118 -16
- package/src/bot/handlers/voice.ts +52 -7
- package/src/bot/image-return.ts +8 -5
- package/src/bot/menu/ephemeral.ts +13 -3
- package/src/bot/menu/keyboard.ts +54 -14
- package/src/bot/menu/refresh.ts +3 -1
- package/src/bot/menu/status-panel.ts +25 -6
- package/src/bot/permission-service.ts +19 -0
- package/src/bot/prompt-anchor.ts +300 -0
- package/src/bot/prompt-content.ts +7 -0
- package/src/bot/registry.ts +94 -1
- package/src/bot/scope.ts +94 -0
- package/src/bot/session-fork.ts +11 -0
- package/src/bot/session-runtime.ts +1254 -83
- package/src/bot/suggestions.ts +489 -0
- package/src/bot/telegram-actions.ts +440 -0
- package/src/bot/telegram-bots.ts +495 -0
- package/src/bot/telegram-io.ts +94 -10
- package/src/cli.ts +2 -0
- package/src/config.ts +242 -2
- package/src/forum/bind-path.ts +146 -0
- package/src/forum/manager.ts +651 -0
- package/src/forum/project-icon.ts +142 -0
- package/src/forum/thread.ts +16 -0
- package/src/forum/topic-store.ts +114 -0
- package/src/forum/types.ts +29 -0
- package/src/grok/client.ts +214 -37
- package/src/grok/plan-approval.ts +72 -0
- package/src/grok/session-log.ts +16 -0
- package/src/grok/types.ts +21 -2
- package/src/import/build-import.ts +132 -0
- package/src/import/history-readers.ts +681 -0
- package/src/import/list-running.ts +100 -0
- package/src/import/sources.ts +78 -0
- package/src/index.ts +315 -30
- package/src/projects/manager.ts +16 -3
- package/src/render/chunk.ts +17 -10
- package/src/render/diff.ts +11 -2
- package/src/render/file-summary.ts +31 -1
- package/src/render/hashtags.ts +5 -1
- package/src/render/markdown.ts +293 -35
- package/src/render/plan.ts +127 -0
- package/src/render/session-comment.ts +318 -0
- package/src/render/telegram-bridge.ts +360 -0
- package/src/render/tool-call-detail.ts +400 -19
- package/src/render/tool-call-merge.ts +115 -0
- package/src/render/tool-call.ts +444 -162
- package/src/render/truncate.ts +85 -0
- package/src/service/platform.ts +44 -7
- package/src/service/windows.ts +30 -6
- package/src/sessions/history.ts +98 -0
- package/src/sessions/process.ts +7 -0
- package/src/sessions/store.ts +3 -0
- package/src/sessions/types.ts +5 -0
- package/src/stream/streamer.ts +90 -15
- package/src/tasks/runner.ts +4 -3
package/src/bot/commands.ts
CHANGED
|
@@ -1,30 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bot command definitions (for the Telegram command menu) and help text.
|
|
3
|
+
*
|
|
4
|
+
* Private chats get the full list (sorted by workflow). Groups/forum topics get
|
|
5
|
+
* a shorter list with cancel/menu first — reply keyboards are unreliable there.
|
|
3
6
|
*/
|
|
7
|
+
|
|
8
|
+
/** Full command menu (private chats + default). Order = Telegram "/" menu order. */
|
|
4
9
|
export const COMMANDS: { command: string; description: string }[] = [
|
|
10
|
+
// Core control
|
|
5
11
|
{ command: "start", description: "Welcome, menu & status panel" },
|
|
6
|
-
{ command: "menu", description: "
|
|
7
|
-
{ command: "
|
|
8
|
-
{ command: "
|
|
12
|
+
{ command: "menu", description: "Open the menu" },
|
|
13
|
+
{ command: "cancel", description: "Stop the current turn" },
|
|
14
|
+
{ command: "stop", description: "Stop the current turn (alias of /cancel)" },
|
|
15
|
+
{ command: "status", description: "Current session, project & queue" },
|
|
16
|
+
{ command: "new", description: "New session in current project" },
|
|
17
|
+
// Sessions
|
|
18
|
+
{ command: "running", description: "Sessions this chat controls" },
|
|
19
|
+
{ command: "sessions", description: "List/resume sessions · /sessions <q>" },
|
|
9
20
|
{ command: "active", description: "Sessions running now on the PC" },
|
|
10
|
-
{ command: "running", description: "Sessions this chat controls \u2014 switch between them" },
|
|
11
|
-
{ command: "killall", description: "Kill all active sessions on the PC" },
|
|
12
|
-
{ command: "mcp", description: "Inspect & toggle MCP servers \u00b7 health-check" },
|
|
13
|
-
{ command: "tasks", description: "Manage scheduled tasks" },
|
|
14
|
-
{ command: "newtask", description: "Create a scheduled task" },
|
|
15
21
|
{ command: "history", description: "Show recent conversation history" },
|
|
16
|
-
{ command: "
|
|
17
|
-
|
|
18
|
-
{ command: "
|
|
19
|
-
|
|
22
|
+
{ command: "import", description: "Import Kiro/OpenCode/Claude/Codex session" },
|
|
23
|
+
// Project
|
|
24
|
+
{ command: "projects", description: "Projects: list / search / open / new" },
|
|
25
|
+
// Queue
|
|
26
|
+
{ command: "btw", description: "Run ASAP: /btw <text>" },
|
|
20
27
|
{ command: "flush", description: "Send queued follow-ups now" },
|
|
21
28
|
{ command: "queue", description: "Show queued follow-ups" },
|
|
22
|
-
|
|
23
|
-
{ command: "
|
|
29
|
+
// Account
|
|
30
|
+
{ command: "accounts", description: "Switch between saved Grok accounts" },
|
|
31
|
+
{ command: "reauth", description: "Sign in to Grok (login or import)" },
|
|
32
|
+
{ command: "usage", description: "Account & context usage" },
|
|
33
|
+
// System / tools
|
|
34
|
+
{ command: "mcp", description: "Inspect & toggle MCP servers" },
|
|
35
|
+
{ command: "tasks", description: "Manage scheduled tasks" },
|
|
36
|
+
{ command: "newtask", description: "Create a scheduled task" },
|
|
37
|
+
{ command: "killall", description: "Kill all active sessions on the PC" },
|
|
24
38
|
{ command: "model", description: "Switch model: /model <id>" },
|
|
25
39
|
{ command: "restart", description: "Restart the Grok agent" },
|
|
26
|
-
{ command: "
|
|
27
|
-
{ command: "
|
|
40
|
+
{ command: "unwatch", description: "Stop following a live session" },
|
|
41
|
+
{ command: "help", description: "Show help" },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Group / forum command menu — keep short; cancel & menu first so topics can
|
|
46
|
+
* stop a turn without the private reply-keyboard bar.
|
|
47
|
+
*/
|
|
48
|
+
export const GROUP_COMMANDS: { command: string; description: string }[] = [
|
|
49
|
+
{ command: "cancel", description: "Stop the current turn" },
|
|
50
|
+
{ command: "stop", description: "Stop the current turn" },
|
|
51
|
+
{ command: "menu", description: "Open topic / group menu" },
|
|
52
|
+
{ command: "status", description: "Session, project & queue" },
|
|
53
|
+
{ command: "new", description: "New session in this topic/project" },
|
|
54
|
+
{ command: "running", description: "Sessions this topic controls" },
|
|
55
|
+
{ command: "sessions", description: "List / resume sessions" },
|
|
56
|
+
{ command: "btw", description: "Queue or run: /btw <text>" },
|
|
57
|
+
{ command: "flush", description: "Run queued follow-ups now" },
|
|
58
|
+
{ command: "model", description: "Switch model: /model <id>" },
|
|
28
59
|
{ command: "help", description: "Show help" },
|
|
29
60
|
];
|
|
30
61
|
|
|
@@ -36,17 +67,28 @@ export const HELP_TEXT = [
|
|
|
36
67
|
"\u2022 Just send a message to chat with Grok in the current project.",
|
|
37
68
|
"\u2022 While Grok is working, anything you send is queued and runs",
|
|
38
69
|
" automatically when the current turn finishes.",
|
|
70
|
+
"\u2022 Persistent bar (private): \u2630 Menu \u00B7 \u{1F195} New session \u00B7 \u{1F9ED} Running \u00B7 \u23F9 Stop.",
|
|
71
|
+
"\u2022 New session: /new or the bar button (not the inline Menu message).",
|
|
72
|
+
"\u2022 In groups / forum topics: use /cancel or Menu \u2192 \u23F9 Stop",
|
|
73
|
+
" (the persistent bar is unreliable in topics; use /new or topic menu).",
|
|
74
|
+
"",
|
|
75
|
+
"COMMANDS (core)",
|
|
76
|
+
"/menu \u2014 open the menu",
|
|
77
|
+
"/cancel or /stop \u2014 stop the current turn",
|
|
78
|
+
"/status \u2014 session, project and queue",
|
|
79
|
+
"/new \u2014 new session in the current project (also bar: \u{1F195} New session)",
|
|
80
|
+
"",
|
|
81
|
+
"COMMANDS (sessions)",
|
|
82
|
+
"/running \u2014 sessions this chat/topic controls",
|
|
83
|
+
"/sessions \u2014 resume a recent Grok session",
|
|
84
|
+
"/active \u2014 attach to a session running on the PC",
|
|
85
|
+
"/history \u2014 latest messages of the current session",
|
|
86
|
+
"/import \u2014 import a /running session from another agent",
|
|
39
87
|
"",
|
|
40
|
-
"COMMANDS",
|
|
41
|
-
"/projects \u2014 choose which folder Grok works in",
|
|
42
|
-
"/
|
|
43
|
-
"/active \u2014 attach to a session currently running on the PC",
|
|
44
|
-
"/history \u2014 show the latest messages of the current session",
|
|
45
|
-
"/new \u2014 start a brand-new session in the current project",
|
|
46
|
-
"/btw <text> \u2014 run it now if idle, otherwise right after the current task",
|
|
88
|
+
"COMMANDS (more)",
|
|
89
|
+
"/projects \u2014 choose which folder Grok works in (private chats)",
|
|
90
|
+
"/btw <text> \u2014 run now if idle, else right after the current task",
|
|
47
91
|
"/flush \u2014 run queued follow-ups immediately",
|
|
48
|
-
"/
|
|
49
|
-
"/
|
|
50
|
-
"/reauth \u2014 sign in to Grok (grok login, or import an existing login)",
|
|
51
|
-
"/accounts \u2014 switch between saved Grok accounts",
|
|
92
|
+
"/reauth \u2014 sign in to Grok",
|
|
93
|
+
"/accounts \u2014 switch saved Grok accounts",
|
|
52
94
|
].join("\n");
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-prompt complexity steering (fully automatic — never asks the user).
|
|
3
|
+
*
|
|
4
|
+
* On the first user message of a fresh session the bot prepends a directive so
|
|
5
|
+
* Grok itself decides Simple vs Complex:
|
|
6
|
+
* • Simple — implement directly.
|
|
7
|
+
* • Complex — enter plan mode, investigate carefully, write a plan,
|
|
8
|
+
* exit_plan_mode (auto-approved by this bridge), implement,
|
|
9
|
+
* then re-review the result.
|
|
10
|
+
*
|
|
11
|
+
* No Telegram buttons, no user choice, no waiting.
|
|
12
|
+
*/
|
|
13
|
+
import type { PromptInput } from "../app/types.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Agent-only directive. Must stay free of real `{progress: N%}` digit markers
|
|
17
|
+
* (history cleaner strips those). Uses the letter N only if mentioning format.
|
|
18
|
+
*/
|
|
19
|
+
export const AUTO_COMPLEXITY_DIRECTIVE = [
|
|
20
|
+
"COMPLEXITY (decide yourself — never ask the user):",
|
|
21
|
+
"Silently classify this task as Simple or Complex. Do NOT ask the user which it is. Do NOT show Simple/Complex buttons or questions.",
|
|
22
|
+
"",
|
|
23
|
+
"If SIMPLE (clear path, small change, obvious fix, short answer):",
|
|
24
|
+
" implement or answer directly with normal care.",
|
|
25
|
+
"",
|
|
26
|
+
"If COMPLEX (ambiguity, multi-file architecture, high rework risk, unclear approach):",
|
|
27
|
+
" 1. Enter plan mode (enter_plan_mode) when available.",
|
|
28
|
+
" 2. Investigate carefully: explore the codebase, map patterns, edge cases, and risks before coding.",
|
|
29
|
+
" 3. Write a solid plan to the plan file; prefer investigation over speed.",
|
|
30
|
+
" 4. Call exit_plan_mode when ready. This Telegram bridge auto-approves plan exit",
|
|
31
|
+
" (there is no TUI plan popup). After exit_plan_mode succeeds, implement fully.",
|
|
32
|
+
" Do NOT wait for the user to \"approve a popup\" — just call exit_plan_mode and proceed.",
|
|
33
|
+
" 5. After implementation, re-review your work (verify correctness, edge cases, and that the plan was followed) before finishing.",
|
|
34
|
+
"",
|
|
35
|
+
"User task:",
|
|
36
|
+
].join("\n");
|
|
37
|
+
|
|
38
|
+
/** Optional mode ids Grok may advertise for plan mode (best-effort only). */
|
|
39
|
+
export const PLAN_MODE_CANDIDATES = ["plan", "planning", "architect", "design"] as const;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Prepend the auto-complexity directive so the agent decides Simple vs Complex
|
|
43
|
+
* without any user interaction.
|
|
44
|
+
*/
|
|
45
|
+
export function wrapAutoComplexityPrompt(input: PromptInput): PromptInput {
|
|
46
|
+
const body = input.text.trim() || "(see attached media / files)";
|
|
47
|
+
// Avoid double-wrapping if a retry/queue path already applied it.
|
|
48
|
+
if (body.startsWith("COMPLEXITY (decide yourself")) return input;
|
|
49
|
+
return {
|
|
50
|
+
...input,
|
|
51
|
+
text: `${AUTO_COMPLEXITY_DIRECTIVE}\n${body}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Pick a plan-mode id from the agent's advertised modes, if any. */
|
|
56
|
+
export function pickPlanModeId(
|
|
57
|
+
modes: Array<{ id: string; name: string }>,
|
|
58
|
+
hasMode: (id: string) => boolean,
|
|
59
|
+
): string | undefined {
|
|
60
|
+
for (const id of PLAN_MODE_CANDIDATES) {
|
|
61
|
+
if (hasMode(id)) return id;
|
|
62
|
+
}
|
|
63
|
+
for (const m of modes) {
|
|
64
|
+
if (/plan|architect|design/i.test(m.id) || /plan|architect|design/i.test(m.name)) {
|
|
65
|
+
return m.id;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
package/src/bot/deps.ts
CHANGED
|
@@ -10,12 +10,15 @@ import type { AppConfig } from "../config.js";
|
|
|
10
10
|
import type { SttService } from "../app/stt.js";
|
|
11
11
|
import type { UsageService } from "../app/usage.js";
|
|
12
12
|
import type { ProjectEntry, ProjectManager } from "../projects/manager.js";
|
|
13
|
+
import type { ImportableSession } from "../import/list-running.js";
|
|
14
|
+
import type { ImportSourceId } from "../import/sources.js";
|
|
13
15
|
import type { SessionMeta } from "../sessions/types.js";
|
|
14
16
|
import type { SessionStore } from "../sessions/store.js";
|
|
15
17
|
import type { TaskRunner } from "../tasks/runner.js";
|
|
16
18
|
import type { TaskStore } from "../tasks/store.js";
|
|
17
19
|
import type { StatusPanel } from "./menu/status-panel.js";
|
|
18
20
|
import type { Ephemeral } from "./menu/ephemeral.js";
|
|
21
|
+
import type { ForumManager } from "../forum/manager.js";
|
|
19
22
|
import type { RuntimeRegistry } from "./registry.js";
|
|
20
23
|
import type { TaskWizard } from "./wizard/task-wizard.js";
|
|
21
24
|
|
|
@@ -36,12 +39,18 @@ export interface BotDeps {
|
|
|
36
39
|
stt: SttService;
|
|
37
40
|
usage: UsageService;
|
|
38
41
|
accounts: AccountManager;
|
|
42
|
+
/** Present when TOPIC_GROUP_ID is configured. */
|
|
43
|
+
forum?: ForumManager;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
/** Caches the last project list shown per chat for callback resolution. */
|
|
42
47
|
export class MenuCache {
|
|
43
48
|
private readonly projectLists = new Map<number, ProjectEntry[]>();
|
|
44
49
|
private readonly sessionLists = new Map<number, { metas: SessionMeta[]; heading: string }>();
|
|
50
|
+
private readonly importLists = new Map<
|
|
51
|
+
number,
|
|
52
|
+
{ sourceId: ImportSourceId; sessions: ImportableSession[] }
|
|
53
|
+
>();
|
|
45
54
|
|
|
46
55
|
setProjects(chatId: number, list: ProjectEntry[]): void {
|
|
47
56
|
this.projectLists.set(chatId, list);
|
|
@@ -64,4 +73,17 @@ export class MenuCache {
|
|
|
64
73
|
getSessions(chatId: number): { metas: SessionMeta[]; heading: string } | undefined {
|
|
65
74
|
return this.sessionLists.get(chatId);
|
|
66
75
|
}
|
|
76
|
+
|
|
77
|
+
/** Cache foreign sessions shown by Import session (callback index → session). */
|
|
78
|
+
setImportSessions(chatId: number, sourceId: ImportSourceId, sessions: ImportableSession[]): void {
|
|
79
|
+
this.importLists.set(chatId, { sourceId, sessions });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getImportSessions(chatId: number): { sourceId: ImportSourceId; sessions: ImportableSession[] } | undefined {
|
|
83
|
+
return this.importLists.get(chatId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getImportSession(chatId: number, index: number): ImportableSession | undefined {
|
|
87
|
+
return this.importLists.get(chatId)?.sessions[index];
|
|
88
|
+
}
|
|
67
89
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local "group memory" search across forum topics, session metadata, and
|
|
3
|
+
* recent session history. Telegram bots have no general message-search API,
|
|
4
|
+
* so this indexes what the bridge already stores on disk.
|
|
5
|
+
*/
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { readHistory } from "../sessions/history.js";
|
|
8
|
+
import type { SessionMeta } from "../sessions/types.js";
|
|
9
|
+
import type { SessionStore } from "../sessions/store.js";
|
|
10
|
+
import type { ForumTopicBinding } from "../forum/types.js";
|
|
11
|
+
|
|
12
|
+
export type MemoryHitKind = "topic" | "session" | "history";
|
|
13
|
+
|
|
14
|
+
export interface MemoryHit {
|
|
15
|
+
kind: MemoryHitKind;
|
|
16
|
+
title: string;
|
|
17
|
+
snippet: string;
|
|
18
|
+
score: number;
|
|
19
|
+
path?: string;
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
threadId?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GroupMemorySearchOpts {
|
|
25
|
+
query: string;
|
|
26
|
+
limit?: number;
|
|
27
|
+
sessionsDir: string;
|
|
28
|
+
store: SessionStore;
|
|
29
|
+
topics?: ForumTopicBinding[];
|
|
30
|
+
/** Max sessions whose JSONL tails are scanned. */
|
|
31
|
+
maxSessions?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Tokenize a query into lowercase alphanumeric tokens (min length 2). */
|
|
35
|
+
export function tokenizeQuery(query: string): string[] {
|
|
36
|
+
return query
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.split(/[^a-z0-9_./\\-]+/i)
|
|
39
|
+
.map((t) => t.trim())
|
|
40
|
+
.filter((t) => t.length >= 2)
|
|
41
|
+
.slice(0, 12);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Score how well `haystack` matches tokens (0 if no token hits). */
|
|
45
|
+
export function scoreTokens(haystack: string, tokens: string[]): number {
|
|
46
|
+
if (!tokens.length || !haystack) return 0;
|
|
47
|
+
const h = haystack.toLowerCase();
|
|
48
|
+
let score = 0;
|
|
49
|
+
let hits = 0;
|
|
50
|
+
for (const t of tokens) {
|
|
51
|
+
if (!h.includes(t)) continue;
|
|
52
|
+
hits++;
|
|
53
|
+
// Prefer whole-word-ish hits slightly.
|
|
54
|
+
const re = new RegExp(`(?:^|[^a-z0-9])${escapeReg(t)}(?:[^a-z0-9]|$)`, "i");
|
|
55
|
+
score += re.test(h) ? 3 : 1;
|
|
56
|
+
// Density bonus for short fields.
|
|
57
|
+
if (h.length < 120) score += 1;
|
|
58
|
+
}
|
|
59
|
+
if (hits === 0) return 0;
|
|
60
|
+
// All-token bonus.
|
|
61
|
+
if (hits === tokens.length) score += 4;
|
|
62
|
+
return score;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Search topics + session store + recent history. Pure ranking over provided
|
|
67
|
+
* store; safe to call on the main bot thread (capped I/O).
|
|
68
|
+
*/
|
|
69
|
+
export function searchGroupMemory(opts: GroupMemorySearchOpts): MemoryHit[] {
|
|
70
|
+
const tokens = tokenizeQuery(opts.query);
|
|
71
|
+
if (tokens.length === 0) return [];
|
|
72
|
+
const limit = Math.max(1, Math.min(20, opts.limit ?? 8));
|
|
73
|
+
const maxSessions = opts.maxSessions ?? 40;
|
|
74
|
+
const hits: MemoryHit[] = [];
|
|
75
|
+
|
|
76
|
+
for (const t of opts.topics ?? []) {
|
|
77
|
+
const hay = [t.name, t.projectPath ?? "", t.kind, t.sessionId ?? ""].join("\n");
|
|
78
|
+
const score = scoreTokens(hay, tokens);
|
|
79
|
+
if (score <= 0) continue;
|
|
80
|
+
hits.push({
|
|
81
|
+
kind: "topic",
|
|
82
|
+
title: t.name,
|
|
83
|
+
snippet: t.projectPath ? `path: ${t.projectPath}` : `kind: ${t.kind}`,
|
|
84
|
+
score: score + 2, // slight boost for topic map
|
|
85
|
+
path: t.projectPath ?? undefined,
|
|
86
|
+
threadId: t.threadId,
|
|
87
|
+
sessionId: t.sessionId,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let metas: SessionMeta[] = [];
|
|
92
|
+
try {
|
|
93
|
+
metas = opts.store.list(maxSessions);
|
|
94
|
+
} catch {
|
|
95
|
+
metas = [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const m of metas) {
|
|
99
|
+
const hay = [m.title, m.comment ?? "", m.cwd, m.sessionId].join("\n");
|
|
100
|
+
const score = scoreTokens(hay, tokens);
|
|
101
|
+
if (score > 0) {
|
|
102
|
+
hits.push({
|
|
103
|
+
kind: "session",
|
|
104
|
+
title: m.title || m.sessionId.slice(0, 8),
|
|
105
|
+
snippet: clamp(
|
|
106
|
+
[m.comment, m.cwd].filter(Boolean).join(" · ") || m.sessionId,
|
|
107
|
+
220,
|
|
108
|
+
),
|
|
109
|
+
score,
|
|
110
|
+
path: m.cwd || undefined,
|
|
111
|
+
sessionId: m.sessionId,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// History tail (cheap): few entries, short text.
|
|
116
|
+
if (m.historyBytes <= 0) continue;
|
|
117
|
+
try {
|
|
118
|
+
const path = join(opts.sessionsDir, `${m.sessionId}.jsonl`);
|
|
119
|
+
const entries = readHistory(path, 12);
|
|
120
|
+
for (const e of entries) {
|
|
121
|
+
if (!e.text?.trim()) continue;
|
|
122
|
+
const s = scoreTokens(e.text, tokens);
|
|
123
|
+
if (s <= 0) continue;
|
|
124
|
+
hits.push({
|
|
125
|
+
kind: "history",
|
|
126
|
+
title: `${e.role} · ${(m.title || m.sessionId.slice(0, 8)).slice(0, 40)}`,
|
|
127
|
+
snippet: clamp(e.text.replace(/\s+/g, " ").trim(), 220),
|
|
128
|
+
score: s,
|
|
129
|
+
path: m.cwd || undefined,
|
|
130
|
+
sessionId: m.sessionId,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
/* ignore unreadable logs */
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
hits.sort((a, b) => b.score - a.score || a.kind.localeCompare(b.kind));
|
|
139
|
+
// Dedupe near-identical snippets.
|
|
140
|
+
const seen = new Set<string>();
|
|
141
|
+
const out: MemoryHit[] = [];
|
|
142
|
+
for (const h of hits) {
|
|
143
|
+
const key = `${h.kind}|${h.sessionId ?? h.threadId ?? ""}|${h.snippet.slice(0, 80)}`;
|
|
144
|
+
if (seen.has(key)) continue;
|
|
145
|
+
seen.add(key);
|
|
146
|
+
out.push(h);
|
|
147
|
+
if (out.length >= limit) break;
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function clamp(s: string, max: number): string {
|
|
153
|
+
if (s.length <= max) return s;
|
|
154
|
+
return s.slice(0, max - 1) + "\u2026";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function escapeReg(s: string): string {
|
|
158
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
159
|
+
}
|
|
@@ -11,6 +11,7 @@ import { type Bot, type Context, InlineKeyboard } from "grammy";
|
|
|
11
11
|
import { AuthService } from "../../app/auth-service.js";
|
|
12
12
|
import type { StoredAccount } from "../../app/accounts.js";
|
|
13
13
|
import { UNSUPPORTED_LOGIN_HELP } from "../../app/grok-credentials.js";
|
|
14
|
+
import { formatCliBillingLines } from "../../app/usage.js";
|
|
14
15
|
import { createLogger } from "../../logger.js";
|
|
15
16
|
import type { BotDeps } from "../deps.js";
|
|
16
17
|
|
|
@@ -48,12 +49,35 @@ async function view(deps: BotDeps, note?: string): Promise<{ text: string; keybo
|
|
|
48
49
|
} else {
|
|
49
50
|
lines.push("\u{1F7E2} Signed in (identity unknown).", "");
|
|
50
51
|
}
|
|
52
|
+
// Live Grok CLI monthly quota for the host login (same source as OmniRoute-style clients).
|
|
53
|
+
if (loggedIn) {
|
|
54
|
+
const { billing, error } = await deps.usage.cliBilling().catch((e) => ({
|
|
55
|
+
billing: undefined,
|
|
56
|
+
error: (e as Error).message,
|
|
57
|
+
}));
|
|
58
|
+
if (billing) {
|
|
59
|
+
lines.push(...formatCliBillingLines(billing), "");
|
|
60
|
+
} else if (error) {
|
|
61
|
+
lines.push(`\u{1F4B3} Grok CLI quota: unavailable (${error})`, "");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
51
64
|
if (list.length === 0) {
|
|
52
65
|
lines.push("No saved accounts yet.", "", "Save the current login below, or sign in via /reauth.");
|
|
53
66
|
} else {
|
|
54
67
|
for (const a of list) {
|
|
55
68
|
lines.push(accountLine(a, a.id === active));
|
|
56
|
-
|
|
69
|
+
const usage = deps.accounts.formatUsageLine(a);
|
|
70
|
+
if (usage) lines.push(` \u2514 \u{1F4CA} ${usage}`);
|
|
71
|
+
else lines.push(" \u2514 \u{1F4CA} No recorded usage yet");
|
|
72
|
+
if (a.warning) {
|
|
73
|
+
lines.push(
|
|
74
|
+
` \u2514 \u26A0\uFE0F Skipped by auto-rotate: ${a.warning.reason.slice(0, 80)}${a.warning.reason.length > 80 ? "\u2026" : ""}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const totals = summarizeUsage(list);
|
|
79
|
+
if (totals) {
|
|
80
|
+
lines.push("", `\u{1F4CA} All saved accounts: ${totals}`);
|
|
57
81
|
}
|
|
58
82
|
}
|
|
59
83
|
const rotate = deps.accounts.autoRotateEnabled();
|
|
@@ -86,6 +110,32 @@ function trim(s: string, n = 22): string {
|
|
|
86
110
|
return s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
|
|
87
111
|
}
|
|
88
112
|
|
|
113
|
+
/** Aggregate usage across saved accounts for the footer line. */
|
|
114
|
+
function summarizeUsage(list: StoredAccount[]): string {
|
|
115
|
+
let turns = 0;
|
|
116
|
+
let credits = 0;
|
|
117
|
+
let used = 0;
|
|
118
|
+
for (const a of list) {
|
|
119
|
+
const u = a.usage;
|
|
120
|
+
if (!u) continue;
|
|
121
|
+
if (u.turns > 0 || u.credits > 0) used++;
|
|
122
|
+
turns += u.turns || 0;
|
|
123
|
+
credits += u.credits || 0;
|
|
124
|
+
}
|
|
125
|
+
if (used === 0 && turns === 0 && credits === 0) return "";
|
|
126
|
+
const parts: string[] = [];
|
|
127
|
+
if (turns > 0) parts.push(`${turns} turn${turns === 1 ? "" : "s"}`);
|
|
128
|
+
if (credits > 0) parts.push(`${fmtNum(credits)} credits total`);
|
|
129
|
+
parts.push(`${used}/${list.length} accounts used`);
|
|
130
|
+
return parts.join(" \u00B7 ");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function fmtNum(n: number): string {
|
|
134
|
+
if (!Number.isFinite(n)) return String(n);
|
|
135
|
+
if (Number.isInteger(n)) return n.toLocaleString("en-US");
|
|
136
|
+
return n.toFixed(2);
|
|
137
|
+
}
|
|
138
|
+
|
|
89
139
|
export async function showAccounts(ctx: Context, deps: BotDeps): Promise<void> {
|
|
90
140
|
const { text, keyboard } = await view(deps);
|
|
91
141
|
await deps.ephemeral.open(ctx);
|
|
@@ -244,6 +294,13 @@ export function registerAccounts(bot: Bot, deps: BotDeps): void {
|
|
|
244
294
|
await deps.acp.start(true).catch(() => {});
|
|
245
295
|
throw e;
|
|
246
296
|
}
|
|
297
|
+
// Remember preferred account for this chat/topic scope (model/reasoning peers).
|
|
298
|
+
try {
|
|
299
|
+
const { resolveScope } = await import("../scope.js");
|
|
300
|
+
resolveScope(ctx, deps).rt.setPreferredAccountId(id);
|
|
301
|
+
} catch {
|
|
302
|
+
/* non-fatal */
|
|
303
|
+
}
|
|
247
304
|
const note = (await deps.usage.isLoggedIn())
|
|
248
305
|
? `\u2705 Now signed in as ${meta.label}. Your next message runs on this account.`
|
|
249
306
|
: `\u26A0\uFE0F Switched to ${meta.label}, but no usable login is active. ${UNSUPPORTED_LOGIN_HELP}`;
|