@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.
- package/dist/agent.d.ts +2 -2
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +15 -3
- package/dist/agent.js.map +1 -1
- package/dist/asset-implementation.d.ts +7 -0
- package/dist/asset-implementation.d.ts.map +1 -1
- package/dist/asset-implementation.js +1 -0
- package/dist/asset-implementation.js.map +1 -1
- package/dist/cli.js +8 -21
- package/dist/cli.js.map +1 -1
- package/dist/commands/agent.d.ts +2 -42
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +8 -382
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +3 -0
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/reference-images.d.ts +6 -0
- package/dist/commands/reference-images.d.ts.map +1 -0
- package/dist/commands/reference-images.js +28 -0
- package/dist/commands/reference-images.js.map +1 -0
- package/dist/contract.d.ts +4 -46
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -8
- package/dist/contract.js.map +1 -1
- package/dist/generate.d.ts +2 -0
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/output.js +6 -6
- package/dist/output.js.map +1 -1
- package/dist/schemas.d.ts +3 -46
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +20 -46
- package/dist/schemas.js.map +1 -1
- package/dist/skill.d.ts +1 -1
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +2 -0
- package/dist/skill.js.map +1 -1
- package/package.json +1 -1
- package/skills/market/SKILL.md +2 -0
- package/src/agent.ts +17 -4
- package/src/asset-implementation.ts +8 -0
- package/src/cli.ts +19 -33
- package/src/commands/agent.ts +11 -474
- package/src/commands/generate.ts +5 -0
- package/src/commands/reference-images.ts +25 -0
- package/src/contract.ts +3 -9
- package/src/generate.ts +2 -0
- package/src/index.ts +0 -7
- package/src/output.ts +5 -5
- package/src/schemas.ts +24 -53
- 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
|
|
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
|
-
.
|
|
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
|
-
|
|
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: {
|
|
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
|
})
|
package/src/commands/agent.ts
CHANGED
|
@@ -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
|
|
3
|
+
import { agentAndWait } from '../agent.js'
|
|
4
|
+
import { loadReferenceImages } from './reference-images.js'
|
|
8
5
|
import { assetVersionRef } from '../output.js'
|
|
9
|
-
import type {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
}
|
package/src/commands/generate.ts
CHANGED
|
@@ -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
|
+
}
|