@deepdockai/cli 1.0.0 → 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.
- package/CHANGELOG.md +10 -0
- package/README.md +15 -5
- package/dist/deepdock.js +107 -27
- package/package.json +1 -1
- package/skill/SKILL.md +2 -2
- package/skill/references/calling-actions.md +35 -2
- package/skill/references/cli.md +2 -2
- package/skill/references/concepts.md +6 -4
- package/skill/references/discovery.md +4 -2
- package/skill/references/errors.md +20 -6
- package/skill/references/idempotency.md +2 -2
- package/skill/references/testing.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.2
|
|
4
|
+
|
|
5
|
+
- The install commands no longer pin a major version: `npx @deepdockai/cli auth login`, `npm install -g @deepdockai/cli`.
|
|
6
|
+
|
|
7
|
+
## 1.0.1
|
|
8
|
+
|
|
9
|
+
- The agent skill: page with `nextCursor` until it is `null` (a short page can carry a cursor), and cover the codes `access_outdated`, `access_missing`, `record_locked`, `record_not_editable`, `unknown_provider`, `payload_too_large` and the system status `unknown`, in the skill and the CLI's exit-code table.
|
|
10
|
+
- The agent skill: one statement on how to fetch the OpenAPI document, and a MEDICAL OFFICE recipe with its action names.
|
|
11
|
+
- The agent skill states the id rule in full: reads and creates answer ids as integers, a create's new id included; every id you send is a string of digits without a leading zero, at most 10 characters, in a query string and a write body alike.
|
|
12
|
+
|
|
3
13
|
## 1.0.0
|
|
4
14
|
|
|
5
15
|
First release of `@deepdockai/cli`, the `deepdock` command.
|
package/README.md
CHANGED
|
@@ -10,14 +10,14 @@ This repository holds the partner CLI, published as [`@deepdockai/cli`](https://
|
|
|
10
10
|
with the command `deepdock`, and the skill it installs for coding agents.
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npx @deepdockai/cli
|
|
13
|
+
npx @deepdockai/cli auth login
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
The CLI manages: your organisations, the devices paired to them, and your API keys, and it lists
|
|
17
17
|
the systems and actions a device offers. It never runs an action and never keeps or uses an API
|
|
18
18
|
key. Actions run from your own code with a key the CLI wrote into your env file.
|
|
19
19
|
|
|
20
|
-
Node 22.13 or newer. Installed globally (`npm install -g @deepdockai/cli
|
|
20
|
+
Node 22.13 or newer. Installed globally (`npm install -g @deepdockai/cli`) the command is
|
|
21
21
|
`deepdock`; the examples below use that form.
|
|
22
22
|
|
|
23
23
|
## Before you start
|
|
@@ -94,7 +94,7 @@ deepdock skill
|
|
|
94
94
|
| `auth logout` | revoke the sign-in and every key this CLI created |
|
|
95
95
|
| `org list`, `org select [slug]` | your organisations; set the default one |
|
|
96
96
|
| `systems list` | the systems and actions assigned to your organisation |
|
|
97
|
-
| `systems openapi <system>` | a system's OpenAPI document on stdout;
|
|
97
|
+
| `systems openapi <system>` | a system's OpenAPI document on stdout, bare when redirected; save it to a file |
|
|
98
98
|
| `devices list` | paired devices with id, label, status, systems and last seen |
|
|
99
99
|
| `devices pair <code> [--label <text>]` | claim a device by the code on its screen |
|
|
100
100
|
| `devices rename <id> <label>`, `devices release <id>` | rename a device; unpair it from the organisation (admin) |
|
|
@@ -144,7 +144,9 @@ On a terminal you get tables and plain sentences. When stdout is not a terminal,
|
|
|
144
144
|
}
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
`systems openapi` is the one exception off a terminal: it prints the OpenAPI document itself, so
|
|
148
|
+
`deepdock systems openapi <system> > openapi.json` saves a document a generator can read (`--json`
|
|
149
|
+
gives the envelope). For list and show commands `data` is the API's answer unchanged, so what the CLI shows is what
|
|
148
150
|
your code will receive; `auth`, `org select` and `keys create` build it from named fields. A
|
|
149
151
|
failure is one document on stderr, `{ "error": { "code", "message" }, "meta": { "hint" } }`,
|
|
150
152
|
with the API's error object unchanged. Switch on `code` or on the exit code, never on `message`.
|
|
@@ -235,7 +237,7 @@ macOS and Linux need nothing special. On Windows:
|
|
|
235
237
|
CLI created (never their values) and a pending approval request. A file other users can read
|
|
236
238
|
is refused with the `chmod` to run.
|
|
237
239
|
- **Environment:** `DEEPDOCK_ORG`, `DEEPDOCK_BASE_URL` (https, or http for localhost only),
|
|
238
|
-
`NO_COLOR
|
|
240
|
+
`NO_COLOR`, `FORCE_COLOR` (colours messages on a stream that is not a terminal; `NO_COLOR` wins). A sign-in or pending request is never sent to a different base URL than the one it
|
|
239
241
|
was made against.
|
|
240
242
|
|
|
241
243
|
Report a vulnerability by email to torben@deepdock.ai, not in a public issue: [SECURITY.md](SECURITY.md).
|
|
@@ -260,3 +262,11 @@ DEEPDOCK_BASE_URL=http://127.0.0.1:<port> node dist/deepdock.js auth login
|
|
|
260
262
|
## Licence
|
|
261
263
|
|
|
262
264
|
[MIT](LICENSE).
|
|
265
|
+
|
|
266
|
+
## Releases
|
|
267
|
+
|
|
268
|
+
Publishing is automatic once a change is merged; nobody runs `npm publish`. To ship a change, bump `version` in `package.json` and add a matching
|
|
269
|
+
`## <version>` entry to `CHANGELOG.md` in your pull request. A check on the pull request requires
|
|
270
|
+
the bump whenever a published file changes. When the pull request merges to `main`, the release
|
|
271
|
+
workflow tests the code, scans the exact tarball, publishes it to npm with provenance, then tags
|
|
272
|
+
`v<version>` and creates a GitHub Release with the changelog entry as notes.
|
package/dist/deepdock.js
CHANGED
|
@@ -10864,10 +10864,17 @@ var EXIT_CODES = {
|
|
|
10864
10864
|
check_failed: 2,
|
|
10865
10865
|
idempotency_conflict: 2,
|
|
10866
10866
|
conflict: 2,
|
|
10867
|
+
already_member: 2,
|
|
10867
10868
|
no_open_case: 2,
|
|
10868
10869
|
unknown_author: 2,
|
|
10870
|
+
unknown_provider: 2,
|
|
10871
|
+
record_locked: 2,
|
|
10872
|
+
record_not_editable: 2,
|
|
10873
|
+
payload_too_large: 2,
|
|
10869
10874
|
device_offline: 3,
|
|
10870
10875
|
action_paused: 3,
|
|
10876
|
+
access_outdated: 3,
|
|
10877
|
+
access_missing: 3,
|
|
10871
10878
|
system_unavailable: 3,
|
|
10872
10879
|
target_busy: 3,
|
|
10873
10880
|
temporarily_unavailable: 3,
|
|
@@ -10999,6 +11006,7 @@ var DEVICE_SYSTEM_COLUMNS = [
|
|
|
10999
11006
|
"systems",
|
|
11000
11007
|
"system",
|
|
11001
11008
|
"name",
|
|
11009
|
+
"action",
|
|
11002
11010
|
"version",
|
|
11003
11011
|
"status",
|
|
11004
11012
|
"state",
|
|
@@ -11035,12 +11043,37 @@ var scalar = (value) => {
|
|
|
11035
11043
|
return String(value);
|
|
11036
11044
|
return "";
|
|
11037
11045
|
};
|
|
11046
|
+
var GOOD = new Set(["connected", "active", "running", "ok"]);
|
|
11047
|
+
var WARN = new Set(["offline", "paused", "outdated", "unavailable", "degraded"]);
|
|
11048
|
+
var BAD = new Set(["revoked", "failed", "denied", "expired"]);
|
|
11049
|
+
var cellFor = (key, value) => {
|
|
11050
|
+
const text = scalar(value);
|
|
11051
|
+
if (text === "")
|
|
11052
|
+
return text;
|
|
11053
|
+
if (key === "reason" || key === "pausedReason")
|
|
11054
|
+
return words(text);
|
|
11055
|
+
if (key === "revokedAt")
|
|
11056
|
+
return { text, style: "red" };
|
|
11057
|
+
if (key === "name" || key === "action")
|
|
11058
|
+
return { text, style: "bold" };
|
|
11059
|
+
if (key.endsWith("At") || key === "since")
|
|
11060
|
+
return { text, style: "dim" };
|
|
11061
|
+
if (key === "status" || key === "state") {
|
|
11062
|
+
if (GOOD.has(text))
|
|
11063
|
+
return { text, style: "green" };
|
|
11064
|
+
if (WARN.has(text))
|
|
11065
|
+
return { text, style: "yellow" };
|
|
11066
|
+
if (BAD.has(text))
|
|
11067
|
+
return { text, style: "red" };
|
|
11068
|
+
}
|
|
11069
|
+
return text;
|
|
11070
|
+
};
|
|
11038
11071
|
var genericTable = (items, style, allow) => {
|
|
11039
11072
|
const records = items.filter(isRecord);
|
|
11040
11073
|
const shown = allow.filter((key) => records.some((record) => (key in record) && isScalar(record[key])));
|
|
11041
11074
|
if (shown.length === 0)
|
|
11042
11075
|
return NOTHING_TO_SHOW;
|
|
11043
|
-
return renderTable(shown.map(columnTitle), records.map((record) => shown.map((key) =>
|
|
11076
|
+
return renderTable(shown.map(columnTitle), records.map((record) => shown.map((key) => cellFor(key, record[key]))), style);
|
|
11044
11077
|
};
|
|
11045
11078
|
var relativeTime = (iso, now) => {
|
|
11046
11079
|
if (typeof iso !== "string")
|
|
@@ -11084,10 +11117,10 @@ var deviceTable = (devices, now, style) => {
|
|
|
11084
11117
|
return genericTable(records, style, DEVICE_COLUMNS);
|
|
11085
11118
|
return renderTable(["ID", "LABEL", "STATUS", "SYSTEMS", "LAST SEEN"], records.map((device) => [
|
|
11086
11119
|
field(device, "id"),
|
|
11087
|
-
field(device, "label"),
|
|
11120
|
+
{ text: field(device, "label"), style: "bold" },
|
|
11088
11121
|
deviceStatusCell(field(device, "status")),
|
|
11089
11122
|
systemsLabel(device),
|
|
11090
|
-
relativeTime(device.lastSeenAt, now)
|
|
11123
|
+
{ text: relativeTime(device.lastSeenAt, now), style: "dim" }
|
|
11091
11124
|
]), style);
|
|
11092
11125
|
};
|
|
11093
11126
|
var isScalar = (value) => value === null || ["string", "number", "boolean"].includes(typeof value);
|
|
@@ -11151,6 +11184,32 @@ var countValue = (data, word) => {
|
|
|
11151
11184
|
return Object.values(data).reduce((sum, item) => sum + countValue(item, word), 0);
|
|
11152
11185
|
return 0;
|
|
11153
11186
|
};
|
|
11187
|
+
var roleLabel = (role) => {
|
|
11188
|
+
const bare = role.replace(/^org:/, "").replace(/[_-]+/g, " ");
|
|
11189
|
+
return bare === "" ? role : bare.charAt(0).toUpperCase() + bare.slice(1);
|
|
11190
|
+
};
|
|
11191
|
+
var orgTable = (orgs, defaultSlug, style) => {
|
|
11192
|
+
const isDefault = (org) => org.slug !== null && org.slug === defaultSlug;
|
|
11193
|
+
const ordered = [...orgs].sort((a, b) => Number(isDefault(b)) - Number(isDefault(a)));
|
|
11194
|
+
return renderTable(["NAME", "SLUG", "ROLE", ""], ordered.map((org) => [
|
|
11195
|
+
{ text: org.name, style: "bold" },
|
|
11196
|
+
{ text: org.slug ?? "(no slug)", style: "dim" },
|
|
11197
|
+
roleLabel(org.role),
|
|
11198
|
+
isDefault(org) ? { text: "default", style: "green" } : ""
|
|
11199
|
+
]), style);
|
|
11200
|
+
};
|
|
11201
|
+
var words = (code) => code.replace(/_/g, " ");
|
|
11202
|
+
var systemsTable = (items, style) => {
|
|
11203
|
+
const records = items.filter(isRecord);
|
|
11204
|
+
if (!records.some((item) => ("system" in item)))
|
|
11205
|
+
return genericTable(records, style, SYSTEM_COLUMNS);
|
|
11206
|
+
return renderTable(["SYSTEM", "NAME", "ACTIONS"], records.map((item) => {
|
|
11207
|
+
const actions = Array.isArray(item.actions) ? item.actions.filter(isRecord) : undefined;
|
|
11208
|
+
const writes = actions?.filter((action) => action.kind === "write").length ?? 0;
|
|
11209
|
+
const counted = actions ? `${actions.length} (${actions.length - writes} read, ${writes} write)` : "";
|
|
11210
|
+
return [{ text: field(item, "system"), style: "bold" }, field(item, "label"), counted];
|
|
11211
|
+
}), style);
|
|
11212
|
+
};
|
|
11154
11213
|
|
|
11155
11214
|
// src/commands/actions.ts
|
|
11156
11215
|
var fieldNames = (input) => {
|
|
@@ -11159,14 +11218,25 @@ var fieldNames = (input) => {
|
|
|
11159
11218
|
const required = Array.isArray(input.required) ? input.required : [];
|
|
11160
11219
|
return Object.keys(input.properties).map((name) => required.includes(name) ? name : `${name} (optional)`).join(", ");
|
|
11161
11220
|
};
|
|
11221
|
+
var summary = (description) => {
|
|
11222
|
+
if (typeof description !== "string")
|
|
11223
|
+
return;
|
|
11224
|
+
const line = description.split(`
|
|
11225
|
+
`).find((text) => text.trim() !== "")?.trim() ?? "";
|
|
11226
|
+
const sentence = /^.*?[.!?](?=\s+\p{Lu}|$)/u.exec(line)?.[0] ?? line;
|
|
11227
|
+
const chars = Array.from(sentence);
|
|
11228
|
+
return chars.length > 160 ? `${chars.slice(0, 159).join("").trimEnd()}…` : sentence;
|
|
11229
|
+
};
|
|
11162
11230
|
var actionRow = (action) => {
|
|
11163
11231
|
const method = typeof action.method === "string" ? action.method : undefined;
|
|
11164
11232
|
const fields = fieldNames(action.input);
|
|
11165
|
-
const { name, method: _method, input: _input, output: _output, ...rest } = action;
|
|
11233
|
+
const { name, method: _method, input: _input, output: _output, description, ...rest } = action;
|
|
11234
|
+
const brief = summary(description);
|
|
11166
11235
|
return {
|
|
11167
11236
|
...name === undefined ? {} : { name },
|
|
11168
11237
|
...method ? { kind: method.toUpperCase() === "GET" ? "read" : "write" } : {},
|
|
11169
11238
|
...fields ? { fields } : {},
|
|
11239
|
+
...brief ? { description: brief } : {},
|
|
11170
11240
|
...rest
|
|
11171
11241
|
};
|
|
11172
11242
|
};
|
|
@@ -11342,7 +11412,7 @@ import { spawn } from "node:child_process";
|
|
|
11342
11412
|
import { homedir, hostname } from "node:os";
|
|
11343
11413
|
|
|
11344
11414
|
// src/version.ts
|
|
11345
|
-
var VERSION = "1.0.
|
|
11415
|
+
var VERSION = "1.0.2";
|
|
11346
11416
|
|
|
11347
11417
|
// src/runtime.ts
|
|
11348
11418
|
var DEFAULT_BASE_URL = "https://api.deepdock.ai";
|
|
@@ -11831,7 +11901,8 @@ var updateConfig = (rt, change) => withLock(rt, async () => {
|
|
|
11831
11901
|
// src/output/styles.ts
|
|
11832
11902
|
import { styleText } from "node:util";
|
|
11833
11903
|
var makeStyler = (rt, stream) => {
|
|
11834
|
-
const
|
|
11904
|
+
const forced = rt.env.FORCE_COLOR !== undefined && !["", "0"].includes(rt.env.FORCE_COLOR);
|
|
11905
|
+
const enabled = (stream.isTTY || forced) && !rt.env.NO_COLOR && rt.env.TERM !== "dumb";
|
|
11835
11906
|
return (name, text) => enabled ? styleText(name, text, { validateStream: false }) : text;
|
|
11836
11907
|
};
|
|
11837
11908
|
|
|
@@ -11958,7 +12029,7 @@ Examples
|
|
|
11958
12029
|
|
|
11959
12030
|
More
|
|
11960
12031
|
Exit codes: \`deepdock help exit-codes\`. API reference per system: \`deepdock systems openapi <system>\`.
|
|
11961
|
-
Environment: DEEPDOCK_ORG, DEEPDOCK_BASE_URL, NO_COLOR.
|
|
12032
|
+
Environment: DEEPDOCK_ORG, DEEPDOCK_BASE_URL, NO_COLOR, FORCE_COLOR.
|
|
11962
12033
|
Actions run with an API key from your own code, never from this CLI: \`deepdock keys create --help\`.
|
|
11963
12034
|
`;
|
|
11964
12035
|
var AUTH_LOGIN_HELP = `Sign in to Deepdock as yourself.
|
|
@@ -12340,10 +12411,6 @@ var upsertPending = async (ctx, pending) => {
|
|
|
12340
12411
|
];
|
|
12341
12412
|
});
|
|
12342
12413
|
};
|
|
12343
|
-
var orgRows = (orgs) => orgs.map((org) => [org.slug ?? "(no slug)", org.name, org.role]);
|
|
12344
|
-
var orgTable = (ctx, orgs) => renderTable(["SLUG", "NAME", "ROLE"], orgRows(orgs), ctx.out).split(`
|
|
12345
|
-
`).map((line) => ` ${line}`).join(`
|
|
12346
|
-
`);
|
|
12347
12414
|
var finishLogin = async (ctx, pending) => {
|
|
12348
12415
|
const { answer, requestId } = await pollUntilApproved(ctx.rt, ctx.api, "loginPoll", {
|
|
12349
12416
|
kind: "login",
|
|
@@ -12380,7 +12447,7 @@ var finishLogin = async (ctx, pending) => {
|
|
|
12380
12447
|
if (answer.organisations.length === 0) {
|
|
12381
12448
|
lines.push("You are not a member of an organisation yet.");
|
|
12382
12449
|
} else {
|
|
12383
|
-
lines.push("Your organisations:", orgTable(
|
|
12450
|
+
lines.push("Your organisations:", indented(orgTable(answer.organisations, config.defaultOrg, ctx.out)), "");
|
|
12384
12451
|
lines.push("Choose one with `deepdock org select <slug>` or pass --org to each command.");
|
|
12385
12452
|
}
|
|
12386
12453
|
return {
|
|
@@ -12404,6 +12471,7 @@ var login = async (ctx, options) => {
|
|
|
12404
12471
|
}
|
|
12405
12472
|
assertSameBaseUrl(pending.baseUrl, ctx.baseUrl, "request");
|
|
12406
12473
|
assertNotExpired(ctx.rt, "login", pending.expiresAt);
|
|
12474
|
+
ctx.note("Waiting for approval in the browser.");
|
|
12407
12475
|
return finishLogin(ctx, pending);
|
|
12408
12476
|
}
|
|
12409
12477
|
const started = await startLogin(ctx.api, ctx.rt);
|
|
@@ -12442,6 +12510,9 @@ var keyJson = (ctx, key) => ({
|
|
|
12442
12510
|
expiresAt: key.expiresAt,
|
|
12443
12511
|
written: { file: displayPath(ctx.rt, key.file), name: key.variable }
|
|
12444
12512
|
});
|
|
12513
|
+
var indented = (table) => table.split(`
|
|
12514
|
+
`).map((line) => ` ${line}`).join(`
|
|
12515
|
+
`);
|
|
12445
12516
|
var status = async (ctx) => {
|
|
12446
12517
|
const config = await ctx.config();
|
|
12447
12518
|
const baseUrl = ctx.baseUrl;
|
|
@@ -12472,11 +12543,15 @@ var status = async (ctx) => {
|
|
|
12472
12543
|
organisations: signedIn ? config.organisations ?? [] : [],
|
|
12473
12544
|
apiKeysCreated
|
|
12474
12545
|
};
|
|
12546
|
+
const defaultOrg = requestedSlug(ctx.opts.org, ctx.rt.env, config);
|
|
12475
12547
|
const lines = [];
|
|
12476
12548
|
if (signedIn && config.signIn) {
|
|
12477
|
-
lines.push(`Signed in as ${config.signIn.email} (sign-in valid until ${config.signIn.expiresAt.slice(0, 10)})
|
|
12478
|
-
if (
|
|
12479
|
-
lines.push("",
|
|
12549
|
+
lines.push(`Signed in as ${config.signIn.email} (sign-in valid until ${config.signIn.expiresAt.slice(0, 10)}).`);
|
|
12550
|
+
if (baseUrl !== DEFAULT_BASE_URL)
|
|
12551
|
+
lines.push(ctx.out("yellow", `API: ${baseUrl} (not production)`));
|
|
12552
|
+
if (data.organisations.length > 0) {
|
|
12553
|
+
lines.push("", "Organisations:", indented(orgTable(data.organisations, defaultOrg, ctx.out)), "", defaultOrg ? "Switch the default with `deepdock org select <slug>`." : "No default yet: choose one with `deepdock org select <slug>`.");
|
|
12554
|
+
}
|
|
12480
12555
|
} else {
|
|
12481
12556
|
lines.push(config.signIn ? "Your sign-in has expired. Run `deepdock auth login`." : "Not signed in. Run `deepdock auth login`.");
|
|
12482
12557
|
}
|
|
@@ -12641,7 +12716,7 @@ var updateNotes = (ctx, devices) => {
|
|
|
12641
12716
|
const update = field(device, "updateAvailable");
|
|
12642
12717
|
if (update) {
|
|
12643
12718
|
const name = field(device, "label") || field(device, "id");
|
|
12644
|
-
ctx.note(`${stripControl(name)}: update available (client ${stripControl(update)}).`);
|
|
12719
|
+
ctx.note(ctx.err("yellow", `${stripControl(name)}: update available (client ${stripControl(update)}).`));
|
|
12645
12720
|
}
|
|
12646
12721
|
}
|
|
12647
12722
|
};
|
|
@@ -12878,6 +12953,7 @@ var create = async (ctx, name, options) => {
|
|
|
12878
12953
|
if (!pending) {
|
|
12879
12954
|
throw usageError(`There is no pending key request ${options.wait}.`, "Start one with `deepdock keys create ... --no-wait`.");
|
|
12880
12955
|
}
|
|
12956
|
+
ctx.note("Waiting for approval in the browser.");
|
|
12881
12957
|
return finishKey(ctx, pending, false);
|
|
12882
12958
|
}
|
|
12883
12959
|
if (name === undefined || name.trim() === "" || name.length > 100 || stripControl(name) !== name) {
|
|
@@ -12986,12 +13062,12 @@ var list3 = async (ctx) => {
|
|
|
12986
13062
|
current.organisations = orgs;
|
|
12987
13063
|
});
|
|
12988
13064
|
const defaultOrg = requestedSlug(ctx.opts.org, ctx.rt.env, config);
|
|
12989
|
-
const text = orgs.length === 0 ? "You are not a member of an organisation yet." :
|
|
12990
|
-
|
|
12991
|
-
|
|
12992
|
-
org
|
|
12993
|
-
|
|
12994
|
-
|
|
13065
|
+
const text = orgs.length === 0 ? "You are not a member of an organisation yet." : [
|
|
13066
|
+
orgTable(orgs, defaultOrg, ctx.out),
|
|
13067
|
+
"",
|
|
13068
|
+
"Switch the default with `deepdock org select <slug>`."
|
|
13069
|
+
].join(`
|
|
13070
|
+
`);
|
|
12995
13071
|
return {
|
|
12996
13072
|
data: response.data,
|
|
12997
13073
|
meta: response.requestId ? { requestId: response.requestId } : {},
|
|
@@ -13018,7 +13094,7 @@ var select = async (ctx, slug) => {
|
|
|
13018
13094
|
throw usageError("You are not a member of an organisation yet.");
|
|
13019
13095
|
ctx.note("Which organisation should be the default?");
|
|
13020
13096
|
choices.forEach((org, index) => {
|
|
13021
|
-
ctx.note(` ${index + 1} ${org.slug.padEnd(18)} ${org.name.padEnd(24)} ${org.role}`);
|
|
13097
|
+
ctx.note(stripControl(` ${index + 1} ${org.slug.padEnd(18)} ${org.name.padEnd(24)} ${roleLabel(org.role)}`));
|
|
13022
13098
|
});
|
|
13023
13099
|
const current = Math.max(1, choices.findIndex((org) => org.slug === config.defaultOrg) + 1);
|
|
13024
13100
|
const index = await pickIndex(ctx.rt, choices.length, current);
|
|
@@ -13171,10 +13247,10 @@ var registerSystems = (program, run) => {
|
|
|
13171
13247
|
systems.command("list").description("List the systems assigned to the organisation").action((_options, cmd) => run(cmd, async (ctx) => {
|
|
13172
13248
|
const response = await ctx.callInOrg({ method: "GET", path: "/partner/systems" });
|
|
13173
13249
|
const items = Array.isArray(response.data) ? response.data : [];
|
|
13174
|
-
const text = items.length === 0 ? "No systems are assigned to this organisation." :
|
|
13250
|
+
const text = items.length === 0 ? "No systems are assigned to this organisation." : systemsTable(items, ctx.out);
|
|
13175
13251
|
return { data: response.data, meta: { requestId: response.requestId }, text };
|
|
13176
13252
|
}));
|
|
13177
|
-
systems.command("openapi").description("Print a system's OpenAPI document (
|
|
13253
|
+
systems.command("openapi").description("Print a system's OpenAPI document (the document itself when redirected)").argument("<system>", "the system name").action((system, _options, cmd) => run(cmd, async (ctx) => {
|
|
13178
13254
|
const name = systemArg(system);
|
|
13179
13255
|
const response = await ctx.callInOrg({
|
|
13180
13256
|
method: "GET",
|
|
@@ -13184,7 +13260,8 @@ var registerSystems = (program, run) => {
|
|
|
13184
13260
|
return {
|
|
13185
13261
|
data: response.data,
|
|
13186
13262
|
meta: { requestId: response.requestId },
|
|
13187
|
-
text: JSON.stringify(document, null, 2)
|
|
13263
|
+
text: JSON.stringify(document, null, 2),
|
|
13264
|
+
bare: true
|
|
13188
13265
|
};
|
|
13189
13266
|
}));
|
|
13190
13267
|
};
|
|
@@ -13278,7 +13355,10 @@ var buildProgram = (rt, state) => {
|
|
|
13278
13355
|
const meta = { ...ctx.org ? { org: ctx.org } : {}, ...result.meta };
|
|
13279
13356
|
if (meta.requestId === undefined)
|
|
13280
13357
|
delete meta.requestId;
|
|
13281
|
-
if (ctx.json)
|
|
13358
|
+
if (ctx.json && result.bare && !ctx.opts.json)
|
|
13359
|
+
rt.stdout.write(`${result.text}
|
|
13360
|
+
`);
|
|
13361
|
+
else if (ctx.json)
|
|
13282
13362
|
rt.stdout.write(toLine(successDoc(result.data, meta)));
|
|
13283
13363
|
else
|
|
13284
13364
|
rt.stdout.write(`${result.text}
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
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
|
|
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 MEDICAL OFFICE) 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
4
|
metadata:
|
|
5
5
|
cli: "@deepdockai/cli"
|
|
6
6
|
---
|
|
@@ -27,7 +27,7 @@ integration against a demo device. It is not for building or changing actions: D
|
|
|
27
27
|
5. Every write needs an `Idempotency-Key`: a new key per intended write, the same key on every retry,
|
|
28
28
|
and a repeat answers the stored result with `Idempotent-Replayed: true`, writing nothing twice.
|
|
29
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
|
|
30
|
+
whose result does not match is not saved and the action is paused.
|
|
31
31
|
7. A device can be offline and an action can be paused; your code treats both as normal answers.
|
|
32
32
|
8. Every answer is `{ "data": ... }` or `{ "error": { "code", "message" } }`; `code` is the contract,
|
|
33
33
|
`message` is for people.
|
|
@@ -19,8 +19,10 @@ curl "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/searchCustomers?lastN
|
|
|
19
19
|
{ "data": { "items": [ { "customerId": 1001, "lastName": "Müller", "city": "Beispielstadt" } ], "nextCursor": null } }
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
- `nextCursor`
|
|
23
|
-
|
|
22
|
+
- Pass `nextCursor` back as `cursor` unchanged while it is not `null`, and stop only on `null`,
|
|
23
|
+
never on a short page: a page of long entries (notes) ends before `limit` rows to keep the
|
|
24
|
+
device's answer under 1 MiB and still carries a cursor. It is opaque: never parse, build or store
|
|
25
|
+
it beyond the next request.
|
|
24
26
|
- Items are the customer's records. Values can be `null` where the schema says so. Dates and other
|
|
25
27
|
non-numeric values arrive as text in the system's own format (the schema and description say
|
|
26
28
|
which); numbers as JSON numbers.
|
|
@@ -69,6 +71,37 @@ of a customer, the note id of an update. The pattern is always the same:
|
|
|
69
71
|
|
|
70
72
|
Never invent an id, copy one from another device, or hard-code one from a test run.
|
|
71
73
|
|
|
74
|
+
## Recipe: MEDICAL OFFICE (`medical-office`)
|
|
75
|
+
|
|
76
|
+
The examples above use the placeholder system `example-erp`. For a practice running MEDICAL OFFICE
|
|
77
|
+
the same patterns use real actions (the live OpenAPI document is still the authority for fields):
|
|
78
|
+
|
|
79
|
+
1. `readUsers` lists the users who can author an entry; pass one id as `authorUserId`.
|
|
80
|
+
2. `searchPatients?lastName=...` finds a patient: a prefix match with umlauts spelled out
|
|
81
|
+
(`Müller` and `Mueller` match, `Muller` does not), answering `patientId`, `lastName`,
|
|
82
|
+
`firstName`, `birthDate` (`yyyy-mm-dd`, `null` when the system holds no readable date).
|
|
83
|
+
3. `readCases` lists the patient's cases; a write that needs an open case (`createDiagnosis` and
|
|
84
|
+
the note actions) refuses with `422 no_open_case` when there is none, or takes a `caseId`.
|
|
85
|
+
`readProviders` lists the providers for `providerId` (`422 unknown_provider` otherwise).
|
|
86
|
+
4. Write with `createDiagnosis` (and the other create actions) under a fresh `Idempotency-Key`,
|
|
87
|
+
then read back.
|
|
88
|
+
|
|
89
|
+
Things the schemas show but are easy to get wrong:
|
|
90
|
+
|
|
91
|
+
- **Ids come back as integers and go in as strings.** A read and a create answer ids as integers
|
|
92
|
+
(a create's new `entryId` or `diagnosisId` too); only an update echoes the key you sent, as the
|
|
93
|
+
string you sent. `patientId: 1001` in a search result goes back as `patientId=1001` in a read's
|
|
94
|
+
query string and as `"patientId": "1001"` in a write body: digits only, no leading zero, at most
|
|
95
|
+
10 characters.
|
|
96
|
+
- **Nothing is undone through the API.** No action deletes or reverts a record; a write you
|
|
97
|
+
regret stays until someone removes it in MEDICAL OFFICE itself. Write only what you mean to keep.
|
|
98
|
+
- **Mark every test text `TEST-DEEPDOCK-<id>`** (a diagnosis text, a note), on the demo device
|
|
99
|
+
only, and list what you created ([testing.md](testing.md)).
|
|
100
|
+
- Text fields accept MEDICAL OFFICE's Windows-1252 repertoire only; anything else (a tab or line
|
|
101
|
+
break outside a note) answers `422 invalid_input` before a write. Note actions take a `format` of `text` (default, stored as
|
|
102
|
+
typed) or `markdown`.
|
|
103
|
+
- A read action's query string treats `+` as a space: send a literal `+` as `%2B`.
|
|
104
|
+
|
|
72
105
|
## Timeouts and latency
|
|
73
106
|
|
|
74
107
|
- Every action call is sent to the customer's PC and run there. Expect a read to take from a few
|
package/skill/references/cli.md
CHANGED
|
@@ -81,8 +81,8 @@ A failure is one document on stderr: the API's error object unchanged plus what
|
|
|
81
81
|
|---|---|---|
|
|
82
82
|
| 0 | done | |
|
|
83
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` |
|
|
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 (not `access_outdated` and `access_missing`, see exit 3) |
|
|
85
|
+
| 3 | retry later (`access_outdated` and `access_missing`: after the practice repeats "Erneut einrichten" in the Deepdock client) | `device_offline`, `action_paused`, `access_outdated`, `access_missing`, `system_unavailable`, `target_busy`, `temporarily_unavailable`, `internal_error`, `connection_lost` |
|
|
86
86
|
| 4 | the CLI is too old for the API: run `npx @deepdockai/cli@latest` | `client_outdated` |
|
|
87
87
|
| 5 | sign-in needed: run `deepdock auth login` | `sign_in_needed`, `approval_denied`, `approval_expired` |
|
|
88
88
|
|
|
@@ -50,10 +50,12 @@ has a display `label` (may be `null`), a `version` the device reported, a `statu
|
|
|
50
50
|
| system `status` | `running` | the customer's program is running on the device | normal |
|
|
51
51
|
| system `status` | `stopped` | the program is installed but not running | calls answer `system_unavailable`; the site starts the program |
|
|
52
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 |
|
|
53
55
|
| action state | `active` | the action can be called | normal |
|
|
54
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 |
|
|
55
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 |
|
|
56
|
-
| pause `reason` | `check_failed` | a write's result did not match
|
|
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 |
|
|
57
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 |
|
|
58
60
|
|
|
59
61
|
Switch on these values; any value you do not know is shown as it is.
|
|
@@ -69,8 +71,8 @@ catalogue.
|
|
|
69
71
|
- A **read** is `GET` with query parameters and pages.
|
|
70
72
|
- A **write** is `POST` (creates a record, answers `201`) or `PATCH` (updates a record, answers
|
|
71
73
|
`200`), with a JSON body of string fields and an `Idempotency-Key` header.
|
|
72
|
-
- A write **either fully applies or not**: Deepdock checks the result before confirming, and
|
|
73
|
-
|
|
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`).
|
|
74
76
|
- An action is **active** or **paused**. `GET /v1/devices/{deviceId}/systems` shows each action's
|
|
75
77
|
state with `reason` and `pausedAt`.
|
|
76
78
|
|
|
@@ -82,7 +84,7 @@ the action expects:
|
|
|
82
84
|
| Reason | What you get | Meaning |
|
|
83
85
|
|---|---|---|
|
|
84
86
|
| `structure_changed` | `503 action_paused` | the system changed (for example a vendor update); Deepdock reviews and resumes |
|
|
85
|
-
| `check_failed` | that call `409 check_failed`, later calls `503 action_paused` | a write's result did not match
|
|
87
|
+
| `check_failed` | that call `409 check_failed`, later calls `503 action_paused` | a write's result did not match, so nothing was saved |
|
|
86
88
|
| `system_stopped`, `system_missing` | `503 system_unavailable` | the customer's program is stopped or not found on the device |
|
|
87
89
|
|
|
88
90
|
Each pause opens an **incident** (`GET /v1/incidents`). Only Deepdock lifts a pause; staff at the
|
|
@@ -7,7 +7,7 @@ Never hard-code an action's fields. Three sources, each authoritative for one th
|
|
|
7
7
|
| Which systems and actions does my organisation have? | `deepdock systems list --json` | CLI sign-in |
|
|
8
8
|
| What does this device run, and is each action active or paused? | `GET /v1/devices/{deviceId}/systems` | organisation |
|
|
9
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>`
|
|
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
11
|
|
|
12
12
|
## The OpenAPI document
|
|
13
13
|
|
|
@@ -17,7 +17,9 @@ deepdock systems openapi example-erp --org <slug> > openapi/example-erp.json
|
|
|
17
17
|
curl "$BASE/v1/systems/example-erp/openapi.json" -H "Authorization: Bearer $DEEPDOCK_API_KEY"
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
OpenAPI 3.1, one document per system, cacheable for 60 seconds.
|
|
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
|
|
21
23
|
document into the repository and refresh it with the same command when you touch the integration.
|
|
22
24
|
|
|
23
25
|
What to read off it:
|
|
@@ -14,12 +14,14 @@ with the **same** `Idempotency-Key`); **Show** says what the end user should see
|
|
|
14
14
|
| Code | Status | Meaning | Retry | Show |
|
|
15
15
|
|---|---|---|---|---|
|
|
16
16
|
| `invalid_request` | 400 (405 for a wrong method) | malformed request: bad query, `limit` or `cursor`; missing or malformed `Idempotency-Key`; a body that is not an object of strings; a malformed pairing code or device id; a write called with the other method (`Allow` names the right one) | no; fix the code | nothing; a developer bug |
|
|
17
|
+
| `payload_too_large` | 413 | the request body is over 1 MiB | no; send less | nothing; a developer bug, or shorten the text |
|
|
17
18
|
| `unauthenticated` | 401 | the key is missing, malformed, for the other environment, unknown, revoked or expired | no | "Deepdock API key refused; check it in the dashboard" to an operator |
|
|
18
19
|
| `not_found` | 404 | no such route; an update whose id matches no record; a write naming a record that does not exist or belongs to another one | no | "record not found" when it is a record; otherwise a bug |
|
|
19
20
|
| `rate_limited` | 429 | too many failed pairing attempts with this key (5 per 60 s) | yes, after up to a minute (`Retry-After` when present) | "try again in a minute" |
|
|
20
21
|
|
|
21
22
|
Codes your `/v1` code never receives, listed so the enum is complete: `organization_required`
|
|
22
|
-
(403), `forbidden` (403)
|
|
23
|
+
(403), `forbidden` (403), `conflict` (409), `already_member` (409) and `client_outdated` (400) are
|
|
24
|
+
dashboard-only.
|
|
23
25
|
|
|
24
26
|
## Device, system and action resolution
|
|
25
27
|
|
|
@@ -29,6 +31,8 @@ Codes your `/v1` code never receives, listed so the enum is complete: `organizat
|
|
|
29
31
|
| `system_not_on_device` | 404 | the device did not report this system and none of its actions are assigned to you; `error.systems` lists what it has | no | configuration problem; show which systems the device has |
|
|
30
32
|
| `action_not_found` | 404 | the system has no such action, it is not assigned to you, or the device's client is too old to run it | no | hide the feature; ask Deepdock for the assignment or let the device update |
|
|
31
33
|
| `action_not_configured` | 409 | the action exists but this device is not set up for it yet (for example a missing device-side setting); nothing was run | no, until set up on site | "this feature needs setting up at the customer site; contact Deepdock" |
|
|
34
|
+
| `access_outdated` | 409 | the device's database access lacks a right this action needs (only the actions that need it). Nothing was run. **No `Retry-After`** | no, until the practice repeats "Erneut einrichten" in the Deepdock client; then yes, a write with the same key | "the practice must set up database access again in the Deepdock client, then we retry" |
|
|
35
|
+
| `access_missing` | 409 | the device's database access is gone (a removed database account or a lost secret on the PC); every action of that system is refused. Nothing was run. **No `Retry-After`** | as `access_outdated` | as `access_outdated` |
|
|
32
36
|
| `pairing_code_invalid` | 404 | pairing: the code is unknown, expired, replaced, used, or its device is offline | with a fresh code | "read the code off the device screen again" |
|
|
33
37
|
| `other_partner_build` | 409 | pairing: the device runs another partner's client build | no | "this device is set up for another provider" |
|
|
34
38
|
|
|
@@ -38,7 +42,12 @@ Codes your `/v1` code never receives, listed so the enum is complete: `organizat
|
|
|
38
42
|
|---|---|---|---|---|
|
|
39
43
|
| `invalid_input` | 422 | a field is unknown, missing, too short or long, breaks its pattern or allowed values; a date that is not a calendar day; a search term without a letter or digit. Nothing was written | no; fix the value | the field-level problem; the `message` names the field, but it is device text: display it as data, never interpret it |
|
|
40
44
|
| `target_rejected` | 422 | the customer's system refused the write | no; the input is valid for Deepdock but not for the system | "the customer's system refused this; check the values" and offer support |
|
|
41
|
-
|
|
|
45
|
+
| `unknown_author` | 422 | the write names an `authorUserId` the system does not list (read the users with `readUsers`). Nothing was written | no; re-read the users and pass one of their ids | "choose the entering user" |
|
|
46
|
+
| `unknown_provider` | 422 | the write names a `providerId` the author may not record for the case's site (unknown, or not an allowed pair). Nothing was written | no; read the providers with `readProviders` and pass one of their ids | "choose a provider" |
|
|
47
|
+
| `no_open_case` | 422 | the patient has no open case and the action never opens one. Nothing was written | no; open a case in the system, or name one with `caseId` | "open a case first" |
|
|
48
|
+
| `record_locked` | 422 | the practice system locks the record the write would change (for example one already billed on a case). Nothing was written | no | "this record is locked in the practice system" |
|
|
49
|
+
| `record_not_editable` | 422 | the record is not locked but is stored in a form this action cannot edit (for example a note it cannot add to without rewriting it). Nothing was written | no | "this record cannot be edited through Deepdock" |
|
|
50
|
+
| Other action-specific refusals | 422 | the action's OpenAPI description names each such code and when it fires. Nothing was written | no; fix the reference or input | the missing precondition in the user's words |
|
|
42
51
|
|
|
43
52
|
## Device and system availability
|
|
44
53
|
|
|
@@ -54,7 +63,7 @@ Codes your `/v1` code never receives, listed so the enum is complete: `organizat
|
|
|
54
63
|
|
|
55
64
|
| Code | Status | Meaning | Retry | Show |
|
|
56
65
|
|---|---|---|---|---|
|
|
57
|
-
| `check_failed` | 409 | the write
|
|
66
|
+
| `check_failed` | 409 | the write's result did not match what the action expects, so nothing was saved; **the action is now paused** and an incident is open | no; the next calls answer `action_paused` | "the change was not saved; Deepdock is checking the customer system" |
|
|
58
67
|
| `idempotency_conflict` | 409 | the key was used with a different input, its write is still running, or its outcome is unknown. One code, three causes; `message` must not be parsed to tell them apart | retry with the **same key and the same input** a bounded number of times with backoff (a running write finishes within seconds); if it still conflicts, handle it like `outcome_unknown`: read back, then decide | "a previous attempt is still in progress" |
|
|
59
68
|
| `outcome_unknown` | 500 | the system did not confirm the write; it may or may not have landed. The key stays blocked | **no.** Read the record back (a search) to see whether it landed before writing again under a new key; involve a human when the read cannot tell | "we could not confirm the change; please check in the customer system before trying again" |
|
|
60
69
|
| `internal_error` | 500 | something failed inside Deepdock; `error.requestId` is set | once, later; a write with the same key | "something went wrong"; log `requestId` for Deepdock support |
|
|
@@ -62,7 +71,7 @@ Codes your `/v1` code never receives, listed so the enum is complete: `organizat
|
|
|
62
71
|
## A reaction table for code
|
|
63
72
|
|
|
64
73
|
```ts
|
|
65
|
-
type Reaction = 'fix-code' | 'fix-input' | 'retry-same-key' | 'retry-later' | 'wait-paused' | 'check-then-decide' | 'stop'
|
|
74
|
+
type Reaction = 'fix-code' | 'fix-input' | 'retry-same-key' | 'retry-later' | 'wait-paused' | 'wait-setup' | 'check-then-decide' | 'stop'
|
|
66
75
|
|
|
67
76
|
const REACTION: Record<string, Reaction> = {
|
|
68
77
|
invalid_request: 'fix-code',
|
|
@@ -79,6 +88,12 @@ const REACTION: Record<string, Reaction> = {
|
|
|
79
88
|
target_rejected: 'fix-input',
|
|
80
89
|
unknown_author: 'fix-input',
|
|
81
90
|
no_open_case: 'fix-input',
|
|
91
|
+
unknown_provider: 'fix-input',
|
|
92
|
+
record_locked: 'stop',
|
|
93
|
+
record_not_editable: 'stop',
|
|
94
|
+
payload_too_large: 'fix-code',
|
|
95
|
+
access_outdated: 'wait-setup',
|
|
96
|
+
access_missing: 'wait-setup',
|
|
82
97
|
device_offline: 'retry-same-key',
|
|
83
98
|
action_paused: 'wait-paused',
|
|
84
99
|
system_unavailable: 'retry-later',
|
|
@@ -92,8 +107,7 @@ const REACTION: Record<string, Reaction> = {
|
|
|
92
107
|
const reactionFor = (code: string): Reaction => REACTION[code] ?? 'stop'
|
|
93
108
|
```
|
|
94
109
|
|
|
95
|
-
Action-specific 422 codes (`
|
|
96
|
-
lists) map to `fix-input`. Add them from the document, not from memory. `retry-same-key` means the
|
|
110
|
+
Action-specific 422 codes beyond the ones above (any the OpenAPI description lists) map to `fix-input`. Add them from the document, not from memory. `wait-setup` means a person at the practice must repeat the database access setup ("Erneut einrichten") in the Deepdock client first; there is no `Retry-After`, so do not loop. Check `access` on the system in `GET .../systems` (`ok` again), then retry; a write keeps its key. `retry-same-key` means the
|
|
97
111
|
pending write stays persisted and is retried from that state with backoff, also across process
|
|
98
112
|
restarts; a device can be offline for hours, so the retry loop lives in a job or queue, not in the
|
|
99
113
|
request that started the write.
|
|
@@ -13,7 +13,7 @@ refused with `400 invalid_request`.
|
|
|
13
13
|
- Keys are remembered **per device**. The same key on another device is a different write.
|
|
14
14
|
- A successful key is remembered for at least 7 days.
|
|
15
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
|
|
16
|
+
action-specific 422 such as `unknown_author`, `not_found`) or not saved (`check_failed`) frees its
|
|
17
17
|
key: you may retry with the same key. After changing the input, use a new key; the old one may
|
|
18
18
|
still be remembered with the old input. `idempotency_conflict` does **not** free a key.
|
|
19
19
|
|
|
@@ -59,7 +59,7 @@ create, search by the value you wrote; for an update, read the record by id) and
|
|
|
59
59
|
|
|
60
60
|
### `409 check_failed`
|
|
61
61
|
|
|
62
|
-
The write
|
|
62
|
+
The write's result did not match, so nothing was saved, and the action is now paused. Do not retry;
|
|
63
63
|
the next calls answer `503 action_paused` until Deepdock resumes it. Tell the user the change was not
|
|
64
64
|
saved.
|
|
65
65
|
|
|
@@ -27,7 +27,7 @@ gets written to. These rules are not optional.
|
|
|
27
27
|
|
|
28
28
|
## Cleanup
|
|
29
29
|
|
|
30
|
-
No action deletes anything. So:
|
|
30
|
+
No action deletes anything, and nothing is undone through the API. So:
|
|
31
31
|
|
|
32
32
|
1. Every test run prints what it created: record id, device, time, and how many change-log entries
|
|
33
33
|
the updates added. The CLI runs no action, so your own test prints this.
|