@inceptionstack/roundhouse 0.5.4 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +1 -3
  2. package/architecture.md +37 -19
  3. package/package.json +2 -1
  4. package/skills/pr-merge-discipline/SKILL.md +36 -0
  5. package/skills/roundhouse-cron/SKILL.md +136 -0
  6. package/src/agents/kiro/kiro-adapter.ts +1 -4
  7. package/src/agents/pi/pi-adapter.ts +1 -4
  8. package/src/cli/cli.ts +6 -1
  9. package/src/cli/doctor/checks/system.ts +1 -1
  10. package/src/cli/setup/args.ts +8 -9
  11. package/src/cli/setup/flows.ts +47 -14
  12. package/src/cli/{setup-logger.ts → setup/logger.ts} +4 -4
  13. package/src/cli/{setup-prompts.ts → setup/prompts.ts} +23 -2
  14. package/src/cli/setup/runtime.ts +1 -1
  15. package/src/cli/setup/steps.ts +5 -5
  16. package/src/cli/{setup-telegram.ts → setup/telegram.ts} +4 -4
  17. package/src/cli/setup/types.ts +4 -3
  18. package/src/cli/setup.ts +8 -8
  19. package/src/cli/systemd.ts +2 -0
  20. package/src/cli/update.ts +111 -0
  21. package/src/cron/runner.ts +2 -1
  22. package/src/gateway/commands.ts +29 -4
  23. package/src/{gateway.ts → gateway/gateway.ts} +126 -100
  24. package/src/gateway/helpers.ts +1 -1
  25. package/src/gateway/index.ts +2 -5
  26. package/src/gateway/streaming.ts +1 -1
  27. package/src/gateway/tools-inject.ts +45 -0
  28. package/src/gateway/tools.md +54 -0
  29. package/src/{bundle.ts → provisioning/bundle.ts} +32 -0
  30. package/src/transports/index.ts +6 -0
  31. package/src/{telegram-html.ts → transports/telegram/html.ts} +2 -2
  32. package/src/{pairing.ts → transports/telegram/pairing.ts} +1 -1
  33. package/src/transports/telegram/telegram-adapter.ts +111 -0
  34. package/src/transports/types.ts +71 -0
  35. package/src/voice/providers/whisper.ts +37 -94
  36. package/src/voice/stt-service.ts +35 -17
  37. package/src/voice/types.ts +1 -3
  38. package/src/commands/update.ts +0 -69
  39. /package/src/{commands.ts → transports/telegram/bot-commands.ts} +0 -0
  40. /package/src/{telegram-format.ts → transports/telegram/format.ts} +0 -0
  41. /package/src/{notify/telegram.ts → transports/telegram/notify.ts} +0 -0
  42. /package/src/{telegram-progress.ts → transports/telegram/progress.ts} +0 -0
@@ -1,12 +1,12 @@
1
1
  /**
2
- * cli/setup-telegram.ts — Telegram API helpers for 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 { randomBytes } from "node:crypto";
9
- import { BOT_COMMANDS } from "../commands";
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 ?? `rh-${randomBytes(8).toString("hex")}`;
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
@@ -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
- /** Fully headless automation (no TTY prompts) */
27
- headless: boolean;
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
  }
package/src/cli/setup.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { readFile } from "node:fs/promises";
13
- import { BOT_COMMANDS } from "../commands";
13
+ import { BOT_COMMANDS } from "../transports/telegram/bot-commands";
14
14
  import { atomicWriteJson, execSafe } from "./setup/helpers";
15
15
  import { type SetupOptions } from "./setup/types";
16
16
  import { parseSetupArgs } from "./setup/args";
