diff --git a/CHANGELOG.md b/CHANGELOG.md index 097e764..0bb309c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,20 @@ The format is inspired by Keep a Changelog and this project follows SemVer for ` ## [Unreleased] +## [0.15.0] - 2026-09-24 + +### Added +- `organization.devices.getDevices` now lists the hub's device filters: `name` (case-insensitive substring), `status`, `effective_status`, `model_id`, `connection_method` (`native`/`c2c`/`edge`) and `connector_id`. Pass several `status` / `effective_status` values as a comma-separated string, e.g. `--query-json '{"status":"online,error"}'`. +- Device responses from `getDevices`, `getDevice` and `claimDevice` document the new `connector: { type, id, name }` block. + ### Changed +- The hub now rejects any other `getDevices` query parameter, and invalid filter values, with `422`. +- Device pagination guidance now names `next_page` only; the API docs were corrected from `has_next_page`. - `organization.devices.getHistories` catalog notes and skill docs now document `page`/`per_page` pagination (422 outside 1..1000, not clamped), the 31-day inclusive `from`/`to` window (422 when exceeded or reversed), the stable newest-first ordering, and guidance to prefer narrow windows and filters over deep page walks. +- Bumped package metadata and lockfile to `0.15.0`. ### Upgrade notes +- Run `xyte-cli skills refresh` after upgrading so installed skill bundles pick up the new filters. - Existing workspaces that already installed the shipped skill bundle should run `xyte-cli skills refresh` after upgrading — the previous `getHistories` example used a wide `from=0` window that now returns 422 against production. ## [0.14.0] - 2026-08-03 diff --git a/README.md b/README.md index eea2ff3..f730e03 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Key params: - `--tenant ` - `--output-mode envelope` for contract output - `--strict-json` for machine parsing -- For `organization.devices.getDevices`, increment `page` until `next_page` is null/absent or the response reports `has_next_page=false`. +- For `organization.devices.getDevices`, increment `page` until `next_page` is null/absent. Filters: `space_id`, `name`, `status`, `effective_status`, `model_id`, `connection_method`, `connector_id` (comma-separated for multiple statuses); any other query param returns 422. ### 2b) Device or space notes diff --git a/docs/commands.md b/docs/commands.md index b8ae35a..06d277d 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -144,7 +144,9 @@ xyte-cli api endpoints describe organization.groups.addUsers xyte-cli api endpoints describe partner.organizations.createOrganization xyte-cli api call organization.devices.getDevices --tenant --query-json '{"page":1,"per_page":100}' xyte-cli api call organization.devices.getDevices --tenant --query-json '{"page":1,"per_page":100}' --output-mode envelope --strict-json [--note ] -# For complete device inventories, increment page until the envelope response reports no continuation (`next_page` is null/absent, or `has_next_page=false` on tenants that return that field). +xyte-cli api call organization.devices.getDevices --tenant --query-json '{"name":"lobby","status":"offline,error","connection_method":"c2c"}' +# Filters: space_id, name, status, effective_status, model_id, connection_method, connector_id. Multi-value status/effective_status is a comma-separated string. Any other query param returns 422. +# For complete device inventories, increment page until the envelope response reports no continuation (`next_page` is null/absent). xyte-cli api call organization.devices.mergeDevice --tenant --path-json '{"device_id":""}' --body-json '{"with_device_ids":[""]}' --note "approved merge" xyte-cli api call organization.devices.splitDevice --tenant --path-json '{"device_id":""}' --body-json '{"shadow_device_id":""}' --note "approved split" xyte-cli api call organization.notes.getDeviceNotes --tenant --path-json '{"device_id":""}' --query-json '{"page":1,"per_page":100}' diff --git a/docs/flows/agent-ops.md b/docs/flows/agent-ops.md index a558c12..a84fd0e 100644 --- a/docs/flows/agent-ops.md +++ b/docs/flows/agent-ops.md @@ -254,7 +254,7 @@ xyte-cli api call organization.commands.getCommands \ ```bash mkdir -p ./artifacts ./reports xyte-cli api call organization.devices.getDevices --tenant --query-json '{"space_id":"","page":1,"per_page":100}' --output-mode envelope --output json > ./artifacts/source-devices.page-1.json -# Repeat page 2, 3, ... until the response reports no continuation (`next_page` is null/absent, or `has_next_page=false` on tenants that return that field), then combine items into ./artifacts/source-devices.json. +# Repeat page 2, 3, ... until the response reports no continuation (`next_page` is null/absent), then combine items into ./artifacts/source-devices.json. xyte-cli api call organization.spaces.getSpaces --tenant --query path_includes= --output json > ./artifacts/target-spaces.json xyte-cli util match --tenant --source ./artifacts/source-devices.json --target ./artifacts/target-spaces.json --source-field name --target-field name --out ./artifacts/device-moves.csv xyte-cli ops report generate --tenant --input ./artifacts/device-moves.csv.summary.json --out ./reports/device-migration-pre.md --render markdown @@ -264,7 +264,7 @@ xyte-cli ops inspect fleet --tenant --output json --out ./artifacts/ ``` - Expected artifacts: - - complete source device inventory JSON, collected across all `getDevices` pages until no `next_page` / `has_next_page` continuation remains, and target space inventory JSON. + - complete source device inventory JSON, collected across all `getDevices` pages until no `next_page` continuation remains, and target space inventory JSON. - deterministic move CSV at `./artifacts/device-moves.csv` plus summary JSON sidecar. - pre-migration markdown report at `./reports/device-migration-pre.md`. - dry-run and apply NDJSON row reports for move execution. diff --git a/package-lock.json b/package-lock.json index 7b5dd0d..4fe6025 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@xyteai/cli", - "version": "0.14.0", + "version": "0.15.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@xyteai/cli", - "version": "0.14.0", + "version": "0.15.0", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/package.json b/package.json index ec83d04..116d5ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xyteai/cli", - "version": "0.14.0", + "version": "0.15.0", "description": "Xyte CLI, TUI, skills, and headless contracts for zero-friction AI operations", "license": "Apache-2.0", "repository": { diff --git a/skills/xyte-cli/data/public-endpoints.json b/skills/xyte-cli/data/public-endpoints.json index ae833b1..b79c9b0 100644 --- a/skills/xyte-cli/data/public-endpoints.json +++ b/skills/xyte-cli/data/public-endpoints.json @@ -105,7 +105,8 @@ "Docs page: https://docs.xyte.io/reference/claim-device", "Docs category: /branches/2.0/categories/reference/API Access", "Docs parent: /branches/2.0/reference/devices-2", - "Response includes effective_status (nullable string): one of ok, warning, error, offline, disconnected, never_seen. Computed device status reflecting incident priority, connectivity, telemetry-seen state, and raw device status." + "Response includes effective_status (nullable string): one of ok, warning, error, offline, disconnected, never_seen. Computed device status reflecting incident priority, connectivity, telemetry-seen state, and raw device status.", + "Response includes connector: { type: native|c2c|edge, id: uuid|null, name: string|null }." ] }, { @@ -296,7 +297,8 @@ "Docs category: /branches/2.0/categories/reference/API Access", "Docs parent: /branches/2.0/reference/devices-2", "Docs raw path params: device_id", - "Response includes effective_status (nullable string): one of ok, warning, error, offline, disconnected, never_seen. Computed device status reflecting incident priority, connectivity, telemetry-seen state, and raw device status." + "Response includes effective_status (nullable string): one of ok, warning, error, offline, disconnected, never_seen. Computed device status reflecting incident priority, connectivity, telemetry-seen state, and raw device status.", + "Response includes connector: { type: native|c2c|edge, id: uuid|null, name: string|null }." ] }, { @@ -312,7 +314,13 @@ "queryParams": [ "page", "per_page", - "space_id" + "space_id", + "name", + "status", + "effective_status", + "model_id", + "connection_method", + "connector_id" ], "authScope": "organization", "bodyType": "none", @@ -322,8 +330,11 @@ "Docs page: https://docs.xyte.io/reference/get-devices", "Docs category: /branches/2.0/categories/reference/API Access", "Docs parent: /branches/2.0/reference/devices-2", - "Paginated response includes items plus a continuation field. Shared docs describe has_next_page; live Verve/Playground responses on 2026-07-06 returned next_page. Handle either field. page starts at 1; per_page defaults to 100 and is capped at 100.", - "Response includes effective_status (nullable string): one of ok, warning, error, offline, disconnected, never_seen. Computed device status reflecting incident priority, connectivity, telemetry-seen state, and raw device status." + "Paginated response includes items plus next_page (null/absent on the last page). Docs previously described has_next_page and were corrected to next_page. page starts at 1; per_page defaults to 100 and values above 100 are rejected.", + "Filters combine with AND: name is a case-insensitive substring match; status is one or more of online, offline, unavailable, error; effective_status is one or more of ok, warning, error, offline, disconnected, never_seen; model_id is a device model UUID; connection_method is one of native, c2c, edge; connector_id is the UUID of a C2C integration or edge (a device's connector.id). Pass multi-value status/effective_status as a comma-separated string, e.g. {\"status\":\"online,error\"}. Status filters can trail the live status by up to a minute, so a returned device's status may not match the filter yet.", + "Any other query parameter returns 422 Unsupported query parameter(s). Invalid enum values or non-UUID model_id/connector_id return 422.", + "Response includes effective_status (nullable string): one of ok, warning, error, offline, disconnected, never_seen. Computed device status reflecting incident priority, connectivity, telemetry-seen state, and raw device status.", + "Each device includes connector: { type: native|c2c|edge, id: uuid|null, name: string|null }." ] }, { diff --git a/skills/xyte-cli/references/endpoints.md b/skills/xyte-cli/references/endpoints.md index efc1d0e..b0065fa 100644 --- a/skills/xyte-cli/references/endpoints.md +++ b/skills/xyte-cli/references/endpoints.md @@ -42,7 +42,7 @@ Derived from the bundled public endpoint spec. | Endpoint Key | Query Fields | Pagination Fields | Notes | | --- | --- | --- | --- | | `organization.spaces.getSpaces` | `id`, `name`, `parent_id`, `space_type`, `created_before`, `created_after`, `path_includes` | none | Main listing endpoint with server-side filtering | -| `organization.devices.getDevices` | `page`, `per_page`, `space_id` | `page`, `per_page` | Filter devices by one space; shared docs mention `has_next_page`, live Verve/Playground responses returned `next_page`; handle either continuation field | +| `organization.devices.getDevices` | `page`, `per_page`, `space_id`, `name`, `status`, `effective_status`, `model_id`, `connection_method`, `connector_id` | `page`, `per_page` | Filters combine with AND; `name` is a case-insensitive substring; `status` (online/offline/unavailable/error) and `effective_status` (ok/warning/error/offline/disconnected/never_seen) take a comma-separated list and can trail the live `status` by up to a minute; `connection_method` is native/c2c/edge; `model_id`/`connector_id` are UUIDs (`connector_id` = a device's `connector.id`). Unknown params and invalid values return 422. Continue while `next_page` is set. Each device carries `connector: {type, id, name}` | | `organization.devices.getHistories` | `status`, `from`, `to`, `device_id`, `space_id`, `name`, `page`, `per_page` | `page`, `per_page` | Filtered history lookup. `from`/`to` window must not exceed 31 days (422 otherwise); defaults: `from` = 1 week ago, `to` = now, applied independently — send both. `status` filters the device's *current* status. | | `organization.commands.getCommands` | `status`, `page`, `per_page` | `page`, `per_page` | Command history pagination and status filter | | `organization.incidents.getIncidents` | `from`, `to`, `status`, `priority`, `title`, `description`, `issue`, `device_model`, `partner_name`, `sub_model`, `space_id`, `page`, `per_page` | `page`, `per_page` | Incident filtering matrix. Use integer `from` and `to`; for reliable active-incident fetches use both (`from=0`, `to=`). | diff --git a/skills/xyte-cli/references/flow-recipes.md b/skills/xyte-cli/references/flow-recipes.md index 5ce9498..7b342c0 100644 --- a/skills/xyte-cli/references/flow-recipes.md +++ b/skills/xyte-cli/references/flow-recipes.md @@ -118,7 +118,7 @@ xyte-cli api call organization.commands.getCommands \ ```bash mkdir -p ./artifacts ./reports xyte-cli api call organization.devices.getDevices --tenant --query-json '{"space_id":"","page":1,"per_page":100}' --output-mode envelope --output json > ./artifacts/source-devices.page-1.json -# Repeat page 2, 3, ... until the response reports no continuation (`next_page` is null/absent, or `has_next_page=false` on tenants that return that field), then combine items into ./artifacts/source-devices.json. +# Repeat page 2, 3, ... until the response reports no continuation (`next_page` is null/absent), then combine items into ./artifacts/source-devices.json. xyte-cli api call organization.spaces.getSpaces --tenant --query path_includes= --output json > ./artifacts/target-spaces.json xyte-cli util match --tenant --source ./artifacts/source-devices.json --target ./artifacts/target-spaces.json --source-field name --target-field name --out ./artifacts/device-moves.csv xyte-cli ops report generate --tenant --input ./artifacts/device-moves.csv.summary.json --out ./reports/device-migration-pre.md --render markdown diff --git a/src/api-catalog/public-endpoints.json b/src/api-catalog/public-endpoints.json index ae833b1..b79c9b0 100644 --- a/src/api-catalog/public-endpoints.json +++ b/src/api-catalog/public-endpoints.json @@ -105,7 +105,8 @@ "Docs page: https://docs.xyte.io/reference/claim-device", "Docs category: /branches/2.0/categories/reference/API Access", "Docs parent: /branches/2.0/reference/devices-2", - "Response includes effective_status (nullable string): one of ok, warning, error, offline, disconnected, never_seen. Computed device status reflecting incident priority, connectivity, telemetry-seen state, and raw device status." + "Response includes effective_status (nullable string): one of ok, warning, error, offline, disconnected, never_seen. Computed device status reflecting incident priority, connectivity, telemetry-seen state, and raw device status.", + "Response includes connector: { type: native|c2c|edge, id: uuid|null, name: string|null }." ] }, { @@ -296,7 +297,8 @@ "Docs category: /branches/2.0/categories/reference/API Access", "Docs parent: /branches/2.0/reference/devices-2", "Docs raw path params: device_id", - "Response includes effective_status (nullable string): one of ok, warning, error, offline, disconnected, never_seen. Computed device status reflecting incident priority, connectivity, telemetry-seen state, and raw device status." + "Response includes effective_status (nullable string): one of ok, warning, error, offline, disconnected, never_seen. Computed device status reflecting incident priority, connectivity, telemetry-seen state, and raw device status.", + "Response includes connector: { type: native|c2c|edge, id: uuid|null, name: string|null }." ] }, { @@ -312,7 +314,13 @@ "queryParams": [ "page", "per_page", - "space_id" + "space_id", + "name", + "status", + "effective_status", + "model_id", + "connection_method", + "connector_id" ], "authScope": "organization", "bodyType": "none", @@ -322,8 +330,11 @@ "Docs page: https://docs.xyte.io/reference/get-devices", "Docs category: /branches/2.0/categories/reference/API Access", "Docs parent: /branches/2.0/reference/devices-2", - "Paginated response includes items plus a continuation field. Shared docs describe has_next_page; live Verve/Playground responses on 2026-07-06 returned next_page. Handle either field. page starts at 1; per_page defaults to 100 and is capped at 100.", - "Response includes effective_status (nullable string): one of ok, warning, error, offline, disconnected, never_seen. Computed device status reflecting incident priority, connectivity, telemetry-seen state, and raw device status." + "Paginated response includes items plus next_page (null/absent on the last page). Docs previously described has_next_page and were corrected to next_page. page starts at 1; per_page defaults to 100 and values above 100 are rejected.", + "Filters combine with AND: name is a case-insensitive substring match; status is one or more of online, offline, unavailable, error; effective_status is one or more of ok, warning, error, offline, disconnected, never_seen; model_id is a device model UUID; connection_method is one of native, c2c, edge; connector_id is the UUID of a C2C integration or edge (a device's connector.id). Pass multi-value status/effective_status as a comma-separated string, e.g. {\"status\":\"online,error\"}. Status filters can trail the live status by up to a minute, so a returned device's status may not match the filter yet.", + "Any other query parameter returns 422 Unsupported query parameter(s). Invalid enum values or non-UUID model_id/connector_id return 422.", + "Response includes effective_status (nullable string): one of ok, warning, error, offline, disconnected, never_seen. Computed device status reflecting incident priority, connectivity, telemetry-seen state, and raw device status.", + "Each device includes connector: { type: native|c2c|edge, id: uuid|null, name: string|null }." ] }, { diff --git a/src/workflows/flow-catalog.ts b/src/workflows/flow-catalog.ts index 1b073e7..baf3a77 100644 --- a/src/workflows/flow-catalog.ts +++ b/src/workflows/flow-catalog.ts @@ -668,7 +668,7 @@ const FLOWS: Record = { recipeCommands: [ 'mkdir -p ./artifacts ./reports', `xyte-cli api call organization.devices.getDevices --tenant --query-json '{"space_id":"","page":1,"per_page":100}' --output-mode envelope --output json > ./artifacts/source-devices.page-1.json`, - '# Repeat page 2, 3, ... until the response reports no continuation (`next_page` is null/absent, or `has_next_page=false` on tenants that return that field), then combine items into ./artifacts/source-devices.json.', + '# Repeat page 2, 3, ... until the response reports no continuation (`next_page` is null/absent), then combine items into ./artifacts/source-devices.json.', 'xyte-cli api call organization.spaces.getSpaces --tenant --query path_includes= --output json > ./artifacts/target-spaces.json', 'xyte-cli util match --tenant --source ./artifacts/source-devices.json --target ./artifacts/target-spaces.json --source-field name --target-field name --out ./artifacts/device-moves.csv', 'xyte-cli ops report generate --tenant --input ./artifacts/device-moves.csv.summary.json --out ./reports/device-migration-pre.md --render markdown', diff --git a/tests/cli.test.ts b/tests/cli.test.ts index adcc746..44bb4c0 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -2158,6 +2158,52 @@ describe('cli integration', () => { expect(parsed.response.status).toBe(200); }); + it('serializes getDevices filters as scalar query params with comma lists', async () => { + const profileStore = new MemoryProfileStore(); + await profileStore.upsertTenant({ id: 'acme' }); + await profileStore.setActiveTenant('acme'); + const secretStore = new MemorySecretStore(); + await secretStore.setSecret('acme', 'xyte-org', 'org-key'); + const stdout = { write: vi.fn() }; + const stderr = { write: vi.fn() }; + const program = createCli({ profileStore, secretStore, stdout, stderr }); + + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ items: [], next_page: null }), { + status: 200, + headers: { 'content-type': 'application/json' } + }) + ); + vi.stubGlobal('fetch', fetchMock); + + await program.parseAsync([ + 'node', + 'xyte-cli', + 'api', + 'call', + 'organization.devices.getDevices', + '--tenant', + 'acme', + '--query-json', + '{"name":"lobby","status":"online,error","effective_status":"warning","connection_method":"c2c","connector_id":"5b1f0c2e-7a4d-4c1e-9f3a-2d6b8e1c0a11","page":1,"per_page":100}' + ]); + + const [requestUrl, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + const url = new URL(String(requestUrl)); + expect(init.method).toBe('GET'); + expect(init.body).toBeUndefined(); + expect(url.pathname).toBe('/core/v1/organization/devices'); + expect(Object.fromEntries(url.searchParams)).toEqual({ + name: 'lobby', + status: 'online,error', + effective_status: 'warning', + connection_method: 'c2c', + connector_id: '5b1f0c2e-7a4d-4c1e-9f3a-2d6b8e1c0a11', + page: '1', + per_page: '100' + }); + }); + it('rejects invalid output-mode values for api call', async () => { const profileStore = new MemoryProfileStore(); await profileStore.upsertTenant({ id: 'acme' }); diff --git a/tests/endpoints.test.ts b/tests/endpoints.test.ts index 1c3b245..f013906 100644 --- a/tests/endpoints.test.ts +++ b/tests/endpoints.test.ts @@ -143,9 +143,22 @@ describe('public endpoint catalog', () => { it('includes documented filter params for key read endpoints', () => { const getDevices = endpoints.find((item) => item.key === 'organization.devices.getDevices'); - expect(getDevices?.queryParams).toEqual(['page', 'per_page', 'space_id']); - expect(getDevices?.notes?.join(' ')).toContain('has_next_page'); + expect(getDevices?.queryParams).toEqual([ + 'page', + 'per_page', + 'space_id', + 'name', + 'status', + 'effective_status', + 'model_id', + 'connection_method', + 'connector_id' + ]); expect(getDevices?.notes?.join(' ')).toContain('next_page'); + expect(getDevices?.notes?.join(' ')).toContain('Unsupported query parameter'); + for (const key of ['organization.devices.getDevices', 'organization.devices.getDevice', 'organization.devices.claimDevice']) { + expect(endpoints.find((item) => item.key === key)?.notes?.join(' ')).toContain('connector: { type: native|c2c|edge'); + } const getHistories = endpoints.find((item) => item.key === 'organization.devices.getHistories'); expect(getHistories?.queryParams).toEqual([ diff --git a/tests/skills-edge-params-guidance.test.ts b/tests/skills-edge-params-guidance.test.ts index d17cfc8..8579d94 100644 --- a/tests/skills-edge-params-guidance.test.ts +++ b/tests/skills-edge-params-guidance.test.ts @@ -46,7 +46,8 @@ describe('edge custom params docs and skill guidance', () => { for (const content of [readme, commands]) { expect(content).toContain('organization.devices.getDevices'); expect(content).toContain('"page":1,"per_page":100'); - expect(content).toContain('has_next_page=false'); + expect(content).toContain('`next_page` is null/absent'); + expect(content).toContain('connector_id'); } });