Skip to content
dsh.fish
Bundle

dsh-xmemo

Native DeepSeek Harness (dsh) plugin: hybrid local and XMemo cloud memory for Cordis-based agents.

Source
yonro
stars
1 stars
License
MIT
Updated
Updated 14 days ago

Readme

<div align="center">
  <a href="https://xmemo.dev">
    <img src="https://cdn.jsdelivr.net/gh/yonro/xmemo-claude-plugin@main/assets/icon.png" alt="XMemo" width="112" />
  </a>

  <h1>XMemo for DeepSeek Harness</h1>

  <p><strong>Native local-first + cloud memory for <code>dsh</code>.</strong></p>
  <p>
    Hybrid local storage with an offline durable write queue, plus real OAuth 2.1
    login — reimplemented as an ordinary Cordis plugin instead of a thin MCP bridge.
  </p>

  <p>
    <a href="LICENSE"><img alt="License" src="https://img.shields.io/github/license/yonro/xmemo-deepseek-plugin?style=flat-square" /></a>
    <img alt="Plugin version" src="https://img.shields.io/badge/plugin-v0.1.0-8B5CF6?style=flat-square" />
    <a href="https://github.com/yonro/xmemo-deepseek-plugin/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/yonro/xmemo-deepseek-plugin?style=flat-square&amp;logo=github" /></a>
  </p>

  <p>
    <img alt="Native Cordis plugin" src="https://img.shields.io/badge/dsh-native%20Cordis%20plugin-06B6D4?style=flat-square" />
    <img alt="REST, not MCP" src="https://img.shields.io/badge/backend-MemoryOS%20REST-334155?style=flat-square" />
    <img alt="OAuth 2.1 + PKCE" src="https://img.shields.io/badge/auth-OAuth%202.1%20%2B%20PKCE-10B981?style=flat-square" />
    <img alt="Local + cloud memory" src="https://img.shields.io/badge/memory-local--first%20%2B%20cloud-6E56CF?style=flat-square" />
  </p>

  <p>
    <a href="#quick-start">Quick start</a> ·
    <a href="#auth">Auth</a> ·
    <a href="#config">Config</a> ·
    <a href="#web-gui-card">Web GUI card</a> ·
    <a href="#tools">Tools</a> ·
    <a href="#capabilities-and-boundaries">Capabilities</a> ·
    <a href="#known-limitations">Limitations</a>
  </p>
</div>

---

