@deepdockai/cli 0.0.0-stage → 1.0.0

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,159 @@
1
+ # Worked example: the clients and one flow
2
+
3
+ Two full, tested clients ship with this skill; every other language gets `curl` below and ports
4
+ one of the two. Each client does the four things the OpenAPI document cannot tell you: the bearer
5
+ header, the envelope unwrap, the `Idempotency-Key` header with replay detection, and a typed error
6
+ carrying the code. Both also carry `attempt()`, one try of a persisted pending write that returns
7
+ the row's new status. Field names in the flow below are illustrative: read them from the live
8
+ document for your system.
9
+
10
+ | Language | File | Needs |
11
+ |---|---|---|
12
+ | TypeScript | [clients/deepdock.ts](clients/deepdock.ts) | `fetch` (Node 18+, Bun, Deno, browsers); no dependencies |
13
+ | Python | [clients/deepdock.py](clients/deepdock.py) | Python 3.9+, `httpx` |
14
+ | anything else | the `curl` calls below | port either client, keeping its shape |
15
+
16
+ Copy the file into the project; do not reach into the skill directory at runtime.
17
+
18
+ ## What the clients give you
19
+
20
+ ```ts
21
+ import { Deepdock, DeepdockError, DeepdockTransportError, attempt, type PendingWrite } from './deepdock'
22
+
23
+ const dd = new Deepdock(process.env.DEEPDOCK_API_KEY!) // 30 s timeout by default
24
+ const page = await dd.read<Note>(deviceId, 'example-erp', 'searchNotes', { text: 'TEST-DEEPDOCK-' }, { limit: 50 })
25
+ for await (const p of dd.readAll<Note>(deviceId, 'example-erp', 'searchNotes', { text: 'TEST-DEEPDOCK-' })) { /* every page */ }
26
+ const { data, replayed } = await dd.write<{ noteId: number }>(deviceId, 'example-erp', 'createNote', 'POST', key, input)
27
+ const systems = await dd.systems(deviceId) // live; can throw device_offline
28
+ const device = await dd.pair('K7Q-4MX', 'Demo') // keep device.id
29
+ ```
30
+
31
+ ```python
32
+ from deepdock import Deepdock, DeepdockError, DeepdockTransportError, PendingWrite, attempt
33
+
34
+ dd = Deepdock(os.environ["DEEPDOCK_API_KEY"])
35
+ page = dd.read(device_id, "example-erp", "searchNotes", {"text": "TEST-DEEPDOCK-"}, limit=50)
36
+ res = dd.write(device_id, "example-erp", "createNote", "POST", key, body) # res.data, res.replayed
37
+ ```
38
+
39
+ Errors: `DeepdockError` has `status`, `code`, `requestId`, `systems`; switch on `code`
40
+ ([errors.md](errors.md)). `DeepdockTransportError` means no answer or not Deepdock's envelope
41
+ (timeout, network, a proxy page): for a write, retry with the same key.
42
+
43
+ ## Sending a write: persist first, then one attempt at a time
44
+
45
+ Persist the pending write (key, device, action, method, input, status) **before** the first send,
46
+ so a crash between send and answer cannot produce a second record under a new key. `attempt()`
47
+ moves the row to `done`, `sent` (retry later, same key), `unknown` (read back before anything
48
+ else) or `refused` (nothing was written; a changed input gets a new key). A job retries `sent`
49
+ rows with backoff: seconds, then minutes, for hours if the device stays offline.
50
+
51
+ ```ts
52
+ const row: PendingWrite = {
53
+ key: `order-${order.id}-create-note`, // 8 to 128 chars of A-Z a-z 0-9 _ -, stable per business event
54
+ deviceId, system: 'example-erp', action: 'createNote', method: 'POST',
55
+ input, status: 'pending', conflictRetries: 0,
56
+ }
57
+ await store.save(row) // before the first send
58
+ const next = await attempt(dd, row) // one try
59
+ await store.save(next) // the job picks up 'sent' rows later
60
+ ```
61
+
62
+ ## One flow: create a record on the demo device and read it back
63
+
64
+ ```ts
65
+ import { randomUUID } from 'node:crypto'
66
+ import { Deepdock, attempt, type PendingWrite } from './deepdock'
67
+
68
+ const dd = new Deepdock(process.env.DEEPDOCK_API_KEY!)
69
+ const deviceId = process.env.DEEPDOCK_TEST_DEVICE! // the demo device the human named
70
+ const system = 'example-erp'
71
+ type Note = { noteId: number; text: string }
72
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
73
+
74
+ export const run = async (store: { save: (row: PendingWrite) => Promise<void> }) => {
75
+ const marker = `TEST-DEEPDOCK-${randomUUID().replaceAll('-', '').slice(0, 6).toUpperCase()}`
76
+ let row: PendingWrite = {
77
+ key: `demo-${marker}-create`, deviceId, system, action: 'createNote', method: 'POST',
78
+ input: { text: marker, orderNumber: '1001' },
79
+ status: 'pending', conflictRetries: 0,
80
+ }
81
+ await store.save(row)
82
+
83
+ // The demo retries inline a few times; a product retries from the store in a job.
84
+ for (let delay = 2_000; row.status === 'pending' || row.status === 'sent'; delay = Math.min(delay * 2, 60_000)) {
85
+ row = await attempt(dd, row)
86
+ await store.save(row)
87
+ if (row.status === 'sent') await sleep(delay)
88
+ }
89
+ if (row.status === 'refused') throw new Error(`refused: ${row.refusedCode}`)
90
+
91
+ if (row.status === 'unknown') {
92
+ // Read back before deciding: the write may have landed.
93
+ const page = await dd.read<Note>(deviceId, system, 'searchNotes', { text: marker })
94
+ const landed = page.items.find((p) => p.text === marker)
95
+ if (!landed) throw new Error('write did not land; resend under a NEW key from the store')
96
+ row = { ...row, status: 'done', result: { noteId: landed.noteId } }
97
+ await store.save(row)
98
+ }
99
+
100
+ const created = row.result as { noteId: number }
101
+ const page = await dd.read<Note>(deviceId, system, 'searchNotes', { text: marker })
102
+ if (!page.items.some((p) => p.noteId === created.noteId)) throw new Error('created record not found by search')
103
+
104
+ // Cleanup list for the PR: record id, device, time.
105
+ console.log(`CREATED ${system} noteId=${created.noteId} on device ${deviceId} at ${new Date().toISOString()}`)
106
+ }
107
+ ```
108
+
109
+ ## curl, for any other language
110
+
111
+ ```bash
112
+ BASE=https://api.deepdock.ai
113
+ AUTH="Authorization: Bearer $DEEPDOCK_API_KEY"
114
+
115
+ # Pair a device (keep data.id)
116
+ curl -sS -X POST "$BASE/v1/devices" -H "$AUTH" -H 'Content-Type: application/json' \
117
+ -d '{"code":"K7Q-4MX","label":"Demo"}'
118
+
119
+ # The device's systems and action states (live; can answer device_offline)
120
+ curl -sS "$BASE/v1/devices/$DEVICE/systems" -H "$AUTH"
121
+
122
+ # The action index (your assigned actions, with schemas)
123
+ curl -sS "$BASE/v1/devices/$DEVICE/systems/example-erp/actions" -H "$AUTH"
124
+
125
+ # A read, paged: pass data.nextCursor back as cursor
126
+ curl -sS "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/searchNotes?text=TEST-DEEPDOCK-&limit=50" -H "$AUTH"
127
+
128
+ # A create: new Idempotency-Key per intended write, the same on every retry; 201, or 200 with
129
+ # Idempotent-Replayed: true on a replay (-i shows the headers)
130
+ curl -sS -i -X POST "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/createNote" \
131
+ -H "$AUTH" -H "Idempotency-Key: demo-$(date +%s)-create" -H 'Content-Type: application/json' \
132
+ -d '{"text":"TEST-DEEPDOCK-K3M9QA","orderNumber":"1001"}'
133
+
134
+ # An update: PATCH, same rules
135
+ curl -sS -i -X PATCH "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/updateNote" \
136
+ -H "$AUTH" -H "Idempotency-Key: demo-$(date +%s)-update" -H 'Content-Type: application/json' \
137
+ -d '{"noteId":"1001","text":"TEST-DEEPDOCK-K3M9QA edited"}'
138
+
139
+ # Release a device
140
+ curl -sS -X DELETE "$BASE/v1/devices/$DEVICE" -H "$AUTH"
141
+ ```
142
+
143
+ A port in another language keeps the clients' shape: one `send` that sets the bearer header,
144
+ parses the envelope, throws a typed error with `code` on `{ error }` and a transport error on no
145
+ answer or non-JSON; `read` with `limit` and `cursor`; `write` with `Idempotency-Key` and the
146
+ `Idempotent-Replayed` header; `attempt` over a persisted pending write.
147
+
148
+ ## Mapping the user's intent to a call (checklist)
149
+
150
+ 1. Which device? From your product's configuration for that customer site; never from the
151
+ user's free text.
152
+ 2. Is the action active? `systems()` once per session; show paused features as paused.
153
+ 3. Which action and method? From the stored index for that system.
154
+ 4. Does the write need a reference (author user, open case, record id)? Read it first.
155
+ 5. Build the input as strings; validate against the schema (`pattern`, `minLength`, `maxLength`,
156
+ `enum`) before sending so the user sees the rule where they typed.
157
+ 6. Choose the idempotency key from your business event and persist it with the pending write.
158
+ 7. Send; map the code with the reaction table; show the user the right sentence.
159
+ 8. Store the returned id as the system gave it.
@@ -0,0 +1,97 @@
1
+ # Idempotency for writes
2
+
3
+ Every `POST` or `PATCH` to an action carries an `Idempotency-Key` header. Without it the request is
4
+ refused with `400 invalid_request`.
5
+
6
+ ## The key
7
+
8
+ - 8 to 128 characters from `A-Z a-z 0-9 _ -`.
9
+ - **One key per intended write.** Derive it from your own business event (an order id, a form
10
+ submission id, a job id plus the action name), or generate a UUID and persist it with the pending
11
+ write before sending. The key must survive a crash of your process, or a restart will write twice.
12
+ - **The same key for every retry of that write.** Never regenerate it on retry.
13
+ - Keys are remembered **per device**. The same key on another device is a different write.
14
+ - A successful key is remembered for at least 7 days.
15
+ - A write that was refused before anything was written (`invalid_input`, `target_rejected`, an
16
+ action-specific 422 such as `unknown_author`, `not_found`) or undone (`check_failed`) frees its
17
+ key: you may retry with the same key. After changing the input, use a new key; the old one may
18
+ still be remembered with the old input. `idempotency_conflict` does **not** free a key.
19
+
20
+ ## What a repeat answers
21
+
22
+ | Situation | Answer |
23
+ |---|---|
24
+ | same key, same input, earlier write succeeded | `200` with the original `data` and `Idempotent-Replayed: true`; nothing written again |
25
+ | same key, different input | `409 idempotency_conflict` |
26
+ | same key, the earlier write is still running | `409 idempotency_conflict`; wait, then retry with the same key and input |
27
+ | same key, the earlier write's outcome is unknown | `409 idempotency_conflict`; the key stays blocked |
28
+
29
+ The three `idempotency_conflict` causes share one code and `message` is not to be parsed. One rule
30
+ covers all three: retry with the same key and the same input a bounded number of times with
31
+ backoff (a running write finishes within seconds); if it still conflicts, treat it as
32
+ `outcome_unknown` below.
33
+
34
+ Treat a replayed `200` as success. Read the header if you need to know it was a replay, for
35
+ example to avoid double-logging.
36
+
37
+ ## The three outcomes you must plan for
38
+
39
+ ### `503 device_offline` on a write
40
+
41
+ The device did not answer within 20 seconds, or was not connected. **The write may still have
42
+ landed.** Retry with the **same key**: you get either the stored result (replayed) or a fresh run,
43
+ never two writes. Back off between retries (seconds, then minutes); a device can be off for hours,
44
+ so the retry runs from the persisted pending write in a job or queue, not inside the request that
45
+ started it. A client-side timeout or a network failure on a write is the same case: same key.
46
+
47
+ ### `500 outcome_unknown`
48
+
49
+ The customer's system did not confirm the write. Deepdock cannot tell whether it landed. The key is
50
+ blocked: any reuse answers `409 idempotency_conflict`.
51
+
52
+ Do not write again under a new key until you know. Read the record back with a read action (for a
53
+ create, search by the value you wrote; for an update, read the record by id) and decide:
54
+
55
+ - found with the written values: treat as done, record it, move on;
56
+ - not found: write again under a **new** key;
57
+ - cannot tell (the read action does not expose the field): stop and hand the case to a human with
58
+ the record identifiers.
59
+
60
+ ### `409 check_failed`
61
+
62
+ The write was undone because its result did not match, and the action is now paused. Do not retry;
63
+ the next calls answer `503 action_paused` until Deepdock resumes it. Tell the user the change was not
64
+ saved.
65
+
66
+ ## Persisting state
67
+
68
+ A robust integration keeps, per intended write: the idempotency key, the device id, the action, the
69
+ input, a status (`pending`, `sent`, `done`, `unknown`, `refused`) and the result. Transitions:
70
+
71
+ ```
72
+ pending ──send──▶ sent ──2xx (fresh or replayed)──────────────▶ done
73
+ │ ──device_offline, timeout, network error, target_busy, temporarily_unavailable, internal_error ▶ sent (retry later, same key)
74
+ │ ──idempotency_conflict, retries left ───▶ sent (retry, same key, same input)
75
+ │ ──idempotency_conflict, retries spent ──▶ unknown (read back before anything else)
76
+ │ ──outcome_unknown ──────────────────────▶ unknown (read back before anything else)
77
+ │ ──invalid_input, target_rejected, action-specific 422, not_found, check_failed ▶ refused (key is free; new input gets a new key)
78
+ │ ──action_paused, system_unavailable ────▶ sent (retry once the action is active again)
79
+ ```
80
+
81
+ ## Example
82
+
83
+ ```ts
84
+ const key = `order-${order.id}-create-note` // stable per business event, 8 to 128 chars
85
+
86
+ const res = await fetch(url, {
87
+ method: 'POST',
88
+ headers: {
89
+ Authorization: `Bearer ${apiKey}`,
90
+ 'Idempotency-Key': key,
91
+ 'Content-Type': 'application/json',
92
+ },
93
+ body: JSON.stringify(input),
94
+ signal: AbortSignal.timeout(30_000),
95
+ })
96
+ const replayed = res.headers.get('Idempotent-Replayed') === 'true'
97
+ ```
@@ -0,0 +1,80 @@
1
+ # Testing safely
2
+
3
+ Actions reach a real installation of the customer's system: the device you name is the one that
4
+ gets written to. These rules are not optional.
5
+
6
+ ## The test device
7
+
8
+ - Test against **a demo device of your own**: a Windows machine with the same system installed as
9
+ your customers run, with the Deepdock client paired to your organisation, and invented, marked
10
+ data on it.
11
+ - **Never run a write against a device the human has not named as the test device.** If no device
12
+ has been named, ask. Do not pick the first device from `devices list`; it may be a live customer.
13
+ - Pin the demo device's UUID in an environment variable (`DEEPDOCK_TEST_DEVICE`) and refuse to run
14
+ tests without it.
15
+
16
+ ## Test data
17
+
18
+ - Invented values only: no real customer, employee or address data, even "just one row",
19
+ even from a screenshot. A fixture is copied into every clone and CI log for ever.
20
+ - Every record you create carries the marker **`TEST-DEEPDOCK-<6 random characters>`** in its
21
+ name-like field (surname, company name), so it can be found and removed. Keep the marker through
22
+ updates: when you test a name update, write a second `TEST-DEEPDOCK-...` value.
23
+ - Other fields take short values valid for their schema rule: a real past date, the first enum
24
+ member, `1001` for an order number, `Beispielstadt` for a city.
25
+ - Keep the system's own text rules in mind: the OpenAPI document's field patterns and descriptions
26
+ say what a text field accepts; stick to plain Latin letters in test values.
27
+
28
+ ## Cleanup
29
+
30
+ No action deletes anything. So:
31
+
32
+ 1. Every test run prints what it created: record id, device, time, and how many change-log entries
33
+ the updates added. The CLI runs no action, so your own test prints this.
34
+ 2. Paste the list into the PR or the task that created it, with an owner and a date by which it is
35
+ removed.
36
+ 3. The partner removes the records through the system's own user interface on the demo machine, or
37
+ resets the machine from a snapshot. Change-log entries stay until the reset.
38
+
39
+ ## The loop your test runs
40
+
41
+ The CLI runs no action, so this loop is your code's, against the demo device:
42
+
43
+ 1. `deepdock devices systems <device> --org <slug> --json`: the system is running and every action
44
+ you need is active; otherwise stop and read the pause reason and `deepdock incidents list`.
45
+ 2. **Create** with a fresh idempotency key and `TEST-DEEPDOCK-` names: expect `201` and an id.
46
+ 3. **Replay** the same request with the same key: expect `200`, `Idempotent-Replayed: true`, no
47
+ second record.
48
+ 4. **Update** each update action once against that id, a new key each, changed values of the same
49
+ shape (a name update writes a second `TEST-DEEPDOCK-` name).
50
+ 5. **Read back** with the last written name as the search term: expect the id with the updated
51
+ values.
52
+ 6. Print the cleanup list.
53
+
54
+ The proof is your client; the CLI only shows what the device offers.
55
+
56
+ ## Keys during development
57
+
58
+ - Run `deepdock keys create "<project> dev" --expires 30d --write-env .env.local --org <slug>`.
59
+ A human approves it in the browser; the CLI writes the key into the file and keeps no copy.
60
+ Neither you nor the human sees the value; you work with its id, prefix and file name only.
61
+ - Never ask for, read, print, echo, log or commit a key value. Use the key only against the demo
62
+ device.
63
+ - It reaches every device of the organisation until it expires or is revoked. Tell the human to
64
+ revoke it when the integration ships and to create the live key with the same flow from their
65
+ own CLI.
66
+
67
+ ## Logs and prompts
68
+
69
+ - Never log an action's `input` or a read's `items` in your running product: they are the customer's
70
+ records. Log the device id, the action, the code and the request id.
71
+ - Never paste real records into an agent prompt or a ticket. Use the `TEST-DEEPDOCK-` records from
72
+ the demo device when you need a concrete example.
73
+
74
+ ## Unit tests of your client
75
+
76
+ Mock the HTTP layer with the shapes from the OpenAPI document and this skill: `{ data }`,
77
+ `{ error: { code } }`, `Idempotent-Replayed: true`, a `503 device_offline` on a write followed by
78
+ a replayed `200`, a `500 outcome_unknown` followed by a read back. Cover every row of
79
+ [errors.md](errors.md) your product reacts to. Do not record real responses from a customer
80
+ device into fixtures.