@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.
package/dist/config.d.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  export declare const CONFIG: {
2
2
  server: string;
3
+ /**
4
+ * Where links connect.
5
+ *
6
+ * Not the core server: links are carried by their own service, so the SDK holds two
7
+ * addresses rather than one.
8
+ */
9
+ link: string;
3
10
  healthcheckPath: string;
4
11
  paths: {
5
12
  conversation: {
@@ -52,6 +59,28 @@ export declare const CONFIG: {
52
59
  };
53
60
  };
54
61
  };
62
+ jobs: {
63
+ /** The collection. One job is `${base}/${jobId}`, its cancel `${base}/${jobId}/cancel`. */
64
+ base: string;
65
+ /** The owner's job settings, which live on the user rather than on a job. */
66
+ settings: string;
67
+ };
68
+ outreach: {
69
+ /** The inbox. One delivery's answer is `${base}/${deliveryId}/answer`. */
70
+ base: string;
71
+ };
55
72
  };
56
73
  };
57
74
  export type APIPath = keyof typeof CONFIG.paths.conversation;
75
+ /**
76
+ * Which server a link should connect to.
77
+ *
78
+ * `linkUrl` names it outright and always wins. Failing that, a `serverUrl` pointing anywhere
79
+ * other than the hosted core is taken at its word: a self-hosted stack is usually one address,
80
+ * and sending an API key to a host nobody named would be a worse surprise than a wrong path.
81
+ * Otherwise it is the hosted link service, which is what the core server used to be and is not.
82
+ */
83
+ export declare function resolveLinkUrl(config: {
84
+ linkUrl?: string;
85
+ serverUrl?: string;
86
+ }): string;
package/dist/config.js CHANGED
@@ -1,8 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CONFIG = void 0;
4
+ exports.resolveLinkUrl = resolveLinkUrl;
4
5
  exports.CONFIG = {
5
6
  server: "https://core.butler.now",
7
+ /**
8
+ * Where links connect.
9
+ *
10
+ * Not the core server: links are carried by their own service, so the SDK holds two
11
+ * addresses rather than one.
12
+ */
13
+ link: "https://link.butler.now",
6
14
  healthcheckPath: "/api/healthcheck",
7
15
  paths: {
8
16
  conversation: {
@@ -42,6 +50,32 @@ exports.CONFIG = {
42
50
  policy: {
43
51
  v3: { base: "/api/user/usage/v3/policy" },
44
52
  }
53
+ },
54
+ jobs: {
55
+ /** The collection. One job is `${base}/${jobId}`, its cancel `${base}/${jobId}/cancel`. */
56
+ base: "/api/jobs",
57
+ /** The owner's job settings, which live on the user rather than on a job. */
58
+ settings: "/api/user/jobs-settings",
59
+ },
60
+ outreach: {
61
+ /** The inbox. One delivery's answer is `${base}/${deliveryId}/answer`. */
62
+ base: "/api/outreach",
45
63
  }
46
64
  }
47
65
  };
66
+ const withoutTrailingSlash = (url) => url.replace(/\/+$/, "");
67
+ /**
68
+ * Which server a link should connect to.
69
+ *
70
+ * `linkUrl` names it outright and always wins. Failing that, a `serverUrl` pointing anywhere
71
+ * other than the hosted core is taken at its word: a self-hosted stack is usually one address,
72
+ * and sending an API key to a host nobody named would be a worse surprise than a wrong path.
73
+ * Otherwise it is the hosted link service, which is what the core server used to be and is not.
74
+ */
75
+ function resolveLinkUrl(config) {
76
+ if (config.linkUrl)
77
+ return config.linkUrl;
78
+ if (config.serverUrl && withoutTrailingSlash(config.serverUrl) !== withoutTrailingSlash(exports.CONFIG.server))
79
+ return config.serverUrl;
80
+ return exports.CONFIG.link;
81
+ }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ import { APIPath } from "./config";
2
2
  import { Link, LinkOptions } from "./link";
3
3
  import { Conversation, ConversationOptions } from "./modules/conversation";
4
4
  import { UsagePolicyDataOptions } from "./modules/usage";
5
+ import { type CancelJobOptions, type GetJobOptions, type ListJobsOptions, type UpdateJobOptions, type UpdateJobSettingsOptions } from "./modules/jobs";
6
+ import { type AnswerDeliveryOptions, type ListDeliveriesOptions } from "./modules/outreach";
5
7
  type OptionalApiKey<T> = Omit<T, "apiKey"> & {
6
8
  /** Optional API key, defaults to API key specified in client */
7
9
  apiKey?: string;
@@ -9,6 +11,14 @@ type OptionalApiKey<T> = Omit<T, "apiKey"> & {
9
11
  export type ButlerBotClientOptions = {
10
12
  /** The server endpoint, API calls are sent here */
11
13
  serverUrl?: string;
14
+ /**
15
+ * Where links connect. Defaults to the hosted link service.
16
+ *
17
+ * Links are no longer carried by the core server, so this is a second address rather than
18
+ * a path on the first. A `serverUrl` pointing at your own stack is used for links too,
19
+ * unless this names somewhere else.
20
+ */
21
+ linkUrl?: string;
12
22
  /** The API key to use with ButlerBot */
13
23
  apiKey: string;
14
24
  /** Whether to enable debug logs */
@@ -17,6 +27,7 @@ export type ButlerBotClientOptions = {
17
27
  export declare class ButlerBotClient {
18
28
  private apiKey;
19
29
  private serverUrl;
30
+ private linkUrl;
20
31
  private debug;
21
32
  constructor(config: ButlerBotClientOptions);
22
33
  /** Checks the health of the server returning true if server is alive */
@@ -25,17 +36,42 @@ export declare class ButlerBotClient {
25
36
  createConversation<V extends APIPath = "v4">(config?: OptionalApiKey<ConversationOptions<V>>): Conversation<V>;
26
37
  /**
27
38
  * Creates a Link: a live connection that can register tools and hooks, and carry
28
- * conversations. Inherits the client's API key and server URL.
39
+ * conversations. Inherits the client's API key and link URL.
40
+ *
41
+ * The link URL, not the server URL: links are served by their own service. Pass
42
+ * `serverUrl` here, or `linkUrl` to the client, to point somewhere else.
29
43
  */
30
44
  createLink(config: OptionalApiKey<LinkOptions>): Link;
31
45
  /** Get current usage policy data */
32
46
  getUsagePolicyData(config: OptionalApiKey<UsagePolicyDataOptions>): Promise<import("./types/usage/policy").UsagePolicyData>;
47
+ /** A page of this user's jobs, with what their jobs may spend today */
48
+ listJobs(config?: OptionalApiKey<ListJobsOptions>): Promise<import("./types/type_registry").JobListResponse>;
49
+ /** One job, with its plan, its journal and the questions waiting on the user */
50
+ getJob(config: OptionalApiKey<GetJobOptions>): Promise<import("./types/type_registry").JobDetailResponse>;
51
+ /** Stops a job. Throws a `ButlerBotAPIError` with `isConflict` when it had already finished */
52
+ cancelJob(config: OptionalApiKey<CancelJobOptions>): Promise<import("./types/type_registry").JobCancelResponse>;
53
+ /** Changes one job's autonomy: how far it may act, until when, and what it always asks about */
54
+ updateJob(config: OptionalApiKey<UpdateJobOptions>): Promise<import("./types/type_registry").JobUpdateResponse>;
55
+ /** Changes this user's job settings: their daily allowance and what new jobs start with */
56
+ updateJobSettings(config?: OptionalApiKey<UpdateJobSettingsOptions>): Promise<import("./types/type_registry").JobSettingsResponse>;
57
+ /** A page of the inbox: what Alfred has told or asked this user outside a chat */
58
+ listDeliveries(config?: OptionalApiKey<ListDeliveriesOptions>): Promise<import("./types/type_registry").DeliveryListResponse>;
59
+ /** Answers a delivery. Throws a `ButlerBotAPIError` with `isConflict` when it was already answered */
60
+ answerDelivery(config: OptionalApiKey<AnswerDeliveryOptions>): Promise<import("./types/type_registry").DeliveryAnswerResponse>;
61
+ /** The client's own server and key underneath whatever the call named itself. */
62
+ private forRequest;
33
63
  }
34
64
  export * from "./types/type_registry";
35
65
  export * from "./link";
36
- export { Conversation, ConversationOptions };
66
+ export { Conversation };
67
+ export type { ConversationOptions };
37
68
  export type { APIPath };
38
69
  export type { ConversationStream, ConversationTransport, TransportTurnRequest, TransportHandlers, } from "./modules/transport";
70
+ export { ButlerBotAPIError } from "./util/api_error";
71
+ export { listJobs, getJob, cancelJob, updateJob, updateJobSettings } from "./modules/jobs";
72
+ export type { JobsRequestOptions, ListJobsOptions, GetJobOptions, CancelJobOptions, UpdateJobOptions, UpdateJobSettingsOptions, } from "./modules/jobs";
73
+ export { listDeliveries, answerDelivery } from "./modules/outreach";
74
+ export type { OutreachRequestOptions, ListDeliveriesOptions, AnswerDeliveryOptions, } from "./modules/outreach";
39
75
  export { LinkConversationTransport } from "./modules/transport_link";
40
76
  export { SSEConversationTransport } from "./modules/transport_sse";
41
77
  export type { SteerResult, TurnStopMode, TurnStopped } from "./modules/transport";
package/dist/index.js CHANGED
@@ -14,16 +14,31 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.SSEConversationTransport = exports.LinkConversationTransport = exports.Conversation = exports.ButlerBotClient = void 0;
17
+ exports.SSEConversationTransport = exports.LinkConversationTransport = exports.answerDelivery = exports.listDeliveries = exports.updateJobSettings = exports.updateJob = exports.cancelJob = exports.getJob = exports.listJobs = exports.ButlerBotAPIError = exports.Conversation = exports.ButlerBotClient = void 0;
18
18
  const config_1 = require("./config");
19
19
  const link_1 = require("./link");
20
20
  const conversation_1 = require("./modules/conversation");
21
21
  Object.defineProperty(exports, "Conversation", { enumerable: true, get: function () { return conversation_1.Conversation; } });
22
22
  const usage_1 = require("./modules/usage");
23
+ const jobs_1 = require("./modules/jobs");
24
+ const outreach_1 = require("./modules/outreach");
25
+ /**
26
+ * The options a caller actually gave, with the keys they left out removed.
27
+ *
28
+ * A client's own API key and URLs are the fallback for whatever a factory call omits, and the
29
+ * usual way to omit something is to forward an optional setting that happens to be unset —
30
+ * `createLink({ serverUrl: process.env.LINK_URL })`. Spread as it stands, that `undefined`
31
+ * lands on top of the client's resolved value and erases it, so the link ends up at the hosted
32
+ * service rather than the self-hosted stack the client was pointed at. Absent means "not given".
33
+ */
34
+ function given(config) {
35
+ return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined));
36
+ }
23
37
  class ButlerBotClient {
24
38
  constructor(config) {
25
39
  this.apiKey = config.apiKey;
26
40
  this.serverUrl = config.serverUrl || config_1.CONFIG.server;
41
+ this.linkUrl = (0, config_1.resolveLinkUrl)(config);
27
42
  this.debug = config.debug || false;
28
43
  }
29
44
  /** Checks the health of the server returning true if server is alive */
@@ -41,24 +56,70 @@ class ButlerBotClient {
41
56
  }
42
57
  /** Spawns a new Conversation, inherits api key and server URL */
43
58
  createConversation(config = {}) {
44
- return new conversation_1.Conversation({ debug: this.debug, apiKey: this.apiKey, serverUrl: this.serverUrl, ...config });
59
+ return new conversation_1.Conversation({ debug: this.debug, apiKey: this.apiKey, serverUrl: this.serverUrl, ...given(config) });
45
60
  }
46
61
  /**
47
62
  * Creates a Link: a live connection that can register tools and hooks, and carry
48
- * conversations. Inherits the client's API key and server URL.
63
+ * conversations. Inherits the client's API key and link URL.
64
+ *
65
+ * The link URL, not the server URL: links are served by their own service. Pass
66
+ * `serverUrl` here, or `linkUrl` to the client, to point somewhere else.
49
67
  */
50
68
  createLink(config) {
51
- return new link_1.Link({ debug: this.debug, apiKey: this.apiKey, serverUrl: this.serverUrl, ...config });
69
+ return new link_1.Link({ debug: this.debug, apiKey: this.apiKey, serverUrl: this.linkUrl, ...given(config) });
52
70
  }
53
71
  /** Get current usage policy data */
54
72
  getUsagePolicyData(config) {
55
73
  return (0, usage_1.getUsagePolicyData)({ serverURL: this.serverUrl, apiKey: this.apiKey, debug: this.debug, ...config });
56
74
  }
75
+ /** A page of this user's jobs, with what their jobs may spend today */
76
+ listJobs(config = {}) {
77
+ return (0, jobs_1.listJobs)(this.forRequest(config));
78
+ }
79
+ /** One job, with its plan, its journal and the questions waiting on the user */
80
+ getJob(config) {
81
+ return (0, jobs_1.getJob)(this.forRequest(config));
82
+ }
83
+ /** Stops a job. Throws a `ButlerBotAPIError` with `isConflict` when it had already finished */
84
+ cancelJob(config) {
85
+ return (0, jobs_1.cancelJob)(this.forRequest(config));
86
+ }
87
+ /** Changes one job's autonomy: how far it may act, until when, and what it always asks about */
88
+ updateJob(config) {
89
+ return (0, jobs_1.updateJob)(this.forRequest(config));
90
+ }
91
+ /** Changes this user's job settings: their daily allowance and what new jobs start with */
92
+ updateJobSettings(config = {}) {
93
+ return (0, jobs_1.updateJobSettings)(this.forRequest(config));
94
+ }
95
+ /** A page of the inbox: what Alfred has told or asked this user outside a chat */
96
+ listDeliveries(config = {}) {
97
+ return (0, outreach_1.listDeliveries)(this.forRequest(config));
98
+ }
99
+ /** Answers a delivery. Throws a `ButlerBotAPIError` with `isConflict` when it was already answered */
100
+ answerDelivery(config) {
101
+ return (0, outreach_1.answerDelivery)(this.forRequest(config));
102
+ }
103
+ /** The client's own server and key underneath whatever the call named itself. */
104
+ forRequest(config) {
105
+ return { serverURL: this.serverUrl, apiKey: this.apiKey, debug: this.debug, ...given(config) };
106
+ }
57
107
  }
58
108
  exports.ButlerBotClient = ButlerBotClient;
59
109
  // Expose types from subsequent modules
60
110
  __exportStar(require("./types/type_registry"), exports);
61
111
  __exportStar(require("./link"), exports);
112
+ var api_error_1 = require("./util/api_error");
113
+ Object.defineProperty(exports, "ButlerBotAPIError", { enumerable: true, get: function () { return api_error_1.ButlerBotAPIError; } });
114
+ var jobs_2 = require("./modules/jobs");
115
+ Object.defineProperty(exports, "listJobs", { enumerable: true, get: function () { return jobs_2.listJobs; } });
116
+ Object.defineProperty(exports, "getJob", { enumerable: true, get: function () { return jobs_2.getJob; } });
117
+ Object.defineProperty(exports, "cancelJob", { enumerable: true, get: function () { return jobs_2.cancelJob; } });
118
+ Object.defineProperty(exports, "updateJob", { enumerable: true, get: function () { return jobs_2.updateJob; } });
119
+ Object.defineProperty(exports, "updateJobSettings", { enumerable: true, get: function () { return jobs_2.updateJobSettings; } });
120
+ var outreach_2 = require("./modules/outreach");
121
+ Object.defineProperty(exports, "listDeliveries", { enumerable: true, get: function () { return outreach_2.listDeliveries; } });
122
+ Object.defineProperty(exports, "answerDelivery", { enumerable: true, get: function () { return outreach_2.answerDelivery; } });
62
123
  var transport_link_1 = require("./modules/transport_link");
63
124
  Object.defineProperty(exports, "LinkConversationTransport", { enumerable: true, get: function () { return transport_link_1.LinkConversationTransport; } });
64
125
  var transport_sse_1 = require("./modules/transport_sse");
@@ -49,6 +49,7 @@ export type LinkOptions = {
49
49
  * one takes over and the older one's registrations are released.
50
50
  */
51
51
  linkId: string;
52
+ /** The link service. Defaults to the hosted one, which is not the core server. */
52
53
  serverUrl?: string;
53
54
  /** Informational, shown in server logs. Defaults to the SDK name. */
54
55
  client?: string;
package/dist/link/link.js CHANGED
@@ -66,7 +66,7 @@ class Link {
66
66
  reconnect: true,
67
67
  debug: false,
68
68
  client: "@butlerbot/sdk",
69
- serverUrl: config_1.CONFIG.server,
69
+ serverUrl: config_1.CONFIG.link,
70
70
  socketFactory: socket_1.defaultSocketFactory,
71
71
  ...stripUndefined(options),
72
72
  };
@@ -236,12 +236,19 @@ export type LinkServerPayloads = {
236
236
  chatId?: string;
237
237
  message: string;
238
238
  };
239
+ /**
240
+ * A turn ended, one way or another.
241
+ *
242
+ * `lastEventId` comes with `turn_suspended`: the turn is still being answered somewhere
243
+ * and this is where a `conversation.attach` should pick it back up.
244
+ */
239
245
  "conversation.done": {
240
246
  chatId?: string;
241
247
  ok: boolean;
242
248
  code?: string;
243
249
  error?: string;
244
250
  message?: string;
251
+ lastEventId?: string;
245
252
  };
246
253
  /** A turn was stopped. `mode` is what was applied, `requestedMode` what was asked for. */
247
254
  "conversation.stopped": {
@@ -0,0 +1,18 @@
1
+ export type APIRequestOptions = {
2
+ url: string;
3
+ /** GET when nothing says otherwise. */
4
+ method?: "GET" | "POST" | "PATCH" | "DELETE";
5
+ /** Sent as JSON. A route that takes no body is called without one. */
6
+ body?: unknown;
7
+ /** What the caller was trying to do, for the message a failure carries. */
8
+ action: string;
9
+ };
10
+ /**
11
+ * One call to a JSON route, with the failure turned into a `ButlerBotAPIError`.
12
+ *
13
+ * The body is read as text once and parsed here rather than through `response.json()`, because
14
+ * a failing route's body has to be readable both as the parsed `error`/`errormessage` and as
15
+ * whatever else it carries — the job on a rejected cancel, the delivery on a rejected answer —
16
+ * and a response body can only be consumed once.
17
+ */
18
+ export declare function requestAPI<T>(options: APIRequestOptions): Promise<T>;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requestAPI = requestAPI;
4
+ const api_error_1 = require("../util/api_error");
5
+ /**
6
+ * One call to a JSON route, with the failure turned into a `ButlerBotAPIError`.
7
+ *
8
+ * The body is read as text once and parsed here rather than through `response.json()`, because
9
+ * a failing route's body has to be readable both as the parsed `error`/`errormessage` and as
10
+ * whatever else it carries — the job on a rejected cancel, the delivery on a rejected answer —
11
+ * and a response body can only be consumed once.
12
+ */
13
+ async function requestAPI(options) {
14
+ const init = { method: options.method || "GET" };
15
+ if (options.body !== undefined) {
16
+ init.headers = { "Content-Type": "application/json" };
17
+ init.body = JSON.stringify(options.body);
18
+ }
19
+ const response = await fetch(options.url, init);
20
+ const text = await response.text();
21
+ const data = parseJSON(text);
22
+ const envelope = (data && typeof data === "object" ? data : {});
23
+ if (!response.ok || envelope.success !== true) {
24
+ throw new api_error_1.ButlerBotAPIError({
25
+ action: options.action,
26
+ status: response.status,
27
+ statusText: response.statusText,
28
+ error: envelope.error,
29
+ errormessage: envelope.errormessage,
30
+ body: data ?? text,
31
+ });
32
+ }
33
+ return data;
34
+ }
35
+ function parseJSON(text) {
36
+ if (!text)
37
+ return undefined;
38
+ try {
39
+ return JSON.parse(text);
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
@@ -0,0 +1,48 @@
1
+ import type { JobAutonomy, JobCancelResponse, JobDetailResponse, JobListResponse, JobSettingsResponse, JobSettingsUpdate, JobStatus, JobUpdateResponse } from "../types/jobs";
2
+ /** What every jobs call needs: where the server is, and who is asking. */
3
+ export type JobsRequestOptions = {
4
+ serverURL?: string;
5
+ /** The base path for jobs, when it is not the default. */
6
+ path?: string;
7
+ apiKey: string;
8
+ debug?: boolean;
9
+ };
10
+ export type ListJobsOptions = JobsRequestOptions & {
11
+ /** 1-based. The first page when it is not given. */
12
+ page?: number;
13
+ /** How many jobs per page. */
14
+ limit?: number;
15
+ /** Only jobs in this status. */
16
+ status?: JobStatus;
17
+ };
18
+ export type GetJobOptions = JobsRequestOptions & {
19
+ jobId: string;
20
+ };
21
+ export type CancelJobOptions = JobsRequestOptions & {
22
+ jobId: string;
23
+ };
24
+ export type UpdateJobOptions = JobsRequestOptions & {
25
+ jobId: string;
26
+ /** How far this job may act outward on its own. */
27
+ autonomy?: JobAutonomy;
28
+ /** When that autonomy lapses back to asking. Null clears it. */
29
+ autonomyUntil?: string | number | null;
30
+ /** Things this job asks about however free it otherwise is. */
31
+ alwaysAsk?: string[];
32
+ };
33
+ export type UpdateJobSettingsOptions = JobsRequestOptions & JobSettingsUpdate;
34
+ /** A page of the caller's jobs, newest first, with what their jobs may spend today. */
35
+ export declare function listJobs(options: ListJobsOptions): Promise<JobListResponse>;
36
+ /** One job with its plan, its journal and the questions of its nobody has answered. */
37
+ export declare function getJob(options: GetJobOptions): Promise<JobDetailResponse>;
38
+ /**
39
+ * Stops a job.
40
+ *
41
+ * A job that had already finished is a 409: the call throws a `ButlerBotAPIError` whose
42
+ * `isConflict` is true and whose `body` still carries the job as it stands.
43
+ */
44
+ export declare function cancelJob(options: CancelJobOptions): Promise<JobCancelResponse>;
45
+ /** Changes one job's autonomy: how far it may act, until when, and what it always asks about. */
46
+ export declare function updateJob(options: UpdateJobOptions): Promise<JobUpdateResponse>;
47
+ /** Changes the owner's job settings: their daily allowance and the autonomy new jobs start with. */
48
+ export declare function updateJobSettings(options: UpdateJobSettingsOptions): Promise<JobSettingsResponse>;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listJobs = listJobs;
4
+ exports.getJob = getJob;
5
+ exports.cancelJob = cancelJob;
6
+ exports.updateJob = updateJob;
7
+ exports.updateJobSettings = updateJobSettings;
8
+ const config_1 = require("../config");
9
+ const url_formatter_1 = require("../util/url_formatter");
10
+ const api_request_1 = require("./api_request");
11
+ function jobsBase(options) {
12
+ return (options.serverURL || config_1.CONFIG.server) + (options.path || config_1.CONFIG.paths.jobs.base);
13
+ }
14
+ /** Only what was actually given: `URLSearchParams` would otherwise send the word "undefined". */
15
+ function query(params) {
16
+ const given = {};
17
+ for (const [key, value] of Object.entries(params)) {
18
+ if (value !== undefined)
19
+ given[key] = String(value);
20
+ }
21
+ return given;
22
+ }
23
+ /** Only the keys a caller set, so an omitted field is not sent as an explicit `undefined`. */
24
+ function body(fields) {
25
+ return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
26
+ }
27
+ /** A page of the caller's jobs, newest first, with what their jobs may spend today. */
28
+ async function listJobs(options) {
29
+ const url = (0, url_formatter_1.formatURL)(jobsBase(options), query({ page: options.page, limit: options.limit, status: options.status }), { apiKey: options.apiKey, debug: options.debug });
30
+ return (0, api_request_1.requestAPI)({ url, action: "list jobs" });
31
+ }
32
+ /** One job with its plan, its journal and the questions of its nobody has answered. */
33
+ async function getJob(options) {
34
+ const url = (0, url_formatter_1.formatURL)(`${jobsBase(options)}/${encodeURIComponent(options.jobId)}`, {}, { apiKey: options.apiKey, debug: options.debug });
35
+ return (0, api_request_1.requestAPI)({ url, action: `read job ${options.jobId}` });
36
+ }
37
+ /**
38
+ * Stops a job.
39
+ *
40
+ * A job that had already finished is a 409: the call throws a `ButlerBotAPIError` whose
41
+ * `isConflict` is true and whose `body` still carries the job as it stands.
42
+ */
43
+ async function cancelJob(options) {
44
+ const url = (0, url_formatter_1.formatURL)(`${jobsBase(options)}/${encodeURIComponent(options.jobId)}/cancel`, {}, { apiKey: options.apiKey, debug: options.debug });
45
+ return (0, api_request_1.requestAPI)({ url, method: "POST", action: `cancel job ${options.jobId}` });
46
+ }
47
+ /** Changes one job's autonomy: how far it may act, until when, and what it always asks about. */
48
+ async function updateJob(options) {
49
+ const url = (0, url_formatter_1.formatURL)(`${jobsBase(options)}/${encodeURIComponent(options.jobId)}`, {}, { apiKey: options.apiKey, debug: options.debug });
50
+ return (0, api_request_1.requestAPI)({
51
+ url,
52
+ method: "PATCH",
53
+ body: body({ autonomy: options.autonomy, autonomyUntil: options.autonomyUntil, alwaysAsk: options.alwaysAsk }),
54
+ action: `update job ${options.jobId}`,
55
+ });
56
+ }
57
+ /** Changes the owner's job settings: their daily allowance and the autonomy new jobs start with. */
58
+ async function updateJobSettings(options) {
59
+ const endpoint = (options.serverURL || config_1.CONFIG.server) + (options.path || config_1.CONFIG.paths.jobs.settings);
60
+ const url = (0, url_formatter_1.formatURL)(endpoint, {}, { apiKey: options.apiKey, debug: options.debug });
61
+ return (0, api_request_1.requestAPI)({
62
+ url,
63
+ method: "PATCH",
64
+ body: body({
65
+ allowanceUsd: options.allowanceUsd,
66
+ allowancePercent: options.allowancePercent,
67
+ autonomy: options.autonomy,
68
+ alwaysAsk: options.alwaysAsk,
69
+ }),
70
+ action: "update job settings",
71
+ });
72
+ }
@@ -0,0 +1,31 @@
1
+ import type { DeliveryAnswerResponse, DeliveryListResponse } from "../types/outreach";
2
+ /** What every outreach call needs: where the server is, and who is asking. */
3
+ export type OutreachRequestOptions = {
4
+ serverURL?: string;
5
+ /** The base path for outreach, when it is not the default. */
6
+ path?: string;
7
+ apiKey: string;
8
+ debug?: boolean;
9
+ };
10
+ export type ListDeliveriesOptions = OutreachRequestOptions & {
11
+ /** 1-based. The first page when it is not given. */
12
+ page?: number;
13
+ /** How many deliveries per page. */
14
+ limit?: number;
15
+ };
16
+ export type AnswerDeliveryOptions = OutreachRequestOptions & {
17
+ deliveryId: string;
18
+ /** The user's answer, in their own words. */
19
+ text: string;
20
+ /** On an approval, what they decided. */
21
+ decision?: "approve" | "deny";
22
+ };
23
+ /** A page of the inbox: what Alfred has told or asked this user outside a chat. */
24
+ export declare function listDeliveries(options: ListDeliveriesOptions): Promise<DeliveryListResponse>;
25
+ /**
26
+ * Answers one delivery, which closes it and wakes the job that asked.
27
+ *
28
+ * A delivery somebody has already answered is a 409: the call throws a `ButlerBotAPIError`
29
+ * whose `isConflict` is true and whose `body` still carries the delivery with the answer on it.
30
+ */
31
+ export declare function answerDelivery(options: AnswerDeliveryOptions): Promise<DeliveryAnswerResponse>;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listDeliveries = listDeliveries;
4
+ exports.answerDelivery = answerDelivery;
5
+ const config_1 = require("../config");
6
+ const url_formatter_1 = require("../util/url_formatter");
7
+ const api_request_1 = require("./api_request");
8
+ function outreachBase(options) {
9
+ return (options.serverURL || config_1.CONFIG.server) + (options.path || config_1.CONFIG.paths.outreach.base);
10
+ }
11
+ /** Only what was actually given: `URLSearchParams` would otherwise send the word "undefined". */
12
+ function query(params) {
13
+ const given = {};
14
+ for (const [key, value] of Object.entries(params)) {
15
+ if (value !== undefined)
16
+ given[key] = String(value);
17
+ }
18
+ return given;
19
+ }
20
+ /** A page of the inbox: what Alfred has told or asked this user outside a chat. */
21
+ async function listDeliveries(options) {
22
+ const url = (0, url_formatter_1.formatURL)(outreachBase(options), query({ page: options.page, limit: options.limit }), { apiKey: options.apiKey, debug: options.debug });
23
+ return (0, api_request_1.requestAPI)({ url, action: "list deliveries" });
24
+ }
25
+ /**
26
+ * Answers one delivery, which closes it and wakes the job that asked.
27
+ *
28
+ * A delivery somebody has already answered is a 409: the call throws a `ButlerBotAPIError`
29
+ * whose `isConflict` is true and whose `body` still carries the delivery with the answer on it.
30
+ */
31
+ async function answerDelivery(options) {
32
+ const url = (0, url_formatter_1.formatURL)(`${outreachBase(options)}/${encodeURIComponent(options.deliveryId)}/answer`, {}, { apiKey: options.apiKey, debug: options.debug });
33
+ return (0, api_request_1.requestAPI)({
34
+ url,
35
+ method: "POST",
36
+ body: options.decision === undefined ? { text: options.text } : { text: options.text, decision: options.decision },
37
+ action: `answer delivery ${options.deliveryId}`,
38
+ });
39
+ }
@@ -50,6 +50,35 @@ export declare class LinkConversationTransport implements ConversationTransport
50
50
  */
51
51
  attach(request: TransportAttachRequest, handlers: TransportHandlers): ConversationStream;
52
52
  private runTurn;
53
+ /**
54
+ * One `conversation.attach`: streams a turn's events to the caller and resolves with how
55
+ * that watch ended.
56
+ *
57
+ * Shared by watching somebody else's turn and by rejoining one of our own, because from
58
+ * here the two are the same act. `progress` is carried rather than returned: a watch that
59
+ * dies halfway still has to leave behind where it got to, or a resume would replay from
60
+ * the beginning and the caller would read the answer twice.
61
+ */
62
+ private watch;
63
+ /**
64
+ * Picks a turn back up after losing sight of it.
65
+ *
66
+ * Two ways to lose one, one way to get it back. The socket can go — a deploy of the link
67
+ * service, a proxy timing out — which rejects the exchange carrying the turn. Or core can
68
+ * suspend the turn at its own deploy and hand it to another instance, which the link
69
+ * service follows for two minutes before giving up and saying `turn_suspended`. Either way
70
+ * the turn is still being answered and the conversation still holds it, so this re-attaches
71
+ * from the last event the caller was actually given and the stream reads as one answer.
72
+ *
73
+ * `no_active_turn` is the ambiguous reply and it is deliberately not treated as an ending:
74
+ * during a handover it means "not picked up yet" far more often than it means "gone", and
75
+ * the window is what decides between them.
76
+ *
77
+ * Nothing here throws. It runs behind a stream the caller already holds, so the outcomes
78
+ * that matter are the ones delivered into it: a completion, or a failure that says plainly
79
+ * that the answer was lost track of rather than that it failed.
80
+ */
81
+ private resume;
53
82
  /** Opens a session, or reuses the open one when it is for the same conversation. */
54
83
  private session;
55
84
  }