A native [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) plugin: hybrid
local + [XMemo](https://xmemo.dev) cloud memory, with offline durable write queuing, active state,
timeline, TODOs, decisions, and restart snapshots — the same tool surface as
[xmemo-cindy-plugin](https://github.com/yonro/xmemo-cindy-plugin), reimplemented as an ordinary
Cordis plugin instead of going through Cindy's host-specific plugin protocol.

This plugin talks directly to the **MemoryOS** REST API (the actual backend behind XMemo) and keeps
its own local store, rather than going through XMemo's hosted MCP server (`https://xmemo.dev/mcp`)
— dsh can also reach XMemo the MCP way via the harness's own `@deepseek-ai/dsh-mcp-client` bridge,
but this repo is the deeper, native alternative. Endpoint paths, request/response field names, and
the auth header were verified against the MemoryOS source, not guessed from another client —
including live, end-to-end verification of the OAuth flow against production `xmemo.dev`.

> [!NOTE]
> DeepSeek Harness itself is a developer preview. This plugin has been exercised locally against
> production `xmemo.dev` (unit tests, a live web GUI session, and a real OAuth registration +
> authorize round trip) but has not seen broad multi-user usage yet.

## At a glance

| | |
|---|---|
| Package | `dsh-xmemo` |
| Plugin ID | `xmemo` |
| Runtime | DeepSeek Harness (`dsh`) |
| Version | `0.1.0` |
| Bundle | Native Cordis plugin (host) + web GUI settings card (browser) |
| Backend | MemoryOS REST API — `https://xmemo.dev` |
| Authentication | OAuth 2.1 + PKCE (recommended), or a static API key (compatibility) |
| Local storage | JSON hybrid store with a durable offline write queue |
| Tool surface | 16 tools, same names/schemas as `xmemo-cindy-plugin` |
| License | MIT |

### What it adds

- **Focused recall** — local token-overlap matches merged with cloud recall, fail-closed on any
  bucket/scope mismatch.
- **Durable outcomes** — remember decisions, preferences, and facts; record timeline events; track
  TODOs and decisions.
- **Working continuity** — active task state and restart snapshots so a session can resume instead
  of replaying history.
- **Offline resilience** — every write lands locally first; safe (idempotent) writes replay to the
  cloud automatically, uncertain ones wait for explicit approval.
- **Real account login** — OAuth 2.1 + PKCE against XMemo's actual authorization server, not just a
  pasted API key.
- **Recoverable deletion** — `xmemo_forget` defaults to a soft, recoverable delete.

## Quick start

```sh
dsh plugin --profile <name> add dsh-xmemo
# or, to track main instead of the latest npm release:
dsh plugin --profile <name> add github:yonro/xmemo-deepseek-plugin
# or, from a local checkout:
dsh plugin --profile <name> add ./xmemo-deepseek-plugin
```

A git install fetches source, not the built `lib/`, so pnpm blocks this package's `prepare` script
(which builds both halves — see [Development](#development)) on the first `add` and prints the exact
key to allow. Add it to the profile's `pnpm-workspace.yaml` and re-run `add`:

```yaml
allowBuilds:
  dsh-xmemo: true
```

Connect an XMemo account or set an API key (see [Auth](#auth) below), then verify the row loaded:

```sh
dsh --profile <name> --dump-config   # look for "# == dsh-xmemo"
```

## Auth

Two methods, resolved in this order on every request (`src/auth.ts`) — never both at once:

1. **OAuth 2.1 + PKCE** (recommended). Connect from the web GUI's plugin card (see
   [Web GUI card](#web-gui-card)) or trigger it programmatically by writing a
   `connect:<anything>` value to the `XMEMO_OAUTH_ACTION` credential — `src/oauth.ts` listens for
   this via the seam-wide `credentials/updated` event. Connecting: registers a fresh public OAuth
   client through MemoryOS's Dynamic Client Registration (`POST /oauth/register`, one per connect
   attempt — cheap, and avoids caching a client_id with its own staleness edge cases), opens your
   default browser to `/oauth/authorize` with a PKCE challenge, and runs a temporary
   `127.0.0.1:<ephemeral-port>` HTTP listener as the redirect target — the same loopback-native-app
   pattern MemoryOS documents for Cindy's desktop OAuth flow (verified live against production
   `xmemo.dev`: DCR accepts a brand-new client's loopback `redirect_uri`, and `/oauth/authorize`
   accepts that client_id + PKCE + explicit `resource` end to end). The resulting access/refresh
   token pair is stored under the `XMEMO_OAUTH` credential reference and refreshed automatically
   (with rotation — a used refresh token is never replayed) a minute before it expires. Disconnect
   the same way with a `disconnect:<anything>` value, which also revokes both tokens server-side.
2. **Static API key** (fallback). Set the `XMEMO_KEY` credential through dsh's own credential seam
   — any of an environment variable (`XMEMO_KEY=... dsh --profile <name>`),
   `$DSH_HOME/.credentials.yaml` (`XMEMO_KEY: ...`), or `<project>/.env` / `$DSH_HOME/.env`. Sent as
   the `X-API-Key` header (MemoryOS's primary auth header — `Authorization: Bearer` is only its
   fallback; see `auth/api_key.py`). The credential reference name is configurable
   (`apiKeyCredential` in `cordis.patch.yml`) if you'd rather not use `XMEMO_KEY`.

Unlike Cindy, whose *host* runs the OAuth dance generically for any plugin that declares an "OAuth
credential source," `dsh` has no such primitive (see [Known Limitations](#known-limitations)) — this
plugin runs the whole flow itself in `src/oauth.ts`, with no deepseek-harness changes required.

## Config

Set in this bundle's `cordis.patch.yml`, or override per-profile/home `cordis.patch.yml`:

| Field | Default | Meaning |
|---|---|---|
| `mode` | `hybrid` | `hybrid` (local-first + cloud sync), `local-only` (never calls MemoryOS), or `cloud-only` (no local persistence). Overridable at runtime — see [Web GUI card](#web-gui-card). |
| `apiKeyCredential` | `XMEMO_KEY` | Credential reference resolved through `ctx.credentials`. |
| `apiBaseUrl` | `https://xmemo.dev` | MemoryOS REST API base. Override for a local dev server, e.g. `http://localhost:8000`. |
| `defaultScope` | `dsh` | Default `scope` tag when a tool call omits one. |
| `agentId` | `DeepSeek Harness` | Sent as `X-Memory-OS-Agent-ID`. |
| `requestTimeoutMs` | `30000` | Default per-request timeout. |
| `longRequestTimeoutMs` | `60000` | Timeout for `xmemo_recall` and `xmemo_restore_progress`. |

`mode` is resolved per tool call (`src/mode.ts`, mirroring how `src/auth.ts` resolves the API key) rather
than read once at boot: a `hybrid`/`local-only`/`cloud-only` value stored under the `XMEMO_MODE`
credential reference wins over the `cordis.patch.yml` default, so a change saved through the web GUI's
mode selector reaches the very next tool call without a restart.

## Tools

`xmemo_status`, `xmemo_update_state`, `xmemo_record_event`, `xmemo_list_timeline`, `xmemo_remember`,
`xmemo_recall`, `xmemo_forget`, `xmemo_create_todo`, `xmemo_list_todos`, `xmemo_complete_todo`,
`xmemo_create_decision`, `xmemo_list_decisions`, `xmemo_resolve_decision`, `xmemo_save_progress`,
`xmemo_restore_progress`, `xmemo_sync` — same names, schemas, and behavior as
`xmemo-cindy-plugin`'s tool surface.

| Area | Tools |
|---|---|
| Status & sync | `xmemo_status`, `xmemo_sync` |
| Working state | `xmemo_update_state`, `xmemo_save_progress`, `xmemo_restore_progress` |
| Timeline | `xmemo_record_event`, `xmemo_list_timeline` |
| Memory lifecycle | `xmemo_remember`, `xmemo_recall`, `xmemo_forget` |
| TODOs | `xmemo_create_todo`, `xmemo_list_todos`, `xmemo_complete_todo` |
| Decisions | `xmemo_create_decision`, `xmemo_list_decisions`, `xmemo_resolve_decision` |

Skill instructions (when to call which tool) are intentionally not part of this package — reuse
`xmemo-claude-plugin/skills/*/SKILL.md` as-is by dropping them into a project's `.agents/skills/`;
dsh's `skill-filesystem` provider discovers the same frontmatter format from that directory.

## Web GUI card

The `dsh --profile web` GUI's Settings → 插件配置 (Plugin Config) panel shows an "XMemo" card
alongside the first-party Bash/Agent-loop/Web-search cards, via a browser bundle this same package
ships (`src/client/`, built to `lib/client.js`, declared through the `dsh.client` manifest field in
`package.json` — no changes to deepseek-harness itself are needed; `dsh-client-modules` scans every
loaded plugin's `package.json` for that field, not just first-party ones).

Three controls are genuinely live:

- **XMemo account login** (recommended, shown first) — Connect/Disconnect buttons driving the OAuth
  flow described in [Auth](#auth). Its status badge names whichever method is actually in effect —
  `已连接 · OAuth`/`Connected · OAuth` or `已连接 · API Key`/`Connected · API key` — mirroring
  `auth.ts`'s own "OAuth first, API key fallback" precedence exactly, and only falls back to
  `未连接`/Not connected when neither is configured, so a working API-key setup never reads as
  broken just because OAuth hasn't been connected. When both happen to be configured at once, a
  short note under the button says so and states that OAuth is the one actually being used. Since
  this card has no direct RPC into host-side code (the same `credentials.*`-only constraint below),
  the buttons relay through the write-only `XMEMO_OAUTH_ACTION` signal credential rather than
  calling anything directly, then poll `credentials.describe('XMEMO_OAUTH')` (every 2s, up to ~5.5
  minutes) to detect when the browser login completes.
- **API key** (compatibility fallback) — reflects and can change the actual stored key via real
  `credentials.describe`/`credentials.set` calls, including correctly showing it as read-only when
  `XMEMO_KEY` is supplied by the launch environment rather than the credentials store.
- **Memory mode** — a real `<select>` (hybrid / local-only / cloud-only) with an explicit Save
  button, saved under the `XMEMO_MODE` credential reference and picked up by `src/mode.ts` on the
  very next tool call. Unlike the API key field, the select can't show which value is currently
  stored — `credentials.describe` deliberately never exposes a credential's value, only whether it's
  configured (see [Known Limitations](#known-limitations)) — so it always starts from the `hybrid`
  default and a "customized"/"default" badge stands in for the value itself, the same way the API
  key field's "configured" badge never reveals the secret.

The other five config fields (`apiBaseUrl`/`defaultScope`/`agentId`/`requestTimeoutMs`/
`longRequestTimeoutMs`) aren't shown in the card at all — their defaults are fine for the vast
majority of setups; override them via `cordis.patch.yml` (see [Config](#config)) if needed.

Build with `npm run build:client` (separate from the host build — needs its own `tsconfig.client.json`
and `scripts/build-client.mjs`, since the host and browser halves target different runtimes).

## Architecture notes

Ported from `xmemo-cindy-plugin/plugins/xmemo-memory/main.js`'s business logic:

- `src/http.ts` — request/error handling (`AUTH_REQUIRED`, `RATE_LIMITED`, `XMEMO_SERVER_ERROR`, …).
  One structural change from upstream: Cindy's host injected `Authorization` based on request
  hostname; this plugin resolves and attaches the auth header itself (`src/auth.ts`, `src/oauth.ts`).
- `src/store.ts` — the local JSON store (schema, prune limits, quota guard, corrupt-file → backup
  fallback, write-temp-then-rename atomicity).
- `src/outbox.ts` — the durable write queue: `staged → sent | pending | held | failed`, idempotent
  ops auto-retry (5 attempts), non-idempotent ops go straight to `held` and need an explicit
  `xmemo_sync {action: 'push', include_held: true}`, local-id → cloud-id dependency resolution for
  writes that target an entity that hasn't synced yet (e.g. completing a TODO offline).
- `src/recall.ts` — local token-overlap scoring merged with cloud recall, including the fail-closed
  bucket/scope filter (`compactCloudRecallItems`): a cloud item that doesn't exactly match the
  request is dropped, and any single violation suppresses all opaque cloud text in that response.
- `src/redact.ts` — the 7-category credential-like-text redaction, ported verbatim.
- `src/oauth.ts` — the whole OAuth 2.1 + PKCE client: Dynamic Client Registration, an ephemeral
  loopback HTTP listener as the redirect target, token exchange/refresh with rotation, and revoke.

One acknowledged divergence: main.js's store is one dynamic JSON blob, so it can replay a queued
write generically. This port's store is typed per entity kind, so a **replayed** write (via
`xmemo_sync`, possibly in a later process) only generically patches `cloud_id` + `sync_status`;
richer field back-fill happens only on the first attempt, made synchronously inside the same tool
call. See the comment at the top of `src/outbox.ts`.

## Capabilities and boundaries

| Capability | Included | Boundary |
|---|:---:|---|
| Local-first hybrid memory | Yes | Local store is not encrypted at rest |
| OAuth 2.1 + PKCE login | Yes | Needs a desktop session to open a browser in; headless `dsh` falls back to the API key |
| Static API key auth | Yes | Compatibility fallback only — never sent alongside a connected OAuth session |
| Durable offline write queue | Yes | Non-idempotent writes wait in `held` for explicit replay approval |
| Recoverable deletion | Yes | `xmemo_forget` is soft delete by default |
| Web GUI settings card | Yes | Only OAuth login, the API key, and memory mode are editable from the browser — the other five config fields need `cordis.patch.yml` |
| Recall and search | Yes | Limited to memory visible to the authenticated account, and fail-closed on any bucket/scope mismatch |
| Cross-agent continuity | Yes | Other agents/clients need their own authorized XMemo connection |
| PII redaction | No | Only credential-like text is redacted, matching `xmemo-cindy-plugin` upstream |
| MCP transport | No | Talks to MemoryOS REST directly; dsh's own `dsh-mcp-client` bridge is a separate, untouched path |
| Bundled skill instructions | No | Reuse `xmemo-claude-plugin/skills/*/SKILL.md` instead — see [Tools](#tools) |
| Encryption at rest | No | Same as upstream — honest that there isn't one, see [Known Limitations](#known-limitations) |

## Known Limitations

- **No central plugin marketplace exists for `dsh` by design.** Discovery is just the `dsh-plugin`
  GitHub topic ("Contribute to the ecosystem" in
  [`CONTRIBUTING.md`](https://github.com/deepseek-ai/deepseek-harness/blob/main/CONTRIBUTING.md)),
  which this repo is tagged with; there's no submission, review, or PR-based listing process to
  opt into beyond that.
- **OAuth connect needs a desktop with a browser.** The flow opens a system browser and waits on a
  loopback listener for its redirect; a headless `dsh` instance (no desktop session to open a
  browser in) can't complete it — use the static API key there instead.
- **`XMEMO_OAUTH` and `XMEMO_OAUTH_ACTION` appear in `$DSH_HOME/.credentials.yaml` and the Models
  page's credentials list**, alongside `XMEMO_MODE` — an accepted tradeoff of reusing the one
  channel that's actually open to an out-of-tree plugin (see the web GUI card limitation below).
  `XMEMO_OAUTH` holds the access/refresh token pair as an opaque JSON blob; `XMEMO_OAUTH_ACTION` is
  a transient write-only signal, never holding anything meaningful at rest; `XMEMO_MODE` isn't a
  secret at all.
- **A fresh OAuth client is registered on every connect attempt** rather than cached — Dynamic
  Client Registration exists precisely for this kind of ad hoc self-registration, and skipping the
  cache avoids persisting a fourth credential ref with its own staleness edge cases. Each registered
  client is a self-verifying signed token MemoryOS never has to store server-side, so this has no
  accumulating cost.
- **No encryption at rest.** Same as upstream — the Cindy host's storage encryption, if any, was
  opaque to the plugin; this port is honest that there isn't one.
- **Single in-process store lock.** Concurrent `dsh` processes writing to the same store directory
  are not coordinated; run one instance per store directory.
- **PII redaction is not implemented**, matching upstream: `xmemo-cindy-plugin`'s tool descriptions
  claim email/phone redaction, but no such code exists in `main.js` either — it's server-side or
  aspirational. This port's tool descriptions say only what the code does.
- **Replayed writes only back-fill `cloud_id`**, not richer response fields — see Architecture notes.
- **The web GUI card can only edit OAuth login, `apiKeyCredential`, and `mode`; the other five
  config fields aren't editable from the browser at all.** The harness's generic
  settings-persistence pipeline (`ctx.settingsScope`) is gated by a hardcoded namespace allowlist in
  deepseek-harness's own `packages/host/apiproxy/src/api-proxy.ts` (`WEB_SETTINGS_NAMESPACES`) that
  an out-of-tree plugin cannot extend from its own package (the source comment there calls
  generalizing it "deferred work"). Only the ungated `credentials.*` RPC is open to third-party
  plugins today — the same channel `apiKeyCredential` already used for its API key — so `mode` and
  OAuth both piggyback on it too (`XMEMO_MODE`, `XMEMO_OAUTH`/`XMEMO_OAUTH_ACTION`), and the
  remaining five config fields have no open channel to bind to at all.
- **The mode selector can't show the currently saved value, only whether one has been saved.**
  `credentials.describe` reports `configured`/`writable` but never a credential's value — correct
  for secrets, but it means the mode select can't be pre-filled with the true stored mode the way
  Cindy's `/kv`-backed selector can. The select always starts from the `hybrid` default; saving
  always overwrites blindly, same as the API key field already does.

## Security and privacy

- **Credentials never reach plugin memory as browser-readable state.** The web GUI card only ever
  sees `configured`/`writable` booleans (`credentials.describe`) — it can change the API key, OAuth
  tokens, and mode, but never read their values back.
- **OAuth tokens are stored, not logged.** The access/refresh token pair lives under the `XMEMO_OAUTH`
  credential reference; refresh tokens rotate on every use, and a reused (already-consumed) refresh
  token is rejected server-side rather than silently accepted.
- **Obvious credential-like text is redacted before memory and timeline writes** (`src/redact.ts`,
  7 categories, ported verbatim from upstream) — a safeguard, not a substitute for keeping secrets
  out of prompts.
- **Local memory is private but not encrypted by this plugin** — protect the device and do not store
  secrets in memory content.
- **Deletion is recoverable by default.** `xmemo_forget` soft-deletes unless a caller explicitly
  requests a hard, permanent delete.
- **This repository contains no production credentials, tokens, or private memory** — only
  credential *reference names* (e.g. `XMEMO_KEY`), never values.

Canonical service policies: [Privacy policy](https://xmemo.dev/legal/privacy) ·
[Terms of service](https://xmemo.dev/legal/tos) · [Support](https://xmemo.dev/support)

## Development

```sh
npm install
npm run build         # tsc -> lib/ (host half)
npm run build:client   # tsc --emitDeclarationOnly + esbuild -> lib/client.js (browser half)
npm run typecheck
npm test               # node --test over tests/*.spec.ts
```

### Releasing

Bump `version` in `package.json`, commit, then tag and push:

```sh
git tag -a v0.1.1 -m "v0.1.1"
git push origin v0.1.1
```

[`.github/workflows/publish.yml`](.github/workflows/publish.yml) picks up any `v*.*.*` tag,
rejects it if it doesn't match `package.json`'s version, runs typecheck/test/build, publishes to
npm with provenance using the `NPM_TOKEN` repository secret, and creates a matching GitHub Release.
`workflow_dispatch` (with a `version` input) works the same way for a manual re-run from a commit
already tagged that way.

## Agent-readable metadata

| Field | Value |
|---|---|
| Package | `dsh-xmemo` |
| Plugin ID | `xmemo` |
| Runtime | DeepSeek Harness (`dsh`) |
| Version | `0.1.0` |
| Role | Native Cordis plugin + web GUI settings card |
| Service | `https://xmemo.dev` |
| Backend | MemoryOS REST API (not MCP) |
| Authentication | OAuth 2.1 + PKCE (recommended), or static API key via `XMEMO_KEY` |
| OAuth scopes | `memory:read memory:write` |
| Tool profile | 16 tools, same names/schemas as `xmemo-cindy-plugin` |
| Local storage | Yes — JSON hybrid store with a durable outbox, not encrypted |
| Repository | `https://github.com/yonro/xmemo-deepseek-plugin` |
| License | MIT |

## Links

- Product: https://xmemo.dev
- Account & API keys: https://xmemo.dev/me#api-keys
- Documentation: https://xmemo.dev/product/docs
- Support: https://xmemo.dev/support
- DeepSeek Harness: https://github.com/deepseek-ai/deepseek-harness
- Sibling plugins: [Cindy](https://github.com/yonro/xmemo-cindy-plugin) ·
  [Claude](https://github.com/yonro/xmemo-claude-plugin) ·
  [Codex](https://github.com/yonro/xmemo-codex-plugin) ·
  [Hermes Agent](https://github.com/yonro/hermes-xmemo-plugin) ·
  [OpenClaw](https://github.com/yonro/xmemo-openclaw-memory)

## License

[MIT](LICENSE) © 2026 Yonro

Install

dsh plugin --profile web add github:yonro/xmemo-deepseek-plugin

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source