@soba-so/sdk 0.0.5

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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright © 2026 Soba. All rights reserved.
2
+
3
+ This software is licensed, not sold, and it is not open source.
4
+
5
+ Subject to your agreement with Soba, you are granted a non-exclusive,
6
+ non-transferable, non-sublicensable, revocable licence to install this package
7
+ and use it in your own applications for the sole purpose of interfacing with
8
+ the Soba service.
9
+
10
+ You may not redistribute it, publish it, sell it, sublicense it, incorporate it
11
+ into another distributed library, or create and distribute derivative works of
12
+ it. No rights are granted in the Soba name or logo.
13
+
14
+ This software is provided "as is", without warranty of any kind, express or
15
+ implied. To the maximum extent permitted by law, Soba shall not be liable for
16
+ any claim, damages or other liability arising from the software or its use.
17
+
18
+ Nothing here limits any right you have that cannot be limited by the law that
19
+ applies to you.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # @soba-so/sdk
2
+
3
+ **One request that comes back finished, executed on compute the end user owns.**
4
+
5
+ ```js
6
+ import { Soba } from "@soba-so/sdk"
7
+
8
+ const soba = new Soba({ apiKey: process.env.SOBA_KEY })
9
+
10
+ const run = await soba.run({
11
+ user: session.user.id,
12
+ model: "auto",
13
+ prompt: "Summarise everything I saved this week",
14
+ tools: [listNotes, readNote], // your code, executed in your app
15
+ })
16
+
17
+ for await (const event of run) {
18
+ if (event.type === "delta") process.stdout.write(event.text)
19
+ if (event.type === "done") console.log(event.usage)
20
+ }
21
+ ```
22
+
23
+ ## Why this exists alongside `chat/completions`
24
+
25
+ `POST /v1/chat/completions` is stateless and **the caller owns the loop**: you
26
+ send messages, you get a tool call back, you execute it, you send everything
27
+ again. That works for a model. It cannot work for an agent.
28
+
29
+ Claude Code and Codex run their own loop, and they never hand a tool call back to
30
+ wait on an HTTP round trip. So reaching them means **Soba owns the loop**, and
31
+ your tools have to be registered up front rather than returned mid-conversation.
32
+ That is the whole difference between the two entry points, and this package is
33
+ the second one.
34
+
35
+ If you only need models, you do not need this package at all: point an OpenAI
36
+ client at `https://api.soba.so/v1` and pass `user`.
37
+
38
+ ## What it is
39
+
40
+ A convenience over `POST /v1/runs`, not a different product. It adds four things
41
+ that are tedious to write once and easy to write wrongly:
42
+
43
+ | | |
44
+ |---|---|
45
+ | The stream | Server-sent events parsed into the five [event types](https://soba.so/docs/reference/events) |
46
+ | The tool loop | A `tool_call` arrives, **your** function runs here, the result goes back |
47
+ | Approvals | An escalation is answered, and **fails closed** if it cannot be |
48
+ | Failure | The three kinds of failure told apart, and retries only where they are safe |
49
+
50
+ ## The shapes
51
+
52
+ ```ts
53
+ new Soba({ apiKey, baseURL, maxRetries, connectTimeoutMs, onWarning })
54
+
55
+ soba.run({ user, prompt | messages, system, model, runtime, tools, route, policy, approve, signal })
56
+
57
+ soba.users.get(id) // plan, allowance, usage this period
58
+ soba.users.compute(id) // each connected machine and its live state
59
+ soba.usage.list(query) // the ledger, in integer micros
60
+ soba.plans.list()
61
+ soba.request(method, path, options) // the escape hatch
62
+ ```
63
+
64
+ `await soba.run(…)` resolves when the response **headers** arrive, not when the
65
+ run finishes. That is the same boundary the API draws: a failure before the first
66
+ byte throws here, and a failure after it arrives as a terminal `error` event.
67
+
68
+ ### `user` is required, and cannot be defaulted
69
+
70
+ It is how a run is attributed to a person, therefore how it is metered, and
71
+ therefore how Soba knows whose machine it may reach. Sending a run without one is
72
+ refused locally rather than at the server, because it is the mistake every
73
+ integration makes exactly once.
74
+
75
+ ### Tools are your code
76
+
77
+ The schema travels to the model. The implementation never leaves your process.
78
+
79
+ ```js
80
+ import { tool } from "@soba-so/sdk"
81
+
82
+ const listNotes = tool({
83
+ name: "listNotes",
84
+ description: "List the notes this user saved.",
85
+ inputSchema: { type: "object", properties: { since: { type: "string" } } },
86
+ execute: async ({ since }, ctx) => db.notes.since(ctx.user, since),
87
+ })
88
+ ```
89
+
90
+ Throwing inside `execute` is **not** a run failure: the error is handed to the
91
+ model as a failed call, which it can see and work around. `ctx.signal` is aborted
92
+ when the run ends, and long tools should honour it.
93
+
94
+ ### Approvals fail closed
95
+
96
+ A tool inside the machine's askable set needs a live answer. No approver, a
97
+ throw, or anything that is not the string `"allow"` is a denial, because an
98
+ approval channel that fails open turns "ask" into "allow".
99
+
100
+ ```js
101
+ await soba.run({
102
+ user, prompt,
103
+ approve: async ({ tool, input, cwd }) => (tool === "bash" ? "deny" : "allow"),
104
+ })
105
+ ```
106
+
107
+ ### Retries happen in one place, and only before the first byte
108
+
109
+ `POST /v1/runs` is not idempotent: retrying a run that already started is a
110
+ second run, billed as one. So the SDK retries `429`, `5xx` and transport
111
+ failures **before** a response arrives and never after, and it does not retry
112
+ `409 no_compute` at all: that clears when a laptop wakes up, which is minutes
113
+ away, not milliseconds. Your app owns that decision.
114
+
115
+ ## Errors
116
+
117
+ One class, `SobaError`, carrying `code`, `type`, `status`, `retryable` and
118
+ `retryAfterMs`. A stream that ends without `done` or `error` is
119
+ `stream_truncated`: a transport failure, retryable, and deliberately distinct
120
+ from a delivered `error` event, which is final.
121
+
122
+ ## The HTTP contract
123
+
124
+ Everything here is the documented API plus three answer-back routes, which exist
125
+ because a tool loop needs a return path and an SSE stream only goes one way:
126
+
127
+ | | |
128
+ |---|---|
129
+ | `POST /v1/runs` | Start a run. Responds `text/event-stream` with the run id in `Soba-Run-Id` |
130
+ | `POST /v1/runs/:id/tool_result` | `{ callId, result, isError }` |
131
+ | `POST /v1/runs/:id/approval` | `{ approvalId, decision, reason? }` |
132
+ | `POST /v1/runs/:id/cancel` | Best effort. The local stream drops immediately |
133
+
134
+ The stream carries the run's [events](https://soba.so/docs/reference/events), plus
135
+ a tool call or an approval request when one is waiting on you. Anything the SDK does
136
+ not recognise is skipped rather than fatal, so a server that ships ahead of your
137
+ installed version does not break the stream.
138
+
139
+ ## Types
140
+
141
+ Ships its own `.d.ts`. Every shape above — the run options, the event types, the
142
+ usage record, `SobaError` — is typed, with no separate `@types` package to install.
143
+
144
+ © 2026 Soba. All rights reserved. Licensed for use with the Soba service; see LICENSE.
@@ -0,0 +1,111 @@
1
+ import type { Message, RouteRequest, RunPolicy } from "./protocol.js";
2
+ import type { ConnectedMachine, PlanSummary, SobaUser, UsagePage, UsageQuery } from "./resources.js";
3
+ import { type Approver, Run } from "./run.js";
4
+ import { type ToolDefinition } from "./tools.js";
5
+ /**
6
+ * Where a run goes when nobody says otherwise.
7
+ *
8
+ * `api.`, which now exists — for months it had no DNS record, and the default
9
+ * pointed at `www` because a host that answers beat a host that does not.
10
+ *
11
+ * `www` was never right, only reachable. A run needs a socket held open for its
12
+ * whole duration and a round trip to Postgres per turn; Vercel gives neither,
13
+ * so `/v1/runs` is not served there and never will be. It 404s. Everything else
14
+ * in this API — sessions, pairing, machines — IS served there, which is why the
15
+ * two hosts both exist and why only this one is the run endpoint.
16
+ */
17
+ export declare const DEFAULT_BASE_URL = "https://api.soba.so/v1";
18
+ export interface SobaOptions {
19
+ /** Defaults to `SOBA_KEY`, then `SOBA_API_KEY`. Shaped `sk_soba_<id>.<secret>`. */
20
+ apiKey?: string;
21
+ /** Defaults to `SOBA_BASE_URL`, then https://api.soba.so/v1. */
22
+ baseURL?: string;
23
+ fetch?: typeof globalThis.fetch;
24
+ /** Retries before the first byte only. Never after a stream has started. */
25
+ maxRetries?: number;
26
+ /** How long to wait for response HEADERS. The run itself is not capped here. */
27
+ connectTimeoutMs?: number;
28
+ headers?: Record<string, string>;
29
+ /** Told about failures that have nowhere else to go: an undeliverable tool
30
+ * result, a cancel the broker never heard. Silent by default. */
31
+ onWarning?: (message: string, detail?: unknown) => void;
32
+ /** An app key is a SERVER credential. Shipping it to a browser hands every
33
+ * visitor the ability to spend your account. Only set this for a trusted
34
+ * first-party context you control. */
35
+ dangerouslyAllowBrowser?: boolean;
36
+ }
37
+ export interface RunParams {
38
+ /**
39
+ * The end user this run belongs to. Load-bearing, and the SDK refuses without
40
+ * it: it is how a run is attributed to a person, therefore how it is metered,
41
+ * and therefore how Soba knows whose machine it may reach.
42
+ */
43
+ user: string;
44
+ /** A single user turn. Shorthand for `messages: [{role:"user", content}]`. */
45
+ prompt?: string;
46
+ messages?: Message[];
47
+ system?: string;
48
+ /** Defaults to "auto": let the route decide. */
49
+ model?: string;
50
+ /** Name a specific runtime only when you have a reason to. `route` is the
51
+ * declaration that survives a user connecting something different. */
52
+ runtime?: string;
53
+ /** Your code, executed in your app. Schemas go to the model; nothing else moves. */
54
+ tools?: readonly ToolDefinition[];
55
+ /** Which economics this run may use. A plan compiles to one on every run, so
56
+ * most apps never write this by hand. */
57
+ route?: RouteRequest;
58
+ /** What the run REQUESTS of the machine. The machine's own grant still wins. */
59
+ policy?: RunPolicy;
60
+ /** Answers escalations for tools inside the machine's askable set. Absent
61
+ * means every escalation is denied. */
62
+ approve?: Approver;
63
+ metadata?: Record<string, string>;
64
+ signal?: AbortSignal;
65
+ }
66
+ /**
67
+ * The Soba client.
68
+ *
69
+ * ```js
70
+ * const soba = new Soba({ apiKey: process.env.SOBA_KEY })
71
+ * const run = await soba.run({ user: session.user.id, prompt: "…" })
72
+ * for await (const event of run) …
73
+ * ```
74
+ *
75
+ * `soba.run()` exists alongside `chat/completions` for one reason:
76
+ * `chat/completions` is stateless and the CALLER owns the agent loop, so it
77
+ * reaches models. Claude Code and Codex run their own loop and never hand a
78
+ * tool call back to wait on an HTTP round trip, so reaching them means SOBA
79
+ * owns the loop, and that is this call.
80
+ */
81
+ export declare class Soba {
82
+ #private;
83
+ readonly users: {
84
+ /** What this user has: plan, allowance, usage this period, what they connected. */
85
+ get(id: string): Promise<SobaUser>;
86
+ /** Each connected machine, its runtimes, cost classes and live state. */
87
+ compute(id: string): Promise<ConnectedMachine[]>;
88
+ };
89
+ readonly usage: {
90
+ list(query?: UsageQuery): Promise<UsagePage>;
91
+ };
92
+ readonly plans: {
93
+ list(): Promise<PlanSummary[]>;
94
+ };
95
+ constructor(options?: SobaOptions);
96
+ get baseURL(): string;
97
+ /**
98
+ * Start a run and get back something you iterate.
99
+ *
100
+ * Resolves when the response HEADERS arrive, not when the run finishes. That
101
+ * is the same boundary the API draws: a failure before the first byte throws
102
+ * here, and a failure after it arrives as a terminal `error` event.
103
+ */
104
+ run(params: RunParams): Promise<Run>;
105
+ /** The escape hatch. Every typed read above is this call with a shape on it. */
106
+ request<T>(method: "GET" | "POST" | "DELETE", path: string, options?: {
107
+ query?: Record<string, string | number | boolean | undefined>;
108
+ body?: unknown;
109
+ signal?: AbortSignal;
110
+ }): Promise<T>;
111
+ }
@@ -0,0 +1,67 @@
1
+ export type SobaErrorCode =
2
+ /** 400 malformed, or caught here before the round trip */
3
+ "invalid_request"
4
+ /** 422 no user. It cannot be defaulted: a run with no user cannot be metered */
5
+ | "user_required"
6
+ /** 401 missing, malformed, revoked, or another app's key */
7
+ | "unauthorized"
8
+ /** 402 the plan is exhausted and its overage is `stop` */
9
+ | "payment_required"
10
+ /** 403 asked for something this key may not have, e.g. a metered class on a dev key */
11
+ | "forbidden"
12
+ /** 404 no such model or runtime alias */
13
+ | "not_found"
14
+ /** 409 nothing this run is allowed to use is connected and awake */
15
+ | "no_compute"
16
+ /** 429 back off; `retryAfterMs` says how long */
17
+ | "rate_limited"
18
+ /** 5xx ours */
19
+ | "server_error"
20
+ /** the request never reached a server */
21
+ | "connection"
22
+ /** no response headers within `connectTimeoutMs` */
23
+ | "timeout"
24
+ /** the stream ended without `done` or `error`: transport, not outcome */
25
+ | "stream_truncated"
26
+ /** a delivered terminal `error` event, surfaced by `run.text()` */
27
+ | "run_failed"
28
+ /** the caller aborted */
29
+ | "canceled" | "unknown";
30
+ export interface SobaErrorInit {
31
+ message: string;
32
+ code: SobaErrorCode;
33
+ /** The server's own `error.type`, when it sent one. */
34
+ type?: string;
35
+ status?: number;
36
+ retryable?: boolean;
37
+ retryAfterMs?: number;
38
+ runId?: string;
39
+ cause?: unknown;
40
+ }
41
+ /**
42
+ * `retryable` is advice, not an instruction, and it is deliberately FALSE for
43
+ * `no_compute`. That failure clears when a laptop wakes up, so retrying it on a
44
+ * millisecond backoff inside the SDK would spin against a machine that is
45
+ * asleep. Minutes, not milliseconds, and the app owns that decision.
46
+ */
47
+ export declare class SobaError extends Error {
48
+ readonly code: SobaErrorCode;
49
+ readonly type: string;
50
+ readonly status: number | undefined;
51
+ readonly retryable: boolean;
52
+ readonly retryAfterMs: number | undefined;
53
+ readonly runId: string | undefined;
54
+ constructor(init: SobaErrorInit);
55
+ }
56
+ export declare function codeForStatus(status: number): SobaErrorCode;
57
+ /**
58
+ * Build an error from a failed response. The envelope is
59
+ * `{ error: { type, message, code } }` on every endpoint, but a proxy, a
60
+ * gateway timeout page or an HTML error can arrive here too, so nothing about
61
+ * the body is assumed.
62
+ */
63
+ export declare function errorFromResponse(response: Response): Promise<SobaError>;
64
+ /** RFC 7231 allows seconds or an HTTP date. Both appear in the wild. */
65
+ export declare function retryAfterMs(header: string | null): number | undefined;
66
+ /** Anything thrown by `fetch` or an abort, normalised. */
67
+ export declare function asSobaError(err: unknown): SobaError;
package/dist/http.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { SobaError } from "./errors.js";
2
+ export interface HttpConfig {
3
+ baseURL: string;
4
+ apiKey: string;
5
+ headers: Record<string, string>;
6
+ fetch: typeof globalThis.fetch;
7
+ /** Covers HEADERS ONLY. A run streams for minutes; a deadline that kept
8
+ * running would kill the answer it was meant to protect. */
9
+ connectTimeoutMs: number;
10
+ maxRetries: number;
11
+ }
12
+ export interface RequestOptions {
13
+ method: "GET" | "POST" | "DELETE";
14
+ path: string;
15
+ query?: Record<string, string | number | boolean | undefined>;
16
+ body?: unknown;
17
+ accept?: string;
18
+ signal?: AbortSignal;
19
+ /** Default true. Set false for anything that must happen at most once. */
20
+ retry?: boolean;
21
+ }
22
+ export declare function httpRequest(cfg: HttpConfig, opts: RequestOptions): Promise<Response>;
23
+ export declare function requestJson<T>(cfg: HttpConfig, opts: RequestOptions): Promise<T>;
24
+ export declare function buildUrl(baseURL: string, path: string, query?: Record<string, string | number | boolean | undefined>): string;
25
+ /**
26
+ * Exponential backoff with jitter, and `Retry-After` wins when the server sent
27
+ * one. The jitter is not decoration: a fleet of servers retrying a rate limit
28
+ * in lockstep re-creates the burst that caused it.
29
+ */
30
+ export declare function backoffMs(attempt: number, last: SobaError | undefined): number;
31
+ export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
32
+ /** Forward one signal's abort onto a controller, and stop forwarding on demand. */
33
+ export declare function chain(parent: AbortSignal | undefined, child: AbortController): () => void;
@@ -0,0 +1,7 @@
1
+ export { DEFAULT_BASE_URL, type RunParams, Soba, type SobaOptions } from "./client.js";
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, ConnectedMachine, MachineRuntime, PlanSummary, SobaUser, UsagePage, UsageQuery, UsageRow, } from "./resources.js";
5
+ export { type ApprovalDecision, type ApprovalRequest, type Approver, Run, } from "./run.js";
6
+ export { type ToolContext, type ToolDefinition, tool } from "./tools.js";
7
+ export { VERSION } from "./version.js";
package/dist/index.mjs ADDED
@@ -0,0 +1,4 @@
1
+ var s=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??F.has(e.code),this.retryAfterMs=e.retryAfterMs,this.runId=e.runId}},F=new Set(["rate_limited","server_error","connection","timeout","stream_truncated"]),j={400:"invalid_request",401:"unauthorized",402:"payment_required",403:"forbidden",404:"not_found",409:"no_compute",422:"user_required",429:"rate_limited"};function W(t){let e=j[t];return e||(t>=500?"server_error":t>=400?"invalid_request":"unknown")}async function k(t){let e=t.status,n="";try{n=await t.text()}catch{}let r=B(n);return new s({code:r?.code??W(e),type:r?.type,message:r?.message??G(e,t.statusText,n),status:e,retryAfterMs:J(t.headers.get("retry-after")),runId:t.headers.get("soba-run-id")??void 0})}function B(t){if(!t)return null;let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object")return null;let n=e.error;if(!n||typeof n!="object")return null;let r=n;return{message:typeof r.message=="string"&&r.message?r.message:void 0,type:typeof r.type=="string"&&r.type?r.type:void 0,code:K.has(r.code)?r.code:void 0}}var K=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 G(t,e,n){let r=n.trim().slice(0,200),o=r?`: ${r}`:"";return`Soba returned ${t}${e?` ${e}`:""}${o}`}function J(t){if(!t)return;let e=Number(t);if(Number.isFinite(e)&&e>=0)return Math.round(e*1e3);let n=Date.parse(t);if(!Number.isNaN(n))return Math.max(0,n-Date.now())}function f(t){if(t instanceof s)return t;let e=t;return e?.name==="AbortError"||e?.name==="TimeoutError"?new s({code:"canceled",message:e.message||"The request was aborted.",retryable:!1,cause:t}):new s({code:"connection",message:e?.message?`Could not reach Soba: ${e.message}`:"Could not reach Soba.",cause:t})}var w="0.0.5";var H=8e3,Y=3e4;async function g(t,e){let n=Q(t.baseURL,e.path,e.query),r=e.retry===!1?1:Math.max(0,t.maxRetries)+1,o;for(let i=0;i<r;i++){i>0&&await Z(X(i,o),e.signal);let a=new AbortController,c=S(e.signal,a),m=setTimeout(()=>{a.abort(new s({code:"timeout",message:`Soba did not answer within ${t.connectTimeoutMs}ms.`}))},t.connectTimeoutMs),d;try{d=await t.fetch(n,{method:e.method,headers:z(t,e),body:e.body===void 0?void 0:JSON.stringify(e.body),signal:a.signal})}catch(l){if(c(),o=f(a.signal.aborted?a.signal.reason??l:l),!o.retryable||i===r-1)throw o;continue}finally{clearTimeout(m)}if(d.ok)return d;if(c(),o=await k(d),!o.retryable||i===r-1)throw o}throw o??new s({code:"unknown",message:"The request failed with no error."})}async function C(t,e){let n=await g(t,{accept:"application/json",...e}),r=await n.text();if(r)try{return JSON.parse(r)}catch(o){throw new s({code:"server_error",message:`Soba returned a body that is not JSON: ${r.slice(0,200)}`,status:n.status,cause:o})}}function z(t,e){let n={authorization:`Bearer ${t.apiKey}`,accept:e.accept??"application/json","user-agent":V(),...t.headers};return e.body!==void 0&&(n["content-type"]="application/json"),n}function V(){let t=typeof process>"u"?"":` node/${process.versions?.node??"?"}`;return`soba-sdk/${w}${t}`}function Q(t,e,n){let r=t.replace(/\/+$/,""),o=new URL(`${r}${e.startsWith("/")?e:`/${e}`}`);for(let[i,a]of Object.entries(n??{}))a!==void 0&&o.searchParams.set(i,String(a));return o.toString()}function X(t,e){if(e?.retryAfterMs!==void 0)return Math.min(e.retryAfterMs,Y);let n=Math.min(H,500*2**(t-1));return Math.round(n*(.5+Math.random()/2))}function Z(t,e){return new Promise((n,r)=>{if(e?.aborted)return r(f(e.reason));let o=setTimeout(()=>{e?.removeEventListener("abort",i),n()},t);function i(){clearTimeout(o),r(f(e?.reason))}e?.addEventListener("abort",i,{once:!0})})}function S(t,e){if(!t)return()=>{};if(t.aborted)return e.abort(t.reason),()=>{};let n=()=>e.abort(t.reason);return t.addEventListener("abort",n,{once:!0}),()=>t.removeEventListener("abort",n)}var _=["open-weights","frontier"];function ee(t){return _.includes(t)}function te(t){return t==="user-hardware"||t==="user-subscription"||t==="open-weights"||t==="frontier"}function T(t){return t.type==="done"||t.type==="error"}var ne=["search","fetch","read","write","edit","bash","files","agent","todo","think","sources","tool"];var U=new Set(["delta","thinking","status","done","error"]);function v(t){let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e))return null;let n=e,r=u(n.runId);if(typeof n.type=="string"&&U.has(n.type)){let o=I(n);return o?{kind:"event",runId:r,event:o}:null}switch(n.type){case"event":{if(!n.event||typeof n.event!="object")return null;let o=I(n.event);return o?{kind:"event",runId:r,event:o}:null}case"usage":{let o=P(n.usage);return o?{kind:"usage",runId:r,usage:o}:null}case"tool_call":{let o=u(n.callId),i=u(n.name);return!o||!i?null:{kind:"tool_call",runId:r,callId:o,name:i,input:n.input}}case"approval_request":{let o=u(n.approvalId),i=u(n.tool);return!o||!i?null:{kind:"approval",runId:r,approvalId:o,tool:i,input:n.input,cwd:u(n.cwd)}}default:return null}}function I(t){let e=typeof t.type=="string"&&U.has(t.type)?t.type:null;if(!e)return null;let n={type:e},r=u(t.text);r!==void 0&&(n.text=r);let o=u(t.message);o!==void 0&&(n.message=o);let i=u(t.tool);i!==void 0&&(n.tool=i);let a=u(t.tier);a!==void 0&&(n.tier=a);let c=P(t.usage);return c&&(n.usage=c),n}function P(t){if(!t||typeof t!="object")return null;let e=t,n={model:u(e.model)??"unknown",inputTokens:y(e.inputTokens),outputTokens:y(e.outputTokens),cacheReadTokens:y(e.cacheReadTokens),cacheWriteTokens:y(e.cacheWriteTokens),webSearches:y(e.webSearches)},r=u(e.costClass);r!==void 0&&(n.costClass=r);let o=u(e.provider);return o!==void 0&&(n.provider=o),typeof e.costMicros=="number"&&Number.isFinite(e.costMicros)&&(n.costMicros=e.costMicros),n}function u(t){return typeof t=="string"&&t.length>0?t:void 0}function y(t){return typeof t=="number"&&Number.isFinite(t)?t:0}async function*O(t){let e=t.body;if(!e)throw new s({code:"connection",message:"The run response arrived with no body to stream.",status:t.status});let n=t.headers.get("content-type")??"",r=n.includes("ndjson")||n.includes("jsonlines"),o=e.getReader(),i=new TextDecoder,a="",c=[];try{for(;;){let d=await o.read();if(d.done)break;for(a+=i.decode(d.value,{stream:!0});;){let l=a.indexOf(`
2
+ `);if(l===-1)break;let b=q(a.slice(0,l));if(a=a.slice(l+1),r){let p=b.trim()?v(b):null;p&&(yield p);continue}if(b===""){let p=c.join(`
3
+ `);if(c=[],p==="[DONE]")return;let A=p?v(p):null;A&&(yield A);continue}M(b,c)}}!r&&a&&M(q(a),c);let m=r?a.trim():c.join(`
4
+ `);if(m&&m!=="[DONE]"){let d=v(m);d&&(yield d)}}finally{o.cancel().catch(()=>{})}}function M(t,e){if(t.startsWith(":"))return;let n=t.indexOf(":");if(n===-1||t.slice(0,n)!=="data")return;let r=t.slice(n+1);e.push(r.startsWith(" ")?r.slice(1):r)}function q(t){return t.endsWith("\r")?t.slice(0,-1):t}function re(t){return t}function D(t){let e=new Map;for(let n of t??[]){if(!n?.name)throw new s({code:"invalid_request",message:"Every tool needs a name."});if(typeof n.execute!="function")throw new s({code:"invalid_request",message:`Tool "${n.name}" has no execute function. A tool Soba cannot call is not a tool.`});if(e.has(n.name))throw new s({code:"invalid_request",message:`Two tools are named "${n.name}". Tool names must be unique within a run.`});e.set(n.name,n)}return e}function L(t){return[...t.values()].map(e=>({name:e.name,description:e.description??"",inputSchema:e.inputSchema??{type:"object"}}))}function N(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 R(t){return t instanceof Error?t.message||t.name:typeof t=="string"?t:String(t)}var h=class{#e;#n;#r;#a=!1;#t;#o=new AbortController;#s=new AbortController;#i=new Set;#u;constructor(e){this.#t=e,this.#e=e.response.headers.get("soba-run-id")??"",this.#u=S(e.signal,this.#o)}get runId(){return this.#e}get tier(){return this.#n}get usage(){return this.#r}[Symbol.asyncIterator](){return this.#d()}async text(){let e="";for await(let n of this)if(n.type==="delta"&&n.text&&(e+=n.text),n.type==="error")throw new s({code:"run_failed",message:n.message??"The run ended with an error.",runId:this.#e,retryable:!1});return e}async cancel(){if(this.#o.abort(new s({code:"canceled",message:"The run was cancelled."})),this.#s.abort(new s({code:"canceled",message:"The run was cancelled."})),!!this.#e)try{await g(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*#d(){if(this.#a)throw new s({code:"invalid_request",message:"This run has already been consumed. A run's events can only be read once.",runId:this.#e});this.#a=!0;let e=!1;try{for await(let n of O(this.#t.response)){if(n.runId&&!this.#e&&(this.#e=n.runId),n.kind==="event"){let r=n.event;if(r.tier&&(this.#n=r.tier),r.usage&&(this.#r=r.usage),e=T(r),yield r,e)return;continue}if(n.kind==="usage"){this.#r=n.usage;continue}this.#l(this.#p(n))}throw new s({code:"stream_truncated",message:"The run stream ended without a `done` or `error` event.",runId:this.#e})}finally{e||this.#s.abort(new s({code:"canceled",message:"The run ended before this tool finished."})),await Promise.allSettled([...this.#i]),this.#o.abort(),this.#u()}}#l(e){this.#i.add(e),e.finally(()=>this.#i.delete(e)).catch(()=>{})}async#p(e){if(e.kind==="tool_call")return this.#f(e);if(e.kind==="approval")return this.#g(e)}async#f(e){let n=this.#t.tools.get(e.name),r,o=!1;if(!n)r=`No tool named "${e.name}" is registered by this app.`,o=!0;else{let i={runId:this.#e,user:this.#t.user,callId:e.callId,signal:this.#s.signal};try{r=await n.execute(e.input,i)}catch(a){r=R(a),o=!0}}await this.#c(`/runs/${encodeURIComponent(this.#e)}/tool_result`,{callId:e.callId,result:N(r),isError:o})}async#g(e){let n="deny",r;if(!this.#t.approve)r="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),n=await this.#t.approve(o)==="allow"?"allow":"deny"}catch(o){r=R(o)}await this.#c(`/runs/${encodeURIComponent(this.#e)}/approval`,{approvalId:e.approvalId,decision:n,...r?{reason:r}:{}})}async#c(e,n){if(!this.#e){this.#t.onWarning("Soba sent a frame before the run had an id, so it could not be answered.",n);return}try{await(await g(this.#t.config,{method:"POST",path:e,body:n})).text()}catch(r){this.#t.onWarning(`Could not deliver an answer to ${e}.`,f(r))}}};var $="https://api.soba.so/v1",oe=3e4,se=2,x=class{#e;#n;users;usage;plans;constructor(e={}){let n=e.apiKey??E("SOBA_KEY")??E("SOBA_API_KEY");if(!n)throw new s({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(ie()&&!e.dangerouslyAllowBrowser)throw new s({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:n,baseURL:e.baseURL??E("SOBA_BASE_URL")??$,headers:e.headers??{},fetch:e.fetch??globalThis.fetch,connectTimeoutMs:e.connectTimeoutMs??oe,maxRetries:e.maxRetries??se},typeof this.#e.fetch!="function")throw new s({code:"invalid_request",message:"No global fetch. Run on Node 22 or newer, or pass your own `fetch`."});this.users={get:r=>this.request("GET",`/users/${encodeURIComponent(r)}`),compute:r=>this.request("GET",`/users/${encodeURIComponent(r)}/compute`)},this.usage={list:r=>this.request("GET","/usage",{query:{...r}})},this.plans={list:()=>this.request("GET","/plans")}}get baseURL(){return this.#e.baseURL}async run(e){if(!e.user)throw new s({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 s({code:"invalid_request",status:400,message:"A run needs a `prompt` or a non-empty `messages`."});let n=D(e.tools),r={user:e.user,model:e.model??"auto"};e.prompt&&(r.prompt=e.prompt),e.messages?.length&&(r.messages=e.messages),e.system&&(r.system=e.system),e.runtime&&(r.runtime=e.runtime),n.size&&(r.tools=L(n)),e.route&&(r.route=e.route),e.policy&&(r.policy=e.policy),e.metadata&&(r.metadata=e.metadata);let o=await g(this.#e,{method:"POST",path:"/runs",body:r,accept:"text/event-stream",signal:e.signal});return new h({config:this.#e,response:o,user:e.user,tools:n,approve:e.approve,onWarning:this.#n,signal:e.signal})}async request(e,n,r={}){return C(this.#e,{method:e,path:n,...r})}};function E(t){if(typeof process>"u")return;let e=process.env?.[t];return e||void 0}function ie(){let t=globalThis;return typeof t.window=="object"&&typeof t.window?.document=="object"}export{$ as DEFAULT_BASE_URL,_ as METERED_CLASSES,h as Run,x as Soba,s as SobaError,ne as TOOL_SLUGS,w as VERSION,te as isCostClass,ee as isMetered,T as isTerminal,re as tool};
@@ -0,0 +1,94 @@
1
+ /**
2
+ * WHO PAYS for a run.
3
+ *
4
+ * user-hardware the user's own silicon. Free to everyone
5
+ * user-subscription a plan the user already pays for. Free to the broker
6
+ * open-weights an open model on GPU bought wholesale. METERED
7
+ * frontier a frontier provider API. METERED
8
+ */
9
+ export type CostClass = "user-hardware" | "user-subscription" | "open-weights" | "frontier";
10
+ /** The classes somebody has to be billed for. The user-* classes are already paid. */
11
+ export declare const METERED_CLASSES: readonly ["open-weights", "frontier"];
12
+ export declare function isMetered(c: CostClass): boolean;
13
+ export declare function isCostClass(v: unknown): v is CostClass;
14
+ /** Token and tool usage from a completed run: what a meter bills against. */
15
+ export interface RunUsage {
16
+ /** Which economics served this run. Present so a usage frame is self-describing. */
17
+ costClass?: CostClass;
18
+ /** What it cost, in micros, for the metered classes. Absent when nothing is owed. */
19
+ costMicros?: number;
20
+ /** Provider-neutral model alias, e.g. "sonnet", "gpt-5-codex". */
21
+ model: string;
22
+ /** Which provider actually served it, since aliases collide. */
23
+ provider?: string;
24
+ inputTokens: number;
25
+ outputTokens: number;
26
+ cacheReadTokens: number;
27
+ cacheWriteTokens: number;
28
+ webSearches: number;
29
+ }
30
+ /** FROZEN. New information arrives as optional fields on these, never as a new type. */
31
+ export type AgentEventType = "delta" | "thinking" | "status" | "done" | "error";
32
+ export interface AgentEvent {
33
+ type: AgentEventType;
34
+ /** `delta`: the NEW text, append it. `thinking`: the RUNNING TOTAL, replace it. */
35
+ text?: string;
36
+ /** `status` / `error`: human-readable copy, safe to show a user. */
37
+ message?: string;
38
+ /** On a `status` raised by a tool: a stable slug the UI maps to an icon. */
39
+ tool?: string;
40
+ /** On `done`: what the run consumed. Absent when the runtime cannot report it. */
41
+ usage?: RunUsage;
42
+ /** Which economics is serving this run, announced on the opening `status`,
43
+ * BEFORE any output. A visible downgrade beats a silent one. */
44
+ tier?: CostClass;
45
+ }
46
+ export declare function isTerminal(e: AgentEvent): boolean;
47
+ /** Stable tool slugs. Adapters map their runtime's native names onto these. */
48
+ export declare const TOOL_SLUGS: readonly ["search", "fetch", "read", "write", "edit", "bash", "files", "agent", "todo", "think", "sources", "tool"];
49
+ export type ToolSlug = (typeof TOOL_SLUGS)[number];
50
+ /** One turn of a conversation. `prompt` is shorthand for a single user message. */
51
+ export interface Message {
52
+ role: "user" | "assistant";
53
+ content: string;
54
+ }
55
+ /** An app-defined tool as it travels on the wire: schema only, no implementation. */
56
+ export interface ToolSchema {
57
+ name: string;
58
+ description: string;
59
+ /** JSON Schema for the tool's input. */
60
+ inputSchema: Record<string, unknown>;
61
+ }
62
+ /**
63
+ * - "ask" escalations round-trip to an approver (see `askTools`)
64
+ * - "deny" anything outside the allowlist fails the call, the run continues
65
+ * - "auto" the runtime proceeds within the resolved allowlist, no prompting
66
+ */
67
+ export type PermissionMode = "ask" | "deny" | "auto";
68
+ /**
69
+ * What a run REQUESTS. Never a grant: the machine's owner has their own policy
70
+ * and the worker resolves the two by intersection, so this can only ever narrow
71
+ * what the machine already allows.
72
+ */
73
+ export interface RunPolicy {
74
+ cwd?: string;
75
+ allowTools?: string[];
76
+ denyTools?: string[];
77
+ askTools?: string[];
78
+ timeoutMs?: number;
79
+ permissionMode?: PermissionMode;
80
+ allowNetwork?: string[];
81
+ }
82
+ /**
83
+ * WHICH ECONOMICS a run may use. You declare intent, not a runtime, which is
84
+ * what lets one call site back a free-forever app and an enterprise app.
85
+ */
86
+ export interface RouteRequest {
87
+ /** Ordered preference. The first reachable class wins. */
88
+ prefer: CostClass[];
89
+ /** Classes usable at all. Omitted = anything the broker can reach.
90
+ * A HARD CEILING, enforced on the worker as well as the broker. */
91
+ allow?: CostClass[];
92
+ /** Ceiling on what one run may cost, in micros. Metered classes only. */
93
+ maxCostMicros?: number;
94
+ }
@@ -0,0 +1,85 @@
1
+ import type { CostClass } from "./protocol.js";
2
+ export interface PlanSummary {
3
+ id: string;
4
+ name: string;
5
+ priceMicros?: number;
6
+ interval?: "month" | "year" | string;
7
+ /** What a user earns for connecting their own compute, as a credit. */
8
+ connectedRewardMicros?: number;
9
+ /** What happens past the allowance. */
10
+ overage?: "stop" | "meter" | string;
11
+ }
12
+ export interface Allowance {
13
+ includedMicros?: number;
14
+ usedMicros?: number;
15
+ /** Credit earned by connecting compute, spent before money. */
16
+ earnedMicros?: number;
17
+ periodStart?: string;
18
+ periodEnd?: string;
19
+ }
20
+ /** `GET /v1/users/:id` — what this user has. */
21
+ export interface SobaUser {
22
+ id: string;
23
+ email?: string | null;
24
+ plan?: PlanSummary | null;
25
+ allowance?: Allowance | null;
26
+ /** Whether anything of theirs can serve a run right now. */
27
+ connected?: boolean;
28
+ machines?: number;
29
+ createdAt?: string;
30
+ }
31
+ export interface MachineRuntime {
32
+ id: string;
33
+ displayName: string;
34
+ version?: string | null;
35
+ models?: string[];
36
+ costClass: CostClass;
37
+ supportsTools?: boolean;
38
+ /** TRI-STATE. `null` means the worker could not tell, which is not `false`. */
39
+ authenticated?: boolean | null;
40
+ authHint?: string | null;
41
+ }
42
+ /** `GET /v1/users/:id/compute` — one connected machine and its live state. */
43
+ export interface ConnectedMachine {
44
+ id: string;
45
+ label?: string | null;
46
+ platform?: string | null;
47
+ arch?: string | null;
48
+ state: "online" | "offline" | "busy" | string;
49
+ alwaysOn?: boolean;
50
+ active?: number;
51
+ capacity?: number;
52
+ lastSeenAt?: string | null;
53
+ runtimes: MachineRuntime[];
54
+ }
55
+ /** `GET /v1/usage` — one row, self-describing about who paid for it. */
56
+ export interface UsageRow {
57
+ runId: string;
58
+ user: string;
59
+ createdAt: string;
60
+ model: string;
61
+ provider?: string | null;
62
+ costClass: CostClass;
63
+ inputTokens: number;
64
+ outputTokens: number;
65
+ cacheReadTokens: number;
66
+ cacheWriteTokens: number;
67
+ webSearches: number;
68
+ costMicros: number;
69
+ /** What this run saved: zero on a metered run, the counterfactual otherwise. */
70
+ avoidedCostMicros: number;
71
+ environment?: string | null;
72
+ }
73
+ export interface UsageQuery {
74
+ user?: string;
75
+ /** ISO 8601. */
76
+ from?: string;
77
+ to?: string;
78
+ environment?: "development" | "preview" | "production" | string;
79
+ limit?: number;
80
+ cursor?: string;
81
+ }
82
+ export interface UsagePage {
83
+ data: UsageRow[];
84
+ nextCursor?: string | null;
85
+ }
package/dist/run.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { type HttpConfig } from "./http.js";
2
+ import { type AgentEvent, type CostClass, type RunUsage } from "./protocol.js";
3
+ import { type ToolDefinition } from "./tools.js";
4
+ export type ApprovalDecision = "allow" | "deny";
5
+ export interface ApprovalRequest {
6
+ runId: string;
7
+ approvalId: string;
8
+ /** The tool the model wants to use. */
9
+ tool: string;
10
+ input: unknown;
11
+ /** The directory the run is working in, when the runtime reports one. */
12
+ cwd?: string;
13
+ }
14
+ export type Approver = (request: ApprovalRequest) => ApprovalDecision | Promise<ApprovalDecision>;
15
+ export interface RunInit {
16
+ config: HttpConfig;
17
+ response: Response;
18
+ user: string;
19
+ tools: Map<string, ToolDefinition>;
20
+ approve: Approver | undefined;
21
+ onWarning: (message: string, detail?: unknown) => void;
22
+ signal: AbortSignal | undefined;
23
+ }
24
+ /**
25
+ * A run in flight.
26
+ *
27
+ * `await soba.run()` resolves as soon as the response headers arrive, which is
28
+ * the boundary docs/errors.md draws: a failure before the first byte is an HTTP
29
+ * status you catch, and a failure after it is a terminal `error` event you read.
30
+ *
31
+ * Iterate it once. Tool calls and approvals are answered underneath the
32
+ * iteration, so a consumer that only cares about text never has to know they
33
+ * happened.
34
+ */
35
+ export declare class Run implements AsyncIterable<AgentEvent> {
36
+ #private;
37
+ constructor(init: RunInit);
38
+ /** Empty only until the first frame, if the server omitted the header. */
39
+ get runId(): string;
40
+ /** Which economics served this run, from the opening `status`. */
41
+ get tier(): CostClass | undefined;
42
+ /** What the run consumed. Absent until `done`, and absent after it when the
43
+ * runtime could not report it, which is exactly when the run was free. */
44
+ get usage(): RunUsage | undefined;
45
+ [Symbol.asyncIterator](): AsyncIterator<AgentEvent>;
46
+ /** Drain the run and return the assembled text. Throws on a terminal error. */
47
+ text(): Promise<string>;
48
+ /**
49
+ * Stop the run. Best effort by design: the local stream is dropped
50
+ * immediately, and the broker is told so the machine stops working, but a
51
+ * subprocess that has already produced tokens has already produced them.
52
+ */
53
+ cancel(): Promise<void>;
54
+ }
@@ -0,0 +1,31 @@
1
+ import type { AgentEvent, RunUsage } from "./protocol.js";
2
+ export type StreamFrame = {
3
+ kind: "event";
4
+ runId?: string;
5
+ event: AgentEvent;
6
+ } | {
7
+ kind: "usage";
8
+ runId?: string;
9
+ usage: RunUsage;
10
+ } | {
11
+ kind: "tool_call";
12
+ runId?: string;
13
+ callId: string;
14
+ name: string;
15
+ input: unknown;
16
+ } | {
17
+ kind: "approval";
18
+ runId?: string;
19
+ approvalId: string;
20
+ tool: string;
21
+ input: unknown;
22
+ cwd?: string;
23
+ };
24
+ export declare function parseStreamFrame(raw: string): StreamFrame | null;
25
+ /**
26
+ * Yield frames off a live response body.
27
+ *
28
+ * SSE by default; newline-delimited JSON when the server says so, which is what
29
+ * a hand-rolled test server is likely to send.
30
+ */
31
+ export declare function readFrames(response: Response): AsyncGenerator<StreamFrame>;
@@ -0,0 +1,42 @@
1
+ import type { ToolSchema } from "./protocol.js";
2
+ export interface ToolContext {
3
+ /** The run that asked. */
4
+ runId: string;
5
+ /** The end user the run is attributed to. */
6
+ user: string;
7
+ /** This specific call, which the result is keyed to. */
8
+ callId: string;
9
+ /** Aborted when the run ends or the caller cancels. Honour it in long tools. */
10
+ signal: AbortSignal;
11
+ }
12
+ export interface ToolDefinition<Input = Record<string, unknown>> {
13
+ name: string;
14
+ /** What the tool does, in the model's words. This is prompt, not documentation. */
15
+ description?: string;
16
+ /** JSON Schema for the input. Omitted means an object with no declared fields. */
17
+ inputSchema?: Record<string, unknown>;
18
+ /**
19
+ * Your code. Return anything JSON-serialisable; a string is passed through
20
+ * verbatim. Throwing is not a run failure: the error is handed to the model
21
+ * as a failed call, which it can see and work around.
22
+ */
23
+ execute(input: Input, ctx: ToolContext): unknown | Promise<unknown>;
24
+ }
25
+ /** Identity, for the type inference. `tool({...})` reads better than a cast. */
26
+ export declare function tool<Input = Record<string, unknown>>(def: ToolDefinition<Input>): ToolDefinition<Input>;
27
+ /**
28
+ * Index the tools by name, refusing a duplicate rather than letting the second
29
+ * one win silently. Two tools with one name is a bug the model surfaces as
30
+ * baffling behaviour hours later.
31
+ */
32
+ export declare function indexTools(tools: readonly ToolDefinition[] | undefined): Map<string, ToolDefinition>;
33
+ /** What goes on the wire: schema only. `execute` is stripped here, not by luck. */
34
+ export declare function toolSchemas(byName: Map<string, ToolDefinition>): ToolSchema[];
35
+ /**
36
+ * Make a tool's return value safe to put in a JSON body. A value that cannot
37
+ * be serialised (a BigInt, a cycle, a class instance with a throwing getter)
38
+ * becomes its string form rather than an exception that would strand the run
39
+ * waiting for a result that never arrives.
40
+ */
41
+ export declare function jsonSafe(value: unknown): unknown;
42
+ export declare function errorText(err: unknown): string;
@@ -0,0 +1 @@
1
+ export declare const VERSION = "0.0.5";
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@soba-so/sdk",
3
+ "version": "0.0.5",
4
+ "description": "One request that comes back finished, executed on compute the end user owns.",
5
+ "license": "UNLICENSED",
6
+ "homepage": "https://soba.so/docs/runs",
7
+ "type": "module",
8
+ "main": "./dist/index.mjs",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.mjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "!dist/**/*.map",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "node ../../scripts/build-sdk.mjs",
23
+ "prepublishOnly": "node ../../scripts/build-sdk.mjs"
24
+ },
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "keywords": [
29
+ "soba",
30
+ "agent",
31
+ "claude",
32
+ "codex",
33
+ "runs",
34
+ "byoc"
35
+ ],
36
+ "devDependencies": {
37
+ "@soba-so/protocol": "workspace:*"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }