feat: DeepSeek Harness plugin (dsh-postiz)

Add plugins/dsh-postiz, an installable DeepSeek Harness bundle
(`dsh plugin --profile web add "github:gitroomhq/postiz-agent#path:/plugins/dsh-postiz"`).

- cordis.patch.yml mounts a `postiz` provider row plus one
  @deepseek-ai/dsh-mcp-client row pointed at https://mcp.postiz.com/mcp
  (streamable HTTP, Bearer auth from POSTIZ_API_KEY; baseUrl overridable
  for self-hosted instances)
- the provider exposes ctx.postiz (url/headers) for the mcp row's !!js
  config and registers a `postiz` skill covering the integrationList →
  integrationSchema → schedulePostTool workflow and HTML content rules
- node:test unit tests; verified against dsh 0.1.5-rc: the mcp row
  activates, sends the Bearer header, and retries on auth failure
- README / CHANGELOG entries; ignore plugin-local lockfiles

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Nevo David
2026-09-11 11:22:57 +07:00
parent 6d3be9c0ae
commit b4960beefa
10 changed files with 524 additions and 0 deletions
+3
View File
@@ -36,3 +36,6 @@ coverage/
# Temporary files
*.tmp
*.temp
# Plugin subpackages install their peer deps locally for tests
plugins/*/pnpm-lock.yaml
+3
View File
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- DeepSeek Harness plugin: `plugins/dsh-postiz` is an installable `dsh` bundle (`dsh plugin --profile web add dsh-postiz`). It mounts one `@deepseek-ai/dsh-mcp-client` row pointed at the hosted Postiz MCP server (`https://mcp.postiz.com/mcp`, Bearer auth from `POSTIZ_API_KEY`) and registers a `postiz` skill describing the integrationList → integrationSchema → schedulePostTool workflow. Self-hosted instances override `baseUrl` on the `postiz` row. Listed on [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin); the repo now carries the `dsh-plugin` topic.
## [2.0.17] - 2026-09-01
### Added
+12
View File
@@ -37,6 +37,18 @@ This repo ships a [Cursor plugin](https://cursor.com/docs/reference/plugins) man
The plugin exposes the `postiz` skill, which drives the `postiz` CLI (the CLI handles media uploads, which is required for image/video posts). Make sure the CLI is installed (`npm install -g postiz`) and authenticated (`postiz auth:login` or `export POSTIZ_API_KEY=...`) before asking the agent to post.
### DeepSeek Harness plugin
This repo ships a [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) bundle at [`plugins/dsh-postiz`](plugins/dsh-postiz). It connects the agent to the hosted Postiz MCP server and registers a `postiz` workflow skill.
```bash
dsh plugin --profile web add "github:gitroomhq/postiz-agent#path:/plugins/dsh-postiz"
export POSTIZ_API_KEY=your-api-key # Postiz → Settings → Developers → Public API
dsh web
```
The Postiz tools then appear as `mcp__postiz__*` (`integrationList`, `integrationSchema`, `schedulePostTool`, ...). Self-hosted instances override `baseUrl` on the `postiz` row. See the [plugin README](plugins/dsh-postiz/README.md) for configuration.
# Postiz CLI
**Social media automation CLI for AI agents** - Schedule posts across 28+ platforms programmatically.
+17
View File
@@ -0,0 +1,17 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2024 Nevo David
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
+117
View File
@@ -0,0 +1,117 @@
# dsh-postiz
[![npm](https://img.shields.io/npm/v/dsh-postiz)](https://www.npmjs.com/package/dsh-postiz)
**npm:** [`dsh-postiz`](https://www.npmjs.com/package/dsh-postiz) ·
**source:** [gitroomhq/postiz-agent](https://github.com/gitroomhq/postiz-agent/tree/main/plugins/dsh-postiz)
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) plugin for
[Postiz](https://postiz.com), the open-source social media scheduler. It connects the agent
to the Postiz MCP server so it can list your connected channels, fetch each platform's
posting rules, and schedule, draft, or publish posts across 28+ platforms (X, LinkedIn,
Instagram, Facebook, Threads, TikTok, YouTube, Reddit, Pinterest, Bluesky, Mastodon,
Discord, Slack, Telegram, and more).
## Install
From the repository:
```bash
dsh plugin --profile web add "github:gitroomhq/postiz-agent#path:/plugins/dsh-postiz"
```
Or from npm:
```bash
dsh plugin --profile web add dsh-postiz
```
Then give it a Postiz API key. Copy it from **Postiz → Settings → Developers → Public API**
and export it before starting dsh, or put it in `$DSH_HOME/.env`:
```bash
export POSTIZ_API_KEY=your-api-key
dsh web
```
Restart the profile after installing. Ask the agent *"List my connected social media
accounts"* to verify the connection.
## What you get
The bundle mounts two rows:
| Row | Package | Role |
|---|---|---|
| `postiz` | `dsh-postiz` (this package) | Reads the API key, exposes `ctx.postiz` (`url`, `headers`), and registers the `postiz` skill that teaches the agent the posting workflow and the HTML content rules. |
| `postiz-mcp` | `@deepseek-ai/dsh-mcp-client` (shipped with dsh) | Connects to `https://mcp.postiz.com/mcp` over streamable HTTP with a `Bearer` header and registers the server's tools. |
The model sees the Postiz MCP tools under the `mcp__postiz__` namespace:
| Tool | What it does |
|---|---|
| `mcp__postiz__integrationList` | List connected channels (optionally filtered by group) |
| `mcp__postiz__groupList` | List customer groups |
| `mcp__postiz__integrationSchema` | Posting rules, character limits, and required settings for a platform |
| `mcp__postiz__triggerTool` | Platform helpers (list Discord channels, search subreddits, list LinkedIn pages) |
| `mcp__postiz__schedulePostTool` | Schedule, draft, or immediately publish posts |
| `mcp__postiz__postsListTool` | List posts scheduled between two dates |
| `mcp__postiz__postSettingsTool` | Update settings of an unpublished post |
| `mcp__postiz__generateImageTool` | Generate an image for a post |
| `mcp__postiz__generateVideoOptions` / `videoFunctionTool` / `generateVideoTool` | Video generation options and generation |
The tool list comes from the server at connect time, so new Postiz tools appear without a
plugin update. See the [Postiz MCP tools reference](https://docs.postiz.com/mcp/tools).
## Configuration
Override the `postiz` row in your profile's `cordis.patch.yml` (`$DSH_HOME/profiles/web/cordis.patch.yml`).
A bare `id:` configures the existing row:
```yaml
- id: postiz
config:
apiKeyEnv: POSTIZ_API_KEY # env var that holds the key (default)
baseUrl: https://mcp.postiz.com # self-hosted: https://your-postiz-server.com
skill: true # register the `postiz` workflow skill
```
| Field | Default | Description |
|---|---|---|
| `apiKeyEnv` | `POSTIZ_API_KEY` | Environment variable read at boot for the API key. |
| `apiKey` | `''` | Inline key. Prefer the env var; this exists for patch-level overrides. |
| `baseUrl` | `https://mcp.postiz.com` | Postiz host. The MCP endpoint is `<baseUrl>/mcp`. Self-hosted instances point this at their backend. |
| `skill` | `true` | Register the `postiz` skill on `ctx.skills`. |
Without a key the `postiz` row logs a warning naming the variable to set, and the
`postiz-mcp` row registers no tools. dsh keeps booting.
## Self-hosted Postiz
The MCP server is part of the Postiz backend and listens at `/mcp` (Bearer auth). Point
`baseUrl` at your backend and make sure your reverse proxy forwards `/mcp` with streaming
HTTP enabled. See [Reverse Proxies](https://docs.postiz.com/self-host/reverse-proxies/caddy).
## Development
```bash
cd plugins/dsh-postiz
pnpm install
pnpm test
```
Link a local checkout into a profile:
```bash
dsh plugin --profile web add /absolute/path/to/postiz-agent/plugins/dsh-postiz
```
## Related
- [Postiz CLI](https://github.com/gitroomhq/postiz-agent) — the `postiz` command-line tool and the Claude Code / Cursor / Grok plugins in this repository.
- [Postiz MCP docs](https://docs.postiz.com/mcp/introduction)
- [Postiz public API](https://docs.postiz.com/public-api/introduction)
## License
AGPL-3.0, same as the rest of this repository.
+38
View File
@@ -0,0 +1,38 @@
# Patch layer contributed by the dsh-postiz bundle.
#
# Two rows:
# 1. `postiz` - this package's provider plugin. Reads the Postiz API key
# (POSTIZ_API_KEY by default), exposes `ctx.postiz` with the
# MCP endpoint URL and auth headers, and registers the
# `postiz` skill that teaches the agent the posting workflow.
# 2. `postiz-mcp` - one @deepseek-ai/dsh-mcp-client instance that connects to
# the Postiz MCP server over streamable HTTP and registers
# its tools as mcp__postiz__* (integrationList,
# integrationSchema, schedulePostTool, ...).
#
# Configuration (override the `postiz` row in your profile's cordis.patch.yml):
# apiKeyEnv - env var holding the Postiz API key (default: POSTIZ_API_KEY)
# apiKey - inline key; prefer the env var or a .env file in $DSH_HOME
# baseUrl - Postiz host (default: https://mcp.postiz.com; self-hosted
# instances use their own backend URL)
# skill - register the `postiz` workflow skill (default: true)
- insert:
- id: postiz
name: dsh-postiz
config:
apiKeyEnv: POSTIZ_API_KEY
baseUrl: https://mcp.postiz.com
- id: postiz-mcp
name: '@deepseek-ai/dsh-mcp-client'
inject: [postiz]
config:
transport: streamable-http
serverName: postiz
url: !!js ctx.postiz.url
headers: !!js ctx.postiz.headers
failOnStartupError: false
reconnect:
enabled: true
initialDelayMs: 2000
maxDelayMs: 30000
maxAttempts: 10
+103
View File
@@ -0,0 +1,103 @@
/**
* dsh-postiz provider plugin.
*
* Provides the `postiz` service so the bundle's cordis.patch.yml can point one
* `@deepseek-ai/dsh-mcp-client` row at the Postiz MCP server without putting
* the API key in configuration: the mcp-client row injects `postiz` and reads
* `ctx.postiz.url` / `ctx.postiz.headers` in its `!!js` config.
*
* It also registers the `postiz` skill on `ctx.skills` (when a skills service
* is mounted) so the agent knows the integrationList → integrationSchema →
* schedulePostTool workflow and the HTML content rules.
*
* Named exports only (no default export): a stray default export makes the
* Loader collapse the namespace and drop `inject`/`apply`.
*/
import z from '@deepseek-ai/schemastery'
import { SKILL_CONTENT, SKILL_DESCRIPTION, SKILL_NAME } from './skill.js'
/** Stable Cordis plugin name. */
export const name = 'dsh-postiz'
/** Service provided by this plugin and injected by the mcp-client row. */
export const POSTIZ_SERVICE = 'postiz'
/** Default Postiz Cloud host; self-hosted instances override `baseUrl`. */
export const DEFAULT_BASE_URL = 'https://mcp.postiz.com'
export const Config = z.object({
/** Environment variable that holds the Postiz API key. */
apiKeyEnv: z.string().default('POSTIZ_API_KEY'),
/** Inline API key. Prefer `apiKeyEnv`; this exists for patch-level overrides. */
apiKey: z.string().role('secret').default(''),
/** Postiz host. The MCP endpoint is `<baseUrl>/mcp`. */
baseUrl: z.string().default(DEFAULT_BASE_URL),
/** Register the `postiz` workflow skill on `ctx.skills`. */
skill: z.boolean().default(true),
})
/**
* Resolve the connection values from config and environment.
*
* @param {object} config - validated plugin config.
* @param {Record<string, string | undefined>} env - environment to read the key from.
* @returns {{ url: string, headers: Record<string, string>, configured: boolean, source: string }}
*/
export function resolveConnection(config, env = process.env) {
const base = String(config?.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '')
const url = base.endsWith('/mcp') ? base : `${base}/mcp`
const envName = String(config?.apiKeyEnv || 'POSTIZ_API_KEY')
const inline = String(config?.apiKey || '').trim()
const fromEnv = String(env[envName] || '').trim()
const key = inline || fromEnv
const source = inline ? 'config.apiKey' : fromEnv ? `env.${envName}` : 'none'
return {
url,
headers: key ? { Authorization: `Bearer ${key}` } : {},
configured: key.length > 0,
source,
}
}
/**
* @param {object} ctx - plugin context.
* @param {object} config - validated plugin config.
*/
export function apply(ctx, config) {
const cfg = Config(config ?? {})
const conn = resolveConnection(cfg)
const log = ctx.logger ?? console
if (conn.configured) {
log.info?.(`dsh-postiz: Postiz MCP endpoint ${conn.url} (key from ${conn.source})`)
} else {
log.warn?.(
`dsh-postiz: no Postiz API key found — set ${cfg.apiKeyEnv} (Postiz → Settings → Developers → Public API) ` +
`or \`apiKey\` on the \`postiz\` row; the postiz-mcp row will connect to ${conn.url} without credentials and register no tools.`,
)
}
// Plain, unfrozen values on purpose: the mcp-client row validates the
// interpolated `headers` with schemastery, which rejects frozen objects.
// The getter hands every evaluation its own copy.
ctx.provide(POSTIZ_SERVICE, {
url: conn.url,
get headers() {
return { ...conn.headers }
},
configured: conn.configured,
})
if (cfg.skill) {
ctx.inject(['skills'], (sctx) => {
if (typeof sctx.skills?.register !== 'function') return
sctx.skills.register({
name: SKILL_NAME,
description: SKILL_DESCRIPTION,
content: SKILL_CONTENT,
source: 'runtime',
invocation: { modelInvocable: true, userInvocable: true },
})
})
}
}
+88
View File
@@ -0,0 +1,88 @@
/**
* The `postiz` skill body registered on `ctx.skills`.
*
* It teaches the agent the order of operations for the Postiz MCP tools that
* the sibling `postiz-mcp` row exposes as `mcp__postiz__*`. Kept as a plain
* string so the plugin ships no extra files and needs no filesystem access.
*/
export const SKILL_NAME = 'postiz'
export const SKILL_DESCRIPTION =
'Schedule, draft, or publish social media posts through Postiz (X, LinkedIn, Instagram, Facebook, Threads, TikTok, YouTube, Reddit, Bluesky, Mastodon, Discord, Slack, Telegram and more). Use when the user wants to post to social media, list connected channels, or plan a content calendar.'
export const SKILL_CONTENT = `# Postiz
Postiz is a social media scheduler. The \`mcp__postiz__*\` tools talk to the
user's Postiz account. Always follow this order:
## 1. Find the channel
Call \`mcp__postiz__integrationList\` to get connected accounts. Each entry has
an \`id\` (the integration ID), a \`name\`, and a \`platform\` such as \`x\`,
\`linkedin\`, \`instagram\`, \`facebook\`, \`threads\`, \`tiktok\`, \`youtube\`,
\`reddit\`, \`bluesky\`, \`mastodon\`, \`discord\`, \`slack\`, \`telegram\`.
If the account has customer groups, \`mcp__postiz__groupList\` lists them and
\`integrationList\` accepts a \`group\` filter.
Never guess an integration ID. If the user names a platform that is not in the
list, say so and stop.
## 2. Learn the platform rules
Call \`mcp__postiz__integrationSchema\` with \`{ platform, isPremium }\` before
composing. It returns \`maxLength\`, \`rules\`, a \`settings\` JSON schema of
required per-platform settings, and helper \`tools\` (for example: list Discord
channels, search subreddits, list LinkedIn pages). Run helpers with
\`mcp__postiz__triggerTool\` when a setting needs an ID you do not have.
## 3. Compose the content
- Content is HTML. Wrap every line in \`<p>\`. Allowed tags: \`<p>\`, \`<h1>\`,
\`<h2>\`, \`<h3>\`, \`<strong>\`, \`<u>\`, \`<ul>\`, \`<li>\`. Do not combine
\`<u>\` and \`<strong>\` in one element.
- Respect \`maxLength\` from the schema.
- \`postsAndComments\` is an array. On thread platforms (X, Threads, Bluesky)
each item is a new post in the thread; on comment platforms (LinkedIn,
Facebook) the first item is the post and the rest are comments.
- \`attachments\` are media URLs. To generate media first, use
\`mcp__postiz__generateImageTool\` or \`mcp__postiz__generateVideoTool\`
(check \`generateVideoOptions\` and \`videoFunctionTool\` for settings).
## 4. Schedule or publish
Call \`mcp__postiz__schedulePostTool\` with a \`socialPost\` array. Each item:
\`\`\`json
{
"integrationId": "<id from integrationList>",
"isPremium": false,
"date": "2025-01-15T10:00:00.000Z",
"shortLink": false,
"type": "schedule",
"postsAndComments": [{ "content": "<p>Hello world</p>", "attachments": [] }],
"settings": [{ "key": "<from integrationSchema>", "value": "..." }]
}
\`\`\`
- \`type\` is \`draft\`, \`schedule\`, or \`now\`. Use \`draft\` when the user
wants to review first; use \`now\` only when they explicitly ask to publish
immediately.
- \`date\` is UTC ISO-8601. Convert the user's local time; if no time is given,
ask or pick a sensible one and state it.
- One item per channel per time slot. Twenty posts across a month is twenty
items in one call.
## 5. Review and adjust
\`mcp__postiz__postsListTool\` lists posts between two dates.
\`mcp__postiz__postSettingsTool\` updates the settings of a post that has not
been published yet.
## Setup
The tools are missing when no API key is configured. The key comes from the
\`POSTIZ_API_KEY\` environment variable (Postiz → Settings → Developers →
Public API). Self-hosted instances set \`baseUrl\` on the \`postiz\` row in the
profile's \`cordis.patch.yml\`.
`
+62
View File
@@ -0,0 +1,62 @@
{
"name": "dsh-postiz",
"version": "0.1.0",
"description": "DeepSeek Harness plugin that connects the agent to Postiz over MCP: list connected channels, fetch platform posting rules, and schedule, draft, or publish social media posts across 28+ platforms.",
"type": "module",
"main": "./lib/index.js",
"exports": {
".": "./lib/index.js",
"./package.json": "./package.json",
"./cordis.patch.yml": "./cordis.patch.yml"
},
"files": [
"lib/",
"cordis.patch.yml",
"README.md",
"LICENSE"
],
"scripts": {
"test": "node --test"
},
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
},
"peerDependencies": {
"@deepseek-ai/cordis": "^4.0.1",
"@deepseek-ai/schemastery": "^3.18.1"
},
"engines": {
"node": ">=20.0.0"
},
"keywords": [
"dsh",
"dsh-plugin",
"deepseek-harness",
"postiz",
"mcp",
"social-media",
"social-media-scheduler",
"scheduling",
"automation"
],
"author": {
"name": "Nevo David",
"email": "nevo@postiz.com",
"url": "https://postiz.com"
},
"homepage": "https://github.com/gitroomhq/postiz-agent/tree/main/plugins/dsh-postiz#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/gitroomhq/postiz-agent.git",
"directory": "plugins/dsh-postiz"
},
"bugs": {
"url": "https://github.com/gitroomhq/postiz-agent/issues"
},
"license": "AGPL-3.0",
"publishConfig": {
"access": "public"
}
}
+81
View File
@@ -0,0 +1,81 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import * as plugin from '../lib/index.js'
import { SKILL_CONTENT, SKILL_NAME } from '../lib/skill.js'
const here = dirname(fileURLToPath(import.meta.url))
test('named exports only, with a stable plugin name', () => {
assert.equal(plugin.name, 'dsh-postiz')
assert.equal(typeof plugin.apply, 'function')
assert.equal('default' in plugin, false)
})
test('resolveConnection reads the key from the configured env var', () => {
const conn = plugin.resolveConnection(
{ apiKeyEnv: 'POSTIZ_API_KEY', apiKey: '', baseUrl: 'https://mcp.postiz.com' },
{ POSTIZ_API_KEY: 'abc123' },
)
assert.equal(conn.url, 'https://mcp.postiz.com/mcp')
assert.deepEqual(conn.headers, { Authorization: 'Bearer abc123' })
assert.equal(conn.configured, true)
assert.equal(conn.source, 'env.POSTIZ_API_KEY')
})
test('resolveConnection prefers an inline key and normalizes self-hosted URLs', () => {
const conn = plugin.resolveConnection(
{ apiKeyEnv: 'POSTIZ_API_KEY', apiKey: 'inline', baseUrl: 'https://postiz.example.com/' },
{ POSTIZ_API_KEY: 'ignored' },
)
assert.equal(conn.url, 'https://postiz.example.com/mcp')
assert.deepEqual(conn.headers, { Authorization: 'Bearer inline' })
assert.equal(conn.source, 'config.apiKey')
})
test('resolveConnection reports an unconfigured key without headers', () => {
const conn = plugin.resolveConnection({ apiKeyEnv: 'POSTIZ_API_KEY' }, {})
assert.equal(conn.configured, false)
assert.deepEqual(conn.headers, {})
})
test('apply provides ctx.postiz and registers the skill', () => {
const provided = {}
const registered = []
const ctx = {
logger: { info() {}, warn() {} },
provide(key, value) { provided[key] = value },
inject(deps, cb) {
assert.deepEqual(deps, ['skills'])
cb({ skills: { register(skill) { registered.push(skill); return () => {} } } })
},
}
plugin.apply(ctx, { apiKey: 'k' })
assert.equal(provided.postiz.url, 'https://mcp.postiz.com/mcp')
assert.equal(provided.postiz.headers.Authorization, 'Bearer k')
assert.equal(provided.postiz.configured, true)
assert.equal(registered.length, 1)
assert.equal(registered[0].name, SKILL_NAME)
assert.equal(registered[0].content, SKILL_CONTENT)
assert.match(registered[0].content, /mcp__postiz__schedulePostTool/)
})
test('apply skips the skill when disabled', () => {
let injected = false
const ctx = { logger: { info() {}, warn() {} }, provide() {}, inject() { injected = true } }
plugin.apply(ctx, { apiKey: 'k', skill: false })
assert.equal(injected, false)
})
test('bundle manifest points at the patch and the patch wires both rows', () => {
const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'))
assert.equal(pkg.dsh.bundle.patch, './cordis.patch.yml')
const patch = readFileSync(join(here, '..', 'cordis.patch.yml'), 'utf8')
assert.match(patch, /name: dsh-postiz/)
assert.match(patch, /name: '@deepseek-ai\/dsh-mcp-client'/)
assert.match(patch, /serverName: postiz/)
assert.match(patch, /url: !!js ctx\.postiz\.url/)
})