# Composiohq Composio Implement CLI Command

> Implement new CLI commands in ts/packages/cli/ using Effect.ts patterns, service wiring, and @effect/cli declarations.

- Skill: `tuyv/composiohq-composio-implement-cli-command` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add tuyv/composiohq-composio-implement-cli-command`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tuyv/composiohq-composio-implement-cli-command/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tuyv (https://skillmd.com/u/tuyv)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tuyv/composiohq-composio-implement-cli-command

---


# Implement CLI Command

Implement new commands and subcommands in `ts/packages/cli/`. Covers file creation, Effect patterns, service wiring, option declaration, output conventions, and registration.

## When to Use

- Implementing a new CLI command from a spec or design
- Adding a subcommand to an existing command group
- Wiring a new command into the command tree
- Understanding how existing commands work to extend them

For CLI **design** (arguments, flags, help text, UX), see the `create-cli` skill instead.
For CLI **e2e tests**, see the `create-cli-e2e` skill instead.

## Architecture

The CLI uses `@effect/cli` for command declaration, `effect` for the runtime, and a service-oriented architecture with dependency injection via Effect layers.

See `ts/packages/cli/AGENTS.md` for the full architecture reference (services, effects, models, dependencies, vendor submodule locations).

```
src/
├── bin.ts                    # Entry point — layer composition, error handling, runtime
├── commands/
│   ├── index.ts              # Command tree — registers all commands
│   ├── $default.cmd.ts       # Root command with global options (--log-level)
│   ├── version.cmd.ts        # Simple data command
│   ├── whoami.cmd.ts         # Data command with service dependency
│   ├── login.cmd.ts          # Complex command (options, spinner, polling)
│   ├── logout.cmd.ts         # Action command (no stdout data)
│   ├── upgrade.cmd.ts        # Action command (delegates to service)
│   ├── generate/
│   │   ├── generate.cmd.ts   # Parent command group for `composio generate`
│   │   ├── generate-py.cmd.ts # `composio generate py`
│   │   └── generate-ts.cmd.ts # `composio generate ts`
│   ├── manage/
│   │   └── manage.cmd.ts     # Parent command group for `composio manage`
│   ├── ts/
│   │   ├── ts.cmd.ts         # Existing TS generation internals, referenced from generate/
│   │   └── commands/
│   │       └── ts.generate.cmd.ts  # Reusable TS generation logic
│   └── py/
│       ├── py.cmd.ts         # Existing Python generation internals, referenced from generate/
│       └── commands/
│           └── py.generate.cmd.ts
├── services/                 # Effect services (dependency injection)
├── effects/                  # Reusable Effect computations
├── models/                   # Effect Schema definitions
├── generation/               # Code generation pipeline
├── effect-errors/            # Error capture and formatting
└── ui/                       # Terminal output helpers
```

### File Naming Convention

- Command files: `<name>.cmd.ts` (e.g., `version.cmd.ts`, `login.cmd.ts`)
- Subcommand implementation files: `<parent>.<name>.cmd.ts` inside `commands/` (e.g., `ts.generate.cmd.ts`)
- Parent command groups: `<name>.cmd.ts` at the group level (e.g., `generate/generate.cmd.ts`, `manage/manage.cmd.ts`)
- Wrapper subcommand entrypoints can also live beside their parent group (e.g., `generate/generate-py.cmd.ts`, `generate/generate-ts.cmd.ts`)

## Creating a New Command
### Step 1: Create the Command File

Create `src/commands/<name>.cmd.ts`.

**Minimal template** (data command, no options):

```typescript
import { Command } from '@effect/cli';
import { Effect } from 'effect';
import { TerminalUI } from 'src/services/terminal-ui';

export const myCmd = Command.make('my-command', {}).pipe(
  Command.withDescription('Brief description of what the command does.'),
  Command.withHandler(() =>
    Effect.gen(function* () {
      const ui = yield* TerminalUI;

      // Compute result...
      const result = 'some-value';

      yield* ui.log.info(result);   // Decoration → stderr
      yield* ui.output(result);     // Data → stdout (for scripts)
    })
  )
);
```

**Template with options:**

```typescript
import { Command, Options } from '@effect/cli';
import { Effect, Option } from 'effect';
import { TerminalUI } from 'src/services/terminal-ui';
import { ComposioToolkitsRepository } from 'src/services/composio-clients';

// Define options at module level
const toolkitSlug = Options.text('toolkit').pipe(
  Options.withDescription('Toolkit slug to look up.')
);

const searchOpt = Options.optional(
  Options.text('search')
).pipe(
  Options.withDescription('Search query to filter results.')
);

