grok-telegram-bot 2.3.0 → 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.
- package/.env.example +26 -0
- package/CHANGELOG.md +55 -0
- package/package.json +1 -1
- package/scripts/analyze-jsonl.ts +33 -0
- package/scripts/delayed-restart.ps1 +29 -0
- package/scripts/probe-exit-response-shape.py +77 -0
- package/scripts/probe-plan-exit.py +60 -0
- package/scripts/probe-plan-exit2.py +48 -0
- package/scripts/probe-plan-fields.py +41 -0
- package/scripts/probe-plan-fields2.py +58 -0
- package/scripts/probe-plan-response-path.py +48 -0
- package/scripts/sample-claude-tooluse.ts +21 -0
- package/scripts/sample-kiro-events.ts +31 -0
- package/scripts/smoke-exit-plan.ts +274 -0
- package/scripts/smoke-exit-shapes.ts +252 -0
- package/scripts/smoke-import.mjs +82 -0
- package/scripts/smoke-import.ts +73 -0
- package/src/app/accounts.ts +84 -0
- package/src/app/instance-lock.ts +6 -0
- package/src/app/types.ts +19 -2
- package/src/app/updater.ts +17 -6
- package/src/app/usage.ts +204 -7
- package/src/bot/account-rotator.ts +71 -2
- package/src/bot/bot.ts +36 -0
- package/src/bot/chat-controller.ts +35 -0
- package/src/bot/commands.ts +2 -0
- package/src/bot/complexity-gate.ts +69 -0
- package/src/bot/deps.ts +19 -0
- package/src/bot/handlers/accounts.ts +55 -5
- package/src/bot/handlers/import-session.ts +290 -0
- package/src/bot/handlers/menu.ts +17 -38
- package/src/bot/handlers/message.ts +1 -0
- package/src/bot/handlers/running.ts +35 -5
- package/src/bot/handlers/session-card.ts +12 -0
- package/src/bot/handlers/sessions.ts +14 -3
- package/src/bot/handlers/usage.ts +118 -16
- package/src/bot/menu/keyboard.ts +5 -4
- package/src/bot/menu/status-panel.ts +19 -6
- package/src/bot/prompt-content.ts +4 -0
- package/src/bot/reauth-controller.ts +2 -2
- package/src/bot/session-fork.ts +11 -0
- package/src/bot/session-runtime.ts +831 -64
- package/src/bot/suggestions.ts +429 -0
- package/src/config.ts +41 -0
- package/src/grok/client.ts +106 -20
- 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 +179 -24
- package/src/render/diff.ts +11 -2
- package/src/render/file-summary.ts +31 -1
- package/src/render/markdown.ts +293 -35
- package/src/render/plan.ts +127 -0
- package/src/render/session-comment.ts +261 -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 +405 -142
- package/src/render/truncate.ts +85 -0
- package/src/service/windows.ts +14 -2
- package/src/sessions/history.ts +57 -0
- package/src/sessions/store.ts +3 -0
- package/src/sessions/types.ts +5 -0
- package/src/stream/streamer.ts +73 -9
- package/src/tasks/runner.ts +4 -3
package/src/app/accounts.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/app/instance-lock.ts
CHANGED
|
@@ -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(
|
|
69
|
-
|
|
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
|
}
|
package/src/app/updater.ts
CHANGED
|
@@ -154,12 +154,23 @@ export class Updater {
|
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
156
|
log.info("re-executing the updated bot");
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
3
|
-
*
|
|
4
|
-
* ~/.grok/auth.json
|
|
5
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
}
|
|
@@ -30,6 +30,12 @@ export interface RotationTarget {
|
|
|
30
30
|
label: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export interface RotationState {
|
|
34
|
+
generation: number;
|
|
35
|
+
activeId?: string;
|
|
36
|
+
activeLabel?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
export interface AccountRotator {
|
|
34
40
|
/** Whether auto-rotate is switched on. */
|
|
35
41
|
enabled(): boolean;
|
|
@@ -39,9 +45,21 @@ export interface AccountRotator {
|
|
|
39
45
|
activate(id: string): Promise<void>;
|
|
40
46
|
/** Quarantine an account after an account-specific failure. Undefined means active. */
|
|
41
47
|
markFailed(id: string | undefined, reason: string): Promise<void>;
|
|
48
|
+
/** Process/account generation observed when a turn failed. */
|
|
49
|
+
state(): RotationState;
|
|
50
|
+
/** Serialize a complete rotation probe. `changed` means another chat already
|
|
51
|
+
* selected/restarted an account while this caller was waiting. */
|
|
52
|
+
withRotationLock<T>(observed: RotationState, run: (changed: boolean) => Promise<T>): Promise<T>;
|
|
53
|
+
/** Wait for an in-progress rotation probe before re-binding a stale session. */
|
|
54
|
+
waitForIdle(): Promise<void>;
|
|
55
|
+
/** Record a completed turn's usage against the active saved account. */
|
|
56
|
+
recordTurnUsage(stats: { credits?: number; contextPct?: number }): void;
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
export class AccountRotatorImpl implements AccountRotator {
|
|
60
|
+
private generation = 0;
|
|
61
|
+
private rotationTail: Promise<void> = Promise.resolve();
|
|
62
|
+
|
|
45
63
|
constructor(
|
|
46
64
|
private readonly accounts: AccountManager,
|
|
47
65
|
private readonly acp: GrokClient,
|
|
@@ -51,6 +69,50 @@ export class AccountRotatorImpl implements AccountRotator {
|
|
|
51
69
|
return this.accounts.autoRotateEnabled();
|
|
52
70
|
}
|
|
53
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
|
+
|
|
80
|
+
state(): RotationState {
|
|
81
|
+
const activeId = this.accounts.activeAccountId();
|
|
82
|
+
return {
|
|
83
|
+
generation: this.generation,
|
|
84
|
+
activeId,
|
|
85
|
+
activeLabel: activeId ? this.accounts.get(activeId)?.label : undefined,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async withRotationLock<T>(observed: RotationState, run: (changed: boolean) => Promise<T>): Promise<T> {
|
|
90
|
+
const previous = this.rotationTail;
|
|
91
|
+
let release!: () => void;
|
|
92
|
+
this.rotationTail = new Promise<void>((resolve) => {
|
|
93
|
+
release = resolve;
|
|
94
|
+
});
|
|
95
|
+
await previous;
|
|
96
|
+
try {
|
|
97
|
+
const current = this.state();
|
|
98
|
+
const changed =
|
|
99
|
+
current.generation !== observed.generation || current.activeId !== observed.activeId;
|
|
100
|
+
return await run(changed);
|
|
101
|
+
} finally {
|
|
102
|
+
release();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async waitForIdle(): Promise<void> {
|
|
107
|
+
// Include work queued while we were waiting, not only the first captured
|
|
108
|
+
// promise, so callers never re-bind in the middle of a candidate switch.
|
|
109
|
+
for (;;) {
|
|
110
|
+
const pending = this.rotationTail;
|
|
111
|
+
await pending;
|
|
112
|
+
if (pending === this.rotationTail) return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
54
116
|
async targets(): Promise<RotationTarget[]> {
|
|
55
117
|
const list = this.accounts.list();
|
|
56
118
|
const activeId = this.accounts.activeAccountId();
|
|
@@ -96,12 +158,19 @@ export class AccountRotatorImpl implements AccountRotator {
|
|
|
96
158
|
log.info(`rotating: auth.json now ${meta.label}; starting Grok CLI + re-auth`);
|
|
97
159
|
// start() → connect() → initialize + authenticate(cached_token) against
|
|
98
160
|
// the freshly written auth.json. A live process would keep the old token.
|
|
99
|
-
await this.acp.start();
|
|
161
|
+
await this.acp.start(true);
|
|
162
|
+
this.generation++;
|
|
100
163
|
log.info(`rotating: Grok CLI up on ${meta.label}`);
|
|
101
164
|
} catch (e) {
|
|
102
165
|
// Best-effort recover the agent so the bot stays usable even if the
|
|
103
166
|
// target login was bad.
|
|
104
|
-
await this.acp.
|
|
167
|
+
await this.acp.stopAndWait().catch(() => {});
|
|
168
|
+
await this.acp
|
|
169
|
+
.start(true)
|
|
170
|
+
.then(() => {
|
|
171
|
+
this.generation++;
|
|
172
|
+
})
|
|
173
|
+
.catch((err) => log.warn("post-rotate restart failed:", (err as Error).message));
|
|
105
174
|
throw e;
|
|
106
175
|
}
|
|
107
176
|
}
|
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
|
}
|
package/src/bot/commands.ts
CHANGED
|
@@ -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",
|