grok-telegram-bot 2.3.1 → 2.4.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 (67) hide show
  1. package/.env.example +26 -0
  2. package/CHANGELOG.md +37 -0
  3. package/package.json +1 -1
  4. package/scripts/analyze-jsonl.ts +33 -0
  5. package/scripts/delayed-restart.ps1 +29 -0
  6. package/scripts/probe-exit-response-shape.py +77 -0
  7. package/scripts/probe-plan-exit.py +60 -0
  8. package/scripts/probe-plan-exit2.py +48 -0
  9. package/scripts/probe-plan-fields.py +41 -0
  10. package/scripts/probe-plan-fields2.py +58 -0
  11. package/scripts/probe-plan-response-path.py +48 -0
  12. package/scripts/sample-claude-tooluse.ts +21 -0
  13. package/scripts/sample-kiro-events.ts +31 -0
  14. package/scripts/smoke-exit-plan.ts +274 -0
  15. package/scripts/smoke-exit-shapes.ts +252 -0
  16. package/scripts/smoke-import.mjs +82 -0
  17. package/scripts/smoke-import.ts +73 -0
  18. package/src/app/accounts.ts +84 -0
  19. package/src/app/instance-lock.ts +6 -0
  20. package/src/app/types.ts +19 -2
  21. package/src/app/updater.ts +17 -6
  22. package/src/app/usage.ts +204 -7
  23. package/src/bot/account-rotator.ts +10 -0
  24. package/src/bot/bot.ts +36 -0
  25. package/src/bot/chat-controller.ts +35 -0
  26. package/src/bot/commands.ts +2 -0
  27. package/src/bot/complexity-gate.ts +69 -0
  28. package/src/bot/deps.ts +19 -0
  29. package/src/bot/handlers/accounts.ts +51 -1
  30. package/src/bot/handlers/import-session.ts +290 -0
  31. package/src/bot/handlers/menu.ts +17 -38
  32. package/src/bot/handlers/message.ts +1 -0
  33. package/src/bot/handlers/running.ts +35 -5
  34. package/src/bot/handlers/session-card.ts +12 -0
  35. package/src/bot/handlers/sessions.ts +14 -3
  36. package/src/bot/handlers/usage.ts +118 -16
  37. package/src/bot/menu/keyboard.ts +5 -4
  38. package/src/bot/menu/status-panel.ts +19 -6
  39. package/src/bot/prompt-content.ts +4 -0
  40. package/src/bot/session-fork.ts +11 -0
  41. package/src/bot/session-runtime.ts +740 -58
  42. package/src/bot/suggestions.ts +429 -0
  43. package/src/config.ts +41 -0
  44. package/src/grok/client.ts +91 -16
  45. package/src/grok/plan-approval.ts +72 -0
  46. package/src/grok/session-log.ts +16 -0
  47. package/src/grok/types.ts +21 -2
  48. package/src/import/build-import.ts +132 -0
  49. package/src/import/history-readers.ts +681 -0
  50. package/src/import/list-running.ts +100 -0
  51. package/src/import/sources.ts +78 -0
  52. package/src/index.ts +179 -24
  53. package/src/render/diff.ts +11 -2
  54. package/src/render/file-summary.ts +31 -1
  55. package/src/render/markdown.ts +293 -35
  56. package/src/render/plan.ts +127 -0
  57. package/src/render/session-comment.ts +261 -0
  58. package/src/render/tool-call-detail.ts +400 -19
  59. package/src/render/tool-call-merge.ts +115 -0
  60. package/src/render/tool-call.ts +405 -142
  61. package/src/render/truncate.ts +85 -0
  62. package/src/service/windows.ts +14 -2
  63. package/src/sessions/history.ts +57 -0
  64. package/src/sessions/store.ts +3 -0
  65. package/src/sessions/types.ts +5 -0
  66. package/src/stream/streamer.ts +73 -9
  67. package/src/tasks/runner.ts +4 -3