export const myCmd = Command.make('my-command', { toolkitSlug, searchOpt }).pipe(
  Command.withDescription('Brief description.'),
  Command.withHandler(({ toolkitSlug, searchOpt }) =>
    Effect.gen(function* () {
      const ui = yield* TerminalUI;
      const client = yield* ComposioToolkitsRepository;

      yield* ui.intro('composio my-command');

      // Use options — searchOpt is Option<string>
      const search = Option.getOrUndefined(searchOpt);

      // Fetch data with spinner
      const result = yield* ui.withSpinner(
        'Fetching data...',
        client.getToolkits(),
        { successMessage: 'Done', errorMessage: 'Failed to fetch' }
      );

      // Output
      yield* ui.note(formatResult(result), 'Result');
      yield* ui.output(formatResult(result));
      yield* ui.outro('Done');
    })
  )
);
```

### Step 2: Register the Command

Add the command to `src/commands/index.ts`:

```typescript
import { myCmd } from './my-command.cmd';

const $cmd = $defaultCmd.pipe(
  Command.withSubcommands([
    versionCmd,
    upgradeCmd,
    whoamiCmd,
    loginCmd,
    logoutCmd,
    generateCmd,
    manageCmd,
    myCmd,  // Add here
  ])
);
```

### Step 3: Add Required Service Layers (if any)

If your command uses a new service not already in `bin.ts`, add its layer:

```typescript
// In src/bin.ts
const layers = Layer.mergeAll(
  // ... existing layers
  MyNewServiceLive,  // Add if needed
);
```

Most commands only use services already provided. The `ComposioToolkitsRepository` service is provided by `ComposioToolkitsRepositoryCachedLive` in `bin.ts` — you do not need to add a separate layer for the base repository.

## Creating a Subcommand Group

For commands like `composio manage toolkits list`, `composio manage toolkits info`:

### Step 1: Create the Directory Structure

```
src/commands/manage/toolkits/
├── toolkits.cmd.ts              # Parent command group under `manage`
└── commands/
    ├── toolkits.list.cmd.ts     # composio manage toolkits list
    └── toolkits.info.cmd.ts     # composio manage toolkits info
```

### Step 2: Create the Parent Command

`src/commands/manage/toolkits/toolkits.cmd.ts`:
```typescript
import { Command } from '@effect/cli';
import { toolkitsCmd$List } from './commands/toolkits.list.cmd';
import { toolkitsCmd$Info } from './commands/toolkits.info.cmd';

export const toolkitsCmd = Command.make('toolkits').pipe(
  Command.withDescription('Discover and inspect available toolkits.'),
  Command.withSubcommands([toolkitsCmd$List, toolkitsCmd$Info])
);
```

### Step 3: Create Each Subcommand

`src/commands/manage/toolkits/commands/toolkits.list.cmd.ts`:
```typescript
import { Command, Options } from '@effect/cli';
import { Effect, Option } from 'effect';
import { TerminalUI } from 'src/services/terminal-ui';
import { ComposioToolkitsRepository } from 'src/services/composio-clients';

const searchOpt = Options.optional(
  Options.text('search')
).pipe(
  Options.withDescription('Search toolkits by name or description.')
);

export const toolkitsCmd$List = Command.make('list', { searchOpt }).pipe(
  Command.withDescription('List available toolkits.'),
  Command.withHandler(({ searchOpt }) =>
    Effect.gen(function* () {
      const ui = yield* TerminalUI;
      const client = yield* ComposioToolkitsRepository;

      const toolkits = yield* ui.withSpinner(
        'Fetching toolkits...',
        client.getToolkits(),
        { successMessage: 'Toolkits loaded' }
      );

      // Format and output
      const output = toolkits
        .map(t => `${t.slug} - ${t.meta.description}`)
        .join('\n');

      yield* ui.log.info(output);
      yield* ui.output(output);
    })
  )
);
```

### Step 4: Register the Parent in `manage/manage.cmd.ts`

```typescript
import { Command } from '@effect/cli';
import { toolkitsCmd } from './toolkits/toolkits.cmd';

export const manageCmd = Command.make('manage').pipe(
  Command.withDescription('Manage existing Composio resources.'),
  Command.withSubcommands([
    // ... existing manage subcommands
    toolkitsCmd,
  ])
);
```

## Option Declaration Patterns
Options are declared at module level using `@effect/cli`'s `Options` API. The template above demonstrates the most common types (required text, optional text). For other option types, see `ts/vendor/effect/packages/cli/src/Options.ts`.

Both `Options.optional(Options.text(...))` (wrapping) and `Options.text(...).pipe(Options.optional)` (piped) are valid. Use whichever reads better.

### Common patterns:

```typescript
import { Options } from '@effect/cli';

