switchroom 0.13.35 → 0.13.36

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.
@@ -47711,8 +47711,8 @@ var {
47711
47711
  } = import__.default;
47712
47712
 
47713
47713
  // src/build-info.ts
47714
- var VERSION = "0.13.35";
47715
- var COMMIT_SHA = "c41aabe5";
47714
+ var VERSION = "0.13.36";
47715
+ var COMMIT_SHA = "73e8bb05";
47716
47716
 
47717
47717
  // src/cli/agent.ts
47718
47718
  init_source();
@@ -61580,6 +61580,23 @@ function registerVaultBackupCommand(vault, program3) {
61580
61580
  });
61581
61581
  }
61582
61582
 
61583
+ // src/cli/vault-denied-envelope.ts
61584
+ var ENVELOPE_SENTINEL = "ERROR-ENVELOPE:";
61585
+ function writeVaultDeniedEnvelope(vaultKey, brokerCode, human) {
61586
+ const envelope = {
61587
+ v: 1,
61588
+ code: "VAULT-BROKER-DENIED",
61589
+ human: `${brokerCode}: ${human}`,
61590
+ fix: {
61591
+ kind: "request_vault_grant",
61592
+ vault_key: vaultKey
61593
+ },
61594
+ request_id: `vault-cli-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
61595
+ };
61596
+ process.stderr.write(`${ENVELOPE_SENTINEL} ${JSON.stringify(envelope)}
61597
+ `);
61598
+ }
61599
+
61583
61600
  // src/cli/vault.ts
61584
61601
  function isSandboxContext() {
61585
61602
  return process.env.SWITCHROOM_RUNTIME === "docker";
@@ -61805,6 +61822,7 @@ function registerVaultCommand(program3) {
61805
61822
  }
61806
61823
  process.stderr.write(`VAULT-BROKER-DENIED [${result.code}]: ${result.msg}
61807
61824
  `);
61825
+ writeVaultDeniedEnvelope(key, result.code, result.msg);
61808
61826
  process.exit(VAULT_EXIT_DENIED);
61809
61827
  }
61810
61828
  if (inSandbox) {
@@ -62020,6 +62038,7 @@ Push passphrase to broker for future requests? [Y/n]: `);
62020
62038
  process.stderr.write(`VAULT-BROKER-DENIED [${result.code}]: ${result.msg}
62021
62039
  ` + `${recoveryHint("denied", key)}
62022
62040
  `);
62041
+ writeVaultDeniedEnvelope(key, result.code, result.msg);
62023
62042
  process.exit(2);
62024
62043
  }
