@madewithremy/admin 0.1.24 → 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
@@ -11,6 +11,18 @@ interface AdminContext {
11
11
  apiKey: string;
12
12
  /** The app every operation is scoped to. */
13
13
  appId: string;
14
+ /**
15
+ * Who is acting, when that is not `appId` itself — set only when the caller
16
+ * has retargeted at another app (`--app`, or `forApp()`), and carrying the
17
+ * app they came FROM.
18
+ *
19
+ * Writes that record authorship attach it, so a cross-app issue says which
20
+ * app filed it without the caller having to remember a second flag. That
21
+ * matters more than it sounds: the information was always available here and
22
+ * being optional at the call site is precisely how it came to be omitted
23
+ * everywhere.
24
+ */
25
+ originAppId?: string;
14
26
  /** API origin, no trailing slash. */
15
27
  baseUrl: string;
16
28
  }
@@ -4648,7 +4660,7 @@ interface FilesLsParams {
4648
4660
  * prefix: 'images/',
4649
4661
  * });
4650
4662
  */
4651
- declare function ls(ctx: AdminContext, params: FilesLsParams): Promise<FilesLsResult>;
4663
+ declare function ls$1(ctx: AdminContext, params: FilesLsParams): Promise<FilesLsResult>;
4652
4664
  /**
4653
4665
  * Store-level summary: every store with object counts, total bytes, and
4654
4666
  * AV/PII scan status histograms.
@@ -4672,14 +4684,13 @@ type files_FileRef = FileRef;
4672
4684
  type files_FilesLsParams = FilesLsParams;
4673
4685
  type files_FilesPutParams = FilesPutParams;
4674
4686
  declare const files_fetchBytes: typeof fetchBytes;
4675
- declare const files_ls: typeof ls;
4676
4687
  declare const files_put: typeof put;
4677
4688
  declare const files_remove: typeof remove;
4678
4689
  declare const files_sign: typeof sign;
4679
4690
  declare const files_stat: typeof stat;
4680
4691
  declare const files_summary: typeof summary;
4681
4692
  declare namespace files {
4682
- 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 };
4683
4694
  }
4684
4695
 
4685
4696
  /**
@@ -5226,9 +5237,10 @@ interface IssuesCreateParams {
5226
5237
  /** Labels to file it under. Open set, at most 20, 64 chars each. */
5227
5238
  labels?: string[];
5228
5239
  /**
5229
- * The app filing this, when it isn't the app the issue lands in. Set it when
5230
- * targeting another app (`forApp`) so the thread records who wrote — the
5231
- * recipient's reply comes back as a comment on this same issue.
5240
+ * The app filing this, when it isn't the app the issue lands in. Defaults to
5241
+ * `ctx.originAppId` — set whenever the caller retargeted at another app — so
5242
+ * cross-app authorship is recorded without anyone opting in. Pass it
5243
+ * explicitly only to override that.
5232
5244
  */
5233
5245
  originAppId?: string;
5234
5246
  }
@@ -6386,6 +6398,129 @@ declare namespace prerender {
6386
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 };
6387
6399
  }
6388
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
+
6389
6524
  /**
6390
6525
  * Release operations: list, get, by-commit, dashboard live-release, and the
6391
6526
  * composite waitForCommit op that owns the two poll loops.
@@ -7531,7 +7666,14 @@ type BoundOps<T> = {
7531
7666
  declare function buildClient(ctx: AdminContext): {
7532
7667
  /** The resolved context this client is bound to. */
7533
7668
  context: Readonly<AdminContext>;
7534
- /** A sibling client for another app, sharing credentials. */
7669
+ /**
7670
+ * A sibling client for another app, sharing credentials.
7671
+ *
7672
+ * The app we came from becomes the origin, so a write through the returned
7673
+ * client records which app acted — the same rule the CLI's `--app` follows.
7674
+ * Chaining `forApp` twice keeps the FIRST app as the origin, since that is
7675
+ * the one whose workspace this code is running in.
7676
+ */
7535
7677
  forApp(appId: string): /*elided*/ any;
7536
7678
  agent: BoundOps<typeof agent>;
7537
7679
  analytics: BoundOps<typeof analytics>;
