npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-pi-adapter

v0.1.0

Published

Run pi coding-agent extensions (ExtensionAPI) inside DeepSeek Harness via a cordis plugin bridge.

Readme

dsh-pi-adapter

Run pi coding-agent extensions (~/.pi/agent/extensions/*.ts) inside DeepSeek Harness (dsh).

A pi extension is export default (pi: ExtensionAPI) => void; every capability comes from the injected ExtensionAPI object. This project implements that contract on a cordis context so pi extensions run unmodified inside dsh.

How it works

pi extension (.ts factory)                  dsh (cordis)
┌────────────────────────────┐   jiti   ┌──────────────────────────────┐
│ export default (pi) => {   │ ───────► │ ExtensionBridge (PiCompat)   │
│   pi.registerTool({...})   │  alias   │   registerTool → ctx.tools   │
│   pi.registerCommand(...)  │  to pi   │   registerCommand → commands │
│   pi.on("tool_call", fn)   │  runtime │   on(event) → ctx.on(map)    │
│ }                          │          │   dispose() → unwind effects │
└────────────────────────────┘          └──────────────────────────────┘
  • jiti loader loads extensions the same way pi does (TS, no build step).
  • pi runtime aliases resolve @earendil-works/pi-ai / pi-tui / pi-coding-agent / typebox from the global pi install (npm root -g), since those packages only exist there.
  • ExtensionBridge implements the full ExtensionAPI surface. Each method is one of:
    • [direct] shape matches a cordis seam one-to-one
    • [adapt] a compatibility adapter wraps the dsh surface
    • [degrade] a write-only sink with no dsh equivalent — logs loudly
    • [unsupported] no truthful answer exists — raises UnsupportedSeamError

The answer-truthfully rule

The bridge never fabricates a value an extension will act on. The split is by direction, not by importance:

  • a seam the extension reads (its return value drives a decision) with no truthful dsh answer → throw, naming the extension and the seam
  • a seam the extension only writes (notify, setStatus, widgets, shortcuts) → log and continue; nothing branches on it

The nuance is concrete: permission-gate.ts gates on ctx.ui.select(), and a headless dsh host has no user to ask. Two wrong answers were on the table. Silently returning undefined with nothing in the log hides the missing capability; quarantining the extension (the earlier behavior) kills the gate for the rest of the session — every later call denied with a misleading "unimplemented seam" reason, which is what a headless rc.2 smoke run actually produced. pi's own contract offers the honest middle: select/confirm resolve undefined/false on user-cancel, and correctly written extensions treat that as non-affirmative (the same branch they take under !ctx.hasUI). So an UNANSWERED ask resolves to that no-selection convention, with a loud log naming why nobody answered — a gate that blocks unless the answer is an explicit "Yes" (the real permission-gate shape) keeps gating with its own precise reason. A gate written to block only on an explicit "No" allows on no-selection — but that shape fails open on a plain Esc under real pi too. strictUi: true restores raise-and-quarantine for deployments that prefer it. Seams with no truthful answer at all (ctx.ui.editor(), ctx.isProjectTrusted()) still raise.

ctx.ui.theme moved the other way: pi's Theme is pure string decoration, so rendering each span as its own plain text is faithful for a host with no ANSI surface, not a degradation.

When an extension hits an unsupported seam

| where | what happens | |---|---| | its factory (mount) | partial registrations unwound, extension not mounted | | a tool_call gate | the call is denied, permanently — a gate that cannot run never becomes one that permits | | a tool_result handler | the tool's own result passes through unchanged | | a tool body / command | that invocation fails with the seam named | | any other handler | logged; the extension is quarantined |

Quarantine disables the extension without unsubscribing it, so its tool-call gates keep denying instead of vanishing. Both moments are also appended to the session log as log-only audit events — pi-adapter/quarantine (extension, seam, call site) and pi-adapter/gate-deny (extension, tool name, callId, reason) — so a replayed session tells the operator which extension was quarantined and which calls its broken gate went on to deny. In a multi-session host each event lands on the session of the agent whose call was denied (a dsh Agent exposes .session), falling back to the last observed session only when no agent context exists. The events never enter the model surface; the stderr log line remains the floor when no session has been observed yet.

One upstream limit, stated plainly: none of the adapter's plugin-owned event types — pi-adapter/quarantine, pi-adapter/gate-deny, and the existing pi/entry / pi/label from pi.appendEntry/setLabel — are in dsh session-persistence's KNOWN_SESSION_EVENT_TYPES, and Session.append() has no way to mark an appended event ignorable. A session containing any of them therefore cannot resume under a standard harness with session-persistence mounted: the read path refuses the unknown type with SessionFormatUnsupportedError (deliberately — an unrecognized log might belong to a newer harness, and dsh over-refuses rather than resuming a session it cannot interpret). A feature request has been filed upstream for a plugin-owned event registration surface; until it lands, the audit trail is exact for live sessions but costs resumability.

onUnsupported: 'fail' refuses to start instead of quarantining. It propagates wherever a caller exists (mount, tools, commands, the tool-call waterfall); a fire-and-forget event listener has no caller, so there it degrades to the same loud log.

Translation tiers

adapt-interactive sits between adapt and unsupported: the seam exists in dsh, but only behind a capability the HOST may or may not mount — when an interactive host mounted a user-questions provider the adapter asks the real human; when it has not, the adapter answers with pi's no-selection convention (undefined/false, loudly logged), which extensions already treat as non-affirmative — never an invented answer. strictUi: true turns an unanswered ask into a seam error instead.

| pi API | dsh seam | tier | |---|---|---| | pi.registerTool({parameters, execute}) | ctx.tools.register() with mandatory output {schema, render}; typebox → parameters JSON Schema (1.x's non-enumerable ~kind/~optional/~unsafe metadata is stripped — dsh requires lossless JSON and rejects the whole catalogue otherwise); execute(callId, args, signal) arg bridge | adapt | | pi.registerCommand(name, {handler}) | ctx.commands.register({name, description, handler(invocation)}); the registration window is cordis's own DI: a ctx.inject(['commands']) child flushes buffered registrations the moment the commands fiber goes ACTIVE (during boot that is after plugin mount, so a mount-time probe always loses — dsh's plan-mode registers commands the same hook). Load completion warns how many commands are still buffered; a host without dsh-commands is caught at the first session/created checkpoint, which degrades each buffered command by name without dropping it (a late-activating service still registers it) | adapt | | pi.registerFlag / getFlag | in-process registry | direct | | pi.on("tool_call") (block) | tools/pre-execute waterfall — block{kind:'deny'}; non-block delegates next(); event.input is a mutable clone of dsh's frozen arguments, so in-place rewrites are dropped (dsh decisions can't rewrite args) instead of throwing | adapt | | pi.on("tool_result") | tools/post-execute waterfall — returned content{kind:'accept', content} | adapt | | pi.on("turn_start/end") | session/event stream filtered to turn/start / turn/end | adapt | | pi.on("session_start/shutdown") | session/created / session/disposed | direct | | pi.on("agent_start/end") | agent/session-start / agent/settled (closest verified events; payload shapes differ) | adapt | | pi.on("input") | agent/prompt-submit waterfall — carries the claimed UserMessage, so event.text is real; {action:'transform'}{kind:'allow', content}, {action:'handled'}{kind:'block'}. A handler that cannot run delegates rather than blocking: this admits the USER's own prompt, so failing closed would wedge the session | adapt | | pi.on("before_agent_start") | agent/prompt-submit — pi's "prompt settled, turn about to open" is the same edge. event.systemPrompt is absent (dsh assembles it asynchronously later); result-side prompt/message replacement degrades | adapt | | pi.on("session_compact") | compact/end without error in the session/event stream. reason: 'manual' only when the owner turn is null (a standalone manual transaction); compactionEntry/fromExtension/willRetry are omitted, not invented | adapt | | pi.on("agent_settled") | agent/status reaching idle. NOT agent/settled: that fires per terminal turn and still flickers mid-retry, whereas idle means "parked, waiting for queued work" — pi's actual "no retry/compaction/follow-up pending" | adapt | | pi.events extension bus | cordis pi-ext/* namespaced events (and emit is real, not a silent no-op) | adapt | | ctx.isIdle | live agent.status read | direct | | ctx.hasPendingMessages | inbox depth tracked through agent/inbox/* lifecycle events | adapt | | ctx.abort() | agent.cancel({kind:'user'}) — pi's abort is a human interrupt | adapt | | ctx.compact() | ctx.compact.compactNow() with the agent's reserveTurnAdmission(); fire-and-forget, failed attempts stay durable in the log | adapt | | ctx.getContextUsage() | folded from the log: latest request/context window + latest assistant/message usage; usage before a compact/start is stale (pi's "null right after compaction") | adapt | | ctx.getSystemPrompt / pi.getAllTools / getActiveTools | the latest request's assembled request/header — the exact text/tool set the model saw | adapt | | pi.getCommands | ctx.commands.list(agent); source/sourceInfo omitted — dsh descriptors carry no pi provenance | adapt | | pi.exec() | ctx.shell ShellExecutor resolve()run(); pi's argv words are shell-quoted, since dsh takes a shell string where pi never uses a shell | adapt | | pi.sendUserMessage() | agent.followup(), or agent.steer() for deliverAs: 'steer'. NOT inject(): dsh documents that as appending context without running the model, while pi's contract always opens a turn | adapt | | ctx.ui.notify / setStatus / setWidget / setFooter | logged; strictUi promotes these to unsupported too | degrade | | ctx.ui.theme | plain-text Theme — identity styling, correct for a host with no ANSI surface | direct | | ctx.ui.confirm / select / input | dsh's ctx.userQuestions service (@deepseek-ai/dsh-user-questions): when the host mounted a UI provider the adapter asks the real human (confirm → Yes/No question, select → option list, input → free-text form; a cancelled/skipped ask maps to pi's undefined/false, which is a real outcome, not a fabricated choice). The calling agent and the active call's abort signal are passed through, so dsh's live-caller boundary (CALLER_NOT_LIVE/DELEGATED_CALLER — an owned child agent has no human to ask) and cancellation apply. When the host is headless — no service, or the service answers NO_PROVIDER — the ask resolves to pi's cancel convention (undefined/false) with a loud log, instead of inventing the answer; strictUi: true raises instead | adapt-interactive | | ctx.hasUI | live probe of the ctx.userQuestions SERVICE: it reports that the service is mounted, not that a UI provider sits behind it — with no provider registered the interactive seams above answer no-selection (loudly logged) rather than asking a human | adapt | | ctx.ui.editor / custom / onTerminalInput | no dsh vocabulary exists for these richer interaction shapes | unsupported | | ctx.isProjectTrusted, ctx.sessionManager.getSessionFile, ctx.modelRegistry.*, pi.getThinkingLevel | no truthful dsh answer | unsupported | | setSessionName / getSessionName (dsh HAS ctx.sessionTitle, but the service would not register in any host wiring reachable from this repo's harness, so the bridge is unproven and therefore unclaimed) / setModel / setThinkingLevel / registerProvider / registerShortcut / register*Renderer | — | degrade (loud) | | pi.appendEntry / setLabel + ctx.sessionManager.getEntries / getBranch / getEntry / getLeafEntry / getLeafId / getLabel / getHeader | the dsh session log — pi/entry and pi/label plugin-owned events, projected back through SessionProjection. See that module for where the two models diverge (linear log, so getBranch()getEntries()) | adapt | | unmapped pi events (model_select, message_*, tool_execution_*, before_provider_*, …) | — | degrade (loud at subscribe time) |

Usage

Install via a plugin manager

The published lib/ bundle is self-contained (jiti/schemastery inlined; only cordis stays external, so the host instance keeps service identity) and the repo carries a dsh.plugin.json manifest, so dsh plugin managers can install it directly from git:

dshx install https://github.com/cyzlmh/dsh-pi-adapter.git   # marisa (dir|tgz|git-url)
# plugin-registry (dir / tarball):
git clone https://github.com/cyzlmh/dsh-pi-adapter.git && dsh plugin install ./dsh-pi-adapter

Build from source

pnpm install
pnpm build       # tsdown bundle -> lib/index.js + tsc types -> lib/types/ (lib/ is checked in for manager installs)
pnpm typecheck
pnpm test              # hermetic fixtures + real dsh registry integration (self-skips without a sibling dsh checkout)
PI_ADAPTER_REAL_EXT=1 pnpm test   # opt-in: also load real extensions from ~/.pi/agent/extensions

The integration suite imports the built dsh packages from a sibling npm installation of @deepseek-ai/dsh (../dsh-npm by default, override with DSH_REPO=/path/to/dsh-npm-installation) and runs registrations, tool execution, and the pre-execute waterfall veto against the real ToolRuntime / CommandRuntime.

Mount as a cordis plugin in a dsh cordis.yml (verified end-to-end with the headless-agent example — a live DeepSeek request called a bridged pi_echo tool and a real ~/.pi todo.ts extension):

# Plugin specifiers resolve against this file's location, so a relative path
# to the BUILT adapter entry works without publishing the package.
# NOTE the asymmetry: `extensions`/`scanDirs` entries below resolve against
# the HOST PROCESS cwd, not this file — prefer absolute paths in overlays.
- id: pi-adapter
  name: ../../../dsh-pi-adapter/lib/index.js
  config:
    extensions:
      - ../dsh-pi-adapter/test/fixtures   # resolved against process cwd
    includePiHome: false     # set true to also scan ~/.pi/agent/extensions
    toolPrefix: 'pi_'        # avoid collisions with built-in tools
    strictUi: false
    onUnsupported: disable   # 'fail' refuses to start unless every extension fits

Run it (from the dsh repo root, needs DEEPSEEK_API_KEY via $DSH_HOME/.env):

node --import tsx packages/examples/cli-demo/src/bin.ts \
  --config examples/headless-agent/your-overlay.yml \
  "Use the pi_echo tool with text 'hello dsh' and report what it returned."

Layout

src/
  index.ts              # cordis plugin entry (apply + Config schema)
  compat/ExtensionApi.ts# ExtensionAPI bridge — full interface surface
  loader/jiti-loader.ts # jiti loading + pi-runtime alias resolution
test/
  fixtures/             # hermetic pi extensions (import-free factories)
  fixtures.test.ts      # loader tests (hermetic + opt-in real ~/.pi loading)
  bridge.test.ts        # unit tests against a contract-fake ctx (mirrors dsh's
                        # register() output validation, waterfall veto, frozen args)
  integration.test.ts   # ground truth against the real built dsh registry
  artifact.test.ts      # artifact plane: the published lib/index.js under plain
                        # Node (via scripts/artifact-smoke.mjs), guarding the
                        # bundled jiti's runtime babel.cjs
demo/                   # live E2E in a real dsh TUI session — see demo/README.md
scripts/
  copy-babel.mjs        # build step: ship jiti's dist/babel.cjs at package root
  artifact-smoke.mjs    # plain-Node smoke used by test/artifact.test.ts

Status

Working bridge for the core seams — tool/command registration and execution, tool_call/tool_result waterfalls, session/turn/agent lifecycle events, exec(), and loud degradation for everything else — verified against the real built dsh registry AND end-to-end through the dsh headless-agent example with live model calls (fixture pi_echo and a real ~/.pi todo.ts extension both executed). Remaining gaps: per-prompt agent_start (dsh has no exact equivalent), ctx.ui.editor/custom and other rich interaction shapes, provider/model bridging, pi's custom session-entry persistence, and event.input argument rewriting in tool_call (dsh pre-execute decisions cannot rewrite arguments).