@inceptionstack/roundhouse 0.5.13 → 0.5.14

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/CHANGELOG.md ADDED
@@ -0,0 +1,235 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@inceptionstack/roundhouse` are documented here.
4
+
5
+ ## [0.5.10] — 2026-05-09
6
+
7
+ ### Fixed
8
+ - **Cron notifications actually delivered** — replaced IPC socket loopback with direct callback injection from gateway
9
+ - `shouldNotify` onlyOn filter now applies to all notification routes (was only explicit Telegram)
10
+
11
+ ## [0.5.9] — 2026-05-09
12
+
13
+ ### Added
14
+ - **Cron IPC broadcast** — cron jobs without explicit `notify.telegram.chatIds` now broadcast results via IPC socket to all active transports
15
+ - Expanded tools.md: mcporter, playwright-cli, codex exec, AWS CLI, memory management docs
16
+
17
+ ### Fixed
18
+ - Completed one-shot jobs hidden from `/crons` and `roundhouse cron list` (use `--all` to see them)
19
+ - playwright-cli command names corrected (requests, cookie-list, eval)
20
+ - mcporter examples use actual configured server names (aws-mcp, aws-documentation)
21
+
22
+ ## [0.5.8] — 2026-05-09
23
+
24
+ ### Added
25
+ - **IPC unix socket** — `roundhouse message "text"` sends messages to active transports via `~/.roundhouse/gateway.sock`
26
+ - **Session routing** — `--session main` targets primary chat, numeric ID targets specific chat
27
+ - **Provision tools.md** — bundled tools.md auto-copied to `~/.roundhouse/` on setup/update (never overwrites)
28
+ - 13 IPC integration tests
29
+ - IPC barrel exports for consistent import paths
30
+
31
+ ### Security
32
+ - Socket mode 0600 (owner-only access)
33
+ - Stale socket cleanup with liveness probe (500ms timeout)
34
+ - 64KB payload guard, 5s request timeout
35
+
36
+ ## [0.5.7] — 2026-05-09
37
+
38
+ ### Added
39
+ - **`<tools>` section injection** — bundled `tools.md` injected into every agent prompt so agent knows it can schedule cron jobs (PR #50)
40
+ - **Extension updates in `/update`** — pi-hard-no and pi-branch-enforcer updated alongside roundhouse (PR #51)
41
+ - User-customizable `~/.roundhouse/tools.md` overrides bundled tools documentation
42
+ - Per-extension progress messages shown to user during update
43
+
44
+ ### Fixed
45
+ - Tools injection runs after STT enrichment so voice-only messages also get tools context
46
+ - XML tag sanitization prevents prompt injection from user-customized tools.md
47
+ - `/update` version-check failure returns distinct error (not misleading "already-latest")
48
+ - Error messages truncated to 200 chars for Telegram safety
49
+
50
+ ## [0.5.6] — 2026-05-09
51
+
52
+ ### Added
53
+ - **STT agent prompt injection** — when whisper/ffmpeg are missing, injects install prompt into agent turn instead of complex auto-install chains (PR #45)
54
+ - **getMissingDeps()** on whisper provider + SttService — reports what’s missing so gateway can act
55
+ - **Duration-exceeded “skipped” status** — audio too long gets status “skipped” (not “failed”), preventing false install prompts
56
+ - **Text+audio handling** — when user sends caption + voice, install prompt appends to existing text
57
+
58
+ ### Changed
59
+ - **Removed `autoInstall` config** — no longer needed; agent handles installation autonomously
60
+ - **Simplified whisper.ts** — removed installWhisperWithPip, installWhisperWithUv, ensureFfmpeg (~150 lines deleted)
61
+ - **User notification** — “Asking agent to install...” (accurate) replaces “Setting up...” (misleading)
62
+
63
+ ### Fixed
64
+ - **systemd: TimeoutStopSec=15 + KillMode=mixed** (PR #43) — hung whisper subprocesses no longer block shutdown for 90s
65
+ - **Stale autoInstall references** in CLI setup wizard, doctor checks, and usability report (PR #46)
66
+
67
+ ## [0.5.5] — 2026-05-09
68
+
69
+ ### Added
70
+ - **TransportAdapter interface** — enrichPrompt, postMessage, registerCommands, ownsThread, notify, isPairingPending, handlePairing (PR #37)
71
+ - **TelegramAdapter** — implements TransportAdapter, pairing logic moved from gateway (PR #39)
72
+ - **Bundled skills** — roundhouse-cron + pr-merge-discipline ship with package
73
+ - **STT typing indicator** — Telegram shows “typing” during voice transcription (PR #42)
74
+ - **PairingResult widened** to `string | number` for future Slack/Discord support (PR #41)
75
+ - **Agent chooser** — interactive numbered menu in setup wizard (PR #37)
76
+
77
+ ### Fixed
78
+ - **Gateway imports** — 9 broken `./` → `../` paths after module reorg (PR #40)
79
+ - **Naming conventions** — adapter.ts → telegram-adapter.ts, TelegramTransportAdapter → TelegramAdapter (PR #38)
80
+
81
+ ### Changed
82
+ - **Module reorganization** (PR #36) — gateway/, transports/telegram/, cli/setup/, provisioning/
83
+ - 376 tests passing
84
+
85
+ ## [0.5.0–0.5.4] — 2026-05-08
86
+
87
+ ### Added
88
+ - **Shared "main" session** — all direct messages route to a single `main` agent thread
89
+ - Telegram DMs, CLI TUI, CLI agent, future Slack/Discord all share one conversation
90
+ - Sessions stored in `~/.roundhouse/sessions/main/`
91
+ - `SESSIONS_DIR` exported from config
92
+ - `resolveAgentThreadId()` routes DMs → `main`, groups → `group:<chatId>`
93
+ - `roundhouse tui` with no args opens `main` session directly (no scanning/prompting)
94
+ - `roundhouse agent` defaults to `main` thread; `--ephemeral` for one-off behavior
95
+ - **Silent agent failure detection** — model_error event, pi-telegram conflict warning, safety net posts "no response" if turn silent (v0.5.4)
96
+ - **macOS LaunchAgent support** — auto-start, Plist generation (v0.5.2)
97
+ - **Phase 2 refactoring** — cron dispatcher, pi-adapter extraction, setup.ts split (v0.5.3)
98
+
99
+ ### Fixed
100
+ - **Pairing userId extraction** — reads `author.userId` matching Telegram adapter shape
101
+ - **Session reaper race** — tracks `inFlight` counter, skips busy sessions during reap
102
+ - **/compact concurrency** — now acquires per-thread lock like normal prompts
103
+ - **Attachment permissions** — dirs created with 0700, files with 0600
104
+ - **Memory state permissions** — writes with mode 0600, dirs 0700
105
+ - **Cron template cwd** — uses `agentCfg.cwd` instead of `process.cwd()`
106
+ - **Cron TDZ crash** — `agentCfg` was referenced before declaration
107
+ - **cmdRun shell injection** — uses `execFileSync` instead of shell string interpolation
108
+
109
+ ### Removed
110
+ - Legacy `threadIdToDirLegacy()` and all backward-compat fallback code
111
+ - Per-platform session directories (old `telegram_c*` dirs no longer used)
112
+
113
+ ## [0.3.18] — 2026-04-30
114
+
115
+ ### Added
116
+ - **`--agent` flag** for `roundhouse setup` — agent-aware setup with `AgentDefinition` registry
117
+ - Pi as default agent, extensible to future agent types
118
+ - `stepInstallPackages`, `stepPreflight`, `stepConfigure` all driven by agent definition
119
+ - `resolveAgentForSetup()` wires Pi-specific configure/installExtension
120
+ - Unknown agent types rejected with available list
121
+
122
+ ## [0.3.17] — 2026-04-29
123
+
124
+ ### Added
125
+ - **`setup --telegram`** — interactive wizard + headless automation
126
+ - Interactive: BotFather guide, masked token prompt, QR pairing link, 10-step guided flow
127
+ - Headless: `--headless` with structured JSON logging, persistent pairing file
128
+ - Gateway completes pairing on `/start <nonce>` — `handlePendingPairing()` method
129
+ - `--bot-token` rejected in headless mode (argv visible in process listings)
130
+ - `qrcode-terminal` dependency for pairing QR codes
131
+ - Seed `~/.roundhouse/.env` with commented-out example template (mode 0600)
132
+
133
+ ## [0.3.16] — 2026-04-29
134
+
135
+ ### Fixed
136
+ - Show Telegram checks in CLI doctor, deduplicate token resolution
137
+ - Flip psst default to off
138
+
139
+ ## [0.3.15] — 2026-04-29
140
+
141
+ ### Fixed
142
+ - Validate pathValue and guard against non-string unit values in systemd generator
143
+ - Newline-injection guard in `generateUnit`, harden `whichSync`
144
+
145
+ ### Changed
146
+ - Consolidate systemd/shell helpers, add systemd tests
147
+ - Extract shared env-file and systemd modules (DRY/SRP)
148
+
149
+ ## [0.3.14] — 2026-04-28
150
+
151
+ ### Changed
152
+ - Refactor: extract shared env-file and systemd modules
153
+
154
+ ## [0.3.13] — 2026-04-28
155
+
156
+ ### Fixed
157
+ - `cmdInstall` tsx fallback now uses 'run' subcommand
158
+ - Split start/run: 'start' launches daemon, 'run' runs foreground
159
+
160
+ ## [0.3.12] — 2026-04-28
161
+
162
+ ### Changed
163
+ - Rename env file to `.env` with legacy fallback + deprecation warning
164
+
165
+ ## [0.3.11] — 2026-04-28
166
+
167
+ ### Fixed
168
+ - E2E findings: show step ⑥ skip message, doctor checks global npm for Pi SDK
169
+ - Allow `--dry-run` without bot token
170
+
171
+ ## [0.3.10] — 2026-04-27
172
+
173
+ ### Fixed
174
+ - 6 findings from Codex full-codebase review (2 HIGH, 4 MEDIUM)
175
+ - Attachment size: `Blob.size` check before Buffer materialization
176
+ - Shell injection in `runSudo`: `execFileSync` with arg arrays
177
+ - `threadIdToDir` collision: injective `_xNNNN` encoding
178
+ - `/restart` checks both allowlists
179
+ - `isCommand` validates @bot suffix
180
+ - Cron `lastScheduledAt` pre-advance documented
181
+
182
+ ## [0.3.9] — 2026-04-27
183
+
184
+ ### Fixed
185
+ - Setup fixes from E2E test findings on fresh EC2 instances
186
+
187
+ ## [0.3.8] — 2026-04-27
188
+
189
+ ### Added
190
+ - **`roundhouse setup`** — one-command install & configure with psst integration
191
+ - **`roundhouse pair`** — standalone Telegram pairing command
192
+ - **`roundhouse agent`** — CLI command to send messages to configured agent
193
+ - `roundhouse --version` / `-v` flag
194
+ - Shared `BOT_COMMANDS` constant
195
+ - `allowedUserIds` (immutable numeric IDs) in gateway auth
196
+
197
+ ## [0.3.6] — 2026-04-26
198
+
199
+ ### Added
200
+ - **Memory system** (Option B) — roundhouse-managed by default
201
+ - MEMORY.md, daily notes, newspaper-style injection
202
+ - Proactive compaction: soft/hard/emergency thresholds
203
+ - Pre-compact flush in all modes
204
+ - `/status` shows memory mode and system CPU/RAM
205
+ - Rich startup notification with version, model, cron counts
206
+
207
+ ## [0.3.5] — 2026-04-25
208
+
209
+ ### Added
210
+ - **Cron system** — internal scheduler with `p-queue` and `croner`
211
+ - `roundhouse cron add/list/show/trigger/runs/edit/pause/resume/delete`
212
+ - Standard cron, interval, and one-shot schedule types
213
+ - Fresh agent per run, timeout with abort, template variables
214
+ - `/crons` and `/jobs` Telegram commands
215
+ - Built-in heartbeat job (reads HEARTBEAT.md every 30min)
216
+
217
+ ## [0.3.2] — 2026-04-24
218
+
219
+ ### Added
220
+ - **Voice support** — download attachments, STT via whisper
221
+ - **Doctor** — 21 checks across 7 categories
222
+ - Config migration `~/.config/roundhouse/` → `~/.roundhouse/`
223
+
224
+ ## [0.3.1] — 2026-04-23
225
+
226
+ ### Added
227
+ - Gateway with Telegram adapter, per-thread agent sessions
228
+ - `/new`, `/restart`, `/status`, `/compact`, `/verbose`, `/stop`, `/doctor` commands
229
+ - Auto-register bot commands with Telegram on startup
230
+ - Draining/drain_complete notification system
231
+ - Context token usage with progress bar
232
+
233
+ ## [0.2.0] — 2026-04-22
234
+
235
+ - Initial release
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inceptionstack/roundhouse",
3
- "version": "0.5.13",
3
+ "version": "0.5.14",
4
4
  "type": "module",
5
5
  "description": "Multi-platform chat gateway that routes messages through a configured AI agent",
6
6
  "license": "MIT",
@@ -32,6 +32,7 @@
32
32
  "src/",
33
33
  "bin/",
34
34
  "skills/",
35
+ "CHANGELOG.md",
35
36
  "LICENSE",
36
37
  "README.md",
37
38
  "architecture.md",
@@ -151,20 +151,15 @@ export async function handleCompact(ctx: CommandContext): Promise<void> {
151
151
 
152
152
  // ── /status ──────────────────────────────────────────
153
153
 
154
- export async function handleStatus(ctx: CommandContext): Promise<void> {
155
- const { thread, agent, agentThreadId, config, allowedUsers, verboseThreads, postWithFallback } = ctx;
154
+ // ── Status helpers ────────────────────────────────────────
156
155
 
157
- const uptimeSec = process.uptime();
158
- const uptimeStr = uptimeSec < 3600
159
- ? `${Math.floor(uptimeSec / 60)}m ${Math.floor(uptimeSec % 60)}s`
160
- : `${Math.floor(uptimeSec / 3600)}h ${Math.floor((uptimeSec % 3600) / 60)}m`;
161
- const platforms = Object.keys(config.chat.adapters).join(", ");
162
- const debugStream = process.env.ROUNDHOUSE_DEBUG_STREAM === "1";
163
- const nodeVer = process.version;
164
- const memMB = (process.memoryUsage.rss() / 1024 / 1024).toFixed(1);
156
+ function formatUptime(sec: number): string {
157
+ return sec < 3600
158
+ ? `${Math.floor(sec / 60)}m ${Math.floor(sec % 60)}s`
159
+ : `${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`;
160
+ }
165
161
 
166
- // Check for available update (async, non-blocking)
167
- let updateAvailable = "";
162
+ async function checkAvailableUpdate(): Promise<string> {
168
163
  try {
169
164
  const { exec } = await import("node:child_process");
170
165
  const { promisify } = await import("node:util");
@@ -172,14 +167,25 @@ export async function handleStatus(ctx: CommandContext): Promise<void> {
172
167
  const { stdout } = await execAsync("npm view @inceptionstack/roundhouse version 2>/dev/null", { timeout: 10_000 });
173
168
  const latest = stdout.trim().split("\n").pop()!.trim();
174
169
  if (latest && /^\d+\.\d+\.\d+$/.test(latest) && latest !== ROUNDHOUSE_VERSION) {
175
- // Simple semver comparison: split and compare numerically
176
170
  const [lM, lm, lp] = latest.split(".").map(Number);
177
171
  const [cM, cm, cp] = ROUNDHOUSE_VERSION.split(".").map(Number);
178
172
  if (lM > cM || (lM === cM && lm > cm) || (lM === cM && lm === cm && lp > cp)) {
179
- updateAvailable = latest;
173
+ return latest;
180
174
  }
181
175
  }
182
- } catch { /* network unavailable — skip */ }
176
+ } catch { /* network unavailable */ }
177
+ return "";
178
+ }
179
+
180
+ export async function handleStatus(ctx: CommandContext): Promise<void> {
181
+ const { thread, agent, agentThreadId, config, allowedUsers, verboseThreads, postWithFallback } = ctx;
182
+
183
+ const uptimeStr = formatUptime(process.uptime());
184
+ const platforms = Object.keys(config.chat.adapters).join(", ");
185
+ const debugStream = process.env.ROUNDHOUSE_DEBUG_STREAM === "1";
186
+ const nodeVer = process.version;
187
+ const memMB = (process.memoryUsage.rss() / 1024 / 1024).toFixed(1);
188
+ const updateAvailable = await checkAvailableUpdate();
183
189
 
184
190
  const info = agent.getInfo ? agent.getInfo(agentThreadId) : {};
185
191
  const agentVersion = info.version ? `v${info.version}` : "";
@@ -31,6 +31,7 @@ import { hostname } from "node:os";
31
31
  import { join } from "node:path";
32
32
  import { injectToolsSection } from "./tools-inject";
33
33
  import { injectPersonaSection, loadPersona } from "./persona-inject";
34
+ import { checkVersionChange } from "./whats-new";
34
35
 
35
36
  /** Bot username for command suffix validation (set during gateway init) */
36
37
  let _botUsername = "";
@@ -237,45 +238,32 @@ export class Gateway {
237
238
  if (isCommand(userText, "/start")) return;
238
239
  if (!userText.trim() && !rawAttachments.length) return;
239
240
 
240
- // Handle /new command
241
- if (isCommand(userText.trim(), "/new")) {
242
- await handleNew(this.buildCommandContext(thread, message, agentThreadId, authorName, allowedUsers, allowedUserIds, verboseThreads, threadLocks));
243
- return;
244
- }
245
-
246
- // Handle /restart command
247
- if (isCommand(userText.trim(), "/restart")) {
248
- await handleRestart(this.buildCommandContext(thread, message, agentThreadId, authorName, allowedUsers, allowedUserIds, verboseThreads, threadLocks));
249
- return;
250
- }
251
-
252
- // Handle /update command
253
- if (isCommand(userText.trim(), "/update")) {
254
- await handleUpdate(this.buildCommandContext(thread, message, agentThreadId, authorName, allowedUsers, allowedUserIds, verboseThreads, threadLocks));
255
- return;
256
- }
257
-
258
- // Handle /compact command
259
- if (isCommand(userText.trim(), "/compact")) {
260
- await handleCompact(this.buildCommandContext(thread, message, agentThreadId, authorName, allowedUsers, allowedUserIds, verboseThreads, threadLocks));
261
- return;
262
- }
263
-
264
- // Handle /status command
265
- if (isCommand(userText.trim(), "/status")) {
266
- await handleStatus(this.buildCommandContext(thread, message, agentThreadId, authorName, allowedUsers, allowedUserIds, verboseThreads, threadLocks));
267
- return;
241
+ // ── Command dispatch (registry-based) ───
242
+ const trimmed = userText.trim();
243
+
244
+ // Commands using standard CommandContext
245
+ const COMMAND_REGISTRY: Record<string, (ctx: CommandContext) => Promise<void>> = {
246
+ "/new": handleNew,
247
+ "/restart": handleRestart,
248
+ "/update": handleUpdate,
249
+ "/compact": handleCompact,
250
+ "/status": handleStatus,
251
+ };
252
+
253
+ for (const [cmd, handler] of Object.entries(COMMAND_REGISTRY)) {
254
+ if (isCommand(trimmed, cmd)) {
255
+ await handler(this.buildCommandContext(thread, message, agentThreadId, authorName, allowedUsers, allowedUserIds, verboseThreads, threadLocks));
256
+ return;
257
+ }
268
258
  }
269
259
 
270
- // Handle /model command
271
- if (isCommandWithArgs(userText.trim(), "/model") || isCommand(userText.trim(), "/model")) {
272
- await handleModel({ thread, text: userText.trim(), postWithFallback: (t, txt) => this.postWithFallback(t, txt) });
260
+ // Commands with custom context (accept args)
261
+ if (isCommandWithArgs(trimmed, "/model") || isCommand(trimmed, "/model")) {
262
+ await handleModel({ thread, text: trimmed, postWithFallback: (t, txt) => this.postWithFallback(t, txt) });
273
263
  return;
274
264
  }
275
-
276
- // Handle /later command
277
- if (isCommandWithArgs(userText.trim(), "/later") || isCommand(userText.trim(), "/later")) {
278
- await handleLater({ thread, text: userText.trim(), postWithFallback: (t, txt) => this.postWithFallback(t, txt) });
265
+ if (isCommandWithArgs(trimmed, "/later") || isCommand(trimmed, "/later")) {
266
+ await handleLater({ thread, text: trimmed, postWithFallback: (t, txt) => this.postWithFallback(t, txt) });
279
267
  return;
280
268
  }
281
269
 
@@ -759,6 +747,9 @@ export class Gateway {
759
747
  cronInfo = `Cron jobs: ${cs.enabledCount}/${cs.jobCount} enabled`;
760
748
  }
761
749
 
750
+ // Check if this is a fresh update (call once, before loop)
751
+ const whatsNew = checkVersionChange();
752
+
762
753
  for (const chatId of chatIds) {
763
754
  const sessionId = Number(chatId) < 0 ? `group:${chatId}` : "main";
764
755
  const perChatText = [
@@ -780,7 +771,8 @@ export class Gateway {
780
771
  ` Process: ${memMB} MB RSS`,
781
772
  ].filter(line => line != null).join("\n");
782
773
 
783
- await this.transport.notify([chatId], perChatText);
774
+ const fullText = whatsNew ? `${perChatText}\n\n${whatsNew}` : perChatText;
775
+ await this.transport.notify([chatId], fullText);
784
776
  }
785
777
  }
786
778
 
@@ -0,0 +1,59 @@
1
+ /**
2
+ * gateway/whats-new.ts — Detect version changes and format "what's new" text
3
+ *
4
+ * On startup, compares current ROUNDHOUSE_VERSION against the last-known
5
+ * version stored in ~/.roundhouse/.last-version. If different, reads the
6
+ * latest CHANGELOG entry and formats it for the startup notification.
7
+ */
8
+
9
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
10
+ import { join, dirname } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { ROUNDHOUSE_DIR, ROUNDHOUSE_VERSION } from "../config";
13
+
14
+ const VERSION_FILE = join(ROUNDHOUSE_DIR, ".last-version");
15
+
16
+ /** Read the bundled CHANGELOG.md and extract the latest version's entry. */
17
+ function getLatestChangelog(): string {
18
+ const changelogPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "CHANGELOG.md");
19
+ try {
20
+ const content = readFileSync(changelogPath, "utf8");
21
+ // Find first ## [x.y.z] section and extract until next ## or end
22
+ const match = content.match(/^## \[[\d.]+\].*?\n([\s\S]*?)(?=\n## \[|$)/m);
23
+ if (!match) return "";
24
+ // Clean up: take first 5 meaningful lines (skip blank)
25
+ const lines = match[1].trim().split("\n")
26
+ .filter(l => l.trim())
27
+ .slice(0, 6)
28
+ .map(l => l.replace(/^### /, "").replace(/^\*\*/, "• ").replace(/\*\*$/, "").replace(/^- /, "• "));
29
+ return lines.join("\n");
30
+ } catch {
31
+ return "";
32
+ }
33
+ }
34
+
35
+ /** Check if version changed since last startup. Returns "what's new" text or null. */
36
+ export function checkVersionChange(): string | null {
37
+ let lastVersion = "";
38
+ try {
39
+ lastVersion = readFileSync(VERSION_FILE, "utf8").trim();
40
+ } catch { /* first run or file missing */ }
41
+
42
+ // Always update the version file
43
+ try {
44
+ mkdirSync(ROUNDHOUSE_DIR, { recursive: true });
45
+ writeFileSync(VERSION_FILE, ROUNDHOUSE_VERSION + "\n");
46
+ } catch {}
47
+
48
+ // No change
49
+ if (lastVersion === ROUNDHOUSE_VERSION) return null;
50
+
51
+ // First run (no previous version)
52
+ if (!lastVersion) return null;
53
+
54
+ // Version changed — this is an update
55
+ const changelog = getLatestChangelog();
56
+ const header = `🆕 Updated: v${lastVersion} → v${ROUNDHOUSE_VERSION}`;
57
+ if (!changelog) return header;
58
+ return `${header}\n\n${changelog}`;
59
+ }