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 ADDED
@@ -0,0 +1,32 @@
1
+ # artbot
2
+
3
+ Command-line client for ArtBot market research runs.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g artbot
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ The CLI talks to an ArtBot API server. By default it uses `http://localhost:4000`.
14
+
15
+ ```bash
16
+ artbot runs list
17
+ artbot research artist --artist "Burhan Dogancay" --wait
18
+ artbot runs show --run-id <id>
19
+ ```
20
+
21
+ To point the CLI at a different backend:
22
+
23
+ ```bash
24
+ export API_BASE_URL=https://your-artbot-api.example.com
25
+ artbot runs list
26
+ ```
27
+
28
+ For local development in this monorepo, start the API and worker first:
29
+
30
+ ```bash
31
+ pnpm run start:artbot
32
+ ```
package/bin/artbot.cjs ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ (async () => {
4
+ try {
5
+ const { runCli } = await import("../dist/index.js");
6
+ process.exitCode = await runCli(process.argv);
7
+ } catch (error) {
8
+ console.error(error);
9
+ process.exitCode = 1;
10
+ }
11
+ })();
@@ -0,0 +1,667 @@
1
+ // src/lib/file-system.ts
2
+ import fs from "node:fs";
3
+ function pathExists(filePath) {
4
+ try {
5
+ fs.accessSync(filePath, fs.constants.F_OK);
6
+ return true;
7
+ } catch {
8
+ return false;
9
+ }
10
+ }
11
+ function statFile(filePath) {
12
+ try {
13
+ return fs.statSync(filePath);
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ // src/setup/env.ts
20
+ import fs2 from "node:fs";
21
+ import path from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ import { config as loadDotenv } from "dotenv";
24
+ var SETUP_PROFILE_BLUEPRINTS = [
25
+ {
26
+ id: "artsy-auth",
27
+ mode: "authorized",
28
+ sourceName: "Artsy",
29
+ sourcePatterns: ["artsy"]
30
+ },
31
+ {
32
+ id: "mutualart-auth",
33
+ mode: "authorized",
34
+ sourceName: "MutualArt",
35
+ sourcePatterns: ["mutualart"]
36
+ },
37
+ {
38
+ id: "sanatfiyat-license",
39
+ mode: "licensed",
40
+ sourceName: "Sanatfiyat",
41
+ sourcePatterns: ["sanatfiyat"]
42
+ },
43
+ {
44
+ id: "askart-license",
45
+ mode: "licensed",
46
+ sourceName: "askART",
47
+ sourcePatterns: ["askart"]
48
+ }
49
+ ];
50
+ var CLI_MODULE_DIR = fileURLToPath(new URL(".", import.meta.url));
51
+ var loadedEnvPath = null;
52
+ function formatEnvValue(value) {
53
+ if (/^[A-Za-z0-9_./,:-]+$/.test(value)) {
54
+ return value;
55
+ }
56
+ if (!value.includes("'") && !value.includes("\n") && !value.includes("\r")) {
57
+ return `'${value}'`;
58
+ }
59
+ return JSON.stringify(value);
60
+ }
61
+ function isWorkspaceRoot(directory) {
62
+ return fs2.existsSync(path.join(directory, "pnpm-workspace.yaml")) || fs2.existsSync(path.join(directory, "turbo.json"));
63
+ }
64
+ function coerceSearchDirectory(candidate) {
65
+ const resolved = path.resolve(candidate);
66
+ try {
67
+ return fs2.statSync(resolved).isDirectory() ? resolved : path.dirname(resolved);
68
+ } catch {
69
+ return path.extname(resolved) ? path.dirname(resolved) : resolved;
70
+ }
71
+ }
72
+ function findWorkspaceRoot(candidate) {
73
+ let current = coerceSearchDirectory(candidate);
74
+ while (true) {
75
+ if (isWorkspaceRoot(current)) {
76
+ return current;
77
+ }
78
+ const parent = path.dirname(current);
79
+ if (parent === current) {
80
+ return null;
81
+ }
82
+ current = parent;
83
+ }
84
+ }
85
+ function detectWorkspaceRoot(cwd = process.cwd()) {
86
+ const candidates = [
87
+ process.env.INIT_CWD,
88
+ cwd,
89
+ process.env.ARTBOT_ROOT,
90
+ process.env.RUNS_ROOT,
91
+ process.env.DATABASE_PATH,
92
+ CLI_MODULE_DIR
93
+ ].filter((candidate) => Boolean(candidate && candidate.trim().length > 0));
94
+ for (const candidate of candidates) {
95
+ const workspaceRoot = findWorkspaceRoot(candidate);
96
+ if (workspaceRoot) {
97
+ return workspaceRoot;
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+ function resolveWorkspaceRoot(cwd = process.cwd()) {
103
+ return detectWorkspaceRoot(cwd) ?? path.resolve(cwd);
104
+ }
105
+ function hasLocalBackendWorkspace(cwd = process.cwd()) {
106
+ const workspaceRoot = detectWorkspaceRoot(cwd);
107
+ if (!workspaceRoot) {
108
+ return false;
109
+ }
110
+ return fs2.existsSync(path.join(workspaceRoot, "apps", "api", "package.json")) && fs2.existsSync(path.join(workspaceRoot, "apps", "worker", "package.json"));
111
+ }
112
+ function resolveEnvFilePath(cwd = process.cwd()) {
113
+ return path.resolve(resolveWorkspaceRoot(cwd), ".env");
114
+ }
115
+ function loadWorkspaceEnv(cwd = process.cwd()) {
116
+ const envPath = resolveEnvFilePath(cwd);
117
+ if (loadedEnvPath === envPath) {
118
+ return envPath;
119
+ }
120
+ loadDotenv({ path: envPath, override: false });
121
+ loadedEnvPath = envPath;
122
+ return envPath;
123
+ }
124
+ function parseBooleanEnv(value, fallback) {
125
+ if (value == null || value.trim() === "") return fallback;
126
+ return value.trim().toLowerCase() === "true";
127
+ }
128
+ function readEnvFile(envPath) {
129
+ try {
130
+ return fs2.readFileSync(envPath, "utf-8");
131
+ } catch {
132
+ return "";
133
+ }
134
+ }
135
+ function upsertEnvFile(envPath, updates) {
136
+ const existing = readEnvFile(envPath);
137
+ const lines = existing.length > 0 ? existing.split(/\r?\n/) : [];
138
+ const consumed = /* @__PURE__ */ new Set();
139
+ const nextLines = lines.map((line) => {
140
+ const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
141
+ if (!match) {
142
+ return line;
143
+ }
144
+ const key = match[1];
145
+ if (!(key in updates)) {
146
+ return line;
147
+ }
148
+ consumed.add(key);
149
+ return `${key}=${formatEnvValue(updates[key])}`;
150
+ });
151
+ for (const [key, value] of Object.entries(updates)) {
152
+ if (consumed.has(key)) continue;
153
+ nextLines.push(`${key}=${formatEnvValue(value)}`);
154
+ }
155
+ fs2.writeFileSync(envPath, `${nextLines.filter(Boolean).join("\n")}
156
+ `, "utf-8");
157
+ }
158
+ function buildDefaultAuthProfiles(options = {}) {
159
+ const cwd = resolveWorkspaceRoot(options.cwd ?? process.cwd());
160
+ const enableOptionalProbes = options.enableOptionalProbes ?? false;
161
+ const enableLicensedIntegrations = options.enableLicensedIntegrations ?? false;
162
+ return SETUP_PROFILE_BLUEPRINTS.filter((profile) => {
163
+ if (profile.id === "artsy-auth" || profile.id === "mutualart-auth") {
164
+ return enableOptionalProbes;
165
+ }
166
+ return enableLicensedIntegrations;
167
+ }).map((profile) => ({
168
+ id: profile.id,
169
+ mode: profile.mode,
170
+ sourcePatterns: profile.sourcePatterns,
171
+ storageStatePath: path.resolve(cwd, "playwright", ".auth", `${profile.id}.json`)
172
+ }));
173
+ }
174
+ function buildSetupEnvUpdates(values) {
175
+ return {
176
+ LLM_BASE_URL: values.llmBaseUrl,
177
+ API_BASE_URL: values.apiBaseUrl,
178
+ ENABLE_OPTIONAL_PROBE_ADAPTERS: String(values.enableOptionalProbes),
179
+ ENABLE_LICENSED_INTEGRATIONS: String(values.enableLicensedIntegrations),
180
+ DEFAULT_LICENSED_INTEGRATIONS: values.defaultLicensedIntegrations.join(","),
181
+ DEFAULT_AUTH_PROFILE: "",
182
+ AUTH_PROFILES_JSON: JSON.stringify(values.authProfiles)
183
+ };
184
+ }
185
+ function defaultSourceUrlForProfile(profileId) {
186
+ switch (profileId) {
187
+ case "artsy-auth":
188
+ return "https://www.artsy.net";
189
+ case "mutualart-auth":
190
+ return "https://www.mutualart.com";
191
+ case "sanatfiyat-license":
192
+ return "https://www.sanatfiyat.com";
193
+ case "askart-license":
194
+ return "https://www.askart.com";
195
+ default:
196
+ return "https://example.com";
197
+ }
198
+ }
199
+
200
+ // src/setup/auth.ts
201
+ import path2 from "node:path";
202
+ function normalizeSourcePattern(value) {
203
+ return value.trim().toLowerCase();
204
+ }
205
+ function buildAuthProfilesParseCandidates(rawValue) {
206
+ const trimmed = rawValue.trim();
207
+ const candidates = [trimmed];
208
+ const pushCandidate = (candidate) => {
209
+ if (candidate.length > 0 && !candidates.includes(candidate)) {
210
+ candidates.push(candidate);
211
+ }
212
+ };
213
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
214
+ pushCandidate(trimmed.slice(1, -1));
215
+ }
216
+ for (const candidate of [...candidates]) {
217
+ const repaired = candidate.replace(/\\"/g, '"');
218
+ if (repaired !== candidate) {
219
+ pushCandidate(repaired);
220
+ }
221
+ }
222
+ return candidates;
223
+ }
224
+ function parseAuthProfilesJson(rawValue) {
225
+ if (!rawValue || rawValue.trim().length === 0) {
226
+ return { profiles: [], error: null };
227
+ }
228
+ try {
229
+ let parsed;
230
+ let parsedSuccessfully = false;
231
+ let lastError;
232
+ for (const candidate of buildAuthProfilesParseCandidates(rawValue)) {
233
+ try {
234
+ parsed = candidate;
235
+ for (let depth = 0; depth < 3 && typeof parsed === "string"; depth += 1) {
236
+ parsed = JSON.parse(parsed);
237
+ }
238
+ parsedSuccessfully = true;
239
+ break;
240
+ } catch (error) {
241
+ lastError = error;
242
+ }
243
+ }
244
+ if (!parsedSuccessfully) {
245
+ throw lastError ?? new Error("AUTH_PROFILES_JSON is not valid JSON.");
246
+ }
247
+ if (!Array.isArray(parsed)) {
248
+ return {
249
+ profiles: [],
250
+ error: {
251
+ message: "AUTH_PROFILES_JSON must be a JSON array.",
252
+ rawValue
253
+ }
254
+ };
255
+ }
256
+ const profiles = [];
257
+ for (const [index, entry] of parsed.entries()) {
258
+ if (!entry || typeof entry !== "object") {
259
+ return {
260
+ profiles: [],
261
+ error: {
262
+ message: `AUTH_PROFILES_JSON entry ${index} is not an object.`,
263
+ rawValue
264
+ }
265
+ };
266
+ }
267
+ const candidate = entry;
268
+ if (typeof candidate.id !== "string" || candidate.id.trim().length === 0) {
269
+ return {
270
+ profiles: [],
271
+ error: {
272
+ message: `AUTH_PROFILES_JSON entry ${index} is missing a valid id.`,
273
+ rawValue
274
+ }
275
+ };
276
+ }
277
+ if (candidate.mode !== "authorized" && candidate.mode !== "licensed") {
278
+ return {
279
+ profiles: [],
280
+ error: {
281
+ message: `AUTH_PROFILES_JSON entry ${index} has invalid mode "${String(candidate.mode)}".`,
282
+ rawValue
283
+ }
284
+ };
285
+ }
286
+ if (!Array.isArray(candidate.sourcePatterns) || candidate.sourcePatterns.some((pattern) => typeof pattern !== "string")) {
287
+ return {
288
+ profiles: [],
289
+ error: {
290
+ message: `AUTH_PROFILES_JSON entry ${index} must define sourcePatterns as an array of strings.`,
291
+ rawValue
292
+ }
293
+ };
294
+ }
295
+ profiles.push({
296
+ id: candidate.id.trim(),
297
+ mode: candidate.mode,
298
+ sourcePatterns: candidate.sourcePatterns.map((pattern) => pattern.trim()).filter(Boolean),
299
+ cookieFile: candidate.cookieFile,
300
+ usernameEnv: candidate.usernameEnv,
301
+ passwordEnv: candidate.passwordEnv,
302
+ apiKeyEnv: candidate.apiKeyEnv,
303
+ storageStatePath: candidate.storageStatePath,
304
+ sessionTtlMinutes: candidate.sessionTtlMinutes
305
+ });
306
+ }
307
+ return { profiles, error: null };
308
+ } catch (error) {
309
+ return {
310
+ profiles: [],
311
+ error: {
312
+ message: "AUTH_PROFILES_JSON is not valid JSON.",
313
+ details: error instanceof Error ? error.message : String(error),
314
+ rawValue
315
+ }
316
+ };
317
+ }
318
+ }
319
+ function resolveAuthProfilesFromEnv(env = process.env) {
320
+ if (env === process.env) {
321
+ loadWorkspaceEnv();
322
+ }
323
+ return parseAuthProfilesJson(env.AUTH_PROFILES_JSON);
324
+ }
325
+ function resolveStorageStatePath(profile, cwd = process.cwd()) {
326
+ return profile.storageStatePath ? path2.resolve(cwd, profile.storageStatePath) : path2.resolve(cwd, "playwright", ".auth", `${profile.id}.json`);
327
+ }
328
+ function inspectSessionState(profile, cwd = process.cwd(), now = /* @__PURE__ */ new Date()) {
329
+ const storageStatePath = resolveStorageStatePath(profile, cwd);
330
+ const exists = pathExists(storageStatePath);
331
+ const stat = exists ? statFile(storageStatePath) : null;
332
+ const lastModifiedAtIso = stat?.mtime.toISOString() ?? null;
333
+ const ttlMinutes = profile.sessionTtlMinutes ?? 6 * 60;
334
+ let expired = true;
335
+ if (exists && stat) {
336
+ expired = now.getTime() - stat.mtime.getTime() > ttlMinutes * 60 * 1e3;
337
+ }
338
+ return {
339
+ profileId: profile.id,
340
+ storageStatePath,
341
+ exists,
342
+ lastModifiedAtIso,
343
+ expired
344
+ };
345
+ }
346
+ function inspectSessionStates(profiles, cwd = process.cwd(), now = /* @__PURE__ */ new Date()) {
347
+ return profiles.map((profile) => inspectSessionState(profile, cwd, now));
348
+ }
349
+ function findAuthRelevantProfiles(profiles, sourceNames) {
350
+ const loweredSources = sourceNames.map(normalizeSourcePattern);
351
+ return profiles.map((profile) => {
352
+ const matchedSources = loweredSources.filter(
353
+ (source) => profile.sourcePatterns.some((pattern) => {
354
+ try {
355
+ return new RegExp(pattern, "i").test(source);
356
+ } catch {
357
+ return source.includes(normalizeSourcePattern(pattern));
358
+ }
359
+ })
360
+ );
361
+ return { profile, matchedSources };
362
+ }).filter((entry) => entry.matchedSources.length > 0);
363
+ }
364
+ function buildAuthCaptureCommand(profile, sourceUrl, storageStatePath = resolveStorageStatePath(profile)) {
365
+ const command = `artbot auth capture ${profile.id}`;
366
+ return {
367
+ profileId: profile.id,
368
+ sourceUrl,
369
+ storageStatePath,
370
+ command
371
+ };
372
+ }
373
+
374
+ // src/setup/workflow.ts
375
+ import * as clack from "@clack/prompts";
376
+ import picocolors from "picocolors";
377
+
378
+ // src/setup/backend.ts
379
+ import fs3 from "node:fs";
380
+ import path3 from "node:path";
381
+ import { spawn } from "node:child_process";
382
+ function resolveBackendStartMetadata(cwd = process.cwd(), apiBaseUrl = "http://localhost:4000") {
383
+ const workspaceRoot = resolveWorkspaceRoot(cwd);
384
+ return {
385
+ api: {
386
+ service: "api",
387
+ command: "pnpm --filter @artbot/api dev",
388
+ cwd: workspaceRoot,
389
+ displayName: "ArtBot API"
390
+ },
391
+ worker: {
392
+ service: "worker",
393
+ command: "pnpm --filter @artbot/worker dev",
394
+ cwd: workspaceRoot,
395
+ displayName: "ArtBot worker"
396
+ },
397
+ apiHealthPath: `${apiBaseUrl.replace(/\/$/, "")}/health`,
398
+ recommendedEntryCommand: "pnpm run start:artbot"
399
+ };
400
+ }
401
+ function spawnDetachedProcess(command, cwd, logPath) {
402
+ const logFd = fs3.openSync(logPath, "a");
403
+ const child = spawn(command, {
404
+ cwd,
405
+ shell: true,
406
+ detached: true,
407
+ stdio: ["ignore", logFd, logFd]
408
+ });
409
+ child.unref();
410
+ fs3.closeSync(logFd);
411
+ return child.pid ?? -1;
412
+ }
413
+ function startLocalBackendServices(cwd = process.cwd()) {
414
+ if (!hasLocalBackendWorkspace(cwd)) {
415
+ throw new Error("Local backend auto-start is only available from an ArtBot workspace checkout.");
416
+ }
417
+ const workspaceRoot = resolveWorkspaceRoot(cwd);
418
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
419
+ const logDir = path3.resolve(workspaceRoot, ".artbot-logs");
420
+ fs3.mkdirSync(logDir, { recursive: true });
421
+ const apiLogPath = path3.join(logDir, `api-${stamp}.log`);
422
+ const workerLogPath = path3.join(logDir, `worker-${stamp}.log`);
423
+ const metadata = resolveBackendStartMetadata(workspaceRoot);
424
+ const apiPid = spawnDetachedProcess(metadata.api.command, workspaceRoot, apiLogPath);
425
+ const workerPid = spawnDetachedProcess(metadata.worker.command, workspaceRoot, workerLogPath);
426
+ return {
427
+ logDir,
428
+ apiLogPath,
429
+ workerLogPath,
430
+ apiPid,
431
+ workerPid
432
+ };
433
+ }
434
+
435
+ // src/setup/health.ts
436
+ async function fetchWithTimeout(url, init, timeoutMs) {
437
+ const controller = new AbortController();
438
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
439
+ try {
440
+ return await fetch(url, { ...init, signal: controller.signal });
441
+ } finally {
442
+ clearTimeout(timeout);
443
+ }
444
+ }
445
+ async function checkLlmHealth(baseUrl, apiKey = "", timeoutMs = 1500) {
446
+ const headers = {};
447
+ if (apiKey) {
448
+ headers.authorization = `Bearer ${apiKey}`;
449
+ }
450
+ try {
451
+ const response = await fetchWithTimeout(`${baseUrl.replace(/\/$/, "")}/models`, { headers }, timeoutMs);
452
+ if (!response.ok) {
453
+ return {
454
+ ok: false,
455
+ baseUrl,
456
+ statusCode: response.status,
457
+ reason: `HTTP ${response.status}`
458
+ };
459
+ }
460
+ const payload = await response.json();
461
+ return {
462
+ ok: true,
463
+ baseUrl,
464
+ modelId: payload.data?.[0]?.id
465
+ };
466
+ } catch (error) {
467
+ return {
468
+ ok: false,
469
+ baseUrl,
470
+ reason: error instanceof Error ? error.message : String(error)
471
+ };
472
+ }
473
+ }
474
+ async function checkApiHealth(apiBaseUrl, apiKey = "", timeoutMs = 1500) {
475
+ const headers = {};
476
+ if (apiKey) {
477
+ headers["x-api-key"] = apiKey;
478
+ }
479
+ try {
480
+ const response = await fetchWithTimeout(`${apiBaseUrl.replace(/\/$/, "")}/health`, { headers }, timeoutMs);
481
+ if (!response.ok) {
482
+ return {
483
+ ok: false,
484
+ apiBaseUrl,
485
+ statusCode: response.status,
486
+ reason: `HTTP ${response.status}`
487
+ };
488
+ }
489
+ return { ok: true, apiBaseUrl };
490
+ } catch (error) {
491
+ return {
492
+ ok: false,
493
+ apiBaseUrl,
494
+ reason: error instanceof Error ? error.message : String(error)
495
+ };
496
+ }
497
+ }
498
+
499
+ // src/setup/workflow.ts
500
+ function createIssue(severity, code, message, detail) {
501
+ return { severity, code, message, detail };
502
+ }
503
+ async function assessLocalSetup(env = process.env, cwd = process.cwd()) {
504
+ if (env === process.env) {
505
+ loadWorkspaceEnv(cwd);
506
+ }
507
+ const workspaceRoot = detectWorkspaceRoot(cwd);
508
+ const resolvedCwd = resolveWorkspaceRoot(cwd);
509
+ const envPath = resolveEnvFilePath(resolvedCwd);
510
+ const localBackendAvailable = hasLocalBackendWorkspace(cwd);
511
+ const llmBaseUrl = env.LLM_BASE_URL?.trim() || "http://127.0.0.1:1234/v1";
512
+ const apiBaseUrl = env.API_BASE_URL?.trim() || "http://localhost:4000";
513
+ const llmHealth = await checkLlmHealth(llmBaseUrl, env.LLM_API_KEY ?? env.OPENAI_API_KEY ?? "lm-studio");
514
+ const apiHealth = await checkApiHealth(apiBaseUrl, env.ARTBOT_API_KEY);
515
+ const parsedProfiles = parseAuthProfilesJson(env.AUTH_PROFILES_JSON);
516
+ const profiles = parsedProfiles.profiles;
517
+ const enableOptionalProbes = parseBooleanEnv(env.ENABLE_OPTIONAL_PROBE_ADAPTERS, false);
518
+ const enableLicensedIntegrations = parseBooleanEnv(env.ENABLE_LICENSED_INTEGRATIONS, false);
519
+ const enabledSourceNames = [
520
+ ...enableOptionalProbes ? ["Artsy", "MutualArt", "askART"] : [],
521
+ ...enableLicensedIntegrations ? ["Sanatfiyat"] : []
522
+ ];
523
+ const relevantProfiles = findAuthRelevantProfiles(profiles, enabledSourceNames);
524
+ const sessionStates = inspectSessionStates(relevantProfiles.map((entry) => entry.profile), resolvedCwd);
525
+ const issues = [];
526
+ if (!llmHealth.ok) {
527
+ issues.push(createIssue("error", "llm_unreachable", "LM Studio is not reachable.", llmHealth.reason));
528
+ }
529
+ if (!apiHealth.ok) {
530
+ issues.push(createIssue("warning", "api_unreachable", "ArtBot API is not reachable.", apiHealth.reason));
531
+ if (!localBackendAvailable) {
532
+ issues.push(
533
+ createIssue(
534
+ "warning",
535
+ "local_backend_unavailable",
536
+ "Local backend auto-start is unavailable outside the ArtBot repo.",
537
+ "Set API_BASE_URL to a running ArtBot API."
538
+ )
539
+ );
540
+ }
541
+ }
542
+ if (parsedProfiles.error) {
543
+ issues.push(createIssue("error", "auth_profiles_invalid", parsedProfiles.error.message, parsedProfiles.error.details));
544
+ }
545
+ if (enabledSourceNames.length > 0 && profiles.length === 0) {
546
+ issues.push(createIssue("warning", "auth_profiles_missing", "Auth-capable sources are enabled but no auth profiles are configured."));
547
+ }
548
+ for (const session of sessionStates) {
549
+ if (!session.exists) {
550
+ issues.push(createIssue("warning", "auth_session_missing", `Missing browser session for ${session.profileId}.`, session.storageStatePath));
551
+ continue;
552
+ }
553
+ if (session.expired) {
554
+ issues.push(createIssue("warning", "auth_session_expired", `Saved browser session expired for ${session.profileId}.`, session.storageStatePath));
555
+ }
556
+ }
557
+ return {
558
+ cwd: resolvedCwd,
559
+ workspaceRoot,
560
+ envPath,
561
+ localBackendAvailable,
562
+ llmBaseUrl,
563
+ apiBaseUrl,
564
+ llmHealth,
565
+ apiHealth,
566
+ profiles,
567
+ authProfilesError: parsedProfiles.error,
568
+ relevantProfiles,
569
+ sessionStates,
570
+ issues
571
+ };
572
+ }
573
+ async function runSetupWizard(cwd = process.cwd()) {
574
+ const env = process.env;
575
+ const workspaceRoot = resolveWorkspaceRoot(cwd);
576
+ const localBackendAvailable = hasLocalBackendWorkspace(cwd);
577
+ const llmBaseUrl = await clack.text({
578
+ message: "LM Studio base URL",
579
+ initialValue: env.LLM_BASE_URL?.trim() || "http://127.0.0.1:1234/v1",
580
+ validate(input) {
581
+ return input.trim().length === 0 ? "LM Studio URL is required." : void 0;
582
+ }
583
+ });
584
+ if (clack.isCancel(llmBaseUrl)) throw new Error("Setup cancelled.");
585
+ const apiBaseUrl = await clack.text({
586
+ message: "ArtBot API base URL",
587
+ initialValue: env.API_BASE_URL?.trim() || "http://localhost:4000",
588
+ validate(input) {
589
+ return input.trim().length === 0 ? "API URL is required." : void 0;
590
+ }
591
+ });
592
+ if (clack.isCancel(apiBaseUrl)) throw new Error("Setup cancelled.");
593
+ const enableOptionalProbes = await clack.confirm({
594
+ message: "Enable optional probe sources (Artsy, MutualArt, askART)?",
595
+ initialValue: parseBooleanEnv(env.ENABLE_OPTIONAL_PROBE_ADAPTERS, false)
596
+ });
597
+ if (clack.isCancel(enableOptionalProbes)) throw new Error("Setup cancelled.");
598
+ const enableLicensedIntegrations = await clack.confirm({
599
+ message: "Enable licensed integrations?",
600
+ initialValue: parseBooleanEnv(env.ENABLE_LICENSED_INTEGRATIONS, true)
601
+ });
602
+ if (clack.isCancel(enableLicensedIntegrations)) throw new Error("Setup cancelled.");
603
+ const defaultLicensedIntegrations = enableLicensedIntegrations ? ["Sanatfiyat"] : [];
604
+ const authProfiles = buildDefaultAuthProfiles({
605
+ cwd: workspaceRoot,
606
+ enableOptionalProbes,
607
+ enableLicensedIntegrations
608
+ });
609
+ const values = {
610
+ llmBaseUrl: llmBaseUrl.trim(),
611
+ apiBaseUrl: apiBaseUrl.trim(),
612
+ enableOptionalProbes,
613
+ enableLicensedIntegrations,
614
+ defaultLicensedIntegrations,
615
+ authProfiles
616
+ };
617
+ const envPath = resolveEnvFilePath(workspaceRoot);
618
+ upsertEnvFile(envPath, buildSetupEnvUpdates(values));
619
+ clack.log.success(`Updated ${picocolors.bold(envPath)}`);
620
+ let backendStart = null;
621
+ const apiHealth = await checkApiHealth(values.apiBaseUrl, process.env.ARTBOT_API_KEY);
622
+ if (!apiHealth.ok && localBackendAvailable) {
623
+ const shouldStartBackend = await clack.confirm({
624
+ message: "ArtBot API is offline. Start local API and worker now?",
625
+ initialValue: true
626
+ });
627
+ if (clack.isCancel(shouldStartBackend)) throw new Error("Setup cancelled.");
628
+ if (shouldStartBackend) {
629
+ backendStart = startLocalBackendServices(workspaceRoot);
630
+ clack.log.info(`Started local backend. API log: ${backendStart.apiLogPath}`);
631
+ clack.log.info(`Worker log: ${backendStart.workerLogPath}`);
632
+ }
633
+ } else if (!apiHealth.ok) {
634
+ clack.log.info("Local backend auto-start is only available inside the ArtBot repo.");
635
+ clack.log.info("Set API_BASE_URL to a running ArtBot API or start the services manually.");
636
+ }
637
+ const captureNow = authProfiles.length > 0 ? await clack.confirm({
638
+ message: "Capture browser login sessions now?",
639
+ initialValue: false
640
+ }) : false;
641
+ if (clack.isCancel(captureNow)) throw new Error("Setup cancelled.");
642
+ if (captureNow) {
643
+ for (const profile of authProfiles) {
644
+ const shouldCaptureProfile = await clack.confirm({
645
+ message: `Capture session for ${profile.id}?`,
646
+ initialValue: !profile.id.startsWith("artsy") && !profile.id.startsWith("askart")
647
+ });
648
+ if (clack.isCancel(shouldCaptureProfile)) throw new Error("Setup cancelled.");
649
+ if (!shouldCaptureProfile) continue;
650
+ const command = buildAuthCaptureCommand(profile, defaultSourceUrlForProfile(profile.id));
651
+ clack.log.message(`${picocolors.cyan("Auth capture")}: ${command.command}`);
652
+ }
653
+ }
654
+ const assessment = await assessLocalSetup(process.env, workspaceRoot);
655
+ return { assessment, backendStart };
656
+ }
657
+
658
+ export {
659
+ pathExists,
660
+ loadWorkspaceEnv,
661
+ defaultSourceUrlForProfile,
662
+ resolveAuthProfilesFromEnv,
663
+ buildAuthCaptureCommand,
664
+ assessLocalSetup,
665
+ runSetupWizard
666
+ };
667
+ //# sourceMappingURL=chunk-KKAF45ZB.js.map