@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
@@ -0,0 +1,111 @@
1
+ /**
2
+ * transports/telegram/telegram-adapter.ts — Telegram transport adapter
3
+ *
4
+ * Implements TransportAdapter for Telegram, composing existing
5
+ * utility modules (format, html, progress, notify, bot-commands).
6
+ */
7
+
8
+ import type { TransportAdapter, ChatThread, IncomingMessage, PairingResult } from "../types";
9
+ import { isTelegramThread, postTelegramHtml } from "./html";
10
+ import { sendTelegramToMany } from "./notify";
11
+ import { BOT_COMMANDS } from "./bot-commands";
12
+ import { readPendingPairing, completePendingPairing, clearPendingPairing, isStartForNonce } from "./pairing";
13
+
14
+ const TELEGRAM_FORMAT_HINT = "[Format your final answer to be telegram-friendly.]";
15
+
16
+ export class TelegramAdapter implements TransportAdapter {
17
+ readonly name = "telegram";
18
+
19
+ enrichPrompt(text: string): string {
20
+ return `${text}\n\n${TELEGRAM_FORMAT_HINT}`;
21
+ }
22
+
23
+ async postMessage(thread: ChatThread, text: string): Promise<void> {
24
+ if (!isTelegramThread(thread as any)) {
25
+ throw new Error("TelegramAdapter.postMessage called with non-Telegram thread");
26
+ }
27
+ await postTelegramHtml(thread as any, text);
28
+ }
29
+
30
+ async registerCommands(token: string): Promise<void> {
31
+ if (!token) return;
32
+ try {
33
+ const res = await fetch(`https://api.telegram.org/bot${token}/setMyCommands`, {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/json" },
36
+ body: JSON.stringify({ commands: BOT_COMMANDS }),
37
+ });
38
+ if (res.ok) {
39
+ console.log(`[roundhouse] registered ${BOT_COMMANDS.length} bot commands with Telegram`);
40
+ } else {
41
+ const body = await res.text().catch(() => "");
42
+ console.warn(`[roundhouse] failed to register bot commands (${res.status}): ${body.slice(0, 200)}`);
43
+ }
44
+ } catch (err) {
45
+ console.warn(`[roundhouse] bot command registration error:`, (err as Error).message);
46
+ }
47
+ }
48
+
49
+ ownsThread(thread: ChatThread): boolean {
50
+ return isTelegramThread(thread as any);
51
+ }
52
+
53
+ async notify(chatIds: number[], text: string): Promise<void> {
54
+ if (!process.env.TELEGRAM_BOT_TOKEN) {
55
+ console.warn("[roundhouse] TELEGRAM_BOT_TOKEN not set — skipping notification");
56
+ return;
57
+ }
58
+ await sendTelegramToMany(chatIds, text);
59
+ }
60
+
61
+ async isPairingPending(): Promise<boolean> {
62
+ const pending = await readPendingPairing();
63
+ return pending?.status === "pending";
64
+ }
65
+
66
+ async handlePairing(thread: ChatThread, message: IncomingMessage): Promise<PairingResult | null> {
67
+ const text = (message.text ?? "").trim();
68
+ if (!text) return null;
69
+
70
+ const pending = await readPendingPairing();
71
+ if (!pending || pending.status !== "pending" || !isStartForNonce(text, pending.nonce)) {
72
+ return null;
73
+ }
74
+
75
+ // Verify author is allowed
76
+ const authorName = (message.author?.userName ?? message.author?.name ?? "").toLowerCase();
77
+ const originalName = message.author?.userName ?? message.author?.name ?? "";
78
+ const allowed = pending.allowedUsers.map(u => u.toLowerCase());
79
+ if (!authorName || !allowed.includes(authorName)) {
80
+ console.log(`[roundhouse] Pairing nonce from unauthorized user @${originalName}`);
81
+ return null;
82
+ }
83
+
84
+ // Extract Telegram-specific IDs
85
+ const msg = message as any;
86
+ const chatId = typeof msg.chatId === "number"
87
+ ? msg.chatId
88
+ : typeof thread.id === "string" && thread.id.startsWith("telegram:")
89
+ ? parseInt(thread.id.split(":")[1], 10)
90
+ : undefined;
91
+
92
+ const rawUserId = msg.author?.userId ?? msg.author?.id ?? msg.raw?.from?.id;
93
+ const userId = typeof rawUserId === "number"
94
+ ? rawUserId
95
+ : typeof rawUserId === "string"
96
+ ? parseInt(rawUserId, 10)
97
+ : undefined;
98
+
99
+ if (chatId == null || Number.isNaN(chatId) || userId == null || Number.isNaN(userId)) {
100
+ console.error(`[roundhouse] Pairing nonce matched but could not extract IDs: chatId=${chatId} userId=${userId} (raw: msg.chatId=${message.chatId}, thread.id=${thread.id}, author.userId=${message.author?.userId}, author.id=${message.author?.id}, raw.from.id=${message.raw?.from?.id})`);
101
+ await clearPendingPairing();
102
+ await thread.post("⚠️ Pairing failed — could not capture your Telegram IDs. Run: roundhouse setup --telegram");
103
+ return null;
104
+ }
105
+
106
+ // Mark pairing complete in transport state
107
+ await completePendingPairing({ chatId, userId, username: originalName });
108
+
109
+ return { threadId: chatId, userId, username: originalName };
110
+ }
111
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * transports/types.ts — Transport adapter interface
3
+ *
4
+ * Defines the contract for platform-specific transport adapters.
5
+ * The gateway uses this interface to remain transport-agnostic.
6
+ */
7
+
8
+ /** Minimal thread interface (subset of Chat SDK thread) */
9
+ export interface ChatThread {
10
+ id: string;
11
+ post(text: string): Promise<void>;
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ /** Minimal incoming message interface */
16
+ export interface IncomingMessage {
17
+ text?: string;
18
+ author?: { userName?: string; name?: string; userId?: string | number; id?: string };
19
+ chatId?: number;
20
+ raw?: { from?: { id?: number } };
21
+ [key: string]: unknown;
22
+ }
23
+
24
+ /** Result of a successful transport pairing */
25
+ export interface PairingResult {
26
+ /** Thread/channel ID for notifications */
27
+ threadId: string | number;
28
+ /** User ID for allowlist */
29
+ userId: string | number;
30
+ /** Display name */
31
+ username: string;
32
+ }
33
+
34
+ /**
35
+ * TransportAdapter — platform-specific behavior contract.
36
+ *
37
+ * Encapsulates all concerns specific to a messaging platform
38
+ * (Telegram, Slack, Discord, etc.), keeping the gateway transport-agnostic.
39
+ */
40
+ export interface TransportAdapter {
41
+ /** Transport name (e.g. "telegram") */
42
+ readonly name: string;
43
+
44
+ /** Enrich prompt text before sending to agent (e.g. formatting hints) */
45
+ enrichPrompt(text: string): string;
46
+
47
+ /** Post a message using platform-native formatting */
48
+ postMessage(thread: ChatThread, text: string): Promise<void>;
49
+
50
+ /** Register bot commands with the platform */
51
+ registerCommands(token: string): Promise<void>;
52
+
53
+ /** Check if a thread belongs to this transport */
54
+ ownsThread(thread: ChatThread): boolean;
55
+
56
+ /** Send notifications to configured recipients */
57
+ notify(chatIds: number[], text: string): Promise<void>;
58
+
59
+ /**
60
+ * Check if a pairing flow is pending.
61
+ * Gateway uses this to decide whether to attempt pairing on incoming messages.
62
+ */
63
+ isPairingPending(): Promise<boolean>;
64
+
65
+ /**
66
+ * Try to handle an incoming message as a pairing attempt.
67
+ * Returns PairingResult on success, null if not a pairing message.
68
+ * Transport manages its own state (nonce files, OAuth tokens, etc.)
69
+ */
70
+ handlePairing(thread: ChatThread, message: IncomingMessage): Promise<PairingResult | null>;
71
+ }
@@ -2,7 +2,7 @@
2
2
  * voice/providers/whisper.ts — Local Whisper STT provider
3
3
  *
4
4
  * Runs the whisper CLI via child_process. Auto-detects language.
5
- * Can auto-install whisper via pip3 and warm the model on first use.
5
+ * Reports missing dependencies so the agent can install them.
6
6
  */
7
7
 
8
8
  import { execFile } from "node:child_process";
@@ -21,6 +21,12 @@ const WHISPER_PATHS = [
21
21
  "/usr/bin/whisper",
22
22
  ];
23
23
 
24
+ const FFMPEG_PATHS = [
25
+ join(homedir(), ".local", "bin", "ffmpeg"),
26
+ "/usr/local/bin/ffmpeg",
27
+ "/usr/bin/ffmpeg",
28
+ ];
29
+
24
30
  let cachedBinaryPath: string | null | undefined; // undefined = not checked yet
25
31
 
26
32
  async function findWhisperBinary(): Promise<string | null> {
@@ -33,80 +39,22 @@ async function findWhisperBinary(): Promise<string | null> {
33
39
  return p;
34
40
  } catch {}
35
41
  }
36
- cachedBinaryPath = null;
42
+ // Don't cache null — allows detection after agent installs whisper
37
43
  return null;
38
44
  }
39
45
 
40
- /** Reset cached path so next findWhisperBinary() re-scans */
41
- function invalidateCache(): void {
42
- cachedBinaryPath = undefined;
43
- }
44
-
45
- // ── Auto-install ─────────────────────────────────────
46
-
47
- let pipAvailable: boolean | undefined;
48
-
49
- async function checkPip(): Promise<boolean> {
50
- if (pipAvailable !== undefined) return pipAvailable;
51
- return new Promise<boolean>((resolve) => {
52
- execFile("pip3", ["--version"], { timeout: 5000 }, (err) => {
53
- pipAvailable = !err;
54
- resolve(pipAvailable);
55
- });
56
- });
57
- }
58
-
59
- /**
60
- * Install whisper via pip3 --user. Returns the binary path or null on failure.
61
- */
62
- async function installWhisperWithPip(): Promise<string | null> {
63
- if (!(await checkPip())) {
64
- console.warn("[stt/whisper] pip3 not available — cannot auto-install whisper");
65
- return null;
46
+ async function findFfmpeg(): Promise<string | null> {
47
+ for (const p of FFMPEG_PATHS) {
48
+ try {
49
+ await access(p, constants.X_OK);
50
+ return p;
51
+ } catch {}
66
52
  }
53
+ return null;
54
+ }
67
55
 
68
- console.log("[stt/whisper] installing openai-whisper via pip3...");
69
- return new Promise<string | null>((resolve) => {
70
- execFile(
71
- "pip3",
72
- ["install", "--user", "openai-whisper"],
73
- {
74
- timeout: 300_000, // 5 min for install
75
- maxBuffer: 10 * 1024 * 1024, // 10MB for pip output
76
- env: { ...process.env },
77
- },
78
- async (err, stdout, stderr) => {
79
- if (err) {
80
- console.error("[stt/whisper] pip3 install failed:", err.message);
81
- if (stderr) console.error("[stt/whisper] stderr:", stderr.slice(0, 500));
82
- resolve(null);
83
- return;
84
- }
85
- console.log("[stt/whisper] pip3 install succeeded");
86
-
87
- // Re-discover binary
88
- invalidateCache();
89
- const binary = await findWhisperBinary();
90
- if (!binary) {
91
- console.error("[stt/whisper] installed but binary not found in expected paths");
92
- resolve(null);
93
- return;
94
- }
95
56
 
96
- // Validate with --help
97
- execFile(binary, ["--help"], { timeout: 10_000 }, (helpErr) => {
98
- if (helpErr) {
99
- console.error("[stt/whisper] binary found but --help failed:", helpErr.message);
100
- resolve(null);
101
- } else {
102
- console.log(`[stt/whisper] validated binary at ${binary}`);
103
- resolve(binary);
104
- }
105
- });
106
- },
107
- );
108
- });
109
- }
57
+ // ── Model warmup ─────────────────────────────────────
110
58
 
111
59
  /**
112
60
  * Warm the whisper model by running a tiny transcription.
@@ -171,19 +119,15 @@ async function warmWhisperModel(binary: string, model: string): Promise<boolean>
171
119
 
172
120
  // ── Provider ─────────────────────────────────────────
173
121
 
174
- /** Extended provider with install capability */
122
+ /** Extended provider that reports missing dependencies */
175
123
  export interface InstallableWhisperProvider extends SttProvider {
176
124
  ensureInstalled(): Promise<boolean>;
125
+ getMissingDeps(): Promise<string[]>;
177
126
  }
178
127
 
179
- // Singleton promises to prevent concurrent installs
180
- let installPromise: Promise<string | null> | null = null;
181
- let installFailed = false; // sticky failure to prevent retry spam
182
-
183
128
  export function createWhisperProvider(config: SttProviderConfig): InstallableWhisperProvider {
184
129
  const model = (config.model as string) ?? "small";
185
130
  const timeoutMs = config.timeoutMs ?? 30000;
186
- const autoInstall = config.autoInstall === true; // explicit opt-in only
187
131
  let modelWarmed = false;
188
132
  let warmFailed = false; // sticky failure to prevent warmup retry spam
189
133
  let warmPromise: Promise<boolean> | null = null;
@@ -191,24 +135,14 @@ export function createWhisperProvider(config: SttProviderConfig): InstallableWhi
191
135
  const WHISPER_LANGS = new Set(["af","am","ar","as","az","ba","be","bg","bn","bo","br","bs","ca","cs","cy","da","de","el","en","es","et","eu","fa","fi","fo","fr","gl","gu","ha","haw","he","hi","hr","ht","hu","hy","id","is","it","ja","jw","ka","kk","km","kn","ko","la","lb","ln","lo","lt","lv","mg","mi","mk","ml","mn","mr","ms","mt","my","ne","nl","nn","no","oc","pa","pl","ps","pt","ro","ru","sa","sd","si","sk","sl","sn","so","sq","sr","su","sv","sw","ta","te","tg","th","tk","tl","tr","tt","uk","ur","uz","vi","yi","yo","yue","zh"]);
192
136
 
193
137
  async function getBinary(): Promise<string | null> {
194
- // Check if already available
195
138
  const existing = await findWhisperBinary();
196
- if (existing) return existing;
197
-
198
- // Try auto-install
199
- if (!autoInstall) return null;
200
- if (installFailed) return null; // sticky failure — don't retry every message
201
-
202
- // Singleton: join existing install or start new one
203
- if (!installPromise) {
204
- installPromise = installWhisperWithPip().then((result) => {
205
- if (!result) installFailed = true;
206
- return result;
207
- }).finally(() => {
208
- installPromise = null;
209
- });
210
- }
211
- return installPromise;
139
+ if (!existing) return null;
140
+
141
+ // Also need ffmpeg
142
+ const ffmpeg = await findFfmpeg();
143
+ if (!ffmpeg) return null;
144
+
145
+ return existing;
212
146
  }
213
147
 
214
148
  return {
@@ -218,6 +152,15 @@ export function createWhisperProvider(config: SttProviderConfig): InstallableWhi
218
152
  return input.mime.startsWith("audio/");
219
153
  },
220
154
 
155
+ async getMissingDeps(): Promise<string[]> {
156
+ const missing: string[] = [];
157
+ const whisper = await findWhisperBinary();
158
+ if (!whisper) missing.push("whisper");
159
+ const ffmpeg = await findFfmpeg();
160
+ if (!ffmpeg) missing.push("ffmpeg");
161
+ return missing;
162
+ },
163
+
221
164
  async ensureInstalled(): Promise<boolean> {
222
165
  const binary = await getBinary();
223
166
  if (!binary) return false;
@@ -236,7 +179,7 @@ export function createWhisperProvider(config: SttProviderConfig): InstallableWhi
236
179
  }
237
180
  } catch {}
238
181
 
239
- // Run warmup — catch everything so it never rejects
182
+ // Run warmup
240
183
  try {
241
184
  const ok = await warmWhisperModel(binary, model);
242
185
  if (!ok) warmFailed = true;
@@ -258,7 +201,7 @@ export function createWhisperProvider(config: SttProviderConfig): InstallableWhi
258
201
  async transcribe(input: SttInput): Promise<TranscriptionResult> {
259
202
  const binary = await getBinary();
260
203
  if (!binary) {
261
- throw new Error("whisper not available and auto-install failed");
204
+ throw new Error("whisper or ffmpeg not available");
262
205
  }
263
206
 
264
207
  const outputDir = join(homedir(), ".roundhouse", "whisper-tmp", randomBytes(6).toString("hex"));
@@ -19,7 +19,6 @@ export class SttService {
19
19
  private config: SttConfig;
20
20
  private initPromise: Promise<void> | null = null;
21
21
  private activeStt: Promise<void> = Promise.resolve(); // global concurrency: 1 at a time
22
- private installNoticeSent = false;
23
22
 
24
23
  constructor(config: SttConfig) {
25
24
  this.config = config;
@@ -52,12 +51,7 @@ export class SttService {
52
51
  }
53
52
 
54
53
  try {
55
- // Pass autoInstall from service-level config into provider config
56
- const mergedProviderConfig = {
57
- ...providerConfig,
58
- autoInstall: providerConfig.autoInstall ?? this.config.autoInstall ?? false,
59
- };
60
- this.providers.push(factory(mergedProviderConfig));
54
+ this.providers.push(factory(providerConfig));
61
55
  console.log(`[stt] loaded provider: ${providerName} (${type})`);
62
56
  } catch (err) {
63
57
  console.warn(`[stt] failed to create provider "${providerName}":`, (err as Error).message);
@@ -97,6 +91,34 @@ export class SttService {
97
91
  }
98
92
  }
99
93
 
94
+ /**
95
+ * Check which STT dependencies are missing.
96
+ * Returns empty array if everything is installed, or names like ["whisper", "ffmpeg"].
97
+ * Note: returns assumed deps when no providers loaded (safe fallback for default config).
98
+ */
99
+ async getMissingDeps(): Promise<string[]> {
100
+ try {
101
+ await this.ensureInitialized();
102
+ } catch {
103
+ return ["whisper", "ffmpeg"]; // Can't initialize = assume all missing
104
+ }
105
+
106
+ if (this.providers.length === 0) {
107
+ // No providers loaded — most likely whisper not installed (default config uses whisper).
108
+ // Config typos are logged during doInit(); agent install prompt is a safe fallback.
109
+ return ["whisper", "ffmpeg"];
110
+ }
111
+
112
+ // Returns deps from first provider that supports getMissingDeps (single-provider today)
113
+ for (const provider of this.providers) {
114
+ const installable = provider as InstallableWhisperProvider;
115
+ if (installable.getMissingDeps && typeof installable.getMissingDeps === "function") {
116
+ return installable.getMissingDeps();
117
+ }
118
+ }
119
+ return [];
120
+ }
121
+
100
122
  /** Should this attachment be auto-transcribed? */
101
123
  shouldTranscribe(attachment: MessageAttachment): boolean {
102
124
  if (!this.config.enabled || this.config.mode === "off") return false;
@@ -141,7 +163,7 @@ export class SttService {
141
163
  const duration = await getAudioDuration(attachment.localPath);
142
164
  if (duration !== null && duration > maxDuration) {
143
165
  console.log(`[stt] skipping ${attachment.name}: duration ${duration.toFixed(1)}s exceeds ${maxDuration}s limit`);
144
- return null;
166
+ return { text: "", provider: "none", approximate: true as const, status: "skipped" as const, error: `Duration ${duration.toFixed(0)}s exceeds ${maxDuration}s limit` };
145
167
  }
146
168
  } catch {}
147
169
  }
@@ -169,18 +191,12 @@ export class SttService {
169
191
  for (const provider of this.providers) {
170
192
  if (!provider.canTranscribe(input)) continue;
171
193
 
172
- // Ensure provider is installed (with one-time user notification)
194
+ // Ensure provider is installed
173
195
  const installable = provider as InstallableWhisperProvider;
174
196
  if (installable.ensureInstalled && typeof installable.ensureInstalled === "function") {
175
197
  try {
176
198
  const isReady = await installable.ensureInstalled();
177
- if (!isReady) {
178
- if (!this.installNoticeSent && notify) {
179
- this.installNoticeSent = true;
180
- try { await notify("🎤 Voice transcription not available. Whisper install or model download failed."); } catch {}
181
- }
182
- continue;
183
- }
199
+ if (!isReady) continue;
184
200
  } catch {
185
201
  continue;
186
202
  }
@@ -239,6 +255,9 @@ export async function enrichAttachmentsWithTranscripts(
239
255
  const transcript = await sttService.tryTranscribe(att, undefined, notify);
240
256
  if (transcript) {
241
257
  att.transcript = transcript;
258
+ } else if (att.mediaType === "audio" && sttService.shouldTranscribe(att)) {
259
+ // Mark as failed so gateway can detect and act
260
+ att.transcript = { text: "", provider: "none", approximate: true, status: "failed", error: "No STT provider available" };
242
261
  }
243
262
  } catch (err) {
244
263
  console.error(`[stt] unexpected error transcribing ${att.name}:`, (err as Error).message);
@@ -267,7 +286,6 @@ async function getAudioDuration(filePath: string): Promise<number | null> {
267
286
  export const DEFAULT_STT_CONFIG: SttConfig = {
268
287
  enabled: true,
269
288
  mode: "on",
270
- autoInstall: true,
271
289
  chain: ["whisper"],
272
290
  autoTranscribe: {
273
291
  voiceMessages: true,
@@ -35,7 +35,7 @@ export interface AttachmentTranscript {
35
35
  language?: string;
36
36
  confidence?: number;
37
37
  approximate: true;
38
- status: "completed" | "failed";
38
+ status: "completed" | "failed" | "skipped";
39
39
  error?: string;
40
40
  durationMs?: number;
41
41
  }
@@ -45,14 +45,12 @@ export interface AttachmentTranscript {
45
45
  export interface SttProviderConfig {
46
46
  type: string;
47
47
  timeoutMs?: number;
48
- autoInstall?: boolean;
49
48
  [key: string]: unknown;
50
49
  }
51
50
 
52
51
  export interface SttConfig {
53
52
  enabled: boolean;
54
53
  mode: "on" | "off";
55
- autoInstall?: boolean;
56
54
  chain: string[];
57
55
  autoTranscribe: {
58
56
  voiceMessages: boolean;
@@ -1,69 +0,0 @@
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 "../bundle";
12
-
13
- export interface UpdateProgress {
14
- update(text: string): Promise<void>;
15
- }
16
-
17
- export interface UpdateResult {
18
- action: "already-latest" | "updated";
19
- currentVersion: string;
20
- latestVersion?: string;
21
- }
22
-
23
- /**
24
- * Check for updates, install if newer, provision bundle, patch settings.
25
- * Returns the result — caller decides how to present it and whether to restart.
26
- */
27
- export async function performUpdate(progress: UpdateProgress): Promise<UpdateResult> {
28
- // Get current version
29
- const pkg = await import("../../package.json", { with: { type: "json" } });
30
- const currentVersion = pkg.default?.version ?? "unknown";
31
-
32
- // Check latest version on npm
33
- const latestVersion = execSync("npm view @inceptionstack/roundhouse version 2>/dev/null", {
34
- timeout: 30_000,
35
- encoding: "utf8",
36
- }).trim();
37
-
38
- if (!latestVersion || latestVersion === currentVersion) {
39
- return { action: "already-latest", currentVersion };
40
- }
41
-
42
- await progress.update(`📦 Updating v${currentVersion} → v${latestVersion}...`);
43
-
44
- execSync("npm install -g @inceptionstack/roundhouse@latest 2>&1", {
45
- timeout: 120_000,
46
- encoding: "utf8",
47
- });
48
-
49
- // Provision bundle (skills sync + CLI tools + config)
50
- try {
51
- provisionBundle();
52
- } catch (e) {
53
- console.warn("[roundhouse] bundle provisioning failed:", e instanceof Error ? e.message : e);
54
- }
55
-
56
- // Ensure settings.json includes roundhouse package (for pre-bundle upgrades)
57
- try {
58
- const settingsPath = `${homedir()}/.pi/agent/settings.json`;
59
- const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
60
- const selfPkg = "npm:@inceptionstack/roundhouse";
61
- if (!Array.isArray(settings.packages)) settings.packages = [];
62
- if (!settings.packages.includes(selfPkg)) {
63
- settings.packages.push(selfPkg);
64
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
65
- }
66
- } catch { /* settings.json may not exist yet — fine, setup will create it */ }
67
-
68
- return { action: "updated", currentVersion, latestVersion };
69
- }