Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/cli/src/ai-context/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export const MANAGE_REFERENCES = [
id: 'manage-plan',
description: 'Check account plan, entitlements, feature limits, and available locations (`account plan`)',
},
{
id: 'manage-account-members',
description: 'List account members and pending invites (`account members`)',
},
] as const

export const SKILL = {
Expand Down Expand Up @@ -101,7 +105,7 @@ export const ACTIONS = [
},
{
id: 'manage',
description: 'Understand your account plan, entitlements, and feature limits.',
description: 'Understand your account plan, entitlements, feature limits, members, and pending invites.',
references: MANAGE_REFERENCES,
},
] as const
Expand Down
64 changes: 64 additions & 0 deletions packages/cli/src/ai-context/references/manage-account-members.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Account Members

List active account members and pending or expired account invites.

## Usage

```bash
npx checkly account members
npx checkly account members --output json
npx checkly account members --search alice
npx checkly account members --type invite --status pending
npx checkly account members --role admin
npx checkly account members --limit 25
npx checkly account members --hide-id
```

Flags:
- `--search <term>` — match member names, member emails, and invite emails.
- `--type <type>` — `member` or `invite` (case-insensitive).
- `--role <role>` — `owner`, `admin`, `read_write`, `read_run`, or `read_only` (case-insensitive).
- `--status <status>` — `active`, `pending`, or `expired` (case-insensitive).
- `-l, --limit <n>` — number of rows to return, from 1 to 100.
- `--next-id <cursor>` — cursor for the next page. Requires `--limit`.
- `-o, --output <format>` — `table` (default), `json`, or `md`.
- `--hide-id` — hide member and invite IDs in table output.

## JSON response shape

```json
{
"members": [
{
"type": "member",
"accountId": "11111111-1111-1111-1111-111111111111",
"userId": "22222222-2222-2222-2222-222222222222",
"name": "Owner User",
"email": "owner@example.com",
"role": "OWNER",
"status": "ACTIVE",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-02T00:00:00.000Z",
"isSupportMembership": false,
"ssoEnabled": false,
"mfaEnabled": true
},
{
"type": "invite",
"id": "33333333-3333-3333-3333-333333333333",
"accountId": "11111111-1111-1111-1111-111111111111",
"email": "pending@example.com",
"role": "READ_ONLY",
"status": "PENDING",
"inviterEmail": "owner@example.com",
"createdAt": "2026-01-03T00:00:00.000Z",
"updatedAt": "2026-01-03T00:00:00.000Z",
"expiresAt": "2026-02-03T00:00:00.000Z"
}
],
"length": 2,
"nextId": null
}
```

Member roles are returned as `OWNER`, `ADMIN`, `READ_WRITE`, `READ_RUN`, or `READ_ONLY`. Invite roles exclude `OWNER`. Invite statuses are returned as `PENDING` or `EXPIRED`.
2 changes: 1 addition & 1 deletion packages/cli/src/ai-context/references/manage.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Account Management

Understand your account's plan, entitlements, and limits.
Understand your account's plan, entitlements, limits, members, and pending invites.

## Plan-aware workflow

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/ai-context/skill.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: checkly
description: Set up, create, test and manage monitoring checks using the Checkly CLI. Use when working with Agentic Checks, API Checks, Browser Checks, URL Monitors, ICMP Monitors, Playwright Check Suites, Heartbeat Monitors, Alert Channels, Dashboards, or Status Pages. Access Checkly account plan, entitlements and feature limits.
description: Set up, create, test and manage monitoring checks using the Checkly CLI. Use when working with Agentic Checks, API Checks, Browser Checks, URL Monitors, ICMP Monitors, Playwright Check Suites, Heartbeat Monitors, Alert Channels, Dashboards, or Status Pages. Access Checkly account plan, entitlements, feature limits, members, and pending invites.
allowed-tools: Bash(npx:checkly:*) Bash(npm:install:*)
metadata:
author: checkly
Expand Down
32 changes: 32 additions & 0 deletions packages/cli/src/commands/__tests__/account-members.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest'
import {
normalizeAccountMemberRole,
normalizeAccountMemberStatus,
normalizeAccountMemberType,
} from '../account/members'

describe('account members flag normalization', () => {
it('normalizes type values case-insensitively', () => {
expect(normalizeAccountMemberType('member')).toBe('member')
expect(normalizeAccountMemberType('INVITE')).toBe('invite')
expect(normalizeAccountMemberType(' Invite ')).toBe('invite')
})

it('normalizes role values case-insensitively', () => {
expect(normalizeAccountMemberRole('admin')).toBe('ADMIN')
expect(normalizeAccountMemberRole('Read_Run')).toBe('READ_RUN')
expect(normalizeAccountMemberRole(' read_only ')).toBe('READ_ONLY')
})

it('normalizes status values case-insensitively', () => {
expect(normalizeAccountMemberStatus('active')).toBe('ACTIVE')
expect(normalizeAccountMemberStatus('Pending')).toBe('PENDING')
expect(normalizeAccountMemberStatus(' expired ')).toBe('EXPIRED')
})

it('returns undefined for invalid filter values', () => {
expect(normalizeAccountMemberType('user')).toBeUndefined()
expect(normalizeAccountMemberRole('superadmin')).toBeUndefined()
expect(normalizeAccountMemberStatus('disabled')).toBeUndefined()
})
})
2 changes: 2 additions & 0 deletions packages/cli/src/commands/__tests__/command-metadata.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ import PwTest from '../pw-test'
import SyncPlaywright from '../sync-playwright'
import SkillsInstall from '../skills/install'
import AccountPlan from '../account/plan'
import AccountMembers from '../account/members'

const commands: Array<[string, typeof BaseCommand]> = [
['account members', AccountMembers],
['account plan', AccountPlan],
['checks list', ChecksList],
['checks get', ChecksGet],
Expand Down
165 changes: 165 additions & 0 deletions packages/cli/src/commands/account/members.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { Flags } from '@oclif/core'
import { AuthCommand } from '../authCommand'
import { outputFlag } from '../../helpers/flags'
import * as api from '../../rest/api'
import type { OutputFormat } from '../../formatters/render'
import type {
AccountMemberRole,
AccountMemberStatus,
AccountMemberType,
AccountMembersListParams,
} from '../../rest/account-members'
import {
formatAccountMembers,
formatCursorNavigationHints,
formatCursorPaginationInfo,
} from '../../formatters/account-members'

const accountMemberTypes = ['member', 'invite'] as const
const accountMemberRoles = ['OWNER', 'ADMIN', 'READ_WRITE', 'READ_RUN', 'READ_ONLY'] as const
const accountMemberStatuses = ['ACTIVE', 'PENDING', 'EXPIRED'] as const
const accountMemberRoleOptions = accountMemberRoles.map(role => role.toLowerCase())
const accountMemberStatusOptions = accountMemberStatuses.map(status => status.toLowerCase())

function isAccountMemberType (value: string): value is AccountMemberType {
return accountMemberTypes.includes(value as AccountMemberType)
}

function isAccountMemberRole (value: string): value is AccountMemberRole {
return accountMemberRoles.includes(value as AccountMemberRole)
}

function isAccountMemberStatus (value: string): value is AccountMemberStatus {
return accountMemberStatuses.includes(value as AccountMemberStatus)
}

export function normalizeAccountMemberType (value: string | undefined): AccountMemberType | undefined {
if (value === undefined) return undefined
const normalized = value.trim().toLowerCase()
return isAccountMemberType(normalized) ? normalized : undefined
}

export function normalizeAccountMemberRole (value: string | undefined): AccountMemberRole | undefined {
if (value === undefined) return undefined
const normalized = value.trim().toUpperCase()
return isAccountMemberRole(normalized) ? normalized : undefined
}

export function normalizeAccountMemberStatus (value: string | undefined): AccountMemberStatus | undefined {
if (value === undefined) return undefined
const normalized = value.trim().toUpperCase()
return isAccountMemberStatus(normalized) ? normalized : undefined
}

export default class AccountMembers extends AuthCommand {
static hidden = false
static readOnly = true
static idempotent = true
static description = 'List account members and pending invites.'

static flags = {
'search': Flags.string({
char: 's',
description: 'Search members and invites by name or email.',
}),
'type': Flags.string({
description: 'Filter by item type: member or invite.',
}),
'role': Flags.string({
description: `Filter by member or invite role: ${accountMemberRoleOptions.join(', ')}.`,
}),
'status': Flags.string({
description: `Filter by member or invite status: ${accountMemberStatusOptions.join(', ')}.`,
}),
'limit': Flags.integer({
char: 'l',
description: 'Number of account members to return (1-100). Enables cursor pagination.',
}),
'next-id': Flags.string({
description: 'Cursor for next page. Requires --limit.',
}),
'hide-id': Flags.boolean({
description: 'Hide member and invite IDs in table output.',
default: false,
}),
'output': outputFlag({ default: 'table' }),
}

async run (): Promise<void> {
const { flags } = await this.parse(AccountMembers)
this.style.outputFormat = flags.output
const limit = flags.limit

if (limit !== undefined && (limit < 1 || limit > 100)) {
this.error('--limit must be an integer between 1 and 100.')
}

if (flags['next-id'] && limit === undefined) {
this.error('Cannot use --next-id without --limit.')
}

const type = normalizeAccountMemberType(flags.type)
if (flags.type && !type) {
this.error(`Invalid --type "${flags.type}". Valid values: ${accountMemberTypes.join(', ')}.`)
}

const role = normalizeAccountMemberRole(flags.role)
if (flags.role && !role) {
this.error(`Invalid --role "${flags.role}". Valid values: ${accountMemberRoleOptions.join(', ')}.`)
}

const status = normalizeAccountMemberStatus(flags.status)
if (flags.status && !status) {
this.error(`Invalid --status "${flags.status}". Valid values: ${accountMemberStatusOptions.join(', ')}.`)
}

const params: AccountMembersListParams = {
search: flags.search,
type,
role,
status,
limit,
nextId: flags['next-id'],
}

try {
const { data } = await api.accountMembers.getAll(params)

if (flags.output === 'json') {
this.log(JSON.stringify(data, null, 2))
return
}

if (data.members.length === 0) {
this.log('No account members found.')
return
}

const fmt: OutputFormat = flags.output === 'md' ? 'md' : 'terminal'
if (fmt === 'md') {
this.log(formatAccountMembers(data.members, fmt, { showId: !flags['hide-id'] }))
return
}

const output = [
formatAccountMembers(data.members, fmt, { showId: !flags['hide-id'] }),
]

if (limit !== undefined) {
output.push('')
output.push(formatCursorPaginationInfo(data.length, data.nextId))

const navHints = formatCursorNavigationHints(data.nextId)
if (navHints) {
output.push('')
output.push(navHints.replace('<limit>', String(limit)))
}
}

this.log(output.join('\n'))
} catch (err: any) {
this.style.longError('Failed to list account members.', err)
process.exitCode = 1
}
}
}
Loading
Loading