@runappsai/sdk 0.6.3 → 0.6.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runappsai/sdk",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "JavaScript / TypeScript client for the RunApps AI Gateway",
5
5
  "license": "MIT",
6
6
  "author": "RunApps",
@@ -215,6 +215,8 @@ export class ActivityTracker {
215
215
  private onEnd(e: RequestEndEvent): void {
216
216
  const call = this.active.get(e.id);
217
217
  this.active.delete(e.id);
218
+ // File calls only show while in flight: nothing to list or add up.
219
+ if (e.capability === "files") return;
218
220
  this.pushRecent({
219
221
  id: e.id,
220
222
  model: e.model,
@@ -233,6 +235,12 @@ export class ActivityTracker {
233
235
  private onError(e: RequestErrorEvent): void {
234
236
  const call = this.active.get(e.id);
235
237
  this.active.delete(e.id);
238
+ if (e.capability === "files") {
239
+ // Not listed; the LED goes red for a moment unless it was only a
240
+ // missing file, which apps probe for routinely.
241
+ if (e.statusCode !== 404) this.lastErrorAt = Date.now();
242
+ return;
243
+ }
236
244
  this.pushRecent({
237
245
  id: e.id,
238
246
  model: e.model,
@@ -985,12 +985,15 @@ function mountActivityBadge(opts: MountOpts): HTMLButtonElement {
985
985
  const led = document.createElement("span");
986
986
  led.style.cssText = [
987
987
  "position:absolute",
988
- "bottom:-2px;right:-2px",
989
- "width:8px;height:8px;border-radius:50%",
988
+ "bottom:-1px;right:-1px",
989
+ "width:7px;height:7px;border-radius:50%",
990
990
  "background:rgba(255,255,255,0.2)",
991
- "border:2px solid rgba(15,15,20,1)",
991
+ "border:1.5px solid rgba(15,15,20,1)",
992
992
  "box-shadow:0 0 0 0 transparent",
993
- "transition:background-color 150ms ease-out, box-shadow 200ms ease-out",
993
+ // Hidden at rest: it only appears while something is happening
994
+ // (syncing, offline, an error, the green flash after a sync).
995
+ "opacity:0",
996
+ "transition:background-color 150ms ease-out, box-shadow 200ms ease-out, opacity 200ms ease-out",
994
997
  "pointer-events:none",
995
998
  ].join(";");
996
999
 
@@ -1276,6 +1279,7 @@ function mountActivityBadge(opts: MountOpts): HTMLButtonElement {
1276
1279
  // No event bus → static badge. Hide ring + LED.
1277
1280
  ring.style.opacity = "0";
1278
1281
  led.style.background = "rgba(255,255,255,0.2)";
1282
+ led.style.opacity = "0";
1279
1283
  return;
1280
1284
  }
1281
1285
  const snap = tracker.snapshot();
@@ -1383,6 +1387,7 @@ function syncedPing(avatarWrap: HTMLElement, led: HTMLSpanElement): void {
1383
1387
  avatarWrap.appendChild(ping);
1384
1388
  setTimeout(() => ping.remove(), 1000); // in case animationend never fires
1385
1389
  syncFlashUntil = Date.now() + 1200;
1390
+ led.style.opacity = "1";
1386
1391
  led.style.background = "#10b981";
1387
1392
  led.style.boxShadow = "0 0 0 2px rgba(16,185,129,0.35)";
1388
1393
  led.style.animation = "";
@@ -1390,12 +1395,14 @@ function syncedPing(avatarWrap: HTMLElement, led: HTMLSpanElement): void {
1390
1395
  // Back to the resting colour once the flash is over.
1391
1396
  if (Date.now() >= syncFlashUntil) {
1392
1397
  led.style.background = "rgba(255,255,255,0.2)";
1398
+ led.style.opacity = "0";
1393
1399
  led.style.boxShadow = "0 0 0 0 transparent";
1394
1400
  }
1395
1401
  }, 1250);
1396
1402
  }
1397
1403
 
1398
1404
  function updateLED(led: HTMLSpanElement, snap: ActivitySnapshot): void {
1405
+ led.style.opacity = "1"; // the resting branches below hide it again
1399
1406
  switch (snap.status) {
1400
1407
  case "idle": {
1401
1408
  // Nothing in flight: the LED shows the sync instead — blue pulse
@@ -1416,6 +1423,7 @@ function updateLED(led: HTMLSpanElement, snap: ActivitySnapshot): void {
1416
1423
  led.style.animation = "";
1417
1424
  } else {
1418
1425
  led.style.background = "rgba(255,255,255,0.2)";
1426
+ led.style.opacity = "0";
1419
1427
  led.style.boxShadow = "0 0 0 0 transparent";
1420
1428
  led.style.animation = "";
1421
1429
  }
package/src/client.ts CHANGED
@@ -248,7 +248,7 @@ export class RunApps {
248
248
  // `project` also scopes the file URLs (/v1/p/<project>/files/...),
249
249
  // so two apps never issue byte-identical file requests. Omitted →
250
250
  // legacy unscoped /v1/files/... paths.
251
- this.files = new FilesService(transport, options.project);
251
+ this.files = new FilesService(transport, options.project, this.events);
252
252
  this.embeddings = new EmbeddingsService(transport, this.events);
253
253
 
254
254
  // Sync: an app on RunApps gets it unless it says no. It
@@ -259,7 +259,9 @@ export class RunApps {
259
259
  const auth = this.auth;
260
260
  const engine = new SyncEngine(
261
261
  {
262
- remote: new FilesRemote(this.files),
262
+ // Its own file service, without the event bus: sync traffic
263
+ // must not spin the badge every minute.
264
+ remote: new FilesRemote(new FilesService(transport, options.project)),
263
265
  events: this.events,
264
266
  userId: () => auth.ownerId(),
265
267
  onAuthChange: (cb) => auth.onTokenChange(() => cb()),
package/src/events.ts CHANGED
@@ -32,7 +32,10 @@ export type SDKCapability =
32
32
  | "text_to_speech"
33
33
  | "speech_to_text"
34
34
  | "video_generation"
35
- | "computer_use";
35
+ | "computer_use"
36
+ /** A file-space call (`client.files`). Shown on the badge while in
37
+ * flight and on failure, but not listed or charged per call. */
38
+ | "files";
36
39
 
37
40
  /** Fired the moment a request hits the wire. */
38
41
  export interface RequestStartEvent {
package/src/files.ts CHANGED
@@ -8,6 +8,8 @@
8
8
  // token (rrt_*) issued by the grant flow.
9
9
 
10
10
  import type { Transport } from "./transport.js";
11
+ import type { SDKEvents } from "./events.js";
12
+ import { wrapEvents } from "./event-wrap.js";
11
13
 
12
14
  export interface FileObject {
13
15
  path: string;
@@ -121,12 +123,21 @@ export class FilesService {
121
123
  constructor(
122
124
  private readonly transport: Transport,
123
125
  project?: string,
126
+ /** When given, every call emits request:start / end / error so the
127
+ * badge can show it in flight; the sync engine's own file traffic
128
+ * uses a service without one. */
129
+ private readonly events?: SDKEvents,
124
130
  ) {
125
131
  this.base = project
126
132
  ? "/v1/p/" + encodeURIComponent(project) + "/files"
127
133
  : "/v1/files";
128
134
  }
129
135
 
136
+ /** Run one file call, with badge events when the service has a bus. */
137
+ private run<T>(op: string, fn: () => Promise<T>): Promise<T> {
138
+ return this.events ? wrapEvents(this.events, { model: op, capability: "files" }, fn) : fn();
139
+ }
140
+
130
141
  /**
131
142
  * Upload `body` to `path`. Returns the resulting FileObject — its
132
143
  * `url` is the stable public address for that path.
@@ -143,11 +154,11 @@ export class FilesService {
143
154
  const headers: Record<string, string> = {};
144
155
  if (opts?.ifNoneMatch) headers["If-None-Match"] = "*";
145
156
  if (opts?.ifMatch) headers["If-Match"] = `"${opts.ifMatch.replace(/"/g, "")}"`;
146
- return this.transport.putBytes<FileObject>(pathToURL(this.base, path), body, {
157
+ return this.run("files.put", () => this.transport.putBytes<FileObject>(pathToURL(this.base, path), body, {
147
158
  contentType: opts?.contentType,
148
159
  headers: Object.keys(headers).length ? headers : undefined,
149
160
  signal: opts?.signal,
150
- });
161
+ }));
151
162
  }
152
163
 
153
164
  /** Convenience: upload string content with sensible content-type. */
@@ -178,6 +189,10 @@ export class FilesService {
178
189
  * still the version that was read.
179
190
  */
180
191
  async read(path: string, init?: { signal?: AbortSignal; ifNoneMatch?: string }): Promise<FileRead> {
192
+ return this.run("files.read", () => this.readRaw(path, init));
193
+ }
194
+
195
+ private async readRaw(path: string, init?: { signal?: AbortSignal; ifNoneMatch?: string }): Promise<FileRead> {
181
196
  const { data, contentType, etag, notModified } = await this.transport.getRaw(pathToURL(this.base, path), init);
182
197
  return {
183
198
  blob: new Blob([new Uint8Array(data)], { type: contentType || "application/octet-stream" }),
@@ -212,7 +227,7 @@ export class FilesService {
212
227
 
213
228
  /** Stat: HEAD-only object metadata. */
214
229
  async stat(path: string, init?: { signal?: AbortSignal }): Promise<FileObject> {
215
- const { status, headers } = await this.transport.head(pathToURL(this.base, path), init);
230
+ const { status, headers } = await this.run("files.stat", () => this.transport.head(pathToURL(this.base, path), init));
216
231
  if (status === 404) {
217
232
  throw Object.assign(new Error("not found"), { status: 404 });
218
233
  }
@@ -231,13 +246,13 @@ export class FilesService {
231
246
 
232
247
  /** Returns true iff the path resolves to an existing object. */
233
248
  async exists(path: string, init?: { signal?: AbortSignal }): Promise<boolean> {
234
- const { status } = await this.transport.head(pathToURL(this.base, path), init);
249
+ const { status } = await this.run("files.exists", () => this.transport.head(pathToURL(this.base, path), init));
235
250
  return status >= 200 && status < 400;
236
251
  }
237
252
 
238
253
  /** Delete the object at `path`. Idempotent. */
239
254
  async del(path: string, init?: { signal?: AbortSignal }): Promise<void> {
240
- await this.transport.deletePath<{ ok: boolean }>(pathToURL(this.base, path), init);
255
+ await this.run("files.del", () => this.transport.deletePath<{ ok: boolean }>(pathToURL(this.base, path), init));
241
256
  }
242
257
 
243
258
  /** List objects. Pass an empty / omitted prefix for the root. */
@@ -248,10 +263,10 @@ export class FilesService {
248
263
  if (opts?.limit) params.set("limit", String(opts.limit));
249
264
  if (opts?.glob) params.set("glob", opts.glob);
250
265
  const qs = params.toString();
251
- return this.transport.getJSON<FileListResult>(
266
+ return this.run("files.list", () => this.transport.getJSON<FileListResult>(
252
267
  this.base + (qs ? "?" + qs : ""),
253
268
  opts?.signal ? { signal: opts.signal } : undefined,
254
- );
269
+ ));
255
270
  }
256
271
 
257
272
  /**
@@ -268,21 +283,21 @@ export class FilesService {
268
283
  opts: { prefix?: string; glob?: string },
269
284
  init?: { signal?: AbortSignal },
270
285
  ): Promise<{ deleted: number }> {
271
- return this.transport.postJSON<{ deleted: number }>(
286
+ return this.run("files.deleteMany", () => this.transport.postJSON<{ deleted: number }>(
272
287
  this.base + "/delete",
273
288
  { prefix: opts.prefix ?? "", glob: opts.glob ?? "" },
274
289
  init,
275
- );
290
+ ));
276
291
  }
277
292
 
278
293
  /** Atomic rename (copy + delete server-side). */
279
294
  async move(from: string, to: string, init?: { signal?: AbortSignal }): Promise<FileObject> {
280
- return this.transport.postJSON<FileObject>(this.base + "/move", { from, to }, init);
295
+ return this.run("files.move", () => this.transport.postJSON<FileObject>(this.base + "/move", { from, to }, init));
281
296
  }
282
297
 
283
298
  /** Server-side copy. */
284
299
  async copy(from: string, to: string, init?: { signal?: AbortSignal }): Promise<FileObject> {
285
- return this.transport.postJSON<FileObject>(this.base + "/copy", { from, to }, init);
300
+ return this.run("files.copy", () => this.transport.postJSON<FileObject>(this.base + "/copy", { from, to }, init));
286
301
  }
287
302
 
288
303
  /**
@@ -295,7 +310,7 @@ export class FilesService {
295
310
  srcURL: string,
296
311
  opts?: PutFromURLOptions,
297
312
  ): Promise<FileObject> {
298
- return this.transport.postJSON<FileObject>(
313
+ return this.run("files.putFromURL", () => this.transport.postJSON<FileObject>(
299
314
  this.base + "/put-url",
300
315
  {
301
316
  path,
@@ -305,7 +320,7 @@ export class FilesService {
305
320
  if_match: opts?.ifMatch,
306
321
  },
307
322
  opts?.signal ? { signal: opts.signal } : undefined,
308
- );
323
+ ));
309
324
  }
310
325
 
311
326
  /**
@@ -325,6 +340,10 @@ export class FilesService {
325
340
  init?: { signal?: AbortSignal },
326
341
  ): Promise<BatchResult[]> {
327
342
  if (ops.length === 0) return [];
343
+ return this.run("files.batch", () => this.batchRaw(ops, init));
344
+ }
345
+
346
+ private async batchRaw(ops: BatchOp[], init?: { signal?: AbortSignal }): Promise<BatchResult[]> {
328
347
  const chunkSize = 30;
329
348
  if (ops.length <= chunkSize) {
330
349
  const resp = await this.transport.postJSON<{ results: BatchResult[] }>(