@butlerbot/sdk 0.0.33 → 0.0.35

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.
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Outreach: what Alfred has told or asked this user outside a chat, and their answers.
3
+ *
4
+ * A delivery is the record itself, so the inbox listing is the delivery rather than a
5
+ * notification about one. Answering closes it, and when it belongs to a job the answer is put
6
+ * on that job's inbox.
7
+ */
8
+ export declare const DELIVERY_INTENTS: readonly ["question", "update", "approval", "statement"];
9
+ export type DeliveryIntent = typeof DELIVERY_INTENTS[number];
10
+ /** The intents that leave something outstanding until the user answers. */
11
+ export declare const ANSWERABLE_DELIVERY_INTENTS: readonly DeliveryIntent[];
12
+ export declare const DELIVERY_SURFACES: readonly ["web", "discord"];
13
+ /** Where a delivery was put. */
14
+ export type DeliverySurfaceName = typeof DELIVERY_SURFACES[number];
15
+ export declare const DELIVERY_SURFACE_STATUSES: readonly ["pending", "sent", "failed"];
16
+ export type DeliverySurfaceStatus = typeof DELIVERY_SURFACE_STATUSES[number];
17
+ /** How one surface's send went. */
18
+ export type DeliverySurface = {
19
+ surface: DeliverySurfaceName;
20
+ status: DeliverySurfaceStatus;
21
+ /** Where on the surface it went: a Discord channel id, or "DM". */
22
+ channelId?: string | null;
23
+ /** What the channel called the message it sent. */
24
+ messageId?: string | null;
25
+ /** Why the last attempt failed. */
26
+ error?: string | null;
27
+ attempts: number;
28
+ /** When the last attempt was made, in UTC milliseconds. */
29
+ lastAt: number;
30
+ };
31
+ export declare const DELIVERY_ANSWER_VIA: readonly ["web", "discord", "chat", "tool"];
32
+ /** Which surface the answer came back on. */
33
+ export type DeliveryAnsweredVia = typeof DELIVERY_ANSWER_VIA[number];
34
+ export type DeliveryAnswer = {
35
+ at: number;
36
+ text: string;
37
+ via: DeliveryAnsweredVia;
38
+ /** On an approval, what the user decided. */
39
+ decision?: "approve" | "deny";
40
+ };
41
+ /** One delivery, as the inbox reads it. */
42
+ export type Delivery = {
43
+ deliveryId: string;
44
+ ownerUserId: string;
45
+ /** The chat this is about, so answering opens the conversation it belongs to. */
46
+ originConversationId: string | null;
47
+ /** The job this is about, when it is about one. */
48
+ jobId: string | null;
49
+ intent: DeliveryIntent;
50
+ message: string;
51
+ surfaces: DeliverySurface[];
52
+ /** Null until somebody answers. Only ever set once. */
53
+ answered: DeliveryAnswer | null;
54
+ created: number;
55
+ };
56
+ /** Whether a delivery is still waiting on the user. */
57
+ export declare function isDeliveryOpen(delivery: Delivery): boolean;
58
+ export type DeliveryListResponse = {
59
+ success: true;
60
+ deliveries: Delivery[];
61
+ page: number;
62
+ limit: number;
63
+ total: number;
64
+ };
65
+ /** What became of the answer once it was handed to the job that asked. */
66
+ export type DeliveryAnswerJobResult = {
67
+ jobId: string;
68
+ /** What the job did with it, or "dropped" when it could not take it. */
69
+ delivered: string;
70
+ /** Why it could not be delivered, when it could not. */
71
+ error?: string;
72
+ };
73
+ export type DeliveryAnswerResponse = {
74
+ success: true;
75
+ delivery: Delivery;
76
+ /** Present only when the delivery belonged to a job. */
77
+ job?: DeliveryAnswerJobResult;
78
+ };
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ /**
3
+ * Outreach: what Alfred has told or asked this user outside a chat, and their answers.
4
+ *
5
+ * A delivery is the record itself, so the inbox listing is the delivery rather than a
6
+ * notification about one. Answering closes it, and when it belongs to a job the answer is put
7
+ * on that job's inbox.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.DELIVERY_ANSWER_VIA = exports.DELIVERY_SURFACE_STATUSES = exports.DELIVERY_SURFACES = exports.ANSWERABLE_DELIVERY_INTENTS = exports.DELIVERY_INTENTS = void 0;
11
+ exports.isDeliveryOpen = isDeliveryOpen;
12
+ exports.DELIVERY_INTENTS = ["question", "update", "approval", "statement"];
13
+ /** The intents that leave something outstanding until the user answers. */
14
+ exports.ANSWERABLE_DELIVERY_INTENTS = ["question", "approval"];
15
+ exports.DELIVERY_SURFACES = ["web", "discord"];
16
+ exports.DELIVERY_SURFACE_STATUSES = ["pending", "sent", "failed"];
17
+ exports.DELIVERY_ANSWER_VIA = ["web", "discord", "chat", "tool"];
18
+ /** Whether a delivery is still waiting on the user. */
19
+ function isDeliveryOpen(delivery) {
20
+ return exports.ANSWERABLE_DELIVERY_INTENTS.includes(delivery.intent) && !delivery.answered;
21
+ }
@@ -0,0 +1 @@
1
+ export * from "./delivery";
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./delivery"), exports);
@@ -3,4 +3,6 @@ export * from "./response/v4";
3
3
  export * from "./response/v5";
