@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.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +261 -2
- package/SECURITY.md +7 -0
- package/dist/deepdock.js +13417 -0
- package/package.json +46 -4
- package/skill/SKILL.md +109 -0
- package/skill/references/auth-and-keys.md +102 -0
- package/skill/references/calling-actions.md +111 -0
- package/skill/references/cli.md +97 -0
- package/skill/references/clients/deepdock.py +163 -0
- package/skill/references/clients/deepdock.ts +244 -0
- package/skill/references/concepts.md +95 -0
- package/skill/references/discovery.md +78 -0
- package/skill/references/errors.md +110 -0
- package/skill/references/example-integration.md +159 -0
- package/skill/references/idempotency.md +97 -0
- package/skill/references/testing.md +80 -0
|
@@ -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.
|