// Boolean flag (with default)
const verbose = Options.boolean('verbose').pipe(
  Options.withDefault(false),
  Options.withDescription('Enable verbose output.')
);

// Text with alias
const output = Options.optional(
  Options.text('output')
).pipe(
  Options.withAlias('o'),
  Options.withDescription('Output path.')
);

// Choice from fixed set
const format = Options.choice('format', ['json', 'table', 'plain']).pipe(
  Options.withDefault('table'),
  Options.withDescription('Output format.')
);
```

### Using Options in Handler

```typescript
Command.make('my-cmd', { search, verbose }).pipe(
  Command.withHandler(({ search, verbose }) =>
    Effect.gen(function* () {
      // search: Option<string> — use Option.getOrUndefined, Option.match, Option.isSome
      const searchValue = Option.getOrUndefined(search);

      // verbose: boolean — direct use
      if (verbose) { yield* Effect.logDebug('Verbose mode'); }
    })
  )
);
```

## Output Conventions

Follow the output conventions in `ts/packages/cli/AGENTS.md` § "Output Conventions" (stdout for data via `ui.output()`, stderr for decoration).

**Data commands** — produce a value scripts should capture:

```typescript
yield* ui.note(apiKey, 'API Key');   // Decoration → stderr (pretty box)
yield* ui.output(apiKey);            // Data → stdout (scripts capture)
```

**Action commands** — perform a side effect, no data:

```typescript
yield* ui.log.success('Logged out successfully.');
// NO ui.output() call — nothing for scripts to capture
```

## TerminalUI Spinners

The `TerminalUI` service provides two spinner APIs. For output/decoration methods (`output`, `log.*`, `note`, `intro`, `outro`), see `ts/packages/cli/src/services/terminal-ui.ts`.

```typescript
// Automatic: wraps an Effect, auto-stops on success/error
const result = yield* ui.withSpinner(
  'Loading...',
  someEffect,
  { successMessage: 'Done!', errorMessage: 'Failed!' }
);

// Manual: full control over message updates
const result = yield* ui.useMakeSpinner('Loading...', spinner =>
  Effect.gen(function* () {
    yield* spinner.message('Step 1...');
    const data = yield* fetchStep1;
    yield* spinner.message('Step 2...');
    const result = yield* fetchStep2(data);
    yield* spinner.stop('All done!');
    return result;
  })
);
```

## Creating a New Service

If your command requires functionality not covered by existing services (see `ts/packages/cli/AGENTS.md` for the full list):

1. Define the service interface and tag in `src/services/<name>.ts`
2. Create a `Live` layer implementation
3. Register the layer in `src/bin.ts`

Reference `src/services/upgrade-binary.ts` for a simple service pattern, or `src/services/composio-clients.ts` for a complex one with caching.

## Error Handling Patterns

### Optional Values

```typescript
yield* ctx.data.apiKey.pipe(
  Option.match({
    onNone: () => ui.log.warn('Not logged in. Run `composio login`.'),
    onSome: apiKey =>
      Effect.gen(function* () {
        yield* ui.output(apiKey);
      }),
  })
);
```

### Typed Errors with catchTag

```typescript
yield* client.getToolkitsBySlugs(slugs).pipe(
  Effect.catchTag('services/InvalidToolkitsError', error =>
    Effect.gen(function* () {
      yield* ui.log.error(`Invalid toolkits: ${error.invalidToolkits.join(', ')}`);
      return yield* Effect.fail(error);
    })
  )
);
```

### Logging Non-Fatal Errors

```typescript
yield* riskyOperation.pipe(
  Effect.catchAll(error =>
    Effect.logWarning(`Non-critical failure: ${error.message}`)
  )
);
```

## Parallel Data Fetching

Use `Effect.all` with `concurrency` for parallel API calls:

```typescript
const [toolkits, tools, triggerTypes] = yield* Effect.all(
  [
    client.getToolkits(),
    client.getTools(slugs),
    client.getTriggerTypes(slugs),
  ],
  { concurrency: 'unbounded' }
);
```

## Extracting Reusable Logic

For commands that share logic (e.g., `composio generate` delegates to `composio generate ts`):
```typescript
// In ts.generate.cmd.ts — export the logic separately
export function generateTypescriptTypeStubs(params: { ... }) {
  return Effect.gen(function* () {
    const ui = yield* TerminalUI;
    // ... implementation
  });
}

// The command uses it
export const tsCmd$Generate = Command.make('generate', { ... }).pipe(
  Command.withHandler(params => generateTypescriptTypeStubs(params))
);