@@ -27,7 +27,7 @@ import {
27
27
  import {
28
28
  validateBotToken,
29
29
  pairTelegram,
30
- } from "./setup-telegram";
30
+ } from "./setup/telegram";
31
31
  import {
32
32
  stepPreflight,
33
33
  stepValidateToken,
@@ -42,7 +42,7 @@ import {
42
42
  stepPostflight,
43
43
  } from "./setup/steps";
44
44
  import { resolveAgentForSetup, textLog, textStepLog } from "./setup/runtime";
45
- import { runInteractiveTelegramSetup, runHeadlessTelegramSetup } from "./setup/flows";
45
+ import { runInteractiveTelegramSetup, runNonInteractiveTelegramSetup } from "./setup/flows";
46
46
 
47
47
  // ── Orchestrator ─────────────────────────────────────
48
48
 
@@ -63,8 +63,8 @@ export async function cmdSetup(argv: string[]): Promise<void> {
63
63
 
64
64
  // Route to --telegram flows
65
65
  if (opts.telegram) {
66
- if (opts.headless) {
67
- await runHeadlessTelegramSetup(opts);
66
+ if (opts.nonInteractive) {
67
+ await runNonInteractiveTelegramSetup(opts);
68
68
  } else {
69
69
  await runInteractiveTelegramSetup(opts);
70
70
  }
@@ -258,12 +258,12 @@ function printSetupHelp(): void {
258
258
  console.log(`
259
259
  Usage:
260
260
  roundhouse setup --telegram Interactive wizard (recommended)
261
- TELEGRAM_BOT_TOKEN=... roundhouse setup \\\n --telegram --headless --user USERNAME Headless automation (SSM/cloud-init)
261
+ TELEGRAM_BOT_TOKEN=... roundhouse setup \\\n --telegram --non-interactive --user USERNAME Non-interactive automation (SSM/cloud-init)
262
262
  TELEGRAM_BOT_TOKEN=... roundhouse setup \\\n --user USERNAME Legacy (non-wizard) setup
263
263
 
264
264
  Modes:
265
- --telegram Telegram-focused setup (wizard or headless)
266
- --headless Non-interactive automation (implies --non-interactive)
265
+ --telegram Telegram-focused setup (wizard or non-interactive)
266
+ --non-interactive Suppress all prompts (for automation/SSM/cloud-init)
267
267
  Requires TELEGRAM_BOT_TOKEN env var and --user
268
268
 
269
269
  Required (or prompted in interactive --telegram):
@@ -127,6 +127,8 @@ WorkingDirectory=${home}
127
127
  ExecStart=${opts.execStart}
128
128
  Restart=on-failure
129
129
  RestartSec=5
130
+ TimeoutStopSec=15
131
+ KillMode=mixed
130
132
  EnvironmentFile=-${envFilePath}
131
133
  Environment=ROUNDHOUSE_CONFIG=${CONFIG_PATH}
132
134
  Environment=NODE_ENV=production
@@ -0,0 +1,111 @@
1
+ /**
2
+ * commands/update.ts — Handle the /update command
3
+ *
4
+ * Transport-agnostic: receives a ProgressReporter interface,
5
+ * not a Telegram-specific thread object.
6
+ */
7
+
8
+ import { homedir } from "node:os";
9
+ import { execSync } from "node:child_process";
10
+ import { readFileSync, writeFileSync } from "node:fs";
11
+ import { provisionBundle } from "../provisioning/bundle";
12
+
13
+ const GLOBAL_PI_EXTENSION_PACKAGES = [
14
+ "@inceptionstack/pi-hard-no",
15
+ "@inceptionstack/pi-branch-enforcer",
16
+ ];
17
+
18
+ export interface UpdateProgress {
19
+ update(text: string): Promise<void>;
20
+ }
21
+
22
+ export interface UpdateResult {
23
+ action: "already-latest" | "updated" | "error";
24
+ currentVersion: string;
25
+ latestVersion?: string;
26
+ error?: string;
27
+ }
28
+
29
+ /**
30
+ * Check for updates, install if newer, provision bundle, patch settings.
31
+ * Returns the result — caller decides how to present it and whether to restart.
32
+ */
33
+ export async function performUpdate(progress: UpdateProgress): Promise<UpdateResult> {
34
+ // Get current version
35
+ const pkg = await import("../../package.json", { with: { type: "json" } });
36
+ const currentVersion = pkg.default?.version ?? "unknown";
37
+
38
+ // Check latest version on npm
39
+ let latestVersion: string;
40
+ try {
41
+ latestVersion = execSync("npm view @inceptionstack/roundhouse version 2>/dev/null", {
42
+ timeout: 30_000,
43
+ encoding: "utf8",
44
+ }).trim();
45
+ } catch (e) {
46
+ // Update extensions anyway, but flag that version check failed
47
+ latestVersion = "";
48
+ console.warn("[roundhouse] npm view failed:", e instanceof Error ? e.message : e);
49
+ }
50
+
51
+ // Always update extensions (even if roundhouse is already latest)
52
+ if (!latestVersion) {
53
+ await progress.update(`⚠️ Version check failed — updating extensions only`);
54
+ }
55
+ for (const extensionPackage of GLOBAL_PI_EXTENSION_PACKAGES) {
56
+ await progress.update(`📦 Updating extension: ${extensionPackage}...`);
57
+
58
+ try {
59
+ execSync(`npm install -g ${extensionPackage}@latest 2>&1`, {
60
+ timeout: 60_000,
61
+ encoding: "utf8",
62
+ });
63
+ await progress.update(`✅ ${extensionPackage} updated`);
64
+ } catch (e) {
65
+ const msg = e instanceof Error ? e.message : String(e);
66
+ console.warn(`[roundhouse] failed to update extension ${extensionPackage}:`, msg);
67
+ await progress.update(`⚠️ Failed to update ${extensionPackage}: ${msg.slice(0, 150)}`);
68
+ }
69
+ }
70
+
71
+ if (!latestVersion) {
72
+ return { action: "error", currentVersion, error: "Version check failed (extensions updated)" };
73
+ }
74
+ if (latestVersion === currentVersion) {
75
+ return { action: "already-latest", currentVersion };
76
+ }
77
+
78
+ await progress.update(`📦 Updating v${currentVersion} → v${latestVersion}...`);
79
+
80
+ try {
81
+ execSync("npm install -g @inceptionstack/roundhouse@latest 2>&1", {
82
+ timeout: 120_000,
83
+ encoding: "utf8",
84
+ });
85
+ } catch (e) {
86
+ const msg = e instanceof Error ? e.message : String(e);
87
+ console.warn("[roundhouse] self-update failed:", msg);
88
+ return { action: "error", currentVersion, error: `Self-update failed: ${msg}` };
89
+ }
90
+
91
+ // Provision bundle (skills sync + CLI tools + config)
92
+ try {
93
+ provisionBundle();
94
+ } catch (e) {
95
+ console.warn("[roundhouse] bundle provisioning failed:", e instanceof Error ? e.message : e);
96
+ }
97
+
98
+ // Ensure settings.json includes roundhouse package (for pre-bundle upgrades)
99
+ try {
100
+ const settingsPath = `${homedir()}/.pi/agent/settings.json`;
101
+ const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
102
+ const selfPkg = "npm:@inceptionstack/roundhouse";
103
+ if (!Array.isArray(settings.packages)) settings.packages = [];
104
+ if (!settings.packages.includes(selfPkg)) {
105
+ settings.packages.push(selfPkg);
106
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
107
+ }
108
+ } catch { /* settings.json may not exist yet — fine, setup will create it */ }
109
+
110
+ return { action: "updated", currentVersion, latestVersion };
111
+ }
@@ -6,7 +6,8 @@
6
6
  */
7
7
 
8
8
  import { getAgentFactory } from "../agents/registry";
9
- import { sendTelegramToMany } from "../notify/telegram";
9
+ // TODO: route through TransportAdapter.notify() when multi-transport lands
10
+ import { sendTelegramToMany } from "../transports/telegram/notify";
10
11
  import { CronStore, generateRunId } from "./store";
11
12
  import { buildTemplateContext, renderTemplate } from "./template";
12
13
  import type { CronJobConfig, CronRunRecord } from "./types";
@@ -1,5 +1,5 @@
1
1
  /**
2
- * gateway/commands.ts — Telegram command handlers
2
+ * gateway/commands.ts — Chat command handlers
3
3
  *
4
4
  * Each handler is a standalone async function that receives a CommandContext.
5
5
  * Extracted from Gateway.start() to reduce method size and enable unit testing.
@@ -9,7 +9,8 @@ import type { AgentAdapter, AgentStreamEvent, GatewayConfig } from "../types";
9
9
  import { ROUNDHOUSE_VERSION } from "../config";
10
10
  import { startTypingLoop } from "../util";
11
11
  import { prepareMemoryForTurn, finalizeMemoryForTurn, flushMemoryThenCompact, determineMemoryMode } from "../memory/lifecycle";
12
- import { createProgressMessage } from "../telegram-progress";
12
+ // TODO: move progress into TransportAdapter when multi-transport lands
13
+ import { createProgressMessage } from "../transports/telegram/progress";
13
14
  import { getSystemResources } from "./helpers";
14
15
 
15
16
  // ── Types ────────────────────────────────────────────
@@ -70,10 +71,12 @@ export async function handleUpdate(ctx: CommandContext): Promise<void> {
70
71
  console.log(`[roundhouse] /update requested by @${authorName} in thread=${thread.id}`);
71
72
  const progress = await createProgressMessage(thread, "📦 Checking for updates...");
72
73
  try {
73
- const { performUpdate } = await import("../commands/update");
74
+ const { performUpdate } = await import("../cli/update");
74
75
  const result = await performUpdate(progress);
75
76
  if (result.action === "already-latest") {
76
77
  await progress.update(`✅ Already on latest (v${result.currentVersion})`);
78
+ } else if (result.action === "error") {
79
+ await progress.update(`⚠️ ${(result.error ?? "Update failed").slice(0, 200)}`);
77
80
  } else if (result.action === "updated") {
78
81
  await progress.update(`✅ Updated v${result.currentVersion} → v${result.latestVersion}. Restarting...`);
79
82
  console.log(`[roundhouse] updated ${result.currentVersion} -> ${result.latestVersion}, restarting`);
@@ -160,15 +163,37 @@ export async function handleStatus(ctx: CommandContext): Promise<void> {
160
163
  const nodeVer = process.version;
161
164
  const memMB = (process.memoryUsage.rss() / 1024 / 1024).toFixed(1);
162
165
 
166
+ // Check for available update (async, non-blocking)
167
+ let updateAvailable = "";
168
+ try {
169
+ const { exec } = await import("node:child_process");
170
+ const { promisify } = await import("node:util");
171
+ const execAsync = promisify(exec);
172
+ const { stdout } = await execAsync("npm view @inceptionstack/roundhouse version 2>/dev/null", { timeout: 10_000 });
173
+ const latest = stdout.trim().split("\n").pop()!.trim();
174
+ if (latest && /^\d+\.\d+\.\d+$/.test(latest) && latest !== ROUNDHOUSE_VERSION) {
175
+ // Simple semver comparison: split and compare numerically
176
+ const [lM, lm, lp] = latest.split(".").map(Number);
177
+ const [cM, cm, cp] = ROUNDHOUSE_VERSION.split(".").map(Number);
178
+ if (lM > cM || (lM === cM && lm > cm) || (lM === cM && lm === cm && lp > cp)) {
179
+ updateAvailable = latest;
180
+ }
181
+ }
182
+ } catch { /* network unavailable — skip */ }
183
+
163
184
  const info = agent.getInfo ? agent.getInfo(agentThreadId) : {};
164
185
  const agentVersion = info.version ? `v${info.version}` : "";
165
186
  const agentLabel = agentVersion ? `\`${agent.name}\` (${agentVersion})` : `\`${agent.name}\``;
166
187
 
188
+ const versionLine = updateAvailable
189
+ ? `📦 Roundhouse: v${ROUNDHOUSE_VERSION} → ⬆️ v${updateAvailable} available (/update)`
190
+ : `📦 Roundhouse: v${ROUNDHOUSE_VERSION}`;
191
+
167
192
  const lines = [
168
193
  `📊 *Roundhouse Status*`,
169
194
  ``,
170
195
  `🎫 Session: \`${agentThreadId}\``,
171
- `📦 Roundhouse: v${ROUNDHOUSE_VERSION}`,
196
+ versionLine,
172
197
  `🤖 Agent: ${agentLabel}`,
173
198
  ];
174
199