um-dsh-websearch โ€” Exa + Parallel + DeepSeek Official multi-source web search ยท DeepSeek Harness plugin # ๐ŸŒŠ um-dsh-websearch [![License: MIT](https://img.shields.io/badge/License-MIT-4d9fff?style=flat-square)](./LICENSE) [![DSH](https://img.shields.io/badge/DSH-%3E%3D0.2.0--rc.1%20%3C0.3.0-4d9fff?style=flat-square)](https://github.com/deepseek-ai/deepseek-harness) [![Node](https://img.shields.io/badge/Node-%3E%3D20-2ea44f?style=flat-square)](https://nodejs.org) [![Tests](https://img.shields.io/badge/tests-175%20host%20%2B%2016%20visual-blueviolet?style=flat-square)](./tests) [![Lang](https://img.shields.io/badge/lang-CN%20%7C%20EN-8a6d3b?style=flat-square)](./README.en.md) Ko-fi **A multi-provider web search plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)** โ€”โ€” Exa, Parallel, and DeepSeek Official behind a single provider ๐ŸŒŠ ๐Ÿงญ ordered provider chainใ€€ยทใ€€๐Ÿ”€ tier-gated fallbackใ€€ยทใ€€๐Ÿ—๏ธ multi-key strategiesใ€€ยทใ€€โšก concurrent fan-outใ€€ยทใ€€๐Ÿ’พ optimistic cacheใ€€ยทใ€€๐ŸŽจ bilingual settings card on the official primitives Englishใ€€ยทใ€€[๐Ÿ‡จ๐Ÿ‡ณ ็ฎ€ไฝ“ไธญๆ–‡](./README.md)
--- ## ๐Ÿ“– Table of contents - [โœจ Features](#features) - [๐ŸŒ Supported sources](#sources) - [๐Ÿ“ฆ Requirements](#requirements) - [๐Ÿš€ Quick start](#quickstart) - [โš™๏ธ Configuration](#config) - [๐Ÿ”„ Fallback semantics](#fallback) - [๐Ÿ” Credentials](#credentials) - [๐Ÿฉบ Troubleshooting](#troubleshooting) - [๐Ÿงช Tests](#tests) - [๐Ÿงฉ Development versions](#devversions) - [๐Ÿšš Migration](#migration) - [๐Ÿ›๏ธ Architecture decisions & research](#adr) - [๐Ÿ™ Support](#support) - [๐Ÿ“„ License](#license) ## โœจ Features - **๐Ÿงญ Multi-provider strategy layer** โ€” the ordered `providers[]` list IS the priority: each source enables independently, `defaultProvider` picks where the sequential chain starts, and a server-side rejection degrades to the next provider - **๐Ÿ”€ Paid / free tiers with gated fallback** โ€” every source carries a paid REST tier and a keyless anonymous MCP tier, each with its own switch; a tier fallback requires BOTH the exhausted key's direction switch AND the target tier being on, so it never degrades on its own - **๐Ÿ—๏ธ Multi-key strategies** โ€” each source can hold several credential references, used `ordered` (array order) or `random` (shuffled per search), with per-key `allowFreeToPaid` / `allowPaidToFree` switches - **โšก Concurrent fan-out** โ€” with `concurrency > 1` one search is dispatched across every enabled source (provider ร— tier ร— key) and the successes are merged with URL dedup; `= 1` keeps the sequential fallback chain - **๐Ÿ’พ Optimistic cache** โ€” an in-memory result cache with a 1โ€“86400s TTL and a 64-entry cap, invalidated the moment the configuration fingerprint changes - **๐Ÿค– DeepSeek Official integration** โ€” the umbrella wraps the framework's own `DeepSeekSearchProvider` (API parity, zero protocol drift) and never registers under the official id, so `WEB_DUPLICATE_PROVIDER` cannot fire - **๐Ÿ”’ Credential safety (`UM_WS_` namespace)** โ€” the configuration stores reference names only; key literals stay in the credential store or the environment. References default to the `UM_WS_` prefix, isolated from the official packages, and legacy values are copied once and **never deleted** - **๐ŸŽจ Official-primitives settings card** โ€” the card is rebuilt on DSH's own `SettingsForm` / `SettingsFormModel` / `SettingsValueField` / `SettingsSecretField`: the Plugins page row renders the master switch, default provider, concurrency, cache, and provider list in place, key values are written through the official credentials interface, and key literals never land in the settings document - **๐Ÿ›ก๏ธ Pre-write validation, twice** โ€” a pre-persist waterfall scoped to this plugin's rows on the Host (matched by `this?.runtime`'s `Config` identity plus the plugin name) plus a client-side mirror before save: a rejected write never reaches disk, and a doomed candidate is stopped before it leaves the browser with a bilingual warning โ€” never a partial save ## ๐ŸŒ Supported sources | Source | Paid tier (REST + key) | Free tier (anonymous MCP) | Default credential ref | Shipped | | --- | --- | --- | --- | --- | | **Exa** ๐Ÿฅ‡ | `POST /search`
`https://api.exa.ai` | JSON-RPC direct, tool `web_search_exa`
`https://mcp.exa.ai/mcp` | `UM_WS_EXA_API_KEY` | โœ… enabled ยท default | | **Parallel** | `POST /v1/search`
`https://api.parallel.ai` | JSON-RPC direct, tool `web_search`
`https://search.parallel.ai/mcp` | `UM_WS_PARALLEL_API_KEY` | โธ๏ธ disabled | | **DeepSeek Official** ๐Ÿค– | wraps the official `DeepSeekSearchProvider`
`https://api.deepseek.com/anthropic/v1` | โ€” (forced off) | `UM_WS_DEEPSEEK_API_KEY` | โธ๏ธ disabled | MCP transport speaks protocol version `2025-06-18` (JSON-RPC over streamable HTTP); the Parallel side additionally sends `notifications/initialized` after `initialize`. Snippet caps are twofold: 700 characters on the request side, 800 characters retained locally. REST `numResults` is clamped server-side (Exa 1โ€“10, Parallel 1โ€“20). ## ๐Ÿ“ฆ Requirements | Dependency | Requirement | | --- | --- | | DeepSeek Harness | **โ‰ฅ 0.2.0-rc.1 and < 0.3.0** (verified on Desktop 0.2.1-rc.2; the 0.1.7 family is no longer supported, see [ADR-0007](./.um.agents/constraints/ADR-0007-dsh-020-rc2-single-track.md) / [ADR-0008](./.um.agents/constraints/ADR-0008-dsh-021-rc2-peer-only-closure.md)) | | Node.js | โ‰ฅ 20 | > โš ๏ธ **Why the window is `>=0.2.0-rc.1 <0.3.0`, and why a Desktop upgrade needs a reinstall** > `0.1.7 โ†’ 0.2.0`: a prerelease is only matched by a comparator of the same `[major,minor,patch]` tuple, so the old window resolved the runtime closure to `0.1.7-rc.2` (ADR-0007). > `0.2.0 โ†’ 0.2.1`: 0.7.0 still listed the official packages in `dependencies`, so on the 0.2.1 Desktop the profile closure was pinned to `0.2.0-rc.2`; the new 0.2.1 exact-version preflight judges the **package the profile actually resolves**, disables the whole `web` row โ†’ `ctx.web` is never provided โ†’ `tool-web (@deepseek-ai/dsh-tool-web): waiting for web` (ADR-0008). > Fix: the official packages are now **peer + dev only** (the profile's `autoInstallPeers: false` keeps them out of the dependency closure) and resolve from the DSH installation at runtime, so one package works on both 0.2.0 and 0.2.1 hosts. **Existing Desktop installs must be removed and reinstalled** (hoisted official copies do not disappear by themselves); a reinstall restores the plugin row to factory config, so re-enter your settings once. ## ๐Ÿš€ Quick start ```powershell pnpm exec dsh plugin --profile web add # local checkout pnpm exec dsh plugin --profile web add github:UnforgetMemory/um-dsh-websearch ``` Restart, then turn on `enabled` under **Settings โ†’ Plugins โ†’ UM Web Search** (it ships off). ### ๐Ÿงฐ Installing takes over the default search The package declares `dsh.bundle`, so `dsh plugin add` appends it to `dsh.profile.bundles` and mounts its own patch layer (`cordis.patch.yml`) automatically โ€” no manual profile row: ```yaml - insert: - id: um-web-search name: um-dsh-websearch # Automatically takes over the web row's search backend (live on install, gone on uninstall) - id: web config: searchProvider: um-web-search fetchProvider: http ``` Installing takes over the default search backend and uninstalling restores the deployment's own default (`deepseek-official`). To pin a different backend, write the `web` row explicitly in the profile's `cordis.patch.yml` (later layers win, and you must **restate both keys**) โ€” template: [`cordis.patch.snippet.yml`](./cordis.patch.snippet.yml). > โš ๏ธ An id-targeted patch **replaces** the targeted row's whole `config` (it is not a deep merge), so this entry must restate **every** key the `web` row owns. The base `web` row owns both `searchProvider` and `fetchProvider`; omitting the latter deletes it silently, and `web_fetch` then fails with `WEB_PROVIDER_AMBIGUOUS` as soon as a second fetch provider appears in the profile. > โš ๏ธ **Takeover window** โ€” `enabled` ships `false`, so between the takeover and flipping the switch search is unavailable: the `web` row already points at this plugin, and a disabled plugin answers searches with `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. Enable `enabled` in the settings card right after installing. > ๐Ÿ“Œ **Migration note** โ€” if you previously followed the old README and hand-inserted a row with `id: web-search-exa` into your profile's `cordis.patch.yml`, remove that row first; otherwise the bundle layer mounts alongside it and the plugin loads twice. ### ๐Ÿ–ฅ๏ธ Desktop app The `desktop` profile is managed exclusively by the Electron application. The desktop-bundled dsh command (menu-bar "Manage dsh command", or `resources\runtime\cli\bin\dsh.cmd`) allows only the `plugin` subcommands against the desktop profile โ€” every other command (e.g. `--dump-config`) is refused. Install or update with the same `dsh plugin --profile desktop add โ€ฆ` or through the app's plugin management UI, then restart the app. Note that pnpm 11's default 24-hour release-age gate blocks freshly published `0.2.1-rc.*` versions: the desktop profile's `pnpm-workspace.yaml` needs a `minimumReleaseAgeExclude` whitelist (same list as this repo) to resolve the newest prerelease ([ADR-0008](./.um.agents/constraints/ADR-0008-dsh-021-rc2-peer-only-closure.md)). ## โš™๏ธ Configuration Settings are edited in the **Settings โ†’ Plugins โ†’ UM Web Search** card and persisted into the `config` of the profile patch's `um-web-search` row. The form is derived from the `Config` schema the plugin exports, and the fields that can take effect live are declared volatile (`enabled`, `defaultProvider`, `concurrency`, `cache`, `providers`) โ€” saving needs no plugin remount and applies to the next search. ### ๐ŸŒ Global | Key | Default | Meaning | | --- | --- | --- | | `enabled` | `false` | Plugin master switch | | `defaultProvider` | `exa` | Where the sequential chain starts (must be a provider `id`) | | `concurrency` | `1` | `1` = sequential fallback chain; `2โ€“8` = fan out across every enabled source | | `cache.enabled` | `false` | In-memory result cache switch | | `cache.ttlSeconds` | `60` | How long one cached result stays fresh (1โ€“86400 seconds) | ### ๐Ÿงฑ Providers (`providers[]`, array index = priority) | Key | Meaning | | --- | --- | | `id` | Provider identity (built-in: `exa` / `parallel` / `deepseek`) | | `name` | Display name (used by the settings card list and role badges) | | `enabled` | Enable this provider | | `primaryTier` | Tier tried first: `free` (anonymous MCP) or `paid` (REST + key) | | `paid.enabled` / `paid.baseURL` | Paid-tier switch and REST endpoint | | `free.enabled` / `free.baseURL` | Free-tier switch and anonymous MCP endpoint (forced off for deepseek) | | `keys[].ref` | Credential reference, auto-generated as `UM_WS__API_KEY`; appended keys take `_1`, `_2`โ€ฆ | | `keys[].enabled` | Enable this key | | `keys[].allowFreeToPaid` / `.allowPaidToFree` | This key's two-way cross-tier gate switches | | `keysStrategy` | `ordered` (array order) / `random` (shuffled per search) | | `numResults` | Default result count 1โ€“20 (Exa / DeepSeek default 5, Parallel default 10) | | `params` | Exa `searchType` (`auto` / `neural` / `keyword`); Parallel `mode` (`turbo` / `fast` / `basic` / `advanced`); DeepSeek `model` (default `deepseek-v4-flash`) and `maxUses` (default 5) | > ๐Ÿ“Œ **Writing the array by hand** โ€” array elements cannot be declared volatile (a schemastery constraint), so `providers` is volatile as a **whole array** and `cache` as a **whole object**; the settings service only writes them wholesale. Write the entire array when editing the provider list in a profile patch. ### ๐ŸŽจ The settings card The Plugins page row first shows a one-line **effective strategy summary** (enabled sources in order โ†’ concurrency, or an "unavailable" note when everything is off). Expanding it renders the official `SettingsForm`: - **Master switch, default provider, concurrency, cache** โ€” edited right in the row - **Provider list** โ€” each row carries a role badge (`Default` / `Fallback` / `Off`), โ†‘โ†“ ordering, an enable switch, and "Configureโ€ฆ" - **Provider details** โ€” a modal that edits one provider's tiers, endpoints, keys, and parameters, with each key showing a **configured / not configured** badge ## ๐Ÿ”„ Fallback semantics ### Sequential mode (`concurrency = 1`) ```text primary tier โ”€โ”€โ–ถ same-tier keys (per keysStrategy) โ”‚ โ”œโ”€ gated cross-tier โ”€โ”€โ–ถ the other tier's keys โ”‚ โ–ฒ โ”‚ โ””โ”€ requires BOTH "this key's direction switch โˆง target tier on" โ”‚ โ””โ”€ this provider fails terminally โ”€โ”€โ–ถ the next provider ``` ### Concurrent mode (`concurrency > 1`) Every enabled source (provider ร— tier ร— key) runs in parallel, one source's failure does not affect the others, and the successes are merged with URL dedup. There is no "fallback" here โ€” whether the free tier participates is its own switch's business. ### What triggers a fallback | โœ… Triggers a fallback | โŒ Never falls back | | --- | --- | | 401 / 402 / 403 / 429 / 5xx (server-side rejection) | Aborts (`WEB_ABORTED`) | | | Missing credentials (`CREDENTIAL_MISSING`) | | | Client 4xx (including 422) | | | Contract errors (the response breaks the API shape) | | | Network failures | The DeepSeek Official tier exposes no HTTP status, so it uses a three-way classification over the official class's message shape: a message containing `(HTTP NNN)` is judged by the table above; known non-degradable shapes (no `web_search_tool_result`, unprocessable body, `search request failed`, `credential resolution failed`) never degrade. ## ๐Ÿ” Credentials - **๐Ÿ“ Type the key value straight into the card** โ€” saving writes it to the **DSH credential store** through the official credentials interface (`ctx.remote.credentials.set(ref, value)`, positional), and the configuration stores only the auto-generated **reference name** โ€” key literals never land in the settings document. **A blank value keeps the currently stored key.** - **๐Ÿ›ก๏ธ Leak hardening** โ€” the page only ever learns whether a value is non-blank (configured / not-configured badges, fed by `ctx.remote.credentials.describe([refs])`); stored literals are never echoed back, whitespace alone never counts as a value, and written values are trimmed. - **๐Ÿ”‘ References are generated for you** โ€” a source's first key uses `UM_WS__API_KEY`, further keys take `_1`, `_2`โ€ฆ (created once when the row is added, stable thereafter, explicitly isolated from the official packages' `EXA_API_KEY` / `DEEPSEEK_API_KEY`). - **๐Ÿ—‚๏ธ You can also store the key yourself** โ€” in the **DSH credential store** (Settings โ†’ Models API-key area) or as a launch-environment variable; the reference name is all that has to match. A reference shadowed by an environment variable is read-only (`writable: false`) and the card refuses to write it. - **๐ŸŒ Keyless play** โ€” enable a source's free tier (`free.enabled`) to search through its public MCP with no key at all. - **๐Ÿš€ DeepSeek Official quick start** โ€” type the key in the card (it lands under `UM_WS_DEEPSEEK_API_KEY`) โ†’ enable the `deepseek` provider's paid tier โ†’ save, takes effect immediately. - **๐Ÿงน Legacy credential migration** โ€” the legacy default references `EXA_API_KEY` / `PARALLEL_API_KEY` are copied to `UM_WS_EXA_API_KEY` / `UM_WS_PARALLEL_API_KEY` (**copy only, the legacy references are never deleted**). ## ๐Ÿฉบ Troubleshooting | Symptom | Cause & fix | | --- | --- | | Searches answer `WEB_PROVIDER_CONFIGURED_UNAVAILABLE` | The master switch is off. The `web` row already points at this plugin, and `enabled: false` does not fall back to another backend โ€” turn `enabled` on in the settings card | | `web_fetch` fails with `WEB_PROVIDER_AMBIGUOUS` | The profile's `web` row lost its `fetchProvider` (the patch-replaces-whole-config semantics). Restate every key of that row | | Two provider ids are usable and no `searchProvider` is set | This plugin registers `um-web-search` and the legacy alias `exa`, sharing one availability; the seam follows "several usable and none configured โ†’ error" rather than first-come-first-served. A hand-mounted deployment must write `searchProvider` explicitly | | A settings-card save is refused with a bilingual warning | The client-side pre-write validation stopped a doomed candidate (duplicate or blank provider ids, duplicate or invalid key references, an out-of-range concurrency, a default provider that names nothing). Fix it as shown and retry โ€” a refused save never lands partially | | The stored `providers` shows the built-in defaults plus a bilingual warning | The stored value is an object rather than an array (an older format or a foreign writer). The card renders the built-in fallback and refuses a save that would leave that shape standing | | Sources are configured but search is still unavailable | Check in order: the master switch, each provider's `enabled`, the tier switches, and whether the key exists and is not shadowed read-only by an environment variable | | You want the official search backend back | Uninstall this plugin (removing the bundle layer restores `deepseek-official`), or write the `web` row explicitly in the profile patch | Verify the composition: `pnpm exec dsh --profile web --dump-config | Select-String um-web-search`. Runtime semantics: `searchProvider` is read while the WebRuntime is constructed, so the `enabled` switch toggles availability live only. ## ๐Ÿงช Tests ### ๐Ÿ–ฅ๏ธ Host suite (node:test, 175 cases) ```powershell pnpm test ``` | File | Cases | Covers | | --- | --- | --- | | `tests/plugin.test.mjs` | 85 | The strategy state machine, caching, degradation semantics, concurrent fan-out, the DeepSeek wrapper, `UM_WS_` migration, `apply` mounting and waterfall scoping | | `tests/boundary.test.mjs` | 37 | Boundaries and contracts: chain degradation, clamping, cache eviction, state-machine details, the manifest window | | `tests/render.test.mjs` | 47 | The browser half (`lib/client.js`) against a React stub plus an official-primitives stub: rendering, path ops, and the pre-write validation contract | | `tests/version.test.mjs` | 6 | The dev version chain | ### ๐ŸŽฌ Visual interaction suite (Playwright, 16 cases) ```powershell pnpm test:visual # generate the fixture + run the assertions pnpm test:visual:update # refresh the snapshot baselines ``` - the fixture generator `tests/render-visual.mjs` produces one page, `tests/visual/fixtures/card.html`: the inlined `lib/client.js` source plus a minimal React, an official-primitives stub, and a mock DSH client ctx (`configForms` / `remote.credentials` / `slots` / `locale`), with every call recorded on `window.__testLog` - it covers this plugin's own composition and behaviour (mount gating, staged drafts and the save's path ops, provider ordering, key writes and badges, validation blocking); the framework components' own pixels are out of scope - after changing the fixture, regenerate the page with `node tests/render-visual.mjs` before refreshing the baselines ## ๐Ÿงฉ Development versions Dev builds use a base-version + version-code scheme: ```powershell pnpm dev:version # produces something like 0.7.0.20260829102301 (base + a 14-digit yyyyMMddHHmmss code) pnpm dev:version:reset # return to the base version ``` - the version code is second-level unique and strictly monotonic; the script syncs `package.json` with `VERSION` in `lib/index.js` and hard-fails on drift - history stays in the local `.um.agents/memory/` (untracked) - **dev versions are never committed, tagged, or added to the CHANGELOG** โ€” commits and releases target the base version only ## ๐Ÿšš Migration ### 0.6.x โ†’ 0.7.0 | Change | Effect | | --- | --- | | DSH โ‰ฅ 0.1.7-alpha.2 required | This release is rewritten against the 0.1.7 settings subsystem; the 0.1.1-rc.2 family is no longer supported | | The old settings namespace is no longer read | 0.6.x stored settings under the `web-search-exa` namespace; from 0.1.7 on, settings live in the profile patch's **row `config`**, and the namespace IS the Loader row id (`um-web-search`). The old section is **not imported**, so settings fall back to the factory defaults โ€” re-enter them once in the **Settings โ†’ Plugins โ†’ UM Web Search** card | | Legacy flat keys still bridge the gap | While `providers` is still the factory default, the pre-0.6.0 flat keys (`preferred`, `exaEnabled`, `allowAnonymous`, โ€ฆ) are read once and synthesized into `providers[]` (a zero-storage migration). After the first save from the new card the written array IS that synthesis, so those keys become **inert leftovers** the settings service cannot even clear (they are not volatile) and stop affecting behaviour | | The card UI was rebuilt | The bespoke CSS, modal, accordion, class namespace, and pixel baselines are retired in favour of the official primitives, so both the interaction path and the visual presentation changed | ### Revised 0.7.0 window (ADR-0007) The dependency window narrows to `>=0.2.0-rc.1 <0.3.0` (the 0.1.7 family is no longer supported); the test/dev baseline moves to `0.2.0-rc.2` โ€” devDependencies and overrides migrated as a family, and the `SettingsFormModel` mirror was re-verified line-for-line against `dsh-client-ui-primitives@0.2.0-rc.2` (semantics unchanged). ### 0.2.0 โ†’ 0.2.1 Desktop adaptation (ADR-0008) | Change | Effect | | --- | --- | | Official packages no longer ship with the plugin | `dependencies` is empty; `@deepseek-ai/*` is peer-declared only. The profile's `node_modules/@deepseek-ai` no longer carries any `dsh-*` copy, so every row resolves the DSH installation's own versions | | You must **uninstall โ†’ reinstall** | The old install hoisted the `0.2.0-rc.2` official packages into the profile; a version bump alone will not remove them. Reinstall via the plugin-management UI (or `dsh plugin --profile desktop remove` then `add`), then restart the Desktop app | | Settings fall back to factory defaults after reinstall | Uninstalling removes the `um-web-search` row and its `config`; re-enter providers and credential references once in **Settings โ†’ Plugins โ†’ UM Web Search** | | No code or patch-layer migration | The `cordis.patch.yml` row-override semantics, the `um-web-search` settings namespace, and the `web`-row takeover behaviour are unchanged | ## ๐Ÿ›๏ธ Architecture decisions & research | Document | Subject | | --- | --- | | [ADR-0001](./.um.agents/constraints/ADR-0001-web-search-exa.md) | Exa as a built-in DSH web search provider | | [ADR-0002](./.um.agents/constraints/ADR-0002-dual-backend-strategy.md) | The dual-backend strategy pattern (umbrella provider) and the layered settings UI | | [ADR-0003](./.um.agents/constraints/ADR-0003-dev-version-chain.md) | The dev version chain (base version + 14-digit version code) | | [ADR-0004](./.um.agents/constraints/ADR-0004-multi-source-strategy.md) | The multi-source strategy rebuild: the `providers[]` model, the paid/free state machine, concurrency and caching, the DeepSeek Official integration | | [ADR-0005](./.um.agents/constraints/ADR-0005-playwright-visual-testing.md) | Choosing Playwright for real visual interaction tests | | [ADR-0006](./.um.agents/constraints/ADR-0006-dsh-017-adaptation.md) | Adapting to DSH 0.1.7: the settings subsystem rewrite and the card rebuild on the official primitives | | [ADR-0007](./.um.agents/constraints/ADR-0007-dsh-020-rc2-single-track.md) | Narrowing to the single DSH 0.2.0-rc.2 track: Desktop alignment and the semver prerelease resolution defect | | [ADR-0008](./.um.agents/constraints/ADR-0008-dsh-021-rc2-peer-only-closure.md) | Adapting to DSH 0.2.1-rc.2: official packages as peer/dev-only, dropping the profile dependency closure (the `waiting for web` root cause) | | [Parallel API research dossier](./.um.agents/constraints/parallel-search-research.md) | The Parallel REST / MCP shape, its differences from Exa, and the open risks | | [Hardcoded-semantics index](./.um.agents/constraints/hardcode-index.md) | Every hardcoded value with a short path, to be synced one by one on upgrade | ## ๐Ÿ™ Support If this plugin helps you, consider buying me a coffee โ˜•๏ธ
Ko-fi
## ๐Ÿ“„ License [MIT](./LICENSE) ยฉ 2026 [UnforgetMemory](https://github.com/UnforgetMemory)