feat: add OpenCode adapter
Thin OpenCode plugin injecting the ponytail ruleset via experimental.chat.system.transform, reusing the shared instruction builder. Verified end-to-end on OpenCode 1.17.4. Supersedes #15.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
// 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.
|
||||
//
|
||||
// OpenCode loads this as a server plugin — add it to your opencode.json:
|
||||
// { "plugin": ["./.opencode/plugins/ponytail.mjs"] }
|
||||
|
||||
import { createRequire } from 'module';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
// The shared instruction builder is CommonJS; bridge to it from this ES module.
|
||||
const require = createRequire(import.meta.url);
|
||||
const { getPonytailInstructions } = require('../../hooks/ponytail-instructions');
|
||||
const { getDefaultMode, normalizePersistedMode } = require('../../hooks/ponytail-config');
|
||||
|
||||
// OpenCode has no flag-file convention of its own; keep mode beside its config.
|
||||
const statePath = path.join(
|
||||
process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
|
||||
'opencode',
|
||||
'.ponytail-active',
|
||||
);
|
||||
|
||||
function readMode() {
|
||||
try {
|
||||
return normalizePersistedMode(fs.readFileSync(statePath, 'utf8').trim()) || getDefaultMode();
|
||||
} catch (e) {
|
||||
return getDefaultMode();
|
||||
}
|
||||
}
|
||||
|
||||
function writeMode(mode) {
|
||||
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
||||
fs.writeFileSync(statePath, mode);
|
||||
}
|
||||
|
||||
export default async ({ client } = {}) => {
|
||||
const log = (level, message) => {
|
||||
try { client && client.app && client.app.log({ body: { service: 'ponytail', level, message } }); } catch (e) {}
|
||||
};
|
||||
|
||||
return {
|
||||
// Append the ruleset to the system prompt every turn.
|
||||
'experimental.chat.system.transform': async (_input, output) => {
|
||||
const mode = readMode();
|
||||
if (mode === 'off') return;
|
||||
output.system.push(getPonytailInstructions(mode));
|
||||
},
|
||||
|
||||
// Persist `/ponytail <level>` so the next turn's injection follows it.
|
||||
// ponytail: mode applies from the next message, not the current one — the
|
||||
// transform reads the flag the command writes. Good enough; switch to a
|
||||
// synchronous store if same-turn switching ever matters.
|
||||
'command.execute.before': async (input) => {
|
||||
if (!input || input.command !== 'ponytail') return;
|
||||
// `off` is persisted like any mode; the transform reads it and stays silent.
|
||||
const mode = normalizePersistedMode((input.arguments || '').trim()) || getDefaultMode();
|
||||
writeMode(mode);
|
||||
log('info', 'ponytail ' + mode);
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user