@kivel.io/cli 0.9.0 → 0.10.1

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,25 @@
1
1
  # @kivel.io/cli
2
2
 
3
+ ## 0.10.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 10de14b: Publish releases to the npm latest tag, so `npm install @kivel.io/sdk` and `npm install --global @kivel.io/cli` install the newest version. Install instructions no longer need `@beta`.
8
+ - Updated dependencies [10de14b]
9
+ - @kivel.io/sdk@0.10.1
10
+
11
+ ## 0.10.0
12
+
13
+ ### Minor Changes
14
+
15
+ - ad5805c: Add live event streams to the SDK and use them in the webhook listener.
16
+
17
+ ### Patch Changes
18
+
19
+ - ad5805c: Reconnect webhook listeners after transient network failures with backoff and resume from the last processed event cursor.
20
+ - Updated dependencies [ad5805c]
21
+ - @kivel.io/sdk@0.10.0
22
+
3
23
  ## 0.9.0
4
24
 
5
25
  ### Minor Changes
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  The command-line client for [Kivel](https://kivel.io). Requires Node.js
4
4
  22.13+. Commands share the HTTP API's access rules, plan limits and expiry.
5
5
 
6
- Install the current beta:
6
+ Install the `kivel` command globally:
7
7
 
8
8
  ```sh
9
- npx --package=@kivel.io/cli@beta kivel --help
10
- # Or install the command:
11
- npm install --global @kivel.io/cli@beta
9
+ npm install --global @kivel.io/cli
12
10
  kivel --version
11
+ # Or run it once without installing:
12
+ npx --package=@kivel.io/cli kivel --help
13
13
  ```
14
14
 
15
15
  For source development, run `pnpm build:packages` and `pnpm cli --help` from the
@@ -39,8 +39,8 @@ requests time out after 90 seconds. Mutations are never automatically retried.
39
39
 
40
40
  `job`, `status-page`, `smtp`, `phone` and `event wait` use the same owned APIs. Operator-dependent services stay disabled until configured; phone sending and calls are not implemented. Run `kivel --help` for their current commands.
41
41
 
42
- `listen` streams webhook events and can forward them to a local server. It runs on
43
- your machine, and stops on Ctrl+C. The optional server SSH gateway supports bounded
42
+ `listen` streams webhook events and can forward them to a local server. On older
43
+ servers, it uses the webhook wait endpoint. It runs on your machine and stops on Ctrl+C. The optional server SSH gateway supports bounded
44
44
  commands; it does not provide this continuous listener or a shell/SCP/SFTP service.
45
45
 
46
46
  [Command reference](https://kivel.io/docs/cli/) ·
package/dist/listen.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Buffer } from 'node:buffer';
2
+ import { setTimeout as pause } from 'node:timers/promises';
2
3
  import { KivelError } from '@kivel.io/sdk';
3
4
  // Hop-by-hop and proxy headers describe the trip to Kivel, not the original sender.
4
5
  const dropped = /^(host|content-length|connection|keep-alive|transfer-encoding|upgrade|te|trailer|proxy-.*|x-forwarded-.*|x-real-ip|cf-.*|cdn-loop|true-client-ip|x-access-token|authorization)$/i;
@@ -24,27 +25,41 @@ export async function listen(client, inboxId, options, write, signal) {
24
25
  if (options.forward)
25
26
  new URL(options.forward);
26
27
  let after;
27
- if (!options.all) {
28
- const existing = (await client.inspectInbox(inboxId, options.token));
29
- after = existing.requests.at(-1)?.id;
28
+ let delay = 200;
29
+ async function backoff(seconds = 0) {
30
+ await pause(Math.max(delay, seconds * 1000), undefined, { signal }).catch(() => { });
31
+ delay = Math.min(delay * 2, 10000);
30
32
  }
31
- write({ event: 'listening', inboxId, forward: options.forward ?? null });
32
- while (!signal?.aborted) {
33
- let batch;
33
+ async function retry(error) {
34
+ if (signal?.aborted)
35
+ return;
36
+ if (error instanceof KivelError && error.status === 429) {
37
+ await backoff(error.retryAfter ?? 2);
38
+ }
39
+ else if (error instanceof TypeError ||
40
+ (error instanceof DOMException &&
41
+ ['AbortError', 'NetworkError', 'TimeoutError'].includes(error.name))) {
42
+ await backoff();
43
+ }
44
+ else {
45
+ throw error;
46
+ }
47
+ }
48
+ while (!options.all && !signal?.aborted) {
34
49
  try {
35
- batch = await client.waitForRequests(inboxId, {
36
- ...(options.token ? { token: options.token } : {}),
37
- ...(after ? { after } : {}),
38
- });
50
+ after = (await client.inspectInbox(inboxId, options.token)).requests.at(-1)?.id;
51
+ delay = 200;
52
+ break;
39
53
  }
40
54
  catch (error) {
41
- if (error instanceof KivelError && error.status === 429) {
42
- await new Promise((resolve) => setTimeout(resolve, (error.retryAfter ?? 2) * 1000));
43
- continue;
44
- }
45
- throw error;
55
+ await retry(error);
46
56
  }
47
- for (const request of batch.requests) {
57
+ }
58
+ if (signal?.aborted)
59
+ return;
60
+ write({ event: 'listening', inboxId, forward: options.forward ?? null });
61
+ async function report(requests) {
62
+ for (const request of requests) {
48
63
  after = request.id;
49
64
  write({ event: 'request', request });
50
65
  if (!options.forward)
@@ -78,4 +93,46 @@ export async function listen(client, inboxId, options, write, signal) {
78
93
  }
79
94
  }
80
95
  }
96
+ let cursor;
97
+ while (!signal?.aborted) {
98
+ try {
99
+ for await (const change of client.stream([`webhook:${inboxId}`], {
100
+ ...(options.token ? { token: options.token } : {}),
101
+ ...(cursor ? { lastEventId: cursor } : {}),
102
+ ...(signal ? { signal } : {}),
103
+ })) {
104
+ const { requests } = await client.waitForRequests(inboxId, {
105
+ ...(options.token ? { token: options.token } : {}),
106
+ ...(after ? { after } : {}),
107
+ timeoutSeconds: 1,
108
+ });
109
+ await report(requests);
110
+ cursor = change.cursor;
111
+ delay = 200;
112
+ }
113
+ await backoff();
114
+ }
115
+ catch (error) {
116
+ if (signal?.aborted)
117
+ return;
118
+ if (error instanceof KivelError && [404, 501, 503].includes(error.status))
119
+ break;
120
+ await retry(error);
121
+ }
122
+ }
123
+ while (!signal?.aborted) {
124
+ try {
125
+ const batch = await client.waitForRequests(inboxId, {
126
+ ...(options.token ? { token: options.token } : {}),
127
+ ...(after ? { after } : {}),
128
+ });
129
+ await report(batch.requests);
130
+ delay = 200;
131
+ }
132
+ catch (error) {
133
+ if (signal?.aborted)
134
+ return;
135
+ await retry(error);
136
+ }
137
+ }
81
138
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kivel.io/cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "kivel": "./bin/kivel.mjs"
@@ -18,7 +18,7 @@
18
18
  "CHANGELOG.md"
19
19
  ],
20
20
  "dependencies": {
21
- "@kivel.io/sdk": "0.9.0"
21
+ "@kivel.io/sdk": "0.10.1"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^26.6.5",
@@ -42,7 +42,7 @@
42
42
  "publishConfig": {
43
43
  "access": "public",
44
44
  "registry": "https://registry.npmjs.org/",
45
- "tag": "beta"
45
+ "tag": "latest"
46
46
  },
47
47
  "main": "./dist/commands.js",
48
48
  "types": "./dist/commands.d.ts",