Skip to main content
ClaudeWave
Skill1.1k repo starsupdated 23d ago

adding-a-command

This skill creates new CLI commands following the Commander.js pattern in src/commands/, handling command registration in src/cli.ts with telemetry tracking via the tracked() wrapper and option parsing. Use it when adding new commands, subcommands, or related files to src/commands/, but not when modifying or fixing bugs in existing commands.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/caliber-ai-org/ai-setup /tmp/adding-a-command && cp -r /tmp/adding-a-command/skills/adding-a-command ~/.claude/skills/adding-a-command
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# Adding a Command

## Critical

- **Export pattern**: Command must export a named async function: `export async function myCommand(options?: OptionType)`. Never use default exports.
- **Registration in cli.ts**: Every command must be imported and registered with `.command()` chain in `src/cli.ts`, wrapped with `tracked()` for telemetry.
- **Error signaling**: Use `throw new Error('__exit__')` to exit gracefully without printing the error message. Use chalk for user-facing messages.
- **Options typing**: Commands receiving options must define a TypeScript interface for those options. Pass options as a destructured object parameter.

## Instructions

1. **Create the command file** at `src/commands/{commandName}.ts` with named async export.
   - Signature: `export async function {commandName}Command(options?: { optionName?: optionType }) { ... }`
   - Import only what you need (avoid kitchen-sink imports).
   - Return void (handle all output via console.log or chalk).
   - Verify the file follows the naming convention: camelCase function + "Command" suffix.

2. **Handle errors consistently**: Wrap error-prone operations in try/catch. Distinguish between user errors and system errors:
   - User error (bad input): `console.error(chalk.red('message')); throw new Error('__exit__');`
   - System error (missing dependency): `throw new Error('Detailed error message');` — this will print and exit with code 1.
   - Parse-like errors: Use ora spinner with `.fail()` before throwing.
   - This step prevents double error printing in bin.ts.

3. **Import and register in src/cli.ts** in the correct location:
   - Add import at the top: `import { {commandName}Command } from './commands/{commandName}.js';`
   - Register the command in the appropriate section (main commands, or nested under a group like `sources`).
   - For main commands: `.command('{kebab-name}').description('...').option(...).action(tracked('{kebab-name}', {commandName}Command))`
   - For subcommands (like `sources add`): `sources.command('add').description(...).action(tracked('sources:add', sourcesAddCommand))`
   - Key: Wrap handler with `tracked('{command-name}', handler)` for automatic telemetry.
   - Verify the command name in tracked() uses kebab-case for main commands and colon-separated for subcommands.

4. **Define options (if needed)**:
   - Add `.option()` chains before `.action()`: `.option('--flag', 'Description')` or `.option('--opt <value>', 'Description')`
   - For parsed options (like comma-separated agents), add a parse function: `.option('--opt <value>', 'Description', parseFunction)`
   - Pass options to handler: `.action(tracked('name', (opts) => command(opts)))`
   - Define TypeScript interface for the options object.
   - Verify option names use camelCase (Commander converts kebab-case flags to camelCase).

5. **Verify before proceeding**:
   - Function exports correctly and is imported in cli.ts.
   - Command is registered with tracked() wrapper.
   - Output uses chalk for colors, not plain console.log.
   - Error paths throw `new Error('__exit__')` for user errors.

## Examples

### Example 1: Simple command (status)

User says: "Add a command to show config status"

**Actions taken**:
1. Create src/commands/status.ts with statusCommand() export
2. Import and register in src/cli.ts with tracked() wrapper

**Result**: `caliber status` displays config status; `caliber status --json` outputs JSON.

Code example:
```typescript
import chalk from 'chalk';
import { loadConfig } from '../llm/config.js';

export async function statusCommand(options?: { json?: boolean }) {
  const config = loadConfig();
  
  if (options?.json) {
    console.log(JSON.stringify({ configured: !!config }, null, 2));
    return;
  }
  
  console.log(chalk.bold('Status'));
  console.log(`  LLM: ${chalk.green(config?.provider || 'Not configured')}`);
}
```

Registration in src/cli.ts:
```typescript
import { statusCommand } from './commands/status.js';
program
  .command('status')
  .description('Show config status')
  .option('--json', 'Output as JSON')
  .action(tracked('status', statusCommand));
```

---

### Example 2: Subcommand with arguments

User says: "Add a sources add subcommand"

