artbot 0.1.0
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/README.md +32 -0
- package/bin/artbot.cjs +11 -0
- package/dist/chunks/chunk-KKAF45ZB.js +667 -0
- package/dist/chunks/chunk-KKAF45ZB.js.map +7 -0
- package/dist/chunks/interactive-DQDPPJBS.js +994 -0
- package/dist/chunks/interactive-DQDPPJBS.js.map +7 -0
- package/dist/index.js +892 -0
- package/dist/index.js.map +7 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assessLocalSetup,
|
|
3
|
+
buildAuthCaptureCommand,
|
|
4
|
+
defaultSourceUrlForProfile,
|
|
5
|
+
loadWorkspaceEnv,
|
|
6
|
+
resolveAuthProfilesFromEnv,
|
|
7
|
+
runSetupWizard
|
|
8
|
+
} from "./chunks/chunk-KKAF45ZB.js";
|
|
9
|
+
|
|
10
|
+
// src/index.ts
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { createRequire } from "node:module";
|
|
13
|
+
import { pathToFileURL } from "node:url";
|
|
14
|
+
import Table from "cli-table3";
|
|
15
|
+
import { Command, CommanderError } from "commander";
|
|
16
|
+
import ora from "ora";
|
|
17
|
+
import picocolors from "picocolors";
|
|
18
|
+
import { ZodError } from "zod";
|
|
19
|
+
|
|
20
|
+
// ../../packages/shared-types/src/query.ts
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
var researchQuerySchema = z.object({
|
|
23
|
+
artist: z.string().min(1),
|
|
24
|
+
title: z.string().optional(),
|
|
25
|
+
year: z.string().optional(),
|
|
26
|
+
medium: z.string().optional(),
|
|
27
|
+
dimensions: z.object({
|
|
28
|
+
heightCm: z.number().positive().optional(),
|
|
29
|
+
widthCm: z.number().positive().optional(),
|
|
30
|
+
depthCm: z.number().positive().optional(),
|
|
31
|
+
dimensionsText: z.string().optional()
|
|
32
|
+
}).optional(),
|
|
33
|
+
imagePath: z.string().optional(),
|
|
34
|
+
dateRange: z.object({
|
|
35
|
+
from: z.string().optional(),
|
|
36
|
+
to: z.string().optional()
|
|
37
|
+
}).optional(),
|
|
38
|
+
scope: z.enum(["turkey_only", "turkey_plus_international"]).default("turkey_plus_international"),
|
|
39
|
+
turkeyFirst: z.boolean().default(true),
|
|
40
|
+
analysisMode: z.enum(["comprehensive", "balanced", "fast"]).default("balanced"),
|
|
41
|
+
priceNormalization: z.enum(["legacy", "usd_dual", "usd_nominal", "usd_2026"]).default("usd_dual"),
|
|
42
|
+
authProfileId: z.string().optional(),
|
|
43
|
+
cookieFile: z.string().optional(),
|
|
44
|
+
manualLoginCheckpoint: z.boolean().default(false),
|
|
45
|
+
allowLicensed: z.boolean().default(false),
|
|
46
|
+
licensedIntegrations: z.array(z.string()).default([])
|
|
47
|
+
});
|
|
48
|
+
var researchArtistRequestSchema = z.object({
|
|
49
|
+
query: researchQuerySchema
|
|
50
|
+
});
|
|
51
|
+
var researchWorkRequestSchema = z.object({
|
|
52
|
+
query: researchQuerySchema.extend({
|
|
53
|
+
title: z.string().min(1)
|
|
54
|
+
})
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ../../packages/shared-types/src/record.ts
|
|
58
|
+
import { z as z2 } from "zod";
|
|
59
|
+
var priceRecordSchema = z2.object({
|
|
60
|
+
artist_name: z2.string(),
|
|
61
|
+
work_title: z2.string().nullable(),
|
|
62
|
+
alternate_title: z2.string().nullable(),
|
|
63
|
+
year: z2.string().nullable(),
|
|
64
|
+
medium: z2.string().nullable(),
|
|
65
|
+
support: z2.string().nullable(),
|
|
66
|
+
dimensions_text: z2.string().nullable(),
|
|
67
|
+
height_cm: z2.number().nullable(),
|
|
68
|
+
width_cm: z2.number().nullable(),
|
|
69
|
+
depth_cm: z2.number().nullable(),
|
|
70
|
+
signed: z2.boolean().nullable(),
|
|
71
|
+
dated: z2.boolean().nullable(),
|
|
72
|
+
edition_info: z2.string().nullable(),
|
|
73
|
+
is_unique_work: z2.boolean().nullable(),
|
|
74
|
+
venue_name: z2.string(),
|
|
75
|
+
venue_type: z2.enum(["auction_house", "gallery", "dealer", "marketplace", "database", "other"]),
|
|
76
|
+
city: z2.string().nullable(),
|
|
77
|
+
country: z2.string().nullable(),
|
|
78
|
+
source_name: z2.string(),
|
|
79
|
+
source_url: z2.string().url(),
|
|
80
|
+
source_page_type: z2.enum(["lot", "artist_page", "price_db", "listing", "article", "other"]),
|
|
81
|
+
sale_or_listing_date: z2.string().nullable(),
|
|
82
|
+
lot_number: z2.string().nullable(),
|
|
83
|
+
price_type: z2.enum([
|
|
84
|
+
"asking_price",
|
|
85
|
+
"estimate",
|
|
86
|
+
"hammer_price",
|
|
87
|
+
"realized_price",
|
|
88
|
+
"realized_with_buyers_premium",
|
|
89
|
+
"inquiry_only",
|
|
90
|
+
"unknown"
|
|
91
|
+
]),
|
|
92
|
+
estimate_low: z2.number().nullable(),
|
|
93
|
+
estimate_high: z2.number().nullable(),
|
|
94
|
+
price_amount: z2.number().nullable(),
|
|
95
|
+
currency: z2.string().nullable(),
|
|
96
|
+
normalized_price_try: z2.number().nullable(),
|
|
97
|
+
normalized_price_usd: z2.number().nullable(),
|
|
98
|
+
normalized_price_usd_nominal: z2.number().nullable().optional(),
|
|
99
|
+
normalized_price_usd_2026: z2.number().nullable().optional(),
|
|
100
|
+
fx_source: z2.string().nullable().optional(),
|
|
101
|
+
fx_date_used: z2.string().nullable().optional(),
|
|
102
|
+
inflation_source: z2.string().nullable().optional(),
|
|
103
|
+
inflation_base_year: z2.number().int().nullable().optional(),
|
|
104
|
+
buyers_premium_included: z2.boolean().nullable(),
|
|
105
|
+
image_url: z2.string().url().nullable(),
|
|
106
|
+
screenshot_path: z2.string().nullable(),
|
|
107
|
+
raw_snapshot_path: z2.string().nullable(),
|
|
108
|
+
visual_match_score: z2.number().nullable(),
|
|
109
|
+
metadata_match_score: z2.number().nullable(),
|
|
110
|
+
extraction_confidence: z2.number().min(0).max(1),
|
|
111
|
+
entity_match_confidence: z2.number().min(0).max(1),
|
|
112
|
+
source_reliability_confidence: z2.number().min(0).max(1),
|
|
113
|
+
valuation_confidence: z2.number().min(0).max(1),
|
|
114
|
+
overall_confidence: z2.number().min(0).max(1),
|
|
115
|
+
accepted_for_evidence: z2.boolean(),
|
|
116
|
+
accepted_for_valuation: z2.boolean(),
|
|
117
|
+
valuation_lane: z2.enum(["realized", "estimate", "asking", "none"]),
|
|
118
|
+
acceptance_reason: z2.enum([
|
|
119
|
+
"valuation_ready",
|
|
120
|
+
"estimate_range_ready",
|
|
121
|
+
"asking_price_ready",
|
|
122
|
+
"inquiry_only_evidence",
|
|
123
|
+
"price_hidden_evidence",
|
|
124
|
+
"generic_shell_page",
|
|
125
|
+
"missing_numeric_price",
|
|
126
|
+
"missing_currency",
|
|
127
|
+
"missing_estimate_range",
|
|
128
|
+
"unknown_price_type",
|
|
129
|
+
"blocked_access"
|
|
130
|
+
]),
|
|
131
|
+
rejection_reason: z2.string().nullable(),
|
|
132
|
+
valuation_eligibility_reason: z2.string().nullable(),
|
|
133
|
+
price_hidden: z2.boolean(),
|
|
134
|
+
source_access_status: z2.enum(["public_access", "auth_required", "licensed_access", "blocked", "price_hidden"]).default("public_access"),
|
|
135
|
+
notes: z2.array(z2.string())
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ../../packages/shared-types/src/evidence.ts
|
|
139
|
+
import { z as z3 } from "zod";
|
|
140
|
+
var sourceAttemptSchema = z3.object({
|
|
141
|
+
run_id: z3.string(),
|
|
142
|
+
source_name: z3.string(),
|
|
143
|
+
source_url: z3.string().url(),
|
|
144
|
+
canonical_url: z3.string().url().nullable(),
|
|
145
|
+
access_mode: z3.enum(["anonymous", "authorized", "licensed"]),
|
|
146
|
+
source_access_status: z3.enum([
|
|
147
|
+
"public_access",
|
|
148
|
+
"auth_required",
|
|
149
|
+
"licensed_access",
|
|
150
|
+
"blocked",
|
|
151
|
+
"price_hidden"
|
|
152
|
+
]),
|
|
153
|
+
access_reason: z3.string().nullable(),
|
|
154
|
+
blocker_reason: z3.string().nullable(),
|
|
155
|
+
extracted_fields: z3.record(z3.unknown()).default({}),
|
|
156
|
+
discovery_provenance: z3.enum(["seed", "query_variant", "listing_expansion", "signature_expansion", "direct_lot", "web_discovery"]).optional(),
|
|
157
|
+
discovery_score: z3.number().min(0).max(1).nullable().optional(),
|
|
158
|
+
discovered_from_url: z3.string().url().nullable().optional(),
|
|
159
|
+
screenshot_path: z3.string().nullable(),
|
|
160
|
+
pre_auth_screenshot_path: z3.string().nullable().optional(),
|
|
161
|
+
post_auth_screenshot_path: z3.string().nullable().optional(),
|
|
162
|
+
raw_snapshot_path: z3.string().nullable(),
|
|
163
|
+
trace_path: z3.string().nullable().optional(),
|
|
164
|
+
har_path: z3.string().nullable().optional(),
|
|
165
|
+
fetched_at: z3.string(),
|
|
166
|
+
parser_used: z3.string(),
|
|
167
|
+
model_used: z3.string().nullable(),
|
|
168
|
+
extraction_confidence: z3.number().min(0).max(1).nullable().optional(),
|
|
169
|
+
entity_match_confidence: z3.number().min(0).max(1).nullable().optional(),
|
|
170
|
+
source_reliability_confidence: z3.number().min(0).max(1).nullable().optional(),
|
|
171
|
+
confidence_score: z3.number().min(0).max(1),
|
|
172
|
+
accepted: z3.boolean(),
|
|
173
|
+
accepted_for_evidence: z3.boolean().optional(),
|
|
174
|
+
accepted_for_valuation: z3.boolean().optional(),
|
|
175
|
+
valuation_lane: z3.enum(["realized", "estimate", "asking", "none"]).optional(),
|
|
176
|
+
acceptance_reason: z3.enum([
|
|
177
|
+
"valuation_ready",
|
|
178
|
+
"estimate_range_ready",
|
|
179
|
+
"asking_price_ready",
|
|
180
|
+
"inquiry_only_evidence",
|
|
181
|
+
"price_hidden_evidence",
|
|
182
|
+
"generic_shell_page",
|
|
183
|
+
"missing_numeric_price",
|
|
184
|
+
"missing_currency",
|
|
185
|
+
"missing_estimate_range",
|
|
186
|
+
"unknown_price_type",
|
|
187
|
+
"blocked_access"
|
|
188
|
+
]),
|
|
189
|
+
rejection_reason: z3.string().nullable().optional(),
|
|
190
|
+
valuation_eligibility_reason: z3.string().nullable().optional()
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ../../packages/shared-types/src/run.ts
|
|
194
|
+
import { z as z4 } from "zod";
|
|
195
|
+
var runSummarySchema = z4.object({
|
|
196
|
+
run_id: z4.string(),
|
|
197
|
+
total_records: z4.number().int().nonnegative(),
|
|
198
|
+
total_attempts: z4.number().int().nonnegative().optional(),
|
|
199
|
+
evidence_records: z4.number().int().nonnegative().optional(),
|
|
200
|
+
valuation_eligible_records: z4.number().int().nonnegative().optional(),
|
|
201
|
+
accepted_records: z4.number().int().nonnegative(),
|
|
202
|
+
rejected_candidates: z4.number().int().nonnegative(),
|
|
203
|
+
discovered_candidates: z4.number().int().nonnegative(),
|
|
204
|
+
accepted_from_discovery: z4.number().int().nonnegative(),
|
|
205
|
+
priced_source_coverage_ratio: z4.number().min(0).max(1).optional(),
|
|
206
|
+
priced_crawled_source_coverage_ratio: z4.number().min(0).max(1).optional(),
|
|
207
|
+
source_candidate_breakdown: z4.record(z4.string(), z4.number().int().nonnegative()),
|
|
208
|
+
source_status_breakdown: z4.record(
|
|
209
|
+
z4.enum(["public_access", "auth_required", "licensed_access", "blocked", "price_hidden"]),
|
|
210
|
+
z4.number().int().nonnegative()
|
|
211
|
+
),
|
|
212
|
+
auth_mode_breakdown: z4.record(z4.enum(["anonymous", "authorized", "licensed"]), z4.number().int().nonnegative()),
|
|
213
|
+
acceptance_reason_breakdown: z4.record(
|
|
214
|
+
z4.enum([
|
|
215
|
+
"valuation_ready",
|
|
216
|
+
"estimate_range_ready",
|
|
217
|
+
"asking_price_ready",
|
|
218
|
+
"inquiry_only_evidence",
|
|
219
|
+
"price_hidden_evidence",
|
|
220
|
+
"generic_shell_page",
|
|
221
|
+
"missing_numeric_price",
|
|
222
|
+
"missing_currency",
|
|
223
|
+
"missing_estimate_range",
|
|
224
|
+
"unknown_price_type",
|
|
225
|
+
"blocked_access"
|
|
226
|
+
]),
|
|
227
|
+
z4.number().int().nonnegative()
|
|
228
|
+
).optional(),
|
|
229
|
+
valuation_generated: z4.boolean(),
|
|
230
|
+
valuation_reason: z4.string()
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// src/index.ts
|
|
234
|
+
var EXIT_CODES = {
|
|
235
|
+
OK: 0,
|
|
236
|
+
INPUT: 2,
|
|
237
|
+
API: 3,
|
|
238
|
+
TERMINAL: 4
|
|
239
|
+
};
|
|
240
|
+
var require2 = createRequire(import.meta.url);
|
|
241
|
+
var ApiRequestError = class extends Error {
|
|
242
|
+
constructor(status, body) {
|
|
243
|
+
super(`HTTP ${status}`);
|
|
244
|
+
this.status = status;
|
|
245
|
+
this.body = body;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
var InputValidationError = class extends Error {
|
|
249
|
+
};
|
|
250
|
+
var TerminalStateError = class extends Error {
|
|
251
|
+
constructor(details) {
|
|
252
|
+
super(`Run ${details.run.id} ended in a failed or blocked terminal state.`);
|
|
253
|
+
this.details = details;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
function toNumber(value) {
|
|
257
|
+
if (!value) return void 0;
|
|
258
|
+
const parsed = Number(value);
|
|
259
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
260
|
+
}
|
|
261
|
+
function toPositiveInt(value, fallback) {
|
|
262
|
+
if (!value) return fallback;
|
|
263
|
+
const parsed = Number(value);
|
|
264
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || !Number.isInteger(parsed)) {
|
|
265
|
+
throw new InputValidationError(`Expected a positive integer, received "${value}".`);
|
|
266
|
+
}
|
|
267
|
+
return parsed;
|
|
268
|
+
}
|
|
269
|
+
function parseRunStatus(value) {
|
|
270
|
+
if (!value) return void 0;
|
|
271
|
+
const normalized = value.trim().toLowerCase();
|
|
272
|
+
const allowed = ["pending", "running", "completed", "failed"];
|
|
273
|
+
if (!allowed.includes(normalized)) {
|
|
274
|
+
throw new InputValidationError(`Invalid status "${value}". Allowed values: ${allowed.join(", ")}.`);
|
|
275
|
+
}
|
|
276
|
+
return normalized;
|
|
277
|
+
}
|
|
278
|
+
function isRunBlocked(details) {
|
|
279
|
+
const summary = details.summary;
|
|
280
|
+
if (summary.accepted_records > 0) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
return summary.total_records > 0 && (summary.source_status_breakdown.blocked ?? 0) > 0;
|
|
284
|
+
}
|
|
285
|
+
function isFailedOrBlocked(details) {
|
|
286
|
+
if (details.run.status === "failed") {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
return details.run.status === "completed" && isRunBlocked(details);
|
|
290
|
+
}
|
|
291
|
+
function writeLine(writer, text) {
|
|
292
|
+
writer(`${text}
|
|
293
|
+
`);
|
|
294
|
+
}
|
|
295
|
+
function safeJsonParse(text) {
|
|
296
|
+
if (!text) return {};
|
|
297
|
+
return JSON.parse(text);
|
|
298
|
+
}
|
|
299
|
+
function resolveGlobals(command) {
|
|
300
|
+
const raw = command.optsWithGlobals();
|
|
301
|
+
const globals = {
|
|
302
|
+
json: Boolean(raw.json),
|
|
303
|
+
apiBaseUrl: raw.apiBaseUrl ?? process.env.API_BASE_URL ?? "http://localhost:4000",
|
|
304
|
+
apiKey: raw.apiKey ?? process.env.ARTBOT_API_KEY,
|
|
305
|
+
verbose: Boolean(raw.verbose),
|
|
306
|
+
quiet: Boolean(raw.quiet)
|
|
307
|
+
};
|
|
308
|
+
if (globals.verbose && globals.quiet) {
|
|
309
|
+
throw new InputValidationError("Choose either --verbose or --quiet, not both.");
|
|
310
|
+
}
|
|
311
|
+
return globals;
|
|
312
|
+
}
|
|
313
|
+
function logInfo(globals, ctx, text) {
|
|
314
|
+
if (globals.json || globals.quiet) return;
|
|
315
|
+
writeLine(ctx.deps.stdout, text);
|
|
316
|
+
}
|
|
317
|
+
function logVerbose(globals, ctx, text) {
|
|
318
|
+
if (globals.json || globals.quiet || !globals.verbose) return;
|
|
319
|
+
writeLine(ctx.deps.stderr, picocolors.dim(text));
|
|
320
|
+
}
|
|
321
|
+
function logError(globals, ctx, text) {
|
|
322
|
+
if (globals.json) {
|
|
323
|
+
writeLine(ctx.deps.stderr, text);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
writeLine(ctx.deps.stderr, picocolors.red(text));
|
|
327
|
+
}
|
|
328
|
+
function printJson(globals, ctx, payload) {
|
|
329
|
+
if (!globals.json) return;
|
|
330
|
+
writeLine(ctx.deps.stdout, JSON.stringify(payload, null, 2));
|
|
331
|
+
}
|
|
332
|
+
function buildQuery(options, requireTitle) {
|
|
333
|
+
if (requireTitle && !options.title) {
|
|
334
|
+
throw new InputValidationError("Missing required --title for work research.");
|
|
335
|
+
}
|
|
336
|
+
const query = {
|
|
337
|
+
artist: options.artist,
|
|
338
|
+
title: options.title,
|
|
339
|
+
year: options.year,
|
|
340
|
+
medium: options.medium,
|
|
341
|
+
dimensions: options.heightCm || options.widthCm || options.depthCm ? {
|
|
342
|
+
heightCm: toNumber(options.heightCm),
|
|
343
|
+
widthCm: toNumber(options.widthCm),
|
|
344
|
+
depthCm: toNumber(options.depthCm)
|
|
345
|
+
} : void 0,
|
|
346
|
+
imagePath: options.imagePath,
|
|
347
|
+
dateRange: options.dateFrom || options.dateTo ? {
|
|
348
|
+
from: options.dateFrom,
|
|
349
|
+
to: options.dateTo
|
|
350
|
+
} : void 0,
|
|
351
|
+
scope: options.scope ?? "turkey_plus_international",
|
|
352
|
+
turkeyFirst: options.turkeyFirst ?? true,
|
|
353
|
+
analysisMode: options.analysisMode ?? "comprehensive",
|
|
354
|
+
priceNormalization: options.priceNormalization ?? "usd_dual",
|
|
355
|
+
authProfileId: options.authProfile,
|
|
356
|
+
cookieFile: options.cookieFile,
|
|
357
|
+
manualLoginCheckpoint: options.manualLogin ?? false,
|
|
358
|
+
allowLicensed: options.allowLicensed ?? false,
|
|
359
|
+
licensedIntegrations: options.licensedIntegrations ? options.licensedIntegrations.split(",").map((entry) => entry.trim()).filter(Boolean) : []
|
|
360
|
+
};
|
|
361
|
+
return researchQuerySchema.parse(query);
|
|
362
|
+
}
|
|
363
|
+
async function requestJson(ctx, globals, method, path, payload) {
|
|
364
|
+
const response = await ctx.deps.fetchImpl(`${globals.apiBaseUrl}${path}`, {
|
|
365
|
+
method,
|
|
366
|
+
headers: {
|
|
367
|
+
"content-type": "application/json",
|
|
368
|
+
...globals.apiKey ? { "x-api-key": globals.apiKey } : {}
|
|
369
|
+
},
|
|
370
|
+
body: payload === void 0 ? void 0 : JSON.stringify(payload)
|
|
371
|
+
});
|
|
372
|
+
const text = await response.text();
|
|
373
|
+
const body = safeJsonParse(text);
|
|
374
|
+
if (!response.ok) {
|
|
375
|
+
throw new ApiRequestError(response.status, body);
|
|
376
|
+
}
|
|
377
|
+
return body;
|
|
378
|
+
}
|
|
379
|
+
function renderRunsTable(runs) {
|
|
380
|
+
const table = new Table({
|
|
381
|
+
head: ["Run ID", "Type", "Status", "Artist", "Created"],
|
|
382
|
+
wordWrap: true
|
|
383
|
+
});
|
|
384
|
+
for (const run of runs) {
|
|
385
|
+
table.push([
|
|
386
|
+
run.id,
|
|
387
|
+
run.runType,
|
|
388
|
+
run.status,
|
|
389
|
+
run.query.artist,
|
|
390
|
+
new Date(run.createdAt).toLocaleString("en-US")
|
|
391
|
+
]);
|
|
392
|
+
}
|
|
393
|
+
return table.toString();
|
|
394
|
+
}
|
|
395
|
+
function renderRecordsTable(records, limit = 8) {
|
|
396
|
+
const table = new Table({
|
|
397
|
+
head: ["Artist", "Work", "Source", "Price Type", "Amount", "Currency"],
|
|
398
|
+
wordWrap: true
|
|
399
|
+
});
|
|
400
|
+
for (const record of records.slice(0, limit)) {
|
|
401
|
+
table.push([
|
|
402
|
+
record.artist_name,
|
|
403
|
+
record.work_title ?? "-",
|
|
404
|
+
record.source_name,
|
|
405
|
+
record.price_type,
|
|
406
|
+
record.price_amount ?? "-",
|
|
407
|
+
record.currency ?? "-"
|
|
408
|
+
]);
|
|
409
|
+
}
|
|
410
|
+
return table.toString();
|
|
411
|
+
}
|
|
412
|
+
function renderSummaryTable(summary) {
|
|
413
|
+
const table = new Table({
|
|
414
|
+
head: ["Metric", "Value"]
|
|
415
|
+
});
|
|
416
|
+
const crawledCoverage = summary.priced_crawled_source_coverage_ratio ?? summary.priced_source_coverage_ratio;
|
|
417
|
+
table.push(
|
|
418
|
+
["Accepted", summary.accepted_records],
|
|
419
|
+
["Rejected", summary.rejected_candidates],
|
|
420
|
+
["Discovered Candidates", summary.discovered_candidates],
|
|
421
|
+
["Accepted from Discovery", summary.accepted_from_discovery],
|
|
422
|
+
[
|
|
423
|
+
"Priced Coverage (Crawled)",
|
|
424
|
+
crawledCoverage != null ? `${Math.round(crawledCoverage * 100)}%` : "n/a"
|
|
425
|
+
],
|
|
426
|
+
["Valuation Generated", summary.valuation_generated ? "yes" : "no"],
|
|
427
|
+
["Valuation Reason", summary.valuation_reason]
|
|
428
|
+
);
|
|
429
|
+
if (summary.priced_crawled_source_coverage_ratio != null && summary.priced_source_coverage_ratio != null) {
|
|
430
|
+
table.push(["Priced Coverage (Attempted)", `${Math.round(summary.priced_source_coverage_ratio * 100)}%`]);
|
|
431
|
+
}
|
|
432
|
+
return table.toString();
|
|
433
|
+
}
|
|
434
|
+
function renderBreakdownTable(title, values) {
|
|
435
|
+
const table = new Table({
|
|
436
|
+
head: [title, "Count"]
|
|
437
|
+
});
|
|
438
|
+
for (const [key, value] of Object.entries(values)) {
|
|
439
|
+
table.push([key, value]);
|
|
440
|
+
}
|
|
441
|
+
return table.toString();
|
|
442
|
+
}
|
|
443
|
+
function renderSetupAssessment(assessment) {
|
|
444
|
+
const table = new Table({
|
|
445
|
+
head: ["Check", "Status", "Detail"]
|
|
446
|
+
});
|
|
447
|
+
table.push(
|
|
448
|
+
[
|
|
449
|
+
"LM Studio",
|
|
450
|
+
assessment.llmHealth.ok ? picocolors.green("healthy") : picocolors.red("offline"),
|
|
451
|
+
assessment.llmHealth.modelId ?? assessment.llmHealth.reason ?? assessment.llmBaseUrl
|
|
452
|
+
],
|
|
453
|
+
[
|
|
454
|
+
"ArtBot API",
|
|
455
|
+
assessment.apiHealth.ok ? picocolors.green("healthy") : picocolors.yellow("offline"),
|
|
456
|
+
assessment.apiHealth.reason ?? assessment.apiBaseUrl
|
|
457
|
+
],
|
|
458
|
+
[
|
|
459
|
+
"Local Backend",
|
|
460
|
+
assessment.localBackendAvailable ? picocolors.green("available") : picocolors.yellow("external only"),
|
|
461
|
+
assessment.workspaceRoot ?? "No ArtBot workspace detected"
|
|
462
|
+
],
|
|
463
|
+
[
|
|
464
|
+
"Auth Profiles",
|
|
465
|
+
assessment.authProfilesError ? picocolors.red("invalid") : picocolors.green(String(assessment.profiles.length)),
|
|
466
|
+
assessment.authProfilesError?.message ?? `${assessment.profiles.length} configured`
|
|
467
|
+
],
|
|
468
|
+
[
|
|
469
|
+
"Sessions",
|
|
470
|
+
assessment.sessionStates.length === 0 ? picocolors.yellow("none") : picocolors.green(String(assessment.sessionStates.length)),
|
|
471
|
+
assessment.sessionStates.map((session) => `${session.profileId}:${session.exists ? session.expired ? "expired" : "ready" : "missing"}`).join(", ") || "No relevant sessions"
|
|
472
|
+
]
|
|
473
|
+
);
|
|
474
|
+
return table.toString();
|
|
475
|
+
}
|
|
476
|
+
function renderSetupIssues(assessment) {
|
|
477
|
+
if (assessment.issues.length === 0) {
|
|
478
|
+
return picocolors.green("No setup issues detected.");
|
|
479
|
+
}
|
|
480
|
+
return assessment.issues.map((issue) => {
|
|
481
|
+
const prefix = issue.severity === "error" ? picocolors.red("error") : picocolors.yellow("warning");
|
|
482
|
+
return `${prefix} ${issue.message}${issue.detail ? ` (${issue.detail})` : ""}`;
|
|
483
|
+
}).join("\n");
|
|
484
|
+
}
|
|
485
|
+
function renderAuthProfilesTable(assessment) {
|
|
486
|
+
const table = new Table({
|
|
487
|
+
head: ["Profile", "Mode", "Matched Sources", "Storage State"]
|
|
488
|
+
});
|
|
489
|
+
const relevantById = new Map(assessment.relevantProfiles.map((entry) => [entry.profile.id, entry.matchedSources]));
|
|
490
|
+
const sessionsById = new Map(assessment.sessionStates.map((session) => [session.profileId, session]));
|
|
491
|
+
for (const profile of assessment.profiles) {
|
|
492
|
+
const session = sessionsById.get(profile.id);
|
|
493
|
+
table.push([
|
|
494
|
+
profile.id,
|
|
495
|
+
profile.mode,
|
|
496
|
+
(relevantById.get(profile.id) ?? []).join(", ") || "\u2014",
|
|
497
|
+
session ? `${session.exists ? session.expired ? "expired" : "ready" : "missing"} \xB7 ${session.storageStatePath}` : "\u2014"
|
|
498
|
+
]);
|
|
499
|
+
}
|
|
500
|
+
return table.toString();
|
|
501
|
+
}
|
|
502
|
+
function printRunDetailsHuman(globals, ctx, details) {
|
|
503
|
+
logInfo(globals, ctx, `Run ${details.run.id} (${details.run.runType})`);
|
|
504
|
+
logInfo(globals, ctx, `Status: ${details.run.status}`);
|
|
505
|
+
logInfo(globals, ctx, "");
|
|
506
|
+
logInfo(globals, ctx, renderSummaryTable(details.summary));
|
|
507
|
+
logInfo(globals, ctx, "");
|
|
508
|
+
logInfo(globals, ctx, renderBreakdownTable("Source Status", details.summary.source_status_breakdown));
|
|
509
|
+
logInfo(globals, ctx, "");
|
|
510
|
+
logInfo(globals, ctx, renderBreakdownTable("Auth Mode", details.summary.auth_mode_breakdown));
|
|
511
|
+
if (details.records.length > 0) {
|
|
512
|
+
logInfo(globals, ctx, "");
|
|
513
|
+
logInfo(globals, ctx, "Top comparable records:");
|
|
514
|
+
logInfo(globals, ctx, renderRecordsTable(details.records));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
async function waitForRunTerminal(ctx, globals, runId, intervalSeconds) {
|
|
518
|
+
const spinner = globals.json || globals.quiet ? null : ctx.deps.spinnerFactory(`Waiting for run ${runId}...`).start();
|
|
519
|
+
let previousStatus = null;
|
|
520
|
+
while (true) {
|
|
521
|
+
const details = await requestJson(ctx, globals, "GET", `/runs/${runId}`);
|
|
522
|
+
const status = details.run.status;
|
|
523
|
+
if (spinner) {
|
|
524
|
+
spinner.text = `Run ${runId} status: ${status}`;
|
|
525
|
+
}
|
|
526
|
+
if (previousStatus !== status) {
|
|
527
|
+
logVerbose(globals, ctx, `Run ${runId}: ${previousStatus ?? "unknown"} -> ${status}`);
|
|
528
|
+
previousStatus = status;
|
|
529
|
+
}
|
|
530
|
+
if (status === "completed" || status === "failed") {
|
|
531
|
+
if (spinner) {
|
|
532
|
+
if (isFailedOrBlocked(details)) {
|
|
533
|
+
spinner.fail(`Run ${runId} ended in failed/blocked state`);
|
|
534
|
+
} else {
|
|
535
|
+
spinner.succeed(`Run ${runId} completed`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return details;
|
|
539
|
+
}
|
|
540
|
+
await ctx.deps.sleep(intervalSeconds * 1e3);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async function handleResearch(ctx, options, command, runType) {
|
|
544
|
+
const globals = resolveGlobals(command);
|
|
545
|
+
const query = buildQuery(options, runType === "work");
|
|
546
|
+
const created = await requestJson(
|
|
547
|
+
ctx,
|
|
548
|
+
globals,
|
|
549
|
+
"POST",
|
|
550
|
+
runType === "artist" ? "/research/artist" : "/research/work",
|
|
551
|
+
{ query }
|
|
552
|
+
);
|
|
553
|
+
if (!options.wait) {
|
|
554
|
+
printJson(globals, ctx, created);
|
|
555
|
+
if (!globals.json) {
|
|
556
|
+
logInfo(globals, ctx, `Run created: ${created.runId} (${created.status})`);
|
|
557
|
+
logInfo(globals, ctx, `Use "artbot runs show --run-id ${created.runId}" to inspect details.`);
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const waitIntervalSeconds = toPositiveInt(options.waitInterval, 2);
|
|
562
|
+
const details = await waitForRunTerminal(ctx, globals, created.runId, waitIntervalSeconds);
|
|
563
|
+
printJson(globals, ctx, details);
|
|
564
|
+
if (!globals.json) {
|
|
565
|
+
printRunDetailsHuman(globals, ctx, details);
|
|
566
|
+
}
|
|
567
|
+
if (isFailedOrBlocked(details)) {
|
|
568
|
+
throw new TerminalStateError(details);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
async function handleRunsList(ctx, options, command) {
|
|
572
|
+
const globals = resolveGlobals(command);
|
|
573
|
+
const status = parseRunStatus(options.status);
|
|
574
|
+
const limit = toPositiveInt(options.limit, 20);
|
|
575
|
+
const query = new URLSearchParams();
|
|
576
|
+
if (status) query.set("status", status);
|
|
577
|
+
query.set("limit", String(limit));
|
|
578
|
+
const payload = await requestJson(ctx, globals, "GET", `/runs?${query.toString()}`);
|
|
579
|
+
printJson(globals, ctx, payload);
|
|
580
|
+
if (globals.json) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (payload.runs.length === 0) {
|
|
584
|
+
logInfo(globals, ctx, "No runs found for the selected filters.");
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
logInfo(globals, ctx, renderRunsTable(payload.runs));
|
|
588
|
+
}
|
|
589
|
+
async function handleRunsShow(ctx, options, command) {
|
|
590
|
+
const globals = resolveGlobals(command);
|
|
591
|
+
const details = await requestJson(ctx, globals, "GET", `/runs/${options.runId}`);
|
|
592
|
+
printJson(globals, ctx, details);
|
|
593
|
+
if (globals.json) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
printRunDetailsHuman(globals, ctx, details);
|
|
597
|
+
}
|
|
598
|
+
async function handleRunsWatch(ctx, options, command) {
|
|
599
|
+
const globals = resolveGlobals(command);
|
|
600
|
+
const intervalSeconds = toPositiveInt(options.interval, 2);
|
|
601
|
+
const details = await waitForRunTerminal(ctx, globals, options.runId, intervalSeconds);
|
|
602
|
+
printJson(globals, ctx, details);
|
|
603
|
+
if (!globals.json) {
|
|
604
|
+
printRunDetailsHuman(globals, ctx, details);
|
|
605
|
+
}
|
|
606
|
+
if (isFailedOrBlocked(details)) {
|
|
607
|
+
throw new TerminalStateError(details);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async function handleSetup(ctx, command) {
|
|
611
|
+
const globals = resolveGlobals(command);
|
|
612
|
+
const result = await runSetupWizard();
|
|
613
|
+
printJson(globals, ctx, result);
|
|
614
|
+
if (!globals.json) {
|
|
615
|
+
logInfo(globals, ctx, renderSetupAssessment(result.assessment));
|
|
616
|
+
logInfo(globals, ctx, "");
|
|
617
|
+
logInfo(globals, ctx, renderSetupIssues(result.assessment));
|
|
618
|
+
if (result.backendStart) {
|
|
619
|
+
logInfo(globals, ctx, "");
|
|
620
|
+
logInfo(globals, ctx, `Started local backend. API log: ${result.backendStart.apiLogPath}`);
|
|
621
|
+
logInfo(globals, ctx, `Started local backend. Worker log: ${result.backendStart.workerLogPath}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
async function handleDoctor(ctx, command) {
|
|
626
|
+
const globals = resolveGlobals(command);
|
|
627
|
+
const assessment = await assessLocalSetup();
|
|
628
|
+
printJson(globals, ctx, assessment);
|
|
629
|
+
if (globals.json) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
logInfo(globals, ctx, renderSetupAssessment(assessment));
|
|
633
|
+
logInfo(globals, ctx, "");
|
|
634
|
+
logInfo(globals, ctx, renderSetupIssues(assessment));
|
|
635
|
+
}
|
|
636
|
+
async function handleAuthList(ctx, command) {
|
|
637
|
+
const globals = resolveGlobals(command);
|
|
638
|
+
const assessment = await assessLocalSetup();
|
|
639
|
+
printJson(globals, ctx, {
|
|
640
|
+
profiles: assessment.profiles,
|
|
641
|
+
relevant_profiles: assessment.relevantProfiles,
|
|
642
|
+
session_states: assessment.sessionStates,
|
|
643
|
+
auth_profiles_error: assessment.authProfilesError
|
|
644
|
+
});
|
|
645
|
+
if (globals.json) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (assessment.authProfilesError) {
|
|
649
|
+
logError(globals, ctx, assessment.authProfilesError.message);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
logInfo(globals, ctx, renderAuthProfilesTable(assessment));
|
|
653
|
+
}
|
|
654
|
+
async function handleAuthStatus(ctx, command) {
|
|
655
|
+
const globals = resolveGlobals(command);
|
|
656
|
+
const assessment = await assessLocalSetup();
|
|
657
|
+
printJson(globals, ctx, assessment.sessionStates);
|
|
658
|
+
if (globals.json) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
logInfo(globals, ctx, renderSetupAssessment(assessment));
|
|
662
|
+
logInfo(globals, ctx, "");
|
|
663
|
+
logInfo(globals, ctx, renderAuthProfilesTable(assessment));
|
|
664
|
+
}
|
|
665
|
+
async function handleAuthCapture(ctx, options, command) {
|
|
666
|
+
const globals = resolveGlobals(command);
|
|
667
|
+
const parsed = resolveAuthProfilesFromEnv();
|
|
668
|
+
if (parsed.error) {
|
|
669
|
+
throw new InputValidationError(parsed.error.message);
|
|
670
|
+
}
|
|
671
|
+
const profile = parsed.profiles.find((entry) => entry.id === options.profileId);
|
|
672
|
+
if (!profile) {
|
|
673
|
+
throw new InputValidationError(`Unknown auth profile "${options.profileId}".`);
|
|
674
|
+
}
|
|
675
|
+
const capture = buildAuthCaptureCommand(profile, defaultSourceUrlForProfile(profile.id));
|
|
676
|
+
printJson(globals, ctx, capture);
|
|
677
|
+
if (globals.json) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
logInfo(globals, ctx, `Launching Playwright auth capture for ${profile.id}...`);
|
|
681
|
+
const playwrightCli = require2.resolve("playwright/cli");
|
|
682
|
+
const result = spawnSync(
|
|
683
|
+
process.execPath,
|
|
684
|
+
[playwrightCli, "codegen", capture.sourceUrl, `--save-storage=${capture.storageStatePath}`],
|
|
685
|
+
{
|
|
686
|
+
cwd: process.cwd(),
|
|
687
|
+
stdio: "inherit",
|
|
688
|
+
shell: false
|
|
689
|
+
}
|
|
690
|
+
);
|
|
691
|
+
if (result.status !== 0) {
|
|
692
|
+
throw new Error(`Auth capture exited with status ${result.status ?? 1}.`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function addCommonResearchFlags(command, withOptionalTitle) {
|
|
696
|
+
const target = command.requiredOption("--artist <name>", "Artist name").option("--year <year>", "Year").option("--medium <medium>", "Medium").option("--height-cm <number>", "Height in cm").option("--width-cm <number>", "Width in cm").option("--depth-cm <number>", "Depth in cm").option("--scope <scope>", "turkey_only or turkey_plus_international").option("--analysis-mode <mode>", "comprehensive | balanced | fast").option("--price-normalization <mode>", "legacy | usd_dual | usd_nominal | usd_2026").option("--no-turkey-first", "Disable Turkey-first source routing").option("--date-from <date>", "YYYY-MM-DD").option("--date-to <date>", "YYYY-MM-DD").option("--image-path <path>", "Path to local image").option("--auth-profile <id>", "Auth profile id").option("--cookie-file <path>", "Cookie JSON file").option("--manual-login", "Enable manual login checkpoint").option("--allow-licensed", "Allow licensed integrations").option("--licensed-integrations <list>", "Comma-separated source names").option("--wait", "Wait until run reaches terminal state").option("--wait-interval <seconds>", "Polling interval when --wait is enabled");
|
|
697
|
+
if (withOptionalTitle) {
|
|
698
|
+
target.option("--title <title>", "Work title");
|
|
699
|
+
}
|
|
700
|
+
return target;
|
|
701
|
+
}
|
|
702
|
+
function registerResearchCommands(program, ctx) {
|
|
703
|
+
const researchGroup = program.command("research").description("Research commands");
|
|
704
|
+
addCommonResearchFlags(researchGroup.command("artist").description("Research artist prices"), true).action(
|
|
705
|
+
async (options, command) => {
|
|
706
|
+
await handleResearch(ctx, options, command, "artist");
|
|
707
|
+
}
|
|
708
|
+
);
|
|
709
|
+
addCommonResearchFlags(researchGroup.command("work").description("Research specific work prices"), false).requiredOption("--title <title>", "Work title").action(async (options, command) => {
|
|
710
|
+
await handleResearch(ctx, options, command, "work");
|
|
711
|
+
});
|
|
712
|
+
addCommonResearchFlags(program.command("research-artist").description("Research artist prices (legacy)"), true).action(
|
|
713
|
+
async (options, command) => {
|
|
714
|
+
await handleResearch(ctx, options, command, "artist");
|
|
715
|
+
}
|
|
716
|
+
);
|
|
717
|
+
addCommonResearchFlags(program.command("research-work").description("Research specific work prices (legacy)"), false).requiredOption("--title <title>", "Work title").action(async (options, command) => {
|
|
718
|
+
await handleResearch(ctx, options, command, "work");
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
function registerRunsCommands(program, ctx) {
|
|
722
|
+
const runsGroup = program.command("runs").description("Run inspection commands");
|
|
723
|
+
runsGroup.command("list").description("List recent runs").option("--status <status>", "pending|running|completed|failed").option("--limit <number>", "Maximum number of runs").action(async (options, command) => {
|
|
724
|
+
await handleRunsList(ctx, options, command);
|
|
725
|
+
});
|
|
726
|
+
runsGroup.command("show").description("Show a run details summary").requiredOption("--run-id <id>", "Run identifier").action(async (options, command) => {
|
|
727
|
+
await handleRunsShow(ctx, options, command);
|
|
728
|
+
});
|
|
729
|
+
runsGroup.command("watch").description("Watch a run until terminal state").requiredOption("--run-id <id>", "Run identifier").option("--interval <seconds>", "Polling interval in seconds", "2").action(async (options, command) => {
|
|
730
|
+
await handleRunsWatch(ctx, options, command);
|
|
731
|
+
});
|
|
732
|
+
program.command("run-status").description("Show run details summary (legacy alias)").requiredOption("--run-id <id>", "Run identifier").action(async (options, command) => {
|
|
733
|
+
await handleRunsShow(ctx, options, command);
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
function registerSetupCommands(program, ctx) {
|
|
737
|
+
program.command("setup").description("Guided local onboarding for LM Studio, backend services, and auth profiles").action(async (_options, command) => {
|
|
738
|
+
await handleSetup(ctx, command);
|
|
739
|
+
});
|
|
740
|
+
program.command("doctor").description("Inspect local setup and health status").action(async (_options, command) => {
|
|
741
|
+
await handleDoctor(ctx, command);
|
|
742
|
+
});
|
|
743
|
+
const authGroup = program.command("auth").description("Auth profile and session-state commands");
|
|
744
|
+
authGroup.command("list").description("List configured auth profiles and matched sources").action(async (_options, command) => {
|
|
745
|
+
await handleAuthList(ctx, command);
|
|
746
|
+
});
|
|
747
|
+
authGroup.command("status").description("Inspect saved browser session state for auth profiles").action(async (_options, command) => {
|
|
748
|
+
await handleAuthStatus(ctx, command);
|
|
749
|
+
});
|
|
750
|
+
authGroup.command("capture").description("Capture browser auth state for a profile via Playwright").argument("<profileId>", "Profile identifier").action(async (profileId, _options, command) => {
|
|
751
|
+
await handleAuthCapture(ctx, { profileId }, command);
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
function defaultDeps(partial = {}) {
|
|
755
|
+
return {
|
|
756
|
+
fetchImpl: partial.fetchImpl ?? fetch,
|
|
757
|
+
sleep: partial.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
758
|
+
spinnerFactory: partial.spinnerFactory ?? ((text) => ora(text)),
|
|
759
|
+
stdout: partial.stdout ?? ((text) => process.stdout.write(text)),
|
|
760
|
+
stderr: partial.stderr ?? ((text) => process.stderr.write(text))
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
function mapErrorToExitCode(error) {
|
|
764
|
+
if (error instanceof CommanderError && error.code === "commander.helpDisplayed") {
|
|
765
|
+
return EXIT_CODES.OK;
|
|
766
|
+
}
|
|
767
|
+
if (error instanceof CommanderError) {
|
|
768
|
+
return EXIT_CODES.INPUT;
|
|
769
|
+
}
|
|
770
|
+
if (error instanceof InputValidationError || error instanceof ZodError) {
|
|
771
|
+
return EXIT_CODES.INPUT;
|
|
772
|
+
}
|
|
773
|
+
if (error instanceof TerminalStateError) {
|
|
774
|
+
return EXIT_CODES.TERMINAL;
|
|
775
|
+
}
|
|
776
|
+
if (error instanceof ApiRequestError) {
|
|
777
|
+
return EXIT_CODES.API;
|
|
778
|
+
}
|
|
779
|
+
return EXIT_CODES.API;
|
|
780
|
+
}
|
|
781
|
+
function formatError(error) {
|
|
782
|
+
const serializeBody = (value) => {
|
|
783
|
+
try {
|
|
784
|
+
const text = JSON.stringify(value);
|
|
785
|
+
return text.length > 320 ? `${text.slice(0, 317)}...` : text;
|
|
786
|
+
} catch {
|
|
787
|
+
return String(value);
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
if (error instanceof ApiRequestError) {
|
|
791
|
+
const body = serializeBody(error.body);
|
|
792
|
+
if (error.status === 401) {
|
|
793
|
+
return `API authentication failed (401). Next: pass --api-key or set ARTBOT_API_KEY.`;
|
|
794
|
+
}
|
|
795
|
+
if (error.status === 404) {
|
|
796
|
+
return `Requested resource was not found (404). Next: verify identifiers (for example --run-id) and retry.`;
|
|
797
|
+
}
|
|
798
|
+
if (error.status >= 500) {
|
|
799
|
+
return `API server error (${error.status}). Next: inspect API logs and retry.`;
|
|
800
|
+
}
|
|
801
|
+
return `API request failed (${error.status}). Next: rerun with --verbose and confirm request payload. Body: ${body}`;
|
|
802
|
+
}
|
|
803
|
+
if (error instanceof InputValidationError) {
|
|
804
|
+
return `${error.message} Next: run command with --help to verify valid options.`;
|
|
805
|
+
}
|
|
806
|
+
if (error instanceof ZodError) {
|
|
807
|
+
return `Invalid input: ${error.issues.map((issue) => issue.message).join("; ")} Next: run command with --help to verify required flags.`;
|
|
808
|
+
}
|
|
809
|
+
if (error instanceof TerminalStateError) {
|
|
810
|
+
return `Run ${error.details.run.id} reached a failed or blocked terminal state. Next: run "artbot runs show --run-id ${error.details.run.id}" for diagnostics.`;
|
|
811
|
+
}
|
|
812
|
+
if (error instanceof TypeError && /fetch/i.test(error.message)) {
|
|
813
|
+
return `Cannot reach API endpoint. Next: start the API service or set --api-base-url to the correct host.`;
|
|
814
|
+
}
|
|
815
|
+
if (error instanceof Error) {
|
|
816
|
+
return error.message;
|
|
817
|
+
}
|
|
818
|
+
return String(error);
|
|
819
|
+
}
|
|
820
|
+
function createProgram(ctx) {
|
|
821
|
+
const program = new Command();
|
|
822
|
+
program.name("artbot").description("Turkish art price research agent CLI").version("0.2.0");
|
|
823
|
+
program.showHelpAfterError().exitOverride().configureOutput({
|
|
824
|
+
writeOut: (text) => ctx.deps.stdout(text),
|
|
825
|
+
writeErr: (text) => ctx.deps.stderr(text)
|
|
826
|
+
});
|
|
827
|
+
program.option("--json", "Machine-readable JSON output").option("--api-base-url <url>", "API base URL").option("--api-key <key>", "API key override").option("--verbose", "Verbose diagnostics").option("--quiet", "Suppress non-error human output");
|
|
828
|
+
registerResearchCommands(program, ctx);
|
|
829
|
+
registerRunsCommands(program, ctx);
|
|
830
|
+
registerSetupCommands(program, ctx);
|
|
831
|
+
return program;
|
|
832
|
+
}
|
|
833
|
+
async function runCli(argv = process.argv, deps = {}) {
|
|
834
|
+
loadWorkspaceEnv();
|
|
835
|
+
const ctx = {
|
|
836
|
+
deps: defaultDeps(deps),
|
|
837
|
+
exitCode: EXIT_CODES.OK
|
|
838
|
+
};
|
|
839
|
+
const userArgs = argv.slice(2);
|
|
840
|
+
if (userArgs.length === 0 && process.stdout.isTTY) {
|
|
841
|
+
const { startInteractive } = await import("./chunks/interactive-DQDPPJBS.js");
|
|
842
|
+
return startInteractive();
|
|
843
|
+
}
|
|
844
|
+
const program = createProgram(ctx);
|
|
845
|
+
try {
|
|
846
|
+
await program.parseAsync(argv);
|
|
847
|
+
return ctx.exitCode;
|
|
848
|
+
} catch (error) {
|
|
849
|
+
const exitCode = mapErrorToExitCode(error);
|
|
850
|
+
if (!(error instanceof CommanderError && error.code === "commander.helpDisplayed")) {
|
|
851
|
+
const globals = (() => {
|
|
852
|
+
try {
|
|
853
|
+
return resolveGlobals(program);
|
|
854
|
+
} catch {
|
|
855
|
+
return {
|
|
856
|
+
json: false,
|
|
857
|
+
apiBaseUrl: process.env.API_BASE_URL ?? "http://localhost:4000",
|
|
858
|
+
apiKey: process.env.ARTBOT_API_KEY,
|
|
859
|
+
verbose: false,
|
|
860
|
+
quiet: false
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
})();
|
|
864
|
+
logError(globals, ctx, formatError(error));
|
|
865
|
+
}
|
|
866
|
+
return exitCode;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function isMainModule(currentImportUrl) {
|
|
870
|
+
const scriptPath = process.argv[1];
|
|
871
|
+
if (!scriptPath) return false;
|
|
872
|
+
return pathToFileURL(scriptPath).href === currentImportUrl;
|
|
873
|
+
}
|
|
874
|
+
if (isMainModule(import.meta.url)) {
|
|
875
|
+
runCli(process.argv).then((code) => {
|
|
876
|
+
process.exit(code);
|
|
877
|
+
}).catch((error) => {
|
|
878
|
+
const fallbackMessage = error instanceof Error ? error.message : String(error);
|
|
879
|
+
process.stderr.write(`${fallbackMessage}
|
|
880
|
+
`);
|
|
881
|
+
process.exit(EXIT_CODES.API);
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
export {
|
|
885
|
+
createProgram,
|
|
886
|
+
renderBreakdownTable,
|
|
887
|
+
renderRecordsTable,
|
|
888
|
+
renderRunsTable,
|
|
889
|
+
renderSummaryTable,
|
|
890
|
+
runCli
|
|
891
|
+
};
|
|
892
|
+
//# sourceMappingURL=index.js.map
|