@deepdockai/cli 0.0.0-stage → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,163 @@
1
+ """Minimal Deepdock /v1 client. Depends on httpx only (Python 3.9+).
2
+
3
+ It does the four things the OpenAPI document cannot tell you: the bearer header, the envelope
4
+ unwrap, the Idempotency-Key header with replay detection, and a typed error carrying the code.
5
+ Everything action-specific (paths, fields, methods) comes from the live OpenAPI document.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field, replace
11
+ from typing import Any, Dict, Iterator, List, Optional, Tuple
12
+ from urllib.parse import quote
13
+
14
+ import httpx
15
+
16
+
17
+ class DeepdockError(Exception):
18
+ """Deepdock answered `{ "error": { "code", ... } }`. Switch on `code`, never on `message`."""
19
+
20
+ def __init__(self, status: int, code: str, message: str, request_id: Optional[str] = None,
21
+ systems: Optional[List[str]] = None):
22
+ super().__init__(message)
23
+ self.status = status
24
+ self.code = code
25
+ self.request_id = request_id
26
+ self.systems = systems
27
+
28
+
29
+ class DeepdockTransportError(Exception):
30
+ """No answer, or not Deepdock's envelope (timeout, network, a proxy page): a write may still have landed."""
31
+
32
+
33
+ @dataclass
34
+ class Page:
35
+ items: List[Dict[str, Any]]
36
+ next_cursor: Optional[str]
37
+
38
+
39
+ @dataclass
40
+ class WriteResult:
41
+ data: Dict[str, Any]
42
+ replayed: bool
43
+
44
+
45
+ class Deepdock:
46
+ def __init__(self, api_key: str, base: str = "https://api.deepdock.ai", timeout: float = 30.0,
47
+ client: Optional[httpx.Client] = None):
48
+ # 30 s is above Deepdock's 20 s device wait, so device_offline arrives before a client timeout.
49
+ self.c = client or httpx.Client(base_url=base.rstrip("/"), timeout=timeout)
50
+ self.c.headers["Authorization"] = f"Bearer {api_key}"
51
+
52
+ def _send(self, method: str, path: str, **kw: Any) -> Tuple[Any, httpx.Headers]:
53
+ try:
54
+ r = self.c.request(method, path, **kw)
55
+ except httpx.HTTPError as e: # timeouts and network errors
56
+ raise DeepdockTransportError(f"no answer from Deepdock: {type(e).__name__}") from e
57
+ try:
58
+ body = r.json()
59
+ except ValueError:
60
+ raise DeepdockTransportError(f"non-JSON answer with status {r.status_code}")
61
+ if isinstance(body, dict) and "error" in body:
62
+ e = body["error"]
63
+ raise DeepdockError(r.status_code, e["code"], e["message"], e.get("requestId"), e.get("systems"))
64
+ if not isinstance(body, dict) or "data" not in body:
65
+ raise DeepdockTransportError(f"unexpected body with status {r.status_code}")
66
+ return body["data"], r.headers
67
+
68
+ @staticmethod
69
+ def _action_path(device_id: str, system: str, action: str) -> str:
70
+ return f"/v1/devices/{quote(device_id, safe='')}/systems/{quote(system, safe='')}/actions/{quote(action, safe='')}"
71
+
72
+ def read(self, device_id: str, system: str, action: str, params: Dict[str, str],
73
+ limit: Optional[int] = None, cursor: Optional[str] = None) -> Page:
74
+ """Runs a read action. Pass `next_cursor` back as `cursor` for the next page."""
75
+ query: Dict[str, str] = dict(params)
76
+ if limit is not None:
77
+ query["limit"] = str(limit)
78
+ if cursor is not None:
79
+ query["cursor"] = cursor
80
+ data, _ = self._send("GET", self._action_path(device_id, system, action), params=query)
81
+ return Page(items=data["items"], next_cursor=data.get("nextCursor"))
82
+
83
+ def read_all(self, device_id: str, system: str, action: str, params: Dict[str, str],
84
+ limit: Optional[int] = None) -> Iterator[Dict[str, Any]]:
85
+ """Reads every page of a read action, in order."""
86
+ cursor: Optional[str] = None
87
+ while True:
88
+ page = self.read(device_id, system, action, params, limit=limit, cursor=cursor)
89
+ yield from page.items
90
+ if page.next_cursor is None:
91
+ return
92
+ cursor = page.next_cursor
93
+
94
+ def write(self, device_id: str, system: str, action: str, method: str, idempotency_key: str,
95
+ body: Dict[str, str]) -> WriteResult:
96
+ """Runs a write action. `method` is POST or PATCH from the action index; `body` values are all strings."""
97
+ data, headers = self._send(method, self._action_path(device_id, system, action), json=body,
98
+ headers={"Idempotency-Key": idempotency_key})
99
+ return WriteResult(data=data, replayed=headers.get("Idempotent-Replayed") == "true")
100
+
101
+ def systems(self, device_id: str) -> Dict[str, Any]:
102
+ """The device's systems and each action's state. A live call: can answer device_offline."""
103
+ data, _ = self._send("GET", f"/v1/devices/{quote(device_id, safe='')}/systems")
104
+ return data
105
+
106
+ def pair(self, code: str, label: Optional[str] = None) -> Dict[str, Any]:
107
+ """Claims a device by the pairing code it shows. Keep the returned `id`."""
108
+ body: Dict[str, str] = {"code": code}
109
+ if label is not None:
110
+ body["label"] = label
111
+ data, _ = self._send("POST", "/v1/devices", json=body)
112
+ return data
113
+
114
+ def release(self, device_id: str) -> None:
115
+ self._send("DELETE", f"/v1/devices/{quote(device_id, safe='')}")
116
+
117
+
118
+ # --------------------------------------------------------------------------------------------------
119
+ # Pending writes: one attempt against persisted state. Persist the row BEFORE the first send, so a
120
+ # crash between send and answer cannot produce a second record under a new key. A job retries rows
121
+ # left in "sent" with backoff (seconds, then minutes, for hours if the device stays offline).
122
+ # --------------------------------------------------------------------------------------------------
123
+
124
+ CONFLICT_RETRIES = 5 # a running write finishes within seconds; beyond this, treat it as unknown
125
+
126
+ RETRY_SAME_KEY = {"device_offline", "target_busy", "temporarily_unavailable", "internal_error",
127
+ "action_paused", "system_unavailable"}
128
+
129
+
130
+ @dataclass
131
+ class PendingWrite:
132
+ key: str # 8 to 128 chars of A-Z a-z 0-9 _ -, stable per business event
133
+ device_id: str
134
+ system: str
135
+ action: str
136
+ method: str
137
+ input: Dict[str, str]
138
+ status: str = "pending" # pending | sent | done | unknown | refused
139
+ conflict_retries: int = 0
140
+ result: Optional[Dict[str, Any]] = field(default=None)
141
+ request_id: Optional[str] = None
142
+ refused_code: Optional[str] = None
143
+
144
+
145
+ def attempt(dd: Deepdock, row: PendingWrite) -> PendingWrite:
146
+ """One attempt; returns the row with its new status. The caller persists it."""
147
+ try:
148
+ res = dd.write(row.device_id, row.system, row.action, row.method, row.key, row.input)
149
+ return replace(row, status="done", result=res.data)
150
+ except DeepdockTransportError:
151
+ return replace(row, status="sent")
152
+ except DeepdockError as e:
153
+ if e.code in RETRY_SAME_KEY:
154
+ return replace(row, status="sent", request_id=e.request_id)
155
+ if e.code == "idempotency_conflict":
156
+ if row.conflict_retries < CONFLICT_RETRIES:
157
+ return replace(row, status="sent", conflict_retries=row.conflict_retries + 1)
158
+ return replace(row, status="unknown")
159
+ if e.code == "outcome_unknown":
160
+ return replace(row, status="unknown", request_id=e.request_id)
161
+ # invalid_input, target_rejected, action-specific 422s, not_found, check_failed, auth and
162
+ # resolution codes: nothing was written. Fix the cause; a changed input gets a new key.
163
+ return replace(row, status="refused", refused_code=e.code, result={"message": str(e)})
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Minimal Deepdock /v1 client. No dependencies beyond fetch (Node 18+, Bun, Deno, browsers).
3
+ *
4
+ * It does the four things the OpenAPI document cannot tell you: the bearer header, the envelope
5
+ * unwrap, the Idempotency-Key header with replay detection, and a typed error carrying the code.
6
+ * Everything action-specific (paths, fields, methods) comes from the live OpenAPI document.
7
+ */
8
+
9
+ export class DeepdockError extends Error {
10
+ constructor(
11
+ readonly status: number,
12
+ readonly code: string,
13
+ message: string,
14
+ readonly requestId?: string,
15
+ readonly systems?: string[],
16
+ ) {
17
+ super(message)
18
+ this.name = 'DeepdockError'
19
+ }
20
+ }
21
+
22
+ /** No answer, or not Deepdock's envelope (timeout, network, a proxy page): a write may still have landed. */
23
+ export class DeepdockTransportError extends Error {
24
+ constructor(message: string) {
25
+ super(message)
26
+ this.name = 'DeepdockTransportError'
27
+ }
28
+ }
29
+
30
+ type ErrorBody = { code: string; message: string; requestId?: string; systems?: string[] }
31
+ type Envelope<T> = { data: T } | { error: ErrorBody }
32
+
33
+ export type Page<Item> = { items: Item[]; nextCursor: string | null }
34
+ export type WriteResult<T> = { data: T; replayed: boolean }
35
+ export type WriteMethod = 'POST' | 'PATCH'
36
+
37
+ export type Device = {
38
+ id: string
39
+ label: string | null
40
+ hostname: string
41
+ os: string
42
+ clientVersion: string
43
+ status: 'connected' | 'offline' | 'inactive'
44
+ lastSeenAt: string | null
45
+ claimedAt: string
46
+ systems: string[]
47
+ updateAvailable?: string
48
+ }
49
+
50
+ export type ActionState =
51
+ | { state: 'active' }
52
+ | { state: 'paused'; reason: string; pausedAt: string }
53
+
54
+ export type DeviceSystem = {
55
+ label: string | null
56
+ version: string
57
+ status: 'running' | 'stopped' | 'missing'
58
+ actions: Record<string, ActionState>
59
+ }
60
+
61
+ export type DeepdockOptions = {
62
+ base?: string
63
+ /** Above Deepdock's 20 s device wait, so you receive device_offline instead of timing out first. */
64
+ timeoutMs?: number
65
+ fetch?: typeof fetch
66
+ }
67
+
68
+ export class Deepdock {
69
+ private readonly base: string
70
+ private readonly timeoutMs: number
71
+ private readonly fetchImpl: typeof fetch
72
+
73
+ constructor(
74
+ private readonly apiKey: string,
75
+ { base = 'https://api.deepdock.ai', timeoutMs = 30_000, fetch: fetchImpl = fetch }: DeepdockOptions = {},
76
+ ) {
77
+ this.base = base.replace(/\/$/, '')
78
+ this.timeoutMs = timeoutMs
79
+ this.fetchImpl = fetchImpl
80
+ }
81
+
82
+ private async send<T>(
83
+ path: string,
84
+ init: { method: string; headers?: Record<string, string>; body?: string },
85
+ ): Promise<{ data: T; headers: Headers }> {
86
+ let res: Response
87
+ try {
88
+ res = await this.fetchImpl(`${this.base}${path}`, {
89
+ method: init.method,
90
+ headers: { Authorization: `Bearer ${this.apiKey}`, ...init.headers },
91
+ body: init.body,
92
+ signal: AbortSignal.timeout(this.timeoutMs),
93
+ })
94
+ } catch (err) {
95
+ // TimeoutError from AbortSignal.timeout, AbortError, or a network TypeError.
96
+ throw new DeepdockTransportError(`no answer from Deepdock: ${(err as Error).name}`)
97
+ }
98
+ let body: Envelope<T>
99
+ try {
100
+ body = (await res.json()) as Envelope<T>
101
+ } catch {
102
+ throw new DeepdockTransportError(`non-JSON answer with status ${res.status}`)
103
+ }
104
+ if (body !== null && typeof body === 'object' && 'error' in body) {
105
+ const { code, message, requestId, systems } = body.error
106
+ throw new DeepdockError(res.status, code, message, requestId, systems)
107
+ }
108
+ if (body === null || typeof body !== 'object' || !('data' in body)) {
109
+ throw new DeepdockTransportError(`unexpected body with status ${res.status}`)
110
+ }
111
+ return { data: body.data, headers: res.headers }
112
+ }
113
+
114
+ private actionPath(deviceId: string, system: string, action: string): string {
115
+ return `/v1/devices/${encodeURIComponent(deviceId)}/systems/${encodeURIComponent(system)}/actions/${encodeURIComponent(action)}`
116
+ }
117
+
118
+ /** Runs a read action. Pass `nextCursor` back as `cursor` for the next page. */
119
+ async read<Item>(
120
+ deviceId: string,
121
+ system: string,
122
+ action: string,
123
+ input: Record<string, string>,
124
+ page: { limit?: number; cursor?: string } = {},
125
+ ): Promise<Page<Item>> {
126
+ const query = new URLSearchParams(input)
127
+ if (page.limit !== undefined) query.set('limit', String(page.limit))
128
+ if (page.cursor !== undefined) query.set('cursor', page.cursor)
129
+ const suffix = query.size > 0 ? `?${query}` : ''
130
+ return (await this.send<Page<Item>>(`${this.actionPath(deviceId, system, action)}${suffix}`, { method: 'GET' })).data
131
+ }
132
+
133
+ /** Reads every page of a read action, in order. */
134
+ async *readAll<Item>(
135
+ deviceId: string,
136
+ system: string,
137
+ action: string,
138
+ input: Record<string, string>,
139
+ limit?: number,
140
+ ): AsyncGenerator<Item> {
141
+ let cursor: string | undefined
142
+ do {
143
+ const page = await this.read<Item>(deviceId, system, action, input, { limit, cursor })
144
+ for (const item of page.items) yield item
145
+ cursor = page.nextCursor ?? undefined
146
+ } while (cursor !== undefined)
147
+ }
148
+
149
+ /**
150
+ * Runs a write action. `method` comes from the action index or the OpenAPI document; `input`
151
+ * values are all strings. `idempotencyKey` is new per intended write and the same on every retry.
152
+ */
153
+ async write<Result>(
154
+ deviceId: string,
155
+ system: string,
156
+ action: string,
157
+ method: WriteMethod,
158
+ idempotencyKey: string,
159
+ input: Record<string, string>,
160
+ ): Promise<WriteResult<Result>> {
161
+ const { data, headers } = await this.send<Result>(this.actionPath(deviceId, system, action), {
162
+ method,
163
+ headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey },
164
+ body: JSON.stringify(input),
165
+ })
166
+ return { data, replayed: headers.get('Idempotent-Replayed') === 'true' }
167
+ }
168
+
169
+ /** The device's systems and each action's state. A live call to the device: can answer device_offline. */
170
+ async systems(deviceId: string): Promise<Record<string, DeviceSystem>> {
171
+ return (await this.send<Record<string, DeviceSystem>>(`/v1/devices/${encodeURIComponent(deviceId)}/systems`, { method: 'GET' })).data
172
+ }
173
+
174
+ /** Claims a device by the pairing code it shows. Keep the returned `id`. */
175
+ async pair(code: string, label?: string): Promise<Device> {
176
+ return (
177
+ await this.send<Device>('/v1/devices', {
178
+ method: 'POST',
179
+ headers: { 'Content-Type': 'application/json' },
180
+ body: JSON.stringify(label === undefined ? { code } : { code, label }),
181
+ })
182
+ ).data
183
+ }
184
+
185
+ async release(deviceId: string): Promise<void> {
186
+ await this.send<unknown>(`/v1/devices/${encodeURIComponent(deviceId)}`, { method: 'DELETE' })
187
+ }
188
+ }
189
+
190
+ /* ------------------------------------------------------------------------------------------------
191
+ * Pending writes: one attempt against persisted state. Persist the row BEFORE the first send, so a
192
+ * crash between send and answer cannot produce a second record under a new key. A job retries rows
193
+ * left in `sent` with backoff (seconds, then minutes, for hours if the device stays offline).
194
+ * ---------------------------------------------------------------------------------------------- */
195
+
196
+ export type PendingStatus = 'pending' | 'sent' | 'done' | 'unknown' | 'refused'
197
+
198
+ export type PendingWrite = {
199
+ /** 8 to 128 chars of A-Z a-z 0-9 _ -, stable per business event. */
200
+ key: string
201
+ deviceId: string
202
+ system: string
203
+ action: string
204
+ method: WriteMethod
205
+ input: Record<string, string>
206
+ status: PendingStatus
207
+ conflictRetries: number
208
+ result?: unknown
209
+ requestId?: string
210
+ refusedCode?: string
211
+ }
212
+
213
+ /** A running write finishes within seconds; beyond this, treat a conflict as an unknown outcome. */
214
+ export const CONFLICT_RETRIES = 5
215
+
216
+ const RETRY_SAME_KEY = new Set([
217
+ 'device_offline',
218
+ 'target_busy',
219
+ 'temporarily_unavailable',
220
+ 'internal_error',
221
+ 'action_paused',
222
+ 'system_unavailable',
223
+ ])
224
+
225
+ /** One attempt; returns the row with its new status. The caller persists it. */
226
+ export const attempt = async (dd: Deepdock, row: PendingWrite): Promise<PendingWrite> => {
227
+ try {
228
+ const { data } = await dd.write(row.deviceId, row.system, row.action, row.method, row.key, row.input)
229
+ return { ...row, status: 'done', result: data }
230
+ } catch (err) {
231
+ if (err instanceof DeepdockTransportError) return { ...row, status: 'sent' }
232
+ if (!(err instanceof DeepdockError)) throw err
233
+ if (RETRY_SAME_KEY.has(err.code)) return { ...row, status: 'sent', requestId: err.requestId }
234
+ if (err.code === 'idempotency_conflict') {
235
+ return row.conflictRetries < CONFLICT_RETRIES
236
+ ? { ...row, status: 'sent', conflictRetries: row.conflictRetries + 1 }
237
+ : { ...row, status: 'unknown' }
238
+ }
239
+ if (err.code === 'outcome_unknown') return { ...row, status: 'unknown', requestId: err.requestId }
240
+ // invalid_input, target_rejected, action-specific 422s, not_found, check_failed, auth and
241
+ // resolution codes: nothing was written. Fix the cause; a changed input gets a new key.
242
+ return { ...row, status: 'refused', refusedCode: err.code, result: { message: err.message } }
243
+ }
244
+ }
@@ -0,0 +1,97 @@
1
+ # Concepts
2
+
3
+ The words the API, the CLI and the dashboard use. Use them unchanged in code and copy.
4
+
5
+ ## Organisation and partner
6
+
7
+ You, the partner, are an **organisation** in Deepdock. Everything you own (keys, devices,
8
+ incidents) is scoped to it. Members of your organisation see the dashboard; admins manage keys,
9
+ release devices.
10
+
11
+ ## Keys
12
+
13
+ - **API key** (`ddk_live_` on production; a `ddk_test_` key belongs to a test deployment): your product's
14
+ credential on `/v1`. It runs actions, pairs and releases devices, and reads incidents. Created by an
15
+ admin in the dashboard (shown once) or from the CLI, which writes it straight into the app's env
16
+ file, keeps no copy and shows it to nobody; optionally expiring.
17
+ - **CLI sign-in**: one person's credential, used only by the `deepdock` CLI after `deepdock auth
18
+ login`. Valid 90 days. It manages (devices, keys, listing systems) and never runs an action.
19
+
20
+ Details: [auth-and-keys.md](auth-and-keys.md).
21
+
22
+ ## Device
23
+
24
+ The Deepdock client installed on the customer's Windows PC next to their system. It connects
25
+ outwards to Deepdock; nothing in the customer's network is opened to the internet. A device is
26
+ identified by a UUID (`id`), carries a `label`, `hostname`, `os`, `clientVersion`, `status`
27
+ (`connected`, `offline` or `inactive`), `lastSeenAt`, `claimedAt` and the `systems` it reported.
28
+
29
+ - **Pair**: claim a device for your organisation with the 6-character code it shows while unpaired.
30
+ - **Release**: `DELETE /v1/devices/{deviceId}` gives the device back; it shows a new code.
31
+ - **Inactive**: a paired device that has not connected for 7 days. It stays yours and is active again
32
+ when it connects.
33
+ - **Update**: the client updates itself. A device on an older build than Deepdock ships reports
34
+ `updateAvailable`.
35
+
36
+ ## System
37
+
38
+ A program on the device that Deepdock has made reachable, named by a slug such as `example-erp`. Each
39
+ has a display `label` (may be `null`), a `version` the device reported, a `status`, and actions.
40
+ `GET /v1/devices/{deviceId}/systems` is a live call to the device, so it can answer
41
+ `device_offline`.
42
+
43
+ ### Every status value and what it means for you
44
+
45
+ | Field | Value | Meaning | What to do |
46
+ |---|---|---|---|
47
+ | device `status` | `connected` | the device is online and can run actions | normal |
48
+ | device `status` | `offline` | the device is not connected right now (PC off, no network) | calls answer `device_offline`; retry later, show "offline" |
49
+ | device `status` | `inactive` | not connected for 7 days; still paired | as offline; check with the site whether the PC still exists |
50
+ | system `status` | `running` | the customer's program is running on the device | normal |
51
+ | system `status` | `stopped` | the program is installed but not running | calls answer `system_unavailable`; the site starts the program |
52
+ | system `status` | `missing` | the program was not found where the device expects it | calls answer `system_unavailable`; contact the site and Deepdock |
53
+ | system `status` | `unknown` | the device could not tell whether the program runs (its probe failed or is unavailable on that PC) | do not read it as stopped; the device decides when a call arrives |
54
+ | system `access` | `ok`, `outdated`, `missing`, `not_set_up`, or `null` | the database access the device last reported (`null`: none known yet) | `outdated` refuses the actions that need the missing right with `access_outdated`, `missing` refuses every action with `access_missing`; the practice repeats "Erneut einrichten" in the Deepdock client, the incident closes by itself once `access` is `ok`, nothing to resume |
55
+ | action state | `active` | the action can be called | normal |
56
+ | action state | `paused` | the action is paused with a `reason` and `pausedAt` | calls answer `action_paused`; show the feature as paused, poll `GET .../systems` at a low rate |
57
+ | pause `reason` | `structure_changed` | the system changed in a way the action depends on (for example a vendor update); Deepdock reviews and resumes | wait; nothing to do on your side |
58
+ | pause `reason` | `check_failed` | a write's result did not match, so nothing was saved | wait; tell the user the change was not saved |
59
+ | pause `reason` | `system_stopped`, `system_missing` | as the system statuses above; these need no resume and clear when the system is back | as above |
60
+
61
+ Switch on these values; any value you do not know is shown as it is.
62
+
63
+ ## Action
64
+
65
+ One deterministic, validated operation on a system: for example search customers by surname prefix,
66
+ create a note, update a note's text. Deepdock builds and verifies each action before
67
+ offering it; it then runs the same way every time. Actions are
68
+ **assigned** to your organisation by Deepdock, so the set you can call is yours, not the whole
69
+ catalogue.
70
+
71
+ - A **read** is `GET` with query parameters and pages.
72
+ - A **write** is `POST` (creates a record, answers `201`) or `PATCH` (updates a record, answers
73
+ `200`), with a JSON body of string fields and an `Idempotency-Key` header.
74
+ - A write **either fully applies or not**: Deepdock checks the result before confirming, and if the check fails it saves nothing
75
+ and pauses the action (`409 check_failed`).
76
+ - An action is **active** or **paused**. `GET /v1/devices/{deviceId}/systems` shows each action's
77
+ state with `reason` and `pausedAt`.
78
+
79
+ ## Pause and incident
80
+
81
+ Deepdock pauses an action instead of guessing when the customer's system no longer looks the way
82
+ the action expects:
83
+
84
+ | Reason | What you get | Meaning |
85
+ |---|---|---|
86
+ | `structure_changed` | `503 action_paused` | the system changed (for example a vendor update); Deepdock reviews and resumes |
87
+ | `check_failed` | that call `409 check_failed`, later calls `503 action_paused` | a write's result did not match, so nothing was saved |
88
+ | `system_stopped`, `system_missing` | `503 system_unavailable` | the customer's program is stopped or not found on the device |
89
+
90
+ Each pause opens an **incident** (`GET /v1/incidents`). Only Deepdock lifts a pause; staff at the
91
+ customer's site see that an action is paused but cannot resume it; there is no partner call to
92
+ resume. A stopped or missing system needs no resume: its incident closes once the system is back.
93
+
94
+ An incident has `id`, `system`, `version`, `action` (`null` for a system-level incident), `reason`,
95
+ `openedAt`, `closedAt`, and `devices` with `deviceId`, `pausedAt`, `resumedAt` and `inactiveAt`. It
96
+ closes once none of its devices is still paused. `GET /v1/incidents` lists incidents that are open
97
+ or closed within the last 30 days, newest first, at most 100.
@@ -0,0 +1,80 @@
1
+ # Discovering systems and actions
2
+
3
+ Never hard-code an action's fields. Three sources, each authoritative for one thing:
4
+
5
+ | Question | Source | Key |
6
+ |---|---|---|
7
+ | Which systems and actions does my organisation have? | `deepdock systems list --json` | CLI sign-in |
8
+ | What does this device run, and is each action active or paused? | `GET /v1/devices/{deviceId}/systems` | organisation |
9
+ | Which actions can I call on this device's system, with which schemas? | `GET /v1/devices/{deviceId}/systems/{system}/actions` | organisation |
10
+ | The exact paths, methods, fields, patterns, enums, lengths, responses | `deepdock systems openapi <system>` (CLI sign-in) or `GET /v1/systems/{system}/openapi.json` (API key); per device `deepdock actions list <device> <system>` (CLI sign-in) | CLI sign-in or API key |
11
+
12
+ ## The OpenAPI document
13
+
14
+ ```bash
15
+ deepdock systems openapi example-erp --org <slug> > openapi/example-erp.json
16
+ # from your backend, with the API key:
17
+ curl "$BASE/v1/systems/example-erp/openapi.json" -H "Authorization: Bearer $DEEPDOCK_API_KEY"
18
+ ```
19
+
20
+ OpenAPI 3.1, one document per system, cacheable privately for 60 seconds. It is never public:
21
+ `GET /v1/systems/{system}/openapi.json` needs your API key like the rest of `/v1`
22
+ (`401 unauthenticated` without one), and the CLI fetches it with its sign-in instead. Discover through the CLI; check the
23
+ document into the repository and refresh it with the same command when you touch the integration.
24
+
25
+ What to read off it:
26
+
27
+ - **`paths`**: one operation per action at
28
+ `/v1/devices/{deviceId}/systems/{system}/actions/{action}`; the HTTP method is the action's
29
+ kind (`get` read, `post` create, `patch` update). Use the method the document gives; the other
30
+ write method answers `405 invalid_request` with an `Allow` header.
31
+ - **`summary` and `description`**: what the action does and what it changes in the system, what
32
+ an omitted optional input becomes, action-specific refusals (`code` and when), and what the
33
+ system only warns about and what the action does then. Read the description before mapping your
34
+ fields.
35
+ - **Read inputs** are query parameters, all `type: string`, each with `maxLength` and possibly
36
+ `minLength`, `pattern`, `enum` and a `description` such as "prefix match: case, punctuation and
37
+ umlaut spelling are ignored". Plus `limit` (integer, 1 to 250, default 25) and `cursor` (opaque).
38
+ An optional parameter is not in `required` and its description says what an omitted value
39
+ becomes.
40
+ - **Write inputs** are the `requestBody` object schema: `additionalProperties: false`, every
41
+ property `type: string`, `required` lists the mandatory ones. The `Idempotency-Key` header
42
+ parameter carries its own pattern (8 to 128 characters of `A-Z a-z 0-9 _ -`).
43
+ - **Responses**: a read's `200` is `{ data: { items: [...], nextCursor } }`; a create's `201` and a
44
+ replayed `200`; an update's `200`. Item and result fields are typed `string`, `integer` or `number`,
45
+ often nullable (`["integer", "null"]`). The `Error` schema's `code` enum is the full list of codes.
46
+ - **`servers`**: the base URL of the API.
47
+
48
+ The document describes **Deepdock's whole catalogue** for the system. Your organisation may be
49
+ assigned a subset. Write code only for actions that appear in both the document and `systems list`
50
+ (or the device's action index); an action in the document but not assigned to you answers
51
+ `404 action_not_found`.
52
+
53
+ ## The action index
54
+
55
+ ```bash
56
+ curl "$BASE/v1/devices/$DEVICE/systems/example-erp/actions" \
57
+ -H "Authorization: Bearer $DEEPDOCK_API_KEY"
58
+ ```
59
+
60
+ Answers `{ data: [ { name, method, description, input, output } ] }`, filtered to your assigned
61
+ actions, with the same JSON Schemas the OpenAPI document carries. Use it at runtime to confirm an
62
+ action exists before offering it in your UI, or to validate input client-side before sending.
63
+
64
+ ## The device's systems
65
+
66
+ ```bash
67
+ curl "$BASE/v1/devices/$DEVICE/systems" -H "Authorization: Bearer $DEEPDOCK_API_KEY"
68
+ ```
69
+
70
+ A live call to the device. Each system: `label`, `version`, `status`, and per action `active` or
71
+ `paused` with `reason` and `pausedAt`. Answers `503 device_offline` when the device is not
72
+ connected: that is a normal state, not an error in your code. Do not poll it tightly; it reaches the
73
+ customer's PC.
74
+
75
+ ## Generated clients
76
+
77
+ If the project already uses an OpenAPI generator (`openapi-typescript`, `openapi-generator`,
78
+ `kiota`), generating from the document is fine. The generator will not produce the envelope unwrap,
79
+ the idempotency rule or the error-code reactions; those come from this skill. Remember the
80
+ document is the whole catalogue, not your assigned set.