@oberik/sdk 0.1.0 → 0.2.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/dist/cjs/index.js CHANGED
@@ -33,7 +33,8 @@
33
33
  * await handle.done;
34
34
  */
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.AgentFramework = exports.AgentStreamError = exports.AgentApiError = exports.AgentCancelledError = exports.DEFAULT_BASE_URL = void 0;
36
+ exports.OberikProject = exports.DEFAULT_CONTROL_PLANE_URL = exports.AgentFramework = exports.AgentStreamError = exports.AgentApiError = exports.AgentCancelledError = exports.DEFAULT_BASE_URL = void 0;
37
+ exports.createProjectClient = createProjectClient;
37
38
  exports.createClient = createClient;
38
39
  // ============================================================================
39
40
  // Types
@@ -96,6 +97,19 @@ function toolSchemas(tools) {
96
97
  function: { name: t.name, description: t.description, parameters: t.parameters ?? { type: "object", properties: {} } },
97
98
  }));
98
99
  }
100
+ /** The same shape as a client tool on the wire — the server builds a callable tool from
101
+ * it either way. What differs is what happens when the agent calls it: a client tool's
102
+ * result is awaited, and this one's is nothing. */
103
+ function uiToolSchemas(ui) {
104
+ return [...ui.values()].map((c) => ({
105
+ type: "function",
106
+ function: {
107
+ name: c.name,
108
+ description: c.description,
109
+ parameters: c.parameters ?? { type: "object", properties: {} },
110
+ },
111
+ }));
112
+ }
99
113
  /** Run each pending client tool via its handler; failures become error results
100
114
  * (not thrown) so one bad tool doesn't abort the whole turn. */
101
115
  async function executeToolCalls(calls, tools) {
@@ -117,6 +131,26 @@ async function executeToolCalls(calls, tools) {
117
131
  return { tool_call_id: call.id, content };
118
132
  }));
119
133
  }
134
+ /** Put each approval request to the handler and shape the result for the API.
135
+ *
136
+ * A handler that throws is treated as a refusal rather than as a failure. That is the
137
+ * safe direction and the only defensible one: the alternative is a UI bug becoming an
138
+ * irreversible action. */
139
+ async function collectDecisions(pending, handler) {
140
+ const out = [];
141
+ for (const request of pending) {
142
+ let decision;
143
+ try {
144
+ const result = await handler(request);
145
+ decision = typeof result === "boolean" ? { approved: result } : result;
146
+ }
147
+ catch (e) {
148
+ decision = { approved: false, note: String(e?.message ?? e) };
149
+ }
150
+ out.push({ ...decision, tool_call_id: decision.tool_call_id ?? request.tool_call_id });
151
+ }
152
+ return out;
153
+ }
120
154
  /** Put each paused question batch to the handler and shape the result for the API.
121
155
  *
122
156
  * A handler that throws is treated as the user declining rather than as a failure:
@@ -199,6 +233,7 @@ class AgentFramework {
199
233
  opts;
200
234
  _fetch;
201
235
  toolRegistry = new Map();
236
+ uiRegistry = new Map();
202
237
  /** Active session watchers, so a streamed turn can mark its own messages seen. */
203
238
  watchers = new Set();
204
239
  /** The bearer in use: `opts.token` initially, replaced on refresh. */
@@ -220,6 +255,8 @@ class AgentFramework {
220
255
  this.currentToken = opts.token;
221
256
  for (const t of opts.tools ?? [])
222
257
  this.toolRegistry.set(t.name, t);
258
+ for (const c of opts.ui ?? [])
259
+ this.uiRegistry.set(c.name, c);
223
260
  }
