DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

swarm-apps /

swarm-apps/dsh-swarmdrop

Verified

Send files from your DeepSeek Harness agent straight to your phone, and reference what your phone sent back — no account, no public IP, end-to-end encrypted.

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@6fcfe915

dsh-swarmdrop

Give your DeepSeek Harness agent a channel to your own devices. It can push what it just produced straight to your phone, and you can @-reference what your phone sent back — no account, no public IP, end-to-end encrypted.

The transport is SwarmDrop, driven through its CLI.

Status: developer preview. dsh itself declares breaking changes, and this plugin sits on its extension seams. Pin a version.

Pin the dsh line too. The peer ranges target 0.1.0-rc.x; npm's latest tag on the @deepseek-ai/* packages still points at the older 0.0.1-rc.x line, so an unpinned install resolves to packages this plugin does not target.

What you get

In dsh What happens
"send the report to my phone" The agent calls swarmdrop_send_files; a transfer row appears in the conversation and follows it to completion.
/swarmdrop send ./report.pdf phone Same, without a model round trip.
@ in the composer Your inbox — everything your devices sent this machine — as reference candidates.
Your phone sends a file A row appears in the conversation, and the item becomes referenceable.
The SwarmDrop button at the sidebar foot Node status and network posture, start/stop, your paired devices, and pairing a new one — without leaving dsh for a terminal.

The panel

A dot beside Settings says whether a node is running: green for running, amber for stopped, grey while the first answer is still in flight. Opening it gives you

  • Node — running or stopped, its node id, and one button to change that.
  • Network — NAT class, relay reservation, bootstrap, connected peers, listen addresses. Shown only while a node is running, because every field would otherwise read "unknown" and say nothing the node row did not.
  • Devices — what is paired and whether it is online. unknown is its own state, not a synonym for offline.
  • Pairing — "Add a device" issues an invite and staffs the desk; the link it shows opens SwarmDrop's own page, which draws the QR code for your phone to scan. When a device shows up you see its name, system, link type and full node id before deciding.

Pairing still requires a person to look at the far side's identity — that has not been relaxed, only moved. An invite is a one-shot capability that travels as a link, and whoever presents it first consumes it, so SwarmDrop's node refuses every inbound request unless someone is at the desk.

The desk stays staffed until you press Cancel, not until you dismiss the panel — you will usually be looking at your phone at that moment, and a popover that closes when you click away would take the invite with it. The sidebar dot turns amber-grey while a window is open, so a desk left staffed is visible without opening anything.

Install

dsh plugin --profile <name> add dsh-swarmdrop

That is all — the package declares a dsh.bundle, so dsh appends it to the profile's bundle list and its config layer activates on the next start. Verify before launching with dsh --profile <name> --dump-config, which should show a # == dsh-swarmdrop layer.

dsh plugin forwards to pnpm inside the profile directory, so it takes any pnpm target — no npm publish required:

dsh plugin --profile <name> add /path/to/dsh-swarmdrop     # a local checkout
dsh plugin --profile <name> add ./dsh-swarmdrop-0.1.0.tgz  # from `npm pack`

Remove it with dsh plugin --profile <name> remove dsh-swarmdrop, which drops the dependency and the layer together.

The SwarmDrop binary

It comes along as an optional dependency, and the plugin finds that copy itself — nothing else to install. If you already have SwarmDrop from Homebrew or the install script and would rather use it, set SWARMDROP_BIN to its path and the bundled copy is ignored.

You will see pnpm say Ignored build scripts: swarmdrop during the install. That is fine: the npm package fetches its platform binary from a postinstall hook, pnpm blocks those by default, and the shim falls back to fetching on first use instead. The only visible effect is that the first SwarmDrop call after installing takes a few seconds longer than the rest.

swarmdrop 0.5.0 or newer is required. 0.4.0 added swarmdrop watch, which this plugin subscribes to; 0.5.0 added invite create --decide-from-stdin, which is what lets the panel run the pairing desk. On 0.4.0 everything else works and pairing reports that the CLI is too old.

Pair a device from the panel — the plugin has nothing to talk to otherwise. The terminal route still works if you prefer it:

swarmdrop invite create      # scan the QR from your phone's SwarmDrop app

Nothing here requires a SwarmDrop node to be running: the plugin loads cleanly on a machine where you have not started one, the tools say so rather than failing mysteriously, and the panel offers to start one.

Tools

Tool What it does
swarmdrop_send_files Send files or directories to one of your devices.
swarmdrop_send_text Send a short message to a device's inbox.
swarmdrop_list_devices Your paired devices and whether they are online.
swarmdrop_list_inbox What your devices have sent this machine.
swarmdrop_inbox_files Local paths of one inbox entry's files.

presence is three-valued (online / offline / unknown). unknown means no SwarmDrop node is running to probe with — it is not the same as offline, and the distinction is the difference between "your phone is asleep" and "start SwarmDrop".

How it is put together

