@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.
Files changed (35) hide show
  1. package/architecture.md +37 -18
  2. package/package.json +2 -1
  3. package/skills/pr-merge-discipline/SKILL.md +36 -0
  4. package/skills/roundhouse-cron/SKILL.md +136 -0
  5. package/src/agents/kiro/kiro-adapter.ts +1 -4
  6. package/src/agents/pi/pi-adapter.ts +25 -4
  7. package/src/cli/cli.ts +1 -1
  8. package/src/cli/setup/args.ts +8 -9
  9. package/src/cli/setup/flows.ts +47 -14
  10. package/src/cli/{setup-logger.ts → setup/logger.ts} +4 -4
  11. package/src/cli/{setup-prompts.ts → setup/prompts.ts} +23 -2
  12. package/src/cli/setup/runtime.ts +1 -1
  13. package/src/cli/setup/steps.ts +3 -3
  14. package/src/cli/{setup-telegram.ts → setup/telegram.ts} +4 -4
  15. package/src/cli/setup/types.ts +4 -3
  16. package/src/cli/setup.ts +8 -8
  17. package/src/cli/systemd.ts +2 -0
  18. package/src/{commands → cli}/update.ts +1 -1
  19. package/src/cron/runner.ts +2 -1
  20. package/src/gateway/commands.ts +4 -3
  21. package/src/{gateway.ts → gateway/gateway.ts} +63 -97
  22. package/src/gateway/helpers.ts +1 -1
  23. package/src/gateway/index.ts +2 -5
  24. package/src/gateway/streaming.ts +26 -2
  25. package/src/{bundle.ts → provisioning/bundle.ts} +32 -0
  26. package/src/transports/index.ts +6 -0
  27. package/src/{telegram-html.ts → transports/telegram/html.ts} +2 -2
  28. package/src/{pairing.ts → transports/telegram/pairing.ts} +1 -1
  29. package/src/transports/telegram/telegram-adapter.ts +111 -0
  30. package/src/transports/types.ts +71 -0
  31. package/src/types.ts +2 -1
  32. /package/src/{commands.ts → transports/telegram/bot-commands.ts} +0 -0
  33. /package/src/{telegram-format.ts → transports/telegram/format.ts} +0 -0
  34. /package/src/{notify/telegram.ts → transports/telegram/notify.ts} +0 -0
  35. /package/src/{telegram-progress.ts → transports/telegram/progress.ts} +0 -0
@@ -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
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * transports/types.ts — Transport adapter interface
3
+ *
4
+ * Defines the contract for platform-specific transport adapters.
5
+ * The gateway uses this interface to remain transport-agnostic.
6
+ */
7
+
8
+ /** Minimal thread interface (subset of Chat SDK thread) */
9
+ export interface ChatThread {
10
+ id: string;
11
+ post(text: string): Promise<void>;
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ /** Minimal incoming message interface */
16
+ export interface IncomingMessage {
17
+ text?: string;
18
+ author?: { userName?: string; name?: string; userId?: string | number; id?: string };
19
+ chatId?: number;
20
+ raw?: { from?: { id?: number } };
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ /** Result of a successful transport pairing */
25
+ export interface PairingResult {
26
+ /** Thread/channel ID for notifications */
27
+ threadId: string | number;
28
+ /** User ID for allowlist */
29
+ userId: string | number;
30
+ /** Display name */
31
+ username: string;
32
+ }
33
+
34
+ /**
35
+ * TransportAdapter — platform-specific behavior contract.
36
+ *
37
+ * Encapsulates all concerns specific to a messaging platform
38
+ * (Telegram, Slack, Discord, etc.), keeping the gateway transport-agnostic.
39
+ */
40
+ export interface TransportAdapter {
41
+ /** Transport name (e.g. "telegram") */
42
+ readonly name: string;
43
+
44
+ /** Enrich prompt text before sending to agent (e.g. formatting hints) */
45
+ enrichPrompt(text: string): string;
46
+
47
+ /** Post a message using platform-native formatting */
48
+ postMessage(thread: ChatThread, text: string): Promise<void>;
49
+
50
+ /** Register bot commands with the platform */
51
+ registerCommands(token: string): Promise<void>;
52
+
53
+ /** Check if a thread belongs to this transport */
54
+ ownsThread(thread: ChatThread): boolean;
55
+
56
+ /** Send notifications to configured recipients */
57
+ notify(chatIds: number[], text: string): Promise<void>;
58
+
59
+ /**
60
+ * Check if a pairing flow is pending.
61
+ * Gateway uses this to decide whether to attempt pairing on incoming messages.
62
+ */
63
+ isPairingPending(): Promise<boolean>;
64
+
65
+ /**
66
+ * Try to handle an incoming message as a pairing attempt.
67
+ * Returns PairingResult on success, null if not a pairing message.
68
+ * Transport manages its own state (nonce files, OAuth tokens, etc.)
69
+ */
70
+ handlePairing(thread: ChatThread, message: IncomingMessage): Promise<PairingResult | null>;
71
+ }
package/src/types.ts CHANGED
@@ -43,7 +43,8 @@ export type AgentStreamEvent =
43
43
  | { type: "draining" }
44
44
  | { type: "drain_complete" }
45
45
  | { type: "agent_end" }
46
- | { type: "custom_message"; customType: string; content: string };
46
+ | { type: "custom_message"; customType: string; content: string }
47
+ | { type: "model_error"; message: string };
47
48
 
48
49
  // ── AdapterInfo ──────────────────────────────────────
49
50