switchroom 0.16.15 → 0.16.20

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.
@@ -51692,8 +51692,8 @@ import { existsSync, readFileSync } from "node:fs";
51692
51692
  import { dirname, join } from "node:path";
51693
51693
 
51694
51694
  // src/build-info.ts
51695
- var VERSION = "0.16.15";
51696
- var COMMIT_SHA = "a9c59169";
51695
+ var VERSION = "0.16.20";
51696
+ var COMMIT_SHA = "6ea6e873";
51697
51697
 
51698
51698
  // src/cli/resolve-version.ts
51699
51699
  function readPackageVersion() {
@@ -77519,11 +77519,23 @@ function agentLiveness(config, agentName) {
77519
77519
  return "offline";
77520
77520
  }
77521
77521
  function toHermesSession(agent, liveness) {
77522
+ const nowSec = Math.floor(Date.now() / 1000);
77522
77523
  return {
77523
77524
  id: agent.name,
77524
77525
  name: agent.name,
77526
+ title: agent.name,
77525
77527
  status: liveness,
77526
77528
  model: "claude",
77529
+ is_active: liveness === "active",
77530
+ started_at: nowSec,
77531
+ last_active: nowSec,
77532
+ ended_at: null,
77533
+ input_tokens: 0,
77534
+ output_tokens: 0,
77535
+ message_count: 0,
77536
+ tool_call_count: 0,
77537
+ preview: null,
77538
+ source: "switchroom",
77527
77539
  created_at: null,
77528
77540
  updated_at: null,
77529
77541
  quota: agent.primaryAccount ? { used_pct: null, slot: agent.primaryAccount } : null
@@ -77596,11 +77608,19 @@ async function injectInbound(agentsDir, agentName, chatId, threadId, text, promp
77596
77608
  });
77597
77609
  }
77598
77610
  async function handleHermesRest(method, pathname, config) {
77599
- if (method === "GET" && pathname === "/api/sessions") {
77611
+ if (method === "GET" && (pathname === "/api/sessions" || pathname === "/api/profiles/sessions")) {
77600
77612
  const agents = await handleGetAgents(config);
77601
- const agentsDir = resolveAgentsDir(config);
77602
77613
  const sessions = agents.map((a) => toHermesSession(a, agentLiveness(config, a.name)));
77603
- return { status: 200, body: { sessions } };
77614
+ return {
77615
+ status: 200,
77616
+ body: {
77617
+ sessions,
77618
+ total: sessions.length,
77619
+ limit: sessions.length,
77620
+ offset: 0,
77621
+ profile_totals: { default: sessions.length }
77622
+ }
77623
+ };
77604
77624
  }
77605
77625
  const sessionMatch = pathname.match(/^\/api\/sessions\/([^/]+)$/);
77606
77626
  if (method === "GET" && sessionMatch) {
@@ -77624,30 +77644,81 @@ async function handleHermesRest(method, pathname, config) {
77624
77644
  return { status: 200, body: { history: result.turns ?? [] } };
77625
77645
  }
77626
77646
  if (method === "GET" && pathname === "/api/status") {
77627
- const agents = await handleGetAgents(config);
77628
- const fleet = agents.map((a) => ({
77629
- name: a.name,
77630
- status: agentLiveness(config, a.name)
77631
- }));
77632
77647
  return {
77633
77648
  status: 200,
77634
77649
  body: {
77635
- status: "ok",
77636
- provider: "switchroom",
77637
- agents: fleet
77650
+ version: "switchroom",
77651
+ gateway_running: true,
77652
+ gateway_platforms: {},
77653
+ gateway_state: "ready",
77654
+ config_version: 0,
77655
+ latest_config_version: 0,
77656
+ hermes_home: "switchroom",
77657
+ release_date: null,
77658
+ active_sessions: 0,
77659
+ config_path: null,
77660
+ env_path: null,
77661
+ gateway_exit_reason: null,
77662
+ gateway_health_url: null,
77663
+ gateway_pid: null,
77664
+ gateway_updated_at: null
77638
77665
  }
77639
77666
  };
77640
77667
  }
77641
77668
  if (method === "GET" && pathname === "/api/config") {
77642
- const agentNames = Object.keys(config.agents ?? {});
77643
77669
  return {
77644
77670
  status: 200,
77645
77671
  body: {
77646
77672
  provider: "switchroom",
77647
- agents: agentNames
77673
+ model: null,
77674
+ context_length: null,
77675
+ system_prompt: null
77676
+ }
77677
+ };
77678
+ }
77679
+ if (method === "GET" && (pathname === "/api/config/defaults" || pathname === "/api/config/schema")) {
77680
+ return { status: 200, body: {} };
77681
+ }
77682
+ if (method === "GET" && pathname === "/api/model/info") {
77683
+ return {
77684
+ status: 200,
77685
+ body: {
77686
+ model: "claude",
77687
+ provider: "switchroom",
77688
+ capabilities: {}
77648
77689
  }
77649
77690
  };
77650
77691
  }
77692
+ if (method === "GET" && pathname.startsWith("/api/logs")) {
77693
+ return { status: 200, body: { file: "gateway.log", lines: [] } };
77694
+ }
77695
+ if (method === "GET" && pathname.startsWith("/api/cron")) {
77696
+ if (pathname === "/api/cron/jobs")
77697
+ return { status: 200, body: [] };
77698
+ if (pathname.includes("/runs"))
77699
+ return { status: 200, body: { runs: [] } };
77700
+ if (pathname.includes("sessions")) {
77701
+ return { status: 200, body: { sessions: [], total: 0, limit: 0, offset: 0 } };
77702
+ }
77703
+ return { status: 200, body: {} };
77704
+ }
77705
+ if (method === "GET" && pathname.startsWith("/api/messaging")) {
77706
+ if (pathname === "/api/messaging/platforms")
77707
+ return { status: 200, body: { platforms: [] } };
77708
+ return { status: 200, body: {} };
77709
+ }
77710
+ if (method === "GET" && pathname.startsWith("/api/profiles")) {
77711
+ if (pathname.includes("sessions")) {
77712
+ return { status: 200, body: { sessions: [], total: 0, limit: 0, offset: 0 } };
77713
+ }
77714
+ return { status: 200, body: {} };
77715
+ }
77716
+ if (method === "GET" && pathname === "/api/memory/providers") {
77717
+ return { status: 200, body: {} };
77718
+ }
77719
+ if (method === "GET" && pathname === "/api/fs/default-cwd") {
77720
+ return { status: 200, body: { cwd: null, branch: null } };
77721
+ }
77651
77722
  return null;
77652
77723
  }
77653
77724
  function sendEvent(ctx, type, sessionId, payload) {
@@ -77806,9 +77877,13 @@ async function onHermesMessage(ctx, raw) {
77806
77877
  sendResponse(ctx, rpcErr(id, -32603, result.error ?? "inject failed"));
77807
77878
  break;
77808
77879
  }
77880
+ sendEvent(ctx, "message.delta", sessionId, {
77881
+ text: `*(Prompt sent to ${sessionId} \u2014 response will appear in Telegram)*`,
77882
+ prompt_key: promptKey
77883
+ });
77809
77884
  sendEvent(ctx, "message.complete", sessionId, {
77810
- prompt_key: promptKey,
77811
- note: "reply delivered to Telegram thread"
77885
+ text: `*(Prompt sent to ${sessionId} \u2014 response will appear in Telegram)*`,
77886
+ prompt_key: promptKey
77812
77887
  });
77813
77888
  sendResponse(ctx, rpcOk(id, { ok: true, prompt_key: promptKey }));
77814
77889
  break;
@@ -77834,6 +77909,14 @@ async function onHermesMessage(ctx, raw) {
77834
77909
  sendResponse(ctx, rpcOk(id, { ok: true }));
77835
77910
  break;
77836
77911
  }
77912
+ case "setup.status": {
77913
+ sendResponse(ctx, rpcOk(id, { provider_configured: true }));
77914
+ break;
77915
+ }
77916
+ case "setup.runtime_check": {
77917
+ sendResponse(ctx, rpcOk(id, { ok: true }));
77918
+ break;
77919
+ }
77837
77920
  case "approval.request":
77838
77921
  case "approval.respond":
77839
77922
  case "sudo.request":
@@ -22587,7 +22587,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:f
22587
22587
  import { dirname as dirname4, join as join2 } from "node:path";
22588
22588
 
22589
22589
  // src/build-info.ts
22590
- var VERSION = "0.16.15";
22590
+ var VERSION = "0.16.20";
22591
22591
 
22592
22592
  // src/cli/resolve-version.ts
22593
22593
  function readPackageVersion() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.16.15",
3
+ "version": "0.16.20",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -93,6 +93,24 @@ export interface AnswerStreamConfig {
93
93
  ) => Promise<unknown>
94
94
  deleteMessage?: (chatId: string, messageId: number) => Promise<unknown>
95
95
 
96
+ /**
97
+ * Render the raw assistant transcript text into the wire format used for
98
+ * `parse_mode: 'HTML'` sends/edits. Every OTHER outbound lane in the gateway
99
+ * (handleStreamReply, the reply handler, the turn-flush backstop, the PTY
100
+ * partial handler) converts markdown → Telegram HTML via
101
+ * `sanitizeTelegramHtml(markdownToHtml(...))` before sending. The
102
+ * answer-stream lane historically shipped the RAW transcript under
103
+ * `parse_mode: 'HTML'`, so `**bold**` reached the user as literal asterisks
104
+ * and agent narration read as unformatted text.
105
+ *
106
+ * Injected as a dependency (mirroring `renderText` on the PTY partial
107
+ * handler) rather than hard-importing `format`/`html-sanitize` here, so this
108
+ * module stays free of a grammy/format dependency and remains fully
109
+ * testable. When absent, text is sent verbatim — preserving the old
110
+ * behaviour for callers (and tests) that don't wire it.
111
+ */
112
+ renderText?: (text: string) => string
113
+
96
114
  /** Called when a late edit/send resolves but this stream has been superseded. */
97
115
  onSuperseded?: OnSupersededCallback
98
116
  log?: (msg: string) => void
@@ -179,6 +197,7 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
179
197
  replyToMessageId,
180
198
  sendMessage,
181
199
  editMessageText,
200
+ renderText,
182
201
  onSuperseded,
183
202
  log,
184
203
  warn,
@@ -188,6 +207,13 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
188
207
  recordOutbound,
189
208
  } = config
190
209
 
210
+ /**
211
+ * Convert raw transcript text to the wire format before any
212
+ * `parse_mode: 'HTML'` send/edit. Falls back to the verbatim text when no
213
+ * renderer is injected (old behaviour / unwired tests).
214
+ */
215
+ const render = (text: string): string => (renderText != null ? renderText(text) : text)
216
+
191
217
  const effectiveThrottle = Math.max(250, throttleMs)
192
218
 
193
219
  // Stream state
@@ -229,6 +255,10 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
229
255
  }
230
256
 
231
257
  async function sendOrEditViaMessage(trimmed: string, gen: number, prevText: string): Promise<void> {
258
+ // Convert raw transcript markdown → Telegram HTML before sending under
259
+ // parse_mode: 'HTML'. Without this, `**bold**` ships as literal asterisks
260
+ // (the answer-stream-raw-markdown bug). Mirrors every other outbound lane.
261
+ const rendered = render(trimmed)
232
262
  if (typeof streamMsgId === 'number') {
233
263
  // Edit existing message
234
264
  const editParams: Parameters<typeof editMessageText>[3] = {
@@ -237,7 +267,7 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
237
267
  }
238
268
  if (threadId != null) editParams.message_thread_id = threadId
239
269
  try {
240
- await editMessageText(chatId, streamMsgId, trimmed, editParams)
270
+ await editMessageText(chatId, streamMsgId, rendered, editParams)
241
271
  onMetric?.({ kind: 'answer_lane_update', chatId, messageId: streamMsgId, charCount: trimmed.length, transport: 'edit' })
242
272
  } catch (err) {
243
273
  const msg = err instanceof Error ? err.message : String(err)
@@ -266,7 +296,7 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
266
296
  }
267
297
  if (threadId != null) sendParams.message_thread_id = threadId
268
298
  if (replyToMessageId != null) sendParams.reply_parameters = { message_id: replyToMessageId }
269
- const sent = await sendMessage(chatId, trimmed, sendParams)
299
+ const sent = await sendMessage(chatId, rendered, sendParams)
270
300
  const sentId = sent?.message_id
271
301
  if (typeof sentId !== 'number' || !Number.isFinite(sentId)) {
272
302
  warn?.('answer-stream: sendMessage returned no message_id')
@@ -424,7 +454,11 @@ export function createAnswerStream(config: AnswerStreamConfig): AnswerStreamHand
424
454
  // nested quote that looks wrong.
425
455
 
426
456
  try {
427
- const sent = await sendMessage(chatId, textToSend, sendParams)
457
+ // Render markdown Telegram HTML for the wire send. The dedup /
458
+ // silent-marker / history checks above all run on the raw textToSend
459
+ // (so they match the comparisons the other lanes make on raw text);
460
+ // only the actual outbound payload is converted.
461
+ const sent = await sendMessage(chatId, render(textToSend), sendParams)
428
462
  const sentId = sent?.message_id
429
463
  if (typeof sentId === 'number' && Number.isFinite(sentId)) {
430
464
  streamMsgId = sentId