diff --git a/.github/workflows/website-pages.yml b/.github/workflows/website-pages.yml index 842bbc9..5670f78 100644 --- a/.github/workflows/website-pages.yml +++ b/.github/workflows/website-pages.yml @@ -16,6 +16,13 @@ on: # this event must not be path-filtered. pull_request: workflow_dispatch: + # The marketplace repository pings us when its index rebuilds, so the + # generated plugin pages pick up new listings without a website commit; + # the weekly run is the safety net for missed pings (the token is optional). + repository_dispatch: + types: [market-updated] + schedule: + - cron: '41 5 * * 1' permissions: contents: read diff --git a/script/tests/market-generator.test.mjs b/script/tests/market-generator.test.mjs new file mode 100644 index 0000000..4183cd7 --- /dev/null +++ b/script/tests/market-generator.test.mjs @@ -0,0 +1,86 @@ +/** Website marketplace generation boundary tests. @module script/tests/market-generator */ + +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { test } from 'node:test' +import { detailPage, installCommand, readmeBlock, requiresDedicatedProfile, validateMarketIndex } from '../../website/scripts/generate-market.mjs' + +function entry(overrides = {}) { + return { + id: 'loop', + source: 'official', + displayName: 'Loop', + description: 'Recurring prompts.', + descriptionZh: '循环提示。', + author: { name: 'Test' }, + category: 'workflow', + status: 'stable', + surfaces: { server: {} }, + install: { rows: [{ name: 'dsh-loop', npm: { spec: 'dsh-loop' } }] }, + ...overrides, + } +} + +test('market generator rejects unsafe paths and malformed install rows', () => { + assert.throws(() => validateMarketIndex(null), /not an object/) + assert.throws(() => validateMarketIndex({ schemaVersion: 2, entries: [] }), /schema version 2/) + assert.throws(() => validateMarketIndex({ schemaVersion: 1 }), /no entries array/) + assert.throws(() => validateMarketIndex({ schemaVersion: 1, entries: [null] }), /not an object/) + assert.throws(() => validateMarketIndex({ schemaVersion: 1, entries: [entry({ id: '../../outside' })] }), /unsafe id/) + assert.throws(() => validateMarketIndex({ schemaVersion: 1, entries: [entry(), entry()] }), /repeats id/) + assert.throws(() => validateMarketIndex({ schemaVersion: 1, entries: [entry({ displayName: '' })] }), /displayName/) + assert.throws(() => validateMarketIndex({ schemaVersion: 1, entries: [entry({ source: 'unknown' })] }), /unknown source/) + assert.throws(() => validateMarketIndex({ schemaVersion: 1, entries: [entry({ status: 'unknown' })] }), /unknown status/) + assert.throws(() => validateMarketIndex({ schemaVersion: 1, entries: [entry({ surfaces: null })] }), /surfaces/) + assert.throws(() => validateMarketIndex({ schemaVersion: 1, entries: [entry({ install: { rows: [] } })] }), /install.rows/) + assert.throws(() => validateMarketIndex({ schemaVersion: 1, entries: [entry({ install: { rows: [{}] } })] }), /package name and source/) + assert.throws(() => validateMarketIndex({ schemaVersion: 1, entries: [entry({ install: { rows: [{ name: 'x', npm: {} }] } })] }), /npm sources/) + assert.throws(() => validateMarketIndex({ schemaVersion: 1, entries: [entry({ install: { rows: [{ name: 'x', github: {} }] } })] }), /GitHub sources/) + const valid = { schemaVersion: 1, entries: [entry()] } + assert.equal(validateMarketIndex(valid), valid.entries) + const fallback = JSON.parse(readFileSync(new URL('../../website/scripts/market-fallback.json', import.meta.url), 'utf8')) + assert.equal(validateMarketIndex(fallback).length, 14) +}) + +test('third-party README content stays inert in generated Vue Markdown', () => { + const malicious = entry({ + displayName: 'Line one\n# injected', + links: { repo: 'javascript:alert(1)', docs: 'https://example.com/docs' }, + readme: '\n{{ globalThis.location }}\n', + }) + const block = readmeBlock(malicious) + assert.match(block, /^
 {
+  const github = entry({ install: { rows: [
+    { name: 'a', github: { repo: 'owner/repo', ref: 'main', subdir: 'plugins/a' } },
+    { name: 'b', github: { repo: 'owner/repo', ref: 'main', subdir: 'plugins/b' } },
+  ] } })
+  assert.equal(installCommand(github), "dsh plugin --profile  add 'github:owner/repo#main&path:plugins/a' 'github:owner/repo#main&path:plugins/b'")
+  const mixed = entry({ install: { rows: [
+    { name: 'a', npm: { spec: 'a' } },
+    { name: 'b', github: { repo: 'owner/repo', ref: 'main' } },
+  ] } })
+  assert.equal(installCommand(mixed), 'dsh plugin --profile  add ')
+})
+
+test('ACP is documented as a dedicated-profile automation frontend', () => {
+  const acp = entry({
+    id: 'acp',
+    displayName: 'ACP Server',
+    install: { rows: [{ name: '@deepseek-ai/dsh-acp', npm: { spec: '@deepseek-ai/dsh-acp' } }] },
+  })
+  assert.equal(requiresDedicatedProfile(entry()), false)
+  assert.equal(requiresDedicatedProfile(acp), true)
+  assert.equal(installCommand(acp), 'dsh plugin --profile  add @deepseek-ai/dsh-acp')
+  assert.match(detailPage(acp, 'en'), /owns stdio.*dedicated profile/su)
+  assert.match(detailPage(acp, 'zh'), /独占 stdio.*独立 profile/su)
+})
diff --git a/website/.gitignore b/website/.gitignore
new file mode 100644
index 0000000..c71ee85
--- /dev/null
+++ b/website/.gitignore
@@ -0,0 +1,3 @@
+market/p/
+market/catalog.json
+en/market/p/
diff --git a/website/.vitepress/config.ts b/website/.vitepress/config.ts
index d5b88a3..c1c207e 100644
--- a/website/.vitepress/config.ts
+++ b/website/.vitepress/config.ts
@@ -45,11 +45,13 @@ const sharedTheme = {
 // 使用与定制,开发手册收口 /plugins/ 路径下的插件开发内容。
 const navZh = [
   { text: '用户手册', link: '/guide/', activeMatch: '/(guide|dsh|features|reference)' },
+  { text: '插件市场', link: '/market/', activeMatch: '^/market' },
   { text: '开发手册', link: '/plugins/', activeMatch: '^/plugins' },
 ]
 
 const navEn = [
   { text: 'User manual', link: '/en/guide/', activeMatch: '/en/(guide|dsh|features|reference)' },
+  { text: 'Marketplace', link: '/en/market/', activeMatch: '/en/market' },
   { text: 'Developer manual', link: '/en/plugins/', activeMatch: '^/en/plugins' },
 ]
 
@@ -99,6 +101,24 @@ const sidebarZh = {
       ],
     },
   ],
+  '/market/': [
+    {
+      text: '市场',
+      items: [
+        { text: '插件目录', link: '/market/' },
+        { text: '安装与更新', link: '/market/installing' },
+        { text: '信任与安全', link: '/market/trust' },
+      ],
+    },
+    {
+      text: '收录',
+      items: [
+        { text: '提交你的插件', link: '/market/submit' },
+        { text: 'Manifest 规范', link: '/market/manifest' },
+        { text: '审查清单', link: '/market/review' },
+      ],
+    },
+  ],
   '/plugins/': [
     {
       text: '开始',
@@ -185,6 +205,24 @@ const sidebarEn = {
       ],
     },
   ],
+  '/en/market/': [
+    {
+      text: 'Market',
+      items: [
+        { text: 'Plugin catalog', link: '/en/market/' },
+        { text: 'Installing & updating', link: '/en/market/installing' },
+        { text: 'Trust & safety', link: '/en/market/trust' },
+      ],
+    },
+    {
+      text: 'Listings',
+      items: [
+        { text: 'Submit your plugin', link: '/en/market/submit' },
+        { text: 'Manifest spec', link: '/en/market/manifest' },
+        { text: 'Review checklist', link: '/en/market/review' },
+      ],
+    },
+  ],
   '/en/plugins/': [
     {
       text: 'Getting started',
diff --git a/website/.vitepress/theme/components/MarketCatalog.vue b/website/.vitepress/theme/components/MarketCatalog.vue
new file mode 100644
index 0000000..b1ae8dc
--- /dev/null
+++ b/website/.vitepress/theme/components/MarketCatalog.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
diff --git a/website/.vitepress/theme/custom.css b/website/.vitepress/theme/custom.css
index 310948f..21e5e62 100644
--- a/website/.vitepress/theme/custom.css
+++ b/website/.vitepress/theme/custom.css
@@ -532,3 +532,8 @@ html:not(.dark) .brand-home .VPNavBarTitle .VPImage.dark,
     padding: 72px 20px 100px;
   }
 }
+
+.vp-doc .market-readme {
+  white-space: pre-wrap;
+  overflow-wrap: anywhere;
+}
diff --git a/website/.vitepress/theme/index.ts b/website/.vitepress/theme/index.ts
index b8c8884..ff9730b 100644
--- a/website/.vitepress/theme/index.ts
+++ b/website/.vitepress/theme/index.ts
@@ -1,6 +1,7 @@
 import DefaultTheme from 'vitepress/theme'
 import type { Theme } from 'vitepress'
 import Layout from './Layout.vue'
+import MarketCatalog from './components/MarketCatalog.vue'
 import MermaidDiagram from './MermaidDiagram.vue'
 import './custom.css'
 
@@ -9,5 +10,6 @@ export default {
   Layout,
   enhanceApp({ app }) {
     app.component('MermaidDiagram', MermaidDiagram)
+    app.component('MarketCatalog', MarketCatalog)
   },
 } satisfies Theme
