kiro-telegram-bot 1.5.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 +104 -0
- package/LICENSE +21 -0
- package/README.md +517 -0
- package/bin/kiro-tg.mjs +21 -0
- package/docs/INSTALL.md +143 -0
- package/docs/ops/RELEASE_CHECKLIST.md +39 -0
- package/package.json +70 -0
- package/scripts/mq.ts +25 -0
- package/scripts/setup.mjs +78 -0
- package/src/acp/client.ts +456 -0
- package/src/acp/server-handlers.ts +85 -0
- package/src/acp/transport.ts +50 -0
- package/src/acp/types.ts +136 -0
- package/src/agents/catalog.ts +44 -0
- package/src/app/json-store.ts +54 -0
- package/src/app/reasoning.ts +30 -0
- package/src/app/settings-store.ts +31 -0
- package/src/app/stt.ts +53 -0
- package/src/app/types.ts +48 -0
- package/src/app/usage.ts +32 -0
- package/src/bot/auth.ts +27 -0
- package/src/bot/bot.ts +154 -0
- package/src/bot/chat-controller.ts +251 -0
- package/src/bot/commands.ts +48 -0
- package/src/bot/deps.ts +47 -0
- package/src/bot/handlers/control.ts +94 -0
- package/src/bot/handlers/history.ts +58 -0
- package/src/bot/handlers/kill.ts +69 -0
- package/src/bot/handlers/mcp.ts +205 -0
- package/src/bot/handlers/menu.ts +204 -0
- package/src/bot/handlers/message.ts +93 -0
- package/src/bot/handlers/photo.ts +108 -0
- package/src/bot/handlers/projects.ts +83 -0
- package/src/bot/handlers/running.ts +104 -0
- package/src/bot/handlers/session-card.ts +65 -0
- package/src/bot/handlers/sessions.ts +131 -0
- package/src/bot/handlers/system.ts +51 -0
- package/src/bot/handlers/tasks.ts +223 -0
- package/src/bot/handlers/usage.ts +33 -0
- package/src/bot/handlers/voice.ts +53 -0
- package/src/bot/image-return.ts +69 -0
- package/src/bot/menu/keyboard.ts +47 -0
- package/src/bot/menu/refresh.ts +13 -0
- package/src/bot/menu/status-panel.ts +78 -0
- package/src/bot/permission-service.ts +149 -0
- package/src/bot/prompt-content.ts +49 -0
- package/src/bot/prompt-retry.ts +70 -0
- package/src/bot/registry.ts +178 -0
- package/src/bot/session-runtime.ts +670 -0
- package/src/bot/telegram-io.ts +109 -0
- package/src/bot/typing.ts +35 -0
- package/src/bot/wizard/task-wizard.ts +214 -0
- package/src/cli.ts +125 -0
- package/src/config.ts +190 -0
- package/src/index.ts +74 -0
- package/src/logger.ts +78 -0
- package/src/mcp/config.ts +103 -0
- package/src/mcp/probe.ts +218 -0
- package/src/mcp/types.ts +68 -0
- package/src/projects/manager.ts +88 -0
- package/src/render/chunk.ts +57 -0
- package/src/render/diff.ts +48 -0
- package/src/render/escape.ts +22 -0
- package/src/render/markdown.ts +126 -0
- package/src/render/subagent.ts +75 -0
- package/src/render/tool-call.ts +102 -0
- package/src/service/index.ts +24 -0
- package/src/service/linux.ts +83 -0
- package/src/service/macos.ts +91 -0
- package/src/service/platform.ts +59 -0
- package/src/service/types.ts +34 -0
- package/src/service/windows.ts +103 -0
- package/src/sessions/history.ts +181 -0
- package/src/sessions/store.ts +133 -0
- package/src/sessions/tail.ts +86 -0
- package/src/sessions/types.ts +26 -0
- package/src/stream/streamer.ts +167 -0
- package/src/tasks/runner.ts +82 -0
- package/src/tasks/schedule.ts +142 -0
- package/src/tasks/scheduler.ts +53 -0
- package/src/tasks/store.ts +80 -0
- package/src/tasks/types.ts +33 -0
- package/tsconfig.json +19 -0
package/src/acp/types.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Client Protocol (ACP) type definitions for the Kiro CLI agent.
|
|
3
|
+
* Wire format: newline-delimited JSON-RPC 2.0 over stdio.
|
|
4
|
+
* @see https://agentclientprotocol.com @see https://kiro.dev/docs/cli/acp/
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface JsonRpcRequest {
|
|
8
|
+
jsonrpc: "2.0";
|
|
9
|
+
id: number | string;
|
|
10
|
+
method: string;
|
|
11
|
+
params?: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface JsonRpcResponse {
|
|
15
|
+
jsonrpc: "2.0";
|
|
16
|
+
id: number | string;
|
|
17
|
+
result?: unknown;
|
|
18
|
+
error?: { code: number; message: string; data?: unknown };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface JsonRpcNotification {
|
|
22
|
+
jsonrpc: "2.0";
|
|
23
|
+
method: string;
|
|
24
|
+
params?: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type JsonRpcMessage = JsonRpcResponse & JsonRpcNotification & { method?: string };
|
|
28
|
+
|
|
29
|
+
/** A content block in a prompt or message. */
|
|
30
|
+
export interface ContentBlock {
|
|
31
|
+
type: "text" | "image" | "resource";
|
|
32
|
+
text?: string;
|
|
33
|
+
data?: string;
|
|
34
|
+
mimeType?: string;
|
|
35
|
+
[k: string]: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface InitializeResult {
|
|
39
|
+
protocolVersion: number;
|
|
40
|
+
agentCapabilities?: {
|
|
41
|
+
loadSession?: boolean;
|
|
42
|
+
promptCapabilities?: { image?: boolean };
|
|
43
|
+
};
|
|
44
|
+
agentInfo?: { name?: string; version?: string };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface NewSessionResult {
|
|
48
|
+
sessionId: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface PromptResult {
|
|
52
|
+
stopReason?: string; // e.g. "end_turn", "cancelled", "max_tokens"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** session/update notification payload. */
|
|
56
|
+
export interface SessionUpdate {
|
|
57
|
+
sessionUpdate:
|
|
58
|
+
| "agent_message_chunk"
|
|
59
|
+
| "agent_thought_chunk"
|
|
60
|
+
| "tool_call"
|
|
61
|
+
| "tool_call_update"
|
|
62
|
+
| "plan"
|
|
63
|
+
| "user_message_chunk"
|
|
64
|
+
| string;
|
|
65
|
+
content?: ContentBlock;
|
|
66
|
+
// tool_call / tool_call_update fields
|
|
67
|
+
toolCallId?: string;
|
|
68
|
+
title?: string;
|
|
69
|
+
kind?: string; // "read" | "edit" | "execute" | "search" | ...
|
|
70
|
+
status?: "pending" | "in_progress" | "completed" | "failed" | string;
|
|
71
|
+
rawInput?: Record<string, unknown>;
|
|
72
|
+
content_blocks?: ToolCallContent[];
|
|
73
|
+
// ACP also nests content for tool calls as `content`
|
|
74
|
+
[k: string]: unknown;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A piece of tool-call content (text, diff, etc.). */
|
|
78
|
+
export interface ToolCallContent {
|
|
79
|
+
type: "content" | "diff" | string;
|
|
80
|
+
path?: string;
|
|
81
|
+
oldText?: string | null;
|
|
82
|
+
newText?: string;
|
|
83
|
+
content?: ContentBlock;
|
|
84
|
+
[k: string]: unknown;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface SessionNotificationParams {
|
|
88
|
+
sessionId: string;
|
|
89
|
+
update: SessionUpdate;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Permission request from the agent (server -> client). */
|
|
93
|
+
export interface RequestPermissionParams {
|
|
94
|
+
sessionId: string;
|
|
95
|
+
toolCall?: { toolCallId?: string; title?: string; kind?: string; rawInput?: Record<string, unknown> };
|
|
96
|
+
options: Array<{ optionId: string; name: string; kind?: string }>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type PermissionOutcome =
|
|
100
|
+
| { outcome: { outcome: "selected"; optionId: string } }
|
|
101
|
+
| { outcome: { outcome: "cancelled" } };
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* One subagent ("crew" member) as reported by Kiro's
|
|
105
|
+
* `_kiro.dev/subagent/list_update` notification. The list is process-global
|
|
106
|
+
* (it is not scoped to a parent session id on the wire).
|
|
107
|
+
*/
|
|
108
|
+
export interface SubagentInfo {
|
|
109
|
+
/** The subagent's own session id (distinct from the parent session). */
|
|
110
|
+
sessionId: string;
|
|
111
|
+
sessionName?: string;
|
|
112
|
+
agentName?: string;
|
|
113
|
+
role?: string;
|
|
114
|
+
initialQuery?: string;
|
|
115
|
+
status?: { type?: string; message?: string };
|
|
116
|
+
group?: string;
|
|
117
|
+
dependsOn?: string[];
|
|
118
|
+
hasLoop?: boolean;
|
|
119
|
+
loopIteration?: number;
|
|
120
|
+
loopMaxIterations?: number;
|
|
121
|
+
createdAtMs?: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A not-yet-started pipeline stage reported alongside the subagent list. */
|
|
125
|
+
export interface PendingStage {
|
|
126
|
+
name?: string;
|
|
127
|
+
role?: string;
|
|
128
|
+
agentName?: string;
|
|
129
|
+
dependsOn?: string[];
|
|
130
|
+
[k: string]: unknown;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface SubagentListUpdate {
|
|
134
|
+
subagents?: SubagentInfo[];
|
|
135
|
+
pendingStages?: PendingStage[];
|
|
136
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover Kiro custom agents from disk: the global ~/.kiro/agents and the
|
|
3
|
+
* selected project's .kiro/agents directory.
|
|
4
|
+
*/
|
|
5
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { basename, join } from "node:path";
|
|
8
|
+
import { createLogger } from "../logger.js";
|
|
9
|
+
|
|
10
|
+
const log = createLogger("agents");
|
|
11
|
+
|
|
12
|
+
export interface AgentInfo {
|
|
13
|
+
name: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
scope: "project" | "global";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function listAgents(projectPath?: string): AgentInfo[] {
|
|
19
|
+
const found = new Map<string, AgentInfo>();
|
|
20
|
+
if (projectPath) scan(join(projectPath, ".kiro", "agents"), "project", found);
|
|
21
|
+
scan(join(homedir(), ".kiro", "agents"), "global", found);
|
|
22
|
+
return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function scan(dir: string, scope: "project" | "global", out: Map<string, AgentInfo>): void {
|
|
26
|
+
let files: string[];
|
|
27
|
+
try {
|
|
28
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
29
|
+
} catch {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
const name = basename(file, ".json");
|
|
34
|
+
if (out.has(name)) continue;
|
|
35
|
+
let description: string | undefined;
|
|
36
|
+
try {
|
|
37
|
+
const json = JSON.parse(readFileSync(join(dir, file), "utf-8")) as { description?: string };
|
|
38
|
+
description = json.description;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
log.debug(`skip ${file}:`, (e as Error).message);
|
|
41
|
+
}
|
|
42
|
+
out.set(name, { name, description, scope });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal atomic JSON persistence. Reads on construction, writes atomically
|
|
3
|
+
* (temp file + rename) on save. No external dependencies.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
import { createLogger } from "../logger.js";
|
|
8
|
+
|
|
9
|
+
const log = createLogger("json-store");
|
|
10
|
+
|
|
11
|
+
export class JsonStore<T> {
|
|
12
|
+
private data: T;
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
private readonly path: string,
|
|
16
|
+
private readonly fallback: T,
|
|
17
|
+
) {
|
|
18
|
+
this.data = this.read();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get(): T {
|
|
22
|
+
return this.data;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
set(data: T): void {
|
|
26
|
+
this.data = data;
|
|
27
|
+
this.save();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Mutate via a callback, then persist. */
|
|
31
|
+
update(fn: (data: T) => void): void {
|
|
32
|
+
fn(this.data);
|
|
33
|
+
this.save();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private read(): T {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(this.path, "utf-8")) as T;
|
|
39
|
+
} catch {
|
|
40
|
+
return structuredClone(this.fallback);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private save(): void {
|
|
45
|
+
try {
|
|
46
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
47
|
+
const tmp = `${this.path}.tmp`;
|
|
48
|
+
writeFileSync(tmp, JSON.stringify(this.data, null, 2), "utf-8");
|
|
49
|
+
renameSync(tmp, this.path);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
log.error(`failed to save ${this.path}:`, (e as Error).message);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reasoning effort — a per-chat preference that steers how much deliberation
|
|
3
|
+
* the agent applies. Implemented as a concise directive prepended to prompts so
|
|
4
|
+
* it works regardless of backend-specific knobs.
|
|
5
|
+
*/
|
|
6
|
+
import type { ReasoningEffort } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const DIRECTIVE: Record<ReasoningEffort, string> = {
|
|
9
|
+
minimal: "Answer directly and briefly with minimal deliberation.",
|
|
10
|
+
low: "Keep reasoning light; prefer a quick, concise solution.",
|
|
11
|
+
medium: "", // default behaviour — no directive
|
|
12
|
+
high: "Think carefully and thoroughly before answering; verify your work.",
|
|
13
|
+
max: "Use maximum rigor: explore edge cases, double-check assumptions, and verify the result before finishing.",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const LABEL: Record<ReasoningEffort, string> = {
|
|
17
|
+
minimal: "Minimal",
|
|
18
|
+
low: "Low",
|
|
19
|
+
medium: "Medium",
|
|
20
|
+
high: "High",
|
|
21
|
+
max: "Max",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function reasoningDirective(effort: ReasoningEffort): string {
|
|
25
|
+
return DIRECTIVE[effort] ?? "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function reasoningLabel(effort: ReasoningEffort): string {
|
|
29
|
+
return LABEL[effort] ?? effort;
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-chat settings persistence (project, agent, model, reasoning, pinned
|
|
3
|
+
* status message id). Backed by a single JSON file so state survives restarts.
|
|
4
|
+
*/
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { JsonStore } from "./json-store.js";
|
|
7
|
+
import { type ChatSettings, defaultSettings } from "./types.js";
|
|
8
|
+
|
|
9
|
+
type SettingsMap = Record<string, ChatSettings>;
|
|
10
|
+
|
|
11
|
+
export class SettingsStore {
|
|
12
|
+
private readonly store: JsonStore<SettingsMap>;
|
|
13
|
+
|
|
14
|
+
constructor(dataDir: string) {
|
|
15
|
+
this.store = new JsonStore<SettingsMap>(join(dataDir, "settings.json"), {});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get(chatId: number): ChatSettings {
|
|
19
|
+
const existing = this.store.get()[String(chatId)];
|
|
20
|
+
return existing ?? defaultSettings();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
update(chatId: number, patch: Partial<ChatSettings>): ChatSettings {
|
|
24
|
+
const key = String(chatId);
|
|
25
|
+
const next = { ...this.get(chatId), ...patch };
|
|
26
|
+
this.store.update((m) => {
|
|
27
|
+
m[key] = next;
|
|
28
|
+
});
|
|
29
|
+
return next;
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/app/stt.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Speech-to-text via any OpenAI/Whisper-compatible endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Language handling: when STT_LANGUAGE is unset, Whisper auto-detects the
|
|
5
|
+
* spoken language (covers English, Russian, Romanian/Moldovan, and ~100 more),
|
|
6
|
+
* so multilingual voice notes work out of the box.
|
|
7
|
+
*/
|
|
8
|
+
import { createLogger } from "../logger.js";
|
|
9
|
+
|
|
10
|
+
const log = createLogger("stt");
|
|
11
|
+
|
|
12
|
+
export interface SttConfig {
|
|
13
|
+
apiUrl?: string;
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
model: string;
|
|
16
|
+
language?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class SttService {
|
|
20
|
+
constructor(private readonly cfg: SttConfig) {}
|
|
21
|
+
|
|
22
|
+
get enabled(): boolean {
|
|
23
|
+
return Boolean(this.cfg.apiUrl);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Transcribe audio bytes; returns the recognized text (may be empty). */
|
|
27
|
+
async transcribe(bytes: Buffer, mimeType: string, filename: string): Promise<string> {
|
|
28
|
+
if (!this.cfg.apiUrl) throw new Error("STT is not configured (set STT_API_URL).");
|
|
29
|
+
const url = endpoint(this.cfg.apiUrl);
|
|
30
|
+
|
|
31
|
+
const form = new FormData();
|
|
32
|
+
form.append("file", new Blob([new Uint8Array(bytes)], { type: mimeType }), filename);
|
|
33
|
+
form.append("model", this.cfg.model);
|
|
34
|
+
if (this.cfg.language) form.append("language", this.cfg.language);
|
|
35
|
+
|
|
36
|
+
const headers: Record<string, string> = {};
|
|
37
|
+
if (this.cfg.apiKey) headers.Authorization = `Bearer ${this.cfg.apiKey}`;
|
|
38
|
+
|
|
39
|
+
const res = await fetch(url, { method: "POST", headers, body: form });
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const detail = await res.text().catch(() => "");
|
|
42
|
+
throw new Error(`STT HTTP ${res.status}: ${detail.slice(0, 200)}`);
|
|
43
|
+
}
|
|
44
|
+
const data = (await res.json()) as { text?: string };
|
|
45
|
+
log.debug("transcribed", (data.text ?? "").length, "chars");
|
|
46
|
+
return (data.text ?? "").trim();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function endpoint(base: string): string {
|
|
51
|
+
const b = base.replace(/\/$/, "");
|
|
52
|
+
return b.endsWith("/audio/transcriptions") ? b : `${b}/audio/transcriptions`;
|
|
53
|
+
}
|
package/src/app/types.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared application types: per-chat settings, reasoning levels, and the
|
|
3
|
+
* prompt input model (text plus optional images) used across the bot.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const REASONING_LEVELS = ["minimal", "low", "medium", "high", "max"] as const;
|
|
7
|
+
export type ReasoningEffort = (typeof REASONING_LEVELS)[number];
|
|
8
|
+
|
|
9
|
+
export interface ChatSettings {
|
|
10
|
+
projectPath?: string;
|
|
11
|
+
projectName?: string;
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
agent?: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
reasoning: ReasoningEffort;
|
|
16
|
+
/** Telegram message id of the pinned status panel, if any. */
|
|
17
|
+
statusMessageId?: number;
|
|
18
|
+
/** Sessions this chat controls (for multi-session switching). */
|
|
19
|
+
controlledSessions?: ControlledSession[];
|
|
20
|
+
/** Which controlled session is currently in the foreground. */
|
|
21
|
+
foregroundSessionId?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ControlledSession {
|
|
25
|
+
sessionId?: string;
|
|
26
|
+
projectPath: string;
|
|
27
|
+
projectName?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function defaultSettings(): ChatSettings {
|
|
31
|
+
return { reasoning: "medium" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A decoded image to attach to a prompt as an ACP image content block. */
|
|
35
|
+
export interface PromptImage {
|
|
36
|
+
data: string; // base64-encoded bytes
|
|
37
|
+
mimeType: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A unit of work submitted to the agent: text plus optional images. */
|
|
41
|
+
export interface PromptInput {
|
|
42
|
+
text: string;
|
|
43
|
+
images: PromptImage[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function textPrompt(text: string): PromptInput {
|
|
47
|
+
return { text, images: [] };
|
|
48
|
+
}
|
package/src/app/usage.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account info via `kiro-cli whoami`. (Kiro's full billing/quota panel isn't
|
|
3
|
+
* available headlessly over ACP, so /usage shows account + live context usage.)
|
|
4
|
+
*/
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
const run = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
export interface AccountInfo {
|
|
11
|
+
accountType?: string;
|
|
12
|
+
email?: string;
|
|
13
|
+
region?: string;
|
|
14
|
+
startUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class UsageService {
|
|
18
|
+
constructor(private readonly kiroCliPath: string) {}
|
|
19
|
+
|
|
20
|
+
async account(): Promise<AccountInfo | undefined> {
|
|
21
|
+
try {
|
|
22
|
+
const { stdout } = await run(this.kiroCliPath, ["whoami", "--format", "json"], {
|
|
23
|
+
timeout: 10_000,
|
|
24
|
+
encoding: "utf-8",
|
|
25
|
+
});
|
|
26
|
+
const match = stdout.match(/\{[\s\S]*?\}/);
|
|
27
|
+
return match ? (JSON.parse(match[0]) as AccountInfo) : undefined;
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/bot/auth.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization middleware: restricts the bot to ALLOWED_USERS when configured.
|
|
3
|
+
*/
|
|
4
|
+
import type { Context, NextFunction } from "grammy";
|
|
5
|
+
import type { AppConfig } from "../config.js";
|
|
6
|
+
import { createLogger } from "../logger.js";
|
|
7
|
+
|
|
8
|
+
const log = createLogger("auth");
|
|
9
|
+
|
|
10
|
+
export function createAuthMiddleware(cfg: AppConfig) {
|
|
11
|
+
const allowAll = cfg.allowedUsers.size === 0;
|
|
12
|
+
if (allowAll) {
|
|
13
|
+
log.warn("ALLOWED_USERS is empty — the bot will respond to ANY Telegram user.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return async (ctx: Context, next: NextFunction): Promise<void> => {
|
|
17
|
+
const userId = ctx.from?.id ? String(ctx.from.id) : undefined;
|
|
18
|
+
if (allowAll || (userId && cfg.allowedUsers.has(userId))) {
|
|
19
|
+
await next();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
log.warn(`blocked unauthorized user ${userId ?? "unknown"}`);
|
|
23
|
+
if (ctx.chat) {
|
|
24
|
+
await ctx.reply("\u26D4 Not authorized. Ask the bot owner to add your Telegram ID.");
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
package/src/bot/bot.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assemble the grammY bot: dependencies, middleware, handlers, persistent menu,
|
|
3
|
+
* status panel, and the task scheduler. Handler registration order matters:
|
|
4
|
+
* auth -> menu buttons -> wizard input -> commands -> photos -> text prompt.
|
|
5
|
+
*/
|
|
6
|
+
import { Bot } from "grammy";
|
|
7
|
+
import type { AcpClient } from "../acp/client.js";
|
|
8
|
+
import { SettingsStore } from "../app/settings-store.js";
|
|
9
|
+
import { SttService } from "../app/stt.js";
|
|
10
|
+
import { UsageService } from "../app/usage.js";
|
|
11
|
+
import type { AppConfig } from "../config.js";
|
|
12
|
+
import { createLogger } from "../logger.js";
|
|
13
|
+
import { ProjectManager } from "../projects/manager.js";
|
|
14
|
+
import { SessionStore } from "../sessions/store.js";
|
|
15
|
+
import { TaskRunner } from "../tasks/runner.js";
|
|
16
|
+
import { Scheduler } from "../tasks/scheduler.js";
|
|
17
|
+
import { TaskStore } from "../tasks/store.js";
|
|
18
|
+
import { createAuthMiddleware } from "./auth.js";
|
|
19
|
+
import { COMMANDS } from "./commands.js";
|
|
20
|
+
import { type BotDeps, MenuCache } from "./deps.js";
|
|
21
|
+
import { registerControl } from "./handlers/control.js";
|
|
22
|
+
import { registerHistory } from "./handlers/history.js";
|
|
23
|
+
import { registerKill } from "./handlers/kill.js";
|
|
24
|
+
import { registerMcp } from "./handlers/mcp.js";
|
|
25
|
+
import { registerMenu } from "./handlers/menu.js";
|
|
26
|
+
import { registerMessages } from "./handlers/message.js";
|
|
27
|
+
import { registerPhotos } from "./handlers/photo.js";
|
|
28
|
+
import { registerProjects } from "./handlers/projects.js";
|
|
29
|
+
import { registerRunning, switchAndShow } from "./handlers/running.js";
|
|
30
|
+
import { registerSessions } from "./handlers/sessions.js";
|
|
31
|
+
import { registerSystem } from "./handlers/system.js";
|
|
32
|
+
import { registerTasks, registerWizardInput } from "./handlers/tasks.js";
|
|
33
|
+
import { registerUsage } from "./handlers/usage.js";
|
|
34
|
+
import { registerVoice } from "./handlers/voice.js";
|
|
35
|
+
import { StatusPanel } from "./menu/status-panel.js";
|
|
36
|
+
import { PermissionService } from "./permission-service.js";
|
|
37
|
+
import { RuntimeRegistry } from "./registry.js";
|
|
38
|
+
import { TaskWizard } from "./wizard/task-wizard.js";
|
|
39
|
+
|
|
40
|
+
const log = createLogger("bot");
|
|
41
|
+
|
|
42
|
+
/** Telegram methods that support disable_notification (silenced in quiet mode). */
|
|
43
|
+
const SILENCEABLE = new Set([
|
|
44
|
+
"sendMessage",
|
|
45
|
+
"sendPhoto",
|
|
46
|
+
"sendDocument",
|
|
47
|
+
"sendAudio",
|
|
48
|
+
"sendVoice",
|
|
49
|
+
"sendVideo",
|
|
50
|
+
"sendAnimation",
|
|
51
|
+
"sendMediaGroup",
|
|
52
|
+
"copyMessage",
|
|
53
|
+
"forwardMessage",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
export interface BotBundle {
|
|
57
|
+
bot: Bot;
|
|
58
|
+
registry: RuntimeRegistry;
|
|
59
|
+
scheduler: Scheduler;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function createBot(cfg: AppConfig, acp: AcpClient): Promise<BotBundle> {
|
|
63
|
+
const bot = new Bot(cfg.token);
|
|
64
|
+
|
|
65
|
+
// Quiet mode (default): silence every outgoing message unless the caller
|
|
66
|
+
// explicitly set disable_notification:false (turn completion, permission
|
|
67
|
+
// prompts, task results). Edits never notify, so they're unaffected.
|
|
68
|
+
if (cfg.quietNotifications) {
|
|
69
|
+
bot.api.config.use(async (prev, method, payload, signal) => {
|
|
70
|
+
if (SILENCEABLE.has(method)) {
|
|
71
|
+
const p = payload as { disable_notification?: boolean };
|
|
72
|
+
if (p.disable_notification === undefined) p.disable_notification = true;
|
|
73
|
+
}
|
|
74
|
+
return prev(method, payload, signal);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const settings = new SettingsStore(cfg.dataDir);
|
|
79
|
+
const store = new SessionStore(cfg.sessionsDir);
|
|
80
|
+
const registry = new RuntimeRegistry(bot.api, acp, cfg, settings, store);
|
|
81
|
+
const tasks = new TaskStore(cfg.dataDir);
|
|
82
|
+
const taskRunner = new TaskRunner(bot.api, acp);
|
|
83
|
+
const wizard = new TaskWizard(tasks);
|
|
84
|
+
const statusPanel = new StatusPanel(bot.api, settings, registry);
|
|
85
|
+
registry.setRefresher((chatId) => void statusPanel.refresh(chatId));
|
|
86
|
+
|
|
87
|
+
const deps: BotDeps = {
|
|
88
|
+
api: bot.api,
|
|
89
|
+
cfg,
|
|
90
|
+
acp,
|
|
91
|
+
registry,
|
|
92
|
+
store,
|
|
93
|
+
projects: new ProjectManager(cfg.projectRoots),
|
|
94
|
+
menuCache: new MenuCache(),
|
|
95
|
+
settings,
|
|
96
|
+
statusPanel,
|
|
97
|
+
tasks,
|
|
98
|
+
taskRunner,
|
|
99
|
+
wizard,
|
|
100
|
+
stt: new SttService({
|
|
101
|
+
apiUrl: cfg.sttApiUrl,
|
|
102
|
+
apiKey: cfg.sttApiKey,
|
|
103
|
+
model: cfg.sttModel,
|
|
104
|
+
language: cfg.sttLanguage,
|
|
105
|
+
}),
|
|
106
|
+
usage: new UsageService(cfg.kiroCliPath),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Inline approvals: when NOT in trust-all mode, Kiro asks before risky tools.
|
|
110
|
+
const permissions = new PermissionService(bot.api, registry);
|
|
111
|
+
acp.permissionHandler = (p) => permissions.handle(p);
|
|
112
|
+
|
|
113
|
+
bot.use(createAuthMiddleware(cfg));
|
|
114
|
+
|
|
115
|
+
bot.callbackQuery(/^perm:(\d+):(\d+)$/, async (ctx) => {
|
|
116
|
+
const label = permissions.resolveChoice(ctx.match![1]!, Number(ctx.match![2]));
|
|
117
|
+
await ctx.answerCallbackQuery({ text: label ?? "Expired" });
|
|
118
|
+
await ctx.editMessageText(label ? `\u{1F510} ${label}` : "\u{1F510} (expired)").catch(() => {});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
bot.callbackQuery(/^permsw:(\d+)$/, async (ctx) => {
|
|
122
|
+
await ctx.answerCallbackQuery();
|
|
123
|
+
const sid = permissions.sessionFor(ctx.match![1]!);
|
|
124
|
+
if (sid) await switchAndShow(ctx, deps, sid);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
registerMenu(bot, deps); // persistent-keyboard buttons (hears)
|
|
128
|
+
registerWizardInput(bot, deps); // wizard text input (before commands)
|
|
129
|
+
registerControl(bot, deps);
|
|
130
|
+
registerProjects(bot, deps);
|
|
131
|
+
registerSessions(bot, deps);
|
|
132
|
+
registerRunning(bot, deps);
|
|
133
|
+
registerHistory(bot, deps);
|
|
134
|
+
registerSystem(bot, deps);
|
|
135
|
+
registerUsage(bot, deps);
|
|
136
|
+
registerKill(bot, deps);
|
|
137
|
+
registerMcp(bot, deps);
|
|
138
|
+
registerTasks(bot, deps);
|
|
139
|
+
registerPhotos(bot, deps); // photos & image documents
|
|
140
|
+
registerVoice(bot, deps); // voice / audio -> transcription -> prompt
|
|
141
|
+
registerMessages(bot, deps); // catch-all text prompt — keep last
|
|
142
|
+
|
|
143
|
+
bot.catch((err) => {
|
|
144
|
+
log.error("unhandled bot error:", err.error instanceof Error ? err.error.message : err.error);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await bot.api.setMyCommands(COMMANDS);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
log.warn("setMyCommands failed:", (e as Error).message);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { bot, registry, scheduler: new Scheduler(tasks, taskRunner) };
|
|
154
|
+
}
|