switchroom 0.18.28 → 0.18.30

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 (48) hide show
  1. package/bin/handoff-briefing.sh +15 -2
  2. package/dist/agent-scheduler/index.js +111 -7
  3. package/dist/auth-broker/index.js +154 -73
  4. package/dist/cli/autoaccept-poll.js +8 -3
  5. package/dist/cli/drive-write-pretool.mjs +8 -3
  6. package/dist/cli/ms-365-write-pretool.mjs +158 -11
  7. package/dist/cli/notion-write-pretool.mjs +103 -4
  8. package/dist/cli/switchroom.js +2712 -2219
  9. package/dist/host-control/main.js +110 -70
  10. package/dist/vault/approvals/kernel-server.js +116 -70
  11. package/dist/vault/broker/server.js +314 -202
  12. package/package.json +3 -3
  13. package/profiles/_base/start.sh.hbs +105 -34
  14. package/telegram-plugin/dist/bridge/bridge.js +71 -47
  15. package/telegram-plugin/dist/gateway/gateway.js +1128 -666
  16. package/telegram-plugin/dist/server.js +89 -64
  17. package/telegram-plugin/gateway/backstop-delivery.ts +272 -0
  18. package/telegram-plugin/gateway/forward-origin.ts +9 -1
  19. package/telegram-plugin/gateway/gateway.ts +656 -388
  20. package/telegram-plugin/gateway/model-command.ts +331 -602
  21. package/telegram-plugin/gateway/session-model-file.ts +40 -0
  22. package/telegram-plugin/gateway/turn-record-status.ts +45 -0
  23. package/telegram-plugin/gateway/unhandled-message.ts +177 -0
  24. package/telegram-plugin/history.ts +153 -23
  25. package/telegram-plugin/llm-error-present.ts +24 -0
  26. package/telegram-plugin/model-unavailable.ts +55 -0
  27. package/telegram-plugin/operator-events.ts +113 -0
  28. package/telegram-plugin/pending-user-notice.ts +88 -0
  29. package/telegram-plugin/shared/local-time.ts +99 -0
  30. package/telegram-plugin/tests/backstop-delivery.test.ts +250 -0
  31. package/telegram-plugin/tests/catch-all-forwarded-history.test.ts +103 -0
  32. package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +264 -0
  33. package/telegram-plugin/tests/forward-origin.test.ts +30 -3
  34. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +111 -60
  35. package/telegram-plugin/tests/history.test.ts +88 -0
  36. package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +278 -0
  37. package/telegram-plugin/tests/local-time.test.ts +135 -0
  38. package/telegram-plugin/tests/model-command.test.ts +427 -1512
  39. package/telegram-plugin/tests/session-model-file.test.ts +23 -0
  40. package/telegram-plugin/tests/turn-flush-safety.test.ts +34 -0
  41. package/telegram-plugin/tier-downgrade.ts +4 -3
  42. package/telegram-plugin/turn-flush-safety.ts +25 -1
  43. package/vendor/hindsight-memory/scripts/backfill_transcripts.py +399 -2
  44. package/vendor/hindsight-memory/scripts/lib/client.py +47 -0
  45. package/vendor/hindsight-memory/scripts/lib/content.py +93 -7
  46. package/vendor/hindsight-memory/scripts/lib/turnlog.py +450 -0
  47. package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +467 -0
  48. package/vendor/hindsight-memory/tests/test_content.py +63 -7
@@ -4011,7 +4011,8 @@ var init_protocol = __esm(() => {
4011
4011
  v: exports_external.literal(PROTOCOL_VERSION),
4012
4012
  op: exports_external.literal("get-credentials"),
4013
4013
  id: exports_external.string().min(1),
4014
- provider: ProviderNameSchema.optional()
4014
+ provider: ProviderNameSchema.optional(),
4015
+ account: exports_external.string().min(1).optional()
4015
4016
  });
