switchroom 0.14.58 → 0.14.59

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.
@@ -11144,7 +11144,7 @@ var GoogleWorkspaceConfigSchema = exports_external.object({
11144
11144
  tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet — see RFC G §5).")
11145
11145
  }).optional();
11146
11146
  var MicrosoftWorkspaceConfigSchema = exports_external.object({
11147
- microsoft_client_id: exports_external.string().min(1).describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id')."),
11147
+ microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL — omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
11148
11148
  microsoft_client_secret: exports_external.string().min(1).optional().describe("Microsoft OAuth client secret. Optional — public-client apps " + "(Mobile + Desktop platform with 'Allow public client flows' " + "enabled) work without a secret; confidential clients pass " + "one. Either literal or vault reference e.g. " + "'vault:microsoft-oauth-client-secret'."),
11149
11149
  authority: exports_external.string().url().optional().describe("Microsoft authority endpoint. Defaults to " + "'https://login.microsoftonline.com/common' which accepts both " + "personal MSA and work/school tenants. Override only for " + "single-tenant deployments."),
11150
11150
  org_mode: exports_external.boolean().optional().describe("Opt-in to Teams + SharePoint surfaces (RFC §6.4). When true, " + "the v1 scope set adds Sites.ReadWrite.All AND the launcher " + "spawns softeria with --org-mode. Defaults to false — personal " + "MSA + standard work surfaces only. Flipping for an existing " + "consented account requires re-running 'auth microsoft account " + "add --replace' to consent the additional scope.")
@@ -11144,7 +11144,7 @@ var GoogleWorkspaceConfigSchema = exports_external.object({
11144
11144
  tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet — see RFC G §5).")
11145
11145
  }).optional();
11146
11146
  var MicrosoftWorkspaceConfigSchema = exports_external.object({
11147
- microsoft_client_id: exports_external.string().min(1).describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id')."),
11147
+ microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL — omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
11148
11148
  microsoft_client_secret: exports_external.string().min(1).optional().describe("Microsoft OAuth client secret. Optional — public-client apps " + "(Mobile + Desktop platform with 'Allow public client flows' " + "enabled) work without a secret; confidential clients pass " + "one. Either literal or vault reference e.g. " + "'vault:microsoft-oauth-client-secret'."),
11149
11149
  authority: exports_external.string().url().optional().describe("Microsoft authority endpoint. Defaults to " + "'https://login.microsoftonline.com/common' which accepts both " + "personal MSA and work/school tenants. Override only for " + "single-tenant deployments."),
11150
11150
  org_mode: exports_external.boolean().optional().describe("Opt-in to Teams + SharePoint surfaces (RFC §6.4). When true, " + "the v1 scope set adds Sites.ReadWrite.All AND the launcher " + "spawns softeria with --org-mode. Defaults to false — personal " + "MSA + standard work surfaces only. Flipping for an existing " + "consented account requires re-running 'auth microsoft account " + "add --replace' to consent the additional scope.")
@@ -12954,6 +12954,19 @@ function classifyAsyncError2(err) {
12954
12954
  return { ok: false, kind: "provider_error", detail: msg };
12955
12955
  }
12956
12956
 
12957
+ // src/auth/default-oauth-clients.ts
12958
+ var DEFAULT_MICROSOFT_CLIENT_ID = "9dff88fa-3126-457b-9d1d-37e58c219019";
12959
+ function resolveMicrosoftClientId(configClientId) {
12960
+ const env = process.env.SWITCHROOM_MICROSOFT_CLIENT_ID;
12961
+ if (env !== undefined && env.trim().length > 0) {
12962
+ return { clientId: env.trim(), source: "env" };
12963
+ }
12964
+ if (configClientId !== undefined && configClientId.length > 0) {
12965
+ return { clientId: configClientId, source: "config" };
12966
+ }
12967
+ return { clientId: DEFAULT_MICROSOFT_CLIENT_ID, source: "default" };
12968
+ }
12969
+
12957
12970
  // src/auth/broker/google-storage.ts
12958
12971
  import { existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync3 } from "node:fs";
12959
12972
  import { dirname, join as join2, resolve as resolve5 } from "node:path";
@@ -13503,14 +13516,11 @@ class AuthBroker {
13503
13516
  fetcher: opts.fetcher
13504
13517
  }));
13505
13518
  }
13506
- const microsoftClientId = config.microsoft_workspace?.microsoft_client_id;
13507
- if (microsoftClientId !== undefined) {
13508
- this.providers.register(new MicrosoftProvider({
13509
- clientId: microsoftClientId,
13510
- clientSecret: config.microsoft_workspace?.microsoft_client_secret,
13511
- fetcher: opts.fetcher
13512
- }));
13513
- }
13519
+ this.providers.register(new MicrosoftProvider({
13520
+ clientId: resolveMicrosoftClientId(config.microsoft_workspace?.microsoft_client_id).clientId,
13521
+ clientSecret: config.microsoft_workspace?.microsoft_client_secret,
13522
+ fetcher: opts.fetcher
13523
+ }));
13514
13524
  this.assertConfigConsistent(config);
13515
13525
  }
13516
13526
  homeRoot() {
@@ -11892,7 +11892,7 @@ var GoogleWorkspaceConfigSchema = exports_external.object({
11892
11892
  tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet \u2014 see RFC G \u00a75).")
11893
11893
  }).optional();
