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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok plan-mode client reverse-requests (`x.ai/exit_plan_mode`,
|
|
3
|
+
* `x.ai/ask_user_question`).
|
|
4
|
+
*
|
|
5
|
+
* When the agent finishes planning it calls the `exit_plan_mode` tool. Grok
|
|
6
|
+
* intercepts that tool and sends a JSON-RPC **request** to the ACP client
|
|
7
|
+
* (`x.ai/exit_plan_mode`) so a TUI can show the plan approval UI. The Telegram
|
|
8
|
+
* bridge has no plan popup — it must answer the reverse-request immediately.
|
|
9
|
+
*
|
|
10
|
+
* Without a response (or with method-not-found), Grok fails the tool with:
|
|
11
|
+
* "Plan approval could not be completed because the client disconnected."
|
|
12
|
+
* and plan mode stays Active (`awaiting_plan_approval: true`), blocking edits.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Method names Grok sends as client reverse-requests for plan / questions.
|
|
17
|
+
*
|
|
18
|
+
* Live ACP traffic (verified by smoke test) uses a leading underscore:
|
|
19
|
+
* `_x.ai/exit_plan_mode`
|
|
20
|
+
* The bare `x.ai/…` form is kept for older builds / leader-path aliases.
|
|
21
|
+
*/
|
|
22
|
+
export const EXT_EXIT_PLAN_MODE = "_x.ai/exit_plan_mode";
|
|
23
|
+
export const EXT_EXIT_PLAN_MODE_ALT = "x.ai/exit_plan_mode";
|
|
24
|
+
export const EXT_ASK_USER_QUESTION = "_x.ai/ask_user_question";
|
|
25
|
+
export const EXT_ASK_USER_QUESTION_ALT = "x.ai/ask_user_question";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Auto-approve leaving plan mode so the agent can implement.
|
|
29
|
+
*
|
|
30
|
+
* Reverse-request params (ExitPlanModeExtRequest, 3 fields) look like:
|
|
31
|
+
* { sessionId, toolCallId, planContent }
|
|
32
|
+
*
|
|
33
|
+
* Response shape verified by live smoke (`scripts/smoke-exit-shapes.ts`):
|
|
34
|
+
* { outcome: "approved", feedback: "" }
|
|
35
|
+
* → plan_mode.json becomes `Inactive`.
|
|
36
|
+
*
|
|
37
|
+
* Wrong shapes (e.g. `decision: "approved"`, empty `{}`) are treated as
|
|
38
|
+
* "user wants to revise the plan" and leave plan mode Active.
|
|
39
|
+
*/
|
|
40
|
+
export function autoApproveExitPlanMode(_params?: Record<string, unknown>): Record<string, unknown> {
|
|
41
|
+
return {
|
|
42
|
+
outcome: "approved",
|
|
43
|
+
feedback: "",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Headless answer for ask_user_question reverse-request: skip the interview so
|
|
49
|
+
* the agent is not stuck waiting for a TUI that does not exist.
|
|
50
|
+
*
|
|
51
|
+
* Externally-tagged enum variant (`SkipInterview`) from AskUserQuestionExtResponse.
|
|
52
|
+
*/
|
|
53
|
+
export function autoSkipAskUserQuestion(_params?: Record<string, unknown>): Record<string, unknown> {
|
|
54
|
+
return { SkipInterview: null };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Normalize method names: strip optional leading underscore for matching. */
|
|
58
|
+
function normMethod(method: string): string {
|
|
59
|
+
const m = (method || "").trim();
|
|
60
|
+
return m.startsWith("_") ? m.slice(1) : m;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** True when the method is a plan-approval reverse-request we must answer. */
|
|
64
|
+
export function isPlanExitMethod(method: string): boolean {
|
|
65
|
+
const m = normMethod(method);
|
|
66
|
+
return m === "x.ai/exit_plan_mode" || m.endsWith("/exit_plan_mode");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isAskUserQuestionMethod(method: string): boolean {
|
|
70
|
+
const m = normMethod(method);
|
|
71
|
+
return m === "x.ai/ask_user_question" || m.endsWith("/ask_user_question");
|
|
72
|
+
}
|
package/src/grok/session-log.ts
CHANGED
|
@@ -26,6 +26,11 @@ interface SessionFile {
|
|
|
26
26
|
grok_session_id?: string;
|
|
27
27
|
/** Model last used for this session (applied via `grok --model`). */
|
|
28
28
|
model?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Short human comment for Running/Sessions cards: live step while working,
|
|
31
|
+
* AI/local summary when idle. Display-only; not used as agent context.
|
|
32
|
+
*/
|
|
33
|
+
comment?: string;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export class SessionLog {
|
|
@@ -101,6 +106,17 @@ export class SessionLog {
|
|
|
101
106
|
return this.read(id)?.cwd;
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
commentFor(id: string): string | undefined {
|
|
110
|
+
const c = this.read(id)?.comment?.trim();
|
|
111
|
+
return c || undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setComment(id: string, comment: string): void {
|
|
115
|
+
const c = comment.trim();
|
|
116
|
+
if (!c) return;
|
|
117
|
+
this.update(id, { comment: c });
|
|
118
|
+
}
|
|
119
|
+
|
|
104
120
|
// ── event log (history-compatible jsonl) ──────────────────────────────────
|
|
105
121
|
|
|
106
122
|
private append(id: string, kind: string, data: Record<string, unknown>): void {
|
package/src/grok/types.ts
CHANGED
|
@@ -85,23 +85,35 @@ export interface SessionUpdate {
|
|
|
85
85
|
| "plan"
|
|
86
86
|
| "user_message_chunk"
|
|
87
87
|
| string;
|
|
88
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Message chunk body (ContentBlock) OR tool-call content array (ACP standard).
|
|
90
|
+
* Callers must narrow based on sessionUpdate — use {@link contentText} helper.
|
|
91
|
+
*/
|
|
92
|
+
content?: ContentBlock | ToolCallContent[] | (ContentBlock & Record<string, unknown>) | unknown;
|
|
89
93
|
toolCallId?: string;
|
|
94
|
+
/** Human title (sometimes generic "Tool call" from Grok). */
|
|
90
95
|
title?: string;
|
|
96
|
+
/** Stable tool identity when the agent sends it (ACP RFD / Grok extensions). */
|
|
97
|
+
name?: string;
|
|
98
|
+
toolName?: string;
|
|
91
99
|
kind?: string; // "read" | "edit" | "execute" | "search" | ...
|
|
92
100
|
status?: "pending" | "in_progress" | "completed" | "failed" | string;
|
|
93
101
|
rawInput?: Record<string, unknown>;
|
|
102
|
+
rawOutput?: unknown;
|
|
103
|
+
/** Some agents use content_blocks instead of content. */
|
|
94
104
|
content_blocks?: ToolCallContent[];
|
|
105
|
+
locations?: Array<{ path?: string; line?: number }>;
|
|
95
106
|
[k: string]: unknown;
|
|
96
107
|
}
|
|
97
108
|
|
|
98
109
|
/** A piece of tool-call content (text, diff, etc.). */
|
|
99
110
|
export interface ToolCallContent {
|
|
100
|
-
type: "content" | "diff" | string;
|
|
111
|
+
type: "content" | "diff" | "terminal" | string;
|
|
101
112
|
path?: string;
|
|
102
113
|
oldText?: string | null;
|
|
103
114
|
newText?: string;
|
|
104
115
|
content?: ContentBlock;
|
|
116
|
+
terminalId?: string;
|
|
105
117
|
[k: string]: unknown;
|
|
106
118
|
}
|
|
107
119
|
|
|
@@ -110,6 +122,13 @@ export interface SessionNotificationParams {
|
|
|
110
122
|
update: SessionUpdate;
|
|
111
123
|
}
|
|
112
124
|
|
|
125
|
+
/** Safe text extraction when content is a ContentBlock (not a tool content array). */
|
|
126
|
+
export function contentText(content: SessionUpdate["content"]): string | undefined {
|
|
127
|
+
if (!content || typeof content !== "object" || Array.isArray(content)) return undefined;
|
|
128
|
+
const t = (content as ContentBlock).text;
|
|
129
|
+
return typeof t === "string" ? t : undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
113
132
|
/** Permission request from the agent (server -> client) — ACP "ask" mode. */
|
|
114
133
|
export interface RequestPermissionParams {
|
|
115
134
|
sessionId: string;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a lossless import package: full transcript on disk + priming text for
|
|
3
|
+
* the first Grok turn so the conversation can continue with full context.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import type { HistoryEntry } from "../sessions/types.js";
|
|
8
|
+
import { readForeignHistory, type ForeignSessionMeta } from "./history-readers.js";
|
|
9
|
+
import type { ImportSource } from "./sources.js";
|
|
10
|
+
|
|
11
|
+
/** Inline priming budget — keep under typical model input comfort zone. */
|
|
12
|
+
const INLINE_MAX_CHARS = 100_000;
|
|
13
|
+
/** Per-entry cap when writing the archive file (still very generous). */
|
|
14
|
+
const FILE_ENTRY_MAX = 100_000;
|
|
15
|
+
/** Per-entry cap inside the inline priming block. */
|
|
16
|
+
const INLINE_ENTRY_MAX = 12_000;
|
|
17
|
+
|
|
18
|
+
export interface ImportPackage {
|
|
19
|
+
/** Absolute path of the written full-transcript markdown (always present). */
|
|
20
|
+
transcriptPath: string;
|
|
21
|
+
/** Priming preamble to inject into the first Grok prompt. */
|
|
22
|
+
priming: string;
|
|
23
|
+
/** How many history entries were captured. */
|
|
24
|
+
entryCount: number;
|
|
25
|
+
/** Characters written to the transcript file. */
|
|
26
|
+
transcriptChars: number;
|
|
27
|
+
/** True when the full transcript did not fit inline (file is authoritative). */
|
|
28
|
+
truncatedInline: boolean;
|
|
29
|
+
meta: ForeignSessionMeta;
|
|
30
|
+
sourceLabel: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract full history, write a durable transcript under `importsDir`, and
|
|
35
|
+
* build priming text that references it.
|
|
36
|
+
*/
|
|
37
|
+
export function buildImportPackage(
|
|
38
|
+
src: ImportSource,
|
|
39
|
+
meta: ForeignSessionMeta,
|
|
40
|
+
importsDir: string,
|
|
41
|
+
): ImportPackage {
|
|
42
|
+
const entries = readForeignHistory(src.format, src.sessionsRoot, meta.sessionId);
|
|
43
|
+
const fullBody = formatTranscript(entries, FILE_ENTRY_MAX);
|
|
44
|
+
const transcriptChars = fullBody.length;
|
|
45
|
+
|
|
46
|
+
mkdirSync(importsDir, { recursive: true });
|
|
47
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
48
|
+
const short = meta.sessionId.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 12) || "session";
|
|
49
|
+
const fileName = `${src.id}-${short}-${stamp}.md`;
|
|
50
|
+
const transcriptPath = join(importsDir, fileName);
|
|
51
|
+
|
|
52
|
+
const header = [
|
|
53
|
+
`# Imported session transcript`,
|
|
54
|
+
``,
|
|
55
|
+
`- **Source tool:** ${src.label} (\`${src.id}\`)`,
|
|
56
|
+
`- **Source session id:** \`${meta.sessionId}\``,
|
|
57
|
+
`- **Project:** ${meta.projectName ?? "(unknown)"}`,
|
|
58
|
+
`- **Working directory:** \`${meta.cwd || "(unknown)"}\``,
|
|
59
|
+
`- **Title:** ${meta.title}`,
|
|
60
|
+
`- **Entries:** ${entries.length}`,
|
|
61
|
+
`- **Imported at:** ${new Date().toISOString()}`,
|
|
62
|
+
``,
|
|
63
|
+
`---`,
|
|
64
|
+
``,
|
|
65
|
+
fullBody || "_(no history entries found on disk)_",
|
|
66
|
+
``,
|
|
67
|
+
].join("\n");
|
|
68
|
+
|
|
69
|
+
writeFileSync(transcriptPath, header, "utf-8");
|
|
70
|
+
|
|
71
|
+
const inline = formatTranscript(entries, INLINE_ENTRY_MAX);
|
|
72
|
+
let truncatedInline = false;
|
|
73
|
+
let inlineBlock = inline;
|
|
74
|
+
if (inlineBlock.length > INLINE_MAX_CHARS) {
|
|
75
|
+
truncatedInline = true;
|
|
76
|
+
// Keep the *end* of the conversation (most relevant to continue).
|
|
77
|
+
inlineBlock = inlineBlock.slice(inlineBlock.length - INLINE_MAX_CHARS);
|
|
78
|
+
const nl = inlineBlock.indexOf("\n");
|
|
79
|
+
if (nl !== -1) inlineBlock = inlineBlock.slice(nl + 1);
|
|
80
|
+
inlineBlock = `[…earlier messages omitted inline; full transcript is in the file…]\n\n` + inlineBlock;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const priming = [
|
|
84
|
+
`You are continuing a conversation that was imported from **${src.label}** into Grok.`,
|
|
85
|
+
`Nothing from the original session must be lost — the complete transcript is on disk.`,
|
|
86
|
+
``,
|
|
87
|
+
`**Source:** ${src.label}`,
|
|
88
|
+
`**Original session id:** ${meta.sessionId}`,
|
|
89
|
+
`**Project:** ${meta.projectName ?? "(unknown)"}`,
|
|
90
|
+
`**Working directory (cwd):** ${meta.cwd || "(unknown)"}`,
|
|
91
|
+
`**Full transcript file (authoritative, complete):** ${transcriptPath}`,
|
|
92
|
+
``,
|
|
93
|
+
`Instructions:`,
|
|
94
|
+
`1. Treat the transcript below (and the full file above) as your prior conversation history.`,
|
|
95
|
+
`2. If anything seems truncated inline, READ the full transcript file before acting.`,
|
|
96
|
+
`3. Continue seamlessly: same project, same goals, same unfinished work, same decisions.`,
|
|
97
|
+
`4. Do not restart finished work; do not ask the user to re-explain what is already in the transcript.`,
|
|
98
|
+
`5. On this first turn only: reply with a short confirmation (project path + one-sentence summary of the task so far) and WAIT for the user's next instruction. Do not make further tool calls yet unless the transcript shows an urgent incomplete action that would leave the workspace broken.`,
|
|
99
|
+
``,
|
|
100
|
+
`=== IMPORTED TRANSCRIPT (${entries.length} entries${truncatedInline ? ", recent slice inline" : ", full inline"}) ===`,
|
|
101
|
+
inlineBlock || "(empty — see transcript file)",
|
|
102
|
+
`=== END IMPORTED TRANSCRIPT ===`,
|
|
103
|
+
].join("\n");
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
transcriptPath,
|
|
107
|
+
priming,
|
|
108
|
+
entryCount: entries.length,
|
|
109
|
+
transcriptChars,
|
|
110
|
+
truncatedInline,
|
|
111
|
+
meta,
|
|
112
|
+
sourceLabel: src.label,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Full-fidelity plain-text transcript (no 600-char UI truncation). */
|
|
117
|
+
export function formatTranscript(entries: HistoryEntry[], perEntryMax: number): string {
|
|
118
|
+
const label: Record<string, string> = {
|
|
119
|
+
user: "User",
|
|
120
|
+
assistant: "Assistant",
|
|
121
|
+
tool: "Tool",
|
|
122
|
+
system: "System",
|
|
123
|
+
};
|
|
124
|
+
return entries
|
|
125
|
+
.map((e, i) => {
|
|
126
|
+
let text = e.text ?? "";
|
|
127
|
+
if (text.length > perEntryMax) text = text.slice(0, perEntryMax) + " …";
|
|
128
|
+
const tool = e.tool && e.role === "tool" ? ` (${e.tool})` : "";
|
|
129
|
+
return `### ${i + 1}. ${label[e.role] ?? e.role}${tool}\n${text}`;
|
|
130
|
+
})
|
|
131
|
+
.join("\n\n");
|
|
132
|
+
}
|