@runappsai/sdk 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -2
  3. package/dist/activity-tracker.d.ts +113 -0
  4. package/dist/activity-tracker.d.ts.map +1 -0
  5. package/dist/activity-tracker.js +176 -0
  6. package/dist/activity-tracker.js.map +1 -0
  7. package/dist/audio.d.ts +129 -0
  8. package/dist/audio.d.ts.map +1 -0
  9. package/dist/audio.js +267 -0
  10. package/dist/audio.js.map +1 -0
  11. package/dist/browser-auth.d.ts +179 -0
  12. package/dist/browser-auth.d.ts.map +1 -0
  13. package/dist/browser-auth.js +1361 -0
  14. package/dist/browser-auth.js.map +1 -0
  15. package/dist/chat.d.ts +221 -0
  16. package/dist/chat.d.ts.map +1 -0
  17. package/dist/chat.js +155 -0
  18. package/dist/chat.js.map +1 -0
  19. package/dist/client.d.ts +155 -0
  20. package/dist/client.d.ts.map +1 -0
  21. package/dist/client.js +157 -0
  22. package/dist/client.js.map +1 -0
  23. package/dist/computer.d.ts +57 -0
  24. package/dist/computer.d.ts.map +1 -0
  25. package/dist/computer.js +21 -0
  26. package/dist/computer.js.map +1 -0
  27. package/dist/embeddings.d.ts +64 -0
  28. package/dist/embeddings.d.ts.map +1 -0
  29. package/dist/embeddings.js +30 -0
  30. package/dist/embeddings.js.map +1 -0
  31. package/dist/errors.d.ts +24 -0
  32. package/dist/errors.d.ts.map +1 -0
  33. package/dist/errors.js +29 -0
  34. package/dist/errors.js.map +1 -0
  35. package/dist/event-wrap.d.ts +59 -0
  36. package/dist/event-wrap.d.ts.map +1 -0
  37. package/dist/event-wrap.js +142 -0
  38. package/dist/event-wrap.js.map +1 -0
  39. package/dist/events.d.ts +119 -0
  40. package/dist/events.d.ts.map +1 -0
  41. package/dist/events.js +87 -0
  42. package/dist/events.js.map +1 -0
  43. package/dist/files.d.ts +218 -0
  44. package/dist/files.d.ts.map +1 -0
  45. package/dist/files.js +212 -0
  46. package/dist/files.js.map +1 -0
  47. package/dist/image.d.ts +185 -0
  48. package/dist/image.d.ts.map +1 -0
  49. package/dist/image.js +148 -0
  50. package/dist/image.js.map +1 -0
  51. package/dist/index.d.ts +30 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +26 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/media.d.ts +56 -0
  56. package/dist/media.d.ts.map +1 -0
  57. package/dist/media.js +133 -0
  58. package/dist/media.js.map +1 -0
  59. package/dist/model_options.d.ts +165 -0
  60. package/dist/model_options.d.ts.map +1 -0
  61. package/dist/model_options.js +59 -0
  62. package/dist/model_options.js.map +1 -0
  63. package/dist/models.d.ts +88 -0
  64. package/dist/models.d.ts.map +1 -0
  65. package/dist/models.js +55 -0
  66. package/dist/models.js.map +1 -0
  67. package/dist/sdk.umd.js +4 -0
  68. package/dist/sdk.umd.js.map +1 -0
  69. package/dist/transport.d.ts +120 -0
  70. package/dist/transport.d.ts.map +1 -0
  71. package/dist/transport.js +214 -0
  72. package/dist/transport.js.map +1 -0
  73. package/dist/types.d.ts +31 -0
  74. package/dist/types.d.ts.map +1 -0
  75. package/dist/types.js +2 -0
  76. package/dist/types.js.map +1 -0
  77. package/dist/validate.d.ts +16 -0
  78. package/dist/validate.d.ts.map +1 -0
  79. package/dist/validate.js +329 -0
  80. package/dist/validate.js.map +1 -0
  81. package/dist/video.d.ts +134 -0
  82. package/dist/video.d.ts.map +1 -0
  83. package/dist/video.js +60 -0
  84. package/dist/video.js.map +1 -0
  85. package/package.json +54 -4
  86. package/src/activity-tracker.ts +251 -0
  87. package/src/audio.ts +440 -0
  88. package/src/browser-auth.ts +1529 -0
  89. package/src/chat.ts +368 -0
  90. package/src/client.ts +250 -0
  91. package/src/computer.ts +80 -0
  92. package/src/embeddings.ts +97 -0
  93. package/src/errors.ts +29 -0
  94. package/src/event-wrap.ts +168 -0
  95. package/src/events.ts +172 -0
  96. package/src/files.ts +347 -0
  97. package/src/image.ts +339 -0
  98. package/src/index.ts +151 -0
  99. package/src/media.ts +147 -0
  100. package/src/model_options.ts +220 -0
  101. package/src/models.ts +121 -0
  102. package/src/transport.ts +292 -0
  103. package/src/types.ts +31 -0
  104. package/src/validate.ts +364 -0
  105. package/src/video.ts +215 -0
