switchroom 0.19.18 → 0.19.22

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 (76) hide show
  1. package/dist/agent-scheduler/index.js +2 -1
  2. package/dist/auth-broker/index.js +56 -1
  3. package/dist/cli/drive-write-pretool.mjs +48 -5
  4. package/dist/cli/ms-365-write-pretool.mjs +40 -2
  5. package/dist/cli/notion-write-pretool.mjs +2 -1
  6. package/dist/cli/switchroom.js +5242 -2239
  7. package/dist/host-control/main.js +12241 -11375
  8. package/dist/vault/approvals/kernel-server.js +113 -7
  9. package/dist/vault/broker/server.js +259 -76
  10. package/package.json +6 -3
  11. package/profiles/_base/start.sh.hbs +61 -1
  12. package/skills/switchroom-release/SKILL.md +103 -20
  13. package/telegram-plugin/bridge/bridge.ts +14 -0
  14. package/telegram-plugin/card-format.ts +92 -3
  15. package/telegram-plugin/dist/bridge/bridge.js +13 -0
  16. package/telegram-plugin/dist/gateway/gateway.js +2356 -1159
  17. package/telegram-plugin/dist/server.js +13 -0
  18. package/telegram-plugin/edit-flood-fuse.ts +477 -0
  19. package/telegram-plugin/format.ts +19 -7
  20. package/telegram-plugin/gateway/always-allow-persist-queue.ts +97 -11
  21. package/telegram-plugin/gateway/boot-sweep-gate.ts +164 -0
  22. package/telegram-plugin/gateway/callback-query-handlers.ts +454 -81
  23. package/telegram-plugin/gateway/gateway.ts +66 -56
  24. package/telegram-plugin/gateway/inbound-interceptors.ts +27 -4
  25. package/telegram-plugin/gateway/missed-approvals-store.ts +66 -17
  26. package/telegram-plugin/gateway/narrative-lane.ts +49 -3
  27. package/telegram-plugin/gateway/pending-card-store.ts +46 -16
  28. package/telegram-plugin/gateway/scoped-grant-store.ts +39 -14
  29. package/telegram-plugin/gateway/status-pin-api.ts +145 -0
  30. package/telegram-plugin/gateway/store-file.ts +244 -0
  31. package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +325 -45
  32. package/telegram-plugin/hooks/tool-label-pretool.mjs +88 -2
  33. package/telegram-plugin/retry-api-call.ts +15 -2
  34. package/telegram-plugin/send-gate.ts +1 -1
  35. package/telegram-plugin/status-no-truncate.ts +64 -1
  36. package/telegram-plugin/status-pin-driver.ts +50 -27
  37. package/telegram-plugin/status-pin.ts +43 -5
  38. package/telegram-plugin/tests/activity-card-send-gate.test.ts +275 -0
  39. package/telegram-plugin/tests/activity-card-wiring.test.ts +16 -7
  40. package/telegram-plugin/tests/boot-pin-sweep-wiring.test.ts +101 -0
  41. package/telegram-plugin/tests/boot-sweep-gate.test.ts +293 -0
  42. package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
  43. package/telegram-plugin/tests/bridge-tool-parity.test.ts +95 -0
  44. package/telegram-plugin/tests/edit-flood-fuse.test.ts +431 -0
  45. package/telegram-plugin/tests/pinned-card-collapse.test.ts +356 -0
  46. package/telegram-plugin/tests/status-pin-api.test.ts +178 -0
  47. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +94 -11
  48. package/telegram-plugin/tests/status-pin.test.ts +106 -5
  49. package/telegram-plugin/tests/store-atomic-write.test.ts +411 -0
  50. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +631 -1
  51. package/telegram-plugin/tests/tool-activity-summary.test.ts +28 -12
  52. package/telegram-plugin/tests/tool-label-pretool.test.ts +94 -0
  53. package/telegram-plugin/tests/vault-approval-posture.test.ts +6 -1
  54. package/telegram-plugin/tests/vault-passphrase-retry.test.ts +666 -0
  55. package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +42 -21
  56. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +233 -1
  57. package/telegram-plugin/tests/worker-feed-repeat-steps.test.ts +147 -0
  58. package/telegram-plugin/tool-activity-summary.ts +85 -13
  59. package/telegram-plugin/worker-activity-feed.ts +56 -2
  60. package/vendor/hindsight-memory/scripts/drain_pending.py +847 -67
  61. package/vendor/hindsight-memory/scripts/lib/client.py +124 -0
  62. package/vendor/hindsight-memory/scripts/lib/pending.py +944 -33
  63. package/vendor/hindsight-memory/scripts/lib/retain_split.py +460 -0
  64. package/vendor/hindsight-memory/scripts/recall.py +74 -5
  65. package/vendor/hindsight-memory/scripts/session_start.py +48 -0
  66. package/vendor/hindsight-memory/scripts/tests/test_client_document_exists.py +470 -0
  67. package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +2275 -0
  68. package/vendor/hindsight-memory/scripts/tests/test_pending_failure_class.py +105 -0
  69. package/vendor/hindsight-memory/scripts/tests/test_pending_wedge.py +300 -0
  70. package/vendor/hindsight-memory/scripts/tests/test_recall_degraded_notice.py +365 -0
  71. package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +12 -4
  72. package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +27 -2
  73. package/vendor/hindsight-memory/scripts/tests/test_retain_split.py +438 -0
  74. package/vendor/hindsight-memory/scripts/tests/test_session_start_version_skew.py +204 -0
  75. package/vendor/hindsight-memory/tests/test_drain_pending.py +130 -8
  76. package/vendor/hindsight-memory/tests/test_pending.py +32 -7
