feat: publish ponytail as an installable npm package for OpenCode and Pi (#197)

* feat: register slash commands from .opencode/command/*.md

Add parseCommandFile() to read frontmatter-described markdown files
and wire them into opencode's config.command during init.

Extend the config hook to scan .opencode/command/ and register each
.md file as a named slash command.

Update the plugin doc comment to reflect the npm install path
(opencode-ponytail) vs the old relative path.

* chore: rename package to opencode-ponytail

Align package name with npm convention for opencode plugins.
Update keywords to include opencode-plugin and opencode tags.
Fix description back to original correct wording (grammatical
regression introduced during editing).

* chore: add npm metadata and publish workflow

Add author, homepage, repository, bugs, main, exports, files, and
publishConfig fields to package.json for npm publishing.

Add .github/workflows/publish.yml to auto-publish to npm on version
tags (v*) with provenance.

* docs: add npm plugin install for opencode-ponytail

---------

Co-authored-by: Emeriko <dietrich.gebert@gmail.com>
This commit is contained in:
Dang Trung An
2026-06-24 02:09:40 +02:00
committed by GitHub
co-authored by Emeriko
parent 2b426c6ac9
commit 17a466013e
5 changed files with 107 additions and 9 deletions
+25 -4
View File
@@ -1,11 +1,13 @@
// ponytail — OpenCode plugin.
//
// Injects the ponytail ruleset into every chat's system prompt at the active
// intensity, and persists /ponytail mode switches. Reuses the shared instruction
// builder so Claude Code, Codex, pi, and OpenCode all read one source of truth.
// intensity, persists /ponytail mode switches, and registers slash commands so
// they work when the package is installed from npm. Reuses the shared
// instruction builder so Claude Code, Codex, pi, and OpenCode all read one
// source of truth.
//
// OpenCode loads this as a server plugin — add it to your opencode.json:
// { "plugin": ["./.opencode/plugins/ponytail.mjs"] }
// { "plugin": ["opencode-ponytail"] }
import { createRequire } from 'module';
import fs from 'fs';
@@ -40,6 +42,15 @@ function writeMode(mode) {
fs.writeFileSync(statePath, mode);
}
export function parseCommandFile(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
// Tolerate CRLF: a Windows checkout (autocrlf) delivers \r\n, npm ships \n.
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
if (!match) return null;
const description = match[1].match(/description:\s*(.+)/)?.[1]?.trim();
return { description, template: match[2].trim() };
}
export default async ({ client } = {}) => {
const log = (level, message) => {
try { client && client.app && client.app.log({ body: { service: 'ponytail', level, message } }); } catch (e) {}
@@ -48,8 +59,18 @@ export default async ({ client } = {}) => {
const ponytailSkillsDir = path.resolve(__dirname, '../../skills');
return {
// Register skills directory so opencode discovers ponytail skills.
// Register slash commands + skills directory.
config: async (config) => {
if (!config.command) config.command = {};
const commandDir = path.join(__dirname, '..', 'command');
try {
for (const file of fs.readdirSync(commandDir).filter((f) => f.endsWith('.md'))) {
const name = path.basename(file, '.md');
const parsed = parseCommandFile(path.join(commandDir, file));
if (parsed) config.command[name] = parsed;
}
} catch (e) {}
config.skills = config.skills || {};
config.skills.paths = config.skills.paths || [];
if (!config.skills.paths.includes(ponytailSkillsDir)) {