Instructions for AI agents working on @composio/cli. The sibling CLAUDE.md is a symlink to this file.
When you touch CLI code (anything under ts/packages/cli/src/), run pnpm typecheck from the repo root before pushing. Fix all type errors. Build/lint failures block CI.
This package pins its typescript dependency to TypeScript 6 (catalog:ts6 in package.json) because src/generation/typescript/* drives the JS compiler API, which TS7 (tsgo) does not ship. The pin only affects import ts from 'typescript' resolution — the tsc binary the typecheck scripts invoke still comes from the workspace root (TS7), since the TS6 alias package only ships a tsc6 bin. Keep the pin until the generation pipeline moves off the compiler API.
The CLI is built on the Effect.ts ecosystem and runs on Bun. Service-oriented architecture with dependency injection via Effect layers, generator-based control flow (Effect.gen), and structured error handling.
Bootstraps the CLI by composing Effect layers and running the root command via BunRuntime.runMain():
CliConfigLive— @effect/cli behavior (case-sensitive, no auto-correct, no built-ins)ComposioUserContextLive— User authentication state from~/.composio/ComposioSessionRepositoryLive— OAuth2 session managementComposioToolkitsRepositoryCachedLive— Cached API client for toolkits/toolsUpgradeBinaryLive— Self-update from GitHub releasesBunFileSystem.layer,BunContext.layer— Bun runtime integration
Errors are captured via the custom effect-errors/ module (source-mapped stack traces, Effect span timelines, formatted output).
Each command uses @effect/cli's Command.make() pattern. Top-level command files end in .cmd.ts; nested command groups live in their own subdirectory with a <group>.cmd.ts entry. Current top-level commands:
| Group / Command | Purpose |
| -------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| version | Display CLI version |
| whoami | Show logged-in user info (writes raw API key to stdout when piped — see Output Conventions) |
| login | Login with browser redirect or direct user/API key (--no-browser, --no-wait, --key, --user-api-key, --org) |
| logout | Clear stored API key |
| signup | Create a Composio account |
| upgrade | Self-update binary from GitHub releases |
| init | Bootstrap a Composio project in the current directory |
| install | Set up shell integration (PATH and completions) |
| generate {ts | py} | Generate type stubs (auto-detects project language if no subcommand) |
| agent | Manage AI agent presets |
| toolkits | List / inspect / version toolkits |
| tools | List / inspect / execute tools |
| triggers | List / manage trigger types |
| auth-configs | Manage auth-config resources (ac_*) |
| connected-accounts | Manage connected accounts (ca_*) |
| connections | Alias / helper for connected-account flows |
| orgs | Manage organizations |
| projects | Manage projects |
| local-tools | Manage local toolkits (via @composio/cli-local-tools) |
| logs | View tool-execution logs (logs-cmd/) |
| config | Read/write CLI config |
| listen | Listen for events |
| proxy | Proxy authenticated API requests |
| run | Run a saved script / preset |
| dev | Developer-only utilities |
| artifacts | Manage generated artifacts |
Options use Options.text(), Options.boolean(), Options.choice(), Options.directory() with Effect Schema validation. Feature flags live in feature-tags.ts and experimental-features.ts.
| Service | Purpose |
|---|---|
ComposioUserContext |
Auth state — reads/writes ~/.composio/user-config.json, merges env vars |
ComposioSessionRepository |
Creates OAuth2 sessions, polls until linked state |
ComposioToolkitsRepository |
API client — fetches toolkits, tools, trigger types; validates versions |
ComposioToolkitsRepositoryCached |
Decorator over base repository with file-based caching and graceful fallback |
NodeOs |
OS abstraction (homedir, platform, arch) |
JsPackageManagerDetector |
Detects npm/pnpm/yarn/bun for install instructions |
UpgradeBinary |
Fetches latest release from GitHub, downloads and replaces binary |
OS credential storage uses the sibling package @composio/cli-keyring (macOS Keychain / Linux Secret Service).
Reusable Effect computations: app-config (reads COMPOSIO_* env), debug-config, force-config, setup-cache-dir, toolkit-version-overrides (parses COMPOSIO_TOOLKIT_VERSION_<NAME>=<ver>), validate-toolkit-versions, with-log-level, find-composio-core-generated, version, compare-semver, log-metrics.
Effect Schema definitions with fromJSON / toJSON helpers via JSONTransformSchema(): Toolkit, Tool, TriggerType, UserData, Session.
Pipeline for composio generate {ts,py}:
- Fetch — Toolkits, tools, trigger types (filterable via
--toolkits) - Index — Groups by toolkit prefix into
ToolkitIndex - Generate — Builds TS/Python source using
@composio/ts-buildersAST builders - Transpile — Optionally converts TS → ESM JS for
@composio/core/generated
--type-tools includes full type definitions.
- CLI:
cli-config.ts—showBuiltIns: false,autoCorrectLimit: 0,isCaseSensitive: true - Constants:
constants.ts— env prefixes (COMPOSIO_,DEBUG_OVERRIDE_) - User config:
~/.composio/user-config.json - Cache files:
toolkits.json,tools.json,tools-as-enums.json,trigger-types.json
effect, @effect/cli, @effect/platform, @effect/platform-bun, @clack/prompts (terminal UI — stderr by default), picocolors, @composio/client (Composio API), @composio/core (types), @composio/ts-builders (AST gen), @composio/cli-keyring (OS credential store), @composio/cli-local-tools (local toolkit defs), semver, open, decompress.
Follow the Unix convention of separating human-readable decoration from machine-readable data:
- stdout — data only (
ui.output()). Captured by pipes /$(...)/> file. - stderr — all decoration (Clack spinners, logs, notes, intro/outro). Visible in terminal, invisible in pipes.
The three streams are independent contracts. Each capability depends only on the streams that actually serve it — there is deliberately no aggregate "interactive" flag (TerminalCapabilities in src/services/terminal-ui.ts):
- Prompting (
canPrompt) =stdin.isTTY && stderr.isTTY. stdin must accept input and stderr must display the Clack prompt. stdout is irrelevant: piping data must never change prompting or authentication behavior —composio login | teebehaves exactly like an attended login. - Machine output =
!stdout.isTTY.ui.output(data)writes only when stdout is redirected (pipe, subshell, file), or when the caller passes{ force: true }. Redirecting stdin or stderr must never make data leak onto a visible stdout terminal. - Decoration (
canDecorate) =stderr.isTTY. Spinners, logs, and notes only need stderr, so they still render when stdin or stdout is redirected.
Rules:
- All
TerminalUImethods exceptoutput()write to stderr via Clack's{ output: process.stderr }, and only when stderr is a TTY (canDecorate). ui.output(data)writes to stdout only when stdout is piped (or with explicitforce). No other stream participates in that decision.- Prompts (
ui.confirm,ui.select) run only whencanPrompt; otherwise they fall back to their defaults without blocking. - Piped stdout stays clean:
composio whoami | pbcopyputs only the key in the clipboard — decoration still renders on the terminal via stderr, and is suppressed only when stderr itself is captured. - Data commands (whoami, version, login, generate, etc.) call both decoration (stderr) and
ui.output()(stdout). - Action commands (logout, upgrade) produce no stdout data — output is purely decorative.
- Never write data to stderr or decoration to stdout, and never branch program behavior (auth paths, command flow) on stdout's TTY state.
When adding a new command: ask "Does this produce a value scripts should capture?" — yes → ui.output(value) + ui.log.*/ui.note(). No → decoration only.
Generator-based syntax throughout:
Effect.gen(function* () {
const service = yield* ServiceName; // resolve dependency
const result = yield* someEffect; // await computation
yield* Effect.log('message');
return result;
});Key patterns: Effect.all([...], { concurrency: 'unbounded' }) for parallel work, Layer.provide() for dependency composition, Effect.mapError() / Effect.catchTag() for typed errors, Effect.scoped for resource cleanup.
For table-driven tests over independent finite choices, use Effect Array do notation to build a typed Cartesian product instead of enumerating every case or nesting loops:
const cases = pipe(
Arr.Do,
Arr.bind('firstAxis', () => choices),
Arr.bind('secondAxis', () => choices)
);Each Arr.bind adds one independent axis to the generated cases.
- Never branch on an Effect value's internal tag field directly. Use the owning module's public refinement or matcher (
Option,Either,Exit,Cause,ValidationError),Match.valueTagsfor exhaustive unions, orPredicate.isTaggedfor a single narrowing guard. - Do not wrap a plain
ErrorinEffect.failfor expected failures. Give the failure a meaningfulData.TaggedErrortype with structured fields and a preserved cause, then recover withcatchTag/catchTags. ReserveEffect.dieandEffect.dieMessagefor impossible invariants. - Treat
unknown, JSON, persisted state, and API payloads as trust boundaries. Decode them witheffect/Schemaor narrow them withPredicate; anasassertion is not validation, and hand-rolled structural guards ('x' in obj/typeofchains) are not a substitute for a schema.effect/Schemais the CLI's schema tool — do not introduce zod here (zod is the convention in the SDK packages and docs). - Do not inspect private
@effect/clidescriptor shapes. Use publicCommandDescriptoroperations or keep declarative command metadata that can move to Effect v4's public command tree. - Prefer
Effect.mapError,Effect.matchEffect, and typed recovery overcatchAllblocks that flatten distinct failures into one message-only error.
All platform access goes through Effect services. node:path, node:fs, node:os, node:child_process, process.env, and try/catch are lint-banned (oxlint) in src/. Use the sanctioned equivalents:
| Need | Use |
|---|---|
| Path arithmetic (join/resolve/dirname/…) | Path service from @effect/platform (const path = yield* Path.Path) |
| Filesystem I/O | FileSystem service from @effect/platform |
| homedir / tmpdir / platform / arch | NodeOs service (src/services/node-os.ts, the sole node:os boundary) |
| Subprocesses | Command from @effect/platform; children that outlive the CLI via src/services/detached-process.ts (sole detached-spawn boundary) |
| Environment reads | effect/Config |
Sync fallible ops (JSON.parse, new URL, JSON.stringify) |
Either.try with a Data.TaggedError; JSON records via parseJsonRecord (src/utils/parse-json.ts) |
Conversion patterns, in order of preference:
- Yield the service inside existing Effect code.
- Convert a plain helper into an Effect when its callers are Effect-hosted (
Eitheris a subtype ofEffect, so both compose withyield*). - Pass the resolved service instance (e.g.
Path.Path,FileSystem.FileSystem) as a plain parameter into sync callbacks or promise pipelines that cannot become Effects (seetool-permissions.ts,generation/typescript/virtual-compiler-host.ts). - Modules that self-provide layers add
Path.layer/BunFileSystem.layer/NodeOs.Defaultto their stack instead of reaching for Node builtins.
The only code allowed to bypass services sits at declared runtime boundaries: the bin.ts bootstrap, the child-process companion runtime (run-helpers-runtime.ts, run-subagent-* — bundled into .mjs files that run in the user's spawned process), import-time UI setup (ui/colors.ts, ui/redact.ts), environment writes and whole-environment enumeration (which effect/Config cannot express), and spawn-time env handshakes between parent and child composio run processes. Every such boundary is an inline // eslint-disable-next-line <rule> -- <reason> comment registered in lint-boundaries.json. (oxlint honors both the eslint-disable and oxlint-disable spellings; only eslint-disable-next-line is sanctioned here, and the boundary validator rejects oxlint-disable comments in src/. The restricted-syntax rule ships from a JS plugin, so its disables name it eslint-js/no-restricted-syntax.)
Enforcement: pnpm run validate:boundaries (part of pnpm test, CI-blocking) fails when any eslint-disable in src/ is missing from the manifest, lacks a -- reason, or uses a file-wide form. Do not add new disables — thread the service instead. If code genuinely cannot run inside the Effect runtime, that is a new boundary: regenerate the manifest with pnpm run validate:boundaries -- --update and justify the boundary in the PR.
Read-only submodules under ts/vendor/ (do NOT modify — actual deps come from npm):
ts/vendor/effect/packages/effect/src/— core Effect runtimets/vendor/effect/packages/cli/src/—@effect/cli(Command, Options, Args)ts/vendor/effect/packages/platform/src/—@effect/platformts/vendor/clack/packages/prompts/src/—@clack/prompts(text, select, confirm, spinner, note, task, etc.)ts/vendor/clack/packages/core/src/—@clack/coreprimitives
Principles for arguments, flags, help, output, errors, interactivity, configuration, and exit codes:
- Use the repo-local
cli-commandskill for command design, implementation, Effect patterns, output conventions, and source-reference guidance. - Use the repo-local
cli-e2eskill for Docker-based CLI end-to-end tests underts/e2e-tests/cli/.
Use these when adding new commands or making UX decisions.
When modifying src/services/composio-clients.ts, inspect src/services/composio-clients-cached.ts in the same change. The cached repository is a layer wrapper over ComposioToolkitsRepository; method additions, removals, signature changes, and new exported error types must stay in sync. Decide for each new method whether it should be cached or passed through. Validation-style methods are usually passthrough; fetch methods are usually cached.
User-facing CLI commands should ship with VHS recordings (SVG + asciicast) when the command changes a documented workflow, introduces a new visible command surface, or needs demo coverage in release notes. Small internal wiring changes and hidden developer-only helpers can skip recordings if the PR says why. Workflow:
- Add entry to
recordings/recordings.yaml(fields:name,command,description,sleepAfterEnter,height: dynamicfor long output). - Run
bun scripts/record.ts— requiresCOMPOSIO_API_KEYandvhsonPATH.
Outputs land in recordings/{tapes,svgs,ascii}/<group>/<name>.{tape,svg,ascii}.
Use the repo-local cli-release skill before building or publishing first-party CLI binaries.
- A push to
nexttouching CLI paths publishes a rolling beta automatically. - The normal stable path promotes an existing tested beta through the
promote-stableworkflow action. @composio/cliand@composio/cli-local-toolsare ignored by Changesets. Never add a changeset targeting either package; it wedges the TypeScript SDK release action. Put human-facing CLI notes inCHANGELOG.mddirectly.package.jsonuses a private development sentinel and is never a binary-release authority. For an intentional minor or major release, dispatchbuild-betawith its optional version input, verify that beta, then promote it normally.
.github/workflows/build-cli-binaries.yml— binary build + release.github/workflows/cli.test-installation.yml— post-release install smoke tests.github/scripts/cli-release/resolve-release-target.sh— beta/stable target resolution.changeset/config.json