224
261
  /** Seconds before a token's own expiry at which we stop using it.
225
262
  *
@@ -695,6 +732,19 @@ class AgentFramework {
695
732
  this.toolRegistry.set(t.name, t);
696
733
  return this;
697
734
  }
735
+ /** Register a UI component the agent can draw into your app.
736
+ *
737
+ * Unlike a tool, nothing is handed back: the agent calls it, your `render` runs, and
738
+ * the turn carries on without waiting. */
739
+ registerUi(component) {
740
+ this.uiRegistry.set(component.name, component);
741
+ return this;
742
+ }
743
+ registerUiComponents(components) {
744
+ for (const c of components)
745
+ this.uiRegistry.set(c.name, c);
746
+ return this;
747
+ }
698
748
  /** Merge the client-level registry with any per-call tools (per-call wins). */
699
749
  resolveTools(extra) {
700
750
  const m = new Map(this.toolRegistry);
@@ -702,6 +752,29 @@ class AgentFramework {
702
752
  m.set(t.name, t);
703
753
  return m;
704
754
  }
755
+ resolveUi(extra) {
756
+ const m = new Map(this.uiRegistry);
757
+ for (const c of extra ?? [])
758
+ m.set(c.name, c);
759
+ return m;
760
+ }
761
+ /** Draw whatever the agent asked for, in order.
762
+ *
763
+ * A component that throws is logged and skipped: one broken chart must not take down
764
+ * the turn that drew it, and there is nothing to report back to the agent anyway. */
765
+ renderUi(renders, registry) {
766
+ for (const r of renders ?? []) {
767
+ const component = registry.get(r.name);
768
+ if (!component)
769
+ continue;
770
+ try {
771
+ component.render(r.args ?? {});
772
+ }
773
+ catch (e) {
774
+ console.warn(`[oberik] UI component "${r.name}" threw while rendering:`, e);
775
+ }
776
+ }
777
+ }
705
778
  // -- auth headers --------------------------------------------------------
706
779
  /** `bearer` overrides token resolution — used to replay a request with the token a
707
780
  * refresh just produced, instead of asking for one again. */
@@ -1006,6 +1079,50 @@ class AgentFramework {
1006
1079
  },
1007
1080
  };
1008
1081
  // -- documents -----------------------------------------------------------
