@drawcall/market 0.1.75 → 0.1.77

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.
Files changed (57) hide show
  1. package/dist/agent.d.ts +2 -2
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +15 -3
  4. package/dist/agent.js.map +1 -1
  5. package/dist/asset-implementation.d.ts +7 -0
  6. package/dist/asset-implementation.d.ts.map +1 -1
  7. package/dist/asset-implementation.js +1 -0
  8. package/dist/asset-implementation.js.map +1 -1
  9. package/dist/cli.js +8 -21
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands/agent.d.ts +2 -42
  12. package/dist/commands/agent.d.ts.map +1 -1
  13. package/dist/commands/agent.js +8 -382
  14. package/dist/commands/agent.js.map +1 -1
  15. package/dist/commands/generate.d.ts +2 -0
  16. package/dist/commands/generate.d.ts.map +1 -1
  17. package/dist/commands/generate.js +3 -0
  18. package/dist/commands/generate.js.map +1 -1
  19. package/dist/commands/reference-images.d.ts +6 -0
  20. package/dist/commands/reference-images.d.ts.map +1 -0
  21. package/dist/commands/reference-images.js +28 -0
  22. package/dist/commands/reference-images.js.map +1 -0
  23. package/dist/contract.d.ts +4 -46
  24. package/dist/contract.d.ts.map +1 -1
  25. package/dist/contract.js +2 -8
  26. package/dist/contract.js.map +1 -1
  27. package/dist/generate.d.ts +2 -0
  28. package/dist/generate.d.ts.map +1 -1
  29. package/dist/generate.js.map +1 -1
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/output.js +6 -6
  35. package/dist/output.js.map +1 -1
  36. package/dist/schemas.d.ts +3 -46
  37. package/dist/schemas.d.ts.map +1 -1
  38. package/dist/schemas.js +20 -46
  39. package/dist/schemas.js.map +1 -1
  40. package/dist/skill.d.ts +1 -1
  41. package/dist/skill.d.ts.map +1 -1
  42. package/dist/skill.js +2 -0
  43. package/dist/skill.js.map +1 -1
  44. package/package.json +1 -1
  45. package/skills/market/SKILL.md +2 -0
  46. package/src/agent.ts +17 -4
  47. package/src/asset-implementation.ts +8 -0
  48. package/src/cli.ts +19 -33
  49. package/src/commands/agent.ts +11 -474
  50. package/src/commands/generate.ts +5 -0
  51. package/src/commands/reference-images.ts +25 -0
  52. package/src/contract.ts +3 -9
  53. package/src/generate.ts +2 -0
  54. package/src/index.ts +0 -7
  55. package/src/output.ts +5 -5
  56. package/src/schemas.ts +24 -53
  57. package/src/skill.ts +2 -0
package/src/cli.ts CHANGED
@@ -8,7 +8,7 @@ import * as oauthClient from 'openid-client'
8
8
  import { ASSET_TYPES, type AssetAccess, type AssetType } from './schemas.js'
9
9
  import { installCommand } from './commands/install.js'
10
10
  import { searchCommand } from './commands/search.js'
11
- import { agentCommand, waitAgentCommand } from './commands/agent.js'
11
+ import { agentCommand } from './commands/agent.js'
12
12
  import { generateCommand } from './commands/generate.js'
13
13
  import { generateInstallCommand } from './commands/generate-install.js'
14
14
  import { listCommand } from './commands/list.js'
@@ -33,6 +33,12 @@ const DEVICE_CLIENT_ID = 'market-cli'
33
33
  const collect = (value: string, previous: string[]): string[] => previous.concat(value)
34
34
 
35
35
  const typeOption = new Option('--type <type>', 'Asset type').choices([...ASSET_TYPES])
36
+ const referenceImageOption = new Option(
37
+ '--reference-image <file-or-url>',
38
+ 'Reference image the result should match: file path or http(s) URL (repeatable)',
39
+ )
40
+ .argParser(collect)
41
+ .default([])
36
42
  // Omitted → the server picks the default from your entitlement (private if you hold market:private,
37
43
  // else public). `--access private` requires that entitlement.
