@spheredata/sdk 0.0.1 → 0.0.2
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 +1 -1
- package/sphere-client.js +128 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spheredata/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"description": "SPHERE client SDK — talks to the SPHERE backend (auth, metered agent, account).",
|
|
6
6
|
"keywords": ["sphere", "spheredata", "sdk", "ai", "agent", "metering", "wallet", "synthetic-data"],
|
package/sphere-client.js
CHANGED
|
@@ -212,6 +212,31 @@ export function createSphere(opts = {}) {
|
|
|
212
212
|
return `${base}?redirect_to=${encodeURIComponent(redirectTo)}`;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
// Complete a hosted OAuth flow: when the backend redirects back to a surface's
|
|
216
|
+
// `redirect_to`, it appends the tokens as query params
|
|
217
|
+
// (?access_token=…&refresh_token=…&expires_in=…). Parse them and persist a
|
|
218
|
+
// session, exactly as signIn would. Returns true if a session was established,
|
|
219
|
+
// false if no oauth tokens were present (so callers can no-op on a normal load).
|
|
220
|
+
// `input` may be a query string ("?a=b" or "a=b"), a URLSearchParams, or omitted
|
|
221
|
+
// to read globalThis.location.search. The user profile isn't in the redirect, so
|
|
222
|
+
// the bundle's user is null until the next getAccount().
|
|
223
|
+
function completeOAuth(input) {
|
|
224
|
+
const params =
|
|
225
|
+
input instanceof URLSearchParams ? input
|
|
226
|
+
: typeof input === "string" ? new URLSearchParams(input.replace(/^\?/, ""))
|
|
227
|
+
: (typeof globalThis !== "undefined" && globalThis.location)
|
|
228
|
+
? new URLSearchParams(globalThis.location.search)
|
|
229
|
+
: new URLSearchParams();
|
|
230
|
+
const access_token = params.get("access_token");
|
|
231
|
+
if (!access_token) return false;
|
|
232
|
+
saveBundle(bundleFromAuthResponse({
|
|
233
|
+
access_token,
|
|
234
|
+
refresh_token: params.get("refresh_token"),
|
|
235
|
+
expires_in: params.get("expires_in"),
|
|
236
|
+
}));
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
215
240
|
// --- AI -------------------------------------------------------------------
|
|
216
241
|
// Ask the model through SPHERE. Metered to the user's wallet under this portal.
|
|
217
242
|
async function ask(messages, { model, max_tokens } = {}) {
|
|
@@ -316,12 +341,114 @@ export function createSphere(opts = {}) {
|
|
|
316
341
|
return data; // { ok:true, charged, op, bytes, cost_usd, balance_usd }
|
|
317
342
|
}
|
|
318
343
|
|
|
344
|
+
// --- synthetic-dataset upload (direct-to-GCS via AgentDrive) --------------
|
|
345
|
+
// NODE / ELECTRON ONLY (CLI + desktop) — not the browser. Per file:
|
|
346
|
+
// 1. begin → backend mints a GCS resumable-session URL (the AgentDrive
|
|
347
|
+
// credential stays server-side; the client never sees it),
|
|
348
|
+
// 2. PUT bytes DIRECTLY to GCS (bytes never transit the SPHERE backend),
|
|
349
|
+
// 3. commit → backend finalizes the artifact.
|
|
350
|
+
// Large files (e.g. 1 GiB scrna.csv) stream from disk via fs.openAsBlob — never
|
|
351
|
+
// loaded whole into memory. fs/path are imported lazily so the SDK still imports
|
|
352
|
+
// cleanly in the browser (where upload isn't offered).
|
|
353
|
+
const CONTENT_TYPES = {
|
|
354
|
+
csv: "text/csv", tsv: "text/tab-separated-values",
|
|
355
|
+
parquet: "application/vnd.apache.parquet", json: "application/json",
|
|
356
|
+
txt: "text/plain", html: "text/html",
|
|
357
|
+
};
|
|
358
|
+
const guessType = (name) =>
|
|
359
|
+
CONTENT_TYPES[String(name).toLowerCase().split(".").pop()] || "application/octet-stream";
|
|
360
|
+
|
|
361
|
+
// Upload one file. `file` is { name?, path } (a local file, streamed) OR
|
|
362
|
+
// { name, body, size, contentType? } (a pre-read Blob/Buffer + byte length).
|
|
363
|
+
// Returns the AgentDrive artifact { id, path, url, size_bytes, hash, ... }.
|
|
364
|
+
async function uploadFile(dataset, file, defaultContentType) {
|
|
365
|
+
if (!dataset) throw new Error("uploadFile: dataset name required");
|
|
366
|
+
let { name, body, size } = file || {};
|
|
367
|
+
if (file?.path) {
|
|
368
|
+
// openAsBlob lives on node:fs (not node:fs/promises) — gives a streaming
|
|
369
|
+
// Blob backed by the file, so large files are never read into memory.
|
|
370
|
+
const { openAsBlob } = await import("node:fs");
|
|
371
|
+
const blob = await openAsBlob(file.path);
|
|
372
|
+
body = blob;
|
|
373
|
+
size = blob.size;
|
|
374
|
+
if (!name) name = (await import("node:path")).basename(file.path);
|
|
375
|
+
}
|
|
376
|
+
if (!name) throw new Error("uploadFile: file.name or file.path required");
|
|
377
|
+
if (!(size > 0)) throw new Error(`uploadFile: "${name}" has no readable bytes`);
|
|
378
|
+
const contentType = file?.contentType || defaultContentType || guessType(name);
|
|
379
|
+
|
|
380
|
+
const begin = await authedFetch(`${fnBase}/upload-begin${q}`, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: { "Content-Type": "application/json" },
|
|
383
|
+
body: JSON.stringify({ dataset, filename: name, content_type: contentType, size_bytes: size }),
|
|
384
|
+
});
|
|
385
|
+
const b = await begin.json().catch(() => ({}));
|
|
386
|
+
if (!begin.ok) throw new Error(b?.error || `upload-begin failed: ${begin.status}`);
|
|
387
|
+
|
|
388
|
+
// Direct PUT to the GCS session URL — NO Authorization header (the URL is the
|
|
389
|
+
// credential). `duplex` is required by undici for a streamed body; ignored for Blobs.
|
|
390
|
+
const put = await doFetch(b.upload_url, {
|
|
391
|
+
method: "PUT",
|
|
392
|
+
headers: { "Content-Type": contentType, "Content-Range": `bytes 0-${size - 1}/${size}` },
|
|
393
|
+
body,
|
|
394
|
+
duplex: "half",
|
|
395
|
+
});
|
|
396
|
+
if (put.status < 200 || put.status >= 300) {
|
|
397
|
+
throw new Error(`GCS upload failed for "${name}": ${put.status}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const commit = await authedFetch(`${fnBase}/upload-commit${q}`, {
|
|
401
|
+
method: "POST",
|
|
402
|
+
headers: { "Content-Type": "application/json" },
|
|
403
|
+
body: JSON.stringify({ upload_id: b.upload_id }),
|
|
404
|
+
});
|
|
405
|
+
const c = await commit.json().catch(() => ({}));
|
|
406
|
+
if (!commit.ok) throw new Error(c?.error || `upload-commit failed: ${commit.status}`);
|
|
407
|
+
return c.artifact;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Upload a whole synthetic dataset (its files). `files` is an array of the per-file
|
|
411
|
+
// shapes above. Files upload sequentially; `onProgress({done,total,name,artifact})`
|
|
412
|
+
// fires after each. Returns { dataset, artifacts }.
|
|
413
|
+
async function uploadDataset(dataset, files, { onProgress, contentType } = {}) {
|
|
414
|
+
if (!dataset) throw new Error("uploadDataset: dataset name required");
|
|
415
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
416
|
+
throw new Error("uploadDataset: a non-empty files[] is required");
|
|
417
|
+
}
|
|
418
|
+
const artifacts = [];
|
|
419
|
+
for (let i = 0; i < files.length; i++) {
|
|
420
|
+
const art = await uploadFile(dataset, files[i], contentType);
|
|
421
|
+
artifacts.push(art);
|
|
422
|
+
if (onProgress) onProgress({ done: i + 1, total: files.length, name: files[i].name, artifact: art });
|
|
423
|
+
}
|
|
424
|
+
return { dataset, artifacts };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// List the datasets uploaded under this portal (shared catalog). Returns
|
|
428
|
+
// [{ dataset, file_count, total_bytes, updated_at, files:[{name,path,url,...}] }].
|
|
429
|
+
async function listDatasets() {
|
|
430
|
+
const r = await authedFetch(`${fnBase}/datasets${q}`, {});
|
|
431
|
+
const data = await r.json().catch(() => ({}));
|
|
432
|
+
if (!r.ok) throw new Error(data?.error || `datasets failed: ${r.status}`);
|
|
433
|
+
return data.datasets || [];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// List datasets across ALL portals/labs — the cross-lab marketplace. Each item
|
|
437
|
+
// carries its source `portal` plus the same shape as listDatasets().
|
|
438
|
+
async function listMarketplace() {
|
|
439
|
+
const r = await authedFetch(`${fnBase}/marketplace`, {});
|
|
440
|
+
const data = await r.json().catch(() => ({}));
|
|
441
|
+
if (!r.ok) throw new Error(data?.error || `marketplace failed: ${r.status}`);
|
|
442
|
+
return data.datasets || [];
|
|
443
|
+
}
|
|
444
|
+
|
|
319
445
|
return {
|
|
320
446
|
portal, appId, apiBase,
|
|
321
|
-
signUp, signIn, signOut, oauthUrl,
|
|
447
|
+
signUp, signIn, signOut, oauthUrl, completeOAuth,
|
|
322
448
|
ask, streamAgent,
|
|
323
449
|
getAccount, getBalance,
|
|
324
450
|
listCredits, buyCredits, redeem, charge,
|
|
451
|
+
uploadFile, uploadDataset, listDatasets, listMarketplace,
|
|
325
452
|
// NOTE: reflects token PRESENCE, not validity — a present-but-expired token
|
|
326
453
|
// returns true here and is refreshed lazily on the next call.
|
|
327
454
|
isSignedIn: () => !!loadBundle()?.access_token,
|