A command-line tool and TypeScript library for building RisuAI CharX modules, unpacking CharX and RisuM files, bundling Lua, and inspecting RisuSave databases.
- Node.js 20 or later
Install risupack in the project that contains the module sources.
npm install --save-dev risupackRun the local installation with npx risupack.
A module without character prompts needs only a manifest.
sample-module/
└── charx.json
{
"description": "Sample module",
"name": "Sample Module",
"namespace": "sample-module",
"version": "1.0.0"
}Build the CharX from the manifest.
npx risupack build-charx sample-module/charx.jsonWithout output, the command writes ../dist/<name>.charx relative to the manifest directory.
Add lorebooks, regexes, triggers, CSS, toggles, assets, or character prompts only when the module needs them. Reference each source from charx.json.
Unpack an existing CharX into editable sources.
npx risupack unpack-charx character.charx characterBoth unpack commands require an absent or empty output directory. When the output argument is omitted, each command uses the input filename without its extension.
Edit the generated sources and rebuild from the generated manifest.
npx risupack build-charx character/charx.jsonUse unpack-risum for a standalone RisuM file.
npx risupack unpack-risum module.risum moduleThe generated charx.json can be passed to build-charx to package the recovered module as a CharX.
All manifest paths, including output and bundleOutput, are relative to charx.json. A generated source directory can contain the following files when the module uses the corresponding features.
<module>/
├── alternate_greetings/
├── assets/
├── lorebooks/
├── regex/
├── triggers/
├── card.json
├── charx.json
├── description.md
├── first_mes.md
├── style.html
└── toggles.txt
The builder accepts other layouts. Update the paths in charx.json after moving a source file.
name is the only field enforced as required. Set a stable namespace for module identity and lookup.
{
"CSS": "style.html",
"assets": [
{
"file": "assets/background.webp",
"name": "background"
}
],
"creator": "Creator",
"description": "Module description",
"folders": ["Internal"],
"hideIcon": false,
"icon": "assets/icon.png",
"license": "AGPL-3.0-only",
"lorebook": [
{
"alwaysActive": false,
"comment": "Sample",
"file": "lorebooks/sample.md",
"folder": "Internal",
"insertOrder": 100,
"key": "sample",
"mode": "normal",
"secondaryKey": "",
"selective": false,
"useRegex": false
}
],
"lowLevelAccess": false,
"name": "Sample Module",
"namespace": "sample-module",
"output": "../dist/sample-module.charx",
"regex": ["regex/display.md"],
"tags": ["utility"],
"toggles": "toggles.txt",
"triggers": [
{
"bundle": true,
"comment": "Start",
"lowLevelAccess": false,
"lua": "triggers/main.lua",
"type": "start"
}
],
"version": "1.0.0"
}CSS: CSS or HTML source inserted into the RisuAI background HTML fieldassets: Additional files packaged as RisuAI assetscard: Optional character card sourcescreator: Creator namedescription: Package description stored as creator notesfolders: Lorebook folders to create even when no entry references themhideIcon: Whether RisuAI hides the module icon in chat, defaulting tofalseicon: Main module icon; omission creates a transparent placeholder iconlicense: Package license identifier or textlorebook: Lorebook entrieslowLevelAccess: Module-level low-level access, defaulting tofalsename: Required non-empty module namenamespace: Module namespaceoutput: Default CharX output pathregex: Regex entries in execution ordertags: Package tags, defaulting to an empty arraytoggles: Module toggle sourcetriggers: Trigger entriesversion: Package version
CSS, toggles, lorebook contents, and external character text fields accept a path string or an object containing file or content.
{
"CSS": "style.html",
"lorebook": [
{
"comment": "Inline",
"content": "Inline lorebook content"
}
],
"toggles": {
"file": "toggles.txt"
}
}content takes precedence when an object contains both content and file. Bundled Lua lorebooks require file.
Omit card when the module has no character-specific prompts or settings.
Use a card source object to keep the character description and greetings in separate Markdown files.
{
"card": {
"alternate_greetings": ["alternate_greetings/1.md", "alternate_greetings/2.md"],
"defaultVariables": ["language=ko", "", "cards=enabled"],
"description": "description.md",
"file": "card.json",
"first_mes": "first_mes.md",
"globalNoteOverride": "global_note_override.md"
}
}alternate_greetings: Alternate greeting sources in display orderdefaultVariables: RisuAI default variables as one string per linedescription: Character description sourcefile: Optional JSON source for other character card fieldsfirst_mes: First message sourceglobalNoteOverride: Post-history instruction source that overrides the global note
When file is omitted, the builder starts from an empty character card. A referenced card.json may contain {} when all required character content is stored in the external text files. Put other Character Card fields under data.
The unpack commands create character sources only when the packaged card contains character-specific values.
Each lorebook entry accepts the following fields.
activationPercent: RisuAI activation percentagealwaysActive: Constant activation, defaulting tofalsebookVersion: Lorebook format version, defaulting to2bundle: Whether to bundle a Lua source and itsrequire()dependencies, defaulting tofalsebundleOutput: Optional path that retains the bundled Lua outputcomment: Lorebook name, defaulting to the source filenamecontent: Inline lorebook contentextentions: RisuAI-specific lorebook extension values; retain this spellingfile: Lorebook source path, required unlesscontentis presentfolder: RisuAI lorebook folder nameinsertOrder: Insertion order, defaulting to100key: Comma-separated primary activation keys, defaulting to an empty stringmode: RisuAI lorebook mode, defaulting tonormalsecondaryKey: Comma-separated secondary activation keys, defaulting to an empty stringselective: Secondary-key matching, defaulting tofalseuseRegex: Regular expression matching for activation keys, defaulting tofalse
Set bundle to true when the packaged lorebook must contain bundled Lua instead of the Lua entry source.
{
"lorebook": [
{
"alwaysActive": false,
"bundle": true,
"comment": "Renderer",
"file": "lorebooks/renderer.lua",
"insertOrder": 200
}
]
}Omit bundleOutput during normal builds. The builder removes its temporary bundle after packaging.
Store one file-based regex entry in each Markdown file.
---
comment: Display
flag: gs
type: editdisplay
---
IN:
input pattern
OUT:
replacementThe frontmatter accepts comment, flag, and type. The generated RisuAI ableFlag value is true when flag is present and false when flag is absent. Leave the content after OUT: empty for an empty replacement.
Manifest entries accept a path, an object containing file, or inline regex data.
{
"regex": [
"regex/display.md",
{
"file": "regex/request.md"
},
{
"comment": "Inline",
"flag": "gs",
"in": "input pattern",
"out": "replacement",
"type": "editdisplay"
}
]
}Inline entries require string values for in and out. The presence of flag also controls ableFlag for inline entries. comment defaults to an empty string, and type defaults to editdisplay.
Set lua to an unbundled Lua entry file.
{
"triggers": [
{
"bundle": true,
"comment": "Start",
"conditions": [],
"lowLevelAccess": false,
"lua": "triggers/main.lua",
"type": "start"
}
]
}bundle: Whether to bundlerequire()dependencies, defaulting totruebundleOutput: Optional path that retains the bundled Lua outputcomment: Trigger name, defaulting to an empty stringconditions: RisuAI trigger conditions, defaulting to an empty arraylowLevelAccess: Trigger-level low-level accesslua: Lua entry filetype: RisuAI trigger type, defaulting tostart
The bundler resolves require("lib.example") as lib/example.lua relative to the Lua entry file's directory. Omit bundleOutput during normal builds to use a temporary bundle.
A trigger without lua must provide a RisuAI effect array directly. This form preserves triggers that cannot be represented as one Lua effect.
risupack copies CSS and toggle sources into the package without changing their contents.
Wrap custom styles in a <style> element.
<style>
.sample-panel {
color: white;
}
</style>Write one toggle definition per line.
enabled=Enable feature
theme=Theme=select=Light,Dark
label=Label=text
Checkbox values omit the type. Select values use select followed by comma-separated options. Text values use text.
List additional packaged files in assets.
extension: Packaged extension without a leading dot; defaults to the lowercase source extension and accepts ASCII letters and digitsfile: Required asset source pathname: RisuAI asset name, defaulting to the source filename without its extension
The builder sanitizes unsupported archive filename characters and adds numeric suffixes for collisions.
Set SOURCE_DATE_EPOCH to a Unix timestamp to fix timestamps in the generated card and ZIP entries.
SOURCE_DATE_EPOCH=1700000000 npx risupack build-charx module/charx.jsonIdentical inputs and a fixed source date produce byte-identical CharX files.
A built CharX contains the generated card, encoded module, assets, and asset metadata.
module.charx
├── assets/
├── x_meta/
├── card.json
└── module.risum
The archive always contains card.json, module.risum, and a main icon. The source manifest does not require a card or icon field.
Bundle a standalone Lua entry file.
npx risupack bundle-lua triggers/main.lua dist/main.luaThe command writes one Lua file containing resolved require() dependencies and the entry code.
Inspect the module records stored in a RisuSave database.
npx risupack inspect-risusave database.bin
npx risupack inspect-risusave database.bin module-namespaceThe optional query matches a module namespace, name, or ID. The command writes the matching records as JSON to standard output.
Import commands from the package root.
import { buildCharX, unpackCharX } from "risupack";
buildCharX("module/charx.json", "dist/module.charx");
unpackCharX("dist/module.charx", "module-unpacked");The package exports buildCharX(), bundleLua(), createExpandedModuleSources(), decodeModule(), parseModuleBlock(), parseZIP(), unpackCharX(), and unpackRisuM().
risupack is licensed under the GNU Affero General Public License version 3.