@madewithremy/admin 0.1.25 → 0.1.26

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @madewithremy/admin
2
2
 
3
- Admin SDK + CLI for managing production Remy apps: request logs, analytics, releases, users, secrets, database, files, email (including custom sending/receiving domains), scheduled jobs, and more. Every operation wraps the same management API the dashboard uses, so anything answerable in the console is answerable here — from the terminal or from code.
3
+ Admin SDK + CLI for managing production Remy apps: request logs, analytics, releases, users, secrets, database, files, email (including custom sending/receiving domains), scheduled jobs, QA replays, and more. Almost every operation wraps the same management API the dashboard uses, so anything answerable in the console is answerable here — from the terminal or from code. The exception is `qa-recordings export`, which renders a video on the dev box and therefore only runs inside a sandbox.
4
4
 
5
5
  One surface, two skins: every CLI command (`remy-admin <group> <sub>`) is also a typed client method (`admin.<group>.<sub>()`). Both are thin layers over the same ops core.
6
6
 
@@ -28,6 +28,7 @@ Configuration is read from the environment and the workspace — there are no co
28
28
  | `API_BASE_URL` | API origin (default `https://api.mindstudio.ai`) |
29
29
  | `WORKSPACE_DIR` | Workspace root (default `/home/vercel-sandbox/workspace`) |
30
30
  | `${WORKSPACE_DIR}/mindstudio.json` | `appId` — which app the CLI manages |
31
+ | `REMY_SIDECAR_URL` | Sandbox sidecar for `qa-recordings export` (default `http://127.0.0.1:4388`) |
31
32
 
32
33
  ## Import layer
33
34
 
package/dist/index.d.ts CHANGED
@@ -4660,7 +4660,7 @@ interface FilesLsParams {
4660
4660
  * prefix: 'images/',
4661
4661
  * });
4662
4662
  */