4016
4017
  ListStateRequestSchema = exports_external.object({
4017
4018
  v: exports_external.literal(PROTOCOL_VERSION),
@@ -4326,13 +4327,17 @@ class AuthBrokerClient {
4326
4327
  sock.destroy();
4327
4328
  }
4328
4329
  }
4329
- async getCredentials(provider) {
4330
+ async getCredentials(provider, account) {
4330
4331
  const base = {
4331
4332
  v: PROTOCOL_VERSION,
4332
4333
  id: randomUUID(),
4333
4334
  op: "get-credentials"
4334
4335
  };
4335
- const req = provider !== undefined ? { ...base, provider } : base;
4336
+ let req = base;
4337
+ if (provider !== undefined)
4338
+ req = { ...req, provider };
4339
+ if (account !== undefined)
4340
+ req = { ...req, account };
4336
4341
  const data = await this.send(req);
4337
4342
  return data;
4338
4343
  }
@@ -4606,6 +4611,106 @@ var init_client = __esm(() => {
4606
4611
  };
4607
4612
  });
4608
4613
 
4614
+ // src/config/microsoft-workspace-acl.ts
4615
+ var exports_microsoft_workspace_acl = {};
4616
+ __export(exports_microsoft_workspace_acl, {
4617
+ vaultRefKey: () => vaultRefKey,
4618
+ shouldEmitMs365Mcp: () => shouldEmitMs365Mcp,
4619
+ normalizeMicrosoftBindings: () => normalizeMicrosoftBindings,
4620
+ ms365McpKeyForBinding: () => ms365McpKeyForBinding,
4621
+ microsoftAccountSlug: () => microsoftAccountSlug,
4622
+ isMultiAccount: () => isMultiAccount,
4623
+ isMicrosoftClientCredentialKeyForAgent: () => isMicrosoftClientCredentialKeyForAgent,
4624
+ bindingsForAgent: () => bindingsForAgent
4625
+ });
4626
+ function normalizeMicrosoftBindings(mw) {
4627
+ if (!mw)
4628
+ return [];
4629
+ if (mw.accounts !== undefined) {
4630
+ return mw.accounts.filter((b) => b && typeof b.account === "string" && b.account.length > 0).map((b) => ({
4631
+ account: b.account.trim().toLowerCase(),
4632
+ tools: b.tools,
4633
+ org_mode: b.org_mode
4634
+ }));
4635
+ }
4636
+ if (typeof mw.account === "string" && mw.account.length > 0) {
4637
+ return [
4638
+ {
4639
+ account: mw.account.trim().toLowerCase(),
4640
+ tools: mw.tools,
4641
+ org_mode: mw.org_mode
4642
+ }
4643
+ ];
4644
+ }
4645
+ return [];
4646
+ }
4647
+ function isMultiAccount(mw) {
4648
+ return normalizeMicrosoftBindings(mw).length > 1;
4649
+ }
4650
+ function microsoftAccountSlug(account) {
4651
+ const email = account.trim().toLowerCase();
4652
+ const local = email.split("@")[0] ?? email;
4653
+ let base = local.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
4654
+ if (base.length === 0)
4655
+ base = "acct";
4656
+ if (base.length > 24)
4657
+ base = base.slice(0, 24).replace(/-+$/g, "");
4658
+ let h = 2166136261;
4659
+ for (let i = 0;i < email.length; i++) {
4660
+ h ^= email.charCodeAt(i);
4661
+ h = Math.imul(h, 16777619);
4662
+ }
4663
+ const hex = (h >>> 0).toString(16).padStart(8, "0").slice(0, 4);
4664
+ return `${base}-${hex}`;
4665
+ }
4666
+ function ms365McpKeyForBinding(mw, account) {
4667
+ if (isMultiAccount(mw))
4668
+ return `ms-365-${microsoftAccountSlug(account)}`;
4669
+ return "ms-365";
4670
+ }
4671
+ function shouldEmitMs365Mcp(agentName, agentMicrosoftAccount, microsoftAccounts) {
4672
+ if (!agentMicrosoftAccount)
4673
+ return false;
4674
+ const account = agentMicrosoftAccount.trim().toLowerCase();
4675
+ if (account.length === 0)
4676
+ return false;
4677
+ const acctEntry = microsoftAccounts?.[account];
4678
+ if (!acctEntry)
4679
+ return false;
4680
+ const enabledFor = acctEntry.enabled_for ?? [];
4681
+ return enabledFor.includes(agentName);
4682
+ }
4683
+ function bindingsForAgent(agentName, mw, microsoftAccounts) {
4684
+ return normalizeMicrosoftBindings(mw).filter((b) => shouldEmitMs365Mcp(agentName, b.account, microsoftAccounts));
4685
+ }
4686
+ function vaultRefKey(value) {
4687
+ if (typeof value !== "string" || !value.startsWith("vault:"))
4688
+ return null;
4689
+ const key = value.slice("vault:".length).split("#")[0];
4690
+ return key.length > 0 ? key : null;
4691
+ }
4692
+ function isMicrosoftClientCredentialKeyForAgent(config, agentName, key) {
4693
+ if (!agentName || !key)
4694
+ return false;
4695
+ const agentConfig = config.agents?.[agentName];
4696
+ if (!agentConfig)
4697
+ return false;
4698
+ if (agentConfig.mcp_servers?.["ms-365"] === false) {
4699
+ return false;
4700
+ }
4701
+ if (bindingsForAgent(agentName, agentConfig.microsoft_workspace, config.microsoft_accounts).length === 0) {
4702
+ return false;
4703
+ }
4704
+ const mw = config.microsoft_workspace;
4705
+ if (!mw)
4706
+ return false;
4707
+ for (const ref of [mw.microsoft_client_id, mw.microsoft_client_secret]) {
4708
+ if (vaultRefKey(ref) === key)
4709
+ return true;
4710
+ }
4711
+ return false;
4712
+ }
4713
+
4609
4714
  // src/cli/ms-365-write-pretool.ts
4610
4715
  import { readFileSync as readFileSync2 } from "node:fs";
4611
4716
  import { createConnection } from "node:net";
@@ -4621,7 +4726,7 @@ async function loadMicrosoftFromAuthBroker(options = {}) {
4621
4726
  let result;
4622
4727
  try {
4623
4728
  result = await withAuthBrokerClient2(async (client) => {
4624
- return await client.getCredentials("microsoft");
4729
+ return await client.getCredentials("microsoft", options.account);
4625
4730
  }, options.socketPath !== undefined ? { socket: options.socketPath } : undefined);
4626
4731
  } catch (err) {
4627
4732
  if (err instanceof AuthBrokerUnreachableError2 || err instanceof AuthBrokerError2) {
@@ -4644,6 +4749,30 @@ async function loadMicrosoftFromAuthBroker(options = {}) {
4644
4749
  account_email: creds.microsoftOauth?.accountEmail
4645
4750
  };
4646
4751
  }
4752
+ async function resolveMicrosoftAccountBySlug(slug, options = {}) {
4753
+ if (!slug)
4754
+ return null;
4755
+ const { microsoftAccountSlug: microsoftAccountSlug2 } = await Promise.resolve().then(() => exports_microsoft_workspace_acl);
4756
+ let accounts;
4757
+ try {
4758
+ if (options.listAccounts) {
4759
+ accounts = await options.listAccounts();
4760
+ } else {
4761
+ const { withAuthBrokerClient: withAuthBrokerClient2 } = await Promise.resolve().then(() => (init_client(), exports_client));
4762
+ accounts = await withAuthBrokerClient2(async (client) => {
4763
+ const data = await client.listMicrosoftAccounts();
4764
+ return (data.accounts ?? []).map((a) => a.account).filter((a) => typeof a === "string");
4765
+ }, options.socketPath !== undefined ? { socket: options.socketPath } : undefined);
4766
+ }
4767
+ } catch {
4768
+ return null;
4769
+ }
4770
+ for (const account of accounts) {
4771
+ if (microsoftAccountSlug2(account) === slug)
4772
+ return account;
4773
+ }
4774
+ return null;
4775
+ }
4647
4776
  var GRAPH_BASE = "https://graph.microsoft.com/v1.0";
4648
4777
  var GRAPH_TIMEOUT_MS = 4000;
4649
4778
  function defaultFetch() {
@@ -4793,7 +4922,15 @@ var IPC_REPLY_TIMEOUT_MS = 1e4;
4793
4922
  var KERNEL_RPC_TIMEOUT_MS = 3000;
4794
4923
  var GATEWAY_SOCKET = process.env.SWITCHROOM_GATEWAY_SOCKET ?? (process.env.TELEGRAM_STATE_DIR !== undefined ? join3(process.env.TELEGRAM_STATE_DIR, "gateway.sock") : join3(homedir3(), ".claude", "channels", "telegram", "gateway.sock"));
4795
4924
  var KERNEL_SOCKET = process.env.SWITCHROOM_KERNEL_SOCKET ?? "/run/switchroom/kernel/sock";
4796
- var TOOL_PREFIX = "mcp__ms-365__";
4925
+ var MS365_TOOL_RE = /^mcp__ms-365(?:-([a-z0-9-]+))?__(.+)$/;
4926
+ function ms365BareToolName(toolName) {
4927
+ const m = MS365_TOOL_RE.exec(toolName);
4928
+ return m ? m[2] : null;
4929
+ }
4930
+ function ms365AccountSlugFromToolName(toolName) {
4931
+ const m = MS365_TOOL_RE.exec(toolName);
4932
+ return m && m[1] ? m[1] : null;
4933
+ }
4797
4934
  var GATED_MS365_WRITE_TOOLS = new Set([
4798
4935
  "create-calendar",
4799
4936
  "update-calendar",
@@ -4880,9 +5017,9 @@ var KNOWN_SAFE_MS365_READ_TOOLS = new Set([
4880
5017
  "list-accounts"
4881
5018
  ]);
4882
5019
  function isGatedMs365Tool(toolName) {
4883
- if (!toolName.startsWith(TOOL_PREFIX))
5020
+ const bare = ms365BareToolName(toolName);
5021
+ if (bare === null)
4884
5022
  return false;
4885
- const bare = toolName.slice(TOOL_PREFIX.length);
4886
5023
  if (bare.length === 0)
4887
5024
  return false;
4888
5025
  if (KNOWN_SAFE_MS365_READ_TOOLS.has(bare))
@@ -4920,9 +5057,9 @@ function parseHookInput() {
4920
5057
  }
4921
5058
  }
4922
5059
  function isCalendarEventTool(toolName) {
4923
- if (!toolName.startsWith(TOOL_PREFIX))
5060
+ const bare = ms365BareToolName(toolName);
5061
+ if (bare === null)
4924
5062
  return false;
4925
- const bare = toolName.slice(TOOL_PREFIX.length);
4926
5063
  return bare.includes("calendar-event");
4927
5064
  }
4928
5065
  function extractEventId(toolInput) {
@@ -5005,11 +5142,19 @@ function buildCalendarChanges(toolInput, current) {
5005
5142
  async function enrichCalendarPreview(toolName, toolInput, deps = {}) {
5006
5143
  if (!isCalendarEventTool(toolName))
5007
5144
  return {};
5008
- const loadHandle = deps.loadHandle ?? (() => loadMicrosoftFromAuthBroker());
5145
+ const loadHandle = deps.loadHandle ?? ((account2) => loadMicrosoftFromAuthBroker(account2 !== undefined ? { account: account2 } : {}));
5146
+ const resolveAccount = deps.resolveAccount ?? ((slug2) => resolveMicrosoftAccountBySlug(slug2));
5009
5147
  const fetchEvent = deps.fetchEvent ?? ((a) => fetchCalendarEventContext(a));
5148
+ const slug = ms365AccountSlugFromToolName(toolName);
5149
+ let account;
5150
+ try {
5151
+ account = await resolveAccount(slug) ?? undefined;
5152
+ } catch {
5153
+ account = undefined;
5154
+ }
5010
5155
  let handle;
5011
5156
  try {
5012
- handle = await loadHandle();
5157
+ handle = await loadHandle(account);
5013
5158
  } catch {
5014
5159
  return { resolveAttemptedButFailed: true };
5015
5160
  }
@@ -5281,6 +5426,8 @@ if (__require.main == __require.module) {
5281
5426
  });
5282
5427
  }
5283
5428
  export {
5429
+ ms365BareToolName,
5430
+ ms365AccountSlugFromToolName,
5284
5431
  loadAllowFrom,
5285
5432
  isGatedMs365Tool,
5286
5433
  isCalendarEventTool,
@@ -12112,11 +12112,52 @@ var AgentGoogleWorkspaceConfigSchema = exports_external.object({
12112
12112
  approvers: exports_external.array(ApproverIdSchema).min(1).optional().describe("Per-agent approver override. When set, replaces (does not extend) " + "the top-level drive.approvers list for this agent's onboarding card."),
12113
12113
  tier: GoogleWorkspaceTierSchema.optional().describe("Per-agent tier override (RFC G Phase 1). When set, replaces the " + "top-level google_workspace.tier for this agent. Common case: most " + "agents on `core`, one specialist on `extended` for Slides access.")
12114
12114
  }).optional();
12115
+ var MicrosoftAccountEmailSchema = exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
12116
+ message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
12117
+ }).transform((v) => v.trim().toLowerCase());
12118
+ var MicrosoftToolTokenSchema = exports_external.string().min(1).regex(/^[a-z0-9-]+$/, {
12119
+ message: "microsoft_workspace tools[] tokens are forwarded to softeria's " + "--enabled-tools regex; each must be lowercase kebab-case " + "([a-z0-9-]+, e.g. 'mail', 'calendar', 'send-mail') so an unescaped " + "regex metacharacter can't crash the launcher"
12120
+ });
12121
+ var MicrosoftAccountBindingSchema = exports_external.object({
12122
+ account: MicrosoftAccountEmailSchema.describe("The Microsoft account this binding uses. Must be a key in top-level " + "`microsoft_accounts:` with this agent in its `enabled_for[]`."),
12123
+ tools: exports_external.array(MicrosoftToolTokenSchema).min(1).optional().describe("Per-account tool allowlist \u2192 softeria `--enabled-tools <regex>` " + "(tokens joined with `|`). Omitted = all tools exposed for this account."),
12124
+ org_mode: exports_external.boolean().optional().describe("Per-binding org_mode override (RFC #1873 \u00a76.4).")
12125
+ });
12115
12126
  var AgentMicrosoftWorkspaceConfigSchema = exports_external.object({
12116
- account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
12117
- message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
12118
- }).transform((v) => v.trim().toLowerCase()).optional().describe("RFC #1873: the Microsoft account this agent uses for the M365 MCP. " + "Must be a key in top-level `microsoft_accounts:` with this agent " + "listed in its `enabled_for[]`. Read by the auth-broker " + "(get-credentials, provider=microsoft) and by the scaffold to " + "decide whether to emit the `ms-365` MCP entry. Normalized to " + "lowercase so it matches the microsoft_accounts key (which is " + "also normalized)."),
12119
- org_mode: exports_external.boolean().optional().describe("Per-agent org_mode override (RFC #1873 \u00a76.4). When set, replaces " + "the top-level microsoft_workspace.org_mode for this agent. " + "Defaults to top-level value (which defaults to false).")
12127
+ account: MicrosoftAccountEmailSchema.optional().describe("RFC #1873: the Microsoft account this agent uses for the M365 MCP. " + "Must be a key in top-level `microsoft_accounts:` with this agent " + "listed in its `enabled_for[]`. Read by the auth-broker " + "(get-credentials, provider=microsoft) and by the scaffold to " + "decide whether to emit the `ms-365` MCP entry. Normalized to " + "lowercase so it matches the microsoft_accounts key (which is " + "also normalized). Mutually exclusive with `accounts` (plural)."),
12128
+ tools: exports_external.array(MicrosoftToolTokenSchema).min(1).optional().describe("Per-account tool allowlist for the SINGULAR `account` form \u2192 " + "softeria `--enabled-tools <regex>`. Omitted = all tools. Only " + "valid with the singular `account`; using it together with the " + "plural `accounts` (which carries per-binding `tools`) is an error."),
12129
+ org_mode: exports_external.boolean().optional().describe("Per-agent org_mode override (RFC #1873 \u00a76.4). When set, replaces " + "the top-level microsoft_workspace.org_mode for this agent. " + "Defaults to top-level value (which defaults to false)."),
12130
+ accounts: exports_external.array(MicrosoftAccountBindingSchema).min(1).optional().describe("Plural multi-account form: bind MULTIPLE Microsoft accounts to " + "this agent, each with its own tool scope. Mutually exclusive with " + "the singular `account`. Each account gets its own `ms-365-<slug>` " + "MCP server.")
12131
+ }).superRefine((v, ctx) => {
12132
+ const hasSingular = v.account !== undefined;
12133
+ const hasPlural = v.accounts !== undefined;
12134
+ if (hasSingular && hasPlural) {
12135
+ ctx.addIssue({
12136
+ code: exports_external.ZodIssueCode.custom,
12137
+ message: "microsoft_workspace: use EITHER `account` (singular) OR " + "`accounts` (plural array), not both",
12138
+ path: ["accounts"]
12139
+ });
12140
+ }
12141
+ if (hasPlural && v.tools !== undefined) {
12142
+ ctx.addIssue({
12143
+ code: exports_external.ZodIssueCode.custom,
12144
+ message: "microsoft_workspace: block-level `tools` applies to the singular " + "`account` only; with `accounts` put `tools` inside each binding",
12145
+ path: ["tools"]
12146
+ });
12147
+ }
12148
+ if (hasPlural) {
12149
+ const seen = new Set;
12150
+ for (const b of v.accounts) {
12151
+ if (seen.has(b.account)) {
12152
+ ctx.addIssue({
12153
+ code: exports_external.ZodIssueCode.custom,
12154
+ message: `microsoft_workspace: duplicate account '${b.account}' in accounts[]`,
12155
+ path: ["accounts"]
12156
+ });
12157
+ }
12158
+ seen.add(b.account);
12159
+ }
12160
+ }
12120
12161
  }).optional();
12121
12162
  var AgentNotionWorkspaceConfigSchema = exports_external.object({
12122
12163
  databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
@@ -12491,6 +12532,26 @@ var SwitchroomConfigSchema = exports_external.object({
12491
12532
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
12492
12533
  checkServes(a.serves, ["agents", name, "serves"]);
12493
12534
  }
12535
+ const microsoftAccounts = cfg.microsoft_accounts;
12536
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
12537
+ const mw = a.microsoft_workspace;
12538
+ const accounts = mw?.accounts;
12539
+ if (!accounts)
12540
+ continue;
12541
+ accounts.forEach((b, i) => {
12542
+ const acct = b?.account?.trim().toLowerCase();
12543
+ if (!acct)
12544
+ return;
12545
+ const enabledFor = microsoftAccounts?.[acct]?.enabled_for ?? [];
12546
+ if (!enabledFor.includes(name)) {
12547
+ ctx.addIssue({
12548
+ code: exports_external.ZodIssueCode.custom,
12549
+ message: `agent '${name}' binds Microsoft account '${acct}' but is not in ` + `microsoft_accounts['${acct}'].enabled_for[] \u2014 operator must run ` + `\`switchroom auth microsoft enable ${acct} ${name}\``,
12550
+ path: ["agents", name, "microsoft_workspace", "accounts", i, "account"]
12551
+ });
12552
+ }
12553
+ });
12554
+ }
12494
12555
  });
12495
12556
 
12496
12557
  // src/config/paths.ts
@@ -13127,6 +13188,22 @@ function validateNotionWorkspaceConfig(config) {
13127
13188
  return issues;
13128
13189
  }
13129
13190
 
13191
+ // src/config/timezone.ts
13192
+ function isResolvableTimezone(zone) {
13193
+ try {
13194
+ new Intl.DateTimeFormat("en-US", { timeZone: zone });
13195
+ return true;
13196
+ } catch {
13197
+ return false;
13198
+ }
13199
+ }
13200
+ var CONTAINER_DEFAULT_UTC_ZONES = new Set([
13201
+ "UTC",
13202
+ "Etc/UTC",
13203
+ "Etc/Universal",
13204
+ "Universal"
13205
+ ]);
13206
+
13130
13207
  // src/config/loader.ts
13131
13208
  class ConfigError extends Error {
13132
13209
  details;
@@ -13250,8 +13327,30 @@ function loadConfig(configPath) {
13250
13327
  if (notionIssues.length > 0) {
13251
13328
  throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
13252
13329
  }
13330
+ validateAllTimezones(config, filePath);
13253
13331
  return config;
13254
13332
  }
13333
+ function validateAllTimezones(config, filePath) {
13334
+ const issues = [];
13335
+ const check = (zone, where) => {
13336
+ if (zone == null)
13337
+ return;
13338
+ if (!isResolvableTimezone(zone)) {
13339
+ issues.push(` ${where}: "${zone}" is not a resolvable IANA timezone ` + `(shape is valid but no such zone exists \u2014 check for a typo, ` + `e.g. "Australia/Melbourne", "America/New_York", "UTC").`);
13340
+ }
13341
+ };
13342
+ check(config.switchroom?.timezone, "switchroom.timezone");
13343
+ check(config.defaults?.timezone, "defaults.timezone");
13344
+ for (const [profileName, profile] of Object.entries(config.profiles ?? {})) {
13345
+ check(profile?.timezone, `profiles.${profileName}.timezone`);
13346
+ }
13347
+ for (const [agentName, agentRaw] of Object.entries(config.agents)) {
13348
+ check(agentRaw?.timezone, `agents.${agentName}.timezone`);
13349
+ }
13350
+ if (issues.length > 0) {
13351
+ throw new ConfigError(`Invalid timezone configuration in ${filePath}`, issues);
13352
+ }
13353
+ }
13255
13354
  function validateAllCronTopicAliases(config, filePath) {
13256
13355
  const issues = [];
13257
13356
  for (const [agentName, agentRaw] of Object.entries(config.agents)) {