@inceptionstack/roundhouse 0.5.4 → 0.5.7

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 (42) hide show
  1. package/README.md +1 -3
  2. package/architecture.md +37 -19
  3. package/package.json +2 -1
  4. package/skills/pr-merge-discipline/SKILL.md +36 -0
  5. package/skills/roundhouse-cron/SKILL.md +136 -0
  6. package/src/agents/kiro/kiro-adapter.ts +1 -4
  7. package/src/agents/pi/pi-adapter.ts +1 -4
  8. package/src/cli/cli.ts +6 -1
  9. package/src/cli/doctor/checks/system.ts +1 -1
  10. package/src/cli/setup/args.ts +8 -9
  11. package/src/cli/setup/flows.ts +47 -14
  12. package/src/cli/{setup-logger.ts → setup/logger.ts} +4 -4
  13. package/src/cli/{setup-prompts.ts → setup/prompts.ts} +23 -2
  14. package/src/cli/setup/runtime.ts +1 -1
  15. package/src/cli/setup/steps.ts +5 -5
  16. package/src/cli/{setup-telegram.ts → setup/telegram.ts} +4 -4
  17. package/src/cli/setup/types.ts +4 -3
  18. package/src/cli/setup.ts +8 -8
  19. package/src/cli/systemd.ts +2 -0
  20. package/src/cli/update.ts +111 -0
  21. package/src/cron/runner.ts +2 -1
  22. package/src/gateway/commands.ts +29 -4
  23. package/src/{gateway.ts → gateway/gateway.ts} +126 -100
  24. package/src/gateway/helpers.ts +1 -1
  25. package/src/gateway/index.ts +2 -5
  26. package/src/gateway/streaming.ts +1 -1
  27. package/src/gateway/tools-inject.ts +45 -0
  28. package/src/gateway/tools.md +54 -0
  29. package/src/{bundle.ts → provisioning/bundle.ts} +32 -0
  30. package/src/transports/index.ts +6 -0
  31. package/src/{telegram-html.ts → transports/telegram/html.ts} +2 -2
  32. package/src/{pairing.ts → transports/telegram/pairing.ts} +1 -1
  33. package/src/transports/telegram/telegram-adapter.ts +111 -0
  34. package/src/transports/types.ts +71 -0
  35. package/src/voice/providers/whisper.ts +37 -94
  36. package/src/voice/stt-service.ts +35 -17
  37. package/src/voice/types.ts +1 -3
  38. package/src/commands/update.ts +0 -69
  39. /package/src/{commands.ts → transports/telegram/bot-commands.ts} +0 -0
  40. /package/src/{telegram-format.ts → transports/telegram/format.ts} +0 -0
  41. /package/src/{notify/telegram.ts → transports/telegram/notify.ts} +0 -0
  42. /package/src/{telegram-progress.ts → transports/telegram/progress.ts} +0 -0