@@ -18222,7 +18222,7 @@ var require_lib = __commonJS((exports, module) => {
18222
18222
 
18223
18223
  // src/vault/broker/server.ts
18224
18224
  import * as net from "node:net";
18225
- import { mkdirSync as mkdirSync7, chmodSync as chmodSync5, chownSync, existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as readdirSync4, statSync as statSync6, unlinkSync as unlinkSync6, writeFileSync as writeFileSync3, renameSync as renameSync8 } from "node:fs";
18225
+ import { mkdirSync as mkdirSync7, chmodSync as chmodSync5, chownSync, existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as readdirSync5, statSync as statSync7, unlinkSync as unlinkSync6, writeFileSync as writeFileSync3, renameSync as renameSync8 } from "node:fs";
18226
18226
 
18227
18227
  // src/agents/compose.ts
18228
18228
  init_schema();
@@ -18820,6 +18820,47 @@ for (const name of SHARED_FRAGMENTS) {
18820
18820
  }
18821
18821
  }
18822
18822
 
18823
+ // src/litellm/timeout-budget.ts
18824
+ var LITELLM_ROUTER_MARGIN_S = 10;
18825
+ var LITELLM_TIMEOUT_TIERS = {
18826
+ interactive: {
18827
+ group: "gpt-oss-20b",
18828
+ localTimeoutS: 90,
18829
+ fallbackGroup: "gpt-oss-20b-openrouter",
18830
+ fallbackTimeoutS: 60
18831
+ },
18832
+ retain: {
18833
+ group: "gpt-oss-20b-retain",
18834
+ localTimeoutS: 200,
18835
+ fallbackGroup: "gpt-oss-20b-retain-openrouter",
18836
+ fallbackTimeoutS: 90
18837
+ },
18838
+ consolidation: {
18839
+ group: "gpt-oss-20b-consolidation",
18840
+ localTimeoutS: 200,
18841
+ fallbackGroup: "gpt-oss-20b-consolidation-openrouter",
18842
+ fallbackTimeoutS: 90
18843
+ }
18844
+ };
18845
+ function litellmChainSeconds(tier) {
18846
+ assertPositive(tier.localTimeoutS, `${tier.group} localTimeoutS`);
18847
+ if (tier.fallbackGroup === null)
18848
+ return tier.localTimeoutS;
18849
+ assertPositive(tier.fallbackTimeoutS, `${tier.fallbackGroup} fallbackTimeoutS`);
18850
+ return tier.localTimeoutS + tier.fallbackTimeoutS;
18851
+ }
18852
+ function minimumClientBudgetSeconds(tier, marginS = LITELLM_ROUTER_MARGIN_S) {
18853
+ return litellmChainSeconds(tier) + marginS;
18854
+ }
18855
+ function clientBudgetSeconds(tier, floorS = 0, marginS = LITELLM_ROUTER_MARGIN_S) {
18856
+ return Math.max(Math.ceil(floorS), minimumClientBudgetSeconds(tier, marginS));
18857
+ }
18858
+ function assertPositive(value, label) {
18859
+ if (!Number.isFinite(value) || value <= 0) {
18860
+ throw new Error(`litellm timeout budget: ${label} must be a positive number, got ${value}`);
18861
+ }
18862
+ }
18863
+
18823
18864
  // src/setup/hindsight.ts
18824
18865
  var HINDSIGHT_DEFAULT_API_PORT = 18888;
18825
18866
  var HINDSIGHT_DEFAULT_MCP_URL = `http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/`;
@@ -18829,8 +18870,21 @@ var HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight";
18829
18870
  var HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
18830
18871
  var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
18831
18872
  var HINDSIGHT_CREDS_MIRROR_VOLUME = `consumer-creds-${HINDSIGHT_CONSUMER_NAME}`;
18873
+ var HINDSIGHT_DEFAULT_RETAIN_MAX_COMPLETION_TOKENS = 16384;
18874
+ var HINDSIGHT_RETAIN_MIN_TOKENS_PER_SECOND = 80.6;
18875
+ function hindsightRetainLlmTimeoutSeconds(maxCompletionTokens = HINDSIGHT_DEFAULT_RETAIN_MAX_COMPLETION_TOKENS, tokensPerSecond = HINDSIGHT_RETAIN_MIN_TOKENS_PER_SECOND) {
18876
+ if (!(maxCompletionTokens > 0) || !(tokensPerSecond > 0)) {
18877
+ throw new Error(`hindsight retain budget: maxCompletionTokens (${maxCompletionTokens}) and ` + `tokensPerSecond (${tokensPerSecond}) must both be positive`);
18878
+ }
18879
+ return Math.ceil(maxCompletionTokens / tokensPerSecond);
18880
+ }
18881
+ function hindsightRetainClientTimeoutSeconds() {
18882
+ return clientBudgetSeconds(LITELLM_TIMEOUT_TIERS.retain, hindsightRetainLlmTimeoutSeconds());
18883
+ }
18884
+ var HINDSIGHT_RETAIN_CLIENT_DEADLINE_S = hindsightRetainClientTimeoutSeconds() + LITELLM_ROUTER_MARGIN_S;
18832
18885
  var HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)';
