CloddsBot Command System: Handling Native vs Skill-Based Commands
CloddsBot routes both hard-coded native commands and dynamically loaded skill commands through a unified CommandRegistry, storing all definitions in a Map<string, CommandDefinition> while separating core logic from runtime extensions.
The alsk1992/CloddsBot repository implements a dual-layer command architecture that distinguishes between built-in native commands and externally loaded skill-based commands. This design allows the core bot to remain lightweight while supporting runtime extensibility through community-contributed skill packages. Both command types ultimately resolve through the same execution pipeline in src/commands/registry.ts, presenting users with a seamless slash-command interface.
Native Commands: Hard-Coded Core Implementation
Defining Commands in createDefaultCommands
Native commands are statically defined in src/commands/registry.ts within the createDefaultCommands() function. This factory returns an array of CommandDefinition objects, each specifying the command name, description, usage string, and handler implementation.
Core commands include:
/help(lines 66‑78): Lists all available commands and categories/memory(lines 80‑88): Interfaces with the optional MemoryService for persistent storage/bot(lines 68‑500): Manages trading bot instances with an extensive handler
// src/commands/registry.ts
export interface CommandDefinition {
name: string;
description: string;
usage: string;
handler: (args: string, context: CommandContext) => Promise<string>;
}
export function createDefaultCommands(): CommandDefinition[] {
return [
{
name: 'help',
description: 'Display available commands and usage',
usage: '/help [category]',
handler: async (args, ctx) => {
const categories = ctx.registry.listAll();
return formatHelpOutput(categories, args);
}
},
{
name: 'memory',
description: 'Read from MemoryService storage',
usage: '/memory <key>',
handler: async (args, ctx) => {
if (!ctx.memoryService) return 'Memory service unavailable';
return ctx.memoryService.get(args.trim());
}
}
];
}
Registration at Boot Time
When the bot initializes, createCommandRegistry() instantiates a CommandRegistry and calls registerMany() with the array from createDefaultCommands() (lines 80‑96). The registry stores each command in an internal Map<string, CommandDefinition>, enabling O(1) lookup by command name.
// src/commands/registry.ts
export class CommandRegistry {
private commands = new Map<string, CommandDefinition>();
private aliases = new Map<string, string>();
registerMany(definitions: CommandDefinition[]): void {
definitions.forEach(def => {
this.commands.set(def.name, def);
// Additional alias resolution logic
});
}
createCommandRegistry(): CommandRegistry {
const registry = new CommandRegistry();
registry.registerMany(createDefaultCommands());
return registry;
}
}
Execution Pipeline
Incoming slash commands are processed by CommandRegistry.handle() (lines 121‑150). This method parses the command name, resolves aliases through the internal map, retrieves the corresponding CommandDefinition, and invokes its handler within a try/catch block for consistent error logging.
// src/commands/registry.ts
async handle(message: string, context: CommandContext): Promise<string> {
try {
const [commandName, ...argParts] = message.split(' ');
const resolvedName = this.aliases.get(commandName) ?? commandName;
const command = this.commands.get(resolvedName);
if (!command) {
return `Unknown command: ${commandName}`;
}
return await command.handler(argParts.join(' '), context);
} catch (error) {
console.error(`Command execution failed: ${error}`);
return 'An error occurred while processing your request';
}
}
Skill-Based Commands: Dynamic Extension Architecture
Skill Package Structure
Skills are self-contained bundles stored in the ~/.clodds/skills directory (or a custom path via config.skillsDir). Each skill requires a SKILL.md file describing sub-commands and an optional skill.json for environment-specific overrides. The loader in src/skills/loader.ts (lines 263‑311) parses these files to instantiate a Skill object with metadata and command handlers.
<!-- ~/.clodds/skills/market-tracker/SKILL.md -->
# Market Tracker Skill
## Commands
### track
- **name**: track
- **description**: Monitor price movements for specific pairs
- **usage**: /track <pair> [interval]
### report
- **name**: report
- **description**: Generate market analysis reports
- **usage**: /report [timeframe]
Discovery and Loading Process
The SkillManager class in src/skills/index.ts manages the skill lifecycle. Its loadSkills() method walks the skills directory, calling loadSkill() for each SKILL.md file encountered and storing the resulting Skill objects in memory (lines 91‑104). The src/skills/registry.ts module (lines 176‑220) extends this capability to remote registries, handling archive downloads and checksum verification.
// src/skills/index.ts
export class SkillManager {
private skills: Map<string, Skill> = new Map();
async loadSkills(skillsDir: string): Promise<void> {
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const skillPath = path.join(skillsDir, entry.name, 'SKILL.md');
try {
const skill = await this.loadSkill(skillPath);
this.skills.set(skill.meta.name, skill);
} catch (err) {
console.warn(`Failed to load skill from ${entry.name}`);
}
}
}
}
}
Runtime Integration with Command Registry
The agents subsystem in src/agents/index.ts provides a reloadSkills() method that bridges skills with the command system. Invoked during startup or explicit refresh requests, this method iterates over loaded Skill objects and registers each sub-command as a CommandDefinition with the global CommandRegistry.
// src/agents/index.ts
async reloadSkills(): Promise<void> {
await this.skillManager.loadSkills(this.config.skillsDir);
for (const [skillName, skill] of this.skillManager.getSkills()) {
for (const cmdDef of skill.getCommandDefinitions()) {
// Wrap skill execution in CommandDefinition format
this.commandRegistry.registerMany([{
name: cmdDef.name,
description: cmdDef.description,
usage: cmdDef.usage,
handler: async (args, ctx) => {
// Generic dispatcher forwards to skill's internal handler
return skill.handle(cmdDef.name, args, ctx);
}
}]);
}
}
}
Skill Command Execution Flow
When a user invokes a skill-provided command like /track:
- Resolution: The name is resolved by
CommandRegistry.handle()using the same lookup mechanism as native commands - Dispatch: The registered handler acts as a generic dispatcher that forwards the argument string to
skill.handle(commandName, args) - Processing: The skill's internal implementation executes custom logic (e.g., scanning markets, querying APIs)
- Response: The skill returns a string response that the core bot transmits to the user
Unified Command Interface and Categorization
Metadata and UI Integration
Both native and skill commands share the same categorization system. COMMAND_CATEGORIES in src/commands/registry.ts (lines 65‑78) maps every command name (whether hard-coded or dynamically added) to UI categories such as "Core", "Market Data", or "Virtuals & Agents".
The listAll() method (lines 109‑118) constructs a flat array of { name, description, category } objects by iterating over the internal Map that contains both native definitions and skill-registered commands. This unified list drives the help interface and command discovery features.
// src/commands/registry.ts
const COMMAND_CATEGORIES: Record<string, string[]> = {
'Core': ['help', 'memory', 'status'],
'Market Data': ['track', 'report', 'price'],
'Virtuals & Agents': ['bot', 'agent']
};
listAll(): Array<{name: string, description: string, category: string}> {
const results = [];
for (const [name, def] of this.commands) {
const categories = COMMAND_CATEGORIES[name] ?? ['Uncategorized'];
results.push({
name,
description: def.description,
category: categories[0]
});
}
return results;
}
Summary
- Native commands are hard-coded in
src/commands/registry.tsviacreateDefaultCommands()and registered at boot time throughregisterMany(), storing definitions in aMap<string, CommandDefinition> - Skill commands originate from external bundles in
~/.clodds/skills, parsed bysrc/skills/loader.tsand integrated into the registry at runtime viasrc/agents/index.tsreloadSkills() - Unified execution occurs through
CommandRegistry.handle()(lines 121‑150), which resolves both command types through the same alias resolution and error handling pipeline - Metadata merging happens through
COMMAND_CATEGORIESandlistAll()(lines 65‑78, 109‑118), ensuring skill commands appear seamlessly in help listings alongside native functionality - Extensibility requires no core code changes for skills, while native commands demand modification of
src/commands/registry.tsand redeployment
Frequently Asked Questions
Where are native commands defined in the CloddsBot source code?
Native commands are defined in src/commands/registry.ts within the createDefaultCommands() function (lines 66‑88). This function returns an array of CommandDefinition objects containing the command name, description, usage string, and handler implementation. The /help, /memory, and /bot commands are all defined here before being loaded into the registry's internal Map during initialization.
How does CloddsBot load skill commands at runtime?
Skill commands are loaded through the SkillManager class in src/skills/index.ts. The loadSkills() method scans the ~/.clodds/skills directory for SKILL.md files (lines 91‑104), parsing each into a Skill object. The reloadSkills() method in src/agents/index.ts then registers these with the global CommandRegistry, making them available alongside native commands without restarting the core bot process.
Can skill commands override native commands in CloddsBot?
The CommandRegistry uses a Map<string, CommandDefinition> to store all commands, and the registerMany() method (lines 80‑96) will overwrite existing entries if the same name is registered twice. However, the typical loading sequence registers native commands first, then skills, meaning a skill could theoretically override a native command if it uses an identical name. The system does not enforce name-spacing restrictions between skills and core commands.
What file structure is required for a CloddsBot skill package?
A valid skill package requires at least a SKILL.md file describing the sub-commands and their handlers, located within a subdirectory of ~/.clodds/skills. Optionally, a skill.json file can provide environment variable overrides or configuration metadata. The loader in src/skills/loader.ts (lines 263‑311) parses these files to create the Skill object, while src/skills/registry.ts handles remote registry downloads and checksum verification for distributed skill packages.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →