@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/dist/activity-tracker.d.ts.map +1 -1
- package/dist/activity-tracker.js +10 -0
- package/dist/activity-tracker.js.map +1 -1
- package/dist/browser-auth.js +12 -4
- package/dist/browser-auth.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -2
- package/dist/client.js.map +1 -1
- package/dist/events.d.ts +4 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/files.d.ts +14 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +29 -12
- package/dist/files.js.map +1 -1
- package/dist/sdk.umd.js +3 -3
- package/dist/sdk.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/activity-tracker.ts +8 -0
- package/src/browser-auth.ts +12 -4
- package/src/client.ts +4 -2
- package/src/events.ts +4 -1
- package/src/files.ts +32 -13
package/package.json
CHANGED
package/src/activity-tracker.ts
CHANGED
|
@@ -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,
|
package/src/browser-auth.ts
CHANGED
|
@@ -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:-
|
|
989
|
-
"width:
|
|
988
|
+
"bottom:-1px;right:-1px",
|
|
989
|
+
"width:7px;height:7px;border-radius:50%",
|
|
990
990
|
"background:rgba(255,255,255,0.2)",
|
|
991
|
-
"border:
|
|
991
|
+
"border:1.5px solid rgba(15,15,20,1)",
|
|
992
992
|
"box-shadow:0 0 0 0 transparent",
|
|
993
|
-
|
|
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
|
-
|
|
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[] }>(
|