deepline 0.1.55 → 0.1.57

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.
@@ -120,6 +120,7 @@ import {
120
120
  import {
121
121
  applyCsvRenameProjection,
122
122
  stripCsvProjectedFields,
123
+ stripCsvProjectionMetadata,
123
124
  cloneCsvAliasedRow,
124
125
  type CsvRenameOptions,
125
126
  } from '../../../shared_libs/play-runtime/csv-rename';
@@ -129,6 +130,12 @@ import type {
129
130
  LiveNodeProgressMap,
130
131
  LiveNodeProgressSnapshot,
131
132
  } from './runtime/live-progress';
133
+ import {
134
+ ToolHttpError,
135
+ extractErrorBilling,
136
+ isHardBillingToolHttpError,
137
+ normalizeToolHttpErrorMessage,
138
+ } from './runtime/tool-http-errors';
132
139
 
133
140
  // The play's default export. The bundler injects this — see bundle-play-file.ts.
134
141
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -194,23 +201,6 @@ const EXECUTE_TOOL_METADATA_HEADER = 'x-deepline-include-tool-metadata';
194
201
  const EXECUTE_RESPONSE_CONTRACT_HEADER = 'x-deepline-execute-response-contract';
195
202
  const V2_EXECUTE_RESPONSE_CONTRACT = 'v2-tool-execution-result';
196
203
 
197
- class ToolHttpError extends Error {
198
- readonly billing: Record<string, unknown> | null;
199
-
200
- constructor(message: string, billing: Record<string, unknown> | null) {
201
- super(message);
202
- this.name = 'ToolHttpError';
203
- this.billing = billing;
204
- }
205
- }
206
-
207
- function formatCreditAmount(value: unknown): string {
208
- if (typeof value !== 'number' || !Number.isFinite(value)) {
209
- return String(value ?? '-');
210
- }
211
- return Number(value.toFixed(8)).toString();
212
- }
213
-
214
204
  function getStringField(value: unknown, key: string): string | null {
215
205
  if (!isRecord(value)) return null;
216
206
  const field = value[key];
@@ -226,71 +216,6 @@ function getObjectField(
226
216
  return isRecord(field) ? field : null;
227
217
  }
228
218
 
229
- function isInsufficientCreditsBilling(
230
- billing: Record<string, unknown> | null,
231
- ): billing is Record<string, unknown> {
232
- return billing?.kind === 'insufficient_credits';
233
- }
234
-
235
- function formatInsufficientCreditsMessage(input: {
236
- billing: Record<string, unknown>;
237
- toolId: string;
238
- }): string {
239
- const operation =
240
- getStringField(input.billing, 'operation_id') ??
241
- getStringField(input.billing, 'operation') ??
242
- input.toolId;
243
- const balance = formatCreditAmount(input.billing.balance_credits);
244
- const required = formatCreditAmount(input.billing.required_credits);
245
- const recommended = formatCreditAmount(
246
- input.billing.recommended_add_credits ?? input.billing.needed_credits,
247
- );
248
- const billingUrl = getStringField(input.billing, 'billing_url');
249
- const addSuffix =
250
- billingUrl && recommended !== '-'
251
- ? ` Add >=${recommended} at ${billingUrl}.`
252
- : billingUrl
253
- ? ` Add credits at ${billingUrl}.`
254
- : '';
255
- return `Workspace balance ${balance} < required ${required} for ${operation}.${addSuffix}`;
256
- }
257
-
258
- function normalizeToolHttpErrorMessage(input: {
259
- toolId: string;
260
- status: number;
261
- attempt: number;
262
- maxAttempts: number;
263
- bodyText: string;
264
- }): ToolHttpError {
265
- let parsed: Record<string, unknown> | null = null;
266
- try {
267
- const candidate = JSON.parse(input.bodyText);
268
- parsed = isRecord(candidate) ? candidate : null;
269
- } catch {
270
- parsed = null;
271
- }
272
- const billing = getObjectField(parsed, 'billing');
273
- if (isInsufficientCreditsBilling(billing)) {
274
- return new ToolHttpError(
275
- `tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${formatInsufficientCreditsMessage(
276
- {
277
- billing,
278
- toolId: input.toolId,
279
- },
280
- )}`,
281
- billing,
282
- );
283
- }
284
- return new ToolHttpError(
285
- `tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${input.bodyText.slice(0, 500)}`,
286
- billing,
287
- );
288
- }
289
-
290
- function extractErrorBilling(error: unknown): Record<string, unknown> | null {
291
- return error instanceof ToolHttpError ? error.billing : null;
292
- }
293
-
294
219
  /** R2 binding injected by the Worker runtime (when present in deploy metadata). */
295
220
  type WorkerEnv = {
296
221
  PLAYS_BUCKET?: R2Bucket;
@@ -690,6 +615,23 @@ function publicCsvInputRow<T extends Record<string, unknown>>(row: T): T {
690
615
  ) as T;
691
616
  }
692
617
 
618
+ function publicCsvOutputRow<T extends Record<string, unknown>>(row: T): T {
619
+ const stripped = stripCsvProjectionMetadata(row) as Record<string, unknown>;
620
+ const publicRow: Record<string, unknown> = {};
621
+ for (const fieldName of Reflect.ownKeys(stripped)) {
622
+ if (
623
+ typeof fieldName === 'string' &&
624
+ fieldName.startsWith('__deepline')
625
+ ) {
626
+ continue;
627
+ }
628
+ const descriptor = Object.getOwnPropertyDescriptor(stripped, fieldName);
629
+ if (!descriptor) continue;
630
+ Object.defineProperty(publicRow, fieldName, descriptor);
631
+ }
632
+ return publicRow as T;
633
+ }
634
+
693
635
  /**
694
636
  * Strip credentials and JWT-shaped tokens from any string before it lands in
695
637
  * a log buffer or upstream error message. The harness routinely echoes
@@ -1330,7 +1272,7 @@ async function callToolDirect(
1330
1272
  bodyText: text,
1331
1273
  });
1332
1274
  const retryable =
1333
- res.status === 429 ||
1275
+ (res.status === 429 && !isHardBillingToolHttpError(lastError)) ||
1334
1276
  (res.status >= 500 && WORKER_RETRY_SAFE_5XX_TOOLS.has(toolId));
1335
1277
  if (!retryable || attempt >= maxAttempts) {
1336
1278
  throw lastError;
@@ -3742,9 +3684,8 @@ function createMinimalWorkerCtx(
3742
3684
  ? completedRow.__deeplineRowKey
3743
3685
  : derivePlayRowIdentity(publicCsvInputRow(completedRow), name);
3744
3686
  if (key) {
3745
- const { __deeplineRowKey: _rowKey, ...cleanedRow } =
3746
- publicCsvInputRow(completedRow);
3747
- void _rowKey;
3687
+ const cleanedRow = publicCsvOutputRow(completedRow);
3688
+ delete cleanedRow.__deeplineRowKey;
3748
3689
  resultByKey.set(key, cleanedRow as T & Record<string, unknown>);
3749
3690
  }
3750
3691
  }
@@ -3763,8 +3704,9 @@ function createMinimalWorkerCtx(
3763
3704
  return resultByKey.get(key);
3764
3705
  })
3765
3706
  .filter((row): row is T & Record<string, unknown> => Boolean(row));
3707
+ const publicOut = out.map((row) => publicCsvOutputRow(row));
3766
3708
  const hashStartedAt = nowMs();
3767
- const hash = await hashJson(out);
3709
+ const hash = await hashJson(publicOut);
3768
3710
  recordRunnerPerfTrace({
3769
3711
  req,
3770
3712
  phase: 'runner.map_chunk.hash',
@@ -3797,7 +3739,7 @@ function createMinimalWorkerCtx(
3797
3739
  rowsSkipped,
3798
3740
  outputDatasetId: `map:${name}`,
3799
3741
  hash,
3800
- preview: serializeDurableStepValue(out.slice(0, 5)),
3742
+ preview: toWorkflowSerializableValue(publicOut.slice(0, 5)),
3801
3743
  cachedRows:
3802
3744
  out.length <= WORKER_DATASET_IN_MEMORY_ROWS
3803
3745
  ? serializeDurableStepValue(out)
@@ -4333,13 +4275,15 @@ function createMinimalWorkerCtx(
4333
4275
  req,
4334
4276
  allowInline:
4335
4277
  options?.timeoutMs == null && !childNeedsWorkflowScheduler,
4336
- body: {
4337
- name: resolvedName,
4338
- input: isRecord(input) ? input : {},
4339
- orgId: req.orgId,
4340
- parentExecutorToken: req.executorToken,
4341
- userEmail: req.userEmail ?? '',
4342
- profile: 'workers_edge',
4278
+ body: {
4279
+ name: resolvedName,
4280
+ input: isRecord(input) ? input : {},
4281
+ orgId: req.orgId,
4282
+ callbackBaseUrl: req.callbackUrl,
4283
+ baseUrl: req.baseUrl,
4284
+ parentExecutorToken: req.executorToken,
4285
+ userEmail: req.userEmail ?? '',
4286
+ profile: 'workers_edge',
4343
4287
  manifest: childManifest,
4344
4288
  childPlayManifests: req.childPlayManifests ?? null,
4345
4289
  internalRunPlay: {
@@ -5344,6 +5288,10 @@ function serializePlayReturnValue(value: unknown): unknown {
5344
5288
  return serializeValue(value, 0);
5345
5289
  }
5346
5290
 
5291
+ function toWorkflowSerializableValue<T>(value: T): T {
5292
+ return serializeValue(value, 0) as T;
5293
+ }
5294
+
5347
5295
  /**
5348
5296
  * Hard cap on the trimmed result body persisted into Convex run state.
5349
5297
  * Convex docs are bounded (~1 MiB per field) so we keep this comfortably
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  applyCsvRenameProjection,
3
+ stripCsvProjectedFields,
4
+ stripCsvProjectionMetadata,
3
5
  type CsvRenameOptions,
4
6
  } from '../../../../shared_libs/play-runtime/csv-rename';
5
7
  import {
@@ -38,7 +40,32 @@ const datasetCountHints = new WeakMap<object, number | null>();
38
40
  const datasetCapabilities = new WeakMap<object, WorkerDatasetCapabilities>();
39
41
 
40
42
  function cloneRow<T extends DatasetRow>(row: T): T {
41
- return { ...row };
43
+ const cloned: DatasetRow = {};
44
+ for (const key of Reflect.ownKeys(row)) {
45
+ const descriptor = Object.getOwnPropertyDescriptor(row, key);
46
+ if (!descriptor) continue;
47
+ Object.defineProperty(cloned, key, descriptor);
48
+ }
49
+ return cloned as T;
50
+ }
51
+
52
+ function internalDatasetRow<T extends DatasetRow>(row: T): T {
53
+ const stripped = stripCsvProjectionMetadata(row) as DatasetRow;
54
+ const publicRow: DatasetRow = {};
55
+ for (const key of Reflect.ownKeys(stripped)) {
56
+ if (typeof key === 'string' && key.startsWith('__deepline')) continue;
57
+ const descriptor = Object.getOwnPropertyDescriptor(stripped, key);
58
+ if (!descriptor) continue;
59
+ Object.defineProperty(publicRow, key, descriptor);
60
+ }
61
+ return publicRow as T;
62
+ }
63
+
64
+ function materializedDatasetRow<T extends DatasetRow>(row: T): T {
65
+ const stripped = stripCsvProjectedFields(row) as DatasetRow;
66
+ return Object.fromEntries(
67
+ Object.entries(stripped).filter(([key]) => !key.startsWith('__deepline')),
68
+ ) as T;
42
69
  }
43
70
 
44
71
  function registerChunkReader<T extends DatasetRow>(
@@ -138,10 +165,10 @@ export function createPersistedDatasetHandle<T extends DatasetRow>(input: {
138
165
  const count = Math.max(0, Math.floor(input.count));
139
166
  const previewRows = (input.previewRows ?? [])
140
167
  .slice(0, WORKER_DATASET_PREVIEW_ROWS)
141
- .map(cloneRow);
168
+ .map(materializedDatasetRow);
142
169
  const cachedRows =
143
170
  input.cachedRows && input.cachedRows.length <= WORKER_DATASET_IN_MEMORY_ROWS
144
- ? input.cachedRows.map(cloneRow)
171
+ ? input.cachedRows.map(internalDatasetRow)
145
172
  : null;
146
173
 
147
174
  async function loadRows(limit: number, offset: number): Promise<T[]> {
@@ -157,7 +184,7 @@ export function createPersistedDatasetHandle<T extends DatasetRow>(input: {
157
184
  ) {
158
185
  return cachedRows
159
186
  .slice(normalizedOffset, normalizedOffset + normalizedLimit)
160
- .map(cloneRow);
187
+ .map(internalDatasetRow);
161
188
  }
162
189
  const startedAt = input.nowMs();
163
190
  const rows = await input.readRows({
@@ -172,7 +199,7 @@ export function createPersistedDatasetHandle<T extends DatasetRow>(input: {
172
199
  offset: normalizedOffset,
173
200
  rows: rows.length,
174
201
  });
175
- return rows.map(cloneRow);
202
+ return rows.map(internalDatasetRow);
176
203
  }
177
204
 
178
205
  async function* readChunks(chunkSize: number): AsyncGenerator<T[], void, void> {
@@ -205,14 +232,15 @@ export function createPersistedDatasetHandle<T extends DatasetRow>(input: {
205
232
  workProgress: input.workProgress,
206
233
  resolvers: {
207
234
  count: async () => count,
208
- peek: async (limit) => await loadRows(Math.max(0, limit), 0),
235
+ peek: async (limit) =>
236
+ (await loadRows(Math.max(0, limit), 0)).map(materializedDatasetRow),
209
237
  materialize: async (limit) => {
210
238
  const rows: T[] = [];
211
239
  const maxRows = limit ?? count;
212
240
  for await (const chunk of readChunks(STREAM_MATERIALIZE_CHUNK_ROWS)) {
213
241
  for (const row of chunk) {
214
242
  if (rows.length >= maxRows) return rows;
215
- rows.push(row);
243
+ rows.push(materializedDatasetRow(row));
216
244
  }
217
245
  }
218
246
  return rows;
@@ -0,0 +1,198 @@
1
+ export class ToolHttpError extends Error {
2
+ readonly billing: Record<string, unknown> | null;
3
+
4
+ constructor(message: string, billing: Record<string, unknown> | null) {
5
+ super(message);
6
+ this.name = 'ToolHttpError';
7
+ this.billing = billing;
8
+ }
9
+ }
10
+
11
+ function formatCreditAmount(value: unknown): string {
12
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
13
+ return String(value ?? '-');
14
+ }
15
+ return Number(value.toFixed(8)).toString();
16
+ }
17
+
18
+ function isRecord(value: unknown): value is Record<string, unknown> {
19
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
20
+ }
21
+
22
+ function getStringField(value: unknown, key: string): string | null {
23
+ if (!isRecord(value)) return null;
24
+ const field = value[key];
25
+ return typeof field === 'string' && field.trim() ? field : null;
26
+ }
27
+
28
+ function getObjectField(
29
+ value: unknown,
30
+ key: string,
31
+ ): Record<string, unknown> | null {
32
+ if (!isRecord(value)) return null;
33
+ const field = value[key];
34
+ return isRecord(field) ? field : null;
35
+ }
36
+
37
+ function isInsufficientCreditsBilling(
38
+ billing: Record<string, unknown> | null,
39
+ ): billing is Record<string, unknown> {
40
+ return billing?.kind === 'insufficient_credits';
41
+ }
42
+
43
+ function isHardBillingFailurePayload(
44
+ payload: Record<string, unknown> | null,
45
+ ): payload is Record<string, unknown> {
46
+ if (!payload) return false;
47
+ const category = String(
48
+ payload.error_category ?? payload.errorCategory ?? '',
49
+ ).toLowerCase();
50
+ const code = String(payload.code ?? payload.error_code ?? '').toUpperCase();
51
+ const message = String(
52
+ payload.error ?? payload.message ?? payload.failure_description ?? '',
53
+ ).toLowerCase();
54
+ if (category === 'billing') return true;
55
+ if (
56
+ code === 'INSUFFICIENT_CREDITS' ||
57
+ code === 'BILLING_CAP_EXCEEDED' ||
58
+ code === 'MONTHLY_BILLING_LIMIT_EXCEEDED'
59
+ ) {
60
+ return true;
61
+ }
62
+ return (
63
+ (message.includes('billing cap') ||
64
+ message.includes('monthly billing limit') ||
65
+ message.includes('rolling 30-day organization billing cap') ||
66
+ message.includes('insufficient credits')) &&
67
+ !message.includes('rate limit')
68
+ );
69
+ }
70
+
71
+ function normalizeHardBillingPayload(
72
+ payload: Record<string, unknown>,
73
+ ): Record<string, unknown> {
74
+ return {
75
+ kind: 'billing_cap_exceeded',
76
+ code:
77
+ typeof payload.code === 'string' && payload.code.trim()
78
+ ? payload.code
79
+ : 'MONTHLY_BILLING_LIMIT_EXCEEDED',
80
+ error_category: 'billing',
81
+ failure_origin:
82
+ typeof payload.failure_origin === 'string' && payload.failure_origin.trim()
83
+ ? payload.failure_origin
84
+ : 'deepline_billing',
85
+ message:
86
+ typeof payload.error === 'string' && payload.error.trim()
87
+ ? payload.error
88
+ : typeof payload.message === 'string' && payload.message.trim()
89
+ ? payload.message
90
+ : 'Deepline billing cap exceeded.',
91
+ ...payload,
92
+ };
93
+ }
94
+
95
+ function formatHardBillingFailureMessage(input: {
96
+ billing: Record<string, unknown>;
97
+ toolId: string;
98
+ status: number;
99
+ attempt: number;
100
+ maxAttempts: number;
101
+ }): string {
102
+ const code = getStringField(input.billing, 'code');
103
+ const message =
104
+ getStringField(input.billing, 'message') ??
105
+ getStringField(input.billing, 'error') ??
106
+ 'Deepline billing cap exceeded.';
107
+ return [
108
+ `tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}:`,
109
+ 'Deepline billing cap exceeded.',
110
+ 'Run halted before marking remaining rows processed.',
111
+ code ? `code=${code}.` : '',
112
+ message,
113
+ ]
114
+ .filter(Boolean)
115
+ .join(' ');
116
+ }
117
+
118
+ function formatInsufficientCreditsMessage(input: {
119
+ billing: Record<string, unknown>;
120
+ toolId: string;
121
+ }): string {
122
+ const operation =
123
+ getStringField(input.billing, 'operation_id') ??
124
+ getStringField(input.billing, 'operation') ??
125
+ input.toolId;
126
+ const balance = formatCreditAmount(input.billing.balance_credits);
127
+ const required = formatCreditAmount(input.billing.required_credits);
128
+ const recommended = formatCreditAmount(
129
+ input.billing.recommended_add_credits ?? input.billing.needed_credits,
130
+ );
131
+ const billingUrl = getStringField(input.billing, 'billing_url');
132
+ const addSuffix =
133
+ billingUrl && recommended !== '-'
134
+ ? ` Add >=${recommended} at ${billingUrl}.`
135
+ : billingUrl
136
+ ? ` Add credits at ${billingUrl}.`
137
+ : '';
138
+ return `Workspace balance ${balance} < required ${required} for ${operation}.${addSuffix}`;
139
+ }
140
+
141
+ export function normalizeToolHttpErrorMessage(input: {
142
+ toolId: string;
143
+ status: number;
144
+ attempt: number;
145
+ maxAttempts: number;
146
+ bodyText: string;
147
+ }): ToolHttpError {
148
+ let parsed: Record<string, unknown> | null = null;
149
+ try {
150
+ const candidate = JSON.parse(input.bodyText);
151
+ parsed = isRecord(candidate) ? candidate : null;
152
+ } catch {
153
+ parsed = null;
154
+ }
155
+ const billing = getObjectField(parsed, 'billing');
156
+ if (isInsufficientCreditsBilling(billing)) {
157
+ return new ToolHttpError(
158
+ `tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${formatInsufficientCreditsMessage(
159
+ {
160
+ billing,
161
+ toolId: input.toolId,
162
+ },
163
+ )}`,
164
+ billing,
165
+ );
166
+ }
167
+ const hardBillingPayload = isHardBillingFailurePayload(billing)
168
+ ? normalizeHardBillingPayload(billing)
169
+ : isHardBillingFailurePayload(parsed)
170
+ ? normalizeHardBillingPayload(parsed)
171
+ : null;
172
+ if (hardBillingPayload) {
173
+ return new ToolHttpError(
174
+ formatHardBillingFailureMessage({
175
+ billing: hardBillingPayload,
176
+ toolId: input.toolId,
177
+ status: input.status,
178
+ attempt: input.attempt,
179
+ maxAttempts: input.maxAttempts,
180
+ }),
181
+ hardBillingPayload,
182
+ );
183
+ }
184
+ return new ToolHttpError(
185
+ `tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${input.bodyText.slice(0, 500)}`,
186
+ billing,
187
+ );
188
+ }
189
+
190
+ export function extractErrorBilling(
191
+ error: unknown,
192
+ ): Record<string, unknown> | null {
193
+ return error instanceof ToolHttpError ? error.billing : null;
194
+ }
195
+
196
+ export function isHardBillingToolHttpError(error: unknown): boolean {
197
+ return error instanceof ToolHttpError && isHardBillingFailurePayload(error.billing);
198
+ }
@@ -485,6 +485,37 @@ export class DeeplineClient {
485
485
  return `deepline plays run ${target} --input '{...}' --watch`;
486
486
  }
487
487
 
488
+ private starterPlayPath(play: Pick<PlayListItem, 'name' | 'reference'>): string {
489
+ const target = play.reference || play.name;
490
+ const unqualifiedName = target.split('/').pop() || play.name;
491
+ const safeName = unqualifiedName
492
+ .trim()
493
+ .toLowerCase()
494
+ .replace(/[^a-z0-9-]/g, '-')
495
+ .replace(/-+/g, '-')
496
+ .replace(/^-|-$/g, '');
497
+ return `./${safeName || 'play'}.play.ts`;
498
+ }
499
+
500
+ private playCloneEditStarter(
501
+ play: Pick<
502
+ PlayListItem,
503
+ 'name' | 'reference' | 'canClone' | 'canEdit' | 'origin' | 'ownerType'
504
+ >,
505
+ ): PlayDescription['cloneEditStarter'] | undefined {
506
+ const readonlyPrebuilt =
507
+ (play.origin === 'prebuilt' || play.ownerType === 'deepline') &&
508
+ !play.canEdit;
509
+ if (!play.canClone && !readonlyPrebuilt) return undefined;
510
+ const target = play.reference || play.name;
511
+ const path = this.starterPlayPath(play);
512
+ return {
513
+ path,
514
+ command: `deepline plays get ${target} --source --out ${path}`,
515
+ checkCommand: `deepline plays check ${path}`,
516
+ };
517
+ }
518
+
488
519
  private summarizePlayListItem(
489
520
  play: PlayListItem,
490
521
  options?: { compact?: boolean },
@@ -496,6 +527,7 @@ export class DeeplineClient {
496
527
  'rowOutputSchema',
497
528
  );
498
529
  const runCommand = this.playRunCommand(play, { csvInput });
530
+ const cloneEditStarter = this.playCloneEditStarter(play);
499
531
  return {
500
532
  name: play.name,
501
533
  ...(play.reference ? { reference: play.reference } : {}),
@@ -515,6 +547,7 @@ export class DeeplineClient {
515
547
  ...(rowOutputSchema ? { rowOutputSchema } : {}),
516
548
  runCommand,
517
549
  examples: [runCommand],
550
+ ...(cloneEditStarter ? { cloneEditStarter } : {}),
518
551
  currentPublishedVersion: play.currentPublishedVersion ?? null,
519
552
  isDraftDirty: play.isDraftDirty,
520
553
  };
@@ -1472,12 +1505,10 @@ export class DeeplineClient {
1472
1505
 
1473
1506
  async searchPlays(options: {
1474
1507
  query: string;
1475
- origin?: 'prebuilt' | 'owned';
1476
1508
  compact?: boolean;
1477
1509
  }): Promise<PlayDescription[]> {
1478
1510
  const params = new URLSearchParams();
1479
1511
  params.set('search', options.query.trim());
1480
- if (options.origin) params.set('origin', options.origin);
1481
1512
  const response = await this.http.get<{ plays: PlayListItem[] }>(
1482
1513
  `/api/v2/plays?${params.toString()}`,
1483
1514
  );