@@ -1,7 +1,10 @@
1
1
  /**
2
- * /usage — show account info and the current session's context usage.
2
+ * /usage — Grok CLI live monthly quota + session context + bot-tracked
3
+ * per-account turn stats.
3
4
  */
4
5
  import type { Bot, Context } from "grammy";
6
+ import type { StoredAccount } from "../../app/accounts.js";
7
+ import { formatCliBillingLines } from "../../app/usage.js";
5
8
  import type { BotDeps } from "../deps.js";
6
9
 
7
10
  export async function showUsage(ctx: Context, deps: BotDeps): Promise<void> {
@@ -10,29 +13,128 @@ export async function showUsage(ctx: Context, deps: BotDeps): Promise<void> {
10
13
  const acct = await deps.usage.account();
11
14
  const meta = rt.contextInfo();
12
15
  const ctx100 = meta?.contextUsagePercentage;
13
- const savedCount = deps.accounts.list().length;
16
+ const list = deps.accounts.list();
17
+ const activeId = deps.accounts.activeAccountId();
18
+ const activeMeta = activeId ? deps.accounts.get(activeId) : undefined;
19
+ const { billing, error: billingError } = await deps.usage.cliBilling();
14
20
 
15
- const lines = [
16
- "\u{1F4CA} Usage & account",
17
- acct?.email ? `\u{1F464} ${acct.email}` : "",
18
- acct?.accountType ? `\u{1F511} ${acct.accountType}${acct.region ? ` \u00B7 ${acct.region}` : ""}` : "",
21
+ const lines: string[] = [
22
+ "\u{1F4CA} Usage & accounts",
19
23
  "",
20
- `\u{1F9F5} Session: ${rt.sessionId ? rt.sessionId.slice(0, 8) : "none"}`,
21
- `\u{1F9E9} Model: ${rt.model || "default"}`,
22
- `\u{1F4CA} Context used: ${ctx100 !== undefined ? `${ctx100.toFixed(0)}%` : "\u2014"}`,
23
- `\u{1F501} Turns this session: ${rt.turns}`,
24
- meta?.credits !== undefined ? `\u{1FA99} Credits used: ${meta.credits.toLocaleString("en-US")}` : "",
25
- meta?.effort ? `\u{1F9E0} Effort: ${meta.effort}` : "",
26
- savedCount > 0 ? `\u{1F465} Saved accounts: ${savedCount} \u00B7 /accounts to switch` : "",
24
+ "\u{1F464} Current login",
25
+ acct?.email ? ` ${acct.email}` : " (identity unknown)",
26
+ ];
27
+ if (acct?.accountType) {
28
+ lines.push(` \u{1F511} ${acct.accountType}${acct.region ? ` \u00B7 ${acct.region}` : ""}`);
29
+ }
30
+ if (acct?.teamId) lines.push(` Team: ${acct.teamId.slice(0, 8)}\u2026`);
31
+ if (activeMeta) {
32
+ const uLine = deps.accounts.formatUsageLine(activeMeta);
33
+ lines.push(` Saved as: ${activeMeta.label}`);
34
+ if (uLine) lines.push(` Bot-tracked: ${uLine}`);
35
+ }
36
+
37
+ lines.push("");
38
+ if (billing) {
39
+ lines.push(...formatCliBillingLines(billing));
40
+ } else {
41
+ lines.push(
42
+ "\u{1F4B3} Grok CLI monthly quota",
43
+ ` \u26A0\uFE0F ${billingError || "unavailable"}`,
44
+ " (Requires `grok login` OIDC token — not XAI_API_KEY alone.)",
45
+ );
46
+ }
47
+
48
+ lines.push(
27
49
  "",
28
- "\u2139\uFE0F Full billing/quota lives in the Grok app; grok doesn't expose limits headlessly.",
29
- ].filter(Boolean);
50
+ "\u{1F9F5} This session (ACP)",
51
+ ` Id: ${rt.sessionId ? rt.sessionId.slice(0, 8) : "none"}`,
52
+ ` Model: ${rt.model || "default"}`,
53
+ ` Context used: ${ctx100 !== undefined ? `${ctx100.toFixed(0)}%` : "\u2014"}`,
54
+ ` Turns this session: ${rt.turns}`,
55
+ );
56
+ if (meta?.credits !== undefined) {
57
+ lines.push(` Credits (session report): ${fmtNum(meta.credits)}`);
58
+ }
59
+ if (meta?.effort) lines.push(` Effort: ${meta.effort}`);
60
+ if (meta?.totalTokens !== undefined) {
61
+ lines.push(` Tokens (session report): ${meta.totalTokens.toLocaleString("en-US")}`);
62
+ }
63
+
64
+ if (list.length > 0) {
65
+ lines.push("", "\u{1F465} Saved accounts (bot-tracked turns on this machine)");
66
+ for (const a of list) {
67
+ lines.push(accountUsageBlock(deps, a, a.id === activeId));
68
+ }
69
+ const totals = aggregate(list);
70
+ lines.push(
71
+ "",
72
+ `\u{1F4CA} Bot totals: ${totals.turns} turn${totals.turns === 1 ? "" : "s"}` +
73
+ (totals.credits > 0 ? ` \u00B7 ${fmtNum(totals.credits)} session credits` : "") +
74
+ ` \u00B7 ${totals.withUsage}/${list.length} accounts used`,
75
+ );
76
+ } else {
77
+ lines.push("", "\u{1F465} No saved accounts yet \u2014 /accounts to save & switch.");
78
+ }
79
+
80
+ lines.push(
81
+ "",
82
+ "\u2139\uFE0F Monthly quota is live from Grok CLI (`cli-chat-proxy` billing). Bot-tracked turns are local to this bot.",
83
+ );
84
+
85
+ if (!acct) lines.splice(3, 0, " (account info unavailable \u2014 is grok logged in?)");
30
86
 
31
- if (!acct) lines.splice(1, 0, "(account info unavailable \u2014 is grok logged in?)");
32
87
  await deps.ephemeral.open(ctx);
33
88
  await deps.ephemeral.reply(ctx, lines.join("\n"));
34
89
  }
35
90
 
91
+ function accountUsageBlock(deps: BotDeps, a: StoredAccount, active: boolean): string {
92
+ const mark = a.warning ? "\u26A0\uFE0F" : active ? "\u2705" : "\u{1F464}";
93
+ const u = a.usage;
94
+ const head = `${mark} ${a.label}${active ? " (active)" : ""}`;
95
+ if (!u || (u.turns <= 0 && u.credits <= 0 && !u.lastUsedAt)) {
96
+ return `${head}\n no bot-tracked usage yet`;
97
+ }
98
+ const bits: string[] = [];
99
+ bits.push(`${u.turns || 0} turn${(u.turns || 0) === 1 ? "" : "s"}`);
100
+ if (u.credits > 0) bits.push(`${fmtNum(u.credits)} session credits`);
101
+ if (u.lastTurnCredits !== undefined) bits.push(`last turn ${fmtNum(u.lastTurnCredits)}`);
102
+ if (u.lastContextPct !== undefined) bits.push(`last ctx ${u.lastContextPct.toFixed(0)}%`);
103
+ if (u.lastUsedAt) bits.push(`last ${shortWhen(u.lastUsedAt)}`);
104
+ return `${head}\n ${bits.join(" \u00B7 ")}`;
105
+ }
106
+
107
+ function aggregate(list: StoredAccount[]): { turns: number; credits: number; withUsage: number } {
108
+ let turns = 0;
109
+ let credits = 0;
110
+ let withUsage = 0;
111
+ for (const a of list) {
112
+ const u = a.usage;
113
+ if (!u) continue;
114
+ turns += u.turns || 0;
115
+ credits += u.credits || 0;
116
+ if (u.turns > 0 || u.credits > 0 || u.lastUsedAt) withUsage++;
117
+ }
118
+ return { turns, credits, withUsage };
119
+ }
120
+
121
+ function fmtNum(n: number): string {
122
+ if (!Number.isFinite(n)) return String(n);
123
+ if (Number.isInteger(n)) return n.toLocaleString("en-US");
124
+ return n.toFixed(2);
125
+ }
126
+
127
+ function shortWhen(iso: string): string {
128
+ const t = Date.parse(iso);
129
+ if (!Number.isFinite(t)) return iso.slice(0, 10);
130
+ const sec = Math.max(0, Math.round((Date.now() - t) / 1000));
131
+ if (sec < 60) return "just now";
132
+ if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
133
+ if (sec < 86_400) return `${Math.floor(sec / 3600)}h ago`;
134
+ if (sec < 86_400 * 14) return `${Math.floor(sec / 86_400)}d ago`;
135
+ return new Date(t).toISOString().slice(0, 10);
136
+ }
137
+
36
138
  export function registerUsage(bot: Bot, deps: BotDeps): void {
37
139
  bot.command("usage", (ctx) => showUsage(ctx, deps));
38
140
  }
@@ -2,8 +2,9 @@
2
2
  * Menu surfaces:
3
3
  * - a tiny PERSISTENT bar (☰ Menu · 🧭 Running · ⏹ Stop) — minimal footprint;
4
4
  * - a full, organized INLINE menu opened on demand (and hideable).
5
- * Live state (project/agent/model/reasoning/context) lives in the pinned panel,
6
- * so the bar stays clean.
5
+ * Live state (project/model/reasoning/context) lives in the pinned panel,
6
+ * so the bar stays clean. (Agent picker removed — Grok has no useful headless
7
+ * agent switch; plan mode is entered automatically when the agent judges the task complex.)
7
8
  */
8
9
  import { InlineKeyboard, Keyboard } from "grammy";
9
10
 
@@ -18,7 +19,7 @@ export function compactKeyboard(): Keyboard {
18
19
  }
19
20
 
20
21
  /** The full, grouped inline menu (opened via ☰ Menu or /menu). */
21
- export function mainMenuInline(state: { agent: string; model: string; reasoning: string }): InlineKeyboard {
22
+ export function mainMenuInline(state: { model: string; reasoning: string }): InlineKeyboard {
22
23
  const t = (s: string, n: number): string => (s.length > n ? s.slice(0, n - 1) + "\u2026" : s);
23
24
  return new InlineKeyboard()
24
25
  .text("\u{1F4C1} Project", "m:project")
@@ -27,7 +28,7 @@ export function mainMenuInline(state: { agent: string; model: string; reasoning:
27
28
  .text("\u{1F9ED} Running", "m:running")
28
29
  .text("\u{1F5C2} Sessions", "m:sessions")
29
30
  .row()
30
- .text(`\u{1F916} Agent \u00B7 ${t(state.agent, 24)}`, "m:agent")
31
+ .text("\u{1F4E5} Import session", "m:import")
31
32
  .row()
32
33
  .text(`\u{1F9E9} Model \u00B7 ${t(state.model, 24)}`, "m:model")
33
34
  .row()
@@ -49,11 +49,15 @@ export class StatusPanel {
49
49
  const SEP = " | "; // pipe delimiter between inline fields
50
50
  const lines: string[] = [];
51
51
 
52
- // 1) Progress first only while a turn is live (cleared when it ends), so
53
- // the collapsed pin preview shows how far along the current task is.
52
+ // 1) Active plan board first (always above the progress bar) so the user
53
+ // always sees done / in-progress / pending steps while a plan is live.
54
+ const plan = rt.planBoard;
55
+ if (plan) lines.push(plan);
56
+
57
+ // 2) Progress — only while a turn is live (cleared when it ends).
54
58
  if (progress !== undefined) lines.push(`\u{1F4C8} ${progressBar(progress)}`);
55
59
 
56
- // 2) Activity: state + only the counters that currently apply.
60
+ // 3) Activity: state + only the counters that currently apply.
57
61
  const activity: string[] = [rt.isBusy ? "\u23F3 Working" : "\u2705 Idle"];
58
62
  if (rt.queueLength > 0) activity.push(`\u{1F4E5} ${rt.queueLength} queued`);
59
63
  if (running > 1) activity.push(`\u{1F9ED} ${running} sessions`);
@@ -61,13 +65,22 @@ export class StatusPanel {
61
65
  if (subagents) activity.push(`\u{1F465} ${subagents}`);
62
66
  lines.push(activity.join(SEP));
63
67
 
64
- // 3) Where: project | session | context usage.
68
+ // 3b) What is happening now / last summary (same source as Running cards).
69
+ // Prefer plan one-liner over a redundant tool-step when a plan is active.
70
+ const comment = rt.planSummary && rt.isBusy ? undefined : rt.cardComment;
71
+ if (comment) {
72
+ const icon = rt.isBusy ? "\u23F3" : "\u{1F4AC}";
73
+ const shown = comment.length > 120 ? `${comment.slice(0, 119)}\u2026` : comment;
74
+ lines.push(`${icon} ${shown}`);
75
+ }
76
+
77
+ // 4) Where: project | session | context usage.
65
78
  const loc = [`\u{1F4C1} ${project}`, `\u{1F9F5} ${session}`];
66
79
  if (ctxPct !== undefined) loc.push(`\u{1F4CA} ${ctxPct.toFixed(0)}% context`);
67
80
  lines.push(loc.join(SEP));
68
81
 
69
- // 4) How: agent | reasoning | model.
70
- lines.push([`\u{1F916} ${s.agent || "default"}`, `\u{1F9E0} ${reasoningLabel(s.reasoning)}`, `\u{1F9E9} ${s.model || "default"}`].join(SEP));
82
+ // 5) How: reasoning | model (agent picker removed — plan mode is automatic).
83
+ lines.push([`\u{1F9E0} ${reasoningLabel(s.reasoning)}`, `\u{1F9E9} ${s.model || "default"}`].join(SEP));
71
84
 
72
85
  return lines.join("\n");
73
86
  }
@@ -66,6 +66,9 @@ export function mergeInputs(inputs: PromptInput[]): PromptInput {
66
66
  const quotes = inputs
67
67
  .map((i) => i.quotedText?.trim())
68
68
  .filter((q): q is string => !!q);
69
+ // Preserve meta flags: auto-suggestion batches / self-recheck must not re-arm
70
+ // another recheck after merge (dropping this caused infinite recheck loops).
71
+ const skipSelfRecheck = inputs.some((i) => i.skipSelfRecheck);
69
72
  return {
70
73
  text: inputs
71
74
  .map((i) => i.text)
@@ -75,6 +78,7 @@ export function mergeInputs(inputs: PromptInput[]): PromptInput {
75
78
  resourceLinks: inputs.flatMap((i) => i.resourceLinks ?? []),
76
79
  replyTo: inputs.find((i) => i.replyTo !== undefined)?.replyTo,
77
80
  quotedText: quotes.length > 0 ? [...new Set(quotes)].join("\n\n---\n\n") : undefined,
81
+ skipSelfRecheck: skipSelfRecheck || undefined,
78
82
  };
79
83
  }
80
84
 
@@ -33,3 +33,14 @@ export function buildPriming(transcript: string): string {
33
33
  "=== END TRANSCRIPT ===",
34
34
  ].join("\n");
35
35
  }
36
+
37
+ /**
38
+ * First-turn prompt body used after a foreign-session import. The heavy
39
+ * transcript lives in {@link SessionRuntime}'s primingContext; this is the
40
+ * short user message that flushes priming into the live Grok session.
41
+ */
42
+ export const IMPORT_CONFIRM_PROMPT =
43
+ "Session import complete. Confirm you have the full imported context " +
44
+ "(read the transcript file if anything was truncated inline). " +
45
+ "Reply with a one-line ready confirmation: project path + one-sentence " +
46
+ "summary of the task so far. Do not continue work until I send the next message.";