@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
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepdockai/cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Deepdock partner CLI: pair devices, see systems and actions, create the API key your code runs them with.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Deepdock",
|
|
8
|
+
"homepage": "https://github.com/deepdockai/devkit",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/deepdockai/devkit.git"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"deepdock": "dist/deepdock.js"
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist/deepdock.js", "skill", "README.md", "CHANGELOG.md", "LICENSE", "SECURITY.md"],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22.13"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"provenance": true
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "bun scripts/build.ts",
|
|
26
|
+
"format": "biome format .",
|
|
27
|
+
"format:fix": "biome format --write .",
|
|
28
|
+
"lint": "biome lint .",
|
|
29
|
+
"lint:fix": "biome lint --write .",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"gen:codes": "bun scripts/genCodes.ts",
|
|
32
|
+
"check:public": "bun scripts/checkPublic.ts --pack",
|
|
33
|
+
"test": "bun run build && bun --bun vitest run",
|
|
34
|
+
"test:node": "vitest run",
|
|
35
|
+
"ci": "bun run format && bun run lint && bun run typecheck && bun run test && bun run test:node && bun run check:public"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@biomejs/biome": "^2.6.2",
|
|
39
|
+
"@commander-js/extra-typings": "15",
|
|
40
|
+
"@types/bun": "^1.4.3",
|
|
41
|
+
"@types/node": "^26.6.5",
|
|
42
|
+
"arktype": "^2.2.9",
|
|
43
|
+
"commander": "15",
|
|
44
|
+
"openapi-typescript": "7",
|
|
45
|
+
"typescript": "~5.9",
|
|
46
|
+
"vitest": "^5.0.5"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deepdock-integration
|
|
3
|
+
description: Build, debug or test an integration with Deepdock's partner API, the REST actions under /v1 that read and write records in a customer's on-premise business system (for example Example ERP) through a paired Deepdock device. Use when code calls api.deepdock.ai, handles ddk_ API keys, pairs a device by code, calls a system action, handles error codes such as device_offline, action_paused, idempotency_conflict or outcome_unknown, or runs the deepdock CLI.
|
|
4
|
+
metadata:
|
|
5
|
+
cli: "@deepdockai/cli"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Deepdock integration
|
|
9
|
+
|
|
10
|
+
Deepdock makes a customer's on-premise system (a business's software,
|
|
11
|
+
installed on a PC in their office) reachable as a REST API. A small Deepdock client
|
|
12
|
+
runs next to that system; your code talks to Deepdock's cloud API, never to the customer's network.
|
|
13
|
+
|
|
14
|
+
Use this skill to integrate Deepdock into a product, write or review a client for it, or test an
|
|
15
|
+
integration against a demo device. It is not for building or changing actions: Deepdock does that.
|
|
16
|
+
|
|
17
|
+
## The model in eight sentences
|
|
18
|
+
|
|
19
|
+
1. Your **organisation** holds **API keys** (`ddk_live_...`), the only credential that runs
|
|
20
|
+
actions; one is sent as `Authorization: Bearer` on every `/v1` request.
|
|
21
|
+
2. A **device** is the Deepdock client on the customer's PC; you **pair** it with a code shown on its
|
|
22
|
+
screen, and from then on it belongs to your organisation until you release it.
|
|
23
|
+
3. A device reports the **systems** it runs; each system has **actions**, deterministic operations
|
|
24
|
+
that Deepdock builds and verifies before offering them.
|
|
25
|
+
4. A **read** action is a `GET` with query parameters and pages with `limit` and `cursor`; a **write**
|
|
26
|
+
action is a `POST` (create) or `PATCH` (update) with a JSON body of string fields.
|
|
27
|
+
5. Every write needs an `Idempotency-Key`: a new key per intended write, the same key on every retry,
|
|
28
|
+
and a repeat answers the stored result with `Idempotent-Replayed: true`, writing nothing twice.
|
|
29
|
+
6. A write either fully applies or not: Deepdock checks its result before confirming, and a write
|
|
30
|
+
whose result does not match is undone and the action is paused.
|
|
31
|
+
7. A device can be offline and an action can be paused; your code treats both as normal answers.
|
|
32
|
+
8. Every answer is `{ "data": ... }` or `{ "error": { "code", "message" } }`; `code` is the contract,
|
|
33
|
+
`message` is for people.
|
|
34
|
+
|
|
35
|
+
## The recipe
|
|
36
|
+
|
|
37
|
+
Work through these in order. Each step names the CLI command (see [references/cli.md](references/cli.md))
|
|
38
|
+
and what your code does with the result.
|
|
39
|
+
|
|
40
|
+
1. **Sign in.** `deepdock auth login --json`: without a terminal the CLI prints the request and
|
|
41
|
+
exits; show the human the link and wait for them to click Approve in the dashboard, then run
|
|
42
|
+
`deepdock auth login --wait <request>`. You never see the resulting sign-in. Then pass
|
|
43
|
+
`--org <slug>` on every command (`deepdock org list --json` shows the slugs).
|
|
44
|
+
2. **See what you have.** `deepdock systems list --json` (the systems and actions assigned to your
|
|
45
|
+
organisation) and `deepdock devices list --json` (your paired devices). If the human has a demo
|
|
46
|
+
device that is not yet paired, they read the code off its screen and you run
|
|
47
|
+
`deepdock devices pair <code> --label "Demo"`.
|
|
48
|
+
3. **Fetch the schemas.** `deepdock systems openapi <system> --org <slug> > openapi/<system>.json`
|
|
49
|
+
and read it; `deepdock actions list <device> <system> --json` shows the same fields per device.
|
|
50
|
+
Check the document into the repository and refresh it with the same command. Write code only for
|
|
51
|
+
actions that appear both in the document and in `systems list`.
|
|
52
|
+
4. **Write the client.** A thin client in the project's own language: bearer header, envelope
|
|
53
|
+
unwrap, idempotency header with replay detection, and an error-code switch. Read
|
|
54
|
+
[references/calling-actions.md](references/calling-actions.md) and
|
|
55
|
+
[references/example-integration.md](references/example-integration.md) first.
|
|
56
|
+
5. **Get a key into the app.** `deepdock keys create "<project> dev" --expires 30d --write-env
|
|
57
|
+
.env.local --org <slug> --json`, then `--wait <request>` once a human has clicked Approve in the
|
|
58
|
+
dashboard. The CLI receives the key once, writes `DEEPDOCK_API_KEY=<key>` into that file (it
|
|
59
|
+
must be ignored by git) and keeps no copy. It is displayed to you or to the human by nobody.
|
|
60
|
+
You work only with the key's id, prefix and the file name. Never ask for, read, print or paste
|
|
61
|
+
a key value.
|
|
62
|
+
6. **Test it.** Run your own code against the demo device the human named, with the key from
|
|
63
|
+
`.env.local`, and read back with the read action. The CLI runs no action. Read
|
|
64
|
+
[references/testing.md](references/testing.md) before any write.
|
|
65
|
+
7. **Ship.** The human creates the production key the same way, from their own CLI, into the
|
|
66
|
+
production secret store. Tell the human to revoke the development key.
|
|
67
|
+
|
|
68
|
+
## Rules that are not in the OpenAPI document
|
|
69
|
+
|
|
70
|
+
- **Discover, never hard-code.** Field names, patterns, enums and lengths come from the live OpenAPI
|
|
71
|
+
document (`deepdock systems openapi <system>`, or `GET /v1/systems/{system}/openapi.json` with
|
|
72
|
+
your key), never from memory or from this skill. The document lists Deepdock's whole catalogue for the system; your organisation's assigned
|
|
73
|
+
subset is `systems list` (or `GET /v1/devices/{deviceId}/systems/{system}/actions`).
|
|
74
|
+
[references/discovery.md](references/discovery.md).
|
|
75
|
+
- **Keys.** API keys run actions and pair devices. The CLI sign-in manages only, never runs an
|
|
76
|
+
action and never holds an API key. Keys are created and placed by `keys create`; you never ask
|
|
77
|
+
for, read, print or paste a key value, and never put one in a command argument, a URL, a log
|
|
78
|
+
line, a commit or this conversation. In code, read it from the destination the CLI wrote to
|
|
79
|
+
(`DEEPDOCK_API_KEY` by convention).
|
|
80
|
+
[references/auth-and-keys.md](references/auth-and-keys.md).
|
|
81
|
+
- **Idempotency.** New key per intended write, persisted before sending; same key per retry; after
|
|
82
|
+
`device_offline` retry with the same key; after `outcome_unknown` or a persisting
|
|
83
|
+
`idempotency_conflict` do not write again under a new key until a read shows the write did not
|
|
84
|
+
land. [references/idempotency.md](references/idempotency.md).
|
|
85
|
+
- **Errors.** Every `/v1` code, whether to retry, and what to show the user:
|
|
86
|
+
[references/errors.md](references/errors.md). Switch on `code`, never on `message`.
|
|
87
|
+
- **Timeouts.** A call waits for the device; Deepdock gives up after 20 seconds and answers
|
|
88
|
+
`503 device_offline`. Set your own client timeout to 30 seconds and expect a read to take a
|
|
89
|
+
second or more. A client-side timeout or network failure on a write is handled exactly like
|
|
90
|
+
`device_offline`: retry with the same key.
|
|
91
|
+
- **Test data only.** Test against a demo device with invented, marked data: a `TEST-DEEPDOCK-`
|
|
92
|
+
marker in every created record, no real customer data in fixtures, logs or prompts. [references/testing.md](references/testing.md).
|
|
93
|
+
- **Authoring fields.** Some writes need a reference the system already knows (an author user, an
|
|
94
|
+
open case). Read it first with the matching read action and pass the id back; never invent one.
|
|
95
|
+
[references/calling-actions.md](references/calling-actions.md).
|
|
96
|
+
|
|
97
|
+
## Reference files
|
|
98
|
+
|
|
99
|
+
| File | Read when |
|
|
100
|
+
|---|---|
|
|
101
|
+
| [references/concepts.md](references/concepts.md) | you need the vocabulary: device, system, action, pause, incident, release |
|
|
102
|
+
| [references/auth-and-keys.md](references/auth-and-keys.md) | creating, storing, sending or revoking a key; pairing or releasing a device |
|
|
103
|
+
| [references/discovery.md](references/discovery.md) | reading the action index or the OpenAPI document |
|
|
104
|
+
| [references/calling-actions.md](references/calling-actions.md) | writing the client: reads, writes, paging, authoring fields, timeouts |
|
|
105
|
+
| [references/errors.md](references/errors.md) | handling any `error.code` |
|
|
106
|
+
| [references/idempotency.md](references/idempotency.md) | retries, replays, unknown outcomes |
|
|
107
|
+
| [references/cli.md](references/cli.md) | any `deepdock ...` command |
|
|
108
|
+
| [references/testing.md](references/testing.md) | before the first write against a device |
|
|
109
|
+
| [references/example-integration.md](references/example-integration.md) | the full TypeScript and Python clients (`references/clients/`), curl for any other language, and a create-then-read flow |
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Auth, keys and pairing
|
|
2
|
+
|
|
3
|
+
| Environment | Base URL | API key prefix |
|
|
4
|
+
|---|---|---|
|
|
5
|
+
| Production | `https://api.deepdock.ai` | `ddk_live_` |
|
|
6
|
+
|
|
7
|
+
A deployment accepts only its own environment's keys. Test against a demo device of your own with
|
|
8
|
+
invented, marked data ([testing.md](testing.md)).
|
|
9
|
+
|
|
10
|
+
## API keys
|
|
11
|
+
|
|
12
|
+
- Format: the prefix, then 43 characters from `A-Z a-z 0-9 _ -`, then a 6-character checksum. Treat
|
|
13
|
+
it as opaque; never parse it beyond the prefix.
|
|
14
|
+
- Created by an organisation admin in the dashboard (Keys page), where it is shown **once**, or
|
|
15
|
+
with `deepdock keys create <name> --expires <d> --write-env <file>`, where the CLI writes it
|
|
16
|
+
straight into the app's env file and shows it to nobody. Deepdock stores only a hash. The key list shows the
|
|
17
|
+
first 8 characters after the prefix so keys can be told apart.
|
|
18
|
+
- Expiry is optional in the dashboard. A key created from the CLI must expire within 90 days; the
|
|
19
|
+
skill's default is `--expires 30d` with a name that says what it is for (`"<project> dev"`).
|
|
20
|
+
- Revoked by an admin in the dashboard or with `deepdock keys revoke <id>`, immediately.
|
|
21
|
+
- Sent on every `/v1` request, the OpenAPI document included:
|
|
22
|
+
|
|
23
|
+
```http
|
|
24
|
+
Authorization: Bearer ddk_live_<your key>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Every reason a key is refused (missing, malformed, wrong environment, bad checksum, unknown,
|
|
28
|
+
revoked, expired) answers the same `401 unauthenticated` with `WWW-Authenticate: Bearer`. Your code
|
|
29
|
+
cannot tell them apart and should not try: surface "the Deepdock API key was refused; check the key
|
|
30
|
+
in the dashboard" and stop retrying.
|
|
31
|
+
|
|
32
|
+
### Handling the key in code
|
|
33
|
+
|
|
34
|
+
- Read it from the environment or a secret store (`DEEPDOCK_API_KEY` is the convention the
|
|
35
|
+
dashboard's examples use). Never a literal in source, a test fixture, a log or a URL.
|
|
36
|
+
- One key per deployment is enough; there is no per-device key.
|
|
37
|
+
- Getting a key into the app is the CLI's job: `deepdock keys create <name> --expires <d>
|
|
38
|
+
--write-env <file>` opens a link to the dashboard; a human clicks Approve (or Deny); the CLI
|
|
39
|
+
receives the key once and writes `DEEPDOCK_API_KEY=<key>` into the file, which must be ignored
|
|
40
|
+
by git (if git cannot answer, the CLI writes the file and warns on stderr). It is displayed to nobody, neither you nor the human, and the CLI keeps no copy. The
|
|
41
|
+
CLI answers with the key's id, prefix, name, expiry and the file; that is all you work with.
|
|
42
|
+
- Never ask for, read, print or paste a key value, and never try to extract it from the file.
|
|
43
|
+
Code reads it at runtime from where the CLI put it.
|
|
44
|
+
- A development key reaches every device of the organisation until it expires or is revoked; keep
|
|
45
|
+
the expiry short (`30d`), use it only against the demo device, and tell the human to revoke it
|
|
46
|
+
(`deepdock keys revoke <id>`) when the integration ships. The live key is created the same way,
|
|
47
|
+
by the human from their own CLI, into the live secret store.
|
|
48
|
+
|
|
49
|
+
## The CLI sign-in (CLI only)
|
|
50
|
+
|
|
51
|
+
`deepdock auth login` opens a link to the dashboard; the human clicks Approve. The CLI then holds
|
|
52
|
+
the sign-in for that person, valid 90 days, stored in the CLI's own config with owner-only
|
|
53
|
+
permissions. The sign-in belongs to the person, not to an organisation: every command takes
|
|
54
|
+
`--org <slug>`. It does what that person can do in the dashboard (members read, pair and rename;
|
|
55
|
+
admins also release devices, create and revoke API keys) and never runs an action.
|
|
56
|
+
`deepdock auth status` shows the signed-in person, organisations and expiry (exit 5 when not signed in); `deepdock auth
|
|
57
|
+
logout` revokes the sign-in and every API key the CLI created.
|
|
58
|
+
|
|
59
|
+
Without a terminal `auth login` prints the request and exits; show the human the link, wait for
|
|
60
|
+
them to approve, then run `deepdock auth login --wait <request>`. Do not try to answer for them.
|
|
61
|
+
There is no sign-in from an environment variable.
|
|
62
|
+
|
|
63
|
+
## Pairing a device
|
|
64
|
+
|
|
65
|
+
A device is the Deepdock client installed next to the customer's system. The installer is at
|
|
66
|
+
`/public/client/download` under the base URL (production:
|
|
67
|
+
`https://api.deepdock.ai/public/client/download`); the link carries no credential, so it can be
|
|
68
|
+
sent to a new site as is. The pairing code is what ties the device to your organisation.
|
|
69
|
+
|
|
70
|
+
1. While unpaired, the client shows a code such as `K7Q-4MX`: 6 characters from
|
|
71
|
+
`ABCDEFGHJKMNPQRSTVWXYZ23456789`. Case and the dash do not matter.
|
|
72
|
+
2. A code is valid for 10 minutes and is replaced shortly before it expires: read it off the screen
|
|
73
|
+
when you pair.
|
|
74
|
+
3. Claim it:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
curl -X POST "$BASE/v1/devices" \
|
|
78
|
+
-H "Authorization: Bearer $DEEPDOCK_API_KEY" \
|
|
79
|
+
-H "Content-Type: application/json" \
|
|
80
|
+
-d '{"code":"K7Q-4MX","label":"Reception PC"}'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`label` is optional, 1 to 100 characters. The device must be online. Answer: `201` with the device
|
|
84
|
+
(`id`, `label`, `hostname`, `os`, `clientVersion`, `status`, `lastSeenAt`, `claimedAt`, `systems`,
|
|
85
|
+
and `updateAvailable` when it runs an older build). Keep `id`: it is the `deviceId` of every later
|
|
86
|
+
call, and there is no `/v1` call to list devices (use `deepdock devices list` or the dashboard).
|
|
87
|
+
|
|
88
|
+
4. Release with `DELETE /v1/devices/{deviceId}`; the device then shows a new code.
|
|
89
|
+
|
|
90
|
+
Pairing answers:
|
|
91
|
+
|
|
92
|
+
| Code | Status | Do |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `invalid_request` | 400 | the code is malformed; fix the input |
|
|
95
|
+
| `pairing_code_invalid` | 404 | unknown, expired, replaced, already used, or the device is offline; ask for a fresh code |
|
|
96
|
+
| `other_partner_build` | 409 | the device runs another partner's branded client; only that partner can claim it |
|
|
97
|
+
| `rate_limited` | 429 | more than 5 failed claim attempts in 60 seconds with this key; wait up to a minute |
|
|
98
|
+
|
|
99
|
+
Successful claims do not count against the limit; malformed codes are refused before counting.
|
|
100
|
+
|
|
101
|
+
With the CLI: `deepdock devices pair <code> [--label <text>]`, `deepdock devices release <id>`
|
|
102
|
+
(admin).
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Calling actions
|
|
2
|
+
|
|
3
|
+
All paths are under `/v1/devices/{deviceId}/systems/{system}/actions/{action}`. `deviceId` is the
|
|
4
|
+
UUID from pairing; `system` and `action` are the slugs from the action index.
|
|
5
|
+
|
|
6
|
+
Every input value is text and is normalised to Unicode NFC before anything else happens; a
|
|
7
|
+
composed and a decomposed spelling are the same value.
|
|
8
|
+
|
|
9
|
+
## Reads: `GET`
|
|
10
|
+
|
|
11
|
+
Input as query parameters; `limit` (1 to 250, default 25) and `cursor` for paging.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
curl "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/searchCustomers?lastName=M%C3%BCl&limit=50" \
|
|
15
|
+
-H "Authorization: Bearer $DEEPDOCK_API_KEY"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{ "data": { "items": [ { "customerId": 1001, "lastName": "Müller", "city": "Beispielstadt" } ], "nextCursor": null } }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `nextCursor` is set only when a page came back full; pass it back as `cursor` unchanged. It is
|
|
23
|
+
opaque: never parse, build or store it beyond the next request.
|
|
24
|
+
- Items are the customer's records. Values can be `null` where the schema says so. Dates and other
|
|
25
|
+
non-numeric values arrive as text in the system's own format (the schema and description say
|
|
26
|
+
which); numbers as JSON numbers.
|
|
27
|
+
- A read's description may say a field is a **prefix match** and what it ignores (case,
|
|
28
|
+
punctuation, umlaut spelling). Design your search UI around that rule rather than filtering
|
|
29
|
+
client-side.
|
|
30
|
+
|
|
31
|
+
## Writes: `POST` (create) or `PATCH` (update)
|
|
32
|
+
|
|
33
|
+
Body: a JSON object whose values are all strings (numbers and ids too, as strings), with only the
|
|
34
|
+
fields the schema lists (`additionalProperties: false`) and all of its `required` ones. Header
|
|
35
|
+
`Idempotency-Key`, required.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
curl -X POST "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/createNote" \
|
|
39
|
+
-H "Authorization: Bearer $DEEPDOCK_API_KEY" \
|
|
40
|
+
-H "Idempotency-Key: order-8f1c2a-create-note" \
|
|
41
|
+
-H "Content-Type: application/json" \
|
|
42
|
+
-d '{"text":"TEST-DEEPDOCK-K3M9QA","orderNumber":"1001"}'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- A create answers `201` with `{ data: { <returned fields> } }`, typically the new record's id. A
|
|
46
|
+
replay of the same key answers `200` with the same data and `Idempotent-Replayed: true`.
|
|
47
|
+
- An update answers `200` whether it ran now or is a replay; the header tells you which.
|
|
48
|
+
- The returned id is the system's own id (for example `noteId`). Store it as the system gave it;
|
|
49
|
+
do not predict or re-key it. Result field types come from each action's own output schema: a
|
|
50
|
+
create typically returns the new id as an integer, while an update echoes the id you sent as a
|
|
51
|
+
string. Do not reuse one action's result type for another.
|
|
52
|
+
- Each update under a new key writes again and may add an entry to the system's own change log,
|
|
53
|
+
even when values are unchanged. Do not "update" to no-op.
|
|
54
|
+
|
|
55
|
+
Full idempotency rules: [idempotency.md](idempotency.md).
|
|
56
|
+
|
|
57
|
+
## Authoring and reference fields
|
|
58
|
+
|
|
59
|
+
Some writes need a reference the system already knows: an author user for an entry, an open case
|
|
60
|
+
of a customer, the note id of an update. The pattern is always the same:
|
|
61
|
+
|
|
62
|
+
1. Read the reference with the matching read action (for example a users read, a case read, a
|
|
63
|
+
customer search).
|
|
64
|
+
2. Pass its id back as a string field in the write.
|
|
65
|
+
3. Handle the action-specific refusal the description names: for example `422 unknown_author` when
|
|
66
|
+
the user is not one the system lists, or `422 no_open_case` when the customer has no open case and
|
|
67
|
+
the action does not open one. Both mean nothing was written; fix the reference, do not retry
|
|
68
|
+
blindly.
|
|
69
|
+
|
|
70
|
+
Never invent an id, copy one from another device, or hard-code one from a test run.
|
|
71
|
+
|
|
72
|
+
## Timeouts and latency
|
|
73
|
+
|
|
74
|
+
- Every action call is sent to the customer's PC and run there. Expect a read to take from a few
|
|
75
|
+
hundred milliseconds to a few seconds; a write about the same plus its verification.
|
|
76
|
+
- Deepdock waits at most 20 seconds for the device, then answers `503 device_offline`. Set your HTTP
|
|
77
|
+
client timeout to 30 seconds so you receive the code instead of timing out first. A write that
|
|
78
|
+
timed out or failed on the network on your side may still have landed: retry with the same
|
|
79
|
+
`Idempotency-Key`.
|
|
80
|
+
- An answer that is not JSON (a proxy's 502 or 504 page) is a transport failure: for a write, the
|
|
81
|
+
same rule, retry with the same key; never treat it as refused.
|
|
82
|
+
- Run calls sequentially per device where order matters; there is no documented concurrency limit,
|
|
83
|
+
but the device is one PC in one office.
|
|
84
|
+
- `GET /v1/devices/{deviceId}/systems` is a live call too; cache it for the duration of a user
|
|
85
|
+
session rather than calling it per request.
|
|
86
|
+
|
|
87
|
+
## Rate limits
|
|
88
|
+
|
|
89
|
+
The only documented limit is on pairing: 5 failed claim attempts per key per rolling 60 seconds
|
|
90
|
+
(`429 rate_limited`). Handle `429` generically anyway (honour `Retry-After` when present, back off,
|
|
91
|
+
do not loop), since limits can be added additively.
|
|
92
|
+
|
|
93
|
+
## Response headers worth reading
|
|
94
|
+
|
|
95
|
+
| Header | When | Meaning |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `Idempotent-Replayed: true` | a write's `200` | the stored result of an earlier run; nothing written again |
|
|
98
|
+
| `Allow` | `405 invalid_request` | the method the action has |
|
|
99
|
+
| `WWW-Authenticate: Bearer` | `401` | the key was refused |
|
|
100
|
+
| `Retry-After` | `429` | seconds to wait, when present |
|
|
101
|
+
|
|
102
|
+
## The envelope
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
type Ok<T> = { data: T }
|
|
106
|
+
type Err = { error: { code: string; message: string; requestId?: string; systems?: string[] } }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`requestId` comes with a `500`: give it to Deepdock support. `systems` comes with
|
|
110
|
+
`system_not_on_device`: the systems this device reported or you were assigned. Unwrap once in one
|
|
111
|
+
place; never read `message` in code paths, only show it or log it.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# The `deepdock` CLI
|
|
2
|
+
|
|
3
|
+
Package `@deepdockai/cli`. Run with `npx @deepdockai/cli <command>` or
|
|
4
|
+
`bunx @deepdockai/cli <command>`; installed globally it is `deepdock`. If the CLI is missing,
|
|
5
|
+
install it; do not fall back to hand-written HTTP calls with a key in an argument.
|
|
6
|
+
|
|
7
|
+
The CLI runs with the person's sign-in and manages things: organisations, devices, keys, what a
|
|
8
|
+
device offers. It never runs an action and never holds an API key. Actions run from your code with
|
|
9
|
+
the API key the CLI wrote to the app's env file.
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
| Command | What it does | Who |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `auth login` | opens a link to the dashboard where the human clicks Approve; prints `Signed in as <email>.` and the person's organisations; the sign-in lasts 90 days | anyone |
|
|
16
|
+
| `auth status` | signed-in person, sign-in expiry, organisations, default organisation, the API keys this CLI created (id, prefix, expiry, where it was written); prints the same document and exits 5 when not signed in | anyone |
|
|
17
|
+
| `auth logout` | revokes the sign-in and every API key this CLI created, removes the entries it wrote, says what it revoked | anyone |
|
|
18
|
+
| `org list` | the person's organisations: slug, name, role, which one is the default | member |
|
|
19
|
+
| `org select [slug]` | sets the default organisation; without a slug, a pick-list on a terminal | member |
|
|
20
|
+
| `systems list` | the systems and actions assigned to the organisation | member |
|
|
21
|
+
| `systems openapi <system>` | the system's OpenAPI document on stdout; redirect it to a file | member |
|
|
22
|
+
| `devices list` | paired devices with id, label, status, systems, last seen | member |
|
|
23
|
+
| `devices pair <code> [--label <text>]` | claim a device by the code on its screen | member |
|
|
24
|
+
| `devices rename <id> <label>` | rename a device | member |
|
|
25
|
+
| `devices release <id>` | release a device (asks on a terminal; `--yes` otherwise) | admin |
|
|
26
|
+
| `devices systems <id>` | the device's live systems and action state, paused reasons included; `device_offline` is a normal answer | member |
|
|
27
|
+
| `actions list <device> <system>` | the action index: every action with its method and field schemas | member |
|
|
28
|
+
| `keys list` | the organisation's API keys: name, prefix, created, last used, expires, status | member |
|
|
29
|
+
| `keys create <name> --expires <7d\|30d\|90d> --write-env <file>` | create an API key: opens a link, a human approves "Create an API key in <Org>?", the CLI receives the key once and writes `DEEPDOCK_API_KEY=<key>` into the file. It keeps no copy and shows the value to nobody. Answers id, name, prefix, expiry and where it was written | admin |
|
|
30
|
+
| `keys revoke <id>` | revoke a key (asks on a terminal; `--yes` otherwise); removes the env entry if this CLI wrote it | admin |
|
|
31
|
+
| `incidents list` | incidents of the last 30 days | member |
|
|
32
|
+
| `skill [--agent claude\|codex\|cursor\|all]` | writes this skill into the current repository | anyone |
|
|
33
|
+
|
|
34
|
+
Every command that acts in an organisation uses `--org <slug>` after the command, else
|
|
35
|
+
`DEEPDOCK_ORG`, else the default from `org select`; with none it stops with `org_required` and
|
|
36
|
+
lists the organisations. Always pass `--org` from an agent: `org select` changes the human's
|
|
37
|
+
default in every terminal.
|
|
38
|
+
|
|
39
|
+
Global flags: `--org <slug>`, `--json`, `--yes`, `-h`, `-v`. Environment: `DEEPDOCK_ORG`,
|
|
40
|
+
`DEEPDOCK_BASE_URL`, `NO_COLOR`.
|
|
41
|
+
|
|
42
|
+
## Waiting for the browser
|
|
43
|
+
|
|
44
|
+
`auth login` and `keys create` need a human to click Approve. Without a terminal (your shell)
|
|
45
|
+
they do not wait: they print the request and exit 0, and you finish it later:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
deepdock auth login --json # { "data": { "pending": { "request": "rq_…", "url": "…", "expiresAt": "…" } } }
|
|
49
|
+
# show the human the url and wait for them to approve, then:
|
|
50
|
+
deepdock auth login --wait rq_…
|
|
51
|
+
deepdock keys create "<project> dev" --expires 30d --write-env .env.local --org <slug> --json
|
|
52
|
+
deepdock keys create --wait rq_… --json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A request lives 10 minutes; `--wait` on an expired one answers `approval_expired`. Never try to
|
|
56
|
+
approve for the human.
|
|
57
|
+
|
|
58
|
+
## Output
|
|
59
|
+
|
|
60
|
+
JSON is the default when stdout is not a terminal; pass `--json` to be sure. One document on
|
|
61
|
+
stdout:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{ "data": <the API's data, unchanged>, "meta": { "org": { "slug": "…", "name": "…" }, "requestId": "…" } }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`data` is what the API answered, so what you see is what your code will receive. `meta` holds the
|
|
68
|
+
organisation the command acted in, the request id, and, where it applies, `written` (`{ file,
|
|
69
|
+
name }` from `keys create`) and `pending` (from `--no-wait`). `keys create` builds its `data` from
|
|
70
|
+
id, name, prefix, createdAt and expiresAt; the key value is in neither stream.
|
|
71
|
+
|
|
72
|
+
A failure is one document on stderr: the API's error object unchanged plus what to do:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{ "error": { "code": "device_offline", "message": "…", "requestId": "…" }, "meta": { "org": { "…": "…" }, "hint": "Retry later." } }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Exit codes
|
|
79
|
+
|
|
80
|
+
| Exit | Meaning | Codes |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| 0 | done | |
|
|
83
|
+
| 1 | something on the command line to fix | `invalid_usage` (a bad flag, an unknown slug, a question with no terminal and no `--yes`, an env file git does not ignore), `org_required` |
|
|
84
|
+
| 2 | the API refused the request; do not repeat it unchanged | `invalid_request`, `not_found`, `device_not_found`, `pairing_code_invalid`, `other_partner_build`, `rate_limited`, `forbidden`, `conflict` and the other refusals |
|
|
85
|
+
| 3 | retry later | `device_offline`, `action_paused`, `system_unavailable`, `target_busy`, `temporarily_unavailable`, `internal_error`, `connection_lost` |
|
|
86
|
+
| 4 | the CLI is too old for the API: run `npx @deepdockai/cli@latest` | `client_outdated` |
|
|
87
|
+
| 5 | sign-in needed: run `deepdock auth login` | `sign_in_needed`, `approval_denied`, `approval_expired` |
|
|
88
|
+
|
|
89
|
+
Branch on the exit code or on `error.code`, never on `message`.
|
|
90
|
+
|
|
91
|
+
## What the CLI never does
|
|
92
|
+
|
|
93
|
+
- It never prints a key value, neither the sign-in nor an API key, under any flag.
|
|
94
|
+
- It never runs an action and never holds an API key: `actions list` shows what a device offers;
|
|
95
|
+
your code runs the actions.
|
|
96
|
+
- It never names a device by label or picks one for you: a device is its full id, every time.
|
|
97
|
+
- It never retries: exit 3 says retry, you decide when.
|
|
@@ -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)})
|