switchroom 0.13.53 → 0.13.55

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.
@@ -13520,7 +13520,7 @@ var init_zod = __esm(() => {
13520
13520
  });
13521
13521
 
13522
13522
  // src/config/schema.ts
13523
- var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
13523
+ var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
13524
13524
  var init_schema = __esm(() => {
13525
13525
  init_zod();
13526
13526
  CodeRepoEntrySchema = exports_external.object({
@@ -13561,7 +13561,8 @@ var init_schema = __esm(() => {
13561
13561
  recall: exports_external.object({
13562
13562
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
13563
13563
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
13564
- min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0\u20131.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default \u2014 " + "current behaviour). Try 0.10\u20130.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`.")
13564
+ min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum Jaccard token overlap [0.0\u20131.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "Drops low-relevance matches before the count cap so weak hits " + "don't fill the slot on real queries. 0.0 disables (default \u2014 " + "current behaviour). Try 0.10\u20130.20 to start; observe the " + "`overlap_dropped` field via `switchroom memory recall-log`."),
13565
+ topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) \u2192 soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: \u2026' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
13565
13566
  }).optional().describe("Auto-recall tuning knobs")
13566
13567
  }).optional();
13567
13568
  HookEntrySchema = exports_external.object({
@@ -13704,6 +13705,16 @@ var init_schema = __esm(() => {
13704
13705
  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."),
13705
13706
  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.")
13706
13707
  }).optional();
13708
+ NotionWorkspaceConfigSchema = exports_external.object({
13709
+ vault_key: exports_external.string().min(1).default("notion/integration-token").describe("Vault key holding the Notion internal-integration token. Default " + "`notion/integration-token`. Override only for non-standard vault " + "layouts. The broker's --allow ACL on this key is the authoritative " + "list of which agents may receive the token."),
13710
+ databases: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
13711
+ message: "notion_workspace.databases friendly names must match " + "/^[a-z0-9][a-z0-9_-]{0,62}$/ \u2014 lowercase letters, digits, " + "hyphens, underscores. Got: '%s'."
13712
+ }), exports_external.string().regex(/^[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}$/, {
13713
+ message: "notion_workspace.databases values must be Notion database " + "UUIDs (32 hex characters, optional dashes)."
13714
+ })).default({}).describe("Friendly-name \u2192 Notion database UUID map. Operator-managed; agents " + "reference databases by friendly name only \u2014 they never see or type " + "UUIDs. Populate via `switchroom notion list-dbs` (PR 4) after " + "vault-putting the integration token and sharing DBs with the " + "integration in Notion's UI."),
13715
+ mcp_version: exports_external.string().min(1).optional().describe("Optional pin for the upstream `@notionhq/notion-mcp-server` npm " + "package version. Default is the build-time `NOTION_MCP_PINNED_VERSION` " + "constant. Override only when reproducing operator-specific bugs."),
13716
+ rate_limit_rps: exports_external.number().int().positive().max(10).optional().describe("Optional global rate-limit budget in requests per second across all " + "switchroom agents sharing this integration token. Defaults to 3 " + "(Notion's documented public-API limit). Lower it if you also use " + "the integration token from outside switchroom and need to share " + "budget. Higher than 10 is rejected \u2014 if you think you need it, " + "your usage probably needs a workspace-tier upgrade with Notion.")
13717
+ }).optional();
13707
13718
  AgentGoogleWorkspaceConfigSchema = exports_external.object({
13708
13719
  account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
13709
13720
  message: "google_workspace.account must be a Google account email like " + "'alice@example.com' (colons not allowed)"
@@ -13717,6 +13728,13 @@ var init_schema = __esm(() => {
13717
13728
  }).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)."),
13718
13729
  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).")
13719
13730
  }).optional();
13731
+ AgentNotionWorkspaceConfigSchema = exports_external.object({
13732
+ databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
13733
+ message: "notion_workspace.databases entries must be friendly names " + "matching /^[a-z0-9][a-z0-9_-]{0,62}$/ \u2014 these must appear as " + "keys in top-level notion_workspace.databases."
13734
+ })).min(1, {
13735
+ message: "notion_workspace.databases must list at least one friendly " + "name. An empty list rejects every Notion tool call \u2014 if you " + "want to remove this agent's Notion access, delete the entire " + "notion_workspace block instead."
13736
+ }).optional().describe("Optional per-agent allowlist of database friendly names this " + "agent may read/write. Each name must exist as a key in top-level " + "notion_workspace.databases. Omit the field (or leave it undefined) " + "to grant access to every DB the upstream integration can see \u2014 " + "appropriate for an admin/orchestrator agent. Set the list to " + "narrow access for specialist agents.")
13737
+ }).optional();
13720
13738
  ReactionsSchema = exports_external.object({
13721
13739
  enabled: exports_external.boolean().optional().describe("Master switch for the reaction-trigger path. When false, " + "reactions are still persisted via recordReaction but never " + "dispatched to the agent as synthetic inbound turns. Default true."),
13722
13740
  trigger_emojis: exports_external.array(exports_external.string()).optional().describe("Emoji allowlist that triggers a synthetic inbound when reacted " + "to a bot message. Default ['\uD83D\uDC4E', '\u274c', '\uD83D\uDC4D', '\u2705']. Cascade " + "mode: REPLACE (not union) \u2014 setting this at a layer replaces " + "lower layers entirely, so an operator can narrow to [] to " + "disable triggering without flipping `enabled`."),
@@ -13853,6 +13871,7 @@ var init_schema = __esm(() => {
13853
13871
  drive: AgentGoogleWorkspaceConfigSchema.describe("RFC D legacy key \u2014 use `google_workspace:` instead. Per-agent " + "google_workspace overrides (currently approvers + tier). When set, " + "replaces the top-level approvers list for this agent. " + "google_client_id/secret are not per-agent \u2014 they live at the top level."),
13854
13872
  google_workspace: AgentGoogleWorkspaceConfigSchema.describe("RFC G canonical key. Per-agent Google Workspace overrides \u2014 currently " + "approvers (replaces, does not extend the top-level list) and tier " + "(`core` | `extended` | `complete`, replaces top-level default). " + "google_client_id/secret are not per-agent \u2014 they live at the top level. " + "Mutually exclusive with `drive:` on the same agent (loader fails fast " + "if both are set)."),
13855
13873
  microsoft_workspace: AgentMicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Per-agent Microsoft Workspace " + "override \u2014 pins the Microsoft account this agent reads via the " + "auth-broker (must be a key in top-level `microsoft_accounts:` with " + "this agent in its `enabled_for[]`) and optionally overrides org_mode. " + "microsoft_client_id/secret are not per-agent."),
13874
+ notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write \u2014 names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
13856
13875
  repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
13857
13876
  url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
13858
13877
  branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
@@ -13976,6 +13995,7 @@ var init_schema = __esm(() => {
13976
13995
  drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key \u2014 use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
13977
13996
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration \u2014 " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
13978
13997
  microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration \u2014 OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
13998
+ notion_workspace: NotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Top-level Notion integration " + "config \u2014 vault key for the integration token, friendly-name \u2192 " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
13979
13999
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
13980
14000
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
13981
14001
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a \u2014 disabled by default)."),
@@ -14525,6 +14545,46 @@ var init_merge = __esm(() => {
14525
14545
  })(mergeAgentConfig ||= {});
14526
14546
  });
14527
14547
 
14548
+ // src/config/notion-workspace-acl.ts
14549
+ function shouldEmitNotionMcp(agentName, config) {
14550
+ if (!agentName)
14551
+ return false;
14552
+ if (!config.notion_workspace)
14553
+ return false;
14554
+ const agentConfig = config.agents?.[agentName];
14555
+ if (!agentConfig)
14556
+ return false;
14557
+ if (agentConfig.notion_workspace === undefined)
14558
+ return false;
14559
+ return true;
14560
+ }
14561
+ function validateNotionWorkspaceConfig(config) {
14562
+ const issues = [];
14563
+ const dbMap = config.notion_workspace?.databases ?? {};
14564
+ const known = new Set(Object.keys(dbMap));
14565
+ for (const [agentName, agentRaw] of Object.entries(config.agents ?? {})) {
14566
+ if (!agentRaw)
14567
+ continue;
14568
+ const dbFilter = agentRaw.notion_workspace?.databases;
14569
+ if (dbFilter === undefined)
14570
+ continue;
14571
+ if (dbFilter.length === 0) {
14572
+ issues.push(` agents.${agentName}.notion_workspace.databases is an empty ` + `list. Delete the entire notion_workspace block to remove Notion ` + `access, or list at least one database friendly name.`);
14573
+ continue;
14574
+ }
14575
+ if (config.notion_workspace === undefined) {
14576
+ issues.push(` agents.${agentName}.notion_workspace is set but the top-level ` + `notion_workspace block is missing. Configure the integration ` + `globally first (vault_key + databases map), then grant per-agent ` + `access.`);
14577
+ continue;
14578
+ }
14579
+ for (const name of dbFilter) {
14580
+ if (!known.has(name)) {
14581
+ issues.push(` agents.${agentName}.notion_workspace.databases references ` + `unknown database "${name}". Add it to top-level ` + `notion_workspace.databases (run \`switchroom notion list-dbs\` ` + `to see the UUIDs the integration can read), or remove the ` + `reference.`);
14582
+ }
14583
+ }
14584
+ }
14585
+ return issues;
14586
+ }
14587
+
14528
14588
  // src/config/loader.ts
14529
14589
  var exports_loader = {};
14530
14590
  __export(exports_loader, {
@@ -14647,6 +14707,10 @@ function loadConfig(configPath) {
14647
14707
  }
14648
14708
  applyAgentOverlays(config);
14649
14709
  validateAllCronTopicAliases(config, filePath);
14710
+ const notionIssues = validateNotionWorkspaceConfig(config);
14711
+ if (notionIssues.length > 0) {
14712
+ throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
14713
+ }
14650
14714
  return config;
14651
14715
  }
14652
14716
  function validateAllCronTopicAliases(config, filePath) {
@@ -20767,6 +20831,30 @@ function getGdriveMcpSettingsEntry(switchroomCliPath, options = {}) {
20767
20831
  }
20768
20832
  };
20769
20833
  }
20834
+ function getMs365McpSettingsEntry(switchroomCliPath, options = {}) {
20835
+ const orgArgs = options.orgMode ? ["--org-mode"] : [];
20836
+ return {
20837
+ key: "ms-365",
20838
+ value: {
20839
+ command: switchroomCliPath,
20840
+ args: ["m365-mcp-launcher", ...orgArgs]
20841
+ }
20842
+ };
20843
+ }
20844
+ function getNotionMcpSettingsEntry(switchroomCliPath, options = {}) {
20845
+ const args = ["notion-mcp-launcher"];
20846
+ if (options.vaultKey)
20847
+ args.push("--vault-key", options.vaultKey);
20848
+ if (options.mcpVersion)
20849
+ args.push("--mcp-version", options.mcpVersion);
20850
+ return {
20851
+ key: "notion",
20852
+ value: {
20853
+ command: switchroomCliPath,
20854
+ args
20855
+ }
20856
+ };
20857
+ }
20770
20858
  function getBuiltinDefaultMcpEntries() {
20771
20859
  const playwright = getPlaywrightMcpSettingsEntry();
20772
20860
  return [
@@ -20794,7 +20882,7 @@ function getBuiltinDefaultSkillEntries() {
20794
20882
  ...switchroomCore.map((key) => ({ key, optOutKey: key, source: "switchroom" }))
20795
20883
  ];
20796
20884
  }
20797
- var GOOGLE_WORKSPACE_MCP_PINNED_SHA = "9d69115b63e6bc2ef0d4b5d7a3b962396382b44c";
20885
+ var GOOGLE_WORKSPACE_MCP_PINNED_SHA = "9d69115b63e6bc2ef0d4b5d7a3b962396382b44c", MICROSOFT_WORKSPACE_MCP_PINNED_VERSION = "0.113.0", MICROSOFT_WORKSPACE_MCP_PACKAGE = "@softeria/ms-365-mcp-server", NOTION_MCP_PINNED_VERSION = "1.8.1", NOTION_MCP_PACKAGE = "@notionhq/notion-mcp-server", NOTION_TOKEN_ENV = "NOTION_TOKEN";
20798
20886
  var init_scaffold_integration = __esm(() => {
20799
20887
  init_hindsight();
20800
20888
  });
@@ -29571,21 +29659,403 @@ var init_doctor_drive = __esm(() => {
29571
29659
  init_doctor_secret_access();
29572
29660
  });
29573
29661
 
29574
- // src/cli/doctor-credentials-migration.ts
29662
+ // src/cli/doctor-microsoft.ts
29575
29663
  import {
29576
29664
  existsSync as realExistsSync2,
29577
- readdirSync as realReaddirSync,
29578
- statSync as realStatSync
29665
+ readFileSync as realReadFileSync2
29579
29666
  } from "node:fs";
29580
- import { homedir as homedir25 } from "node:os";
29581
29667
  import { join as join45 } from "node:path";
29668
+ import { homedir as homedir25 } from "node:os";
29669
+ function resolveDeps2(deps) {
29670
+ const home2 = deps.homeDir?.() ?? homedir25();
29671
+ return {
29672
+ existsSync: deps.existsSync ?? realExistsSync2,
29673
+ readFileSync: deps.readFileSync ?? realReadFileSync2,
29674
+ agentsDir: join45(home2, ".switchroom", "agents"),
29675
+ now: deps.now ?? Date.now
29676
+ };
29677
+ }
29678
+ function agentMicrosoftAccount(config, agentName) {
29679
+ const agent = config.agents?.[agentName];
29680
+ if (!agent)
29681
+ return;
29682
+ return agent.microsoft_workspace?.account;
29683
+ }
29684
+ function clientValuePresent2(v) {
29685
+ return typeof v === "string" && v.length > 0;
29686
+ }
29687
+ function checkConfigMatrix2(config) {
29688
+ const results = [];
29689
+ const accounts = config.microsoft_accounts ?? {};
29690
+ for (const [name, agent] of Object.entries(config.agents ?? {})) {
29691
+ const acct = agent.microsoft_workspace?.account;
29692
+ if (!acct)
29693
+ continue;
29694
+ const entry = accounts[acct];
29695
+ if (!entry) {
29696
+ results.push({
29697
+ name: `microsoft:matrix:${name}`,
29698
+ status: "fail",
29699
+ detail: `agent '${name}' has microsoft_workspace.account=${acct} but no microsoft_accounts.${acct} block`,
29700
+ fix: `Run 'switchroom auth microsoft account add ${acct}' to register, then 'switchroom auth microsoft enable ${acct} ${name}'.`
29701
+ });
29702
+ continue;
29703
+ }
29704
+ if (!(entry.enabled_for ?? []).includes(name)) {
29705
+ results.push({
29706
+ name: `microsoft:matrix:${name}`,
29707
+ status: "fail",
29708
+ detail: `agent '${name}' is selected to use microsoft account ${acct}, but not listed in microsoft_accounts.${acct}.enabled_for[]`,
29709
+ fix: `Run 'switchroom auth microsoft enable ${acct} ${name}' to grant access.`
29710
+ });
29711
+ continue;
29712
+ }
29713
+ results.push({
29714
+ name: `microsoft:matrix:${name}`,
29715
+ status: "ok",
29716
+ detail: `agent '${name}' aligned with account ${acct}`
29717
+ });
29718
+ }
29719
+ for (const [acct, entry] of Object.entries(accounts)) {
29720
+ for (const name of entry.enabled_for ?? []) {
29721
+ const agentAcct = agentMicrosoftAccount(config, name);
29722
+ if (agentAcct !== acct) {
29723
+ const got = agentAcct ?? "(unset)";
29724
+ results.push({
29725
+ name: `microsoft:matrix:reverse:${name}:${acct}`,
29726
+ status: "fail",
29727
+ detail: `microsoft_accounts.${acct}.enabled_for[] includes '${name}', but agent's microsoft_workspace.account is '${got}' \u2014 broker will return ACCOUNT_NOT_FOUND for this agent`,
29728
+ fix: `Either set agents.${name}.microsoft_workspace.account=${acct}, or remove '${name}' from microsoft_accounts.${acct}.enabled_for[] via 'switchroom auth microsoft disable ${acct} ${name}'.`
29729
+ });
29730
+ }
29731
+ }
29732
+ }
29733
+ return results;
29734
+ }
29735
+ function checkOAuthClient2(config, anyAgentEnabled) {
29736
+ if (!anyAgentEnabled)
29737
+ return [];
29738
+ const mw = config.microsoft_workspace;
29739
+ if (!mw) {
29740
+ return [
29741
+ {
29742
+ name: "microsoft:oauth-client-configured",
29743
+ status: "fail",
29744
+ detail: "agents have microsoft_workspace.account set but the top-level microsoft_workspace: block is missing",
29745
+ 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."
29746
+ }
29747
+ ];
29748
+ }
29749
+ if (!clientValuePresent2(mw.microsoft_client_id)) {
29750
+ return [
29751
+ {
29752
+ name: "microsoft:oauth-client-configured",
29753
+ status: "fail",
29754
+ detail: "microsoft_workspace block present but microsoft_client_id is empty",
29755
+ 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`."
29756
+ }
29757
+ ];
29758
+ }
29759
+ return [
29760
+ {
29761
+ name: "microsoft:oauth-client-configured",
29762
+ status: "ok",
29763
+ detail: clientValuePresent2(mw.microsoft_client_secret) ? "microsoft_client_id + microsoft_client_secret present (confidential client)" : "microsoft_client_id present (public-client app, no secret)"
29764
+ }
29765
+ ];
29766
+ }
29767
+ function readHeartbeat(d, agentName) {
29768
+ const path4 = join45(d.agentsDir, agentName, "m365-launcher.heartbeat.json");
29769
+ if (!d.existsSync(path4)) {
29770
+ return { error: "heartbeat file missing \u2014 launcher has not yet started" };
29771
+ }
29772
+ try {
29773
+ const raw = d.readFileSync(path4, "utf-8");
29774
+ const parsed = JSON.parse(raw);
29775
+ if (typeof parsed.lastRefreshMs !== "number" || typeof parsed.nextRefreshMs !== "number" || typeof parsed.expiresAtMs !== "number") {
29776
+ return { error: "heartbeat file malformed (missing required numeric fields)" };
29777
+ }
29778
+ return parsed;
29779
+ } catch (err) {
29780
+ return { error: `heartbeat parse error: ${err instanceof Error ? err.message : String(err)}` };
29781
+ }
29782
+ }
29783
+ function checkLauncherHeartbeat(msEnabledAgents, d) {
29784
+ const results = [];
29785
+ for (const name of msEnabledAgents) {
29786
+ const hb = readHeartbeat(d, name);
29787
+ if ("error" in hb) {
29788
+ results.push({
29789
+ name: `microsoft:launcher-heartbeat:${name}`,
29790
+ status: "warn",
29791
+ detail: `${hb.error} (path: ~/.switchroom/agents/${name}/m365-launcher.heartbeat.json)`,
29792
+ fix: `Launcher writes the heartbeat on its first refresh tick. If agent has been running for >5min and the file is still missing, check the launcher log via 'docker logs switchroom-${name}'.`
29793
+ });
29794
+ continue;
29795
+ }
29796
+ const now = d.now();
29797
+ const ageSinceLastRefresh = now - hb.lastRefreshMs;
29798
+ const untilNextRefresh = hb.nextRefreshMs - now;
29799
+ if (ageSinceLastRefresh > 90 * 60 * 1000) {
29800
+ results.push({
29801
+ name: `microsoft:launcher-heartbeat:${name}`,
29802
+ status: "fail",
29803
+ detail: `last refresh was ${Math.round(ageSinceLastRefresh / 60000)}min ago (>90min) \u2014 launcher refresh loop appears dead`,
29804
+ fix: `Restart the agent: 'switchroom agent restart ${name}'. If problem recurs, check launcher logs for broker/network failures.`
29805
+ });
29806
+ } else if (untilNextRefresh < -5 * 60 * 1000) {
29807
+ results.push({
29808
+ name: `microsoft:launcher-heartbeat:${name}`,
29809
+ status: "warn",
29810
+ detail: `nextRefreshMs was ${Math.round(-untilNextRefresh / 60000)}min ago \u2014 refresh tick missed its scheduled time`,
29811
+ fix: `Tick is overdue but not yet stale. Monitor; if it doesn't update within a few minutes, restart the agent.`
29812
+ });
29813
+ } else {
29814
+ results.push({
29815
+ name: `microsoft:launcher-heartbeat:${name}`,
29816
+ status: "ok",
29817
+ detail: `last refresh ${Math.round(ageSinceLastRefresh / 60000)}min ago, next in ${Math.round(untilNextRefresh / 60000)}min`
29818
+ });
29819
+ }
29820
+ }
29821
+ return results;
29822
+ }
29823
+ function computeMicrosoftEnabledAgents(config) {
29824
+ const accounts = config.microsoft_accounts ?? {};
29825
+ return Object.keys(config.agents ?? {}).filter((name) => {
29826
+ const acct = agentMicrosoftAccount(config, name);
29827
+ return !!acct && !!accounts[acct] && (accounts[acct].enabled_for ?? []).includes(name);
29828
+ });
29829
+ }
29830
+ function runMicrosoftChecks(config, deps = {}) {
29831
+ const accounts = config.microsoft_accounts;
29832
+ const anyAgentAccount = Object.keys(config.agents ?? {}).some((n) => agentMicrosoftAccount(config, n) !== undefined);
29833
+ const accountsConfigured = !!accounts && Object.keys(accounts).length > 0;
29834
+ if (!accountsConfigured && !anyAgentAccount && !config.microsoft_workspace) {
29835
+ return [];
29836
+ }
29837
+ const d = resolveDeps2(deps);
29838
+ const results = [];
29839
+ results.push(...checkConfigMatrix2(config));
29840
+ const msAgents = computeMicrosoftEnabledAgents(config);
29841
+ results.push(...checkOAuthClient2(config, msAgents.length > 0));
29842
+ results.push(...checkLauncherHeartbeat(msAgents, d));
29843
+ return results;
29844
+ }
29845
+ var init_doctor_microsoft = () => {};
29846
+
29847
+ // src/cli/doctor-notion.ts
29848
+ import {
29849
+ existsSync as realExistsSync3,
29850
+ readFileSync as realReadFileSync3,
29851
+ statSync as realStatSync
29852
+ } from "node:fs";
29853
+ import { join as join46 } from "node:path";
29854
+ import { homedir as homedir26 } from "node:os";
29855
+ function resolveDeps3(deps) {
29856
+ const home2 = deps.homeDir?.() ?? homedir26();
29857
+ return {
29858
+ existsSync: deps.existsSync ?? realExistsSync3,
29859
+ readFileSync: deps.readFileSync ?? realReadFileSync3,
29860
+ statSync: deps.statSync ?? realStatSync,
29861
+ agentsDir: join46(home2, ".switchroom", "agents"),
29862
+ now: deps.now ?? Date.now,
29863
+ vaultAclReader: deps.vaultAclReader ?? (async () => ({ kind: "unreachable", msg: "no default reader wired" }))
29864
+ };
29865
+ }
29866
+ function computeNotionEnabledAgents(config) {
29867
+ return Object.entries(config.agents ?? {}).filter(([, a]) => a && a.notion_workspace !== undefined).map(([name]) => name);
29868
+ }
29869
+ function checkConfigMatrix3(config) {
29870
+ const results = [];
29871
+ const agents = computeNotionEnabledAgents(config);
29872
+ if (agents.length === 0) {
29873
+ return [];
29874
+ }
29875
+ if (!config.notion_workspace) {
29876
+ results.push({
29877
+ name: "notion:top-level-block-present",
29878
+ status: "fail",
29879
+ detail: `${agents.length} agent(s) have notion_workspace: set but the top-level notion_workspace: block is missing`,
29880
+ fix: "Add `notion_workspace:` to switchroom.yaml with at least `databases:` populated. Run `switchroom notion list-dbs` to print a template after putting the integration token in the vault."
29881
+ });
29882
+ return results;
29883
+ }
29884
+ results.push({
29885
+ name: "notion:top-level-block-present",
29886
+ status: "ok"
29887
+ });
29888
+ const known = new Set(Object.keys(config.notion_workspace.databases ?? {}));
29889
+ let any = false;
29890
+ for (const name of agents) {
29891
+ const filter = config.agents?.[name]?.notion_workspace?.databases;
29892
+ if (!filter)
29893
+ continue;
29894
+ any = true;
29895
+ const missing = filter.filter((n) => !known.has(n));
29896
+ if (missing.length > 0) {
29897
+ results.push({
29898
+ name: `notion:db-references-resolvable:${name}`,
29899
+ status: "fail",
29900
+ detail: `agent '${name}' references unknown databases: ${missing.join(", ")}`,
29901
+ fix: `Add the database(s) to notion_workspace.databases in switchroom.yaml, or remove the references from agents.${name}.notion_workspace.databases. Run \`switchroom notion list-dbs\` to see UUIDs.`
29902
+ });
29903
+ } else {
29904
+ results.push({
29905
+ name: `notion:db-references-resolvable:${name}`,
29906
+ status: "ok",
29907
+ detail: `${filter.length} db(s) all resolved`
29908
+ });
29909
+ }
29910
+ }
29911
+ if (!any) {
29912
+ results.push({
29913
+ name: "notion:db-references-resolvable",
29914
+ status: "ok",
29915
+ detail: "no per-agent databases filter set; all admin-shaped"
29916
+ });
29917
+ }
29918
+ return results;
29919
+ }
29920
+ async function checkVaultAclAligned(config, notionAgents, d) {
29921
+ if (notionAgents.length === 0)
29922
+ return [];
29923
+ const key = config.notion_workspace?.vault_key ?? "notion/integration-token";
29924
+ const acl = await d.vaultAclReader(key);
29925
+ if (acl.kind === "unreachable") {
29926
+ return [
29927
+ {
29928
+ name: "notion:vault-acl-aligned",
29929
+ status: "warn",
29930
+ detail: `vault-broker unreachable: ${acl.msg}`,
29931
+ fix: "Ensure the vault-broker is running and the operator socket is reachable, then re-run doctor."
29932
+ }
29933
+ ];
29934
+ }
29935
+ if (acl.kind === "not_found") {
29936
+ return [
29937
+ {
29938
+ name: "notion:integration-token-present",
29939
+ status: "fail",
29940
+ detail: `vault key '${key}' is missing but ${notionAgents.length} agent(s) want Notion`,
29941
+ fix: `Create the Notion integration in Notion settings, then \`switchroom vault set ${key} --allow ${notionAgents.join(",")}\` on the host.`
29942
+ }
29943
+ ];
29944
+ }
29945
+ const allowedSet = new Set(acl.allow);
29946
+ const results = [];
29947
+ results.push({
29948
+ name: "notion:integration-token-present",
29949
+ status: "ok",
29950
+ detail: `vault key '${key}' exists; ${acl.allow.length} agent(s) on ACL`
29951
+ });
29952
+ for (const name of notionAgents) {
29953
+ if (!allowedSet.has(name)) {
29954
+ const updated = [...acl.allow, name].join(",");
29955
+ results.push({
29956
+ name: `notion:vault-acl-aligned:${name}`,
29957
+ status: "fail",
29958
+ detail: `agent '${name}' has notion_workspace: set but is NOT in the vault ACL for ${key} \u2014 launcher will 503 at runtime`,
29959
+ fix: `Re-run \`switchroom vault set ${key} --allow ${updated}\` on the host (vault set overwrites the scope, so re-state the full list including '${name}').`
29960
+ });
29961
+ } else {
29962
+ results.push({
29963
+ name: `notion:vault-acl-aligned:${name}`,
29964
+ status: "ok"
29965
+ });
29966
+ }
29967
+ }
29968
+ for (const a of acl.allow) {
29969
+ if (!notionAgents.includes(a)) {
29970
+ const trimmed = acl.allow.filter((x) => x !== a).join(",");
29971
+ results.push({
29972
+ name: `notion:vault-acl-aligned:${a}`,
29973
+ status: "warn",
29974
+ detail: `agent '${a}' is on the vault ACL for ${key} but has no notion_workspace: block \u2014 never reads the token`,
29975
+ fix: trimmed.length > 0 ? `Re-run \`switchroom vault set ${key} --allow ${trimmed}\` to drop '${a}' from the allowlist, or add a notion_workspace: block to agent '${a}' in switchroom.yaml.` : `Re-run \`switchroom vault set ${key} --allow ''\` to clear the allowlist (no other agent uses this key), or add a notion_workspace: block to agent '${a}' in switchroom.yaml.`
29976
+ });
29977
+ }
29978
+ }
29979
+ return results;
29980
+ }
29981
+ function checkLauncherHeartbeat2(notionAgents, d) {
29982
+ if (notionAgents.length === 0)
29983
+ return [];
29984
+ const results = [];
29985
+ for (const name of notionAgents) {
29986
+ const heartbeatPath = join46(d.agentsDir, name, "notion-launcher.heartbeat.json");
29987
+ if (!d.existsSync(heartbeatPath)) {
29988
+ results.push({
29989
+ name: `notion:launcher-heartbeat:${name}`,
29990
+ status: "warn",
29991
+ detail: `heartbeat file missing at ${heartbeatPath}`,
29992
+ fix: `Verify the launcher started \u2014 \`switchroom agent restart ${name} --wait\` and check the agent's logs for "notion-mcp-launcher" stderr.`
29993
+ });
29994
+ continue;
29995
+ }
29996
+ let mtimeMs;
29997
+ try {
29998
+ mtimeMs = d.statSync(heartbeatPath).mtimeMs;
29999
+ } catch (err) {
30000
+ results.push({
30001
+ name: `notion:launcher-heartbeat:${name}`,
30002
+ status: "warn",
30003
+ detail: `cannot stat heartbeat: ${err.message}`
30004
+ });
30005
+ continue;
30006
+ }
30007
+ const age = d.now() - mtimeMs;
30008
+ if (age > HEARTBEAT_STALE_MS) {
30009
+ results.push({
30010
+ name: `notion:launcher-heartbeat:${name}`,
30011
+ status: "warn",
30012
+ detail: `heartbeat is ${Math.round(age / 1000)}s old (stale: >${HEARTBEAT_STALE_MS / 1000}s)`,
30013
+ fix: `Restart the agent: \`switchroom agent restart ${name} --wait\`.`
30014
+ });
30015
+ } else {
30016
+ results.push({
30017
+ name: `notion:launcher-heartbeat:${name}`,
30018
+ status: "ok",
30019
+ detail: `heartbeat ${Math.round(age / 1000)}s old`
30020
+ });
30021
+ }
30022
+ }
30023
+ return results;
30024
+ }
30025
+ async function runNotionChecks(config, deps = {}) {
30026
+ const notionAgents = computeNotionEnabledAgents(config);
30027
+ if (notionAgents.length === 0 && !config.notion_workspace) {
30028
+ return [];
30029
+ }
30030
+ const d = resolveDeps3(deps);
30031
+ const results = [];
30032
+ results.push(...checkConfigMatrix3(config));
30033
+ if (notionAgents.length > 0 && config.notion_workspace) {
30034
+ results.push(...await checkVaultAclAligned(config, notionAgents, d));
30035
+ results.push(...checkLauncherHeartbeat2(notionAgents, d));
30036
+ }
30037
+ return results;
30038
+ }
30039
+ var HEARTBEAT_STALE_MS;
30040
+ var init_doctor_notion = __esm(() => {
30041
+ HEARTBEAT_STALE_MS = 60 * 1000;
30042
+ });
30043
+
30044
+ // src/cli/doctor-credentials-migration.ts
30045
+ import {
30046
+ existsSync as realExistsSync4,
30047
+ readdirSync as realReaddirSync,
30048
+ statSync as realStatSync2
30049
+ } from "node:fs";
30050
+ import { homedir as homedir27 } from "node:os";
30051
+ import { join as join47 } from "node:path";
29582
30052
  function runCredentialsMigrationChecks(config, deps = {}) {
29583
- const credDir = deps.credentialsDir ?? join45(homedir25(), ".switchroom", "credentials");
29584
- const existsSync49 = deps.existsSync ?? ((p) => realExistsSync2(p));
30053
+ const credDir = deps.credentialsDir ?? join47(homedir27(), ".switchroom", "credentials");
30054
+ const existsSync49 = deps.existsSync ?? ((p) => realExistsSync4(p));
29585
30055
  const readdirSync19 = deps.readdirSync ?? ((p) => realReaddirSync(p));
29586
30056
  const isDirectory = deps.isDirectory ?? ((p) => {
29587
30057
  try {
29588
- return realStatSync(p).isDirectory();
30058
+ return realStatSync2(p).isDirectory();
29589
30059
  } catch {
29590
30060
  return false;
29591
30061
  }
@@ -29608,7 +30078,7 @@ function runCredentialsMigrationChecks(config, deps = {}) {
29608
30078
  const flat = [];
29609
30079
  const perAgentDirs = [];
29610
30080
  for (const e of entries) {
29611
- const full = join45(credDir, e);
30081
+ const full = join47(credDir, e);
29612
30082
  if (isDirectory(full) && agentNames.has(e)) {
29613
30083
  perAgentDirs.push(e);
29614
30084
  } else {
@@ -29731,19 +30201,19 @@ var init_doctor_inlined_secrets = __esm(() => {
29731
30201
 
29732
30202
  // src/cli/doctor-audit-integrity.ts
29733
30203
  import { readFileSync as fsReadFileSync2 } from "node:fs";
29734
- import { homedir as homedir26 } from "node:os";
29735
- import { join as join46 } from "node:path";
30204
+ import { homedir as homedir28 } from "node:os";
30205
+ import { join as join48 } from "node:path";
29736
30206
  function rootWrittenLogs(home2) {
29737
30207
  return [
29738
- { label: "vault-broker", path: join46(home2, ".switchroom", "vault-audit.log") },
30208
+ { label: "vault-broker", path: join48(home2, ".switchroom", "vault-audit.log") },
29739
30209
  {
29740
30210
  label: "hostd",
29741
- path: join46(home2, ".switchroom", "host-control-audit.log")
30211
+ path: join48(home2, ".switchroom", "host-control-audit.log")
29742
30212
  }
29743
30213
  ];
29744
30214
  }
29745
30215
  function runAuditIntegrityChecks(deps = {}) {
29746
- const home2 = deps.homeDir ?? homedir26();
30216
+ const home2 = deps.homeDir ?? homedir28();
29747
30217
  const read = deps.readFileSync ?? ((p) => fsReadFileSync2(p, "utf8"));
29748
30218
  const results = [];
29749
30219
  for (const { label, path: path4 } of rootWrittenLogs(home2)) {
@@ -29881,14 +30351,14 @@ var init_client4 = __esm(() => {
29881
30351
 
29882
30352
  // src/cli/doctor-agent-smoke.ts
29883
30353
  import { existsSync as existsSync49 } from "node:fs";
29884
- import { homedir as homedir27 } from "node:os";
29885
- import { join as join47 } from "node:path";
30354
+ import { homedir as homedir29 } from "node:os";
30355
+ import { join as join49 } from "node:path";
29886
30356
  import { randomUUID as randomUUID4 } from "node:crypto";
29887
30357
  async function runAgentSmokeChecks(config, deps = {}) {
29888
30358
  if (deps.fast)
29889
30359
  return [];
29890
- const home2 = deps.homeDir ?? homedir27();
29891
- const sock = deps.operatorSockPath ?? join47(home2, ".switchroom", "hostd", "operator", "sock");
30360
+ const home2 = deps.homeDir ?? homedir29();
30361
+ const sock = deps.operatorSockPath ?? join49(home2, ".switchroom", "hostd", "operator", "sock");
29892
30362
  if (!deps.hostdRequestImpl && !existsSync49(sock)) {
29893
30363
  return [
29894
30364
  {
@@ -29968,8 +30438,8 @@ var init_doctor_agent_smoke = __esm(() => {
29968
30438
  // src/cli/doctor-vault-broker-durability.ts
29969
30439
  import { execFileSync as execFileSync14 } from "node:child_process";
29970
30440
  import { existsSync as existsSync50, statSync as statSync22 } from "node:fs";
29971
- import { homedir as homedir28 } from "node:os";
29972
- import { join as join48 } from "node:path";
30441
+ import { homedir as homedir30 } from "node:os";
30442
+ import { join as join50 } from "node:path";
29973
30443
  function probeBindMountInode(hostPath, brokerContainerPath, opts) {
29974
30444
  const statHost = opts?.statHost ?? defaultStatHost;
29975
30445
  const statBroker = opts?.statBroker ?? defaultStatBroker;
@@ -30109,19 +30579,19 @@ function defaultBrokerStatusProbe() {
30109
30579
  }
30110
30580
  }
30111
30581
  function runVaultBrokerDurabilityChecks(_config, opts) {
30112
- const home2 = homedir28();
30582
+ const home2 = homedir30();
30113
30583
  const probe2 = opts?.inodeProbe ?? probeBindMountInode;
30114
30584
  return [
30115
30585
  probeBrokerUnlocked(opts?.statusProbe),
30116
30586
  probeAutoUnlockBlob(home2),
30117
30587
  probeMachineIdMount(),
30118
- formatBindMountResult("vault-broker: vault.enc bind mount", join48(home2, ".switchroom", "vault", "vault.enc"), "/state/vault/vault.enc", probe2(join48(home2, ".switchroom", "vault", "vault.enc"), "/state/vault/vault.enc")),
30119
- formatBindMountResult("vault-broker: vault-grants.db bind mount (#1737)", join48(home2, ".switchroom", "vault-grants.db"), "/root/.switchroom/vault-grants.db", probe2(join48(home2, ".switchroom", "vault-grants.db"), "/root/.switchroom/vault-grants.db")),
30120
- formatBindMountResult("vault-broker: vault-audit.log bind mount (#1025)", join48(home2, ".switchroom", "vault-audit.log"), "/root/.switchroom/vault-audit.log", probe2(join48(home2, ".switchroom", "vault-audit.log"), "/root/.switchroom/vault-audit.log"))
30588
+ formatBindMountResult("vault-broker: vault.enc bind mount", join50(home2, ".switchroom", "vault", "vault.enc"), "/state/vault/vault.enc", probe2(join50(home2, ".switchroom", "vault", "vault.enc"), "/state/vault/vault.enc")),
30589
+ formatBindMountResult("vault-broker: vault-grants.db bind mount (#1737)", join50(home2, ".switchroom", "vault-grants.db"), "/root/.switchroom/vault-grants.db", probe2(join50(home2, ".switchroom", "vault-grants.db"), "/root/.switchroom/vault-grants.db")),
30590
+ formatBindMountResult("vault-broker: vault-audit.log bind mount (#1025)", join50(home2, ".switchroom", "vault-audit.log"), "/root/.switchroom/vault-audit.log", probe2(join50(home2, ".switchroom", "vault-audit.log"), "/root/.switchroom/vault-audit.log"))
30121
30591
  ];
30122
30592
  }
30123
30593
  function probeAutoUnlockBlob(home2) {
30124
- const blobPath = join48(home2, ".switchroom", "vault-auto-unlock");
30594
+ const blobPath = join50(home2, ".switchroom", "vault-auto-unlock");
30125
30595
  if (!existsSync50(blobPath)) {
30126
30596
  return {
30127
30597
  name: "vault-broker: auto-unlock blob",
@@ -30232,16 +30702,16 @@ import {
30232
30702
  readdirSync as readdirSync19,
30233
30703
  statSync as statSync23
30234
30704
  } from "node:fs";
30235
- import { dirname as dirname12, join as join49, resolve as resolve30 } from "node:path";
30705
+ import { dirname as dirname12, join as join51, resolve as resolve30 } from "node:path";
30236
30706
  import { createPublicKey, createPrivateKey } from "node:crypto";
30237
30707
  function findInNvm(bin) {
30238
- const nvmRoot = join49(process.env.HOME ?? "", ".nvm", "versions", "node");
30708
+ const nvmRoot = join51(process.env.HOME ?? "", ".nvm", "versions", "node");
30239
30709
  if (!existsSync51(nvmRoot))
30240
30710
  return null;
30241
30711
  try {
30242
30712
  const versions = readdirSync19(nvmRoot).sort().reverse();
30243
30713
  for (const v of versions) {
30244
- const candidate = join49(nvmRoot, v, "bin", bin);
30714
+ const candidate = join51(nvmRoot, v, "bin", bin);
30245
30715
  try {
30246
30716
  const s = statSync23(candidate);
30247
30717
  if (s.isFile() || s.isSymbolicLink()) {
@@ -30406,7 +30876,7 @@ function findChromium(homeDir = process.env.HOME ?? "", envBrowsersPath = proces
30406
30876
  if (envBrowsersPath && envBrowsersPath.length > 0) {
30407
30877
  cacheLocations.push(envBrowsersPath);
30408
30878
  }
30409
- cacheLocations.push(join49(homeDir, ".cache", "ms-playwright"));
30879
+ cacheLocations.push(join51(homeDir, ".cache", "ms-playwright"));
30410
30880
  for (const cacheDir of cacheLocations) {
30411
30881
  if (!existsSync51(cacheDir))
30412
30882
  continue;
@@ -30414,10 +30884,10 @@ function findChromium(homeDir = process.env.HOME ?? "", envBrowsersPath = proces
30414
30884
  const entries = readdirSync19(cacheDir).filter((e) => e.startsWith("chromium"));
30415
30885
  for (const entry of entries) {
30416
30886
  const candidates2 = [
30417
- join49(cacheDir, entry, "chrome-linux64", "chrome"),
30418
- join49(cacheDir, entry, "chrome-linux", "chrome"),
30419
- join49(cacheDir, entry, "chrome-linux64", "headless_shell"),
30420
- join49(cacheDir, entry, "chrome-linux", "headless_shell")
30887
+ join51(cacheDir, entry, "chrome-linux64", "chrome"),
30888
+ join51(cacheDir, entry, "chrome-linux", "chrome"),
30889
+ join51(cacheDir, entry, "chrome-linux64", "headless_shell"),
30890
+ join51(cacheDir, entry, "chrome-linux", "headless_shell")
30421
30891
  ];
30422
30892
  for (const path4 of candidates2) {
30423
30893
  if (existsSync51(path4))
@@ -30527,7 +30997,7 @@ function checkUserDeclaredMcps(name, agentConfig, config, renderedMcpServers) {
30527
30997
  function checkLegacyState() {
30528
30998
  const results = [];
30529
30999
  const h = process.env.HOME ?? "/root";
30530
- const clerkDir = join49(h, LEGACY_STATE_DIR);
31000
+ const clerkDir = join51(h, LEGACY_STATE_DIR);
30531
31001
  const clerkPresent = existsSync51(clerkDir);
30532
31002
  results.push({
30533
31003
  name: "legacy ~/.clerk state",
@@ -30537,7 +31007,7 @@ function checkLegacyState() {
30537
31007
  fix: "Legacy state detected. Run `mv ~/.clerk ~/.switchroom` and rename " + "any top-level `clerk:` key in switchroom.yaml to `switchroom:`. " + "This back-compat shim is REMOVED in v0.13.0 \u2014 no automatic " + "migration exists."
30538
31008
  } : {}
30539
31009
  });
30540
- const legacySock = join49(h, ".switchroom", "vault-broker.sock");
31010
+ const legacySock = join51(h, ".switchroom", "vault-broker.sock");
30541
31011
  let sockStat = null;
30542
31012
  try {
30543
31013
  sockStat = lstatSync5(legacySock);
@@ -30833,7 +31303,7 @@ async function checkHindsight(config) {
30833
31303
  }
30834
31304
  function checkPendingRetainsQueue(dir) {
30835
31305
  const home2 = process.env.HOME ?? "";
30836
- const pendingDir = dir ?? process.env.HINDSIGHT_PENDING_DIR ?? join49(home2, ".hindsight", "pending-retains");
31306
+ const pendingDir = dir ?? process.env.HINDSIGHT_PENDING_DIR ?? join51(home2, ".hindsight", "pending-retains");
30837
31307
  if (!existsSync51(pendingDir)) {
30838
31308
  return {
30839
31309
  name: "pending-retains queue",
@@ -30964,7 +31434,7 @@ async function checkTelegram(config) {
30964
31434
  const plugin = agentConfig.channels?.telegram?.plugin ?? "switchroom";
30965
31435
  if (plugin !== "switchroom")
30966
31436
  continue;
30967
- const envPath = join49(agentsDir, name, "telegram", ".env");
31437
+ const envPath = join51(agentsDir, name, "telegram", ".env");
30968
31438
  const read = tryReadHostFile(envPath);
30969
31439
  if (read.kind === "eacces") {
30970
31440
  results.push({
@@ -31047,7 +31517,7 @@ function checkStartShStale(agentName, startShPath) {
31047
31517
  }
31048
31518
  function checkLeakedHomeSwitchroom(agentName, agentDir) {
31049
31519
  const label = `${agentName}: $HOME/.switchroom symlink (#910)`;
31050
- const path4 = join49(agentDir, "home", ".switchroom");
31520
+ const path4 = join51(agentDir, "home", ".switchroom");
31051
31521
  let stats;
31052
31522
  try {
31053
31523
  stats = lstatSync5(path4);
@@ -31084,7 +31554,7 @@ function checkLeakedHomeSwitchroom(agentName, agentDir) {
31084
31554
  }
31085
31555
  function checkRepoHygiene(repoRoot) {
31086
31556
  const results = [];
31087
- const exportDir = join49(repoRoot, "clerk-export");
31557
+ const exportDir = join51(repoRoot, "clerk-export");
31088
31558
  if (existsSync51(exportDir)) {
31089
31559
  results.push({
31090
31560
  name: "repo hygiene: clerk-export/ on disk (#1072)",
@@ -31093,7 +31563,7 @@ function checkRepoHygiene(repoRoot) {
31093
31563
  fix: `Run scripts/migrate-clerk-export-to-vault.sh to move the bundle ` + `into the vault, then delete the on-disk copy.`
31094
31564
  });
31095
31565
  }
31096
- const knownTarball = join49(repoRoot, "clerk-export-with-secrets.tar.gz");
31566
+ const knownTarball = join51(repoRoot, "clerk-export-with-secrets.tar.gz");
31097
31567
  if (existsSync51(knownTarball)) {
31098
31568
  results.push({
31099
31569
  name: "repo hygiene: clerk-export-with-secrets.tar.gz on disk (#1072)",
@@ -31111,7 +31581,7 @@ function checkRepoHygiene(repoRoot) {
31111
31581
  results.push({
31112
31582
  name: `repo hygiene: ${name} on disk (#1072)`,
31113
31583
  status: "warn",
31114
- detail: `${join49(repoRoot, name)} matches the *-with-secrets*.tar.gz ` + `pattern. Likely contains real credentials.`,
31584
+ detail: `${join51(repoRoot, name)} matches the *-with-secrets*.tar.gz ` + `pattern. Likely contains real credentials.`,
31115
31585
  fix: `Inspect, migrate any secrets into the vault, then delete the ` + `archive.`
31116
31586
  });
31117
31587
  }
@@ -31134,9 +31604,9 @@ function checkRepoHygiene(repoRoot) {
31134
31604
  }
31135
31605
  function isSwitchroomCheckout(dir) {
31136
31606
  try {
31137
- if (!existsSync51(join49(dir, ".git")))
31607
+ if (!existsSync51(join51(dir, ".git")))
31138
31608
  return false;
31139
- const pkgPath = join49(dir, "package.json");
31609
+ const pkgPath = join51(dir, "package.json");
31140
31610
  if (!existsSync51(pkgPath))
31141
31611
  return false;
31142
31612
  const pkg = JSON.parse(readFileSync46(pkgPath, "utf-8"));
@@ -31173,7 +31643,7 @@ function checkAgents(config, configPath) {
31173
31643
  fix: `Rotate the bot token (e.g. via \`switchroom vault\`), then run ` + `\`switchroom agent unquarantine ${name}\` and \`switchroom agent restart ${name}\``
31174
31644
  });
31175
31645
  }
31176
- results.push(checkStartShStale(name, join49(agentDir, "start.sh")));
31646
+ results.push(checkStartShStale(name, join51(agentDir, "start.sh")));
31177
31647
  results.push(checkLeakedHomeSwitchroom(name, agentDir));
31178
31648
  const status = statuses[name];
31179
31649
  const active = status?.active ?? "unknown";
@@ -31250,7 +31720,7 @@ function checkAgents(config, configPath) {
31250
31720
  }
31251
31721
  }
31252
31722
  if (agentConfig.channels?.telegram?.plugin === "switchroom") {
31253
- const mcpJsonPath = join49(agentDir, ".mcp.json");
31723
+ const mcpJsonPath = join51(agentDir, ".mcp.json");
31254
31724
  if (!existsSync51(mcpJsonPath)) {
31255
31725
  results.push({
31256
31726
  name: `${name}: .mcp.json`,
@@ -31552,7 +32022,7 @@ async function checkMffAuthFlow(envPath = mffEnvPath(), timeoutMs = 8000) {
31552
32022
  };
31553
32023
  }
31554
32024
  const credDir = dirname12(envPath);
31555
- const authScript = join49(credDir, "claude-auth.py");
32025
+ const authScript = join51(credDir, "claude-auth.py");
31556
32026
  if (!existsSync51(authScript)) {
31557
32027
  return {
31558
32028
  name: "mff: auth flow",
@@ -31855,6 +32325,32 @@ function registerDoctorCommand(program3) {
31855
32325
  ...await runDriveBrokerReachabilityChecks(config)
31856
32326
  ]
31857
32327
  },
32328
+ {
32329
+ title: "Microsoft 365 (RFC #1873)",
32330
+ results: runMicrosoftChecks(config)
32331
+ },
32332
+ {
32333
+ title: "Notion (RFC notion-integration)",
32334
+ results: await runNotionChecks(config, {
32335
+ vaultAclReader: async (key) => {
32336
+ try {
32337
+ const { getViaBrokerStructured: getViaBrokerStructured2 } = await Promise.resolve().then(() => (init_client(), exports_client));
32338
+ const result = await getViaBrokerStructured2(key);
32339
+ if (result.kind === "ok") {
32340
+ return {
32341
+ kind: "ok",
32342
+ allow: result.entry.scope?.allow ?? []
32343
+ };
32344
+ }
32345
+ if (result.kind === "not_found")
32346
+ return { kind: "not_found" };
32347
+ return { kind: "unreachable", msg: result.msg };
32348
+ } catch (err) {
32349
+ return { kind: "unreachable", msg: err.message };
32350
+ }
32351
+ }
32352
+ })
32353
+ },
31858
32354
  { title: "MFF Skill", results: await checkMff(passphrase, vaultPath, config) }
31859
32355
  ];
31860
32356
  const cwd = process.cwd();
@@ -31914,6 +32410,8 @@ var init_doctor = __esm(() => {
31914
32410
  init_doctor_auth_broker();
31915
32411
  init_doctor_hostd();
31916
32412
  init_doctor_drive();
32413
+ init_doctor_microsoft();
32414
+ init_doctor_notion();
31917
32415
  init_doctor_credentials_migration();
31918
32416
  init_doctor_secret_access();
31919
32417
  init_doctor_inlined_secrets();
@@ -48021,7 +48519,7 @@ __export(exports_server2, {
48021
48519
  TOOLS: () => TOOLS2
48022
48520
  });
48023
48521
  import { randomBytes as randomBytes15 } from "node:crypto";
48024
- import { existsSync as existsSync78, readFileSync as readFileSync65 } from "node:fs";
48522
+ import { existsSync as existsSync79, readFileSync as readFileSync65 } from "node:fs";
48025
48523
  function selfSocketPath() {
48026
48524
  return `/run/switchroom/hostd/${SELF_AGENT}/sock`;
48027
48525
  }
@@ -48036,7 +48534,7 @@ async function dispatchTool2(name, args) {
48036
48534
  return errorText2("hostd MCP: SWITCHROOM_AGENT_NAME env var is not set \u2014 cannot " + "determine which per-agent socket to talk to.");
48037
48535
  }
48038
48536
  const sockPath = selfSocketPath();
48039
- if (!existsSync78(sockPath)) {
48537
+ if (!existsSync79(sockPath)) {
48040
48538
  return errorText2(`hostd MCP: socket not bound at ${sockPath}. The host-control ` + `daemon is either not installed (run \`switchroom hostd install\`) ` + `or this agent isn't admin-flagged in switchroom.yaml. RFC C ` + `bind-mounts the per-agent socket only when host_control.enabled ` + `is true AND the agent has admin: true.`);
48041
48539
  }
48042
48540
  let req;
@@ -48188,13 +48686,13 @@ function resolveAuditLogPath() {
48188
48686
  if (process.env.HOSTD_AUDIT_LOG_PATH)
48189
48687
  return process.env.HOSTD_AUDIT_LOG_PATH;
48190
48688
  const bindMounted = "/host-home/.switchroom/host-control-audit.log";
48191
- if (existsSync78(bindMounted))
48689
+ if (existsSync79(bindMounted))
48192
48690
  return bindMounted;
48193
48691
  return defaultAuditLogPath2();
48194
48692
  }
48195
48693
  function getLastUpdateApplyStatus() {
48196
48694
  const path8 = resolveAuditLogPath();
48197
- if (!existsSync78(path8)) {
48695
+ if (!existsSync79(path8)) {
48198
48696
  return errorText2(`get_status: audit log not found at ${path8}. No update_apply has run yet?`);
48199
48697
  }
48200
48698
  let raw;
@@ -48432,8 +48930,8 @@ var {
48432
48930
  } = import__.default;
48433
48931
 
48434
48932
  // src/build-info.ts
48435
- var VERSION = "0.13.53";
48436
- var COMMIT_SHA = "08726c33";
48933
+ var VERSION = "0.13.55";
48934
+ var COMMIT_SHA = "98cf8e68";
48437
48935
 
48438
48936
  // src/cli/agent.ts
48439
48937
  init_source();
@@ -48680,6 +49178,22 @@ function renderProfileClaudeTemplate(profileName, profilesRoot = PROFILES_ROOT)
48680
49178
 
48681
49179
  // src/agents/scaffold.ts
48682
49180
  init_scaffold_integration();
49181
+
49182
+ // src/config/microsoft-workspace-acl.ts
49183
+ function shouldEmitMs365Mcp(agentName, agentMicrosoftAccount, microsoftAccounts) {
49184
+ if (!agentMicrosoftAccount)
49185
+ return false;
49186
+ const account = agentMicrosoftAccount.trim().toLowerCase();
49187
+ if (account.length === 0)
49188
+ return false;
49189
+ const acctEntry = microsoftAccounts?.[account];
49190
+ if (!acctEntry)
49191
+ return false;
49192
+ const enabledFor = acctEntry.enabled_for ?? [];
49193
+ return enabledFor.includes(agentName);
49194
+ }
49195
+
49196
+ // src/agents/scaffold.ts
48683
49197
  init_reconcile_default_skills();
48684
49198
 
48685
49199
  // src/agents/sub-agent-telegram-prompt.ts
@@ -49799,7 +50313,9 @@ function buildWorkspaceContext(args) {
49799
50313
  hindsightApiBaseUrl,
49800
50314
  hindsightRecallMaxMemories,
49801
50315
  hindsightRecallCacheTtlSecs,
49802
- hindsightRecallMinOverlap
50316
+ hindsightRecallMinOverlap,
50317
+ hindsightTopicAliasesJson,
50318
+ hindsightTopicFilterMode
49803
50319
  } = args;
49804
50320
  return {
49805
50321
  name,
@@ -49833,6 +50349,8 @@ function buildWorkspaceContext(args) {
49833
50349
  hindsightRecallMaxMemories,
49834
50350
  hindsightRecallCacheTtlSecs,
49835
50351
  hindsightRecallMinOverlap,
50352
+ hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
50353
+ hindsightTopicFilterMode,
49836
50354
  switchroomConfigPathQ: switchroomConfigPath ? shellSingleQuote(resolve10(switchroomConfigPath)) : undefined,
49837
50355
  hostHomeQ: process.env.HOME ? shellSingleQuote(process.env.HOME) : undefined,
49838
50356
  modelQ: shellSingleQuote(agentConfig.model ?? SWITCHROOM_DEFAULT_MAIN_MODEL),
@@ -49917,6 +50435,46 @@ function resolveGdriveMcpEntry(agentName, agentConfig, switchroomConfig) {
49917
50435
  };
49918
50436
  return entry;
49919
50437
  }
50438
+ function resolveMs365McpEntry(agentName, agentConfig, switchroomConfig) {
50439
+ if ((agentConfig.mcp_servers ?? {})["ms-365"] === false)
50440
+ return null;
50441
+ const account = agentConfig.microsoft_workspace?.account;
50442
+ const microsoftAccounts = switchroomConfig?.microsoft_accounts;
50443
+ if (!shouldEmitMs365Mcp(agentName, account, microsoftAccounts))
50444
+ return null;
50445
+ const orgMode = agentConfig.microsoft_workspace?.org_mode ?? switchroomConfig?.microsoft_workspace?.org_mode ?? false;
50446
+ const entry = getMs365McpSettingsEntry(DOCKER_SWITCHROOM_CLI_PATH, orgMode ? { orgMode: true } : {});
50447
+ entry.value.env = {
50448
+ SWITCHROOM_CONFIG: DOCKER_CONFIG_PATH,
50449
+ SWITCHROOM_AGENT_NAME: agentName,
50450
+ SWITCHROOM_CONTAINER: "1",
50451
+ SWITCHROOM_AUTH_BROKER_SOCKET: DOCKER_AUTH_BROKER_SOCKET,
50452
+ HOME: DOCKER_AGENT_HOME
50453
+ };
50454
+ return entry;
50455
+ }
50456
+ function resolveNotionMcpEntry(agentName, agentConfig, switchroomConfig) {
50457
+ if ((agentConfig.mcp_servers ?? {})["notion"] === false)
50458
+ return null;
50459
+ if (!switchroomConfig)
50460
+ return null;
50461
+ if (!shouldEmitNotionMcp(agentName, switchroomConfig))
50462
+ return null;
50463
+ const vaultKey = switchroomConfig.notion_workspace?.vault_key ?? "notion/integration-token";
50464
+ const mcpVersion = switchroomConfig.notion_workspace?.mcp_version;
50465
+ const entry = getNotionMcpSettingsEntry(DOCKER_SWITCHROOM_CLI_PATH, {
50466
+ vaultKey,
50467
+ mcpVersion
50468
+ });
50469
+ entry.value.env = {
50470
+ SWITCHROOM_CONFIG: DOCKER_CONFIG_PATH,
50471
+ SWITCHROOM_AGENT_NAME: agentName,
50472
+ SWITCHROOM_CONTAINER: "1",
50473
+ SWITCHROOM_VAULT_BROKER_SOCK: DOCKER_VAULT_BROKER_SOCKET,
50474
+ HOME: DOCKER_AGENT_HOME
50475
+ };
50476
+ return entry;
50477
+ }
49920
50478
  function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchroomConfig, userIdOverride, switchroomConfigPath) {
49921
50479
  const agentConfig = resolveAgentConfig(switchroomConfig?.defaults, switchroomConfig?.profiles, agentConfigRaw);
49922
50480
  const agentDir = resolve10(agentsDir, name);
@@ -49958,6 +50516,9 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
49958
50516
  const hindsightRecallMaxMemories = agentConfig.memory?.recall?.max_memories;
49959
50517
  const hindsightRecallCacheTtlSecs = agentConfig.memory?.recall?.cache_ttl_secs;
49960
50518
  const hindsightRecallMinOverlap = agentConfig.memory?.recall?.min_overlap;
50519
+ const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
50520
+ const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
50521
+ const hindsightTopicFilterMode = agentConfig.memory?.recall?.topic_filter_mode;
49961
50522
  const context = buildWorkspaceContext({
49962
50523
  name,
49963
50524
  agentDir,
@@ -49976,7 +50537,9 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
49976
50537
  hindsightApiBaseUrl,
49977
50538
  hindsightRecallMaxMemories,
49978
50539
  hindsightRecallCacheTtlSecs,
49979
- hindsightRecallMinOverlap
50540
+ hindsightRecallMinOverlap,
50541
+ hindsightTopicAliasesJson,
50542
+ hindsightTopicFilterMode
49980
50543
  });
49981
50544
  const dirs = [
49982
50545
  agentDir,
@@ -50025,6 +50588,14 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
50025
50588
  if (gdrive && !settings.mcpServers[gdrive.key]) {
50026
50589
  settings.mcpServers[gdrive.key] = gdrive.value;
50027
50590
  }
50591
+ const ms365 = resolveMs365McpEntry(name, agentConfig, switchroomConfig);
50592
+ if (ms365 && !settings.mcpServers[ms365.key]) {
50593
+ settings.mcpServers[ms365.key] = ms365.value;
50594
+ }
50595
+ const notion = resolveNotionMcpEntry(name, agentConfig, switchroomConfig);
50596
+ if (notion && !settings.mcpServers[notion.key]) {
50597
+ settings.mcpServers[notion.key] = notion.value;
50598
+ }
50028
50599
  }
50029
50600
  installHindsightPlugin(name, agentDir, switchroomConfig);
50030
50601
  const hindsightOn = isHindsightEnabled(switchroomConfig) && switchroomConfig.agents[name]?.memory?.auto_recall !== false;
@@ -50053,12 +50624,13 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
50053
50624
  const pluginDir = DOCKER_TELEGRAM_PLUGIN_PATH;
50054
50625
  const switchroomCliPath = "/usr/local/bin/switchroom";
50055
50626
  const resolvedConfigPath = DOCKER_CONFIG_PATH;
50627
+ const telegramStateDir = `${DOCKER_AGENT_HOME}/.switchroom/agents/${name}/telegram`;
50056
50628
  const mcpServers = {
50057
50629
  "switchroom-telegram": {
50058
50630
  command: "bun",
50059
50631
  args: ["run", "--cwd", pluginDir, "--shell=bun", "--silent", "start"],
50060
50632
  env: {
50061
- TELEGRAM_STATE_DIR: join8(agentDir, "telegram"),
50633
+ TELEGRAM_STATE_DIR: telegramStateDir,
50062
50634
  SWITCHROOM_CONFIG: resolvedConfigPath,
50063
50635
  SWITCHROOM_CLI_PATH: switchroomCliPath
50064
50636
  }
@@ -50093,6 +50665,14 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
50093
50665
  if (gdrive) {
50094
50666
  mcpServers[gdrive.key] = gdrive.value;
50095
50667
  }
50668
+ const ms365 = resolveMs365McpEntry(name, agentConfig, switchroomConfig);
50669
+ if (ms365) {
50670
+ mcpServers[ms365.key] = ms365.value;
50671
+ }
50672
+ const notion = resolveNotionMcpEntry(name, agentConfig, switchroomConfig);
50673
+ if (notion) {
50674
+ mcpServers[notion.key] = notion.value;
50675
+ }
50096
50676
  }
50097
50677
  if (agentConfig.mcp_servers) {
50098
50678
  const filtered = filterMcpServers(agentConfig.mcp_servers);
@@ -50475,6 +51055,26 @@ function buildSettingsHooksBlock(p) {
50475
51055
  }
50476
51056
  ]
50477
51057
  },
51058
+ {
51059
+ matcher: "^mcp__ms-365__",
51060
+ hooks: [
51061
+ {
51062
+ type: "command",
51063
+ command: wrap("hook:ms-365-write-pretool", `node "${join8(DOCKER_BUNDLED_HOOKS_PATH, "ms-365-write-pretool.mjs")}"`),
51064
+ timeout: 5 * 60 + 30
51065
+ }
51066
+ ]
51067
+ },
51068
+ {
51069
+ matcher: "^mcp__notion__",
51070
+ hooks: [
51071
+ {
51072
+ type: "command",
51073
+ command: wrap("hook:notion-write-pretool", `node "${join8(DOCKER_BUNDLED_HOOKS_PATH, "notion-write-pretool.mjs")}"`),
51074
+ timeout: 30
51075
+ }
51076
+ ]
51077
+ },
50478
51078
  {
50479
51079
  matcher: "^(Write|Edit|MultiEdit)$",
50480
51080
  hooks: [
@@ -50715,6 +51315,9 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
50715
51315
  const hindsightRecallMaxMemories = agentConfig.memory?.recall?.max_memories;
50716
51316
  const hindsightRecallCacheTtlSecs = agentConfig.memory?.recall?.cache_ttl_secs;
50717
51317
  const hindsightRecallMinOverlap = agentConfig.memory?.recall?.min_overlap;
51318
+ const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
51319
+ const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
51320
+ const hindsightTopicFilterMode = agentConfig.memory?.recall?.topic_filter_mode;
50718
51321
  const startShPath = join8(agentDir, "start.sh");
50719
51322
  if (!options.skipProfileTemplates) {
50720
51323
  const basePath = getBaseProfilePath();
@@ -50735,6 +51338,8 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
50735
51338
  hindsightRecallMaxMemories,
50736
51339
  hindsightRecallCacheTtlSecs,
50737
51340
  hindsightRecallMinOverlap,
51341
+ hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
51342
+ hindsightTopicFilterMode,
50738
51343
  hostHomeQ: process.env.HOME ? shellSingleQuote(process.env.HOME) : undefined,
50739
51344
  modelQ: shellSingleQuote(agentConfig.model ?? SWITCHROOM_DEFAULT_MAIN_MODEL),
50740
51345
  thinkingEffort: agentConfig.thinking_effort ?? SWITCHROOM_DEFAULT_THINKING_EFFORT,
@@ -50870,6 +51475,18 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
50870
51475
  } else {
50871
51476
  delete mcpServers["gdrive"];
50872
51477
  }
51478
+ const ms365 = resolveMs365McpEntry(name, agentConfig, switchroomConfig);
51479
+ if (ms365) {
51480
+ mcpServers[ms365.key] = ms365.value;
51481
+ } else {
51482
+ delete mcpServers["ms-365"];
51483
+ }
51484
+ const notion = resolveNotionMcpEntry(name, agentConfig, switchroomConfig);
51485
+ if (notion) {
51486
+ mcpServers[notion.key] = notion.value;
51487
+ } else {
51488
+ delete mcpServers["notion"];
51489
+ }
50873
51490
  }
50874
51491
  if (agentConfig.mcp_servers) {
50875
51492
  for (const [key, value] of Object.entries(agentConfig.mcp_servers)) {
@@ -51014,12 +51631,13 @@ ${body}
51014
51631
  const pluginDir = DOCKER_TELEGRAM_PLUGIN_PATH;
51015
51632
  const switchroomCliPath = "/usr/local/bin/switchroom";
51016
51633
  const resolvedConfigPath = DOCKER_CONFIG_PATH;
51634
+ const telegramStateDir = `${DOCKER_AGENT_HOME}/.switchroom/agents/${name}/telegram`;
51017
51635
  const mcpServers = {
51018
51636
  "switchroom-telegram": {
51019
51637
  command: "bun",
51020
51638
  args: ["run", "--cwd", pluginDir, "--shell=bun", "--silent", "start"],
51021
51639
  env: {
51022
- TELEGRAM_STATE_DIR: join8(agentDir, "telegram"),
51640
+ TELEGRAM_STATE_DIR: telegramStateDir,
51023
51641
  SWITCHROOM_CONFIG: resolvedConfigPath,
51024
51642
  SWITCHROOM_CLI_PATH: switchroomCliPath
51025
51643
  }
@@ -51054,6 +51672,14 @@ ${body}
51054
51672
  if (gdrive) {
51055
51673
  mcpServers[gdrive.key] = gdrive.value;
51056
51674
  }
51675
+ const ms365 = resolveMs365McpEntry(name, agentConfig, switchroomConfig);
51676
+ if (ms365) {
51677
+ mcpServers[ms365.key] = ms365.value;
51678
+ }
51679
+ const notion = resolveNotionMcpEntry(name, agentConfig, switchroomConfig);
51680
+ if (notion) {
51681
+ mcpServers[notion.key] = notion.value;
51682
+ }
51057
51683
  }
51058
51684
  if (agentConfig.mcp_servers) {
51059
51685
  const filtered = filterMcpServers(agentConfig.mcp_servers);
@@ -51094,7 +51720,9 @@ ${body}
51094
51720
  hindsightApiBaseUrl,
51095
51721
  hindsightRecallMaxMemories,
51096
51722
  hindsightRecallCacheTtlSecs,
51097
- hindsightRecallMinOverlap
51723
+ hindsightRecallMinOverlap,
51724
+ hindsightTopicAliasesJson,
51725
+ hindsightTopicFilterMode
51098
51726
  });
51099
51727
  mkdirSync7(reconcileWorkspaceDir, { recursive: true });
51100
51728
  migrateLegacyAgentsMdIfPresent(reconcileWorkspaceDir, changes);
@@ -71545,15 +72173,15 @@ init_loader();
71545
72173
  init_lifecycle();
71546
72174
  import { cpSync as cpSync2, existsSync as existsSync52, mkdirSync as mkdirSync28, readFileSync as readFileSync47, realpathSync as realpathSync5, rmSync as rmSync12, statSync as statSync24 } from "node:fs";
71547
72175
  import { spawnSync as spawnSync8 } from "node:child_process";
71548
- import { join as join50, dirname as dirname13, resolve as resolve31 } from "node:path";
71549
- import { homedir as homedir29 } from "node:os";
71550
- var DEFAULT_COMPOSE_PATH = join50(homedir29(), ".switchroom", "compose", "docker-compose.yml");
72176
+ import { join as join52, dirname as dirname13, resolve as resolve31 } from "node:path";
72177
+ import { homedir as homedir31 } from "node:os";
72178
+ var DEFAULT_COMPOSE_PATH = join52(homedir31(), ".switchroom", "compose", "docker-compose.yml");
71551
72179
  function runningFromSwitchroomCheckout(scriptPath) {
71552
72180
  let dir = dirname13(scriptPath);
71553
72181
  for (let i = 0;i < 12; i++) {
71554
- if (existsSync52(join50(dir, ".git"))) {
72182
+ if (existsSync52(join52(dir, ".git"))) {
71555
72183
  try {
71556
- const pkg = JSON.parse(readFileSync47(join50(dir, "package.json"), "utf-8"));
72184
+ const pkg = JSON.parse(readFileSync47(join52(dir, "package.json"), "utf-8"));
71557
72185
  if (pkg.name === "switchroom")
71558
72186
  return true;
71559
72187
  } catch {}
@@ -71684,7 +72312,7 @@ function planUpdate(opts) {
71684
72312
  return;
71685
72313
  }
71686
72314
  const source = resolve31(import.meta.dirname, "../../skills");
71687
- const dest = join50(homedir29(), ".switchroom", "skills", "_bundled");
72315
+ const dest = join52(homedir31(), ".switchroom", "skills", "_bundled");
71688
72316
  if (!existsSync52(source)) {
71689
72317
  process.stderr.write(`switchroom update: sync-bundled-skills \u2014 CLI bundle has no adjacent skills/ at ${source}; skipping.
71690
72318
  `);
@@ -71798,7 +72426,7 @@ function defaultStatusProbe(composePath) {
71798
72426
  } catch {}
71799
72427
  let dir = dirname13(scriptPath);
71800
72428
  for (let i = 0;i < 8; i++) {
71801
- const pkgPath = join50(dir, "package.json");
72429
+ const pkgPath = join52(dir, "package.json");
71802
72430
  if (existsSync52(pkgPath)) {
71803
72431
  try {
71804
72432
  const pkg = JSON.parse(readFileSync47(pkgPath, "utf-8"));
@@ -72018,7 +72646,7 @@ init_helpers();
72018
72646
  init_lifecycle();
72019
72647
  import { execSync as execSync4 } from "node:child_process";
72020
72648
  import { existsSync as existsSync53, readFileSync as readFileSync48 } from "node:fs";
72021
- import { dirname as dirname14, join as join51 } from "node:path";
72649
+ import { dirname as dirname14, join as join53 } from "node:path";
72022
72650
  function getClaudeCodeVersion() {
72023
72651
  try {
72024
72652
  const out = execSync4("claude --version 2>/dev/null", {
@@ -72068,11 +72696,11 @@ function formatUptime3(timestamp) {
72068
72696
  function locateSwitchroomInstallDir() {
72069
72697
  let dir = import.meta.dirname;
72070
72698
  for (let i = 0;i < 10 && dir && dir !== "/"; i++) {
72071
- const pkgPath = join51(dir, "package.json");
72699
+ const pkgPath = join53(dir, "package.json");
72072
72700
  if (existsSync53(pkgPath)) {
72073
72701
  try {
72074
72702
  const pkg = JSON.parse(readFileSync48(pkgPath, "utf-8"));
72075
- if (pkg.name === "switchroom" && existsSync53(join51(dir, ".git"))) {
72703
+ if (pkg.name === "switchroom" && existsSync53(join53(dir, ".git"))) {
72076
72704
  return dir;
72077
72705
  }
72078
72706
  } catch {}
@@ -72309,7 +72937,7 @@ import {
72309
72937
  writeFileSync as writeFileSync26,
72310
72938
  writeSync as writeSync7
72311
72939
  } from "node:fs";
72312
- import { join as join52 } from "node:path";
72940
+ import { join as join54 } from "node:path";
72313
72941
  import { randomBytes as randomBytes12 } from "node:crypto";
72314
72942
  import { execSync as execSync5 } from "node:child_process";
72315
72943
 
@@ -72629,7 +73257,7 @@ function redactedMarker(ruleId) {
72629
73257
  var ISSUES_FILE = "issues.jsonl";
72630
73258
  var ISSUES_LOCK = "issues.lock";
72631
73259
  function readAll(stateDir) {
72632
- const path4 = join52(stateDir, ISSUES_FILE);
73260
+ const path4 = join54(stateDir, ISSUES_FILE);
72633
73261
  if (!existsSync54(path4))
72634
73262
  return [];
72635
73263
  let raw;
@@ -72707,7 +73335,7 @@ function record(stateDir, input, nowFn = Date.now) {
72707
73335
  });
72708
73336
  }
72709
73337
  function resolve34(stateDir, fingerprint, nowFn = Date.now) {
72710
- if (!existsSync54(join52(stateDir, ISSUES_FILE)))
73338
+ if (!existsSync54(join54(stateDir, ISSUES_FILE)))
72711
73339
  return 0;
72712
73340
  return withLock(stateDir, () => {
72713
73341
  const all = readAll(stateDir);
@@ -72725,7 +73353,7 @@ function resolve34(stateDir, fingerprint, nowFn = Date.now) {
72725
73353
  });
72726
73354
  }
72727
73355
  function resolveAllBySource(stateDir, source, nowFn = Date.now) {
72728
- if (!existsSync54(join52(stateDir, ISSUES_FILE)))
73356
+ if (!existsSync54(join54(stateDir, ISSUES_FILE)))
72729
73357
  return 0;
72730
73358
  return withLock(stateDir, () => {
72731
73359
  const all = readAll(stateDir);
@@ -72743,7 +73371,7 @@ function resolveAllBySource(stateDir, source, nowFn = Date.now) {
72743
73371
  });
72744
73372
  }
72745
73373
  function prune(stateDir, opts = {}) {
72746
- if (!existsSync54(join52(stateDir, ISSUES_FILE)))
73374
+ if (!existsSync54(join54(stateDir, ISSUES_FILE)))
72747
73375
  return 0;
72748
73376
  return withLock(stateDir, () => {
72749
73377
  const all = readAll(stateDir);
@@ -72776,7 +73404,7 @@ function ensureDir(stateDir) {
72776
73404
  mkdirSync29(stateDir, { recursive: true });
72777
73405
  }
72778
73406
  function writeAll(stateDir, events) {
72779
- const path4 = join52(stateDir, ISSUES_FILE);
73407
+ const path4 = join54(stateDir, ISSUES_FILE);
72780
73408
  sweepOrphanTmpFiles(stateDir);
72781
73409
  const tmp = `${path4}.tmp-${process.pid}-${randomBytes12(4).toString("hex")}`;
72782
73410
  const body = events.length === 0 ? "" : events.map((e) => JSON.stringify(e)).join(`
@@ -72798,7 +73426,7 @@ function sweepOrphanTmpFiles(stateDir) {
72798
73426
  for (const entry of entries) {
72799
73427
  if (!entry.startsWith(TMP_PREFIX))
72800
73428
  continue;
72801
- const tmpPath = join52(stateDir, entry);
73429
+ const tmpPath = join54(stateDir, entry);
72802
73430
  try {
72803
73431
  const stat = statSync25(tmpPath);
72804
73432
  if (stat.mtimeMs < cutoff) {
@@ -72810,7 +73438,7 @@ function sweepOrphanTmpFiles(stateDir) {
72810
73438
  var LOCK_RETRY_MS = 25;
72811
73439
  var LOCK_TIMEOUT_MS = 1e4;
72812
73440
  function withLock(stateDir, fn) {
72813
- const lockPath = join52(stateDir, ISSUES_LOCK);
73441
+ const lockPath = join54(stateDir, ISSUES_LOCK);
72814
73442
  const startedAt = Date.now();
72815
73443
  let fd = null;
72816
73444
  while (fd === null) {
@@ -73094,8 +73722,8 @@ function relTime(deltaMs) {
73094
73722
  // src/cli/deps.ts
73095
73723
  init_source();
73096
73724
  import { existsSync as existsSync57 } from "node:fs";
73097
- import { homedir as homedir32 } from "node:os";
73098
- import { join as join55, resolve as resolve35 } from "node:path";
73725
+ import { homedir as homedir34 } from "node:os";
73726
+ import { join as join57, resolve as resolve35 } from "node:path";
73099
73727
 
73100
73728
  // src/deps/python.ts
73101
73729
  import { createHash as createHash11 } from "node:crypto";
@@ -73106,8 +73734,8 @@ import {
73106
73734
  rmSync as rmSync13,
73107
73735
  writeFileSync as writeFileSync27
73108
73736
  } from "node:fs";
73109
- import { dirname as dirname15, join as join53 } from "node:path";
73110
- import { homedir as homedir30 } from "node:os";
73737
+ import { dirname as dirname15, join as join55 } from "node:path";
73738
+ import { homedir as homedir32 } from "node:os";
73111
73739
  import { execFileSync as execFileSync15 } from "node:child_process";
73112
73740
 
73113
73741
  class PythonEnvError extends Error {
@@ -73119,7 +73747,7 @@ class PythonEnvError extends Error {
73119
73747
  }
73120
73748
  }
73121
73749
  function defaultPythonCacheRoot() {
73122
- return join53(homedir30(), ".switchroom", "deps", "python");
73750
+ return join55(homedir32(), ".switchroom", "deps", "python");
73123
73751
  }
73124
73752
  function hashFile(path4) {
73125
73753
  return createHash11("sha256").update(readFileSync50(path4)).digest("hex");
@@ -73131,11 +73759,11 @@ function ensurePythonEnv(opts) {
73131
73759
  if (!existsSync55(requirementsPath)) {
73132
73760
  throw new PythonEnvError(`requirements file not found: ${requirementsPath}`);
73133
73761
  }
73134
- const venvDir = join53(cacheRoot, skillName);
73135
- const stampPath = join53(venvDir, ".requirements.sha256");
73136
- const binDir = join53(venvDir, "bin");
73137
- const pythonBin = join53(binDir, "python");
73138
- const pipBin = join53(binDir, "pip");
73762
+ const venvDir = join55(cacheRoot, skillName);
73763
+ const stampPath = join55(venvDir, ".requirements.sha256");
73764
+ const binDir = join55(venvDir, "bin");
73765
+ const pythonBin = join55(binDir, "python");
73766
+ const pipBin = join55(binDir, "pip");
73139
73767
  const targetHash = hashFile(requirementsPath);
73140
73768
  if (!force && existsSync55(stampPath) && existsSync55(pythonBin)) {
73141
73769
  const existingHash = readFileSync50(stampPath, "utf8").trim();
@@ -73194,8 +73822,8 @@ import {
73194
73822
  rmSync as rmSync14,
73195
73823
  writeFileSync as writeFileSync28
73196
73824
  } from "node:fs";
73197
- import { dirname as dirname16, join as join54 } from "node:path";
73198
- import { homedir as homedir31 } from "node:os";
73825
+ import { dirname as dirname16, join as join56 } from "node:path";
73826
+ import { homedir as homedir33 } from "node:os";
73199
73827
  import { execFileSync as execFileSync16 } from "node:child_process";
73200
73828
 
73201
73829
  class NodeEnvError extends Error {
@@ -73218,7 +73846,7 @@ var LOCKFILES_FOR = {
73218
73846
  npm: ["package-lock.json"]
73219
73847
  };
73220
73848
  function defaultNodeCacheRoot() {
73221
- return join54(homedir31(), ".switchroom", "deps", "node");
73849
+ return join56(homedir33(), ".switchroom", "deps", "node");
73222
73850
  }
73223
73851
  function hashDepInputs(packageJsonPath) {
73224
73852
  const sourceDir = dirname16(packageJsonPath);
@@ -73227,7 +73855,7 @@ function hashDepInputs(packageJsonPath) {
73227
73855
  `);
73228
73856
  hasher.update(readFileSync51(packageJsonPath));
73229
73857
  for (const lockName of ALL_LOCKFILES) {
73230
- const lockPath = join54(sourceDir, lockName);
73858
+ const lockPath = join56(sourceDir, lockName);
73231
73859
  if (existsSync56(lockPath)) {
73232
73860
  hasher.update(`
73233
73861
  `);
@@ -73247,10 +73875,10 @@ function ensureNodeEnv(opts) {
73247
73875
  throw new NodeEnvError(`package.json not found: ${packageJsonPath}`);
73248
73876
  }
73249
73877
  const sourceDir = dirname16(packageJsonPath);
73250
- const envDir = join54(cacheRoot, skillName);
73251
- const stampPath = join54(envDir, ".package.sha256");
73252
- const nodeModulesDir = join54(envDir, "node_modules");
73253
- const binDir = join54(nodeModulesDir, ".bin");
73878
+ const envDir = join56(cacheRoot, skillName);
73879
+ const stampPath = join56(envDir, ".package.sha256");
73880
+ const nodeModulesDir = join56(envDir, "node_modules");
73881
+ const binDir = join56(nodeModulesDir, ".bin");
73254
73882
  const targetHash = hashDepInputs(packageJsonPath);
73255
73883
  if (!force && existsSync56(stampPath) && existsSync56(nodeModulesDir)) {
73256
73884
  const existingHash = readFileSync51(stampPath, "utf8").trim();
@@ -73268,12 +73896,12 @@ function ensureNodeEnv(opts) {
73268
73896
  rmSync14(envDir, { recursive: true, force: true });
73269
73897
  }
73270
73898
  mkdirSync31(envDir, { recursive: true });
73271
- copyFileSync9(packageJsonPath, join54(envDir, "package.json"));
73899
+ copyFileSync9(packageJsonPath, join56(envDir, "package.json"));
73272
73900
  let copiedLockfile = false;
73273
73901
  for (const lockName of LOCKFILES_FOR[installer]) {
73274
- const lockPath = join54(sourceDir, lockName);
73902
+ const lockPath = join56(sourceDir, lockName);
73275
73903
  if (existsSync56(lockPath)) {
73276
- copyFileSync9(lockPath, join54(envDir, lockName));
73904
+ copyFileSync9(lockPath, join56(envDir, lockName));
73277
73905
  copiedLockfile = true;
73278
73906
  }
73279
73907
  }
@@ -73302,7 +73930,7 @@ function ensureNodeEnv(opts) {
73302
73930
 
73303
73931
  // src/cli/deps.ts
73304
73932
  function builtinSkillsRoot() {
73305
- return resolve35(homedir32(), ".switchroom/skills/_bundled");
73933
+ return resolve35(homedir34(), ".switchroom/skills/_bundled");
73306
73934
  }
73307
73935
  function registerDepsCommand(program3) {
73308
73936
  const deps = program3.command("deps").description("Manage cached per-skill dependency environments");
@@ -73312,13 +73940,13 @@ function registerDepsCommand(program3) {
73312
73940
  console.error(source_default.red(`Bundled skills pool dir not found at ${skillsRoot} \u2014 run \`switchroom update\` to install it.`));
73313
73941
  process.exit(1);
73314
73942
  }
73315
- const skillDir = join55(skillsRoot, skill);
73943
+ const skillDir = join57(skillsRoot, skill);
73316
73944
  if (!existsSync57(skillDir)) {
73317
73945
  console.error(source_default.red(`Unknown skill: ${skill} (no dir at ${skillDir})`));
73318
73946
  process.exit(1);
73319
73947
  }
73320
- const requirementsPath = join55(skillDir, "requirements.txt");
73321
- const packageJsonPath = join55(skillDir, "package.json");
73948
+ const requirementsPath = join57(skillDir, "requirements.txt");
73949
+ const packageJsonPath = join57(skillDir, "package.json");
73322
73950
  const wantPython = opts.python ?? (!opts.python && !opts.node && existsSync57(requirementsPath));
73323
73951
  const wantNode = opts.node ?? (!opts.python && !opts.node && existsSync57(packageJsonPath));
73324
73952
  let did = 0;
@@ -74273,7 +74901,7 @@ init_helpers();
74273
74901
  init_loader();
74274
74902
  init_merge();
74275
74903
  import { copyFileSync as copyFileSync10, existsSync as existsSync59, readFileSync as readFileSync52, writeFileSync as writeFileSync29 } from "node:fs";
74276
- import { join as join56, resolve as resolve37 } from "node:path";
74904
+ import { join as join58, resolve as resolve37 } from "node:path";
74277
74905
  init_schema();
74278
74906
  function resolveSoulTargetOrExit(program3, agentName) {
74279
74907
  const config = getConfig(program3);
@@ -74297,7 +74925,7 @@ function resolveSoulTargetOrExit(program3, agentName) {
74297
74925
  profileName,
74298
74926
  profilePath,
74299
74927
  workspaceDir,
74300
- soulPath: join56(workspaceDir, "SOUL.md"),
74928
+ soulPath: join58(workspaceDir, "SOUL.md"),
74301
74929
  soul: merged.soul
74302
74930
  };
74303
74931
  }
@@ -74364,7 +74992,7 @@ function registerSoulCommand(program3) {
74364
74992
  init_helpers();
74365
74993
  init_loader();
74366
74994
  import { existsSync as existsSync60, readFileSync as readFileSync53, readdirSync as readdirSync21, statSync as statSync26 } from "node:fs";
74367
- import { resolve as resolve38, join as join57 } from "node:path";
74995
+ import { resolve as resolve38, join as join59 } from "node:path";
74368
74996
  import { createHash as createHash13 } from "node:crypto";
74369
74997
  init_merge();
74370
74998
  init_hindsight();
@@ -74378,7 +75006,7 @@ function sha256(content) {
74378
75006
  return createHash13("sha256").update(content).digest("hex").slice(0, 16);
74379
75007
  }
74380
75008
  function findLatestTranscriptJsonl(claudeConfigDir) {
74381
- const projectsDir = join57(claudeConfigDir, "projects");
75009
+ const projectsDir = join59(claudeConfigDir, "projects");
74382
75010
  if (!existsSync60(projectsDir))
74383
75011
  return;
74384
75012
  try {
@@ -74387,8 +75015,8 @@ function findLatestTranscriptJsonl(claudeConfigDir) {
74387
75015
  for (const entry of entries) {
74388
75016
  if (!entry.isDirectory())
74389
75017
  continue;
74390
- const projectPath = join57(projectsDir, entry.name);
74391
- const transcriptPath = join57(projectPath, "transcript.jsonl");
75018
+ const projectPath = join59(projectsDir, entry.name);
75019
+ const transcriptPath = join59(projectPath, "transcript.jsonl");
74392
75020
  if (!existsSync60(transcriptPath))
74393
75021
  continue;
74394
75022
  const stat3 = statSync26(transcriptPath);
@@ -74457,11 +75085,11 @@ function registerDebugCommand(program3) {
74457
75085
  process.exit(1);
74458
75086
  }
74459
75087
  const workspaceDir = resolveAgentWorkspaceDir(agentDir);
74460
- const claudeConfigDir = join57(agentDir, ".claude");
74461
- const claudeMdPath = join57(agentDir, "CLAUDE.md");
74462
- const soulMdPath = join57(agentDir, "SOUL.md");
74463
- const workspaceSoulMdPath = join57(workspaceDir, "SOUL.md");
74464
- const handoffPath = join57(agentDir, ".handoff.md");
75088
+ const claudeConfigDir = join59(agentDir, ".claude");
75089
+ const claudeMdPath = join59(agentDir, "CLAUDE.md");
75090
+ const soulMdPath = join59(agentDir, "SOUL.md");
75091
+ const workspaceSoulMdPath = join59(workspaceDir, "SOUL.md");
75092
+ const handoffPath = join59(agentDir, ".handoff.md");
74465
75093
  const lastN = parseInt(opts.last, 10);
74466
75094
  if (isNaN(lastN) || lastN < 1) {
74467
75095
  console.error("--last must be a positive integer");
@@ -74611,8 +75239,8 @@ init_source();
74611
75239
  // src/worktree/claim.ts
74612
75240
  import { execFileSync as execFileSync17 } from "node:child_process";
74613
75241
  import { closeSync as closeSync12, mkdirSync as mkdirSync33, openSync as openSync12, existsSync as existsSync62, unlinkSync as unlinkSync13 } from "node:fs";
74614
- import { join as join59, resolve as resolve40 } from "node:path";
74615
- import { homedir as homedir34 } from "node:os";
75242
+ import { join as join61, resolve as resolve40 } from "node:path";
75243
+ import { homedir as homedir36 } from "node:os";
74616
75244
  import { randomBytes as randomBytes13 } from "node:crypto";
74617
75245
 
74618
75246
  // src/worktree/registry.ts
@@ -74625,13 +75253,13 @@ import {
74625
75253
  existsSync as existsSync61,
74626
75254
  renameSync as renameSync12
74627
75255
  } from "node:fs";
74628
- import { join as join58, resolve as resolve39 } from "node:path";
74629
- import { homedir as homedir33 } from "node:os";
75256
+ import { join as join60, resolve as resolve39 } from "node:path";
75257
+ import { homedir as homedir35 } from "node:os";
74630
75258
  function registryDir() {
74631
- return resolve39(process.env.SWITCHROOM_WORKTREE_DIR ?? join58(homedir33(), ".switchroom", "worktrees"));
75259
+ return resolve39(process.env.SWITCHROOM_WORKTREE_DIR ?? join60(homedir35(), ".switchroom", "worktrees"));
74632
75260
  }
74633
75261
  function recordPath(id) {
74634
- return join58(registryDir(), `${id}.json`);
75262
+ return join60(registryDir(), `${id}.json`);
74635
75263
  }
74636
75264
  function ensureDir2() {
74637
75265
  mkdirSync32(registryDir(), { recursive: true });
@@ -74682,7 +75310,7 @@ function acquireRepoLock(repoPath) {
74682
75310
  const lockDir = registryDir();
74683
75311
  mkdirSync33(lockDir, { recursive: true });
74684
75312
  const lockName = repoPath.replace(/[^A-Za-z0-9]/g, "_");
74685
- const lockPath = join59(lockDir, `.lock-${lockName}`);
75313
+ const lockPath = join61(lockDir, `.lock-${lockName}`);
74686
75314
  const deadline = Date.now() + 5000;
74687
75315
  let fd = null;
74688
75316
  while (fd === null) {
@@ -74709,7 +75337,7 @@ function acquireRepoLock(repoPath) {
74709
75337
  }
74710
75338
  var DEFAULT_CONCURRENCY = 5;
74711
75339
  function worktreesBaseDir() {
74712
- return resolve40(process.env.SWITCHROOM_WORKTREE_BASE ?? join59(homedir34(), ".switchroom", "worktree-checkouts"));
75340
+ return resolve40(process.env.SWITCHROOM_WORKTREE_BASE ?? join61(homedir36(), ".switchroom", "worktree-checkouts"));
74713
75341
  }
74714
75342
  function shortId() {
74715
75343
  return randomBytes13(4).toString("hex");
@@ -74731,7 +75359,7 @@ function resolveRepoPath(repo, codeRepos) {
74731
75359
  }
74732
75360
  function expandHome(p) {
74733
75361
  if (p.startsWith("~/"))
74734
- return join59(homedir34(), p.slice(2));
75362
+ return join61(homedir36(), p.slice(2));
74735
75363
  return p;
74736
75364
  }
74737
75365
  async function claimWorktree(input, codeRepos) {
@@ -74759,7 +75387,7 @@ async function claimWorktree(input, codeRepos) {
74759
75387
  branch = `task/${taskSuffix}-${id}`;
74760
75388
  const baseDir = worktreesBaseDir();
74761
75389
  mkdirSync33(baseDir, { recursive: true });
74762
- worktreePath = join59(baseDir, `${id}-${taskSuffix}`);
75390
+ worktreePath = join61(baseDir, `${id}-${taskSuffix}`);
74763
75391
  const now = new Date().toISOString();
74764
75392
  const record2 = {
74765
75393
  id,
@@ -75014,7 +75642,7 @@ import {
75014
75642
  rmSync as rmSync15,
75015
75643
  writeFileSync as writeFileSync31
75016
75644
  } from "node:fs";
75017
- import { join as join60 } from "node:path";
75645
+ import { join as join62 } from "node:path";
75018
75646
  function encodeCredentialsFilename(email) {
75019
75647
  const SAFE = new Set([
75020
75648
  ..."ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
@@ -75204,16 +75832,16 @@ function resolveCredentialsDir(env2) {
75204
75832
  if (explicit && explicit.length > 0)
75205
75833
  return explicit;
75206
75834
  const stateBase = env2.SWITCHROOM_CONTAINER === "1" ? "/state/agent" : env2.HOME ?? ".";
75207
- return join60(stateBase, "google-workspace-mcp", "credentials");
75835
+ return join62(stateBase, "google-workspace-mcp", "credentials");
75208
75836
  }
75209
75837
  function writeSeedFile(dir, email, seed) {
75210
75838
  mkdirSync34(dir, { recursive: true, mode: 448 });
75211
75839
  chmodSync9(dir, 448);
75212
75840
  for (const name of readdirSync23(dir)) {
75213
- rmSync15(join60(dir, name), { force: true, recursive: true });
75841
+ rmSync15(join62(dir, name), { force: true, recursive: true });
75214
75842
  }
75215
75843
  const filename = encodeCredentialsFilename(email);
75216
- const filePath = join60(dir, filename);
75844
+ const filePath = join62(dir, filename);
75217
75845
  writeFileSync31(filePath, JSON.stringify(seed), { mode: 384 });
75218
75846
  chmodSync9(filePath, 384);
75219
75847
  return filePath;
@@ -75369,9 +75997,599 @@ function registerDriveMcpLauncherCommand(program3) {
75369
75997
  });
75370
75998
  }
75371
75999
 
76000
+ // src/cli/m365-mcp-launcher.ts
76001
+ init_scaffold_integration();
76002
+ import { spawn as spawn6 } from "node:child_process";
76003
+ import { writeFileSync as writeFileSync32, mkdirSync as mkdirSync35 } from "node:fs";
76004
+ import { dirname as dirname17, join as join63 } from "node:path";
76005
+ var SOFTERIA_TOKEN_ENV = "MS365_MCP_OAUTH_TOKEN";
76006
+ var DEFAULT_REFRESH_LEAD_MS = 5 * 60 * 1000;
76007
+ var MAX_REFRESH_INTERVAL_MS = 60 * 60 * 1000;
76008
+ function buildSofteriaArgs(opts = {}) {
76009
+ const pkg = `${MICROSOFT_WORKSPACE_MCP_PACKAGE}@${MICROSOFT_WORKSPACE_MCP_PINNED_VERSION}`;
76010
+ const args = ["-y", pkg];
76011
+ if (opts.orgMode)
76012
+ args.push("--org-mode");
76013
+ return args;
76014
+ }
76015
+ function buildSofteriaEnv(accessToken, parentEnv = process.env) {
76016
+ const env2 = { ...parentEnv };
76017
+ env2[SOFTERIA_TOKEN_ENV] = accessToken;
76018
+ return env2;
76019
+ }
76020
+ function computeRefreshDelayMs(expiresAt, now, leadMs = DEFAULT_REFRESH_LEAD_MS) {
76021
+ const remaining = expiresAt - now - leadMs;
76022
+ if (remaining <= 0)
76023
+ return 0;
76024
+ return Math.min(remaining, MAX_REFRESH_INTERVAL_MS);
76025
+ }
76026
+ function writeRefreshHeartbeat(agentName, data) {
76027
+ const path7 = heartbeatPath(agentName);
76028
+ try {
76029
+ mkdirSync35(dirname17(path7), { recursive: true });
76030
+ writeFileSync32(path7, JSON.stringify(data, null, 2), { mode: 420 });
76031
+ } catch {}
76032
+ }
76033
+ function heartbeatPath(agentName) {
76034
+ const override = process.env.SWITCHROOM_M365_HEARTBEAT_DIR;
76035
+ if (override) {
76036
+ return join63(override, `m365-launcher-${agentName}.heartbeat.json`);
76037
+ }
76038
+ return "/state/agent/m365-launcher.heartbeat.json";
76039
+ }
76040
+ function wireStdio(child) {
76041
+ const onParentStdin = (chunk2) => {
76042
+ try {
76043
+ child.stdin?.write(chunk2);
76044
+ } catch {}
76045
+ };
76046
+ process.stdin.on("data", onParentStdin);
76047
+ child.stdout?.pipe(process.stdout, { end: false });
76048
+ child.stderr?.pipe(process.stderr, { end: false });
76049
+ return () => {
76050
+ process.stdin.off("data", onParentStdin);
76051
+ try {
76052
+ child.stdout?.unpipe(process.stdout);
76053
+ child.stderr?.unpipe(process.stderr);
76054
+ } catch {}
76055
+ };
76056
+ }
76057
+ async function killChild(child, gracefulMs = 3000) {
76058
+ if (child.exitCode !== null || child.signalCode !== null)
76059
+ return;
76060
+ return new Promise((resolve41) => {
76061
+ let killTimer = null;
76062
+ const onExit = () => {
76063
+ if (killTimer) {
76064
+ clearTimeout(killTimer);
76065
+ killTimer = null;
76066
+ }
76067
+ resolve41();
76068
+ };
76069
+ child.once("exit", onExit);
76070
+ try {
76071
+ child.kill("SIGTERM");
76072
+ } catch {
76073
+ resolve41();
76074
+ return;
76075
+ }
76076
+ killTimer = setTimeout(() => {
76077
+ if (child.exitCode === null && child.signalCode === null) {
76078
+ try {
76079
+ child.kill("SIGKILL");
76080
+ } catch {}
76081
+ }
76082
+ }, gracefulMs);
76083
+ });
76084
+ }
76085
+ async function runMs365McpLauncher(opts, rt) {
76086
+ const setTimer = rt.setTimer ?? setTimeout;
76087
+ const clearTimer = rt.clearTimer ?? clearTimeout;
76088
+ const now = rt.now ?? Date.now;
76089
+ const log = rt.log ?? ((msg) => process.stderr.write(`${msg}
76090
+ `));
76091
+ const agentName = process.env.SWITCHROOM_AGENT_NAME ?? "unknown";
76092
+ let currentChild = null;
76093
+ let teardownStdio = null;
76094
+ let refreshTimer = null;
76095
+ let restartingForRefresh = false;
76096
+ let resolveLauncher = null;
76097
+ const exitLauncher = (code) => {
76098
+ if (refreshTimer) {
76099
+ clearTimer(refreshTimer);
76100
+ refreshTimer = null;
76101
+ }
76102
+ if (resolveLauncher) {
76103
+ const r = resolveLauncher;
76104
+ resolveLauncher = null;
76105
+ r(code);
76106
+ }
76107
+ };
76108
+ const launchChild = (accessToken) => {
76109
+ const env2 = buildSofteriaEnv(accessToken);
76110
+ const child = rt.spawnSofteria(env2);
76111
+ teardownStdio = wireStdio(child);
76112
+ child.once("exit", (code, signal) => {
76113
+ if (teardownStdio) {
76114
+ teardownStdio();
76115
+ teardownStdio = null;
76116
+ }
76117
+ if (restartingForRefresh) {
76118
+ return;
76119
+ }
76120
+ const resolved = code ?? (signal ? 128 : 0);
76121
+ log(`m365-launcher: softeria exited unexpectedly code=${resolved} signal=${signal}`);
76122
+ exitLauncher(resolved);
76123
+ });
76124
+ return child;
76125
+ };
76126
+ const scheduleRefresh = (expiresAtMs) => {
76127
+ const delayMs = computeRefreshDelayMs(expiresAtMs, now());
76128
+ const nextRefreshMs = now() + delayMs;
76129
+ writeRefreshHeartbeat(agentName, {
76130
+ lastRefreshMs: now(),
76131
+ nextRefreshMs,
76132
+ expiresAtMs
76133
+ });
76134
+ log(`m365-launcher: scheduled refresh in ${Math.round(delayMs / 1000)}s (token expires at ${new Date(expiresAtMs).toISOString()})`);
76135
+ refreshTimer = setTimer(async () => {
76136
+ try {
76137
+ log("m365-launcher: refreshing token + restarting softeria");
76138
+ restartingForRefresh = true;
76139
+ try {
76140
+ process.stdin.pause();
76141
+ } catch {}
76142
+ const fresh = await rt.fetchCreds();
76143
+ if (currentChild) {
76144
+ await killChild(currentChild);
76145
+ }
76146
+ restartingForRefresh = false;
76147
+ currentChild = launchChild(fresh.accessToken);
76148
+ try {
76149
+ process.stdin.resume();
76150
+ } catch {}
76151
+ scheduleRefresh(fresh.expiresAt);
76152
+ } catch (err) {
76153
+ restartingForRefresh = false;
76154
+ try {
76155
+ process.stdin.resume();
76156
+ } catch {}
76157
+ const msg = err instanceof Error ? err.message : String(err);
76158
+ log(`m365-launcher: refresh failed \u2014 ${msg}`);
76159
+ refreshTimer = setTimer(() => {
76160
+ scheduleRefresh(now() + 60000);
76161
+ }, 30000);
76162
+ }
76163
+ }, delayMs);
76164
+ };
76165
+ let initial;
76166
+ try {
76167
+ initial = await rt.fetchCreds();
76168
+ } catch (err) {
76169
+ const msg = err instanceof Error ? err.message : String(err);
76170
+ log(`m365-launcher: initial broker call failed \u2014 ${msg}`);
76171
+ return 1;
76172
+ }
76173
+ currentChild = launchChild(initial.accessToken);
76174
+ scheduleRefresh(initial.expiresAt);
76175
+ const onSignal = (signal) => {
76176
+ log(`m365-launcher: received ${signal}, shutting down`);
76177
+ if (currentChild) {
76178
+ try {
76179
+ currentChild.kill(signal);
76180
+ } catch {}
76181
+ }
76182
+ const sigCode = signal === "SIGTERM" ? 143 : signal === "SIGINT" ? 130 : 0;
76183
+ exitLauncher(sigCode);
76184
+ };
76185
+ process.on("SIGINT", onSignal);
76186
+ process.on("SIGTERM", onSignal);
76187
+ return new Promise((resolve41) => {
76188
+ resolveLauncher = resolve41;
76189
+ });
76190
+ }
76191
+ function registerM365McpLauncherCommand(program3) {
76192
+ program3.command("m365-mcp-launcher", { hidden: true }).option("--org-mode", "Pass --org-mode to softeria (Teams/SharePoint tools).", false).description("Internal \u2014 Microsoft 365 MCP launcher. Acquires a fresh access token from the auth-broker and execs softeria/ms-365-mcp-server in BYOT mode, restarting it ~55min before token expiry. RFC #1873 PR 3.").action(async (opts) => {
76193
+ const { brokerCall: brokerCall2 } = await Promise.resolve().then(() => (init_broker_call(), exports_broker_call));
76194
+ const code = await runMs365McpLauncher(opts, {
76195
+ fetchCreds: async () => {
76196
+ return await brokerCall2(async (client2) => {
76197
+ const data = await client2.getCredentials("microsoft");
76198
+ const mc = data.credentials ?? {};
76199
+ const accessToken = mc.microsoftOauth?.accessToken;
76200
+ const expiresAt = mc.microsoftOauth?.expiresAt;
76201
+ if (!accessToken || typeof expiresAt !== "number") {
76202
+ throw new Error("auth-broker returned credentials without microsoftOauth.accessToken or .expiresAt");
76203
+ }
76204
+ return { accessToken, expiresAt };
76205
+ });
76206
+ },
76207
+ spawnSofteria: (env2) => {
76208
+ return spawn6("npx", buildSofteriaArgs(opts), {
76209
+ env: env2,
76210
+ stdio: ["pipe", "pipe", "pipe"]
76211
+ });
76212
+ }
76213
+ });
76214
+ process.exit(code);
76215
+ });
76216
+ }
76217
+
76218
+ // src/cli/notion-mcp-launcher.ts
76219
+ init_scaffold_integration();
76220
+ import { spawn as spawn7 } from "node:child_process";
76221
+ import { existsSync as existsSync65, mkdirSync as mkdirSync36, writeFileSync as writeFileSync33 } from "node:fs";
76222
+ import { dirname as dirname18 } from "node:path";
76223
+ var HEARTBEAT_WRITE_INTERVAL_MS = 30 * 1000;
76224
+ var DEFAULT_HEARTBEAT_PATH = "/state/agent/notion-launcher.heartbeat.json";
76225
+ var DEFAULT_VAULT_KEY = "notion/integration-token";
76226
+ function buildNotionMcpArgs(opts) {
76227
+ const version2 = opts.mcpVersion ?? NOTION_MCP_PINNED_VERSION;
76228
+ return ["-y", `${NOTION_MCP_PACKAGE}@${version2}`];
76229
+ }
76230
+ function defaultWriteHeartbeat(path7, contents) {
76231
+ try {
76232
+ const dir = dirname18(path7);
76233
+ if (!existsSync65(dir))
76234
+ mkdirSync36(dir, { recursive: true });
76235
+ writeFileSync33(path7, contents);
76236
+ } catch {}
76237
+ }
76238
+ async function runNotionMcpLauncher(opts, runtime) {
76239
+ const heartbeatPath2 = opts.heartbeatPath ?? DEFAULT_HEARTBEAT_PATH;
76240
+ const writeHeartbeat = runtime.writeHeartbeat ?? defaultWriteHeartbeat;
76241
+ const setTimer = runtime.setTimer ?? ((cb, ms) => setInterval(cb, ms));
76242
+ const clearTimer = runtime.clearTimer ?? clearInterval;
76243
+ const now = runtime.now ?? (() => Date.now());
76244
+ let token;
76245
+ try {
76246
+ token = await runtime.fetchToken();
76247
+ } catch (err) {
76248
+ process.stderr.write(`notion-mcp-launcher: failed to fetch token from vault-broker: ${err.message}
76249
+ `);
76250
+ return 1;
76251
+ }
76252
+ if (!token || typeof token !== "string") {
76253
+ process.stderr.write(`notion-mcp-launcher: vault-broker returned an empty/invalid token.
76254
+ `);
76255
+ return 1;
76256
+ }
76257
+ const childEnv = {
76258
+ ...process.env,
76259
+ [NOTION_TOKEN_ENV]: token
76260
+ };
76261
+ const args = buildNotionMcpArgs(opts);
76262
+ const child = runtime.spawnMcp(childEnv, args);
76263
+ if (child.stdin == null || child.stdout == null || child.stderr == null) {
76264
+ process.stderr.write(`notion-mcp-launcher: child stdio handles missing \u2014 aborting.
76265
+ `);
76266
+ return 1;
76267
+ }
76268
+ process.stdin.pipe(child.stdin);
76269
+ child.stdout.pipe(process.stdout);
76270
+ child.stderr.pipe(process.stderr);
76271
+ const writeOne = () => {
76272
+ const payload = {
76273
+ ts: now(),
76274
+ pid: process.pid,
76275
+ agent: process.env.SWITCHROOM_AGENT_NAME,
76276
+ mcp_package: NOTION_MCP_PACKAGE,
76277
+ mcp_version: opts.mcpVersion ?? NOTION_MCP_PINNED_VERSION
76278
+ };
76279
+ writeHeartbeat(heartbeatPath2, JSON.stringify(payload, null, 2));
76280
+ };
76281
+ writeOne();
76282
+ const heartbeatHandle = setTimer(writeOne, HEARTBEAT_WRITE_INTERVAL_MS);
76283
+ const forward = (sig) => {
76284
+ try {
76285
+ child.kill(sig);
76286
+ } catch {}
76287
+ };
76288
+ process.on("SIGTERM", () => forward("SIGTERM"));
76289
+ process.on("SIGINT", () => forward("SIGINT"));
76290
+ const exitCode = await new Promise((resolve41) => {
76291
+ child.once("exit", (code, signal) => {
76292
+ clearTimer(heartbeatHandle);
76293
+ if (typeof code === "number") {
76294
+ resolve41(code);
76295
+ } else if (signal) {
76296
+ const sigCode = { SIGTERM: 15, SIGINT: 2, SIGKILL: 9 }[signal] ?? 1;
76297
+ resolve41(128 + sigCode);
76298
+ } else {
76299
+ resolve41(1);
76300
+ }
76301
+ });
76302
+ child.once("error", (err) => {
76303
+ clearTimer(heartbeatHandle);
76304
+ process.stderr.write(`notion-mcp-launcher: child spawn error: ${err.message}
76305
+ `);
76306
+ resolve41(1);
76307
+ });
76308
+ });
76309
+ return exitCode;
76310
+ }
76311
+ function registerNotionMcpLauncherCommand(program3) {
76312
+ program3.command("notion-mcp-launcher", { hidden: true }).option("--vault-key <key>", "Override the vault key holding the Notion integration token. " + "Defaults to `notion/integration-token`.", DEFAULT_VAULT_KEY).option("--mcp-version <semver>", "Override the @notionhq/notion-mcp-server version to spawn.").option("--heartbeat-path <path>", "Override the heartbeat file path. Default: /state/agent/notion-launcher.heartbeat.json").description("Internal \u2014 Notion MCP launcher. Fetches the integration token from the vault-broker and execs @notionhq/notion-mcp-server in stdio mode. RFC docs/rfcs/notion-integration.md PR 2.").action(async (opts) => {
76313
+ const { getViaBrokerStructured: getViaBrokerStructured2 } = await Promise.resolve().then(() => (init_client(), exports_client));
76314
+ const code = await runNotionMcpLauncher(opts, {
76315
+ fetchToken: async () => {
76316
+ const key = opts.vaultKey ?? DEFAULT_VAULT_KEY;
76317
+ const result = await getViaBrokerStructured2(key);
76318
+ if (result.kind === "unreachable") {
76319
+ throw new Error(`vault-broker unreachable: ${result.msg}. Is the broker socket mounted?`);
76320
+ }
76321
+ if (result.kind === "not_found") {
76322
+ throw new Error(`vault key ${key} is missing. Run \`switchroom vault set ${key}\` on the host to populate it.`);
76323
+ }
76324
+ if (result.kind === "denied") {
76325
+ throw new Error(`vault-broker denied access to ${key}: ${result.msg}. Re-run \`switchroom vault set ${key} --allow <comma-separated-agents>\` on the host including this agent.`);
76326
+ }
76327
+ if (result.entry.kind !== "string") {
76328
+ throw new Error(`vault key ${key} is not a string entry \u2014 Notion expects an integration token string.`);
76329
+ }
76330
+ return result.entry.value;
76331
+ },
76332
+ spawnMcp: (env2, args) => {
76333
+ return spawn7("npx", args, {
76334
+ env: env2,
76335
+ stdio: ["pipe", "pipe", "pipe"]
76336
+ });
76337
+ }
76338
+ });
76339
+ process.exit(code);
76340
+ });
76341
+ }
76342
+
76343
+ // src/cli/notion.ts
76344
+ init_loader();
76345
+
76346
+ // src/notion/api-client.ts
76347
+ var DEFAULT_BASE = "https://api.notion.com";
76348
+ var DEFAULT_VERSION = "2022-06-28";
76349
+ var DEFAULT_TIMEOUT_MS4 = 5000;
76350
+ function createNotionApiClient(opts) {
76351
+ const base = opts.base ?? DEFAULT_BASE;
76352
+ const version2 = opts.notionVersion ?? DEFAULT_VERSION;
76353
+ const fetchImpl = opts.fetchImpl ?? fetch;
76354
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS4;
76355
+ async function request(method, path7, body) {
76356
+ const ac = new AbortController;
76357
+ const timeout = setTimeout(() => ac.abort(), timeoutMs);
76358
+ try {
76359
+ const res = await fetchImpl(`${base}${path7}`, {
76360
+ method,
76361
+ headers: {
76362
+ Authorization: `Bearer ${opts.token}`,
76363
+ "Notion-Version": version2,
76364
+ "Content-Type": "application/json"
76365
+ },
76366
+ body: body === undefined ? undefined : JSON.stringify(body),
76367
+ signal: ac.signal
76368
+ });
76369
+ if (!res.ok) {
76370
+ let detail = "";
76371
+ try {
76372
+ detail = await res.text();
76373
+ } catch {}
76374
+ throw new Error(`Notion API ${method} ${path7} failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail.slice(0, 200)}` : ""}`);
76375
+ }
76376
+ return await res.json();
76377
+ } finally {
76378
+ clearTimeout(timeout);
76379
+ }
76380
+ }
76381
+ return {
76382
+ async getPage(pageId) {
76383
+ const data = await request("GET", `/v1/pages/${encodeURIComponent(pageId)}`);
76384
+ return { parent: data.parent };
76385
+ },
76386
+ async getBlock(blockId) {
76387
+ const data = await request("GET", `/v1/blocks/${encodeURIComponent(blockId)}`);
76388
+ return { parent: data.parent };
76389
+ },
76390
+ async search(query, pageSize = 20) {
76391
+ return await request("POST", "/v1/search", {
76392
+ query,
76393
+ page_size: pageSize
76394
+ });
76395
+ }
76396
+ };
76397
+ }
76398
+
76399
+ // src/cli/notion.ts
76400
+ init_client();
76401
+ function registerNotionCommand(program3) {
76402
+ const cmd = program3.command("notion").description("Notion integration operator helpers (list-dbs, test). See docs/rfcs/notion-integration.md.");
76403
+ cmd.command("list-dbs").description("List the databases the Notion integration can access. Output is a ready-to-paste YAML block for `notion_workspace.databases`.").option("--vault-key <key>", "Override the vault key holding the integration token.", "notion/integration-token").action(async (opts) => {
76404
+ const code = await runListDbs(opts);
76405
+ process.exit(code);
76406
+ });
76407
+ cmd.command("test <agent>").description("Smoke-test Notion access for an agent. Calls /v1/users/me via the broker and prints the integration's bot user.").option("--vault-key <key>", "Override the vault key holding the integration token.", "notion/integration-token").action(async (agent, opts) => {
76408
+ const code = await runTest(agent, opts);
76409
+ process.exit(code);
76410
+ });
76411
+ }
76412
+ async function runListDbs(opts) {
76413
+ let token;
76414
+ try {
76415
+ token = await fetchToken(opts.vaultKey);
76416
+ } catch (err) {
76417
+ process.stderr.write(`switchroom notion list-dbs: ${err.message}
76418
+ `);
76419
+ return 1;
76420
+ }
76421
+ const client2 = createNotionApiClient({ token });
76422
+ let dbs;
76423
+ try {
76424
+ dbs = await searchAllDatabases(token);
76425
+ } catch (err) {
76426
+ process.stderr.write(`switchroom notion list-dbs: failed to enumerate databases: ${err.message}
76427
+ `);
76428
+ return 1;
76429
+ }
76430
+ if (dbs.length === 0) {
76431
+ process.stderr.write(`switchroom notion list-dbs: the integration has no databases shared with it.
76432
+ ` + "Open Notion \u2192 page/database you want switchroom to access \u2192 top-right \u22ef \u2192 Connections \u2192 add `switchroom`, then re-run.\n");
76433
+ return 1;
76434
+ }
76435
+ process.stdout.write("# Paste under `notion_workspace:` in switchroom.yaml:\n");
76436
+ process.stdout.write(`databases:
76437
+ `);
76438
+ const seen = new Set;
76439
+ for (const db of dbs) {
76440
+ const slug = toFriendlyName(db.title || db.id, seen);
76441
+ seen.add(slug);
76442
+ process.stdout.write(` ${slug}: "${db.id}"
76443
+ `);
76444
+ }
76445
+ if (process.stdout.isTTY) {
76446
+ process.stderr.write(`
76447
+ Found ${dbs.length} database(s). Edit the friendly names to taste \u2014 agents reference them by name in \`agents.<name>.notion_workspace.databases\`.
76448
+ `);
76449
+ }
76450
+ return 0;
76451
+ }
76452
+ function toFriendlyName(title, seen) {
76453
+ let base = title.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
76454
+ if (!base || !/^[a-z0-9]/.test(base))
76455
+ base = `db-${base.replace(/^[^a-z0-9]+/, "")}`;
76456
+ if (!base)
76457
+ base = "db";
76458
+ let candidate = base;
76459
+ let i = 2;
76460
+ while (seen.has(candidate)) {
76461
+ candidate = `${base}-${i}`;
76462
+ i += 1;
76463
+ }
76464
+ return candidate;
76465
+ }
76466
+ async function searchAllDatabases(token) {
76467
+ const out = [];
76468
+ let cursor = null;
76469
+ for (let i = 0;i < 10; i += 1) {
76470
+ const body = {
76471
+ filter: { value: "database", property: "object" },
76472
+ page_size: 100
76473
+ };
76474
+ if (cursor)
76475
+ body.start_cursor = cursor;
76476
+ const res = await postSearch(token, body);
76477
+ for (const hit of res.results) {
76478
+ const title = extractDbTitle(hit) || hit.id;
76479
+ out.push({ id: hit.id, title });
76480
+ }
76481
+ if (!res.next_cursor)
76482
+ break;
76483
+ cursor = res.next_cursor;
76484
+ }
76485
+ return out;
76486
+ }
76487
+ function extractDbTitle(hit) {
76488
+ if (!hit.title || !Array.isArray(hit.title))
76489
+ return null;
76490
+ return hit.title.map((t) => t.plain_text ?? "").join("").trim() || null;
76491
+ }
76492
+ async function postSearch(token, body) {
76493
+ const ac = new AbortController;
76494
+ const timeout = setTimeout(() => ac.abort(), 1e4);
76495
+ try {
76496
+ const res = await fetch("https://api.notion.com/v1/search", {
76497
+ method: "POST",
76498
+ headers: {
76499
+ Authorization: `Bearer ${token}`,
76500
+ "Notion-Version": "2022-06-28",
76501
+ "Content-Type": "application/json"
76502
+ },
76503
+ body: JSON.stringify(body),
76504
+ signal: ac.signal
76505
+ });
76506
+ if (!res.ok) {
76507
+ throw new Error(`Notion /v1/search ${res.status}: ${await res.text()}`);
76508
+ }
76509
+ return await res.json();
76510
+ } finally {
76511
+ clearTimeout(timeout);
76512
+ }
76513
+ }
76514
+ async function runTest(agent, opts) {
76515
+ let config;
76516
+ try {
76517
+ config = loadConfig();
76518
+ } catch (err) {
76519
+ process.stderr.write(`switchroom notion test: failed to load switchroom.yaml: ${err.message}
76520
+ `);
76521
+ return 1;
76522
+ }
76523
+ const agentCfg = config.agents?.[agent];
76524
+ if (!agentCfg) {
76525
+ process.stderr.write(`switchroom notion test: agent '${agent}' not found in switchroom.yaml.
76526
+ `);
76527
+ return 1;
76528
+ }
76529
+ if (!agentCfg.notion_workspace) {
76530
+ process.stderr.write(`switchroom notion test: agent '${agent}' has no notion_workspace block. Set \`agents.${agent}.notion_workspace: {}\` (or with databases:) in switchroom.yaml.
76531
+ `);
76532
+ return 1;
76533
+ }
76534
+ let token;
76535
+ try {
76536
+ token = await fetchToken(opts.vaultKey);
76537
+ } catch (err) {
76538
+ process.stderr.write(`switchroom notion test: ${err.message}
76539
+ `);
76540
+ return 1;
76541
+ }
76542
+ try {
76543
+ const ac = new AbortController;
76544
+ const timeout = setTimeout(() => ac.abort(), 1e4);
76545
+ let res;
76546
+ try {
76547
+ res = await fetch("https://api.notion.com/v1/users/me", {
76548
+ method: "GET",
76549
+ headers: {
76550
+ Authorization: `Bearer ${token}`,
76551
+ "Notion-Version": "2022-06-28"
76552
+ },
76553
+ signal: ac.signal
76554
+ });
76555
+ } finally {
76556
+ clearTimeout(timeout);
76557
+ }
76558
+ if (!res.ok) {
76559
+ process.stderr.write(`switchroom notion test: Notion API returned ${res.status} \u2014 token may be revoked. Detail: ${await res.text()}
76560
+ `);
76561
+ return 1;
76562
+ }
76563
+ const data = await res.json();
76564
+ process.stdout.write(`OK \u2014 agent='${agent}', integration bot='${data.name ?? "?"}', workspace='${data.bot?.workspace_name ?? "?"}'
76565
+ `);
76566
+ return 0;
76567
+ } catch (err) {
76568
+ process.stderr.write(`switchroom notion test: ${err.message}
76569
+ `);
76570
+ return 1;
76571
+ }
76572
+ }
76573
+ async function fetchToken(vaultKey) {
76574
+ const result = await getViaBrokerStructured(vaultKey);
76575
+ if (result.kind === "unreachable") {
76576
+ throw new Error(`vault-broker unreachable: ${result.msg}`);
76577
+ }
76578
+ if (result.kind === "not_found") {
76579
+ throw new Error(`vault key '${vaultKey}' is missing. Run \`switchroom vault set ${vaultKey}\` first.`);
76580
+ }
76581
+ if (result.kind === "denied") {
76582
+ throw new Error(`vault-broker denied access to '${vaultKey}': ${result.msg}`);
76583
+ }
76584
+ if (result.entry.kind !== "string") {
76585
+ throw new Error(`vault key '${vaultKey}' is not a string entry`);
76586
+ }
76587
+ return result.entry.value;
76588
+ }
76589
+
75372
76590
  // src/cli/apply.ts
75373
76591
  init_source();
75374
- import { accessSync as accessSync3, chownSync as chownSync4, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync68, mkdirSync as mkdirSync36, readFileSync as readFileSync56, readdirSync as readdirSync25, renameSync as renameSync13, writeFileSync as writeFileSync33 } from "node:fs";
76592
+ import { accessSync as accessSync3, chownSync as chownSync4, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync69, mkdirSync as mkdirSync38, readFileSync as readFileSync56, readdirSync as readdirSync25, renameSync as renameSync13, writeFileSync as writeFileSync35 } from "node:fs";
75375
76593
  import { mkdir, writeFile } from "node:fs/promises";
75376
76594
  import { spawnSync as childSpawnSync } from "node:child_process";
75377
76595
  import readline from "node:readline";
@@ -75734,16 +76952,16 @@ agents:
75734
76952
 
75735
76953
  // src/cli/apply.ts
75736
76954
  init_resolver();
75737
- import { dirname as dirname19, join as join64, resolve as resolve42 } from "node:path";
75738
- import { homedir as homedir36 } from "node:os";
76955
+ import { dirname as dirname21, join as join67, resolve as resolve42 } from "node:path";
76956
+ import { homedir as homedir38 } from "node:os";
75739
76957
  import { execFileSync as execFileSync20 } from "node:child_process";
75740
76958
  init_vault();
75741
76959
  init_loader();
75742
76960
  init_loader();
75743
76961
 
75744
76962
  // src/cli/update-prompt-hook.ts
75745
- import { existsSync as existsSync65, readFileSync as readFileSync55, writeFileSync as writeFileSync32, chmodSync as chmodSync10, mkdirSync as mkdirSync35 } from "node:fs";
75746
- import { join as join61 } from "node:path";
76963
+ import { existsSync as existsSync66, readFileSync as readFileSync55, writeFileSync as writeFileSync34, chmodSync as chmodSync10, mkdirSync as mkdirSync37 } from "node:fs";
76964
+ import { join as join64 } from "node:path";
75747
76965
  var HOOK_FILENAME = "update-card-on-prompt.sh";
75748
76966
  function updatePromptHookScript() {
75749
76967
  return `#!/bin/bash
@@ -75809,14 +77027,14 @@ exit 0
75809
77027
  `;
75810
77028
  }
75811
77029
  function installUpdatePromptHook(agentDir) {
75812
- const hooksDir = join61(agentDir, ".claude", "hooks");
75813
- mkdirSync35(hooksDir, { recursive: true });
75814
- const scriptPath = join61(hooksDir, HOOK_FILENAME);
77030
+ const hooksDir = join64(agentDir, ".claude", "hooks");
77031
+ mkdirSync37(hooksDir, { recursive: true });
77032
+ const scriptPath = join64(hooksDir, HOOK_FILENAME);
75815
77033
  const desired = updatePromptHookScript();
75816
77034
  let installed = false;
75817
- const existing = existsSync65(scriptPath) ? readFileSync55(scriptPath, "utf-8") : "";
77035
+ const existing = existsSync66(scriptPath) ? readFileSync55(scriptPath, "utf-8") : "";
75818
77036
  if (existing !== desired) {
75819
- writeFileSync32(scriptPath, desired, { mode: 493 });
77037
+ writeFileSync34(scriptPath, desired, { mode: 493 });
75820
77038
  chmodSync10(scriptPath, 493);
75821
77039
  installed = true;
75822
77040
  } else {
@@ -75824,8 +77042,8 @@ function installUpdatePromptHook(agentDir) {
75824
77042
  chmodSync10(scriptPath, 493);
75825
77043
  } catch {}
75826
77044
  }
75827
- const settingsPath = join61(agentDir, ".claude", "settings.json");
75828
- if (!existsSync65(settingsPath)) {
77045
+ const settingsPath = join64(agentDir, ".claude", "settings.json");
77046
+ if (!existsSync66(settingsPath)) {
75829
77047
  return { scriptPath, settingsPath, installed };
75830
77048
  }
75831
77049
  const raw = readFileSync55(settingsPath, "utf-8");
@@ -75861,7 +77079,7 @@ function installUpdatePromptHook(agentDir) {
75861
77079
  });
75862
77080
  hooks.UserPromptSubmit = list2;
75863
77081
  parsed.hooks = hooks;
75864
- writeFileSync32(settingsPath, JSON.stringify(parsed, null, 2) + `
77082
+ writeFileSync34(settingsPath, JSON.stringify(parsed, null, 2) + `
75865
77083
  `, { mode: 384 });
75866
77084
  installed = true;
75867
77085
  }
@@ -75944,13 +77162,13 @@ function detectInstallType() {
75944
77162
  // src/cli/operator-uid.ts
75945
77163
  import {
75946
77164
  chownSync as chownSync3,
75947
- existsSync as existsSync67,
77165
+ existsSync as existsSync68,
75948
77166
  lstatSync as lstatSync7,
75949
77167
  readdirSync as readdirSync24,
75950
77168
  realpathSync as realpathSync6,
75951
77169
  statSync as statSync27
75952
77170
  } from "node:fs";
75953
- import { join as join63 } from "node:path";
77171
+ import { join as join66 } from "node:path";
75954
77172
  function resolveOperatorUid() {
75955
77173
  const sudoUid = process.env.SUDO_UID;
75956
77174
  if (sudoUid !== undefined) {
@@ -75966,19 +77184,19 @@ function resolveOperatorUid() {
75966
77184
  return;
75967
77185
  }
75968
77186
  function operatorOwnedPaths(home2) {
75969
- const root = join63(home2, ".switchroom");
77187
+ const root = join66(home2, ".switchroom");
75970
77188
  return [
75971
- join63(root, "vault"),
75972
- join63(root, "vault-auto-unlock"),
75973
- join63(root, "vault-audit.log"),
75974
- join63(root, "host-control-audit.log"),
75975
- join63(root, "accounts"),
75976
- join63(root, "compose")
77189
+ join66(root, "vault"),
77190
+ join66(root, "vault-auto-unlock"),
77191
+ join66(root, "vault-audit.log"),
77192
+ join66(root, "host-control-audit.log"),
77193
+ join66(root, "accounts"),
77194
+ join66(root, "compose")
75977
77195
  ];
75978
77196
  }
75979
77197
  function restoreOperatorOwnership(home2, operatorUid, deps = {}) {
75980
77198
  const chown = deps.chown ?? ((p, u, g) => chownSync3(p, u, g));
75981
- const exists = deps.exists ?? ((p) => existsSync67(p));
77199
+ const exists = deps.exists ?? ((p) => existsSync68(p));
75982
77200
  const isSymlink = deps.isSymlink ?? ((p) => {
75983
77201
  try {
75984
77202
  return lstatSync7(p).isSymbolicLink();
@@ -76022,7 +77240,7 @@ function restoreOperatorOwnership(home2, operatorUid, deps = {}) {
76022
77240
  } catch {}
76023
77241
  if (isDir(target)) {
76024
77242
  for (const entry of readdir2(target)) {
76025
- visit(join63(target, entry));
77243
+ visit(join66(target, entry));
76026
77244
  }
76027
77245
  }
76028
77246
  };
@@ -76036,17 +77254,17 @@ var EMBEDDED_EXAMPLES = {
76036
77254
  switchroom: switchroom_default,
76037
77255
  minimal: minimal_default
76038
77256
  };
76039
- var DEFAULT_COMPOSE_PATH2 = join64(homedir36(), ".switchroom", "compose", "docker-compose.yml");
77257
+ var DEFAULT_COMPOSE_PATH2 = join67(homedir38(), ".switchroom", "compose", "docker-compose.yml");
76040
77258
  var COMPOSE_PROJECT2 = "switchroom";
76041
77259
  function resolveVaultBindMountDir(homeDir, ctx) {
76042
77260
  const isCustomPath = ctx.migrationKind === "custom-path-skipped";
76043
77261
  if (isCustomPath && ctx.customVaultPath) {
76044
- return dirname19(ctx.customVaultPath);
77262
+ return dirname21(ctx.customVaultPath);
76045
77263
  }
76046
- return join64(homeDir, ".switchroom", "vault");
77264
+ return join67(homeDir, ".switchroom", "vault");
76047
77265
  }
76048
77266
  function inspectVaultBindMountDir(vaultDir) {
76049
- if (!existsSync68(vaultDir))
77267
+ if (!existsSync69(vaultDir))
76050
77268
  return { kind: "missing" };
76051
77269
  const entries = readdirSync25(vaultDir);
76052
77270
  const unknown = [];
@@ -76072,63 +77290,63 @@ function hasVaultRefs(value) {
76072
77290
  return false;
76073
77291
  }
76074
77292
  async function ensureHostMountSources(config) {
76075
- const home2 = homedir36();
77293
+ const home2 = homedir38();
76076
77294
  const dirs = [
76077
- join64(home2, ".switchroom", "approvals"),
76078
- join64(home2, ".switchroom", "scheduler"),
76079
- join64(home2, ".switchroom", "logs"),
76080
- join64(home2, ".switchroom", "compose"),
76081
- join64(home2, ".switchroom", "broker-operator")
77295
+ join67(home2, ".switchroom", "approvals"),
77296
+ join67(home2, ".switchroom", "scheduler"),
77297
+ join67(home2, ".switchroom", "logs"),
77298
+ join67(home2, ".switchroom", "compose"),
77299
+ join67(home2, ".switchroom", "broker-operator")
76082
77300
  ];
76083
77301
  for (const name of Object.keys(config.agents)) {
76084
- dirs.push(join64(home2, ".switchroom", "agents", name));
76085
- dirs.push(join64(home2, ".switchroom", "logs", name));
76086
- dirs.push(join64(home2, ".claude", "projects", name));
76087
- dirs.push(join64(home2, ".switchroom", "audit", name));
76088
- if (existsSync68(join64(home2, ".switchroom-config"))) {
76089
- dirs.push(join64(home2, ".switchroom-config", "agents", name, "personal-skills"));
77302
+ dirs.push(join67(home2, ".switchroom", "agents", name));
77303
+ dirs.push(join67(home2, ".switchroom", "logs", name));
77304
+ dirs.push(join67(home2, ".claude", "projects", name));
77305
+ dirs.push(join67(home2, ".switchroom", "audit", name));
77306
+ if (existsSync69(join67(home2, ".switchroom-config"))) {
77307
+ dirs.push(join67(home2, ".switchroom-config", "agents", name, "personal-skills"));
76090
77308
  }
76091
77309
  }
76092
77310
  for (const dir of dirs) {
76093
77311
  await mkdir(dir, { recursive: true });
76094
77312
  }
76095
- const autoUnlockPath = join64(home2, ".switchroom", "vault-auto-unlock");
76096
- if (!existsSync68(autoUnlockPath)) {
76097
- writeFileSync33(autoUnlockPath, "", { mode: 384 });
77313
+ const autoUnlockPath = join67(home2, ".switchroom", "vault-auto-unlock");
77314
+ if (!existsSync69(autoUnlockPath)) {
77315
+ writeFileSync35(autoUnlockPath, "", { mode: 384 });
76098
77316
  }
76099
- const auditLogPath = join64(home2, ".switchroom", "vault-audit.log");
76100
- if (!existsSync68(auditLogPath)) {
76101
- writeFileSync33(auditLogPath, "", { mode: 420 });
77317
+ const auditLogPath = join67(home2, ".switchroom", "vault-audit.log");
77318
+ if (!existsSync69(auditLogPath)) {
77319
+ writeFileSync35(auditLogPath, "", { mode: 420 });
76102
77320
  }
76103
- const grantsDbPath = join64(home2, ".switchroom", "vault-grants.db");
76104
- if (!existsSync68(grantsDbPath)) {
76105
- writeFileSync33(grantsDbPath, "", { mode: 384 });
77321
+ const grantsDbPath = join67(home2, ".switchroom", "vault-grants.db");
77322
+ if (!existsSync69(grantsDbPath)) {
77323
+ writeFileSync35(grantsDbPath, "", { mode: 384 });
76106
77324
  }
76107
- const hostdAuditLogPath = join64(home2, ".switchroom", "host-control-audit.log");
76108
- if (!existsSync68(hostdAuditLogPath)) {
76109
- writeFileSync33(hostdAuditLogPath, "", { mode: 420 });
77325
+ const hostdAuditLogPath = join67(home2, ".switchroom", "host-control-audit.log");
77326
+ if (!existsSync69(hostdAuditLogPath)) {
77327
+ writeFileSync35(hostdAuditLogPath, "", { mode: 420 });
76110
77328
  }
76111
77329
  for (const name of Object.keys(config.agents)) {
76112
- const tokenPath = join64(home2, ".switchroom", "agents", name, ".vault-token");
76113
- if (!existsSync68(tokenPath)) {
76114
- writeFileSync33(tokenPath, "", { mode: 384 });
77330
+ const tokenPath = join67(home2, ".switchroom", "agents", name, ".vault-token");
77331
+ if (!existsSync69(tokenPath)) {
77332
+ writeFileSync35(tokenPath, "", { mode: 384 });
76115
77333
  }
76116
77334
  try {
76117
77335
  const uid = allocateAgentUid(name);
76118
77336
  chownSync4(tokenPath, uid, uid);
76119
77337
  } catch {}
76120
77338
  }
76121
- const fleetDir = join64(home2, ".switchroom", "fleet");
77339
+ const fleetDir = join67(home2, ".switchroom", "fleet");
76122
77340
  await mkdir(fleetDir, { recursive: true });
76123
- const invariantsPath = join64(fleetDir, "switchroom-invariants.md");
77341
+ const invariantsPath = join67(fleetDir, "switchroom-invariants.md");
76124
77342
  const invariantsCanonical = renderFleetInvariants();
76125
- const invariantsCurrent = existsSync68(invariantsPath) ? readFileSync56(invariantsPath, "utf-8") : null;
77343
+ const invariantsCurrent = existsSync69(invariantsPath) ? readFileSync56(invariantsPath, "utf-8") : null;
76126
77344
  if (invariantsCurrent !== invariantsCanonical) {
76127
- writeFileSync33(invariantsPath, invariantsCanonical, { mode: 420 });
77345
+ writeFileSync35(invariantsPath, invariantsCanonical, { mode: 420 });
76128
77346
  }
76129
- const fleetClaudePath = join64(fleetDir, "CLAUDE.md");
76130
- if (!existsSync68(fleetClaudePath)) {
76131
- writeFileSync33(fleetClaudePath, [
77347
+ const fleetClaudePath = join67(fleetDir, "CLAUDE.md");
77348
+ if (!existsSync69(fleetClaudePath)) {
77349
+ writeFileSync35(fleetClaudePath, [
76132
77350
  "# Switchroom fleet defaults",
76133
77351
  "",
76134
77352
  "Operator-owned fleet brain. Every agent reads this via",
@@ -76159,7 +77377,7 @@ ${out.trim()}`;
76159
77377
  }
76160
77378
  function runApplyPreflight(config, opts = {}) {
76161
77379
  const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
76162
- if (hasVaultRefs(config) && !existsSync68(vaultPath)) {
77380
+ if (hasVaultRefs(config) && !existsSync69(vaultPath)) {
76163
77381
  throw new Error(`Config references vault keys (vault:<name>) but ${vaultPath} is missing. ` + `Run \`switchroom setup\` first to initialise the vault.`);
76164
77382
  }
76165
77383
  const detect = opts.detectComposeV2 ?? detectComposeV2;
@@ -76170,7 +77388,7 @@ function runApplyPreflight(config, opts = {}) {
76170
77388
  detectAndReportLegacyGdriveSlots(vaultPath);
76171
77389
  }
76172
77390
  function detectAndReportLegacyGdriveSlots(vaultPath) {
76173
- if (!existsSync68(vaultPath))
77391
+ if (!existsSync69(vaultPath))
76174
77392
  return;
76175
77393
  const passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
76176
77394
  if (!passphrase)
@@ -76209,18 +77427,18 @@ function detectAndReportLegacyGdriveSlots(vaultPath) {
76209
77427
  `));
76210
77428
  }
76211
77429
  }
76212
- function writeInstallTypeCache(homeDir = homedir36()) {
77430
+ function writeInstallTypeCache(homeDir = homedir38()) {
76213
77431
  const ctx = detectInstallType();
76214
- const dir = join64(homeDir, ".switchroom");
76215
- const out = join64(dir, "install-type.json");
77432
+ const dir = join67(homeDir, ".switchroom");
77433
+ const out = join67(dir, "install-type.json");
76216
77434
  const tmp = `${out}.tmp`;
76217
- mkdirSync36(dir, { recursive: true });
77435
+ mkdirSync38(dir, { recursive: true });
76218
77436
  const payload = {
76219
77437
  install_type: ctx.install_type,
76220
77438
  detected_at: new Date().toISOString(),
76221
77439
  source_paths: ctx.source_paths
76222
77440
  };
76223
- writeFileSync33(tmp, JSON.stringify(payload, null, 2), { mode: 420 });
77441
+ writeFileSync35(tmp, JSON.stringify(payload, null, 2), { mode: 420 });
76224
77442
  renameSync13(tmp, out);
76225
77443
  return out;
76226
77444
  }
@@ -76261,14 +77479,14 @@ Applying switchroom config...
76261
77479
  writeOut(source_default.green(` + ${name}`) + source_default.gray(` (${agentConfig.extends ?? "default"}) \u2014 ${detail}
76262
77480
  `));
76263
77481
  try {
76264
- installUpdatePromptHook(join64(agentsDir, name));
77482
+ installUpdatePromptHook(join67(agentsDir, name));
76265
77483
  } catch (hookErr) {
76266
77484
  writeOut(source_default.gray(` (update-prompt hook install failed for ${name}: ${hookErr.message})
76267
77485
  `));
76268
77486
  }
76269
77487
  try {
76270
77488
  const uid = allocateAgentUid(name);
76271
- alignAgentUid(name, join64(agentsDir, name), uid, {
77489
+ alignAgentUid(name, join67(agentsDir, name), uid, {
76272
77490
  confirm: !options.nonInteractive,
76273
77491
  writeOut
76274
77492
  });
@@ -76305,7 +77523,7 @@ Applying switchroom config...
76305
77523
  for (const name of agentNames) {
76306
77524
  try {
76307
77525
  const uid = allocateAgentUid(name);
76308
- alignAgentUid(name, join64(agentsDir, name), uid, {
77526
+ alignAgentUid(name, join67(agentsDir, name), uid, {
76309
77527
  confirm: !options.nonInteractive,
76310
77528
  writeOut
76311
77529
  });
@@ -76319,7 +77537,7 @@ Applying switchroom config...
76319
77537
  }
76320
77538
  const vaultPathConfigured = config.vault?.path;
76321
77539
  const customVaultPath = vaultPathConfigured ? resolvePath(vaultPathConfigured) : undefined;
76322
- const migrationResult = migrateVaultLayout(homedir36(), {
77540
+ const migrationResult = migrateVaultLayout(homedir38(), {
76323
77541
  customVaultPath
76324
77542
  });
76325
77543
  switch (migrationResult.kind) {
@@ -76345,7 +77563,7 @@ Applying switchroom config...
76345
77563
  writeErr(formatDivergentRecoveryMessage(migrationResult.details));
76346
77564
  process.exit(4);
76347
77565
  }
76348
- const postMigrationInspect = inspectVaultLayout(homedir36());
77566
+ const postMigrationInspect = inspectVaultLayout(homedir38());
76349
77567
  const acceptable = [
76350
77568
  "no-vault",
76351
77569
  "already-migrated",
@@ -76360,7 +77578,7 @@ Applying switchroom config...
76360
77578
  `));
76361
77579
  process.exit(5);
76362
77580
  }
76363
- const vaultDir = resolveVaultBindMountDir(homedir36(), {
77581
+ const vaultDir = resolveVaultBindMountDir(homedir38(), {
76364
77582
  migrationKind: migrationResult.kind,
76365
77583
  customVaultPath
76366
77584
  });
@@ -76390,11 +77608,11 @@ Applying switchroom config...
76390
77608
  imageTag: composeImageTag,
76391
77609
  buildMode: options.buildLocal ? "local" : "pull",
76392
77610
  buildContext: options.buildContext,
76393
- homeDir: homedir36(),
77611
+ homeDir: homedir38(),
76394
77612
  switchroomConfigPath,
76395
77613
  operatorUid
76396
77614
  });
76397
- await mkdir(dirname19(composePath), { recursive: true });
77615
+ await mkdir(dirname21(composePath), { recursive: true });
76398
77616
  await writeFile(composePath, composeContent, {
76399
77617
  encoding: "utf8",
76400
77618
  mode: 384
@@ -76410,7 +77628,7 @@ Wrote `) + composePath + source_default.gray(` (${composeBytes} bytes)
76410
77628
  writeOut(source_default.gray(` (If pull returns 401, login to ghcr.io first: see docs/operators/install.md#ghcr-auth)
76411
77629
  `));
76412
77630
  if (process.geteuid?.() === 0 && operatorUid !== undefined) {
76413
- const restored = restoreOperatorOwnership(homedir36(), operatorUid);
77631
+ const restored = restoreOperatorOwnership(homedir38(), operatorUid);
76414
77632
  if (restored.length > 0) {
76415
77633
  writeOut(source_default.gray(` Restored operator ownership of ${restored.length} ~/.switchroom path(s)
76416
77634
  `));
@@ -76458,18 +77676,18 @@ function copyExampleConfig2(name) {
76458
77676
  throw new Error(`Invalid example name: ${name} (must match /^[a-z0-9_-]+$/)`);
76459
77677
  }
76460
77678
  const dest = resolve42(process.cwd(), "switchroom.yaml");
76461
- if (existsSync68(dest)) {
77679
+ if (existsSync69(dest)) {
76462
77680
  console.error(source_default.yellow("switchroom.yaml already exists \u2014 skipping example copy"));
76463
77681
  return;
76464
77682
  }
76465
77683
  const embedded = EMBEDDED_EXAMPLES[name];
76466
77684
  if (embedded !== undefined) {
76467
- writeFileSync33(dest, embedded, { encoding: "utf8" });
77685
+ writeFileSync35(dest, embedded, { encoding: "utf8" });
76468
77686
  console.log(source_default.green(`Copied ${name}.yaml -> switchroom.yaml`));
76469
77687
  return;
76470
77688
  }
76471
77689
  const exampleFile = resolve42(import.meta.dirname, `../../examples/${name}.yaml`);
76472
- if (!existsSync68(exampleFile)) {
77690
+ if (!existsSync69(exampleFile)) {
76473
77691
  throw new Error(`Example config not found: ${name}.yaml (available: ${Object.keys(EMBEDDED_EXAMPLES).join(", ")})`);
76474
77692
  }
76475
77693
  copyFileSync11(exampleFile, dest);
@@ -76480,8 +77698,8 @@ function findUnwritableAgentDirs(config, opts) {
76480
77698
  const targets = opts.only ? [opts.only] : Object.keys(config.agents ?? {});
76481
77699
  const unwritable = [];
76482
77700
  for (const name of targets) {
76483
- const startSh = join64(agentsDir, name, "start.sh");
76484
- if (!existsSync68(startSh))
77701
+ const startSh = join67(agentsDir, name, "start.sh");
77702
+ if (!existsSync69(startSh))
76485
77703
  continue;
76486
77704
  try {
76487
77705
  accessSync3(startSh, fsConstants6.W_OK);
@@ -76659,9 +77877,9 @@ function runRedactStdin() {
76659
77877
  }
76660
77878
 
76661
77879
  // src/cli/status-ask.ts
76662
- import { readFileSync as readFileSync57, existsSync as existsSync69, readdirSync as readdirSync26 } from "node:fs";
76663
- import { join as join65 } from "node:path";
76664
- import { homedir as homedir37 } from "node:os";
77880
+ import { readFileSync as readFileSync57, existsSync as existsSync70, readdirSync as readdirSync26 } from "node:fs";
77881
+ import { join as join68 } from "node:path";
77882
+ import { homedir as homedir39 } from "node:os";
76665
77883
 
76666
77884
  // src/status-ask/report.ts
76667
77885
  function parseJsonl(content) {
@@ -76982,7 +78200,7 @@ function runReport(opts) {
76982
78200
  function resolveSources(explicitPath) {
76983
78201
  if (explicitPath != null && explicitPath.trim() !== "") {
76984
78202
  const trimmed = explicitPath.trim();
76985
- if (!existsSync69(trimmed)) {
78203
+ if (!existsSync70(trimmed)) {
76986
78204
  process.stderr.write(`status-ask report: ${trimmed}: file not found
76987
78205
  `);
76988
78206
  process.exit(1);
@@ -76996,9 +78214,9 @@ function resolveSources(explicitPath) {
76996
78214
  const config = loadConfig();
76997
78215
  agentsDir = resolveAgentsDir(config);
76998
78216
  } catch {
76999
- agentsDir = join65(homedir37(), ".switchroom", "agents");
78217
+ agentsDir = join68(homedir39(), ".switchroom", "agents");
77000
78218
  }
77001
- if (!existsSync69(agentsDir))
78219
+ if (!existsSync70(agentsDir))
77002
78220
  return [];
77003
78221
  const sources = [];
77004
78222
  let entries;
@@ -77008,8 +78226,8 @@ function resolveSources(explicitPath) {
77008
78226
  return [];
77009
78227
  }
77010
78228
  for (const name of entries) {
77011
- const path8 = join65(agentsDir, name, "runtime-metrics.jsonl");
77012
- if (existsSync69(path8)) {
78229
+ const path8 = join68(agentsDir, name, "runtime-metrics.jsonl");
78230
+ if (existsSync70(path8)) {
77013
78231
  sources.push({ path: path8, agent: name });
77014
78232
  }
77015
78233
  }
@@ -77030,17 +78248,17 @@ function inferAgentFromPath(p) {
77030
78248
 
77031
78249
  // src/cli/agent-config.ts
77032
78250
  init_helpers();
77033
- import { join as join66 } from "node:path";
77034
- import { homedir as homedir38 } from "node:os";
78251
+ import { join as join69 } from "node:path";
78252
+ import { homedir as homedir40 } from "node:os";
77035
78253
  import {
77036
- existsSync as existsSync70,
77037
- mkdirSync as mkdirSync37,
78254
+ existsSync as existsSync71,
78255
+ mkdirSync as mkdirSync39,
77038
78256
  appendFileSync as appendFileSync4,
77039
78257
  readFileSync as readFileSync58
77040
78258
  } from "node:fs";
77041
- var AUDIT_ROOT = join66(homedir38(), ".switchroom", "audit");
78259
+ var AUDIT_ROOT = join69(homedir40(), ".switchroom", "audit");
77042
78260
  function auditPathFor(agent) {
77043
- return join66(AUDIT_ROOT, agent, "agent-config.jsonl");
78261
+ return join69(AUDIT_ROOT, agent, "agent-config.jsonl");
77044
78262
  }
77045
78263
  function appendAudit(agent, cmd, args, exit, opts = {}) {
77046
78264
  const row = {
@@ -77054,8 +78272,8 @@ function appendAudit(agent, cmd, args, exit, opts = {}) {
77054
78272
  const path8 = opts.auditPath ?? auditPathFor(agent);
77055
78273
  const dir = path8.slice(0, path8.lastIndexOf("/"));
77056
78274
  try {
77057
- if (!existsSync70(dir)) {
77058
- mkdirSync37(dir, { recursive: true });
78275
+ if (!existsSync71(dir)) {
78276
+ mkdirSync39(dir, { recursive: true });
77059
78277
  }
77060
78278
  appendFileSync4(path8, JSON.stringify(row) + `
77061
78279
  `, { flag: "a" });
@@ -77066,7 +78284,7 @@ function isContainerContext(env2 = process.env, opts = {}) {
77066
78284
  return true;
77067
78285
  const probe2 = opts.dockerEnvPath ?? "/.dockerenv";
77068
78286
  try {
77069
- if (existsSync70(probe2))
78287
+ if (existsSync71(probe2))
77070
78288
  return true;
77071
78289
  } catch {}
77072
78290
  return false;
@@ -77127,7 +78345,7 @@ function getAgentSlice(config, agent) {
77127
78345
  }
77128
78346
  function readAuditTail(agent, limit, opts = {}) {
77129
78347
  const path8 = opts.auditPath ?? auditPathFor(agent);
77130
- if (!existsSync70(path8))
78348
+ if (!existsSync71(path8))
77131
78349
  return [];
77132
78350
  let raw;
77133
78351
  try {
@@ -77287,9 +78505,9 @@ var import_yaml15 = __toESM(require_dist(), 1);
77287
78505
  init_paths();
77288
78506
  import {
77289
78507
  closeSync as closeSync13,
77290
- existsSync as existsSync71,
78508
+ existsSync as existsSync72,
77291
78509
  fsyncSync as fsyncSync6,
77292
- mkdirSync as mkdirSync38,
78510
+ mkdirSync as mkdirSync40,
77293
78511
  openSync as openSync13,
77294
78512
  readdirSync as readdirSync27,
77295
78513
  readFileSync as readFileSync59,
@@ -77298,34 +78516,34 @@ import {
77298
78516
  unlinkSync as unlinkSync14,
77299
78517
  writeSync as writeSync8
77300
78518
  } from "node:fs";
77301
- import { join as join67, resolve as resolve43 } from "node:path";
78519
+ import { join as join70, resolve as resolve43 } from "node:path";
77302
78520
  var STAGING_SUBDIR = ".staging";
77303
78521
  function overlayPathsFor(agent, opts = {}) {
77304
78522
  const base = opts.root ? resolve43(opts.root, agent) : resolve43(resolveDualPath(`~/.switchroom/agents/${agent}`));
77305
- const scheduleDir = join67(base, "schedule.d");
77306
- const scheduleStagingDir = join67(scheduleDir, STAGING_SUBDIR);
77307
- const skillsDir = join67(base, "skills.d");
77308
- const skillsStagingDir = join67(skillsDir, STAGING_SUBDIR);
78523
+ const scheduleDir = join70(base, "schedule.d");
78524
+ const scheduleStagingDir = join70(scheduleDir, STAGING_SUBDIR);
78525
+ const skillsDir = join70(base, "skills.d");
78526
+ const skillsStagingDir = join70(skillsDir, STAGING_SUBDIR);
77309
78527
  return {
77310
78528
  agentRoot: base,
77311
78529
  scheduleDir,
77312
78530
  scheduleStagingDir,
77313
78531
  skillsDir,
77314
78532
  skillsStagingDir,
77315
- lockPath: join67(base, ".lock"),
78533
+ lockPath: join70(base, ".lock"),
77316
78534
  stagingDir: scheduleStagingDir
77317
78535
  };
77318
78536
  }
77319
78537
  function ensureDirs(paths) {
77320
- mkdirSync38(paths.scheduleDir, { recursive: true });
77321
- mkdirSync38(paths.scheduleStagingDir, { recursive: true });
78538
+ mkdirSync40(paths.scheduleDir, { recursive: true });
78539
+ mkdirSync40(paths.scheduleStagingDir, { recursive: true });
77322
78540
  }
77323
78541
  function ensureSkillsDirs(paths) {
77324
- mkdirSync38(paths.skillsDir, { recursive: true });
77325
- mkdirSync38(paths.skillsStagingDir, { recursive: true });
78542
+ mkdirSync40(paths.skillsDir, { recursive: true });
78543
+ mkdirSync40(paths.skillsStagingDir, { recursive: true });
77326
78544
  }
77327
78545
  function withAgentLock(paths, fn) {
77328
- mkdirSync38(paths.agentRoot, { recursive: true });
78546
+ mkdirSync40(paths.agentRoot, { recursive: true });
77329
78547
  const start = Date.now();
77330
78548
  const TIMEOUT_MS = 5000;
77331
78549
  let fd = null;
@@ -77366,8 +78584,8 @@ function writeOverlayEntry(agent, slug, yamlText, opts = {}) {
77366
78584
  const paths = overlayPathsFor(agent, opts);
77367
78585
  return withAgentLock(paths, () => {
77368
78586
  ensureDirs(paths);
77369
- const stagingPath = join67(paths.scheduleStagingDir, `${slug}.yaml`);
77370
- const finalPath = join67(paths.scheduleDir, `${slug}.yaml`);
78587
+ const stagingPath = join70(paths.scheduleStagingDir, `${slug}.yaml`);
78588
+ const finalPath = join70(paths.scheduleDir, `${slug}.yaml`);
77371
78589
  const fd = openSync13(stagingPath, "w", 384);
77372
78590
  try {
77373
78591
  writeSync8(fd, yamlText);
@@ -77383,8 +78601,8 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
77383
78601
  const paths = overlayPathsFor(agent, opts);
77384
78602
  return withAgentLock(paths, () => {
77385
78603
  ensureSkillsDirs(paths);
77386
- const stagingPath = join67(paths.skillsStagingDir, `${slug}.yaml`);
77387
- const finalPath = join67(paths.skillsDir, `${slug}.yaml`);
78604
+ const stagingPath = join70(paths.skillsStagingDir, `${slug}.yaml`);
78605
+ const finalPath = join70(paths.skillsDir, `${slug}.yaml`);
77388
78606
  const fd = openSync13(stagingPath, "w", 384);
77389
78607
  try {
77390
78608
  writeSync8(fd, yamlText);
@@ -77399,8 +78617,8 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
77399
78617
  function deleteSkillsOverlayEntry(agent, slug, opts = {}) {
77400
78618
  const paths = overlayPathsFor(agent, opts);
77401
78619
  return withAgentLock(paths, () => {
77402
- const finalPath = join67(paths.skillsDir, `${slug}.yaml`);
77403
- if (!existsSync71(finalPath))
78620
+ const finalPath = join70(paths.skillsDir, `${slug}.yaml`);
78621
+ if (!existsSync72(finalPath))
77404
78622
  return false;
77405
78623
  unlinkSync14(finalPath);
77406
78624
  return true;
@@ -77408,13 +78626,13 @@ function deleteSkillsOverlayEntry(agent, slug, opts = {}) {
77408
78626
  }
77409
78627
  function listSkillsOverlayEntries(agent, opts = {}) {
77410
78628
  const paths = overlayPathsFor(agent, opts);
77411
- if (!existsSync71(paths.skillsDir))
78629
+ if (!existsSync72(paths.skillsDir))
77412
78630
  return [];
77413
78631
  const out = [];
77414
78632
  for (const name of readdirSync27(paths.skillsDir)) {
77415
78633
  if (!/\.ya?ml$/i.test(name))
77416
78634
  continue;
77417
- const full = join67(paths.skillsDir, name);
78635
+ const full = join70(paths.skillsDir, name);
77418
78636
  try {
77419
78637
  const raw = readFileSync59(full, "utf-8");
77420
78638
  const slug = name.replace(/\.ya?ml$/i, "");
@@ -77426,8 +78644,8 @@ function listSkillsOverlayEntries(agent, opts = {}) {
77426
78644
  function deleteOverlayEntry(agent, slug, opts = {}) {
77427
78645
  const paths = overlayPathsFor(agent, opts);
77428
78646
  return withAgentLock(paths, () => {
77429
- const finalPath = join67(paths.scheduleDir, `${slug}.yaml`);
77430
- if (!existsSync71(finalPath))
78647
+ const finalPath = join70(paths.scheduleDir, `${slug}.yaml`);
78648
+ if (!existsSync72(finalPath))
77431
78649
  return false;
77432
78650
  unlinkSync14(finalPath);
77433
78651
  return true;
@@ -77435,13 +78653,13 @@ function deleteOverlayEntry(agent, slug, opts = {}) {
77435
78653
  }
77436
78654
  function listOverlayEntries(agent, opts = {}) {
77437
78655
  const paths = overlayPathsFor(agent, opts);
77438
- if (!existsSync71(paths.scheduleDir))
78656
+ if (!existsSync72(paths.scheduleDir))
77439
78657
  return [];
77440
78658
  const out = [];
77441
78659
  for (const name of readdirSync27(paths.scheduleDir)) {
77442
78660
  if (!/\.ya?ml$/i.test(name))
77443
78661
  continue;
77444
- const full = join67(paths.scheduleDir, name);
78662
+ const full = join70(paths.scheduleDir, name);
77445
78663
  try {
77446
78664
  const raw = readFileSync59(full, "utf-8");
77447
78665
  const slug = name.replace(/\.ya?ml$/i, "");
@@ -77586,27 +78804,27 @@ function reconcileAgentCronOnly(agent) {
77586
78804
  // src/cli/agent-config-pending.ts
77587
78805
  import {
77588
78806
  closeSync as closeSync14,
77589
- existsSync as existsSync72,
78807
+ existsSync as existsSync73,
77590
78808
  fsyncSync as fsyncSync7,
77591
- mkdirSync as mkdirSync39,
78809
+ mkdirSync as mkdirSync41,
77592
78810
  openSync as openSync14,
77593
78811
  readdirSync as readdirSync28,
77594
78812
  readFileSync as readFileSync60,
77595
78813
  renameSync as renameSync15,
77596
78814
  unlinkSync as unlinkSync15,
77597
- writeFileSync as writeFileSync34,
78815
+ writeFileSync as writeFileSync36,
77598
78816
  writeSync as writeSync9
77599
78817
  } from "node:fs";
77600
- import { join as join68 } from "node:path";
78818
+ import { join as join71 } from "node:path";
77601
78819
  import { randomBytes as randomBytes14 } from "node:crypto";
77602
78820
  var STAGE_ID_PREFIX = "cap_";
77603
78821
  function pendingDir(agent, opts = {}) {
77604
78822
  const paths = overlayPathsFor(agent, opts);
77605
- return join68(paths.scheduleDir, ".pending");
78823
+ return join71(paths.scheduleDir, ".pending");
77606
78824
  }
77607
78825
  function ensurePendingDir(agent, opts = {}) {
77608
78826
  const dir = pendingDir(agent, opts);
77609
- mkdirSync39(dir, { recursive: true });
78827
+ mkdirSync41(dir, { recursive: true });
77610
78828
  return dir;
77611
78829
  }
77612
78830
  function newStageId() {
@@ -77615,8 +78833,8 @@ function newStageId() {
77615
78833
  function stagePendingScheduleEntry(opts) {
77616
78834
  const dir = ensurePendingDir(opts.agent, { root: opts.root });
77617
78835
  const stageId = opts.stageId ?? newStageId();
77618
- const yamlPath = join68(dir, `${stageId}.yaml`);
77619
- const metaPath = join68(dir, `${stageId}.meta.json`);
78836
+ const yamlPath = join71(dir, `${stageId}.yaml`);
78837
+ const metaPath = join71(dir, `${stageId}.meta.json`);
77620
78838
  const meta = {
77621
78839
  v: 1,
77622
78840
  stage_id: stageId,
@@ -77637,22 +78855,22 @@ function stagePendingScheduleEntry(opts) {
77637
78855
  }
77638
78856
  renameSync15(yamlTmp, yamlPath);
77639
78857
  }
77640
- writeFileSync34(metaPath, JSON.stringify(meta, null, 2) + `
78858
+ writeFileSync36(metaPath, JSON.stringify(meta, null, 2) + `
77641
78859
  `, { mode: 384 });
77642
78860
  return { stageId, yamlPath, metaPath };
77643
78861
  }
77644
78862
  function listPendingScheduleEntries(agent, opts = {}) {
77645
78863
  const dir = pendingDir(agent, opts);
77646
- if (!existsSync72(dir))
78864
+ if (!existsSync73(dir))
77647
78865
  return [];
77648
78866
  const out = [];
77649
78867
  for (const name of readdirSync28(dir).sort()) {
77650
78868
  if (!name.endsWith(".meta.json"))
77651
78869
  continue;
77652
78870
  const stageId = name.slice(0, -".meta.json".length);
77653
- const metaPath = join68(dir, name);
77654
- const yamlPath = join68(dir, `${stageId}.yaml`);
77655
- if (!existsSync72(yamlPath))
78871
+ const metaPath = join71(dir, name);
78872
+ const yamlPath = join71(dir, `${stageId}.yaml`);
78873
+ if (!existsSync73(yamlPath))
77656
78874
  continue;
77657
78875
  try {
77658
78876
  const meta = JSON.parse(readFileSync60(metaPath, "utf-8"));
@@ -77670,8 +78888,8 @@ function commitPendingScheduleEntry(opts) {
77670
78888
  return { committed: false, reason: "not_found" };
77671
78889
  const slug = match.meta.entry.name ?? match.stageId;
77672
78890
  const paths = overlayPathsFor(opts.agent, { root: opts.root });
77673
- const finalPath = join68(paths.scheduleDir, `${slug}.yaml`);
77674
- if (existsSync72(finalPath)) {
78891
+ const finalPath = join71(paths.scheduleDir, `${slug}.yaml`);
78892
+ if (existsSync73(finalPath)) {
77675
78893
  return { committed: false, reason: "slug_collision" };
77676
78894
  }
77677
78895
  renameSync15(match.yamlPath, finalPath);
@@ -77694,7 +78912,7 @@ function denyPendingScheduleEntry(opts) {
77694
78912
 
77695
78913
  // src/cli/agent-config-write.ts
77696
78914
  init_protocol3();
77697
- import { existsSync as existsSync73, readFileSync as readFileSync61 } from "node:fs";
78915
+ import { existsSync as existsSync74, readFileSync as readFileSync61 } from "node:fs";
77698
78916
  var MAX_ENTRIES_PER_AGENT = 20;
77699
78917
  var MIN_CRON_INTERVAL_MIN = 5;
77700
78918
  function extractCronSmallestGapMin(expr) {
@@ -77980,7 +79198,7 @@ function scheduleRemove(opts) {
77980
79198
  }
77981
79199
  let priorContent = null;
77982
79200
  try {
77983
- if (existsSync73(match.path))
79201
+ if (existsSync74(match.path))
77984
79202
  priorContent = readFileSync61(match.path, "utf-8");
77985
79203
  } catch {}
77986
79204
  deleteOverlayEntry(agent, match.slug, { root: opts.root });
@@ -78173,10 +79391,10 @@ function registerAgentConfigWriteCommands(program3) {
78173
79391
 
78174
79392
  // src/cli/agent-config-skill-write.ts
78175
79393
  var import_yaml16 = __toESM(require_dist(), 1);
78176
- import { existsSync as existsSync74 } from "node:fs";
79394
+ import { existsSync as existsSync75 } from "node:fs";
78177
79395
  init_reconcile_default_skills();
78178
79396
  var import_yaml17 = __toESM(require_dist(), 1);
78179
- import { join as join69 } from "node:path";
79397
+ import { join as join72 } from "node:path";
78180
79398
  var MAX_SKILLS_PER_AGENT = 20;
78181
79399
  var V1_ALLOWED_SOURCE_PREFIX = "bundled:";
78182
79400
  function exitCodeFor2(code) {
@@ -78251,8 +79469,8 @@ function skillInstall(opts) {
78251
79469
  return err("E_SKILL_QUOTA_EXCEEDED", `agent ${agent} already has ${used} overlay-installed skills (cap ${MAX_SKILLS_PER_AGENT})`);
78252
79470
  }
78253
79471
  const poolDir = opts.bundledSkillsPoolDir ?? getBundledSkillsPoolDir();
78254
- const skillPath = join69(poolDir, skillName);
78255
- if (!existsSync74(skillPath)) {
79472
+ const skillPath = join72(poolDir, skillName);
79473
+ if (!existsSync75(skillPath)) {
78256
79474
  return err("E_SKILL_NOT_FOUND", `bundled skill not found at ${skillPath}. The operator needs to ` + `place the skill at this path before the agent can opt in.`);
78257
79475
  }
78258
79476
  const yamlText = import_yaml16.stringify({ skills: [skillName] });
@@ -78416,9 +79634,9 @@ function registerAgentConfigSkillWriteCommands(program3) {
78416
79634
  // src/cli/skill.ts
78417
79635
  import {
78418
79636
  closeSync as closeSync15,
78419
- existsSync as existsSync75,
79637
+ existsSync as existsSync76,
78420
79638
  lstatSync as lstatSync8,
78421
- mkdirSync as mkdirSync40,
79639
+ mkdirSync as mkdirSync42,
78422
79640
  mkdtempSync as mkdtempSync5,
78423
79641
  openSync as openSync15,
78424
79642
  readFileSync as readFileSync62,
@@ -78427,10 +79645,10 @@ import {
78427
79645
  renameSync as renameSync16,
78428
79646
  rmSync as rmSync16,
78429
79647
  statSync as statSync29,
78430
- writeFileSync as writeFileSync35
79648
+ writeFileSync as writeFileSync37
78431
79649
  } from "node:fs";
78432
- import { tmpdir as tmpdir4, homedir as homedir39 } from "node:os";
78433
- import { dirname as dirname20, join as join70, relative as relative2, resolve as resolve44 } from "node:path";
79650
+ import { tmpdir as tmpdir4, homedir as homedir41 } from "node:os";
79651
+ import { dirname as dirname22, join as join73, relative as relative2, resolve as resolve44 } from "node:path";
78434
79652
  import { spawnSync as spawnSync10 } from "node:child_process";
78435
79653
 
78436
79654
  // src/cli/skill-common.ts
@@ -78624,10 +79842,10 @@ function scanForClaudeP2(content) {
78624
79842
  function resolveSkillsPoolDir2(override) {
78625
79843
  const raw = override ?? "~/.switchroom/skills";
78626
79844
  if (raw.startsWith("~/")) {
78627
- return join70(homedir39(), raw.slice(2));
79845
+ return join73(homedir41(), raw.slice(2));
78628
79846
  }
78629
79847
  if (raw === "~")
78630
- return homedir39();
79848
+ return homedir41();
78631
79849
  return resolve44(raw);
78632
79850
  }
78633
79851
  function readStdinSync() {
@@ -78663,7 +79881,7 @@ function loadFromDir(dir) {
78663
79881
  const walk2 = (sub) => {
78664
79882
  const entries = readdirSync29(sub, { withFileTypes: true });
78665
79883
  for (const ent of entries) {
78666
- const full = join70(sub, ent.name);
79884
+ const full = join73(sub, ent.name);
78667
79885
  const rel = relative2(abs, full);
78668
79886
  if (ent.isSymbolicLink()) {
78669
79887
  fail2(`refusing to read symlink inside --from dir: ${rel}`);
@@ -78698,7 +79916,7 @@ function loadFromTarball(tarPath) {
78698
79916
  fail2(`tarball contains disallowed path: ${JSON.stringify(entry)} \u2014 ` + `refusing to extract before any file is written`);
78699
79917
  }
78700
79918
  }
78701
- const staging = mkdtempSync5(join70(tmpdir4(), "skill-apply-extract-"));
79919
+ const staging = mkdtempSync5(join73(tmpdir4(), "skill-apply-extract-"));
78702
79920
  try {
78703
79921
  const flags = isGz ? ["-xzf"] : ["-xf"];
78704
79922
  const r = spawnSync10("tar", [
@@ -78784,10 +80002,10 @@ function validatePayload(name, files) {
78784
80002
  errors2.push(`${path8} fails \`bash -n\` syntax check: ${(r.stderr ?? "").trim()}`);
78785
80003
  }
78786
80004
  } else if (PY_SCRIPT_RE2.test(path8)) {
78787
- const tmp = mkdtempSync5(join70(tmpdir4(), "skill-apply-py-"));
78788
- const tmpPy = join70(tmp, "check.py");
80005
+ const tmp = mkdtempSync5(join73(tmpdir4(), "skill-apply-py-"));
80006
+ const tmpPy = join73(tmp, "check.py");
78789
80007
  try {
78790
- writeFileSync35(tmpPy, content);
80008
+ writeFileSync37(tmpPy, content);
78791
80009
  const r = spawnSync10("python3", ["-m", "py_compile", tmpPy], {
78792
80010
  encoding: "utf-8"
78793
80011
  });
@@ -78805,10 +80023,10 @@ function validatePayload(name, files) {
78805
80023
  function diffSummary(currentDir, files) {
78806
80024
  const lines = [];
78807
80025
  const currentFiles = {};
78808
- if (existsSync75(currentDir)) {
80026
+ if (existsSync76(currentDir)) {
78809
80027
  const walk2 = (sub) => {
78810
80028
  for (const ent of readdirSync29(sub, { withFileTypes: true })) {
78811
- const full = join70(sub, ent.name);
80029
+ const full = join73(sub, ent.name);
78812
80030
  const rel = relative2(currentDir, full);
78813
80031
  if (ent.isDirectory()) {
78814
80032
  walk2(full);
@@ -78841,10 +80059,10 @@ function diffSummary(currentDir, files) {
78841
80059
  `);
78842
80060
  }
78843
80061
  function writePayload(poolDir, name, files) {
78844
- if (!existsSync75(poolDir)) {
78845
- mkdirSync40(poolDir, { recursive: true, mode: 493 });
80062
+ if (!existsSync76(poolDir)) {
80063
+ mkdirSync42(poolDir, { recursive: true, mode: 493 });
78846
80064
  }
78847
- const target = join70(poolDir, name);
80065
+ const target = join73(poolDir, name);
78848
80066
  let targetIsSymlink = false;
78849
80067
  try {
78850
80068
  const st = lstatSync8(target);
@@ -78855,15 +80073,15 @@ function writePayload(poolDir, name, files) {
78855
80073
  if (targetIsSymlink) {
78856
80074
  fail2(`refusing to overwrite symlink at ${target}; investigate manually`);
78857
80075
  }
78858
- const staging = mkdtempSync5(join70(poolDir, `.skill-apply-stage-${name}-`));
80076
+ const staging = mkdtempSync5(join73(poolDir, `.skill-apply-stage-${name}-`));
78859
80077
  let oldRename = null;
78860
80078
  try {
78861
80079
  for (const [path8, content] of Object.entries(files)) {
78862
- const full = join70(staging, path8);
78863
- mkdirSync40(dirname20(full), { recursive: true, mode: 493 });
80080
+ const full = join73(staging, path8);
80081
+ mkdirSync42(dirname22(full), { recursive: true, mode: 493 });
78864
80082
  const fd = openSync15(full, "wx");
78865
80083
  try {
78866
- writeFileSync35(fd, content);
80084
+ writeFileSync37(fd, content);
78867
80085
  } finally {
78868
80086
  closeSync15(fd);
78869
80087
  }
@@ -78890,9 +80108,9 @@ function writePayload(poolDir, name, files) {
78890
80108
  try {
78891
80109
  rmSync16(staging, { recursive: true, force: true });
78892
80110
  } catch {}
78893
- if (oldRename && existsSync75(oldRename)) {
80111
+ if (oldRename && existsSync76(oldRename)) {
78894
80112
  try {
78895
- if (existsSync75(target)) {
80113
+ if (existsSync76(target)) {
78896
80114
  rmSync16(target, { recursive: true, force: true });
78897
80115
  }
78898
80116
  renameSync16(oldRename, target);
@@ -78913,7 +80131,7 @@ function registerSkillCommand(program3) {
78913
80131
  files = loadFromStdin();
78914
80132
  } else {
78915
80133
  const fromPath = resolve44(opts.from);
78916
- if (!existsSync75(fromPath)) {
80134
+ if (!existsSync76(fromPath)) {
78917
80135
  fail2(`--from path does not exist: ${opts.from}`);
78918
80136
  }
78919
80137
  const st = statSync29(fromPath);
@@ -78937,7 +80155,7 @@ function registerSkillCommand(program3) {
78937
80155
  }
78938
80156
  const config = loadConfig();
78939
80157
  const poolDir = resolveSkillsPoolDir2(config.switchroom?.skills_dir);
78940
- const currentDir = join70(poolDir, name);
80158
+ const currentDir = join73(poolDir, name);
78941
80159
  console.log(source_default.bold(`Skill: ${name}`) + source_default.gray(` (${Object.keys(files).length} files, ${sumBytes(files)} bytes)`));
78942
80160
  console.log(source_default.bold("Diff vs current pool content:"));
78943
80161
  console.log(diffSummary(currentDir, files));
@@ -78968,9 +80186,9 @@ function sumBytes(files) {
78968
80186
  // src/cli/skill-personal.ts
78969
80187
  import {
78970
80188
  closeSync as closeSync16,
78971
- existsSync as existsSync76,
80189
+ existsSync as existsSync77,
78972
80190
  lstatSync as lstatSync9,
78973
- mkdirSync as mkdirSync41,
80191
+ mkdirSync as mkdirSync43,
78974
80192
  mkdtempSync as mkdtempSync6,
78975
80193
  openSync as openSync16,
78976
80194
  readFileSync as readFileSync63,
@@ -78979,10 +80197,10 @@ import {
78979
80197
  rmSync as rmSync17,
78980
80198
  statSync as statSync30,
78981
80199
  utimesSync,
78982
- writeFileSync as writeFileSync36
80200
+ writeFileSync as writeFileSync38
78983
80201
  } from "node:fs";
78984
- import { dirname as dirname21, join as join71, relative as relative3, resolve as resolve45 } from "node:path";
78985
- import { homedir as homedir40, tmpdir as tmpdir5 } from "node:os";
80202
+ import { dirname as dirname23, join as join74, relative as relative3, resolve as resolve45 } from "node:path";
80203
+ import { homedir as homedir42, tmpdir as tmpdir5 } from "node:os";
78986
80204
  import { spawnSync as spawnSync11 } from "node:child_process";
78987
80205
  init_helpers();
78988
80206
  init_source();
@@ -78992,15 +80210,15 @@ var TRASH_TTL_MS = 24 * 60 * 60 * 1000;
78992
80210
  var PERSONAL_SKILLS_SUBPATH = "personal-skills";
78993
80211
  function resolveConfigSkillsDir(agent) {
78994
80212
  const override = process.env.SWITCHROOM_CONFIG_DIR;
78995
- const candidate = override ? resolve45(override) : join71(homedir40(), ".switchroom-config");
78996
- if (!existsSync76(candidate))
80213
+ const candidate = override ? resolve45(override) : join74(homedir42(), ".switchroom-config");
80214
+ if (!existsSync77(candidate))
78997
80215
  return null;
78998
- return join71(candidate, "agents", agent, PERSONAL_SKILLS_SUBPATH);
80216
+ return join74(candidate, "agents", agent, PERSONAL_SKILLS_SUBPATH);
78999
80217
  }
79000
80218
  var MIRROR_PRIOR_TTL_MS = 24 * 60 * 60 * 1000;
79001
80219
  function sweepMirrorPriors(configSkillsRoot) {
79002
80220
  try {
79003
- if (!existsSync76(configSkillsRoot))
80221
+ if (!existsSync77(configSkillsRoot))
79004
80222
  return;
79005
80223
  const now = Date.now();
79006
80224
  for (const ent of readdirSync30(configSkillsRoot)) {
@@ -79013,7 +80231,7 @@ function sweepMirrorPriors(configSkillsRoot) {
79013
80231
  if (now - ts < MIRROR_PRIOR_TTL_MS)
79014
80232
  continue;
79015
80233
  try {
79016
- rmSync17(join71(configSkillsRoot, ent), { recursive: true, force: true });
80234
+ rmSync17(join74(configSkillsRoot, ent), { recursive: true, force: true });
79017
80235
  } catch {}
79018
80236
  }
79019
80237
  } catch {}
@@ -79022,7 +80240,7 @@ function mirrorToConfigRepo(agent, name, liveSkillDir) {
79022
80240
  const configSkillsRoot = resolveConfigSkillsDir(agent);
79023
80241
  if (!configSkillsRoot)
79024
80242
  return;
79025
- const dest = join71(configSkillsRoot, name);
80243
+ const dest = join74(configSkillsRoot, name);
79026
80244
  try {
79027
80245
  if (liveSkillDir !== null) {
79028
80246
  try {
@@ -79036,32 +80254,32 @@ function mirrorToConfigRepo(agent, name, liveSkillDir) {
79036
80254
  }
79037
80255
  if (liveSkillDir === null) {
79038
80256
  sweepMirrorPriors(configSkillsRoot);
79039
- if (existsSync76(dest)) {
79040
- const trash = join71(configSkillsRoot, `.${name}-trash-${Date.now()}`);
80257
+ if (existsSync77(dest)) {
80258
+ const trash = join74(configSkillsRoot, `.${name}-trash-${Date.now()}`);
79041
80259
  renameSync17(dest, trash);
79042
80260
  }
79043
80261
  return;
79044
80262
  }
79045
- mkdirSync41(configSkillsRoot, { recursive: true, mode: 493 });
80263
+ mkdirSync43(configSkillsRoot, { recursive: true, mode: 493 });
79046
80264
  sweepMirrorPriors(configSkillsRoot);
79047
- const staging = mkdtempSync6(join71(configSkillsRoot, `.${name}-staging-`));
80265
+ const staging = mkdtempSync6(join74(configSkillsRoot, `.${name}-staging-`));
79048
80266
  const walk2 = (src, dst) => {
79049
- mkdirSync41(dst, { recursive: true, mode: 493 });
80267
+ mkdirSync43(dst, { recursive: true, mode: 493 });
79050
80268
  for (const ent of readdirSync30(src, { withFileTypes: true })) {
79051
- const s = join71(src, ent.name);
79052
- const d = join71(dst, ent.name);
80269
+ const s = join74(src, ent.name);
80270
+ const d = join74(dst, ent.name);
79053
80271
  if (ent.isSymbolicLink())
79054
80272
  continue;
79055
80273
  if (ent.isDirectory())
79056
80274
  walk2(s, d);
79057
80275
  else if (ent.isFile()) {
79058
- writeFileSync36(d, readFileSync63(s));
80276
+ writeFileSync38(d, readFileSync63(s));
79059
80277
  }
79060
80278
  }
79061
80279
  };
79062
80280
  walk2(liveSkillDir, staging);
79063
- if (existsSync76(dest)) {
79064
- const prior = join71(configSkillsRoot, `.${name}-prior-${Date.now()}`);
80281
+ if (existsSync77(dest)) {
80282
+ const prior = join74(configSkillsRoot, `.${name}-prior-${Date.now()}`);
79065
80283
  renameSync17(dest, prior);
79066
80284
  }
79067
80285
  renameSync17(staging, dest);
@@ -79088,13 +80306,13 @@ function resolveAgent(opts) {
79088
80306
  function resolveAgentsRoot(opts) {
79089
80307
  if (opts.root)
79090
80308
  return resolve45(opts.root);
79091
- return join71(homedir40(), ".switchroom", "agents");
80309
+ return join74(homedir42(), ".switchroom", "agents");
79092
80310
  }
79093
80311
  function personalSkillDir(agentsRoot, agent, name) {
79094
- return join71(agentsRoot, agent, ".claude", "skills", PERSONAL_PREFIX + name);
80312
+ return join74(agentsRoot, agent, ".claude", "skills", PERSONAL_PREFIX + name);
79095
80313
  }
79096
80314
  function trashDir(agentsRoot, agent) {
79097
- return join71(agentsRoot, agent, ".claude", TRASH_DIRNAME);
80315
+ return join74(agentsRoot, agent, ".claude", TRASH_DIRNAME);
79098
80316
  }
79099
80317
  function readStdinSync2() {
79100
80318
  const chunks = [];
@@ -79124,7 +80342,7 @@ function loadFromDir2(dir) {
79124
80342
  const files = {};
79125
80343
  const walk2 = (sub) => {
79126
80344
  for (const ent of readdirSync30(sub, { withFileTypes: true })) {
79127
- const full = join71(sub, ent.name);
80345
+ const full = join74(sub, ent.name);
79128
80346
  if (ent.isSymbolicLink()) {
79129
80347
  fail3(`refusing to read symlink in --from dir: ${relative3(abs, full)}`);
79130
80348
  }
@@ -79177,10 +80395,10 @@ function behavioralValidate(files) {
79177
80395
  errors2.push(`${path8} fails \`bash -n\`: ${(r.stderr ?? "").trim()}`);
79178
80396
  }
79179
80397
  } else if (PY_SCRIPT_RE.test(path8)) {
79180
- const tmp = mkdtempSync6(join71(tmpdir5(), "skill-personal-py-"));
79181
- const tmpPy = join71(tmp, "check.py");
80398
+ const tmp = mkdtempSync6(join74(tmpdir5(), "skill-personal-py-"));
80399
+ const tmpPy = join74(tmp, "check.py");
79182
80400
  try {
79183
- writeFileSync36(tmpPy, content);
80401
+ writeFileSync38(tmpPy, content);
79184
80402
  const r = spawnSync11("python3", ["-m", "py_compile", tmpPy], {
79185
80403
  encoding: "utf-8"
79186
80404
  });
@@ -79196,13 +80414,13 @@ function behavioralValidate(files) {
79196
80414
  }
79197
80415
  function sweepTrash(agentsRoot, agent) {
79198
80416
  const trash = trashDir(agentsRoot, agent);
79199
- if (!existsSync76(trash))
80417
+ if (!existsSync77(trash))
79200
80418
  return;
79201
80419
  const now = Date.now();
79202
80420
  for (const ent of readdirSync30(trash, { withFileTypes: true })) {
79203
80421
  if (!ent.isDirectory())
79204
80422
  continue;
79205
- const entPath = join71(trash, ent.name);
80423
+ const entPath = join74(trash, ent.name);
79206
80424
  try {
79207
80425
  const st = statSync30(entPath);
79208
80426
  if (now - st.mtimeMs > TRASH_TTL_MS) {
@@ -79222,16 +80440,16 @@ function writePersonalSkill(targetDir, files) {
79222
80440
  if (targetIsSymlink) {
79223
80441
  fail3(`refusing to overwrite symlink at ${targetDir}; investigate manually`);
79224
80442
  }
79225
- mkdirSync41(dirname21(targetDir), { recursive: true, mode: 493 });
79226
- const staging = mkdtempSync6(join71(dirname21(targetDir), `.skill-personal-stage-`));
80443
+ mkdirSync43(dirname23(targetDir), { recursive: true, mode: 493 });
80444
+ const staging = mkdtempSync6(join74(dirname23(targetDir), `.skill-personal-stage-`));
79227
80445
  let oldRename = null;
79228
80446
  try {
79229
80447
  for (const [path8, content] of Object.entries(files)) {
79230
- const full = join71(staging, path8);
79231
- mkdirSync41(dirname21(full), { recursive: true, mode: 493 });
80448
+ const full = join74(staging, path8);
80449
+ mkdirSync43(dirname23(full), { recursive: true, mode: 493 });
79232
80450
  const fd = openSync16(full, "wx");
79233
80451
  try {
79234
- writeFileSync36(fd, content);
80452
+ writeFileSync38(fd, content);
79235
80453
  } finally {
79236
80454
  closeSync16(fd);
79237
80455
  }
@@ -79258,9 +80476,9 @@ function writePersonalSkill(targetDir, files) {
79258
80476
  try {
79259
80477
  rmSync17(staging, { recursive: true, force: true });
79260
80478
  } catch {}
79261
- if (oldRename && existsSync76(oldRename)) {
80479
+ if (oldRename && existsSync77(oldRename)) {
79262
80480
  try {
79263
- if (existsSync76(targetDir)) {
80481
+ if (existsSync77(targetDir)) {
79264
80482
  rmSync17(targetDir, { recursive: true, force: true });
79265
80483
  }
79266
80484
  renameSync17(oldRename, targetDir);
@@ -79312,7 +80530,7 @@ function loadFiles(opts) {
79312
80530
  return loadFromStdin2();
79313
80531
  }
79314
80532
  const p = resolve45(opts.from);
79315
- if (!existsSync76(p)) {
80533
+ if (!existsSync77(p)) {
79316
80534
  fail3(`--from path does not exist: ${opts.from}`);
79317
80535
  }
79318
80536
  const st = statSync30(p);
@@ -79360,10 +80578,10 @@ function editPersonalAction(name, opts) {
79360
80578
  }
79361
80579
  var CLONE_SOURCE_RE = /^(shared|bundled):([a-z0-9][a-z0-9_-]{0,62})$/;
79362
80580
  function defaultSharedRoot() {
79363
- return join71(homedir40(), ".switchroom", "skills");
80581
+ return join74(homedir42(), ".switchroom", "skills");
79364
80582
  }
79365
80583
  function defaultBundledRoot() {
79366
- return join71(homedir40(), ".switchroom", "skills", "_bundled");
80584
+ return join74(homedir42(), ".switchroom", "skills", "_bundled");
79367
80585
  }
79368
80586
  function resolveCloneSource(source, opts) {
79369
80587
  const m = CLONE_SOURCE_RE.exec(source);
@@ -79373,8 +80591,8 @@ function resolveCloneSource(source, opts) {
79373
80591
  const tier = m[1];
79374
80592
  const slug = m[2];
79375
80593
  const root = tier === "bundled" ? opts.bundledRoot ?? defaultBundledRoot() : opts.sharedRoot ?? defaultSharedRoot();
79376
- const dir = join71(root, slug);
79377
- if (!existsSync76(dir)) {
80594
+ const dir = join74(root, slug);
80595
+ if (!existsSync77(dir)) {
79378
80596
  fail3(`clone source ${JSON.stringify(source)} not found at ${dir}; ` + `check \`switchroom skill search --tier ${tier}\``, 1);
79379
80597
  }
79380
80598
  const st = lstatSync9(dir);
@@ -79389,7 +80607,7 @@ function readSourceFiles(dir) {
79389
80607
  const skipped = [];
79390
80608
  const walk2 = (sub) => {
79391
80609
  for (const ent of readdirSync30(sub, { withFileTypes: true })) {
79392
- const full = join71(sub, ent.name);
80610
+ const full = join74(sub, ent.name);
79393
80611
  if (ent.isSymbolicLink()) {
79394
80612
  continue;
79395
80613
  }
@@ -79498,9 +80716,9 @@ function removePersonalAction(name, opts) {
79498
80716
  throw err2;
79499
80717
  }
79500
80718
  const trashRoot = trashDir(agentsRoot, agent);
79501
- mkdirSync41(trashRoot, { recursive: true, mode: 493 });
80719
+ mkdirSync43(trashRoot, { recursive: true, mode: 493 });
79502
80720
  const ts = Date.now();
79503
- const trashTarget = join71(trashRoot, `${name}-${ts}`);
80721
+ const trashTarget = join74(trashRoot, `${name}-${ts}`);
79504
80722
  renameSync17(target, trashTarget);
79505
80723
  const now = new Date(ts);
79506
80724
  utimesSync(trashTarget, now, now);
@@ -79519,16 +80737,16 @@ function listPersonalAction(opts) {
79519
80737
  const agent = resolveAgent(opts);
79520
80738
  const agentsRoot = resolveAgentsRoot(opts);
79521
80739
  sweepTrash(agentsRoot, agent);
79522
- const skillsDir = join71(agentsRoot, agent, ".claude", "skills");
80740
+ const skillsDir = join74(agentsRoot, agent, ".claude", "skills");
79523
80741
  const personal = [];
79524
- if (existsSync76(skillsDir)) {
80742
+ if (existsSync77(skillsDir)) {
79525
80743
  for (const ent of readdirSync30(skillsDir, { withFileTypes: true })) {
79526
80744
  if (!ent.isDirectory())
79527
80745
  continue;
79528
80746
  if (!ent.name.startsWith(PERSONAL_PREFIX))
79529
80747
  continue;
79530
80748
  const skillName = ent.name.slice(PERSONAL_PREFIX.length);
79531
- const skillPath = join71(skillsDir, ent.name);
80749
+ const skillPath = join74(skillsDir, ent.name);
79532
80750
  let fileCount = 0;
79533
80751
  let totalBytes = 0;
79534
80752
  const walk2 = (sub) => {
@@ -79536,10 +80754,10 @@ function listPersonalAction(opts) {
79536
80754
  if (e.isFile()) {
79537
80755
  fileCount += 1;
79538
80756
  try {
79539
- totalBytes += statSync30(join71(sub, e.name)).size;
80757
+ totalBytes += statSync30(join74(sub, e.name)).size;
79540
80758
  } catch {}
79541
80759
  } else if (e.isDirectory()) {
79542
- walk2(join71(sub, e.name));
80760
+ walk2(join74(sub, e.name));
79543
80761
  }
79544
80762
  }
79545
80763
  };
@@ -79578,24 +80796,24 @@ function registerSkillPersonalCommands(program3) {
79578
80796
  // src/cli/skill-search.ts
79579
80797
  init_helpers();
79580
80798
  var import_yaml19 = __toESM(require_dist(), 1);
79581
- import { existsSync as existsSync77, readdirSync as readdirSync31, readFileSync as readFileSync64, statSync as statSync31 } from "node:fs";
79582
- import { homedir as homedir41 } from "node:os";
79583
- import { join as join72, resolve as resolve46 } from "node:path";
80799
+ import { existsSync as existsSync78, readdirSync as readdirSync31, readFileSync as readFileSync64, statSync as statSync31 } from "node:fs";
80800
+ import { homedir as homedir43 } from "node:os";
80801
+ import { join as join75, resolve as resolve46 } from "node:path";
79584
80802
  var PERSONAL_PREFIX2 = "personal-";
79585
80803
  var BUNDLED_SUBDIR = "_bundled";
79586
80804
  var AGENT_NAME_RE3 = /^[a-z][a-z0-9_-]{0,62}$/;
79587
80805
  function defaultAgentsRoot() {
79588
- return resolve46(homedir41(), ".switchroom/agents");
80806
+ return resolve46(homedir43(), ".switchroom/agents");
79589
80807
  }
79590
80808
  function defaultSharedRoot2() {
79591
- return resolve46(homedir41(), ".switchroom/skills");
80809
+ return resolve46(homedir43(), ".switchroom/skills");
79592
80810
  }
79593
80811
  function defaultBundledRoot2() {
79594
- return resolve46(homedir41(), ".switchroom/skills/_bundled");
80812
+ return resolve46(homedir43(), ".switchroom/skills/_bundled");
79595
80813
  }
79596
80814
  function readSkillFrontmatter(skillDir) {
79597
- const mdPath = join72(skillDir, "SKILL.md");
79598
- if (!existsSync77(mdPath))
80815
+ const mdPath = join75(skillDir, "SKILL.md");
80816
+ if (!existsSync78(mdPath))
79599
80817
  return null;
79600
80818
  let content;
79601
80819
  try {
@@ -79627,7 +80845,7 @@ function readSkillFrontmatter(skillDir) {
79627
80845
  return { fm: parsed };
79628
80846
  }
79629
80847
  function statSkillMd(skillDir) {
79630
- const mdPath = join72(skillDir, "SKILL.md");
80848
+ const mdPath = join75(skillDir, "SKILL.md");
79631
80849
  try {
79632
80850
  const st = statSync31(mdPath);
79633
80851
  return { size: st.size, mtime: st.mtime.toISOString() };
@@ -79638,8 +80856,8 @@ function statSkillMd(skillDir) {
79638
80856
  function listPersonalSkills(agent, agentsRoot = defaultAgentsRoot()) {
79639
80857
  if (!AGENT_NAME_RE3.test(agent))
79640
80858
  return [];
79641
- const skillsDir = join72(agentsRoot, agent, ".claude/skills");
79642
- if (!existsSync77(skillsDir))
80859
+ const skillsDir = join75(agentsRoot, agent, ".claude/skills");
80860
+ if (!existsSync78(skillsDir))
79643
80861
  return [];
79644
80862
  const out = [];
79645
80863
  let entries;
@@ -79651,7 +80869,7 @@ function listPersonalSkills(agent, agentsRoot = defaultAgentsRoot()) {
79651
80869
  for (const ent of entries) {
79652
80870
  if (!ent.startsWith(PERSONAL_PREFIX2))
79653
80871
  continue;
79654
- const dirPath = join72(skillsDir, ent);
80872
+ const dirPath = join75(skillsDir, ent);
79655
80873
  try {
79656
80874
  if (!statSync31(dirPath).isDirectory())
79657
80875
  continue;
@@ -79677,7 +80895,7 @@ function listPersonalSkills(agent, agentsRoot = defaultAgentsRoot()) {
79677
80895
  return out;
79678
80896
  }
79679
80897
  function listSharedSkills(sharedRoot = defaultSharedRoot2()) {
79680
- if (!existsSync77(sharedRoot))
80898
+ if (!existsSync78(sharedRoot))
79681
80899
  return [];
79682
80900
  const out = [];
79683
80901
  let entries;
@@ -79691,7 +80909,7 @@ function listSharedSkills(sharedRoot = defaultSharedRoot2()) {
79691
80909
  continue;
79692
80910
  if (ent.startsWith("."))
79693
80911
  continue;
79694
- const dirPath = join72(sharedRoot, ent);
80912
+ const dirPath = join75(sharedRoot, ent);
79695
80913
  try {
79696
80914
  if (!statSync31(dirPath).isDirectory())
79697
80915
  continue;
@@ -79715,7 +80933,7 @@ function listSharedSkills(sharedRoot = defaultSharedRoot2()) {
79715
80933
  return out;
79716
80934
  }
79717
80935
  function listBundledSkills(bundledRoot = defaultBundledRoot2()) {
79718
- if (!existsSync77(bundledRoot))
80936
+ if (!existsSync78(bundledRoot))
79719
80937
  return [];
79720
80938
  const out = [];
79721
80939
  let entries;
@@ -79727,7 +80945,7 @@ function listBundledSkills(bundledRoot = defaultBundledRoot2()) {
79727
80945
  for (const ent of entries) {
79728
80946
  if (ent.startsWith("."))
79729
80947
  continue;
79730
- const dirPath = join72(bundledRoot, ent);
80948
+ const dirPath = join75(bundledRoot, ent);
79731
80949
  try {
79732
80950
  if (!statSync31(dirPath).isDirectory())
79733
80951
  continue;
@@ -79871,9 +81089,9 @@ function registerHostdMcpCommand(program3) {
79871
81089
  // src/cli/hostd.ts
79872
81090
  init_source();
79873
81091
  init_helpers();
79874
- import { existsSync as existsSync79, mkdirSync as mkdirSync42, readdirSync as readdirSync32, readFileSync as readFileSync66, writeFileSync as writeFileSync37, statSync as statSync32, copyFileSync as copyFileSync12 } from "node:fs";
79875
- import { homedir as homedir42 } from "node:os";
79876
- import { join as join73 } from "node:path";
81092
+ import { existsSync as existsSync80, mkdirSync as mkdirSync44, readdirSync as readdirSync32, readFileSync as readFileSync66, writeFileSync as writeFileSync39, statSync as statSync32, copyFileSync as copyFileSync12 } from "node:fs";
81093
+ import { homedir as homedir44 } from "node:os";
81094
+ import { join as join76 } from "node:path";
79877
81095
  import { spawnSync as spawnSync13 } from "node:child_process";
79878
81096
  init_audit_reader();
79879
81097
  var DEFAULT_IMAGE_TAG = "latest";
@@ -79964,14 +81182,14 @@ networks:
79964
81182
  `;
79965
81183
  }
79966
81184
  function hostdDir() {
79967
- return join73(homedir42(), ".switchroom", "hostd");
81185
+ return join76(homedir44(), ".switchroom", "hostd");
79968
81186
  }
79969
81187
  function hostdComposePath() {
79970
- return join73(hostdDir(), "docker-compose.yml");
81188
+ return join76(hostdDir(), "docker-compose.yml");
79971
81189
  }
79972
81190
  function backupExistingCompose() {
79973
81191
  const p = hostdComposePath();
79974
- if (!existsSync79(p))
81192
+ if (!existsSync80(p))
79975
81193
  return null;
79976
81194
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
79977
81195
  const bak = `${p}.bak-${ts}`;
@@ -80004,9 +81222,9 @@ async function doInstall(opts, program3) {
80004
81222
  }
80005
81223
  const dir = hostdDir();
80006
81224
  const composePath = hostdComposePath();
80007
- mkdirSync42(dir, { recursive: true });
81225
+ mkdirSync44(dir, { recursive: true });
80008
81226
  const yaml = renderHostdComposeFile({
80009
- hostHome: homedir42(),
81227
+ hostHome: homedir44(),
80010
81228
  imageTag: opts.tag ?? DEFAULT_IMAGE_TAG,
80011
81229
  operatorUid: resolveOperatorUid()
80012
81230
  });
@@ -80019,7 +81237,7 @@ async function doInstall(opts, program3) {
80019
81237
  const bak = backupExistingCompose();
80020
81238
  if (bak)
80021
81239
  console.log(source_default.dim(` Backed up existing compose to ${bak}`));
80022
- writeFileSync37(composePath, yaml, "utf8");
81240
+ writeFileSync39(composePath, yaml, "utf8");
80023
81241
  console.log(source_default.green(` \u2713 Wrote ${composePath}`));
80024
81242
  console.log(source_default.dim(` admin agents: ${adminAgents.length === 0 ? "(none)" : adminAgents.join(", ")}`));
80025
81243
  console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-hostd:${opts.tag ?? DEFAULT_IMAGE_TAG}\u2026`));
@@ -80048,7 +81266,7 @@ function doStatus() {
80048
81266
  const composeYml = hostdComposePath();
80049
81267
  console.log(source_default.bold("switchroom-hostd"));
80050
81268
  console.log("");
80051
- if (!existsSync79(composeYml)) {
81269
+ if (!existsSync80(composeYml)) {
80052
81270
  console.log(source_default.yellow(" compose: not installed"));
80053
81271
  console.log(source_default.dim(" run `switchroom hostd install` to set up."));
80054
81272
  return;
@@ -80069,14 +81287,14 @@ function doStatus() {
80069
81287
  } else {
80070
81288
  console.log(source_default.green(` container: ${ps.stdout.trim()}`));
80071
81289
  }
80072
- if (existsSync79(dir)) {
81290
+ if (existsSync80(dir)) {
80073
81291
  const entries = [];
80074
81292
  try {
80075
81293
  for (const name of readdirSync32(dir)) {
80076
81294
  if (name === "docker-compose.yml" || name.startsWith("docker-compose.yml."))
80077
81295
  continue;
80078
- const sockPath = join73(dir, name, "sock");
80079
- if (existsSync79(sockPath)) {
81296
+ const sockPath = join76(dir, name, "sock");
81297
+ if (existsSync80(sockPath)) {
80080
81298
  const st = statSync32(sockPath);
80081
81299
  if ((st.mode & 61440) === 49152) {
80082
81300
  entries.push(`${name} \u2192 ${sockPath}`);
@@ -80095,7 +81313,7 @@ function doStatus() {
80095
81313
  }
80096
81314
  function doUninstall() {
80097
81315
  const composeYml = hostdComposePath();
80098
- if (!existsSync79(composeYml)) {
81316
+ if (!existsSync80(composeYml)) {
80099
81317
  console.log(source_default.yellow(" No hostd install detected (no compose file at this path)."));
80100
81318
  return;
80101
81319
  }
@@ -80119,7 +81337,7 @@ function registerHostdCommand(program3) {
80119
81337
  hostd.command("uninstall").description("Stop the hostd container. Leaves the compose file in place for re-install.").action(() => doUninstall());
80120
81338
  hostd.command("audit").description("Tail and filter the hostd audit log (privileged-verb call history)").option("--tail <n>", "Number of matching entries to show (default: 50)", "50").option("--agent <name>", "Filter to a specific caller agent").option("--op <verb>", "Filter to a specific hostd verb (e.g. update_apply, agent_restart)").option("--error", "Show only failed (error/denied) entries").option("--verbose", "Show the captured stderr / error tail under each failed row").option("--path <file>", "Override audit log path (for debugging)").action((opts) => {
80121
81339
  const logPath = opts.path ?? defaultAuditLogPath2();
80122
- if (!existsSync79(logPath)) {
81340
+ if (!existsSync80(logPath)) {
80123
81341
  console.error(source_default.yellow(`Audit log not found at ${logPath}.`) + source_default.gray(`
80124
81342
  The log is created when hostd handles its first privileged-verb request.`));
80125
81343
  return;
@@ -80196,6 +81414,9 @@ registerDebugCommand(program3);
80196
81414
  registerWorktreeCommand(program3);
80197
81415
  registerDriveCommand(program3);
80198
81416
  registerDriveMcpLauncherCommand(program3);
81417
+ registerM365McpLauncherCommand(program3);
81418
+ registerNotionMcpLauncherCommand(program3);
81419
+ registerNotionCommand(program3);
80199
81420
  registerApplyCommand(program3);
80200
81421
  registerSecretDetectCommand(program3);
80201
81422
  registerStatusAskCommand(program3);