38
44
  const accessOption = new Option('--access <access>', 'Asset visibility').choices([
@@ -193,49 +199,21 @@ const agent = program
193
199
  .description('Plan, find, judge and generate a set of assets from a goal')
194
200
  .argument('[goal]', 'What you need, in plain language')
195
201
  .addOption(apiOption)
196
- .option('--image <path>', 'Reference image, repeatable', collect, [])
202
+ .addOption(referenceImageOption)
197
203
  .option('--json', 'Output JSON', false)
198
- .option('--defer-generation', 'Wait for explicit approval before provider generation', false)
199
- .option('--approval-file <path>', 'Start/recovery control containing "approve" or "cancel"')
200
- .option('--run-file <path>', 'Durable sidecar for the deferred run ID')
201
- .option('--wait-run-file <path>', 'Recover and wait for an existing deferred run')
202
204
  .action(
203
205
  async (
204
206
  goal: string | undefined,
205
- opts: {
206
- api?: string
207
- image: string[]
208
- json: boolean
209
- deferGeneration: boolean
210
- approvalFile?: string
211
- runFile?: string
212
- waitRunFile?: string
213
- },
207
+ opts: { api?: string; referenceImage: string[]; json: boolean },
214
208
  ) => {
215
- if (opts.waitRunFile) {
216
- if (goal || opts.deferGeneration || opts.runFile || opts.image.length) {
217
- throw new Error(
218
- '--wait-run-file cannot be combined with a goal or deferred-start options',
219
- )
220
- }
221
- await waitAgentCommand(opts.waitRunFile, {
222
- baseUrl: opts.api,
223
- json: opts.json,
224
- approvalFile: opts.approvalFile,
225
- })
226
- return
227
- }
228
209
  if (!goal) {
229
210
  agent.help({ error: true })
230
211
  return
231
212
  }
232
213
  await agentCommand(goal, {
233
214
  baseUrl: opts.api,
234
- images: opts.image,
215
+ referenceImages: opts.referenceImage,
235
216
  json: opts.json,
236
- deferGeneration: opts.deferGeneration,
237
- approvalFile: opts.approvalFile,
238
- runFile: opts.runFile,
239
217
  })
240
218
  },
241
219
  )
@@ -305,12 +283,19 @@ const generate = program
305
283
  .argument('[description]', 'Asset prompt (omit when using a subcommand)')
306
284
  .addOption(typeOption)
307
285
  .addOption(apiOption)
286
+ .addOption(referenceImageOption)
308
287
  .option('--cwd <dir>', 'Project directory')
309
288
  .addOption(accessOption)
310
289
  .action(
311
290
  async (
312
291
  description: string | undefined,
313
- opts: { type?: AssetType; api?: string; cwd?: string; access?: AssetAccess },
292
+ opts: {
293
+ type?: AssetType
294
+ api?: string
295
+ referenceImage: string[]
296
+ cwd?: string
297
+ access?: AssetAccess
298
+ },
314
299
  ) => {
315
300
  if (!description) {
316
301
  generate.help({ error: true })
@@ -320,6 +305,7 @@ const generate = program
320
305
  await generateCommand(description, {
321
306
  type: opts.type,
322
307
  baseUrl: opts.api,
308
+ referenceImages: opts.referenceImage,
323
309
  cwd: opts.cwd,
324
310
  access: opts.access,
325
311
  })
@@ -1,46 +1,20 @@
1
- import { randomUUID } from 'node:crypto'
2
- import { link, mkdir, open, readFile, rm } from 'node:fs/promises'
3
- import { basename, dirname, extname, join } from 'node:path'
4
- import { ORPCError } from '@orpc/client'
5
1
  import ora from 'ora'
6
2
  import { getCliClient } from '../cli-client.js'
7
- import { agentAndWait, AgentError, type AgentInput } from '../agent.js'
3
+ import { agentAndWait } from '../agent.js'
4
+ import { loadReferenceImages } from './reference-images.js'
8
5
  import { assetVersionRef } from '../output.js'
9
- import type { AgentControlResult, AgentResult, AgentRunStatus } from '../schemas.js'
10
-
11
- export type AgentControlClient = {
12
- asset: {
13
- agent: {
14
- startDeferred(input: DeferredAgentInput & { runId: string }): Promise<{ runId: string }>
15
- status(input: { runId: string }): Promise<AgentRunStatus>
16
- approve(input: { runId: string }): Promise<AgentControlResult>
17
- cancel(input: { runId: string }): Promise<AgentControlResult>
18
- }
19
- }
20
- }
21
-
22
- type DeferredAgentInput = Pick<AgentInput, 'goal'>
23
- type AgentDiagnostic = (line: string) => void
24
-
25
- interface DeferredRunHandle {
26
- version: 1
27
- runId: string
28
- input: DeferredAgentInput
29
- }
6
+ import type { AgentResult } from '../schemas.js'
30
7
 
31
8
  export interface AgentCommandOptions {
32
- images?: string[]
9
+ /** `--reference-image` values: file paths or http(s) URLs. */
10
+ referenceImages?: string[]
33
11
  json?: boolean
34
12
  baseUrl?: string
35
- deferGeneration?: boolean
36
- approvalFile?: string
37
- runFile?: string
38
13
  }
39
14
 
40
15
  export async function agentCommand(goal: string, opts: AgentCommandOptions): Promise<void> {
41
- validateDeferredOptions(opts)
42
16
  const { client } = await getCliClient({ baseUrl: opts.baseUrl, requireAuth: true })
43
- const images = await Promise.all((opts.images ?? []).map(toDataUrl))
17
+ const referenceImages = await loadReferenceImages(opts.referenceImages ?? [])
44
18
 
45
19
  const spinner = ora({
46
20
  text: `Working on "${goal}"`,
@@ -48,435 +22,11 @@ export async function agentCommand(goal: string, opts: AgentCommandOptions): Pro
48
22
  isSilent: !process.stderr.isTTY,
49
23
  }).start()
50
24
 
51
- try {
52
- const result = opts.deferGeneration
53
- ? await runDeferredAgent(client, { goal }, requiredApprovalFile(opts), requiredRunFile(opts))
54
- : await agentAndWait(client, { goal, images: images.length ? images : undefined })
55
- spinner.stop()
56
- printResult(result, opts.json ?? false)
57
- } catch (err) {
58
- spinner.stop()
59
- throw err
60
- }
61
- }
62
-
63
- export interface AgentWaitCommandOptions {
64
- json?: boolean
65
- baseUrl?: string
66
- approvalFile?: string
67
- }
68
-
69
- export async function waitAgentCommand(
70
- runFile: string,
71
- opts: AgentWaitCommandOptions,
72
- ): Promise<void> {
73
- const { client } = await getCliClient({ baseUrl: opts.baseUrl, requireAuth: true })
74
- const handle = await readRunHandle(runFile)
75
- await ensureDeferredStarted(client, handle)
76
- const { runId } = handle
77
- const interrupt = interruptSignal()
78
- const spinner = ora({
79
- text: `Waiting for Market agent run ${runId}`,
80
- isEnabled: Boolean(process.stderr.isTTY),
81
- isSilent: !process.stderr.isTTY,
82
- }).start()
83
-
84
- let settled = false
85
- try {
86
- const result = opts.approvalFile
87
- ? await resumeExistingAgent(
88
- client,
89
- runId,
90
- opts.approvalFile,
91
- interrupt.signal,
92
- writeAgentDiagnostic,
93
- )
94
- : await waitForExistingAgent(client, runId, interrupt.signal, writeAgentDiagnostic)
95
- settled = true
96
- spinner.stop()
97
- printResult(result, opts.json ?? false)
98
- } catch (error) {
99
- spinner.stop()
100
- throw error
101
- } finally {
102
- interrupt.dispose()
103
- if (!settled && opts.approvalFile) {
104
- await client.asset.agent.cancel({ runId }).catch(() => undefined)
105
- }
106
- }
107
- }
108
-
109
- function validateDeferredOptions(opts: AgentCommandOptions): void {
110
- if (opts.deferGeneration && !opts.approvalFile) {
111
- throw new Error('--defer-generation requires --approval-file <path>')
112
- }
113
- if (opts.deferGeneration && !opts.runFile) {
114
- throw new Error('--defer-generation requires --run-file <path>')
115
- }
116
- if (opts.approvalFile && !opts.deferGeneration) {
117
- throw new Error('--approval-file requires --defer-generation')
118
- }
119
- if (opts.runFile && !opts.deferGeneration) {
120
- throw new Error('--run-file requires --defer-generation')
121
- }
122
- if (opts.deferGeneration && opts.images?.length) {
123
- throw new Error('--image is not supported with --defer-generation')
124
- }
125
- }
126
-
127
- function requiredRunFile(opts: AgentCommandOptions): string {
128
- if (!opts.runFile) throw new Error('Run file is required')
129
- return opts.runFile
130
- }
131
-
132
- function requiredApprovalFile(opts: AgentCommandOptions): string {
133
- if (!opts.approvalFile) throw new Error('Approval file is required')
134
- return opts.approvalFile
135
- }
136
-
137
- export async function runDeferredAgent(
138
- client: AgentControlClient,
139
- input: DeferredAgentInput,
140
- approvalFile: string,
141
- runFile: string,
142
- emitDiagnostic: AgentDiagnostic = writeAgentDiagnostic,
143
- ): Promise<AgentResult> {
144
- const interrupt = interruptSignal()
145
- const watch = new AbortController()
146
- const stopWatch = () => watch.abort(interrupt.signal.reason)
147
- interrupt.signal.addEventListener('abort', stopWatch, { once: true })
148
-
149
- const runId = randomUUID()
150
- const handle = { version: 1, runId, input } satisfies DeferredRunHandle
151
- let runReachable = false
152
- let settled = false
153
- try {
154
- // Persist the client-selected idempotency key and restart input before the
155
- // RPC. A lost start response is recoverable without creating a second run.
156
- await persistRunHandle(runFile, handle)
157
- emitDiagnostic(runHandleDiagnosticLine(runId, runFile))
158
- await ensureDeferredStarted(client, handle)
159
- runReachable = true
160
- const outcome = await resumeExistingAgent(
161
- client,
162
- runId,
163
- approvalFile,
164
- watch.signal,
165
- emitDiagnostic,
166
- )
167
- settled = true
168
- return outcome
169
- } finally {
170
- watch.abort()
171
- interrupt.dispose()
172
- if (!settled && runReachable) {
173
- try {
174
- await client.asset.agent.cancel({ runId })
175
- } catch {
176
- // Best-effort cleanup must not replace the actionable local error that
177
- // brought us here. Before approval, the server run remains safely gated
178
- // even if this request is lost.
179
- }
180
- }
181
- }
182
- }
183
-
184
- export function runHandleDiagnosticLine(runId: string, runFile: string): string {
185
- return JSON.stringify({ event: 'market.agent.run.persisted', runId, runFile })
186
- }
187
-
188
- function writeAgentDiagnostic(line: string): void {
189
- process.stderr.write(`${line}\n`)
190
- }
191
-
192
- export function waitForExistingAgent(
193
- client: AgentControlClient,
194
- runId: string,
195
- signal: AbortSignal = new AbortController().signal,
196
- emitDiagnostic: AgentDiagnostic = ignoreDiagnostic,
197
- ): Promise<AgentResult> {
198
- return waitForResult(client, runId, signal, emitDiagnostic)
199
- }
200
-
201
- export async function resumeExistingAgent(
202
- client: AgentControlClient,
203
- runId: string,
204
- approvalFile: string,
205
- signal: AbortSignal = new AbortController().signal,
206
- emitDiagnostic: AgentDiagnostic = ignoreDiagnostic,
207
- ): Promise<AgentResult> {
208
- const operation = new AbortController()
209
- const abortOperation = () => operation.abort(signal.reason)
210
- if (signal.aborted) abortOperation()
211
- else signal.addEventListener('abort', abortOperation, { once: true })
212
- try {
213
- const result = waitForResult(client, runId, operation.signal, emitDiagnostic).then(
214
- (value) => ({ kind: 'result' as const, value }),
215
- (error: unknown) => ({ kind: 'error' as const, error }),
216
- )
217
- const decision = waitForApprovalFile(approvalFile, operation.signal).then(
218
- (value) => ({ kind: 'decision' as const, value }),
219
- (error: unknown) => ({ kind: 'error' as const, error }),
220
- )
221
- const first = await Promise.race([result, decision])
222
- if (first.kind === 'error') throw first.error
223
- if (first.kind === 'result') return first.value
224
-
225
- if (operation.signal.aborted) throw abortReason(operation.signal)
226
- const controlRequest =
227
- first.value === 'approve'
228
- ? client.asset.agent.approve({ runId })
229
- : client.asset.agent.cancel({ runId })
230
- const control = await raceAbort(controlRequest, operation.signal)
231
- if (!control.accepted && control.state !== 'generation-started') {
232
- if (isSettledControlState(control.state)) return unwrapResult(await result)
233
- throw new AgentError(`Agent ${first.value} rejected: ${control.state}`)
234
- }
235
- if (first.value === 'cancel' && control.accepted) {
236
- // Observe the durable terminal status before returning the cancellation
237
- // error so callers receive generationStarted evidence for handle cleanup.
238
- return unwrapResult(await result)
239
- }
240
- return unwrapResult(await result)
241
- } finally {
242
- operation.abort()
243
- signal.removeEventListener('abort', abortOperation)
244
- }
245
- }
246
-
247
- function unwrapResult(
248
- outcome: { kind: 'result'; value: AgentResult } | { kind: 'error'; error: unknown },
249
- ): AgentResult {
250
- if (outcome.kind === 'error') throw outcome.error
251
- return outcome.value
252
- }
253
-
254
- function isSettledControlState(state: AgentControlResult['state']): boolean {
255
- return state === 'completed' || state === 'failed' || state === 'cancelled'
256
- }
257
-
258
- async function waitForResult(
259
- client: AgentControlClient,
260
- runId: string,
261
- signal: AbortSignal,
262
- emitDiagnostic: AgentDiagnostic,
263
- ): Promise<AgentResult> {
264
- for (;;) {
265
- if (signal.aborted) throw abortReason(signal)
266
- const status = await raceAbort(client.asset.agent.status({ runId }), signal)
267
- const result = settledResult(runId, status, emitDiagnostic)
268
- if (result) return result
269
- if (
270
- status.status === 'running' &&
271
- (status.generation === 'deferred' || status.generation === 'awaiting-approval')
272
- ) {
273
- await abortableDelay(250, signal)
274
- }
275
- }
276
- }
277
-
278
- function raceAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
279
- if (signal.aborted) return Promise.reject(abortReason(signal))
280
- return new Promise<T>((resolve, reject) => {
281
- signal.addEventListener('abort', aborted, { once: true })
282
- promise.then(completed, failed)
283
-
284
- function completed(value: T): void {
285
- signal.removeEventListener('abort', aborted)
286
- resolve(value)
287
- }
288
- function failed(error: unknown): void {
289
- signal.removeEventListener('abort', aborted)
290
- reject(error)
291
- }
292
- function aborted(): void {
293
- reject(abortReason(signal))
294
- }
295
- })
296
- }
297
-
298
- function settledResult(
299
- runId: string,
300
- status: AgentRunStatus,
301
- emitDiagnostic: AgentDiagnostic,
302
- ): AgentResult | undefined {
303
- if (status.status === 'running') return undefined
304
- emitDiagnostic(agentTerminalDiagnosticLine(runId, status))
305
- if (status.status === 'completed') return status.result
306
- if (status.status === 'failed') throw new AgentError(status.error)
307
- throw new AgentError('Agent run cancelled')
308
- }
309
-
310
- export function agentTerminalDiagnosticLine(
311
- runId: string,
312
- status: Exclude<AgentRunStatus, { status: 'running' }>,
313
- ): string {
314
- return JSON.stringify({
315
- event: 'market.agent.run.terminal',
316
- runId,
317
- status: status.status,
318
- generationStarted: status.generationStarted ?? null,
319
- })
320
- }
321
-
322
- function ignoreDiagnostic(): void {}
323
-
324
- async function waitForApprovalFile(
325
- path: string,
326
- signal: AbortSignal,
327
- ): Promise<'approve' | 'cancel'> {
328
- for (;;) {
329
- if (signal.aborted) throw abortReason(signal)
330
- try {
331
- const value = (await readFile(path, 'utf8')).trim()
332
- if (value === 'approve' || value === 'cancel') return value
333
- if (value) {
334
- throw new Error(`Approval file must contain exactly "approve" or "cancel": ${path}`)
335
- }
336
- } catch (error) {
337
- if (!isMissingFile(error)) throw error
338
- }
339
- await abortableDelay(100, signal)
340
- }
341
- }
342
-
343
- function interruptSignal(): { signal: AbortSignal; dispose(): void } {
344
- const controller = new AbortController()
345
- const onSigint = () => controller.abort(new AgentError('Interrupted by SIGINT'))
346
- const onSigterm = () => controller.abort(new AgentError('Interrupted by SIGTERM'))
347
- process.once('SIGINT', onSigint)
348
- process.once('SIGTERM', onSigterm)
349
- return {
350
- signal: controller.signal,
351
- dispose() {
352
- process.off('SIGINT', onSigint)
353
- process.off('SIGTERM', onSigterm)
354
- },
355
- }
356
- }
357
-
358
- function abortableDelay(ms: number, signal: AbortSignal): Promise<void> {
359
- if (signal.aborted) return Promise.reject(abortReason(signal))
360
- return new Promise((resolve, reject) => {
361
- const timeout = setTimeout(done, ms)
362
- signal.addEventListener('abort', aborted, { once: true })
363
-
364
- function done(): void {
365
- signal.removeEventListener('abort', aborted)
366
- resolve()
367
- }
368
- function aborted(): void {
369
- clearTimeout(timeout)
370
- reject(abortReason(signal))
371
- }
372
- })
373
- }
374
-
375
- function abortReason(signal: AbortSignal): Error {
376
- return signal.reason instanceof Error ? signal.reason : new AgentError('Agent command aborted')
377
- }
378
-
379
- function isMissingFile(error: unknown): boolean {
380
- return hasErrorCode(error, 'ENOENT')
381
- }
382
-
383
- function hasErrorCode(error: unknown, code: string): boolean {
384
- return error instanceof Error && 'code' in error && error.code === code
385
- }
386
-
387
- async function persistRunHandle(path: string, handle: DeferredRunHandle): Promise<void> {
388
- const directory = dirname(path)
389
- await mkdir(directory, { recursive: true })
390
- const temporary = join(directory, `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`)
391
- let file: Awaited<ReturnType<typeof open>> | undefined
392
- try {
393
- file = await open(temporary, 'wx', 0o600)
394
- await file.writeFile(`${JSON.stringify(handle)}\n`, 'utf8')
395
- await file.sync()
396
- await file.close()
397
- file = undefined
398
-
399
- // A same-directory hard link is an atomic create-if-absent. Refusing to
400
- // replace a previous handle prevents a new run from orphaning an older one.
401
- try {
402
- await link(temporary, path)
403
- } catch (error) {
404
- if (hasErrorCode(error, 'EEXIST')) {
405
- throw new Error(`Run file already exists; recover or remove it first: ${path}`, {
406
- cause: error,
407
- })
408
- }
409
- throw error
410
- }
411
- await rm(temporary)
412
- const directoryHandle = await open(directory, 'r')
413
- try {
414
- await directoryHandle.sync()
415
- } finally {
416
- await directoryHandle.close()
417
- }
418
- } finally {
419
- await file?.close().catch(() => undefined)
420
- await rm(temporary, { force: true }).catch(() => undefined)
421
- }
422
- }
423
-
424
- async function readRunHandle(path: string): Promise<DeferredRunHandle> {
425
- let value: unknown
426
- try {
427
- value = JSON.parse(await readFile(path, 'utf8'))
428
- } catch (error) {
429
- throw new Error(`Run file does not contain a valid Market run handle: ${path}`, {
430
- cause: error,
431
- })
432
- }
433
- if (!isDeferredRunHandle(value)) {
434
- throw new Error(`Run file does not contain a valid Market run handle: ${path}`)
435
- }
436
- return value
437
- }
438
-
439
- function isDeferredRunHandle(value: unknown): value is DeferredRunHandle {
440
- if (!value || typeof value !== 'object') return false
441
- const record = value as Record<string, unknown>
442
- const input = record.input
443
- return (
444
- record.version === 1 &&
445
- typeof record.runId === 'string' &&
446
- /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(record.runId) &&
447
- Boolean(input) &&
448
- typeof input === 'object' &&
449
- typeof (input as Record<string, unknown>).goal === 'string' &&
450
- Object.keys(input as Record<string, unknown>).length === 1
451
- )
452
- }
453
-
454
- async function ensureDeferredStarted(
455
- client: AgentControlClient,
456
- handle: DeferredRunHandle,
457
- ): Promise<void> {
458
- try {
459
- const started = await client.asset.agent.startDeferred({
460
- ...handle.input,
461
- runId: handle.runId,
462
- })
463
- if (started.runId !== handle.runId) {
464
- throw new Error('Market returned a different deferred run ID')
465
- }
466
- } catch (startError) {
467
- if (isAuthoritativeStartRejection(startError)) throw startError
468
- // The start response may be lost after the Durable Object committed. A
469
- // successful owner-checked status proves that this exact run is reachable.
470
- try {
471
- await client.asset.agent.status({ runId: handle.runId })
472
- } catch {
473
- throw startError
474
- }
475
- }
476
- }
477
-
478
- function isAuthoritativeStartRejection(error: unknown): boolean {
479
- return error instanceof ORPCError && error.status >= 400 && error.status < 500
25
+ const result = await agentAndWait(client, {
26
+ goal,
27
+ referenceImages: referenceImages.length ? referenceImages : undefined,
28
+ }).finally(() => spinner.stop())
29
+ printResult(result, opts.json ?? false)
480
30
  }
481
31
 
482
32
  function printResult(result: AgentResult, asJson: boolean): void {
@@ -499,16 +49,3 @@ function printResult(result: AgentResult, asJson: boolean): void {
499
49
  console.log(`Notes: ${notes}`)
500
50
  }
501
51
  }
502
-
503
- async function toDataUrl(path: string): Promise<string> {
504
- const data = await readFile(path)
505
- return `data:${mimeFor(path)};base64,${data.toString('base64')}`
506
- }
507
-
508
- function mimeFor(path: string): string {
509
- const ext = extname(path).toLowerCase()
510
- if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg'
511
- if (ext === '.webp') return 'image/webp'
512
- if (ext === '.gif') return 'image/gif'
513
- return 'image/png'
514
- }
@@ -5,10 +5,13 @@ import type { MarketClient } from '../client.js'
5
5
  import { resolve } from '../resolve.js'
6
6
  import { install as runInstall } from '../install.js'
7
7
  import { generatedInstallResult, generationStartedResult } from '../output.js'
8
+ import { loadReferenceImages } from './reference-images.js'
8
9
  import type { AssetAccess, AssetType } from '../schemas.js'
9
10
 
10
11
  export interface GenerateCommandOptions {
11
12
  type: AssetType
13
+ /** `--reference-image` values: file paths or http(s) URLs. */
14
+ referenceImages?: string[]
12
15
  cwd?: string
13
16
  baseUrl?: string
14
17
  access?: AssetAccess
@@ -18,6 +21,7 @@ export async function generateCommand(
18
21
  description: string,
19
22
  opts: GenerateCommandOptions,
20
23
  ): Promise<void> {
24
+ const referenceImages = await loadReferenceImages(opts.referenceImages ?? [])
21
25
  const { client, baseUrl, authToken } = await getCliClient({
22
26
  baseUrl: opts.baseUrl,
23
27
  requireAuth: true,
@@ -27,6 +31,7 @@ export async function generateCommand(
27
31
  const started = await startGeneration(client, {
28
32
  description,
29
33
  type: opts.type,
34
+ referenceImages: referenceImages.length ? referenceImages : undefined,
30
35
  access: opts.access,
31
36
  })
32
37
  // Fast providers finish inline; slow, job-based ones return a job to poll.
@@ -0,0 +1,25 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { extname } from 'node:path'
3
+
4
+ /**
5
+ * Resolve repeatable `--reference-image` values into what the Market API takes: an http(s) URL
6
+ * passes through untouched (the provider fetches it), a local file becomes a data URI.
7
+ */
8
+ export function loadReferenceImages(refs: string[]): Promise<string[]> {
9
+ return Promise.all(refs.map(loadReferenceImage))
10
+ }
11
+
12
+ async function loadReferenceImage(ref: string): Promise<string> {
13
+ if (ref.startsWith('http://') || ref.startsWith('https://')) return ref
14
+ const data = await readFile(ref)
15
+ return `data:${mimeFor(ref)};base64,${data.toString('base64')}`
16
+ }
17
+
18
+ function mimeFor(path: string): string {
19
+ const ext = extname(path).toLowerCase()
20
+ if (ext === '.png') return 'image/png'
21
+ if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg'
22
+ if (ext === '.webp') return 'image/webp'
23
+ if (ext === '.gif') return 'image/gif'
24
+ throw new Error(`Unsupported reference image type "${ext}": ${path}`)
25
+ }