4
4
  export * from "./state/convo_state_response";
5
5
  export * from "./conversation/v4/conversation_v4";
6
+ export * from "./jobs";
7
+ export * from "./outreach";
6
8
  export * from "./error";
@@ -19,4 +19,6 @@ __exportStar(require("./response/v4"), exports);
19
19
  __exportStar(require("./response/v5"), exports);
20
20
  __exportStar(require("./state/convo_state_response"), exports);
21
21
  __exportStar(require("./conversation/v4/conversation_v4"), exports);
22
+ __exportStar(require("./jobs"), exports);
23
+ __exportStar(require("./outreach"), exports);
22
24
  __exportStar(require("./error"), exports);
@@ -0,0 +1,34 @@
1
+ /**
2
+ * What an API call throws when the server did not answer with a success.
3
+ *
4
+ * One error class for every JSON route rather than a result type per method: the status is
5
+ * what a caller branches on, and it is carried here alongside whatever the server said, so
6
+ * "the job was already finished" (409) and "no job of yours has that id" (404) are told apart
7
+ * without parsing a message. `body` is the parsed response when there was one, so a 409 on a
8
+ * cancel still hands back the job, and a 409 on an answer still hands back the delivery.
9
+ */
10
+ export declare class ButlerBotAPIError extends Error {
11
+ /** The HTTP status the server answered with, or 0 when the call never got a response. */
12
+ readonly status: number;
13
+ readonly statusText: string;
14
+ /** The server's short `error`, e.g. "Already finished". */
15
+ readonly error?: string;
16
+ /** The server's `errormessage`: the same thing in a sentence. */
17
+ readonly errormessage?: string;
18
+ /** The parsed response body, or the raw text when it was not JSON. */
19
+ readonly body: unknown;
20
+ constructor(options: {
21
+ action: string;
22
+ status: number;
23
+ statusText?: string;
24
+ error?: string;
25
+ errormessage?: string;
26
+ body?: unknown;
27
+ });
28
+ /** The resource is not there, or is not this key's. The two are answered alike on purpose. */
29
+ get isNotFound(): boolean;
30
+ /** Somebody got there first: the job had finished, or the delivery was already answered. */
31
+ get isConflict(): boolean;
32
+ /** The request itself was refused: a value the server would not take. */
33
+ get isBadRequest(): boolean;
34
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ButlerBotAPIError = void 0;
4
+ /**
5
+ * What an API call throws when the server did not answer with a success.
6
+ *
7
+ * One error class for every JSON route rather than a result type per method: the status is
8
+ * what a caller branches on, and it is carried here alongside whatever the server said, so
9
+ * "the job was already finished" (409) and "no job of yours has that id" (404) are told apart
10
+ * without parsing a message. `body` is the parsed response when there was one, so a 409 on a
11
+ * cancel still hands back the job, and a 409 on an answer still hands back the delivery.
12
+ */
13
+ class ButlerBotAPIError extends Error {
14
+ constructor(options) {
15
+ const detail = options.errormessage
16
+ || options.error
17
+ // A body that was not JSON at all still says more than "Unknown error" does.
18
+ || (typeof options.body === "string" && options.body ? options.body.slice(0, 500) : "Unknown error");
19
+ super(`Failed to ${options.action}: ${options.status} ${options.statusText || ""} - ${detail}`.replace(/\s+-/, " -"));
20
+ this.name = "ButlerBotAPIError";
21
+ this.status = options.status;
22
+ this.statusText = options.statusText || "";
23
+ this.error = options.error;
24
+ this.errormessage = options.errormessage;
25
+ this.body = options.body;
26
+ // `target: es2020` down-levels the subclass, which breaks `instanceof` without this.
27
+ Object.setPrototypeOf(this, ButlerBotAPIError.prototype);
28
+ }
29
+ /** The resource is not there, or is not this key's. The two are answered alike on purpose. */
30
+ get isNotFound() {
31
+ return this.status === 404;
32
+ }
33
+ /** Somebody got there first: the job had finished, or the delivery was already answered. */
34
+ get isConflict() {
35
+ return this.status === 409;
36
+ }
37
+ /** The request itself was refused: a value the server would not take. */
38
+ get isBadRequest() {
39
+ return this.status === 400;
40
+ }
41
+ }
42
+ exports.ButlerBotAPIError = ButlerBotAPIError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@butlerbot/sdk",
3
- "version": "0.0.33",
3
+ "version": "0.0.35",
4
4
  "description": "The official ButlerBot SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/readme.md CHANGED
@@ -78,7 +78,12 @@ between you and the socket.
78
78
 
79
79
  ## Link
80
80
 
81
- A Link is a live connection to Alfred. It does three things:
81
+ A Link is a live connection to Alfred, served by its own endpoint — `link.butler.now`,
82
+ not the core API server. `createLink` goes there by default; pass `linkUrl` to the client
83
+ (or `serverUrl` to `createLink`) to point somewhere else. A client given a `serverUrl` of
84
+ its own — a self-hosted stack — uses that for links too.
85
+
86
+ It does three things:
82
87
 
83
88
  - **Tools** — Alfred calls code that runs on your machine
84
89
  - **Hooks** — your code wakes the user's background agents when something happens
@@ -286,9 +291,15 @@ Which to use:
286
291
  Best when you are already running a link for tools or hooks, or holding many
287
292
  conversations at once — one socket carries them all.
288
293
 
289
- One difference to know about: sessions are ephemeral. If the connection drops mid-turn
290
- the SDK reopens the session and resends transparently; the conversation itself is
291
- persisted server-side, so nothing is lost.
294
+ A turn survives losing the connection it was asked for on. Sessions are ephemeral the
295
+ server drops one with its socket but the turn belongs to the conversation, so when the
296
+ socket goes mid-answer the SDK reconnects, rejoins the turn from the last event it gave
297
+ you, and carries on into the same stream. The same holds when the platform deploys
298
+ mid-turn and the answer is handed to another instance. A message that had not been
299
+ delivered yet is simply sent again on a fresh session; one that had is never sent twice.
300
+
301
+ If the turn cannot be picked back up within a minute, the stream ends with a failure that
302
+ says so — the reply may still have finished, and reopening the conversation will show it.
292
303
 
293
304
  ### Rejoining a turn already in progress
294
305
 
@@ -311,6 +322,71 @@ seen. A conversation with nothing running simply ends the stream.
311
322
  Neither transport can cancel a turn: `close()` stops delivery locally, and the reply is
312
323
  still generated and stored.
313
324
 
325
+ ## Jobs
326
+
327
+ A job is long-running work Alfred does on its own, run as a sequence of short shifts against
328
+ a plan it writes. The client reads them and steers them; it does not run them.
329
+
330
+ ```typescript
331
+ const { jobs, allowance, spentTodayUsd } = await client.listJobs({ status: "running", page: 1, limit: 20 });
332
+ const { job, plan, journal, openDeliveries, autonomyLine } = await client.getJob({ jobId });
333
+
334
+ await client.updateJob({ jobId, autonomy: "free", autonomyUntil: Date.now() + 86_400_000 });
335
+ await client.cancelJob({ jobId });
336
+ ```
337
+
338
+ Paging is 1-based, and every listing answers with `page`, `limit` and `total`. A listing also
339
+ carries `allowance` — what this user's jobs may spend today and what is left of it, or
340
+ `{ perDayUsd: 0, source: null, refused, message }` when their plan does not include jobs.
341
+
342
+ `autonomy` is how far a job may act outward without asking: `ask`, `free` or `auto`.
343
+ `autonomyUntil` is when a loosened setting lapses back to asking, and `alwaysAsk` is what the
344
+ job asks about however free it otherwise is. `effectiveAutonomy` on a job is what the gate
345
+ actually reads, with the owner's default and any lapse already applied. What new jobs start
346
+ with, and what they may spend, is the user's own setting:
347
+
348
+ ```typescript
349
+ await client.updateJobSettings({ allowanceUsd: 5, autonomy: "ask", alwaysAsk: ["spending money"] });
350
+ ```
351
+
352
+ ## Outreach
353
+
354
+ Outreach is what Alfred has told or asked this user outside a chat. The record is the inbox —
355
+ there is no separate notification — so a question a job is parked on is a delivery with
356
+ `intent: "question"` and no `answered`:
357
+
358
+ ```typescript
359
+ const { deliveries } = await client.listDeliveries({ page: 1, limit: 20 });
360
+ const { delivery, job } = await client.answerDelivery({ deliveryId, text: "The one in Gardens" });
361
+ ```
362
+
363
+ Answering closes the delivery, and when it belongs to a job the answer is put on that job's
364
+ inbox, which wakes a job that was parked waiting for it — `job.delivered` says what became of
365
+ it. An approval takes `decision: "approve" | "deny"` alongside the text.
366
+
367
+ ### When a call fails
368
+
369
+ Every jobs and outreach call throws `ButlerBotAPIError` when the server does not answer with a
370
+ success. The status is on the error, so the cases worth branching on are told apart without
371
+ reading a message, and the parsed body is kept — a rejected cancel still carries the job, a
372
+ rejected answer still carries the delivery:
373
+
374
+ ```typescript
375
+ import { ButlerBotAPIError } from "@butlerbot/sdk";
376
+
377
+ try {
378
+ await client.cancelJob({ jobId });
379
+ } catch (error) {
380
+ if (error instanceof ButlerBotAPIError && error.isConflict) {
381
+ // 409: it had already finished. `error.body.job` is how it settled.
382
+ } else throw error;
383
+ }
384
+ ```
385
+
386
+ `isNotFound` (404), `isConflict` (409) and `isBadRequest` (400) are the three; `status`,
387
+ `error` and `errormessage` are the server's own words. A job or delivery that is not this key's
388
+ is answered exactly as one that does not exist, so a 404 means either.
389
+
314
390
  ## Environment
315
391
 
316
392
  Node 18+. Node 22 and every browser have a built-in WebSocket; on older Node, install