1082
+ /** Agent Plugins — the skills this token can reach for, and the ones it may add.
1083
+ *
1084
+ * Two sources, one list: what the project published plus anything this end-user
1085
+ * uploaded. Uploading needs the `plugins:write` capability; reading does not, because
1086
+ * a project that publishes a procedure wants its agent to use it. */
1087
+ plugins = {
1088
+ list: () => this.request("GET", "/plugins"),
1089
+ delete: (id) => this.request("DELETE", `/plugins/${id}`),
1090
+ /** Publish a skill. Re-uploading a name replaces it.
1091
+ *
1092
+ * Takes whatever the customer actually has:
1093
+ *
1094
+ * - a packaged Agent Plugin (`plugin.json` + `skills/`), read as-is;
1095
+ * - a zipped folder of skills, or a single `SKILL.md` — a manifest is written
1096
+ * for them, because requiring one to publish a file of instructions is a
1097
+ * packaging exercise standing in front of the feature;
1098
+ * - a folder's files, from a directory picker, each keyed by its relative path.
1099
+ *
1100
+ * An end-user's plugin is private to them and unioned on top of the project's —
1101
+ * only a project key can publish to everyone. */
1102
+ upload: async (file, opts = {}) => {
1103
+ const form = new FormData();
1104
+ const blob = typeof Blob !== "undefined" && file instanceof Blob
1105
+ ? file
1106
+ : new Blob([file], { type: "application/zip" });
1107
+ form.append("file", blob, opts.filename ?? file?.name ?? "plugin.zip");
1108
+ return this.request("POST", "/plugins", { form, signal: opts.signal });
1109
+ },
1110
+ /** Publish a folder of skills without zipping it.
1111
+ *
1112
+ * `files` is what a browser directory picker gives you. Each part is sent under
1113
+ * its path relative to the folder, which is all the server needs to lay the
1114
+ * skills out — so no zip library is needed on your side. */
1115
+ uploadFolder: async (files, opts = {}) => {
1116
+ const form = new FormData();
1117
+ for (const { path, content } of files) {
1118
+ const blob = typeof Blob !== "undefined" && content instanceof Blob
1119
+ ? content
1120
+ : new Blob([content]);
1121
+ form.append("files", blob, path);
1122
+ }
1123
+ return this.request("POST", "/plugins", { form, signal: opts.signal });
1124
+ },
1125
+ };
1009
1126
  documents = {
1010
1127
  list: (query = {}) => this.request("GET", "/documents", { query }),
1011
1128
  get: (id) => this.request("GET", `/documents/${id}`),
@@ -1096,20 +1213,32 @@ class AgentFramework {
1096
1213
  get: (id) => this.request("GET", `/tasks/${id}`),
1097
1214
  cancel: (id) => this.request("POST", `/tasks/${id}/cancel`),
1098
1215
  };
1099
- // -- knowledge sources (ai.sync data connectors) -------------------------
1100
- sources = {
1101
- list: () => this.request("GET", "/sources"),
1102
- get: (id) => this.request("GET", `/sources/${id}`),
1103
- types: () => this.request("GET", "/sources/types"),
1104
- /** Rotate credentials, retag, change the refresh cadence or the ACL. A new
1105
- * `config` is tested before it replaces the old one, so a bad DSN fails here
1106
- * (400) instead of at the next scheduled sync. Changing the interval
1107
- * reschedules; 0 turns auto-refresh off. */
1108
- patch: (id, body) => this.request("PATCH", `/sources/${id}`, { body }),
1109
- /** Trigger a sync now; pass `fullRefresh` to re-pull everything. */
1110
- sync: (id, fullRefresh = false) => this.request("POST", `/sources/${id}/sync`, { query: { full_refresh: fullRefresh } }),
1111
- remove: (id) => this.request("DELETE", `/sources/${id}`),
1216
+ /**
1217
+ * What the agent wrote down — remembered facts and wiki pages.
1218
+ *
1219
+ * Retrieval reads these back on every turn, so when an answer looks wrong a
1220
+ * remembered fact is often the reason. The ACL that governs retrieval governs this
1221
+ * too: a caller sees exactly what its token could have retrieved.
1222
+ */
1223
+ memory = {
1224
+ /** `kind` is "memory" (facts, per end-user) or "wiki" (pages, per user or shared). */
1225
+ list: (opts = {}) => this.request("GET", "/memory", { query: { kind: opts.kind ?? "memory", limit: opts.limit } }),
1226
+ /** Forget one. The agent can write it again; this removes what is there now. */
1227
+ delete: (itemId) => this.request("DELETE", `/memory/${itemId}`),
1112
1228
  };
1229
+ /**
1230
+ * What this token can actually do.
1231
+ *
1232
+ * The three gates on a turn are the platform's kill switch, the token's capability
1233
+ * and the per-request `enable_*` flag, and until this existed a client could read
1234
+ * none of them. `flags[].effective` is the useful one: false means setting that flag
1235
+ * changes nothing on this token — which is otherwise indistinguishable from the agent
1236
+ * simply choosing not to use the tool.
1237
+ *
1238
+ * Cheap and safe to call on load: a token asking what it holds is reading its own
1239
+ * claims back, so it needs no capability of its own.
1240
+ */
1241
+ capabilities = () => this.request("GET", "/capabilities");
1113
1242
  // -- governance: audit trail + right-to-be-forgotten (admin) -------------
1114
1243
  audit = {
1115
1244
  /** Read the tenant's audit trail (admin). Filter by action/subject. */
@@ -1162,81 +1291,86 @@ class AgentFramework {
1162
1291
  return res.blob();
1163
1292
  },
1164
1293
  };
1165
- /**
1166
- * Connect an external data source and keep it live in the agent's knowledge.
1167
- * Validates the connector, runs an initial sync, and (when an interval is set)
1168
- * schedules recurring auto-refresh so the data never goes stale.
1169
- *
1170
- * await ai.sync({ name: "orders", connector_type: "postgres",
1171
- * config: { dsn, query: "select id, status, total from orders",
1172
- * cursor_column: "updated_at" }, sync_interval_seconds: 900 });
1173
- */
1174
- sync = (config) => this.request("POST", "/sources", { body: config });
1175
1294
  // ==========================================================================
1176
1295
  // Automatic client-tool dispatch
1177
1296
  // ==========================================================================
1178
1297
  async runWithTools(opts) {
1179
- const { tools: extra, maxToolRounds, onToolCalls, onQuestion, ...body } = opts;
1298
+ const { tools: extra, ui: extraUi, maxToolRounds, onToolCalls, onQuestion, onApproval, ...body } = opts;
1180
1299
  const tools = this.resolveTools(extra);
1300
+ const ui = this.resolveUi(extraUi);
1181
1301
  const schemas = tools.size ? toolSchemas(tools) : undefined;
1302
+ const uiSchemas = ui.size ? uiToolSchemas(ui) : undefined;
1182
1303
  const maxRounds = maxToolRounds ?? 10;
1183
1304
  let resp = await this.request("POST", "/chat", {
1184
- body: { ...body, client_tools: schemas },
1305
+ body: { ...body, client_tools: schemas, ui_tools: uiSchemas },
1185
1306
  signal: opts.signal,
1186
1307
  });
1308
+ this.renderUi(resp.ui, ui);
1187
1309
  let rounds = 0;
1188
- // Two ways a turn pauses: work for the client to run, or a question for the user
1189
- // to answer. Both resume the same way, and the round budget covers both together
1190
- // so a pathological alternation can't loop forever.
1310
+ // Three ways a turn pauses: work for the client to run, a question for the user to
1311
+ // answer, or permission to ask for. All resume the same way, and the round budget
1312
+ // covers them together so a pathological alternation can't loop forever.
1191
1313
  for (;;) {
1192
1314
  const hasTools = resp.requires_action && resp.tool_calls.length > 0;
1193
1315
  const hasQuestions = onQuestion != null && (resp.questions?.length ?? 0) > 0;
1194
- if (!hasTools && !hasQuestions)
1316
+ const hasApprovals = onApproval != null && (resp.approvals?.length ?? 0) > 0;
1317
+ if (!hasTools && !hasQuestions && !hasApprovals)
1195
1318
  return resp;
1196
1319
  if (rounds++ >= maxRounds)
1197
1320
  throw new AgentStreamError(`agent pause loop exceeded maxToolRounds (${maxRounds})`);
1198
- const next = { session_id: resp.session_id, client_tools: schemas };
1321
+ const next = {
1322
+ session_id: resp.session_id, client_tools: schemas, ui_tools: uiSchemas,
1323
+ };
1199
1324
  if (hasTools) {
1200
1325
  onToolCalls?.(resp.tool_calls);
1201
1326
  next.tool_results = await executeToolCalls(resp.tool_calls, tools);
1202
1327
  }
1203
1328
  if (hasQuestions)
1204
1329
  next.question_answers = await collectAnswers(resp.questions, onQuestion);
1330
+ if (hasApprovals)
1331
+ next.approval_decisions = await collectDecisions(resp.approvals, onApproval);
1205
1332
  resp = await this.request("POST", "/chat", { body: next, signal: opts.signal });
1333
+ this.renderUi(resp.ui, ui);
1206
1334
  }
1207
1335
  }
1208
1336
  /** Wrap startStream so a `done` carrying `requires_action` auto-executes the
1209
1337
  * client tools and continues the stream (same session) until the agent ends. */
1210
1338
  startStreamWithTools(body, handlers) {
1211
1339
  const tools = this.resolveTools(handlers.tools);
1340
+ const ui = this.resolveUi(handlers.ui);
1212
1341
  const onQuestion = handlers.onQuestion;
1213
- if (tools.size === 0 && onQuestion == null)
1342
+ const onApproval = handlers.onApproval;
1343
+ if (tools.size === 0 && ui.size === 0 && onQuestion == null && onApproval == null)
1214
1344
  return this.startStream(body, handlers);
1215
1345
  const schemas = tools.size ? toolSchemas(tools) : undefined;
1216
1346
  const maxRounds = handlers.maxToolRounds ?? 10;
1217
1347
  let current;
1218
1348
  let stopped = false;
1219
1349
  const done = (async () => {
1220
- let reqBody = { ...body, client_tools: schemas };
1350
+ const uiSchemas = ui.size ? uiToolSchemas(ui) : undefined;
1351
+ let reqBody = { ...body, client_tools: schemas, ui_tools: uiSchemas };
1221
1352
  let rounds = 0;
1222
1353
  for (;;) {
1223
1354
  current = this.startStream(reqBody, handlers);
1224
1355
  const d = await current.done;
1225
1356
  const hasTools = d.requires_action && d.tool_calls.length > 0;
1226
1357
  const hasQuestions = onQuestion != null && (d.questions?.length ?? 0) > 0;
1227
- if (!hasTools && !hasQuestions)
1358
+ const hasApprovals = onApproval != null && (d.approvals?.length ?? 0) > 0;
1359
+ if (!hasTools && !hasQuestions && !hasApprovals)
1228
1360
  return d;
1229
1361
  if (stopped)
1230
1362
  return d;
1231
1363
  if (rounds++ >= maxRounds)
1232
1364
  throw new AgentStreamError(`agent pause loop exceeded maxToolRounds (${maxRounds})`);
1233
- reqBody = { session_id: d.session_id, client_tools: schemas };
1365
+ reqBody = { session_id: d.session_id, client_tools: schemas, ui_tools: uiSchemas };
1234
1366
  if (hasTools) {
1235
1367
  handlers.onToolCalls?.(d.tool_calls);
1236
1368
  reqBody.tool_results = await executeToolCalls(d.tool_calls, tools);
1237
1369
  }
1238
1370
  if (hasQuestions)
1239
1371
  reqBody.question_answers = await collectAnswers(d.questions, onQuestion);
1372
+ if (hasApprovals)
1373
+ reqBody.approval_decisions = await collectDecisions(d.approvals, onApproval);
1240
1374
  }
1241
1375
  })();
1242
1376
  return makeStreamHandle(done, {
@@ -1373,6 +1507,9 @@ class AgentFramework {
1373
1507
  const onOuterAbort = () => ac.abort();
1374
1508
  handlers.signal?.addEventListener("abort", onOuterAbort);
1375
1509
  const maxRetries = handlers.maxRetries ?? 10;
1510
+ // The components this stream may draw. Resolved once: the registry can be added to
1511
+ // between turns, and a stream should draw with what it was started with.
1512
+ const uiRegistry = handlers.ui || this.uiRegistry.size ? this.resolveUi(handlers.ui) : undefined;
1376
1513
  let runId;
1377
1514
  let lastId = -1;
1378
1515
  let full = "";
@@ -1458,6 +1595,12 @@ class AgentFramework {
1458
1595
  case "todos":
1459
1596
  handlers.onTodos?.(ev.data.todos);
1460
1597
  break;
1598
+ case "ui":
1599
+ // Drawn the moment the agent calls it, mid-turn — waiting for `done`
1600
+ // would mean a chart appearing after the paragraph that refers to it.
1601
+ uiRegistry?.get(ev.data.name)?.render(ev.data.args ?? {});
1602
+ handlers.onUi?.(ev.data);
1603
+ break;
1461
1604
  case "subagent":
1462
1605
  subagents.set(ev.data.subagent.ref, ev.data.subagent);
1463
1606
  handlers.onSubagents?.([...subagents.values()]);
@@ -1743,6 +1886,150 @@ function safeJson(s) {
1743
1886
  return s;
1744
1887
  }
1745
1888
  }
1889
+ // ============================================================================
1890
+ // The server side: a project key, and everything it can do
1891
+ // ============================================================================
1892
+ /** Where projects are administered and end-user tokens are minted. A different host
1893
+ * from {@link DEFAULT_BASE_URL}, and a different credential — the two are not
1894
+ * interchangeable, which is the whole reason there are two clients. */
1895
+ exports.DEFAULT_CONTROL_PLANE_URL = "https://oberik.com";
1896
+ /**
1897
+ * The control plane, from your backend.
1898
+ *
1899
+ * The other client in this package talks to the data plane as one end-user. This one
1900
+ * holds the project key and administers the project itself: minting those tokens,
1901
+ * setting the capability ceiling, curating the corpus.
1902
+ *
1903
+ * They are separate classes on purpose. A project key can mint a token with any
1904
+ * capability the project allows — and create further keys, and delete the project — so
1905
+ * it must never travel to the same place an end-user token does. Two types make that a
1906
+ * decision someone has to make rather than a field they can accidentally set.
1907
+ */
1908
+ class OberikProject {
1909
+ baseUrl;
1910
+ projectId;
1911
+ key;
1912
+ _fetch;
1913
+ constructor(opts) {
1914
+ if (!opts.projectId)
1915
+ throw new Error("projectId is required");
1916
+ if (!opts.projectKey)
1917
+ throw new Error("projectKey is required");
1918
+ // A project key in a browser is never right: it mints any capability the project
1919
+ // allows and can delete the project outright. Bundling this by accident is easy —
1920
+ // it is one import away from the client that DOES belong there — so it fails at
1921
+ // construction, where the stack trace names the file that did it, rather than
1922
+ // shipping and leaking the key to every visitor.
1923
+ if (typeof window !== "undefined" && typeof window.document !== "undefined") {
1924
+ throw new Error("OberikProject holds a project key and must never run in a browser — it can mint " +
1925
+ "any capability the project allows, create more keys, and delete the project. " +
1926
+ "Mint tokens on your server and send the token to the browser instead: " +
1927
+ "createClient({ getToken }).");
1928
+ }
1929
+ this.projectId = opts.projectId;
1930
+ this.key = opts.projectKey;
1931
+ this.baseUrl = (opts.baseUrl || exports.DEFAULT_CONTROL_PLANE_URL).replace(/\/+$/, "");
1932
+ const raw = opts.fetch ?? globalThis.fetch;
1933
+ if (!raw)
1934
+ throw new Error("No fetch available; pass options.fetch");
1935
+ this._fetch = raw.bind(globalThis);
1936
+ }
1937
+ async request(method, path, body, form) {
1938
+ const res = await this._fetch(`${this.baseUrl}/api/projects/${this.projectId}${path}`, {
1939
+ method,
1940
+ headers: {
1941
+ "X-API-Key": this.key,
1942
+ ...(body !== undefined ? { "content-type": "application/json" } : {}),
1943
+ },
1944
+ body: form ?? (body !== undefined ? JSON.stringify(body) : undefined),
1945
+ });
1946
+ const text = await res.text();
1947
+ const data = text ? safeJson(text) : null;
1948
+ if (!res.ok) {
1949
+ throw new AgentApiError(res.status, (data && (data.error || data.message)) || `HTTP ${res.status}`);
1950
+ }
1951
+ return data;
1952
+ }
1953
+ tokens = {
1954
+ /**
1955
+ * Mint a short-lived token for ONE end-user.
1956
+ *
1957
+ * const { access_token } = await oberik.tokens.mint({
1958
+ * subject: `${user.orgId}:${user.id}`,
1959
+ * scope: `${user.orgId}:${user.id}`,
1960
+ * capabilities: ["chat", "documents:read"],
1961
+ * });
1962
+ */
1963
+ mint: (input) => this.request("POST", "/token", input),
1964
+ /**
1965
+ * The same thing shaped as the callback {@link createClient} wants, so the two
1966
+ * halves of this package fit together without a wrapper:
1967
+ *
1968
+ * const ai = createClient({ getToken: oberik.tokens.forUser({ subject: id }) });
1969
+ *
1970
+ * Called again whenever a token expires, so the client refreshes on its own.
1971
+ */
1972
+ forUser: (input) => async () => (await this.request("POST", "/token", input)).access_token,
1973
+ };
1974
+ /** The capability ceiling: the maximum any token minted here may hold. */
1975
+ capabilities = {
1976
+ get: () => this.request("GET", "/capabilities"),
1977
+ /** Merges — send only what you want to change. */
1978
+ set: (caps) => this.request("PATCH", "", { capabilities: caps }),
1979
+ };
1980
+ /** Documents owned by the project rather than by any one end-user: the corpus you
1981
+ * curate and your users only read. */
1982
+ documents = {
1983
+ list: () => this.request("GET", "/documents"),
1984
+ upload: (file, opts = {}) => {
1985
+ const form = new FormData();
1986
+ form.append("file", file, opts.filename ?? file.name ?? "upload.bin");
1987
+ form.append("tags", (opts.tags ?? []).join(","));
1988
+ return this.request("POST", "/documents", undefined, form);
1989
+ },
1990
+ delete: (documentId) => this.request("DELETE", `/documents/${documentId}`),
1991
+ };
1992
+ /** Prepended to every request for this project, above anything a caller sends. */
1993
+ systemPrompt = {
1994
+ set: (systemPrompt) => this.request("PUT", "/system-prompt", { systemPrompt }),
1995
+ };
1996
+ /** Browser origins allowed to call the data plane with this project's tokens. */
1997
+ origins = {
1998
+ set: (origins) => this.request("PUT", "/origins", { origins }),
1999
+ };
2000
+ /** Tools the agent calls by URL. The signing secret comes back once, on create. */
2001
+ webhookTools = {
2002
+ list: () => this.request("GET", "/webhook-tools"),
2003
+ create: (tool) => this.request("POST", "/webhook-tools", tool),
2004
+ delete: (toolId) => this.request("DELETE", `/webhook-tools/${toolId}`),
2005
+ };
2006
+ /** Server-side keys. A created one is returned once and never again. */
2007
+ /**
2008
+ * Further project keys.
2009
+ *
2010
+ * `scope` is the important argument and it defaults to the narrow one. A `mint` key
2011
+ * can turn your signed-in user into an end-user token and nothing else — it cannot
2012
+ * read the corpus, raise the capability ceiling, issue more keys, or delete the
2013
+ * project. That is what almost every backend actually needs, and it is the difference
2014
+ * between a leaked key costing you some tokens and costing you the workspace.
2015
+ */
2016
+ keys = {
2017
+ list: () => this.request("GET", "/keys"),
2018
+ create: (name, opts = {}) => this.request("POST", "/keys", { name, scope: opts.scope ?? "mint" }),
2019
+ revoke: (keyId) => this.request("DELETE", `/keys/${keyId}`),
2020
+ };
2021
+ /** Spend, requests, tokens and latency — including per end-user, since spend is
2022
+ * attributed to the token's subject. */
2023
+ usage = {
2024
+ summary: () => this.request("GET", "/usage"),
2025
+ observability: (windowSeconds = 86_400) => this.request("GET", `/observability?window=${windowSeconds}`),
2026
+ };
2027
+ }
2028
+ exports.OberikProject = OberikProject;
2029
+ /** Factory helper for the server-side client. */
2030
+ function createProjectClient(opts) {
2031
+ return new OberikProject(opts);
2032
+ }
1746
2033
  /** Factory helper. */
1747
2034
  function createClient(opts) {
1748
2035
  return new AgentFramework(opts);