diff --git a/website/en/market/index.md b/website/en/market/index.md
new file mode 100644
index 0000000..7c513f7
--- /dev/null
+++ b/website/en/market/index.md
@@ -0,0 +1,15 @@
+---
+title: Plugin Marketplace
+---
+
+# Plugin Marketplace
+
+Browse vetted Mayfly / dsh plugins: official ones (Ephemeral AI Lab, sources in the [marketplace repository](https://github.com/Ephemeral-AI-Lab/dsh-plugins)), curated dsh optional plugins, and community submissions.
+
+Every entry installs directly through Mayfly's `/plugin` command, or manually with `dsh plugin --profile  add ` (npm and GitHub sources both work).
+
+- Installing? See [Installing & updating](/en/market/installing).
+- Trusting? See [Trust & safety](/en/market/trust).
+- Submitting your own? See [Submit your plugin](/en/market/submit).
+
+
diff --git a/website/en/market/installing.md b/website/en/market/installing.md
new file mode 100644
index 0000000..ff324e1
--- /dev/null
+++ b/website/en/market/installing.md
@@ -0,0 +1,40 @@
+---
+title: Installing & updating
+---
+
+# Installing & updating
+
+## Inside Mayfly (recommended)
+
+```
+/plugin                 # browse, search, open details
+/plugin install     # e.g. /plugin install loop
+/plugin install  --source github   # install from a GitHub source
+/plugin list            # installed / updates / removed-from-market
+/plugin uninstall 
+/plugin refresh         # force an index refresh
+```
+
+In the list, **Enter** opens the detail panel; `i` installs, `u` removes, `r` refreshes; type to filter.
+
+## With the dsh CLI (any profile)
+
+```sh
+# npm source
+dsh plugin --profile mayfly add @deepseek-ai/dsh-terminal-bash @deepseek-ai/dsh-tool-terminal
+
+# GitHub source (monorepo subdirectories work; pin a commit when possible)
+dsh plugin --profile mayfly add 'github:Ephemeral-AI-Lab/dsh-plugins#main&path:plugins/loop'
+```
+
+## After installing
+
+**Bundle membership is a startup boundary**: after install or removal, restart Mayfly (or `dsh --profile `) and **start a new session** for the new tools and commands to appear.
+
+- Plugins with native dependencies (node-pty for codex-terminal) write their `allowBuilds` allowance into the profile's `pnpm-workspace.yaml` at install time — the pnpm ≥10 permit for build scripts.
+- Entries with `profile-patch` activation (most dsh optional plugins) append assembly rows to the profile's `cordis.patch.yml`; `/plugin` does this automatically, manual installs follow each plugin's README.
+- **ACP Server is different**: it is an automation frontend that owns stdin/stdout. Mayfly shows it for discovery and removal but refuses to install it into the current TUI profile; create a dedicated non-Mayfly profile for ACP.
+
+## Updates & offline
+
+`/plugin list` marks entries with newer versions (`↑`). The index caches under `$DSH_HOME/storages/mayfly-plugin-market/` and refreshes hourly by default; on failure the cached catalog still renders. Point the `mayfly.marketIndexUrl` setting at a mirror or a private marketplace if you need to.
diff --git a/website/en/market/manifest.md b/website/en/market/manifest.md
new file mode 100644
index 0000000..980c727
--- /dev/null
+++ b/website/en/market/manifest.md
@@ -0,0 +1,62 @@
+---
+title: Manifest spec
+---
+
+# Manifest spec
+
+A manifest is **discovery- and install-time metadata** — one JSON file per listing; the authoritative schema lives at [`registry/schema/plugin-manifest.v1.json`](https://github.com/Ephemeral-AI-Lab/dsh-plugins/blob/main/registry/schema/plugin-manifest.v1.json) in the marketplace repository. It never participates in runtime loading — the runtime contract of every plugin remains its package plus its `cordis.patch.yml`.
+
+## Field overview
+
+| Field | Required | Meaning |
+| --- | --- | --- |
+| `schemaVersion` | ✅ | currently `1` |
+| `id` | ✅ | unique marketplace slug, used by `/plugin install ` |
+| `source` | ✅ | `official` / `dsh` / `community`, must match the directory |
+| `displayName` | ✅ | list name |
+| `description` / `descriptionZh` | ✅ | one-liners, English and Chinese |
+| `author` | ✅ | `{ name, url? }` |
+| `links` |  | `repo` / `docs` / `npm` |
+| `category` | ✅ | tools / ui / provider / workflow / testing / integration |
+| `status` | ✅ | stable / beta / unstable / deprecated / removed (the last two need `statusNote`) |
+| `surfaces` | ✅ | what the plugin **contributes**, see below |
+| `provides` |  | `{ tools: [...], commands: ["/..."] }`, for display and search |
+| `install.rows[]` | ✅ | ordered install units, see below |
+| `engines` |  | `{ dsh, mayfly, node }` ranges; `surfaces.tui` requires `mayfly` |
+| `capabilities` |  | disclosure (e.g. `shell`, `network`, `credentials`) |
+| `verified` | ✅ | `{ at, packages: [{ name, version }] }` recorded at review |
+
+## surfaces
+
+```jsonc
+"surfaces": {
+  "server": {},                                     // dsh tools/services, any frontend
+  "web":  { "clientModule": true },                // dsh Web React client module
+  "tui":  { "contributions": ["panes", "status"] } // Mayfly UI contributions
+}
+```
+
+The manifest declares **contributions**; usefulness per frontend is derived: **has `server` OR has that frontend's own contribution**. A `server + web` plugin shows "tools work, panel is dsh-Web-only" inside Mayfly.
+
+## install.rows
+
+```jsonc
+"install": {
+  "allowBuilds": ["node-pty"],       // packages allowed to run install scripts
+  "rows": [
+    {
+      "id": "loop",                   // cordis patch row id (required for profile-patch)
+      "name": "dsh-loop",             // runtime package name (the reconcile key)
+      "activation": "bundle",         // bundle (default) | profile-patch
+      "config": {},                   // default config for profile-patch rows
+      "npm":    { "spec": "dsh-loop" },
+      "github": { "repo": "Ephemeral-AI-Lab/dsh-plugins", "ref": "main", "subdir": "plugins/loop" }
+    }
+  ]
+}
+```
+
+- Each row declares at least one of `npm` / `github`; prefer `npm` when published.
+- GitHub spec grammar: `github:/#`, plus `&path:` for monorepos.
+- `activation: profile-patch` rows append to the profile's `cordis.patch.yml` after install (`/plugin` does it automatically).
+- All rows of an entry **install and remove together** — passing them in one command is what lets sibling packages satisfy each other's peers.
diff --git a/website/en/market/review.md b/website/en/market/review.md
new file mode 100644
index 0000000..a399671
--- /dev/null
+++ b/website/en/market/review.md
@@ -0,0 +1,31 @@
+---
+title: Review checklist
+---
+
+# Review checklist
+
+Listings are reviewed on marketplace-repository PRs; the source of truth is [`registry/review-checklist.md`](https://github.com/Ephemeral-AI-Lab/dsh-plugins/blob/main/registry/review-checklist.md). In brief:
+
+## Machine gates (CI, automatic)
+
+- Schema validation passes; `id` unique; `source` matches the directory; `engines.mayfly` present whenever `surfaces.tui` is declared;
+- every npm row exists on the registry with a healthy latest tarball (`cordis.patch.yml`, `dsh.bundle.patch`, or the row declares `activation: profile-patch`);
+- every GitHub row scratch-installs into a throwaway profile whose installed package carries `dsh.bundle.patch` and build output;
+- install lifecycle scripts and native binaries are flagged for the human pass.
+
+## Human review (before merge)
+
+- The PR author controls the package (authorization link in their repository) or the author approved the listing;
+- descriptions and `surfaces` / `provides` / `capabilities` are honest (grep the source for the tool and command names);
+- unload is clean: commands, UI, and listeners disappear with the Cordis fiber (source uses `ctx.effect`/disposers, no global state);
+- `verified.at` is the review date, `verified.packages` records exact versions; GitHub-only entries pin a commit.
+
+## After merge
+
+- The `index-publish` workflow rebuilds `dist/` (via an auto-merged PR) and triggers this site's rebuild;
+- the weekly re-verification marks `updateAvailable` on new upstream versions until the diff is re-reviewed.
+
+## Removal
+
+- Set `status: deprecated/removed` with a `statusNote`; **never delete the file**;
+- security removals say so plainly in the note.
diff --git a/website/en/market/submit.md b/website/en/market/submit.md
new file mode 100644
index 0000000..c5d8f38
--- /dev/null
+++ b/website/en/market/submit.md
@@ -0,0 +1,27 @@
+---
+title: Submit your plugin
+---
+
+# Submit your plugin
+
+## Prerequisites
+
+1. Publish the plugin as an ordinary Cordis package per the developer manual's [publishing guide](/en/plugins/publishing):
+   - `package.json` declares `dsh.bundle.patch` (self-activating bundle), or the plugin assembles through profile patch rows;
+   - the installed artifact ships `cordis.patch.yml` and build output (npm packages via `files`; GitHub sources must commit `lib/` — git installs fetch the source tree).
+2. Prepare bilingual one-line descriptions, a capabilities disclosure (shell/network/credentials), and the tool & command list.
+
+## The flow
+
+1. Copy `registry/submission-template.json` from the [marketplace repository](https://github.com/Ephemeral-AI-Lab/dsh-plugins) to `registry/community/.json` and fill it in;
+2. Open a PR there; CI runs the machine gates (schema validation, npm existence, tarball inspection, GitHub scratch installs);
+3. Open an issue or comment in **your own** repository linking the PR, as the authorization evidence (prevents impersonation);
+4. A maintainer reviews against the [review checklist](/en/market/review); merging lists the plugin — the `index-publish` workflow rebuilds the index and this site's page appears after the rebuild.
+
+## Rules in brief
+
+- One manifest per plugin; the `id` is permanent — to withdraw, set `status: removed` with a reason instead of deleting the file.
+- npm sources preferred; unpublished packages may use a GitHub source with a commit-pinned `ref`.
+- Declare `surfaces` honestly; `capabilities` is disclosure for reviewers and users, not a runtime permission.
+
+Field-by-field details are in the [Manifest spec](/en/market/manifest).
diff --git a/website/en/market/trust.md b/website/en/market/trust.md
new file mode 100644
index 0000000..91d255b
--- /dev/null
+++ b/website/en/market/trust.md
@@ -0,0 +1,29 @@
+---
+title: Trust & safety
+---
+
+# Trust & safety
+
+## Three tiers
+
+| Source | Meaning |
+| --- | --- |
+| **official** | Ephemeral AI Lab plugins, sources in the [marketplace repository](https://github.com/Ephemeral-AI-Lab/dsh-plugins) |
+| **dsh** | DeepSeek's own optional dsh plugins |
+| **community** | third-party listings that passed the machine gates and human review |
+
+## What verified means
+
+Every entry records the package versions and date **at review time** (the Verification block on each page). Listing runs the lightweight tier: entries track the latest version after review, and a weekly scheduled re-verification flags new upstream releases. `verified` means "this version was reviewed", not an ongoing guarantee.
+
+## The honest disclaimer
+
+**Listing is disclosure and review, not a sandbox.** Plugins run with your user privileges: installing a third-party plugin equals installing an arbitrary npm package. Before installing, read the page's:
+
+- **Capabilities**: shell execution, network, credential access, and so on;
+- **Frontend support**: `server` (any frontend), `web` (dsh Web panel), `tui` (Mayfly-native UI);
+- **allowBuilds**: which packages the entry permits to run install scripts (native builds).
+
+## Removal & reporting
+
+Removed plugins keep their page, marked `removed` with the reason — installed users see "removed from the market" in `/plugin list`. Report malicious behavior to the [marketplace repository issues](https://github.com/Ephemeral-AI-Lab/dsh-plugins/issues).
diff --git a/website/en/plugins/publishing.md b/website/en/plugins/publishing.md
index 077fd06..739af9c 100644
--- a/website/en/plugins/publishing.md
+++ b/website/en/plugins/publishing.md
@@ -17,3 +17,5 @@ npm publish --access public
 
 Publish only after explicit authorization for the exact package, version, and
 tag. GitHub repository creation and npm publication are separate actions.
+
+After publishing, [submit the plugin to the Marketplace](/en/market/submit) so Mayfly users can find it with `/plugin`.
diff --git a/website/market/index.md b/website/market/index.md
new file mode 100644
index 0000000..8e523c7
--- /dev/null
+++ b/website/market/index.md
@@ -0,0 +1,15 @@
+---
+title: 插件市场
+---
+
+# 插件市场
+
+浏览经过收录的 Mayfly / dsh 插件:官方(Ephemeral AI Lab,源码在[市场仓库](https://github.com/Ephemeral-AI-Lab/dsh-plugins))、dsh 官方可选插件、以及社区提交的插件。
+
+所有条目都可在 Mayfly 中用 `/plugin` 命令直接安装,也可以用 `dsh plugin --profile  add <包名>` 手动安装(支持 npm 与 GitHub 两种来源)。
+
+- 想装插件?见[安装与更新](/market/installing)。
+- 想知道哪些可信?见[信任与安全](/market/trust)。
+- 想提交自己的插件?见[提交你的插件](/market/submit)。
+
+
diff --git a/website/market/installing.md b/website/market/installing.md
new file mode 100644
index 0000000..689cb9f
--- /dev/null
+++ b/website/market/installing.md
@@ -0,0 +1,40 @@
+---
+title: 安装与更新
+---
+
+# 安装与更新
+
+## 在 Mayfly 中(推荐)
+
+```
+/plugin                 # 浏览、搜索、查看详情
+/plugin install     # 例如 /plugin install loop
+/plugin install  --source github   # 从 GitHub 源安装
+/plugin list            # 已安装 / 可更新 / 已从市场移除
+/plugin uninstall 
+/plugin refresh         # 强制刷新索引
+```
+
+列表里按 **Enter** 打开详情,`i` 安装、`u` 移除、`r` 刷新;直接输入即搜索。
+
+## 用 dsh CLI(任何 profile)
+
+```sh
+# npm 源
+dsh plugin --profile mayfly add @deepseek-ai/dsh-terminal-bash @deepseek-ai/dsh-tool-terminal
+
+# GitHub 源(monorepo 子目录也支持;建议 pin commit)
+dsh plugin --profile mayfly add 'github:Ephemeral-AI-Lab/dsh-plugins#main&path:plugins/loop'
+```
+
+## 安装后
+
+**Bundle 成员是启动边界**:安装/移除后需重启 Mayfly(或 `dsh --profile `)并**新建会话**,新插件的工具与命令才生效。
+
+- 带原生依赖的插件(如 codex-terminal 的 node-pty)会在安装时把 `allowBuilds` 写进 profile 的 `pnpm-workspace.yaml`——这是 pnpm ≥10 运行构建脚本的许可。
+- `profile-patch` 激活的条目(dsh 官方可选插件多为此类)会把组装行写进 profile 的 `cordis.patch.yml`;`/plugin` 自动完成,手动安装时按各插件文档补行。
+- **ACP Server 是例外**:它是会独占 stdin/stdout 的自动化前端。Mayfly 只展示并允许移除,不允许把它安装进当前 TUI profile;ACP 必须使用独立的非 Mayfly profile。
+
+## 更新与离线
+
+`/plugin list` 会标出有新版本的插件(`↑`)。索引缓存于 `$DSH_HOME/storages/mayfly-plugin-market/`,默认每小时刷新;获取失败时展示缓存目录,可在设置 `mayfly.marketIndexUrl` 换用镜像或私有市场。
diff --git a/website/market/manifest.md b/website/market/manifest.md
new file mode 100644
index 0000000..661dea6
--- /dev/null
+++ b/website/market/manifest.md
@@ -0,0 +1,62 @@
+---
+title: Manifest 规范
+---
+
+# Manifest 规范
+
+清单(manifest)是**发现与安装期的元数据**,一份 JSON 一个条目,权威 schema 在市场仓库 [`registry/schema/plugin-manifest.v1.json`](https://github.com/Ephemeral-AI-Lab/dsh-plugins/blob/main/registry/schema/plugin-manifest.v1.json)。它不参与运行时加载——运行时契约始终是包本身加它的 `cordis.patch.yml`。
+
+## 字段速览
+
+| 字段 | 必填 | 含义 |
+| --- | --- | --- |
+| `schemaVersion` | ✅ | 目前为 `1` |
+| `id` | ✅ | 市场内唯一 slug,`/plugin install ` 使用 |
+| `source` | ✅ | `official` / `dsh` / `community`,须与目录一致 |
+| `displayName` | ✅ | 列表显示名 |
+| `description` / `descriptionZh` | ✅ | 双语一句话描述 |
+| `author` | ✅ | `{ name, url? }` |
+| `links` |  | `repo` / `docs` / `npm` |
+| `category` | ✅ | tools / ui / provider / workflow / testing / integration |
+| `status` | ✅ | stable / beta / unstable / deprecated / removed(后两者须写 `statusNote`) |
+| `surfaces` | ✅ | 插件**贡献**什么,见下 |
+| `provides` |  | `{ tools: [...], commands: ["/..."] }`,用于展示与搜索 |
+| `install.rows[]` | ✅ | 有序安装单元,见下 |
+| `engines` |  | `{ dsh, mayfly, node }` 版本范围;声明 `surfaces.tui` 时须有 `mayfly` |
+| `capabilities` |  | 能力披露(如 `shell`、`network`、`credentials`) |
+| `verified` | ✅ | `{ at, packages: [{ name, version }] }` 审查时记录 |
+
+## surfaces
+
+```jsonc
+"surfaces": {
+  "server": {},                                  // dsh 工具/服务,任何前端
+  "web":  { "clientModule": true },             // dsh Web 的 React client module
+  "tui":  { "contributions": ["panes", "status"] } // Mayfly UI 贡献
+}
+```
+
+manifest 声明的是**贡献**;某前端下的可用性由此推导:**有 `server` 或有该前端自己的贡献**。`server + web` 的插件在 Mayfly 里显示"工具可用,面板仅 dsh Web"。
+
+## install.rows
+
+```jsonc
+"install": {
+  "allowBuilds": ["node-pty"],        // 允许跑安装脚本的包(写入 profile 工作区)
+  "rows": [
+    {
+      "id": "loop",                    // cordis patch 行 id(profile-patch 必填)
+      "name": "dsh-loop",              // 运行时包名(reconcile 键)
+      "activation": "bundle",          // bundle(默认)| profile-patch
+      "config": {},                    // profile-patch 行的默认配置
+      "npm":    { "spec": "dsh-loop" },
+      "github": { "repo": "Ephemeral-AI-Lab/dsh-plugins", "ref": "main", "subdir": "plugins/loop" }
+    }
+  ]
+}
+```
+
+- 每行至少声明 `npm` 或 `github` 之一;已发布 npm 的优先 `npm`。
+- GitHub spec 语法:`github:/#`,monorepo 加 `&path:<子目录>`。
+- `activation: profile-patch` 的行在安装后追加进 profile 的 `cordis.patch.yml`(`/plugin` 自动做)。
+- 一个条目的所有行**同装同卸**——同传才能让兄弟包满足彼此的 peer 依赖。
diff --git a/website/market/review.md b/website/market/review.md
new file mode 100644
index 0000000..be81a58
--- /dev/null
+++ b/website/market/review.md
@@ -0,0 +1,31 @@
+---
+title: 审查清单
+---
+
+# 审查清单
+
+收录审查在市场仓库的 PR 上进行,清单原文:[`registry/review-checklist.md`](https://github.com/Ephemeral-AI-Lab/dsh-plugins/blob/main/registry/review-checklist.md)。摘要:
+
+## 机器门禁(CI 自动)
+
+- schema 校验通过;`id` 唯一;`source` 与目录一致;`surfaces.tui` 声明时必有 `engines.mayfly`;
+- 每个 npm 行在 registry 存在,最新 tarball 体检(`cordis.patch.yml`、`dsh.bundle.patch`,或行声明为 `activation: profile-patch`);
+- 每个 GitHub 行 scratch 安装通过,装出的包带 `dsh.bundle.patch` 与构建产物;
+- 安装期脚本(postinstall 等)与原生二进制被标记给人工审查。
+
+## 人工审查(合并前)
+
+- PR 作者控制该包(其仓库的授权链接),或作者明确同意收录;
+- 描述与 `surfaces`、`provides`、`capabilities` 如实(对照源码 grep 工具与命令名);
+- 卸载干净:Cordis Fiber 卸载后命令/UI/监听全部消失(源码用 `ctx.effect`/disposer,无全局态);
+- `verified.at` 为审查当日,`verified.packages` 记录精确版本;GitHub-only 条目 pin 了 commit。
+
+## 合并后
+
+- `index-publish` 工作流自动重建 `dist/`(经自动合并 PR)并触发本站重建;
+- 每周巡检标记新版本 `updateAvailable`,复审 diff 后清除标记。
+
+## 下架
+
+- 置 `status: deprecated/removed` 并写 `statusNote`,**不删文件**;
+- 因安全原因下架的,在说明中直说。
diff --git a/website/market/submit.md b/website/market/submit.md
new file mode 100644
index 0000000..740b05b
--- /dev/null
+++ b/website/market/submit.md
@@ -0,0 +1,27 @@
+---
+title: 提交你的插件
+---
+
+# 提交你的插件
+
+## 前提
+
+1. 按开发手册的[发布指南](/plugins/publishing)把插件发布为普通 Cordis 包:
+   - `package.json` 声明 `dsh.bundle.patch`(自激活 bundle),或可经 profile patch 行组装;
+   - 安装产物内含 `cordis.patch.yml` 与构建输出(npm 包随 `files` 分发;GitHub 源需提交 `lib/`——git 安装装的是源码树)。
+2. 准备双语一句话描述、能力披露(shell/网络/凭据等)、以及工具与命令清单。
+
+## 流程
+
+1. 复制[市场仓库](https://github.com/Ephemeral-AI-Lab/dsh-plugins)的 `registry/submission-template.json` 为 `registry/community/.json` 并填写;
+2. 提 PR 到市场仓库;CI 会做机器门禁(schema 校验、npm 存在性、tarball 体检、GitHub 源 scratch 安装);
+3. 在**你自己的仓库**开 issue 或评论链接该 PR,作为收录授权证据(防冒名);
+4. 维护者按[审查清单](/market/review)人工过审,合并即收录;`index-publish` 工作流自动重建索引,本站详情页在重建后出现。
+
+## 规则摘要
+
+- 一个插件一个 manifest;`id` 永久占用,撤回请置 `status: removed` 并写明原因,不要删文件。
+- npm 源优先;未发布 npm 的包可用 GitHub 源,`ref` 建议 pin 到 commit。
+- `surfaces` 如实声明;`capabilities` 是给审查者和用户看的披露,不是运行时授权。
+
+Manifest 字段说明见 [Manifest 规范](/market/manifest)。
diff --git a/website/market/trust.md b/website/market/trust.md
new file mode 100644
index 0000000..6e73a8e
--- /dev/null
+++ b/website/market/trust.md
@@ -0,0 +1,29 @@
+---
+title: 信任与安全
+---
+
+# 信任与安全
+
+## 三档来源
+
+| 来源 | 含义 |
+| --- | --- |
+| **official** | Ephemeral AI Lab 官方插件,源码在[市场仓库](https://github.com/Ephemeral-AI-Lab/dsh-plugins) |
+| **dsh** | DeepSeek 官方发布的 dsh 可选插件 |
+| **community** | 社区提交、通过机器门禁与人工审查的第三方插件 |
+
+## verified 是什么
+
+每个条目记录**审查时**的包版本与日期(详情页"审核信息")。收录采用轻量档:审查后跟随最新版本,市场每周自动重新验证并在新版本出现时标记。`verified` 表示"这个版本被审过",不是持续保证。
+
+## 诚实声明
+
+**收录是披露与审查,不是沙箱。** 插件以你的用户权限运行:安装一个第三方插件,等同于安装一个任意 npm 包。安装前请看详情页的:
+
+- **能力披露**(capabilities):shell 执行、网络、凭据读取等;
+- **前端支持**:`server`(任何前端可用)、`web`(dsh Web 面板)、`tui`(Mayfly 原生 UI);
+- **allowBuilds**:该插件声明了哪些包允许运行安装脚本(原生编译等)。
+
+## 下架与举报
+
+下架的插件不删条目而是标记 `removed` 并注明原因——已安装的用户会在 `/plugin list` 看到"已从市场移除"。发现恶意行为请到[市场仓库](https://github.com/Ephemeral-AI-Lab/dsh-plugins/issues)举报。
diff --git a/website/package.json b/website/package.json
index 1ff497c..1482548 100644
--- a/website/package.json
+++ b/website/package.json
@@ -5,8 +5,9 @@
   "type": "module",
   "scripts": {
     "dev": "vitepress dev . --host 127.0.0.1 --port 5173",
-    "build": "vitepress build .",
-    "preview": "vitepress preview . --host 127.0.0.1 --port 4173"
+    "build": "node scripts/generate-market.mjs && vitepress build .",
+    "preview": "vitepress preview . --host 127.0.0.1 --port 4173",
+    "build:market": "node scripts/generate-market.mjs"
   },
   "devDependencies": {
     "mermaid": "^11.17.0",
diff --git a/website/plugins/publishing.md b/website/plugins/publishing.md
index 7bb2143..94beb5e 100644
--- a/website/plugins/publishing.md
+++ b/website/plugins/publishing.md
@@ -16,3 +16,5 @@ npm publish --access public
 
 只有用户明确授权 exact package/version/tag 后才执行 publish。GitHub repository
 创建与 npm 发布是两个独立动作。
+
+发布之后,把插件[提交到插件市场](/market/submit),Mayfly 用户就能用 `/plugin` 找到它。
diff --git a/website/scripts/generate-market.mjs b/website/scripts/generate-market.mjs
new file mode 100644
index 0000000..ff3ef56
--- /dev/null
+++ b/website/scripts/generate-market.mjs
@@ -0,0 +1,279 @@
+/**
+ * Generate the marketplace plugin pages from the published index before
+ * VitePress builds. Runs in both the PR check and the deploy job (the
+ * website package's build script) — VitePress only picks up files that
+ * exist before `vitepress build` starts.
+ *
+ * Sources `catalog.json` from MARKET_INDEX_URL (default: the marketplace
+ * repository's main branch). Unknown schema versions fail the build rather
+ * than rendering half-understood entries; an unreachable index falls back to
+ * the bundled snapshot with a loud warning (pre-merge CI, air-gapped runs).
+ *
+ * Output (git-ignored):
+ *   market/p//index.md        one detail page per entry (zh)
+ *   en/market/p//index.md     the same page (en)
+ *   market/catalog.json           the pruned index for 
+ */
+import { mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
+import { dirname, join } from 'node:path'
+import { fileURLToPath, pathToFileURL } from 'node:url'
+
+const websiteRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
+const DEFAULT_INDEX_URL = 'https://raw.githubusercontent.com/Ephemeral-AI-Lab/dsh-plugins/main/dist/catalog.json'
+const INDEX_URL = process.env.MARKET_INDEX_URL || DEFAULT_INDEX_URL
+const SUPPORTED_SCHEMA = 1
+const README_CAP = 32 * 1024
+const DEDICATED_PROFILE_PACKAGES = new Set(['@deepseek-ai/dsh-acp'])
+
+async function fetchIndex() {
+  let response
+  try {
+    response = await fetch(INDEX_URL, { signal: AbortSignal.timeout(30_000) })
+    if (!response.ok) throw new Error(`${INDEX_URL} -> HTTP ${response.status}`)
+  } catch (error) {
+    // The bundled fallback keeps builds green when the index URL is
+    // unreachable (CI before the marketplace repository ships dist/, air-gapped
+    // environments). It is a snapshot, not a live document — warn loudly.
+    console.warn(`market index fetch failed (${error instanceof Error ? error.message : String(error)}); falling back to scripts/market-fallback.json`)
+    return JSON.parse(readFileSync(join(websiteRoot, 'scripts', 'market-fallback.json'), 'utf8'))
+  }
+  return await response.json()
+}
+
+const TIER_LABEL = { official: '官方 official', dsh: 'dsh', community: '社区 community' }
+const TIER_LABEL_EN = { official: 'official', dsh: 'dsh', community: 'community' }
+
+function surfaces(entry) {
+  if (requiresDedicatedProfile(entry)) return 'Automation'
+  const parts = []
+  if (entry.surfaces?.tui !== undefined) parts.push('TUI')
+  if (entry.surfaces?.web !== undefined) parts.push('Web')
+  if (entry.surfaces?.server !== undefined) parts.push('Server')
+  return parts.join('+') || '—'
+}
+
+function verdict(entry, locale) {
+  if (requiresDedicatedProfile(entry)) {
+    return locale === 'zh'
+      ? '| ⚠️ | 仅自动化:会独占 stdio,必须使用独立的非 Mayfly profile。 |\n| ⚠️ | 不可加入 dsh Web profile。 |'
+      : '| ⚠️ | Automation only: owns stdio and requires a dedicated non-Mayfly profile. |\n| ⚠️ | Do not add to a dsh Web profile. |'
+  }
+  const tui = entry.surfaces?.server !== undefined || entry.surfaces?.tui !== undefined
+  const web = entry.surfaces?.web !== undefined || entry.surfaces?.server !== undefined
+  if (locale === 'zh') {
+    return [
+      `| ${tui ? '✅ 工具' : '⚠️ 工具' } | ${tui ? 'Mayfly 终端:' + (entry.surfaces?.server !== undefined ? '工具/命令完整可用' : '原生 UI 贡献') : '无贡献(纯 dsh Web 面板)'} |`,
+      `| ${web ? '✅' : '⚠️'} | dsh Web:${web ? (entry.surfaces?.web !== undefined ? '工具 + 专属面板' : '工具可用(无专属面板)') : '无贡献'} |`,
+    ].join('\n')
+  }
+  return [
+    `| ${tui ? '✅' : '⚠️'} | Mayfly terminal: ${tui ? (entry.surfaces?.server !== undefined ? 'full tools & commands' : 'native UI contribution') : 'no contribution (dsh Web panel only)'} |`,
+    `| ${web ? '✅' : '⚠️'} | dsh Web: ${web ? (entry.surfaces?.web !== undefined ? 'tools + dedicated panel' : 'tools (no dedicated panel)') : 'no contribution'} |`,
+  ].join('\n')
+}
+
+function rowSpec(row, source) {
+  if (source === 'npm') return row.npm?.spec
+  return row.github === undefined
+    ? undefined
+    : `github:${row.github.repo}#${row.github.ref}${row.github.subdir ? `&path:${row.github.subdir}` : ''}`
+}
+
+function shellArg(value) {
+  return /^[A-Za-z0-9@._/+~-]+$/u.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`
+}
+
+export function installCommand(entry) {
+  const rows = entry.install.rows
+  const source = rows.every(row => row.npm !== undefined)
+    ? 'npm'
+    : rows.every(row => row.github !== undefined) ? 'github' : undefined
+  if (source === undefined) return `dsh plugin --profile  add <${entry.id}>`
+  const profile = requiresDedicatedProfile(entry) ? '' : ''
+  return `dsh plugin --profile ${profile} add ${rows.map(row => shellArg(rowSpec(row, source))).join(' ')}`
+}
+
+export function requiresDedicatedProfile(entry) {
+  return entry.install.rows.some(row => DEDICATED_PROFILE_PACKAGES.has(row.name))
+}
+
+function escapeInertHtml(text) {
+  return text
+    .replaceAll('&', '&')
+    .replaceAll('<', '<')
+    .replaceAll('>', '>')
+    .replaceAll('{', '{')
+    .replaceAll('}', '}')
+}
+
+export function readmeBlock(entry) {
+  const text = typeof entry.readme === 'string' ? entry.readme : ''
+  if (text === '') return '_(该插件未提供 README 摘录 / No README excerpt shipped.)_'
+  let excerpt = text.slice(0, README_CAP)
+  if (excerpt.length < text.length) {
+    // Prefer a paragraph boundary so the inert excerpt remains readable.
+    const cut = excerpt.lastIndexOf('\n\n')
+    if (cut > 0) excerpt = excerpt.slice(0, cut)
+    excerpt += '\n\n…'
+  }
+  return `
${escapeInertHtml(excerpt)}
` +} + +function inlineText(value) { + return String(value).replace(/\s+/gu, ' ').trim().replace(/([\\`*_[\]<>])/gu, '\\$1') +} + +function safeHttpUrl(value) { + if (typeof value !== 'string') return undefined + try { + const url = new URL(value) + return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : undefined + } catch { + return undefined + } +} + +export function detailPage(entry, locale) { + const zh = locale === 'zh' + const displayName = inlineText(entry.displayName) + const description = inlineText(zh && entry.descriptionZh ? entry.descriptionZh : entry.description) + const tools = entry.provides?.tools ?? [] + const commands = entry.provides?.commands ?? [] + const verified = entry.verified?.packages?.map(pkg => `${pkg.name}@${pkg.version}`).join(', ') + const links = [ + safeHttpUrl(entry.links?.repo) ? `[${zh ? '仓库' : 'Repository'}](<${safeHttpUrl(entry.links.repo)}>)` : '', + safeHttpUrl(entry.links?.docs) ? `[${zh ? '文档' : 'Docs'}](<${safeHttpUrl(entry.links.docs)}>)` : '', + safeHttpUrl(entry.links?.npm) ? `[npm](<${safeHttpUrl(entry.links.npm)}>)` : '', + ].filter(Boolean).join(' · ') + return `--- +title: ${JSON.stringify(displayName)} +--- + +# ${displayName} + +${description} + +${zh ? `来源:**${TIER_LABEL[entry.source] ?? entry.source}** · 状态:\`${entry.status}\`` : `Source: **${TIER_LABEL_EN[entry.source] ?? entry.source}** · Status: \`${entry.status}\``} +${entry.statusNote ? `> ${inlineText(entry.statusNote)}` : ''} +${requiresDedicatedProfile(entry) ? (zh ? '> [!WARNING]\n> 此条目是独占 stdio 的自动化前端,不能安装进 Mayfly 或 dsh Web profile;请为它创建独立 profile。' : '> [!WARNING]\n> This entry is an automation frontend that owns stdio. Do not install it into a Mayfly or dsh Web profile; give it a dedicated profile.') : ''} + +## ${zh ? '安装' : 'Install'} + +\`\`\`sh +${installCommand(entry)} +\`\`\` + +${zh ? '在 Mayfly 中:`/plugin install ' + entry.id + '`(或 `/plugin` 浏览)。安装后**重启并新建会话**生效。' : 'Inside Mayfly: `/plugin install ' + entry.id + '` (or browse with `/plugin`). **Restart and start a new session** to apply.'} + +## ${zh ? '前端支持' : 'Frontend support'} + +| | | +|---|---| +${verdict(entry, locale)} + +## ${zh ? '提供' : 'Provides'} + +- ${zh ? '工具' : 'Tools'}: ${tools.length > 0 ? tools.map(tool => `\`${tool}\``).join(' · ') : zh ? '无' : 'none'} +- ${zh ? '命令' : 'Commands'}: ${commands.length > 0 ? commands.join(' · ') : zh ? '无' : 'none'} +${entry.capabilities?.length ? `- ${zh ? '能力披露' : 'Capabilities'}: ${entry.capabilities.join(', ')}` : ''} + +## ${zh ? '审核信息' : 'Verification'} + +${verified ? `${zh ? '审核版本' : 'Reviewed versions'}: ${verified}(${entry.verified.at})` : zh ? '未记录' : 'Not recorded.'} ${zh ? '收录是披露与审查,不是沙箱——安装第三方插件等同于安装任意 npm 包。' : 'Listing is disclosure and review, not a sandbox — installing a third-party plugin equals installing an arbitrary npm package.'} + +${links ? `## ${zh ? '链接' : 'Links'}\n\n${links}` : ''} + +## README + +${readmeBlock(entry)} +` +} + +/** Validate the path- and renderer-critical catalog boundary. */ +export function validateMarketIndex(index) { + if (typeof index !== 'object' || index === null || Array.isArray(index)) throw new Error('market catalog is not an object') + if (index.schemaVersion !== SUPPORTED_SCHEMA) { + throw new Error(`market catalog schema version ${index.schemaVersion} is not supported (expected ${SUPPORTED_SCHEMA})`) + } + if (!Array.isArray(index.entries)) throw new Error('market catalog has no entries array') + const ids = new Set() + for (const [position, entry] of index.entries.entries()) { + if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) throw new Error(`market entry ${position} is not an object`) + if (typeof entry.id !== 'string' || !/^[a-z0-9][a-z0-9-]*$/u.test(entry.id) || entry.id.length > 64) { + throw new Error(`market entry ${position} has an unsafe id`) + } + if (ids.has(entry.id)) throw new Error(`market catalog repeats id ${entry.id}`) + ids.add(entry.id) + for (const key of ['displayName', 'description', 'source', 'status', 'category']) { + if (typeof entry[key] !== 'string' || entry[key].length === 0) throw new Error(`${entry.id}: ${key} must be a non-empty string`) + } + if (!['official', 'dsh', 'community'].includes(entry.source)) throw new Error(`${entry.id}: unknown source tier`) + if (!['stable', 'beta', 'unstable', 'deprecated', 'removed'].includes(entry.status)) throw new Error(`${entry.id}: unknown status`) + if (typeof entry.surfaces !== 'object' || entry.surfaces === null || Array.isArray(entry.surfaces)) { + throw new Error(`${entry.id}: surfaces must be an object`) + } + if (typeof entry.install !== 'object' || entry.install === null || !Array.isArray(entry.install.rows) || entry.install.rows.length === 0) { + throw new Error(`${entry.id}: install.rows must be a non-empty array`) + } + for (const row of entry.install.rows) { + if (typeof row !== 'object' || row === null || typeof row.name !== 'string' || (row.npm === undefined && row.github === undefined)) { + throw new Error(`${entry.id}: every install row needs a package name and source`) + } + if (row.npm !== undefined && (typeof row.npm !== 'object' || row.npm === null || typeof row.npm.spec !== 'string')) { + throw new Error(`${entry.id}: npm sources need a string spec`) + } + if (row.github !== undefined && (typeof row.github !== 'object' || row.github === null || typeof row.github.repo !== 'string' || typeof row.github.ref !== 'string')) { + throw new Error(`${entry.id}: GitHub sources need repo and ref strings`) + } + } + } + return index.entries +} + +async function main() { + const index = await fetchIndex() + const entries = validateMarketIndex(index) + const generated = join(websiteRoot, 'market', 'p') + const generatedEn = join(websiteRoot, 'en', 'market', 'p') + for (const dir of [generated, generatedEn]) { + rmSync(dir, { recursive: true, force: true }) + mkdirSync(dir, { recursive: true }) + } + let zhCount = 0 + let enCount = 0 + for (const entry of entries) { + if (!entry?.id || entry.status === 'removed') continue + mkdirSync(join(generated, entry.id), { recursive: true }) + writeFileSync(join(generated, entry.id, 'index.md'), detailPage(entry, 'zh')) + mkdirSync(join(generatedEn, entry.id), { recursive: true }) + writeFileSync(join(generatedEn, entry.id, 'index.md'), detailPage(entry, 'en')) + zhCount += 1 + enCount += 1 + } + // The pruned catalog for the component. + const catalog = entries + .filter(entry => entry?.id && entry.status !== 'removed') + .map(entry => ({ + id: entry.id, + displayName: entry.displayName, + description: entry.description, + descriptionZh: entry.descriptionZh, + source: entry.source, + status: entry.status, + category: entry.category, + surfaces: { + tui: !requiresDedicatedProfile(entry) && entry.surfaces?.tui !== undefined, + web: !requiresDedicatedProfile(entry) && entry.surfaces?.web !== undefined, + server: !requiresDedicatedProfile(entry) && entry.surfaces?.server !== undefined, + automation: requiresDedicatedProfile(entry), + }, + provides: entry.provides ?? {}, + author: entry.author?.name, + })) + const catalogPath = join(websiteRoot, 'market', 'catalog.json') + mkdirSync(dirname(catalogPath), { recursive: true }) + writeFileSync(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`) + console.log(`market pages: ${zhCount} zh + ${enCount} en; catalog: ${catalog.length} entries (${INDEX_URL})`) +} + +if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) await main() diff --git a/website/scripts/market-fallback.json b/website/scripts/market-fallback.json new file mode 100644 index 0000000..d16ba6b --- /dev/null +++ b/website/scripts/market-fallback.json @@ -0,0 +1,974 @@ +{ + "schemaVersion": 1, + "generatedAt": "2026-09-04T14:05:58.931Z", + "counts": { + "total": 14, + "bySource": { + "official": 8, + "dsh": 6, + "community": 0 + } + }, + "entries": [ + { + "schemaVersion": 1, + "id": "codex-terminal", + "source": "official", + "displayName": "Codex Terminal", + "description": "Codex-style persistent shell sessions: start long-running processes with exec_command, poll output, and feed input with write_stdin. PTY transport by default with a pipe fallback.", + "descriptionZh": "Codex 风格持久终端会话:exec_command 启动长驻进程、轮询输出,write_stdin 向会话发送输入。默认 PTY 传输,不可用时回退 pipe。", + "author": { + "name": "Ephemeral AI Lab", + "url": "https://github.com/Ephemeral-AI-Lab" + }, + "links": { + "repo": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/tree/main/plugins/codex-terminal", + "docs": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/blob/main/plugins/codex-terminal/README.md" + }, + "license": "MIT", + "category": "tools", + "status": "stable", + "surfaces": { + "server": {} + }, + "provides": { + "tools": [ + "exec_command", + "write_stdin" + ] + }, + "install": { + "allowBuilds": [ + "node-pty" + ], + "rows": [ + { + "id": "codex-terminal", + "name": "dsh-codex-terminal", + "github": { + "repo": "Ephemeral-AI-Lab/dsh-plugins", + "ref": "main", + "subdir": "plugins/codex-terminal" + } + } + ] + }, + "capabilities": [ + "shell", + "process-spawn" + ], + "verified": { + "at": "2026-09-04", + "packages": [ + { + "name": "dsh-codex-terminal", + "version": "0.1.4" + } + ] + }, + "registryPath": "registry/official/codex-terminal.json", + "npm": { + "dsh-codex-terminal": { + "latestVersion": null, + "integrity": null, + "publishedAt": null, + "downloadsMonth": null, + "readme": null + } + }, + "readmeExcerpt": "# dsh-codex-terminal 🐋\n\nAdds Codex-style `exec_command` and `write_stdin` tools to DeepSeek\nHarness.\n\n## Current release: 0.1.4\n\nCommands that remain live after `yield_time_ms` are now automatically registered\nwith `ctx.jobs`. `exec_command` returns one `job_id` such as\n`codex-terminal-1`; `job_list` reports lifecycle state and `job_kill`\nterminates the underlying session. Terminal output remains exclusive to\n`write_stdin`, and successful or repeated terminal polls render an explicit\ncompletion marker instead of an empty result or stale-session error.\n\nThe package was verified with the full unit suite, build, and an assembled Web\nAPI run. See the [E2E test prompts](e2e-test-prompt.md) and [0.1.4\nchangelog](changelog/0.1.4.md).\n\n## 🚀 1. Install the plugin\n\nInstall it into the DSH profile you use:\n\n```powershell\ndsh plugin --profile web add dsh-codex-terminal@0.1.4\n```\n\nFrom a DeepSeek Harness source checkout:\n\n```powershell\ncd C:/path/to/deepseek-harness\npnpm install\npnpm dsh plugin --profile web add dsh-codex-terminal@0.1.4\n```\n\n> ⚠️ Do not run `npm install dsh-codex-terminal` as a separate setup step. The\n> DSH plugin command installs it into the selected profile. `pnpm install` in\n> the source checkout only bootstraps DSH itself.\n\n## 🐋 2. Create the Codex Whale preset\n\nIn the DSH web UI, open **Settings -> Agent presets** and choose **Draft a\ncustom preset with Creator mode**.\n\nPaste this prompt:\n\n```text\nCreate a user preset named \"Codex Whale\" with ID `codex-whale`.\n\nDuplicate the Standard preset and configure it as follows:\n\n- Add exactly one row:\n - id: codex-terminal\n name: dsh-codex-terminal\n- Disable `tool-bash` and `tool-pwsh`.\n- Keep `tool-jobs` loaded for `job_list`, `job_kill`, lifecycle tracking, and\n completion delivery. Hide `job_output` from the agent tool surface.\n- Disable any other persistent or alternate terminal tools.\n- Keep all non-shell coding tools.\n- Do not modify shipped presets.\n- Do not add duplicate `codex-terminal` rows.\n\nValidate the result before finishing.\n```\n\n### 💡 Important\n\n- Installing the npm plugin enables it in the **DSH profile**.\n- Adding the `codex-terminal` row enables its tools in the **agent preset**.\n- `@deepseek-ai/dsh-jobs-local` and `@deepseek-ai/dsh-tool-jobs` must remain\n loaded; Codex Shell fails loudly if `ctx.jobs` is unavailable.\n- The preset disables the native shell tools.\n\n## 🧰 Tools\n\n### `exec_command`\n\n`exec_command(cmd: string, workdir?: string, yield_time_ms?: number, max_output_tokens?: number)` - Runs one command in the host shell. Short commands return output; commands still running after `yield_time_ms` are automatically promoted and return one `job_id` shared by `write_stdin`, `job_list`, and `job_kill`.\n\n- `cmd` (`string`, required) - Command to run.\n- `workdir` (`string`, optional) - Working directory for the command.\n- `yield_time_ms` (`number`, optional) - Wait time before returning; default `10000` ms.\n- `max_output_tokens` (`number`, optional) - Approximate output-page token budget; default `4000`, capped at `10000` unless configured otherwise.\n\n### `write_stdin`\n\n`write_stdin(job_id: string, chars?: string, yield_time_ms?: number, max_output_tokens?: number)` - Writes input to an existing job or polls for more output.\n\n- `job_id` (`string`, required) - `codex-terminal-N` job ID returned by `exec_command`.\n- `chars` (`string`, optional) - Characters to send; omit or use an empty string to poll.\n- `yield_time_ms` (`number`, optional) - Wait time for output; default `250` ms.\n- `max_output_tokens` (`number`, optional) - Approximate output-page token budget; default `4000`, capped at `10000` unless configured otherwise.\n\nTypical flow: call `exec_command`; if it returns a `job_id`, use that same ID\nwith `job_list`, `job_kill`, and `write_stdin` to inspect status, stop the\nprocess, send input, or collect unread output. A terminal result\nmay contain both `exit_code` and `job_id` when `max_output_tokens` capped\nthe current page; keep polling with ", + "readme": "# dsh-codex-terminal 🐋\n\nAdds Codex-style `exec_command` and `write_stdin` tools to DeepSeek\nHarness.\n\n## Current release: 0.1.4\n\nCommands that remain live after `yield_time_ms` are now automatically registered\nwith `ctx.jobs`. `exec_command` returns one `job_id` such as\n`codex-terminal-1`; `job_list` reports lifecycle state and `job_kill`\nterminates the underlying session. Terminal output remains exclusive to\n`write_stdin`, and successful or repeated terminal polls render an explicit\ncompletion marker instead of an empty result or stale-session error.\n\nThe package was verified with the full unit suite, build, and an assembled Web\nAPI run. See the [E2E test prompts](e2e-test-prompt.md) and [0.1.4\nchangelog](changelog/0.1.4.md).\n\n## 🚀 1. Install the plugin\n\nInstall it into the DSH profile you use:\n\n```powershell\ndsh plugin --profile web add dsh-codex-terminal@0.1.4\n```\n\nFrom a DeepSeek Harness source checkout:\n\n```powershell\ncd C:/path/to/deepseek-harness\npnpm install\npnpm dsh plugin --profile web add dsh-codex-terminal@0.1.4\n```\n\n> ⚠️ Do not run `npm install dsh-codex-terminal` as a separate setup step. The\n> DSH plugin command installs it into the selected profile. `pnpm install` in\n> the source checkout only bootstraps DSH itself.\n\n## 🐋 2. Create the Codex Whale preset\n\nIn the DSH web UI, open **Settings -> Agent presets** and choose **Draft a\ncustom preset with Creator mode**.\n\nPaste this prompt:\n\n```text\nCreate a user preset named \"Codex Whale\" with ID `codex-whale`.\n\nDuplicate the Standard preset and configure it as follows:\n\n- Add exactly one row:\n - id: codex-terminal\n name: dsh-codex-terminal\n- Disable `tool-bash` and `tool-pwsh`.\n- Keep `tool-jobs` loaded for `job_list`, `job_kill`, lifecycle tracking, and\n completion delivery. Hide `job_output` from the agent tool surface.\n- Disable any other persistent or alternate terminal tools.\n- Keep all non-shell coding tools.\n- Do not modify shipped presets.\n- Do not add duplicate `codex-terminal` rows.\n\nValidate the result before finishing.\n```\n\n### 💡 Important\n\n- Installing the npm plugin enables it in the **DSH profile**.\n- Adding the `codex-terminal` row enables its tools in the **agent preset**.\n- `@deepseek-ai/dsh-jobs-local` and `@deepseek-ai/dsh-tool-jobs` must remain\n loaded; Codex Shell fails loudly if `ctx.jobs` is unavailable.\n- The preset disables the native shell tools.\n\n## 🧰 Tools\n\n### `exec_command`\n\n`exec_command(cmd: string, workdir?: string, yield_time_ms?: number, max_output_tokens?: number)` - Runs one command in the host shell. Short commands return output; commands still running after `yield_time_ms` are automatically promoted and return one `job_id` shared by `write_stdin`, `job_list`, and `job_kill`.\n\n- `cmd` (`string`, required) - Command to run.\n- `workdir` (`string`, optional) - Working directory for the command.\n- `yield_time_ms` (`number`, optional) - Wait time before returning; default `10000` ms.\n- `max_output_tokens` (`number`, optional) - Approximate output-page token budget; default `4000`, capped at `10000` unless configured otherwise.\n\n### `write_stdin`\n\n`write_stdin(job_id: string, chars?: string, yield_time_ms?: number, max_output_tokens?: number)` - Writes input to an existing job or polls for more output.\n\n- `job_id` (`string`, required) - `codex-terminal-N` job ID returned by `exec_command`.\n- `chars` (`string`, optional) - Characters to send; omit or use an empty string to poll.\n- `yield_time_ms` (`number`, optional) - Wait time for output; default `250` ms.\n- `max_output_tokens` (`number`, optional) - Approximate output-page token budget; default `4000`, capped at `10000` unless configured otherwise.\n\nTypical flow: call `exec_command`; if it returns a `job_id`, use that same ID\nwith `job_list`, `job_kill`, and `write_stdin` to inspect status, stop the\nprocess, send input, or collect unread output. A terminal result\nmay contain both `exit_code` and `job_id` when `max_output_tokens` capped\nthe current page; keep polling with empty `chars` until `job_id` is no\nlonger returned. Do not use `job_output` for Codex Shell output.\n\nThe authoritative contracts for output pages, pipe transport, background-job\npromotion, polling, cleanup, and completion notices are in [SPEC.md](./SPEC.md).\n\n## 🧭 Current session behavior\n\n- Pipe transport is the default on Windows, macOS, and Linux. A real PTY is\n not required for the `exec_command` plus `write_stdin` lifecycle.\n- Output produced after `exec_command` returns is retained for the next\n `write_stdin` poll.\n- A session still running after `yield_time_ms` is automatically registered as\n a `codex-terminal` job. No `run_in_background` argument exists.\n- `job_list`, `job_kill`, and `write_stdin` use the exact same `codex-terminal-N`\n identifier.\n- An exited process remains pollable while unread output is buffered. The\n heavy session record is released only after its terminal output has been\n collected; a lightweight owner-scoped completion record keeps repeated\n empty polls safe and explicit.\n- `max_output_tokens` limits each response page; it does not discard buffered\n output. Continue polling to retrieve later pages.\n- Natural-exit notifications identify the job and instruct the owner to call\n `write_stdin` with empty `chars` only when completion happened between tool\n calls. If `exec_command` or `write_stdin` returns the terminal `exit_code`\n inside its own yield, the background completion steer is suppressed.\n- A background notice is delivered through `steer`: a running owner consumes\n it at the next step, while an idle owner wakes in a new turn. Codex Shell\n never delivers the notice through `followup`, and owner or service teardown\n suppresses it rather than waking an agent being disposed.\n- Session output and process resources are bounded and cleaned up on terminal\n completion, owner disposal, and plugin disposal.\n\n## 🎯 Why do we need it?\n\nMost Bash or Shell tools use a one-shot model: run a command, read its output,\nand return. That works for `ls`, `git status`, builds, and ordinary tests, but\nnot for a CLI that waits for input while it is still running.\n\nFor example, an interactive `rng` program requires the agent to:\n\n1. Start the process.\n2. Read the generated number.\n3. Send the answer to the same process.\n4. Read `PASS` or `FAIL`.\n5. Confirm the final exit code.\n\nThe same pattern is needed for device-code login, OAuth flows, REPLs, SSH\nsessions, database prompts, and end-to-end CLI tests. Background execution\nalone is not enough if the agent cannot write to the original process.\n\nSee the full motivation in [Why Claude Code, Pi, and DSH cannot complete\ninteractive CLIs](https://x.com/yifanxu_ephai/status/2088905874232459741).\n\n## ⭐ Why Codex-style tools?\n\nThe two-tool design is a good fit for coding agents because it connects the\ncomplete interactive flow:\n\n- **Start** - `exec_command` launches the process and returns early when it is\n still running.\n- **Continue** - `write_stdin` sends input to that same session.\n- **Observe** - `write_stdin` can poll for more output without sending input.\n- **Verify** - the agent can wait for the final output and exit code.\n- **Reuse** - the same small interface works across Windows, macOS, and Linux.\n\n## ✅ 3. Select and restart\n\n1. Set **Codex Whale** as the default preset.\n2. Restart DSH.\n3. Create a new session.\n\nExisting sessions keep their old tools.\n\n## 🔍 4. Verify\n\nCheck the profile:\n\n```powershell\ndsh --profile web --dump-config\n```\n\nIt should contain exactly one:\n\n```text\n- id: codex-terminal\n name: dsh-codex-terminal\n```\n\nIn a new Codex Whale session, confirm that:\n\n- ✅ `exec_command` is available\n- ✅ `write_stdin` is available\n- ✅ `job_list` and `job_kill` are available\n- 🚫 `job_output` is hidden when output is intentionally restricted to\n `write_stdin`\n- 🚫 native Bash/PowerShell tools are unavailable\n\n## 🛠️ Troubleshooting\n\n### `dsh` is not found\n\nRun the command from a DeepSeek Harness checkout with `pnpm dsh`, or install\nthe published DSH CLI.\n\n### `node-pty` build is blocked\n\nAdd this to the target profile's `pnpm-workspace.yaml`:\n\n```yaml\nallowBuilds:\n node-pty: true\n```\n\nThen install the plugin again.\n\n### The profile has an older plugin version\n\n```powershell\ndsh plugin --profile web remove dsh-codex-terminal\ndsh plugin --profile web add dsh-codex-terminal@0.1.4\n```\n\n## 📚 Documentation\n\n- [Implementation specification](SPEC.md)\n- [Reusable E2E test prompts](e2e-test-prompt.md)\n- [0.1.0 changelog](changelog/0.1.0.md)\n- [0.1.1 changelog](changelog/0.1.1.md)\n- [0.1.2 changelog](changelog/0.1.2.md)\n- [0.1.4 changelog](changelog/0.1.4.md)\n- [0.1.3 changelog](changelog/0.1.3.md)\n" + }, + { + "schemaVersion": 1, + "id": "coding-plan", + "source": "official", + "displayName": "Coding Plan (Codex & Grok)", + "description": "Reuse file-backed codex login and grok login subscriptions inside dsh: exposes the Codex ChatGPT plan and the Grok subscription through the existing openai-codex / xai providers.", + "descriptionZh": "在 dsh 中复用文件级 codex login 与 grok login 订阅:通过现有 openai-codex / xai provider 接入 ChatGPT Codex 套餐与 Grok 订阅。", + "author": { + "name": "Ephemeral AI Lab", + "url": "https://github.com/Ephemeral-AI-Lab" + }, + "links": { + "repo": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/tree/main/plugins/coding-plan", + "docs": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/blob/main/plugins/coding-plan/README.md" + }, + "license": "MIT", + "category": "provider", + "status": "stable", + "surfaces": { + "server": {} + }, + "provides": {}, + "install": { + "allowBuilds": [ + "@google/genai", + "protobufjs" + ], + "rows": [ + { + "id": "codex-coding-plan", + "name": "dsh-coding-plan", + "github": { + "repo": "Ephemeral-AI-Lab/dsh-plugins", + "ref": "main", + "subdir": "plugins/coding-plan" + } + } + ] + }, + "capabilities": [ + "credentials", + "oauth-file-read" + ], + "verified": { + "at": "2026-09-04", + "packages": [ + { + "name": "dsh-coding-plan", + "version": "0.1.0" + } + ] + }, + "registryPath": "registry/official/coding-plan.json", + "npm": { + "dsh-coding-plan": { + "latestVersion": null, + "integrity": null, + "publishedAt": null, + "downloadsMonth": null, + "readme": null + } + }, + "readmeExcerpt": "# DSH Coding Plan\n\nOne installable DSH bundle for local Codex and Grok coding-plan logins. The\npublished package is [`dsh-coding-plan`](https://www.npmjs.com/package/dsh-coding-plan);\nthe Codex, Grok, and shared OAuth code is bundled inside that one package.\n\n```text\ncore -> shared file-backed OAuth cache, refresh, and atomic persistence\ncodex -> ~/.codex/auth.json -> openai-codex -> Codex models\ngrok -> ~/.grok/auth.json -> xai -> Grok 4.3/4.5/build\ngrok-4.6 -> ~/.grok/auth.json -> xai-grok-4-6 -> Grok 4.6\nmodels.json -> machine-readable model and thinking-effort offering table\n```\n\n## Install from npm\n\n```sh\ndsh plugin --profile web add dsh-coding-plan\n```\n\nRestart the DSH profile after installing or updating the bundle. The package\nowns one complete `llm-pi-ai` configuration row and mounts both auth\nsynchronizers, so users should install the root package rather than the\nimplementation packages separately.\n\n## Login and model selection\n\nSign in through the native CLIs first:\n\n```sh\ncodex login\ngrok login\n```\n\nThe adapters read `~/.codex/auth.json` and `~/.grok/auth.json`, refresh tokens\nthrough the shared pi-ai runtime, and synchronize only short-lived access\ntokens into DSH's credential service.\n\nSelect the provider and model from the model picker inside a DSH session. The\nSettings → Models page manages provider credentials and routes; it is not the\nactive conversation model picker.\n\nThe current stable Grok 4.6 route is explicit:\n\n```yaml\nagent-default-model:\n provider: xai-grok-4-6\n model: grok-4.6\n```\n\nThe route uses the shared DSH `llm-pi-ai` runtime and does not introduce a\nsecond request engine. Grok 4.6 is exposed through `xai-grok-4-6` because the\nstable DSH pi-ai catalog does not natively list that model yet.\n\n## Model table\n\n[`models.json`](./models.json) records the offered routes, model IDs, API\nfamilies, and exact thinking-effort IDs. It is packaged as a reference\nmanifest; the current runtime configuration is the bundle's\n[`cordis.patch.yml`](./cordis.patch.yml).\n\nSee [`SPEC.md`](./SPEC.md) for the dependency policy, migration boundary, and\nthe optional future merge of Grok 4.6 into the native `xai` group.\n", + "readme": "# DSH Coding Plan\n\nOne installable DSH bundle for local Codex and Grok coding-plan logins. The\npublished package is [`dsh-coding-plan`](https://www.npmjs.com/package/dsh-coding-plan);\nthe Codex, Grok, and shared OAuth code is bundled inside that one package.\n\n```text\ncore -> shared file-backed OAuth cache, refresh, and atomic persistence\ncodex -> ~/.codex/auth.json -> openai-codex -> Codex models\ngrok -> ~/.grok/auth.json -> xai -> Grok 4.3/4.5/build\ngrok-4.6 -> ~/.grok/auth.json -> xai-grok-4-6 -> Grok 4.6\nmodels.json -> machine-readable model and thinking-effort offering table\n```\n\n## Install from npm\n\n```sh\ndsh plugin --profile web add dsh-coding-plan\n```\n\nRestart the DSH profile after installing or updating the bundle. The package\nowns one complete `llm-pi-ai` configuration row and mounts both auth\nsynchronizers, so users should install the root package rather than the\nimplementation packages separately.\n\n## Login and model selection\n\nSign in through the native CLIs first:\n\n```sh\ncodex login\ngrok login\n```\n\nThe adapters read `~/.codex/auth.json` and `~/.grok/auth.json`, refresh tokens\nthrough the shared pi-ai runtime, and synchronize only short-lived access\ntokens into DSH's credential service.\n\nSelect the provider and model from the model picker inside a DSH session. The\nSettings → Models page manages provider credentials and routes; it is not the\nactive conversation model picker.\n\nThe current stable Grok 4.6 route is explicit:\n\n```yaml\nagent-default-model:\n provider: xai-grok-4-6\n model: grok-4.6\n```\n\nThe route uses the shared DSH `llm-pi-ai` runtime and does not introduce a\nsecond request engine. Grok 4.6 is exposed through `xai-grok-4-6` because the\nstable DSH pi-ai catalog does not natively list that model yet.\n\n## Model table\n\n[`models.json`](./models.json) records the offered routes, model IDs, API\nfamilies, and exact thinking-effort IDs. It is packaged as a reference\nmanifest; the current runtime configuration is the bundle's\n[`cordis.patch.yml`](./cordis.patch.yml).\n\nSee [`SPEC.md`](./SPEC.md) for the dependency policy, migration boundary, and\nthe optional future merge of Grok 4.6 into the native `xai` group.\n" + }, + { + "schemaVersion": 1, + "id": "loop", + "source": "official", + "displayName": "Loop", + "description": "Session-scoped recurring prompts: create durable alarms with agent tools, manage them with the /loop command, and watch them in a Web UI panel. Loops resume with the session.", + "descriptionZh": "会话级循环提示:通过 Agent 工具创建持久闹钟,用 /loop 命令管理,Web 端附面板展示。闹钟随会话持久化并恢复。", + "author": { + "name": "Ephemeral AI Lab", + "url": "https://github.com/Ephemeral-AI-Lab" + }, + "links": { + "repo": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/tree/main/plugins/loop", + "docs": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/blob/main/plugins/loop/README.md" + }, + "license": "MIT", + "category": "workflow", + "status": "stable", + "surfaces": { + "server": {}, + "web": { + "clientModule": true + } + }, + "provides": { + "tools": [ + "loop_create", + "loop_list", + "loop_delete" + ], + "commands": [ + "/loop" + ] + }, + "install": { + "rows": [ + { + "id": "loop", + "name": "dsh-loop", + "github": { + "repo": "Ephemeral-AI-Lab/dsh-plugins", + "ref": "main", + "subdir": "plugins/loop" + } + } + ] + }, + "capabilities": [ + "timer", + "session-write" + ], + "verified": { + "at": "2026-09-04", + "packages": [ + { + "name": "dsh-loop", + "version": "0.1.4" + } + ] + }, + "registryPath": "registry/official/loop.json", + "npm": { + "dsh-loop": { + "latestVersion": null, + "integrity": null, + "publishedAt": null, + "downloadsMonth": null, + "readme": null + } + }, + "readmeExcerpt": "# ⏰ dsh-loop\n\nAdds session-scoped recurring alarms to DeepSeek Harness.\n\n## 🚀 Release: 0.1.4\n\nThis patch release fixes the web client bundle registration so the published\npackage loads correctly in DSH Web. It also includes the session-scoped\nrecurring self-prompts, active-session delivery, and web UI from 0.1.2.\n\n## 1. 📦 Install the plugin\n\nThe stable npm release is available through both DSH and npm:\n\n~~~powershell\n# With the DSH CLI:\ndsh plugin --profile web add dsh-loop@0.1.4\n\n# Without the `dsh` CLI:\nnpm install dsh-loop@0.1.4\n~~~\n\nFor a pinned Git-source install, the marketplace can pin a full Git commit and\nthe `loop` monorepo path, then delegate the package change to the official DSH\nCLI. A direct fixed-source spec has this shape:\n\n~~~powershell\ndsh plugin --profile web add \"git+https://github.com/Ephemeral-AI-Lab/dsh-plugins.git#<40-character-commit>&path:loop\"\n~~~\n\nThe plugin is installed into the selected DSH profile. Restart DSH and create\na new session after installing it.\n\nThe fixed Git source contains the compiled `lib` directory and has no\n`preinstall`, `install`, `postinstall`, or `prepare` lifecycle script. Consumer\ninstalls therefore do not download the development toolchain or build the\nplugin. Contributors must run `pnpm build` and commit the regenerated `lib`\nfiles whenever `src` changes.\n\n## 2. ⏱️ Set recurring alarms\n\nThe simplest user-facing request is natural language:\n\n~~~text\nSet a recurring alarm to ask yourself to check the code every 10 seconds.\n~~~\n\nFor a presentation with multiple independent alarms:\n\n~~~text\nSet up four independent recurring alarms for this session:\n\n- Set a recurring alarm to ask yourself to inspect the latest errors, identify\n the most likely cause, and recommend the next fix every 20 seconds.\n- Set a recurring alarm to ask yourself to review the project tasks, find the\n biggest blocker, and update the priority order every 30 seconds.\n- Set a recurring alarm to ask yourself to check the research notes, compare\n them with the current hypothesis, and propose the next experiment every\n 45 seconds.\n- Set a recurring alarm to ask yourself to inspect the draft, find the three\n most important weaknesses, and suggest concrete revisions every 60 seconds.\n~~~\n\nEach alarm is independent. It has its own interval, prompt, next-delivery\ncountdown, and ID, so one alarm can be removed while the others continue.\n\n## 3. 🛠️ Agent tools and command interface\n\nThe plugin exposes three agent-local tools:\n\n- loop_create({ prompt, time_in_seconds })\n- loop_list({})\n- loop_delete({ id })\n\nIt also registers the /loop command for direct command-driven sessions:\n\n- /loop creates an alarm;\n- /loop list lists active alarms;\n- /loop delete removes one alarm.\n\nBoth interfaces use the same validation, persistence, and scheduling path.\n\n## 4. 🖥️ Web UI\n\nThe web client adds a compact Loop dock for the current session. It shows:\n\n- the number of active alarms;\n- each alarm's interval and next-delivery countdown;\n- the full prompt on demand;\n- an expand/collapse control for multiple alarms;\n- a direct delete button with no confirmation step.\n\nDeleting an alarm sends the normal delete operation and removes the row when\nthe projected session state confirms the change.\n\n## 5. 🔄 How delivery works\n\ntime_in_seconds is the only time unit. When an alarm is due, its prompt is\ndelivered as a normal user message through the session inbox with\nwakeup: true.\n\n- An idle agent receives the heartbeat through next-turn.\n- A running agent receives it through next-step.\n\nThis lets DSH process the reminder at the earliest safe step boundary without\ninterrupting the current operation. The plugin calls Agent.send directly and\ndoes not call steer() or followup().\n\nThe delivered message has this shape:\n\n~~~text\n\n loop_...\n Check whether the build is still healthy\n\n~~~\n\nLoop definitions and next-delivery times are durable loop/change session", + "readme": "# ⏰ dsh-loop\n\nAdds session-scoped recurring alarms to DeepSeek Harness.\n\n## 🚀 Release: 0.1.4\n\nThis patch release fixes the web client bundle registration so the published\npackage loads correctly in DSH Web. It also includes the session-scoped\nrecurring self-prompts, active-session delivery, and web UI from 0.1.2.\n\n## 1. 📦 Install the plugin\n\nThe stable npm release is available through both DSH and npm:\n\n~~~powershell\n# With the DSH CLI:\ndsh plugin --profile web add dsh-loop@0.1.4\n\n# Without the `dsh` CLI:\nnpm install dsh-loop@0.1.4\n~~~\n\nFor a pinned Git-source install, the marketplace can pin a full Git commit and\nthe `loop` monorepo path, then delegate the package change to the official DSH\nCLI. A direct fixed-source spec has this shape:\n\n~~~powershell\ndsh plugin --profile web add \"git+https://github.com/Ephemeral-AI-Lab/dsh-plugins.git#<40-character-commit>&path:loop\"\n~~~\n\nThe plugin is installed into the selected DSH profile. Restart DSH and create\na new session after installing it.\n\nThe fixed Git source contains the compiled `lib` directory and has no\n`preinstall`, `install`, `postinstall`, or `prepare` lifecycle script. Consumer\ninstalls therefore do not download the development toolchain or build the\nplugin. Contributors must run `pnpm build` and commit the regenerated `lib`\nfiles whenever `src` changes.\n\n## 2. ⏱️ Set recurring alarms\n\nThe simplest user-facing request is natural language:\n\n~~~text\nSet a recurring alarm to ask yourself to check the code every 10 seconds.\n~~~\n\nFor a presentation with multiple independent alarms:\n\n~~~text\nSet up four independent recurring alarms for this session:\n\n- Set a recurring alarm to ask yourself to inspect the latest errors, identify\n the most likely cause, and recommend the next fix every 20 seconds.\n- Set a recurring alarm to ask yourself to review the project tasks, find the\n biggest blocker, and update the priority order every 30 seconds.\n- Set a recurring alarm to ask yourself to check the research notes, compare\n them with the current hypothesis, and propose the next experiment every\n 45 seconds.\n- Set a recurring alarm to ask yourself to inspect the draft, find the three\n most important weaknesses, and suggest concrete revisions every 60 seconds.\n~~~\n\nEach alarm is independent. It has its own interval, prompt, next-delivery\ncountdown, and ID, so one alarm can be removed while the others continue.\n\n## 3. 🛠️ Agent tools and command interface\n\nThe plugin exposes three agent-local tools:\n\n- loop_create({ prompt, time_in_seconds })\n- loop_list({})\n- loop_delete({ id })\n\nIt also registers the /loop command for direct command-driven sessions:\n\n- /loop creates an alarm;\n- /loop list lists active alarms;\n- /loop delete removes one alarm.\n\nBoth interfaces use the same validation, persistence, and scheduling path.\n\n## 4. 🖥️ Web UI\n\nThe web client adds a compact Loop dock for the current session. It shows:\n\n- the number of active alarms;\n- each alarm's interval and next-delivery countdown;\n- the full prompt on demand;\n- an expand/collapse control for multiple alarms;\n- a direct delete button with no confirmation step.\n\nDeleting an alarm sends the normal delete operation and removes the row when\nthe projected session state confirms the change.\n\n## 5. 🔄 How delivery works\n\ntime_in_seconds is the only time unit. When an alarm is due, its prompt is\ndelivered as a normal user message through the session inbox with\nwakeup: true.\n\n- An idle agent receives the heartbeat through next-turn.\n- A running agent receives it through next-step.\n\nThis lets DSH process the reminder at the earliest safe step boundary without\ninterrupting the current operation. The plugin calls Agent.send directly and\ndoes not call steer() or followup().\n\nThe delivered message has this shape:\n\n~~~text\n\n loop_...\n Check whether the build is still healthy\n\n~~~\n\nLoop definitions and next-delivery times are durable loop/change session\nevents. Timers are disposable and recreated when the session resumes. The\nruntime is session-local: a stopped or cold process cannot run timers or wake\nitself.\n\n## 6. ✅ Verify locally\n\nRun the full Loop test suite:\n\n~~~powershell\npnpm test -- --maxWorkers=1\n~~~\n\nRun the focused E2E suite:\n\n~~~powershell\npnpm test:e2e\n~~~\n\nRun typechecks and build the published artifacts:\n\n~~~powershell\npnpm typecheck\npnpm typecheck:client\npnpm build\n~~~\n\nThe reusable agent-facing scenarios are in\n[e2e-test-prompt.md](./e2e-test-prompt.md). The executable test runner remains\nin the source repository at test/e2e.test.ts.\n\n## 7. 📦 Package scope\n\nThis plugin uses public DSH and Cordis APIs only; it does not modify\ndeepseek-harness. The implementation contract is documented in\n[SPEC.md](./SPEC.md), and the web UI contract is documented in\n[ui.md](./ui.md).\n\n## 📚 Documentation\n\n- [Implementation specification](./SPEC.md)\n- [Cross-session v2 design specification](./SPEC_V2.md)\n- [Web UI contract](./ui.md)\n- [Reusable E2E test prompts](./e2e-test-prompt.md)\n- [Test plan](./TEST_PLAN.md)\n- [Test orchestration](./TEST_ORCHESTRATION.md)\n" + }, + { + "schemaVersion": 1, + "id": "mock", + "source": "official", + "displayName": "Mock", + "description": "Deterministic mock model turns routed through the real dsh AgentLoop and ToolRuntime: /mock run and /mock replay exercise tools, policy, and event flow without a live model. Unstable; API may change.", + "descriptionZh": "确定性 mock 模型回合,走真实 dsh AgentLoop 与 ToolRuntime:/mock run 与 /mock replay 不依赖真实模型即可测试工具、策略与事件流。不稳定,接口可能变化。", + "author": { + "name": "Ephemeral AI Lab", + "url": "https://github.com/Ephemeral-AI-Lab" + }, + "links": { + "repo": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/tree/main/plugins/mock", + "docs": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/blob/main/plugins/mock/README.md" + }, + "license": "MIT", + "category": "testing", + "status": "unstable", + "surfaces": { + "server": {}, + "web": { + "clientModule": true + } + }, + "provides": { + "commands": [ + "/mock" + ] + }, + "install": { + "rows": [ + { + "id": "mock", + "name": "dsh-mock", + "github": { + "repo": "Ephemeral-AI-Lab/dsh-plugins", + "ref": "main", + "subdir": "plugins/mock" + } + } + ] + }, + "capabilities": [ + "llm-adapter-override" + ], + "verified": { + "at": "2026-09-04", + "packages": [ + { + "name": "dsh-mock", + "version": "0.1.1" + } + ] + }, + "registryPath": "registry/official/mock.json", + "npm": { + "dsh-mock": { + "latestVersion": null, + "integrity": null, + "publishedAt": null, + "downloadsMonth": null, + "readme": null + } + }, + "readmeExcerpt": "# 🧪 dsh-mock\n\n> ⚠️ **Unstable release:** `dsh-mock@0.1.1` is available for early testing.\n> The command, API, and UI surface may change before a stable release.\n\n## 📦 Install\n\nAdd the plugin to the DSH `web` profile with one command:\n\n```powershell\ndsh plugin --profile web add dsh-mock@0.1.1\n```\n\nOr install the npm package directly:\n\n```powershell\nnpm install dsh-mock@0.1.1\n```\n\nThis is an external Cordis plugin. It registers the internal per-turn provider\nroute `mock` with the `mock` model and routes accepted `/mock run` and `/mock replay`\ncommands through the public AgentLoop request waterfall. The route is\ndeliberately omitted from the advertised model catalog, so the normal model\nselector remains unchanged; only the slash command activates it. The plugin\nalso registers `mock` in the public slash-command catalog, and its command\nhandler queues the exact line as a normal AgentLoop follow-up. The adapter\nemits ordinary model chunks; the host AgentLoop and ToolRuntime execute and\nrecord the actual calls.\n\nReplay is read-only and in-memory. A relative path is resolved against the\ncurrent session's absolute `session.header.cwd`; when that value is missing,\nrelative paths are rejected. Absolute paths are accepted for read-only input,\nand the plugin never creates, copies, rewrites, renames, or deletes a replay\nsource. `--overwrite-wait-time-ms` changes only explicit waits in the detached\ncanonical plan.\n\nThe plugin emits ephemeral `mock/status` events for a host UI bridge. The\nbridge should render a compact status row above the existing composer and\nleave normal DSH tool/result/error cards authoritative.\n", + "readme": "# 🧪 dsh-mock\n\n> ⚠️ **Unstable release:** `dsh-mock@0.1.1` is available for early testing.\n> The command, API, and UI surface may change before a stable release.\n\n## 📦 Install\n\nAdd the plugin to the DSH `web` profile with one command:\n\n```powershell\ndsh plugin --profile web add dsh-mock@0.1.1\n```\n\nOr install the npm package directly:\n\n```powershell\nnpm install dsh-mock@0.1.1\n```\n\nThis is an external Cordis plugin. It registers the internal per-turn provider\nroute `mock` with the `mock` model and routes accepted `/mock run` and `/mock replay`\ncommands through the public AgentLoop request waterfall. The route is\ndeliberately omitted from the advertised model catalog, so the normal model\nselector remains unchanged; only the slash command activates it. The plugin\nalso registers `mock` in the public slash-command catalog, and its command\nhandler queues the exact line as a normal AgentLoop follow-up. The adapter\nemits ordinary model chunks; the host AgentLoop and ToolRuntime execute and\nrecord the actual calls.\n\nReplay is read-only and in-memory. A relative path is resolved against the\ncurrent session's absolute `session.header.cwd`; when that value is missing,\nrelative paths are rejected. Absolute paths are accepted for read-only input,\nand the plugin never creates, copies, rewrites, renames, or deletes a replay\nsource. `--overwrite-wait-time-ms` changes only explicit waits in the detached\ncanonical plan.\n\nThe plugin emits ephemeral `mock/status` events for a host UI bridge. The\nbridge should render a compact status row above the existing composer and\nleave normal DSH tool/result/error cards authoritative.\n" + }, + { + "schemaVersion": 1, + "id": "preset-builder", + "source": "official", + "displayName": "Preset Builder", + "description": "A Preset details page for the dsh Web Settings screen. Web-only: contributes a client module with no model-facing tools, so it adds nothing in a terminal frontend.", + "descriptionZh": "为 dsh Web 设置页增加 Preset 详情页。仅 Web 端:只包含 client module、无模型工具,在终端前端无作用。", + "author": { + "name": "Ephemeral AI Lab", + "url": "https://github.com/Ephemeral-AI-Lab" + }, + "links": { + "repo": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/tree/main/plugins/preset-builder", + "docs": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/blob/main/plugins/preset-builder/README.md" + }, + "license": "MIT", + "category": "ui", + "status": "beta", + "surfaces": { + "web": { + "clientModule": true + } + }, + "provides": {}, + "install": { + "rows": [ + { + "id": "dsh-preset-builder", + "name": "dsh-preset-builder", + "github": { + "repo": "Ephemeral-AI-Lab/dsh-plugins", + "ref": "main", + "subdir": "plugins/preset-builder" + } + } + ] + }, + "capabilities": [], + "verified": { + "at": "2026-09-04", + "packages": [ + { + "name": "dsh-preset-builder", + "version": "0.1.0" + } + ] + }, + "registryPath": "registry/official/preset-builder.json", + "npm": { + "dsh-preset-builder": { + "latestVersion": null, + "integrity": null, + "publishedAt": null, + "downloadsMonth": null, + "readme": null + } + }, + "readmeExcerpt": "# dsh-preset-builder\n\nA small DeepSeek Harness UI plugin that adds **Preset details** to Settings.\nIt makes preset inspection explicit and shows the exact `agent.cordis.yml`\nreturned by DSH's existing read-only preset API.\n\n```sh\ndsh plugin --profile web add ./preset-builder\n```\n\nRestart DSH after installation. The existing **Agent presets** page remains the\nplace to select, duplicate, edit, and delete presets; this plugin is deliberately\nan inspection prototype, so it does not replace or regress those controls.\n", + "readme": "# dsh-preset-builder\n\nA small DeepSeek Harness UI plugin that adds **Preset details** to Settings.\nIt makes preset inspection explicit and shows the exact `agent.cordis.yml`\nreturned by DSH's existing read-only preset API.\n\n```sh\ndsh plugin --profile web add ./preset-builder\n```\n\nRestart DSH after installation. The existing **Agent presets** page remains the\nplace to select, duplicate, edit, and delete presets; this plugin is deliberately\nan inspection prototype, so it does not replace or regress those controls.\n" + }, + { + "schemaVersion": 1, + "id": "sessions", + "source": "official", + "displayName": "Sessions", + "description": "Multi-session orchestration tools: discover sessions, read them with bounded reads, create new sessions, and deliver messages across agents with session_status, session_create, and session_send.", + "descriptionZh": "多会话编排工具:session_status 发现与有界读取会话,session_create 创建新会话,session_send 跨会话投递消息。", + "author": { + "name": "Ephemeral AI Lab", + "url": "https://github.com/Ephemeral-AI-Lab" + }, + "links": { + "repo": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/tree/main/plugins/sessions", + "docs": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/blob/main/plugins/sessions/README.md" + }, + "license": "MIT", + "category": "tools", + "status": "stable", + "surfaces": { + "server": {} + }, + "provides": { + "tools": [ + "session_status", + "session_create", + "session_send" + ] + }, + "install": { + "rows": [ + { + "id": "dsh-sessions", + "name": "dsh-sessions", + "github": { + "repo": "Ephemeral-AI-Lab/dsh-plugins", + "ref": "main", + "subdir": "plugins/sessions" + } + } + ] + }, + "capabilities": [ + "session-read", + "session-write" + ], + "verified": { + "at": "2026-09-04", + "packages": [ + { + "name": "dsh-sessions", + "version": "0.1.2" + } + ] + }, + "registryPath": "registry/official/sessions.json", + "npm": { + "dsh-sessions": { + "latestVersion": null, + "integrity": null, + "publishedAt": null, + "downloadsMonth": null, + "readme": null + } + }, + "readmeExcerpt": "# dsh-sessions\n\nAdds session discovery, plain session-log paths, and fresh-session creation to\nDeepSeek Harness.\n\n## 1. Release: 0.1.2\n\nThis release provides three public agent tools:\n\n- `session_status` for recent sessions or one exact session;\n- `session_create` for creating a fresh session with an initial prompt;\n- `session_send` for steering or following up with an existing session;\n\nIt supports explicit model provider, model, and thinking-effort selection during\nsession creation.\n\n## 2. Install the plugin\n\nInstall the published package into a DSH profile:\n\n~~~powershell\n# With the DSH CLI:\ndsh plugin --profile web add dsh-sessions@0.1.2\n\n# Without the `dsh` CLI:\nnpm install dsh-sessions@0.1.2\n~~~\n\nThe DSH profile installation is required for Harness to load the plugin. After\nupgrading, restart DSH and create a new agent session so the current tool\nregistry is loaded.\n\nFor local development, build the package and install the checkout into the\ntarget profile:\n\n~~~powershell\npnpm build\ndsh plugin --profile web add C:\\path\\to\\dsh-plugins\\sessions\n~~~\n\n## 3. Quick start\n\nCreate a child session, then inspect its status and log path:\n\n~~~text\nsession_create({ \"prompt\": \"Reply with exactly READY and nothing else.\" })\n\nsession_status({ \"session_id\": \"\" })\n\n~~~\n\n`session_create` returns a queued result containing the new session ID. Use\nthat ID with `session_status`. The returned `session_path` is a plain JSONL\nfile that ordinary `read`, `grep`, or `bash` tools can inspect.\n\n## 4. Agent tools\n\n### Agent tools\n\n| Tool | Arguments | Behavior |\n| --- | --- | --- |\n| `session_status` | `session_id?`, `recent_n?` | Lists recent sessions, or returns one exact status row with the backend-owned `session_path`. Defaults to the 50 most recently updated sessions. |\n| `session_create` | `prompt`, `preset?`, `model?`, `cwd?` | Creates a fresh session and queues its initial prompt. |\n| `session_send` | `session_id`, `message`, `mode?` | Sends text to an existing session. `mode` defaults to `steer`; `followup` queues another turn. |\n\nAn explicit creation model has this shape:\n\n~~~json\n{\n \"provider\": \"\",\n \"model\": \"\",\n \"reasoningEffort\": \"\"\n}\n~~~\n\nThe adapter validates the effort identifier against the selected model. The\n`cwd` option must be an existing absolute directory.\n\nNo slash commands are registered. Use the agent tools.\n\n## 5. Session lifecycle and delivery\n\n- `session_create` only creates a fresh session and queues its initial prompt;\n it does not wait for model completion.\n- `session_send` delivers to a live session or resumes a cold session using its\n stored preset before delivery.\n- `session_status` is inspection-only and defaults to 50 recent sessions.\n- Session persistence is configured for uncompressed, unpacked JSONL so the\n returned paths are directly readable by ordinary filesystem tools.\n\n## 6. E2E testing\n\nReusable prompt fixtures are in\n[`test/e2e/prompts`](./test/e2e/prompts/). The recommended flow is create,\ncapture the returned `session_id`, and substitute it into the status prompt.\n\nThe exact registered names are required. The session-read fixture is no longer\npart of the package; use the returned `session_path` with normal tools.\n\n## 7. Verify locally\n\n~~~powershell\npnpm typecheck\npnpm test -- --runInBand\npnpm build\npnpm pack --dry-run\n~~~\n\nThe published package includes the generated `lib` directory, the plugin patch,\nthe README, the implementation specification, and the reusable E2E prompt\nfixtures. Unit-test sources and development dependencies are not included.\n\n## 8. Package scope\n\nThe plugin uses public DeepSeek Harness and Cordis APIs and does not modify\nDeepSeek Harness source code. `dsh-loop` owns recurring self-prompts for the\ncurrent session; `dsh-sessions` owns cross-session inspection and creation.\n\n## 9. Documentation\n\n- [Session-tree implementation specification](./SPEC.md)\n- [Session-tree UI specification](./u.md)\n- [E2E prompt fixtures](./test/e2e/prompts/README.", + "readme": "# dsh-sessions\n\nAdds session discovery, plain session-log paths, and fresh-session creation to\nDeepSeek Harness.\n\n## 1. Release: 0.1.2\n\nThis release provides three public agent tools:\n\n- `session_status` for recent sessions or one exact session;\n- `session_create` for creating a fresh session with an initial prompt;\n- `session_send` for steering or following up with an existing session;\n\nIt supports explicit model provider, model, and thinking-effort selection during\nsession creation.\n\n## 2. Install the plugin\n\nInstall the published package into a DSH profile:\n\n~~~powershell\n# With the DSH CLI:\ndsh plugin --profile web add dsh-sessions@0.1.2\n\n# Without the `dsh` CLI:\nnpm install dsh-sessions@0.1.2\n~~~\n\nThe DSH profile installation is required for Harness to load the plugin. After\nupgrading, restart DSH and create a new agent session so the current tool\nregistry is loaded.\n\nFor local development, build the package and install the checkout into the\ntarget profile:\n\n~~~powershell\npnpm build\ndsh plugin --profile web add C:\\path\\to\\dsh-plugins\\sessions\n~~~\n\n## 3. Quick start\n\nCreate a child session, then inspect its status and log path:\n\n~~~text\nsession_create({ \"prompt\": \"Reply with exactly READY and nothing else.\" })\n\nsession_status({ \"session_id\": \"\" })\n\n~~~\n\n`session_create` returns a queued result containing the new session ID. Use\nthat ID with `session_status`. The returned `session_path` is a plain JSONL\nfile that ordinary `read`, `grep`, or `bash` tools can inspect.\n\n## 4. Agent tools\n\n### Agent tools\n\n| Tool | Arguments | Behavior |\n| --- | --- | --- |\n| `session_status` | `session_id?`, `recent_n?` | Lists recent sessions, or returns one exact status row with the backend-owned `session_path`. Defaults to the 50 most recently updated sessions. |\n| `session_create` | `prompt`, `preset?`, `model?`, `cwd?` | Creates a fresh session and queues its initial prompt. |\n| `session_send` | `session_id`, `message`, `mode?` | Sends text to an existing session. `mode` defaults to `steer`; `followup` queues another turn. |\n\nAn explicit creation model has this shape:\n\n~~~json\n{\n \"provider\": \"\",\n \"model\": \"\",\n \"reasoningEffort\": \"\"\n}\n~~~\n\nThe adapter validates the effort identifier against the selected model. The\n`cwd` option must be an existing absolute directory.\n\nNo slash commands are registered. Use the agent tools.\n\n## 5. Session lifecycle and delivery\n\n- `session_create` only creates a fresh session and queues its initial prompt;\n it does not wait for model completion.\n- `session_send` delivers to a live session or resumes a cold session using its\n stored preset before delivery.\n- `session_status` is inspection-only and defaults to 50 recent sessions.\n- Session persistence is configured for uncompressed, unpacked JSONL so the\n returned paths are directly readable by ordinary filesystem tools.\n\n## 6. E2E testing\n\nReusable prompt fixtures are in\n[`test/e2e/prompts`](./test/e2e/prompts/). The recommended flow is create,\ncapture the returned `session_id`, and substitute it into the status prompt.\n\nThe exact registered names are required. The session-read fixture is no longer\npart of the package; use the returned `session_path` with normal tools.\n\n## 7. Verify locally\n\n~~~powershell\npnpm typecheck\npnpm test -- --runInBand\npnpm build\npnpm pack --dry-run\n~~~\n\nThe published package includes the generated `lib` directory, the plugin patch,\nthe README, the implementation specification, and the reusable E2E prompt\nfixtures. Unit-test sources and development dependencies are not included.\n\n## 8. Package scope\n\nThe plugin uses public DeepSeek Harness and Cordis APIs and does not modify\nDeepSeek Harness source code. `dsh-loop` owns recurring self-prompts for the\ncurrent session; `dsh-sessions` owns cross-session inspection and creation.\n\n## 9. Documentation\n\n- [Session-tree implementation specification](./SPEC.md)\n- [Session-tree UI specification](./u.md)\n- [E2E prompt fixtures](./test/e2e/prompts/README.md)\n" + }, + { + "schemaVersion": 1, + "id": "sidechat", + "source": "official", + "displayName": "Sidechat", + "description": "A side-chat panel on the dsh Web workbench for quick questions in a separate, memory-only session. Two packages: Workbench UI first, then Sidechat itself.", + "descriptionZh": "dsh Web 工作台侧边的快速问答面板,使用独立的内存会话。两包安装:先 Workbench UI,再 Sidechat。", + "author": { + "name": "Ephemeral AI Lab", + "url": "https://github.com/Ephemeral-AI-Lab" + }, + "links": { + "repo": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/tree/main/plugins/sidechat", + "docs": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/blob/main/plugins/sidechat/README.md" + }, + "license": "MIT", + "category": "ui", + "status": "beta", + "surfaces": { + "server": {}, + "web": { + "clientModule": true + } + }, + "provides": {}, + "install": { + "rows": [ + { + "id": "workbench-ui", + "name": "dsh-workbench-ui", + "github": { + "repo": "Ephemeral-AI-Lab/dsh-plugins", + "ref": "main", + "subdir": "plugins/work-bench-ui" + } + }, + { + "id": "sidechat", + "name": "dsh-sidechat", + "github": { + "repo": "Ephemeral-AI-Lab/dsh-plugins", + "ref": "main", + "subdir": "plugins/sidechat" + } + } + ] + }, + "capabilities": [ + "session-write" + ], + "verified": { + "at": "2026-09-04", + "packages": [ + { + "name": "dsh-workbench-ui", + "version": "0.1.0" + }, + { + "name": "dsh-sidechat", + "version": "0.1.0" + } + ] + }, + "registryPath": "registry/official/sidechat.json", + "npm": { + "dsh-workbench-ui": { + "latestVersion": null, + "integrity": null, + "publishedAt": null, + "downloadsMonth": null, + "readme": null + }, + "dsh-sidechat": { + "latestVersion": null, + "integrity": null, + "publishedAt": null, + "downloadsMonth": null, + "readme": null + } + }, + "readmeExcerpt": "# dsh-workbench-ui\n\nReusable resizable right-side Workbench surface for DeepSeek Harness Web plugins.\n\nThe package owns the panel shell and exposes a client registry. Feature plugins\nregister their own tab and React content without modifying the Workbench UI. The\npanel occupies the host's block-level details column; opening it reduces the\nconversation width, and the host drag handle controls the panel width.\n\n```ts\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { WorkbenchService } from 'dsh-workbench-ui/client'\nimport type {} from 'dsh-workbench-ui/client'\nimport { TerminalPanel } from './TerminalPanel.js'\n\nexport const inject = ['workbench']\n\nexport function apply(ctx: ClientContext): void {\n const workbench = ctx.get('workbench') as WorkbenchService\n ctx.effect(() => workbench.register({\n id: 'terminal',\n label: 'Terminal',\n component: TerminalPanel,\n }), 'terminal: Workbench panel')\n}\n```\n\nRegistered components receive `{ close }` and own the complete display area.\nUse `workbench.open('terminal')`, `workbench.close()`, or\n`workbench.toggle('terminal')` from another client component to control the\nsurface. Opening and closing also drives the host details column. Registration\nis disposed with the owning plugin fiber.\n\nInstall it into the Web profile with:\n\n```sh\ndsh plugin --profile web add ./work-bench-ui\n```\n", + "readme": "# dsh-workbench-ui\n\nReusable resizable right-side Workbench surface for DeepSeek Harness Web plugins.\n\nThe package owns the panel shell and exposes a client registry. Feature plugins\nregister their own tab and React content without modifying the Workbench UI. The\npanel occupies the host's block-level details column; opening it reduces the\nconversation width, and the host drag handle controls the panel width.\n\n```ts\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { WorkbenchService } from 'dsh-workbench-ui/client'\nimport type {} from 'dsh-workbench-ui/client'\nimport { TerminalPanel } from './TerminalPanel.js'\n\nexport const inject = ['workbench']\n\nexport function apply(ctx: ClientContext): void {\n const workbench = ctx.get('workbench') as WorkbenchService\n ctx.effect(() => workbench.register({\n id: 'terminal',\n label: 'Terminal',\n component: TerminalPanel,\n }), 'terminal: Workbench panel')\n}\n```\n\nRegistered components receive `{ close }` and own the complete display area.\nUse `workbench.open('terminal')`, `workbench.close()`, or\n`workbench.toggle('terminal')` from another client component to control the\nsurface. Opening and closing also drives the host details column. Registration\nis disposed with the owning plugin fiber.\n\nInstall it into the Web profile with:\n\n```sh\ndsh plugin --profile web add ./work-bench-ui\n```\n" + }, + { + "schemaVersion": 1, + "id": "workbench-ui", + "source": "official", + "displayName": "Workbench UI", + "description": "The Web workbench frame: provides the workbench client service that other Web plugins (like Sidechat) register tabs into. Web-only; install it before plugins that depend on it.", + "descriptionZh": "Web 端工作台框架:提供 workbench client 服务,供 Sidechat 等插件注册标签页。仅 Web 端;需先于依赖它的插件安装。", + "author": { + "name": "Ephemeral AI Lab", + "url": "https://github.com/Ephemeral-AI-Lab" + }, + "links": { + "repo": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/tree/main/plugins/work-bench-ui", + "docs": "https://github.com/Ephemeral-AI-Lab/dsh-plugins/blob/main/plugins/work-bench-ui/README.md" + }, + "license": "MIT", + "category": "ui", + "status": "beta", + "surfaces": { + "web": { + "clientModule": true + } + }, + "provides": {}, + "install": { + "rows": [ + { + "id": "workbench-ui", + "name": "dsh-workbench-ui", + "github": { + "repo": "Ephemeral-AI-Lab/dsh-plugins", + "ref": "main", + "subdir": "plugins/work-bench-ui" + } + } + ] + }, + "capabilities": [], + "verified": { + "at": "2026-09-04", + "packages": [ + { + "name": "dsh-workbench-ui", + "version": "0.1.0" + } + ] + }, + "registryPath": "registry/official/workbench-ui.json", + "npm": { + "dsh-workbench-ui": { + "latestVersion": null, + "integrity": null, + "publishedAt": null, + "downloadsMonth": null, + "readme": null + } + }, + "readmeExcerpt": "# dsh-workbench-ui\n\nReusable resizable right-side Workbench surface for DeepSeek Harness Web plugins.\n\nThe package owns the panel shell and exposes a client registry. Feature plugins\nregister their own tab and React content without modifying the Workbench UI. The\npanel occupies the host's block-level details column; opening it reduces the\nconversation width, and the host drag handle controls the panel width.\n\n```ts\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { WorkbenchService } from 'dsh-workbench-ui/client'\nimport type {} from 'dsh-workbench-ui/client'\nimport { TerminalPanel } from './TerminalPanel.js'\n\nexport const inject = ['workbench']\n\nexport function apply(ctx: ClientContext): void {\n const workbench = ctx.get('workbench') as WorkbenchService\n ctx.effect(() => workbench.register({\n id: 'terminal',\n label: 'Terminal',\n component: TerminalPanel,\n }), 'terminal: Workbench panel')\n}\n```\n\nRegistered components receive `{ close }` and own the complete display area.\nUse `workbench.open('terminal')`, `workbench.close()`, or\n`workbench.toggle('terminal')` from another client component to control the\nsurface. Opening and closing also drives the host details column. Registration\nis disposed with the owning plugin fiber.\n\nInstall it into the Web profile with:\n\n```sh\ndsh plugin --profile web add ./work-bench-ui\n```\n", + "readme": "# dsh-workbench-ui\n\nReusable resizable right-side Workbench surface for DeepSeek Harness Web plugins.\n\nThe package owns the panel shell and exposes a client registry. Feature plugins\nregister their own tab and React content without modifying the Workbench UI. The\npanel occupies the host's block-level details column; opening it reduces the\nconversation width, and the host drag handle controls the panel width.\n\n```ts\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport type { WorkbenchService } from 'dsh-workbench-ui/client'\nimport type {} from 'dsh-workbench-ui/client'\nimport { TerminalPanel } from './TerminalPanel.js'\n\nexport const inject = ['workbench']\n\nexport function apply(ctx: ClientContext): void {\n const workbench = ctx.get('workbench') as WorkbenchService\n ctx.effect(() => workbench.register({\n id: 'terminal',\n label: 'Terminal',\n component: TerminalPanel,\n }), 'terminal: Workbench panel')\n}\n```\n\nRegistered components receive `{ close }` and own the complete display area.\nUse `workbench.open('terminal')`, `workbench.close()`, or\n`workbench.toggle('terminal')` from another client component to control the\nsurface. Opening and closing also drives the host details column. Registration\nis disposed with the owning plugin fiber.\n\nInstall it into the Web profile with:\n\n```sh\ndsh plugin --profile web add ./work-bench-ui\n```\n" + }, + { + "schemaVersion": 1, + "id": "acp", + "source": "dsh", + "displayName": "ACP Server", + "description": "An Agent Client Protocol server (JSON-RPC over stdio) that lets automation clients and CLIs drive dsh harness agents programmatically.", + "descriptionZh": "Agent Client Protocol 服务器(stdio 上的 JSON-RPC),让自动化客户端与 CLI 以编程方式驱动 dsh harness agent。", + "author": { + "name": "DeepSeek AI", + "url": "https://github.com/deepseek-ai" + }, + "links": { + "repo": "https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/acp", + "docs": "https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/acp/README.md" + }, + "license": "MIT", + "category": "integration", + "status": "stable", + "surfaces": { + "server": {} + }, + "provides": {}, + "install": { + "rows": [ + { + "id": "acp", + "name": "@deepseek-ai/dsh-acp", + "activation": "profile-patch", + "npm": { + "spec": "@deepseek-ai/dsh-acp" + } + } + ] + }, + "capabilities": [ + "network", + "process-spawn" + ], + "verified": { + "at": "2026-09-04", + "packages": [ + { + "name": "@deepseek-ai/dsh-acp", + "version": "0.0.1-rc.1" + } + ] + }, + "registryPath": "registry/dsh/acp.json", + "npm": { + "@deepseek-ai/dsh-acp": { + "latestVersion": "0.0.1-rc.1", + "integrity": "sha512-RTJmbXz9g4fTN5ebcd9Ra21Ca7UEMA9gxBnsXNcJy9eoDrOAb8DcrBkIlxRnAvluWLqWf9dyPGiUvLO4Oh9uTg==", + "publishedAt": "2026-08-10T19:41:14.563Z", + "downloadsMonth": 8926, + "readme": null + } + }, + "readmeExcerpt": null, + "readme": null + }, + { + "schemaVersion": 1, + "id": "code-runtime-py", + "source": "dsh", + "displayName": "Code Runtime (Python)", + "description": "Python execution environment behind run_code for PTC mode, as the alternative to the TypeScript worker-thread runtime. Containment, not a security boundary; bash-equivalent trust by design.", + "descriptionZh": "PTC 模式 run_code 的 Python 执行环境,是 TypeScript worker-thread 运行时的替代选项。隔离而非安全边界,信任级别等同 bash。", + "author": { + "name": "DeepSeek AI", + "url": "https://github.com/deepseek-ai" + }, + "links": { + "repo": "https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/code-runtime", + "docs": "https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/code-runtime/README.md" + }, + "license": "MIT", + "category": "tools", + "status": "stable", + "surfaces": { + "server": {} + }, + "provides": { + "tools": [ + "run_code" + ] + }, + "install": { + "rows": [ + { + "id": "code-runtime", + "name": "@deepseek-ai/dsh-code-runtime-python", + "activation": "profile-patch", + "npm": { + "spec": "@deepseek-ai/dsh-code-runtime-python" + } + } + ] + }, + "capabilities": [ + "code-execution" + ], + "verified": { + "at": "2026-09-04", + "packages": [ + { + "name": "@deepseek-ai/dsh-code-runtime-python", + "version": "0.1.0-rc.8" + } + ] + }, + "registryPath": "registry/dsh/code-runtime-py.json", + "npm": { + "@deepseek-ai/dsh-code-runtime-python": { + "latestVersion": "0.1.0-rc.8", + "integrity": "sha512-VAXOqpE34SO2cQTTu+5VJLp77OvMOdnW0LMHGC6CDyqE7ZJMlv1bMPoAGDAuutIYSdhaSvhJB+iMw2xSr8D9qQ==", + "publishedAt": "2026-08-19T15:42:20.456Z", + "downloadsMonth": 473, + "readme": null + } + }, + "readmeExcerpt": null, + "readme": null + }, + { + "schemaVersion": 1, + "id": "code-runtime-ts", + "source": "dsh", + "displayName": "Code Runtime (TypeScript)", + "description": "The execution environment behind run_code for PTC mode: each program runs in one fresh worker thread with an empty environment, heap cap, and hard termination. Containment, not a security boundary; bash-equivalent trust by design.", + "descriptionZh": "PTC 模式 run_code 的执行环境:每个程序在全新 worker 线程中运行,空环境、堆上限、硬终止。这是隔离而非安全边界,信任级别等同 bash。", + "author": { + "name": "DeepSeek AI", + "url": "https://github.com/deepseek-ai" + }, + "links": { + "repo": "https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/code-runtime", + "docs": "https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/code-runtime/README.md" + }, + "license": "MIT", + "category": "tools", + "status": "stable", + "surfaces": { + "server": {} + }, + "provides": { + "tools": [ + "run_code" + ] + }, + "install": { + "rows": [ + { + "id": "code-runtime", + "name": "@deepseek-ai/dsh-code-runtime-worker-thread", + "activation": "profile-patch", + "npm": { + "spec": "@deepseek-ai/dsh-code-runtime-worker-thread" + } + } + ] + }, + "capabilities": [ + "code-execution" + ], + "verified": { + "at": "2026-09-04", + "packages": [ + { + "name": "@deepseek-ai/dsh-code-runtime-worker-thread", + "version": "0.0.1-rc.3" + } + ] + }, + "registryPath": "registry/dsh/code-runtime-ts.json", + "npm": { + "@deepseek-ai/dsh-code-runtime-worker-thread": { + "latestVersion": "0.0.1-rc.3", + "integrity": "sha512-l65X/u0lfphAHU3c6690fOG7w3PM5fGY9PK/mREiKQ7mvDDbosutkV/C6KqYgvONeCOat7VNHw4IxQRlXBHnqQ==", + "publishedAt": "2026-08-12T20:35:16.524Z", + "downloadsMonth": 1236500, + "readme": null + } + }, + "readmeExcerpt": null, + "readme": null + }, + { + "schemaVersion": 1, + "id": "lsp", + "source": "dsh", + "displayName": "LSP Navigation", + "description": "One read-only lsp tool (goToDefinition / findReferences / goToImplementation / hover) over stdio language servers, for precise navigation when textual matches are ambiguous. Requires the session cwd as its workspace root.", + "descriptionZh": "基于 stdio 语言服务器的只读 lsp 工具(定义跳转 / 引用查找 / 实现跳转 / hover),在文本搜索有歧义时提供精确导航。需要会话 cwd 作为工作区根。", + "author": { + "name": "DeepSeek AI", + "url": "https://github.com/deepseek-ai" + }, + "links": { + "repo": "https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/lsp", + "docs": "https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/lsp/README.md" + }, + "license": "MIT", + "category": "integration", + "status": "stable", + "surfaces": { + "server": {} + }, + "provides": { + "tools": [ + "lsp" + ] + }, + "install": { + "rows": [ + { + "id": "lsp-stdio", + "name": "@deepseek-ai/dsh-lsp-stdio", + "activation": "profile-patch", + "npm": { + "spec": "@deepseek-ai/dsh-lsp-stdio" + } + }, + { + "id": "tool-lsp", + "name": "@deepseek-ai/dsh-tool-lsp", + "activation": "profile-patch", + "npm": { + "spec": "@deepseek-ai/dsh-tool-lsp" + } + } + ] + }, + "capabilities": [ + "process-spawn" + ], + "verified": { + "at": "2026-09-04", + "packages": [ + { + "name": "@deepseek-ai/dsh-lsp-stdio", + "version": "0.0.1-rc.5" + }, + { + "name": "@deepseek-ai/dsh-tool-lsp", + "version": "0.0.1-rc.1" + } + ] + }, + "registryPath": "registry/dsh/lsp.json", + "npm": { + "@deepseek-ai/dsh-lsp-stdio": { + "latestVersion": "0.0.1-rc.5", + "integrity": "sha512-3f4EDc+BgtipyMhbMVhzSoNlLpgFWHJzhLmCsv/NooIXjacKMIehbjYSiOfLCRpGyCr1PFuKWc52BTGyXTHy5g==", + "publishedAt": "2026-08-12T22:37:17.244Z", + "downloadsMonth": 3863, + "readme": null + }, + "@deepseek-ai/dsh-tool-lsp": { + "latestVersion": "0.0.1-rc.1", + "integrity": "sha512-tK8VebAYJO4rD/lsZ5tNZ7hmlXnfEcVWzhtPmoZCMzejCWhF9pRhrP+jmSvui4K9QZg097LRy0Tvzu8OQjx/JA==", + "publishedAt": "2026-08-10T19:43:20.091Z", + "downloadsMonth": 3856, + "readme": null + } + }, + "readmeExcerpt": null, + "readme": null + }, + { + "schemaVersion": 1, + "id": "mcp", + "source": "dsh", + "displayName": "MCP Client", + "description": "The Model Context Protocol client: external servers contribute mcp__server__tool tools. Servers are declared in the profile patch; Mayfly's /mcp browser shows their status and schemas.", + "descriptionZh": "Model Context Protocol 客户端:外部服务器以 mcp__server__tool 形式贡献工具。服务器在 profile patch 中声明;Mayfly 的 /mcp 浏览器可查看状态与 schema。", + "author": { + "name": "DeepSeek AI", + "url": "https://github.com/deepseek-ai" + }, + "links": { + "repo": "https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/mcp", + "docs": "https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/mcp/README.md" + }, + "license": "MIT", + "category": "integration", + "status": "stable", + "surfaces": { + "server": {} + }, + "provides": {}, + "install": { + "rows": [ + { + "id": "mcp-client", + "name": "@deepseek-ai/dsh-mcp-client", + "activation": "profile-patch", + "npm": { + "spec": "@deepseek-ai/dsh-mcp-client" + } + } + ] + }, + "capabilities": [ + "network", + "process-spawn" + ], + "verified": { + "at": "2026-09-04", + "packages": [ + { + "name": "@deepseek-ai/dsh-mcp-client", + "version": "0.0.1-rc.1" + } + ] + }, + "registryPath": "registry/dsh/mcp.json", + "npm": { + "@deepseek-ai/dsh-mcp-client": { + "latestVersion": "0.0.1-rc.1", + "integrity": "sha512-YPsOvuWFmWl1Pai+9FrsDLNuWFuMXIEZHn9uXJFqDfo/dTyJRZuYFM+jbs5g9eGEKhHgqXibsgeCm4Px1tb4DQ==", + "publishedAt": "2026-08-10T19:38:15.865Z", + "downloadsMonth": 1241964, + "readme": null + } + }, + "readmeExcerpt": null, + "readme": null + }, + { + "schemaVersion": 1, + "id": "terminal", + "source": "dsh", + "displayName": "Terminal (PTY)", + "description": "The persistent interactive terminal of Codex / Claude Code-style TUIs: six tools (terminal_open / send / read / signal / close / list) over a PTY backend that composes with the sandbox. Mayfly renders its output as terminal cards.", + "descriptionZh": "Codex / Claude Code 式 TUI 的持久交互终端:terminal_open / send / read / signal / close / list 六个工具,PTY 后端且与沙箱组合。Mayfly 以终端卡片渲染输出。", + "author": { + "name": "DeepSeek AI", + "url": "https://github.com/deepseek-ai" + }, + "links": { + "repo": "https://github.com/deepseek-ai/deepseek-harness/tree/master/packages/terminal", + "docs": "https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/terminal/README.md" + }, + "license": "MIT", + "category": "tools", + "status": "stable", + "surfaces": { + "server": {} + }, + "provides": { + "tools": [ + "terminal_open", + "terminal_send", + "terminal_read", + "terminal_signal", + "terminal_close", + "terminal_list" + ] + }, + "install": { + "rows": [ + { + "id": "terminal-bash", + "name": "@deepseek-ai/dsh-terminal-bash", + "activation": "profile-patch", + "npm": { + "spec": "@deepseek-ai/dsh-terminal-bash" + } + }, + { + "id": "tool-terminal", + "name": "@deepseek-ai/dsh-tool-terminal", + "activation": "profile-patch", + "npm": { + "spec": "@deepseek-ai/dsh-tool-terminal" + } + } + ] + }, + "capabilities": [ + "shell", + "process-spawn" + ], + "verified": { + "at": "2026-09-04", + "packages": [ + { + "name": "@deepseek-ai/dsh-terminal-bash", + "version": "0.0.1-rc.3" + }, + { + "name": "@deepseek-ai/dsh-tool-terminal", + "version": "0.0.1-rc.5" + } + ] + }, + "registryPath": "registry/dsh/terminal.json", + "npm": { + "@deepseek-ai/dsh-terminal-bash": { + "latestVersion": "0.0.1-rc.3", + "integrity": "sha512-O6SczKhyDzyJlnwjwLmBX3/rwoHjE+SNIm4AJ4qsLjDwlCZfrr+EMFT20LXobvGBUFanlDmKoOyZBOsyTrinUg==", + "publishedAt": "2026-08-12T20:35:47.152Z", + "downloadsMonth": 1237978, + "readme": null + }, + "@deepseek-ai/dsh-tool-terminal": { + "latestVersion": "0.0.1-rc.5", + "integrity": "sha512-c/BARJ3tWoaviyy/b/8JVk97KcRyue55wE1hsw1a59k3SNDDkFp1PhQhZqIBFjfmSHUqghZLpFSa7okaQuJPIA==", + "publishedAt": "2026-08-12T22:38:18.942Z", + "downloadsMonth": 1926, + "readme": null + } + }, + "readmeExcerpt": null, + "readme": null + } + ] +}