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.
- package/.env.example +30 -0
- package/CHANGELOG.md +409 -0
- package/README.md +48 -9
- package/package.json +5 -1
- package/src/acp/client.ts +109 -13
- package/src/app/auth-service.ts +325 -0
- package/src/app/settings-store.ts +7 -0
- package/src/app/types.ts +4 -2
- package/src/app/updater.ts +234 -0
- package/src/app/version.ts +41 -0
- package/src/bot/bot.ts +57 -1
- package/src/bot/chat-controller.ts +70 -7
- package/src/bot/commands.ts +4 -3
- package/src/bot/deps.ts +18 -0
- package/src/bot/handlers/auth.ts +89 -0
- package/src/bot/handlers/control.ts +11 -3
- package/src/bot/handlers/history.ts +7 -2
- package/src/bot/handlers/kill.ts +5 -20
- package/src/bot/handlers/mcp.ts +2 -1
- package/src/bot/handlers/menu.ts +12 -7
- package/src/bot/handlers/message.ts +7 -2
- package/src/bot/handlers/photo.ts +13 -4
- package/src/bot/handlers/projects.ts +119 -19
- package/src/bot/handlers/running.ts +98 -21
- package/src/bot/handlers/session-card.ts +16 -0
- package/src/bot/handlers/session-kill.ts +95 -0
- package/src/bot/handlers/sessions.ts +43 -26
- package/src/bot/handlers/tasks.ts +2 -1
- package/src/bot/handlers/usage.ts +2 -1
- package/src/bot/handlers/voice.ts +1 -1
- package/src/bot/menu/ephemeral.ts +117 -0
- package/src/bot/menu/status-panel.ts +3 -0
- package/src/bot/prompt-content.ts +6 -0
- package/src/bot/reauth-controller.ts +462 -0
- package/src/bot/session-fork.ts +35 -0
- package/src/bot/session-runtime.ts +265 -67
- package/src/config.ts +24 -0
- package/src/index.ts +3 -1
- package/src/projects/manager.ts +16 -5
- package/src/render/device-flow.ts +76 -0
- package/src/render/file-summary.ts +111 -0
- package/src/render/hashtags.ts +34 -0
- package/src/render/markdown.ts +4 -0
- package/src/render/progress.ts +80 -0
- package/src/render/tool-call.ts +97 -3
- package/src/service/linux.ts +2 -0
- package/src/service/macos.ts +10 -0
- package/src/service/platform.ts +5 -0
- package/src/service/types.ts +2 -0
- package/src/service/windows.ts +116 -21
- package/src/sessions/history.ts +45 -1
- package/src/sessions/process.ts +30 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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)
|
|
309
|
-
|
|
310
|
-
|
|
339
|
+
if (this.restartTimer) {
|
|
340
|
+
clearTimeout(this.restartTimer);
|
|
341
|
+
this.restartTimer = undefined;
|
|
342
|
+
}
|
|
343
|
+
void this.killCurrent();
|
|
311
344
|
}
|
|
312
345
|
|
|
313
|
-
/**
|
|
314
|
-
|
|
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)
|
|
317
|
-
|
|
318
|
-
|
|
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
|
}
|