18833
18886
  var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
18887
+ var DOCKER_PROBE_TIMEOUT_MS = 60 * 1000;
18834
18888
 
18835
18889
  // src/memory/hindsight.ts
18836
18890
  var DEFAULT_RETAIN_MISSION = "Extract durable facts that will still be true and useful weeks from now: " + "user preferences and standing rules, ongoing projects and recurring " + "commitments, technical and architectural decisions with their rationale, " + "and people/tool relationships. A preference revealed by a request is " + "durable — record the preference (what the user likes, wants, or always " + `does), not the request itself.
@@ -19546,7 +19600,8 @@ var ApprovalDecisionMetaSchema = exports_external.object({
19546
19600
  ttl_expires_at: exports_external.number().nullable(),
19547
19601
  last_used_at: exports_external.number().nullable(),
19548
19602
  revoked_at: exports_external.number().nullable(),
19549
- revoke_reason: exports_external.string().nullable()
19603
+ revoke_reason: exports_external.string().nullable(),
19604
+ origin: exports_external.enum(["agent", "operator"]).optional()
19550
19605
  });
19551
19606
  var OkApprovalLookupResponseSchema = exports_external.object({
19552
19607
  ok: exports_external.literal(true),
@@ -19745,9 +19800,9 @@ function migrateLegacyGrantsDbLocation(newDbPath = getGrantsDbPath(), deps) {
19745
19800
  var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
19746
19801
 
19747
19802
  // src/vault/broker/server.ts
19748
- import { dirname as dirname7, resolve as resolve9, basename as basename4 } from "node:path";
19803
+ import { dirname as dirname7, resolve as resolve9, basename as basename5 } from "node:path";
19749
19804
  import * as os3 from "node:os";
19750
- import * as path4 from "node:path";
19805
+ import * as path5 from "node:path";
19751
19806
 
19752
19807
  // src/vault/migrate-layout.ts
19753
19808
  import {
@@ -19981,9 +20036,9 @@ function readAutoUnlockFile(filePath) {
19981
20036
  var DEFAULT_AUTO_UNLOCK_PATH = "~/.switchroom/vault-auto-unlock";
19982
20037
 
19983
20038
  // src/vault/broker/audit-log.ts
19984
- import * as fs2 from "node:fs";
20039
+ import * as fs3 from "node:fs";
19985
20040
  import * as os2 from "node:os";
19986
- import * as path2 from "node:path";
20041
+ import * as path3 from "node:path";
19987
20042
 
19988
20043
  // src/util/audit-hashchain.ts
19989
20044
  import { createHash as createHash4 } from "node:crypto";
@@ -20100,76 +20155,107 @@ function safeAuditLogPath(requestedPath) {
20100
20155
  return cachedTmpAuditLog;
20101
20156
  }
20102
20157
 
20103
- // src/vault/broker/audit-log.ts
20104
- function failOpenStatePath(logPath) {
20105
- return `${logPath}.failopen`;
20106
- }
20107
- function readFailOpenState(statePath) {
20158
+ // src/util/log-rotation.ts
20159
+ import * as fs2 from "node:fs";
20160
+ import * as path2 from "node:path";
20161
+ function fsyncAndMeasure(snapshotPath, logPath, tag) {
20162
+ let step = "open";
20108
20163
  try {
20109
- const raw = fs2.readFileSync(statePath, "utf8");
20110
- const parsed = JSON.parse(raw);
20111
- const count = Number(parsed.failOpenCount);
20112
- return {
20113
- failOpenCount: Number.isFinite(count) && count > 0 ? count : 0,
20114
- lastFailureTs: typeof parsed.lastFailureTs === "string" ? parsed.lastFailureTs : undefined,
20115
- lastError: typeof parsed.lastError === "string" ? parsed.lastError : undefined
20116
- };
20117
- } catch {
20118
- return { failOpenCount: 0 };
20164
+ const fd = fs2.openSync(snapshotPath, "r");
20165
+ try {
20166
+ step = "fsync";
20167
+ fs2.fsyncSync(fd);
20168
+ step = "measure";
20169
+ const size = fs2.fstatSync(fd).size;
20170
+ step = "close";
20171
+ return size;
20172
+ } finally {
20173
+ fs2.closeSync(fd);
20174
+ }
20175
+ } catch (err) {
20176
+ process.stderr.write(`[${tag}] ERROR: could not ${step} snapshot ${snapshotPath}; leaving active log ${logPath} intact to avoid data loss: ${err.message}
20177
+ `);
20178
+ return null;
20119
20179
  }
20120
20180
  }
20121
- var DEFAULT_AUDIT_MAX_BYTES = 32 * 1024 * 1024;
20122
- var DEFAULT_AUDIT_MAX_FILES = 5;
20123
- function resolveRotation(opts) {
20124
- const envBytes = Number(process.env.SWITCHROOM_VAULT_AUDIT_MAX_BYTES);
20125
- const envFiles = Number(process.env.SWITCHROOM_VAULT_AUDIT_MAX_FILES);
20126
- const maxBytes = opts.maxBytes !== undefined && opts.maxBytes !== 0 ? opts.maxBytes : Number.isFinite(envBytes) && envBytes !== 0 ? envBytes : DEFAULT_AUDIT_MAX_BYTES;
20127
- const maxFiles = opts.maxFiles !== undefined && opts.maxFiles > 0 ? opts.maxFiles : Number.isFinite(envFiles) && envFiles > 0 ? envFiles : DEFAULT_AUDIT_MAX_FILES;
20128
- return { maxBytes, maxFiles };
20129
- }
20130
- function rotateAuditLog(logPath, maxFiles) {
20131
- const oldest = `${logPath}.${maxFiles}`;
20181
+ function rotateLogFile(logPath, maxFiles, tag, stillHeld) {
20182
+ const keep = Math.max(1, Math.floor(maxFiles));
20183
+ const held = (what) => {
20184
+ if (!stillHeld || stillHeld())
20185
+ return true;
20186
+ process.stderr.write(`[${tag}] WARN: rotation lock for ${logPath} was reclaimed while we were inside it; declining to ${what}
20187
+ `);
20188
+ return false;
20189
+ };
20190
+ let entryBytes;
20191
+ try {
20192
+ entryBytes = fs2.statSync(logPath).size;
20193
+ } catch (err) {
20194
+ process.stderr.write(`[${tag}] ERROR: could not stat active log ${logPath} before rotating it: ${err.message}
20195
+ `);
20196
+ return false;
20197
+ }
20198
+ const notRotatedUnderUs = (what) => {
20199
+ let live;
20200
+ try {
20201
+ live = fs2.statSync(logPath).size;
20202
+ } catch (err) {
20203
+ process.stderr.write(`[${tag}] ERROR: could not re-stat active log ${logPath}; declining to ${what}: ${err.message}
20204
+ `);
20205
+ return false;
20206
+ }
20207
+ if (live < entryBytes) {
20208
+ process.stderr.write(`[${tag}] WARN: active log ${logPath} shrank from ${entryBytes} to ${live} bytes while we were rotating it; another rotator snapshotted it and these generations are its, not ours — declining to ${what}
20209
+ `);
20210
+ return false;
20211
+ }
20212
+ return true;
20213
+ };
20214
+ const oldest = `${logPath}.${keep}`;
20132
20215
  if (fs2.existsSync(oldest)) {
20216
+ if (!held(`drop ${oldest}`))
20217
+ return false;
20218
+ if (!notRotatedUnderUs(`drop ${oldest}`))
20219
+ return false;
20133
20220
  try {
20134
20221
  fs2.unlinkSync(oldest);
20135
20222
  } catch (err) {
20136
- process.stderr.write(`[vault-audit] ERROR: could not drop oldest rotation ${oldest}: ${err.message}
20223
+ process.stderr.write(`[${tag}] ERROR: could not drop oldest rotation ${oldest}: ${err.message}
20137
20224
  `);