62025
62044
  } else {
@@ -76198,7 +76217,34 @@ function denyPendingScheduleEntry(opts) {
76198
76217
 
76199
76218
  // src/cli/agent-config-write.ts
76200
76219
  import { existsSync as existsSync73, readFileSync as readFileSync59 } from "node:fs";
76220
+ import { randomUUID as randomUUID5 } from "node:crypto";
76201
76221
  var MAX_ENTRIES_PER_AGENT = 20;
76222
+ var MIN_CRON_INTERVAL_MIN = 5;
76223
+ function extractCronSmallestGapMin(expr) {
76224
+ const fields = expr.trim().split(/\s+/);
76225
+ if (fields.length < 5)
76226
+ return 0;
76227
+ const min = fields[0];
76228
+ if (min === "*")
76229
+ return 1;
76230
+ const step = min.match(/^\*\/(\d+)$/);
76231
+ if (step)
76232
+ return Number(step[1]);
76233
+ if (min.includes(",")) {
76234
+ const parts = min.split(",").map((s) => Number(s)).filter((n) => Number.isFinite(n));
76235
+ if (parts.length >= 2) {
76236
+ const sorted = [...parts].sort((a, b) => a - b);
76237
+ let smallest = Infinity;
76238
+ for (let i = 1;i < sorted.length; i++) {
76239
+ const gap = sorted[i] - sorted[i - 1];
76240
+ if (gap > 0 && gap < smallest)
76241
+ smallest = gap;
76242
+ }
76243
+ return Number.isFinite(smallest) ? smallest : 0;
76244
+ }
76245
+ }
76246
+ return 0;
76247
+ }
76202
76248
  function checkOperatorContext(verb, env2 = process.env) {
76203
76249
  if (env2.SWITCHROOM_OPERATOR === "1")
76204
76250
  return { ok: true };
@@ -76211,8 +76257,45 @@ function checkOperatorContext(verb, env2 = process.env) {
76211
76257
  }
76212
76258
  return { ok: true };
76213
76259
  }
76260
+ function buildEnvelopeForCode(code, message, extra) {
76261
+ const request_id = `agent-config-${randomUUID5()}`;
76262
+ if (code === "E_CRON_TOO_FREQUENT") {
76263
+ return {
76264
+ v: 1,
76265
+ code,
76266
+ human: message,
76267
+ fix: {
76268
+ kind: "quota_exceeded",
76269
+ quota: "cron_min_interval_minutes",
76270
+ current: typeof extra.requested_interval_min === "number" ? extra.requested_interval_min : 0,
76271
+ limit: MIN_CRON_INTERVAL_MIN
76272
+ },
76273
+ request_id
76274
+ };
76275
+ }
76276
+ if (code === "E_QUOTA_EXCEEDED") {
76277
+ const current = typeof extra.current === "number" ? extra.current : MAX_ENTRIES_PER_AGENT;
76278
+ return {
76279
+ v: 1,
76280
+ code,
76281
+ human: message,
76282
+ fix: {
76283
+ kind: "quota_exceeded",
76284
+ quota: "schedule_entries_per_agent",
76285
+ current,
76286
+ limit: MAX_ENTRIES_PER_AGENT
76287
+ },
76288
+ request_id
76289
+ };
76290
+ }
76291
+ return;
76292
+ }
76214
76293
  function emitError(code, message, extra = {}) {
76215
- process.stderr.write(JSON.stringify({ code, message, ...extra }) + `
76294
+ const error_envelope = buildEnvelopeForCode(code, message, extra);
76295
+ const line = { code, message, ...extra };
76296
+ if (error_envelope)
76297
+ line.error_envelope = error_envelope;
76298
+ process.stderr.write(JSON.stringify(line) + `
76216
76299
  `);
76217
76300
  }
76218
76301
  function exitCodeFor(code) {
@@ -76276,7 +76359,8 @@ function scheduleAdd(opts) {
76276
76359
  ok: false,
76277
76360
  code: "E_CRON_TOO_FREQUENT",
76278
76361
  message: "cron interval is tighter than the minimum (5 minutes)",
76279
- exit: 9
76362
+ exit: 9,
76363
+ meta: { requested_interval_min: extractCronSmallestGapMin(opts.cronExpr) }
76280
76364
  };
76281
76365
  }
76282
76366
  const rej = filterOverlaySecrets(dry.doc, "overlay");
@@ -76294,7 +76378,8 @@ function scheduleAdd(opts) {
76294
76378
  ok: false,
76295
76379
  code: "E_QUOTA_EXCEEDED",
76296
76380
  message: `agent already has ${existing.length} overlay entries (max ${MAX_ENTRIES_PER_AGENT})`,
76297
- exit: 9
76381
+ exit: 9,
76382
+ meta: { current: existing.length }
76298
76383
  };
76299
76384
  }
76300
76385
  const hash2 = cronUnitHash(opts.cronExpr, opts.prompt);
@@ -76493,7 +76578,7 @@ function registerAgentConfigWriteCommands(program3) {
76493
76578
  else if (process.env.SWITCHROOM_AGENT_NAME)
76494
76579
  resolvedAgent = process.env.SWITCHROOM_AGENT_NAME;
76495
76580
  if (!r.ok) {
76496
- emitError(r.code, r.message);
76581
+ emitError(r.code, r.message, r.meta ?? {});
76497
76582
  appendAudit(resolvedAgent, "schedule.add", { cron: opts.cron, prompt: opts.prompt, name: opts.name, code: r.code, would_recreate: false }, r.exit);
76498
76583
  process.exit(r.exit);
76499
76584
  }
@@ -76606,7 +76691,7 @@ function registerAgentConfigWriteCommands(program3) {
76606
76691
  else if (process.env.SWITCHROOM_AGENT_NAME)
76607
76692
  resolvedAgent = process.env.SWITCHROOM_AGENT_NAME;
76608
76693
  if (!r.ok) {
76609
- emitError(r.code, r.message);
76694
+ emitError(r.code, r.message, r.meta ?? {});
76610
76695
  appendAudit(resolvedAgent, "schedule.remove", { ...opts, code: r.code }, r.exit);
76611
76696
  process.exit(r.exit);
76612
76697
  }
@@ -14920,16 +14920,6 @@ function deniedResponse(request_id, error, duration_ms = 0) {
14920
14920
  error
14921
14921
  };
14922
14922
  }
14923
- function errorResponse(request_id, error, duration_ms = 0) {
14924
- return {
14925
- v: 1,
14926
- request_id,
14927
- result: "error",
14928
- exit_code: null,
14929
- duration_ms,
14930
- error
14931
- };
14932
- }
14933
14923
 
14934
14924
  // src/analytics/error-friction.ts
14935
14925
  import { createHash as createHash2 } from "node:crypto";
@@ -19515,6 +19505,11 @@ class ErrorBuilder {
19515
19505
  return this;
19516
19506
  }
19517
19507
  docs(url) {
19508
+ try {
19509
+ new URL(url);
19510
+ } catch {
19511
+ throw new TypeError(`err().docs(): invalid URL: ${JSON.stringify(url)} — ErrorEnvelopeSchema requires a fully-qualified URL`);
19512
+ }
19518
19513
  this._docs = url;
19519
19514
  return this;
19520
19515
  }
@@ -20391,6 +20386,14 @@ var CONFIG_APPROVAL_TIMEOUT_MS = 10 * 60 * 1000;
20391
20386
  function defaultApprovalId() {
20392
20387
  return randomBytes(4).toString("hex");
20393
20388
  }
20389
+ function formatConfigApprovalDenyError(approval, approvalId) {
20390
+ if (approval.denySource === "dispatch_failure") {
20391
+ const detail = approval.reason ?? "card dispatch failed";
20392
+ return `E_APPROVAL_DISPATCH_FAILED: ${detail} (approval_id=${approvalId})`;
20393
+ }
20394
+ const suffix = approval.reason ? `: ${approval.reason}` : "";
20395
+ return `E_DENIED: operator denied config_propose_edit${suffix} (approval_id=${approvalId})`;
20396
+ }
20394
20397
  function unlinkSyncBestEffort(path2) {
20395
20398
  try {
20396
20399
  unlinkSync(path2);
@@ -20627,8 +20630,10 @@ class HostdServer {
20627
20630
  resp = await this.handleConfigProposeEdit(req, caller, started);
20628
20631
  break;
20629
20632
  }
20630
- } catch (err2) {
20631
- resp = errorResponse(req.request_id, `hostd dispatch failed: ${err2.message}`, Date.now() - started);
20633
+ } catch (e) {
20634
+ const msg = e.message;
20635
+ resp = err("E_DISPATCH_FAILED", "hostd dispatch failed").why(msg).op(req.op).caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
20636
+ resp = { ...resp, error: `hostd dispatch failed: ${msg}` };
20632
20637
  }
20633
20638
  await this.writeAudit({ caller, req, resp });
20634
20639
  socket.write(encodeResponse(resp));
@@ -20924,7 +20929,7 @@ class HostdServer {
20924
20929
  async handleConfigProposeEdit(req, caller, started) {
20925
20930
  const enabled = this.opts.config.hostd?.config_edit_enabled === true;
20926
20931
  if (!enabled) {
20927
- return err("E_CONFIG_EDIT_DISABLED", "config_propose_edit is disabled").why("operator opt-in per RFC §3.3").fixFlipFlag("hostd.config_edit_enabled", true).docs("https://switchroom.dev/docs/config-edit#opt-in").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
20932
+ return err("E_CONFIG_EDIT_DISABLED", "config_propose_edit is disabled").why("operator opt-in per RFC §3.3").fixFlipFlag("hostd.config_edit_enabled", true).docs("https://switchroom.dev/docs/config-edit#opt-in").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).asDenied().build(req.request_id, Date.now() - started);
20928
20933
  }
20929
20934
  const configPath = this.opts.configPath ?? req.args.target_path;
20930
20935
  const verdict = validateConfigEdit({
@@ -20933,10 +20938,12 @@ class HostdServer {
20933
20938
  unifiedDiff: req.args.unified_diff
20934
20939
  });
20935
20940
  if (!verdict.ok) {
20936
- return errorResponse(req.request_id, `${verdict.code}: ${verdict.detail}`, Date.now() - started);
20941
+ return err(verdict.code, verdict.detail).fixBadInput("unified_diff").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
20937
20942
  }
20938
20943
  if (!this.opts.approvalGateway) {
20939
- return errorResponse(req.request_id, "E_NO_APPROVAL_GATEWAY: validation passed but hostd was " + "started without an approval-gateway wiring; the operator " + "build is missing the telegram-plugin link", Date.now() - started);
20944
+ return err("E_NO_APPROVAL_GATEWAY", "validation passed but hostd was started without an approval-gateway wiring; the operator build is missing the telegram-plugin link").fixOperatorAction("infra", [
20945
+ "ensure hostd was launched with --approval-gateway / telegram-plugin link"
20946
+ ]).op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
20940
20947
  }
20941
20948
  const callerName = caller.kind === "agent" ? caller.name : "operator";
20942
20949
  const approvalId = (this.opts.generateApprovalId ?? defaultApprovalId)();
@@ -20948,35 +20955,45 @@ class HostdServer {
20948
20955
  timeoutMs: CONFIG_APPROVAL_TIMEOUT_MS
20949
20956
  });
20950
20957
  if (approval.verdict === "deny") {
20951
- return errorResponse(req.request_id, `E_DENIED: operator denied config_propose_edit (approval_id=${approvalId})`, Date.now() - started);
20958
+ const legacy = formatConfigApprovalDenyError(approval, approvalId);
20959
+ const isDispatchFailure = approval.denySource === "dispatch_failure";
20960
+ const code = isDispatchFailure ? "E_APPROVAL_DISPATCH_FAILED" : "E_DENIED";
20961
+ const human = isDispatchFailure ? "approval card dispatch failed before the operator could see it" : "operator denied config_propose_edit";
20962
+ const b = err(code, human).why(legacy).fixOperatorAction(isDispatchFailure ? "infra" : "policy_denied", isDispatchFailure ? ["check telegram-plugin gateway connectivity"] : undefined).op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined);
20963
+ if (!isDispatchFailure)
20964
+ b.asDenied();
20965
+ const built = b.build(req.request_id, Date.now() - started);
20966
+ return { ...built, error: legacy };
20952
20967
  }
20953
20968
  if (approval.verdict === "timeout") {
20954
- return errorResponse(req.request_id, `E_APPROVAL_TIMEOUT: operator approval card expired without a tap (approval_id=${approvalId})`, Date.now() - started);
20969
+ const legacy = `E_APPROVAL_TIMEOUT: operator approval card expired without a tap (approval_id=${approvalId})`;
20970
+ const built = err("E_APPROVAL_TIMEOUT", "operator approval card expired without a tap").why(`approval_id=${approvalId}`).fixOperatorAction("policy_denied").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).asDenied().build(req.request_id, Date.now() - started);
20971
+ return { ...built, error: legacy };
20955
20972
  }
20956
20973
  const release = await this.acquireConfigApplyLock();
20957
20974
  try {
20958
20975
  let snapshot;
20959
20976
  try {
20960
20977
  snapshot = readFileSync5(configPath, "utf-8");
20961
- } catch (err2) {
20978
+ } catch (e) {
20962
20979
  await approval.finalize({
20963
20980
  outcome: "reconcile_failed_rolled_back",
20964
- detail: `pre-write snapshot read failed: ${err2.message}`
20981
+ detail: `pre-write snapshot read failed: ${e.message}`
20965
20982
  });
20966
- return errorResponse(req.request_id, `E_RECONCILE_FAILED_ROLLED_BACK: snapshot read failed: ${err2.message}`, Date.now() - started);
20983
+ return this.reconcileFailedRolledBack(`snapshot read failed: ${e.message}`, req, caller, started);
20967
20984
  }
20968
20985
  const postApply = verdict.postApplyContent;
20969
20986
  const tmp = configPath + ".tmp";
20970
20987
  try {
20971
20988
  writeFileSync3(tmp, postApply);
20972
20989
  renameSync(tmp, configPath);
20973
- } catch (err2) {
20990
+ } catch (e) {
20974
20991
  unlinkSyncBestEffort(tmp);
20975
20992
  await approval.finalize({
20976
20993
  outcome: "reconcile_failed_rolled_back",
20977
- detail: `atomic write failed: ${err2.message}`
20994
+ detail: `atomic write failed: ${e.message}`
20978
20995
  });
20979
- return errorResponse(req.request_id, `E_RECONCILE_FAILED_ROLLED_BACK: write failed: ${err2.message}`, Date.now() - started);
20996
+ return this.reconcileFailedRolledBack(`write failed: ${e.message}`, req, caller, started);
20980
20997
  }
20981
20998
  const runner = this.opts.runReconcile ?? (async () => this.runSwitchroom(["apply"]));
20982
20999
  const recRes = await runner({ requestId: approvalId });
@@ -20996,13 +21013,13 @@ class HostdServer {
20996
21013
  try {
20997
21014
  writeFileSync3(tmp, snapshot);
20998
21015
  renameSync(tmp, configPath);
20999
- } catch (err2) {
21000
- rollbackDetail = `snapshot restore failed: ${err2.message}`;
21016
+ } catch (e) {
21017
+ rollbackDetail = `snapshot restore failed: ${e.message}`;
21001
21018
  await approval.finalize({
21002
21019
  outcome: "reconcile_failed_rolled_back",
21003
21020
  detail: rollbackDetail
21004
21021
  });
21005
- return errorResponse(req.request_id, `E_RECONCILE_FAILED_ROLLED_BACK: ${rollbackDetail}`, Date.now() - started);
21022
+ return this.reconcileFailedRolledBack(rollbackDetail, req, caller, started);
21006
21023
  }
21007
21024
  const recRes2 = await runner({ requestId: approvalId });
21008
21025
  const recoveryNote = recRes2.exit_code === 0 ? "rolled back successfully" : `rolled back but recovery reconcile also failed (exit ${recRes2.exit_code})`;
@@ -21010,11 +21027,18 @@ class HostdServer {
21010
21027
  outcome: "reconcile_failed_rolled_back",
21011
21028
  detail: recoveryNote
21012
21029
  });
21013
- return errorResponse(req.request_id, `E_RECONCILE_FAILED_ROLLED_BACK: reconcile exit ${recRes.exit_code}; ${recoveryNote}`, Date.now() - started);
21030
+ return this.reconcileFailedRolledBack(`reconcile exit ${recRes.exit_code}; ${recoveryNote}`, req, caller, started);
21014
21031
  } finally {
21015
21032
  release();
21016
21033
  }
21017
21034
  }
21035
+ reconcileFailedRolledBack(detail, req, caller, started) {
21036
+ const legacy = `E_RECONCILE_FAILED_ROLLED_BACK: ${detail}`;
21037
+ const built = err("E_RECONCILE_FAILED_ROLLED_BACK", "config write or reconcile failed; live file rolled back to snapshot").why(detail).fixOperatorAction("infra", [
21038
+ "inspect hostd logs + switchroom apply output to identify the underlying failure"
21039
+ ]).op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
21040
+ return { ...built, error: legacy };
21041
+ }
21018
21042
  configApplyLock = Promise.resolve();
21019
21043
  async acquireConfigApplyLock() {
21020
21044
  let release;
@@ -21180,7 +21204,9 @@ class HostdServer {
21180
21204
  handleGetStatus(req, _caller, started) {
21181
21205
  const entry = this.statusByRequestId.get(req.args.target_request_id);
21182
21206
  if (!entry) {
21183
- return errorResponse(req.request_id, `get_status: internal: entry missing despite gate accept`, Date.now() - started);
21207
+ const legacy = `get_status: internal: entry missing despite gate accept`;
21208
+ const built = err("E_INTERNAL", "entry missing despite gate accept").op("get_status").build(req.request_id, Date.now() - started);
21209
+ return { ...built, error: legacy };
21184
21210
  }
21185
21211
  return this.statusEntryToResponse(req.request_id, entry);
21186
21212
  }
@@ -21339,6 +21365,8 @@ class SocketApprovalGateway {
21339
21365
  if (sockPath === null) {
21340
21366
  return {
21341
21367
  verdict: "deny",
21368
+ reason: "no reachable telegram gateway socket for this agent",
21369
+ denySource: "dispatch_failure",
21342
21370
  finalize: async () => {}
21343
21371
  };
21344
21372
  }
@@ -21379,7 +21407,12 @@ class SocketApprovalGateway {
21379
21407
  return;
21380
21408
  resolved = true;
21381
21409
  log(`request_config_approval write failed (requestId=${req.requestId}): ${err2.message}`);
21382
- resolve6({ verdict: "deny", finalize: async () => {} });
21410
+ resolve6({
21411
+ verdict: "deny",
21412
+ reason: `request_config_approval write failed: ${err2.message}`,
21413
+ denySource: "dispatch_failure",
21414
+ finalize: async () => {}
21415
+ });
21383
21416
  }
21384
21417
  });
21385
21418
  client2.on("data", (chunk2) => {
@@ -21400,8 +21433,13 @@ class SocketApprovalGateway {
21400
21433
  const obj = parsed;
21401
21434
  if (obj.type === "config_approval_resolved" && obj.requestId === req.requestId && (obj.verdict === "approve" || obj.verdict === "deny" || obj.verdict === "timeout") && !resolved) {
21402
21435
  resolved = true;
21436
+ const verdict = obj.verdict;
21437
+ const reasonField = typeof obj.reason === "string" ? obj.reason : undefined;
21438
+ const denySource = verdict === "deny" ? obj.denySource === "dispatch_failure" ? "dispatch_failure" : "operator" : undefined;
21403
21439
  resolve6({
21404
- verdict: obj.verdict,
21440
+ verdict,
21441
+ ...reasonField !== undefined ? { reason: reasonField } : {},
21442
+ ...denySource !== undefined ? { denySource } : {},
21405
21443
  finalize
21406
21444
  });
21407
21445
  }
@@ -21412,13 +21450,23 @@ class SocketApprovalGateway {
21412
21450
  return;
21413
21451
  resolved = true;
21414
21452
  log(`gateway socket error (requestId=${req.requestId}): ${err2.message}`);
21415
- resolve6({ verdict: "deny", finalize: async () => {} });
21453
+ resolve6({
21454
+ verdict: "deny",
21455
+ reason: `gateway socket error: ${err2.message}`,
21456
+ denySource: "dispatch_failure",
21457
+ finalize: async () => {}
21458
+ });
21416
21459
  });
21417
21460
  client2.on("close", () => {
21418
21461
  if (resolved)
21419
21462
  return;
21420
21463
  resolved = true;
21421
- resolve6({ verdict: "deny", finalize: async () => {} });
21464
+ resolve6({
21465
+ verdict: "deny",
21466
+ reason: "gateway socket closed before verdict",
21467
+ denySource: "dispatch_failure",
21468
+ finalize: async () => {}
21469
+ });
21422
21470
  });
21423
21471
  });
21424
21472
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.13.35",
3
+ "version": "0.13.36",
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": {