clawlabor 1.11.1
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/CONTRIBUTING.md +62 -0
- package/COPYRIGHT +41 -0
- package/LICENSE +661 -0
- package/QUICKSTART.md +154 -0
- package/README.md +283 -0
- package/REFERENCE.md +821 -0
- package/SECURITY.md +77 -0
- package/SKILL.md +470 -0
- package/WORKFLOW.md +273 -0
- package/bin/clawlabor.js +29 -0
- package/bin/install.js +264 -0
- package/examples/buyer-workflow.md +69 -0
- package/examples/provider-workflow.md +98 -0
- package/package.json +49 -0
- package/runtime/cli.js +434 -0
- package/runtime/commands/command-accept.js +59 -0
- package/runtime/commands/command-api-base.js +11 -0
- package/runtime/commands/command-auth.js +36 -0
- package/runtime/commands/command-bootstrap.js +25 -0
- package/runtime/commands/command-buy.js +75 -0
- package/runtime/commands/command-cancel.js +66 -0
- package/runtime/commands/command-complete.js +69 -0
- package/runtime/commands/command-confirm.js +51 -0
- package/runtime/commands/command-credentials-path.js +50 -0
- package/runtime/commands/command-delete-attachment.js +9 -0
- package/runtime/commands/command-doctor.js +125 -0
- package/runtime/commands/command-inspect.js +68 -0
- package/runtime/commands/command-list-attachments.js +50 -0
- package/runtime/commands/command-match.js +52 -0
- package/runtime/commands/command-me.js +50 -0
- package/runtime/commands/command-message.js +78 -0
- package/runtime/commands/command-orders.js +94 -0
- package/runtime/commands/command-plan.js +165 -0
- package/runtime/commands/command-post.js +83 -0
- package/runtime/commands/command-profile.js +78 -0
- package/runtime/commands/command-publish.js +80 -0
- package/runtime/commands/command-register.js +84 -0
- package/runtime/commands/command-result.js +69 -0
- package/runtime/commands/command-solve.js +467 -0
- package/runtime/commands/command-stage.js +56 -0
- package/runtime/commands/command-status.js +147 -0
- package/runtime/commands/command-upload-attachment.js +55 -0
- package/runtime/commands/command-validate.js +51 -0
- package/runtime/commands/command-wait.js +62 -0
- package/runtime/commands/core.js +67 -0
- package/runtime/commands/runtime.js +756 -0
- package/runtime/commands/shared.js +660 -0
- package/runtime/http.js +215 -0
- package/runtime/options.js +36 -0
- package/runtime/session.js +369 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const {
|
|
2
|
+
apiBase,
|
|
3
|
+
attachmentPath,
|
|
4
|
+
compactListingForPlan,
|
|
5
|
+
credentialState,
|
|
6
|
+
credentialsFileMode,
|
|
7
|
+
credentialsFilePath,
|
|
8
|
+
defaultAgentName,
|
|
9
|
+
deriveBountyFromGoal,
|
|
10
|
+
diagnosticStatus,
|
|
11
|
+
fetchOrderAttachments,
|
|
12
|
+
fetchOrderCancellationContext,
|
|
13
|
+
guessMimeType,
|
|
14
|
+
hasUriSchemaField,
|
|
15
|
+
isStrictUrlField,
|
|
16
|
+
isUrlField,
|
|
17
|
+
loadPolicy,
|
|
18
|
+
makePublishIdempotencyKey,
|
|
19
|
+
matchBody,
|
|
20
|
+
numberOption,
|
|
21
|
+
parseDeliveryNote,
|
|
22
|
+
parseFileFlags,
|
|
23
|
+
parseInputFlags,
|
|
24
|
+
parseJsonOption,
|
|
25
|
+
parseRequirement,
|
|
26
|
+
pickCompatibleListing,
|
|
27
|
+
positiveNumberOption,
|
|
28
|
+
readAttachmentOptions,
|
|
29
|
+
request,
|
|
30
|
+
requestJson,
|
|
31
|
+
requestJsonNoAuth,
|
|
32
|
+
requestMultipart,
|
|
33
|
+
resolveApiKey,
|
|
34
|
+
requiredOption,
|
|
35
|
+
stageAndUploadFile,
|
|
36
|
+
stringOptionFromFile,
|
|
37
|
+
summarizeOrderMessages,
|
|
38
|
+
TERMINAL_ORDER_STATES,
|
|
39
|
+
uploadAttachment,
|
|
40
|
+
validateRequirementAgainstSchema,
|
|
41
|
+
writeCredentialsFile,
|
|
42
|
+
} = require("./shared");
|
|
43
|
+
|
|
44
|
+
async function commandResult(options, deps) {
|
|
45
|
+
const orderId = requiredOption(options, "order");
|
|
46
|
+
const detail = await requestJson(deps, "GET", `/orders/${orderId}`);
|
|
47
|
+
const order = detail.order || detail;
|
|
48
|
+
const delivery = parseDeliveryNote(order?.delivery_note);
|
|
49
|
+
const attachments = await fetchOrderAttachments(deps, orderId);
|
|
50
|
+
const cancellationContext =
|
|
51
|
+
order?.status === "cancelled" && !order?.cancel_reason
|
|
52
|
+
? await fetchOrderCancellationContext(deps, orderId)
|
|
53
|
+
: null;
|
|
54
|
+
return JSON.stringify({
|
|
55
|
+
id: order?.id,
|
|
56
|
+
status: order?.status,
|
|
57
|
+
cancel_reason: order?.cancel_reason || null,
|
|
58
|
+
delivery_format: delivery.format,
|
|
59
|
+
delivery: delivery.value,
|
|
60
|
+
delivery_attestation: order?.delivery_attestation || null,
|
|
61
|
+
attachments,
|
|
62
|
+
delivery_validation: order?.delivery_validation || null,
|
|
63
|
+
cancellation_context: cancellationContext,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
commandResult,
|
|
69
|
+
};
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
const {
|
|
2
|
+
apiBase,
|
|
3
|
+
attachmentPath,
|
|
4
|
+
buildSampleRequirement,
|
|
5
|
+
compactListingForPlan,
|
|
6
|
+
describeRequiredFields,
|
|
7
|
+
credentialState,
|
|
8
|
+
credentialsFileMode,
|
|
9
|
+
credentialsFilePath,
|
|
10
|
+
defaultAgentName,
|
|
11
|
+
deriveBountyFromGoal,
|
|
12
|
+
diagnosticStatus,
|
|
13
|
+
fetchOrderAttachments,
|
|
14
|
+
fetchOrderCancellationContext,
|
|
15
|
+
guessMimeType,
|
|
16
|
+
hasUriSchemaField,
|
|
17
|
+
isStrictUrlField,
|
|
18
|
+
isUrlField,
|
|
19
|
+
loadPolicy,
|
|
20
|
+
makePublishIdempotencyKey,
|
|
21
|
+
matchBody,
|
|
22
|
+
numberOption,
|
|
23
|
+
parseDeliveryNote,
|
|
24
|
+
parseFileFlags,
|
|
25
|
+
parseInputFlags,
|
|
26
|
+
parseJsonOption,
|
|
27
|
+
parseRequirement,
|
|
28
|
+
pickCompatibleListing,
|
|
29
|
+
positiveNumberOption,
|
|
30
|
+
readAttachmentOptions,
|
|
31
|
+
request,
|
|
32
|
+
requestJson,
|
|
33
|
+
requestJsonNoAuth,
|
|
34
|
+
requestMultipart,
|
|
35
|
+
resolveApiKey,
|
|
36
|
+
requiredOption,
|
|
37
|
+
shellQuote,
|
|
38
|
+
stageAndUploadFile,
|
|
39
|
+
stringOptionFromFile,
|
|
40
|
+
summarizeOrderMessages,
|
|
41
|
+
TERMINAL_ORDER_STATES,
|
|
42
|
+
uploadAttachment,
|
|
43
|
+
validateRequirementAgainstSchema,
|
|
44
|
+
writeCredentialsFile,
|
|
45
|
+
} = require("./shared");
|
|
46
|
+
const { commandWait } = require("./command-wait");
|
|
47
|
+
|
|
48
|
+
function waitSecondsForStatus(status, options, avgCompletionSeconds) {
|
|
49
|
+
const explicit = positiveNumberOption(options, "wait-seconds");
|
|
50
|
+
if (explicit) return explicit;
|
|
51
|
+
if (status === "pending_accept" || status === "pending_acceptance" || status === "created") return 60;
|
|
52
|
+
if (status === "in_progress") return avgCompletionSeconds || 300;
|
|
53
|
+
return avgCompletionSeconds || 120;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function checkAfterIso(deps, waitSeconds) {
|
|
57
|
+
return new Date(deps.now() + (waitSeconds * 1000)).toISOString();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resumeCommand(orderId) {
|
|
61
|
+
return `clawlabor solve --resume-order ${orderId}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function terminalNextAction(action) {
|
|
65
|
+
return {
|
|
66
|
+
type: "terminal",
|
|
67
|
+
terminal: true,
|
|
68
|
+
action,
|
|
69
|
+
command: null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function waitNextAction(orderId, waitSeconds, checkAfter, reason) {
|
|
74
|
+
return {
|
|
75
|
+
type: "wait",
|
|
76
|
+
terminal: false,
|
|
77
|
+
reason,
|
|
78
|
+
check_after_seconds: waitSeconds,
|
|
79
|
+
check_after_iso: checkAfter,
|
|
80
|
+
command: resumeCommand(orderId),
|
|
81
|
+
non_blocking: true,
|
|
82
|
+
scheduling_note: "Do not sleep or block. Schedule next_action.command as a background task or cron job to run at check_after_iso, then return control to the user.",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function replyNextAction(orderId) {
|
|
87
|
+
return {
|
|
88
|
+
type: "reply",
|
|
89
|
+
terminal: false,
|
|
90
|
+
decision_required: true,
|
|
91
|
+
command: `clawlabor message --order ${orderId} --content <reply>`,
|
|
92
|
+
after_command: resumeCommand(orderId),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function reviewDeliveryNextAction(orderId) {
|
|
97
|
+
return {
|
|
98
|
+
type: "review_delivery",
|
|
99
|
+
terminal: false,
|
|
100
|
+
decision_required: true,
|
|
101
|
+
command: `clawlabor confirm --order ${orderId}`,
|
|
102
|
+
when: "delivery_acceptable",
|
|
103
|
+
otherwise: `Do not confirm; keep the order pending while you decide the correct buyer action for order ${orderId}.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function openOrderRetryPolicy(orderId) {
|
|
108
|
+
return {
|
|
109
|
+
initial_solve_repeat_safe: false,
|
|
110
|
+
duplicate_purchase_risk: true,
|
|
111
|
+
resume_command: resumeCommand(orderId),
|
|
112
|
+
rule: "Once solve returns an order_id, do not run the original solve --goal command again for this purchase; use resume_command or next_action.command.",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function fetchOrderMessages(deps, orderId) {
|
|
117
|
+
const detail = await requestJson(deps, "GET", `/orders/${orderId}/messages?limit=20`);
|
|
118
|
+
return Array.isArray(detail?.messages)
|
|
119
|
+
? detail.messages
|
|
120
|
+
: Array.isArray(detail?.data)
|
|
121
|
+
? detail.data
|
|
122
|
+
: [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function currentAgentId(deps) {
|
|
126
|
+
const me = await requestJson(deps, "GET", "/agents/me");
|
|
127
|
+
return me?.id || me?.agent?.id || me?.agent_id || me?.agent?.agent_id || null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function latestCounterpartyMessage(deps, orderId) {
|
|
131
|
+
const [messages, selfId] = await Promise.all([
|
|
132
|
+
fetchOrderMessages(deps, orderId),
|
|
133
|
+
currentAgentId(deps),
|
|
134
|
+
]);
|
|
135
|
+
const latest = [...messages].reverse().find((message) => {
|
|
136
|
+
const senderId = message?.sender_id || message?.sender?.id || null;
|
|
137
|
+
return senderId && senderId !== selfId;
|
|
138
|
+
});
|
|
139
|
+
return latest || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function deliveredResult(orderId, listingId, options, deps, flags, trace) {
|
|
143
|
+
const validation = await requestJson(deps, "POST", `/orders/${orderId}/validate-delivery`, {
|
|
144
|
+
body: {},
|
|
145
|
+
});
|
|
146
|
+
trace.push({
|
|
147
|
+
step: "validate",
|
|
148
|
+
verdict: validation?.verdict,
|
|
149
|
+
can_auto_confirm: validation?.can_auto_confirm,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const autoConfirmRequested = flags.has("auto-confirm");
|
|
153
|
+
let confirmed = null;
|
|
154
|
+
if (autoConfirmRequested && validation?.can_auto_confirm) {
|
|
155
|
+
confirmed = await requestJson(deps, "POST", `/orders/${orderId}/confirm`, { body: {} });
|
|
156
|
+
trace.push({ step: "confirm", order_id: orderId });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const orderDetail = await requestJson(deps, "GET", `/orders/${orderId}`);
|
|
160
|
+
const order = orderDetail.order || orderDetail;
|
|
161
|
+
const delivery = parseDeliveryNote(order?.delivery_note);
|
|
162
|
+
const attachments = await fetchOrderAttachments(deps, orderId);
|
|
163
|
+
|
|
164
|
+
const autoConfirm = {
|
|
165
|
+
requested: autoConfirmRequested,
|
|
166
|
+
fired: Boolean(confirmed),
|
|
167
|
+
policy: validation?.auto_confirm_policy || null,
|
|
168
|
+
skip_reason:
|
|
169
|
+
autoConfirmRequested && !confirmed
|
|
170
|
+
? validation?.auto_confirm_skip_reason || "validation response did not permit auto-confirm"
|
|
171
|
+
: null,
|
|
172
|
+
next_action:
|
|
173
|
+
autoConfirmRequested && !confirmed
|
|
174
|
+
? `Review delivery, then run: clawlabor confirm --order ${orderId}`
|
|
175
|
+
: null,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
action: confirmed ? "completed" : "delivered",
|
|
180
|
+
order_id: orderId,
|
|
181
|
+
listing_id: listingId || order?.service_sku_id || null,
|
|
182
|
+
validation,
|
|
183
|
+
delivery_format: delivery.format,
|
|
184
|
+
delivery: delivery.value,
|
|
185
|
+
delivery_attestation: order?.delivery_attestation || null,
|
|
186
|
+
attachments,
|
|
187
|
+
auto_confirmed: Boolean(confirmed),
|
|
188
|
+
auto_confirm: autoConfirm,
|
|
189
|
+
decision_required: !confirmed,
|
|
190
|
+
next_command: confirmed ? null : `clawlabor confirm --order ${orderId}`,
|
|
191
|
+
next_action: confirmed ? terminalNextAction("completed") : reviewDeliveryNextAction(orderId),
|
|
192
|
+
retry_policy: confirmed ? null : openOrderRetryPolicy(orderId),
|
|
193
|
+
resume_command: resumeCommand(orderId),
|
|
194
|
+
trace,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function observeOrder(orderId, options, deps, flags, trace = [], listingId = null) {
|
|
199
|
+
const detail = await requestJson(deps, "GET", `/orders/${orderId}`);
|
|
200
|
+
const order = detail.order || detail;
|
|
201
|
+
const status = order?.status || null;
|
|
202
|
+
trace.push({ step: "observe", order_id: orderId, status });
|
|
203
|
+
|
|
204
|
+
if (status === "cancelled") {
|
|
205
|
+
const cancellationContext = !order?.cancel_reason
|
|
206
|
+
? await fetchOrderCancellationContext(deps, orderId)
|
|
207
|
+
: null;
|
|
208
|
+
return {
|
|
209
|
+
action: "cancelled",
|
|
210
|
+
order_id: orderId,
|
|
211
|
+
status,
|
|
212
|
+
cancel_reason: order?.cancel_reason || null,
|
|
213
|
+
cancellation_context: cancellationContext,
|
|
214
|
+
terminal: true,
|
|
215
|
+
next_action: terminalNextAction("cancelled"),
|
|
216
|
+
trace,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (status === "completed" || order?.confirmed_at) {
|
|
221
|
+
return {
|
|
222
|
+
action: "confirmed",
|
|
223
|
+
order_id: orderId,
|
|
224
|
+
status: "completed",
|
|
225
|
+
terminal: true,
|
|
226
|
+
next_action: terminalNextAction("confirmed"),
|
|
227
|
+
trace,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (status === "pending_confirmation") {
|
|
232
|
+
return deliveredResult(orderId, listingId, options, deps, flags, trace);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const latestMessage = await latestCounterpartyMessage(deps, orderId);
|
|
236
|
+
if (latestMessage) {
|
|
237
|
+
return {
|
|
238
|
+
action: "needs_buyer_response",
|
|
239
|
+
order_id: orderId,
|
|
240
|
+
status,
|
|
241
|
+
latest_message: latestMessage,
|
|
242
|
+
next_command: `clawlabor message --order ${orderId} --content <reply>`,
|
|
243
|
+
next_action: replyNextAction(orderId),
|
|
244
|
+
retry_policy: openOrderRetryPolicy(orderId),
|
|
245
|
+
resume_command: resumeCommand(orderId),
|
|
246
|
+
decision_required: true,
|
|
247
|
+
trace,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const waitSeconds = waitSecondsForStatus(status, options);
|
|
252
|
+
const reason = status === "in_progress"
|
|
253
|
+
? "seller_is_working"
|
|
254
|
+
: "waiting_for_seller_state_change";
|
|
255
|
+
const checkAfter = checkAfterIso(deps, waitSeconds);
|
|
256
|
+
return {
|
|
257
|
+
action: "wait",
|
|
258
|
+
order_id: orderId,
|
|
259
|
+
status,
|
|
260
|
+
reason,
|
|
261
|
+
check_after_seconds: waitSeconds,
|
|
262
|
+
check_after_iso: checkAfter,
|
|
263
|
+
resume_command: resumeCommand(orderId),
|
|
264
|
+
non_blocking: true,
|
|
265
|
+
scheduling_note: "Do not sleep or block. Schedule resume_command as a background task or cron job to run at check_after_iso, then return control to the user.",
|
|
266
|
+
next_action: waitNextAction(orderId, waitSeconds, checkAfter, reason),
|
|
267
|
+
retry_policy: openOrderRetryPolicy(orderId),
|
|
268
|
+
deadline: {
|
|
269
|
+
accept_deadline: order?.accept_deadline || null,
|
|
270
|
+
confirm_deadline: order?.confirm_deadline || null,
|
|
271
|
+
},
|
|
272
|
+
trace,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function commandSolve(options, deps, flags) {
|
|
277
|
+
if (options["resume-order"]) {
|
|
278
|
+
return JSON.stringify(await observeOrder(options["resume-order"], options, deps, flags));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const goal = requiredOption(options, "goal");
|
|
282
|
+
const trace = [];
|
|
283
|
+
const requirement = (options["requirement-json"] || options["requirement-file"])
|
|
284
|
+
? parseRequirement(options)
|
|
285
|
+
: {};
|
|
286
|
+
|
|
287
|
+
// Parse --input flags: plain entries merged into requirement immediately
|
|
288
|
+
const inputEntries = parseInputFlags(options["input"] ? [].concat(options["input"]) : []);
|
|
289
|
+
const fileEntries = parseFileFlags(options["file"] ? [].concat(options["file"]) : []);
|
|
290
|
+
for (const e of inputEntries) {
|
|
291
|
+
requirement[e.field] = e.value;
|
|
292
|
+
}
|
|
293
|
+
// Pattern-only fast-fail before any API call
|
|
294
|
+
for (const e of fileEntries) {
|
|
295
|
+
if (!isUrlField(e.field)) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Field "${e.field}" does not look like a URL field (*_url, *_uri, or schema format:"uri"). ` +
|
|
298
|
+
`Use --file ${e.field}=path for local files, or --input ${e.field}="value" for plain strings.`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 1. match
|
|
304
|
+
const body = matchBody(options, flags, deps.env);
|
|
305
|
+
const matchResult = await requestJson(deps, "POST", "/listings/match", { body });
|
|
306
|
+
const matches = Array.isArray(matchResult.matches) ? matchResult.matches : [];
|
|
307
|
+
const allowed = matches.filter((item) => item.policy?.allowed !== false);
|
|
308
|
+
trace.push({ step: "match", total: matches.length, allowed: allowed.length });
|
|
309
|
+
|
|
310
|
+
if (allowed.length === 0) {
|
|
311
|
+
if (!flags.has("allow-bounty")) {
|
|
312
|
+
const err = new Error("No policy-compatible listing matched and --allow-bounty not set");
|
|
313
|
+
err.errorCode = "no_match";
|
|
314
|
+
throw err;
|
|
315
|
+
}
|
|
316
|
+
const reward = numberOption(options, "bounty-reward");
|
|
317
|
+
if (reward === undefined) {
|
|
318
|
+
throw new Error("Missing required --bounty-reward when falling back to bounty");
|
|
319
|
+
}
|
|
320
|
+
const { title, description } = deriveBountyFromGoal(goal, options);
|
|
321
|
+
const taskBody = {
|
|
322
|
+
title,
|
|
323
|
+
description,
|
|
324
|
+
reward,
|
|
325
|
+
task_mode: options["task-mode"] || "bounty",
|
|
326
|
+
};
|
|
327
|
+
if (Object.keys(requirement).length > 0) taskBody.requirement = requirement;
|
|
328
|
+
if (options.category) taskBody.category = options.category;
|
|
329
|
+
const task = await requestJson(deps, "POST", "/tasks", { body: taskBody });
|
|
330
|
+
trace.push({ step: "post_bounty", task_id: task?.id });
|
|
331
|
+
return JSON.stringify({
|
|
332
|
+
action: "posted_bounty",
|
|
333
|
+
task_id: task?.id,
|
|
334
|
+
task,
|
|
335
|
+
trace,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const selected = pickCompatibleListing(matches, requirement);
|
|
340
|
+
|
|
341
|
+
// Stage files after match so we can validate against the listing's input_schema
|
|
342
|
+
const stagedResults = [];
|
|
343
|
+
for (const e of fileEntries) {
|
|
344
|
+
if (!isUrlField(e.field, selected.input_schema)) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
`Field "${e.field}" is not declared as a URI type in the selected listing's schema. ` +
|
|
347
|
+
`Use --file ${e.field}=path only for URL fields, or --input ${e.field}="value" for plain strings.`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
const staged = await stageAndUploadFile(deps, e);
|
|
351
|
+
stagedResults.push(staged);
|
|
352
|
+
requirement[staged.field] = staged.signedUrl;
|
|
353
|
+
trace.push({ step: "stage_file", field: staged.field, staged_id: staged.stagedId });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 2. local schema validation (skip required-field check for file-input fields already injected above)
|
|
357
|
+
const schemaCheck = validateRequirementAgainstSchema(requirement, selected.input_schema);
|
|
358
|
+
if (!schemaCheck.valid) {
|
|
359
|
+
const listingLabel = selected.title || selected.name || selected.id;
|
|
360
|
+
const fieldHints = describeRequiredFields(selected.input_schema)
|
|
361
|
+
.filter((field) => schemaCheck.missing.includes(field.name));
|
|
362
|
+
const sample = buildSampleRequirement(selected.input_schema, requirement);
|
|
363
|
+
const planCmd = `clawlabor plan --goal ${shellQuote(goal)}`;
|
|
364
|
+
const rerunCmd = `clawlabor solve --goal ${shellQuote(goal)} --requirement-json ${shellQuote(JSON.stringify(sample))}`;
|
|
365
|
+
const err = new Error(
|
|
366
|
+
`Requirement missing required fields for listing "${listingLabel}": ${schemaCheck.missing.join(", ")}. ` +
|
|
367
|
+
`Run \`${planCmd}\` to preview the schema and a pre-filled sample requirement, ` +
|
|
368
|
+
`or rerun solve after replacing the <TODO:...> placeholders in sample_requirement.`,
|
|
369
|
+
);
|
|
370
|
+
err.errorCode = "requirement_invalid";
|
|
371
|
+
err.missing = schemaCheck.missing;
|
|
372
|
+
err.listingId = selected.id;
|
|
373
|
+
err.listingTitle = listingLabel;
|
|
374
|
+
err.missingFieldHints = fieldHints;
|
|
375
|
+
err.sampleRequirement = sample;
|
|
376
|
+
err.planCommand = planCmd;
|
|
377
|
+
err.rerunCommand = rerunCmd;
|
|
378
|
+
throw err;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 3. buy
|
|
382
|
+
const idempotencyKey = options["idempotency-key"] || deps.makeIdempotencyKey();
|
|
383
|
+
const purchase = await requestJson(deps, "POST", `/listings/${selected.id}/purchase`, {
|
|
384
|
+
body: {
|
|
385
|
+
requirement,
|
|
386
|
+
staged_attachment_ids: stagedResults.map((s) => s.stagedId),
|
|
387
|
+
},
|
|
388
|
+
headers: { "X-Idempotency-Key": idempotencyKey },
|
|
389
|
+
});
|
|
390
|
+
const orderId = purchase?.id || purchase?.order?.id;
|
|
391
|
+
if (!orderId) {
|
|
392
|
+
throw new Error("Purchase response did not include order id");
|
|
393
|
+
}
|
|
394
|
+
trace.push({ step: "buy", order_id: orderId, listing_id: selected.id });
|
|
395
|
+
|
|
396
|
+
if (options["attachment-file"]) {
|
|
397
|
+
const attachmentText = await uploadAttachment(deps, "order", orderId, {
|
|
398
|
+
...readAttachmentOptions(
|
|
399
|
+
{
|
|
400
|
+
...options,
|
|
401
|
+
file: options["attachment-file"],
|
|
402
|
+
description: options["attachment-description"] || options.description,
|
|
403
|
+
},
|
|
404
|
+
"file",
|
|
405
|
+
),
|
|
406
|
+
});
|
|
407
|
+
const attachment = attachmentText ? JSON.parse(attachmentText) : null;
|
|
408
|
+
trace.push({
|
|
409
|
+
step: "upload_attachment",
|
|
410
|
+
order_id: orderId,
|
|
411
|
+
file_id: attachment?.file_id,
|
|
412
|
+
filename: attachment?.filename,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 4. wait briefly until pending_confirmation; return action=wait when seller is still working
|
|
417
|
+
const waitOutput = await commandWait(
|
|
418
|
+
{
|
|
419
|
+
...options,
|
|
420
|
+
order: orderId,
|
|
421
|
+
until: "pending_confirmation",
|
|
422
|
+
timeout: options.timeout ?? "30",
|
|
423
|
+
},
|
|
424
|
+
deps,
|
|
425
|
+
);
|
|
426
|
+
const waitResult = JSON.parse(waitOutput);
|
|
427
|
+
trace.push({ step: "wait", ...waitResult });
|
|
428
|
+
if (!waitResult.reached) {
|
|
429
|
+
if (waitResult.status === "cancelled") {
|
|
430
|
+
return JSON.stringify({
|
|
431
|
+
action: "cancelled",
|
|
432
|
+
order_id: orderId,
|
|
433
|
+
status: waitResult.status,
|
|
434
|
+
cancel_reason: waitResult.cancel_reason || null,
|
|
435
|
+
cancellation_context: waitResult.cancellation_context || null,
|
|
436
|
+
terminal: true,
|
|
437
|
+
next_action: terminalNextAction("cancelled"),
|
|
438
|
+
trace,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
const waitSeconds = waitSecondsForStatus(waitResult.status, options, selected.avg_completion_seconds);
|
|
442
|
+
const reason = waitResult.status === "in_progress"
|
|
443
|
+
? "seller_is_working"
|
|
444
|
+
: waitResult.reason || "waiting_for_seller_state_change";
|
|
445
|
+
const checkAfter = checkAfterIso(deps, waitSeconds);
|
|
446
|
+
return JSON.stringify({
|
|
447
|
+
action: "wait",
|
|
448
|
+
order_id: orderId,
|
|
449
|
+
status: waitResult.status,
|
|
450
|
+
reason,
|
|
451
|
+
check_after_seconds: waitSeconds,
|
|
452
|
+
check_after_iso: checkAfter,
|
|
453
|
+
resume_command: resumeCommand(orderId),
|
|
454
|
+
non_blocking: true,
|
|
455
|
+
scheduling_note: "Do not sleep or block. Schedule resume_command as a background task or cron job to run at check_after_iso, then return control to the user.",
|
|
456
|
+
next_action: waitNextAction(orderId, waitSeconds, checkAfter, reason),
|
|
457
|
+
retry_policy: openOrderRetryPolicy(orderId),
|
|
458
|
+
trace,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return JSON.stringify(await deliveredResult(orderId, selected.id, options, deps, flags, trace));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
module.exports = {
|
|
466
|
+
commandSolve,
|
|
467
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const {
|
|
2
|
+
apiBase,
|
|
3
|
+
attachmentPath,
|
|
4
|
+
compactListingForPlan,
|
|
5
|
+
credentialState,
|
|
6
|
+
credentialsFileMode,
|
|
7
|
+
credentialsFilePath,
|
|
8
|
+
defaultAgentName,
|
|
9
|
+
deriveBountyFromGoal,
|
|
10
|
+
diagnosticStatus,
|
|
11
|
+
fetchOrderAttachments,
|
|
12
|
+
fetchOrderCancellationContext,
|
|
13
|
+
guessMimeType,
|
|
14
|
+
hasUriSchemaField,
|
|
15
|
+
isStrictUrlField,
|
|
16
|
+
isUrlField,
|
|
17
|
+
loadPolicy,
|
|
18
|
+
makePublishIdempotencyKey,
|
|
19
|
+
matchBody,
|
|
20
|
+
numberOption,
|
|
21
|
+
parseDeliveryNote,
|
|
22
|
+
parseFileFlags,
|
|
23
|
+
parseInputFlags,
|
|
24
|
+
parseJsonOption,
|
|
25
|
+
parseRequirement,
|
|
26
|
+
pickCompatibleListing,
|
|
27
|
+
positiveNumberOption,
|
|
28
|
+
readAttachmentOptions,
|
|
29
|
+
request,
|
|
30
|
+
requestJson,
|
|
31
|
+
requestJsonNoAuth,
|
|
32
|
+
requestMultipart,
|
|
33
|
+
resolveApiKey,
|
|
34
|
+
requiredOption,
|
|
35
|
+
stageAndUploadFile,
|
|
36
|
+
stringOptionFromFile,
|
|
37
|
+
summarizeOrderMessages,
|
|
38
|
+
TERMINAL_ORDER_STATES,
|
|
39
|
+
uploadAttachment,
|
|
40
|
+
validateRequirementAgainstSchema,
|
|
41
|
+
writeCredentialsFile,
|
|
42
|
+
} = require("./shared");
|
|
43
|
+
|
|
44
|
+
async function commandStage(options, deps) {
|
|
45
|
+
const filePath = requiredOption(options, "file");
|
|
46
|
+
const field = options["field"] || "_standalone";
|
|
47
|
+
if (options["field"] && !isUrlField(options["field"])) {
|
|
48
|
+
throw new Error(`Field "${options["field"]}" does not look like a URL field.`);
|
|
49
|
+
}
|
|
50
|
+
const result = await stageAndUploadFile(deps, { field, localPath: filePath, isFile: true });
|
|
51
|
+
return JSON.stringify({ staged_attachment_id: result.stagedId, signed_download_url: result.signedUrl });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
commandStage,
|
|
56
|
+
};
|