switchroom 0.14.74 → 0.14.75

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.
@@ -10991,6 +10991,7 @@ var AgentToolsSchema = exports_external.object({
10991
10991
  var AgentMemorySchema = exports_external.object({
10992
10992
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
10993
10993
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
10994
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
10994
10995
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
10995
10996
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
10996
10997
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
@@ -10991,6 +10991,7 @@ var AgentToolsSchema = exports_external.object({
10991
10991
  var AgentMemorySchema = exports_external.object({
10992
10992
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
10993
10993
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
10994
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
10994
10995
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
10995
10996
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
10996
10997
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
@@ -11739,6 +11739,7 @@ var AgentToolsSchema = exports_external.object({
11739
11739
  var AgentMemorySchema = exports_external.object({
11740
11740
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
11741
11741
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
11742
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
11742
11743
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
11743
11744
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
11744
11745
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
@@ -13555,6 +13555,7 @@ var init_schema = __esm(() => {
13555
13555
  AgentMemorySchema = exports_external.object({
13556
13556
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
13557
13557
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
13558
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
13558
13559
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
13559
13560
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
13560
13561
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
@@ -28938,6 +28939,82 @@ var init_manifest = __esm(() => {
28938
28939
  ]);
28939
28940
  });
28940
28941
 
28942
+ // src/cli/doctor-memory.ts
28943
+ import { execFileSync as execFileSync17 } from "node:child_process";
28944
+ function classifyShmSize(bytes) {
28945
+ const mib = Math.round(bytes / 1024 / 1024);
28946
+ if (bytes < MIN_HINDSIGHT_SHM_BYTES) {
28947
+ return {
28948
+ name: "hindsight shm-size",
28949
+ status: "fail",
28950
+ detail: `${mib}MB \u2014 PostgreSQL needs ~533MB+ for shared segments; writes will ` + `fail with "No space left on device"`,
28951
+ fix: "Recreate hindsight with a larger shm. The launch path now sets " + "--shm-size=2g (#2190); pull a release that includes it and run " + "`switchroom memory --restart`, or recreate the container manually " + "preserving the `switchroom-hindsight-data` volume."
28952
+ };
28953
+ }
28954
+ const gib = (bytes / 1024 / 1024 / 1024).toFixed(bytes % 1024 ** 3 === 0 ? 0 : 1);
28955
+ return { name: "hindsight shm-size", status: "ok", detail: `${gib}g` };
28956
+ }
28957
+ function classifyExtractionLogs(logs) {
28958
+ const noSpace = /No space left on device|could not resize shared memory/i.test(logs);
28959
+ const llmError = /Claude Code returned an error result|claude_code_llm[\s\S]{0,80}error|Fact extraction failed|Content extraction failed/i.test(logs);
28960
+ const quotaHint = /weekly limit|api_error_status["':\s]+429|\b429\b|hit your[\s\S]{0,24}limit/i.test(logs);
28961
+ const zeroFacts = (logs.match(/Extract facts:\s*0 facts/gi) ?? []).length;
28962
+ const okFacts = (logs.match(/Extract facts:\s*[1-9]\d* facts/gi) ?? []).length;
28963
+ if (noSpace) {
28964
+ return {
28965
+ name: "hindsight extraction",
28966
+ status: "fail",
28967
+ detail: "shared-memory exhaustion in recent logs \u2014 memory writes are failing",
28968
+ fix: "See the `hindsight shm-size` check \u2014 the container's /dev/shm is too small."
28969
+ };
28970
+ }
28971
+ if (llmError && okFacts === 0) {
28972
+ return {
28973
+ name: "hindsight extraction",
28974
+ status: "fail",
28975
+ detail: "fact-extraction LLM calls are failing" + (quotaHint ? " (429 / weekly-limit detected)" : "") + " \u2014 retains are accepted but extract 0 facts, so nothing becomes recallable",
28976
+ fix: "Usually hindsight's `auth.consumers[hindsight].account` is quota-" + "exhausted or its OAuth broke. Repoint it to an account with quota in " + "switchroom.yaml, then `docker restart switchroom-auth-broker` and " + "`docker restart switchroom-hindsight` (single-file config mount needs " + "the broker restart to re-read). Confirm with a `claude -p` inside the " + "container."
28977
+ };
28978
+ }
28979
+ if (zeroFacts >= 3 && okFacts === 0) {
28980
+ return {
28981
+ name: "hindsight extraction",
28982
+ status: "warn",
28983
+ detail: `${zeroFacts} recent extractions produced 0 facts and none succeeded \u2014 ` + "fact extraction may be failing",
28984
+ fix: "Inspect `docker logs switchroom-hindsight` for the extraction error."
28985
+ };
28986
+ }
28987
+ return {
28988
+ name: "hindsight extraction",
28989
+ status: "ok",
28990
+ detail: okFacts > 0 ? `healthy (${okFacts} recent successful extractions)` : "no recent extraction activity to assess"
28991
+ };
28992
+ }
28993
+ function checkHindsightContainerHealth(opts) {
28994
+ const name = opts?.containerName ?? "switchroom-hindsight";
28995
+ const exec = opts?.exec ?? ((cmd, args) => execFileSync17(cmd, args, { stdio: ["ignore", "pipe", "ignore"], timeout: 8000 }).toString());
28996
+ const results = [];
28997
+ let shmRaw;
28998
+ try {
28999
+ shmRaw = exec("docker", ["inspect", name, "--format", "{{.HostConfig.ShmSize}}"]).trim();
29000
+ } catch {
29001
+ return [];
29002
+ }
29003
+ const shmBytes = parseInt(shmRaw, 10);
29004
+ if (Number.isFinite(shmBytes) && shmBytes > 0) {
29005
+ results.push(classifyShmSize(shmBytes));
29006
+ }
29007
+ try {
29008
+ const logs = exec("docker", ["logs", "--since", "10m", name]);
29009
+ results.push(classifyExtractionLogs(logs));
29010
+ } catch {}
29011
+ return results;
29012
+ }
29013
+ var MIN_HINDSIGHT_SHM_BYTES;
29014
+ var init_doctor_memory = __esm(() => {
29015
+ MIN_HINDSIGHT_SHM_BYTES = 1024 * 1024 * 1024;
29016
+ });
29017
+
28941
29018
  // src/cli/doctor-docker.ts
28942
29019
  import { readFileSync as readFileSync47 } from "node:fs";
28943
29020
  function imageTagOf(ref) {
@@ -31017,7 +31094,7 @@ var init_doctor_agent_smoke = __esm(() => {
31017
31094
  });
31018
31095
 
31019
31096
  // src/cli/doctor-vault-broker-durability.ts
31020
- import { execFileSync as execFileSync17 } from "node:child_process";
31097
+ import { execFileSync as execFileSync18 } from "node:child_process";
31021
31098
  import { existsSync as existsSync54, statSync as statSync22 } from "node:fs";
31022
31099
  import { homedir as homedir33 } from "node:os";
31023
31100
  import { join as join55 } from "node:path";
@@ -31079,7 +31156,7 @@ function spawnDockerStat(p) {
31079
31156
  }
31080
31157
  function spawnDockerStatForContainer(containerName2, p) {
31081
31158
  try {
31082
- const stdout = execFileSync17("docker", ["exec", containerName2, "stat", "-c", "%i %s", p], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
31159
+ const stdout = execFileSync18("docker", ["exec", containerName2, "stat", "-c", "%i %s", p], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
31083
31160
  return { status: 0, stdout, stderr: "", error: null };
31084
31161
  } catch (err) {
31085
31162
  const e = err;
@@ -31153,7 +31230,7 @@ function probeBrokerUnlocked(opts) {
31153
31230
  }
31154
31231
  function defaultBrokerStatusProbe() {
31155
31232
  try {
31156
- const out = execFileSync17("switchroom", ["vault", "broker", "status"], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
31233
+ const out = execFileSync18("switchroom", ["vault", "broker", "status"], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
31157
31234
  const parsed = JSON.parse(out.trim());
31158
31235
  if (!parsed.running)
31159
31236
  return null;
@@ -31947,6 +32024,7 @@ async function checkHindsight(config) {
31947
32024
  detail: `${probe2.serverName} ${probe2.serverVersion} at ${host}:${port}`
31948
32025
  });
31949
32026
  results.push(checkHindsightConsumer(config));
32027
+ results.push(...checkHindsightContainerHealth());
31950
32028
  for (const [agentName, agentConfig] of Object.entries(config.agents)) {
31951
32029
  const bankId = agentConfig.memory?.collection ?? agentName;
31952
32030
  const hasBankMission = !!agentConfig.memory?.bank_mission;
@@ -33076,6 +33154,7 @@ var init_doctor = __esm(() => {
33076
33154
  init_accounts();
33077
33155
  init_manifest();
33078
33156
  init_hindsight();
33157
+ init_doctor_memory();
33079
33158
  init_doctor_docker();
33080
33159
  init_doctor_auth_broker();
33081
33160
  init_doctor_hostd();
@@ -49601,8 +49680,8 @@ var {
49601
49680
  } = import__.default;
49602
49681
 
49603
49682
  // src/build-info.ts
49604
- var VERSION = "0.14.74";
49605
- var COMMIT_SHA = "7e9ebb67";
49683
+ var VERSION = "0.14.75";
49684
+ var COMMIT_SHA = "8c331b53";
49606
49685
 
49607
49686
  // src/cli/agent.ts
49608
49687
  init_source();
@@ -51112,6 +51191,10 @@ function seedWorkspaceBootstrapFiles(params) {
51112
51191
  }
51113
51192
  if (entry === ".gitkeep")
51114
51193
  continue;
51194
+ if (params.seedMemoryFile === false && relPath.replace(/\.hbs$/, "") === "MEMORY.md") {
51195
+ params.skipped.push(join8(agentWorkspaceDir, "MEMORY.md"));
51196
+ continue;
51197
+ }
51115
51198
  if (entry.endsWith(".hbs")) {
51116
51199
  const destRel = relPath.replace(/\.hbs$/, "");
51117
51200
  const destPath = join8(agentWorkspaceDir, destRel);
@@ -51757,7 +51840,8 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
51757
51840
  context,
51758
51841
  created,
51759
51842
  skipped,
51760
- rewrittenWithBackup
51843
+ rewrittenWithBackup,
51844
+ seedMemoryFile: agentConfig.memory?.file !== false
51761
51845
  });
51762
51846
  ensureClaudeMdSymlinks(phase5WorkspaceDir, created);
51763
51847
  const persistentHomeDir = join8(agentDir, "home");
@@ -52786,7 +52870,8 @@ ${body}
52786
52870
  context: workspaceContext,
52787
52871
  created: changes,
52788
52872
  skipped: [],
52789
- rewrittenWithBackup: changes
52873
+ rewrittenWithBackup: changes,
52874
+ seedMemoryFile: agentConfig.memory?.file !== false
52790
52875
  });
52791
52876
  ensureClaudeMdSymlinks(reconcileWorkspaceDir, changes);
52792
52877
  }
@@ -66066,6 +66151,7 @@ var HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8;
66066
66151
  var HINDSIGHT_DEFAULT_MEM_LIMIT = "4g";
66067
66152
  var HINDSIGHT_DEFAULT_MEM_RESERVATION = "2g";
66068
66153
  var HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000;
66154
+ var HINDSIGHT_DEFAULT_SHM_SIZE = "2g";
66069
66155
  function isPortFree(port) {
66070
66156
  return new Promise((resolve27) => {
66071
66157
  const server = createServer4();
@@ -66156,6 +66242,7 @@ function startHindsight(ports) {
66156
66242
  `--memory=${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
66157
66243
  `--memory-reservation=${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
66158
66244
  `--pids-limit=${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
66245
+ `--shm-size=${HINDSIGHT_DEFAULT_SHM_SIZE}`,
66159
66246
  "-p",
66160
66247
  `127.0.0.1:${apiPort}:8888`,
66161
66248
  "-p",
@@ -66209,6 +66296,7 @@ function generateHindsightComposeSnippet() {
66209
66296
  ` mem_limit: ${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
66210
66297
  ` mem_reservation: ${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
66211
66298
  ` pids_limit: ${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
66299
+ ` shm_size: ${HINDSIGHT_DEFAULT_SHM_SIZE}`,
66212
66300
  " volumes:",
66213
66301
  " - switchroom-hindsight-data:/home/hindsight/.pg0",
66214
66302
  ` - ${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`,
@@ -71403,10 +71491,66 @@ async function proposeConfigEditViaHostd(args) {
71403
71491
  }
71404
71492
  }
71405
71493
 
71494
+ // src/web/api.ts
71495
+ import { randomUUID as randomUUID4 } from "node:crypto";
71496
+
71497
+ // src/web/microsoft-connect.ts
71498
+ init_oauth2();
71499
+ init_resolver();
71500
+ init_client2();
71501
+ async function startMicrosoftConnect(deps = {}) {
71502
+ const resolved = resolveMicrosoftClientId(deps.configClientId);
71503
+ if (isVaultReference(resolved.clientId)) {
71504
+ return { kind: "byo-vault", ref: resolved.clientId };
71505
+ }
71506
+ const scopes = selectMicrosoftScopes(deps.orgMode ?? false);
71507
+ const cfg = { client_id: resolved.clientId, scopes };
71508
+ try {
71509
+ const device = await (deps.requestDeviceCode ?? requestDeviceCode2)(cfg);
71510
+ return { kind: "started", device, clientId: resolved.clientId, scopes, source: resolved.source };
71511
+ } catch (err) {
71512
+ return { kind: "error", message: err.message };
71513
+ }
71514
+ }
71515
+ async function runMicrosoftConnectPoll(flow, deps = {}) {
71516
+ const now = deps.now ?? Date.now;
71517
+ const cfg = { client_id: flow.clientId, scopes: flow.scopes };
71518
+ let tokens;
71519
+ try {
71520
+ tokens = await (deps.pollDeviceToken ?? pollDeviceToken2)(cfg, flow.device, { now });
71521
+ } catch (err) {
71522
+ return { state: "failed", message: err.message };
71523
+ }
71524
+ const built = buildMicrosoftCredentials({
71525
+ tokens,
71526
+ clientId: flow.clientId,
71527
+ accountEmail: "",
71528
+ fallbackScope: flow.scopes.join(" "),
71529
+ now
71530
+ });
71531
+ if (!built.credentials.microsoftOauth.refreshToken) {
71532
+ return { state: "no-refresh-token" };
71533
+ }
71534
+ const account = built.resolvedEmail;
71535
+ if (!account) {
71536
+ return { state: "failed", message: "Microsoft returned no account identity (no id_token)." };
71537
+ }
71538
+ const addAccount = deps.addAccount ?? ((label, creds) => withAuthBrokerClient((client2) => client2.addAccount(label, creds, true, "microsoft")));
71539
+ try {
71540
+ await addAccount(account, built.credentials);
71541
+ } catch (err) {
71542
+ return { state: "failed", message: err.message };
71543
+ }
71544
+ return {
71545
+ state: "connected",
71546
+ account,
71547
+ accountType: built.credentials.microsoftOauth.accountType
71548
+ };
71549
+ }
71550
+
71406
71551
  // src/web/api.ts
71407
71552
  init_account_store();
71408
71553
  init_client2();
71409
- import { randomUUID as randomUUID4 } from "node:crypto";
71410
71554
 
71411
71555
  // telegram-plugin/registry/turns-schema.ts
71412
71556
  import { chmodSync as chmodSync8, mkdirSync as mkdirSync26 } from "fs";
@@ -72017,6 +72161,71 @@ function reapConnectionAccessStatuses(now = Date.now()) {
72017
72161
  function handleGetConnectionAccessStatus(requestId) {
72018
72162
  return connectionAccessStatuses.get(requestId) ?? { state: "unknown" };
72019
72163
  }
72164
+ var microsoftConnectStatuses = new Map;
72165
+ function reapMicrosoftConnects(now = Date.now()) {
72166
+ for (const [id, s] of microsoftConnectStatuses) {
72167
+ const ttl = s.state === "pending" ? s.expiresInSec * 1000 + 60000 : 30 * 60000;
72168
+ if (now - s.startedAt > ttl)
72169
+ microsoftConnectStatuses.delete(id);
72170
+ }
72171
+ }
72172
+ async function handleStartMicrosoftConnect(config, deps = {}) {
72173
+ const now = deps.now ?? Date.now;
72174
+ const configClientId = config.microsoft_workspace?.microsoft_client_id;
72175
+ const orgMode = deps.orgMode ?? config.microsoft_workspace?.org_mode === true;
72176
+ const started = await startMicrosoftConnect({ ...deps, configClientId, orgMode });
72177
+ if (started.kind === "byo-vault") {
72178
+ return {
72179
+ ok: false,
72180
+ error: `This install uses a vaulted custom Microsoft app (${started.ref}) the dashboard can't read. ` + `Connect from the host: switchroom auth microsoft account add <email>.`
72181
+ };
72182
+ }
72183
+ if (started.kind === "error") {
72184
+ return { ok: false, error: started.message };
72185
+ }
72186
+ const requestId = randomUUID4();
72187
+ microsoftConnectStatuses.set(requestId, {
72188
+ state: "pending",
72189
+ startedAt: now(),
72190
+ userCode: started.device.user_code,
72191
+ verificationUri: started.device.verification_uri,
72192
+ expiresInSec: started.device.expires_in
72193
+ });
72194
+ reapMicrosoftConnects(now());
72195
+ runMicrosoftConnectPoll({ device: started.device, clientId: started.clientId, scopes: started.scopes }, deps).then((res) => {
72196
+ const startedAt = microsoftConnectStatuses.get(requestId)?.startedAt ?? now();
72197
+ if (res.state === "connected") {
72198
+ microsoftConnectStatuses.set(requestId, {
72199
+ state: "connected",
72200
+ startedAt,
72201
+ account: res.account,
72202
+ accountType: res.accountType
72203
+ });
72204
+ captureEvent("microsoft_connect", { outcome: "connected", source: "web_api" });
72205
+ } else {
72206
+ const reason = res.state === "no-refresh-token" ? "Microsoft returned no refresh token (account would expire in ~1h)." : res.message;
72207
+ microsoftConnectStatuses.set(requestId, { state: "failed", startedAt, reason });
72208
+ }
72209
+ }).catch((err) => {
72210
+ const startedAt = microsoftConnectStatuses.get(requestId)?.startedAt ?? now();
72211
+ microsoftConnectStatuses.set(requestId, {
72212
+ state: "failed",
72213
+ startedAt,
72214
+ reason: err instanceof Error ? err.message : String(err)
72215
+ });
72216
+ captureException(err, { action: "microsoft_connect" });
72217
+ });
72218
+ return {
72219
+ ok: true,
72220
+ requestId,
72221
+ userCode: started.device.user_code,
72222
+ verificationUri: started.device.verification_uri,
72223
+ expiresInSec: started.device.expires_in
72224
+ };
72225
+ }
72226
+ function handleGetMicrosoftConnectStatus(requestId) {
72227
+ return microsoftConnectStatuses.get(requestId) ?? { state: "unknown" };
72228
+ }
72020
72229
  function handleSetConnectionAccess(configPath, config, args, deps = {}) {
72021
72230
  const provider = args.provider;
72022
72231
  if (provider !== "google" && provider !== "microsoft") {
@@ -72800,6 +73009,16 @@ function parseRoute(pathname, method) {
72800
73009
  params: { requestId: decodeURIComponent(accessStatusMatch[1]) }
72801
73010
  };
72802
73011
  }
73012
+ if (method === "POST" && pathname === "/api/connections/microsoft/connect") {
73013
+ return { handler: "startMicrosoftConnect", params: {} };
73014
+ }
73015
+ const msConnectMatch = pathname.match(/^\/api\/connections\/microsoft\/connect\/([^/]+)$/);
73016
+ if (method === "GET" && msConnectMatch) {
73017
+ return {
73018
+ handler: "getMicrosoftConnectStatus",
73019
+ params: { requestId: decodeURIComponent(msConnectMatch[1]) }
73020
+ };
73021
+ }
72803
73022
  if (method === "POST" && pathname === "/api/auth/use") {
72804
73023
  return { handler: "useAccount", params: {} };
72805
73024
  }
@@ -72976,6 +73195,13 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
72976
73195
  }
72977
73196
  case "getConnectionAccessStatus":
72978
73197
  return jsonResponse(handleGetConnectionAccessStatus(route.params.requestId));
73198
+ case "startMicrosoftConnect":
73199
+ return (async () => {
73200
+ const result = await handleStartMicrosoftConnect(freshConfig());
73201
+ return jsonResponse(result, result.ok ? 200 : 400);
73202
+ })();
73203
+ case "getMicrosoftConnectStatus":
73204
+ return jsonResponse(handleGetMicrosoftConnectStatus(route.params.requestId));
72979
73205
  case "refreshQuota": {
72980
73206
  return (async () => {
72981
73207
  let body = {};
@@ -75714,7 +75940,7 @@ import {
75714
75940
  } from "node:fs";
75715
75941
  import { dirname as dirname15, join as join60 } from "node:path";
75716
75942
  import { homedir as homedir35 } from "node:os";
75717
- import { execFileSync as execFileSync18 } from "node:child_process";
75943
+ import { execFileSync as execFileSync19 } from "node:child_process";
75718
75944
 
75719
75945
  class PythonEnvError extends Error {
75720
75946
  stderr;
@@ -75761,7 +75987,7 @@ function ensurePythonEnv(opts) {
75761
75987
  }
75762
75988
  mkdirSync33(dirname15(venvDir), { recursive: true });
75763
75989
  try {
75764
- execFileSync18(hostPython, ["-m", "venv", venvDir], { stdio: "pipe" });
75990
+ execFileSync19(hostPython, ["-m", "venv", venvDir], { stdio: "pipe" });
75765
75991
  } catch (err) {
75766
75992
  const e = err;
75767
75993
  throw new PythonEnvError(`Failed to create venv for skill "${skillName}" with ${hostPython}: ${e.message}`, e.stderr?.toString());
@@ -75773,7 +75999,7 @@ function ensurePythonEnv(opts) {
75773
75999
  delete childEnv.PIP_TARGET;
75774
76000
  delete childEnv.PIP_PREFIX;
75775
76001
  delete childEnv.PYTHONUSERBASE;
75776
- execFileSync18(pipBin, ["install", "--disable-pip-version-check", "-r", requirementsPath], { stdio: "pipe", env: childEnv });
76002
+ execFileSync19(pipBin, ["install", "--disable-pip-version-check", "-r", requirementsPath], { stdio: "pipe", env: childEnv });
75777
76003
  } catch (err) {
75778
76004
  const e = err;
75779
76005
  throw new PythonEnvError(`Failed to install requirements for skill "${skillName}": ${e.message}`, e.stderr?.toString());
@@ -75802,7 +76028,7 @@ import {
75802
76028
  } from "node:fs";
75803
76029
  import { dirname as dirname16, join as join61 } from "node:path";
75804
76030
  import { homedir as homedir36 } from "node:os";
75805
- import { execFileSync as execFileSync19 } from "node:child_process";
76031
+ import { execFileSync as execFileSync20 } from "node:child_process";
75806
76032
 
75807
76033
  class NodeEnvError extends Error {
75808
76034
  stderr;
@@ -75886,10 +76112,10 @@ function ensureNodeEnv(opts) {
75886
76112
  try {
75887
76113
  if (installer === "bun") {
75888
76114
  const args = copiedLockfile ? ["install", "--frozen-lockfile"] : ["install"];
75889
- execFileSync19("bun", args, { cwd: envDir, stdio: "pipe" });
76115
+ execFileSync20("bun", args, { cwd: envDir, stdio: "pipe" });
75890
76116
  } else {
75891
76117
  const args = copiedLockfile ? ["ci"] : ["install"];
75892
- execFileSync19("npm", args, { cwd: envDir, stdio: "pipe" });
76118
+ execFileSync20("npm", args, { cwd: envDir, stdio: "pipe" });
75893
76119
  }
75894
76120
  } catch (err) {
75895
76121
  const e = err;
@@ -77217,7 +77443,7 @@ function registerDebugCommand(program3) {
77217
77443
  init_source();
77218
77444
 
77219
77445
  // src/worktree/claim.ts
77220
- import { execFileSync as execFileSync20 } from "node:child_process";
77446
+ import { execFileSync as execFileSync21 } from "node:child_process";
77221
77447
  import { closeSync as closeSync12, mkdirSync as mkdirSync36, openSync as openSync12, existsSync as existsSync66, unlinkSync as unlinkSync13 } from "node:fs";
77222
77448
  import { join as join66, resolve as resolve42 } from "node:path";
77223
77449
  import { homedir as homedir39 } from "node:os";
@@ -77384,7 +77610,7 @@ async function claimWorktree(input, codeRepos) {
77384
77610
  releaseLock();
77385
77611
  }
77386
77612
  try {
77387
- execFileSync20("git", ["worktree", "add", "-b", branch, worktreePath], {
77613
+ execFileSync21("git", ["worktree", "add", "-b", branch, worktreePath], {
77388
77614
  cwd: repoPath,
77389
77615
  stdio: "pipe"
77390
77616
  });
@@ -77397,7 +77623,7 @@ async function claimWorktree(input, codeRepos) {
77397
77623
  }
77398
77624
 
77399
77625
  // src/worktree/release.ts
77400
- import { execFileSync as execFileSync21 } from "node:child_process";
77626
+ import { execFileSync as execFileSync22 } from "node:child_process";
77401
77627
  import { existsSync as existsSync67 } from "node:fs";
77402
77628
  function releaseWorktree(input) {
77403
77629
  const { id } = input;
@@ -77408,7 +77634,7 @@ function releaseWorktree(input) {
77408
77634
  let gitSuccess = true;
77409
77635
  if (existsSync67(record2.path)) {
77410
77636
  try {
77411
- execFileSync21("git", ["worktree", "remove", "--force", record2.path], {
77637
+ execFileSync22("git", ["worktree", "remove", "--force", record2.path], {
77412
77638
  cwd: record2.repo,
77413
77639
  stdio: "pipe"
77414
77640
  });
@@ -77444,16 +77670,16 @@ function listWorktrees() {
77444
77670
  }
77445
77671
 
77446
77672
  // src/worktree/reaper.ts
77447
- import { execFileSync as execFileSync22 } from "node:child_process";
77673
+ import { execFileSync as execFileSync23 } from "node:child_process";
77448
77674
  import { existsSync as existsSync68 } from "node:fs";
77449
77675
  var STALE_THRESHOLD_MS = 10 * 60 * 1000;
77450
77676
  function isPathInUse(path7) {
77451
77677
  try {
77452
- execFileSync22("fuser", [path7], { stdio: "pipe" });
77678
+ execFileSync23("fuser", [path7], { stdio: "pipe" });
77453
77679
  return true;
77454
77680
  } catch {}
77455
77681
  try {
77456
- const out = execFileSync22("lsof", ["-t", path7], {
77682
+ const out = execFileSync23("lsof", ["-t", path7], {
77457
77683
  stdio: ["ignore", "pipe", "ignore"]
77458
77684
  }).toString().trim();
77459
77685
  if (out.length > 0)
@@ -77463,7 +77689,7 @@ function isPathInUse(path7) {
77463
77689
  }
77464
77690
  function hasUncommittedChanges(repoPath, worktreePath) {
77465
77691
  try {
77466
- const out = execFileSync22("git", ["-C", worktreePath, "status", "--porcelain"], { stdio: "pipe" }).toString();
77692
+ const out = execFileSync23("git", ["-C", worktreePath, "status", "--porcelain"], { stdio: "pipe" }).toString();
77467
77693
  return out.trim().length > 0;
77468
77694
  } catch {
77469
77695
  return false;
@@ -77477,7 +77703,7 @@ function reapRecord(record2) {
77477
77703
  warning = `[worktree-reaper] Reaped worktree with uncommitted changes: ` + `id=${id} branch=${branch} agent=${ownerAgent ?? "unknown"} path=${path7}`;
77478
77704
  }
77479
77705
  try {
77480
- execFileSync22("git", ["worktree", "remove", "--force", path7], {
77706
+ execFileSync23("git", ["worktree", "remove", "--force", path7], {
77481
77707
  cwd: repo,
77482
77708
  stdio: "pipe"
77483
77709
  });
@@ -78960,7 +79186,7 @@ agents:
78960
79186
  init_resolver();
78961
79187
  import { dirname as dirname21, join as join72, resolve as resolve44 } from "node:path";
78962
79188
  import { homedir as homedir41 } from "node:os";
78963
- import { execFileSync as execFileSync23 } from "node:child_process";
79189
+ import { execFileSync as execFileSync24 } from "node:child_process";
78964
79190
  init_vault();
78965
79191
  init_loader();
78966
79192
  init_loader();
@@ -79368,7 +79594,7 @@ async function ensureHostMountSources(config) {
79368
79594
  }
79369
79595
  function detectComposeV2() {
79370
79596
  try {
79371
- const out = execFileSync23("docker", ["compose", "version"], {
79597
+ const out = execFileSync24("docker", ["compose", "version"], {
79372
79598
  stdio: ["ignore", "pipe", "pipe"],
79373
79599
  encoding: "utf8"
79374
79600
  });
@@ -559,6 +559,51 @@
559
559
  }
560
560
  }
561
561
 
562
+ // Start an in-browser Microsoft connect: show the device code + link,
563
+ // then poll until the operator completes sign-in on Microsoft's site.
564
+ async function connectMicrosoft() {
565
+ const card = document.getElementById('ms-connect-card');
566
+ const show = (html) => { if (card) card.innerHTML = html; };
567
+ show('<div class="loading" style="padding:.8rem">Starting…</div>');
568
+ try {
569
+ const res = await fetch(`${API}/api/connections/microsoft/connect`, { method: 'POST', headers: authHeaders() });
570
+ const data = await res.json();
571
+ if (!res.ok || !data.ok) { show(''); showError(data.error || `HTTP ${res.status}`); return; }
572
+ const url = data.verificationUri, code = data.userCode;
573
+ show(`<div class="account-card" style="border-color:var(--accent)">
574
+ <div class="account-card-header"><div class="account-label">Connect a Microsoft account</div></div>
575
+ <div style="padding:.3rem 0;line-height:1.7">
576
+ 1. Open <a href="${escapeHtml(url)}" target="_blank" rel="noopener" style="color:var(--accent)">${escapeHtml(url)}</a><br>
577
+ 2. Enter code: <code style="font-size:1.15rem;letter-spacing:.08em">${escapeHtml(code)}</code><br>
578
+ 3. Approve the requested permissions (Mail, Calendar, Files).
579
+ </div>
580
+ <div id="ms-connect-status" style="color:var(--text-dim);margin-top:.3rem">Waiting for sign-in… (this card expires in ~15 min)</div>
581
+ </div>`);
582
+ const statusEl = () => document.getElementById('ms-connect-status');
583
+ const started = Date.now();
584
+ const poll = async () => {
585
+ const sres = await fetch(`${API}/api/connections/microsoft/connect/${encodeURIComponent(data.requestId)}`, { headers: authHeaders() });
586
+ const s = sres.ok ? await sres.json() : { state: 'failed', reason: `HTTP ${sres.status}` };
587
+ if (s.state === 'pending') {
588
+ if (Date.now() - started > ((data.expiresInSec || 900) * 1000 + 30000)) { const e = statusEl(); if (e) e.textContent = 'Expired — click Connect to try again.'; return; }
589
+ setTimeout(poll, 3000);
590
+ return;
591
+ }
592
+ if (s.state === 'connected') {
593
+ show(`<div class="loading" style="padding:.8rem;color:var(--green)">✓ Connected ${escapeHtml(s.account)} (${escapeHtml(s.accountType)}). Use the access toggles below to grant an agent.</div>`);
594
+ fetchConnections();
595
+ } else {
596
+ show('');
597
+ showError(s.reason || 'connect failed');
598
+ }
599
+ };
600
+ setTimeout(poll, 3000);
601
+ } catch (err) {
602
+ show('');
603
+ showError(err.message);
604
+ }
605
+ }
606
+
562
607
  async function fetchSchedule() {
563
608
  try {
564
609
  const res = await fetch(`${API}/api/schedule`, { headers: authHeaders() });
@@ -1115,11 +1160,18 @@
1115
1160
  google.map(a => renderOAuthAccountCard(a, { showType: false, provider: 'google', agentNames })).join(''),
1116
1161
  );
1117
1162
 
1118
- const microsoftSection = _connectionSection(
1119
- 'Microsoft 365',
1120
- 'No Microsoft accounts. Connect one from Telegram with <code>/connect microsoft</code> (admin DM), or <code>switchroom auth microsoft account add</code>.',
1121
- microsoft.map(a => renderOAuthAccountCard(a, { showType: true, provider: 'microsoft', agentNames })).join(''),
1122
- );
1163
+ const msCards = microsoft.map(a => renderOAuthAccountCard(a, { showType: true, provider: 'microsoft', agentNames })).join('');
1164
+ const microsoftSection = `
1165
+ <div style="margin-bottom:1.5rem">
1166
+ <h3 style="margin:0 0 .6rem;font-size:.95rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.04em">
1167
+ Microsoft 365
1168
+ <button onclick="connectMicrosoft()" class="usage-pill primary" style="margin-left:.6rem;cursor:pointer;border:none;text-transform:none;font-weight:600">+ Connect a Microsoft account</button>
1169
+ </h3>
1170
+ <div id="ms-connect-card"></div>
1171
+ ${msCards
1172
+ ? `<div class="accounts-grid">${msCards}</div>`
1173
+ : `<div class="loading" style="padding:.8rem">No Microsoft accounts yet — click <b>Connect a Microsoft account</b> above (or <code>/connect microsoft</code> from Telegram).</div>`}
1174
+ </div>`;
1123
1175
 
1124
1176
  let notionCards = '';
1125
1177
  if (notion.configured) {
@@ -13726,6 +13726,7 @@ var AgentToolsSchema = exports_external.object({
13726
13726
  var AgentMemorySchema = exports_external.object({
13727
13727
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
13728
13728
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
13729
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
13729
13730
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
13730
13731
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
13731
13732
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
@@ -11312,6 +11312,7 @@ var init_schema = __esm(() => {
11312
11312
  AgentMemorySchema = exports_external.object({
11313
11313
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
11314
11314
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
11315
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
11315
11316
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
11316
11317
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
11317
11318
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
@@ -11312,6 +11312,7 @@ var init_schema = __esm(() => {
11312
11312
  AgentMemorySchema = exports_external.object({
11313
11313
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
11314
11314
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
11315
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
11315
11316
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
11316
11317
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
11317
11318
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.74",
3
+ "version": "0.14.75",
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": {
@@ -23815,6 +23815,7 @@ var init_schema = __esm(() => {
23815
23815
  AgentMemorySchema = exports_external.object({
23816
23816
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
23817
23817
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
23818
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
23818
23819
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
23819
23820
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
23820
23821
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
@@ -52819,10 +52820,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
52819
52820
  }
52820
52821
 
52821
52822
  // ../src/build-info.ts
52822
- var VERSION = "0.14.74";
52823
- var COMMIT_SHA = "7e9ebb67";
52824
- var COMMIT_DATE = "2026-06-06T02:56:40Z";
52825
- var LATEST_PR = 2189;
52823
+ var VERSION = "0.14.75";
52824
+ var COMMIT_SHA = "8c331b53";
52825
+ var COMMIT_DATE = "2026-06-06T06:33:56Z";
52826
+ var LATEST_PR = 2192;
52826
52827
  var COMMITS_AHEAD_OF_TAG = 0;
52827
52828
 
52828
52829
  // gateway/boot-version.ts