@madewithremy/admin 0.1.5 → 0.1.7

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/prod.js CHANGED
@@ -228,6 +228,16 @@ function parseCommand(spec, argv) {
228
228
 
229
229
  // src/http.ts
230
230
  var REQUEST_TIMEOUT_MS = 3e4;
231
+ var TRANSIENT_GRACE_MS = 2 * 6e4;
232
+ function isTransientError(err) {
233
+ if (err instanceof AdminApiError) {
234
+ return err.status >= 500;
235
+ }
236
+ if (err instanceof AdminTimeoutError) {
237
+ return true;
238
+ }
239
+ return err instanceof TypeError;
240
+ }
231
241
  var REPORT_TIMEOUT_MS = 6e4;
232
242
  function seg(value) {
233
243
  return encodeURIComponent(String(value));
@@ -913,7 +923,9 @@ function summarizeRelease(release) {
913
923
  }
914
924
  return {
915
925
  ...summary2,
916
- error: buildFailureReason(release) ?? "Build failed (no error entry in the build log)"
926
+ error: release.failureReason ?? buildFailureReason(release) ?? "Build failed (no error entry in the build log)",
927
+ ...release.failureKind ? { failureKind: release.failureKind } : {},
928
+ ...release.failurePhase ? { failurePhase: release.failurePhase } : {}
917
929
  };
918
930
  }
919
931
  var POLL_MS = 3e3;
@@ -1164,6 +1176,12 @@ the exit code so you can branch on $? without parsing:
1164
1176
  Any other failure (bad arguments, auth, API error) exits 10, so exit 1 always
1165
1177
  means the build itself failed.
1166
1178
 
1179
+ Every release shape ('get', 'list', 'status', 'wait') carries why a failed
1180
+ build failed: 'failureReason' (message), 'failurePhase' (the stage that was
1181
+ running), and 'failureKind' \u2014 'build' (the code), 'unbuildable' (commit is
1182
+ not a readable app), or 'interrupted' (a platform restart killed the build;
1183
+ a retry is already running, so don't re-push).
1184
+
1167
1185
  Examples:
1168
1186
  git push origin HEAD && remy-admin releases wait
1169
1187
  remy-admin releases wait --commit 91ca67a --timeout 600
@@ -2919,12 +2937,26 @@ async function waitForIngest(ctx, params) {
2919
2937
  }
2920
2938
  const wanted = new Set(documentIds);
2921
2939
  const start = Date.now();
2940
+ let transientSince = null;
2922
2941
  for (; ; ) {
2923
- const { documents: docs } = await call(
2924
- ctx,
2925
- "GET",
2926
- `${base(ctx.appId)}/documents?slug=${encodeURIComponent(slug)}`
2927
- );
2942
+ let docs;
2943
+ try {
2944
+ docs = await allDocuments(ctx, { slug, ids: documentIds });
2945
+ transientSince = null;
2946
+ } catch (err) {
2947
+ if (!isTransientError(err)) {
2948
+ throw err;
2949
+ }
2950
+ transientSince ??= Date.now();
2951
+ if (Date.now() - transientSince > TRANSIENT_GRACE_MS) {
2952
+ throw err;
2953
+ }
2954
+ onProgress?.(
2955
+ `retrying after a gateway error\u2026 (${Math.round((Date.now() - start) / 1e3)}s)`
2956
+ );
2957
+ await sleep(POLL_MS2);
2958
+ continue;
2959
+ }
2928
2960
  const tracked = (docs ?? []).filter((d) => wanted.has(d.id));
2929
2961
  const pending = tracked.filter((d) => d.status === "processing");
2930
2962
  const failed = tracked.filter((d) => d.status === "error");
@@ -2949,12 +2981,41 @@ async function waitForIngest(ctx, params) {
2949
2981
  await sleep(POLL_MS2);
2950
2982
  }
2951
2983
  }
2984
+ var DOCUMENTS_IDS_MAX = 200;
2952
2985
  function documents(ctx, params) {
2953
2986
  return call(
2954
2987
  ctx,
2955
2988
  "GET",
2956
- `${base(ctx.appId)}/documents${qs({ slug: params.slug, candidate: params.candidate || void 0 })}`
2957
- );
2989
+ `${base(ctx.appId)}/documents${qs({
2990
+ slug: params.slug,
2991
+ candidate: params.candidate || void 0,
2992
+ ids: params.ids?.length ? params.ids.join(",") : void 0,
2993
+ limit: params.limit,
2994
+ cursor: params.cursor
2995
+ })}`
2996
+ );
2997
+ }
2998
+ async function allDocuments(ctx, params) {
2999
+ const groups = params.ids ? Array.from(
3000
+ { length: Math.ceil(params.ids.length / DOCUMENTS_IDS_MAX) },
3001
+ (_, i) => params.ids.slice(i * DOCUMENTS_IDS_MAX, (i + 1) * DOCUMENTS_IDS_MAX)
3002
+ ) : [void 0];
3003
+ const out2 = [];
3004
+ for (const ids of groups) {
3005
+ let cursor;
3006
+ do {
3007
+ const page = await documents(ctx, {
3008
+ slug: params.slug,
3009
+ candidate: params.candidate,
3010
+ ids,
3011
+ limit: 1e3,
3012
+ cursor
3013
+ });
3014
+ out2.push(...page.documents ?? []);
3015
+ cursor = page.nextCursor ?? void 0;
3016
+ } while (cursor);
3017
+ }
3018
+ return out2;
2958
3019
  }
2959
3020
  function list8(ctx) {
2960
3021
  return call(ctx, "GET", base(ctx.appId));
@@ -2996,7 +3057,7 @@ function configGet(ctx, slug) {
2996
3057
  );
2997
3058
  }
2998
3059
  function configSet(ctx, params) {
2999
- const { slug, ingest, retrieval } = params;
3060
+ const { slug, ingest, retrieval, placement } = params;
3000
3061
  return call(
3001
3062
  ctx,
3002
3063
  "POST",
@@ -3004,10 +3065,75 @@ function configSet(ctx, params) {
3004
3065
  {
3005
3066
  slug,
3006
3067
  ...ingest ? { ingest } : {},
3007
- ...retrieval ? { retrieval } : {}
3068
+ ...retrieval ? { retrieval } : {},
3069
+ ...placement !== void 0 ? { placement } : {}
3008
3070
  }
3009
3071
  );
3010
3072
  }
3073
+ function create2(ctx, params) {
3074
+ const { slug, name, ingest, placement } = params;
3075
+ return call(ctx, "POST", base(ctx.appId), {
3076
+ slug,
3077
+ ...name ? { name } : {},
3078
+ ...ingest ? { ingest } : {},
3079
+ ...placement !== void 0 ? { placement } : {}
3080
+ });
3081
+ }
3082
+ function move(ctx, params) {
3083
+ return call(ctx, "POST", `${base(ctx.appId)}/move`, {
3084
+ slug: params.slug,
3085
+ placement: params.placement
3086
+ });
3087
+ }
3088
+ async function waitForMove(ctx, params) {
3089
+ const timeoutMs = params.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
3090
+ const start = Date.now();
3091
+ let transientSince = null;
3092
+ for (; ; ) {
3093
+ let source;
3094
+ try {
3095
+ const { dataSources } = await list8(ctx);
3096
+ source = dataSources.find((s) => s.slug === params.slug);
3097
+ transientSince = null;
3098
+ } catch (err) {
3099
+ if (!isTransientError(err)) {
3100
+ throw err;
3101
+ }
3102
+ transientSince ??= Date.now();
3103
+ if (Date.now() - transientSince > TRANSIENT_GRACE_MS) {
3104
+ throw err;
3105
+ }
3106
+ params.onProgress?.(
3107
+ `retrying after a gateway error\u2026 (${Math.round((Date.now() - start) / 1e3)}s)`
3108
+ );
3109
+ await sleep(POLL_MS2);
3110
+ continue;
3111
+ }
3112
+ if (!source) {
3113
+ throw new Error(`Data source "${params.slug}" not found.`);
3114
+ }
3115
+ const migration = source.migration;
3116
+ if (!migration) {
3117
+ return { status: "done", source };
3118
+ }
3119
+ if (migration.error) {
3120
+ return { status: "failed", source, error: migration.error };
3121
+ }
3122
+ if (Date.now() - start > timeoutMs) {
3123
+ return {
3124
+ status: "timeout",
3125
+ source,
3126
+ error: `Timed out after ${Math.round(timeoutMs / 1e3)}s with ${migration.copied}/${migration.total} documents copied`
3127
+ };
3128
+ }
3129
+ params.onProgress?.(
3130
+ `moving\u2026 ${migration.copied}/${migration.total} documents (${Math.round(
3131
+ (Date.now() - start) / 1e3
3132
+ )}s)`
3133
+ );
3134
+ await sleep(POLL_MS2);
3135
+ }
3136
+ }
3011
3137
  function revectorize(ctx, params) {
3012
3138
  const { slug, ingest } = params;
3013
3139
  return call(
@@ -3065,8 +3191,8 @@ function deleteSource(ctx, slug) {
3065
3191
  // src/commands/dataSources.ts
3066
3192
  var DEFAULT_SOURCE = "default";
3067
3193
  var POLL_MS3 = 3e3;
3068
- var CONFIG_FLAGS = {
3069
- // Pinned — changing any of these needs a re-vectorize.
3194
+ var INGEST_FLAGS = {
3195
+ chunking: { type: "string" },
3070
3196
  "max-chars": { type: "number", min: 200 },
3071
3197
  "min-chars": { type: "number", min: 0 },
3072
3198
  "drop-blocks": { type: "string" },
@@ -3074,9 +3200,12 @@ var CONFIG_FLAGS = {
3074
3200
  "contextual-model": { type: "string" },
3075
3201
  "describe-images": { type: "string" },
3076
3202
  "embedding-model": { type: "string" },
3203
+ "embedding-dimensions": { type: "number", min: 1 },
3077
3204
  "image-model": { type: "string" },
3078
- "extraction-model": { type: "string" },
3079
- // Live — take effect on the next search, no rebuild.
3205
+ "extraction-model": { type: "string" }
3206
+ };
3207
+ var CHUNKING_STRATEGIES = ["structural", "whole"];
3208
+ var RETRIEVAL_FLAGS = {
3080
3209
  rerank: { type: "string" },
3081
3210
  // Which cross-encoder reranks. Live, not pinned: a reranker runs at query
3082
3211
  // time over text already in the index, so switching costs nothing and is
@@ -3085,6 +3214,10 @@ var CONFIG_FLAGS = {
3085
3214
  hybrid: { type: "string" },
3086
3215
  "top-k": { type: "number", min: 1 }
3087
3216
  };
3217
+ var CONFIG_FLAGS = {
3218
+ ...INGEST_FLAGS,
3219
+ ...RETRIEVAL_FLAGS
3220
+ };
3088
3221
  var dataSourcesSpecs = {
3089
3222
  "datasources add": {
3090
3223
  usage: "Usage: remy-admin datasources add [--source <slug>] [--metadata <k=v,...>] [--wait] [--timeout <sec>] <file...>",
@@ -3124,9 +3257,37 @@ var dataSourcesSpecs = {
3124
3257
  highlight: { type: "boolean" }
3125
3258
  }
3126
3259
  },
3260
+ "datasources create": {
3261
+ usage: "Usage: remy-admin datasources create --source <slug> [--name <name>] [--placement <resource-id|shared>] [ingest settings...]",
3262
+ flags: {
3263
+ source: { type: "string" },
3264
+ name: { type: "string" },
3265
+ placement: { type: "string" },
3266
+ ...INGEST_FLAGS
3267
+ }
3268
+ },
3127
3269
  "datasources config": {
3128
- usage: "Usage: remy-admin datasources config [--source <slug>] [settings...]",
3129
- flags: { source: { type: "string" }, ...CONFIG_FLAGS }
3270
+ usage: "Usage: remy-admin datasources config [--source <slug>] [--placement <resource-id|shared>] [settings...]",
3271
+ flags: {
3272
+ source: { type: "string" },
3273
+ // Where the corpus lives: a dedicated resource id from `infra list`, or
3274
+ // `shared`. A populated source starts a background move (see `move`).
3275
+ placement: { type: "string" },
3276
+ ...CONFIG_FLAGS
3277
+ }
3278
+ },
3279
+ "datasources move": {
3280
+ usage: "Usage: remy-admin datasources move [--source <slug>] --to <resource-id|shared> [--wait] [--timeout <sec>]",
3281
+ flags: {
3282
+ source: { type: "string" },
3283
+ to: { type: "string" },
3284
+ wait: { type: "boolean" },
3285
+ timeout: { type: "string" }
3286
+ },
3287
+ requireAnyOf: {
3288
+ flags: ["to"],
3289
+ message: "--to is required: a resource id from `infra list`, or `shared`."
3290
+ }
3130
3291
  },
3131
3292
  "datasources revectorize": {
3132
3293
  usage: "Usage: remy-admin datasources revectorize [--source <slug>] [settings...] [--wait]",
@@ -3215,10 +3376,10 @@ async function dataSourcesList(ctx) {
3215
3376
  }
3216
3377
  async function dataSourcesStatus(ctx, a) {
3217
3378
  const slug = sourceOf(a);
3218
- const { documents: documents2 } = await documents(ctx, { slug });
3379
+ const documents2 = await allDocuments(ctx, { slug });
3219
3380
  out({
3220
3381
  dataSource: slug,
3221
- documents: (documents2 ?? []).map(summarize)
3382
+ documents: documents2.map(summarize)
3222
3383
  });
3223
3384
  }
3224
3385
  async function dataSourcesRm(ctx, a) {
@@ -3327,6 +3488,15 @@ function triState(a, flag, key) {
3327
3488
  }
3328
3489
  function configFromFlags(a) {
3329
3490
  const chunking = {};
3491
+ const strategy = a.str("chunking");
3492
+ if (strategy !== void 0) {
3493
+ if (!CHUNKING_STRATEGIES.includes(strategy)) {
3494
+ fatal(
3495
+ `--chunking must be one of: ${CHUNKING_STRATEGIES.join(", ")} (got "${strategy}").`
3496
+ );
3497
+ }
3498
+ chunking.strategy = strategy;
3499
+ }
3330
3500
  if (a.num("max-chars") !== void 0) {
3331
3501
  chunking.maxChars = a.num("max-chars");
3332
3502
  }
@@ -3346,6 +3516,7 @@ function configFromFlags(a) {
3346
3516
  ...a.str("image-model") ? { modelId: a.str("image-model") } : {}
3347
3517
  };
3348
3518
  const embeddingModel = a.str("embedding-model");
3519
+ const embeddingDimensions = a.num("embedding-dimensions");
3349
3520
  const extractionModel = a.str("extraction-model");
3350
3521
  const ingest = {};
3351
3522
  if (Object.keys(chunking).length) {
@@ -3357,8 +3528,11 @@ function configFromFlags(a) {
3357
3528
  if (Object.keys(images).length) {
3358
3529
  ingest.images = images;
3359
3530
  }
3360
- if (embeddingModel) {
3361
- ingest.embedding = { modelId: embeddingModel };
3531
+ if (embeddingModel || embeddingDimensions !== void 0) {
3532
+ ingest.embedding = {
3533
+ ...embeddingModel ? { modelId: embeddingModel } : {},
3534
+ ...embeddingDimensions !== void 0 ? { dimensions: embeddingDimensions } : {}
3535
+ };
3362
3536
  }
3363
3537
  if (extractionModel) {
3364
3538
  ingest.extraction = { modelId: extractionModel };
@@ -3381,21 +3555,80 @@ function configFromFlags(a) {
3381
3555
  retrieval: Object.keys(retrieval).length ? retrieval : void 0
3382
3556
  };
3383
3557
  }
3558
+ var placementOf = (a) => {
3559
+ const flag = a.str("placement");
3560
+ return flag === void 0 ? void 0 : flag === "shared" ? "shared" : { resourceId: flag };
3561
+ };
3562
+ var MOVE_NOTE = "Moving in the background; `datasources list` shows progress, or re-run with `datasources move --wait`.";
3563
+ async function dataSourcesCreate(ctx, a) {
3564
+ const slug = a.str("source");
3565
+ if (!slug) {
3566
+ fatal("--source is required.");
3567
+ }
3568
+ const { ingest } = configFromFlags(a);
3569
+ const name = a.str("name");
3570
+ const placement = placementOf(a);
3571
+ const result = await create2(ctx, {
3572
+ slug,
3573
+ ...name ? { name } : {},
3574
+ ...ingest ? { ingest } : {},
3575
+ ...placement !== void 0 ? { placement } : {}
3576
+ });
3577
+ out(
3578
+ result.migration && !result.migration.error ? { ...result, note: MOVE_NOTE } : result
3579
+ );
3580
+ }
3384
3581
  async function dataSourcesConfig(ctx, a) {
3385
3582
  const slug = sourceOf(a);
3386
3583
  const { ingest, retrieval } = configFromFlags(a);
3387
- if (!ingest && !retrieval) {
3584
+ const placement = placementOf(a);
3585
+ if (!ingest && !retrieval && placement === void 0) {
3388
3586
  out(await configGet(ctx, slug));
3389
3587
  return;
3390
3588
  }
3589
+ const result = await configSet(ctx, {
3590
+ slug,
3591
+ ...ingest ? { ingest } : {},
3592
+ ...retrieval ? { retrieval } : {},
3593
+ ...placement !== void 0 ? { placement } : {}
3594
+ });
3391
3595
  out(
3392
- await configSet(ctx, {
3393
- slug,
3394
- ...ingest ? { ingest } : {},
3395
- ...retrieval ? { retrieval } : {}
3396
- })
3596
+ result.placementChanged && result.migration && !result.migration.error ? { ...result, note: MOVE_NOTE } : result
3397
3597
  );
3398
3598
  }
3599
+ async function dataSourcesMove(ctx, a) {
3600
+ const slug = sourceOf(a);
3601
+ const to = a.str("to");
3602
+ const placement = to === "shared" ? "shared" : { resourceId: to };
3603
+ const started = await move(ctx, { slug, placement });
3604
+ if (!started.migration || !a.bool("wait")) {
3605
+ out({
3606
+ dataSource: slug,
3607
+ ...started,
3608
+ ...started.migration ? { note: MOVE_NOTE } : {}
3609
+ });
3610
+ return;
3611
+ }
3612
+ const timeoutSec = a.num("timeout");
3613
+ const result = await waitForMove(ctx, {
3614
+ slug,
3615
+ ...timeoutSec ? { timeoutMs: timeoutSec * 1e3 } : {},
3616
+ onProgress: progress
3617
+ });
3618
+ out({
3619
+ dataSource: slug,
3620
+ status: result.status,
3621
+ placement: result.source.placement,
3622
+ migration: result.source.migration,
3623
+ ..."error" in result ? { error: result.error } : {}
3624
+ });
3625
+ if (result.status === "failed") {
3626
+ process.exit(EXIT.buildFailed);
3627
+ }
3628
+ if (result.status === "timeout") {
3629
+ process.exit(EXIT.timeout);
3630
+ }
3631
+ }
3399
3632
  async function dataSourcesRevectorize(ctx, a) {
3400
3633
  const slug = sourceOf(a);
3401
3634
  const { ingest } = configFromFlags(a);
@@ -3411,12 +3644,12 @@ async function dataSourcesRevectorize(ctx, a) {
3411
3644
  const timeoutMs = timeoutSec ? timeoutSec * 1e3 : DEFAULT_WAIT_TIMEOUT_MS;
3412
3645
  const start = Date.now();
3413
3646
  for (; ; ) {
3414
- const { documents: documents2 } = await documents(ctx, {
3647
+ const documents2 = await allDocuments(ctx, {
3415
3648
  slug,
3416
3649
  candidate: true
3417
3650
  });
3418
- const pending = (documents2 ?? []).filter((d) => d.status === "processing");
3419
- const failed = (documents2 ?? []).filter((d) => d.status === "error");
3651
+ const pending = documents2.filter((d) => d.status === "processing");
3652
+ const failed = documents2.filter((d) => d.status === "error");
3420
3653
  if (pending.length === 0) {
3421
3654
  out({
3422
3655
  dataSource: slug,
@@ -3469,7 +3702,9 @@ var dataSourcesHandlers = {
3469
3702
  "datasources status": dataSourcesStatus,
3470
3703
  "datasources rm": dataSourcesRm,
3471
3704
  "datasources search": dataSourcesSearch,
3705
+ "datasources create": dataSourcesCreate,
3472
3706
  "datasources config": dataSourcesConfig,
3707
+ "datasources move": dataSourcesMove,
3473
3708
  "datasources revectorize": dataSourcesRevectorize,
3474
3709
  "datasources promote": dataSourcesPromote,
3475
3710
  "datasources drop": dataSourcesDrop,
@@ -3486,7 +3721,9 @@ Subcommands:
3486
3721
  status Show per-document ingest state
3487
3722
  rm Remove a document and its vectors
3488
3723
  search Query a corpus \u2014 useful to sanity-check one you just built
3724
+ create Create an empty source, optionally on dedicated capacity
3489
3725
  config Show or change how a corpus is processed and searched
3726
+ move Move a corpus between shared and dedicated capacity, data intact
3490
3727
  revectorize Rebuild a corpus under new settings, alongside the live one
3491
3728
  promote Make a rebuilt version live
3492
3729
  drop Discard a candidate or a superseded version
@@ -3498,7 +3735,9 @@ Usage:
3498
3735
  remy-admin datasources status [--source <slug>]
3499
3736
  remy-admin datasources rm [--source <slug>] --document <id>
3500
3737
  remy-admin datasources search [--source <slug>] [search options] <query>
3501
- remy-admin datasources config [--source <slug>] [settings...]
3738
+ remy-admin datasources create --source <slug> [--name <name>] [--placement <resource-id|shared>] [rebuild settings...]
3739
+ remy-admin datasources config [--source <slug>] [--placement <resource-id|shared>] [settings...]
3740
+ remy-admin datasources move [--source <slug>] --to <resource-id|shared> [--wait] [--timeout <sec>]
3502
3741
  remy-admin datasources revectorize [--source <slug>] [settings...] [--wait]
3503
3742
  remy-admin datasources promote [--source <slug>] [--force]
3504
3743
  remy-admin datasources drop [--source <slug>] [--version <n>]
@@ -3540,6 +3779,13 @@ Tuning a corpus:
3540
3779
  reprocessed. Changing these on a corpus that already has documents is
3541
3780
  REJECTED; use \`revectorize\` instead, which builds a new version alongside
3542
3781
  the live one so search never degrades:
3782
+ --chunking <structural|whole>
3783
+ structural (default) splits on headings, blocks
3784
+ and paragraphs. whole embeds each document as one
3785
+ chunk: for corpora of short records (articles,
3786
+ tickets, product rows) where the document is the
3787
+ unit you want back. Documents over --max-chars
3788
+ (default 24000 for whole) fall back to structural.
3543
3789
  --max-chars <n> Target chunk size (default 2000)
3544
3790
  --min-chars <n> Merge chunks smaller than this (default 120)
3545
3791
  --drop-blocks <a,b> Block types to discard, e.g. footer,header
@@ -3555,10 +3801,30 @@ Tuning a corpus:
3555
3801
  invisible to search rather than merely ranked low.
3556
3802
  --image-model <id>
3557
3803
  --embedding-model <id>
3804
+ --embedding-dimensions <n> Vector width, for models that offer several
3805
+ (Matryoshka). Must be the model's native size or
3806
+ one it lists; the error names the choices.
3807
+ Smaller vectors cut index memory, and dedicated
3808
+ capacity needs, roughly in proportion.
3558
3809
  --extraction-model <id>
3559
3810
 
3811
+ Placement (shared pool vs dedicated capacity, see \`infra --help\`):
3812
+ --source defaults to "${DEFAULT_SOURCE}" and is created on first use, on the
3813
+ shared pool. Start a corpus on a resource with \`create --placement\`, or
3814
+ move one at any time with \`move --to <resource-id|shared>\`.
3815
+ A move copies the corpus's stored vectors onto the new capacity in the
3816
+ background: no re-extraction, no re-embedding, no charge beyond the
3817
+ resource itself. Search keeps working from the old placement until the
3818
+ copy lands; add, rm, config, revectorize and delete are refused with
3819
+ data_source_migrating until then. \`datasources list\` shows progress;
3820
+ \`move --wait\` blocks on it (exits ${EXIT.buildFailed} if the move failed,
3821
+ ${EXIT.timeout} on timeout). Moving to \`shared\` is how a source leaves a
3822
+ resource you mean to destroy.
3823
+ A move is refused while documents are still building or a candidate version
3824
+ exists (data_source_busy), when the target is not active
3825
+ (capacity_<phase>), or when it would not fit (capacity_exceeded).
3826
+
3560
3827
  Notes:
3561
- --source defaults to "${DEFAULT_SOURCE}" and is created on first use.
3562
3828
  --wait blocks until processing finishes, so you can search immediately after.
3563
3829
  Exits ${EXIT.buildFailed} if a document failed, ${EXIT.timeout} on timeout.
3564
3830
  Re-adding an unchanged file is free: no upload, no re-embedding. Re-adding
@@ -5333,6 +5599,373 @@ Examples:
5333
5599
  remy-admin cron list --runs 50
5334
5600
  remy-admin cron run dailyDigest`;
5335
5601
 
5602
+ // src/ops/infra.ts
5603
+ var DEFAULT_WAIT_TIMEOUT_MS2 = 10 * 60 * 1e3;
5604
+ var POLL_MS4 = 3e3;
5605
+ function base2(appId) {
5606
+ return `/_internal/v2/apps/${appId}/infra`;
5607
+ }
5608
+ function list11(ctx, params) {
5609
+ return call(
5610
+ ctx,
5611
+ "GET",
5612
+ `${base2(ctx.appId)}${params?.includeDestroyed ? "?includeDestroyed=true" : ""}`
5613
+ );
5614
+ }
5615
+ function get8(ctx, params) {
5616
+ return call(
5617
+ ctx,
5618
+ "GET",
5619
+ `${base2(ctx.appId)}/${encodeURIComponent(params.id)}`
5620
+ );
5621
+ }
5622
+ function provision(ctx, params) {
5623
+ return call(ctx, "POST", base2(ctx.appId), {
5624
+ offeringId: params.offeringId,
5625
+ ...params.name ? { name: params.name } : {}
5626
+ });
5627
+ }
5628
+ function hibernate(ctx, params) {
5629
+ return call(
5630
+ ctx,
5631
+ "POST",
5632
+ `${base2(ctx.appId)}/${encodeURIComponent(params.id)}/hibernate`
5633
+ );
5634
+ }
5635
+ function resume(ctx, params) {
5636
+ return call(
5637
+ ctx,
5638
+ "POST",
5639
+ `${base2(ctx.appId)}/${encodeURIComponent(params.id)}/resume`
5640
+ );
5641
+ }
5642
+ function destroy(ctx, params) {
5643
+ return call(
5644
+ ctx,
5645
+ "POST",
5646
+ `${base2(ctx.appId)}/${encodeURIComponent(params.id)}/destroy`
5647
+ );
5648
+ }
5649
+ function resize(ctx, params) {
5650
+ return call(
5651
+ ctx,
5652
+ "POST",
5653
+ `${base2(ctx.appId)}/${encodeURIComponent(params.id)}/resize`,
5654
+ { offeringId: params.offeringId }
5655
+ );
5656
+ }
5657
+ function rename(ctx, params) {
5658
+ return call(
5659
+ ctx,
5660
+ "POST",
5661
+ `${base2(ctx.appId)}/${encodeURIComponent(params.id)}/rename`,
5662
+ { name: params.name }
5663
+ );
5664
+ }
5665
+ async function waitForPhase(ctx, params) {
5666
+ const timeoutMs = params.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS2;
5667
+ const wanted = new Set(params.phases);
5668
+ const start = Date.now();
5669
+ let transientSince = null;
5670
+ for (; ; ) {
5671
+ let resource;
5672
+ try {
5673
+ ({ resource } = await get8(ctx, { id: params.id }));
5674
+ transientSince = null;
5675
+ } catch (err) {
5676
+ if (!isTransientError(err)) {
5677
+ throw err;
5678
+ }
5679
+ transientSince ??= Date.now();
5680
+ if (Date.now() - transientSince > TRANSIENT_GRACE_MS) {
5681
+ throw err;
5682
+ }
5683
+ params.onProgress?.(
5684
+ `retrying after a gateway error\u2026 (${Math.round((Date.now() - start) / 1e3)}s)`
5685
+ );
5686
+ await sleep(POLL_MS4);
5687
+ continue;
5688
+ }
5689
+ if (wanted.has(resource.phase) && (params.until?.(resource) ?? true)) {
5690
+ return { status: "done", resource };
5691
+ }
5692
+ if (resource.phase === "failed") {
5693
+ return {
5694
+ status: "failed",
5695
+ resource,
5696
+ error: resource.lastError ?? "The platform could not complete the change."
5697
+ };
5698
+ }
5699
+ if (Date.now() - start > timeoutMs) {
5700
+ return {
5701
+ status: "timeout",
5702
+ resource,
5703
+ error: `Timed out after ${Math.round(timeoutMs / 1e3)}s; resource is still ${resource.phase}`
5704
+ };
5705
+ }
5706
+ params.onProgress?.(
5707
+ `${resource.phase}${resource.detail ? ` \xB7 ${resource.detail}` : ""}${resource.resizingTo ? ` \xB7 resizing to ${resource.resizingTo.label}` : ""}\u2026 (${Math.round((Date.now() - start) / 1e3)}s)`
5708
+ );
5709
+ await sleep(POLL_MS4);
5710
+ }
5711
+ }
5712
+
5713
+ // src/commands/infra.ts
5714
+ var infraSpecs = {
5715
+ "infra list": {
5716
+ usage: "Usage: remy-admin infra list [--include-destroyed]",
5717
+ flags: { "include-destroyed": { type: "boolean" } }
5718
+ },
5719
+ "infra get": {
5720
+ usage: "Usage: remy-admin infra get <id>",
5721
+ positionals: [{ name: "id", required: true }]
5722
+ },
5723
+ "infra provision": {
5724
+ usage: "Usage: remy-admin infra provision --offering <id> [--name <name>] [--wait] [--timeout <sec>]",
5725
+ flags: {
5726
+ offering: { type: "string" },
5727
+ name: { type: "string" },
5728
+ wait: { type: "boolean" },
5729
+ timeout: { type: "string" }
5730
+ },
5731
+ requireAnyOf: {
5732
+ flags: ["offering"],
5733
+ message: "--offering is required (see `infra list` for the catalog)."
5734
+ }
5735
+ },
5736
+ "infra hibernate": {
5737
+ usage: "Usage: remy-admin infra hibernate <id> [--wait] [--timeout <sec>]",
5738
+ positionals: [{ name: "id", required: true }],
5739
+ flags: { wait: { type: "boolean" }, timeout: { type: "string" } }
5740
+ },
5741
+ "infra resume": {
5742
+ usage: "Usage: remy-admin infra resume <id> [--wait] [--timeout <sec>]",
5743
+ positionals: [{ name: "id", required: true }],
5744
+ flags: { wait: { type: "boolean" }, timeout: { type: "string" } }
5745
+ },
5746
+ "infra destroy": {
5747
+ usage: "Usage: remy-admin infra destroy <id> [--wait] [--timeout <sec>]",
5748
+ positionals: [{ name: "id", required: true }],
5749
+ flags: { wait: { type: "boolean" }, timeout: { type: "string" } }
5750
+ },
5751
+ "infra rename": {
5752
+ usage: "Usage: remy-admin infra rename <id> --name <name>",
5753
+ positionals: [{ name: "id", required: true }],
5754
+ flags: { name: { type: "string" } },
5755
+ requireAnyOf: { flags: ["name"], message: "--name is required." }
5756
+ },
5757
+ "infra resize": {
5758
+ usage: "Usage: remy-admin infra resize <id> --offering <id> [--wait] [--timeout <sec>]",
5759
+ positionals: [{ name: "id", required: true }],
5760
+ flags: {
5761
+ offering: { type: "string" },
5762
+ wait: { type: "boolean" },
5763
+ timeout: { type: "string" }
5764
+ },
5765
+ requireAnyOf: {
5766
+ flags: ["offering"],
5767
+ message: "--offering is required (see `infra list` for the sizes)."
5768
+ }
5769
+ }
5770
+ };
5771
+ async function waitAndReport(ctx, a, id, phases, summary2, until) {
5772
+ const timeoutSec = a.str("timeout");
5773
+ const result = await waitForPhase(ctx, {
5774
+ id,
5775
+ phases,
5776
+ ...until ? { until } : {},
5777
+ ...timeoutSec ? { timeoutMs: Number(timeoutSec) * 1e3 } : {},
5778
+ onProgress: progress
5779
+ });
5780
+ out({ ...summary2, ...result });
5781
+ if (result.status === "failed") {
5782
+ throw new CliError(result.error, EXIT.buildFailed);
5783
+ }
5784
+ if (result.status === "timeout") {
5785
+ throw new CliError(result.error, EXIT.timeout);
5786
+ }
5787
+ }
5788
+ async function infraList(ctx, a) {
5789
+ out(await list11(ctx, { includeDestroyed: a.bool("include-destroyed") }));
5790
+ }
5791
+ async function infraGet(ctx, a) {
5792
+ out(await get8(ctx, { id: a.req("id") }));
5793
+ }
5794
+ function pricingOf(resource) {
5795
+ const offering = resource.offering;
5796
+ if (!offering) {
5797
+ return null;
5798
+ }
5799
+ return {
5800
+ offering: offering.id,
5801
+ activeMonthlyDollars: offering.monthlyPriceDollars,
5802
+ hibernatedMonthlyDollars: offering.hibernatedMonthlyPriceDollars,
5803
+ note: "Bills hourly from activation. A month's credits were verified before the lease was accepted."
5804
+ };
5805
+ }
5806
+ async function infraProvision(ctx, a) {
5807
+ const offeringId = a.str("offering");
5808
+ const name = a.str("name");
5809
+ const { resource } = await provision(ctx, {
5810
+ offeringId,
5811
+ ...name ? { name } : {}
5812
+ });
5813
+ const pricing = pricingOf(resource);
5814
+ if (!a.bool("wait")) {
5815
+ out({
5816
+ resource,
5817
+ pricing,
5818
+ note: "Provisioning; poll `infra get <id>` or re-run with --wait."
5819
+ });
5820
+ return;
5821
+ }
5822
+ await waitAndReport(ctx, a, resource.id, ["active"], {
5823
+ action: "provision",
5824
+ pricing
5825
+ });
5826
+ }
5827
+ async function infraHibernate(ctx, a) {
5828
+ const id = a.req("id");
5829
+ const { resource } = await hibernate(ctx, { id });
5830
+ if (!a.bool("wait")) {
5831
+ out({ resource });
5832
+ return;
5833
+ }
5834
+ await waitAndReport(ctx, a, id, ["hibernated"], { action: "hibernate" });
5835
+ }
5836
+ async function infraResume(ctx, a) {
5837
+ const id = a.req("id");
5838
+ const { resource } = await resume(ctx, { id });
5839
+ if (!a.bool("wait")) {
5840
+ out({ resource });
5841
+ return;
5842
+ }
5843
+ await waitAndReport(ctx, a, id, ["active"], { action: "resume" });
5844
+ }
5845
+ async function infraDestroy(ctx, a) {
5846
+ const id = a.req("id");
5847
+ const { resource } = await destroy(ctx, { id });
5848
+ if (!a.bool("wait")) {
5849
+ out({ resource });
5850
+ return;
5851
+ }
5852
+ await waitAndReport(ctx, a, id, ["destroyed"], { action: "destroy" });
5853
+ }
5854
+ async function infraRename(ctx, a) {
5855
+ out(
5856
+ await rename(ctx, {
5857
+ id: a.req("id"),
5858
+ name: a.str("name")
5859
+ })
5860
+ );
5861
+ }
5862
+ async function infraResize(ctx, a) {
5863
+ const id = a.req("id");
5864
+ const { resource } = await resize(ctx, {
5865
+ id,
5866
+ offeringId: a.str("offering")
5867
+ });
5868
+ const pricing = pricingOf({
5869
+ ...resource,
5870
+ offering: resource.resizingTo ?? resource.offering
5871
+ });
5872
+ if (!a.bool("wait")) {
5873
+ out({
5874
+ resource,
5875
+ pricing,
5876
+ note: "Resizing; searches on its sources pause while it is parked and restored. Poll `infra get <id>` or re-run with --wait."
5877
+ });
5878
+ return;
5879
+ }
5880
+ await waitAndReport(
5881
+ ctx,
5882
+ a,
5883
+ id,
5884
+ [resource.phase],
5885
+ { action: "resize", pricing },
5886
+ (r) => r.resizingTo === null
5887
+ );
5888
+ }
5889
+ var infraHandlers = {
5890
+ "infra list": infraList,
5891
+ "infra get": infraGet,
5892
+ "infra provision": infraProvision,
5893
+ "infra hibernate": infraHibernate,
5894
+ "infra resume": infraResume,
5895
+ "infra destroy": infraDestroy,
5896
+ "infra rename": infraRename,
5897
+ "infra resize": infraResize
5898
+ };
5899
+ var infraHelp = `remy-admin infra \u2014 Dedicated infrastructure your app leases from the platform.
5900
+
5901
+ Today the one kind is dedicated retrieval: the app's own vector-store capacity
5902
+ for data sources, isolated from the shared pool. Sizes and prices come from the
5903
+ platform catalog (\`infra list\` prints them); nothing is priced client-side.
5904
+
5905
+ A resource bills by the hour from activation, at the retained-storage rate while
5906
+ hibernated, and not at all once destroyed. Provisioning (and growing) needs a
5907
+ month's worth of credits available in the workspace, checked before the request
5908
+ is accepted; nothing is charged until activation. The provision output prints
5909
+ the price it committed to. Every action below is audited.
5910
+
5911
+ Subcommands:
5912
+ list Resources on this app, plus the offerings you can provision
5913
+ get One resource with its state timeline and attached sources
5914
+ provision Lease a new resource (needs a month's credits; prints the price)
5915
+ hibernate Park it: data kept, searches paused, compute charge stops
5916
+ resume Bring a hibernated resource back
5917
+ resize Change its size, data intact (passes through hibernated)
5918
+ destroy Delete it (refused while data sources are placed on it)
5919
+ rename Change the display name
5920
+
5921
+ Usage:
5922
+ remy-admin infra list [--include-destroyed]
5923
+ remy-admin infra get <id>
5924
+ remy-admin infra provision --offering <id> [--name <name>] [--wait] [--timeout <sec>]
5925
+ remy-admin infra hibernate <id> [--wait] [--timeout <sec>]
5926
+ remy-admin infra resume <id> [--wait] [--timeout <sec>]
5927
+ remy-admin infra resize <id> --offering <id> [--wait] [--timeout <sec>]
5928
+ remy-admin infra destroy <id> [--wait] [--timeout <sec>]
5929
+ remy-admin infra rename <id> --name <name>
5930
+
5931
+ Placing a data source on a resource:
5932
+ remy-admin datasources create --source <slug> --placement <resource-id>
5933
+ remy-admin datasources move --source <slug> --to <resource-id|shared> [--wait]
5934
+
5935
+ Start a new corpus on a resource with \`create\`; \`add\` creates a source on
5936
+ the shared pool. A populated source moves with \`datasources move\`: its
5937
+ stored vectors are copied onto the new capacity in the background (no
5938
+ re-embedding), search keeps working from the old placement until the copy
5939
+ lands, and adding or removing documents is refused with
5940
+ data_source_migrating until it finishes. Moving to \`shared\` is how a source
5941
+ leaves a resource you mean to destroy. See \`datasources --help\`.
5942
+
5943
+ Searches on a source whose resource is not active fail with
5944
+ capacity_<phase>: capacity_hibernated once parked, capacity_hibernating /
5945
+ capacity_resuming while it moves. \`active\` means searchable: a resumed
5946
+ resource reports active only once its data is restored. capacity_restoring
5947
+ is the rare case of an active instance that was replaced (node loss) and is
5948
+ refilling from its snapshot. The messages are written for the app's end
5949
+ user; the stable code is what to act on.
5950
+
5951
+ Resizing:
5952
+ A resize keeps the data. The resource is parked (a fresh snapshot), its spec
5953
+ is swapped, and it comes back up restored from that snapshot, so searches on
5954
+ its sources pause for the few minutes it takes. A hibernated resource swaps
5955
+ in place and stays parked. Growing needs a month's credits at the new rate;
5956
+ shrinking is refused below what the resource holds (resize_too_small).
5957
+ \`infra get\` shows resizingTo until the swap has happened.
5958
+
5959
+ Phases and timing:
5960
+ requested \u2192 provisioning \u2192 active \u21C4 hibernated \u2192 destroyed, with hibernating /
5961
+ resuming / decommissioning in between and failed when a change cannot complete
5962
+ (retried automatically; resume or hibernate to retry by hand). Each transition
5963
+ takes about two minutes, more with more data: snapshots and restores scale
5964
+ with the collection. --wait defaults to 600s; pass --timeout for a large one.
5965
+
5966
+ --wait exit codes: 0 reached the target phase \xB7 1 the platform reported failure \xB7
5967
+ 2 still transitioning when --timeout elapsed (default 600s).`;
5968
+
5336
5969
  // src/commands/index.ts
5337
5970
  var SPECS = {
5338
5971
  ...requestsSpecs,
@@ -5356,7 +5989,8 @@ var SPECS = {
5356
5989
  ...jewelsSpecs,
5357
5990
  ...settingsSpecs,
5358
5991
  ...emailSpecs,
5359
- ...cronSpecs
5992
+ ...cronSpecs,
5993
+ ...infraSpecs
5360
5994
  };
5361
5995
  var HANDLERS = {
5362
5996
  ...requestsHandlers,
@@ -5380,7 +6014,8 @@ var HANDLERS = {
5380
6014
  ...jewelsHandlers,
5381
6015
  ...settingsHandlers,
5382
6016
  ...emailHandlers,
5383
- ...cronHandlers
6017
+ ...cronHandlers,
6018
+ ...infraHandlers
5384
6019
  };
5385
6020
  var GROUP_HELP = {
5386
6021
  requests: requestsHelp,
@@ -5404,7 +6039,8 @@ var GROUP_HELP = {
5404
6039
  jewels: jewelsHelp,
5405
6040
  settings: settingsHelp,
5406
6041
  email: emailHelp,
5407
- cron: cronHelp
6042
+ cron: cronHelp,
6043
+ infra: infraHelp
5408
6044
  };
5409
6045
 
5410
6046
  // src/ctx.ts
@@ -5486,6 +6122,7 @@ Commands:
5486
6122
  settings App settings \u2014 signup allowlist, test accounts, embed origins, toggles
5487
6123
  email Email \u2014 delivery log, blast stats, unsubscribes, inbox, custom domains
5488
6124
  cron Scheduled jobs \u2014 schedules, run history, manual trigger
6125
+ infra Dedicated infrastructure \u2014 provision, hibernate, resize + destroy retrieval capacity
5489
6126
 
5490
6127
  Run 'remy-admin <command> --help' for details on each command.
5491
6128
 
@@ -5517,7 +6154,8 @@ var GROUPS = [
5517
6154
  "jewels",
5518
6155
  "settings",
5519
6156
  "email",
5520
- "cron"
6157
+ "cron",
6158
+ "infra"
5521
6159
  ];
5522
6160
  var KEYS = new Set(Object.keys(SPECS));
5523
6161
  function resolveRoute(argv) {