package/src/chat.ts ADDED
@@ -0,0 +1,368 @@
1
+ import type { Transport } from "./transport.js";
2
+ import type { Usage } from "./types.js";
3
+ import type { SDKEvents } from "./events.js";
4
+ import { wrapEvents, wrapStream } from "./event-wrap.js";
5
+ import { APIError } from "./errors.js";
6
+
7
+ /* ------------------------------------------------------------------ */
8
+ /* Request types */
9
+ /* ------------------------------------------------------------------ */
10
+
11
+ /**
12
+ * A single message in a chat conversation. Content is a string for plain
13
+ * text, or an array of `ContentPart` for multi-modal messages.
14
+ */
15
+ export interface ChatMessage {
16
+ role: "system" | "user" | "assistant" | "tool";
17
+ content: string | ContentPart[] | null;
18
+ name?: string;
19
+ tool_call_id?: string;
20
+ tool_calls?: ChatToolCall[];
21
+ }
22
+
23
+ /** One block inside a multi-modal message content array.
24
+ *
25
+ * Multi-modal LLMs see content as a heterogeneous list — some mix
26
+ * text + images (gpt-4o, claude), some accept video frames or raw
27
+ * audio (Gemini 2.x). The shape mirrors OpenAI's chat completions
28
+ * schema and is forwarded verbatim to the gateway; unsupported
29
+ * variants are rejected server-side with a clear error rather than
30
+ * silently ignored. */
31
+ export type ContentPart =
32
+ | { type: "text"; text: string }
33
+ | { type: "image_url"; image_url: { url: string; detail?: "auto" | "low" | "high" } }
34
+ | { type: "video_url"; video_url: { url: string } }
35
+ | { type: "audio_url"; audio_url: { url: string } };
36
+
37
+ export interface ChatTool {
38
+ type: "function";
39
+ function: {
40
+ name: string;
41
+ description?: string;
42
+ parameters?: Record<string, unknown>;
43
+ };
44
+ }
45
+
46
+ export interface StreamOptions {
47
+ include_usage: boolean;
48
+ }
49
+
50
+ /**
51
+ * Names of platform-executed tools the SDK can ask the gateway to run on
52
+ * its behalf during a chat completion. When you set `server_tools` on a
53
+ * `ChatCompletionParams`, the gateway resolves each name to its tool
54
+ * schema, sends both server tools and your own `tools` (if any) to the
55
+ * model, then handles any server-tool calls itself — looping with the
56
+ * model until it produces a final answer. Your code only sees the final
57
+ * response; you don't have to implement web search yourself.
58
+ *
59
+ * The whitelist is intentionally narrow. For image, audio, file, and
60
+ * vision generation the SDK already exposes dedicated endpoints
61
+ * (`client.image.*`, `client.audio.*`, etc.) that callers should hit
62
+ * directly instead of paying for an LLM-in-the-middle. Search-class
63
+ * tools live here because they need the model in the loop — "search →
64
+ * read result → fetch → answer" can't be expressed as a single call.
65
+ */
66
+ export type ServerToolName = "web_search" | "web_fetch" | "twitter_search";
67
+
68
+ /** Typed constants for {@link ServerToolName} so you get autocomplete. */
69
+ export const ServerTools = {
70
+ WebSearch: "web_search",
71
+ WebFetch: "web_fetch",
72
+ TwitterSearch: "twitter_search",
73
+ } as const;
74
+
75
+ export interface ChatCompletionParams {
76
+ model: string;
77
+ messages: ChatMessage[];
78
+ temperature?: number;
79
+ top_p?: number;
80
+ max_tokens?: number;
81
+ stop?: string[];
82
+ frequency_penalty?: number;
83
+ presence_penalty?: number;
84
+ n?: number;
85
+ tools?: ChatTool[];
86
+ tool_choice?: unknown;
87
+ user?: string;
88
+ metadata?: Record<string, unknown>;
89
+ stream?: boolean;
90
+ stream_options?: StreamOptions;
91
+ /**
92
+ * Names of platform-executed tools — the gateway will run these for
93
+ * you and loop with the model until it produces a final answer. Mix
94
+ * freely with your own `tools`: the model can call either; the
95
+ * platform handles the server ones, your code handles the rest.
96
+ */
97
+ server_tools?: ServerToolName[];
98
+ /**
99
+ * Maximum number of LLM round-trips the server-tool loop will make
100
+ * (default 5, hard cap 10). Only meaningful when `server_tools` is
101
+ * set. Tune up for chains like "search → fetch → answer", down for
102
+ * tighter cost guarantees.
103
+ */
104
+ max_server_iterations?: number;
105
+ /**
106
+ * One of the app's private prompts, kept out of the browser: the
107
+ * gateway reads `bundle.json (or .runapps/prompts/)<id>.md` from the app's bundle, fills
108
+ * `{{var}}` placeholders from `vars`, and prepends it as the system
109
+ * message. `template` sends the text inline instead — for a creator's
110
+ * local dev server, before a bundle exists; the gateway accepts it
111
+ * only from the app's creator.
112
+ */
113
+ prompt?: { id?: string; vars?: Record<string, unknown>; template?: string };
114
+ /** Pass-through for provider-specific knobs. */
115
+ [extra: string]: unknown;
116
+ }
117
+
118
+ /* ------------------------------------------------------------------ */
119
+ /* Response types */
120
+ /* ------------------------------------------------------------------ */
121
+
122
+ export interface ChatToolCall {
123
+ index?: number;
124
+ id?: string;
125
+ type?: string;
126
+ function: {
127
+ name: string;
128
+ arguments: string;
129
+ };
130
+ }
131
+
132
+ export interface ChatChoiceMessage {
133
+ role: string;
134
+ content: string | null;
135
+ tool_calls?: ChatToolCall[];
136
+ }
137
+
138
+ export interface ChatChoice {
139
+ index: number;
140
+ message: ChatChoiceMessage;
141
+ finish_reason: string;
142
+ }
143
+
144
+ export interface ChatCompletion {
145
+ id: string;
146
+ object: string;
147
+ created: number;
148
+ model: string;
149
+ choices: ChatChoice[];
150
+ usage: Usage;
151
+ }
152
+
153
+ export interface ChatChunkDelta {
154
+ role?: string;
155
+ content?: string;
156
+ tool_calls?: ChatToolCall[];
157
+ }
158
+
159
+ export interface ChatChunkChoice {
160
+ index: number;
161
+ delta: ChatChunkDelta;
162
+ finish_reason?: string | null;
163
+ }
164
+
165
+ export interface ChatCompletionChunk {
166
+ id: string;
167
+ object: string;
168
+ created: number;
169
+ model: string;
170
+ choices: ChatChunkChoice[];
171
+ usage?: Usage;
172
+ }
173
+
174
+ /* ------------------------------------------------------------------ */
175
+ /* Convenience builders */
176
+ /* ------------------------------------------------------------------ */
177
+
178
+ export const userMessage = (content: string): ChatMessage =>
179
+ ({ role: "user", content });
180
+
181
+ export const systemMessage = (content: string): ChatMessage =>
182
+ ({ role: "system", content });
183
+
184
+ export const assistantMessage = (content: string): ChatMessage =>
185
+ ({ role: "assistant", content });
186
+
187
+ export const toolResultMessage = (toolCallId: string, content: string): ChatMessage =>
188
+ ({ role: "tool", tool_call_id: toolCallId, content });
189
+
190
+ export const userMessageParts = (...parts: ContentPart[]): ChatMessage =>
191
+ ({ role: "user", content: parts });
192
+
193
+ export const textPart = (text: string): ContentPart => ({ type: "text", text });
194
+
195
+ export const imagePart = (
196
+ url: string,
197
+ detail?: "auto" | "low" | "high",
198
+ ): ContentPart => ({
199
+ type: "image_url",
200
+ image_url: detail ? { url, detail } : { url },
201
+ });
202
+
203
+ /**
204
+ * Video content part. Only supported by chat models whose
205
+ * input_modalities includes `"video"` (currently Gemini 3.x). Other
206
+ * models 400 at the gateway. URL may be http(s) or a
207
+ * `data:video/mp4;base64,...` inline upload.
208
+ */
209
+ export const videoPart = (url: string): ContentPart => ({
210
+ type: "video_url",
211
+ video_url: { url },
212
+ });
213
+
214
+ /**
215
+ * Audio content part. Only supported by chat models whose
216
+ * input_modalities includes `"audio"` (currently Gemini 3.x). Other
217
+ * models 400 at the gateway. URL may be http(s) or a
218
+ * `data:audio/wav;base64,...` inline upload.
219
+ */
220
+ export const audioPart = (url: string): ContentPart => ({
221
+ type: "audio_url",
222
+ audio_url: { url },
223
+ });
224
+
225
+ /* ------------------------------------------------------------------ */
226
+ /* Service */
227
+ /* ------------------------------------------------------------------ */
228
+
229
+ export class ChatService {
230
+ constructor(
231
+ private readonly transport: Transport,
232
+ private readonly events: SDKEvents,
233
+ ) {}
234
+
235
+ /**
236
+ * Create a chat completion (non-streaming).
237
+ *
238
+ * Streaming is forced off — pass `stream()` instead for incremental output.
239
+ */
240
+ async create(
241
+ params: ChatCompletionParams,
242
+ init?: { signal?: AbortSignal },
243
+ ): Promise<ChatCompletion> {
244
+ const body = { ...params, stream: false };
245
+ return wrapEvents(
246
+ this.events,
247
+ { model: params.model, capability: "text" },
248
+ () =>
249
+ this.transport.postJSON<ChatCompletion>(
250
+ "/v1/chat/completions",
251
+ body,
252
+ init,
253
+ ),
254
+ (r) => ({
255
+ totalTokens: r.usage?.completion_tokens,
256
+ costUSD: r.usage?.total_cost,
257
+ finishReason: r.choices?.[0]?.finish_reason,
258
+ }),
259
+ );
260
+ }
261
+
262
+ /**
263
+ * Create a streaming chat completion. Returns an async iterator over
264
+ * `ChatCompletionChunk`. The final chunk carries `usage` (token / cost
265
+ * totals) — `stream_options.include_usage` is set automatically.
266
+ *
267
+ * ```ts
268
+ * let cost = 0;
269
+ * for await (const chunk of client.chat.stream({ ... })) {
270
+ * for (const c of chunk.choices) process.stdout.write(c.delta.content ?? "");
271
+ * if (chunk.usage) cost = chunk.usage.total_cost;
272
+ * }
273
+ * ```
274
+ *
275
+ * Also fires `request:streamDelta` on `client.events` for each chunk
276
+ * that carried text — drives the badge's tokens/sec rate display.
277
+ */
278
+ async *stream(
279
+ params: ChatCompletionParams,
280
+ init?: { signal?: AbortSignal },
281
+ ): AsyncGenerator<ChatCompletionChunk, void, unknown> {
282
+ const body = {
283
+ ...params,
284
+ stream: true,
285
+ stream_options: params.stream_options ?? { include_usage: true },
286
+ };
287
+ const sourceGen = async function* (this: ChatService) {
288
+ const resp = await this.transport.postJSONStream(
289
+ "/v1/chat/completions",
290
+ body,
291
+ init,
292
+ );
293
+ if (!resp.body) {
294
+ throw new Error("runapps: streaming response had no body");
295
+ }
296
+ yield* parseSSE<ChatCompletionChunk>(resp.body);
297
+ }.bind(this);
298
+ yield* wrapStream(
299
+ this.events,
300
+ { model: params.model, capability: "text" },
301
+ sourceGen,
302
+ (chunk) => ({
303
+ deltaText: chunk.choices?.[0]?.delta?.content ?? "",
304
+ completionTokens: chunk.usage?.completion_tokens,
305
+ costUSD: chunk.usage?.total_cost,
306
+ finishReason: chunk.choices?.[0]?.finish_reason ?? undefined,
307
+ }),
308
+ );
309
+ }
310
+ }
311
+
312
+ /* ------------------------------------------------------------------ */
313
+ /* SSE parsing */
314
+ /* ------------------------------------------------------------------ */
315
+
316
+ /**
317
+ * Decode an `text/event-stream` body into a stream of parsed JSON chunks.
318
+ * Stops at the `[DONE]` sentinel. Lines that don't start with `data: ` are
319
+ * ignored (keep-alive comments, etc.).
320
+ */
321
+ async function* parseSSE<T>(
322
+ body: ReadableStream<Uint8Array>,
323
+ ): AsyncGenerator<T, void, unknown> {
324
+ const decoder = new TextDecoder();
325
+ const reader = body.getReader();
326
+ let buffer = "";
327
+ try {
328
+ while (true) {
329
+ const { value, done } = await reader.read();
330
+ if (done) break;
331
+ buffer += decoder.decode(value, { stream: true });
332
+
333
+ let nl: number;
334
+ while ((nl = buffer.indexOf("\n")) !== -1) {
335
+ const line = buffer.slice(0, nl).replace(/\r$/, "");
336
+ buffer = buffer.slice(nl + 1);
337
+ if (!line.startsWith("data: ")) continue;
338
+ const data = line.slice(6).trim();
339
+ if (data === "[DONE]") return;
340
+ if (!data) continue;
341
+ let parsed: unknown;
342
+ try {
343
+ parsed = JSON.parse(data);
344
+ } catch (e) {
345
+ throw new Error(
346
+ `runapps: decode stream chunk: ${(e as Error).message}`,
347
+ );
348
+ }
349
+ // The gateway reports mid-stream failures as an SSE error event
350
+ // (`data: {"error":{...}}`) followed by [DONE]. Such an event has no
351
+ // "choices", so without this it would surface as an empty chunk and
352
+ // the stream would end looking merely empty.
353
+ const err = (parsed as { error?: { code?: number; type?: string; message?: string } })
354
+ .error;
355
+ if (err) {
356
+ throw new APIError(
357
+ err.code ?? 0,
358
+ err.type ?? "upstream_error",
359
+ err.message ?? data,
360
+ );
361
+ }
362
+ yield parsed as T;
363
+ }
364
+ }
365
+ } finally {
366
+ reader.releaseLock();
367
+ }
368
+ }
package/src/client.ts ADDED
@@ -0,0 +1,250 @@
1
+ import { Transport } from "./transport.js";
2
+ import { ChatService } from "./chat.js";
3
+ import { ModelsService } from "./models.js";
4
+ import { ImageService } from "./image.js";
5
+ import { AudioService } from "./audio.js";
6
+ import { VideoService } from "./video.js";
7
+ import { ComputerService } from "./computer.js";
8
+ import { FilesService } from "./files.js";
9
+ import { EmbeddingsService } from "./embeddings.js";
10
+ import { BrowserAuth, type BrowserUser } from "./browser-auth.js";
11
+ import { SDKEvents } from "./events.js";
12
+
13
+ // The platform, not the bare gateway: personal rk_ keys are checked and
14
+ // billed by www.runapps.ai, which forwards to the gateway. api.runapps.ai
15
+ // only knows the gateway's own keys.
16
+ const DEFAULT_BASE_URL = "https://www.runapps.ai";
17
+
18
+ /**
19
+ * Auth strategy.
20
+ *
21
+ * - `"static"` (default): use `apiKey` or `apiKeyResolver` directly.
22
+ * - `"runapps"`: opt into the built-in browser auth flow that
23
+ * handshakes with `https://www.runapps.ai/api/sdk/grant`. No
24
+ * external auth.js script needed; the SDK consumes the URL
25
+ * fragment after the redirect-back, exposes `client.user` and
26
+ * `client.signIn()`, and renders an identity badge. Only useful
27
+ * in browsers — in Node this falls back to throwing the same
28
+ * "missing apiKey" error you'd get with no auth config.
29
+ */
30
+ // "runapps" is the current name of the platform sign-in; "runjobs" is the
31
+ // same thing under the name from before the rename.
32
+ export type AuthProvider = "static" | "runapps" | "runjobs";
33
+
34
+ export interface ClientOptions {
35
+ /** Static gateway API key (typically prefixed `gw-` / `rj_` / `rrt_`).
36
+ * Required unless `apiKeyResolver` or `authProvider: "runapps"` is supplied. */
37
+ apiKey?: string;
38
+ /**
39
+ * Dynamic API key resolver. See `AuthProvider` for the typical
40
+ * runapps.ai browser-bundle wiring.
41
+ *
42
+ * Wins over `apiKey` when both are supplied.
43
+ */
44
+ apiKeyResolver?: () => string | Promise<string>;
45
+ /**
46
+ * Auth strategy — defaults to `"static"`. Pass `"runapps"` to
47
+ * activate the built-in browser auth flow against runapps.ai;
48
+ * baseURL defaults to `https://www.runapps.ai` in that mode.
49
+ */
50
+ authProvider?: AuthProvider;
51
+ /**
52
+ * Show the floating identity badge in `runjobs` auth mode.
53
+ * **Default `true`** — the badge is now a desktop-ball-style
54
+ * real-time activity indicator (LED dot + progress ring around
55
+ * the avatar + click-through popover with active calls / recent
56
+ * completions / session totals). Set `false` only when the
57
+ * bundle ships its own status UI and you don't want the platform
58
+ * badge in the corner.
59
+ */
60
+ showIdentityBadge?: boolean;
61
+ /**
62
+ * Which corner the identity badge floats in. Defaults to
63
+ * `"bottom-right"` — the canonical tray-app position. Switch to
64
+ * `"bottom-left"` when the bundle's own UI occupies the right
65
+ * gutter (e.g. a chat dock); the top corners are useful for
66
+ * fullscreen / portrait kiosk layouts.
67
+ *
68
+ * The popover anchors itself to the same corner — bottom-left
69
+ * badge → popover opens up + right of the badge; top-* badges →
70
+ * popover opens down. Each corner only nudges the CSS; the badge
71
+ * + popover content is identical.
72
+ */
73
+ badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
74
+ /**
75
+ * @deprecated Use `showIdentityBadge` instead. Kept for back-compat;
76
+ * acts as `showIdentityBadge: !hideIdentityBadge` when set.
77
+ */
78
+ hideIdentityBadge?: boolean;
79
+ /**
80
+ * Pin the runapps.ai grant flow to a specific project (only used
81
+ * when `authProvider: "runapps"`). When set, `/api/sdk/grant`
82
+ * receives `project_id=<value>` and mints an `rrt_*` resource
83
+ * token bound to THAT project — required for `client.files.*`
84
+ * calls from any origin that isn't a registered (origin, app)
85
+ * pair (e.g. a bundle running on `localhost:5173` during dev).
86
+ *
87
+ * Production bundles served from a registered origin can omit
88
+ * this; the gateway derives the project from the (origin, app)
89
+ * pair.
90
+ */
91
+ project?: string;
92
+ /** Override the default base URL, `https://www.runapps.ai` (the
93
+ * platform: personal keys and app tokens are both accepted there). */
94
+ baseURL?: string;
95
+ /** Optional fetch override (e.g. node-fetch with custom agent). */
96
+ fetch?: typeof fetch;
97
+ }
98
+
99
+ /**
100
+ * Top-level RunApps SDK client. Construct once, share across services.
101
+ *
102
+ * Browser bundle (resource projects on runapps.ai):
103
+ *
104
+ * ```html
105
+ * <script src="https://cdn.jsdelivr.net/npm/@runappsai/sdk/dist/sdk.umd.js"></script>
106
+ * <script>
107
+ * const client = new RunApps.Client({ authProvider: "runapps" });
108
+ * const res = await client.chat.create({
109
+ * model: "gpt-4o-mini",
110
+ * messages: [{ role: "user", content: "hello" }],
111
+ * });
112
+ * </script>
113
+ * ```
114
+ *
115
+ * Node / server-side:
116
+ *
117
+ * ```ts
118
+ * import { RunApps } from "@runappsai/sdk";
119
+ * const client = new RunApps({ apiKey: process.env.RUNJOBS_API_KEY! });
120
+ * ```
121
+ */
122
+ export class RunApps {
123
+ readonly chat: ChatService;
124
+ readonly models: ModelsService;
125
+ readonly image: ImageService;
126
+ readonly audio: AudioService;
127
+ readonly video: VideoService;
128
+ readonly computer: ComputerService;
129
+ readonly files: FilesService;
130
+ readonly embeddings: EmbeddingsService;
131
+
132
+ /**
133
+ * Browser auth helper, populated only when `authProvider: "runapps"`.
134
+ * Use `client.signIn()` and `client.user` for the common cases; the
135
+ * full instance is exposed for advanced flows (manual `onTokenChange`
136
+ * subscription, etc.).
137
+ */
138
+ readonly auth: BrowserAuth | null = null;
139
+
140
+ /**
141
+ * Runtime event bus for SDK-call telemetry. Every LLM-ish service
142
+ * (chat / embeddings / image / audio / video / computer) fires
143
+ * `request:start`, `request:streamDelta`, `request:end`, and
144
+ * `request:error` events here so UI overlays — like the bottom-right
145
+ * identity badge's activity ring — can render real-time state
146
+ * without business code threading anything through.
147
+ *
148
+ * Zero-cost when nobody subscribes (emit is a few property reads).
149
+ * Subscribe via `client.events.on("request:start", handler)` — the
150
+ * returned closure is the unsubscribe.
151
+ */
152
+ readonly events: SDKEvents = new SDKEvents();
153
+
154
+ constructor(options: ClientOptions = {}) {
155
+ const provider = options.authProvider ?? "static";
156
+ let apiKeyResolver = options.apiKeyResolver;
157
+ let baseURL = options.baseURL;
158
+ let onUnauthorized: (() => void) | undefined;
159
+
160
+ if (provider === "runapps" || provider === "runjobs") {
161
+ // The grant page (/api/sdk/grant) and the gateway both live on
162
+ // www.runapps.ai. An explicit baseURL keeps control.
163
+ baseURL = baseURL ?? "https://www.runapps.ai";
164
+ // Badge default: SHOWN. The badge is now a real-time activity
165
+ // indicator (LED + ring + popover) — useful enough that we'd
166
+ // rather have the rare "I already have my own UI" bundle opt
167
+ // OUT with `showIdentityBadge: false` than have every bundle
168
+ // miss the live feedback by default. The legacy
169
+ // `hideIdentityBadge` flag still wins when set, for callers
170
+ // that explicitly suppressed the badge under the old default.
171
+ const showBadge =
172
+ options.showIdentityBadge !== undefined
173
+ ? options.showIdentityBadge
174
+ : options.hideIdentityBadge !== undefined
175
+ ? !options.hideIdentityBadge
176
+ : true;
177
+ const auth = new BrowserAuth({
178
+ origin: baseURL,
179
+ hideBadge: !showBadge,
180
+ // Pipe the shared event bus into BrowserAuth so the badge can
181
+ // render its real-time activity ring / LED / popover. Safe to
182
+ // pass even when the caller never subscribes — emit is a few
183
+ // property reads when nobody's listening.
184
+ events: this.events,
185
+ ...(options.badgePosition !== undefined && { badgePosition: options.badgePosition }),
186
+ ...(options.project !== undefined && { project: options.project }),
187
+ });
188
+ (this as { auth: BrowserAuth | null }).auth = auth;
189
+ // The browser auth resolver wins over any caller-supplied one
190
+ // because the auth flow owns token issuance + refresh from now on.
191
+ apiKeyResolver = auth.getToken;
192
+ // 401 from the gateway → token was revoked server-side (typically
193
+ // because the user unsubscribed in another tab). Drop the cached
194
+ // token so the transport's auto-retry hits a fresh signIn() flow.
195
+ onUnauthorized = () => auth.invalidate();
196
+ }
197
+
198
+ if (!options.apiKey && !apiKeyResolver) {
199
+ throw new Error(
200
+ "runapps: pass either `apiKey`, `apiKeyResolver`, or `authProvider: \"runapps\"`",
201
+ );
202
+ }
203
+ const transport = new Transport({
204
+ baseURL: baseURL ?? DEFAULT_BASE_URL,
205
+ ...(options.apiKey !== undefined && { apiKey: options.apiKey }),
206
+ ...(apiKeyResolver !== undefined && { apiKeyResolver }),
207
+ ...(onUnauthorized !== undefined && { onUnauthorized }),
208
+ ...(options.fetch ? { fetchImpl: options.fetch } : {}),
209
+ });
210
+ // Service constructors receive the shared event bus so each call
211
+ // path (chat/embeddings/image/audio/video/computer) can emit
212
+ // start/end/error around its network operations. Files and Models
213
+ // intentionally don't take it — they're infrastructure, not
214
+ // user-facing LLM calls, and surfacing them would clutter the
215
+ // badge with /v1/models polls.
216
+ this.chat = new ChatService(transport, this.events);
217
+ this.models = new ModelsService(transport);
218
+ this.image = new ImageService(transport, this.events);
219
+ this.audio = new AudioService(transport, this.events);
220
+ this.video = new VideoService(transport, this.events);
221
+ this.computer = new ComputerService(transport, this.events);
222
+ // `project` also scopes the file URLs (/v1/p/<project>/files/...),
223
+ // so two apps never issue byte-identical file requests. Omitted →
224
+ // legacy unscoped /v1/files/... paths.
225
+ this.files = new FilesService(transport, options.project);
226
+ this.embeddings = new EmbeddingsService(transport, this.events);
227
+ }
228
+
229
+ /** Force a redirect to the runapps.ai grant page. No-op in static
230
+ * auth mode or in Node. */
231
+ signIn(): void {
232
+ this.auth?.signIn();
233
+ }
234
+
235
+ /**
236
+ * Clear the cached token + identity from memory and localStorage.
237
+ * The next API call will trigger sign-in again (redirect to grant
238
+ * page). Wire this to a "Sign out" button in your settings UI.
239
+ *
240
+ * No-op in static auth mode or in Node.
241
+ */
242
+ signOut(): void {
243
+ this.auth?.signOut();
244
+ }
245
+
246
+ /** Currently-authenticated user (runapps auth mode), or null. */
247
+ get user(): BrowserUser | null {
248
+ return this.auth?.user ?? null;
249
+ }
250
+ }
@@ -0,0 +1,80 @@
1
+ import type { Transport } from "./transport.js";
2
+ import type { Usage } from "./types.js";
3
+ import type { SDKEvents } from "./events.js";
4
+ import { wrapEvents } from "./event-wrap.js";
5
+
6
+ /** Single-step computer-use request — a screenshot + history → next action(s). */
7
+ export interface ComputerStepParams {
8
+ /**
9
+ * Conversation history including screenshots and tool results. Each
10
+ * entry has `role` + `content` (string or array of blocks). Format is
11
+ * intentionally opaque to support both Anthropic and OpenAI computer-use
12
+ * protocols — the gateway adapts.
13
+ */
14
+ messages: Record<string, unknown>[];
15
+ /** Screen resolution. Defaults to 1920×1080 if omitted. */
16
+ display_width?: number;
17
+ display_height?: number;
18
+ max_tokens?: number;
19
+ enable_zoom?: boolean;
20
+ /** OpenAI Responses-API state chaining. */
21
+ previous_response_id?: string;
22
+ /** Follow-up `computer_call_output` for OpenAI's Responses API. */
23
+ openai_input?: unknown;
24
+ user?: string;
25
+ }
26
+
27
+ /** One block of a computer-use response: text, tool_use (Anthropic), or computer_call (OpenAI). */
28
+ export interface ComputerContentBlock {
29
+ type: string;
30
+ text?: string;
31
+ id?: string;
32
+ name?: string;
33
+ /** Anthropic tool input (raw JSON). */
34
+ input?: unknown;
35
+ call_id?: string;
36
+ /** OpenAI computer-call action. */
37
+ action?: Record<string, unknown>;
38
+ }
39
+
40
+ export interface ComputerResponse {
41
+ content: ComputerContentBlock[];
42
+ stop_reason?: string;
43
+ usage: Usage;
44
+ response_id?: string;
45
+ /** "anthropic" | "openai" — which protocol the upstream returned. */
46
+ protocol?: string;
47
+ }
48
+
49
+ export class ComputerService {
50
+ constructor(
51
+ private readonly transport: Transport,
52
+ private readonly events: SDKEvents,
53
+ ) {}
54
+
55
+ /**
56
+ * Execute one step of a computer-use agent loop. Given a screenshot
57
+ * and conversation history, returns the next action(s) the model
58
+ * wants the caller to execute.
59
+ */
60
+ async step(
61
+ model: string,
62
+ params: ComputerStepParams,
63
+ init?: { signal?: AbortSignal },
64
+ ): Promise<ComputerResponse> {
65
+ return wrapEvents(
66
+ this.events,
67
+ { model, capability: "computer_use" },
68
+ () =>
69
+ this.transport.postJSON<ComputerResponse>(
70
+ "/v1/computer/step",
71
+ { model, ...params },
72
+ init,
73
+ ),
74
+ (r) => ({
75
+ totalTokens: r.usage?.completion_tokens,
76
+ costUSD: r.usage?.total_cost,
77
+ }),
78
+ );
79
+ }
80
+ }