@madewithremy/admin 0.1.25 → 0.1.27

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
@@ -635,16 +635,17 @@ interface EventsTailFrame {
635
635
 
636
636
  interface EventsScopeParams {
637
637
  /**
638
- * Environment scope to inspect: `live` (default), `preview:<releaseId>`,
639
- * or `dev:<sessionId>` — publishes and subscriptions never cross scopes,
640
- * so a tunnel session's events are visible only under its `dev:` scope.
638
+ * Environment scope to inspect: `live` (the default for reads),
639
+ * `preview:<releaseId>`, or `dev:<sessionId>` — publishes and subscriptions
640
+ * never cross scopes, so a dev session's events are visible only under its
641
+ * `dev:` scope. A publish without one goes to the caller's own dev session.
641
642
  */
642
643
  scope?: string;
643
644
  }
644
645
  interface EventsPublishParams extends EventsScopeParams {
645
646
  /** Exact channel name(s) — no wildcards anywhere in app events. */
646
647
  channels: string | string[];
647
- /** The payload subscribers receive (≤32k serialized characters). */
648
+ /** The payload subscribers receive (≤256k serialized characters). */
648
649
  data: unknown;
649
650
  }
650
651
  /**
@@ -665,10 +666,13 @@ declare function channelsList(ctx: AdminContext, params?: EventsScopeParams): Pr
665
666
  * same validation, same delivery, same metering. Lets a frontend subscriber
666
667
  * be verified before the backend trigger exists.
667
668
  *
668
- * `delivered` counts live subscriber connections per published channel;
669
- * `0` is normal (nobody listening), never an error.
669
+ * Without `scope` it goes to the caller's own dev session, the preview they
670
+ * are building in; `scope: 'live'` reaches real users. The result's `scope`
671
+ * says where it went. `delivered` counts live subscriber connections per
672
+ * published channel; `0` is normal (nobody listening), never an error.
670
673
  *
671
- * @throws AdminApiError `invalid_channels` / `payload_too_large` (400).
674
+ * @throws AdminApiError `invalid_channels` / `payload_too_large` (400), and
675
+ * `no_dev_session` (400) with no `scope` when the caller has no dev session.
672
676
  * @example
673
677
  * const { delivered } = await admin.events.publish({
674
678
  * channels: 'jobs:usr_123',
@@ -4660,7 +4664,7 @@ interface FilesLsParams {
4660
4664
  * prefix: 'images/',
4661
4665
  * });
4662
4666
  */
4663
- declare function ls(ctx: AdminContext, params: FilesLsParams): Promise<FilesLsResult>;
4667
+ declare function ls$1(ctx: AdminContext, params: FilesLsParams): Promise<FilesLsResult>;
4664
4668
  /**
4665
4669
  * Store-level summary: every store with object counts, total bytes, and
4666
4670
  * AV/PII scan status histograms.
@@ -4684,14 +4688,13 @@ type files_FileRef = FileRef;
4684
4688
  type files_FilesLsParams = FilesLsParams;
4685
4689
  type files_FilesPutParams = FilesPutParams;
4686
4690
  declare const files_fetchBytes: typeof fetchBytes;
4687
- declare const files_ls: typeof ls;
4688
4691
  declare const files_put: typeof put;
4689
4692
  declare const files_remove: typeof remove;
4690
4693
  declare const files_sign: typeof sign;
4691
4694
  declare const files_stat: typeof stat;
4692
4695
  declare const files_summary: typeof summary;
4693
4696
  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 };
4697
+ 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
4698
  }
4696
4699
 
4697
4700
  /**
@@ -6399,6 +6402,129 @@ declare namespace prerender {
6399
6402
  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
6403
  }
6401
6404
 
6405
+ /**
6406
+ * Response types for browser-QA replay recordings.
6407
+ *
6408
+ * Chunks are ordinary private files in the app's `qa-recordings` store, keyed
6409
+ * `{sessionId}/{runId}/{seq}.json`, so `ls` here is a grouped view over
6410
+ * `files.ls` rather than its own endpoint. Transcribed from youai-api:
6411
+ * src/http/routes/V2Apps/develop/recordings.ts
6412
+ * src/common/Recordings/sessions.ts
6413
+ *
6414
+ * Export is the exception — it renders on the dev box (see ../ops/recordings).
6415
+ */
6416
+ /** One document lifetime within a session: a page load and everything after it
6417
+ * until the next one. Its lowest-seq chunk carries the rrweb FullSnapshot. */
6418
+ interface QaRecordingRun {
6419
+ runId: string;
6420
+ chunks: number;
6421
+ bytes: number;
6422
+ /** Store-relative keys, in seq order. */
6423
+ keys: string[];
6424
+ }
6425
+ /** One recording session — a single tunnel process's continuous recording. */
6426
+ interface QaRecordingSession {
6427
+ sessionId: string;
6428
+ chunks: number;
6429
+ bytes: number;
6430
+ /** ISO-8601, from the chunk objects' last-modified times. */
6431
+ startedAt?: string;
6432
+ endedAt?: string;
6433
+ runs: QaRecordingRun[];
6434
+ }
6435
+ interface QaRecordingsLsResult {
6436
+ store: string;
6437
+ sessions: QaRecordingSession[];
6438
+ /** Present when the listing was truncated — pass it back as `cursor`. */
6439
+ cursor?: string;
6440
+ }
6441
+ interface QaRecordingsExportResult {
6442
+ jobId: string;
6443
+ status: 'completed' | 'failed' | 'cancelled';
6444
+ /** Public target only. A private one gives `store`/`key`; sign it with
6445
+ * `files sign` when you want a link. */
6446
+ url?: string;
6447
+ store?: string;
6448
+ key?: string;
6449
+ width?: number;
6450
+ height?: number;
6451
+ /** Length of the rendered clip, dead air already collapsed. */
6452
+ durationMs?: number;
6453
+ error?: string;
6454
+ errorCode?: string;
6455
+ }
6456
+
6457
+ /**
6458
+ * Browser-QA replay recordings: list what the QA agent recorded, and render a
6459
+ * replay to an mp4.
6460
+ *
6461
+ * `ls` is a grouped view over `files.ls` — a replay chunk is an ordinary
6462
+ * private file in the app's `qa-recordings` store and needs no endpoint of its
6463
+ * own.
6464
+ *
6465
+ * `export` is the one operation in this SDK that does NOT go through the
6466
+ * management API. Rendering a replay means playing it in a real browser and
6467
+ * encoding the frames, and the only machine set up to do that is the dev box
6468
+ * the recording was made on: it already has the headless Chrome, the rrweb
6469
+ * replayer and ffmpeg. So this talks to the sandbox's local sidecar, and says
6470
+ * so plainly when there isn't one. (CLI-only paths are established here —
6471
+ * `methods invoke --stream`, `jewels export --file` — but this is the first
6472
+ * that needs local compute rather than local disk.)
6473
+ */
6474
+
6475
+ /** The app store QA replay chunks are written to (private). */
6476
+ declare const RECORDING_STORE = "qa-recordings";
6477
+ interface QaRecordingsLsParams {
6478
+ /** Only this session. Omitted → every session in the store. */
6479
+ sessionId?: string;
6480
+ cursor?: string;
6481
+ /** Chunk objects to scan, not sessions returned. */
6482
+ limit?: number;
6483
+ }
6484
+ /**
6485
+ * Recording sessions in this app, grouped from the chunk keys.
6486
+ *
6487
+ * @example
6488
+ * const { sessions } = await admin.qaRecordings.ls({});
6489
+ * // → [{ sessionId, chunks, bytes, runs: [{ runId, keys }] }]
6490
+ */
6491
+ declare function ls(ctx: AdminContext, params?: QaRecordingsLsParams): Promise<QaRecordingsLsResult>;
6492
+ interface QaRecordingsExportParams {
6493
+ /** The recording session to render (32 hex characters). */
6494
+ sessionId: string;
6495
+ /** Window of the session, as epoch milliseconds. Both required: a replay is
6496
+ * a slice of a continuous recording, not a file. */
6497
+ from: number;
6498
+ to: number;
6499
+ /** Where the mp4 is written. Defaults to `assets`, public. */
6500
+ store?: string;
6501
+ access?: 'public' | 'private';
6502
+ /** Milliseconds to wait for the render. */
6503
+ timeoutMs?: number;
6504
+ }
6505
+ /**
6506
+ * Render a window of a recording session to an mp4 on the dev box.
6507
+ *
6508
+ * Blocks for the whole render — the clip plays in real time, then encodes and
6509
+ * uploads — and answers with where the video landed. Runs only on a Remy dev
6510
+ * box, since that is where the browser and ffmpeg are.
6511
+ *
6512
+ * @example
6513
+ * const { url } = await admin.qaRecordings.exportVideo({
6514
+ * sessionId, from: 1758000000000, to: 1758000042000,
6515
+ * });
6516
+ */
6517
+ declare function exportVideo(_ctx: AdminContext, params: QaRecordingsExportParams): Promise<QaRecordingsExportResult>;
6518
+
6519
+ type qaRecordings_QaRecordingsExportParams = QaRecordingsExportParams;
6520
+ type qaRecordings_QaRecordingsLsParams = QaRecordingsLsParams;
6521
+ declare const qaRecordings_RECORDING_STORE: typeof RECORDING_STORE;
6522
+ declare const qaRecordings_exportVideo: typeof exportVideo;
6523
+ declare const qaRecordings_ls: typeof ls;
6524
+ declare namespace qaRecordings {
6525
+ 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 };
6526
+ }
6527
+
6402
6528
  /**
6403
6529
  * Release operations: list, get, by-commit, dashboard live-release, and the
6404
6530
  * composite waitForCommit op that owns the two poll loops.
@@ -7590,6 +7716,7 @@ declare function buildClient(ctx: AdminContext): {
7590
7716
  jewels: BoundOps<typeof jewels>;
7591
7717
  methods: BoundOps<typeof methods>;
7592
7718
  prerender: BoundOps<typeof prerender>;
7719
+ qaRecordings: BoundOps<typeof qaRecordings>;
7593
7720
  releases: BoundOps<typeof releases>;
7594
7721
  requests: BoundOps<typeof requests>;
7595
7722
  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),