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
@@ -20,6 +20,23 @@ import type { AccountInfo } from "./usage.js";
20
20
 
21
21
  const log = createLogger("accounts");
22
22
 
23
+ /** Real usage counters recorded by the bot from completed turns (not billing API). */
24
+ export interface AccountUsage {
25
+ /** Completed (non-cancelled) turns on this account. */
26
+ turns: number;
27
+ /**
28
+ * Sum of per-turn credit figures Grok reported via `_grok.dev/metadata`.
29
+ * Only increments when the agent actually sends a credits value.
30
+ */
31
+ credits: number;
32
+ /** ISO timestamp of the last successful turn on this account. */
33
+ lastUsedAt?: string;
34
+ /** Credits reported on the most recent turn (if any). */
35
+ lastTurnCredits?: number;
36
+ /** Context-window % from the most recent turn that reported it. */
37
+ lastContextPct?: number;
38
+ }
39
+
23
40
  /** Persisted, non-secret metadata about a saved account. */
24
41
  export interface StoredAccount {
25
42
  id: string;
@@ -32,6 +49,8 @@ export interface StoredAccount {
32
49
  startUrl?: string;
33
50
  accountType?: string;
34
51
  region?: string;
52
+ /** Live usage stats accumulated while this account was active. */
53
+ usage?: AccountUsage;
35
54
  /** Excluded from automatic rotation after an account-specific quota/billing failure. */
36
55
  warning?: {
37
56
  reason: string;
@@ -262,6 +281,53 @@ export class AccountManager {
262
281
  return updated;
263
282
  }
264
283
 
284
+ /**
285
+ * Record a completed turn against the active (or given) saved account.
286
+ * Credits are only added when Grok reported a figure for the turn; turns
287
+ * always increment so /accounts shows real activity even without credits.
288
+ */
289
+ recordTurnUsage(
290
+ stats: { credits?: number; contextPct?: number },
291
+ accountId?: string,
292
+ ): StoredAccount | undefined {
293
+ const id = accountId ?? this.activeAccountId() ?? this.markedActiveId();
294
+ if (!id) return undefined;
295
+ let updated: StoredAccount | undefined;
296
+ this.store.update((d) => {
297
+ const account = d.accounts.find((a) => a.id === id);
298
+ if (!account) return;
299
+ const prev = account.usage ?? { turns: 0, credits: 0 };
300
+ const credits =
301
+ typeof stats.credits === "number" && Number.isFinite(stats.credits) && stats.credits > 0
302
+ ? stats.credits
303
+ : undefined;
304
+ const next: AccountUsage = {
305
+ turns: (prev.turns || 0) + 1,
306
+ credits: (prev.credits || 0) + (credits ?? 0),
307
+ lastUsedAt: new Date().toISOString(),
308
+ lastTurnCredits: credits ?? prev.lastTurnCredits,
309
+ lastContextPct:
310
+ typeof stats.contextPct === "number" && Number.isFinite(stats.contextPct)
311
+ ? stats.contextPct
312
+ : prev.lastContextPct,
313
+ };
314
+ account.usage = next;
315
+ updated = account;
316
+ });
317
+ return updated;
318
+ }
319
+
320
+ /** Compact one-line usage summary for menus (empty when no stats yet). */
321
+ formatUsageLine(account: StoredAccount): string {
322
+ const u = account.usage;
323
+ if (!u || (u.turns <= 0 && u.credits <= 0 && !u.lastUsedAt)) return "";
324
+ const parts: string[] = [];
325
+ if (u.turns > 0) parts.push(`${u.turns} turn${u.turns === 1 ? "" : "s"}`);
326
+ if (u.credits > 0) parts.push(`${fmtUsageNumber(u.credits)} credits`);
327
+ if (u.lastUsedAt) parts.push(`last ${fmtRelative(u.lastUsedAt)}`);
328
+ return parts.join(" \u00B7 ");
329
+ }
330
+
265
331
  async forget(id: string): Promise<boolean> {
266
332
  const existed = !!this.get(id);
267
333
  await rm(this.snapshotPath(id), { force: true }).catch(() => {});
@@ -272,3 +338,21 @@ export class AccountManager {
272
338
  return existed;
273
339
  }
274
340
  }
341
+
342
+ function fmtUsageNumber(n: number): string {
343
+ if (!Number.isFinite(n)) return String(n);
344
+ if (Number.isInteger(n)) return n.toLocaleString("en-US");
345
+ return n.toFixed(2);
346
+ }
347
+
348
+ /** Short relative time for usage lines ("2h ago", "just now"). */
349
+ function fmtRelative(iso: string): string {
350
+ const t = Date.parse(iso);
351
+ if (!Number.isFinite(t)) return iso.slice(0, 10);
352
+ const sec = Math.max(0, Math.round((Date.now() - t) / 1000));
353
+ if (sec < 60) return "just now";
354
+ if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
355
+ if (sec < 86_400) return `${Math.floor(sec / 3600)}h ago`;
356
+ if (sec < 86_400 * 14) return `${Math.floor(sec / 86_400)}d ago`;
357
+ return new Date(t).toISOString().slice(0, 10);
358
+ }
@@ -88,6 +88,12 @@ export class InstanceLock {
88
88
  }
89
89
  }
90
90
 
91
+ /** Refresh lock metadata while we still hold it (lifetime heartbeat). */
92
+ touch(): void {
93
+ if (!this.held) return;
94
+ this.write();
95
+ }
96
+
91
97
  private write(): void {
92
98
  const data: LockData = { pid: process.pid, startedAt: Date.now(), supervised: this.supervised };
93
99
  try {
package/src/app/types.ts CHANGED
@@ -63,8 +63,25 @@ export interface PromptInput {
63
63
  * to. See {@link ../bot/reply-context.ts}.
64
64
  */
65
65
  quotedText?: string;
66
+ /**
67
+ * System/meta turns (self-recheck, auto-approved suggestion batches) must not
68
+ * trigger another self-recheck — only real user prompts do (once each).
69
+ */
70
+ skipSelfRecheck?: boolean;
66
71
  }
67
72
 
68
- export function textPrompt(text: string, replyTo?: number, quotedText?: string): PromptInput {
69
- return { text, images: [], resourceLinks: [], replyTo, quotedText };
73
+ export function textPrompt(
74
+ text: string,
75
+ replyTo?: number,
76
+ quotedText?: string,
77
+ opts?: { skipSelfRecheck?: boolean },
78
+ ): PromptInput {
79
+ return {
80
+ text,
81
+ images: [],
82
+ resourceLinks: [],
83
+ replyTo,
84
+ quotedText,
85
+ skipSelfRecheck: opts?.skipSelfRecheck,
86
+ };
70
87
  }
@@ -154,12 +154,23 @@ export class Updater {
154
154
  return;
155
155
  }
156
156
  log.info("re-executing the updated bot");
157
- const child = spawn(
158
- process.execPath,
159
- ["--import", "tsx", join(this.opts.projectRoot, "src", "index.ts"), "--instance", this.opts.instanceDir],
160
- { detached: true, stdio: "ignore", cwd: this.opts.projectRoot, env: process.env },
161
- );
162
- child.unref();
157
+ try {
158
+ const child = spawn(
159
+ process.execPath,
160
+ ["--import", "tsx", join(this.opts.projectRoot, "src", "index.ts"), "--instance", this.opts.instanceDir],
161
+ { detached: true, stdio: "ignore", cwd: this.opts.projectRoot, env: process.env },
162
+ );
163
+ child.unref();
164
+ if (!child.pid) {
165
+ log.error("re-exec spawn produced no pid — staying alive");
166
+ return;
167
+ }
168
+ log.info(`re-exec child pid ${child.pid}`);
169
+ } catch (e) {
170
+ // Never exit if replacement failed — silent death is worse than stale code.
171
+ log.error(`re-exec failed: ${(e as Error).message} — staying alive`);
172
+ return;
173
+ }
163
174
  setTimeout(() => process.exit(0), 500);
164
175
  }
165
176
 
package/src/app/usage.ts CHANGED
@@ -1,10 +1,27 @@
1
1
  /**
2
- * Account info for Grok Build. Grok signs in with your xAI account
3
- * (`grok login`), so /usage surfaces the signed-in identity (from the token in
4
- * ~/.grok/auth.json) plus the live per-session context usage the ACP agent
5
- * reports.
2
+ * Account + Grok CLI billing usage.
3
+ *
4
+ * Identity comes from ~/.grok/auth.json. Live Grok Build quota is fetched from
5
+ * the same CLI chat proxy OmniRoute-style clients use:
6
+ * GET https://cli-chat-proxy.grok.com/v1/billing
7
+ * with the OIDC token from `grok login` (Bearer).
8
+ *
9
+ * Session-level context/credits still come from ACP `_grok.dev/metadata`.
6
10
  */
7
- import { hasLogin, identityFromAuth, loginLabel } from "./grok-credentials.js";
11
+ import { createLogger } from "../logger.js";
12
+ import {
13
+ currentAuthEntry,
14
+ currentToken,
15
+ hasLogin,
16
+ identityFromAuth,
17
+ loginLabel,
18
+ } from "./grok-credentials.js";
19
+
20
+ const log = createLogger("usage");
21
+
22
+ const BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing";
23
+ /** Cache live billing for a short window so /usage + /accounts don't spam the API. */
24
+ const BILLING_TTL_MS = 30_000;
8
25
 
9
26
  export interface AccountInfo {
10
27
  /** Signed-in identity (email when the token carries one, else a label). */
@@ -14,25 +31,205 @@ export interface AccountInfo {
14
31
  region?: string;
15
32
  /** Stable identifier for matching saved accounts. */
16
33
  startUrl?: string;
34
+ /** JWT tier claim when present (Grok CLI subscription tier). */
35
+ tier?: string | number;
36
+ teamId?: string;
37
+ }
38
+
39
+ /** Live Grok CLI monthly quota from cli-chat-proxy. */
40
+ export interface GrokCliBilling {
41
+ /** Included monthly allowance (raw units from the API). */
42
+ monthlyLimit: number;
43
+ /** Used so far this billing period. */
44
+ used: number;
45
+ /** Remaining = max(0, limit - used). */
46
+ remaining: number;
47
+ /** 0–100 percent of monthly limit consumed. */
48
+ usedPct: number;
49
+ onDemandCap: number;
50
+ billingPeriodStart?: string;
51
+ billingPeriodEnd?: string;
52
+ /** Prior cycles when the API returns them. */
53
+ history?: Array<{ year: number; month: number; totalUsed: number }>;
54
+ fetchedAt: string;
55
+ }
56
+
57
+ interface BillingCache {
58
+ at: number;
59
+ data?: GrokCliBilling;
60
+ error?: string;
17
61
  }
18
62
 
19
63
  export class UsageService {
64
+ private billingCache: BillingCache | undefined;
65
+
20
66
  // Kept for signature compatibility; Grok state lives in ~/.grok/auth.json.
21
67
  constructor(private readonly grokCliPath: string) {}
22
68
 
23
69
  async account(): Promise<AccountInfo | undefined> {
24
70
  if (!hasLogin()) {
25
- // XAI_API_KEY with no browser login still counts as usable.
26
71
  if (process.env.XAI_API_KEY?.trim()) return { email: "XAI_API_KEY", accountType: "api key" };
27
72
  return undefined;
28
73
  }
29
74
  const id = identityFromAuth();
30
75
  const label = loginLabel();
31
- return { email: id.email || label, accountType: undefined, startUrl: label };
76
+ const entry = currentAuthEntry();
77
+ const tok = currentToken();
78
+ let tier: string | number | undefined;
79
+ let teamId = typeof entry?.team_id === "string" ? entry.team_id : undefined;
80
+ if (tok) {
81
+ const claims = decodeJwt(tok);
82
+ if (claims?.tier !== undefined) tier = claims.tier as string | number;
83
+ if (!teamId && typeof claims?.team_id === "string") teamId = claims.team_id;
84
+ }
85
+ return {
86
+ email: id.email || label,
87
+ accountType: tier !== undefined ? `tier ${tier}` : undefined,
88
+ startUrl: label,
89
+ tier,
90
+ teamId,
91
+ };
32
92
  }
33
93
 
34
94
  /** Whether Grok has a usable sign-in (browser token or XAI_API_KEY). */
35
95
  async isLoggedIn(): Promise<boolean> {
36
96
  return hasLogin();
37
97
  }
98
+
99
+ /**
100
+ * Live Grok Build monthly quota for the active CLI login.
101
+ * Same endpoint OmniRoute uses for grok-cli remaining % dashboards.
102
+ */
103
+ async cliBilling(force = false): Promise<{ billing?: GrokCliBilling; error?: string }> {
104
+ const now = Date.now();
105
+ if (
106
+ !force &&
107
+ this.billingCache &&
108
+ now - this.billingCache.at < BILLING_TTL_MS
109
+ ) {
110
+ return { billing: this.billingCache.data, error: this.billingCache.error };
111
+ }
112
+
113
+ const token = currentToken();
114
+ if (!token) {
115
+ if (process.env.XAI_API_KEY?.trim()) {
116
+ return { error: "XAI_API_KEY mode — CLI monthly quota is only available after `grok login`" };
117
+ }
118
+ return { error: "Not signed in (no grok login token)" };
119
+ }
120
+
121
+ try {
122
+ const res = await fetch(BILLING_URL, {
123
+ method: "GET",
124
+ headers: {
125
+ Authorization: `Bearer ${token}`,
126
+ Accept: "application/json",
127
+ },
128
+ signal: AbortSignal.timeout(12_000),
129
+ });
130
+ if (!res.ok) {
131
+ const err = `billing HTTP ${res.status}`;
132
+ log.warn(err);
133
+ this.billingCache = { at: now, error: err };
134
+ return { error: err };
135
+ }
136
+ const json = (await res.json()) as BillingWire;
137
+ const billing = parseBilling(json);
138
+ this.billingCache = { at: now, data: billing };
139
+ return { billing };
140
+ } catch (e) {
141
+ const err = (e as Error).message || "billing fetch failed";
142
+ log.warn("cli billing:", err);
143
+ this.billingCache = { at: now, error: err };
144
+ return { error: err };
145
+ }
146
+ }
147
+ }
148
+
149
+ interface BillingWire {
150
+ config?: {
151
+ monthlyLimit?: { val?: number };
152
+ used?: { val?: number };
153
+ onDemandCap?: { val?: number };
154
+ billingPeriodStart?: string;
155
+ billingPeriodEnd?: string;
156
+ history?: Array<{
157
+ billingCycle?: { year?: number; month?: number };
158
+ totalUsed?: { val?: number };
159
+ includedUsed?: { val?: number };
160
+ onDemandUsed?: { val?: number };
161
+ }>;
162
+ };
163
+ }
164
+
165
+ function parseBilling(json: BillingWire): GrokCliBilling {
166
+ const c = json.config ?? {};
167
+ const monthlyLimit = num(c.monthlyLimit?.val);
168
+ const used = num(c.used?.val);
169
+ const remaining = Math.max(0, monthlyLimit - used);
170
+ const usedPct = monthlyLimit > 0 ? Math.min(100, Math.round((used / monthlyLimit) * 1000) / 10) : 0;
171
+ const history = (c.history ?? [])
172
+ .map((h) => ({
173
+ year: h.billingCycle?.year ?? 0,
174
+ month: h.billingCycle?.month ?? 0,
175
+ totalUsed: num(h.totalUsed?.val ?? h.includedUsed?.val),
176
+ }))
177
+ .filter((h) => h.year > 0);
178
+ return {
179
+ monthlyLimit,
180
+ used,
181
+ remaining,
182
+ usedPct,
183
+ onDemandCap: num(c.onDemandCap?.val),
184
+ billingPeriodStart: c.billingPeriodStart,
185
+ billingPeriodEnd: c.billingPeriodEnd,
186
+ history: history.length ? history : undefined,
187
+ fetchedAt: new Date().toISOString(),
188
+ };
189
+ }
190
+
191
+ function num(v: unknown): number {
192
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
193
+ }
194
+
195
+ function decodeJwt(jwt: string): Record<string, unknown> | undefined {
196
+ const parts = jwt.split(".");
197
+ if (parts.length < 2) return undefined;
198
+ try {
199
+ return JSON.parse(Buffer.from(parts[1]!, "base64url").toString("utf-8")) as Record<string, unknown>;
200
+ } catch {
201
+ return undefined;
202
+ }
203
+ }
204
+
205
+ /** Format billing for Telegram plain-text UIs. */
206
+ export function formatCliBillingLines(b: GrokCliBilling): string[] {
207
+ const lines = [
208
+ "\u{1F4B3} Grok CLI monthly quota (cli-chat-proxy)",
209
+ ` Used: ${fmt(b.used)} / ${fmt(b.monthlyLimit)} (${b.usedPct}%)`,
210
+ ` Remaining: ${fmt(b.remaining)}`,
211
+ ];
212
+ if (b.billingPeriodStart || b.billingPeriodEnd) {
213
+ lines.push(
214
+ ` Period: ${fmtDate(b.billingPeriodStart)} \u2192 ${fmtDate(b.billingPeriodEnd)}`,
215
+ );
216
+ }
217
+ if (b.onDemandCap > 0) lines.push(` On-demand cap: ${fmt(b.onDemandCap)}`);
218
+ if (b.history?.length) {
219
+ const prev = b.history
220
+ .slice(0, 3)
221
+ .map((h) => `${h.year}-${String(h.month).padStart(2, "0")}: ${fmt(h.totalUsed)}`)
222
+ .join(" \u00B7 ");
223
+ lines.push(` History: ${prev}`);
224
+ }
225
+ return lines;
226
+ }
227
+
228
+ function fmt(n: number): string {
229
+ return n.toLocaleString("en-US");
230
+ }
231
+
232
+ function fmtDate(iso?: string): string {
233
+ if (!iso) return "\u2014";
234
+ return iso.slice(0, 10);
38
235
  }
@@ -52,6 +52,8 @@ export interface AccountRotator {
52
52
  withRotationLock<T>(observed: RotationState, run: (changed: boolean) => Promise<T>): Promise<T>;
53
53
  /** Wait for an in-progress rotation probe before re-binding a stale session. */
54
54
  waitForIdle(): Promise<void>;
55
+ /** Record a completed turn's usage against the active saved account. */
56
+ recordTurnUsage(stats: { credits?: number; contextPct?: number }): void;
55
57
  }
56
58
 
57
59
  export class AccountRotatorImpl implements AccountRotator {
@@ -67,6 +69,14 @@ export class AccountRotatorImpl implements AccountRotator {
67
69
  return this.accounts.autoRotateEnabled();
68
70
  }
69
71
 
72
+ recordTurnUsage(stats: { credits?: number; contextPct?: number }): void {
73
+ try {
74
+ this.accounts.recordTurnUsage(stats);
75
+ } catch (e) {
76
+ log.debug("recordTurnUsage failed:", (e as Error).message);
77
+ }
78
+ }
79
+
70
80
  state(): RotationState {
71
81
  const activeId = this.accounts.activeAccountId();
72
82
  return {
package/src/bot/bot.ts CHANGED
@@ -11,6 +11,7 @@ import { SettingsStore } from "../app/settings-store.js";
11
11
  import { SttService } from "../app/stt.js";
12
12
  import { Updater } from "../app/updater.js";
13
13
  import { UsageService } from "../app/usage.js";
14
+ import { textPrompt } from "../app/types.js";
14
15
  import type { AppConfig } from "../config.js";
15
16
  import { INSTANCE_DIR } from "../config.js";
16
17
  import { createLogger } from "../logger.js";
@@ -26,6 +27,7 @@ import { type BotDeps, MenuCache } from "./deps.js";
26
27
  import { registerControl } from "./handlers/control.js";
27
28
  import { registerDocuments } from "./handlers/document.js";
28
29
  import { registerHistory } from "./handlers/history.js";
30
+ import { registerImportSession } from "./handlers/import-session.js";
29
31
  import { registerKill } from "./handlers/kill.js";
30
32
  import { registerMcp } from "./handlers/mcp.js";
31
33
  import { registerMenu } from "./handlers/menu.js";
@@ -171,11 +173,45 @@ export async function createBot(cfg: AppConfig, acp: GrokClient): Promise<BotBun
171
173
  if (sid) await switchAndShow(ctx, deps, sid);
172
174
  });
173
175
 
176
+ // Legacy complexity buttons (removed — agent decides; auto-plan if complex).
177
+ bot.callbackQuery(/^cplx:(simple|complex)$/, async (ctx) => {
178
+ await ctx.answerCallbackQuery({ text: "Complexity is automatic now" });
179
+ await ctx
180
+ .editMessageText("\u2705 Complexity is decided by the agent automatically \u2014 just send your task.", {
181
+ reply_markup: { inline_keyboard: [] },
182
+ })
183
+ .catch(() => {});
184
+ });
185
+
186
+ // Post-turn suggestion buttons on the Done message.
187
+ bot.callbackQuery(/^sug:(\d+):(\d+)$/, async (ctx) => {
188
+ const batchId = Number(ctx.match![1]);
189
+ const index = Number(ctx.match![2]);
190
+ const rt = deps.registry.get(ctx.chat!.id);
191
+ const text = rt.takeSuggestion(batchId, index);
192
+ if (!text) {
193
+ await ctx.answerCallbackQuery({ text: "Suggestion expired", show_alert: true });
194
+ return;
195
+ }
196
+ await ctx.answerCallbackQuery({ text: "Sending\u2026" });
197
+ // Dim the keyboard so double-taps don't re-fire.
198
+ await ctx.editMessageReplyMarkup({ reply_markup: { inline_keyboard: [] } }).catch(() => {});
199
+ try {
200
+ const outcome = await rt.submit(textPrompt(text, ctx.callbackQuery.message?.message_id));
201
+ if (outcome === "queued") {
202
+ await ctx.reply(`\u{1F4E5} Queued suggestion (position ${rt.queueLength}).`).catch(() => {});
203
+ }
204
+ } catch (e) {
205
+ await ctx.reply(`\u274C Couldn't run suggestion: ${(e as Error).message}`).catch(() => {});
206
+ }
207
+ });
208
+
174
209
  registerMenu(bot, deps); // persistent-keyboard buttons (hears)
175
210
  registerWizardInput(bot, deps); // wizard text input (before commands)
176
211
  registerControl(bot, deps);
177
212
  registerProjects(bot, deps);
178
213
  registerSessions(bot, deps);
214
+ registerImportSession(bot, deps);
179
215
  registerSessionKill(bot, deps);
180
216
  registerRunning(bot, deps);
181
217
  registerHistory(bot, deps);
@@ -23,6 +23,11 @@ export interface RunningSession {
23
23
  unread: number;
24
24
  /** Latest task-completion % (0–100) for this session, if known. */
25
25
  progress?: number;
26
+ /**
27
+ * Card comment: live step while working, chat summary when idle
28
+ * ("what is happening / what was done").
29
+ */
30
+ comment?: string;
26
31
  }
27
32
 
28
33
  export interface SwitchResult {
@@ -76,6 +81,7 @@ export class ChatController {
76
81
  foreground: rt.isForeground,
77
82
  unread: this.unreadCount(rt),
78
83
  progress: rt.taskProgress,
84
+ comment: rt.cardComment,
79
85
  }));
80
86
  }
81
87
 
@@ -142,6 +148,28 @@ export class ChatController {
142
148
  return rt;
143
149
  }
144
150
 
151
+ /**
152
+ * Import a foreign session (Kiro / OpenCode / Claude / Codex) as a new
153
+ * controlled Grok session in the same project, primed with the full transcript.
154
+ * The new session becomes the foreground /running entry.
155
+ */
156
+ async addImport(
157
+ cwd: string,
158
+ projectName: string | undefined,
159
+ priming: string,
160
+ ): Promise<SessionRuntime> {
161
+ this.ensureRestored();
162
+ const prevFg = this.fg;
163
+ const rt = this.create({ cwd, projectName });
164
+ this.runtimes.push(rt);
165
+ this.fg = rt;
166
+ void this.background(prevFg);
167
+ await rt.startImportedSession(cwd, projectName, priming);
168
+ this.markSeen(rt);
169
+ this.persist();
170
+ return rt;
171
+ }
172
+
145
173
  /**
146
174
  * Connect to a session with resume-or-fork semantics (used by /sessions),
147
175
  * adding it as a controlled session and bringing it to the foreground.
@@ -251,6 +279,13 @@ export class ChatController {
251
279
  return this.runtimes.find((r) => r.sessionId === sessionId)?.taskProgress;
252
280
  }
253
281
 
282
+ /** Live step / chat summary for a controlled session id. */
283
+ commentFor(sessionId?: string): string | undefined {
284
+ if (!sessionId) return undefined;
285
+ this.ensureRestored();
286
+ return this.runtimes.find((r) => r.sessionId === sessionId)?.cardComment;
287
+ }
288
+
254
289
  findBySession(sessionId: string): boolean {
255
290
  return this.runtimes.some((r) => r.sessionId === sessionId);
256
291
  }
@@ -8,6 +8,7 @@ export const COMMANDS: { command: string; description: string }[] = [
8
8
  { command: "sessions", description: "List/resume sessions (active first) \u00b7 /sessions <q>" },
9
9
  { command: "active", description: "Sessions running now on the PC" },
10
10
  { command: "running", description: "Sessions this chat controls \u2014 switch between them" },
11
+ { command: "import", description: "Import a Kiro/OpenCode/Claude/Codex /running session" },
11
12
  { command: "killall", description: "Kill all active sessions on the PC" },
12
13
  { command: "mcp", description: "Inspect & toggle MCP servers \u00b7 health-check" },
13
14
  { command: "tasks", description: "Manage scheduled tasks" },
@@ -41,6 +42,7 @@ export const HELP_TEXT = [
41
42
  "/projects \u2014 choose which folder Grok works in",
42
43
  "/sessions \u2014 resume one of your recent Grok sessions",
43
44
  "/active \u2014 attach to a session currently running on the PC",
45
+ "/import \u2014 import a /running session from Kiro, OpenCode, Claude, or Codex",
44
46
  "/history \u2014 show the latest messages of the current session",
45
47
  "/new \u2014 start a brand-new session in the current project",
46
48
  "/btw <text> \u2014 run it now if idle, otherwise right after the current task",
@@ -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
+ }