@@ -7570,6 +7712,7 @@ declare function buildClient(ctx: AdminContext): {
7570
7712
  jewels: BoundOps<typeof jewels>;
7571
7713
  methods: BoundOps<typeof methods>;
7572
7714
  prerender: BoundOps<typeof prerender>;
7715
+ qaRecordings: BoundOps<typeof qaRecordings>;
7573
7716
  releases: BoundOps<typeof releases>;
7574
7717
  requests: BoundOps<typeof requests>;
7575
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";
@@ -2116,6 +2117,9 @@ __export(issues_exports, {
2116
2117
  list: () => list8,
2117
2118
  reopen: () => reopen
2118
2119
  });
2120
+ function authorship(ctx) {
2121
+ return ctx.originAppId ? { originAppId: ctx.originAppId } : {};
2122
+ }
2119
2123
  function listQuery(params) {
2120
2124
  return qs({
2121
2125
  status: params.status,
@@ -2157,8 +2161,9 @@ function create2(ctx, params) {
2157
2161
  if (params.labels !== void 0) {
2158
2162
  requestBody.labels = params.labels;
2159
2163
  }
2160
- if (params.originAppId !== void 0) {
2161
- requestBody.originAppId = params.originAppId;
2164
+ const originAppId = params.originAppId ?? ctx.originAppId;
2165
+ if (originAppId !== void 0) {
2166
+ requestBody.originAppId = originAppId;
2162
2167
  }
2163
2168
  return call(
2164
2169
  ctx,
@@ -2172,13 +2177,14 @@ function comment(ctx, number, body) {
2172
2177
  ctx,
2173
2178
  "POST",
2174
2179
  `/_internal/v2/apps/${ctx.appId}/issues/${seg(number)}/comments`,
2175
- { body, authorKind: "agent" }
2180
+ { body, authorKind: "agent", ...authorship(ctx) }
2176
2181
  );
2177
2182
  }
2178
2183
  function close(ctx, number, comment2) {
2179
2184
  const requestBody = {
2180
2185
  status: "closed",
2181
- authorKind: "agent"
2186
+ authorKind: "agent",
2187
+ ...authorship(ctx)
2182
2188
  };
2183
2189
  if (comment2 !== void 0) {
2184
2190
  requestBody.comment = comment2;
@@ -2195,11 +2201,14 @@ function reopen(ctx, number) {
2195
2201
  ctx,
2196
2202
  "POST",
2197
2203
  `/_internal/v2/apps/${ctx.appId}/issues/${seg(number)}/update`,
2198
- { status: "open", authorKind: "agent" }
2204
+ { status: "open", authorKind: "agent", ...authorship(ctx) }
2199
2205
  );
2200
2206
  }
2201
2207
  function edit(ctx, number, params) {
2202
- const requestBody = { authorKind: "agent" };
2208
+ const requestBody = {
2209
+ authorKind: "agent",
2210
+ ...authorship(ctx)
2211
+ };
2203
2212
  if (params.title !== void 0) {
2204
2213
  requestBody.title = params.title;
2205
2214
  }
@@ -2407,6 +2416,112 @@ function invalidate(ctx, params = {}) {
2407
2416
  );
2408
2417
  }
2409
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
+
2410
2525
  // src/ops/requests.ts
2411
2526
  var requests_exports = {};
2412
2527
  __export(requests_exports, {
@@ -2694,9 +2809,20 @@ function buildClient(ctx) {
2694
2809
  return {
2695
2810
  /** The resolved context this client is bound to. */
2696
2811
  context: ctx,
2697
- /** A sibling client for another app, sharing credentials. */
2812
+ /**
2813
+ * A sibling client for another app, sharing credentials.
2814
+ *
2815
+ * The app we came from becomes the origin, so a write through the returned
2816
+ * client records which app acted — the same rule the CLI's `--app` follows.
2817
+ * Chaining `forApp` twice keeps the FIRST app as the origin, since that is
2818
+ * the one whose workspace this code is running in.
2819
+ */
2698
2820
  forApp(appId) {
2699
- return buildClient({ ...ctx, appId });
2821
+ return buildClient({
2822
+ ...ctx,
2823
+ appId,
2824
+ originAppId: appId === ctx.appId ? ctx.originAppId : ctx.originAppId ?? ctx.appId
2825
+ });
2700
2826
  },
2701
2827
  agent: bindOps(agent_exports, ctx),
2702
2828
  analytics: bindOps(analytics_exports, ctx),
@@ -2729,6 +2855,7 @@ function buildClient(ctx) {
2729
2855
  jewels: bindOps(jewels_exports, ctx),
2730
2856
  methods: bindOps(methods_exports, ctx),
2731
2857
  prerender: bindOps(prerender_exports, ctx),
2858
+ qaRecordings: bindOps(qaRecordings_exports, ctx),
2732
2859
  releases: bindOps(releases_exports, ctx),
2733
2860
  requests: bindOps(requests_exports, ctx),
2734
2861
  secrets: bindOps(secrets_exports, ctx),