Documentation

Use the app

How it works

Build on it

Get involved

Developing Cordis plugins

A Cordis plugin adds code to the assistant: a tool, an event listener or a policy. This guide takes you from an empty folder to a tool the assistant can call. Read Harness design first if Cordis is new to you, and check choosing an extension point: a skill or an MCP server may do the job with less risk.

You need the app installed and a text editor. You do not need access to any private repository, because the packages a tool plugin uses are public on npm.

Your first plugin

The complete example is in docs/plugin-examples/hello-tool.

package.json

{
  "name": "hackathon-hello-tool",
  "version": "0.1.0",
  "type": "module",
  "main": "index.js",
  "exports": { ".": "./index.js" },
  "peerDependencies": { "@deepseek-ai/dsh-tools": "0.2.1-rc.5" }
}

index.js

import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'hello-tool'
export const inject = ['tools']

export function apply(ctx, config = {}) {
  ctx.tools.register(defineTool({
    name: 'hello_microscope',
    description: 'Say hello from a user-written Cordis plugin.',
    parameters: { who: { type: 'string', description: 'Who to greet.' } },
    output: {
      schema: { type: 'json' },
      render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
    },
    async execute(args) {
      return { greeting: `${config.greeting ?? 'Hello'}, ${args.who ?? 'microscopist'}` }
    },
  }))
}

The three exports are the whole contract:

Export Meaning
name The plugin's name
inject Services needed before apply runs
apply(ctx, config) Runs when the plugin starts

Peer dependency versions must match the installed Harness. A mismatch is refused at install time.

Install it

  1. Open the control panel (Cmd+,), then Overview, and select Stop. Plugins can only be installed while the assistant is stopped.
  2. Open Plugins → Install Plugin.
  3. Select Choose folder or .tgz and pick your plugin folder. You can also type an npm package name.
  4. Add configuration as JSON if your plugin takes any, for example {"greeting": "Guten Tag"}.
  5. Select Install Plugin and confirm the warning.
  6. On Overview, select Start.

A folder is copied at install time. After changing your code, remove the plugin and install it again.

What a plugin can use

Ask for services by name in inject, then use them on ctx.

Service Package Use
tools dsh-tools Register tools, restrict, guard
agents, sessions dsh-agent, dsh-session Agents and persisted sessions
skills dsh-skill The skill catalog
settings, credentials dsh-settings, dsh-credentials Configuration and secrets
logger cordis Logging, through ctx.logger
edmoreEvents EDMORE core The EDMORE event hub
edmoreConversationExtensions EDMORE core Workspace panels
edmoreCapabilities EDMORE core Feature status

Harness services are documented in client/node_modules/@deepseek-ai/<package>/README.md. EDMORE packages are private and not on npm, so you can use their services at run time but cannot import their types. EDMORE services are internal and may change between versions.

Common patterns

Tool parameters

defineTool validates arguments before execute runs. Types are string, number, integer, boolean, null, array, object, json and oneOf. Mark needed fields with required: true.

parameters: {
  path: { type: 'string', required: true, description: 'Absolute file path' },
  limit: { type: 'number' },
},

execute(args, exec) receives exec.signal. Pass it to long operations so cancellation works. Throwing is fine: the model sees Error: <message> and the turn continues.

Reacting to events

Listeners are removed automatically when the plugin stops.

export const inject = ['tools']

export function apply(ctx) {
  ctx.on('tools/pre-execute', async (exec, next) => {
    ctx.logger.info('tool %s', exec.name)
    return next()
  })
}

Enforcing a rule

A guard is final: nothing later can reverse its denial. It runs synchronously, and is removed when the plugin stops. Return a reason to deny, and nothing to allow. The model receives Error: <reason>. This one blocks stage moves:

export function apply(ctx) {
  ctx.tools.guard(exec =>
    exec.name === 'mcp__tem_twin__move_stage' ? 'Stage moves are disabled on this computer.' : undefined)
}

Cleaning up

Listeners and services are cleaned up for you. For anything else, return a disposer from ctx.effect:

ctx.effect(() => {
  const timer = setInterval(poll, 5000)
  return () => clearInterval(timer)
})

Adding a workspace panel

Register with edmoreConversationExtensions. The plugin serves the panel's script, here held in panelSource, and the workspace loads it from /api/edmore/extensions/<id>/panel.js. The id must be lowercase letters, digits and hyphens. Follow plugin-tango/src/conversation.mjs and static/panel.js.

export const inject = ['edmoreConversationExtensions']

export function apply(ctx) {
  ctx.effect(() => ctx.edmoreConversationExtensions.register({
    id: 'my-panel',
    title: 'My panel',
    state: async session => ({ note: 'hello' }),
    update: async (session, input) => ({ note: input.note }),
    asset: async file => file === 'panel.js' ? { body: panelSource, type: 'text/javascript; charset=utf-8' } : undefined,
  }))
}

Publish it

Your plugin is an ordinary npm package. Either publish it to npm, or pack it and share the file:

npm pack

Others install the .tgz the same way, under Choose folder or .tgz.

Tools and Workflows

Plan Judge and Workflow are not in the desktop package, so a plugin tool is available to the assistant directly. In a source checkout with the Workflow Engine on, workflow steps refer to MCP tools, so a tool registered only with ctx.tools may not appear in the Workflow editor.

Debugging

Symptom Check
The tool never appears A service in inject may be missing. The plugin stays pending; read Overview → Process Logs
Install fails with a version error Match peerDependencies to the installed Harness version
Your change has no effect Remove and reinstall: folders are copied at install
The plugin is gone after an app update The app reinstalls plugins after updates. If a folder has moved, the Plugins page names what failed

To exercise a plugin without the app, load it into a Cordis context with a stand-in service. Run this where @deepseek-ai/cordis and @deepseek-ai/dsh-tools can be resolved:

import { Context, Service } from '@deepseek-ai/cordis'
import * as plugin from './index.js'

class Tools extends Service {
  registered = []
  constructor(ctx) { super(ctx, 'tools') }
  register(tool) { this.registered.push(tool); return () => {} }
}

const ctx = new Context()
await ctx.plugin(Tools)
await ctx.plugin(plugin, { greeting: 'Guten Tag' })

Security

A plugin is trusted code. It runs inside the assistant process, where it can read saved credentials, use connected instruments, and act without any approval. The app shows a warning before installing one.

Reference