@inceptionstack/roundhouse 0.5.13 → 0.5.15

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