@kivel.io/sdk 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,22 @@
1
1
  # @kivel.io/sdk
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
+ ## 0.9.0
10
+
11
+ ### Minor Changes
12
+
13
+ - fde4ed4: Show clear messages and structured details for plan allowances and request rate limits.
14
+ - fde4ed4: Type the usage response, including workspace allowance counters and reset times when the server provides them.
15
+
16
+ ### Patch Changes
17
+
18
+ - 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.
19
+
3
20
  ## 0.8.0
4
21
 
5
22
  ### Minor Changes
package/README.md CHANGED
@@ -24,6 +24,11 @@ await kivel.remove('inboxes', inbox.id);
24
24
 
25
25
  `createInbox()` returns a `webhookUrl` that accepts senders without credentials. Reading and waiting still require your workspace key or the private `inspectionToken`. For token-required receiving, pass `{ visibility: 'private' }` and give the sender the returned `accessToken` for its `X-Access-Token` header.
26
26
 
27
+
28
+ Use `stream(['webhook:' + inbox.id], { token, signal })` to receive change notices
29
+ when live updates are available. Fetch the request bodies with the normal inbox
30
+ methods. Pass the last notice's `cursor` as `lastEventId` when reconnecting.
31
+
27
32
  `baseUrl` defaults to `https://kivel.io`; set it to your self-hosted origin.
28
33
  Remote origins require HTTPS. The default request timeout is 25 seconds. Requests
29
34
  are not automatically retried and redirects are refused.
@@ -44,7 +49,7 @@ write a local credential file. Do not log responses that contain keys or tokens.
44
49
 
