diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6dc69c..144b7c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,21 +27,5 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: npm - # Hybrid Link tier transitions need the companion SDK PR. Verify this PR - # against that immutable source revision until its package is published. - - run: npm ci --ignore-scripts - - uses: actions/checkout@v6 - with: - persist-credentials: false - repository: polymorfa/sdks - ref: 6271f4734476c195b4a84d1863561222a8e515d1 - path: .sdk-source - run: npm ci - working-directory: .sdk-source - - run: npm run build -w @polymorfa/sdk - working-directory: .sdk-source - - run: npm pack -w @polymorfa/sdk --pack-destination .. --ignore-scripts - working-directory: .sdk-source - - run: npm install --no-save --package-lock=false --ignore-scripts ./polymorfa-sdk-*.tgz - - run: npm run build - run: npm run verify diff --git a/README.md b/README.md index f83edf2..d83f6f0 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ prerelease in `package.json`. The version output includes both runtime components: ```text -polymorfa/0.1.0-dev.0 sdk/0.1.0-dev.20261001215108 +polymorfa/0.1.0-dev.0 sdk/0.1.0-dev.20261003052138 ``` The SDK component reports the version built into the installed SDK. Dev @@ -1573,8 +1573,20 @@ polymorfa widget view --project project_123 polymorfa widget update --project project_123 --input widget-patch.json polymorfa session stop session_1 session_2 --project project_123 polymorfa session delete session_1 session_2 --project project_123 --yes +polymorfa session capabilities support-line ``` +`session capabilities` shows the WhatsApp configuration gates and limits for +one number, as of its last configuration sync. An enabled gate does not prove +that the feature is usable in a particular client or environment, or that +WhatsApp will accept an operation. Each line +is the capability key, its value (`enabled`, `off`, a limit, or `unknown`), and +its source: `server` (WhatsApp sent the setting), `client_default` (WhatsApp +sent none, so its default applies) or `account_type` (the capability does not +apply to this kind of account, for example a business-only feature on a +personal number). Before the number's first sync every value is `unknown`. +It is a beta: the team must be enrolled, and it needs `sessions:read`. + Key and project-token lists contain metadata only; neither command returns raw bearer material. The API's reported API-key scope mask is retained in JSON but must not be treated as proof of live authorization. `key revoke` requires @@ -2304,17 +2316,15 @@ Business Platform > Disconnect Account. An ineligible Number or pair fails with `hybrid_transition_ineligible` (exit code 4) and the API's reason in the message. Merging requires Hybrid Link access, which the API enforces. -These commands are tested against the companion SDK source, which pins API -contract `fcf7b21015dd185e987b5aa3580e8af5f4c7f1a4`. Update the registry SDK -dependency after that SDK is published and before releasing this CLI. +These commands use published SDK `0.1.0-dev.20261003052138`, which pins API +contract `ee885ec4bf62af36b9b85df4310c505f38a224db`. ### Hybrid Link controls -These additions are under development and are not available in the published -CLI. They are tested against the companion Hybrid Link SDK source using API -contract `d5245f76d5d6bc7b9af439571bd1f157185b1b87`. The registry SDK dependency -must be updated after that SDK artifact is published before releasing this CLI. -The companion SDK defaults native requests to API revision `2026-09-22`. An +These additions require a CLI release containing the Hybrid Link commands. +They use published SDK `0.1.0-dev.20261003052138`, which pins API contract +`ee885ec4bf62af36b9b85df4310c505f38a224db`. The SDK defaults native requests +to API revision `2026-09-22`. An explicit `--api-version` or saved-profile version remains explicit; retired versions fail without an automatic upgrade or resend. diff --git a/package-lock.json b/package-lock.json index 0ace622..a1a2b55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@modelcontextprotocol/server": "2.0.0", "@napi-rs/keyring": "1.3.0", "@oclif/core": "^4.13.5", - "@polymorfa/sdk": "0.1.0-dev.20261002023214", + "@polymorfa/sdk": "0.1.0-dev.20261003052138", "jq-wasm": "3.0.0-jq-1.8.2", "zod": "4.2.0" }, @@ -997,9 +997,10 @@ } }, "node_modules/@polymorfa/sdk": { - "version": "0.1.0-dev.20261002023214", - "resolved": "https://registry.npmjs.org/@polymorfa/sdk/-/sdk-0.1.0-dev.20261002023214.tgz", - "integrity": "sha512-ElS5mQaYHAoF53o77MZqD8ETXbR6qNNNgYNcEXfOBwQFw2reYqoIRtlS0QOKcTwTVIdaQbZtN7nBLyNymhtU0A==", + "version": "0.1.0-dev.20261003052138", + "resolved": "https://registry.npmjs.org/@polymorfa/sdk/-/sdk-0.1.0-dev.20261003052138.tgz", + "integrity": "sha512-NJLO6WcbDgCzmEiFiuQhSDdrF9cCuuVtKVH1CgmyH27yFQ/mFW62TK/p2lOHRVH5p45eyzX3r/7iCqSF2cOECA==", + "license": "MIT", "engines": { "node": ">=20" } diff --git a/package.json b/package.json index e36bc80..6d095e4 100644 --- a/package.json +++ b/package.json @@ -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.20261002023214", + "@polymorfa/sdk": "0.1.0-dev.20261003052138", "jq-wasm": "3.0.0-jq-1.8.2", "zod": "4.2.0" }, diff --git a/src/commands/session/capabilities.ts b/src/commands/session/capabilities.ts new file mode 100644 index 0000000..5711fc4 --- /dev/null +++ b/src/commands/session/capabilities.ts @@ -0,0 +1,33 @@ +import {Args} from '@oclif/core' + +import {BaseCommand} from '../../base-command.js' +import { + getSessionCapabilities, + renderSessionCapabilities, +} from '../../services/platform-management.js' + +export default class SessionCapabilitiesCommand extends BaseCommand { + static override args = { + session: Args.string({ + description: 'Session ID or name.', + required: true, + }), + } + + static override description = + "Show the WhatsApp features and limits WhatsApp has enabled for a number, as of the number's last configuration sync. Beta: the team must be enrolled." + + static override examples = [ + '<%= config.bin %> session capabilities support-line', + '<%= config.bin %> session capabilities support-line --json', + ] + + async run(): Promise { + const {args, flags} = await this.parse(SessionCapabilitiesCommand) + const result = await getSessionCapabilities( + await this.platformClient(flags), + args.session, + ) + this.writeValue(result, renderSessionCapabilities(result.data), flags) + } +} diff --git a/src/services/platform-management.ts b/src/services/platform-management.ts index 330cc06..af2b706 100644 --- a/src/services/platform-management.ts +++ b/src/services/platform-management.ts @@ -17,6 +17,7 @@ import { type SessionBatchRequest, type SessionBatchStopResult, type PlatformSession, + type SessionCapabilities, type UpdateQuickLinkSettingsInput, type OrganizationQuickLinkSettings, type ProjectQuickLinkSettings, @@ -188,6 +189,38 @@ export async function stopSessions( return result(response.data.data, response.metadata) } +/** WhatsApp features and limits WhatsApp enabled for a number (beta). */ +export async function getSessionCapabilities( + client: ClientInstance<'organization'>, + session: string, +): Promise> { + const response = await client.sessions.getCapabilities(session) + return result(response.data.data, response.metadata) +} + +export function renderSessionCapabilities( + capabilities: SessionCapabilities, +): string { + const header = [ + `Session\t${capabilities.session}`, + `Status\t${capabilities.status}`, + `Synced\t${capabilities.syncedAt ?? 'never'}`, + `Account\t${capabilities.accountType ?? 'unknown'}`, + ] + const rows = capabilities.capabilities.map((capability) => { + const value = + capability.value === null + ? 'unknown' + : typeof capability.value === 'boolean' + ? capability.value + ? 'enabled' + : 'off' + : `${capability.value}${capability.unit === null ? '' : ` ${capability.unit}`}` + return `${capability.key}\t${value}\t${capability.source ?? '-'}` + }) + return [...header, '', ...rows].join('\n') +} + export async function deleteSessions( client: ClientInstance<'organization'>, sessionIds: readonly string[], diff --git a/test/commands/platform-management.test.ts b/test/commands/platform-management.test.ts index 735ef2f..2eafd47 100644 --- a/test/commands/platform-management.test.ts +++ b/test/commands/platform-management.test.ts @@ -487,6 +487,60 @@ describe('Platform management commands', () => { expect(server.requests).toHaveLength(0) }) + test("reads a number's WhatsApp capabilities", async () => { + server = await startHttpServer({ + body: { + data: { + session: 'support/line', + projectId: 'project_1', + status: 'synced', + syncedAt: '2026-09-24T10:00:00.000Z', + checkedAt: '2026-09-24T11:00:00.000Z', + accountType: 'personal', + capabilities: [ + { + key: 'channels', + kind: 'feature', + unit: null, + value: true, + source: 'server', + }, + { + key: 'messageEdit.windowSeconds', + kind: 'limit', + unit: 'seconds', + value: 1200, + source: 'client_default', + }, + { + key: 'metaAi', + kind: 'feature', + unit: null, + value: null, + source: null, + }, + ], + }, + }, + }) + + const result = await platformEnvironment(() => + runCommand('session capabilities support/line'), + ) + + expect(result.error, result.stderr).toBeUndefined() + expect(server.requests[0]).toMatchObject({ + method: 'GET', + url: '/platform/sessions/support%2Fline/capabilities', + }) + expect(result.stdout).toContain('Status\tsynced') + expect(result.stdout).toContain('channels\tenabled\tserver') + expect(result.stdout).toContain( + 'messageEdit.windowSeconds\t1200 seconds\tclient_default', + ) + expect(result.stdout).toContain('metaAi\tunknown\t-') + }) + test('requests asynchronous stops for a bounded session batch', async () => { server = await startHttpServer({body: {data: {stopping: 1}}})