kiro-telegram-bot 1.5.1 → 1.7.1

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 (53) hide show
  1. package/.env.example +30 -0
  2. package/CHANGELOG.md +409 -0
  3. package/README.md +48 -9
  4. package/package.json +5 -1
  5. package/src/acp/client.ts +109 -13
  6. package/src/app/auth-service.ts +325 -0
  7. package/src/app/settings-store.ts +7 -0
  8. package/src/app/types.ts +4 -2
  9. package/src/app/updater.ts +234 -0
  10. package/src/app/version.ts +41 -0
  11. package/src/bot/bot.ts +57 -1
  12. package/src/bot/chat-controller.ts +70 -7
  13. package/src/bot/commands.ts +4 -3
  14. package/src/bot/deps.ts +18 -0
  15. package/src/bot/handlers/auth.ts +89 -0
  16. package/src/bot/handlers/control.ts +11 -3
  17. package/src/bot/handlers/history.ts +7 -2
  18. package/src/bot/handlers/kill.ts +5 -20
  19. package/src/bot/handlers/mcp.ts +2 -1
  20. package/src/bot/handlers/menu.ts +12 -7
  21. package/src/bot/handlers/message.ts +7 -2
  22. package/src/bot/handlers/photo.ts +13 -4
  23. package/src/bot/handlers/projects.ts +119 -19
  24. package/src/bot/handlers/running.ts +98 -21
  25. package/src/bot/handlers/session-card.ts +16 -0
  26. package/src/bot/handlers/session-kill.ts +95 -0
  27. package/src/bot/handlers/sessions.ts +43 -26
  28. package/src/bot/handlers/tasks.ts +2 -1
  29. package/src/bot/handlers/usage.ts +2 -1
  30. package/src/bot/handlers/voice.ts +1 -1
  31. package/src/bot/menu/ephemeral.ts +117 -0
  32. package/src/bot/menu/status-panel.ts +3 -0
  33. package/src/bot/prompt-content.ts +6 -0
  34. package/src/bot/reauth-controller.ts +462 -0
  35. package/src/bot/session-fork.ts +35 -0
  36. package/src/bot/session-runtime.ts +265 -67
  37. package/src/config.ts +24 -0
  38. package/src/index.ts +3 -1
  39. package/src/projects/manager.ts +16 -5
  40. package/src/render/device-flow.ts +76 -0
  41. package/src/render/file-summary.ts +111 -0
  42. package/src/render/hashtags.ts +34 -0
  43. package/src/render/markdown.ts +4 -0
  44. package/src/render/progress.ts +80 -0
  45. package/src/render/tool-call.ts +97 -3
  46. package/src/service/linux.ts +2 -0
  47. package/src/service/macos.ts +10 -0
  48. package/src/service/platform.ts +5 -0
  49. package/src/service/types.ts +2 -0
  50. package/src/service/windows.ts +116 -21
  51. package/src/sessions/history.ts +45 -1
  52. package/src/sessions/process.ts +30 -0
  53. package/src/stream/streamer.ts +57 -8
package/src/acp/client.ts CHANGED
@@ -50,6 +50,23 @@ export function isTransientAcpError(err: Error): boolean {
50
50
  return TRANSIENT_RE.test(err.message);
51
51
  }
52
52
 
53
+ /** Patterns that specifically indicate the request exceeded the model's context
54
+ * window (as opposed to a generic rate-limit/backend hiccup). */
55
+ const CONTEXT_EXHAUSTED_RE =
56
+ /context (?:length|window|limit|size|overflow)|maximum context|input (?:is )?too long|prompt (?:is )?too long|too many (?:input )?tokens|token limit|exceeds? (?:the )?(?:maximum|context|token)|reduce the (?:length|size)|context.{0,24}exhaust/i;
57
+
58
+ /**
59
+ * Heuristic: did this failure come from an exhausted context window? Unlike a
60
+ * generic transient error, this will NOT clear by retrying the same oversized
61
+ * prompt — the session must be compacted/forked into a fresh, smaller context.
62
+ * Note: throttling on a near-full session often surfaces as a plain "-32603 …
63
+ * throttled" with no context keywords, so callers should *also* consult the
64
+ * session's tracked context-usage % (see SessionRuntime.isContextRelatedFailure).
65
+ */
66
+ export function isContextExhaustedError(err: Error): boolean {
67
+ return CONTEXT_EXHAUSTED_RE.test(err.message);
68
+ }
69
+
53
70
  /** Compact, log/Telegram-safe stringification of an error's data payload. */