45
50
  [Method reference](https://kivel.io/docs/sdk/) ·
46
51
  [Agent workflows](https://kivel.io/docs/agent-workflows/) ·
47
- [Plan limits](https://kivel.io/plans/)
52
+ [Plan limits](https://kivel.io/pricing/)
48
53
 
49
54
  Licensed under Apache-2.0. Support: support@kivel.io.
50
55
 
package/dist/index.d.ts CHANGED
@@ -51,13 +51,55 @@ export interface Execution {
51
51
  resetsAt: string;
52
52
  };
53
53
  }
54
+ export interface KivelUsage {
55
+ plan: 'anonymous' | 'free' | 'pro' | 'team';
56
+ limits: Record<string, unknown>;
57
+ runsToday: number;
58
+ resetsAt: string;
59
+ period?: {
60
+ start: string;
61
+ end: string;
62
+ };
63
+ items?: {
64
+ key: string;
65
+ label: string;
66
+ unit: 'count' | 'bytes';
67
+ used: number;
68
+ max: number;
69
+ resetsAt?: string;
70
+ }[];
71
+ }
72
+ export interface KivelLimitDetails {
73
+ code: 'limit_reached' | 'rate_limited';
74
+ limit: string;
75
+ used?: number;
76
+ max?: number;
77
+ plan?: KivelUsage['plan'];
78
+ }
79
+ export interface StreamOptions {
80
+ token?: string;
81
+ lastEventId?: string;
82
+ signal?: AbortSignal;
83
+ }
84
+ export type KivelLiveEvent = {
85
+ type: 'ready';
86
+ cursor: string;
87
+ } | {
88
+ type: 'change';
89
+ cursor: string;
90
+ topic: string;
91
+ scopeId: string;
92
+ id: string;
93
+ event: string;
94
+ };
54
95
  export declare class KivelError extends Error {
55
96
  readonly status: number;
56
97
  readonly requestId?: string | undefined;
57
98
  readonly retryAfter?: number | undefined;
58
99
  readonly requiredPlan?: string | undefined;
59
100
  readonly upgradeUrl?: string | undefined;
60
- constructor(message: string, status: number, requestId?: string | undefined, retryAfter?: number | undefined, requiredPlan?: string | undefined, upgradeUrl?: string | undefined);
101
+ readonly limitDetails?: KivelLimitDetails | undefined;
102
+ constructor(message: string, status: number, requestId?: string | undefined, retryAfter?: number | undefined, requiredPlan?: string | undefined, upgradeUrl?: string | undefined, limitDetails?: KivelLimitDetails | undefined);
61
103
  }
62
104
  export declare class Kivel {
63
105
  private options;
@@ -66,6 +108,7 @@ export declare class Kivel {
66
108
  private timeoutMs;
67
109
  constructor(options?: ClientOptions);
68
110
  request<T = unknown>(method: string, path: string, body?: unknown, resourceToken?: string, headers?: Record<string, string>): Promise<T>;
111
+ stream(topics: string[], options?: StreamOptions): AsyncGenerator<KivelLiveEvent>;
69
112
  tools(): Promise<{
70
113
  tools: {
71
114
  id: string;
@@ -355,14 +398,16 @@ export declare class Kivel {
355
398
  inboxDeliveries(id: string, token?: string): Promise<unknown>;
356
399
  private waitTimeout;
357
400
  private waitQuery;
358
- usage(): Promise<unknown>;
401
+ usage(): Promise<KivelUsage>;
359
402
  resources(): Promise<unknown>;
360
403
  /** Senders can POST to webhookUrl without credentials by default. Inspection stays private.
361
404
  * Set visibility to private to require X-Access-Token on incoming webhooks. */
362
405
  createInbox(settings?: ResourceSettings): Promise<ResourceCredentials & {
363
406
  webhookUrl: string;
364
407
  }>;
365
- inspectInbox(id: string, token?: string): Promise<unknown>;
408
+ inspectInbox(id: string, token?: string): Promise<{
409
+ requests: CapturedRequest[];
410
+ }>;
366
411
  /**
367
412
  * Long-polls until the inbox captures a request newer than `after` (any request when omitted).
368
413
  * Resolves with `timedOut: true` when nothing arrives; call again to keep waiting.
package/dist/index.js CHANGED
@@ -5,16 +5,38 @@ export class KivelError extends Error {
5
5
  retryAfter;
6
6
  requiredPlan;
7
7
  upgradeUrl;
8
- constructor(message, status, requestId, retryAfter, requiredPlan, upgradeUrl) {
8
+ limitDetails;
9
+ constructor(message, status, requestId, retryAfter, requiredPlan, upgradeUrl, limitDetails) {
9
10
  super(message);
10
11
  this.status = status;
11
12
  this.requestId = requestId;
12
13
  this.retryAfter = retryAfter;
13
14
  this.requiredPlan = requiredPlan;
14
15
  this.upgradeUrl = upgradeUrl;
16
+ this.limitDetails = limitDetails;
15
17
  this.name = 'KivelError';
16
18
  }
17
19
  }
20
+ function apiFailure(response, value) {
21
+ const error = value && typeof value === 'object' ? value : {};
22
+ const code = error.code ?? error.error;
23
+ const details = (code === 'limit_reached' || code === 'rate_limited') && typeof error.limit === 'string'
24
+ ? {
25
+ code,
26
+ limit: error.limit,
27
+ ...(typeof error.used === 'number' ? { used: error.used } : {}),
28
+ ...(typeof error.max === 'number' ? { max: error.max } : {}),
29
+ ...(['anonymous', 'free', 'pro', 'team'].some((plan) => plan === error.plan)
30
+ ? { plan: error.plan }
31
+ : {}),
32
+ }
33
+ : undefined;
34
+ return new KivelError(typeof error.message === 'string'
35
+ ? error.message
36
+ : typeof error.error === 'string'
37
+ ? error.error
38
+ : `Request failed (${response.status}).`, response.status, response.headers.get('x-request-id') ?? undefined, Number(response.headers.get('retry-after')) || undefined, typeof error.requiredPlan === 'string' ? error.requiredPlan : undefined, typeof error.upgradeUrl === 'string' ? error.upgradeUrl : undefined, details);
39
+ }
18
40
  export class Kivel {
19
41
  options;
20
42
  baseUrl;
@@ -84,27 +106,90 @@ export class Kivel {
84
106
  return text;
85
107
  throw new KivelError('The server returned an invalid response.', response.status, response.headers.get('x-request-id') ?? undefined);
86
108
  }
87
- if (!response.ok) {
88
- const message = value &&
89
- typeof value === 'object' &&
90
- 'error' in value &&
91
- typeof value.error === 'string'
92
- ? value.error
93
- : `Request failed (${response.status}).`;
94
- throw new KivelError(message, response.status, response.headers.get('x-request-id') ?? undefined, Number(response.headers.get('retry-after')) || undefined, value &&
95
- typeof value === 'object' &&
96
- 'requiredPlan' in value &&
97
- typeof value.requiredPlan === 'string'
98
- ? value.requiredPlan
99
- : undefined, value &&
100
- typeof value === 'object' &&
101
- 'upgradeUrl' in value &&
102
- typeof value.upgradeUrl === 'string'
103
- ? value.upgradeUrl
104
- : undefined);
105
- }
109
+ if (!response.ok)
110
+ throw apiFailure(response, value);
106
111
  return value;
107
112
  }
113
+ async *stream(topics, options = {}) {
114
+ if (!topics.length ||
115
+ topics.length > 10 ||
116
+ topics.some((topic) => !topic || /[,\s]/.test(topic)))
117
+ throw new Error('Choose 1 to 10 live topics.');
118
+ const query = new URLSearchParams({ topics: topics.join(',') });
119
+ const headers = new Headers({ Accept: 'text/event-stream' });
120
+ if (this.options.apiKey)
121
+ headers.set('Authorization', `Bearer ${this.options.apiKey}`);
122
+ if (options.token)
123
+ headers.set('X-Resource-Token', options.token);
124
+ if (options.lastEventId)
125
+ headers.set('Last-Event-ID', options.lastEventId);
126
+ const timeout = AbortSignal.timeout(60_000);
127
+ const response = await this.fetcher(`${this.baseUrl}/api/stream?${query}`, {
128
+ headers,
129
+ redirect: 'error',
130
+ credentials: 'omit',
131
+ signal: options.signal ? AbortSignal.any([timeout, options.signal]) : timeout,
132
+ });
133
+ if (!response.ok)
134
+ throw apiFailure(response, await response.json().catch(() => undefined));
135
+ const invalid = () => new KivelError('The server returned an invalid event stream.', response.status);
136
+ if (!response.body ||
137
+ !response.headers.get('content-type')?.startsWith('text/event-stream'))
138
+ throw invalid();
139
+ const reader = response.body.getReader();
140
+ const decoder = new TextDecoder();
141
+ let pending = '';
142
+ try {
143
+ for (;;) {
144
+ const { value, done } = await reader.read();
145
+ if (done)
146
+ break;
147
+ pending += decoder.decode(value, { stream: true });
148
+ for (let end = /\r?\n\r?\n/.exec(pending); end; end = /\r?\n\r?\n/.exec(pending)) {
149
+ if (end.index > 16_384)
150
+ throw invalid();
151
+ const lines = pending.slice(0, end.index).split(/\r?\n/);
152
+ pending = pending.slice(end.index + end[0].length);
153
+ const cursor = lines.find((line) => line.startsWith('id: '))?.slice(4);
154
+ const type = lines.find((line) => line.startsWith('event: '))?.slice(7);
155
+ if (!cursor || (type !== 'ready' && type !== 'change'))
156
+ continue;
157
+ if (type === 'ready') {
158
+ yield { type, cursor };
159
+ continue;
160
+ }
161
+ const json = lines
162
+ .filter((line) => line.startsWith('data: '))
163
+ .map((line) => line.slice(6))
164
+ .join('\n');
165
+ let data;
166
+ try {
167
+ data = JSON.parse(json);
168
+ }
169
+ catch {
170
+ throw invalid();
171
+ }
172
+ if (!data ||
173
+ ['topic', 'scopeId', 'id', 'event'].some((key) => typeof data[key] !== 'string'))
174
+ throw invalid();
175
+ yield {
176
+ type,
177
+ cursor,
178
+ topic: data.topic,
179
+ scopeId: data.scopeId,
180
+ id: data.id,
181
+ event: data.event,
182
+ };
183
+ }
184
+ if (pending.length > 16_384)
185
+ throw invalid();
186
+ }
187
+ }
188
+ finally {
189
+ await reader.cancel().catch(() => { });
190
+ reader.releaseLock();
191
+ }
192
+ }
108
193
  tools() {
109
194
  return this.request('GET', '/api/tools');
110
195
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kivel.io/sdk",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {