An AI-assisted TypeScript plugin for the DeepSeek Harness desktop app that drafts replies to Discord messages from a persona prompt and a local style file, and leaves each one in the message box for the user to read and send.
The plugin connects to the Discord desktop client through a debugging port restricted to the same computer, and follows Discord's own message events through a bridge injected into the page. Each chat has its own harness session on a DeepSeek model, with recent chat context, a persona prompt and a local style file. By default each reply is placed in the message box for the user to read, edit and send; sending automatically is opt-in. Automating a user account is against Discord's Terms of Service and can get the account banned: the project's README says so at the top, and draft mode does not change that.
Friends' messages are treated as untrusted input: a guard at harness level refuses every tool call from these sessions, so the model can only answer in text.
Messages and images go to DeepSeek through the user's own DeepSeek Harness account, so the model does not run on the local computer. Links are removed first.
Works inside the logged-in Discord page and reads or stores no Discord token. It needs Windows, Discord PTB and BetterDiscord.
Chat data stays out of the public repository: the style file and session state live in the user's home folder, and the project's rules forbid committing them.
Written with AI assistance: Claude is the credited co-author on 8 of the 9 commits, all of which fall on one day.
§ 02
In motionInmotion
By default the plugin stops at a draft: a person reads it, edits it and presses Enter.§ 03
How it worksHowitworks
1
Discord desktop client
Runs with a debugging port restricted to this computer; a bridge injected into the page reports other people's new messages.
New message, over the local port
2
Plugin core
Answers the open chat by default, removes links, keeps each chat in order and adds earlier messages as context.
Prompt, queued per chat
3
Harness session
One session per chat, with a persona prompt and a local style file. A guard refuses every tool call.
Model request, through the user's account
4
DeepSeek model
Reached through the DeepSeek account, not run locally: messages and images leave this computer for the model.
Finished reply
5
Discord message box
By default the reply goes into that chat's message box or saved draft, and the plugin stops there.
Read, edit, press Enter
6
The user
Decides whether the reply goes out. Automatic sending is opt-in, and either way automating a user account is against Discord's Terms of Service.
§ 04
Engineering notesEngineeringnotes
Dispatch № 1
Friends' messages must never reach a tool
The problem
Every incoming Discord message becomes a prompt to an agent, on a computer where other plugins attach shell, terminal and memory tools to every session. Friends' messages therefore have to be treated as untrusted input.
The decision
Two layers. Tools are hidden from each Discord session, which also keeps prompts small, and a guard at harness level refuses every tool call from those sessions. The guard is the real boundary, because some agent-scoped tools cannot be hidden.
The result
As the code stands, these sessions can call no tools, so the model's only output is text. The guard dates from the first commit, and the project's notes say never to weaken it. It covers tool calls only; it is not a claim about everything the model can see.
Dispatch № 2
The last step stays with a person
The problem
A reply written by a model would reach a friend from a real person's account. With nobody reading it first, the user would be answering for text they had never seen.
The decision
Delivery defaults to draft: the text goes into the chat's message box and the plugin stops. Automatic sending is a mode the user must switch on, and that one setting also controls the local API's send route, which otherwise returns 403.
The result
By default nothing is posted until the user reads the draft and presses Enter. That is a safety default, not permission: automating a user account is against Discord's Terms of Service, the README warns that it can get the account banned, and draft mode does not change that.
Dispatch № 3
TypeScript with no toolchain, and the assumption that broke
The problem
The project wanted TypeScript and no npm dependencies, and the page bridge has to stay a single expression because it is evaluated inside Discord's page.
The decision
The rewrite relied on Node stripping types at load time, with no build step. GitHub installs land in node_modules, where Node will not do that, so a 25-line build script using Node's own stripper now writes a committed dist folder, and the bridge loads the prebuilt file first.
The result
The package still has no npm dependencies. The costs are that dist must be committed with the sources and that nothing type-checks the code: the types serve editors only.
state.draft = (channelId, text) => {
const existing: string = drafts.getDraft(channelId, 0) ?? ''
const addition = existing.trim() ? `\n${text}` : text
const box = selected.getChannelId() === channelId ? componentDispatch() : undefined
if (box) {
// The open message box: type at the cursor, like a paste.
box.dispatchToLastSubscribed('INSERT_TEXT', { plainText: addition, rawText: addition })
return 'inserted'
}
// Another chat: append to its saved draft, shown when you open it.
draftActions.changeDraft(channelId, existing + addition, 0)
return 'drafted'
}
Draft delivery in full: the text is inserted into the open box or saved to another chat, and no send call appears. Sending is a separate method, used only when send mode is on.
// Friends' messages must never drive tools (shell, files, memory, teammates...).
// The guard is the hard stop; hiding the tools also keeps prompts small.
const isDiscordSession = (id: string): boolean => {
if (state.owned.includes(id)) return true
try {
const session = ctx.get('sessions')?.get(id)
const preset = session ? controller().agents.presetForSession(session) : undefined
return (typeof preset === 'object' ? preset.id : preset) === config.agentPreset
} catch { return false }
}
ctx.effect(() => ctx.tools.guard(exec => exec.agent !== undefined && isDiscordSession(exec.agent.id)
? 'Tools are disabled in Discord auto-reply sessions.' : undefined), 'discord-autoreply: tool guard')
The guard refuses any tool call from a Discord session, recognised by the plugin's own session list or by its agent preset. Hiding the tools is the second layer, a few lines further down the file.
§ 06
Development timelineDevelopmenttimeline
No recorded video for this project.
24 Oct 2026
READMEs in three languages, with the ban warning at the top