@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RunJobs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,275 @@
1
- # Temporary Holding Version
1
+ # RunApps SDK for JavaScript / TypeScript
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The client for building apps on [RunApps](https://www.runapps.ai): call any model (Claude, GPT, Gemini, DeepSeek, Qwen, MiniMax, GLM, Grok), generate images, audio and video, keep per-user files, and let the platform handle sign-in and billing. Zero runtime dependencies — native `fetch`, `FormData` and `ReadableStream` (browsers, Node 18+, Deno, Bun).
4
+
5
+ Two ways to use it:
6
+
7
+ - **In an app you publish on RunApps** — a static bundle served at `<slug>.runapps.dev`. Users sign in with their RunApps account and pay for their own usage; you never hold a key. This is what `authProvider: "runapps"` is for.
8
+ - **From your own code** — server, script or CLI — with a personal API key (`rk_…`).
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @runappsai/sdk
14
+ ```
15
+
16
+ Or as a script tag:
17
+
18
+ ```html
19
+ <script src="https://cdn.jsdelivr.net/npm/@runappsai/sdk/dist/sdk.umd.js"></script>
20
+ <script>
21
+ const client = new RunApps({ authProvider: "runapps" });
22
+ </script>
23
+ ```
24
+
25
+ ## Building an app
26
+
27
+ ### 1. Create the app, then develop against it
28
+
29
+ An app gets its slug the moment you create it on the dashboard (Creator → Publish) — no zip needed yet. Pass that slug as `project` and your dev server runs against the real platform:
30
+
31
+ ```ts
32
+ import { RunApps } from "@runappsai/sdk";
33
+
34
+ const client = new RunApps({
35
+ authProvider: "runapps", // sign-in, token, refresh — all handled
36
+ project: "my-tool", // the draft's slug; unchanged after you upload
37
+ });
38
+
39
+ const resp = await client.chat.create({
40
+ model: "Claude Sonnet 4.6",
41
+ messages: [{ role: "user", content: "Hello!" }],
42
+ });
43
+ console.log(resp.choices[0].message.content);
44
+ ```
45
+
46
+ - Sign-in works from `http://localhost:<any port>`; the first call redirects to the RunApps grant page and back.
47
+ - Calls are billed to the signed-in user — while developing, that is you.
48
+ - Files and private prompts already use the app's real namespace, so nothing moves when you upload.
49
+
50
+ When the app is ready, build it, zip it, and upload it to the same app. Set it private (reachable by link) or submit it for review to be listed in the library.
51
+
52
+ ### 2. Sign-in details
53
+
54
+ ```ts
55
+ await client.chat.create({ ... }); // redirects to the grant page on first use
56
+
57
+ client.signIn(); // force the redirect
58
+ client.signOut(); // clear the cached token (sticky across reloads)
59
+ client.user; // { id, name } | null
60
+ client.auth?.onTokenChange(t => {}); // subscribe to (re)acquisition
61
+ client.auth?.hasFreshToken(); // avoid showing a "sign in" button needlessly
62
+ ```
63
+
64
+ `showIdentityBadge` (default `true`) draws a small activity badge; `badgePosition` places it.
65
+
66
+ `project` is optional on `*.runapps.dev` — the origin already says which app it is. It is required on localhost and when the client runs on a personal key (see below). Sign-in only works on the app's own `*.runapps.dev` address and on `http://localhost`. In Node, `authProvider: "runapps"` is a no-op; use `apiKey`.
67
+
68
+ ### 3. Private prompts
69
+
70
+ Anything in your frontend can be read in the browser. Name your prompt files in a `bundle.json` at the root of your zip — `{"prompts": {"outline": "prompts/outline.md"}}` — or put them under `.runapps/prompts/<id>.md`. The platform never serves `bundle.json`, the files it names, or any dot folder; call the prompts by id:
71
+
72
+ ```ts
73
+ await client.chat.create({
74
+ model: "Claude Sonnet 4.6",
75
+ messages: [{ role: "user", content: "A detective in a lighthouse." }],
76
+ prompt: { id: "outline", vars: { genre: "mystery" } }, // {{genre}} in the file
77
+ });
78
+ ```
79
+
80
+ The gateway prepends the prompt as the system message and asks the model to keep it confidential; a reply that repeats 30+ characters of it verbatim is withheld (`finish_reason: "content_filter"`). While developing locally, before a bundle exists, send the text inline as `prompt.template` — accepted only from the app's creator — and keep it out of production builds with `import.meta.env.DEV`.
81
+
82
+ ### 4. Per-user files
83
+
84
+ Every app gets a file space per signed-in user, addressed by POSIX-style paths. `FileObject.url` is a stable public address you can drop into `<img src>`.
85
+
86
+ ```ts
87
+ // Write. body: Blob | File | ArrayBuffer | Uint8Array | ReadableStream | string
88
+ const obj = await client.files.put("assets/logo.png", pngBlob, { contentType: "image/png" });
89
+ await client.files.putString("notes.md", "# hello");
90
+ await client.files.putFromURL("cache/cat.jpg", "https://example.com/cat.jpg"); // gateway fetches it
91
+
92
+ // Read.
93
+ const blob = await client.files.get("assets/logo.png");
94
+ const meta = await client.files.stat("assets/logo.png"); // size, etag, last_modified, url
95
+ await client.files.exists("assets/logo.png");
96
+
97
+ // List, with an optional shell glob (*, **, ?, [abc]) matched against the whole path.
98
+ const page = await client.files.list({ prefix: "assets/", glob: "**/*.png", limit: 100 });
99
+ // page.next_cursor → pass as cursor for the next page
100
+
101
+ // Mutate.
102
+ await client.files.move("draft.md", "published.md");
103
+ await client.files.copy("template.md", "instances/today.md");
104
+ await client.files.del("old.log");
105
+ await client.files.deleteMany({ prefix: "tmp/" });
106
+ await client.files.deleteMany({ glob: "**/*.tmp" });
107
+
108
+ // Several ops in one round trip; each result has its own ok/error.
109
+ await client.files.batch([
110
+ { op: "put_url", path: "a.jpg", src_url: "https://example.com/a.jpg" },
111
+ { op: "copy", from: "a.jpg", to: "b.jpg" },
112
+ { op: "del", path: "a.jpg" },
113
+ ]);
114
+ ```
115
+
116
+ **Keeping app state in a file** — read, modify, write, without overwriting another tab's save. Every read carries the file's ETag; `ifMatch` makes the write fail (412) if the file changed since:
117
+
118
+ ```ts
119
+ const { value, etag } = await client.files.readJSON<{ score: number }>("state.json");
120
+ const next = { ...(value ?? { score: 0 }), score: (value?.score ?? 0) + 1 };
121
+ try {
122
+ await client.files.put("state.json", JSON.stringify(next), {
123
+ contentType: "application/json",
124
+ ...(etag ? { ifMatch: etag } : { ifNoneMatch: true }), // first save: must not exist
125
+ });
126
+ } catch (e) {
127
+ if (e instanceof APIError && e.statusCode === 412) { /* re-read, merge, retry */ }
128
+ }
129
+ ```
130
+
131
+ `files.read(path)` is `get` plus the ETag for non-JSON files. `ifNoneMatch: true` refuses to overwrite (409).
132
+
133
+ ## From your own code
134
+
135
+ ```ts
136
+ import { RunApps, APIError } from "@runappsai/sdk";
137
+
138
+ const client = new RunApps({ apiKey: "rk_…" }); // Dashboard → API Keys
139
+
140
+ const resp = await client.chat.create({
141
+ model: "Gemini 3 Flash",
142
+ messages: [{ role: "user", content: "Hello!" }],
143
+ });
144
+ console.log(resp.usage.total_cost); // USD
145
+ ```
146
+
147
+ The base URL is `https://www.runapps.ai` in both modes; `baseURL` overrides it.
148
+
149
+ **Files with a personal key** — a personal key has no app of its own, so name the app: `new RunApps({ apiKey, project: "my-tool" })`. You get your own files in that app, the same ones the app sees for you, provided you have added the app or created it.
150
+
151
+ **Any OpenAI-compatible SDK works too.** Point it at the platform with your `rk_…` key:
152
+
153
+ ```python
154
+ from openai import OpenAI
155
+ client = OpenAI(api_key="rk_...", base_url="https://www.runapps.ai/v1")
156
+ client.chat.completions.create(model="Claude Sonnet 4.6", messages=[{"role": "user", "content": "Hello!"}])
157
+ ```
158
+
159
+ Browse `GET /v1/models` (no auth) for the live catalogue and per-model prices.
160
+
161
+ ## Chat
162
+
163
+ OpenAI-compatible, streaming or not. The final streamed chunk carries `usage`.
164
+
165
+ ```ts
166
+ for await (const chunk of client.chat.stream({ model: "Gemini 3 Flash", messages })) {
167
+ for (const c of chunk.choices) process.stdout.write(c.delta.content ?? "");
168
+ if (chunk.usage) console.log("\ncost", chunk.usage.total_cost);
169
+ }
170
+ ```
171
+
172
+ **Images, video and audio in messages** — check the model first with `acceptsModality(model, "video")`; the gateway rejects unsupported parts with a 400 before billing:
173
+
174
+ ```ts
175
+ import { userMessageParts, textPart, imagePart, videoPart, audioPart } from "@runappsai/sdk";
176
+
177
+ await client.chat.create({
178
+ model: "Gemini 3 Vision",
179
+ messages: [userMessageParts(textPart("Summarise this clip"), videoPart("https://example.com/clip.mp4"))],
180
+ });
181
+ ```
182
+
183
+ **Your own tools** — pass `tools: [{ type: "function", function: { name, description, parameters } }]`, read `choices[0].message.tool_calls`, answer with `toolResultMessage(toolCallId, output)` on the next turn.
184
+
185
+ **Server tools** — the platform searches and reads the web for the model, looping until it answers; your code only sees the result. Combine freely with your own `tools`.
186
+
187
+ ```ts
188
+ import { ServerTools } from "@runappsai/sdk";
189
+
190
+ const resp = await client.chat.create({
191
+ model: "Claude Sonnet 4.6",
192
+ messages: [{ role: "user", content: "What is the latest stable Go version?" }],
193
+ server_tools: [ServerTools.WebSearch, ServerTools.WebFetch],
194
+ max_server_iterations: 5, // default 5, max 10
195
+ });
196
+ for (const t of resp.usage.tool_costs ?? []) console.log(t.name, t.count, t.cost);
197
+ ```
198
+
199
+ `WebSearch` and `TwitterSearch` are priced per call, `WebFetch` is free; `usage.total_cost` includes tools and tokens. With `stream: true` the tool rounds run silently and the final answer streams.
200
+
201
+ Message builders: `userMessage`, `systemMessage`, `assistantMessage`, `toolResultMessage`, `userMessageParts`, `textPart`, `imagePart`, `videoPart`, `audioPart`.
202
+
203
+ ## Models
204
+
205
+ ```ts
206
+ const all = await client.models.list();
207
+ const video = await client.models.list({ capability: "video_generation" });
208
+ ```
209
+
210
+ Prices are in pips per million tokens (1 USD = 1,000,000 pips). Each model carries `capability_tags` (stable ids such as `t2v`, `i2v`, `voice_clone`, `timestamps` — filter with `hasCapabilityTag`), chat models carry `input_modalities` (`acceptsModality`), and every model exposes its input contract through `getOptionsSchema(model)`: accepted fields, bounds, enums, cross-field constraints, and a catalogue of voices or emotions. `validateRequest(schema, body)` returns every problem at once for a form to show.
211
+
212
+ ## Image, audio, video, embeddings
213
+
214
+ ```ts
215
+ // Image — data: URI (sync) or hosted blob URL (async); decodeMediaUrl() gives bytes.
216
+ const img = await client.image.generate("MiniMax Image-01", {
217
+ prompt: "a developer at a laptop, anime style",
218
+ resolution: "2K", aspect_ratio: "16:9", // or size: "WxH"
219
+ reference_image_urls: ["https://…"],
220
+ });
221
+ await client.image.edit("GPT Image", { image: { data: bytes, filename: "photo.png" }, prompt: "add a party hat" });
222
+ // generateAsync / submitGenerate + getAsyncStatus for jobs longer than ~100 s
223
+
224
+ // Speech.
225
+ const speech = await client.audio.speech("MiniMax Speech 2.6 HD", { input: "Hello", voice: "English_radiant_girl", emotion: "happy" });
226
+ const text = await client.audio.transcribe("OpenAI/Whisper", { file: { data: audioBytes, filename: "a.mp3" } });
227
+ // speechAsync / transcribeAsync for long jobs; voices and emotions are in getOptionsSchema(model).catalog
228
+
229
+ // Video — always async.
230
+ const task = await client.video.generate("MiniMax Hailuo 2.3", { prompt: "a gentle ocean wave", duration: 5 });
231
+ const status = await client.video.wait(task.id, { pollIntervalMs: 5000 });
232
+ if (status.status === "succeeded") await client.video.getContent(task.id);
233
+
234
+ // Embeddings.
235
+ const e = await client.embeddings.create("text-embedding-3-small", { input: ["alpha", "beta"] });
236
+ ```
237
+
238
+ `VideoGenerateParams` covers keyframes (`first_frame_url` / `last_frame_url`), reference inputs, source video/image/audio drivers, `service_tier: "flex"`, and more; every `*_url` accepts `https://` or a `data:` URI (`encodeImageUrl(bytes)`).
239
+
240
+ ## Errors and cancellation
241
+
242
+ ```ts
243
+ try {
244
+ await client.chat.create({ ... }, { signal: controller.signal });
245
+ } catch (e) {
246
+ if (e instanceof APIError) console.error(e.statusCode, e.type, e.message);
247
+ else throw e; // network errors are the fetch rejection
248
+ }
249
+ ```
250
+
251
+ Every call takes `{ signal }`. In browser-auth mode a 401 is retried once after refreshing the token.
252
+
253
+ ## API reference
254
+
255
+ | Service | Methods |
256
+ |---|---|
257
+ | `client.chat` | `create`, `stream` |
258
+ | `client.models` | `list` |
259
+ | `client.files` | `put`, `putString`, `putFromURL`, `get`, `read`, `readJSON`, `stat`, `exists`, `list`, `del`, `deleteMany`, `move`, `copy`, `batch` |
260
+ | `client.image` | `generate`, `edit`, `generateAsync`, `submitGenerate`, `getAsyncStatus` |
261
+ | `client.audio` | `speech`, `speechAsync`, `transcribe`, `transcribeAsync` |
262
+ | `client.video` | `generate`, `getStatus`, `wait`, `getContent` |
263
+ | `client.embeddings` | `create` |
264
+
265
+ Browser auth: `client.signIn()`, `client.signOut()`, `client.user`, `client.auth`.
266
+
267
+ Helpers: `hasCapabilityTag`, `acceptsModality`, `getOptionsSchema`, `acceptsField`, `requiresField`, `allowedValuesFor`, `validateRequest`, `supportsVoiceClone`, `supportsInstructText`, `defaultVoice`, `encodeImageUrl`, `decodeMediaUrl`, the message builders, `ServerTools`.
268
+
269
+ ## Compatibility
270
+
271
+ Browsers (any modern), Node 18+, Deno, Bun. Never ship an `rk_…` key to a browser — an app published on RunApps uses `authProvider: "runapps"` instead, and the user pays for their own usage.
272
+
273
+ ## License
274
+
275
+ MIT
@@ -0,0 +1,113 @@
1
+ /**
2
+ * In-memory session-scoped activity index — turns the raw event
3
+ * stream from `SDKEvents` into the snapshot the badge UI reads each
4
+ * frame: which calls are in flight, what just finished, what the
5
+ * running session cost is.
6
+ *
7
+ * Memory bounded:
8
+ * - `active` map shrinks on every end/error (calls don't accumulate
9
+ * past their settle event).
10
+ * - `recent` is a bounded ring of the last RECENT_MAX completions.
11
+ * - tokens/sec windows hold only `RATE_WINDOW_MS` worth of deltas;
12
+ * stale samples get dropped on every push.
13
+ *
14
+ * Snapshot pattern (vs. emitting another event per change): the
15
+ * badge re-renders on `requestAnimationFrame` while it's open, so
16
+ * a pull-snapshot API matches that cadence — pushing on every
17
+ * delta would over-paint and cost battery on token-heavy streams.
18
+ */
19
+ import type { SDKCapability, SDKEvents } from "./events.js";
20
+ /** One LLM call currently in flight. */
21
+ export interface ActiveCall {
22
+ id: string;
23
+ model: string;
24
+ capability: SDKCapability;
25
+ startedAt: number;
26
+ /** True for stream:true. Drives the pulsing-ring animation. */
27
+ streaming: boolean;
28
+ /** Cumulative completion tokens (0 until first delta). */
29
+ tokensSoFar: number;
30
+ /** Tokens per second over the last RATE_WINDOW_MS. 0 until the
31
+ * window has 2+ samples. */
32
+ tokensPerSec: number;
33
+ /** Internal sample buffer — exposed for tests; UI ignores. */
34
+ rateSamples: Array<{
35
+ t: number;
36
+ tokens: number;
37
+ }>;
38
+ }
39
+ /** One LLM call that already finished (success OR failure). */
40
+ export interface CompletedCall {
41
+ id: string;
42
+ model: string;
43
+ capability: SDKCapability;
44
+ latencyMs: number;
45
+ totalTokens: number;
46
+ costUSD?: number;
47
+ finishReason?: string;
48
+ /** false for RequestErrorEvent settles. */
49
+ ok: boolean;
50
+ /** Error message when ok=false. */
51
+ errorMessage?: string;
52
+ /** Wall clock at settle — drives the "30s ago" relative display. */
53
+ endedAt: number;
54
+ }
55
+ /** Running session totals — shown in the popover's footer. */
56
+ export interface SessionStats {
57
+ totalCalls: number;
58
+ totalCostUSD: number;
59
+ errorCount: number;
60
+ /** Wall clock when the tracker first saw an event. Drives the
61
+ * "session lasted N minutes" string. */
62
+ startedAt: number;
63
+ }
64
+ /** Pull-snapshot of the tracker — taken once per badge re-render. */
65
+ export interface ActivitySnapshot {
66
+ active: ActiveCall[];
67
+ recent: CompletedCall[];
68
+ session: SessionStats;
69
+ /** Highest-priority status colour for the LED dot:
70
+ * "error" → recent failure within ERROR_HIGHLIGHT_MS
71
+ * "active" → at least one in-flight call
72
+ * "idle" → nothing happening
73
+ * The badge's CSS uses this as a class. */
74
+ status: "idle" | "active" | "error";
75
+ }
76
+ export declare class ActivityTracker {
77
+ private active;
78
+ private recent;
79
+ private session;
80
+ /** Track most recent error wall-clock so the LED stays red for
81
+ * ERROR_HIGHLIGHT_MS even after the error scrolls out of recent. */
82
+ private lastErrorAt;
83
+ /** "Snapshot changed" listeners. Fired AFTER every event handler
84
+ * runs (start / delta / end / error), letting UIs re-render in
85
+ * push-mode instead of polling. Without this hook, the badge's
86
+ * ring/LED stayed idle until the user opened the popover —
87
+ * events landed in the tracker but nobody told the DOM. */
88
+ private changeListeners;
89
+ /**
90
+ * Subscribe the tracker to a bus. Returns a single disposer that
91
+ * unsubscribes all four handlers — callers (BrowserAuth in our
92
+ * case) attach this on construction and dispose on teardown.
93
+ */
94
+ attach(events: SDKEvents): () => void;
95
+ /**
96
+ * Subscribe to "snapshot may have changed" notifications. Fires
97
+ * synchronously after each event handler runs — typical consumer
98
+ * is the badge's `requestRedraw()` which coalesces multiple
99
+ * notifications into one rAF paint.
100
+ *
101
+ * Returns a disposer; call to unsubscribe.
102
+ */
103
+ onChange(handler: () => void): () => void;
104
+ private notify;
105
+ /** O(active+recent) — small in practice, fine on every frame. */
106
+ snapshot(): ActivitySnapshot;
107
+ private onStart;
108
+ private onDelta;
109
+ private onEnd;
110
+ private onError;
111
+ private pushRecent;
112
+ }
113
+ //# sourceMappingURL=activity-tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-tracker.d.ts","sourceRoot":"","sources":["../src/activity-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAKV,aAAa,EACb,SAAS,EACV,MAAM,aAAa,CAAC;AAYrB,wCAAwC;AACxC,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,aAAa,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,SAAS,EAAE,OAAO,CAAC;IACnB,0DAA0D;IAC1D,WAAW,EAAE,MAAM,CAAC;IACpB;iCAC6B;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,WAAW,EAAE,KAAK,CAAC;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACnD;AAED,+DAA+D;AAC/D,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,aAAa,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,EAAE,EAAE,OAAO,CAAC;IACZ,mCAAmC;IACnC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,8DAA8D;AAC9D,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB;6CACyC;IACzC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,OAAO,EAAE,YAAY,CAAC;IACtB;;;;gDAI4C;IAC5C,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;CACrC;AAOD,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,OAAO,CAKb;IACF;yEACqE;IACrE,OAAO,CAAC,WAAW,CAAK;IACxB;;;;gEAI4D;IAC5D,OAAO,CAAC,eAAe,CAAyB;IAEhD;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,IAAI;IAYrC;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAKzC,OAAO,CAAC,MAAM;IAWd,iEAAiE;IACjE,QAAQ,IAAI,gBAAgB;IAiB5B,OAAO,CAAC,OAAO;IAaf,OAAO,CAAC,OAAO;IAkBf,OAAO,CAAC,KAAK;IAkBb,OAAO,CAAC,OAAO;IAkBf,OAAO,CAAC,UAAU;CAKnB"}
@@ -0,0 +1,176 @@
1
+ /**
2
+ * In-memory session-scoped activity index — turns the raw event
3
+ * stream from `SDKEvents` into the snapshot the badge UI reads each
4
+ * frame: which calls are in flight, what just finished, what the
5
+ * running session cost is.
6
+ *
7
+ * Memory bounded:
8
+ * - `active` map shrinks on every end/error (calls don't accumulate
9
+ * past their settle event).
10
+ * - `recent` is a bounded ring of the last RECENT_MAX completions.
11
+ * - tokens/sec windows hold only `RATE_WINDOW_MS` worth of deltas;
12
+ * stale samples get dropped on every push.
13
+ *
14
+ * Snapshot pattern (vs. emitting another event per change): the
15
+ * badge re-renders on `requestAnimationFrame` while it's open, so
16
+ * a pull-snapshot API matches that cadence — pushing on every
17
+ * delta would over-paint and cost battery on token-heavy streams.
18
+ */
19
+ /** Sliding window for tokens/sec — 1.5s gives a snappy display
20
+ * without jittering on every chunk arrival (most providers emit
21
+ * ~5-30 deltas/s). */
22
+ const RATE_WINDOW_MS = 1500;
23
+ /** Cap on the recent-completion ring buffer. The popover lists the
24
+ * last 5 by default but we keep a few extra so a future "show more"
25
+ * UI doesn't need a refactor. */
26
+ const RECENT_MAX = 20;
27
+ /** Window during which a recent error keeps the LED red. 30s is
28
+ * long enough to grab attention but short enough that an old
29
+ * failure doesn't dominate when subsequent calls succeed. */
30
+ const ERROR_HIGHLIGHT_MS = 30_000;
31
+ export class ActivityTracker {
32
+ active = new Map();
33
+ recent = [];
34
+ session = {
35
+ totalCalls: 0,
36
+ totalCostUSD: 0,
37
+ errorCount: 0,
38
+ startedAt: Date.now(),
39
+ };
40
+ /** Track most recent error wall-clock so the LED stays red for
41
+ * ERROR_HIGHLIGHT_MS even after the error scrolls out of recent. */
42
+ lastErrorAt = 0;
43
+ /** "Snapshot changed" listeners. Fired AFTER every event handler
44
+ * runs (start / delta / end / error), letting UIs re-render in
45
+ * push-mode instead of polling. Without this hook, the badge's
46
+ * ring/LED stayed idle until the user opened the popover —
47
+ * events landed in the tracker but nobody told the DOM. */
48
+ changeListeners = new Set();
49
+ /**
50
+ * Subscribe the tracker to a bus. Returns a single disposer that
51
+ * unsubscribes all four handlers — callers (BrowserAuth in our
52
+ * case) attach this on construction and dispose on teardown.
53
+ */
54
+ attach(events) {
55
+ const offs = [
56
+ events.on("request:start", (e) => { this.onStart(e); this.notify(); }),
57
+ events.on("request:streamDelta", (e) => { this.onDelta(e); this.notify(); }),
58
+ events.on("request:end", (e) => { this.onEnd(e); this.notify(); }),
59
+ events.on("request:error", (e) => { this.onError(e); this.notify(); }),
60
+ ];
61
+ return () => {
62
+ for (const o of offs)
63
+ o();
64
+ };
65
+ }
66
+ /**
67
+ * Subscribe to "snapshot may have changed" notifications. Fires
68
+ * synchronously after each event handler runs — typical consumer
69
+ * is the badge's `requestRedraw()` which coalesces multiple
70
+ * notifications into one rAF paint.
71
+ *
72
+ * Returns a disposer; call to unsubscribe.
73
+ */
74
+ onChange(handler) {
75
+ this.changeListeners.add(handler);
76
+ return () => this.changeListeners.delete(handler);
77
+ }
78
+ notify() {
79
+ for (const h of this.changeListeners) {
80
+ try {
81
+ h();
82
+ }
83
+ catch (err) {
84
+ // eslint-disable-next-line no-console
85
+ console.error("[runjobs sdk] ActivityTracker onChange listener threw:", err);
86
+ }
87
+ }
88
+ }
89
+ /** O(active+recent) — small in practice, fine on every frame. */
90
+ snapshot() {
91
+ const now = Date.now();
92
+ return {
93
+ active: Array.from(this.active.values()),
94
+ recent: this.recent.slice(),
95
+ session: { ...this.session },
96
+ status: now - this.lastErrorAt < ERROR_HIGHLIGHT_MS
97
+ ? "error"
98
+ : this.active.size > 0
99
+ ? "active"
100
+ : "idle",
101
+ };
102
+ }
103
+ // ─── handlers ─────────────────────────────────────────────────
104
+ onStart(e) {
105
+ this.active.set(e.id, {
106
+ id: e.id,
107
+ model: e.model,
108
+ capability: e.capability,
109
+ startedAt: e.startedAt,
110
+ streaming: e.streaming,
111
+ tokensSoFar: 0,
112
+ tokensPerSec: 0,
113
+ rateSamples: [],
114
+ });
115
+ }
116
+ onDelta(e) {
117
+ const call = this.active.get(e.id);
118
+ if (!call)
119
+ return; // delta after end — drop
120
+ call.tokensSoFar = e.totalTokens;
121
+ const now = Date.now();
122
+ call.rateSamples.push({ t: now, tokens: e.deltaTokens });
123
+ // Drop samples older than the window.
124
+ const cutoff = now - RATE_WINDOW_MS;
125
+ while (call.rateSamples.length > 0 && call.rateSamples[0].t < cutoff) {
126
+ call.rateSamples.shift();
127
+ }
128
+ if (call.rateSamples.length >= 2) {
129
+ const sumTokens = call.rateSamples.reduce((s, x) => s + x.tokens, 0);
130
+ const spanMs = now - call.rateSamples[0].t;
131
+ call.tokensPerSec = spanMs > 0 ? (sumTokens * 1000) / spanMs : 0;
132
+ }
133
+ }
134
+ onEnd(e) {
135
+ const call = this.active.get(e.id);
136
+ this.active.delete(e.id);
137
+ this.pushRecent({
138
+ id: e.id,
139
+ model: e.model,
140
+ capability: e.capability,
141
+ latencyMs: e.latencyMs,
142
+ totalTokens: e.totalTokens || call?.tokensSoFar || 0,
143
+ ...(e.costUSD !== undefined && { costUSD: e.costUSD }),
144
+ ...(e.finishReason !== undefined && { finishReason: e.finishReason }),
145
+ ok: true,
146
+ endedAt: Date.now(),
147
+ });
148
+ this.session.totalCalls += 1;
149
+ if (e.costUSD !== undefined)
150
+ this.session.totalCostUSD += e.costUSD;
151
+ }
152
+ onError(e) {
153
+ const call = this.active.get(e.id);
154
+ this.active.delete(e.id);
155
+ this.pushRecent({
156
+ id: e.id,
157
+ model: e.model,
158
+ capability: e.capability,
159
+ latencyMs: e.latencyMs,
160
+ totalTokens: call?.tokensSoFar || 0,
161
+ ok: false,
162
+ errorMessage: e.error.message,
163
+ endedAt: Date.now(),
164
+ });
165
+ this.session.totalCalls += 1;
166
+ this.session.errorCount += 1;
167
+ this.lastErrorAt = Date.now();
168
+ }
169
+ pushRecent(c) {
170
+ // Newest first — popover renders top-to-bottom newest→oldest.
171
+ this.recent.unshift(c);
172
+ if (this.recent.length > RECENT_MAX)
173
+ this.recent.length = RECENT_MAX;
174
+ }
175
+ }
176
+ //# sourceMappingURL=activity-tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-tracker.js","sourceRoot":"","sources":["../src/activity-tracker.ts"],"names":[],"mappings":"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"}