package/README.md CHANGED
@@ -188,7 +188,6 @@ Without a config file, defaults are used with env vars (`TELEGRAM_BOT_TOKEN`, `B
188
188
  | `chat.notifyChatIds` | Telegram chat IDs to notify on startup (env: `NOTIFY_CHAT_IDS`) |
189
189
  | `chat.adapters.telegram` | `{ "mode": "polling" \| "webhook" \| "auto" }` |
190
190
  | `voice.stt.enabled` | Enable automatic voice transcription (default: off unless configured) |
191
- | `voice.stt.autoInstall` | Auto-install whisper via pip3 if missing (default: false) |
192
191
  | `voice.stt.chain` | STT provider chain, e.g. `["whisper"]` |
193
192
  | `voice.stt.providers.whisper` | `{ "model": "small", "timeoutMs": 30000 }` |
194
193
 
@@ -307,7 +306,7 @@ Roundhouse can automatically transcribe voice messages using [OpenAI Whisper](ht
307
306
  pip install openai-whisper
308
307
  ```
309
308
 
310
- Or set `autoInstall: true` in config to have roundhouse install whisper automatically on first voice message.
309
+ If whisper/ffmpeg aren't installed when a voice message arrives, roundhouse automatically injects a prompt into the agent's turn asking it to install the missing dependencies. The user is notified that setup is in progress.
311
310
 
312
311
  **Enable in config:**
313
312
  ```json
@@ -316,7 +315,6 @@ Or set `autoInstall: true` in config to have roundhouse install whisper automati
316
315
  "stt": {
317
316
  "enabled": true,
318
317
  "mode": "on",
319
- "autoInstall": true,
320
318
  "chain": ["whisper"],
321
319
  "autoTranscribe": {
322
320
  "voiceMessages": true,
package/architecture.md CHANGED
@@ -192,7 +192,6 @@ gateway.config.json
192
192
  └── stt
193
193
  ├── enabled: true
194
194
  ├── mode: "on" | "off"
195
- ├── autoInstall: false # auto-install whisper via pip3
196
195
  ├── chain: ["whisper"] # Provider chain (try in order)
197
196
  ├── autoTranscribe
198
197
  │ ├── voiceMessages: true
@@ -282,15 +281,13 @@ src/
282
281
 
283
282
  ├── gateway.ts # Gateway class: chat SDK wiring, handleAgentTurn
284
283
  │ ├── gateway/
285
- │ │ ├── commands.ts # 9 Telegram command handlers (/new, /stop, /status, etc.)
284
+ │ │ ├── commands.ts # 9 command handlers (/new, /stop, /status, etc.)
286
285
  │ │ ├── streaming.ts # Agent event → Telegram message stream mapper
287
286
  │ │ ├── attachments.ts # File save, validation, size limits
288
- │ │ ├── helpers.ts # Pure utils: splitMessage, isAllowed, threadIdToDir
287
+ │ │ ├── helpers.ts # isCommand, resolveAgentThreadId, getSystemResources, toolIcon
289
288
  │ │ └── index.ts # Barrel re-export
290
- │ ├── commands/update.ts # /update handler → bundle provisioning
291
289
  │ ├── cron/scheduler.ts # Tick loop, catch-up, job dispatch
292
290
  │ ├── memory/ # Session memory hooks (flush, compact, inject)
293
- │ ├── notify/telegram.ts # Startup/error notifications
294
291
  │ └── voice/
295
292
  │ ├── stt-service.ts # STT orchestration (provider chain)
296
293
  │ └── providers/whisper.ts # Whisper CLI provider
@@ -309,15 +306,15 @@ src/
309
306
  │ ├── setup.ts # Setup dispatcher (300 lines): cmdSetup, cmdPair, help
310
307
  │ ├── setup/
311
308
  │ │ ├── steps.ts # 11 step functions (preflight → postflight)
312
- │ │ ├── flows.ts # Interactive + headless orchestrators
313
- │ │ ├── runtime.ts # Logger factories, agent resolution
309
+ │ │ ├── flows.ts # Interactive + non-interactive orchestrators
310
+ │ │ ├── runtime.ts # Agent resolution, StepLog bridge
314
311
  │ │ ├── args.ts # Argument parser
315
312
  │ │ ├── helpers.ts # Atomic writes, exec wrappers
316
313
  │ │ ├── types.ts # SetupOptions, StepLog interface
314
+ │ │ ├── prompts.ts # TTY prompt helpers (text, masked, choice)
315
+ │ │ ├── logger.ts # JSON/text logger for non-interactive diagnostics
316
+ │ │ ├── telegram.ts # Telegram API: validate token, pair, register commands
317
317
  │ │ └── index.ts # Barrel export
318
- │ ├── setup-telegram.ts # Telegram API: validate token, pair, register commands
319
- │ ├── setup-prompts.ts # TTY prompt helpers
320
- │ ├── setup-logger.ts # JSON/text logger for headless diagnostics
321
318
  │ ├── qr.ts # QR code generation for pairing links
322
319
  │ └── doctor/ # Health checks (8 check modules + runner)
323
320
 
@@ -338,19 +335,40 @@ src/
338
335
  │ ├── bootstrap.ts, inject.ts # Session bootstrapping, context injection
339
336
  │ ├── state.ts, types.ts # State tracking, interfaces
340
337
 
341
- ├── telegram-format.ts # Markdown Telegram HTML converter
342
- ├── telegram-html.ts # HTML entity utilities
343
- ├── telegram-progress.ts # Typing indicator + progress edits
344
- ├── bundle.ts # Skill/extension bundle provisioning
345
- ├── pairing.ts # Nonce-based Telegram pairing protocol
346
- ├── commands.ts # Bot command definitions
338
+ ├── transports/ # Transport adapter layer
339
+ ├── types.ts # TransportAdapter interface + PairingResult
340
+ ├── index.ts # Barrel export
341
+ │ └── telegram/ # Telegram implementation
342
+ ├── telegram-adapter.ts # TelegramAdapter (implements TransportAdapter)
343
+ ├── format.ts # Markdown Telegram HTML converter
344
+ │ ├── html.ts # HTML streaming + entity utilities
345
+ │ ├── progress.ts # Typing indicator + progress edits
346
+ │ ├── bot-commands.ts # Bot command definitions
347
+ │ ├── pairing.ts # Nonce-based Telegram pairing protocol
348
+ │ └── notify.ts # Send messages to notify chat IDs
349
+
350
+ ├── provisioning/
351
+ │ └── bundle.ts # Skill/extension bundle provisioning
352
+
347
353
  └── util.ts # Runtime helpers (crypto, path)
348
354
  ```
349
355
 
356
+ **Repo-root directories (outside `src/`):**
357
+ ```
358
+ skills/ # Bundled skills (shipped in package)
359
+ ├── roundhouse-cron/SKILL.md # Cron job skill for pi
360
+ └── pr-merge-discipline/SKILL.md # PR merge workflow skill
361
+ ```
362
+
350
363
  **Dependency rules:**
351
364
  - No circular dependencies
352
365
  - `types.ts`, `config.ts`, `util.ts` are pure leaf modules
353
- - `bundle.ts` is a leaf (only `node:*` imports)
354
- - Gateway modules (`gateway/*.ts`) import from `../types`, `../config`, `../util`, `../memory/*`, `../telegram-*`
355
- - CLI modules never import from `gateway.ts` (separation of concerns)
366
+ - `provisioning/bundle.ts` is a leaf (only `node:*` imports)
367
+ - Gateway modules (`gateway/*.ts`) import from `../transports` (via TransportAdapter interface), `../types`, `../config`, `../util`, `../memory/*`
368
+ - Gateway holds a `TransportAdapter` instance all platform-specific operations go through this interface
369
+ - `transports/telegram/progress.ts` is still imported directly by gateway (deferred from adapter extraction)
370
+ - `gateway/streaming.ts` imports `transports/telegram/html.ts` directly (deferred — streaming is tightly coupled to Telegram HTML wire format)
371
+ - `cron/runner.ts` imports `transports/telegram/notify` directly (deferred — will route through adapter when multi-transport lands)
372
+ - CLI modules never import from `gateway/` (separation of concerns)
373
+ - CLI setup modules (`cli/setup/*.ts`) import from `transports/telegram/` directly (by design — setup is inherently transport-specific)
356
374
  - Agent adapters depend on their SDK + `../../types`, `../../config`, `../../util`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inceptionstack/roundhouse",
3
- "version": "0.5.4",
3
+ "version": "0.5.7",
4
4
  "type": "module",
5
5
  "description": "Multi-platform chat gateway that routes messages through a configured AI agent",
6
6
  "license": "MIT",
@@ -31,6 +31,7 @@
31
31
  "files": [
32
32
  "src/",
33
33
  "bin/",
34
+ "skills/",
34
35
  "LICENSE",
35
36
  "README.md",
36
37
  "architecture.md",
@@ -0,0 +1,36 @@
1
+ # PR Merge Discipline
2
+
3
+ Never merge a PR without checking for review comments first.
4
+
5
+ ## When to Use
6
+
7
+ Activate this skill when:
8
+ - About to merge a PR
9
+ - Using `gh pr merge`
10
+ - PR has been approved or CI passed
11
+
12
+ ## Rules
13
+
14
+ 1. **Always check PR comments before merging**: `gh pr view <number> --comments`
15
+ 2. **If there are unresolved comments**, read and address them first
16
+ 3. **If there are review requests**, wait for or acknowledge them
17
+ 4. **Only merge when**: CI green + no unresolved comments + no pending reviews
18
+ 5. **After merge**: pull main and verify clean state
19
+
20
+ ## Workflow
21
+
22
+ ```bash
23
+ # Before any merge:
24
+ gh pr view <number> --comments
25
+ gh pr checks <number>
26
+
27
+ # Only then:
28
+ gh pr merge <number> --squash --admin
29
+ ```
30
+
31
+ ## Anti-Patterns
32
+
33
+ - ❌ Merging immediately after CI passes without checking comments
34
+ - ❌ Using `sleep && gh pr merge` without a comment check in between
35
+ - ❌ Force-merging over unresolved review feedback
36
+ - ❌ Assuming "no issues found" from automated review means no human comments exist
@@ -0,0 +1,136 @@
1
+ # Roundhouse Cron Jobs
2
+
3
+ Schedule tasks, add cron jobs, trigger actions at specific times or intervals using the `roundhouse cron` CLI.
4
+
5
+ ## When to Use
6
+
7
+ Activate this skill when the user asks to:
8
+ - Add a scheduled job or cron job
9
+ - Run something every X minutes/hours/days
10
+ - Trigger something at a specific time
11
+ - List, edit, pause, resume, or delete scheduled jobs
12
+ - Check job run history
13
+
14
+ ## CLI Reference
15
+
16
+ ### Add a job
17
+
18
+ ```bash
19
+ roundhouse cron add <id> --prompt "..." --cron "0 8 * * *"
20
+ roundhouse cron add <id> --prompt "..." --every "6h"
21
+ roundhouse cron add <id> --prompt "..." --at "30m"
22
+ ```
23
+
24
+ **Required flags:**
25
+ - `--prompt "..."` — The prompt sent to the agent when the job fires
26
+ - One schedule type (pick one):
27
+ - `--cron "0 8 * * *"` — Standard cron expression
28
+ - `--every "6h"` — Interval (e.g., `30m`, `2h`, `1d`)
29
+ - `--at "..."` — One-shot (e.g., `30m` from now, or ISO date `2026-05-10T14:00:00Z`)
30
+
31
+ **Optional flags:**
32
+ - `--tz "Asia/Jerusalem"` — Timezone (default: system timezone)
33
+ - `--telegram "123456,789012"` — Notify these Telegram chat IDs
34
+ - `--notify-on "always|success|failure"` — When to send notifications
35
+ - `--var "key=value,key2=value2"` — Template variables for the prompt
36
+ - `--timeout "30m"` — Max execution time
37
+ - `--description "..."` — Human-readable description
38
+ - `--replace` — Overwrite existing job with same ID
39
+ - `--json` — Output job config as JSON
40
+
41
+ ### List jobs
42
+
43
+ ```bash
44
+ roundhouse cron list
45
+ roundhouse cron list --json
46
+ ```
47
+
48
+ ### Show job details
49
+
50
+ ```bash
51
+ roundhouse cron show <id>
52
+ roundhouse cron show <id> --json
53
+ ```
54
+
55
+ ### Trigger a job manually
56
+
57
+ ```bash
58
+ roundhouse cron trigger <id>
59
+ ```
60
+
61
+ ### View run history
62
+
63
+ ```bash
64
+ roundhouse cron runs <id>
65
+ roundhouse cron runs <id> --limit 20
66
+ ```
67
+
68
+ ### Edit a job
69
+
70
+ ```bash
71
+ roundhouse cron edit <id> --prompt "new prompt"
72
+ roundhouse cron edit <id> --every "12h"
73
+ roundhouse cron edit <id> --cron "0 */4 * * *" --tz "UTC"
74
+ ```
75
+
76
+ ### Pause / Resume
77
+
78
+ ```bash
79
+ roundhouse cron pause <id>
80
+ roundhouse cron resume <id>
81
+ ```
82
+
83
+ ### Delete a job
84
+
85
+ ```bash
86
+ roundhouse cron delete <id>
87
+ ```
88
+
89
+ ## Rules
90
+
91
+ 1. **Always use `roundhouse cron` CLI** — do not edit cron files directly
92
+ 2. **Job IDs** must be lowercase alphanumeric with hyphens (e.g., `daily-report`, `check-ssl`)
93
+ 3. **Prompts** are sent to the agent as-is — write them as clear instructions
94
+ 4. **Template variables** use `{{var}}` syntax in prompts (e.g., `--prompt "Check {{url}}" --var "url=https://example.com"`)
95
+ 5. **One-shot jobs** (`--at`) run once and stay in history — delete them after if not needed
96
+ 6. **Built-in jobs** (prefixed `_`) cannot be edited or deleted
97
+
98
+ ## Examples
99
+
100
+ ### Daily morning briefing at 8am
101
+ ```bash
102
+ roundhouse cron add morning-brief \
103
+ --prompt "Give me a summary of overnight alerts, pending PRs, and today's calendar" \
104
+ --cron "0 8 * * *" \
105
+ --tz "America/New_York" \
106
+ --telegram "123456" \
107
+ --description "Morning briefing"
108
+ ```
109
+
110
+ ### Check SSL expiry every 12 hours
111
+ ```bash
112
+ roundhouse cron add check-ssl \
113
+ --prompt "Check SSL certificate expiry for {{domain}} and alert if < 14 days" \
114
+ --every "12h" \
115
+ --var "domain=loki.run" \
116
+ --telegram "123456" \
117
+ --notify-on "failure"
118
+ ```
119
+
120
+ ### Run a one-shot reminder in 30 minutes
121
+ ```bash
122
+ roundhouse cron add remind-standup \
123
+ --prompt "Reminder: standup starts now!" \
124
+ --at "30m" \
125
+ --telegram "123456"
126
+ ```
127
+
128
+ ### Weekly dependency audit
129
+ ```bash
130
+ roundhouse cron add weekly-deps \
131
+ --prompt "Run npm audit on all repos in ~/repos/ and report any high/critical vulnerabilities" \
132
+ --cron "0 9 * * 1" \
133
+ --tz "UTC" \
134
+ --timeout "10m" \
135
+ --description "Monday dependency audit"
136
+ ```
@@ -180,10 +180,7 @@ class KiroAdapter extends BaseAdapter {
180
180
  };
181
181
  }
182
182
 
183
- prepareMessage(_threadId: string, message: AgentMessage, context: MessageContext): AgentMessage {
184
- if (context.platform === "telegram" && message.text) {
185
- return { ...message, text: message.text + "\n\n[Format your final answer for Telegram: concise, use markdown sparingly, avoid long code blocks.]" };
186
- }
183
+ prepareMessage(_threadId: string, message: AgentMessage, _context: MessageContext): AgentMessage {
187
184
  return message;
188
185
  }
189
186
 
@@ -576,10 +576,7 @@ export const createPiAgentAdapter: AgentAdapterFactory = (config) => {
576
576
  };
577
577
  },
578
578
 
579
- prepareMessage(_threadId: string, message: AgentMessage, context: MessageContext): AgentMessage {
580
- if (context.platform === "telegram" && message.text) {
581
- return { ...message, text: message.text + "\n\n[Format your final answer for Telegram: concise, use markdown sparingly, avoid long code blocks.]" };
582
- }
579
+ prepareMessage(_threadId: string, message: AgentMessage, _context: MessageContext): AgentMessage {
583
580
  return message;
584
581
  },
585
582
  };
package/src/cli/cli.ts CHANGED
@@ -9,7 +9,7 @@ import { readFile } from "node:fs/promises";
9
9
  import { readdirSync, statSync } from "node:fs";
10
10
  import { execSync, execFileSync, spawn } from "node:child_process";
11
11
  import { fileURLToPath } from "node:url";
12
- import { performUpdate } from "../commands/update";
12
+ import { performUpdate } from "./update";
13
13
 
14
14
  import {
15
15
  CONFIG_PATH,
@@ -155,6 +155,11 @@ async function cmdUpdate() {
155
155
  return;
156
156
  }
157
157
 
158
+ if (result.action === "error") {
159
+ console.error(`[roundhouse] Update failed: ${result.error}`);
160
+ process.exit(1);
161
+ }
162
+
158
163
  console.log(`[roundhouse] Updated to v${result.latestVersion}`);
159
164
 
160
165
  const svc = getServiceManager();
@@ -34,7 +34,7 @@ export const systemChecks: DoctorCheck[] = [
34
34
  return {
35
35
  id: "pip3", category: "system", name: "pip3", summary: ver ? ver.split(" ")[1] ?? ver : "not found",
36
36
  status: ver ? "pass" : "warn",
37
- details: !ver ? ["Needed for whisper STT auto-install"] : undefined,
37
+ details: !ver ? ["Used by agent to install whisper for STT"] : undefined,
38
38
  };
39
39
  }),
40
40
 
@@ -18,11 +18,10 @@ export function parseSetupArgs(argv: string[]): SetupOptions {
18
18
  systemd: platform() === "linux",
19
19
  voice: platform() === "linux", // Default off on macOS (whisper install is heavy)
20
20
  psst: false,
21
- nonInteractive: false,
22
21
  force: false,
23
22
  dryRun: false,
24
23
  telegram: false,
25
- headless: false,
24
+ nonInteractive: false,
26
25
  qr: "auto",
27
26
  agent: "pi",
28
27
  };
@@ -46,9 +45,9 @@ export function parseSetupArgs(argv: string[]): SetupOptions {
46
45
  case "--no-voice": opts.voice = false; break;
47
46
  case "--with-psst": opts.psst = true; break;
48
47
  case "--non-interactive": opts.nonInteractive = true; break;
48
+ case "--headless": opts.nonInteractive = true; break; // alias
49
49
  case "--telegram": opts.telegram = true; break;
50
- case "--headless": opts.headless = true; opts.nonInteractive = true; break;
51
- case "--agent": opts.agent = next().toLowerCase(); break;
50
+ case "--agent": opts.agent = next().toLowerCase(); opts._agentExplicit = true; break;
52
51
  case "--qr": opts.qr = "always"; break;
53
52
  case "--no-qr": opts.qr = "never"; break;
54
53
  case "--force": opts.force = true; break;
@@ -64,11 +63,11 @@ export function parseSetupArgs(argv: string[]): SetupOptions {
64
63
  opts.botToken = process.env.TELEGRAM_BOT_TOKEN ?? "";
65
64
  }
66
65
 
67
- // Headless: reject --bot-token (argv visible in process listings)
68
- if (opts.headless && argv.some((a) => a === "--bot-token")) {
66
+ // Non-interactive: warn about --bot-token (argv visible in process listings)
67
+ if (opts.nonInteractive && argv.some((a) => a === "--bot-token")) {
69
68
  throw new Error(
70
- "--bot-token is not accepted in --headless mode (argv visible in process listings).\n" +
71
- "Use: TELEGRAM_BOT_TOKEN=... roundhouse setup --telegram --headless --user USERNAME",
69
+ "--bot-token is not accepted in --non-interactive mode (argv visible in process listings).\n" +
70
+ "Use: TELEGRAM_BOT_TOKEN=... roundhouse setup --telegram --non-interactive --user USERNAME",
72
71
  );
73
72
  }
74
73
 
@@ -80,7 +79,7 @@ export function parseSetupArgs(argv: string[]): SetupOptions {
80
79
  }
81
80
 
82
81
  // Interactive --telegram defers token/user prompting to the wizard
83
- const isInteractiveTelegram = opts.telegram && !opts.headless && !opts.nonInteractive && process.stdin.isTTY;
82
+ const isInteractiveTelegram = opts.telegram && !opts.nonInteractive && process.stdin.isTTY;
84
83
 
85
84
  // Validate
86
85
  if (!opts.botToken && !opts.dryRun && !isInteractiveTelegram) {
@@ -1,8 +1,8 @@
1
1
  import { platform } from "node:os";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { type SetupOptions } from "./types";
4
- import { promptText, promptMasked } from "../setup-prompts";
5
- import { createJsonLogger, type SetupDiagnostics, printDiagnosticError } from "../setup-logger";
4
+ import { promptText, promptMasked, promptChoice } from "./prompts";
5
+ import { createJsonLogger, type SetupDiagnostics, printDiagnosticError } from "./logger";
6
6
  import { printQr } from "../qr";
7
7
  import {
8
8
  createPairingNonce,
@@ -10,10 +10,11 @@ import {
10
10
  readPendingPairing,
11
11
  writePendingPairing,
12
12
  type PendingPairing,
13
- } from "../../pairing";
13
+ } from "../../transports/telegram/pairing";
14
14
  import { detectEnvironment, formatDetectionResults } from "../detect";
15
+ import { listAvailableAgentTypes } from "../../agents/registry";
15
16
  import { fileExists, ROUNDHOUSE_DIR, CONFIG_PATH, ENV_FILE_PATH as ENV_PATH } from "../../config";
16
- import { pairTelegram } from "../setup-telegram";
17
+ import { pairTelegram } from "./telegram";
17
18
  import {
18
19
  stepPreflight,
19
20
  stepValidateToken,
@@ -30,7 +31,7 @@ import { resolveAgentForSetup, textLog, textStepLog, createStepLog } from "./run
30
31
 
31
32
  export async function runInteractiveTelegramSetup(opts: SetupOptions): Promise<void> {
32
33
  const logger = textStepLog;
33
- const agent = resolveAgentForSetup(opts, logger);
34
+ let agent = resolveAgentForSetup(opts, logger);
34
35
  textLog("\n🔧 Roundhouse Telegram Setup");
35
36
  textLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━");
36
37
 
@@ -44,14 +45,37 @@ export async function runInteractiveTelegramSetup(opts: SetupOptions): Promise<v
44
45
  for (const line of formatDetectionResults(env)) {
45
46
  logger.ok(line);
46
47
  }
47
- if (!opts.force) {
48
- const selected = env.agents.find(a => a.type === opts.agent);
49
- if (selected?.configured) {
50
- opts._skipAgentInstall = true;
51
- }
48
+ }
49
+
50
+ // Agent chooser: only prompt if kiro is available and --agent not explicit
51
+ if (!opts._agentExplicit && !opts.nonInteractive) {
52
+ const availableTypes = listAvailableAgentTypes();
53
+ const kiroDetected = env.agents.some(a => a.type === "kiro" && availableTypes.includes(a.type));
54
+ if (kiroDetected && availableTypes.length > 1) {
55
+ const choices = availableTypes.map((t, i) => ({
56
+ label: t,
57
+ hint: env.agents.find(a => a.type === t)?.configured ? "configured" : i === 0 ? "default" : undefined,
58
+ }));
59
+ textLog("");
60
+ textLog(" Kiro CLI detected. Choose agent backend:");
61
+ const idx = await promptChoice(choices, { defaultIndex: 0 });
62
+ opts.agent = availableTypes[idx];
63
+ logger.ok(`Using ${opts.agent}`);
64
+ }
65
+ // else: no kiro detected, silently use default (pi)
66
+ }
67
+
68
+ // Determine if agent install can be skipped (based on final opts.agent)
69
+ if (!opts.force) {
70
+ const selected = env.agents.find(a => a.type === opts.agent);
71
+ if (selected?.configured) {
72
+ opts._skipAgentInstall = true;
52
73
  }
53
74
  }
54
75
 
76
+ // Re-resolve agent definition if chooser changed it
77
+ agent = resolveAgentForSetup(opts, logger);
78
+
55
79
  if (!opts.botToken) {
56
80
  textLog("");
57
81
  printBotFatherGuide();
@@ -127,18 +151,18 @@ export async function runInteractiveTelegramSetup(opts: SetupOptions): Promise<v
127
151
  }
128
152
  }
129
153
 
130
- export async function runHeadlessTelegramSetup(opts: SetupOptions): Promise<void> {
154
+ export async function runNonInteractiveTelegramSetup(opts: SetupOptions): Promise<void> {
131
155
  const logger = createJsonLogger();
132
156
  const stepLogger = createStepLog(logger);
133
157
  const agent = resolveAgentForSetup(opts, stepLogger);
134
158
 
135
159
  try {
136
160
  if (!opts.botToken) {
137
- logger.error("validation.failed", "TELEGRAM_BOT_TOKEN env var required for --headless");
161
+ logger.error("validation.failed", "TELEGRAM_BOT_TOKEN env var required for --non-interactive");
138
162
  process.exit(2);
139
163
  }
140
164
  if (opts.users.length === 0) {
141
- logger.error("validation.failed", "--user is required for --headless");
165
+ logger.error("validation.failed", "--user is required for --non-interactive");
142
166
  process.exit(2);
143
167
  }
144
168
 
@@ -146,6 +170,15 @@ export async function runHeadlessTelegramSetup(opts: SetupOptions): Promise<void
146
170
  await stepPreflight(stepLogger, opts, agent);
147
171
  logger.ok("Preflight passed");
148
172
 
173
+ // Detect existing agent to skip redundant install
174
+ const env = detectEnvironment();
175
+ if (!opts.force) {
176
+ const selected = env.agents.find(a => a.type === opts.agent);
177
+ if (selected?.configured) {
178
+ opts._skipAgentInstall = true;
179
+ }
180
+ }
181
+
149
182
  logger.step(2, 9, "telegram.validate", "Validating Telegram bot token");
150
183
  const botInfo = await stepValidateToken(stepLogger, opts);
151
184
  logger.ok(`Bot: @${botInfo.username} (id: ${botInfo.id})`);
@@ -225,7 +258,7 @@ export async function runHeadlessTelegramSetup(opts: SetupOptions): Promise<void
225
258
  }
226
259
  }
227
260
 
228
- logger.info("setup.complete", "Headless setup complete", {
261
+ logger.info("setup.complete", "Non-interactive setup complete", {
229
262
  botUsername: botInfo.username,
230
263
  pairingLink,
231
264
  pairingStatus: "pending",
@@ -1,8 +1,8 @@
1
1
  /**
2
- * cli/setup-logger.ts — Structured logging for setup.
2
+ * cli/setup/logger.ts — Structured logging for setup.
3
3
  *
4
4
  * Interactive mode: human-friendly text with step numbers and emoji.
5
- * Headless mode: JSON lines for SSM/cloud-init/Docker log parsing.
5
+ * Non-interactive mode: JSON lines for SSM/cloud-init/Docker log parsing.
6
6
  */
7
7
 
8
8
  export interface SetupLogger {
@@ -113,8 +113,8 @@ export interface SetupDiagnostics {
113
113
  error: { name: string; message: string; stack?: string };
114
114
  }
115
115
 
116
- export function printDiagnosticError(diag: SetupDiagnostics, headless: boolean): void {
117
- if (headless) {
116
+ export function printDiagnosticError(diag: SetupDiagnostics, nonInteractive: boolean): void {
117
+ if (nonInteractive) {
118
118
  console.error(JSON.stringify({
119
119
  ts: ts(),
120
120
  level: "error",
@@ -1,8 +1,8 @@
1
1
  /**
2
- * cli/setup-prompts.ts — Interactive prompts using only Node built-in readline.
2
+ * cli/setup/prompts.ts — Interactive prompts using only Node built-in readline.
3
3
  * No external dependencies.
4
4
  */
5
- import { createInterface, Interface } from "node:readline";
5
+ import { createInterface } from "node:readline";
6
6
 
7
7
  /**
8
8
  * Prompt the user for text input with optional default.
@@ -76,3 +76,24 @@ export async function promptConfirm(
76
76
  if (!answer) return !!options?.defaultYes;
77
77
  return answer.toLowerCase().startsWith("y");
78
78
  }
79
+
80
+ /**
81
+ * Prompt user to pick from a numbered list of choices.
82
+ * Returns the index of the selected item (0-based).
83
+ */
84
+ export async function promptChoice(
85
+ items: { label: string; hint?: string }[],
86
+ options?: { defaultIndex?: number },
87
+ ): Promise<number> {
88
+ if (items.length === 0) throw new Error("promptChoice: items must not be empty");
89
+ const def = Math.min(Math.max(0, options?.defaultIndex ?? 0), items.length - 1);
90
+ for (let i = 0; i < items.length; i++) {
91
+ const marker = i === def ? "*" : " ";
92
+ const hint = items[i].hint ? ` (${items[i].hint})` : "";
93
+ console.log(` ${marker} ${i + 1}. ${items[i].label}${hint}`);
94
+ }
95
+ const answer = await promptText(` Select`, { defaultValue: String(def + 1) });
96
+ const num = parseInt(answer || String(def + 1), 10);
97
+ if (isNaN(num) || num < 1 || num > items.length) return def;
98
+ return num - 1;
99
+ }
@@ -7,7 +7,7 @@ import {
7
7
  type AgentDefinition,
8
8
  type AgentSetupContext,
9
9
  } from "../../agents/registry";
10
- import { type SetupLogger } from "../setup-logger";
10
+ import { type SetupLogger } from "./logger";
11
11
 
12
12
  export function resolveAgentForSetup(opts: SetupOptions, logger: StepLog): AgentDefinition {
13
13
  const agent = { ...getAgentDefinition(opts.agent) };
@@ -3,8 +3,8 @@ import { resolve } from "node:path";
3
3
  import { readFile, writeFile, mkdir, unlink, realpath, stat } from "node:fs/promises";
4
4
  import { execFileSync } from "node:child_process";
5
5
  import { randomBytes } from "node:crypto";
6
- import { BOT_COMMANDS } from "../../commands";
7
- import { provisionBundle, type ProvisionLog } from "../../bundle";
6
+ import { BOT_COMMANDS } from "../../transports/telegram/bot-commands";
7
+ import { provisionBundle, type ProvisionLog } from "../../provisioning/bundle";
8
8
  import {
9
9
  ROUNDHOUSE_DIR,
10
10
  CONFIG_PATH,
@@ -33,7 +33,7 @@ import {
33
33
  sendMessage,
34
34
  type BotInfo,
35
35
  type PairResult,
36
- } from "../setup-telegram";
36
+ } from "./telegram";
37
37
 
38
38
  export async function stepPreflight(logger: StepLog, opts: SetupOptions, agent: AgentDefinition): Promise<void> {
39
39
  logger.step("①", "Preflight checks...");
@@ -603,8 +603,8 @@ export async function stepPostflight(logger: StepLog): Promise<void> {
603
603
 
604
604
  if (platform() === "linux" || process.env.ROUNDHOUSE_VOICE === "1") {
605
605
  if (!whichSync("whisper")) {
606
- logger.warn("whisper not found — STT will auto-install on first voice message");
607
- logger.log(" Pre-install: pip3 install openai-whisper");
606
+ logger.warn("whisper not found — agent will be prompted to install on first voice message");
607
+ logger.log(" Or pre-install manually: pip3 install --user openai-whisper");
608
608
  } else {
609
609
  logger.ok("whisper available");
610
610
  }