Out-of-process plugins
Hosihikari.PluginLoader.Rpc drives a child process with line-delimited JSON-RPC 2.0 over stdio. The RPC bridge reads only the command from the manifest and the protocol. Unload means the process exits, taking the child process and its state with it.
Pick the HostingShim variant for the server (see Choosing a carrier); the examples below use LeviLamina:
lip install github.com/HosihikariFramework/HostingShim#levilaminalip install github.com/HosihikariFramework/RpcLoaderThe RPC templates cover Node.js/TypeScript, Python, and Kotlin/JVM.
The plugin package also declares a dependency on hosihikari-rpc. The operator provides the executable: put it on PATH or use an absolute path.
Package layout (Node)
Section titled “Package layout (Node)”plugins/hello-node/ manifest.json main.mjs hosihikari.mjs ← copied from the loader package's protocol/node hello-node.conf{ "name": "hello-node", "entry": "main.mjs", "type": "hosihikari", "version": "1.0.0", "dependencies": [ { "name": "hosihikari" }, { "name": "hosihikari-rpc" } ], "extraInfo": { "api_version": "2", "manifest_version": "1", "loader": "rpc", "command": "[\"node\", \"./hosihikari.mjs\", \"./main.mjs\"]" }}command is a JSON array encoded as a string; the working directory is the package directory, and no shell is involved.
Copy the binding from the installed loader package:
cp <server>/plugins/hosihikari-rpc/protocol/node/hosihikari.mjs .Plugin
Section titled “Plugin”import fs from 'node:fs';import path from 'node:path';
export default { onLoad(ctx) { fs.writeFileSync(path.join(ctx.dataDirectory, 'loaded.txt'), 'loaded'); ctx.logInfo(`loaded, package ${ctx.packageDirectory}`); },
onEnable(ctx) { const greeting = ctx.config('greeting'); fs.writeFileSync(path.join(ctx.dataDirectory, 'enabled.txt'), greeting ?? ''); ctx.logInfo(`enabled, greeting=${greeting}`); },
onDisable(ctx) { return true; },
onUnload(ctx) {},};Hooks may be async. Return values: undefined or true allows; false or { allowed: false, reason } refuses; a thrown exception is a fault.
Context
Section titled “Context”ctx provides:
| Member | Meaning |
|---|---|
name / packageDirectory / dataDirectory |
Plugin name, package directory, data directory. |
config(key) / configObject |
The layered configuration. |
logInfo / logWarn / logError |
Structured logs over the protocol. |
writeData(file, text) |
Write a file under the data directory. |
publish(topic, json) |
Publish an event. |
subscribe(topic, fn, { sync }) |
Subscribe; with sync: true the publisher waits for an ack. |
scheduleOnce(seconds, fn) / schedulePeriodic(seconds, fn) |
Timers. |
Events are asynchronous by default. Disabling closes the gate; subscription callbacks and timers stop, and a refused disable reopens it. Unload ends the child process.
stdout carries the protocol; stderr carries text logs. The Node binding redirects console.log to stderr; do not write to stdout.
Bindings
Section titled “Bindings”The loader package ships reference bindings:
protocol/node/hosihikari.mjsandhosihikari.d.ts(TypeScript declarations)protocol/quickjs/hosihikari.js(requiresqjs; JSDoc types)protocol/python/hosihikari.pyandhosihikari.pyi(module-levelon_loadand friends; type stubs)protocol/java/hosihikari(shared by Java and Kotlin;commandpoints athosihikari.Binding)
Host requests from the Node and QuickJS bindings time out after 30 seconds; a timed-out request rejects.
The command forms and context APIs are in the RpcLoader README.
Endstone and PATH
Section titled “Endstone and PATH”The Endstone CLI replaces the child process PATH with Python's sys.path; if the executable cannot be found, use an absolute path in command.
Protocol
Section titled “Protocol”A custom binding implements the same protocol:
- Host → plugin:
hosihikari/hello,hosihikari/enable,hosihikari/disable,hosihikari/unload,hosihikari/events/deliver - Plugin → host:
hosihikari/events/publish,hosihikari/events/subscribe,hosihikari/events/unsubscribe,hosihikari/log - Unload:
unloadrequest → close stdin → wait 5 seconds → kill the process tree.
