@soba-so/sdk 0.0.9 → 0.0.11

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/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Message, RouteRequest, RunPolicy } from "./protocol.js";
2
- import type { ConnectContextResponse, MachineListItem, PlanListResponse, SobaUser, UsagePage, UsageQuery } from "./resources.js";
2
+ import type { ConnectContextResponse, FeatureCheckResult, MachineListItem, PlanListResponse, QuantityResult, SobaUser, TrackResult, UsagePage, UsageQuery } from "./resources.js";
3
3
  import { type Approver, Run } from "./run.js";
4
4
  import { type ToolDefinition } from "./tools.js";
5
5
  /**
@@ -15,11 +15,18 @@ import { type ToolDefinition } from "./tools.js";
15
15
  * two hosts both exist and why only this one is the run endpoint.
16
16
  */
17
17
  export declare const DEFAULT_BASE_URL = "https://api.soba.so/v1";
18
+ export declare const DEFAULT_WEB_BASE_URL = "https://soba.so/v1";
18
19
  export interface SobaOptions {
19
20
  /** Defaults to `SOBA_KEY`, then `SOBA_API_KEY`. Shaped `sk_soba_<id>.<secret>`. */
20
21
  apiKey?: string;
21
22
  /** Defaults to `SOBA_BASE_URL`, then https://api.soba.so/v1. */
22
23
  baseURL?: string;
24
+ /**
25
+ * Where the calls that touch your Stripe go: `users.setQuantity`. Defaults
26
+ * to `SOBA_WEB_URL`, then https://soba.so/v1 — the dashboard holds the
27
+ * Stripe App connection, and the run gateway at `baseURL` does not.
28
+ */
29
+ webBaseURL?: string;
23
30
  fetch?: typeof globalThis.fetch;
24
31
  /** Retries before the first byte only. Never after a stream has started. */
25
32
  maxRetries?: number;
@@ -56,7 +63,12 @@ export interface RunParams {
56
63
  prompt?: string;
57
64
  messages?: Message[];
58
65
  system?: string;
59
- /** Defaults to "auto": let the route decide. */
66
+ /**
67
+ * Defaults to "auto": the machine's own default model. `"small"` asks for its
68
+ * quick, cheap model and `"large"` for its most capable one, so a summary or a
69
+ * quick lookup does not spend the person's plan on the biggest model. A model
70
+ * name pins one.
71
+ */
60
72
  model?: string;
61
73
  /** Name a specific runtime only when you have a reason to. `route` is the
62
74
  * declaration that survives a user connecting something different. */
@@ -72,7 +84,21 @@ export interface RunParams {
72
84
  * means every escalation is denied. */
73
85
  approve?: Approver;
74
86
  metadata?: Record<string, string>;
87
+ /**
88
+ * The task in your app this run is part of — any id you choose, like
89
+ * `report_8812`. On a plan limited by tasks, every run with the same id
90
+ * counts once, so a task that takes several agent runs is one task. A run
91
+ * without one is a task on its own.
92
+ */
93
+ task?: string;
75
94
  signal?: AbortSignal;
95
+ /**
96
+ * Stop the run after this many milliseconds. It can only shorten a run: the
97
+ * app's and the organisation's limits still apply, and the lowest wins. A run
98
+ * stopped at its limit ends with an `error` whose `reason` is `run_too_long`.
99
+ * https://soba.so/docs/runs#how-long-a-run-may-last
100
+ */
101
+ maxDurationMs?: number;
76
102
  /**
77
103
  * Keep the run going if this request goes away, and keep its frames so it
78
104
  * can be read again: `soba.runs.attach(run.runId, { user, tools, after:
@@ -83,6 +109,22 @@ export interface RunParams {
83
109
  */
84
110
  resumable?: boolean;
85
111
  }
112
+ /** A detached run, started. */
113
+ export interface StartedRun {
114
+ id: string;
115
+ /**
116
+ * WHERE A PAGE CAN WATCH IT, read-only: hand both to the browser and read the
117
+ * run there (`watchRun` in `@soba-so/react`) rather than streaming it through
118
+ * your server. Absent when the gateway has no run-token key.
119
+ */
120
+ stream?: {
121
+ url: string;
122
+ token: string;
123
+ expiresAt: number;
124
+ };
125
+ /** With `answerTools: "here"`: settles when the run ends, after its last tool call is answered. */
126
+ answered?: Promise<void>;
127
+ }
86
128
  /** Reading a resumable run again. See `RunParams.resumable`. */
87
129
  export interface AttachParams {
88
130
  /** The same `user` the run was started for: your tools are handed it, as on `soba.run`. */
@@ -105,20 +147,90 @@ export interface AttachParams {
105
147
  * anywhere and the app reads it with `soba.runs.attach(id, …)` — no public URL,
106
148
  * no tunnel on a laptop.
107
149
  */
150
+ export type OpKind = "git.snapshot" | "git.diff" | "git.push" | "git.update" | "git.worktree.add" | "git.worktree.remove" | "fs.list" | "fs.read" | "fs.write" | "fs.copy";
151
+ export interface OpParams {
152
+ /** Whose machine. Resolve it on your server, never from the browser. */
153
+ user: string;
154
+ kind: OpKind;
155
+ /** The repository checkout on that machine. Must be inside a granted root. */
156
+ repo: string;
157
+ /** The workspace's branch; its worktree lives under `repo`. */
158
+ branch?: string;
159
+ /** The branch it compares to or updates from. */
160
+ base?: string;
161
+ /** `git.diff`, `fs.read`, `fs.write`: one file. `fs.list`: a folder. Relative to the worktree. */
162
+ path?: string;
163
+ /** `fs.write`: the file's new text. Only an existing file is written. */
164
+ text?: string;
165
+ /** `fs.write`: the hash `fs.read` gave; a file changed since is refused with `conflict`. */
166
+ expect?: string;
167
+ /** `fs.copy`: files relative to `repo` to copy into the worktree. */
168
+ files?: string[];
169
+ mode?: "rebase" | "merge";
170
+ /** `git.worktree.add`: fetch the branch from this remote first. */
171
+ remote?: string;
172
+ }
173
+ /** The machine's answer. A refusal is an answer: `ok: false` with its reason. */
174
+ export interface OpResult<T = unknown> {
175
+ ok: boolean;
176
+ data?: T;
177
+ error?: string;
178
+ /** `not_allowed` · `outside_roots` · `no_worktree` · `conflict` · `dirty` · `failed` */
179
+ code?: string;
180
+ }
181
+ export interface TerminalParams {
182
+ /** Whose machine. Resolve it on your server, never from the browser. */
183
+ user: string;
184
+ /** The repository checkout on that machine. Must be inside a granted root. */
185
+ repo: string;
186
+ /** A workspace's branch: the shell opens in its worktree under `repo`. */
187
+ branch?: string;
188
+ cols?: number;
189
+ rows?: number;
190
+ }
191
+ /**
192
+ * A booked web terminal. Hand `url` to your page; it opens a WebSocket there
193
+ * within a minute, once. Frames: send `{t:"i",d}` keystrokes and
194
+ * `{t:"r",cols,rows}` sizes; receive `{t:"o",d}` output and `{t:"x",code,error?}`.
195
+ */
196
+ export interface TerminalBooking {
197
+ termId: string;
198
+ url: string;
199
+ expiresAt: string;
200
+ }
108
201
  export interface DetachedRunParams extends Omit<RunParams, "approve" | "signal" | "resumable"> {
202
+ /**
203
+ * Where Soba sends the run's tool calls (and, unless `events: "tools"`, its
204
+ * every event). `events: "tools"` is for an app whose page watches the run
205
+ * itself with `stream`: one request per tool call and one for the ending,
206
+ * instead of one per event.
207
+ */
109
208
  webhook?: {
110
209
  url: string;
111
210
  secret: string;
211
+ events?: "all" | "tools";
112
212
  };
213
+ /**
214
+ * `"here"`: answer the run's tool calls from this process, with `tools`,
215
+ * instead of a webhook. For localhost and long-lived servers, where nothing
216
+ * public can be called back; a serverless function would be stopped mid-run.
217
+ * The work continues in the background; `answered` on the result settles
218
+ * when the run ends.
219
+ */
220
+ answerTools?: "here";
113
221
  /** `terminal`: open it as the machine owner's own interactive session, in a
114
222
  * window they can watch and type into. The machine must allow it. */
115
223
  surface?: "headless" | "terminal";
116
224
  /** What the terminal window is called. */
117
225
  title?: string;
118
- /** Work in a git worktree on this branch, inside `policy.cwd`. */
226
+ /** Work in a git worktree on this branch, inside `policy.cwd`. A new branch
227
+ * starts from `base`, which is also what the run's git report compares to. */
119
228
  worktree?: {
120
229
  branch: string;
230
+ base?: string;
121
231
  };
232
+ /** Continue the conversation a previous run reported as its `session`. */
233
+ resume?: string;
122
234
  }
123
235
  /**
124
236
  * The Soba client.
@@ -155,6 +267,39 @@ export declare class Soba {
155
267
  * client built for it: `new Soba({ maxRetries: 0, connectTimeoutMs: 1500 })`.
156
268
  */
157
269
  compute(id: string): Promise<MachineListItem[]>;
270
+ /**
271
+ * HOW MANY OF A PRICED FEATURE THIS USER HAS IN USE NOW — seats.
272
+ *
273
+ * SETS THE TOTAL, never adds: call it with the true count wherever it
274
+ * changes (a member added, a member removed, a bulk import), and a missed
275
+ * or doubled call is corrected by the next. A count that already matches
276
+ * writes nothing to Stripe, so calling it on a schedule as a resync is
277
+ * free. Stripe bills what is past the plan's included units on the next
278
+ * invoice; Soba never counts seats itself.
279
+ */
280
+ setQuantity(id: string, feature: string, quantity: number): Promise<QuantityResult>;
281
+ /**
282
+ * MAY THIS PERSON DO THIS — ask before a gated action: create a workspace,
283
+ * run an export. Answers from their plan, whether it has ended, and how
284
+ * many are in use, with a `reason` when the answer is no.
285
+ *
286
+ * Pass `use` to count it in the same call: a yes then records that many,
287
+ * atomically against the limit, so two requests for the last one cannot
288
+ * both get it. Without `use` it only asks.
289
+ *
290
+ * ```ts
291
+ * const { allowed, reason } = await soba.users.check(userId, "workspaces", { use: 1 })
292
+ * if (!allowed) return showUpgrade(reason)
293
+ * ```
294
+ */
295
+ check(id: string, feature: string, options?: {
296
+ use?: number;
297
+ }): Promise<FeatureCheckResult>;
298
+ /**
299
+ * THEY JUST DID IT — record use without asking. `amount` defaults to 1 and
300
+ * may be negative: a workspace deleted is `-1`. For seats, use `setQuantity`.
301
+ */
302
+ track(id: string, feature: string, amount?: number): Promise<TrackResult>;
158
303
  };
159
304
  readonly usage: {
160
305
  list(query?: UsageQuery): Promise<UsagePage>;
@@ -183,14 +328,27 @@ export declare class Soba {
183
328
  };
184
329
  /** Detached and resumable runs. See `DetachedRunParams` and `RunParams.resumable`. */
185
330
  readonly runs: {
186
- start(params: DetachedRunParams): Promise<{
187
- id: string;
188
- }>;
331
+ start(params: DetachedRunParams): Promise<StartedRun>;
189
332
  /** Read a resumable run from where you left off, then live. Tool calls it is waiting on run here. */
190
333
  attach(id: string, params: AttachParams): Promise<Run>;
191
334
  /** Stop a run. For a terminal run, the window closes. */
192
335
  cancel(id: string): Promise<void>;
193
336
  };
337
+ /**
338
+ * Agent workspaces: ask one of a user's machines about, or act on, a git
339
+ * worktree it made for a run — a diff, a push, an update from the base. The
340
+ * machine only answers with `agentOps` on in its owner's policy.
341
+ */
342
+ readonly ops: {
343
+ run<T = unknown>(params: OpParams): Promise<OpResult<T>>;
344
+ };
345
+ /**
346
+ * Web terminals: a shell on one of a user's machines, typed into from a
347
+ * browser. Only on a machine whose owner set `webTerminal` in its policy.
348
+ */
349
+ readonly terminals: {
350
+ open(params: TerminalParams): Promise<TerminalBooking>;
351
+ };
194
352
  constructor(options?: SobaOptions);
195
353
  get baseURL(): string;
196
354
  /**
package/dist/errors.d.ts CHANGED
@@ -13,6 +13,13 @@ export type SobaErrorCode =
13
13
  | "not_found"
14
14
  /** 409 nothing this run is allowed to use is connected and awake */
15
15
  | "no_compute"
16
+ /** 404 on `runs.attach`, a tool result or a cancel: Soba no longer has this
17
+ * run, because it ended long enough ago or Soba restarted under it */
18
+ | "unknown_run"
19
+ /** 409 on `runs.attach`: the run was not started resumable */
20
+ | "not_resumable"
21
+ /** 409 on a tool result or approval: the machine serving the run has gone */
22
+ | "machine_gone"
16
23
  /** 429 back off; `retryAfterMs` says how long */
17
24
  | "rate_limited"
18
25
  /** 5xx ours */
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export { type AttachParams, DEFAULT_BASE_URL, type DetachedRunParams, type RunParams, Soba, type SobaOptions, } from "./client.js";
1
+ export { type AttachParams, DEFAULT_BASE_URL, DEFAULT_WEB_BASE_URL, type DetachedRunParams, type OpKind, type OpParams, type OpResult, type RunParams, Soba, type SobaOptions, type StartedRun, type TerminalBooking, type TerminalParams, } from "./client.js";
2
2
  export { SobaError, type SobaErrorCode } from "./errors.js";
3
- export { type AgentEvent, type AgentEventType, type CostClass, isCostClass, isMetered, isTerminal, METERED_CLASSES, type Message, type PermissionMode, type RouteRequest, type RunPolicy, type RunUsage, TOOL_SLUGS, type ToolSchema, type ToolSlug, } from "./protocol.js";
4
- export type { Allowance, ConnectContextResponse, ConnectedMachine, CurrentPlanItem, MachineListItem, MachineListState, MachineRuntime, PlanListItem, PlanListResponse, PlanSummary, RuntimeListItem, SobaUser, UsagePage, UsageQuery, UsageRow, } from "./resources.js";
3
+ export { type AgentEvent, type AgentEventType, type CostClass, type GitSnapshot, isCostClass, isMetered, isTerminal, METERED_CLASSES, type Message, type PermissionMode, type RouteRequest, type RunPolicy, type RunUsage, TOOL_SLUGS, type ToolSchema, type ToolSlug, } from "./protocol.js";
4
+ export type { Allowance, ConnectContextResponse, ConnectedMachine, CurrentPlanItem, FeatureCheckResult, FeatureDenial, MachineListItem, MachineListState, MachineRuntime, PlanFeatureWire, PlanListItem, PlanListResponse, PlanSummary, QuantityResult, RuntimeListItem, SobaUser, TrackResult, UsagePage, UsageQuery, UsageRow, } from "./resources.js";
5
5
  export { type ApprovalDecision, type ApprovalRequest, type Approver, Run, } from "./run.js";
6
6
  export { type ToolContext, type ToolDefinition, tool } from "./tools.js";
7
7
  export { VERSION } from "./version.js";
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- var i=class extends Error{code;type;status;retryable;retryAfterMs;runId;constructor(e){super(e.message,e.cause===void 0?void 0:{cause:e.cause}),this.name="SobaError",this.code=e.code,this.type=e.type??e.code,this.status=e.status,this.retryable=e.retryable??j.has(e.code),this.retryAfterMs=e.retryAfterMs,this.runId=e.runId}},j=new Set(["rate_limited","server_error","connection","timeout","stream_truncated"]),B={400:"invalid_request",401:"unauthorized",402:"payment_required",403:"forbidden",404:"not_found",409:"no_compute",422:"user_required",429:"rate_limited"};function G(t){let e=B[t];return e||(t>=500?"server_error":t>=400?"invalid_request":"unknown")}async function M(t){let e=t.status,r="";try{r=await t.text()}catch{}let n=K(r);return new i({code:n?.code??G(e),type:n?.type,message:n?.message??J(e,t.statusText,r),status:e,retryAfterMs:z(t.headers.get("retry-after")),runId:t.headers.get("soba-run-id")??void 0})}function K(t){if(!t)return null;let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object")return null;let r=e.error;if(!r||typeof r!="object")return null;let n=r;return{message:typeof n.message=="string"&&n.message?n.message:void 0,type:typeof n.type=="string"&&n.type?n.type:void 0,code:H.has(n.code)?n.code:void 0}}var H=new Set(["invalid_request","user_required","unauthorized","payment_required","forbidden","not_found","no_compute","rate_limited","server_error","connection","timeout","stream_truncated","run_failed","canceled","unknown"]);function J(t,e,r){let n=r.trim().slice(0,200),o=n?`: ${n}`:"";return`Soba returned ${t}${e?` ${e}`:""}${o}`}function z(t){if(!t)return;let e=Number(t);if(Number.isFinite(e)&&e>=0)return Math.round(e*1e3);let r=Date.parse(t);if(!Number.isNaN(r))return Math.max(0,r-Date.now())}function b(t){if(t instanceof i)return t;let e=t;return e?.name==="AbortError"||e?.name==="TimeoutError"?new i({code:"canceled",message:e.message||"The request was aborted.",retryable:!1,cause:t}):new i({code:"connection",message:e?.message?`Could not reach Soba: ${e.message}`:"Could not reach Soba.",cause:t})}var E="0.0.9";var Y=new Set(["ECONNREFUSED","ENOTFOUND","EAI_AGAIN"]);function V(t){if(t.code==="rate_limited")return!0;if(t.code!=="connection")return!1;let e=t.cause;for(let r=0;e&&r<4;r++){let n=e.code;if(typeof n=="string"&&Y.has(n))return!0;e=e.cause}return!1}var Q=8e3,X=3e4;async function h(t,e){let r=te(t.baseURL,e.path,e.query),n=e.retry===!1?1:Math.max(0,t.maxRetries)+1,o,s=a=>a.retryable&&(e.retry!=="unsent"||V(a));for(let a=0;a<n;a++){a>0&&await re(ne(a,o),e.signal);let u=new AbortController,g=x(e.signal,u),y=setTimeout(()=>{u.abort(new i({code:"timeout",message:`Soba did not answer within ${t.connectTimeoutMs}ms.`}))},t.connectTimeoutMs),c;try{c=await t.fetch(r,{method:e.method,headers:Z(t,e),body:e.body===void 0?void 0:JSON.stringify(e.body),signal:u.signal})}catch(p){if(g(),o=b(u.signal.aborted?u.signal.reason??p:p),!s(o)||a===n-1)throw o;continue}finally{clearTimeout(y)}if(c.ok)return c;if(g(),o=await M(c),!s(o)||a===n-1)throw o}throw o??new i({code:"unknown",message:"The request failed with no error."})}async function k(t,e){let r=await h(t,{accept:"application/json",...e}),n=await r.text();if(n)try{return JSON.parse(n)}catch(o){throw new i({code:"server_error",message:`Soba returned a body that is not JSON: ${n.slice(0,200)}`,status:r.status,cause:o})}}function Z(t,e){let r={authorization:`Bearer ${t.apiKey}`,accept:e.accept??"application/json","user-agent":ee(),...t.headers};return e.body!==void 0&&(r["content-type"]="application/json"),r}function ee(){let t=typeof process>"u"?"":` node/${process.versions?.node??"?"}`;return`soba-sdk/${E}${t}`}function te(t,e,r){let n=t.replace(/\/+$/,""),o=new URL(`${n}${e.startsWith("/")?e:`/${e}`}`);for(let[s,a]of Object.entries(r??{}))a!==void 0&&o.searchParams.set(s,String(a));return o.toString()}function ne(t,e){if(e?.retryAfterMs!==void 0)return Math.min(e.retryAfterMs,X);let r=Math.min(Q,500*2**(t-1));return Math.round(r*(.5+Math.random()/2))}function re(t,e){return new Promise((r,n)=>{if(e?.aborted)return n(b(e.reason));let o=setTimeout(()=>{e?.removeEventListener("abort",s),r()},t);function s(){clearTimeout(o),n(b(e?.reason))}e?.addEventListener("abort",s,{once:!0})})}function x(t,e){if(!t)return()=>{};if(t.aborted)return e.abort(t.reason),()=>{};let r=()=>e.abort(t.reason);return t.addEventListener("abort",r,{once:!0}),()=>t.removeEventListener("abort",r)}var U=["open-weights","frontier"];function oe(t){return U.includes(t)}function se(t){return t==="user-hardware"||t==="user-subscription"||t==="user-api-key"||t==="open-weights"||t==="frontier"}function A(t){return t.type==="done"||t.type==="error"}var ie=["search","fetch","read","write","edit","bash","files","agent","todo","think","sources","tool"];var D=new Set(["delta","thinking","status","done","error"]);function _(t){let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e))return null;let r=e,n=l(r.runId);if(typeof r.type=="string"&&D.has(r.type)){let o=O(r);return o?{kind:"event",runId:n,event:o}:null}switch(r.type){case"event":{if(!r.event||typeof r.event!="object")return null;let o=O(r.event);return o?{kind:"event",runId:n,event:o}:null}case"usage":{let o=$(r.usage);return o?{kind:"usage",runId:n,usage:o}:null}case"tool_call":{let o=l(r.callId),s=l(r.name);return!o||!s?null:{kind:"tool_call",runId:n,callId:o,name:s,input:r.input,...r.answered===!0?{answered:!0}:{}}}case"approval_request":{let o=l(r.approvalId),s=l(r.tool);return!o||!s?null:{kind:"approval",runId:n,approvalId:o,tool:s,input:r.input,cwd:l(r.cwd),...r.answered===!0?{answered:!0}:{}}}default:return null}}function O(t){let e=typeof t.type=="string"&&D.has(t.type)?t.type:null;if(!e)return null;let r={type:e},n=l(t.text);n!==void 0&&(r.text=n);let o=l(t.message);o!==void 0&&(r.message=o);let s=l(t.tool);s!==void 0&&(r.tool=s);let a=l(t.tier);a!==void 0&&(r.tier=a);let u=$(t.usage);return u&&(r.usage=u),r}function $(t){if(!t||typeof t!="object")return null;let e=t,r={model:l(e.model)??"unknown",inputTokens:v(e.inputTokens),outputTokens:v(e.outputTokens),cacheReadTokens:v(e.cacheReadTokens),cacheWriteTokens:v(e.cacheWriteTokens),webSearches:v(e.webSearches)},n=l(e.costClass);n!==void 0&&(r.costClass=n);let o=l(e.provider);return o!==void 0&&(r.provider=o),typeof e.costMicros=="number"&&Number.isFinite(e.costMicros)&&(r.costMicros=e.costMicros),r}function l(t){return typeof t=="string"&&t.length>0?t:void 0}function v(t){return typeof t=="number"&&Number.isFinite(t)?t:0}async function*F(t){let e=t.body;if(!e)throw new i({code:"connection",message:"The run response arrived with no body to stream.",status:t.status});let r=t.headers.get("content-type")??"",n=r.includes("ndjson")||r.includes("jsonlines"),o=e.getReader(),s=new TextDecoder,a="",u=[],g,y=c=>g===void 0?c:{...c,seq:g};try{for(;;){let p=await o.read();if(p.done)break;for(a+=s.decode(p.value,{stream:!0});;){let m=a.indexOf(`
2
- `);if(m===-1)break;let f=L(a.slice(0,m));if(a=a.slice(m+1),n){let d=f.trim()?_(f):null;d&&(yield d);continue}if(f===""){let d=u.join(`
3
- `);if(u=[],d==="[DONE]")return;let q=d?_(d):null;q&&(yield y(q)),g=void 0;continue}if(f.startsWith("id:")){let d=Number(f.slice(3).trim());g=Number.isInteger(d)&&d>0?d:void 0;continue}N(f,u)}}!n&&a&&N(L(a),u);let c=n?a.trim():u.join(`
4
- `);if(c&&c!=="[DONE]"){let p=_(c);p&&(yield n?p:y(p))}}finally{o.cancel().catch(()=>{})}}function N(t,e){if(t.startsWith(":"))return;let r=t.indexOf(":");if(r===-1||t.slice(0,r)!=="data")return;let n=t.slice(r+1);e.push(n.startsWith(" ")?n.slice(1):n)}function L(t){return t.endsWith("\r")?t.slice(0,-1):t}function ae(t){return t}function w(t){let e=new Map;for(let r of t??[]){if(!r?.name)throw new i({code:"invalid_request",message:"Every tool needs a name."});if(typeof r.execute!="function")throw new i({code:"invalid_request",message:`Tool "${r.name}" has no execute function. A tool Soba cannot call is not a tool.`});if(e.has(r.name))throw new i({code:"invalid_request",message:`Two tools are named "${r.name}". Tool names must be unique within a run.`});e.set(r.name,r)}return e}function C(t){return[...t.values()].map(e=>({name:e.name,description:e.description??"",inputSchema:e.inputSchema??{type:"object"}}))}function R(t){if(t===void 0)return null;if(typeof t=="string")return t;try{return JSON.parse(JSON.stringify(t))}catch{return String(t)}}function T(t){return t instanceof Error?t.message||t.name:typeof t=="string"?t:String(t)}var S=class{#e;#n;#r;#u=!1;#o;#t;#s=new AbortController;#i=new AbortController;#a=new Set;#c;constructor(e){this.#t=e,this.#e=e.response.headers.get("soba-run-id")??"",this.#o=e.after??0,this.#c=x(e.signal,this.#s)}get cursor(){return this.#o}get resumable(){return this.#t.response.headers.get("soba-resumable")==="1"}get runId(){return this.#e}get tier(){return this.#n}get usage(){return this.#r}[Symbol.asyncIterator](){return this.#l()}async text(){let e="";for await(let r of this)if(r.type==="delta"&&r.text&&(e+=r.text),r.type==="error")throw new i({code:"run_failed",message:r.message??"The run ended with an error.",runId:this.#e,retryable:!1});return e}async cancel(){if(this.#s.abort(new i({code:"canceled",message:"The run was cancelled."})),this.#i.abort(new i({code:"canceled",message:"The run was cancelled."})),!!this.#e)try{await h(this.#t.config,{method:"POST",path:`/runs/${encodeURIComponent(this.#e)}/cancel`,retry:!1})}catch(e){this.#t.onWarning("Could not tell Soba the run was cancelled.",e)}}async*#l(){if(this.#u)throw new i({code:"invalid_request",message:"This run has already been consumed. A run's events can only be read once.",runId:this.#e});this.#u=!0;let e=!1;try{for await(let r of F(this.#t.response)){if(r.runId&&!this.#e&&(this.#e=r.runId),r.seq!==void 0&&(this.#o=r.seq),r.kind==="event"){let n=r.event;if(n.tier&&(this.#n=n.tier),n.usage&&(this.#r=n.usage),e=A(n),yield n,e)return;continue}if(r.kind==="usage"){this.#r=r.usage;continue}this.#p(this.#f(r))}throw new i({code:"stream_truncated",message:"The run stream ended without a `done` or `error` event.",runId:this.#e})}finally{e||this.#i.abort(new i({code:"canceled",message:"The run ended before this tool finished."})),await Promise.allSettled([...this.#a]),this.#s.abort(),this.#c()}}#p(e){this.#a.add(e),e.finally(()=>this.#a.delete(e)).catch(()=>{})}async#f(e){if(!((e.kind==="tool_call"||e.kind==="approval")&&e.answered)){if(e.kind==="tool_call")return this.#g(e);if(e.kind==="approval")return this.#m(e)}}async#g(e){let r=this.#t.tools.get(e.name),n,o=!1;if(!r)n=`No tool named "${e.name}" is registered by this app.`,o=!0;else{let s={runId:this.#e,user:this.#t.user,callId:e.callId,signal:this.#i.signal};try{n=await r.execute(e.input,s)}catch(a){n=T(a),o=!0}}await this.#d(`/runs/${encodeURIComponent(this.#e)}/tool_result`,{callId:e.callId,result:R(n),isError:o})}async#m(e){let r="deny",n;if(!this.#t.approve)n="This app did not supply an approver, so escalations are denied.";else try{let o={runId:this.#e,approvalId:e.approvalId,tool:e.tool,input:e.input};e.cwd&&(o.cwd=e.cwd),r=await this.#t.approve(o)==="allow"?"allow":"deny"}catch(o){n=T(o)}await this.#d(`/runs/${encodeURIComponent(this.#e)}/approval`,{approvalId:e.approvalId,decision:r,...n?{reason:n}:{}})}async#d(e,r){if(!this.#e){this.#t.onWarning("Soba sent a frame before the run had an id, so it could not be answered.",r);return}try{await(await h(this.#t.config,{method:"POST",path:e,body:r})).text()}catch(n){this.#t.onWarning(`Could not deliver an answer to ${e}.`,b(n))}}};var W="https://api.soba.so/v1",ue=3e4,ce=2,P=class{#e;#n;users;usage;connect;plans;runs;constructor(e={}){let r=e.apiKey??I("SOBA_KEY")??I("SOBA_API_KEY");if(!r)throw new i({code:"unauthorized",message:"No Soba API key. Pass `apiKey`, or set SOBA_KEY. Keys are shaped sk_soba_<id>.<secret> and come from your dashboard."});if(de()&&!e.dangerouslyAllowBrowser)throw new i({code:"invalid_request",message:"A Soba app key is a server credential and this looks like a browser. Call Soba from your server, or pass dangerouslyAllowBrowser if you are certain."});if(this.#n=e.onWarning??(()=>{}),this.#e={apiKey:r,baseURL:e.baseURL??I("SOBA_BASE_URL")??W,headers:e.headers??{},fetch:e.fetch??globalThis.fetch,connectTimeoutMs:e.connectTimeoutMs??ue,maxRetries:e.maxRetries??ce},typeof this.#e.fetch!="function")throw new i({code:"invalid_request",message:"No global fetch. Run on Node 22 or newer, or pass your own `fetch`."});this.users={get:n=>this.request("GET",`/users/${encodeURIComponent(n)}`),compute:async n=>(await this.request("GET",`/users/${encodeURIComponent(n)}/compute`)).machines},this.usage={list:n=>this.request("GET","/usage",{query:{...n}})},this.connect=()=>this.request("GET","/connect"),this.runs={start:async n=>{if(!n.user)throw new i({code:"user_required",status:422,message:"`user` is required."});let o=w(n.tools),s={user:n.user,model:n.model??"auto",detached:!0};return n.webhook&&(s.webhook=n.webhook),n.prompt&&(s.prompt=n.prompt),n.messages?.length&&(s.messages=n.messages),n.system&&(s.system=n.system),n.runtime&&(s.runtime=n.runtime),o.size&&(s.tools=C(o)),n.route&&(s.route=n.route),n.policy&&(s.policy=n.policy),n.metadata&&(s.metadata=n.metadata),n.surface&&(s.surface=n.surface),n.title&&(s.title=n.title),n.worktree&&(s.worktree=n.worktree),{id:(await k(this.#e,{method:"POST",path:"/runs",body:s,retry:"unsent"})).id}},attach:async(n,o)=>{if(!o.user)throw new i({code:"user_required",status:422,message:"`user` is required."});let s=await h(this.#e,{method:"GET",path:`/runs/${encodeURIComponent(n)}/stream`,...o.after?{query:{after:o.after}}:{},accept:"text/event-stream",signal:o.signal});return new S({config:this.#e,response:s,user:o.user,tools:w(o.tools),approve:o.approve,onWarning:this.#n,signal:o.signal,after:o.after??0})},cancel:async n=>{await this.request("POST",`/runs/${encodeURIComponent(n)}/cancel`,{body:{}})}},this.plans={list:n=>this.request("GET","/plans",{...n?.user?{query:{user:n.user}}:{}})}}get baseURL(){return this.#e.baseURL}async run(e){if(!e.user)throw new i({code:"user_required",status:422,message:"`user` is required. A run with no user cannot be attributed or metered, so it cannot be defaulted."});if(!e.prompt&&!e.messages?.length)throw new i({code:"invalid_request",status:400,message:"A run needs a `prompt` or a non-empty `messages`."});let r=w(e.tools),n={user:e.user,model:e.model??"auto"};e.prompt&&(n.prompt=e.prompt),e.messages?.length&&(n.messages=e.messages),e.system&&(n.system=e.system),e.runtime&&(n.runtime=e.runtime),r.size&&(n.tools=C(r)),e.route&&(n.route=e.route),e.policy&&(n.policy=e.policy),e.metadata&&(n.metadata=e.metadata),e.resumable&&(n.resumable=!0);let o=await h(this.#e,{method:"POST",path:"/runs",body:n,accept:"text/event-stream",signal:e.signal,retry:"unsent"});return new S({config:this.#e,response:o,user:e.user,tools:r,approve:e.approve,onWarning:this.#n,signal:e.signal})}async request(e,r,n={}){return k(this.#e,{method:e,path:r,...n})}};function I(t){if(typeof process>"u")return;let e=process.env?.[t];return e||void 0}function de(){let t=globalThis;return typeof t.window=="object"&&typeof t.window?.document=="object"}var le=300;function pe(t){return[...new Uint8Array(t)].map(e=>e.toString(16).padStart(2,"0")).join("")}function fe(t,e){if(t.length!==e.length)return!1;let r=0;for(let n=0;n<t.length;n++)r|=t.charCodeAt(n)^e.charCodeAt(n);return r===0}async function ge(t,e,r,n={}){let o=Object.fromEntries((e??"").split(",").map(f=>{let d=f.indexOf("=");return[f.slice(0,d).trim(),f.slice(d+1).trim()]})),s=Number(o.t),a=o.v1??"",u=f=>new i({code:"unauthorized",status:401,message:f});if(!Number.isFinite(s)||!a)throw u("Missing or malformed Soba-Signature.");let g=Math.floor((n.now??Date.now())/1e3);if(Math.abs(g-s)>(n.toleranceSeconds??le))throw u("This delivery is too old to accept.");let y=await globalThis.crypto.subtle.importKey("raw",new TextEncoder().encode(r),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),c=pe(await globalThis.crypto.subtle.sign("HMAC",y,new TextEncoder().encode(`${s}.${t}`)));if(!fe(c,a))throw u("The Soba-Signature does not match.");let p;try{p=JSON.parse(t)}catch{throw new i({code:"invalid_request",message:"The delivery is not JSON."})}let m=p;if(m.type!=="run.event"&&m.type!=="run.tool_call"||typeof m.run_id!="string")throw new i({code:"invalid_request",message:"Not a Soba run delivery."});return m}async function me(t,e){if(t.type!=="run.tool_call")return null;let{call_id:r,name:n,input:o}=t.data,s=w(e.tools).get(n);if(!s)return{result:`Unknown tool: ${n}`,is_error:!0};try{let a=await s.execute(o??{},{runId:t.run_id,user:e.user??"",callId:r,signal:AbortSignal.timeout(25e3)});return{result:R(a)}}catch(a){return{result:T(a),is_error:!0}}}export{W as DEFAULT_BASE_URL,U as METERED_CLASSES,S as Run,P as Soba,i as SobaError,ie as TOOL_SLUGS,E as VERSION,me as handleRunWebhook,se as isCostClass,oe as isMetered,A as isTerminal,ae as tool,ge as verifyWebhook};
1
+ var a=class extends Error{code;type;status;retryable;retryAfterMs;runId;constructor(e){super(e.message,e.cause===void 0?void 0:{cause:e.cause}),this.name="SobaError",this.code=e.code,this.type=e.type??e.code,this.status=e.status,this.retryable=e.retryable??B.has(e.code),this.retryAfterMs=e.retryAfterMs,this.runId=e.runId}},B=new Set(["rate_limited","server_error","connection","timeout","stream_truncated"]),G={400:"invalid_request",401:"unauthorized",402:"payment_required",403:"forbidden",404:"not_found",409:"no_compute",422:"user_required",429:"rate_limited"};function K(n){let e=G[n];return e||(n>=500?"server_error":n>=400?"invalid_request":"unknown")}async function q(n){let e=n.status,r="";try{r=await n.text()}catch{}let t=H(r);return new a({code:t?.code??K(e),type:t?.type,message:t?.message??z(e,n.statusText,r),status:e,retryAfterMs:Q(n.headers.get("retry-after")),runId:n.headers.get("soba-run-id")??void 0})}function H(n){if(!n)return null;let e;try{e=JSON.parse(n)}catch{return null}if(!e||typeof e!="object")return null;let r=e.error;if(!r||typeof r!="object")return null;let t=r;return{message:typeof t.message=="string"&&t.message?t.message:void 0,type:typeof t.type=="string"&&t.type?t.type:void 0,code:J.has(t.code)?t.code:void 0}}var J=new Set(["invalid_request","user_required","unauthorized","payment_required","forbidden","not_found","no_compute","unknown_run","not_resumable","machine_gone","rate_limited","server_error","connection","timeout","stream_truncated","run_failed","canceled","unknown"]);function z(n,e,r){let t=r.trim().slice(0,200),o=t?`: ${t}`:"";return`Soba returned ${n}${e?` ${e}`:""}${o}`}function Q(n){if(!n)return;let e=Number(n);if(Number.isFinite(e)&&e>=0)return Math.round(e*1e3);let r=Date.parse(n);if(!Number.isNaN(r))return Math.max(0,r-Date.now())}function w(n){if(n instanceof a)return n;let e=n;return e?.name==="AbortError"||e?.name==="TimeoutError"?new a({code:"canceled",message:e.message||"The request was aborted.",retryable:!1,cause:n}):new a({code:"connection",message:e?.message?`Could not reach Soba: ${e.message}`:"Could not reach Soba.",cause:n})}var E="0.0.11";var Y=new Set(["ECONNREFUSED","ENOTFOUND","EAI_AGAIN"]);function V(n){if(n.code==="rate_limited")return!0;if(n.code!=="connection")return!1;let e=n.cause;for(let r=0;e&&r<4;r++){let t=e.code;if(typeof t=="string"&&Y.has(t))return!0;e=e.cause}return!1}var X=8e3,Z=3e4;async function b(n,e){let r=ne(n.baseURL,e.path,e.query),t=e.retry===!1?1:Math.max(0,n.maxRetries)+1,o,s=i=>i.retryable&&(e.retry!=="unsent"||V(i));for(let i=0;i<t;i++){i>0&&await oe(re(i,o),e.signal);let u=new AbortController,d=A(e.signal,u),h=setTimeout(()=>{u.abort(new a({code:"timeout",message:`Soba did not answer within ${n.connectTimeoutMs}ms.`}))},n.connectTimeoutMs),l;try{l=await n.fetch(r,{method:e.method,headers:ee(n,e),body:e.body===void 0?void 0:JSON.stringify(e.body),signal:u.signal})}catch(p){if(d(),o=w(u.signal.aborted?u.signal.reason??p:p),!s(o)||i===t-1)throw o;continue}finally{clearTimeout(h)}if(l.ok)return l;if(d(),o=await q(l),!s(o)||i===t-1)throw o}throw o??new a({code:"unknown",message:"The request failed with no error."})}async function R(n,e){let r=await b(n,{accept:"application/json",...e}),t=await r.text();if(t)try{return JSON.parse(t)}catch(o){throw new a({code:"server_error",message:`Soba returned a body that is not JSON: ${t.slice(0,200)}`,status:r.status,cause:o})}}function ee(n,e){let r={authorization:`Bearer ${n.apiKey}`,accept:e.accept??"application/json","user-agent":te(),...n.headers};return e.body!==void 0&&(r["content-type"]="application/json"),r}function te(){let n=typeof process>"u"?"":` node/${process.versions?.node??"?"}`;return`soba-sdk/${E}${n}`}function ne(n,e,r){let t=n.replace(/\/+$/,""),o=new URL(`${t}${e.startsWith("/")?e:`/${e}`}`);for(let[s,i]of Object.entries(r??{}))i!==void 0&&o.searchParams.set(s,String(i));return o.toString()}function re(n,e){if(e?.retryAfterMs!==void 0)return Math.min(e.retryAfterMs,Z);let r=Math.min(X,500*2**(n-1));return Math.round(r*(.5+Math.random()/2))}function oe(n,e){return new Promise((r,t)=>{if(e?.aborted)return t(w(e.reason));let o=setTimeout(()=>{e?.removeEventListener("abort",s),r()},n);function s(){clearTimeout(o),t(w(e?.reason))}e?.addEventListener("abort",s,{once:!0})})}function A(n,e){if(!n)return()=>{};if(n.aborted)return e.abort(n.reason),()=>{};let r=()=>e.abort(n.reason);return n.addEventListener("abort",r,{once:!0}),()=>n.removeEventListener("abort",r)}var M=["open-weights","frontier"];function se(n){return M.includes(n)}function ie(n){return n==="user-hardware"||n==="user-subscription"||n==="user-api-key"||n==="open-weights"||n==="frontier"}function _(n){return n.type==="done"||n.type==="error"}var ae=["search","fetch","read","write","edit","bash","files","agent","todo","think","sources","tool"];var N=new Set(["delta","thinking","status","done","error"]);function C(n){let e;try{e=JSON.parse(n)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e))return null;let r=e,t=c(r.runId);if(typeof r.type=="string"&&N.has(r.type)){let o=O(r);return o?{kind:"event",runId:t,event:o}:null}switch(r.type){case"event":{if(!r.event||typeof r.event!="object")return null;let o=O(r.event);return o?{kind:"event",runId:t,event:o}:null}case"usage":{let o=$(r.usage);return o?{kind:"usage",runId:t,usage:o}:null}case"tool_call":{let o=c(r.callId),s=c(r.name);return!o||!s?null:{kind:"tool_call",runId:t,callId:o,name:s,input:r.input,...r.answered===!0?{answered:!0}:{}}}case"approval_request":{let o=c(r.approvalId),s=c(r.tool);return!o||!s?null:{kind:"approval",runId:t,approvalId:o,tool:s,input:r.input,cwd:c(r.cwd),...r.answered===!0?{answered:!0}:{}}}default:return null}}function O(n){let e=typeof n.type=="string"&&N.has(n.type)?n.type:null;if(!e)return null;let r={type:e},t=c(n.text);t!==void 0&&(r.text=t);let o=c(n.message);o!==void 0&&(r.message=o);let s=c(n.tool);s!==void 0&&(r.tool=s);let i=c(n.tier);i!==void 0&&(r.tier=i);let u=$(n.usage);u&&(r.usage=u);let d=c(n.reason);d!==void 0&&(r.reason=d),typeof n.retryable=="boolean"&&(r.retryable=n.retryable);let h=n.turn&&typeof n.turn=="object"?c(n.turn.text):void 0;h!==void 0&&(r.turn={text:h});let l=c(n.session);l!==void 0&&(r.session=l);let p=c(n.toolInput);p!==void 0&&(r.toolInput=p);let m=ue(n.git);return m&&(r.git=m),(n.state==="interrupted"||n.state==="resumed")&&(r.state=n.state),r}function ue(n){if(!n||typeof n!="object")return null;let e=n,r=c(e.branch);return!r||!Array.isArray(e.files)||!Array.isArray(e.commits)?null:{branch:r,base:c(e.base)??"",head:c(e.head)??"",ahead:y(e.ahead),behind:y(e.behind),files:e.files.filter(t=>!!t&&typeof t=="object"&&typeof t.path=="string").map(t=>({path:t.path,status:t.status==="A"||t.status==="D"||t.status==="R"?t.status:"M",added:y(t.added),removed:y(t.removed)})),commits:e.commits.filter(t=>!!t&&typeof t=="object").map(t=>({sha:c(t.sha)??"",subject:c(t.subject)??""})),dirty:e.dirty===!0}}function $(n){if(!n||typeof n!="object")return null;let e=n,r={model:c(e.model)??"unknown",inputTokens:y(e.inputTokens),outputTokens:y(e.outputTokens),cacheReadTokens:y(e.cacheReadTokens),cacheWriteTokens:y(e.cacheWriteTokens),webSearches:y(e.webSearches)},t=c(e.costClass);t!==void 0&&(r.costClass=t);let o=c(e.provider);return o!==void 0&&(r.provider=o),typeof e.costMicros=="number"&&Number.isFinite(e.costMicros)&&(r.costMicros=e.costMicros),r}function c(n){return typeof n=="string"&&n.length>0?n:void 0}function y(n){return typeof n=="number"&&Number.isFinite(n)?n:0}async function*F(n){let e=n.body;if(!e)throw new a({code:"connection",message:"The run response arrived with no body to stream.",status:n.status});let r=n.headers.get("content-type")??"",t=r.includes("ndjson")||r.includes("jsonlines"),o=e.getReader(),s=new TextDecoder,i="",u=[],d,h=l=>d===void 0?l:{...l,seq:d};try{for(;;){let p=await o.read();if(p.done)break;for(i+=s.decode(p.value,{stream:!0});;){let m=i.indexOf(`
2
+ `);if(m===-1)break;let f=L(i.slice(0,m));if(i=i.slice(m+1),t){let g=f.trim()?C(f):null;g&&(yield g);continue}if(f===""){let g=u.join(`
3
+ `);if(u=[],g==="[DONE]")return;let U=g?C(g):null;U&&(yield h(U)),d=void 0;continue}if(f.startsWith("id:")){let g=Number(f.slice(3).trim());d=Number.isInteger(g)&&g>0?g:void 0;continue}D(f,u)}}!t&&i&&D(L(i),u);let l=t?i.trim():u.join(`
4
+ `);if(l&&l!=="[DONE]"){let p=C(l);p&&(yield t?p:h(p))}}finally{o.cancel().catch(()=>{})}}function D(n,e){if(n.startsWith(":"))return;let r=n.indexOf(":");if(r===-1||n.slice(0,r)!=="data")return;let t=n.slice(r+1);e.push(t.startsWith(" ")?t.slice(1):t)}function L(n){return n.endsWith("\r")?n.slice(0,-1):n}function ce(n){return n}function S(n){let e=new Map;for(let r of n??[]){if(!r?.name)throw new a({code:"invalid_request",message:"Every tool needs a name."});if(typeof r.execute!="function")throw new a({code:"invalid_request",message:`Tool "${r.name}" has no execute function. A tool Soba cannot call is not a tool.`});if(e.has(r.name))throw new a({code:"invalid_request",message:`Two tools are named "${r.name}". Tool names must be unique within a run.`});e.set(r.name,r)}return e}function I(n){return[...n.values()].map(e=>({name:e.name,description:e.description??"",inputSchema:e.inputSchema??{type:"object"}}))}function v(n){if(n===void 0)return null;if(typeof n=="string")return n;try{return JSON.parse(JSON.stringify(n))}catch{return String(n)}}function T(n){return n instanceof Error?n.message||n.name:typeof n=="string"?n:String(n)}var k=class{#e;#n;#r;#u=!1;#o;#t;#s=new AbortController;#i=new AbortController;#a=new Set;#c;constructor(e){this.#t=e,this.#e=e.response.headers.get("soba-run-id")??"",this.#o=e.after??0,this.#c=A(e.signal,this.#s)}get cursor(){return this.#o}get resumable(){return this.#t.response.headers.get("soba-resumable")==="1"}get runId(){return this.#e}get tier(){return this.#n}get usage(){return this.#r}[Symbol.asyncIterator](){return this.#l()}async text(){let e="";for await(let r of this)if(r.type==="delta"&&r.text&&(e+=r.text),r.type==="error")throw new a({code:"run_failed",message:r.message??"The run ended with an error.",runId:this.#e,retryable:!1});return e}async cancel(){if(this.#s.abort(new a({code:"canceled",message:"The run was cancelled."})),this.#i.abort(new a({code:"canceled",message:"The run was cancelled."})),!!this.#e)try{await b(this.#t.config,{method:"POST",path:`/runs/${encodeURIComponent(this.#e)}/cancel`,retry:!1})}catch(e){this.#t.onWarning("Could not tell Soba the run was cancelled.",e)}}async*#l(){if(this.#u)throw new a({code:"invalid_request",message:"This run has already been consumed. A run's events can only be read once.",runId:this.#e});this.#u=!0;let e=!1;try{for await(let r of F(this.#t.response)){if(r.runId&&!this.#e&&(this.#e=r.runId),r.seq!==void 0&&(this.#o=r.seq),r.kind==="event"){let t=r.event;if(t.tier&&(this.#n=t.tier),t.usage&&(this.#r=t.usage),e=_(t),yield t,e)return;continue}if(r.kind==="usage"){this.#r=r.usage;continue}this.#p(this.#g(r))}throw new a({code:"stream_truncated",message:"The run stream ended without a `done` or `error` event.",runId:this.#e})}finally{e||this.#i.abort(new a({code:"canceled",message:"The run ended before this tool finished."})),await Promise.allSettled([...this.#a]),this.#s.abort(),this.#c()}}#p(e){this.#a.add(e),e.finally(()=>this.#a.delete(e)).catch(()=>{})}async#g(e){if(!((e.kind==="tool_call"||e.kind==="approval")&&e.answered)){if(e.kind==="tool_call")return this.#f(e);if(e.kind==="approval")return this.#m(e)}}async#f(e){let r=this.#t.tools.get(e.name),t,o=!1;if(!r)t=`No tool named "${e.name}" is registered by this app.`,o=!0;else{let s={runId:this.#e,user:this.#t.user,callId:e.callId,signal:this.#i.signal};try{t=await r.execute(e.input,s)}catch(i){t=T(i),o=!0}}await this.#d(`/runs/${encodeURIComponent(this.#e)}/tool_result`,{callId:e.callId,result:v(t),isError:o})}async#m(e){let r="deny",t;if(!this.#t.approve)t="This app did not supply an approver, so escalations are denied.";else try{let o={runId:this.#e,approvalId:e.approvalId,tool:e.tool,input:e.input};e.cwd&&(o.cwd=e.cwd),r=await this.#t.approve(o)==="allow"?"allow":"deny"}catch(o){t=T(o)}await this.#d(`/runs/${encodeURIComponent(this.#e)}/approval`,{approvalId:e.approvalId,decision:r,...t?{reason:t}:{}})}async#d(e,r){if(!this.#e){this.#t.onWarning("Soba sent a frame before the run had an id, so it could not be answered.",r);return}try{await(await b(this.#t.config,{method:"POST",path:e,body:r})).text()}catch(t){this.#t.onWarning(`Could not deliver an answer to ${e}.`,w(t))}}};var j="https://api.soba.so/v1",W="https://soba.so/v1",de=3e4,le=2,P=class{#e;#n;#r;users;usage;connect;plans;runs;ops;terminals;constructor(e={}){let r=e.apiKey??x("SOBA_KEY")??x("SOBA_API_KEY");if(!r)throw new a({code:"unauthorized",message:"No Soba API key. Pass `apiKey`, or set SOBA_KEY. Keys are shaped sk_soba_<id>.<secret> and come from your dashboard."});if(pe()&&!e.dangerouslyAllowBrowser)throw new a({code:"invalid_request",message:"A Soba app key is a server credential and this looks like a browser. Call Soba from your server, or pass dangerouslyAllowBrowser if you are certain."});if(this.#r=e.onWarning??(()=>{}),this.#e={apiKey:r,baseURL:e.baseURL??x("SOBA_BASE_URL")??j,headers:e.headers??{},fetch:e.fetch??globalThis.fetch,connectTimeoutMs:e.connectTimeoutMs??de,maxRetries:e.maxRetries??le},this.#n={...this.#e,baseURL:e.webBaseURL??x("SOBA_WEB_URL")??W},typeof this.#e.fetch!="function")throw new a({code:"invalid_request",message:"No global fetch. Run on Node 22 or newer, or pass your own `fetch`."});this.users={get:t=>this.request("GET",`/users/${encodeURIComponent(t)}`),compute:async t=>(await this.request("GET",`/users/${encodeURIComponent(t)}/compute`)).machines,setQuantity:(t,o,s)=>R(this.#n,{method:"POST",path:`/end_users/${encodeURIComponent(t)}/features/${encodeURIComponent(o)}/quantity`,body:{quantity:s}}),check:(t,o,s)=>R(s?.use?{...this.#n,maxRetries:0}:this.#n,{method:"POST",path:`/end_users/${encodeURIComponent(t)}/features/${encodeURIComponent(o)}/check`,body:s?.use?{use:s.use}:{}}),track:(t,o,s=1)=>R({...this.#n,maxRetries:0},{method:"POST",path:`/end_users/${encodeURIComponent(t)}/features/${encodeURIComponent(o)}/track`,body:{amount:s}})},this.usage={list:t=>this.request("GET","/usage",{query:{...t}})},this.connect=()=>this.request("GET","/connect"),this.runs={start:async t=>{if(!t.user)throw new a({code:"user_required",status:422,message:"`user` is required."});let o=S(t.tools),s={user:t.user,model:t.model??"auto",detached:!0};t.webhook&&(s.webhook=t.webhook),t.prompt&&(s.prompt=t.prompt),t.messages?.length&&(s.messages=t.messages),t.system&&(s.system=t.system),t.runtime&&(s.runtime=t.runtime),o.size&&(s.tools=I(o)),t.route&&(s.route=t.route),t.policy&&(s.policy=t.policy),t.metadata&&(s.metadata=t.metadata),t.task&&(s.task=t.task),t.surface&&(s.surface=t.surface),t.maxDurationMs&&(s.maxDurationMs=t.maxDurationMs),t.title&&(s.title=t.title),t.worktree&&(s.worktree=t.worktree),t.resume&&(s.resume=t.resume);let i=await R(this.#e,{method:"POST",path:"/runs",body:s,retry:"unsent"}),u={id:i.id};return i.stream&&(u.stream={url:i.stream.url,token:i.stream.token,expiresAt:i.stream.expires_at}),t.answerTools==="here"&&(u.answered=(async()=>{let d=await this.runs.attach(i.id,{user:t.user,tools:t.tools??[]});for await(let h of d);})().catch(d=>this.#r("Answering this run's tool calls stopped.",d))),u},attach:async(t,o)=>{if(!o.user)throw new a({code:"user_required",status:422,message:"`user` is required."});let s=await b(this.#e,{method:"GET",path:`/runs/${encodeURIComponent(t)}/stream`,...o.after?{query:{after:o.after}}:{},accept:"text/event-stream",signal:o.signal});return new k({config:this.#e,response:s,user:o.user,tools:S(o.tools),approve:o.approve,onWarning:this.#r,signal:o.signal,after:o.after??0})},cancel:async t=>{await this.request("POST",`/runs/${encodeURIComponent(t)}/cancel`,{body:{}})}},this.ops={run:t=>{if(!t.user)throw new a({code:"user_required",status:422,message:"`user` is required \u2014 it is how Soba knows whose machine to ask."});return this.request("POST","/ops",{body:{...t}})}},this.terminals={open:async t=>{if(!t.user)throw new a({code:"user_required",status:422,message:"`user` is required \u2014 it is how Soba knows whose machine to open it on."});let o=await this.request("POST","/term",{body:{...t}}),s=new URL(this.#e.baseURL);s.protocol=s.protocol==="http:"?"ws:":"wss:";let i=new URL(o.path,`${s.protocol}//${s.host}`);return i.searchParams.set("ticket",o.ticket),{termId:o.termId,url:i.toString(),expiresAt:o.expiresAt}}},this.plans={list:t=>this.request("GET","/plans",{...t?.user?{query:{user:t.user}}:{}})}}get baseURL(){return this.#e.baseURL}async run(e){if(!e.user)throw new a({code:"user_required",status:422,message:"`user` is required. A run with no user cannot be attributed or metered, so it cannot be defaulted."});if(!e.prompt&&!e.messages?.length)throw new a({code:"invalid_request",status:400,message:"A run needs a `prompt` or a non-empty `messages`."});let r=S(e.tools),t={user:e.user,model:e.model??"auto"};e.prompt&&(t.prompt=e.prompt),e.messages?.length&&(t.messages=e.messages),e.system&&(t.system=e.system),e.runtime&&(t.runtime=e.runtime),r.size&&(t.tools=I(r)),e.route&&(t.route=e.route),e.policy&&(t.policy=e.policy),e.metadata&&(t.metadata=e.metadata),e.task&&(t.task=e.task),e.resumable&&(t.resumable=!0),e.maxDurationMs&&(t.maxDurationMs=e.maxDurationMs);let o=await b(this.#e,{method:"POST",path:"/runs",body:t,accept:"text/event-stream",signal:e.signal,retry:"unsent"});return new k({config:this.#e,response:o,user:e.user,tools:r,approve:e.approve,onWarning:this.#r,signal:e.signal})}async request(e,r,t={}){return R(this.#e,{method:e,path:r,...t})}};function x(n){if(typeof process>"u")return;let e=process.env?.[n];return e||void 0}function pe(){let n=globalThis;return typeof n.window=="object"&&typeof n.window?.document=="object"}var ge=300;function fe(n){return[...new Uint8Array(n)].map(e=>e.toString(16).padStart(2,"0")).join("")}function me(n,e){if(n.length!==e.length)return!1;let r=0;for(let t=0;t<n.length;t++)r|=n.charCodeAt(t)^e.charCodeAt(t);return r===0}async function he(n,e,r,t={}){let o=Object.fromEntries((e??"").split(",").map(f=>{let g=f.indexOf("=");return[f.slice(0,g).trim(),f.slice(g+1).trim()]})),s=Number(o.t),i=o.v1??"",u=f=>new a({code:"unauthorized",status:401,message:f});if(!Number.isFinite(s)||!i)throw u("Missing or malformed Soba-Signature.");let d=Math.floor((t.now??Date.now())/1e3);if(Math.abs(d-s)>(t.toleranceSeconds??ge))throw u("This delivery is too old to accept.");let h=await globalThis.crypto.subtle.importKey("raw",new TextEncoder().encode(r),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),l=fe(await globalThis.crypto.subtle.sign("HMAC",h,new TextEncoder().encode(`${s}.${n}`)));if(!me(l,i))throw u("The Soba-Signature does not match.");let p;try{p=JSON.parse(n)}catch{throw new a({code:"invalid_request",message:"The delivery is not JSON."})}let m=p;if(m.type!=="run.event"&&m.type!=="run.tool_call"||typeof m.run_id!="string")throw new a({code:"invalid_request",message:"Not a Soba run delivery."});return m}async function ye(n,e){if(n.type!=="run.tool_call")return null;let{call_id:r,name:t,input:o}=n.data,s=S(e.tools).get(t);if(!s)return{result:`Unknown tool: ${t}`,is_error:!0};try{let i=await s.execute(o??{},{runId:n.run_id,user:e.user??"",callId:r,signal:AbortSignal.timeout(25e3)});return{result:v(i)}}catch(i){return{result:T(i),is_error:!0}}}export{j as DEFAULT_BASE_URL,W as DEFAULT_WEB_BASE_URL,M as METERED_CLASSES,k as Run,P as Soba,a as SobaError,ae as TOOL_SLUGS,E as VERSION,ye as handleRunWebhook,ie as isCostClass,se as isMetered,_ as isTerminal,ce as tool,he as verifyWebhook};
@@ -43,7 +43,47 @@ export interface AgentEvent {
43
43
  usage?: RunUsage;
44
44
  /** Which economics is serving this run, announced on the opening `status`,
45
45
  * BEFORE any output. A visible downgrade beats a silent one. */
46
- tier?: CostClass;
46
+ tier?: CostClass; /**
47
+ * On an `error`: a stable code to branch on, beside the prose in `message`.
48
+ * `run_too_long` is a run stopped at its time limit; `subscription_exhausted`
49
+ * a user's own plan that has run out.
50
+ */
51
+ reason?: string;
52
+ /** On an `error`: false when starting the same run again will end the same way. */
53
+ retryable?: boolean;
54
+ /** On a `status` from Soba: the computer serving the run went offline
55
+ * (`interrupted`) or came back (`resumed`). The run goes on either way. */
56
+ state?: "interrupted" | "resumed";
57
+ /** On a `status` from a terminal run: what the agent said at the end of a turn. */
58
+ turn?: {
59
+ text: string;
60
+ };
61
+ /** On `done` / `error`: the runtime's conversation id. Send it back as `resume`
62
+ * to make the next run a follow-up of this one. */
63
+ session?: string;
64
+ /** On a tool `status`: what the tool was pointed at — a path, a command. */
65
+ toolInput?: string;
66
+ /** On a `status` from a run in a git checkout: where its branch stands. */
67
+ git?: GitSnapshot;
68
+ }
69
+ /** A branch against the branch it forked from, counted from the merge base. */
70
+ export interface GitSnapshot {
71
+ branch: string;
72
+ base: string;
73
+ head: string;
74
+ ahead: number;
75
+ behind: number;
76
+ files: {
77
+ path: string;
78
+ status: "A" | "M" | "D" | "R";
79
+ added: number;
80
+ removed: number;
81
+ }[];
82
+ commits: {
83
+ sha: string;
84
+ subject: string;
85
+ }[];
86
+ dirty: boolean;
47
87
  }
48
88
  export declare function isTerminal(e: AgentEvent): boolean;
49
89
  /** Stable tool slugs. Adapters map their runtime's native names onto these. */
@@ -80,6 +120,8 @@ export interface RunPolicy {
80
120
  timeoutMs?: number;
81
121
  permissionMode?: PermissionMode;
82
122
  allowNetwork?: string[];
123
+ /** The owner's own browser, every action approved live. Only if their machine grants it. */
124
+ browser?: boolean;
83
125
  }
84
126
  /**
85
127
  * WHICH ECONOMICS a run may use. You declare intent, not a runtime, which is
@@ -105,7 +105,11 @@ export interface PlanListItem {
105
105
  price: number;
106
106
  interval: string;
107
107
  unit: string;
108
+ /** Runs a month on AI the app pays for. Null is unlimited. */
108
109
  included_units: number | null;
110
+ /** Runs a month on the user's own AI, counted apart from `included_units`.
111
+ * Null is unlimited. Optional so an older server that omits it parses. */
112
+ own_included_units?: number | null;
109
113
  /** What happens past the allowance: `stop` or `meter`. The difference
110
114
  * between a wall and a bill, and not guessable from the price. */
111
115
  overage: string;
@@ -116,9 +120,20 @@ export interface PlanListItem {
116
120
  route_allow: string[] | null;
117
121
  /** Whether it can be taken without a payment rail. */
118
122
  selectable: boolean;
123
+ /** What the app calls a run, plural: "tasks". Optional for older servers;
124
+ * absent reads "runs". */
125
+ task_noun?: string;
126
+ /** About how many runs a credit plan's allowance buys at this app's typical
127
+ * run cost: "About 265 tasks a month". Null when it cannot be said. */
128
+ estimated_tasks?: number | null;
129
+ /** A plan sold by conversation: "Talk to us" links here. */
130
+ contact_url?: string | null;
131
+ /** "Everything in" this plan id; its card lists that, then `features_own`. */
132
+ features_from?: string | null;
133
+ features_own?: PlanFeatureWire[] | null;
119
134
  /**
120
- * What connecting their own compute earns: `none` | `unlimited` | `included`
121
- * | `credit`. Here for the same reason every other field is: these objects
135
+ * What connecting their own compute earns: `none` | `included` | `credit`.
136
+ * Here for the same reason every other field is: these objects
122
137
  * are handed straight to `<SobaProvider initialPlans>` and rendered, so the
123
138
  * shape has to be the component's, not a report's.
124
139
  */
@@ -127,12 +142,44 @@ export interface PlanListItem {
127
142
  connected_included_units: number | null;
128
143
  /** Dollars back per interval on a `credit` reward. Zero otherwise. */
129
144
  connected_credit: number;
145
+ /** What the vendor's app enforces, as the card lists it (INT-91). Optional
146
+ * so an older server that omits it parses. */
147
+ features?: PlanFeatureWire[];
130
148
  }
131
149
  export interface CurrentPlanItem {
132
150
  plan_id: string;
151
+ /** Spent against `included_units`. */
133
152
  units_used: number;
153
+ /** Spent against `own_included_units`. Optional for older servers. */
154
+ own_units_used?: number;
155
+ /**
156
+ * WHAT SOBA ENFORCES: both allowances, what is left, when they reset. The
157
+ * gateway stops a run here; your app never has to. Optional for older
158
+ * servers, null when the plan is no longer in the catalogue.
159
+ */
160
+ limits?: {
161
+ unit: string;
162
+ included_units: number | null;
163
+ units_used: number;
164
+ remaining: number | null;
165
+ own_included_units: number | null;
166
+ own_units_used: number;
167
+ own_remaining: number | null;
168
+ resets_at: string | null;
169
+ } | null;
170
+ /**
171
+ * WHAT YOUR APP ENFORCES (INT-91): feature id to value, as this person
172
+ * bought it. Soba stops nothing on these; read them and decide.
173
+ */
174
+ features?: Record<string, boolean | number | string | null>;
175
+ /** How many of each priced feature are in use, as your app last reported
176
+ * (`{ seats: 12 }`). Separate from `features`, where `seats` is the cap. */
177
+ quantities?: Record<string, number>;
134
178
  period_end: string | null;
135
179
  trial_ends_at: string | null;
180
+ /** A free plan past its time limit: runs are refused, and every feature
181
+ * reads as off, so stop what the plan carried too. Absent on older servers. */
182
+ ended?: boolean;
136
183
  }
137
184
  export interface PlanListResponse {
138
185
  plans: PlanListItem[];
@@ -203,3 +250,56 @@ export interface ConnectContextResponse {
203
250
  /** Where a browser polls this user's machines (the gateway). Hand the whole context to `<SobaProvider initialConnect>` and it is used. */
204
251
  machines_url?: string | null;
205
252
  }
253
+ /** A plan feature: what the vendor's app enforces, never Soba (INT-91). */
254
+ export interface PlanFeatureWire {
255
+ id: string;
256
+ name: string;
257
+ type: "boolean" | "number" | "text";
258
+ /** `true`/`false`, a whole number, or null for unlimited. */
259
+ value: boolean | number | string | null;
260
+ unit?: string;
261
+ /** "month": the count starts over each billing period. Absent: never. */
262
+ resets?: "month";
263
+ /** Seats: a price per unit, in micros, past `included` units. */
264
+ price?: {
265
+ amountMicros: number;
266
+ included: number;
267
+ };
268
+ }
269
+ /** What `users.setQuantity` answers. */
270
+ /**
271
+ * WHY A FEATURE IS NOT AVAILABLE: no plan, a free plan whose time is up, a
272
+ * feature the plan does not carry, or a counted one with nothing left.
273
+ */
274
+ export type FeatureDenial = "no_plan" | "plan_ended" | "not_included" | "limit_reached";
275
+ /** The answer to `users.check`. */
276
+ export interface FeatureCheckResult {
277
+ feature: string;
278
+ allowed: boolean;
279
+ /** Null when allowed. */
280
+ reason: FeatureDenial | null;
281
+ /** The plan's cap, or null when unlimited or a switch. */
282
+ limit: number | null;
283
+ /** In use now, after this call's `use` if it was counted. */
284
+ used: number;
285
+ /** Left under the cap. Null when unlimited or a switch. */
286
+ remaining: number | null;
287
+ unlimited: boolean;
288
+ }
289
+ /** The answer to `users.track`. */
290
+ export interface TrackResult {
291
+ feature: string;
292
+ /** The running total now. */
293
+ used: number;
294
+ }
295
+ export interface QuantityResult {
296
+ feature: string;
297
+ quantity: number;
298
+ /** The count before this report. Null on the first one. */
299
+ previous: number | null;
300
+ /** False when the count already matched, and nothing was written. */
301
+ changed: boolean;
302
+ /** `updated` when it reached the subscription; `no_subscription` when the
303
+ * user's plan was not bought through Stripe, so there is nothing to bill. */
304
+ billing?: "updated" | "no_subscription";
305
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.0.9";
1
+ export declare const VERSION = "0.0.11";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soba-so/sdk",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "One request that comes back finished, executed on compute the end user owns.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://soba.so/docs/runs",
@@ -22,6 +22,10 @@
22
22
  "!dist/**/*.map",
23
23
  "README.md"
24
24
  ],
25
+ "scripts": {
26
+ "build": "node ../../scripts/build-sdk.mjs",
27
+ "prepublishOnly": "node ../../scripts/build-sdk.mjs"
28
+ },
25
29
  "engines": {
26
30
  "node": ">=22"
27
31
  },
@@ -34,12 +38,9 @@
34
38
  "byoc"
35
39
  ],
36
40
  "devDependencies": {
37
- "@soba-so/protocol": "0.1.0"
41
+ "@soba-so/protocol": "workspace:*"
38
42
  },
39
43
  "publishConfig": {
40
44
  "access": "public"
41
- },
42
- "scripts": {
43
- "build": "node ../../scripts/build-sdk.mjs"
44
45
  }
45
- }
46
+ }