LookupBot is a channel-aware Discord support bot for Zrips plugins. The same /lookup command automatically searches the plugin assigned to the current Discord channel, so CMI questions stay inside the CMI data set, Jobs questions stay inside Jobs, and file filters cannot cross plugin boundaries.
The repository is still named CMIBot, but /lookup is the only registered slash command.
- Channel-ID-based plugin routing
- Exact, whole-word, and broad searches
- Safe indexed-file filtering for config searches
- English locale searches with shared CMILib data
- Curated command, permission, placeholder, FAQ, material, and tab-complete indexes where available
- Automatic runtime extraction of commands, permissions, and placeholders from initialized plugin jars
- In-memory caches with global admin-only reloads
- Clean first-install plugin data generated from a disposable Paper server
- Local version inventory plus scheduled Paper and Spigot resource checks
- Paper 26.2 stable/API drift checks plus Java 25 and Java 26 smoke commands
- One canonical CMILib cache composed into every supporting plugin context
- Layered user/channel/global rate limits, input validation, disabled mentions, role-ID access checks, and JSONL audit logs
- Context-aware help, stats, language stats, latest versions, and debug output
| Context | Search features |
|---|---|
| CMI | config, language, placeholder, material, command, permission, FAQ, tab-complete |
| Jobs | config, language, placeholder, command, permission, FAQ |
| Residence | config, language, placeholder, command, permission |
| SelectionVisualizer (SVIS) | config, language, command, permission |
| MobFarmManager (MFM) | config, language, generated command, generated permission |
| TryMe | config, language, generated placeholder, generated command, generated permission |
| TradeMe | config, language, generated placeholder, generated command, generated permission |
| BottledExp | config, language, generated command, generated permission |
All contexts also support help, stats, langstats, and latest. The global debug and reload commands are admin-only. Unsupported search features are hidden from the context-specific available list and are reported clearly if called directly.
CMILib config and English locale files are shared with every plugin context.
/lookup help
/lookup config <keyword>
/lookup language <keyword>
/lookup lang <keyword>
/lookup placeholder <keyword>
/lookup material <keyword>
/lookup command <keyword>
/lookup cmd <keyword>
/lookup permission <keyword>
/lookup perm <keyword>
/lookup faq <keyword>
/lookup tabcomplete <keyword>
/lookup langstats
/lookup stats
/lookup latest
/lookup latest public:true
/lookup latest scope:all
/lookup debug # admin only
/lookup reload
language|lang, command|cmd, and permission|perm are equivalent long and short forms.
mode:exactis the default case-insensitive phrase search.mode:wholematches complete words or phrases, sothodoes not matchthousand.mode:broadmatches all meaningful query terms when they are not adjacent.limit:1-15controls most result lists and defaults toDEFAULT_RESULT_LIMIT.- CMI
materialsupports up to 25 results and defaults to 25. file:<name>restricts config results to a valid indexed file in the active context.- Source filenames and repository paths are metadata, not searchable content. Use
file:when you intentionally want to restrict a config lookup to an indexed file. - Found totals and file counts are calculated before the display limit is applied.
related:trueadds nearby YAML entries to config and language results.summary:truerequests an AI summary only when OpenAI support and the configured AI role are enabled.
/lookup config dynmap
/lookup config chat file:Chat.yml
/lookup config coal file:miner.yml
/lookup config "mini message" mode:whole
/lookup language "was fireballed by"
/lookup placeholder balance
/lookup material shulker
/lookup cmd balance
/lookup perm cmi.command.balance
/lookup faq refund
/lookup cmd bottle
/lookup perm bottledexp.command.consume
/lookup latest
/lookup latest public:true
/lookup latest scope:all
Only IDs listed in DISCORD_ALLOWED_CHANNEL_IDS can use the bot. The active plugin is then selected from these explicit mappings:
DISCORD_CMI_CHANNEL_IDSDISCORD_JOBS_CHANNEL_IDSDISCORD_SVIS_CHANNEL_IDSDISCORD_MFM_CHANNEL_IDSDISCORD_TRYME_CHANNEL_IDSDISCORD_TRADEME_CHANNEL_IDSDISCORD_RESIDENCE_CHANNEL_IDSDISCORD_BOTTLEDEXP_CHANNEL_IDS
Current default routes, including the BottledExp production channel, are documented in .env.example.
Configured test channels can switch context without restarting the bot:
/lookup debug context:cmi
/lookup debug context:jobs
/lookup debug context:bottledexp
/lookup debug context:auto
Only an admin role can use /lookup debug or change the test-channel override. auto returns the test channel to DISCORD_TEST_DEFAULT_CONTEXT.
The bot's YAML data is generated from a clean first-install Paper server instead of copied from a customized live server.
servers/
|- _template-Paper-26.2/
| `- companions/
`- Paper-26.2/
`- companions/
servers/ is ignored by Git. Never start or modify _template-Paper-26.2 directly. It is a reusable source containing Paper, its cache/libraries, and the plugin jars. Non-Paper companion artifacts belong in the template's companions/ directory, which the refresh copies into the disposable server without loading those jars as plugins.
For a premium plugin update, preserve or remove the superseded jar so plugins/ contains exactly one active jar for that plugin, copy the verified replacement into _template-Paper-26.2/plugins/, and run npm run refresh:data. Preserve superseded jars under the ignored servers/plugin-archive/ tree when needed; that directory is outside the template, so archived jars are neither copied into the disposable server nor loaded by Paper.
The maintained template uses PaperScript's STABLE channel, same-version build upgrades, and the fixed Paper-{version}.jar filename. Its broad process-name fallback is disabled because another project can legitimately run a jar with the same name; exact test-port detection remains enabled.
Run the complete refresh with:
npm run refresh:dataThe refresh script performs these steps:
- Moves the existing
servers/Paper-26.2to a temporary backup. - Clones
_template-Paper-26.2into a new disposable working server, including its non-loadedcompanions/inventory. - Removes generated plugin, world, log, and Paper config state from the clone only.
- Runs Paperclip's documented patch-only mode in the clone so the exact stable Paper API and runtime libraries are present without starting the template.
- Builds
LookupRuntimeExporterwith JDK 25 against the API coordinate inruntime-exporter/compatibility.json, verifies Java 25 class bytecode, and places the jar in the disposable clone only. - Starts Paper with a 2 GB ceiling and waits for the server's
Donestate. - Runs the exporter after every plugin is initialized, waits for its explicit completion marker, then sends a clean
stopand requires a successful shutdown. - Verifies every core config and English locale exists, then synchronizes generated Zrips config, locale, text, JSON, and image files into the plugin directories.
- Regenerates supplemental command, permission, and placeholder indexes from runtime metadata, with each jar's
plugin.ymlas a fallback for root commands and declared permissions. - Writes
data/versions.jsonfrom Paper state and every jar'splugin.ymlmetadata. The internal exporter is excluded from this catalog. - Removes the temporary backup only after the entire refresh succeeds.
Before synchronization, the workflow also backs up every managed repository plugin tree and data/versions.json. If startup, synchronization, index generation, or version generation fails, both the previous working server and repository lookup data are restored automatically; the failed clone is retained under servers/Paper-26.2.failed-* for diagnosis.
Runtime databases, logs, backups, .DS_Store, and security.key are never synchronized. Curated files under each plugin's data/ directory are preserved, including FAQ, detailed command, permission, placeholder, material, and tab-complete indexes. Only generated-commands.log, generated-permissions.log, and generated-placeholders.log are rebuilt automatically.
Curated entries always win when a generated key describes the same command, permission, or placeholder. Generated entries fill missing coverage, while variable spellings such as $1 and [playerName] are normalized for deduplication. This keeps hand-written descriptions and examples intact without losing newly added upstream entries.
To regenerate the repository index files from the most recent runtime export and current plugin.yml files without starting Paper again:
npm run refresh:indexesThe runtime exporter can inspect initialized command classes, permission enums, and placeholder enums that are not declared in plugin.yml. Its reflection is intentionally isolated to third-party plugin metadata whose public APIs do not expose a complete enumerable index; it does not use Paper NMS or CraftBukkit internals. Values created only for a particular online player, external expansion, or live server state may still be impossible to enumerate. plugin.yml remains the fallback, and curated indexes remain authoritative.
The exporter source lives under runtime-exporter/. Its compiled jar and raw TSV output stay inside ignored servers/ paths; neither is deployed with LookupBot. You can compile it independently with:
npm run build:exporterUse this lighter command to rebuild only data/versions.json from an already generated server:
npm run refresh:versionsBottledExpPlugin/
CMIPlugin/CMI/
CMIPlugin/data/
CMILibPlugin/CMILib/
CMILibPlugin/data/
JobsPlugin/
JobsPlugin/data/
MFMPlugin/
MFMPlugin/data/
ResidencePlugin/
ResidencePlugin/data/
SVISPlugin/
SVISPlugin/data/
TradeMePlugin/
TradeMePlugin/data/
TryMePlugin/
TryMePlugin/data/
data/versions.json
runtime-exporter/
|- compatibility.json
scripts/
src/
Generated files outside data/ are replaced on each clean refresh so removed or renamed upstream settings disappear from the bot too. Curated files belong in a plugin's data/ directory so the refresh preserves them. The clearly named generated-*.log files are the only generated exception inside those directories.
/lookup latest privately shows the clean snapshot version for the active plugin, CMILib, and Paper. /lookup latest public:true posts a compact public response containing only the latest upstream versions for the active plugin and CMILib, followed by an upgrade recommendation. It never includes the local clean snapshot, Paper, internal generation timestamps, or other tracked resources.
/lookup latest scope:all privately lists every jar in the clean reference server, support dependencies such as LuckPerms and PlaceholderAPI, and the tracked CMI companion resources. Public output is intentionally limited to the current channel context, so scope:all public:true is rejected privately.
The all-resources response is grouped for readability: the first private message lists the main Zrips plugins, and the second lists CMI companion resources followed by Paper and other third-party resources.
The CMI companion section always tracks CMI-API, CMI-Bungee, CMI-Velocity, CMI-Vault, and CMI-E-Injector through their official GitHub or Zrips sources. Their local jars are stored in the ignored servers/_template-Paper-26.2/companions/ directory and copied to servers/Paper-26.2/companions/ during a refresh. They are inventoried for version comparisons but are never placed in Paper's plugins/ directory or started by the clean server.
Tracked Spigot resource versions are checked through the public Spiget API, Paper builds through Paper's official Fill API, LuckPerms through its official metadata service, and PlaceholderAPI through its latest successful Jenkins artifact. CMI companion downloads use their Zrips listings, while CMI-API uses its GitHub project version. PlaceholderAPI output includes both its plugin version and Jenkins build number. A failed or disabled network check never prevents the bot from starting; the command continues to show the local inventory.
Upstream refreshes are resilient per resource. After a resource has checked successfully during the current bot process, a temporary timeout or provider failure retains that last-known version in RAM while unrelated successful checks still update normally. Retained values are clearly marked as last known in private and public version output, and recover automatically after the provider succeeds again. A resource with no successful check yet remains unavailable rather than inventing a version.
Version controls:
VERSION_CATALOG_PATH=data/versions.jsonVERSION_CHECK_ENABLED=trueVERSION_CHECK_INTERVAL_HOURS=12VERSION_CHECK_TIMEOUT_SECONDS=8PAPER_VERSION=26.2PAPER_VERSION_CHANNELS=STABLE
The scheduled timer is in memory and starts with the bot. Restarting the bot resets the timer; no separate cron job is required.
runtime-exporter/compatibility.json is the source of truth for the internal Paper tooling. It currently pins Paper 26.2 build 87 on STABLE, API 26.2.build.87-stable, exporter 1.0.1, and Java target 25.
Verify the tracked metadata, PaperScript source/config, installed jar checksum, exact API jar, JDKs, and the live latest-stable build:
npm run check:paperRun maintained-server startup, plugin-list, exporter-enable, and clean-shutdown checks:
npm run smoke:java25
npm run smoke:java26The scripts use the exact JDK paths from the compatibility manifest by default. JAVA_HOME or JAVA_25_HOME/JAVA_26_HOME can select another matching JDK installation; JAVA_BIN, JAVAC_BIN, and JAR_BIN can override individual tools. Feature mismatches fail before build or startup, and the exporter remains Java 25 bytecode even when tested on Java 26.
npm run check performs syntax plus offline compatibility drift validation. npm run check:paper additionally contacts Paper's official Fill API and fails if the pinned build is no longer the latest stable 26.2 build.
All indexed YAML plus curated and jar-generated log data is loaded into RAM during startup. /lookup reload globally rebuilds every plugin cache, reloads the version catalog, and refreshes upstream version checks.
CMILib is cached once through the CMILIB_*_INCLUDE_GLOBS profiles. Plugin contexts retain only their own entries, then compose the matching shared CMILib config, language, and placeholder entries into searches at read time. This preserves the same /lookup results and per-context stats without retaining another copy of every CMILib entry for every Zrips plugin.
The startup, reload, and debug global totals describe entries actually retained in RAM: local plugin entries plus one CMILib copy. Per-context /lookup stats totals continue to describe everything searchable in that channel, including shared CMILib data. Legacy plugin include globs that still mention CMILibPlugin/** are excluded from local caches defensively.
Reload is transactional. The next search cache and version snapshot are prepared separately from live state while lookups continue using the previous snapshots. Both are committed together only after every profile and the version catalog finish successfully; otherwise the prepared data is discarded and the complete previous state remains active.
Use reload after adding, replacing, renaming, or removing indexed files:
/lookup reload
The command is restricted to ADMIN_ROLE_IDS. Regular searches continue to use the old in-memory snapshot until reload or restart completes.
Reload reports are private. If the global per-plugin breakdown exceeds one Discord message, the bot continues it in additional ephemeral follow-ups instead of trimming contexts from the end.
/lookup stats reports only the current plugin context. Startup and /lookup reload report every context, followed by a separate Shared CMILib data section.
/lookup debug is admin-only, ephemeral, and reports:
- active plugin and channel route
- tracked contexts and supported commands
- context/global cache totals and largest bucket
- cache and version-check timestamps
- clean version-catalog plugin count
- Paper build, stable API coordinate, and exporter Java target
- Node and discord.js versions
- process uptime, RSS, and heap usage
- project and per-plugin disk footprints
- active test-channel overrides
- Access is matched by immutable Discord role IDs, never role names.
- Debug, reload, and test-context changes are admin-only.
- When enabled, AI features use their own role-ID list and hard enable switch; disabled installations do not require AI credentials or roles.
- Queries have configurable length, filler-word, and character validation.
- Short valid terms such as
rt,rtp,tp, and placeholders can be allowlisted. - Discord mentions are disabled on all bot responses.
file:only resolves against files already indexed in the active plugin profile.- A sliding-window limit follows each user across every subcommand, so switching commands cannot bypass throttling.
- Authorized support commands also share per-channel and process-wide windows to contain coordinated bursts.
- Lookup and AI-summary operations retain separate per-user cooldowns; lookup throttling runs before cache filtering.
- Debug has a global cooldown, while reload has both a global cooldown and a single-flight guard.
- Rate-limit state is memory-bounded and expired buckets are pruned automatically.
- Repeated rate-limit audit events are coalesced to prevent JSONL log flooding.
- Usage is written as JSON lines to
logs/cmibot-usage.jsonl. - Every interaction runs behind a top-level rejection boundary. Unexpected command and fallback-response errors are logged without terminating the bot.
- Discord client and shard errors have explicit listeners so an emitted runtime error cannot become an unhandled
errorevent.
The abuse-control defaults are configurable. Set a limit or its window to 0 to disable that layer:
COMMAND_USER_RATE_LIMIT=10
COMMAND_CHANNEL_RATE_LIMIT=30
COMMAND_GLOBAL_RATE_LIMIT=100
COMMAND_RATE_WINDOW_SECONDS=30
LOOKUP_COOLDOWN_SECONDS=3
SUMMARY_COOLDOWN_SECONDS=15
DEBUG_COOLDOWN_SECONDS=10
RELOAD_COOLDOWN_SECONDS=30
RATE_LIMIT_AUDIT_COOLDOWN_SECONDS=30Requirements:
- Node.js 20 or newer
- JDK 25.0.4 at
/Library/Java/JavaVirtualMachines/jdk-25.0.4.jdk/Contents/Homefor Paper and Java 25 exporter bytecode - JDK 26.0.2 at
/Library/Java/JavaVirtualMachines/jdk-26.0.2.jdk/Contents/Homefor the optional forward-runtime smoke test unzipfor reading plugin metadata from jars
Install and start:
npm install
cp .env.example .env
npm startFill in the Discord token, application ID, guild ID, channel IDs, and role IDs in .env. The real .env is ignored and must be created independently on each machine.
OpenAI support is optional and disabled by default with OPENAI_ENABLED=false. In disabled mode, the bot stays lexical-only: it does not import the OpenAI SDK, construct an API client, or require AI_ROLE_IDS/OPENAI_API_KEY. This avoids the SDK's runtime memory overhead until AI is explicitly enabled.
The CLI defaults to CMI, or accepts a plugin context first:
npm run lookup -- cmi stats
npm run lookup -- jobs config --file generalConfig.yml income
npm run lookup -- tryme config reward
npm run lookup -- bottledexp language experience
npm run lookup -- cmi latest
npm run lookup -- cmi latest allnpm run check
npm run build:exporter
VERSION_CHECK_ENABLED=false npm run lookup -- cmi stats
VERSION_CHECK_ENABLED=false npm run lookup -- jobs stats
VERSION_CHECK_ENABLED=false npm run lookup -- bottledexp stats.env,logs/,node_modules/, databases, keys, macOS metadata, and the entireservers/tree are ignored..env.example, generated clean plugin files, curated and jar-generateddata/files, refresh scripts, anddata/versions.jsonare tracked.- Always inspect
git status --ignored --shortbefore the first push from a new machine.