20138
20225
  }
20139
20226
  }
20140
- for (let n = maxFiles - 1;n >= 1; n--) {
20227
+ for (let n = keep - 1;n >= 1; n--) {
20141
20228
  const from = `${logPath}.${n}`;
20142
20229
  const to = `${logPath}.${n + 1}`;
20143
20230
  if (!fs2.existsSync(from))
20144
20231
  continue;
20232
+ if (!held(`shift ${from} → ${to}`))
20233
+ return false;
20234
+ if (!notRotatedUnderUs(`shift ${from} → ${to}`))
20235
+ return false;
20145
20236
  try {
20146
20237
  fs2.renameSync(from, to);
20147
20238
  } catch (err) {
20148
- process.stderr.write(`[vault-audit] ERROR: could not rotate ${from} → ${to}: ${err.message}
20239
+ process.stderr.write(`[${tag}] ERROR: could not rotate ${from} → ${to}: ${err.message}
20149
20240
  `);
20150
- return;
20241
+ return false;
20151
20242
  }
20152
20243
  }
20153
20244
  const snapshotPath = `${logPath}.1`;
20245
+ if (!held(`overwrite ${snapshotPath}`))
20246
+ return false;
20247
+ if (!notRotatedUnderUs(`overwrite ${snapshotPath}`))
20248
+ return false;
20154
20249
  try {
20155
20250
  fs2.copyFileSync(logPath, snapshotPath);
20156
20251
  } catch (err) {
20157
- process.stderr.write(`[vault-audit] ERROR: could not snapshot active audit log ${logPath} → ${snapshotPath}: ${err.message}
20252
+ process.stderr.write(`[${tag}] ERROR: could not snapshot active log ${logPath} → ${snapshotPath}: ${err.message}
20158
20253
  `);
20159
- return;
20160
- }
20161
- try {
20162
- const fd = fs2.openSync(snapshotPath, "r");
20163
- try {
20164
- fs2.fsyncSync(fd);
20165
- } finally {
20166
- fs2.closeSync(fd);
20167
- }
20168
- } catch (err) {
20169
- process.stderr.write(`[vault-audit] ERROR: could not fsync audit snapshot ${snapshotPath}; leaving active log intact to avoid data loss: ${err.message}
20170
- `);
20171
- return;
20254
+ return false;
20172
20255
  }
20256
+ const snapshotBytes = fsyncAndMeasure(snapshotPath, logPath, tag);
20257
+ if (snapshotBytes === null)
20258
+ return false;
20173
20259
  try {
20174
20260
  const dirFd = fs2.openSync(path2.dirname(snapshotPath), "r");
20175
20261
  try {
@@ -20178,15 +20264,78 @@ function rotateAuditLog(logPath, maxFiles) {
20178
20264
  fs2.closeSync(dirFd);
20179
20265
  }
20180
20266
  } catch {}
20267
+ if (!held(`truncate ${logPath}`))
20268
+ return false;
20269
+ let liveBytes;
20270
+ try {
20271
+ liveBytes = fs2.statSync(logPath).size;
20272
+ } catch (err) {
20273
+ process.stderr.write(`[${tag}] ERROR: could not re-stat active log ${logPath} before truncating; leaving it intact to avoid data loss: ${err.message}
20274
+ `);
20275
+ return false;
20276
+ }
20277
+ if (liveBytes < snapshotBytes) {
20278
+ process.stderr.write(`[${tag}] WARN: active log ${logPath} shrank from ${snapshotBytes} to ${liveBytes} bytes after we snapshotted it; another rotator truncated it and these rows are its, not ours — declining to truncate
20279
+ `);
20280
+ return false;
20281
+ }
20181
20282
  try {
20182
20283
  fs2.truncateSync(logPath, 0);
20183
20284
  } catch (err) {
20184
- process.stderr.write(`[vault-audit] ERROR: could not truncate active audit log ${logPath}: ${err.message}
20285
+ process.stderr.write(`[${tag}] ERROR: could not truncate active log ${logPath}: ${err.message}
20286
+ `);
20287
+ return false;
20288
+ }
20289
+ if (stillHeld && !stillHeld()) {
20290
+ process.stderr.write(`[${tag}] ERROR: truncated ${logPath} after our rotation lock was reclaimed; rows another rotator wrote between our size check and our truncate are LOST
20185
20291
  `);
20186
20292
  }
20293
+ return true;
20294
+ }
20295
+ function resolveRotationConfig(args) {
20296
+ const env = args.env ?? process.env;
20297
+ const envBytes = Number(env[args.envBytesVar]);
20298
+ const envFiles = Number(env[args.envFilesVar]);
20299
+ const maxBytes = args.maxBytes !== undefined && args.maxBytes !== 0 ? args.maxBytes : Number.isFinite(envBytes) && envBytes !== 0 ? envBytes : args.defaultBytes;
20300
+ const maxFiles = args.maxFiles !== undefined && args.maxFiles > 0 ? args.maxFiles : Number.isFinite(envFiles) && envFiles > 0 ? envFiles : args.defaultFiles;
20301
+ return { maxBytes, maxFiles };
20302
+ }
20303
+
20304
+ // src/vault/broker/audit-log.ts
20305
+ function failOpenStatePath(logPath) {
20306
+ return `${logPath}.failopen`;
20307
+ }
20308
+ function readFailOpenState(statePath) {
20309
+ try {
20310
+ const raw = fs3.readFileSync(statePath, "utf8");
20311
+ const parsed = JSON.parse(raw);
20312
+ const count = Number(parsed.failOpenCount);
20313
+ return {
20314
+ failOpenCount: Number.isFinite(count) && count > 0 ? count : 0,
20315
+ lastFailureTs: typeof parsed.lastFailureTs === "string" ? parsed.lastFailureTs : undefined,
20316
+ lastError: typeof parsed.lastError === "string" ? parsed.lastError : undefined
20317
+ };
20318
+ } catch {
20319
+ return { failOpenCount: 0 };
20320
+ }
20321
+ }
20322
+ var DEFAULT_AUDIT_MAX_BYTES = 32 * 1024 * 1024;
20323
+ var DEFAULT_AUDIT_MAX_FILES = 5;
20324
+ function resolveRotation(opts) {
20325
+ return resolveRotationConfig({
20326
+ maxBytes: opts.maxBytes,
20327
+ maxFiles: opts.maxFiles,
20328
+ envBytesVar: "SWITCHROOM_VAULT_AUDIT_MAX_BYTES",
20329
+ envFilesVar: "SWITCHROOM_VAULT_AUDIT_MAX_FILES",
20330
+ defaultBytes: DEFAULT_AUDIT_MAX_BYTES,
20331
+ defaultFiles: DEFAULT_AUDIT_MAX_FILES
20332
+ });
20333
+ }
20334
+ function rotateAuditLog(logPath, maxFiles) {
20335
+ rotateLogFile(logPath, maxFiles, "vault-audit");
20187
20336
  }
20188
20337
  function defaultAuditLogPath() {
20189
- return path2.join(os2.homedir(), ".switchroom", "vault-audit.log");
20338
+ return path3.join(os2.homedir(), ".switchroom", "vault-audit.log");
20190
20339
  }
20191
20340
  function callerFromPeer(peer) {
20192
20341
  if (peer.systemdUnit !== null && peer.systemdUnit.length > 0) {
@@ -20208,7 +20357,7 @@ function createAuditLogger(opts = {}) {
20208
20357
  lastError: String(err.message).slice(0, 300)
20209
20358
  };
20210
20359
  try {
20211
- fs2.writeFileSync(statePath, `${JSON.stringify(state)}
20360
+ fs3.writeFileSync(statePath, `${JSON.stringify(state)}
20212
20361
  `, { mode: 384 });
20213
20362
  } catch (persistErr) {
20214
20363
  process.stderr.write(`[vault-audit] ERROR: could not persist fail-open counter ${statePath}: ${persistErr.message}
@@ -20222,7 +20371,7 @@ function createAuditLogger(opts = {}) {
20222
20371
  write(entry) {
20223
20372
  if (rotation.maxBytes > 0) {
20224
20373
  try {
20225
- const size = fs2.statSync(logPath).size;
20374
+ const size = fs3.statSync(logPath).size;
20226
20375
  if (size >= rotation.maxBytes) {
20227
20376
  rotateAuditLog(logPath, rotation.maxFiles);
20228
20377
  }
@@ -20231,7 +20380,7 @@ function createAuditLogger(opts = {}) {
20231
20380
  const { line, next } = chainRow(chain, entry);
20232
20381
  let fd;
20233
20382
  try {
20234
- fd = fs2.openSync(logPath, "a", 384);
20383
+ fd = fs3.openSync(logPath, "a", 384);
20235
20384
  } catch (err) {
20236
20385
  process.stderr.write(`[vault-audit] ERROR: could not open audit log ${logPath}: ${err.message}
20237
20386
  `);
@@ -20240,7 +20389,7 @@ function createAuditLogger(opts = {}) {
20240
20389
  }
20241
20390
  let durable = false;
20242
20391
  try {
20243
- fs2.writeSync(fd, line);
20392
+ fs3.writeSync(fd, line);
20244
20393
  chain = next;
20245
20394
  durable = true;
20246
20395
  } catch (err) {
@@ -20249,7 +20398,7 @@ function createAuditLogger(opts = {}) {
20249
20398
  recordFailOpen(err);
20250
20399
  } finally {
20251
20400
  try {
20252
- fs2.closeSync(fd);
20401
+ fs3.closeSync(fd);
20253
20402
  } catch (closeErr) {
20254
20403
  process.stderr.write(`[vault-audit] ERROR: could not close audit log fd: ${closeErr.message}
20255
20404
  `);
@@ -22168,8 +22317,8 @@ function adminOnlyKeysBeingAdded(requestedKeys, existingKeys, patterns) {
22168
22317
  }
22169
22318
 
22170
22319
  // src/vault/grants-db.ts
22171
- import * as path3 from "node:path";
22172
- import * as fs3 from "node:fs";
22320
+ import * as path4 from "node:path";
22321
+ import * as fs4 from "node:fs";
22173
22322
  import { Database } from "bun:sqlite";
22174
22323
 
22175
22324
  // src/vault/approvals/schema.ts
@@ -22262,12 +22411,12 @@ function isGrantsDbCorruption(err) {
22262
22411
  function quarantineGrantsDb(dbPath) {
22263
22412
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
22264
22413
  const quarantinePath = `${dbPath}.corrupt-${stamp}`;
22265
- fs3.renameSync(dbPath, quarantinePath);
22414
+ fs4.renameSync(dbPath, quarantinePath);
22266
22415
  for (const suffix of WAL_SIDECAR_SUFFIXES2) {
22267
22416
  const sidecar = `${dbPath}${suffix}`;
22268
- if (fs3.existsSync(sidecar)) {
22417
+ if (fs4.existsSync(sidecar)) {
22269
22418
  try {
22270
- fs3.renameSync(sidecar, `${quarantinePath}${suffix}`);
22419
+ fs4.renameSync(sidecar, `${quarantinePath}${suffix}`);
22271
22420
  } catch {}
22272
22421
  }
22273
22422
  }
@@ -22277,7 +22426,7 @@ function openAndMigrate(dbPath) {
22277
22426
  const db = new Database(dbPath, { create: true });
22278
22427
  try {
22279
22428
  try {
22280
- fs3.chmodSync(dbPath, 384);
22429
+ fs4.chmodSync(dbPath, 384);
22281
22430
  } catch {}
22282
22431
  db.run("PRAGMA journal_mode=WAL");
22283
22432
  db.run("PRAGMA busy_timeout=5000");
@@ -22293,8 +22442,8 @@ function openAndMigrate(dbPath) {
22293
22442
  return db;
22294
22443
  }
22295
22444
  function openGrantsDb(dbPath = DEFAULT_GRANTS_DB_PATH, opener = openAndMigrate) {
22296
- const dir = path3.dirname(dbPath);
22297
- fs3.mkdirSync(dir, { recursive: true });
22445
+ const dir = path4.dirname(dbPath);
22446
+ fs4.mkdirSync(dir, { recursive: true });
22298
22447
  migrateLegacyGrantsDbLocation(dbPath);
22299
22448
  try {
22300
22449
  return opener(dbPath);
@@ -22318,6 +22467,7 @@ function canonicalizeApproverSet(approvers) {
22318
22467
  }
22319
22468
 
22320
22469
  // src/vault/approvals/kernel.ts
22470
+ var ALLOW_ONCE_CONSUMED_REASON = "allow_once_consumed";
22321
22471
  function generateRequestId() {
22322
22472
  return randomBytes5(16).toString("hex");
22323
22473
  }
@@ -22444,6 +22594,38 @@ function evaluateDecisionRow(db, row, currentCanonical, opts, now) {
22444
22594
  if (decision.decision === "deny") {
22445
22595
  return { state: "denied", decision };
22446
22596
  }
22597
+ if (decision.decision === "allow_once") {
22598
+ const burn = db.run(`UPDATE approval_decisions
22599
+ SET last_used_at = ?, revoked_at = ?, revoke_reason = ?
22600
+ WHERE id = ? AND revoked_at IS NULL`, [now, now, ALLOW_ONCE_CONSUMED_REASON, decision.id]);
22601
+ if ((burn.changes ?? 0) === 0) {
22602
+ audit(db, "deny", {
22603
+ agent_unit: decision.agent_unit,
22604
+ scope: decision.scope,
22605
+ action: decision.action,
22606
+ decision_id: decision.id,
22607
+ context: { reason: ALLOW_ONCE_CONSUMED_REASON }
22608
+ });
22609
+ return { state: "denied", decision };
22610
+ }
22611
+ decision.last_used_at = now;
22612
+ decision.revoked_at = now;
22613
+ decision.revoke_reason = ALLOW_ONCE_CONSUMED_REASON;
22614
+ audit(db, "match", {
22615
+ agent_unit: decision.agent_unit,
22616
+ scope: decision.scope,
22617
+ action: decision.action,
22618
+ decision_id: decision.id
22619
+ });
22620
+ audit(db, "revoke", {
22621
+ agent_unit: decision.agent_unit,
22622
+ scope: decision.scope,
22623
+ action: decision.action,
22624
+ decision_id: decision.id,
22625
+ context: { actor: "kernel", reason: ALLOW_ONCE_CONSUMED_REASON }
22626
+ });
22627
+ return { state: "granted", decision };
22628
+ }
22447
22629
  if (decision.decision === "allow_ttl") {
22448
22630
  const maxLifetime = opts.max_ttl_lifetime_ms ?? DEFAULT_MAX_TTL_LIFETIME_MS;
22449
22631
  const hardCap = decision.granted_at + maxLifetime;
@@ -22512,7 +22694,8 @@ function consumeNonce(db, request_id, now = Date.now()) {
22512
22694
  }
22513
22695
  function recordDecision(db, input, now = Date.now()) {
22514
22696
  const id = randomUUID();
22515
- const ttl_expires_at = input.decision === "allow_ttl" && input.ttl_ms ? now + input.ttl_ms : null;
22697
+ const maxLifetime = input.max_ttl_lifetime_ms ?? DEFAULT_MAX_TTL_LIFETIME_MS;
22698
+ const ttl_expires_at = input.decision === "allow_ttl" && input.ttl_ms ? now + Math.min(input.ttl_ms, maxLifetime) : null;
22516
22699
  const canonical = canonicalizeApproverSet(input.approver_set);
22517
22700
  db.run(`INSERT INTO approval_decisions
22518
22701
  (id, agent_unit, scope, action, decision,
@@ -23827,10 +24010,10 @@ class VaultBroker {
23827
24010
  return;
23828
24011
  }
23829
24012
  try {
23830
- const agentsDir = this.config ? resolveAgentsDir(this.config) : path4.join(os3.homedir(), ".switchroom", "agents");
23831
- const tokenDir = path4.join(agentsDir, agent);
24013
+ const agentsDir = this.config ? resolveAgentsDir(this.config) : path5.join(os3.homedir(), ".switchroom", "agents");
24014
+ const tokenDir = path5.join(agentsDir, agent);
23832
24015
  mkdirSync7(tokenDir, { recursive: true });
23833
- const tokenPath = path4.join(tokenDir, ".vault-token");
24016
+ const tokenPath = path5.join(tokenDir, ".vault-token");
23834
24017
  const tmpPath = `${tokenPath}.tmp.${process.pid}`;
23835
24018
  writeFileSync3(tmpPath, mintResult.token, { mode: 384 });
23836
24019
  renameSync8(tmpPath, tokenPath);
@@ -23892,8 +24075,8 @@ class VaultBroker {
23892
24075
  try {
23893
24076
  const row = this.grantsDb.query("SELECT agent_slug FROM vault_grants WHERE id = ?").get(id);
23894
24077
  if (row && AgentNameSchema.safeParse(row.agent_slug).success) {
23895
- const agentsDir = this.config ? resolveAgentsDir(this.config) : path4.join(os3.homedir(), ".switchroom", "agents");
23896
- const tokenPath = path4.join(agentsDir, row.agent_slug, ".vault-token");
24078
+ const agentsDir = this.config ? resolveAgentsDir(this.config) : path5.join(os3.homedir(), ".switchroom", "agents");
24079
+ const tokenPath = path5.join(agentsDir, row.agent_slug, ".vault-token");
23897
24080
  if (existsSync12(tokenPath)) {
23898
24081
  try {
23899
24082
  unlinkSync6(tokenPath);
@@ -24135,7 +24318,7 @@ class VaultBroker {
24135
24318
  if (!existsSync12(filePath))
24136
24319
  return false;
24137
24320
  try {
24138
- if (statSync6(filePath).size === 0)
24321
+ if (statSync7(filePath).size === 0)
24139
24322
  return false;
24140
24323
  } catch {
24141
24324
  return false;
@@ -24214,12 +24397,12 @@ class VaultBroker {
24214
24397
  }
24215
24398
  function detectVaultLayoutDrift(vaultPath) {
24216
24399
  const dir = dirname7(vaultPath);
24217
- if (basename4(dir) !== "vault")
24400
+ if (basename5(dir) !== "vault")
24218
24401
  return;
24219
- if (basename4(vaultPath) !== "vault.enc")
24402
+ if (basename5(vaultPath) !== "vault.enc")
24220
24403
  return;
24221
24404
  const switchroomDir = dirname7(dir);
24222
- if (basename4(switchroomDir) !== ".switchroom")
24405
+ if (basename5(switchroomDir) !== ".switchroom")
24223
24406
  return;
24224
24407
  const home2 = dirname7(switchroomDir);
24225
24408
  const result = inspectVaultLayout(home2);
@@ -24248,7 +24431,7 @@ async function main() {
24248
24431
  let perAgentTargets = [];
24249
24432
  try {
24250
24433
  if (existsSync12(perAgentDir)) {
24251
- const entries = readdirSync4(perAgentDir, { withFileTypes: true });
24434
+ const entries = readdirSync5(perAgentDir, { withFileTypes: true });
24252
24435
  const flat = [];
24253
24436
  const subdirs = [];
24254
24437
  for (const e of entries) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "switchroom",
3
3
  "//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
4
- "version": "0.19.18",
4
+ "version": "0.19.22",
5
5
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -26,19 +26,22 @@
26
26
  "pretest": "npm run build",
27
27
  "test": "vitest run && bun test telegram-plugin/tests/catch-all-forwarded-history.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
28
28
  "test:vitest": "vitest run",
29
- "test:bun": "bun test telegram-plugin/tests/catch-all-forwarded-history.test.ts src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-scope-persist.test.ts src/vault/broker/server-tokenless-scope.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
29
+ "test:bun": "bun test telegram-plugin/tests/catch-all-forwarded-history.test.ts src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-scope-persist.test.ts src/vault/broker/server-tokenless-scope.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/self-approval-bypass.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
30
30
  "test:watch": "vitest",
31
- "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-litellm-config-guard.mjs",
31
+ "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs",
32
32
  "lint:tsc": "tsc --noEmit",
33
33
  "lint:plugin-references": "node scripts/check-plugin-references.mjs",
34
34
  "lint:bot-api-wrapping": "bash scripts/check-bot-api-wrapping.sh",
35
35
  "lint:bun-test-imports": "node scripts/check-bun-test-imports.mjs",
36
+ "lint:bun-module-mock-scope": "node scripts/check-bun-module-mock-scope.mjs",
36
37
  "lint:no-pii": "node scripts/check-no-pii-secrets.mjs",
38
+ "lint:auth-test-hermeticity": "node scripts/check-auth-test-hermeticity.mjs",
37
39
  "lint:web-subscription-honest": "node scripts/check-web-subscription-honest.mjs",
38
40
  "lint:no-broadcast-delivery": "node scripts/check-no-broadcast-delivery.mjs",
39
41
  "lint:gateway-line-ratchet": "node scripts/check-gateway-line-ratchet.mjs",
40
42
  "lint:mcp-instructions-budget": "node scripts/check-mcp-instructions-budget.mjs",
41
43
  "lint:litellm-config-guard": "node scripts/check-litellm-config-guard.mjs",
44
+ "lint:release-asset-contract": "node scripts/check-release-asset-names.mjs",
42
45
  "prepublishOnly": "npm run build && npm run lint && npm test"
43
46
  },
44
47
  "dependencies": {