@kivel.io/cli 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @kivel.io/cli
2
2
 
3
+ ## 0.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ad5805c: Add live event streams to the SDK and use them in the webhook listener.
8
+
9
+ ### Patch Changes
10
+
11
+ - ad5805c: Reconnect webhook listeners after transient network failures with backoff and resume from the last processed event cursor.
12
+ - Updated dependencies [ad5805c]
13
+ - @kivel.io/sdk@0.10.0
14
+
15
+ ## 0.9.0
16
+
17
+ ### Minor Changes
18
+
19
+ - fde4ed4: Show clear messages and structured details for plan allowances and request rate limits.
20
+
21
+ ### Patch Changes
22
+
23
+ - fde4ed4: Read the machine code from the new `code` field while retaining support for older servers. The HTTP `error` field remains a readable message. Exhausted hosted tool, email, and resource allowances return 402. Request rate limits return 429. Timed limits include `Retry-After`, which the SDK and CLI expose to callers.
24
+ - Updated dependencies [fde4ed4]
25
+ - Updated dependencies [fde4ed4]
26
+ - Updated dependencies [fde4ed4]
27
+ - @kivel.io/sdk@0.9.0
28
+
3
29
  ## 0.8.0
4
30
 
5
31
  ### Minor Changes
package/README.md CHANGED
@@ -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/cli.js CHANGED
@@ -58,6 +58,6 @@ try {
58
58
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
59
59
  }
60
60
  catch (error) {
61
- process.stderr.write(`${JSON.stringify({ error: error instanceof Error ? error.message : 'Command failed.', ...(error instanceof KivelError ? { status: error.status, requestId: error.requestId, retryAfter: error.retryAfter, requiredPlan: error.requiredPlan, upgradeUrl: error.upgradeUrl } : {}) })}\n`);
61
+ process.stderr.write(`${JSON.stringify({ error: error instanceof Error ? error.message : 'Command failed.', ...(error instanceof KivelError ? { status: error.status, requestId: error.requestId, retryAfter: error.retryAfter, requiredPlan: error.requiredPlan, upgradeUrl: error.upgradeUrl, code: error.limitDetails?.code, limitDetails: error.limitDetails } : {}) })}\n`);
62
62
  process.exitCode = 1;
63
63
  }
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.8.0",
3
+ "version": "0.10.0",
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.8.0"
21
+ "@kivel.io/sdk": "0.10.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^26.6.5",