This file defines how agentic tools should work in this repository. It applies to the entire tree under the repo root.
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
- Prefer automation: execute requested actions without extra confirmation unless blocked by missing info or safety/irreversibility.
- Monorepo for ChatLuna: TypeScript ESM Koishi plugins providing LLM integration.
- Packages: adapters (OpenAI, Claude, etc), extensions, services, and shared utilities.
- Default branch is
main; usemainororigin/mainfor diffs. - Each package under
packages/*/hassrc/and builds tolib/. - If
.cursor/rules,.cursorrules, or.github/copilot-instructions.mdare added later, follow them in addition to this file.
- Use Node.js >= 18 and Yarn.
- Install dependencies:
yarn install
- Full build (all packages, includes dynamic-import processing):
yarn build - Fast build (all packages, skip dynamic-import step):
yarn fast-build - Build specific package:
yarn fast-build <package-name>(e.g.,yarn fast-build core) - Both delegate to
yakumowhich runstscthenesbuildper package.
- Run ESLint:
yarn lint - Auto-fix:
yarn lint-fix - ESLint uses
@typescript-eslint,prettier, andstandardconfigs. - Do not add new linters/formatters or config files without an explicit request.
- There are currently no test files or test runner configured.
- Do not introduce a test framework unless explicitly requested.
Monorepo managed by Yarn workspaces + yakumo. All packages live under packages/:
packages/
core/ — Main plugin: services, chains, commands, middleware, presets
shared-adapter/ — Shared base classes for model adapters (client, requester)
shared-prompt-renderer/ — Shared prompt rendering utilities
adapter-openai/ — OpenAI adapter
adapter-claude/ — Claude adapter
adapter-gemini/ — Gemini adapter
adapter-deepseek/ — DeepSeek adapter
adapter-openai-like/ — Generic OpenAI-compatible adapter
adapter-*/ — Other model adapters (qwen, doubao, ollama, zhipu, etc.)
extension-tools/ — Tool-calling extension
extension-agent/ — MCP, Skills extension
extension-long-memory/ — Long-term memory extension
extension-variable/ — Variable extension
service-embeddings/ — Embedding service
service-multimodal/ — Multimodal (vision/audio) service
service-search/ — Web search service
service-vector-store/ — Vector store service
renderer-image/ — Image rendering for markdown output
Each package has src/ for source and builds to lib/. Typical adapter
structure: index.ts (plugin entry), client.ts (platform client),
requester.ts (API requester), locales/ (i18n).
The project extends Koishi's type system via declare module 'koishi':
declare module 'koishi' {
export interface Context {
chatluna: ChatLunaService
}
}When adding new services or events, follow this exact pattern in the relevant file.
THIS IS THE MOST IMPORTANT RULE FOR AGENT-WRITTEN CODE.
Write the simplest possible code that works. Fewer abstractions, fewer functions, fewer variables. If in doubt, inline it.
- Do NOT create
resolveXXX,normalizeXXX,ensureXXX,toSafeXXXfunctions. These are banned patterns. - Do NOT add defensive/fallback checks. Do not guess what types or structures might be. Use the most probable type directly. If it turns out wrong at runtime, we will tell you and you fix it then.
- Do NOT wrap values in helper functions. If a value needs a simple transform, do it inline.
- Do NOT create extra functions for short logic. If a function body would be 1-5 lines, inline it at the call site instead.
// BANNED — do not write code like this
function normalizeNumberValue(value?: number | string | bigint | null) {
if (value == null) return undefined
const numberValue = Number(value)
if (Number.isNaN(numberValue)) return undefined
return numberValue
}
// BANNED — unnecessary wrapper
function resolveGuildId(session: Session): string {
return session.guildId ?? session.channelId ?? ''
}
// BANNED — unnecessary abstraction
function ensureArray<T>(value: T | T[]): T[] {
return Array.isArray(value) ? value : [value]
}
// GOOD — use the value directly, trust the type
const guildId = session.guildId
const count = Number(rawCount)THIS RULE IS MANDATORY.
- Always implement the full feature. Never leave placeholder comments like
// TODO: implement later,// ... rest of implementation, or// add more cases as needed. - Never write stub functions that return dummy values or throw "not implemented" errors.
- Never truncate code with
// ...or// similar for other cases. Write every case, every branch, every line. - If a function needs 200 lines, write 200 lines. Do not artificially split it or leave parts unfinished.
- If you are uncertain about a specific implementation detail, make your best guess and implement it fully. We will correct you if wrong. An incorrect full implementation is always preferable to a correct but incomplete skeleton.
// BANNED — incomplete implementation
async function handleResponse(response: string) {
const parsed = parseMessageContent(response)
// TODO: handle voice and sticker cases
return parsed
}
// GOOD — full implementation, every case handled
async function handleResponse(response: string) {
const parsed = parseMessageContent(response)
if (parsed.messageType === 'voice') {
// full voice handling code here...
}
if (parsed.sticker) {
// full sticker handling code here...
}
return parsed
}Prefer inline code over extracting into functions. A function is justified only when it is called from multiple distinct call sites with non-trivial logic (> ~5 lines).
// GOOD — inline one-shot logic
const messages = config.configs[guildId]
? Object.assign({}, config, config.configs[guildId])
: config
// BAD — unnecessary extraction for single-use logic
function getMergedConfig(config: Config, guildId: string) {
const guildConfig = config.configs[guildId]
return guildConfig ? Object.assign({}, config, guildConfig) : config
}
const messages = getMergedConfig(config, guildId)Do not add type guards, null checks, or fallback values unless there is concrete evidence (from existing code or reported error) that the value can actually be null/undefined/wrong-type.
// BANNED — speculative defense
const name = session.username ?? session.userId ?? 'unknown'
const elements = session.elements ?? []
const content = typeof raw === 'string' ? raw : String(raw ?? '')
// GOOD — trust the types, use directly
const name = session.username
const elements = session.elements
const content = rawIf something actually can be undefined (as declared in the interface with ?),
a single simple check is fine. But do not chain fallbacks or add checks for
types that are not declared optional.
If a method inside a class does not depend on the class instance (this) —
i.e. it does not read or write instance fields, call other instance methods, or
use injected services — it must be extracted as a standalone module-level
function.
Class methods should only contain logic that genuinely needs instance state. Pure computation, formatting, parsing, and other self-contained logic belong outside the class.
// BAD — method does not use `this` at all
class MessageCollector extends Service {
formatTimestamp(ts: number): string {
return new Date(ts).toISOString()
}
}
// GOOD — extracted as a standalone function
function formatTimestamp(ts: number): string {
return new Date(ts).toISOString()
}Do not define named constants unless the value is truly important and reused. Inline literal values directly at the call site when they are used once or are self-explanatory.
Only extract a constant when all of the following apply:
- The value is used in multiple places, OR
- The meaning of the literal is not obvious from context, OR
- The value is a critical tuning parameter that may need adjustment.
// BAD — unnecessary constants for one-off or obvious values
const MAX_RETRY_COUNT = 1
const DEFAULT_SEPARATOR = '\n'
const EMPTY_STRING = ''
// GOOD — inline obvious values
await retry(() => fetch(url), 1)
messages.join('\n')
// GOOD — constant is justified (tuning parameter, used in multiple places)
const WINDOW_SIZE = 10
const MIN_COOLDOWN_TIME = 3000- Use modern ES modules (
.ts). - Target
es2022as configured intsconfig.json. - Prefer
constoverletand never usevar. - Prefer early returns over deep nesting and
elsechains. - Avoid
anywhere possible; if needed for interop (LangChain, koishi internals), keep it localized. - Use interfaces/types for exported shapes; define them in
types.tsfor shared types or near the top of the file for local types. - Use TypeScript's type inference for local variables; avoid redundant type annotations.
- Prefer functional array methods (
map,filter,flatMap) over manual loops when clarity is maintained.
The codebase uses advanced TypeScript patterns for deriving types. Follow these patterns:
// Derive from function return type
type ParsedResponse = Awaited<ReturnType<typeof parseResponse>>
// Extract from service method parameters
type Configurable = Parameters<
ChatLunaService['promptRenderer']['renderTemplate']
>[2]['configurable']Use discriminated unions with a type field for variant types:
export type NextReplyPredicate =
| { type: 'time'; seconds: number }
| { type: 'id'; userId: string }
| { type: 'time_id'; seconds: number; userId: string }- 4-space indentation (configured in
.prettierrc). - Single quotes for strings (
'hello'not"hello"). - No semicolons (configured:
semi: false). - No trailing commas (configured:
trailingComma: "none"). - Max line width: 80 (prettier) / 160 (eslint warning).
- Arrow functions always use parentheses:
(x) => xnotx => x. - Follow existing code style in surrounding context when editing.
- Use ESM import syntax at the top of the file.
- Group imports roughly in this order:
- Node built-ins (
fs,path,url). - Third-party packages (
@langchain/core,js-yaml,marked,he). - Koishi framework (
koishi). - ChatLuna imports (
koishi-plugin-chatluna/...). - Local imports (
./types,./utils,../service/message).
- Node built-ins (
- Empty type-only imports for augmentation are used and accepted:
import type {} from 'koishi-plugin-chatluna/services/chat' import {} from '@initencounter/vits'
- PascalCase for classes, interfaces, types, enums:
MessageCollector,GroupInfo,PresetTemplate. - camelCase for functions, variables, parameters:
calculateActivityScore,groupInfos,triggerReason. - UPPER_SNAKE_CASE for constants:
WINDOW_SIZE,RECENT_WINDOW,MIN_COOLDOWN_TIME. - Underscore prefix for private class members:
_messages,_filters,_groupLocks. - Files use lowercase:
chat.ts,filter.ts,message.ts. - Plugin entry functions are always named
apply. - For new TypeScript locals, parameters, and small helpers, prefer short
names when they stay clear:
ctx,el,msg,cfg,err,opts.- Multi-word names are fine when a single word would be confusing
(
activityScore,triggerReason,currentTokens).
- Multi-word names are fine when a single word would be confusing
(
THIS RULE IS MANDATORY FOR AGENT-WRITTEN CODE.
- Use short names by default for new locals, params, and helper functions.
- Multi-word names are allowed only when a single word would be unclear or ambiguous.
- Do not introduce new camelCase compounds when a short single-word alternative is clear.
- Before finishing edits, review touched lines and shorten newly introduced identifiers where possible.
- Good short names to prefer:
ctx,cfg,err,opts,el,msg,idx,state,result. - Examples to avoid unless truly required:
inputElement,existingConfig,resolvedTimeout,formattedMessage.
// Good
const score = calculateActivityScore(info, now)
function format(msg: Message) {}
// Bad
const activityScoreResult = calculateActivityScore(groupInfo, currentTime)
function formatSingleMessage(messageItem: Message) {}Reduce total variable count by inlining when a value is only used once.
// Good
const prompt = `${systemPrompt}\n${formatTimestamp(Date.now())}`
// Bad
const timestamp = formatTimestamp(Date.now())
const prompt = `${systemPrompt}\n${timestamp}`Avoid unnecessary destructuring. Use dot notation to preserve context.
// Good
session.guildId
session.userId
config.maxTokens
// Bad
const { guildId, userId } = session
const { maxTokens } = configException: destructuring is fine when it improves readability in function parameters or when the source object name is very long.
Prefer const over let. Use ternaries or early returns instead of
reassignment.
// Good
const merged = guildConfig ? Object.assign({}, config, guildConfig) : config
// Bad
let merged
if (guildConfig) merged = Object.assign({}, config, guildConfig)
else merged = configAvoid else statements when a simple early return works.
// Good
function getPreset(name: string) {
if (!name) return defaultPreset
return presets.find((p) => p.name === name)
}
// Bad
function getPreset(name: string) {
if (!name) return defaultPreset
else return presets.find((p) => p.name === name)
}Every sub-plugin exports an apply function:
export function apply(ctx: Context, config: Config) {
// plugin logic
}Sub-plugins in src/plugins/ are called directly as functions (not via
ctx.plugin()), sequentially in plugin.ts.
Services extend Service and call super(ctx, 'service_name'):
export class MessageCollector extends Service {
constructor(
public readonly ctx: Context,
public _config: Config
) {
super(ctx, 'chatluna_character')
}
}ctx.on('ready', async () => {
/* ... */
})
ctx.on('dispose', () => {
/* cleanup */
})
ctx.middleware((session, next) => {
/* ... */
})
ctx.setInterval(fn, ms)ctx.command('chatluna.character')
.option('flag', '-f <value>')
.action(async ({ session, options }) => {
/* ... */
})- Use
try/catcharound LLM calls and file I/O; log vialogger.error(e). - Always release response locks in
finallyblocks. - Use
ChatLunaErrorwith specific error codes for preset/model errors. - For streaming retry: catch, sleep 3s, retry once, then propagate.
- Do NOT add speculative error handling for paths that have no evidence of failing.
| Package | Usage |
|---|---|
koishi |
Framework: Context, Service, Session, Schema, h (element builder), Logger |
koishi-plugin-chatluna |
LLM platform: models, chains, agents, prompt rendering, token counting |
@langchain/core |
Message types (BaseMessage, HumanMessage, AIMessageChunk), RunnableConfig |
js-yaml |
YAML preset file parsing |
marked |
Markdown-to-element rendering for model output |
he |
HTML entity decoding in parsed text |
- When in doubt about patterns, look at
packages/core/src/— it is the most representative package. - Adapter packages follow a consistent structure (
index.ts,client.ts,requester.ts). When creating or modifying adapters, referencepackages/adapter-openai/as the canonical example.