Shared CLI utilities for LLM-focused command-line tools.
- Output Formatting: JSON and TOON (Token-Oriented Object Notation) output with field filtering
- Internationalization: Language detection and message management (Korean/English)
- Progress Indicators: Emoji-based status messages for long-running operations
- Input Validation: Type-safe validation for common input patterns
- Structured Errors: Machine-readable error codes, suggestions, and exit-code mapping for agent-friendly CLIs
bun add @pleaseai/cli-toolkitFormat command output as JSON or TOON with optional field filtering:
import { outputData, parseFields } from '@pleaseai/cli-toolkit/output'
const data = [
{ number: 123, title: 'Feature request', state: 'OPEN' },
{ number: 124, title: 'Bug fix', state: 'CLOSED' },
]
// JSON output with all fields
outputData(data, 'json')
// TOON output for LLM consumption (58.9% token savings vs JSON)
outputData(data, 'toon')
// JSON output with field filtering
const fields = parseFields('number,title')
outputData(data, 'json', fields)
// Output: [{"number": 123, "title": "Feature request"}, ...]Manage multilingual messages for your CLI:
import { detectSystemLanguage, I18nManager } from '@pleaseai/cli-toolkit/i18n'
const i18n = new I18nManager()
// Register messages
i18n.register('myapp.issues', 'en', {
creating: 'Creating issue...',
created: (number: number) => `Issue #${number} created!`,
})
i18n.register('myapp.issues', 'ko', {
creating: '이슈 생성 중...',
created: (number: number) => `이슈 #${number}가 생성되었습니다!`,
})
// Auto-detect language and get messages
const lang = detectSystemLanguage() // 'ko' or 'en' based on LANG env var
const msg = i18n.get('myapp.issues', lang)
console.log(msg.creating) // 'Creating issue...' or '이슈 생성 중...'
console.log(msg.created(123)) // 'Issue #123 created!' or '이슈 #123가 생성되었습니다!'Display user-friendly progress indicators:
import { createProgressIndicator } from '@pleaseai/cli-toolkit/progress'
const progress = createProgressIndicator()
progress.start('Fetching data...') // 📡 Fetching data...
progress.update('Processing records...') // ⏳ Processing records...
progress.success('Operation completed!') // ✅ Operation completed!
progress.error('Operation failed') // ❌ Operation failed
progress.info('Additional info') // ℹ️ Additional infoValidate user inputs with clear error messages:
import {
validateNonEmptyString,
validatePattern,
validatePositiveInteger,
} from '@pleaseai/cli-toolkit/validation'
// Validate positive integers
const id = validatePositiveInteger('123', 'Issue ID') // 123
// validatePositiveInteger('0', 'Issue ID') → throws "Issue ID must be a positive integer"
// Validate non-empty strings
const body = validateNonEmptyString('Hello world', 'Comment body') // 'Hello world' (trimmed)
// validateNonEmptyString('', 'Comment body') → throws "Comment body cannot be empty"
// Validate patterns
const username = validatePattern('user123', /^[a-z0-9]+$/, 'Username', 'alphanumeric only')
// validatePattern('user-123', /^[a-z0-9]+$/, 'Username') → throws "Username must be alphanumeric only"Throw structured errors with machine-readable codes and render them consistently
for agents to parse. Built on @vercel/error,
so every error can answer what happened (message), why (reason), what
could help (hint), how to fix it (fix), and where to learn more (link):
import {
CliError,
exitCodeForError,
toErrorOutput,
VALIDATION_ERROR,
} from '@pleaseai/cli-toolkit/errors'
import { outputData } from '@pleaseai/cli-toolkit/output'
try {
if (!issue) {
throw new CliError('Issue #999 not found', {
code: 'NOT_FOUND',
fix: 'Run `gh-please issue list` to see open issues',
})
}
}
catch (error) {
// Render a stable { error, code, reason?, hint?, fix?, link? } payload (JSON or TOON)
outputData(toErrorOutput(error), 'toon')
// Map to a process exit code (2 for validation errors, otherwise 1)
process.exitCode = exitCodeForError(error)
}For tools with many error sites, createErrors (re-exported from
@vercel/error) builds a factory that injects a shared scope and derives docs
links from error codes:
import { CliError, createErrors } from '@pleaseai/cli-toolkit/errors'
const errors = createErrors({
scope: 'gh-please',
ErrorClass: CliError,
docsBaseUrl: 'https://example.com/docs/errors',
})
errors.raise('Issue #999 not found', { code: 'NOT_FOUND' })
// throws a CliError with link https://example.com/docs/errors/NOT_FOUNDThe built-in validators already throw CliError tagged with VALIDATION_ERROR
(exit code 2), so wrapping a command in the try/catch above gives you
consistent structured errors and exit codes for free.
Detecting a
CliError: use theisCliError(error)type guard instead of a bareerror instanceof CliError. Each subpath (/errors,/output,/validation) bundles its own copy ofCliError, so a directinstanceofcheck can silently returnfalsefor an error thrown by a different subpath (or by a second copy of the package in the dependency tree).isCliError,toErrorOutput, andexitCodeForErroralready handle this for you.
import { isCliError } from '@pleaseai/cli-toolkit/errors'
if (isCliError(error)) {
// error.code / error.reason / error.hint / error.fix are safe to read here
}The collapseHomeDirectory helper (in the output module) keeps paths portable
in structured output:
import { collapseHomeDirectory } from '@pleaseai/cli-toolkit/output'
collapseHomeDirectory('/Users/alice/project/bin', '/Users/alice') // '~/project/bin'
// The home directory defaults to os.homedir() when omitted.outputData(data, format, fields?)- Output data in JSON or TOON formatoutputJson(data)- Output as JSONoutputToon(data)- Output as TOON (58.9% token savings)parseFields(fieldString)- Parse comma-separated field listfilterFields(data, fields)- Filter object/array to specified fieldsisStructuredOutput(options)- Check if structured output is requestedvalidateFormat(format)- Validate output formatcollapseHomeDirectory(path, homeDir?)- Replace a leading home-dir prefix with~
detectSystemLanguage()- Detect language from environment variablesI18nManager- Message registry and managerregister(domain, lang, messages)- Register message dictionaryget(domain, lang)- Get messages for domain and languagehas(domain)- Check if domain is registeredhasLanguage(domain, lang)- Check if language is registered
getCommonMessages(lang)- Get built-in common messages
createProgressIndicator()- Create console-based progress indicatorformatSuccess(message)- Format success message with ✅formatError(message)- Format error message with ❌formatInfo(message)- Format info message with ℹ️formatWarning(message)- Format warning message with⚠️ formatProgress(message)- Format progress message with ⏳
Numeric:
validatePositiveInteger(value, fieldName?)- Validate positive integer stringvalidateNumericId(value, fieldName?)- Validate positive integer (string or number)validateRange(value, min, max, fieldName?)- Validate number within range
Text:
validateNonEmptyString(value, fieldName?)- Validate non-empty stringvalidateMaxLength(value, maxLength, fieldName?)- Validate string lengthvalidateString(value, maxLength, fieldName?)- Validate non-empty + max lengthvalidatePattern(value, pattern, fieldName?, description?)- Validate regex pattern
Validators throw
CliErrorwith codeVALIDATION_ERROR(a subclass ofError, so existingtry/catchkeeps working).
CliError- Structured error extendingVercelError; carriescode(always set) plusreason,hint,fix,link,metadata,cause, ...isCliError(error)- Type guard forCliError, robust across subpath/version boundaries (prefer overinstanceof)exitCodeForError(error)- Map an error to a process exit code (2for validation, else1)errorOutput(message, code, details?)- Build a{ error, code, reason?, hint?, fix?, link? }payloadtoErrorOutput(error)- Convert any thrown value into a structured payloadVALIDATION_ERROR/UNKNOWN_ERROR- Built-in error code constants- Re-exported from
@vercel/error:VercelError,createErrors,isVercelError,isError,isErrorLike,hasCode,getMessage,getRootCause
All modules are fully typed with TypeScript. Import types directly:
import type { Language, OutputFormat, ProgressIndicator } from '@pleaseai/cli-toolkit'bun test
bun test --coverageMIT
PleaseAI