@inceptionstack/roundhouse 0.5.12 → 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 +235 -0
- package/package.json +2 -1
- package/src/gateway/commands.ts +21 -15
- package/src/gateway/gateway.ts +33 -36
- package/src/gateway/later-command.ts +1 -1
- package/src/gateway/model-command.ts +2 -1
- package/src/gateway/persona-inject.ts +94 -0
- package/src/gateway/soul.md +35 -0
- package/src/gateway/tools.md +2 -0
- package/src/gateway/user.md +10 -0
- package/src/gateway/whats-new.ts +59 -0
- package/src/provisioning/bundle.ts +2 -0
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.
|
|
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",
|
package/src/gateway/commands.ts
CHANGED
|
@@ -151,20 +151,15 @@ export async function handleCompact(ctx: CommandContext): Promise<void> {
|
|
|
151
151
|
|
|
152
152
|
// ── /status ──────────────────────────────────────────
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
const { thread, agent, agentThreadId, config, allowedUsers, verboseThreads, postWithFallback } = ctx;
|
|
154
|
+
// ── Status helpers ────────────────────────────────────────
|
|
156
155
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
? `${Math.floor(
|
|
160
|
-
: `${Math.floor(
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
+
return latest;
|
|
180
174
|
}
|
|
181
175
|
}
|
|
182
|
-
} catch { /* network unavailable
|
|
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}` : "";
|
package/src/gateway/gateway.ts
CHANGED
|
@@ -30,6 +30,8 @@ import type { TransportAdapter } from "../transports";
|
|
|
30
30
|
import { hostname } from "node:os";
|
|
31
31
|
import { join } from "node:path";
|
|
32
32
|
import { injectToolsSection } from "./tools-inject";
|
|
33
|
+
import { injectPersonaSection, loadPersona } from "./persona-inject";
|
|
34
|
+
import { checkVersionChange } from "./whats-new";
|
|
33
35
|
|
|
34
36
|
/** Bot username for command suffix validation (set during gateway init) */
|
|
35
37
|
let _botUsername = "";
|
|
@@ -236,45 +238,32 @@ export class Gateway {
|
|
|
236
238
|
if (isCommand(userText, "/start")) return;
|
|
237
239
|
if (!userText.trim() && !rawAttachments.length) return;
|
|
238
240
|
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// Handle /compact command
|
|
258
|
-
if (isCommand(userText.trim(), "/compact")) {
|
|
259
|
-
await handleCompact(this.buildCommandContext(thread, message, agentThreadId, authorName, allowedUsers, allowedUserIds, verboseThreads, threadLocks));
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Handle /status command
|
|
264
|
-
if (isCommand(userText.trim(), "/status")) {
|
|
265
|
-
await handleStatus(this.buildCommandContext(thread, message, agentThreadId, authorName, allowedUsers, allowedUserIds, verboseThreads, threadLocks));
|
|
266
|
-
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
|
+
}
|
|
267
258
|
}
|
|
268
259
|
|
|
269
|
-
//
|
|
270
|
-
if (isCommandWithArgs(
|
|
271
|
-
await handleModel({ thread, text:
|
|
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) });
|
|
272
263
|
return;
|
|
273
264
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (isCommandWithArgs(userText.trim(), "/later") || isCommand(userText.trim(), "/later")) {
|
|
277
|
-
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) });
|
|
278
267
|
return;
|
|
279
268
|
}
|
|
280
269
|
|
|
@@ -327,6 +316,9 @@ export class Gateway {
|
|
|
327
316
|
await handleOrAbort(thread, message);
|
|
328
317
|
});
|
|
329
318
|
|
|
319
|
+
// ── Load persona files at startup (cached for process lifetime) ───
|
|
320
|
+
loadPersona();
|
|
321
|
+
|
|
330
322
|
// ── Handle inline keyboard callbacks ───
|
|
331
323
|
this.chat.onAction(MODEL_ACTION_ID, async (event: any) => {
|
|
332
324
|
await handleModelAction({ value: event.value, thread: event.thread });
|
|
@@ -408,6 +400,7 @@ export class Gateway {
|
|
|
408
400
|
|
|
409
401
|
// Inject tools section (after STT enrichment so voice-only messages get it too)
|
|
410
402
|
if (agentMessage.text) {
|
|
403
|
+
agentMessage.text = injectPersonaSection(agentMessage.text);
|
|
411
404
|
agentMessage.text = injectToolsSection(agentMessage.text);
|
|
412
405
|
}
|
|
413
406
|
|
|
@@ -754,6 +747,9 @@ export class Gateway {
|
|
|
754
747
|
cronInfo = `Cron jobs: ${cs.enabledCount}/${cs.jobCount} enabled`;
|
|
755
748
|
}
|
|
756
749
|
|
|
750
|
+
// Check if this is a fresh update (call once, before loop)
|
|
751
|
+
const whatsNew = checkVersionChange();
|
|
752
|
+
|
|
757
753
|
for (const chatId of chatIds) {
|
|
758
754
|
const sessionId = Number(chatId) < 0 ? `group:${chatId}` : "main";
|
|
759
755
|
const perChatText = [
|
|
@@ -775,7 +771,8 @@ export class Gateway {
|
|
|
775
771
|
` Process: ${memMB} MB RSS`,
|
|
776
772
|
].filter(line => line != null).join("\n");
|
|
777
773
|
|
|
778
|
-
|
|
774
|
+
const fullText = whatsNew ? `${perChatText}\n\n${whatsNew}` : perChatText;
|
|
775
|
+
await this.transport.notify([chatId], fullText);
|
|
779
776
|
}
|
|
780
777
|
}
|
|
781
778
|
|
|
@@ -33,7 +33,7 @@ function ensureLaterFile(): void {
|
|
|
33
33
|
|
|
34
34
|
export async function handleLater(ctx: LaterCommandContext): Promise<void> {
|
|
35
35
|
const { thread, text, postWithFallback } = ctx;
|
|
36
|
-
const idea = text.replace(/^\/later
|
|
36
|
+
const idea = text.replace(/^\/later(@\S+)?\s*/i, "").trim();
|
|
37
37
|
|
|
38
38
|
// No argument: show contents
|
|
39
39
|
if (!idea) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
14
14
|
|
|
15
15
|
/** Known model aliases → Bedrock model IDs */
|
|
16
16
|
export const MODEL_ALIASES: Record<string, { provider: string; model: string; label: string }> = {
|
|
@@ -58,6 +58,7 @@ function readSettings(): Record<string, any> {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function writeSettings(settings: Record<string, any>): void {
|
|
61
|
+
mkdirSync(join(homedir(), ".pi", "agent"), { recursive: true });
|
|
61
62
|
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gateway/persona-inject.ts — Inject <persona> section into agent prompts
|
|
3
|
+
*
|
|
4
|
+
* Reads user.md and soul.md (user-customized or bundled defaults) and
|
|
5
|
+
* prepends them as a structured section so the agent has identity and
|
|
6
|
+
* user context on every turn.
|
|
7
|
+
*
|
|
8
|
+
* Cached with mtime-based invalidation: stat() on each turn (~0.1ms),
|
|
9
|
+
* only re-reads if files have been modified since last load.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, statSync } from "node:fs";
|
|
13
|
+
import { join, dirname } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { ROUNDHOUSE_DIR } from "../config";
|
|
16
|
+
|
|
17
|
+
let cachedPersona: string | null = null;
|
|
18
|
+
let lastMtime = 0;
|
|
19
|
+
|
|
20
|
+
const SOUL_PATH = join(ROUNDHOUSE_DIR, "soul.md");
|
|
21
|
+
const USER_PATH = join(ROUNDHOUSE_DIR, "user.md");
|
|
22
|
+
|
|
23
|
+
function getMaxMtime(): number {
|
|
24
|
+
let max = 0;
|
|
25
|
+
try { max = Math.max(max, statSync(SOUL_PATH).mtimeMs); } catch {}
|
|
26
|
+
try { max = Math.max(max, statSync(USER_PATH).mtimeMs); } catch {}
|
|
27
|
+
return max;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function loadFile(filename: string): string {
|
|
31
|
+
const userPath = join(ROUNDHOUSE_DIR, filename);
|
|
32
|
+
const bundledPath = join(dirname(fileURLToPath(import.meta.url)), filename);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
return readFileSync(userPath, "utf8");
|
|
36
|
+
} catch {
|
|
37
|
+
try {
|
|
38
|
+
return readFileSync(bundledPath, "utf8");
|
|
39
|
+
} catch {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildPersona(): string {
|
|
46
|
+
const soul = loadFile("soul.md").trim();
|
|
47
|
+
const user = loadFile("user.md").trim();
|
|
48
|
+
|
|
49
|
+
if (!soul && !user) return "";
|
|
50
|
+
|
|
51
|
+
const parts: string[] = [];
|
|
52
|
+
if (soul) parts.push(soul);
|
|
53
|
+
if (user) parts.push(user);
|
|
54
|
+
return parts.join("\n\n---\n\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load persona files and cache the result.
|
|
59
|
+
* Call at gateway startup to eagerly load.
|
|
60
|
+
*/
|
|
61
|
+
export function loadPersona(): void {
|
|
62
|
+
cachedPersona = buildPersona();
|
|
63
|
+
lastMtime = getMaxMtime();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Reload persona from disk. Call after agent edits user.md/soul.md
|
|
68
|
+
* (e.g. from an IPC handler or post-tool-execution hook).
|
|
69
|
+
*/
|
|
70
|
+
export function reloadPersona(): void {
|
|
71
|
+
cachedPersona = buildPersona();
|
|
72
|
+
lastMtime = getMaxMtime();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Prepend a <persona> section to the prompt text.
|
|
77
|
+
* Only injects if soul.md or user.md have content.
|
|
78
|
+
* Auto-reloads if files have been modified since last load.
|
|
79
|
+
*/
|
|
80
|
+
export function injectPersonaSection(text: string): string {
|
|
81
|
+
if (cachedPersona === null) {
|
|
82
|
+
loadPersona();
|
|
83
|
+
} else {
|
|
84
|
+
// Cheap mtime check — auto-reload if agent edited the files
|
|
85
|
+
const currentMtime = getMaxMtime();
|
|
86
|
+
if (currentMtime !== lastMtime) {
|
|
87
|
+
reloadPersona();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!cachedPersona) return text;
|
|
91
|
+
// Escape any literal </persona> in content to prevent XML injection
|
|
92
|
+
const safe = cachedPersona.replace(/<\/persona>/gi, "</persona>");
|
|
93
|
+
return `<persona>\n${safe}\n</persona>\n\n${text}`;
|
|
94
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Who You Are
|
|
2
|
+
|
|
3
|
+
_You're not a chatbot. You're a technical partner._
|
|
4
|
+
|
|
5
|
+
## Core Identity
|
|
6
|
+
|
|
7
|
+
**Name:** Loki
|
|
8
|
+
**Role:** Senior engineer and ops partner. You help build, deploy, debug, and maintain software and infrastructure. You have opinions and you share them.
|
|
9
|
+
|
|
10
|
+
## Core Truths
|
|
11
|
+
|
|
12
|
+
**Be genuinely helpful, not performatively helpful.** Skip the filler — just help. Actions speak louder than words.
|
|
13
|
+
|
|
14
|
+
**Have opinions.** When something is a bad pattern, say so. When there's a better approach, recommend it. You're not a yes-machine.
|
|
15
|
+
|
|
16
|
+
**Be resourceful before asking.** Check the docs. Read the file. Try things. _Then_ ask if you're stuck.
|
|
17
|
+
|
|
18
|
+
**Earn trust through competence.** You have access to tools, shell, and infrastructure. Use them wisely. Be careful with destructive operations. Be bold with read operations.
|
|
19
|
+
|
|
20
|
+
**Think holistically.** Consider the broader context — architecture, maintainability, security, user experience.
|
|
21
|
+
|
|
22
|
+
## Boundaries
|
|
23
|
+
|
|
24
|
+
- Ask before destructive operations (deletions, config changes with blast radius)
|
|
25
|
+
- Read freely — list, describe, get operations are safe
|
|
26
|
+
- Private things stay private
|
|
27
|
+
- Never send half-baked replies
|
|
28
|
+
|
|
29
|
+
## Vibe
|
|
30
|
+
|
|
31
|
+
Direct, technical, concise. Think senior engineer talking to senior engineer. Thorough when it matters, brief when it doesn't. No corporate speak.
|
|
32
|
+
|
|
33
|
+
## Continuity
|
|
34
|
+
|
|
35
|
+
Each session, you wake up fresh. Your workspace files _are_ your memory. Read them. Update them.
|
package/src/gateway/tools.md
CHANGED
|
@@ -10,6 +10,8 @@ Do NOT create files directly in `~/` or pollute the home directory. Use subdirec
|
|
|
10
10
|
- `~/.roundhouse/workspace/later.md` — ideas saved via `/later`
|
|
11
11
|
- `~/.roundhouse/workspace/<project>/` — project-specific files if needed
|
|
12
12
|
|
|
13
|
+
> 💡 When looking for things to work on, check `~/.roundhouse/workspace/later.md` for saved ideas and tasks.
|
|
14
|
+
|
|
13
15
|
## roundhouse cron add
|
|
14
16
|
|
|
15
17
|
Schedule recurring or one-shot jobs. The user may ask you to "remind me", "check every X", "do Y later", or "schedule Z".
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# About Your Human
|
|
2
|
+
|
|
3
|
+
- **Name:** (not yet set)
|
|
4
|
+
- **What to call them:** (use their Telegram username until they tell you otherwise)
|
|
5
|
+
- **Timezone:** UTC
|
|
6
|
+
- **Notes:** (learn about them through conversation)
|
|
7
|
+
|
|
8
|
+
## Preferences
|
|
9
|
+
|
|
10
|
+
- (Will be filled in as you learn what they prefer)
|
|
@@ -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
|
+
}
|
|
@@ -320,6 +320,8 @@ export function provisionWorkspaceFiles(opts: ProvisionOpts = {}): void {
|
|
|
320
320
|
// Files to provision: [bundled filename, target filename]
|
|
321
321
|
const files: [string, string][] = [
|
|
322
322
|
["tools.md", "tools.md"],
|
|
323
|
+
["soul.md", "soul.md"],
|
|
324
|
+
["user.md", "user.md"],
|
|
323
325
|
];
|
|
324
326
|
|
|
325
327
|
try {
|