11894
11894
  var MicrosoftWorkspaceConfigSchema = exports_external.object({
11895
- microsoft_client_id: exports_external.string().min(1).describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id')."),
11895
+ microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL \u2014 omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
11896
11896
  microsoft_client_secret: exports_external.string().min(1).optional().describe("Microsoft OAuth client secret. Optional \u2014 public-client apps " + "(Mobile + Desktop platform with 'Allow public client flows' " + "enabled) work without a secret; confidential clients pass " + "one. Either literal or vault reference e.g. " + "'vault:microsoft-oauth-client-secret'."),
11897
11897
  authority: exports_external.string().url().optional().describe("Microsoft authority endpoint. Defaults to " + "'https://login.microsoftonline.com/common' which accepts both " + "personal MSA and work/school tenants. Override only for " + "single-tenant deployments."),
11898
11898
  org_mode: exports_external.boolean().optional().describe("Opt-in to Teams + SharePoint surfaces (RFC \u00a76.4). When true, " + "the v1 scope set adds Sites.ReadWrite.All AND the launcher " + "spawns softeria with --org-mode. Defaults to false \u2014 personal " + "MSA + standard work surfaces only. Flipping for an existing " + "consented account requires re-running 'auth microsoft account " + "add --replace' to consent the additional scope.")
@@ -13708,7 +13708,7 @@ var init_schema = __esm(() => {
13708
13708
  tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet \u2014 see RFC G \u00a75).")
13709
13709
  }).optional();
13710
13710
  MicrosoftWorkspaceConfigSchema = exports_external.object({
13711
- microsoft_client_id: exports_external.string().min(1).describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id')."),
13711
+ microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL \u2014 omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
13712
13712
  microsoft_client_secret: exports_external.string().min(1).optional().describe("Microsoft OAuth client secret. Optional \u2014 public-client apps " + "(Mobile + Desktop platform with 'Allow public client flows' " + "enabled) work without a secret; confidential clients pass " + "one. Either literal or vault reference e.g. " + "'vault:microsoft-oauth-client-secret'."),
13713
13713
  authority: exports_external.string().url().optional().describe("Microsoft authority endpoint. Defaults to " + "'https://login.microsoftonline.com/common' which accepts both " + "personal MSA and work/school tenants. Override only for " + "single-tenant deployments."),
13714
13714
  org_mode: exports_external.boolean().optional().describe("Opt-in to Teams + SharePoint surfaces (RFC \u00a76.4). When true, " + "the v1 scope set adds Sites.ReadWrite.All AND the launcher " + "spawns softeria with --org-mode. Defaults to false \u2014 personal " + "MSA + standard work surfaces only. Flipping for an existing " + "consented account requires re-running 'auth microsoft account " + "add --replace' to consent the additional scope.")
@@ -30156,23 +30156,12 @@ function checkOAuthClient2(config, anyAgentEnabled) {
30156
30156
  if (!anyAgentEnabled)
30157
30157
  return [];
30158
30158
  const mw = config.microsoft_workspace;
30159
- if (!mw) {
30159
+ if (!mw || !clientValuePresent2(mw.microsoft_client_id)) {
30160
30160
  return [
30161
30161
  {
30162
30162
  name: "microsoft:oauth-client-configured",
30163
- status: "fail",
30164
- detail: "agents have microsoft_workspace.account set but the top-level microsoft_workspace: block is missing",
30165
- fix: "Add a microsoft_workspace block with microsoft_client_id (and optionally microsoft_client_secret) to switchroom.yaml. See `switchroom auth microsoft account add` error output for the full walkthrough."
30166
- }
30167
- ];
30168
- }
30169
- if (!clientValuePresent2(mw.microsoft_client_id)) {
30170
- return [
30171
- {
30172
- name: "microsoft:oauth-client-configured",
30173
- status: "fail",
30174
- detail: "microsoft_workspace block present but microsoft_client_id is empty",
30175
- fix: "Register an Entra app at https://entra.microsoft.com \u2192 App registrations \u2192 New. Copy the Application (client) ID and vault it: `switchroom vault set microsoft-oauth-client-id`."
30163
+ status: "ok",
30164
+ detail: "using switchroom's shipped default Microsoft OAuth app (no BYO app configured)"
30176
30165
  }
30177
30166
  ];
30178
30167
  }
@@ -49463,8 +49452,8 @@ var {
49463
49452
  } = import__.default;
49464
49453
 
49465
49454
  // src/build-info.ts
49466
- var VERSION = "0.14.58";
49467
- var COMMIT_SHA = "fc4023ed";
49455
+ var VERSION = "0.14.59";
49456
+ var COMMIT_SHA = "178c6d14";
49468
49457
 
49469
49458
  // src/cli/agent.ts
49470
49459
  init_source();
@@ -57729,6 +57718,21 @@ function pruneEmptyMap(doc, path) {
57729
57718
 
57730
57719
  // src/cli/auth-microsoft.ts
57731
57720
  init_helpers();
57721
+
57722
+ // src/auth/default-oauth-clients.ts
57723
+ var DEFAULT_MICROSOFT_CLIENT_ID = "9dff88fa-3126-457b-9d1d-37e58c219019";
57724
+ function resolveMicrosoftClientId(configClientId) {
57725
+ const env2 = process.env.SWITCHROOM_MICROSOFT_CLIENT_ID;
57726
+ if (env2 !== undefined && env2.trim().length > 0) {
57727
+ return { clientId: env2.trim(), source: "env" };
57728
+ }
57729
+ if (configClientId !== undefined && configClientId.length > 0) {
57730
+ return { clientId: configClientId, source: "config" };
57731
+ }
57732
+ return { clientId: DEFAULT_MICROSOFT_CLIENT_ID, source: "default" };
57733
+ }
57734
+
57735
+ // src/cli/auth-microsoft.ts
57732
57736
  function registerAuthMicrosoftSubcommands(program3, authParent) {
57733
57737
  const microsoft = authParent.command("microsoft").description("Manage Microsoft 365 accounts shared across agents (RFC #1873 \u2014 see docs/rfcs/microsoft-workspace.md)");
57734
57738
  registerEnable2(microsoft, program3);
@@ -57871,13 +57875,13 @@ function registerAccountAdd2(accountParent) {
57871
57875
  ]);
57872
57876
  const config = loadConfig2();
57873
57877
  const mw = config.microsoft_workspace;
57874
- if (!mw) {
57875
- throw new Error(microsoftClientSetupGuidance("switchroom.yaml has no `microsoft_workspace:` block, so there is no Microsoft OAuth app to connect accounts against."));
57876
- }
57877
- let clientIdRaw = process.env.SWITCHROOM_MICROSOFT_CLIENT_ID ?? mw.microsoft_client_id;
57878
- let clientSecretRaw = process.env.SWITCHROOM_MICROSOFT_CLIENT_SECRET ?? mw.microsoft_client_secret;
57879
- if (!clientIdRaw) {
57880
- throw new Error(microsoftClientSetupGuidance("The `microsoft_workspace:` block is present but `microsoft_client_id` is empty."));
57878
+ const resolvedClientId = resolveMicrosoftClientId(mw?.microsoft_client_id);
57879
+ let clientIdRaw = resolvedClientId.clientId;
57880
+ let clientSecretRaw = process.env.SWITCHROOM_MICROSOFT_CLIENT_SECRET ?? mw?.microsoft_client_secret;
57881
+ if (resolvedClientId.source === "default") {
57882
+ console.error(source_default.gray(` Using switchroom's shipped Microsoft OAuth app (zero-config).
57883
+ ` + " To use your own Entra app instead, set " + `microsoft_workspace.microsoft_client_id in switchroom.yaml
57884
+ ` + " (or the SWITCHROOM_MICROSOFT_CLIENT_ID env var)."));
57881
57885
  }
57882
57886
  const needsVault = isVaultReference2(clientIdRaw) || clientSecretRaw !== undefined && isVaultReference2(clientSecretRaw);
57883
57887
  if (needsVault) {
@@ -57928,7 +57932,7 @@ function registerAccountAdd2(accountParent) {
57928
57932
  clientSecretRaw = await resolveRef(clientSecretRaw, "microsoft_client_secret");
57929
57933
  }
57930
57934
  }
57931
- const orgMode = opts["orgMode"] ?? mw.org_mode ?? false;
57935
+ const orgMode = opts["orgMode"] ?? mw?.org_mode ?? false;
57932
57936
  const scopes = selectMicrosoftScopes(orgMode);
57933
57937
  const oauthCfg = {
57934
57938
  client_id: clientIdRaw,
@@ -58195,51 +58199,6 @@ async function readHiddenLine2(prompt) {
58195
58199
  process.stdin.on("data", onData);
58196
58200
  });
58197
58201
  }
58198
- function microsoftClientSetupGuidance(reason) {
58199
- return [
58200
- reason,
58201
- "",
58202
- "Switchroom ships no shared Microsoft OAuth app \u2014 register your own.",
58203
- "One-time per install:",
58204
- "",
58205
- " 1. Go to https://entra.microsoft.com \u2192 App registrations \u2192 New",
58206
- " registration",
58207
- " 2. Supported account types: 'Accounts in any organizational",
58208
- " directory AND personal Microsoft accounts' (multi-tenant + MSA)",
58209
- " 3. Redirect URI: platform 'Mobile and desktop applications',",
58210
- " value `http://localhost` (port-agnostic; Microsoft ignores port",
58211
- " for loopback URIs)",
58212
- " 4. Authentication \u2192 Advanced settings \u2192 'Allow public client",
58213
- " flows': Yes (enables device-code on personal MSA)",
58214
- " 5. Copy the Application (client) ID from the Overview page",
58215
- " 6. Optional: create a client secret in 'Certificates & secrets'",
58216
- " (skip if using public-client flow only)",
58217
- " 7. Vault the credentials:",
58218
- "",
58219
- " switchroom vault set microsoft-oauth-client-id",
58220
- " switchroom vault set microsoft-oauth-client-secret # optional",
58221
- "",
58222
- " 8. If your work tenant requires admin consent, your IT admin must",
58223
- " grant the requested Graph scopes (Files.ReadWrite.All etc.) at",
58224
- " first sign-in. Personal MSA accounts (outlook.com / hotmail.com)",
58225
- " don't need this step.",
58226
- " 9. Add to ~/.switchroom/switchroom.yaml (top level):",
58227
- "",
58228
- " microsoft_workspace:",
58229
- ' microsoft_client_id: "vault:microsoft-oauth-client-id"',
58230
- ' microsoft_client_secret: "vault:microsoft-oauth-client-secret" # omit if public-client',
58231
- " authority: https://login.microsoftonline.com/common",
58232
- " org_mode: false",
58233
- "",
58234
- "Env vars SWITCHROOM_MICROSOFT_CLIENT_ID / _SECRET override the block",
58235
- "for one-off debugging.",
58236
- "",
58237
- "Full walkthrough: docs/microsoft-workspace.md (PR 5 will land this).",
58238
- "Don't reuse an existing Entra app \u2014 see RFC \u00a74.1 (signInAudience",
58239
- "mismatch + token-version drift make app sharing brittle)."
58240
- ].join(`
58241
- `);
58242
- }
58243
58202
 
58244
58203
  // src/cli/auth.ts
58245
58204
  init_auth_active_yaml();
@@ -13879,7 +13879,7 @@ var GoogleWorkspaceConfigSchema = exports_external.object({
13879
13879
  tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet — see RFC G §5).")
13880
13880
  }).optional();
13881
13881
  var MicrosoftWorkspaceConfigSchema = exports_external.object({
13882
- microsoft_client_id: exports_external.string().min(1).describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id')."),
13882
+ microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL — omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
13883
13883
  microsoft_client_secret: exports_external.string().min(1).optional().describe("Microsoft OAuth client secret. Optional — public-client apps " + "(Mobile + Desktop platform with 'Allow public client flows' " + "enabled) work without a secret; confidential clients pass " + "one. Either literal or vault reference e.g. " + "'vault:microsoft-oauth-client-secret'."),
13884
13884
  authority: exports_external.string().url().optional().describe("Microsoft authority endpoint. Defaults to " + "'https://login.microsoftonline.com/common' which accepts both " + "personal MSA and work/school tenants. Override only for " + "single-tenant deployments."),
13885
13885
  org_mode: exports_external.boolean().optional().describe("Opt-in to Teams + SharePoint surfaces (RFC §6.4). When true, " + "the v1 scope set adds Sites.ReadWrite.All AND the launcher " + "spawns softeria with --org-mode. Defaults to false — personal " + "MSA + standard work surfaces only. Flipping for an existing " + "consented account requires re-running 'auth microsoft account " + "add --replace' to consent the additional scope.")
@@ -11465,7 +11465,7 @@ var init_schema = __esm(() => {
11465
11465
  tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet — see RFC G §5).")
11466
11466
  }).optional();
11467
11467
  MicrosoftWorkspaceConfigSchema = exports_external.object({
11468
- microsoft_client_id: exports_external.string().min(1).describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id')."),
11468
+ microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL — omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
11469
11469
  microsoft_client_secret: exports_external.string().min(1).optional().describe("Microsoft OAuth client secret. Optional — public-client apps " + "(Mobile + Desktop platform with 'Allow public client flows' " + "enabled) work without a secret; confidential clients pass " + "one. Either literal or vault reference e.g. " + "'vault:microsoft-oauth-client-secret'."),
11470
11470
  authority: exports_external.string().url().optional().describe("Microsoft authority endpoint. Defaults to " + "'https://login.microsoftonline.com/common' which accepts both " + "personal MSA and work/school tenants. Override only for " + "single-tenant deployments."),
11471
11471
  org_mode: exports_external.boolean().optional().describe("Opt-in to Teams + SharePoint surfaces (RFC §6.4). When true, " + "the v1 scope set adds Sites.ReadWrite.All AND the launcher " + "spawns softeria with --org-mode. Defaults to false — personal " + "MSA + standard work surfaces only. Flipping for an existing " + "consented account requires re-running 'auth microsoft account " + "add --replace' to consent the additional scope.")
@@ -11465,7 +11465,7 @@ var init_schema = __esm(() => {
11465
11465
  tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet — see RFC G §5).")
11466
11466
  }).optional();
11467
11467
  MicrosoftWorkspaceConfigSchema = exports_external.object({
11468
- microsoft_client_id: exports_external.string().min(1).describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id')."),
11468
+ microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL — omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
11469
11469
  microsoft_client_secret: exports_external.string().min(1).optional().describe("Microsoft OAuth client secret. Optional — public-client apps " + "(Mobile + Desktop platform with 'Allow public client flows' " + "enabled) work without a secret; confidential clients pass " + "one. Either literal or vault reference e.g. " + "'vault:microsoft-oauth-client-secret'."),
11470
11470
  authority: exports_external.string().url().optional().describe("Microsoft authority endpoint. Defaults to " + "'https://login.microsoftonline.com/common' which accepts both " + "personal MSA and work/school tenants. Override only for " + "single-tenant deployments."),
11471
11471
  org_mode: exports_external.boolean().optional().describe("Opt-in to Teams + SharePoint surfaces (RFC §6.4). When true, " + "the v1 scope set adds Sites.ReadWrite.All AND the launcher " + "spawns softeria with --org-mode. Defaults to false — personal " + "MSA + standard work surfaces only. Flipping for an existing " + "consented account requires re-running 'auth microsoft account " + "add --replace' to consent the additional scope.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.58",
3
+ "version": "0.14.59",
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": {
@@ -23874,7 +23874,7 @@ var init_schema = __esm(() => {
23874
23874
  tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet \u2014 see RFC G \u00a75).")
23875
23875
  }).optional();
23876
23876
  MicrosoftWorkspaceConfigSchema = exports_external.object({
23877
- microsoft_client_id: exports_external.string().min(1).describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id')."),
23877
+ microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL \u2014 omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
23878
23878
  microsoft_client_secret: exports_external.string().min(1).optional().describe("Microsoft OAuth client secret. Optional \u2014 public-client apps " + "(Mobile + Desktop platform with 'Allow public client flows' " + "enabled) work without a secret; confidential clients pass " + "one. Either literal or vault reference e.g. " + "'vault:microsoft-oauth-client-secret'."),
23879
23879
  authority: exports_external.string().url().optional().describe("Microsoft authority endpoint. Defaults to " + "'https://login.microsoftonline.com/common' which accepts both " + "personal MSA and work/school tenants. Override only for " + "single-tenant deployments."),
23880
23880
  org_mode: exports_external.boolean().optional().describe("Opt-in to Teams + SharePoint surfaces (RFC \u00a76.4). When true, " + "the v1 scope set adds Sites.ReadWrite.All AND the launcher " + "spawns softeria with --org-mode. Defaults to false \u2014 personal " + "MSA + standard work surfaces only. Flipping for an existing " + "consented account requires re-running 'auth microsoft account " + "add --replace' to consent the additional scope.")
@@ -32729,7 +32729,7 @@ var MIRROR_MAX_LINES = 6;
32729
32729
  function escapeFeedHtml(s) {
32730
32730
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
32731
32731
  }
32732
- function renderActivityFeed(lines, final = false) {
32732
+ function renderActivityFeed(lines, final = false, liveSuffix = "") {
32733
32733
  if (lines.length === 0)
32734
32734
  return null;
32735
32735
  const shown = lines.slice(-MIRROR_MAX_LINES);
@@ -32740,7 +32740,7 @@ function renderActivityFeed(lines, final = false) {
32740
32740
  const lastIdx = shown.length - 1;
32741
32741
  shown.forEach((l, i) => {
32742
32742
  const esc = escapeFeedHtml(l);
32743
- out.push(i === lastIdx && !final ? `<b>\u2192 ${esc}</b>` : `<i>\u2713 ${esc}</i>`);
32743
+ out.push(i === lastIdx && !final ? `<b>\u2192 ${esc}${liveSuffix}</b>` : `<i>\u2713 ${esc}</i>`);
32744
32744
  });
32745
32745
  return out.join(`
32746
32746
  `);
@@ -32748,10 +32748,10 @@ function renderActivityFeed(lines, final = false) {
32748
32748
  var NESTED_MAX_LINES = 4;
32749
32749
  var NESTED_LINE_MAX = 90;
32750
32750
  var NESTED_PREFIX = " \u21b3 ";
32751
- function renderActivityFeedWithNested(lines, childLines, final = false) {
32751
+ function renderActivityFeedWithNested(lines, childLines, final = false, liveSuffix = "") {
32752
32752
  const children = childLines.map((s) => s.trim()).filter((s) => s.length > 0);
32753
32753
  if (children.length === 0)
32754
- return renderActivityFeed(lines, final);
32754
+ return renderActivityFeed(lines, final, liveSuffix);
32755
32755
  const out = [];
32756
32756
  const shownParent = lines.slice(-MIRROR_MAX_LINES);
32757
32757
  const hiddenParent = lines.length - shownParent.length;
@@ -32767,7 +32767,7 @@ function renderActivityFeedWithNested(lines, childLines, final = false) {
32767
32767
  shownChild.forEach((l, i) => {
32768
32768
  const t = l.length > NESTED_LINE_MAX ? l.slice(0, NESTED_LINE_MAX - 1) + "\u2026" : l;
32769
32769
  const esc = escapeFeedHtml(t);
32770
- out.push(i === lastChildIdx && !final ? `${NESTED_PREFIX}<b>\u2192 ${esc}</b>` : `${NESTED_PREFIX}<i>${esc}</i>`);
32770
+ out.push(i === lastChildIdx && !final ? `${NESTED_PREFIX}<b>\u2192 ${esc}${liveSuffix}</b>` : `${NESTED_PREFIX}<i>${esc}</i>`);
32771
32771
  });
32772
32772
  return out.length > 0 ? out.join(`
32773
32773
  `) : null;
@@ -47132,6 +47132,92 @@ function createPendingInboundBuffer(opts = {}) {
47132
47132
  };
47133
47133
  }
47134
47134
 
47135
+ // gateway/obligation-ledger.ts
47136
+ class ObligationLedger {
47137
+ maxRepresents;
47138
+ open = new Map;
47139
+ constructor(maxRepresents = 2) {
47140
+ this.maxRepresents = maxRepresents;
47141
+ }
47142
+ openIfAbsent(input) {
47143
+ if (this.open.has(input.originTurnId))
47144
+ return false;
47145
+ this.open.set(input.originTurnId, { ...input, representCount: 0 });
47146
+ return true;
47147
+ }
47148
+ close(originTurnId) {
47149
+ if (originTurnId == null)
47150
+ return false;
47151
+ return this.open.delete(originTurnId);
47152
+ }
47153
+ isOpen(originTurnId) {
47154
+ return this.open.has(originTurnId);
47155
+ }
47156
+ hasOpen() {
47157
+ return this.open.size > 0;
47158
+ }
47159
+ size() {
47160
+ return this.open.size;
47161
+ }
47162
+ list() {
47163
+ return [...this.open.values()].sort((a, b) => a.openedAt - b.openedAt);
47164
+ }
47165
+ oldest() {
47166
+ let best;
47167
+ for (const o of this.open.values()) {
47168
+ if (best === undefined || o.openedAt < best.openedAt)
47169
+ best = o;
47170
+ }
47171
+ return best;
47172
+ }
47173
+ decideAtIdle() {
47174
+ const o = this.oldest();
47175
+ if (o === undefined)
47176
+ return { action: "none" };
47177
+ if (o.representCount >= this.maxRepresents)
47178
+ return { action: "escalate", obligation: o };
47179
+ return { action: "represent", obligation: o };
47180
+ }
47181
+ resolveCloseTarget(echoedTurnId, liveTurnId) {
47182
+ if (echoedTurnId != null)
47183
+ return echoedTurnId;
47184
+ if (liveTurnId != null && this.open.size === 1 && this.open.has(liveTurnId))
47185
+ return liveTurnId;
47186
+ return null;
47187
+ }
47188
+ markRepresented(originTurnId) {
47189
+ const o = this.open.get(originTurnId);
47190
+ if (o === undefined)
47191
+ return 0;
47192
+ o.representCount += 1;
47193
+ return o.representCount;
47194
+ }
47195
+ }
47196
+ var REPRESENT_PREVIEW_MAX = 200;
47197
+ function buildObligationRepresentInbound(o, now) {
47198
+ const preview = o.text.length > REPRESENT_PREVIEW_MAX ? o.text.slice(0, REPRESENT_PREVIEW_MAX - 1) + "\u2026" : o.text;
47199
+ const topicClause = o.threadId != null ? " in this topic" : "";
47200
+ return {
47201
+ type: "inbound",
47202
+ chatId: o.chatId,
47203
+ ...o.threadId != null ? { threadId: o.threadId } : {},
47204
+ messageId: o.messageId,
47205
+ user: "switchroom",
47206
+ userId: 0,
47207
+ ts: now,
47208
+ text: `You have an earlier message${topicClause} that you started but never actually ` + `answered (you may have set it aside mid-work): "${preview}". Answer it now via the ` + `reply tool \u2014 deliver the real answer, don't just acknowledge it. If you've lost the ` + `surrounding context, call get_recent_messages for this chat${topicClause} first. ` + `That quoted text may be only the first ~200 characters of the original.`,
47209
+ meta: {
47210
+ source: "obligation_represent",
47211
+ origin_turn_id: o.originTurnId,
47212
+ represent_count: String(o.representCount + 1)
47213
+ }
47214
+ };
47215
+ }
47216
+ function obligationEscalationText(o) {
47217
+ const preview = o.text.length > REPRESENT_PREVIEW_MAX ? o.text.slice(0, REPRESENT_PREVIEW_MAX - 1) + "\u2026" : o.text;
47218
+ return `\u26a0\ufe0f I may have missed an earlier message and I'm not sure I answered it: ` + `"${preview}". If you still need it, please re-send.`;
47219
+ }
47220
+
47135
47221
  // gateway/inbound-spool.ts
47136
47222
  function spoolId(msg) {
47137
47223
  if (msg.meta?.source === "subagent_handback" && typeof msg.meta?.subagent_jsonl_id === "string" && msg.meta.subagent_jsonl_id.length > 0) {
@@ -52224,10 +52310,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
52224
52310
  }
52225
52311
 
52226
52312
  // ../src/build-info.ts
52227
- var VERSION = "0.14.58";
52228
- var COMMIT_SHA = "fc4023ed";
52229
- var COMMIT_DATE = "2026-06-04T02:50:15Z";
52230
- var LATEST_PR = 2142;
52313
+ var VERSION = "0.14.59";
52314
+ var COMMIT_SHA = "178c6d14";
52315
+ var COMMIT_DATE = "2026-06-04T07:31:45Z";
52316
+ var LATEST_PR = 2146;
52231
52317
  var COMMITS_AHEAD_OF_TAG = 0;
52232
52318
 
52233
52319
  // gateway/boot-version.ts
@@ -53425,6 +53511,10 @@ var _deliveryTimeoutParsed = _deliveryTimeoutRaw != null && _deliveryTimeoutRaw
53425
53511
  var DELIVERY_CONFIRM_TIMEOUT_MS = Number.isFinite(_deliveryTimeoutParsed) && _deliveryTimeoutParsed > 0 ? _deliveryTimeoutParsed : 15000;
53426
53512
  var DELIVERY_CONFIRM_SWEEP_MS = 5000;
53427
53513
  var deliveryQueue = createDeliveryQueue();
53514
+ var OBLIGATION_LEDGER_ENABLED = process.env.SWITCHROOM_OBLIGATION_LEDGER === "1";
53515
+ var OBLIGATION_REPRESENT_MAX = 2;
53516
+ var OBLIGATION_SWEEP_MS = 5000;
53517
+ var obligationLedger = new ObligationLedger(OBLIGATION_REPRESENT_MAX);
53428
53518
  var SERIALIZE_UNTIL_REPLIED_ENABLED = process.env.SWITCHROOM_SERIALIZE_UNTIL_REPLIED !== "0";
53429
53519
  var _noReplyDrainRaw = process.env.SWITCHROOM_SERIALIZE_NOREPLY_DRAIN_MS;
53430
53520
  var _noReplyDrainParsed = _noReplyDrainRaw != null && _noReplyDrainRaw !== "" ? Number(_noReplyDrainRaw) : 2500;
@@ -53433,6 +53523,16 @@ var TURN_ORIGIN_ROUTING_ENABLED = process.env.SWITCHROOM_TURN_ORIGIN_ROUTING !==
53433
53523
  var TOPIC_FRAMING_ENABLED = process.env.SWITCHROOM_TOPIC_FRAMING !== "0";
53434
53524
  var QUEUED_STATUS_UX_ENABLED = process.env.SWITCHROOM_QUEUED_STATUS_UX !== "0";
53435
53525
  var FEED_REOPEN_AFTER_ACK_ENABLED = process.env.SWITCHROOM_FEED_REOPEN_AFTER_ACK !== "0";
53526
+ var FEED_HEARTBEAT_ENABLED = process.env.SWITCHROOM_FEED_HEARTBEAT !== "0";
53527
+ var FEED_HEARTBEAT_TICK_MS = 6000;
53528
+ var FEED_HEARTBEAT_MIN_STALE_MS = 6000;
53529
+ function formatFeedElapsed(ms) {
53530
+ const s = Math.floor(ms / 1000);
53531
+ if (s < 60)
53532
+ return `${s}s`;
53533
+ const m = Math.floor(s / 60);
53534
+ return `${m}m${(s % 60).toString().padStart(2, "0")}s`;
53535
+ }
53436
53536
  function turnInFlightForGate() {
53437
53537
  return isDeliveryCutoverEnabled() ? isMachineInTurn() : claudeBusyKeys.size > 0;
53438
53538
  }
@@ -53473,6 +53573,37 @@ function findTurnByOriginId(originTurnId) {
53473
53573
  return currentTurn;
53474
53574
  return recentTurnsById.get(originTurnId) ?? null;
53475
53575
  }
53576
+ function closeObligationOnSubstantiveReply(args, liveTurn) {
53577
+ if (!OBLIGATION_LEDGER_ENABLED)
53578
+ return;
53579
+ const echoed = findTurnByOriginId(args.origin_turn_id);
53580
+ const target = obligationLedger.resolveCloseTarget(echoed?.turnId, liveTurn?.turnId);
53581
+ if (target != null)
53582
+ obligationLedger.close(target);
53583
+ }
53584
+ function openObligationFromInbound(inboundMsg, gate) {
53585
+ if (!OBLIGATION_LEDGER_ENABLED)
53586
+ return;
53587
+ if (!shouldTrackDelivery({
53588
+ isSteering: gate.isSteering,
53589
+ isInterrupt: gate.isInterrupt,
53590
+ hasSource: inboundMsg.meta?.source != null,
53591
+ effectiveText: gate.effectiveText
53592
+ })) {
53593
+ return;
53594
+ }
53595
+ const oid = deriveTurnId(inboundMsg.chatId, inboundMsg.threadId, inboundMsg.messageId);
53596
+ if (oid == null)
53597
+ return;
53598
+ obligationLedger.openIfAbsent({
53599
+ originTurnId: oid,
53600
+ chatId: inboundMsg.chatId,
53601
+ threadId: inboundMsg.threadId,
53602
+ messageId: inboundMsg.messageId,
53603
+ text: inboundMsg.text ?? "",
53604
+ openedAt: Date.now()
53605
+ });
53606
+ }
53476
53607
  function postQueuedStatus(chatId, bufferedThread, inFlightThread) {
53477
53608
  if (!QUEUED_STATUS_UX_ENABLED)
53478
53609
  return;
@@ -54748,6 +54879,40 @@ var inboundSpool = STATIC ? undefined : createInboundSpool({
54748
54879
  }
54749
54880
  });
54750
54881
  var pendingInboundBuffer = createPendingInboundBuffer({ spool: inboundSpool });
54882
+ function obligationSweep() {
54883
+ if (!OBLIGATION_LEDGER_ENABLED)
54884
+ return;
54885
+ if (!obligationLedger.hasOpen())
54886
+ return;
54887
+ if (turnInFlightForGate())
54888
+ return;
54889
+ const agent = process.env.SWITCHROOM_AGENT_NAME ?? "";
54890
+ if (pendingInboundBuffer.depth(agent) > 0)
54891
+ return;
54892
+ const decision = obligationLedger.decideAtIdle();
54893
+ const o = decision.obligation;
54894
+ if (decision.action === "none" || o == null)
54895
+ return;
54896
+ if (decision.action === "represent") {
54897
+ pendingInboundBuffer.push(agent, buildObligationRepresentInbound(o, Date.now()));
54898
+ const attempt = obligationLedger.markRepresented(o.originTurnId);
54899
+ process.stderr.write(`telegram gateway: obligation re-presented origin=${o.originTurnId} attempt=${attempt}/${OBLIGATION_REPRESENT_MAX}
54900
+ `);
54901
+ return;
54902
+ }
54903
+ obligationLedger.close(o.originTurnId);
54904
+ process.stderr.write(`telegram gateway: obligation escalated (exhausted ${OBLIGATION_REPRESENT_MAX} re-presents) origin=${o.originTurnId}
54905
+ `);
54906
+ robustApiCall(() => bot.api.sendMessage(o.chatId, obligationEscalationText(o), {
54907
+ ...o.threadId != null ? { message_thread_id: o.threadId } : {}
54908
+ }), { chat_id: o.chatId, ...o.threadId != null ? { threadId: o.threadId } : {}, verb: "obligation.escalate" }).catch((err) => {
54909
+ process.stderr.write(`telegram gateway: obligation escalation send failed: ${err}
54910
+ `);
54911
+ });
54912
+ }
54913
+ if (!STATIC && OBLIGATION_LEDGER_ENABLED) {
54914
+ setInterval(obligationSweep, OBLIGATION_SWEEP_MS).unref();
54915
+ }
54751
54916
  if (bootResumeInbound != null) {
54752
54917
  if (inboundSpool != null) {
54753
54918
  inboundSpool.put(bootResumeInbound.agent, bootResumeInbound.msg);
@@ -55724,6 +55889,8 @@ ${url}`;
55724
55889
  text: decision.mergedText,
55725
55890
  disableNotification
55726
55891
  });
55892
+ if (turn2.finalAnswerSubstantive)
55893
+ closeObligationOnSubstantiveReply(args, turn2);
55727
55894
  }
55728
55895
  outboundDedup.record(chat_id, threadId, decision.mergedText, Date.now(), turn2?.registryKey ?? null);
55729
55896
  silentAnchorEditDone = true;
@@ -55925,6 +56092,8 @@ ${url}`;
55925
56092
  turn.finalAnswerDelivered = true;
55926
56093
  turn.finalAnswerSubstantive = isSubstantiveFinalReply({ text: rawText, disableNotification });
55927
56094
  finalizeStatusReaction(chat_id, threadId, "done");
56095
+ if (turn.finalAnswerSubstantive)
56096
+ closeObligationOnSubstantiveReply(args, turn);
55928
56097
  }
55929
56098
  releaseTurnBufferGate(statusKey(chat_id, threadId), turn ?? undefined);
55930
56099
  if (turn?.finalAnswerDelivered === true) {
@@ -56095,6 +56264,8 @@ async function executeStreamReply(args) {
56095
56264
  disableNotification: args.disable_notification === true,
56096
56265
  done: args.done === true
56097
56266
  });
56267
+ if (turn.finalAnswerSubstantive)
56268
+ closeObligationOnSubstantiveReply(args, turn);
56098
56269
  const streamThreadIdForClear = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
56099
56270
  clearSilentEndState(statusKey(streamChatId, streamThreadIdForClear));
56100
56271
  }
@@ -57086,12 +57257,12 @@ function closeProgressLane(chatId, threadId) {
57086
57257
  }
57087
57258
  }
57088
57259
  var FOREGROUND_SUBAGENT_ACCUM_MAX = 12;
57089
- function composeTurnActivity(turn, final = false) {
57260
+ function composeTurnActivity(turn, final = false, liveSuffix = "") {
57090
57261
  const childLines = [];
57091
57262
  for (const narrative of turn.foregroundSubAgents.values()) {
57092
57263
  childLines.push(...narrative);
57093
57264
  }
57094
- return renderActivityFeedWithNested(turn.mirrorLines, childLines, final);
57265
+ return renderActivityFeedWithNested(turn.mirrorLines, childLines, final, liveSuffix);
57095
57266
  }
57096
57267
  async function drainActivitySummary(turn) {
57097
57268
  try {
@@ -57130,6 +57301,30 @@ async function drainActivitySummary(turn) {
57130
57301
  turn.activityInFlight = null;
57131
57302
  }
57132
57303
  }
57304
+ function feedHeartbeatTick() {
57305
+ const turn = currentTurn;
57306
+ if (turn == null)
57307
+ return;
57308
+ if (turn.activityMessageId == null)
57309
+ return;
57310
+ if (turn.finalAnswerDelivered)
57311
+ return;
57312
+ if (turn.lastToolLabelAt == null)
57313
+ return;
57314
+ const elapsed = Date.now() - turn.lastToolLabelAt;
57315
+ if (elapsed < FEED_HEARTBEAT_MIN_STALE_MS)
57316
+ return;
57317
+ const rendered = composeTurnActivity(turn, false, ` \xB7 ${formatFeedElapsed(elapsed)}`);
57318
+ if (rendered == null)
57319
+ return;
57320
+ turn.activityPendingRender = rendered;
57321
+ if (turn.activityInFlight == null) {
57322
+ turn.activityInFlight = drainActivitySummary(turn);
57323
+ }
57324
+ }
57325
+ if (!STATIC && FEED_HEARTBEAT_ENABLED) {
57326
+ setInterval(feedHeartbeatTick, FEED_HEARTBEAT_TICK_MS).unref();
57327
+ }
57133
57328
  function clearActivitySummary(turn, finalHtmlOverride) {
57134
57329
  const chat = turn.sessionChatId;
57135
57330
  const thread = turn.sessionThreadId;
@@ -57314,6 +57509,7 @@ function handleSessionEvent(ev) {
57314
57509
  }
57315
57510
  const rendered = appendActivityLabel(turn.mirrorLines, ev.label);
57316
57511
  if (rendered != null) {
57512
+ turn.lastToolLabelAt = Date.now();
57317
57513
  turn.activityPendingRender = composeTurnActivity(turn) ?? rendered;
57318
57514
  if (turn.activityInFlight == null) {
57319
57515
  turn.activityInFlight = drainActivitySummary(turn);
@@ -58638,6 +58834,11 @@ ${preBlock(write.output)}`;
58638
58834
  }
58639
58835
  return;
58640
58836
  }
58837
+ openObligationFromInbound(inboundMsg, {
58838
+ isSteering,
58839
+ isInterrupt: interrupt.isInterrupt,
58840
+ effectiveText
58841
+ });
58641
58842
  if (decideInboundDelivery({
58642
58843
  turnInFlight: turnInFlightAtReceipt,
58643
58844
  isSteering,