4663
- declare function ls(ctx: AdminContext, params: FilesLsParams): Promise<FilesLsResult>;
4663
+ declare function ls$1(ctx: AdminContext, params: FilesLsParams): Promise<FilesLsResult>;
4664
4664
  /**
4665
4665
  * Store-level summary: every store with object counts, total bytes, and
4666
4666
  * AV/PII scan status histograms.
@@ -4684,14 +4684,13 @@ type files_FileRef = FileRef;
4684
4684
  type files_FilesLsParams = FilesLsParams;
4685
4685
  type files_FilesPutParams = FilesPutParams;
4686
4686
  declare const files_fetchBytes: typeof fetchBytes;
4687
- declare const files_ls: typeof ls;
4688
4687
  declare const files_put: typeof put;
4689
4688
  declare const files_remove: typeof remove;
4690
4689
  declare const files_sign: typeof sign;
4691
4690
  declare const files_stat: typeof stat;
4692
4691
  declare const files_summary: typeof summary;
4693
4692
  declare namespace files {
4694
- export { type files_FileAccess as FileAccess, type files_FileRef as FileRef, type files_FilesLsParams as FilesLsParams, type files_FilesPutParams as FilesPutParams, files_fetchBytes as fetchBytes, files_ls as ls, files_put as put, files_remove as remove, files_sign as sign, files_stat as stat, files_summary as summary };
4693
+ export { type files_FileAccess as FileAccess, type files_FileRef as FileRef, type files_FilesLsParams as FilesLsParams, type files_FilesPutParams as FilesPutParams, files_fetchBytes as fetchBytes, ls$1 as ls, files_put as put, files_remove as remove, files_sign as sign, files_stat as stat, files_summary as summary };
4695
4694
  }
4696
4695
 
4697
4696
  /**
@@ -6399,6 +6398,129 @@ declare namespace prerender {
6399
6398
  export { type prerender_PrerenderInvalidateParams as PrerenderInvalidateParams, type prerender_PrerenderPagesParams as PrerenderPagesParams, type prerender_PrerenderViewParams as PrerenderViewParams, prerender_invalidate as invalidate, prerender_pages as pages, prerender_view as view };
6400
6399
  }
6401
6400
 
6401
+ /**
6402
+ * Response types for browser-QA replay recordings.
6403
+ *
6404
+ * Chunks are ordinary private files in the app's `qa-recordings` store, keyed
6405
+ * `{sessionId}/{runId}/{seq}.json`, so `ls` here is a grouped view over
6406
+ * `files.ls` rather than its own endpoint. Transcribed from youai-api:
6407
+ * src/http/routes/V2Apps/develop/recordings.ts
6408
+ * src/common/Recordings/sessions.ts
6409
+ *
6410
+ * Export is the exception — it renders on the dev box (see ../ops/recordings).
6411
+ */
6412
+ /** One document lifetime within a session: a page load and everything after it
6413
+ * until the next one. Its lowest-seq chunk carries the rrweb FullSnapshot. */
6414
+ interface QaRecordingRun {
6415
+ runId: string;
6416
+ chunks: number;
6417
+ bytes: number;
6418
+ /** Store-relative keys, in seq order. */
6419
+ keys: string[];
6420
+ }
6421
+ /** One recording session — a single tunnel process's continuous recording. */
6422
+ interface QaRecordingSession {
6423
+ sessionId: string;
6424
+ chunks: number;
6425
+ bytes: number;
6426
+ /** ISO-8601, from the chunk objects' last-modified times. */
6427
+ startedAt?: string;
6428
+ endedAt?: string;
6429
+ runs: QaRecordingRun[];
6430
+ }
6431
+ interface QaRecordingsLsResult {
6432
+ store: string;
6433
+ sessions: QaRecordingSession[];
6434
+ /** Present when the listing was truncated — pass it back as `cursor`. */
6435
+ cursor?: string;
6436
+ }
6437
+ interface QaRecordingsExportResult {
6438
+ jobId: string;
6439
+ status: 'completed' | 'failed' | 'cancelled';
6440
+ /** Public target only. A private one gives `store`/`key`; sign it with
6441
+ * `files sign` when you want a link. */
6442
+ url?: string;
6443
+ store?: string;
6444
+ key?: string;
6445
+ width?: number;
6446
+ height?: number;
6447
+ /** Length of the rendered clip, dead air already collapsed. */
6448
+ durationMs?: number;
6449
+ error?: string;
6450
+ errorCode?: string;
6451
+ }
6452
+
6453
+ /**
6454
+ * Browser-QA replay recordings: list what the QA agent recorded, and render a
6455
+ * replay to an mp4.
6456
+ *
6457
+ * `ls` is a grouped view over `files.ls` — a replay chunk is an ordinary
6458
+ * private file in the app's `qa-recordings` store and needs no endpoint of its
6459
+ * own.
6460
+ *
6461
+ * `export` is the one operation in this SDK that does NOT go through the
6462
+ * management API. Rendering a replay means playing it in a real browser and
6463
+ * encoding the frames, and the only machine set up to do that is the dev box
6464
+ * the recording was made on: it already has the headless Chrome, the rrweb
6465
+ * replayer and ffmpeg. So this talks to the sandbox's local sidecar, and says
6466
+ * so plainly when there isn't one. (CLI-only paths are established here —
6467
+ * `methods invoke --stream`, `jewels export --file` — but this is the first
6468
+ * that needs local compute rather than local disk.)
6469
+ */
6470
+
6471
+ /** The app store QA replay chunks are written to (private). */
6472
+ declare const RECORDING_STORE = "qa-recordings";
6473
+ interface QaRecordingsLsParams {
6474
+ /** Only this session. Omitted → every session in the store. */
6475
+ sessionId?: string;
6476
+ cursor?: string;
6477
+ /** Chunk objects to scan, not sessions returned. */
6478
+ limit?: number;
6479
+ }
6480
+ /**
6481
+ * Recording sessions in this app, grouped from the chunk keys.
6482
+ *
6483
+ * @example
6484
+ * const { sessions } = await admin.qaRecordings.ls({});
6485
+ * // → [{ sessionId, chunks, bytes, runs: [{ runId, keys }] }]
6486
+ */
6487
+ declare function ls(ctx: AdminContext, params?: QaRecordingsLsParams): Promise<QaRecordingsLsResult>;
6488
+ interface QaRecordingsExportParams {
6489
+ /** The recording session to render (32 hex characters). */
6490
+ sessionId: string;
6491
+ /** Window of the session, as epoch milliseconds. Both required: a replay is
6492
+ * a slice of a continuous recording, not a file. */
6493
+ from: number;
6494
+ to: number;
6495
+ /** Where the mp4 is written. Defaults to `assets`, public. */
6496
+ store?: string;
6497
+ access?: 'public' | 'private';
6498
+ /** Milliseconds to wait for the render. */
6499
+ timeoutMs?: number;
6500
+ }
6501
+ /**
6502
+ * Render a window of a recording session to an mp4 on the dev box.
6503
+ *
6504
+ * Blocks for the whole render — the clip plays in real time, then encodes and
6505
+ * uploads — and answers with where the video landed. Runs only on a Remy dev
6506
+ * box, since that is where the browser and ffmpeg are.
6507
+ *
6508
+ * @example
6509
+ * const { url } = await admin.qaRecordings.exportVideo({
6510
+ * sessionId, from: 1758000000000, to: 1758000042000,
6511
+ * });
6512
+ */
6513
+ declare function exportVideo(_ctx: AdminContext, params: QaRecordingsExportParams): Promise<QaRecordingsExportResult>;
6514
+
6515
+ type qaRecordings_QaRecordingsExportParams = QaRecordingsExportParams;
6516
+ type qaRecordings_QaRecordingsLsParams = QaRecordingsLsParams;
6517
+ declare const qaRecordings_RECORDING_STORE: typeof RECORDING_STORE;
6518
+ declare const qaRecordings_exportVideo: typeof exportVideo;
6519
+ declare const qaRecordings_ls: typeof ls;
6520
+ declare namespace qaRecordings {
6521
+ export { type qaRecordings_QaRecordingsExportParams as QaRecordingsExportParams, type qaRecordings_QaRecordingsLsParams as QaRecordingsLsParams, qaRecordings_RECORDING_STORE as RECORDING_STORE, qaRecordings_exportVideo as exportVideo, qaRecordings_ls as ls };
6522
+ }
6523
+
6402
6524
  /**
6403
6525
  * Release operations: list, get, by-commit, dashboard live-release, and the
6404
6526
  * composite waitForCommit op that owns the two poll loops.
@@ -7590,6 +7712,7 @@ declare function buildClient(ctx: AdminContext): {
7590
7712
  jewels: BoundOps<typeof jewels>;
7591
7713
  methods: BoundOps<typeof methods>;
7592
7714
  prerender: BoundOps<typeof prerender>;
7715
+ qaRecordings: BoundOps<typeof qaRecordings>;
7593
7716
  releases: BoundOps<typeof releases>;
7594
7717
  requests: BoundOps<typeof requests>;
7595
7718
  secrets: BoundOps<typeof secrets>;
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import path from "path";
10
10
 
11
11
  // src/config.ts
12
12
  var WORKSPACE_DIR = process.env["WORKSPACE_DIR"] || "/home/remy/workspace";
13
+ var SIDECAR_URL = process.env["REMY_SIDECAR_URL"] || "http://127.0.0.1:4388";
13
14
 
14
15
  // src/parseJsonConfig.ts
15
16
  import JSON5 from "json5";
@@ -2415,6 +2416,112 @@ function invalidate(ctx, params = {}) {
2415
2416
  );
2416
2417
  }
2417
2418
 
2419
+ // src/ops/qaRecordings.ts
2420
+ var qaRecordings_exports = {};
2421
+ __export(qaRecordings_exports, {
2422
+ RECORDING_STORE: () => RECORDING_STORE,
2423
+ exportVideo: () => exportVideo,
2424
+ ls: () => ls2
2425
+ });
2426
+ var RECORDING_STORE = "qa-recordings";
2427
+ async function ls2(ctx, params = {}) {
2428
+ const page = await ls(ctx, {
2429
+ store: RECORDING_STORE,
2430
+ access: "private",
2431
+ ...params.sessionId ? { prefix: `${params.sessionId}/` } : {},
2432
+ ...params.cursor ? { cursor: params.cursor } : {},
2433
+ ...params.limit ? { limit: params.limit } : {}
2434
+ });
2435
+ const sessions = /* @__PURE__ */ new Map();
2436
+ for (const entry of page.files) {
2437
+ const parts = entry.key.split("/");
2438
+ if (parts.length !== 3) {
2439
+ continue;
2440
+ }
2441
+ const [sessionId, runId] = parts;
2442
+ let session = sessions.get(sessionId);
2443
+ if (!session) {
2444
+ session = { sessionId, chunks: 0, bytes: 0, runs: [] };
2445
+ sessions.set(sessionId, session);
2446
+ }
2447
+ let run3 = session.runs.find((r) => r.runId === runId);
2448
+ if (!run3) {
2449
+ run3 = { runId, chunks: 0, bytes: 0, keys: [] };
2450
+ session.runs.push(run3);
2451
+ }
2452
+ const size = entry.size ?? 0;
2453
+ session.chunks += 1;
2454
+ session.bytes += size;
2455
+ run3.chunks += 1;
2456
+ run3.bytes += size;
2457
+ run3.keys.push(entry.key);
2458
+ if (entry.updatedAt) {
2459
+ if (!session.startedAt || entry.updatedAt < session.startedAt) {
2460
+ session.startedAt = entry.updatedAt;
2461
+ }
2462
+ if (!session.endedAt || entry.updatedAt > session.endedAt) {
2463
+ session.endedAt = entry.updatedAt;
2464
+ }
2465
+ }
2466
+ }
2467
+ for (const session of sessions.values()) {
2468
+ for (const run3 of session.runs) {
2469
+ run3.keys.sort((a, b) => seqOf(a) - seqOf(b));
2470
+ }
2471
+ }
2472
+ return {
2473
+ store: RECORDING_STORE,
2474
+ sessions: [...sessions.values()].sort(
2475
+ (a, b) => (b.endedAt ?? "").localeCompare(a.endedAt ?? "")
2476
+ ),
2477
+ ...page.cursor ? { cursor: page.cursor } : {}
2478
+ };
2479
+ }
2480
+ function seqOf(key) {
2481
+ const n = Number(
2482
+ key.split("/").pop()?.replace(/\.json$/, "")
2483
+ );
2484
+ return Number.isFinite(n) ? n : 0;
2485
+ }
2486
+ var DEFAULT_EXPORT_TIMEOUT_MS = 66e4;
2487
+ async function exportVideo(_ctx, params) {
2488
+ const body = {
2489
+ recordingSessionId: params.sessionId,
2490
+ startTs: params.from,
2491
+ endTs: params.to,
2492
+ ...params.store ? { store: params.store } : {},
2493
+ ...params.access ? { access: params.access } : {}
2494
+ };
2495
+ let res;
2496
+ try {
2497
+ res = await fetch(`${SIDECAR_URL}/export-recording`, {
2498
+ method: "POST",
2499
+ headers: { "Content-Type": "application/json" },
2500
+ body: JSON.stringify(body),
2501
+ signal: AbortSignal.timeout(
2502
+ params.timeoutMs ?? DEFAULT_EXPORT_TIMEOUT_MS
2503
+ )
2504
+ });
2505
+ } catch (err) {
2506
+ throw new Error(
2507
+ `Could not reach the sandbox at ${SIDECAR_URL} (${err instanceof Error ? err.message : String(err)}). Rendering a replay needs the dev box that recorded it \u2014 this command only works inside a Remy sandbox.`
2508
+ );
2509
+ }
2510
+ const payload = await res.json().catch(() => ({}));
2511
+ if (!res.ok) {
2512
+ throw new Error(
2513
+ typeof payload.error === "string" ? payload.error : `The sandbox refused the export (HTTP ${res.status}).`
2514
+ );
2515
+ }
2516
+ const status2 = payload.export;
2517
+ if (!status2) {
2518
+ throw new Error(
2519
+ "The sandbox accepted the export but reported no result. Check the box's tunnel logs."
2520
+ );
2521
+ }
2522
+ return status2;
2523
+ }
2524
+
2418
2525
  // src/ops/requests.ts
2419
2526
  var requests_exports = {};
2420
2527
  __export(requests_exports, {
@@ -2748,6 +2855,7 @@ function buildClient(ctx) {
2748
2855
  jewels: bindOps(jewels_exports, ctx),
2749
2856
  methods: bindOps(methods_exports, ctx),
2750
2857
  prerender: bindOps(prerender_exports, ctx),
2858
+ qaRecordings: bindOps(qaRecordings_exports, ctx),
2751
2859
  releases: bindOps(releases_exports, ctx),
2752
2860
  requests: bindOps(requests_exports, ctx),
2753
2861
  secrets: bindOps(secrets_exports, ctx),