@inceptionstack/roundhouse 0.5.4 → 0.5.5
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/architecture.md +37 -18
- package/package.json +2 -1
- package/skills/pr-merge-discipline/SKILL.md +36 -0
- package/skills/roundhouse-cron/SKILL.md +136 -0
- package/src/agents/kiro/kiro-adapter.ts +1 -4
- package/src/agents/pi/pi-adapter.ts +1 -4
- package/src/cli/cli.ts +1 -1
- package/src/cli/setup/args.ts +8 -9
- package/src/cli/setup/flows.ts +47 -14
- package/src/cli/{setup-logger.ts → setup/logger.ts} +4 -4
- package/src/cli/{setup-prompts.ts → setup/prompts.ts} +23 -2
- package/src/cli/setup/runtime.ts +1 -1
- package/src/cli/setup/steps.ts +3 -3
- package/src/cli/{setup-telegram.ts → setup/telegram.ts} +4 -4
- package/src/cli/setup/types.ts +4 -3
- package/src/cli/setup.ts +8 -8
- package/src/cli/systemd.ts +2 -0
- package/src/{commands → cli}/update.ts +1 -1
- package/src/cron/runner.ts +2 -1
- package/src/gateway/commands.ts +4 -3
- package/src/{gateway.ts → gateway/gateway.ts} +63 -97
- package/src/gateway/helpers.ts +1 -1
- package/src/gateway/index.ts +2 -5
- package/src/gateway/streaming.ts +1 -1
- package/src/{bundle.ts → provisioning/bundle.ts} +32 -0
- package/src/transports/index.ts +6 -0
- package/src/{telegram-html.ts → transports/telegram/html.ts} +2 -2
- package/src/{pairing.ts → transports/telegram/pairing.ts} +1 -1
- package/src/transports/telegram/telegram-adapter.ts +111 -0
- package/src/transports/types.ts +71 -0
- /package/src/{commands.ts → transports/telegram/bot-commands.ts} +0 -0
- /package/src/{telegram-format.ts → transports/telegram/format.ts} +0 -0
- /package/src/{notify/telegram.ts → transports/telegram/notify.ts} +0 -0
- /package/src/{telegram-progress.ts → transports/telegram/progress.ts} +0 -0
package/src/cli/setup.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { readFile } from "node:fs/promises";
|
|
13
|
-
import { BOT_COMMANDS } from "../commands";
|
|
13
|
+
import { BOT_COMMANDS } from "../transports/telegram/bot-commands";
|
|
14
14
|
import { atomicWriteJson, execSafe } from "./setup/helpers";
|
|
15
15
|
import { type SetupOptions } from "./setup/types";
|
|
16
16
|
import { parseSetupArgs } from "./setup/args";
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
import {
|
|
28
28
|
validateBotToken,
|
|
29
29
|
pairTelegram,
|
|
30
|
-
} from "./setup
|
|
30
|
+
} from "./setup/telegram";
|
|
31
31
|
import {
|
|
32
32
|
stepPreflight,
|
|
33
33
|
stepValidateToken,
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
stepPostflight,
|
|
43
43
|
} from "./setup/steps";
|
|
44
44
|
import { resolveAgentForSetup, textLog, textStepLog } from "./setup/runtime";
|
|
45
|
-
import { runInteractiveTelegramSetup,
|
|
45
|
+
import { runInteractiveTelegramSetup, runNonInteractiveTelegramSetup } from "./setup/flows";
|
|
46
46
|
|
|
47
47
|
// ── Orchestrator ─────────────────────────────────────
|
|
48
48
|
|
|
@@ -63,8 +63,8 @@ export async function cmdSetup(argv: string[]): Promise<void> {
|
|
|
63
63
|
|
|
64
64
|
// Route to --telegram flows
|
|
65
65
|
if (opts.telegram) {
|
|
66
|
-
if (opts.
|
|
67
|
-
await
|
|
66
|
+
if (opts.nonInteractive) {
|
|
67
|
+
await runNonInteractiveTelegramSetup(opts);
|
|
68
68
|
} else {
|
|
69
69
|
await runInteractiveTelegramSetup(opts);
|
|
70
70
|
}
|
|
@@ -258,12 +258,12 @@ function printSetupHelp(): void {
|
|
|
258
258
|
console.log(`
|
|
259
259
|
Usage:
|
|
260
260
|
roundhouse setup --telegram Interactive wizard (recommended)
|
|
261
|
-
TELEGRAM_BOT_TOKEN=... roundhouse setup \\\n --telegram --
|
|
261
|
+
TELEGRAM_BOT_TOKEN=... roundhouse setup \\\n --telegram --non-interactive --user USERNAME Non-interactive automation (SSM/cloud-init)
|
|
262
262
|
TELEGRAM_BOT_TOKEN=... roundhouse setup \\\n --user USERNAME Legacy (non-wizard) setup
|
|
263
263
|
|
|
264
264
|
Modes:
|
|
265
|
-
--telegram Telegram-focused setup (wizard or
|
|
266
|
-
--
|
|
265
|
+
--telegram Telegram-focused setup (wizard or non-interactive)
|
|
266
|
+
--non-interactive Suppress all prompts (for automation/SSM/cloud-init)
|
|
267
267
|
Requires TELEGRAM_BOT_TOKEN env var and --user
|
|
268
268
|
|
|
269
269
|
Required (or prompted in interactive --telegram):
|
package/src/cli/systemd.ts
CHANGED
|
@@ -127,6 +127,8 @@ WorkingDirectory=${home}
|
|
|
127
127
|
ExecStart=${opts.execStart}
|
|
128
128
|
Restart=on-failure
|
|
129
129
|
RestartSec=5
|
|
130
|
+
TimeoutStopSec=15
|
|
131
|
+
KillMode=mixed
|
|
130
132
|
EnvironmentFile=-${envFilePath}
|
|
131
133
|
Environment=ROUNDHOUSE_CONFIG=${CONFIG_PATH}
|
|
132
134
|
Environment=NODE_ENV=production
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { execSync } from "node:child_process";
|
|
10
10
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
11
|
-
import { provisionBundle } from "../bundle";
|
|
11
|
+
import { provisionBundle } from "../provisioning/bundle";
|
|
12
12
|
|
|
13
13
|
export interface UpdateProgress {
|
|
14
14
|
update(text: string): Promise<void>;
|
package/src/cron/runner.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { getAgentFactory } from "../agents/registry";
|
|
9
|
-
|
|
9
|
+
// TODO: route through TransportAdapter.notify() when multi-transport lands
|
|
10
|
+
import { sendTelegramToMany } from "../transports/telegram/notify";
|
|
10
11
|
import { CronStore, generateRunId } from "./store";
|
|
11
12
|
import { buildTemplateContext, renderTemplate } from "./template";
|
|
12
13
|
import type { CronJobConfig, CronRunRecord } from "./types";
|
package/src/gateway/commands.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* gateway/commands.ts —
|
|
2
|
+
* gateway/commands.ts — Chat command handlers
|
|
3
3
|
*
|
|
4
4
|
* Each handler is a standalone async function that receives a CommandContext.
|
|
5
5
|
* Extracted from Gateway.start() to reduce method size and enable unit testing.
|
|
@@ -9,7 +9,8 @@ import type { AgentAdapter, AgentStreamEvent, GatewayConfig } from "../types";
|
|
|
9
9
|
import { ROUNDHOUSE_VERSION } from "../config";
|
|
10
10
|
import { startTypingLoop } from "../util";
|
|
11
11
|
import { prepareMemoryForTurn, finalizeMemoryForTurn, flushMemoryThenCompact, determineMemoryMode } from "../memory/lifecycle";
|
|
12
|
-
|
|
12
|
+
// TODO: move progress into TransportAdapter when multi-transport lands
|
|
13
|
+
import { createProgressMessage } from "../transports/telegram/progress";
|
|
13
14
|
import { getSystemResources } from "./helpers";
|
|
14
15
|
|
|
15
16
|
// ── Types ────────────────────────────────────────────
|
|
@@ -70,7 +71,7 @@ export async function handleUpdate(ctx: CommandContext): Promise<void> {
|
|
|
70
71
|
console.log(`[roundhouse] /update requested by @${authorName} in thread=${thread.id}`);
|
|
71
72
|
const progress = await createProgressMessage(thread, "📦 Checking for updates...");
|
|
72
73
|
try {
|
|
73
|
-
const { performUpdate } = await import("../
|
|
74
|
+
const { performUpdate } = await import("../cli/update");
|
|
74
75
|
const result = await performUpdate(progress);
|
|
75
76
|
if (result.action === "already-latest") {
|
|
76
77
|
await progress.update(`✅ Already on latest (v${result.currentVersion})`);
|
|
@@ -7,29 +7,30 @@
|
|
|
7
7
|
|
|
8
8
|
import { Chat } from "chat";
|
|
9
9
|
import { createMemoryState } from "@chat-adapter/state-memory";
|
|
10
|
-
import type { AgentAdapter, AgentMessage, AgentRouter, AgentStreamEvent, GatewayConfig } from "
|
|
11
|
-
import { splitMessage, isAllowed, startTypingLoop } from "
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
import
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
10
|
+
import type { AgentAdapter, AgentMessage, AgentRouter, AgentStreamEvent, GatewayConfig } from "../types";
|
|
11
|
+
import { splitMessage, isAllowed, startTypingLoop } from "../util";
|
|
12
|
+
import { SttService, enrichAttachmentsWithTranscripts, DEFAULT_STT_CONFIG } from "../voice/stt-service";
|
|
13
|
+
import { runDoctor, formatDoctorTelegram, createDoctorContext } from "../cli/doctor/runner";
|
|
14
|
+
import { ROUNDHOUSE_DIR, ROUNDHOUSE_VERSION } from "../config";
|
|
15
|
+
import { CronSchedulerService } from "../cron/scheduler";
|
|
16
|
+
import { prepareMemoryForTurn, finalizeMemoryForTurn, flushMemoryThenCompact } from "../memory/lifecycle";
|
|
17
|
+
import { maxPressure } from "../memory/policy";
|
|
18
|
+
import type { PressureLevel } from "../memory/types";
|
|
19
|
+
// TODO: move progress into TransportAdapter when multi-transport lands
|
|
20
|
+
import { createProgressMessage } from "../transports/telegram/progress";
|
|
21
|
+
import { isCommand as _isCmd, isCommandWithArgs as _isCmdArgs, resolveAgentThreadId as _resolveThread, getSystemResources as _getSysRes } from "./helpers";
|
|
22
|
+
import { saveAttachments as _saveAttachments, type AttachmentResult } from "./attachments";
|
|
23
|
+
import { handleStreaming as _handleStream } from "./streaming";
|
|
24
|
+
import { handleNew, handleRestart, handleUpdate, handleCompact, handleStatus, handleStop, handleVerbose, handleDoctor, handleCrons, type CommandContext } from "./commands";
|
|
25
|
+
import { TelegramAdapter } from "../transports";
|
|
26
|
+
import type { TransportAdapter } from "../transports";
|
|
27
|
+
import { hostname } from "node:os";
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
|
|
30
30
|
/** Bot username for command suffix validation (set during gateway init) */
|
|
31
31
|
let _botUsername = "";
|
|
32
32
|
|
|
33
|
+
/** Match a bot command, handling optional @botname suffix */
|
|
33
34
|
function isCommand(text: string, cmd: string): boolean {
|
|
34
35
|
return _isCmd(text, cmd, _botUsername);
|
|
35
36
|
}
|
|
@@ -38,12 +39,10 @@ function isCommand(text: string, cmd: string): boolean {
|
|
|
38
39
|
function isCommandWithArgs(text: string, cmd: string): boolean {
|
|
39
40
|
return _isCmdArgs(text, cmd, _botUsername);
|
|
40
41
|
}
|
|
41
|
-
import { hostname } from "node:os";
|
|
42
42
|
|
|
43
43
|
function getSystemResources() {
|
|
44
44
|
return _getSysRes();
|
|
45
45
|
}
|
|
46
|
-
import { join } from "node:path";
|
|
47
46
|
|
|
48
47
|
|
|
49
48
|
function resolveAgentThreadId(thread: any, message: any): string {
|
|
@@ -68,11 +67,6 @@ async function buildChatAdapters(
|
|
|
68
67
|
return adapters;
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
function toolIcon(name: string): string {
|
|
72
|
-
return _toolIcon(name);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
70
|
async function saveAttachments(threadId: string, attachments: any[]): Promise<AttachmentResult> {
|
|
77
71
|
return _saveAttachments(threadId, attachments);
|
|
78
72
|
}
|
|
@@ -83,6 +77,7 @@ export class Gateway {
|
|
|
83
77
|
private chat!: Chat;
|
|
84
78
|
private router: AgentRouter;
|
|
85
79
|
private config: GatewayConfig;
|
|
80
|
+
private transport: TransportAdapter;
|
|
86
81
|
private pairingComplete = false;
|
|
87
82
|
private sttService: SttService | null = null;
|
|
88
83
|
private cronScheduler: CronSchedulerService | null = null;
|
|
@@ -90,62 +85,41 @@ export class Gateway {
|
|
|
90
85
|
constructor(router: AgentRouter, config: GatewayConfig) {
|
|
91
86
|
this.router = router;
|
|
92
87
|
this.config = config;
|
|
88
|
+
this.transport = new TelegramAdapter();
|
|
93
89
|
_botUsername = config.chat.botUsername || "";
|
|
94
90
|
}
|
|
95
91
|
|
|
96
|
-
/** Handle pending
|
|
92
|
+
/** Handle pending pairing via transport adapter. Returns true if handled. */
|
|
97
93
|
private async handlePendingPairing(
|
|
98
|
-
text: string,
|
|
99
94
|
message: any,
|
|
100
95
|
thread: any,
|
|
101
|
-
authorName: string,
|
|
102
96
|
): Promise<boolean> {
|
|
103
97
|
try {
|
|
104
|
-
const
|
|
105
|
-
if (!
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
98
|
+
const result = await this.transport.handlePairing(thread, message);
|
|
99
|
+
if (!result) return false;
|
|
108
100
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
101
|
+
const { threadId: rawThreadId, userId: rawUserId, username } = result;
|
|
102
|
+
// Config arrays are currently number[] — coerce with guard.
|
|
103
|
+
// When a string-ID transport (Slack/Discord) arrives, widen config types too.
|
|
104
|
+
const threadId = typeof rawThreadId === "string" ? Number(rawThreadId) : rawThreadId;
|
|
105
|
+
const userId = typeof rawUserId === "string" ? Number(rawUserId) : rawUserId;
|
|
115
106
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
: typeof thread.id === "string" && thread.id.startsWith("telegram:")
|
|
120
|
-
? parseInt(thread.id.split(":")[1], 10)
|
|
121
|
-
: undefined;
|
|
122
|
-
// Chat SDK Telegram adapter provides userId (not id)
|
|
123
|
-
const rawUserId = message.author?.userId ?? message.author?.id ?? message.raw?.from?.id;
|
|
124
|
-
const userId = typeof rawUserId === "number"
|
|
125
|
-
? rawUserId
|
|
126
|
-
: typeof rawUserId === "string"
|
|
127
|
-
? parseInt(rawUserId, 10)
|
|
128
|
-
: undefined;
|
|
129
|
-
|
|
130
|
-
if (chatId == null || Number.isNaN(chatId) || userId == null || Number.isNaN(userId)) {
|
|
131
|
-
console.error(`[roundhouse] Pairing nonce matched but could not extract IDs: chatId=${chatId} userId=${userId}. Pairing left pending.`);
|
|
132
|
-
await thread.post("⚠️ Pairing nonce accepted but could not capture your Telegram IDs. Try sending /start again, or run: roundhouse pair");
|
|
133
|
-
return true;
|
|
107
|
+
if (!Number.isFinite(threadId) || !Number.isFinite(userId)) {
|
|
108
|
+
console.error(`[roundhouse] Pairing returned non-numeric IDs: threadId=${rawThreadId} userId=${rawUserId}`);
|
|
109
|
+
return false;
|
|
134
110
|
}
|
|
135
111
|
|
|
136
|
-
await completePendingPairing({ chatId, userId, username: authorName });
|
|
137
|
-
|
|
138
112
|
// Update in-memory config
|
|
139
113
|
if (!this.config.chat.allowedUserIds) this.config.chat.allowedUserIds = [];
|
|
140
114
|
if (!this.config.chat.allowedUserIds.includes(userId)) {
|
|
141
115
|
this.config.chat.allowedUserIds.push(userId);
|
|
142
116
|
}
|
|
143
117
|
if (!this.config.chat.notifyChatIds) this.config.chat.notifyChatIds = [];
|
|
144
|
-
if (!this.config.chat.notifyChatIds.includes(
|
|
145
|
-
this.config.chat.notifyChatIds.push(
|
|
118
|
+
if (!this.config.chat.notifyChatIds.includes(threadId)) {
|
|
119
|
+
this.config.chat.notifyChatIds.push(threadId);
|
|
146
120
|
}
|
|
147
121
|
|
|
148
|
-
//
|
|
122
|
+
// Persist config atomically
|
|
149
123
|
try {
|
|
150
124
|
const { readFile: rf, rename: mvf, writeFile: wf, unlink: ulf } = await import("node:fs/promises");
|
|
151
125
|
const { randomBytes: rb } = await import("node:crypto");
|
|
@@ -155,7 +129,7 @@ export class Gateway {
|
|
|
155
129
|
if (!configRaw.chat.allowedUserIds) configRaw.chat.allowedUserIds = [];
|
|
156
130
|
if (!configRaw.chat.allowedUserIds.includes(userId)) configRaw.chat.allowedUserIds.push(userId);
|
|
157
131
|
if (!configRaw.chat.notifyChatIds) configRaw.chat.notifyChatIds = [];
|
|
158
|
-
if (!configRaw.chat.notifyChatIds.includes(
|
|
132
|
+
if (!configRaw.chat.notifyChatIds.includes(threadId)) configRaw.chat.notifyChatIds.push(threadId);
|
|
159
133
|
const tmp = `${cfgPath}.tmp.${rb(4).toString("hex")}`;
|
|
160
134
|
await wf(tmp, JSON.stringify(configRaw, null, 2) + "\n");
|
|
161
135
|
await mvf(tmp, cfgPath).catch(async (e) => { try { await ulf(tmp); } catch {} throw e; });
|
|
@@ -163,7 +137,7 @@ export class Gateway {
|
|
|
163
137
|
console.error("[roundhouse] failed to update config after pairing:", cfgErr);
|
|
164
138
|
}
|
|
165
139
|
|
|
166
|
-
console.log(`[roundhouse]
|
|
140
|
+
console.log(`[roundhouse] Pairing complete: @${username} threadId=${threadId} userId=${userId}`);
|
|
167
141
|
this.pairingComplete = true;
|
|
168
142
|
await thread.post("✅ Roundhouse paired successfully!\n\nSend /status to verify everything is working.");
|
|
169
143
|
return true;
|
|
@@ -243,9 +217,9 @@ export class Gateway {
|
|
|
243
217
|
`[roundhouse] ${thread.id} -> ${agentThreadId} @${authorName}: "${userText.slice(0, 120)}"${rawAttachments.length ? ` +${rawAttachments.length} attachment(s)` : ""}`
|
|
244
218
|
);
|
|
245
219
|
|
|
246
|
-
// Check for pending
|
|
247
|
-
if (
|
|
248
|
-
const handled = await this.handlePendingPairing(
|
|
220
|
+
// Check for pending pairing via transport adapter
|
|
221
|
+
if (!this.pairingComplete && await this.transport.isPairingPending()) {
|
|
222
|
+
const handled = await this.handlePendingPairing(message, thread);
|
|
249
223
|
if (handled) return;
|
|
250
224
|
}
|
|
251
225
|
|
|
@@ -385,14 +359,23 @@ export class Gateway {
|
|
|
385
359
|
try {
|
|
386
360
|
console.log(`[roundhouse] → ${agent.name} | thread=${agentThreadId}`);
|
|
387
361
|
|
|
388
|
-
// Enrich audio attachments with transcripts (STT)
|
|
389
|
-
|
|
362
|
+
// Enrich audio attachments with transcripts (STT) — show typing while processing
|
|
363
|
+
if (agentMessage.attachments?.some((a: any) => a.mediaType === "audio")) {
|
|
364
|
+
const sttTyping = startTypingLoop(thread);
|
|
365
|
+
try {
|
|
366
|
+
await this.enrichWithStt(thread, agentMessage);
|
|
367
|
+
} finally {
|
|
368
|
+
sttTyping();
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
await this.enrichWithStt(thread, agentMessage);
|
|
372
|
+
}
|
|
390
373
|
|
|
391
374
|
// Let the agent adapter apply platform-specific message transforms
|
|
392
375
|
if (agent.prepareMessage) {
|
|
393
376
|
try {
|
|
394
377
|
agentMessage = agent.prepareMessage(agentThreadId, agentMessage, {
|
|
395
|
-
platform:
|
|
378
|
+
platform: this.transport.name,
|
|
396
379
|
hasAttachments: !!(agentMessage.attachments?.length),
|
|
397
380
|
});
|
|
398
381
|
} catch (err) {
|
|
@@ -528,6 +511,11 @@ export class Gateway {
|
|
|
528
511
|
return null;
|
|
529
512
|
}
|
|
530
513
|
|
|
514
|
+
// Enrich prompt via transport adapter
|
|
515
|
+
if (agentMessage.text) {
|
|
516
|
+
agentMessage.text = this.transport.enrichPrompt(agentMessage.text);
|
|
517
|
+
}
|
|
518
|
+
|
|
531
519
|
return agentMessage;
|
|
532
520
|
}
|
|
533
521
|
|
|
@@ -612,9 +600,8 @@ export class Gateway {
|
|
|
612
600
|
|
|
613
601
|
/** Post text with markdown, falling back to plain text */
|
|
614
602
|
private async postWithFallback(thread: any, text: string) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
await postTelegramHtml(thread, text);
|
|
603
|
+
if (this.transport.ownsThread(thread)) {
|
|
604
|
+
await this.transport.postMessage(thread, text);
|
|
618
605
|
return;
|
|
619
606
|
}
|
|
620
607
|
for (const chunk of splitMessage(text, 4000)) {
|
|
@@ -636,25 +623,9 @@ export class Gateway {
|
|
|
636
623
|
*/
|
|
637
624
|
private async registerBotCommands() {
|
|
638
625
|
if (!this.config.chat.adapters.telegram) return;
|
|
639
|
-
|
|
640
626
|
const token = process.env.TELEGRAM_BOT_TOKEN;
|
|
641
627
|
if (!token) return;
|
|
642
|
-
|
|
643
|
-
try {
|
|
644
|
-
const res = await fetch(`https://api.telegram.org/bot${token}/setMyCommands`, {
|
|
645
|
-
method: "POST",
|
|
646
|
-
headers: { "Content-Type": "application/json" },
|
|
647
|
-
body: JSON.stringify({ commands: BOT_COMMANDS }),
|
|
648
|
-
});
|
|
649
|
-
if (res.ok) {
|
|
650
|
-
console.log(`[roundhouse] registered ${BOT_COMMANDS.length} bot commands with Telegram`);
|
|
651
|
-
} else {
|
|
652
|
-
const body = await res.text().catch(() => "");
|
|
653
|
-
console.warn(`[roundhouse] failed to register bot commands (${res.status}): ${body.slice(0, 200)}`);
|
|
654
|
-
}
|
|
655
|
-
} catch (err) {
|
|
656
|
-
console.warn(`[roundhouse] failed to register bot commands:`, (err as Error).message);
|
|
657
|
-
}
|
|
628
|
+
await this.transport.registerCommands(token);
|
|
658
629
|
}
|
|
659
630
|
|
|
660
631
|
/**
|
|
@@ -666,11 +637,6 @@ export class Gateway {
|
|
|
666
637
|
const chatIds = this.config.chat.notifyChatIds;
|
|
667
638
|
if (!chatIds?.length) return;
|
|
668
639
|
|
|
669
|
-
if (!process.env.TELEGRAM_BOT_TOKEN) {
|
|
670
|
-
console.warn("[roundhouse] notifyChatIds configured but TELEGRAM_BOT_TOKEN not set — skipping startup notification");
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
640
|
const bootTime = process.uptime();
|
|
675
641
|
const host = hostname();
|
|
676
642
|
const agentName = this.config.agent.type;
|
|
@@ -715,7 +681,7 @@ export class Gateway {
|
|
|
715
681
|
` Process: ${memMB} MB RSS`,
|
|
716
682
|
].filter(line => line != null).join("\n");
|
|
717
683
|
|
|
718
|
-
await
|
|
684
|
+
await this.transport.notify([chatId], perChatText);
|
|
719
685
|
}
|
|
720
686
|
}
|
|
721
687
|
|
package/src/gateway/helpers.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { hostname, loadavg, totalmem, freemem, cpus } from "node:os";
|
|
|
10
10
|
// ── Command Matching ─────────────────────────────────
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Match a
|
|
13
|
+
* Match a bot command, handling optional @botname suffix.
|
|
14
14
|
*/
|
|
15
15
|
export function isCommand(text: string, cmd: string, botUsername: string): boolean {
|
|
16
16
|
if (text === cmd) return true;
|
package/src/gateway/index.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* gateway/index.ts — Barrel export for gateway
|
|
3
|
-
*
|
|
4
|
-
* Re-exports helpers, attachments, streaming, and commands for
|
|
5
|
-
* external consumers. The Gateway class itself lives at src/gateway.ts
|
|
6
|
-
* and is imported directly (not through this barrel).
|
|
2
|
+
* gateway/index.ts — Barrel export for gateway module
|
|
7
3
|
*/
|
|
8
4
|
|
|
5
|
+
export { Gateway } from "./gateway";
|
|
9
6
|
export { isCommand, isCommandWithArgs, resolveAgentThreadId, getSystemResources, toolIcon } from "./helpers";
|
|
10
7
|
export { saveAttachments } from "./attachments";
|
|
11
8
|
export { handleStreaming } from "./streaming";
|
package/src/gateway/streaming.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { AgentStreamEvent } from "../types";
|
|
12
12
|
import { READ_ONLY_TOOLS } from "../memory/types";
|
|
13
|
-
import { isTelegramThread, handleTelegramHtmlStream } from "../telegram
|
|
13
|
+
import { isTelegramThread, handleTelegramHtmlStream } from "../transports/telegram/html";
|
|
14
14
|
import { DEBUG_STREAM } from "../util";
|
|
15
15
|
import { toolIcon } from "./helpers";
|
|
16
16
|
|
|
@@ -182,12 +182,44 @@ export function provisionMcporterConfig(opts: ProvisionOpts = {}): void {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Sync bundled skills that ship with roundhouse (additive, overwrites on each provision).
|
|
187
|
+
*/
|
|
188
|
+
export function syncBundledSkills(opts: ProvisionOpts = {}): void {
|
|
189
|
+
const log = opts.log ?? consoleLog;
|
|
190
|
+
// Resolves to <package-root>/skills/ (two levels up from src/provisioning/)
|
|
191
|
+
const bundledDir = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
|
|
192
|
+
if (!existsSync(bundledDir)) return;
|
|
193
|
+
|
|
194
|
+
const entries = readdirSync(bundledDir, { withFileTypes: true })
|
|
195
|
+
.filter(e => e.isDirectory() && !e.name.startsWith("."));
|
|
196
|
+
if (entries.length === 0) return;
|
|
197
|
+
|
|
198
|
+
mkdirSync(SKILLS_DIR, { recursive: true });
|
|
199
|
+
let count = 0;
|
|
200
|
+
for (const entry of entries) {
|
|
201
|
+
const src = resolve(bundledDir, entry.name);
|
|
202
|
+
const dest = resolve(SKILLS_DIR, entry.name);
|
|
203
|
+
if (!dest.startsWith(SKILLS_DIR + "/")) continue;
|
|
204
|
+
try {
|
|
205
|
+
execFileSync("rm", ["-rf", dest], { stdio: "pipe", timeout: 10_000 });
|
|
206
|
+
execFileSync("cp", ["-r", src, dest], { stdio: "pipe", timeout: 30_000 });
|
|
207
|
+
count++;
|
|
208
|
+
} catch (e: any) {
|
|
209
|
+
log.warn(`Failed to copy bundled skill '${entry.name}': ${e.message}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (count > 0) log.ok(`${count} bundled skill(s) synced`);
|
|
213
|
+
}
|
|
214
|
+
|
|
185
215
|
/**
|
|
186
216
|
* Provision all bundle dependencies (skills + CLI tools + config + extensions).
|
|
187
217
|
* Non-fatal — logs warnings on failure but never throws.
|
|
188
218
|
*/
|
|
189
219
|
export function provisionBundle(opts: ProvisionOpts = {}): void {
|
|
190
220
|
syncSkillsFromRepo(opts);
|
|
221
|
+
// Must run after syncSkillsFromRepo so bundled skills take precedence on name collision
|
|
222
|
+
syncBundledSkills(opts);
|
|
191
223
|
provisionMcporter(opts);
|
|
192
224
|
provisionPlaywright(opts);
|
|
193
225
|
provisionUvx(opts);
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* typing indicators, command handling, authorization, message history.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { markdownToTelegramHtml, truncateHtmlSafe } from "./
|
|
12
|
-
import { splitMessage } from "
|
|
11
|
+
import { markdownToTelegramHtml, truncateHtmlSafe } from "./format";
|
|
12
|
+
import { splitMessage } from "../../util";
|
|
13
13
|
|
|
14
14
|
/** Max Telegram message length */
|
|
15
15
|
const TELEGRAM_LIMIT = 4096;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
|
|
9
9
|
import { dirname, resolve } from "node:path";
|
|
10
10
|
import { randomBytes } from "node:crypto";
|
|
11
|
-
import { ROUNDHOUSE_DIR } from "
|
|
11
|
+
import { ROUNDHOUSE_DIR } from "../../config";
|
|
12
12
|
|
|
13
13
|
export interface PendingPairing {
|
|
14
14
|
version: 1;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transports/telegram/telegram-adapter.ts — Telegram transport adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements TransportAdapter for Telegram, composing existing
|
|
5
|
+
* utility modules (format, html, progress, notify, bot-commands).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TransportAdapter, ChatThread, IncomingMessage, PairingResult } from "../types";
|
|
9
|
+
import { isTelegramThread, postTelegramHtml } from "./html";
|
|
10
|
+
import { sendTelegramToMany } from "./notify";
|
|
11
|
+
import { BOT_COMMANDS } from "./bot-commands";
|
|
12
|
+
import { readPendingPairing, completePendingPairing, clearPendingPairing, isStartForNonce } from "./pairing";
|
|
13
|
+
|
|
14
|
+
const TELEGRAM_FORMAT_HINT = "[Format your final answer to be telegram-friendly.]";
|
|
15
|
+
|
|
16
|
+
export class TelegramAdapter implements TransportAdapter {
|
|
17
|
+
readonly name = "telegram";
|
|
18
|
+
|
|
19
|
+
enrichPrompt(text: string): string {
|
|
20
|
+
return `${text}\n\n${TELEGRAM_FORMAT_HINT}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async postMessage(thread: ChatThread, text: string): Promise<void> {
|
|
24
|
+
if (!isTelegramThread(thread as any)) {
|
|
25
|
+
throw new Error("TelegramAdapter.postMessage called with non-Telegram thread");
|
|
26
|
+
}
|
|
27
|
+
await postTelegramHtml(thread as any, text);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async registerCommands(token: string): Promise<void> {
|
|
31
|
+
if (!token) return;
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(`https://api.telegram.org/bot${token}/setMyCommands`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "Content-Type": "application/json" },
|
|
36
|
+
body: JSON.stringify({ commands: BOT_COMMANDS }),
|
|
37
|
+
});
|
|
38
|
+
if (res.ok) {
|
|
39
|
+
console.log(`[roundhouse] registered ${BOT_COMMANDS.length} bot commands with Telegram`);
|
|
40
|
+
} else {
|
|
41
|
+
const body = await res.text().catch(() => "");
|
|
42
|
+
console.warn(`[roundhouse] failed to register bot commands (${res.status}): ${body.slice(0, 200)}`);
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.warn(`[roundhouse] bot command registration error:`, (err as Error).message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ownsThread(thread: ChatThread): boolean {
|
|
50
|
+
return isTelegramThread(thread as any);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async notify(chatIds: number[], text: string): Promise<void> {
|
|
54
|
+
if (!process.env.TELEGRAM_BOT_TOKEN) {
|
|
55
|
+
console.warn("[roundhouse] TELEGRAM_BOT_TOKEN not set — skipping notification");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
await sendTelegramToMany(chatIds, text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async isPairingPending(): Promise<boolean> {
|
|
62
|
+
const pending = await readPendingPairing();
|
|
63
|
+
return pending?.status === "pending";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async handlePairing(thread: ChatThread, message: IncomingMessage): Promise<PairingResult | null> {
|
|
67
|
+
const text = (message.text ?? "").trim();
|
|
68
|
+
if (!text) return null;
|
|
69
|
+
|
|
70
|
+
const pending = await readPendingPairing();
|
|
71
|
+
if (!pending || pending.status !== "pending" || !isStartForNonce(text, pending.nonce)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Verify author is allowed
|
|
76
|
+
const authorName = (message.author?.userName ?? message.author?.name ?? "").toLowerCase();
|
|
77
|
+
const originalName = message.author?.userName ?? message.author?.name ?? "";
|
|
78
|
+
const allowed = pending.allowedUsers.map(u => u.toLowerCase());
|
|
79
|
+
if (!authorName || !allowed.includes(authorName)) {
|
|
80
|
+
console.log(`[roundhouse] Pairing nonce from unauthorized user @${originalName}`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Extract Telegram-specific IDs
|
|
85
|
+
const msg = message as any;
|
|
86
|
+
const chatId = typeof msg.chatId === "number"
|
|
87
|
+
? msg.chatId
|
|
88
|
+
: typeof thread.id === "string" && thread.id.startsWith("telegram:")
|
|
89
|
+
? parseInt(thread.id.split(":")[1], 10)
|
|
90
|
+
: undefined;
|
|
91
|
+
|
|
92
|
+
const rawUserId = msg.author?.userId ?? msg.author?.id ?? msg.raw?.from?.id;
|
|
93
|
+
const userId = typeof rawUserId === "number"
|
|
94
|
+
? rawUserId
|
|
95
|
+
: typeof rawUserId === "string"
|
|
96
|
+
? parseInt(rawUserId, 10)
|
|
97
|
+
: undefined;
|
|
98
|
+
|
|
99
|
+
if (chatId == null || Number.isNaN(chatId) || userId == null || Number.isNaN(userId)) {
|
|
100
|
+
console.error(`[roundhouse] Pairing nonce matched but could not extract IDs: chatId=${chatId} userId=${userId} (raw: msg.chatId=${message.chatId}, thread.id=${thread.id}, author.userId=${message.author?.userId}, author.id=${message.author?.id}, raw.from.id=${message.raw?.from?.id})`);
|
|
101
|
+
await clearPendingPairing();
|
|
102
|
+
await thread.post("⚠️ Pairing failed — could not capture your Telegram IDs. Run: roundhouse setup --telegram");
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Mark pairing complete in transport state
|
|
107
|
+
await completePendingPairing({ chatId, userId, username: originalName });
|
|
108
|
+
|
|
109
|
+
return { threadId: chatId, userId, username: originalName };
|
|
110
|
+
}
|
|
111
|
+
}
|