src/
  cli.ts         the `swarmdrop` binary: one-shot calls, the subscription, pairing
  machine.ts     what this machine looks like, folded from the subscription
  pairing.ts     the pairing desk: one window, and who is standing at it
  revision.ts    the shared "something changed" counter the panel parks on
  bridge.ts      machine-wide happenings  →  per-session events
  panel.ts       the panel's RPC channel (status, devices, pairing)
  panel-wire.ts  the panel's wire contract, compiled by both halves
  projection.ts  the inbox roll, as a Session projection (what `@` reads)
  tools.ts       what the model can call
  command.ts     what you can type
  types.ts       the Session event family this plugin owns
  client/        the browser half: the panel, conversation rows, the `@` source

Four decisions worth knowing before changing anything:

Two kinds of data, two carriers. Conversation rows and @ candidates travel in the session log: they must rebuild identically after a refresh, a history page, or a replay months later, so the @ menu reads a session projection — the Node half registers a pure fold, the framework drives it over committed events in log order, and the browser receives a finished value.

The panel's data does not go there. Node liveness, devices and network posture are facts about now; a session event claiming "the node is up" would be a claim about a moment, persisted forever, and read back as though still true. So the panel has a channel of its own — ctx.connection.rpc.handle('/swarmdrop', …) on the Host, rpc.call in the browser. Both work under every dsh carrier, including reaching a dsh at home from your phone.

An earlier version of this file said dsh gives third-party plugins no Client→Node RPC. That was wrong. What is true, and load-bearing, is the split above: the transcript rebuilds from the log, and nothing may bypass that.

The panel long-polls, because it cannot be pushed to. dsh forwards Host events to the browser from a fixed allowlist a third-party plugin cannot extend. So the panel parks a request on the Host until something changes — which is not a downgrade from a push: the request is already waiting when the change lands, so the answer leaves immediately rather than at the next tick of a timer.

Events record what happened, not what is. swarmdrop/sent, swarmdrop/inbox-received and swarmdrop/transfer are things that occurred at a point in time, so replaying a conversation months later still explains it. The one whole-value event, swarmdrop/inbox-baseline, answers "what did you have at hand when this started" — which is exactly the context a reader needs.

Every payload carries a version. These land in your session log, which outlives the process and gets replayed. A format change that still parses but means something different is the worst failure available.

A limitation you should know about

dsh refuses to read a session log containing an event type it does not know, unless the event is marked ignorable. Neither escape is available to a third-party plugin: the known set is generated from the types declared inside the dsh repository, and Session.append() offers no way to set the marker. dsh knows — its own source says a registration surface for out-of-repo events "is deferred until such a consumer exists".

This plugin is that consumer, so at load it announces its four event types to the running harness. That makes them readable here. It does not put ignorable on the events, so:

Disable this plugin rather than uninstalling it, if conversations that used it still matter to you. A harness without the plugin refuses to open a session log containing its events — you would see "unknown to this harness", and the whole conversation, not just the SwarmDrop rows, becomes unreadable.

The announcement also relies on the plugin and dsh resolving the same @deepseek-ai/dsh-session module instance. That holds for an ordinary install; it does not hold when dsh is run from a source checkout under tsx, where the two halves get separate module graphs and the announcement lands on a copy nothing reads.

Development

npm install
npm run typecheck   # both halves
npm run build       # lib/index.js + lib/client.js

The two halves compile as separate TypeScript programs, and that is not optional. dsh augments Context.sessions differently on the two sides (Node: SessionStore; browser: ISessions), so putting both in one program makes the browser half compile against the Node service surface and fail with errors that point nowhere near the cause. The same rule applies inside the source: client files must never import a package root — only /types and /client subpaths, which carry no Context augmentation.

The browser bundle is not an ordinary ESM build. dsh's loader expects the ./client entry to register itself with window.__ModuleLoader__.load({ id, factory }), resolving externals through an injected require — no import map, no globals. dsh builds its own with a shared tsdown preset that is not published, so scripts/build-client.mjs reimplements the wrapper; it is 30 lines. id must equal the package name, because that is the entry name the host composed into window.__DSH_BOOT__.

Two things a reader will otherwise rediscover the hard way: Two things a reader will otherwise rediscover the hard way:

  • The conversation-node cookbook's snippet does not compile as written. ChatNodeViewProps bundles t: TranslateNS<'conversation'>, but the slot only injects t when the registration passes locale, and the namespace value first-party code passes is not exported. See src/client/nodes.tsx.
  • exec.agent is optional. A nested Code-Mode dispatch has no agent, so a send still happens but has no conversation to attribute itself to.
  • npm's latest tag lags the real version line. npm view @deepseek-ai/… reports 0.0.1-rc.1, whose client packages depend on @deepseek-ai/dsh-compact — a package that is not published, making that line unresolvable. The line actually in use is 0.1.0-rc.x, which resolves cleanly. Check npm view <pkg> versions rather than the bare version.

License

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit 6fcfe915e079

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout