Skip to content
Merged
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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,30 @@ optional address lookup stops after five seconds without retrying and respects
a shorter `--timeout`. If it fails
or times out, the command keeps the password on screen and exits successfully.

### Call usage and limits

When Calls access is enabled for a team, Polymorfa measures the calls its numbers host. Usage is measured, not charged.
Commands require `sessions:read`; a project token reads only its own project's
usage and needs `--project` or a project-bound profile. `usage gates` needs a
team API key, because gate state covers the whole team.

```shell
polymorfa usage summary
polymorfa usage summary --period 2026-09 --session support
polymorfa usage records --call 3EB0C431C26A1916E07A
polymorfa usage records --session support --meter call.duration --limit 50
polymorfa usage records --session support --cursor <cursor from the last page>
polymorfa usage gates --session support
```

`usage summary` totals one calendar month (UTC) by meter and by number, and
defaults to the current month; without `--project`, each number row also shows
its project. `usage records` lists records newest first with each record's
project, and prints the cursor for the next page. `usage gates` shows each gate's mode,
limit, usage this month and how often an action went over the limit in the last
30 days: in `record` mode nothing is refused, and in `enforce` mode an action
over the limit exits with code 4 (`gate_limit_reached`).

## Session call rejection and stable identities

`call reject` is a separate command for the Linked Device session route. It
Expand Down
9 changes: 4 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@modelcontextprotocol/server": "2.0.0",
"@napi-rs/keyring": "1.3.0",
"@oclif/core": "^4.13.5",
"@polymorfa/sdk": "0.1.0-dev.20260923124417",
"@polymorfa/sdk": "0.1.0-dev.20260923132258",
"jq-wasm": "3.0.0-jq-1.8.2",
"zod": "4.2.0"
},
Expand Down
40 changes: 40 additions & 0 deletions src/commands/usage/gates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {Flags} from '@oclif/core'

import {BaseCommand} from '../../base-command.js'
import {
createUsageResource,
listUsageGates,
renderUsageGates,
} from '../../services/usage.js'

export default class UsageGates extends BaseCommand {
static override description =
'List your usage gates with their mode, limit, usage this month and how often an action went over the limit in the last 30 days. In record mode a gate never refuses an action; in enforce mode an action over the limit fails with gate_limit_reached. Pass --session to include limits that apply per number. Gate state covers the whole team, so this needs a team API key; project API keys are refused. A limit that cannot be checked fails the action with 503 in either mode.'

static override examples = [
'<%= config.bin %> usage gates',
'<%= config.bin %> usage gates --session support --json',
]

static override flags = {
session: Flags.string({
description: 'Include limits that apply to one number (session name).',
}),
}

static override summary = 'List usage gates and their limits.'

async run(): Promise<void> {
const {flags} = await this.parse(UsageGates)
const result = await listUsageGates(
createUsageResource(
await this.developerEnvironment(flags, {inferOwner: true}),
),
{
...(flags.project === undefined ? {} : {project: flags.project}),
...(flags.session === undefined ? {} : {session: flags.session}),
},
)
this.writeValue(result, renderUsageGates(result.data), flags)
}
}
56 changes: 56 additions & 0 deletions src/commands/usage/records.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {Flags} from '@oclif/core'

import {BaseCommand} from '../../base-command.js'
import {
createUsageResource,
listUsageRecords,
renderUsageRecords,
} from '../../services/usage.js'

export default class UsageRecords extends BaseCommand {
static override description =
'List usage records, newest first. Filter by call, number, meter or calendar month. A call has one call.duration record, and an outgoing Cloud API call also a call.cloud_pulses record. Pass the printed cursor to read the next page.'

static override examples = [
'<%= config.bin %> usage records --call 3EB0C431C26A1916E07A',
'<%= config.bin %> usage records --session support --meter call.duration --limit 50 --json',
]

static override flags = {
call: Flags.string({description: 'One call, by call ID.'}),
cursor: Flags.string({description: 'Continue from a previous page.'}),
limit: Flags.integer({
description: 'Records per page, 1 to 100.',
max: 100,
min: 1,
}),
meter: Flags.string({
description: 'One meter, for example call.duration.',
}),
period: Flags.string({
description: 'Calendar month in UTC as YYYY-MM.',
}),
session: Flags.string({description: 'Limit to one number (session name).'}),
}

static override summary = 'List usage records.'

async run(): Promise<void> {
const {flags} = await this.parse(UsageRecords)
const result = await listUsageRecords(
createUsageResource(
await this.developerEnvironment(flags, {inferOwner: true}),
),
{
...(flags.call === undefined ? {} : {callId: flags.call}),
...(flags.cursor === undefined ? {} : {cursor: flags.cursor}),
...(flags.limit === undefined ? {} : {limit: flags.limit}),
...(flags.meter === undefined ? {} : {meter: flags.meter}),
...(flags.period === undefined ? {} : {period: flags.period}),
...(flags.project === undefined ? {} : {project: flags.project}),
...(flags.session === undefined ? {} : {session: flags.session}),
},
)
this.writeValue(result, renderUsageRecords(result.data), flags)
}
}
42 changes: 42 additions & 0 deletions src/commands/usage/summary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {Flags} from '@oclif/core'

import {BaseCommand} from '../../base-command.js'
import {
createUsageResource,
getUsageSummary,
renderUsageSummary,
} from '../../services/usage.js'

export default class UsageSummary extends BaseCommand {
static override description =
'Show metered usage for one calendar month (UTC), by meter and by number. Usage is measured, not charged: no credits are deducted for it. Requires sessions:read. A project token reads only its own project.'

static override examples = [
'<%= config.bin %> usage summary',
'<%= config.bin %> usage summary --period 2026-09 --session support --json',
]

static override flags = {
period: Flags.string({
description: 'Calendar month in UTC as YYYY-MM. Defaults to this month.',
}),
session: Flags.string({description: 'Limit to one number (session name).'}),
}

static override summary = 'Show metered usage for a month.'

async run(): Promise<void> {
const {flags} = await this.parse(UsageSummary)
const result = await getUsageSummary(
createUsageResource(
await this.developerEnvironment(flags, {inferOwner: true}),
),
{
...(flags.period === undefined ? {} : {period: flags.period}),
...(flags.project === undefined ? {} : {project: flags.project}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Send the resolved project UUID to usage APIs

When an organization-key user passes a project slug, developerEnvironment resolves it to a canonical UUID, but this reconstructs the projectId query from the original flags.project value. The API therefore receives the slug in a UUID field and rejects an otherwise supported --project <slug> invocation. The records and gates commands repeat the same pattern; retain the resolved environment and pass environment.context.projectId, as the call-analytics commands do.

Useful? React with 👍 / 👎.

...(flags.session === undefined ? {} : {session: flags.session}),
},
)
this.writeValue(result, renderUsageSummary(result.data), flags)
}
}
Loading