@inceptionstack/roundhouse 0.5.3 → 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 +25 -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 +26 -2
- 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/types.ts +2 -1
- /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
|
|
|
@@ -116,6 +116,8 @@ export async function handleStreaming(
|
|
|
116
116
|
};
|
|
117
117
|
|
|
118
118
|
let hasTextInCurrentTurn = false;
|
|
119
|
+
let hasContentThisTurn = false;
|
|
120
|
+
let modelErrorPosted = false;
|
|
119
121
|
let eventCount = 0;
|
|
120
122
|
let drainingNotified = false;
|
|
121
123
|
|
|
@@ -125,8 +127,9 @@ export async function handleStreaming(
|
|
|
125
127
|
break;
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
eventCount++;
|
|
131
|
+
|
|
128
132
|
if (DEBUG_STREAM) {
|
|
129
|
-
eventCount++;
|
|
130
133
|
const preview = event.type === "text_delta" ? `"${event.text.slice(0, 30)}"`
|
|
131
134
|
: event.type === "custom_message" ? `${event.customType}:${event.content.slice(0, 30)}`
|
|
132
135
|
: event.type === "tool_start" || event.type === "tool_end" ? event.toolName
|
|
@@ -139,12 +142,14 @@ export async function handleStreaming(
|
|
|
139
142
|
ensureStream();
|
|
140
143
|
currentPush!(event.text);
|
|
141
144
|
hasTextInCurrentTurn = true;
|
|
145
|
+
hasContentThisTurn = true;
|
|
142
146
|
break;
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
case "tool_start": {
|
|
146
150
|
activeTools.set(event.toolCallId, event.toolName);
|
|
147
151
|
if (!READ_ONLY_TOOLS.has(event.toolName)) usedFileModifyingTools = true;
|
|
152
|
+
hasContentThisTurn = true;
|
|
148
153
|
if (verbose) {
|
|
149
154
|
try { await thread.post(`${toolIcon(event.toolName)} Running \`${event.toolName}\`…`); } catch {}
|
|
150
155
|
}
|
|
@@ -161,10 +166,22 @@ export async function handleStreaming(
|
|
|
161
166
|
await flushCurrentStream();
|
|
162
167
|
hasTextInCurrentTurn = false;
|
|
163
168
|
}
|
|
169
|
+
hasContentThisTurn = true;
|
|
164
170
|
await postWithFallback(thread, event.content);
|
|
165
171
|
break;
|
|
166
172
|
}
|
|
167
173
|
|
|
174
|
+
case "model_error": {
|
|
175
|
+
await flushCurrentStream();
|
|
176
|
+
hasTextInCurrentTurn = false;
|
|
177
|
+
hasContentThisTurn = true;
|
|
178
|
+
modelErrorPosted = true;
|
|
179
|
+
const safeMsg = event.message.split("\n")[0].slice(0, 400);
|
|
180
|
+
console.warn(`[roundhouse] model error: ${safeMsg}`);
|
|
181
|
+
try { await thread.post(`\u26a0\ufe0f Agent error: ${safeMsg}`); } catch {}
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
|
|
168
185
|
case "turn_end": {
|
|
169
186
|
if (hasTextInCurrentTurn) {
|
|
170
187
|
await flushCurrentStream();
|
|
@@ -207,5 +224,12 @@ export async function handleStreaming(
|
|
|
207
224
|
await flushCurrentStream();
|
|
208
225
|
}
|
|
209
226
|
|
|
227
|
+
// Safety net: if the entire turn produced no visible content and no error
|
|
228
|
+
// was already reported, notify the user so they don't stare at "typing" forever.
|
|
229
|
+
if (!hasContentThisTurn && !modelErrorPosted) {
|
|
230
|
+
console.warn(`[roundhouse] agent returned no content this turn (${eventCount} events received)`);
|
|
231
|
+
try { await thread.post("\u26a0\ufe0f Agent returned no response. Check roundhouse logs."); } catch {}
|
|
232
|
+
}
|
|
233
|
+
|
|
210
234
|
return { usedTools: usedFileModifyingTools };
|
|
211
235
|
}
|
|
@@ -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;
|