Creating Custom Skills¶
Extend Qalam's functionality by creating your own custom skills. Skills are modular plugins that add new commands and features.
Skill Structure¶
Basic Skill Template¶
// ~/.qalam/skills/myskill.js
import Skill from './base-skill.js';
import chalk from 'chalk';
class MySkill extends Skill {
constructor() {
super('myskill', 'Description of what my skill does');
}
async execute(args) {
const [action, ...params] = args;
switch (action) {
case 'hello':
return this.sayHello(params[0]);
case 'help':
return this.help();
default:
return chalk.red('Unknown action. Use "myskill help"');
}
}
sayHello(name = 'World') {
return chalk.green(`Hello, ${name}!`);
}
help() {
return `
${chalk.bold('MySkill - Custom Skill Example')}
${chalk.cyan('Usage:')}
myskill hello [name] Say hello to someone
myskill help Show this help message
${chalk.cyan('Examples:')}
myskill hello
myskill hello Alice
`;
}
}
export default MySkill;
Required Methods¶
Every skill must implement:
- constructor() - Initialize skill with name and description
- execute(args) - Main entry point for skill execution
- help() - Return help text for the skill
Installation¶
1. Create Skills Directory¶
mkdir -p ~/.qalam/skills
2. Create Your Skill File¶
touch ~/.qalam/skills/myskill.js
3. Enable Auto-loading¶
qalam config set skills.autoLoad true
4. Test Your Skill¶
qalam myskill hello
# Output: Hello, World!
Advanced Features¶
Using External Packages¶
import axios from 'axios';
import ora from 'ora';
class ApiSkill extends Skill {
async execute(args) {
const spinner = ora('Fetching data...').start();
try {
const response = await axios.get('https://api.example.com/data');
spinner.succeed('Data fetched successfully');
return JSON.stringify(response.data, null, 2);
} catch (error) {
spinner.fail('Failed to fetch data');
return chalk.red(error.message);
}
}
}
Database Integration¶
import Database from '../../core/database.js';
class DataSkill extends Skill {
constructor() {
super('data', 'Manage custom data');
this.db = new Database();
}
async execute(args) {
const [action, ...params] = args;
switch (action) {
case 'save':
return this.saveData(params[0], params[1]);
case 'get':
return this.getData(params[0]);
default:
return this.help();
}
}
async saveData(key, value) {
await this.db.run(
'INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)',
[key, value]
);
return chalk.green(`Saved: ${key} = ${value}`);
}
async getData(key) {
const row = await this.db.get(
'SELECT value FROM config WHERE key = ?',
[key]
);
return row ? row.value : chalk.yellow('Not found');
}
}
Interactive Prompts¶
import inquirer from 'inquirer';
class InteractiveSkill extends Skill {
async execute(args) {
if (args[0] === 'setup') {
return this.interactiveSetup();
}
return this.help();
}
async interactiveSetup() {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'What is your name?',
default: 'User'
},
{
type: 'list',
name: 'color',
message: 'Choose a color:',
choices: ['red', 'green', 'blue', 'yellow']
},
{
type: 'confirm',
name: 'confirm',
message: 'Save settings?',
default: true
}
]);
if (answers.confirm) {
// Save settings
return chalk.green('Settings saved!');
}
return chalk.yellow('Cancelled');
}
}
Subprocess Execution¶
import { execa } from 'execa';
class GitSkill extends Skill {
async execute(args) {
const [action, ...params] = args;
switch (action) {
case 'status':
return this.gitStatus();
case 'branch':
return this.gitBranch(params[0]);
default:
return this.help();
}
}
async gitStatus() {
try {
const { stdout } = await execa('git', ['status', '--short']);
return stdout || chalk.green('Working tree clean');
} catch (error) {
return chalk.red('Not a git repository');
}
}
async gitBranch(name) {
if (!name) {
const { stdout } = await execa('git', ['branch']);
return stdout;
}
try {
await execa('git', ['checkout', '-b', name]);
return chalk.green(`Created and switched to branch: ${name}`);
} catch (error) {
return chalk.red(error.message);
}
}
}
Real-World Examples¶
Deployment Skill¶
class DeploySkill extends Skill {
constructor() {
super('deploy', 'Deployment automation');
}
async execute(args) {
const [environment, ...options] = args;
const validEnvs = ['dev', 'staging', 'prod'];
if (!validEnvs.includes(environment)) {
return chalk.red(`Invalid environment. Use: ${validEnvs.join(', ')}`);
}
return this.deploy(environment, options);
}
async deploy(env, options) {
const steps = [
{ name: 'Running tests', cmd: ['npm', 'test'] },
{ name: 'Building application', cmd: ['npm', 'run', `build:${env}`] },
{ name: 'Deploying to AWS', cmd: ['aws', 's3', 'sync', 'dist/', `s3://bucket-${env}/`] }
];
for (const step of steps) {
const spinner = ora(step.name).start();
try {
await execa(step.cmd[0], step.cmd.slice(1));
spinner.succeed();
} catch (error) {
spinner.fail();
return chalk.red(`Deployment failed: ${error.message}`);
}
}
return chalk.green(`✅ Successfully deployed to ${env}`);
}
}
Database Backup Skill¶
class BackupSkill extends Skill {
constructor() {
super('backup', 'Database backup management');
}
async execute(args) {
const [action] = args;
switch (action) {
case 'create':
return this.createBackup();
case 'restore':
return this.restoreBackup(args[1]);
case 'list':
return this.listBackups();
default:
return this.help();
}
}
async createBackup() {
const timestamp = new Date().toISOString().replace(/:/g, '-');
const filename = `backup-${timestamp}.sql`;
try {
await execa('pg_dump', [
'-h', 'localhost',
'-U', 'user',
'-d', 'database',
'-f', `./backups/${filename}`
]);
return chalk.green(`Backup created: ${filename}`);
} catch (error) {
return chalk.red(`Backup failed: ${error.message}`);
}
}
async restoreBackup(filename) {
if (!filename) {
return chalk.red('Please specify a backup file');
}
const confirm = await inquirer.prompt([{
type: 'confirm',
name: 'proceed',
message: 'This will replace the current database. Continue?',
default: false
}]);
if (!confirm.proceed) {
return chalk.yellow('Restore cancelled');
}
try {
await execa('psql', [
'-h', 'localhost',
'-U', 'user',
'-d', 'database',
'-f', `./backups/${filename}`
]);
return chalk.green('Database restored successfully');
} catch (error) {
return chalk.red(`Restore failed: ${error.message}`);
}
}
}
Best Practices¶
Error Handling¶
async execute(args) {
try {
// Your skill logic
return await this.performAction(args);
} catch (error) {
// Log error for debugging
if (process.env.DEBUG) {
console.error(error);
}
// Return user-friendly message
return chalk.red(`Error: ${error.message}`);
}
}
Input Validation¶
async execute(args) {
// Validate required arguments
if (!args[0]) {
return chalk.red('Missing required argument') + '\n' + this.help();
}
// Validate argument format
const email = args[0];
if (!email.includes('@')) {
return chalk.red('Invalid email format');
}
return this.processEmail(email);
}
Progress Feedback¶
async longRunningTask() {
const spinner = ora('Processing...').start();
try {
// Step 1
spinner.text = 'Fetching data...';
await this.fetchData();
// Step 2
spinner.text = 'Processing data...';
await this.processData();
// Step 3
spinner.text = 'Saving results...';
await this.saveResults();
spinner.succeed('Task completed successfully');
return chalk.green('Done!');
} catch (error) {
spinner.fail('Task failed');
throw error;
}
}
Testing Your Skills¶
Manual Testing¶
# Test basic functionality
qalam myskill hello
# Test with parameters
qalam myskill hello Alice
# Test error handling
qalam myskill invalid-action
Unit Testing¶
// test/myskill.test.js
import MySkill from '../skills/myskill.js';
describe('MySkill', () => {
const skill = new MySkill();
test('says hello', async () => {
const result = await skill.execute(['hello']);
expect(result).toContain('Hello, World!');
});
test('says hello with name', async () => {
const result = await skill.execute(['hello', 'Alice']);
expect(result).toContain('Hello, Alice!');
});
});
Publishing Skills¶
Share with Others¶
- Create a repository for your skill
- Add installation instructions
- Share the repository URL
Installation from Git¶
# Clone skill repository
git clone https://github.com/user/qalam-skill-example.git
cp qalam-skill-example/skill.js ~/.qalam/skills/
Troubleshooting¶
Skill Not Loading¶
- Check file location:
~/.qalam/skills/ - Verify auto-load is enabled:
qalam config get skills.autoLoad - Check for syntax errors:
node -c ~/.qalam/skills/myskill.js - Ensure proper export:
export default MySkill;
Import Errors¶
// Use correct import paths
import Skill from '../../src/core/skillManager.js'; // ❌ Wrong
import Skill from './base-skill.js'; // ✅ Correct
Permission Issues¶
# Ensure skills directory has correct permissions
chmod 755 ~/.qalam/skills
chmod 644 ~/.qalam/skills/*.js