@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/architecture.md
CHANGED
|
@@ -282,15 +282,13 @@ src/
|
|
|
282
282
|
│
|
|
283
283
|
├── gateway.ts # Gateway class: chat SDK wiring, handleAgentTurn
|
|
284
284
|
│ ├── gateway/
|
|
285
|
-
│ │ ├── commands.ts # 9
|
|
285
|
+
│ │ ├── commands.ts # 9 command handlers (/new, /stop, /status, etc.)
|
|
286
286
|
│ │ ├── streaming.ts # Agent event → Telegram message stream mapper
|
|
287
287
|
│ │ ├── attachments.ts # File save, validation, size limits
|
|
288
|
-
│ │ ├── helpers.ts #
|
|
288
|
+
│ │ ├── helpers.ts # isCommand, resolveAgentThreadId, getSystemResources, toolIcon
|
|
289
289
|
│ │ └── index.ts # Barrel re-export
|
|
290
|
-
│ ├── commands/update.ts # /update handler → bundle provisioning
|
|
291
290
|
│ ├── cron/scheduler.ts # Tick loop, catch-up, job dispatch
|
|
292
291
|
│ ├── memory/ # Session memory hooks (flush, compact, inject)
|
|
293
|
-
│ ├── notify/telegram.ts # Startup/error notifications
|
|
294
292
|
│ └── voice/
|
|
295
293
|
│ ├── stt-service.ts # STT orchestration (provider chain)
|
|
296
294
|
│ └── providers/whisper.ts # Whisper CLI provider
|
|
@@ -309,15 +307,15 @@ src/
|
|
|
309
307
|
│ ├── setup.ts # Setup dispatcher (300 lines): cmdSetup, cmdPair, help
|
|
310
308
|
│ ├── setup/
|
|
311
309
|
│ │ ├── steps.ts # 11 step functions (preflight → postflight)
|
|
312
|
-
│ │ ├── flows.ts # Interactive +
|
|
313
|
-
│ │ ├── runtime.ts #
|
|
310
|
+
│ │ ├── flows.ts # Interactive + non-interactive orchestrators
|
|
311
|
+
│ │ ├── runtime.ts # Agent resolution, StepLog bridge
|
|
314
312
|
│ │ ├── args.ts # Argument parser
|
|
315
313
|
│ │ ├── helpers.ts # Atomic writes, exec wrappers
|
|
316
314
|
│ │ ├── types.ts # SetupOptions, StepLog interface
|
|
315
|
+
│ │ ├── prompts.ts # TTY prompt helpers (text, masked, choice)
|
|
316
|
+
│ │ ├── logger.ts # JSON/text logger for non-interactive diagnostics
|
|
317
|
+
│ │ ├── telegram.ts # Telegram API: validate token, pair, register commands
|
|
317
318
|
│ │ └── 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
319
|
│ ├── qr.ts # QR code generation for pairing links
|
|
322
320
|
│ └── doctor/ # Health checks (8 check modules + runner)
|
|
323
321
|
│
|
|
@@ -338,19 +336,40 @@ src/
|
|
|
338
336
|
│ ├── bootstrap.ts, inject.ts # Session bootstrapping, context injection
|
|
339
337
|
│ ├── state.ts, types.ts # State tracking, interfaces
|
|
340
338
|
│
|
|
341
|
-
├──
|
|
342
|
-
├──
|
|
343
|
-
├──
|
|
344
|
-
|
|
345
|
-
├──
|
|
346
|
-
├──
|
|
339
|
+
├── transports/ # Transport adapter layer
|
|
340
|
+
│ ├── types.ts # TransportAdapter interface + PairingResult
|
|
341
|
+
│ ├── index.ts # Barrel export
|
|
342
|
+
│ └── telegram/ # Telegram implementation
|
|
343
|
+
│ ├── telegram-adapter.ts # TelegramAdapter (implements TransportAdapter)
|
|
344
|
+
│ ├── format.ts # Markdown → Telegram HTML converter
|
|
345
|
+
│ ├── html.ts # HTML streaming + entity utilities
|
|
346
|
+
│ ├── progress.ts # Typing indicator + progress edits
|
|
347
|
+
│ ├── bot-commands.ts # Bot command definitions
|
|
348
|
+
│ ├── pairing.ts # Nonce-based Telegram pairing protocol
|
|
349
|
+
│ └── notify.ts # Send messages to notify chat IDs
|
|
350
|
+
│
|
|
351
|
+
├── provisioning/
|
|
352
|
+
│ └── bundle.ts # Skill/extension bundle provisioning
|
|
353
|
+
│
|
|
347
354
|
└── util.ts # Runtime helpers (crypto, path)
|
|
348
355
|
```
|
|
349
356
|
|
|
357
|
+
**Repo-root directories (outside `src/`):**
|
|
358
|
+
```
|
|
359
|
+
skills/ # Bundled skills (shipped in package)
|
|
360
|
+
├── roundhouse-cron/SKILL.md # Cron job skill for pi
|
|
361
|
+
└── pr-merge-discipline/SKILL.md # PR merge workflow skill
|
|
362
|
+
```
|
|
363
|
+
|
|
350
364
|
**Dependency rules:**
|
|
351
365
|
- No circular dependencies
|
|
352
366
|
- `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
|
|
355
|
-
-
|
|
367
|
+
- `provisioning/bundle.ts` is a leaf (only `node:*` imports)
|
|
368
|
+
- Gateway modules (`gateway/*.ts`) import from `../transports` (via TransportAdapter interface), `../types`, `../config`, `../util`, `../memory/*`
|
|
369
|
+
- Gateway holds a `TransportAdapter` instance — all platform-specific operations go through this interface
|
|
370
|
+
- `transports/telegram/progress.ts` is still imported directly by gateway (deferred from adapter extraction)
|
|
371
|
+
- `gateway/streaming.ts` imports `transports/telegram/html.ts` directly (deferred — streaming is tightly coupled to Telegram HTML wire format)
|
|
372
|
+
- `cron/runner.ts` imports `transports/telegram/notify` directly (deferred — will route through adapter when multi-transport lands)
|
|
373
|
+
- CLI modules never import from `gateway/` (separation of concerns)
|
|
374
|
+
- CLI setup modules (`cli/setup/*.ts`) import from `transports/telegram/` directly (by design — setup is inherently transport-specific)
|
|
356
375
|
- 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.
|
|
3
|
+
"version": "0.5.5",
|
|
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,
|
|
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
|
|
|
@@ -205,6 +205,23 @@ export const createPiAgentAdapter: AgentAdapterFactory = (config) => {
|
|
|
205
205
|
} else {
|
|
206
206
|
console.log(`[pi-agent] no memory extension detected — roundhouse memory will manage`);
|
|
207
207
|
}
|
|
208
|
+
|
|
209
|
+
// Warn about pi extensions that bridge a chat platform directly.
|
|
210
|
+
// They hijack agent_start/message_update/agent_end and short-circuit
|
|
211
|
+
// Roundhouse's streaming pipeline — Telegram shows "typing" forever.
|
|
212
|
+
const conflicting = extNames.filter((n) => /pi-telegram(\b|[\/\\])/i.test(n));
|
|
213
|
+
if (conflicting.length > 0) {
|
|
214
|
+
const lines = [
|
|
215
|
+
"",
|
|
216
|
+
"\u26a0\ufe0f CONFLICT: detected pi extension(s) that bridge a chat platform directly:",
|
|
217
|
+
...conflicting.map((n) => ` - ${n}`),
|
|
218
|
+
" Roundhouse already drives Telegram. Loading a bridge extension inside",
|
|
219
|
+
" the pi session causes lost replies (typing indicator without text).",
|
|
220
|
+
" Remove the extension from ~/.pi/agent/extensions or pi config and restart.",
|
|
221
|
+
"",
|
|
222
|
+
];
|
|
223
|
+
for (const line of lines) console.warn(line);
|
|
224
|
+
}
|
|
208
225
|
}
|
|
209
226
|
|
|
210
227
|
return entry;
|
|
@@ -353,6 +370,13 @@ export const createPiAgentAdapter: AgentAdapterFactory = (config) => {
|
|
|
353
370
|
streamEvent = { type: "tool_end", toolName: event.toolName, toolCallId: event.toolCallId, isError: event.isError };
|
|
354
371
|
} else if (event.type === "turn_end") {
|
|
355
372
|
streamEvent = { type: "turn_end" };
|
|
373
|
+
} else if (event.type === "message_end") {
|
|
374
|
+
// Pi records provider failures (auth, throttling, etc.) on the
|
|
375
|
+
// assistant message instead of throwing — surface them.
|
|
376
|
+
const msg = (event as any).message;
|
|
377
|
+
if (msg?.role === "assistant" && msg.stopReason === "error" && msg.errorMessage) {
|
|
378
|
+
streamEvent = { type: "model_error", message: msg.errorMessage };
|
|
379
|
+
}
|
|
356
380
|
}
|
|
357
381
|
}
|
|
358
382
|
|
|
@@ -552,10 +576,7 @@ export const createPiAgentAdapter: AgentAdapterFactory = (config) => {
|
|
|
552
576
|
};
|
|
553
577
|
},
|
|
554
578
|
|
|
555
|
-
prepareMessage(_threadId: string, message: AgentMessage,
|
|
556
|
-
if (context.platform === "telegram" && message.text) {
|
|
557
|
-
return { ...message, text: message.text + "\n\n[Format your final answer for Telegram: concise, use markdown sparingly, avoid long code blocks.]" };
|
|
558
|
-
}
|
|
579
|
+
prepareMessage(_threadId: string, message: AgentMessage, _context: MessageContext): AgentMessage {
|
|
559
580
|
return message;
|
|
560
581
|
},
|
|
561
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 "
|
|
12
|
+
import { performUpdate } from "./update";
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
CONFIG_PATH,
|
package/src/cli/setup/args.ts
CHANGED
|
@@ -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
|
-
|
|
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 "--
|
|
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
|
-
//
|
|
68
|
-
if (opts.
|
|
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 --
|
|
71
|
-
"Use: TELEGRAM_BOT_TOKEN=... roundhouse setup --telegram --
|
|
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.
|
|
82
|
+
const isInteractiveTelegram = opts.telegram && !opts.nonInteractive && process.stdin.isTTY;
|
|
84
83
|
|
|
85
84
|
// Validate
|
|
86
85
|
if (!opts.botToken && !opts.dryRun && !isInteractiveTelegram) {
|
package/src/cli/setup/flows.ts
CHANGED
|
@@ -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 "
|
|
5
|
-
import { createJsonLogger, type SetupDiagnostics, printDiagnosticError } from "
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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 --
|
|
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 --
|
|
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", "
|
|
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
|
|
2
|
+
* cli/setup/logger.ts — Structured logging for setup.
|
|
3
3
|
*
|
|
4
4
|
* Interactive mode: human-friendly text with step numbers and emoji.
|
|
5
|
-
*
|
|
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,
|
|
117
|
-
if (
|
|
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
|
|
2
|
+
* cli/setup/prompts.ts — Interactive prompts using only Node built-in readline.
|
|
3
3
|
* No external dependencies.
|
|
4
4
|
*/
|
|
5
|
-
import { createInterface
|
|
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
|
+
}
|
package/src/cli/setup/runtime.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type AgentDefinition,
|
|
8
8
|
type AgentSetupContext,
|
|
9
9
|
} from "../../agents/registry";
|
|
10
|
-
import { type SetupLogger } from "
|
|
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) };
|
package/src/cli/setup/steps.ts
CHANGED
|
@@ -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 "
|
|
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...");
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* cli/setup
|
|
2
|
+
* cli/setup/telegram.ts — Telegram API helpers for setup
|
|
3
3
|
*
|
|
4
4
|
* Zero-dependency Telegram Bot API client using global fetch.
|
|
5
5
|
* Token is never logged — redacted in all error messages.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import { BOT_COMMANDS } from "
|
|
8
|
+
import { createPairingNonce } from "../../transports/telegram/pairing";
|
|
9
|
+
import { BOT_COMMANDS } from "../../transports/telegram/bot-commands";
|
|
10
10
|
|
|
11
11
|
// ── Types ────────────────────────────────────────────
|
|
12
12
|
|
|
@@ -90,7 +90,7 @@ export async function pairTelegram(
|
|
|
90
90
|
log: (msg: string) => void = console.log,
|
|
91
91
|
opts?: { nonce?: string; showLink?: boolean },
|
|
92
92
|
): Promise<PairResult | null> {
|
|
93
|
-
const nonce = opts?.nonce ??
|
|
93
|
+
const nonce = opts?.nonce ?? createPairingNonce();
|
|
94
94
|
const normalizedUsers = allowedUsers.map((u) => u.replace(/^@/, "").toLowerCase());
|
|
95
95
|
|
|
96
96
|
// Clear stale updates — advance offset past existing
|
package/src/cli/setup/types.ts
CHANGED
|
@@ -18,17 +18,18 @@ export interface SetupOptions {
|
|
|
18
18
|
systemd: boolean;
|
|
19
19
|
voice: boolean;
|
|
20
20
|
psst: boolean;
|
|
21
|
-
nonInteractive: boolean;
|
|
22
21
|
force: boolean;
|
|
23
22
|
dryRun: boolean;
|
|
24
23
|
/** Telegram-focused setup flow */
|
|
25
24
|
telegram: boolean;
|
|
26
|
-
/**
|
|
27
|
-
|
|
25
|
+
/** Non-interactive mode (no TTY prompts) */
|
|
26
|
+
nonInteractive: boolean;
|
|
28
27
|
/** QR code display mode */
|
|
29
28
|
qr: "auto" | "always" | "never";
|
|
30
29
|
/** Agent type (default: pi) */
|
|
31
30
|
agent: string;
|
|
31
|
+
/** Whether --agent was explicitly passed on CLI */
|
|
32
|
+
_agentExplicit?: boolean;
|
|
32
33
|
/** Set by detection: skip agent package install if already configured */
|
|
33
34
|
_skipAgentInstall?: boolean;
|
|
34
35
|
}
|