# dsh-feyagate-gateway bundle patch
#
# Installed through the official CLI:
#
#   dsh plugin --profile <name> add @dsh-external/dsh-feyagate-gateway
#
# The command reconciles `dsh.profile.bundles` against installed packages and,
# seeing `dsh.bundle.patch` in package.json, appends this bundle to the stack.
# The profile boot then merges this patch as one more layer — no profile file
# edits, no manual mount line.
#
# ── What gets inserted, and why two rows ─────────────────────────────────────
#
#   1. `feyagate-gateway`      the plugin itself: host half (supervises the
#                              miloco-mcp-server child process, serves the
#                              settings API) + browser half (settings UI).
#   2. `feyagate-gateway-mcp`  the stock MCP bridge, pointed at the plugin's
#                              own loopback facade instead of at the child
#                              process directly.
#
# ── Why the bridge points at the plugin's facade ─────────────────────────────
#
# The obvious design is to point the bridge straight at miloco-mcp-server.
# That breaks in three ways we measured:
#
#   * the child's HTTP port has to be configurable, but this YAML row is static
#     text evaluated at boot;
#   * the bridge's *default* reconnect budget is ~2.5 minutes (500ms doubling
#     to 30s, 10 attempts) and then it unregisters the server's tools for good —
#     so a user who installs the server *after* boot ends up with no tools and a
#     "plugin is broken" impression. This patch raises the budget to 60 attempts
#     (~30 minutes) as extra slack, but that is a buffer, not the fix: the fix is
#     that the facade is up before the bridge's first attempt;
#   * every version swap stops the child for a moment, which is fine, but a
#     long download before the first start is not.
#
# So the plugin runs a tiny always-on loopback MCP facade and the bridge talks
# to that. The facade answers `initialize` immediately, returns an empty tool
# list while the child is absent, and proxies to the real child once healthy —
# including `notifications/tools/list_changed` when it comes up. Consequences:
#
#   * the child's port stays a runtime detail, and changing it needs no restart;
#   * installing the server later does not burn the reconnect budget;
#   * swapping/upgrading the binary is invisible to the model.
#
# The facade port is the only port that must be known before the plugin's
# `apply()` runs, because this row is evaluated before it. The expression below
# reads it from the plugin's own state file (written by `apply()`, which the
# loader has already awaited — entry rows are processed in order), and falls
# back to the default when the file does not exist yet, which is exactly the
# first-boot case. `process.getBuiltinModule` is how a `!!js` expression in an
# ESM host reaches `node:fs` without `require`.
- insert:
    - id: feyagate-gateway
      name: 'dsh-feyagate'
    - id: feyagate-gateway-mcp
      name: '@deepseek-ai/dsh-mcp-client'
      config:
        transport: streamable-http
        serverName: feyagate
        # Multi-line quoted scalar: EVERY line of it, including the closing one,
        # must be indented deeper than `url:` itself. A closing line at the same
        # indent as the key makes the parser end the scalar early and the whole
        # patch file fails to load — taking the profile's boot with it.
        url: !!js "(() => {
          try {
            const fs = process.getBuiltinModule('node:fs');
            const path = process.getBuiltinModule('node:path');
            const os = process.getBuiltinModule('node:os');
            const home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
            const override = process.env.DSH_FEYAGATE_HOME;
            const root = (override && override.trim()) || path.join(home, 'dsh-feyagate');
            const file = path.join(root, 'state.json');
            const state = JSON.parse(fs.readFileSync(file, 'utf8'));
            const port = (state.facade && state.facade.port) || 38081;
            return 'http://127.0.0.1:' + port + '/mcp';
          } catch (error) {
            return 'http://127.0.0.1:38081/mcp';
          }
          })()"
        # Camera/vision tool calls legitimately take minutes (P2P connect +
        # snapshot + VLM round trip), so this is well above the 60s default.
        toolCallTimeoutMs: 180000
        failOnStartupError: false
        reconnect:
          enabled: true
          initialDelayMs: 500
          maxDelayMs: 30000
          maxAttempts: 60
