@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 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@1 auth login
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@1`) the command is
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; redirect it to a file |
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
- For list and show commands `data` is the API's answer unchanged, so what the CLI shows is what
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`. A sign-in or pending request is never sent to a different base URL than the one it
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) => scalar(record[key]))), style);
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.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 enabled = stream.isTTY && !rt.env.NO_COLOR && rt.env.TERM !== "dumb";
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(ctx, answer.organisations), "");
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)}).`, ` API: ${baseUrl}`, ` Default org: ${config.defaultOrg ?? "(none, run `deepdock org select`)"}`);
12478
- if (data.organisations.length > 0)
12479
- lines.push("", "Organisations:", orgTable(ctx, data.organisations));
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." : renderTable(["SLUG", "NAME", "ROLE", "DEFAULT"], orgs.map((org) => [
12990
- org.slug ?? "(no slug)",
12991
- org.name,
12992
- org.role,
12993
- org.slug !== null && org.slug === defaultOrg ? "yes" : ""
12994
- ]), ctx.out);
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." : genericTable(items, ctx.out, SYSTEM_COLUMNS);
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 (redirect to keep it)").argument("<system>", "the system name").action((system, _options, cmd) => run(cmd, async (ctx) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepdockai/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Deepdock partner CLI: pair devices, see systems and actions, create the API key your code runs them with.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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 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.
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 undone and the action is paused.
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` 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.
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
@@ -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 and was undone | wait; tell the user the change was not saved |
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 undoes
73
- the write and pauses the action if the check fails (`409 check_failed`).
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 and was undone |
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>` or `deepdock actions list <device> <system>` (or `GET /v1/systems/{system}/openapi.json`) | CLI sign-in or API key |
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. Discover through the CLI; check the
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) and `conflict` (409) are dashboard-only.
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
- | Action-specific refusals, e.g. `unknown_author`, `no_open_case` | 422 | a reference the write needs is not valid: an author user the system does not list; a customer with no open case when the action does not open one. Nothing was written. The action's OpenAPI description names each such code and when it fires | no; re-read the reference and pass a valid one | the missing precondition in the user's words ("choose the entering user", "open a case first") |
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 was undone because its result did not match what the action expects; **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" |
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 (`unknown_author` and `no_open_case` are two; any the OpenAPI description
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 undone (`check_failed`) frees its
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 was undone because its result did not match, and the action is now paused. Do not retry;
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.