**Actions taken**:
1. Create src/commands/sources.ts with sourcesAddCommand() export
2. Register under sources group with tracked('sources:add', ...)

**Result**: `caliber sources add ../lib` adds a source.

Code example:
```typescript
export async function sourcesAddCommand(sourcePath: string) {
  if (!fs.existsSync(sourcePath)) {
    console.log(chalk.red(`Path not found: ${sourcePath}`));
    throw new Error('__exit__');
  }
  const existing = loadSourcesConfig(process.cwd());
  existing.push({ type: 'repo', path: sourcePath });
  writeSourcesConfig(process.cwd(), existing);
  console.log(chalk.green(`Added ${sourcePath}`));
}
```

Registration:
```typescript
const sources = program.command('sources');
sources
  .command('add')
  .argument('<path>', 'Path to add')
  .action(tracked('sources:add', sourcesAddCommand));
```

---

### Example 3: Command with option parsing

User says: "Add init with --agent flag supporting comma-separated values"

**Actions taken**:
1. Create parseAgentOption() parser in src/cli.ts
2. Create src/commands/init.ts with initCommand(options)
3. Register with custom parser

**Result**: `caliber init --agent claude,cursor` passes parsed array to handler.

Parser code:
```typescript
function parseAgentOption(value: string) {
  const agents = value.split(',').map(s => s.trim().toLowerCase());
  if (agents.length === 0) {
    console.error('Invalid agent');
    process.exit(1);
  }
  return agents;
}

program.command('init')
  .option('--agent <type>', 'Agents (comma-separated)', parseAgentOption)
  .action(tracked('init', initCommand));
```

## Common Issues

**Issue**: "SyntaxError: The requested module does not provide an export named 'myCommand'"
- **Cause**: Function not exported or exported as default instead of named.
- **Fix**: U
caliber-testingSkill

Writes Vitest tests following project patterns: __tests__/ directories, vi.mock() for module mocking with vi.hoisted() for test-time factories, global LLM mock from src/test/setup.ts, environment variable save/restore in beforeEach/afterEach, vi.clearAllMocks() lifecycle, and test file organization. Use when user says 'write tests', 'add test coverage', 'test this', creates *.test.ts files, or when test failures appear in CI. Do NOT use for non-test code or for debugging without writing tests.

find-skillsSkill

Discovers and installs community skills from the public registry. Use when the user mentions a technology, framework, or task that could benefit from specialized skills not yet installed, asks 'how do I do X', 'find a skill for X', or starts work in a new technology area. Proactively suggest when the user's task involves tools or frameworks without existing skills.

llm-providerSkill

Adds a new LLM provider implementing LLMProvider interface with call() and stream() methods. Integrates with provider factory in src/llm/index.ts, config detection in src/llm/config.ts, and error handling via tracking and recovery. Use when adding a new model backend, integrating a third-party LLM API, or extending LLM platform support. Do NOT use for fixing bugs in existing providers, modifying existing provider behavior, or changing the LLMProvider interface.

save-learningSkill

Saves user instructions as persistent learnings for future sessions. Use when the user says 'remember this', 'always do X', 'from now on', 'never do Y', or gives any instruction they want persisted across sessions. Proactively suggest when the user states a preference, convention, or rule they clearly want followed in the future.

scoring-checksSkill

Add a new deterministic scoring check in src/scoring/checks/ that evaluates config quality. Follows the Check[] return pattern, uses point constants from src/scoring/constants.ts, and integrates via filterChecksForTarget() in src/scoring/index.ts. Use when user says 'add scoring check', 'new check', 'modify scoring criteria', or works in src/scoring/checks/. Do NOT use for display changes or refactoring scoring logic.

setup-caliberSkill

Sets up Caliber for automatic AI agent context sync. Installs pre-commit hooks so CLAUDE.md, Cursor rules, and Copilot instructions update automatically on every commit. Use when Caliber hooks are not yet installed or when the user asks about keeping agent configs in sync.

writers-patternSkill

Add a new platform writer module in src/writers/ that generates and writes agent config files for a supported platform. Each writer exports a function that accepts a config interface, creates directories (rules/, skills/, mcp configs), writes files with proper formatting and frontmatter, and returns string[] of written file paths. Use when adding platform support for a new agent, integrating a new code AI tool, or extending caliber to support new targets. Do NOT use for modifying existing writers, refactoring scoring logic, or changing how writers are invoked.