// Other commands can reuse it
// In generate/generate-ts.cmd.ts
import { generateTypescriptTypeStubs } from '../ts/commands/ts.generate.cmd';
yield* Match.value(envLang).pipe(
  Match.when('TypeScript', () => generateTypescriptTypeStubs({ ... })),
  Match.when('Python', () => generatePythonTypeStubs({ ... })),
  Match.exhaustive
);
```

## Retry with Exponential Backoff

For polling operations (e.g., waiting for OAuth):

```typescript
import { Schedule } from 'effect';

const result = yield* ui.useMakeSpinner('Waiting...', spinner =>
  Effect.retry(
    Effect.gen(function* () {
      const status = yield* client.getSession(session);
      if (status.status === 'linked') return status;
      return yield* Effect.fail(new Error('Still pending'));
    }),
    Schedule.exponential('0.3 seconds').pipe(
      Schedule.intersect(Schedule.recurs(15)),
      Schedule.intersect(Schedule.spaced('5 seconds'))
    )
  ).pipe(
    Effect.tap(() => spinner.stop('Success!')),
    Effect.tapError(() => spinner.error('Timed out'))
  )
);
```

## Checklist

When implementing a new command:

1. Create `src/commands/<name>.cmd.ts` (or `src/commands/<group>/commands/<group>.<name>.cmd.ts` for subcommands)
2. Define options at module level using `Options.*`
3. Create the command with `Command.make(name, options).pipe(Command.withDescription(...), Command.withHandler(...))`
4. In the handler, resolve services with `yield* ServiceName`
5. Follow the output convention: `ui.output()` for data, `ui.log.*` for decoration
6. Register in `src/commands/index.ts` (or in the parent group's command file)
7. If using a new service, add its layer to `src/bin.ts`
8. Build to verify: `cd ts/packages/cli && pnpm build`
9. Add recordings for the new command (see **Recording** below)

If the build fails, check for: (1) missing service imports, (2) `Option<string>` being used where `string` is expected (use `Option.getOrUndefined` or `Option.match`), (3) the command not being exported from its file.

## Recording

New commands should have VHS recordings for documentation. Recordings produce SVGs and asciicasts that demonstrate the command in action.

### Step 1: Add Entries to recordings.yaml

Add recording entries to `ts/packages/cli/recordings/recordings.yaml` under the appropriate group:

```yaml
recordings:
  my-command:
    - name: help
      description: Show my-command help
      command: "composio my-command --help"
      height: dynamic  # Use for commands whose output exceeds 750px

    - name: basic
      description: Run my-command with default options
      command: "composio my-command"
```

Each entry has:
- `name` — filename for the recording (produces `<name>.svg`, `<name>.ascii`, `<name>.tape`)
- `command` — the exact shell command to record
- `description` — (optional) comment shown instantly above the command
- `sleepAfterEnter` — (optional) override the default wait time after Enter (default: `6s`)
- `height` — (optional) `'dynamic'` for auto-sizing via two-pass recording, or a fixed pixel number. Omit to use the default height (750px)

Use `height: dynamic` for commands that produce long output (help text, full listings). Fixed height is fine for short-output commands (version, no-results, limited queries).

### Step 2: Run the Recorder

```bash
cd ts/packages/cli
bun scripts/record.ts
```

Requires `COMPOSIO_API_KEY` in the environment and `vhs` + `composio` on `PATH`.

### Output Structure

```
recordings/
├── recordings.yaml                    # Config
├── tapes/<group>/<name>.tape          # Generated VHS tape files (committed)
├── svgs/<group>/<name>.svg            # SVG recordings
└── ascii/<group>/<name>.ascii         # Asciicast recordings
```

## Reference Files

| File | Purpose |
|---|---|
| `src/commands/version.cmd.ts` | Simplest command (no options, no services beyond TerminalUI) |
| `src/commands/whoami.cmd.ts` | Data command with service dependency |
| `src/commands/login.cmd.ts` | Complex command (options, spinner, polling, retry) |
| `src/commands/logout.cmd.ts` | Action command (no stdout data) |
| `src/commands/upgrade.cmd.ts` | Action command delegating to service |
| `src/commands/generate/generate.cmd.ts` | Parent `generate` command and delegation entrypoint |
| `src/commands/manage/manage.cmd.ts` | Parent `manage` command and subcommand registration |
| `src/commands/ts/commands/ts.generate.cmd.ts` | Reusable TS generation logic used by `generate ts` |
| `src/commands/index.ts` | Command tree registration |
| `src/bin.ts` | Entry point, layer composition, error handling |
| `src/services/terminal-ui.ts` | TerminalUI service interface |
| `src/services/composio-clients.ts` | API client service (HTTP, pagination, metrics) |
| `ts/packages/cli/AGENTS.md` | CLI architecture, services, effects, output conventions |