54
71
  function shortJson(v: unknown): string {
55
72
  try {
@@ -142,24 +159,31 @@ export class AcpClient extends EventEmitter {
142
159
  if (this.opts.agent) args.push("--agent", this.opts.agent);
143
160
 
144
161
  log.info(`spawning: ${this.opts.kiroCliPath} ${args.join(" ")}`);
145
- this.proc = spawn(this.opts.kiroCliPath, args, {
162
+ const proc = spawn(this.opts.kiroCliPath, args, {
146
163
  stdio: ["pipe", "pipe", "pipe"],
147
164
  cwd: this.opts.workspace,
148
165
  env: { ...process.env, KIRO_LOG_LEVEL: process.env.KIRO_LOG_LEVEL || "error" },
149
166
  }) as ChildProcessWithoutNullStreams;
150
-
151
- this.proc.on("exit", (code) => {
167
+ this.proc = proc;
168
+
169
+ proc.on("exit", (code) => {
170
+ // Ignore the exit of a process we've already replaced (a deliberate
171
+ // restart/stop). Its teardown must NOT fail the new process's pending
172
+ // requests nor trigger a competing auto-restart — that race was the cause
173
+ // of "/reauth → agent restart failed: kiro-cli acp exited (code null)".
174
+ if (this.proc !== proc) return;
152
175
  log.warn(`kiro-cli acp exited (code ${code})`);
153
176
  this.failAllPending(new Error(`kiro-cli acp exited (code ${code})`));
154
177
  this.emit("exit", code);
155
178
  this.maybeRestart();
156
179
  });
157
- this.proc.on("error", (err) => {
180
+ proc.on("error", (err) => {
181
+ if (this.proc !== proc) return;
158
182
  log.error("failed to spawn kiro-cli:", err.message);
159
183
  this.failAllPending(err);
160
184
  });
161
185
 
162
- this.transport = new JsonRpcTransport(this.proc);
186
+ this.transport = new JsonRpcTransport(proc);
163
187
  this.transport.on("message", (m: JsonRpcMessage) => this.onMessage(m));
164
188
 
165
189
  const init = (await this.request("initialize", {
@@ -202,6 +226,13 @@ export class AcpClient extends EventEmitter {
202
226
  return Boolean(this.capabilities?.loadSession);
203
227
  }
204
228
 
229
+ /** True while any prompt (a chat turn or a scheduled task) awaits a response —
230
+ * i.e. the agent is actively working. Used to gate idle-only auto-updates. */
231
+ hasInflightPrompt(): boolean {
232
+ for (const p of this.pending.values()) if (p.method === "session/prompt") return true;
233
+ return false;
234
+ }
235
+
205
236
  /** PID of the bot's own kiro-cli acp process (to avoid killing ourselves). */
206
237
  get pid(): number | undefined {
207
238
  return this.proc?.pid;
@@ -305,22 +336,87 @@ export class AcpClient extends EventEmitter {
305
336
 
306
337
  stop(): void {
307
338
  this.stopped = true;
308
- if (this.restartTimer) clearTimeout(this.restartTimer);
309
- this.proc?.kill();
310
- this.proc = undefined;
339
+ if (this.restartTimer) {
340
+ clearTimeout(this.restartTimer);
341
+ this.restartTimer = undefined;
342
+ }
343
+ void this.killCurrent();
311
344
  }
312
345
 
313
- /** Manually restart the agent (used by the /restart command). */
314
- async restart(): Promise<void> {
346
+ /**
347
+ * Stop the agent and WAIT for the process to fully exit, leaving it stopped
348
+ * (no auto-restart until start()/restart()). Used by /reauth to release the
349
+ * held session BEFORE logging out — otherwise the live agent keeps refreshing
350
+ * and re-persisting the old token, silently restoring the previous identity.
351
+ */
352
+ async stopAndWait(): Promise<void> {
315
353
  this.stopped = true;
316
- if (this.restartTimer) clearTimeout(this.restartTimer);
317
- this.proc?.kill();
318
- this.proc = undefined;
354
+ if (this.restartTimer) {
355
+ clearTimeout(this.restartTimer);
356
+ this.restartTimer = undefined;
357
+ }
358
+ await this.killCurrent();
359
+ }
360
+
361
+ /**
362
+ * Manually restart the agent (used by /restart, /reauth and the MCP toggle).
363
+ * The old process is fully torn down BEFORE a fresh one is spawned, so its
364
+ * exit can't fail the new connection's `initialize` — which previously
365
+ * surfaced as "agent restart failed: kiro-cli acp exited (code null)".
366
+ */
367
+ async restart(): Promise<void> {
368
+ if (this.restartTimer) {
369
+ clearTimeout(this.restartTimer);
370
+ this.restartTimer = undefined;
371
+ }
372
+ this.stopped = true; // suppress auto-restart while we swap processes
373
+ this.restartAttempts = 0;
374
+ await this.killCurrent();
319
375
  this.stopped = false;
320
376
  await this.connect();
321
377
  this.emit("restarted");
322
378
  }
323
379
 
380
+ /**
381
+ * Terminate the current process and wait for it to fully exit. Clearing
382
+ * `this.proc` first makes the connect()-registered exit/error handlers
383
+ * short-circuit, so a deliberate teardown is silent (no `exit` event, no
384
+ * auto-restart). In-flight requests are rejected here (the handler no longer
385
+ * will). Escalates to SIGKILL if the process lingers, and never hangs.
386
+ */
387
+ private killCurrent(): Promise<void> {
388
+ const proc = this.proc;
389
+ this.proc = undefined;
390
+ this.transport = undefined;
391
+ this.failAllPending(new Error("kiro-cli acp is restarting"));
392
+ if (!proc || proc.exitCode !== null || proc.signalCode !== null) {
393
+ return Promise.resolve();
394
+ }
395
+ return new Promise<void>((resolve) => {
396
+ let settled = false;
397
+ const done = (): void => {
398
+ if (settled) return;
399
+ settled = true;
400
+ clearTimeout(hard);
401
+ resolve();
402
+ };
403
+ const hard = setTimeout(() => {
404
+ try {
405
+ proc.kill("SIGKILL");
406
+ } catch {
407
+ /* ignore */
408
+ }
409
+ setTimeout(done, 500); // give the OS a beat, then proceed regardless
410
+ }, 4000);
411
+ proc.once("exit", done);
412
+ try {
413
+ proc.kill();
414
+ } catch {
415
+ done();
416
+ }
417
+ });
418
+ }
419
+
324
420
  // ── JSON-RPC plumbing ──────────────────────────────────────────────────────
325
421
 
326
422
  private request(method: string, params: unknown): Promise<unknown> {
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Kiro authentication control for /reauth: `kiro-cli logout` then an interactive
3
+ * `kiro-cli login --use-device-flow`. The device flow prints a verification URL
4
+ * + code to stdout (no browser redirect on the bot host), which we stream back
5
+ * to Telegram so the user can complete it on their own device.
6
+ */
7
+ import { execFile, spawn } from "node:child_process";
8
+ import { rm } from "node:fs/promises";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { promisify } from "node:util";
12
+ import { createLogger } from "../logger.js";
13
+
14
+ const run = promisify(execFile);
15
+ const log = createLogger("auth");
16
+
17
+ // Strip ANSI colour/cursor escapes so the Telegram transcript stays readable.
18
+ // eslint-disable-next-line no-control-regex
19
+ const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
20
+
21
+ export interface LoginResult {
22
+ ok: boolean;
23
+ code: number | null;
24
+ /** True when the login was aborted via the supplied AbortSignal. */
25
+ cancelled?: boolean;
26
+ /** Human-readable failure reason, when known (shown in the chat). */
27
+ error?: string;
28
+ }
29
+
30
+ export interface LoginOptions {
31
+ /** Extra CLI flags (e.g. `--license pro`). `--use-device-flow` is added if absent. */
32
+ extraArgs?: string[];
33
+ /** Receives decoded stdout/stderr chunks as they arrive. */
34
+ onOutput: (text: string) => void;
35
+ /** Overall timeout before the login process is killed (default 5 min). */
36
+ timeoutMs?: number;
37
+ /** Abort to cancel the in-flight login — kills the process (Cancel button). */
38
+ signal?: AbortSignal;
39
+ }
40
+
41
+ export interface IdcLoginOptions {
42
+ /** IAM Identity Center start URL (e.g. https://my-org.awsapps.com/start). */
43
+ startUrl: string;
44
+ /** AWS region of the Identity Center (e.g. us-east-1). */
45
+ region: string;
46
+ /** Receives decoded output chunks as they arrive. */
47
+ onOutput: (text: string) => void;
48
+ /** Overall timeout before the login process is killed (default 5 min). */
49
+ timeoutMs?: number;
50
+ /** Abort to cancel the in-flight login — kills the process (Cancel button). */
51
+ signal?: AbortSignal;
52
+ }
53
+
54
+ /** Minimal shape of the optional `node-pty` module we rely on. */
55
+ interface IPty {
56
+ onData(cb: (data: string) => void): void;
57
+ onExit(cb: (e: { exitCode: number; signal?: number }) => void): void;
58
+ write(data: string): void;
59
+ kill(signal?: string): void;
60
+ }
61
+ interface PtyModule {
62
+ spawn(
63
+ file: string,
64
+ args: string[],
65
+ options: { name?: string; cols?: number; rows?: number; cwd?: string; env?: NodeJS.ProcessEnv },
66
+ ): IPty;
67
+ }
68
+
69
+ export class AuthService {
70
+ constructor(private readonly kiroCliPath: string) {}
71
+
72
+ /** Run `kiro-cli logout` (non-interactive). */
73
+ async logout(): Promise<{ ok: boolean; out: string }> {
74
+ try {
75
+ const { stdout, stderr } = await run(this.kiroCliPath, ["logout"], {
76
+ timeout: 30_000,
77
+ encoding: "utf-8",
78
+ });
79
+ return { ok: true, out: clean(`${stdout}${stderr}`) };
80
+ } catch (e) {
81
+ const err = e as { stdout?: string; stderr?: string; message?: string };
82
+ const out = clean(`${err.stdout ?? ""}${err.stderr ?? ""}`) || err.message || "logout failed";
83
+ return { ok: false, out };
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Best-effort removal of Kiro's cached auth token (`~/.aws/sso/cache/
89
+ * kiro-auth-token.json`) — the file that carries the logged-in identity
90
+ * (accessToken + refreshToken). Removing it after `logout` guarantees the
91
+ * next `login` performs a genuine device-flow authentication instead of
92
+ * silently reusing the previous account's cached/refreshable token.
93
+ *
94
+ * Surgical and safe: it touches ONLY Kiro's own token file, never the shared,
95
+ * account-agnostic OIDC client registrations, and is a no-op if absent.
96
+ */
97
+ async clearTokenCache(): Promise<boolean> {
98
+ const path = join(homedir(), ".aws", "sso", "cache", "kiro-auth-token.json");
99
+ try {
100
+ await rm(path, { force: true });
101
+ log.info(`cleared cached auth token (${path})`);
102
+ return true;
103
+ } catch (e) {
104
+ log.debug("clearTokenCache failed:", (e as Error).message);
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * to `onOutput` as it arrives (so the device code/URL reaches the user fast).
111
+ * Resolves when the process exits, the timeout fires, or the signal aborts.
112
+ */
113
+ login(opts: LoginOptions): Promise<LoginResult> {
114
+ const { extraArgs = [], onOutput, timeoutMs = 300_000, signal } = opts;
115
+ return new Promise<LoginResult>((resolve) => {
116
+ if (signal?.aborted) {
117
+ resolve({ ok: false, code: null, cancelled: true });
118
+ return;
119
+ }
120
+ const args = ["login"];
121
+ if (!extraArgs.includes("--use-device-flow")) args.push("--use-device-flow");
122
+ args.push(...extraArgs);
123
+ log.info(`spawning login: ${this.kiroCliPath} ${args.join(" ")}`);
124
+
125
+ let proc;
126
+ try {
127
+ // stdin ignored: any interactive prompt gets EOF rather than hanging.
128
+ proc = spawn(this.kiroCliPath, args, { stdio: ["ignore", "pipe", "pipe"] });
129
+ } catch (e) {
130
+ onOutput(`error: ${(e as Error).message}`);
131
+ resolve({ ok: false, code: null });
132
+ return;
133
+ }
134
+
135
+ let cancelled = false;
136
+ let settled = false;
137
+ let hardKill: NodeJS.Timeout | undefined;
138
+
139
+ const onAbort = (): void => {
140
+ cancelled = true;
141
+ try {
142
+ proc.kill();
143
+ } catch {
144
+ /* ignore */
145
+ }
146
+ // Escalate if the CLI ignores the polite signal.
147
+ hardKill = setTimeout(() => {
148
+ try {
149
+ proc.kill("SIGKILL");
150
+ } catch {
151
+ /* ignore */
152
+ }
153
+ }, 2000);
154
+ };
155
+
156
+ const finish = (r: LoginResult): void => {
157
+ if (settled) return;
158
+ settled = true;
159
+ clearTimeout(timer);
160
+ if (hardKill) clearTimeout(hardKill);
161
+ signal?.removeEventListener("abort", onAbort);
162
+ resolve(r);
163
+ };
164
+
165
+ const feed = (b: Buffer): void => {
166
+ const t = clean(b.toString("utf-8"));
167
+ if (t) onOutput(t);
168
+ };
169
+ proc.stdout.on("data", feed);
170
+ proc.stderr.on("data", feed);
171
+
172
+ const timer = setTimeout(() => {
173
+ onOutput("\n\u23F1\uFE0F Timed out waiting for login to complete.");
174
+ try {
175
+ proc.kill();
176
+ } catch {
177
+ /* ignore */
178
+ }
179
+ }, timeoutMs);
180
+
181
+ signal?.addEventListener("abort", onAbort, { once: true });
182
+
183
+ proc.on("error", (e: Error) => {
184
+ onOutput(`error: ${e.message}`);
185
+ finish({ ok: false, code: null, cancelled });
186
+ });
187
+ proc.on("exit", (code: number | null) => {
188
+ finish({ ok: code === 0 && !cancelled, code, cancelled });
189
+ });
190
+ });
191
+ }
192
+
193
+ /**
194
+ * IAM Identity Center (Pro) login. Unlike the Builder ID device flow, this
195
+ * CLI path is *interactive*: it always prompts ("Enter Start URL", "Enter
196
+ * Region", and — when the account has several — an Identity Center profile
197
+ * picker) and refuses to run without a real terminal. So we drive it inside a
198
+ * pseudo-terminal (the optional `node-pty` module), answering each prompt with
199
+ * the start URL / region the user supplied and accepting the default profile.
200
+ * The device verification URL + code still stream out via `onOutput`.
201
+ */
202
+ loginIdc(opts: IdcLoginOptions): Promise<LoginResult> {
203
+ const { startUrl, region, onOutput, timeoutMs = 300_000, signal } = opts;
204
+ return new Promise<LoginResult>((resolve) => {
205
+ if (signal?.aborted) {
206
+ resolve({ ok: false, code: null, cancelled: true });
207
+ return;
208
+ }
209
+ void (async () => {
210
+ // Load the optional native PTY lazily via a variable specifier so a
211
+ // missing module is a graceful runtime error, not an install/type break.
212
+ let pty: PtyModule;
213
+ try {
214
+ const specifier = "@homebridge/node-pty-prebuilt-multiarch";
215
+ const mod = (await import(specifier)) as unknown as PtyModule & { default?: PtyModule };
216
+ pty = typeof mod.spawn === "function" ? mod : (mod.default as PtyModule);
217
+ if (!pty || typeof pty.spawn !== "function") throw new Error("invalid pty module");
218
+ } catch {
219
+ resolve({
220
+ ok: false,
221
+ code: null,
222
+ error:
223
+ "IAM Identity Center login needs the PTY module. Run `npm install` in the bot folder, then try again.",
224
+ });
225
+ return;
226
+ }
227
+
228
+ // Pass the start URL + region as flags so the interactive prompts come
229
+ // PREFILLED — we then just press Enter to accept them. Typing the values
230
+ // ourselves makes the terminal echo them, which doubles the captured
231
+ // input (e.g. "us-east-1us-east-1" → bad OIDC endpoint). Q_FAKE_IS_REMOTE
232
+ // forces the CLI to PRINT the verification URL instead of opening a
233
+ // browser on the bot host, so it streams to Telegram.
234
+ const args = [
235
+ "login",
236
+ "--license",
237
+ "pro",
238
+ "--identity-provider",
239
+ startUrl,
240
+ "--region",
241
+ region,
242
+ "--use-device-flow",
243
+ ];
244
+ log.info(`spawning IDC login (pty): ${this.kiroCliPath} ${args.join(" ")}`);
245
+
246
+ let term: IPty;
247
+ try {
248
+ term = pty.spawn(this.kiroCliPath, args, {
249
+ name: "xterm-color",
250
+ cols: 120,
251
+ rows: 30,
252
+ env: { ...process.env, Q_FAKE_IS_REMOTE: "1" },
253
+ });
254
+ } catch (e) {
255
+ resolve({ ok: false, code: null, error: (e as Error).message });
256
+ return;
257
+ }
258
+
259
+ let buf = "";
260
+ let sentUrl = false;
261
+ let sentRegion = false;
262
+ let sentProfile = false;
263
+ let cancelled = false;
264
+ let settled = false;
265
+
266
+ const onAbort = (): void => {
267
+ cancelled = true;
268
+ try {
269
+ term.kill();
270
+ } catch {
271
+ /* ignore */
272
+ }
273
+ };
274
+
275
+ const finish = (r: LoginResult): void => {
276
+ if (settled) return;
277
+ settled = true;
278
+ clearTimeout(timer);
279
+ signal?.removeEventListener("abort", onAbort);
280
+ try {
281
+ term.kill();
282
+ } catch {
283
+ /* ignore */
284
+ }
285
+ resolve(r);
286
+ };
287
+
288
+ const timer = setTimeout(() => {
289
+ onOutput("\n\u23F1\uFE0F Timed out waiting for login to complete.");
290
+ try {
291
+ term.kill();
292
+ } catch {
293
+ /* ignore */
294
+ }
295
+ }, timeoutMs);
296
+
297
+ signal?.addEventListener("abort", onAbort, { once: true });
298
+
299
+ term.onData((d: string) => {
300
+ const t = clean(d);
301
+ if (t) onOutput(t);
302
+ buf += t;
303
+ // Each prompt is PREFILLED (from the flags); just press Enter to accept
304
+ // it. We answer each exactly once, in order.
305
+ if (!sentUrl && /start url/i.test(buf)) {
306
+ sentUrl = true;
307
+ term.write("\r");
308
+ } else if (sentUrl && !sentRegion && /enter region/i.test(buf)) {
309
+ sentRegion = true;
310
+ term.write("\r");
311
+ } else if (sentRegion && !sentProfile && /select an iam identity center profile/i.test(buf)) {
312
+ sentProfile = true;
313
+ term.write("\r"); // accept the default (first) profile
314
+ }
315
+ });
316
+
317
+ term.onExit(({ exitCode }) => finish({ ok: exitCode === 0 && !cancelled, code: exitCode, cancelled }));
318
+ })();
319
+ });
320
+ }
321
+ }
322
+
323
+ function clean(s: string): string {
324
+ return s.replace(ANSI_RE, "").replace(/\r/g, "");
325
+ }
@@ -28,4 +28,11 @@ export class SettingsStore {
28
28
  });
29
29
  return next;
30
30
  }
31
+
32
+ /** All chat ids that have interacted (for broadcast announcements). */
33
+ chatIds(): number[] {
34
+ return Object.keys(this.store.get())
35
+ .map(Number)
36
+ .filter((n) => Number.isFinite(n));
37
+ }
31
38
  }
package/src/app/types.ts CHANGED
@@ -41,8 +41,10 @@ export interface PromptImage {
41
41
  export interface PromptInput {
42
42
  text: string;
43
43
  images: PromptImage[];
44
+ /** Telegram message id of the prompt, so the reply threads to it. */
45
+ replyTo?: number;
44
46
  }
45
47
 
46
- export function textPrompt(text: string): PromptInput {
47
- return { text, images: [] };
48
+ export function textPrompt(text: string, replyTo?: number): PromptInput {
49
+ return { text, images: [], replyTo };
48
50
  }