@qloo/qloo-harness 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/setup.js ADDED
@@ -0,0 +1,1138 @@
1
+ import { createRequire as __qlooCreateRequire } from "node:module";
2
+ const require = __qlooCreateRequire(import.meta.url);
3
+
4
+ // apps/qloo-harness/dist/setup.js
5
+ import { createInterface } from "node:readline/promises";
6
+
7
+ // packages/qloo-client-ts/dist/config.js
8
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { dirname, isAbsolute, join, parse, resolve } from "node:path";
11
+ var QLOO_PRODUCTION_BASE_URL = "https://api.qloo.com";
12
+ function nonEmptyString(value) {
13
+ if (typeof value !== "string")
14
+ return void 0;
15
+ const trimmed = value.trim();
16
+ return trimmed || void 0;
17
+ }
18
+ function selectFileConfig(value) {
19
+ if (value === null || typeof value !== "object" || Array.isArray(value))
20
+ return {};
21
+ const record = value;
22
+ const apiKey = nonEmptyString(record.api_key);
23
+ const baseUrl = nonEmptyString(record.base_url);
24
+ const trustedBaseUrl = nonEmptyString(record.trusted_base_url);
25
+ const tasteResolverUrl = nonEmptyString(record.taste_resolver_url);
26
+ return {
27
+ ...apiKey ? { api_key: apiKey } : {},
28
+ ...baseUrl ? { base_url: baseUrl } : {},
29
+ ...trustedBaseUrl ? { trusted_base_url: trustedBaseUrl } : {},
30
+ ...tasteResolverUrl ? { taste_resolver_url: tasteResolverUrl } : {}
31
+ };
32
+ }
33
+ function comparableBaseUrl(value) {
34
+ try {
35
+ const url = new URL(value);
36
+ if (url.username || url.password || url.search || url.hash)
37
+ return void 0;
38
+ url.pathname = url.pathname.replace(/\/+$/u, "") || "/";
39
+ return url.toString();
40
+ } catch {
41
+ return void 0;
42
+ }
43
+ }
44
+ function areEquivalentQlooBaseUrls(left, right) {
45
+ const normalizedLeft = comparableBaseUrl(left);
46
+ return normalizedLeft !== void 0 && normalizedLeft === comparableBaseUrl(right);
47
+ }
48
+ function isOfficialQlooBaseUrl(value) {
49
+ return areEquivalentQlooBaseUrls(value, QLOO_PRODUCTION_BASE_URL);
50
+ }
51
+ function parseQlooConfig(content) {
52
+ const trimmed = content.trim();
53
+ if (!trimmed)
54
+ return {};
55
+ if (trimmed.startsWith("{"))
56
+ return selectFileConfig(JSON.parse(trimmed));
57
+ const values = {};
58
+ for (const line of content.split(/\r?\n/u)) {
59
+ const candidate = line.trim();
60
+ if (!candidate || candidate.startsWith("#"))
61
+ continue;
62
+ const separator = candidate.indexOf("=");
63
+ if (separator < 0)
64
+ continue;
65
+ const key = candidate.slice(0, separator).trim();
66
+ let value = candidate.slice(separator + 1).trim();
67
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
68
+ value = value.slice(1, -1);
69
+ }
70
+ values[key] = value;
71
+ }
72
+ return selectFileConfig(values);
73
+ }
74
+ function loadQlooConfig(configPath, readFile) {
75
+ try {
76
+ const content = readFile ? readFile(configPath) : readFileSync(configPath, "utf8");
77
+ return parseQlooConfig(content);
78
+ } catch {
79
+ return {};
80
+ }
81
+ }
82
+ function serializeQlooConfig(config) {
83
+ const lines = [
84
+ ["api_key", config.api_key],
85
+ ["base_url", config.base_url],
86
+ ["trusted_base_url", config.trusted_base_url],
87
+ ["taste_resolver_url", config.taste_resolver_url]
88
+ ].flatMap(([key, value]) => value ? [`${key}=${value}`] : []);
89
+ return `${lines.join("\n")}${lines.length > 0 ? "\n" : ""}`;
90
+ }
91
+ function saveQlooConfigValue(key, value, configPath = resolveQlooConfigPath()) {
92
+ const normalized = nonEmptyString(value);
93
+ if (!normalized || /[\r\n\u0000]/u.test(normalized)) {
94
+ throw new Error("Qloo configuration values must be non-empty single-line strings.");
95
+ }
96
+ const current = loadQlooConfig(configPath);
97
+ const directory = dirname(configPath);
98
+ const created = mkdirSync(directory, { recursive: true, mode: 448 });
99
+ if (created !== void 0 && process.platform !== "win32")
100
+ chmodSync(directory, 448);
101
+ writeFileSync(configPath, serializeQlooConfig({ ...current, [key]: normalized }), {
102
+ encoding: "utf8",
103
+ mode: 384
104
+ });
105
+ if (process.platform !== "win32")
106
+ chmodSync(configPath, 384);
107
+ }
108
+ function resolveQlooConfigPath(options = {}) {
109
+ const env = options.env ?? process.env;
110
+ const homeDirectory = resolve(options.homeDirectory ?? homedir());
111
+ const configuredRoot = nonEmptyString(env.QLOO_HOME);
112
+ if (!configuredRoot)
113
+ return join(homeDirectory, ".qloo", "config");
114
+ const root = configuredRoot === "~" ? homeDirectory : configuredRoot.startsWith("~/") || configuredRoot.startsWith("~\\") ? join(homeDirectory, configuredRoot.slice(2)) : isAbsolute(configuredRoot) ? resolve(configuredRoot) : resolve(configuredRoot);
115
+ if (root === homeDirectory || root === parse(root).root) {
116
+ throw new Error("QLOO_HOME must name a dedicated subdirectory, not a home or filesystem root.");
117
+ }
118
+ return join(root, "config");
119
+ }
120
+ function resolveQlooClientConfiguration(options = {}) {
121
+ const env = options.env ?? process.env;
122
+ const configPath = options.configPath ?? resolveQlooConfigPath({ env });
123
+ const fileConfig = loadQlooConfig(configPath, options.readFile);
124
+ const environmentApiKey = nonEmptyString(env.QLOO_API_KEY);
125
+ const environmentBaseUrl = nonEmptyString(env.QLOO_BASE_URL);
126
+ const environmentTrustedBaseUrl = nonEmptyString(env.QLOO_TRUSTED_BASE_URL);
127
+ const apiKey = environmentApiKey ?? fileConfig.api_key;
128
+ const baseUrl = environmentBaseUrl ?? fileConfig.base_url;
129
+ const effectiveBaseUrl = baseUrl ?? QLOO_PRODUCTION_BASE_URL;
130
+ const baseUrlTrustSource = isOfficialQlooBaseUrl(effectiveBaseUrl) ? "official" : environmentTrustedBaseUrl && areEquivalentQlooBaseUrls(effectiveBaseUrl, environmentTrustedBaseUrl) ? "environment" : fileConfig.trusted_base_url && areEquivalentQlooBaseUrls(effectiveBaseUrl, fileConfig.trusted_base_url) ? "config" : "missing";
131
+ return {
132
+ ...apiKey ? { apiKey } : {},
133
+ apiKeySource: environmentApiKey ? "environment" : fileConfig.api_key ? "config" : "missing",
134
+ ...baseUrl ? { baseUrl } : {},
135
+ baseUrlSource: environmentBaseUrl ? "environment" : fileConfig.base_url ? "config" : "default",
136
+ baseUrlTrusted: baseUrlTrustSource !== "missing",
137
+ baseUrlTrustSource,
138
+ fileConfig
139
+ };
140
+ }
141
+
142
+ // packages/qloo-client-ts/dist/errors.js
143
+ var QlooClientError = class extends Error {
144
+ code;
145
+ retryable;
146
+ status;
147
+ requestId;
148
+ details;
149
+ constructor(code, message, options = {}) {
150
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
151
+ this.name = "QlooClientError";
152
+ this.code = code;
153
+ this.retryable = options.retryable ?? false;
154
+ if (options.status !== void 0)
155
+ this.status = options.status;
156
+ if (options.requestId !== void 0)
157
+ this.requestId = options.requestId;
158
+ if (options.details !== void 0)
159
+ this.details = options.details;
160
+ }
161
+ toJSON() {
162
+ return {
163
+ name: this.name,
164
+ code: this.code,
165
+ message: this.message,
166
+ retryable: this.retryable,
167
+ ...this.status === void 0 ? {} : { status: this.status },
168
+ ...this.requestId === void 0 ? {} : { requestId: this.requestId },
169
+ ...this.details === void 0 ? {} : { details: this.details }
170
+ };
171
+ }
172
+ };
173
+ var DNS_CODES = /* @__PURE__ */ new Set([
174
+ "EAI_AGAIN",
175
+ "EAI_FAIL",
176
+ "ENODATA",
177
+ "ENOTFOUND",
178
+ "ENOTIMP",
179
+ "ENOTINITIALIZED",
180
+ "ESERVFAIL"
181
+ ]);
182
+ var CONNECTION_CODES = /* @__PURE__ */ new Set([
183
+ "ECONNABORTED",
184
+ "ECONNREFUSED",
185
+ "ECONNRESET",
186
+ "EHOSTDOWN",
187
+ "EHOSTUNREACH",
188
+ "ENETDOWN",
189
+ "ENETUNREACH",
190
+ "EPIPE",
191
+ "ETIMEDOUT",
192
+ "UND_ERR_CONNECT_TIMEOUT",
193
+ "UND_ERR_SOCKET"
194
+ ]);
195
+ var TLS_CODES = /* @__PURE__ */ new Set([
196
+ "CERT_HAS_EXPIRED",
197
+ "CERT_NOT_YET_VALID",
198
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
199
+ "ERR_TLS_CERT_ALTNAME_INVALID",
200
+ "SELF_SIGNED_CERT_IN_CHAIN",
201
+ "UNABLE_TO_GET_ISSUER_CERT",
202
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
203
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE"
204
+ ]);
205
+ var PROXY_CODES = /* @__PURE__ */ new Set([
206
+ "ERR_PROXY_AUTH_UNSUPPORTED",
207
+ "UND_ERR_PRX"
208
+ ]);
209
+ function isRecord(value) {
210
+ return typeof value === "object" && value !== null;
211
+ }
212
+ function safeErrorCode(value) {
213
+ if (!isRecord(value))
214
+ return void 0;
215
+ const code = value.code;
216
+ return typeof code === "string" && /^[A-Z][A-Z0-9_]{1,63}$/u.test(code) ? code : void 0;
217
+ }
218
+ function errorChain(value) {
219
+ const chain = [];
220
+ const seen = /* @__PURE__ */ new Set();
221
+ let current = value;
222
+ for (let depth = 0; depth < 8 && current !== void 0 && !seen.has(current); depth += 1) {
223
+ chain.push(current);
224
+ seen.add(current);
225
+ if (!isRecord(current))
226
+ break;
227
+ if (Array.isArray(current.errors) && current.errors.length > 0) {
228
+ chain.push(...current.errors.slice(0, 4));
229
+ }
230
+ current = current.cause;
231
+ }
232
+ return chain;
233
+ }
234
+ function classifyQlooNetworkFailure(cause) {
235
+ const chain = errorChain(cause);
236
+ const systemCode = chain.map(safeErrorCode).find((value) => value !== void 0);
237
+ const names = chain.map((value) => isRecord(value) && typeof value.name === "string" ? value.name : void 0).filter((value) => value !== void 0);
238
+ if (names.includes("AbortError") || systemCode === "ABORT_ERR") {
239
+ return {
240
+ code: "QLOO_ABORTED",
241
+ kind: "aborted",
242
+ message: "Qloo API request was cancelled.",
243
+ retryable: false,
244
+ ...systemCode ? { systemCode } : {}
245
+ };
246
+ }
247
+ if (systemCode && DNS_CODES.has(systemCode)) {
248
+ return {
249
+ code: "QLOO_DNS_ERROR",
250
+ kind: "dns",
251
+ message: "Could not resolve the Qloo API hostname. Check DNS and VPN configuration.",
252
+ retryable: true,
253
+ systemCode
254
+ };
255
+ }
256
+ if (systemCode && (TLS_CODES.has(systemCode) || systemCode.startsWith("ERR_TLS_") || systemCode.startsWith("ERR_SSL_"))) {
257
+ return {
258
+ code: "QLOO_TLS_ERROR",
259
+ kind: "tls",
260
+ message: "Could not establish a trusted TLS connection to the Qloo API.",
261
+ retryable: false,
262
+ systemCode
263
+ };
264
+ }
265
+ if (systemCode && PROXY_CODES.has(systemCode)) {
266
+ return {
267
+ code: "QLOO_PROXY_ERROR",
268
+ kind: "proxy",
269
+ message: "The configured network proxy could not reach the Qloo API.",
270
+ retryable: true,
271
+ systemCode
272
+ };
273
+ }
274
+ if (systemCode && CONNECTION_CODES.has(systemCode)) {
275
+ return {
276
+ code: "QLOO_CONNECTION_ERROR",
277
+ kind: "connection",
278
+ message: "Could not open a connection to the Qloo API. Check VPN, routing, and firewall configuration.",
279
+ retryable: true,
280
+ systemCode
281
+ };
282
+ }
283
+ return {
284
+ code: "QLOO_NETWORK_ERROR",
285
+ kind: "network",
286
+ message: "Unable to reach the Qloo API because of an unclassified network failure.",
287
+ retryable: true,
288
+ ...systemCode ? { systemCode } : {}
289
+ };
290
+ }
291
+
292
+ // packages/qloo-client-ts/dist/client.js
293
+ var DEFAULT_TIMEOUT_MS = 2e4;
294
+ var DEFAULT_MAX_ATTEMPTS = 3;
295
+ var DEFAULT_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
296
+ var DEFAULT_MAX_RETRY_DELAY_MS = 3e4;
297
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
298
+ function validatePositiveInteger(name, value) {
299
+ if (!Number.isSafeInteger(value) || value < 1) {
300
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", `${name} must be a positive integer.`);
301
+ }
302
+ }
303
+ function normalizeBaseUrl(value) {
304
+ let url;
305
+ try {
306
+ url = new URL(value);
307
+ } catch (cause) {
308
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", "Qloo base URL is invalid.", {
309
+ cause
310
+ });
311
+ }
312
+ const isLoopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
313
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) {
314
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", "Qloo base URL must use HTTPS (HTTP is allowed only for a loopback test server).");
315
+ }
316
+ if (url.username || url.password || url.search || url.hash) {
317
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", "Qloo base URL cannot contain credentials, query parameters, or a fragment.");
318
+ }
319
+ url.pathname = url.pathname.replace(/\/+$/, "") + "/";
320
+ return url;
321
+ }
322
+ function appendQuery(url, query) {
323
+ for (const [key, value] of Object.entries(query)) {
324
+ if (value === void 0 || value === null || value === "")
325
+ continue;
326
+ const serialized = Array.isArray(value) ? value.join(",") : String(value);
327
+ if (serialized !== "")
328
+ url.searchParams.set(key, serialized);
329
+ }
330
+ }
331
+ function requestIdFrom(response) {
332
+ return response.headers.get("x-qloo-request-id") ?? response.headers.get("x-request-id") ?? response.headers.get("request-id") ?? void 0;
333
+ }
334
+ function responseErrorMetadata(response) {
335
+ const requestId = requestIdFrom(response);
336
+ return {
337
+ status: response.status,
338
+ ...requestId ? { requestId } : {}
339
+ };
340
+ }
341
+ function retryAfterMilliseconds(response, now = Date.now()) {
342
+ const value = response.headers.get("retry-after");
343
+ if (!value)
344
+ return void 0;
345
+ const seconds = Number(value);
346
+ if (Number.isFinite(seconds) && seconds >= 0)
347
+ return seconds * 1e3;
348
+ const date = Date.parse(value);
349
+ if (!Number.isNaN(date))
350
+ return Math.max(0, date - now);
351
+ return void 0;
352
+ }
353
+ function responseMessage(body, response) {
354
+ if (body && typeof body === "object") {
355
+ for (const key of ["message", "reason", "error"]) {
356
+ const value = Reflect.get(body, key);
357
+ if (typeof value === "string" && value.trim())
358
+ return value;
359
+ }
360
+ }
361
+ return `Qloo API request failed with ${response.status} ${response.statusText || "HTTP error"}.`;
362
+ }
363
+ function createAttemptSignal(parent, timeoutMs) {
364
+ const controller = new AbortController();
365
+ let timedOut = false;
366
+ const timeout = setTimeout(() => {
367
+ timedOut = true;
368
+ controller.abort(new Error("Qloo request timed out."));
369
+ }, timeoutMs);
370
+ timeout.unref?.();
371
+ const onParentAbort = () => controller.abort(parent?.reason);
372
+ if (parent?.aborted)
373
+ onParentAbort();
374
+ else
375
+ parent?.addEventListener("abort", onParentAbort, { once: true });
376
+ return {
377
+ signal: controller.signal,
378
+ didTimeout: () => timedOut,
379
+ dispose: () => {
380
+ clearTimeout(timeout);
381
+ parent?.removeEventListener("abort", onParentAbort);
382
+ }
383
+ };
384
+ }
385
+ async function defaultSleep(milliseconds, signal) {
386
+ if (signal?.aborted)
387
+ throw signal.reason;
388
+ await new Promise((resolve2, reject) => {
389
+ const cleanup = () => signal?.removeEventListener("abort", onAbort);
390
+ const timeout = setTimeout(() => {
391
+ cleanup();
392
+ resolve2();
393
+ }, milliseconds);
394
+ const onAbort = () => {
395
+ clearTimeout(timeout);
396
+ cleanup();
397
+ reject(signal?.reason);
398
+ };
399
+ signal?.addEventListener("abort", onAbort, { once: true });
400
+ });
401
+ }
402
+ async function readBoundedBody(response, maxBytes) {
403
+ const contentLength = response.headers.get("content-length");
404
+ if (contentLength !== null && Number(contentLength) > maxBytes) {
405
+ await response.body?.cancel();
406
+ throw new QlooClientError("QLOO_RESPONSE_TOO_LARGE", `Qloo API response exceeded the ${maxBytes}-byte limit.`, responseErrorMetadata(response));
407
+ }
408
+ if (!response.body)
409
+ return "";
410
+ const reader = response.body.getReader();
411
+ const chunks = [];
412
+ let total = 0;
413
+ while (true) {
414
+ const { done, value } = await reader.read();
415
+ if (done)
416
+ break;
417
+ if (!value)
418
+ continue;
419
+ total += value.byteLength;
420
+ if (total > maxBytes) {
421
+ await reader.cancel();
422
+ throw new QlooClientError("QLOO_RESPONSE_TOO_LARGE", `Qloo API response exceeded the ${maxBytes}-byte limit.`, responseErrorMetadata(response));
423
+ }
424
+ chunks.push(value);
425
+ }
426
+ const body = new Uint8Array(total);
427
+ let offset = 0;
428
+ for (const chunk of chunks) {
429
+ body.set(chunk, offset);
430
+ offset += chunk.byteLength;
431
+ }
432
+ return new TextDecoder().decode(body);
433
+ }
434
+ function parseJsonBody(text, response) {
435
+ if (!text.trim())
436
+ return {};
437
+ try {
438
+ return JSON.parse(text);
439
+ } catch (cause) {
440
+ throw new QlooClientError("QLOO_RESPONSE_ERROR", "Qloo API returned invalid JSON.", {
441
+ cause,
442
+ retryable: response.status >= 500,
443
+ ...responseErrorMetadata(response)
444
+ });
445
+ }
446
+ }
447
+ var QlooClient = class {
448
+ baseUrl;
449
+ #apiKey;
450
+ #baseUrl;
451
+ #timeoutMs;
452
+ #maxAttempts;
453
+ #maxResponseBytes;
454
+ #maxRetryDelayMs;
455
+ #userAgent;
456
+ #fetch;
457
+ #sleep;
458
+ #random;
459
+ constructor(options) {
460
+ if (!options.apiKey?.trim()) {
461
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", "Qloo API key is required.");
462
+ }
463
+ this.#apiKey = options.apiKey;
464
+ this.#baseUrl = normalizeBaseUrl(options.baseUrl ?? QLOO_PRODUCTION_BASE_URL);
465
+ if (!isOfficialQlooBaseUrl(this.#baseUrl.toString()) && !options.allowCustomBaseUrl) {
466
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", "Custom Qloo base URL is not trusted. Approve the exact endpoint before sending credentials.");
467
+ }
468
+ this.baseUrl = this.#baseUrl.toString().replace(/\/$/, "");
469
+ this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
470
+ this.#maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
471
+ this.#maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
472
+ this.#maxRetryDelayMs = options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
473
+ this.#userAgent = options.userAgent ?? "agentic-qloo-client/0.1.0";
474
+ this.#fetch = options.fetch ?? globalThis.fetch;
475
+ this.#sleep = options.sleep ?? defaultSleep;
476
+ this.#random = options.random ?? Math.random;
477
+ validatePositiveInteger("timeoutMs", this.#timeoutMs);
478
+ validatePositiveInteger("maxAttempts", this.#maxAttempts);
479
+ validatePositiveInteger("maxResponseBytes", this.#maxResponseBytes);
480
+ validatePositiveInteger("maxRetryDelayMs", this.#maxRetryDelayMs);
481
+ }
482
+ async get(path, query = {}, options = {}) {
483
+ if (path.includes("\\") || path.startsWith("//") || /^[A-Za-z][A-Za-z\d+.-]*:/u.test(path) || /[?#\u0000-\u001f\u007f]/u.test(path)) {
484
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", "Qloo request path must be a relative API path without a query or fragment.");
485
+ }
486
+ const relativePath = path.replace(/^\/+/, "");
487
+ const url = new URL(relativePath, this.#baseUrl);
488
+ if (url.origin !== this.#baseUrl.origin || !url.pathname.startsWith(this.#baseUrl.pathname)) {
489
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", "Qloo request path cannot escape the trusted API base URL.");
490
+ }
491
+ appendQuery(url, query);
492
+ for (let attempt = 1; attempt <= this.#maxAttempts; attempt += 1) {
493
+ if (options.signal?.aborted)
494
+ throw options.signal.reason;
495
+ const attemptSignal = createAttemptSignal(options.signal, this.#timeoutMs);
496
+ let response;
497
+ let body;
498
+ try {
499
+ response = await this.#fetch(url, {
500
+ method: "GET",
501
+ headers: {
502
+ Accept: "application/json",
503
+ "X-Api-Key": this.#apiKey,
504
+ "User-Agent": this.#userAgent,
505
+ ...options.correlationId ? { "X-Correlation-Id": options.correlationId } : {}
506
+ },
507
+ signal: attemptSignal.signal
508
+ });
509
+ const text = await readBoundedBody(response, this.#maxResponseBytes);
510
+ body = parseJsonBody(text, response);
511
+ } catch (cause) {
512
+ const timedOut = attemptSignal.didTimeout();
513
+ attemptSignal.dispose();
514
+ if (options.signal?.aborted)
515
+ throw options.signal.reason;
516
+ if (cause instanceof QlooClientError) {
517
+ if (cause.retryable && attempt < this.#maxAttempts) {
518
+ await this.#sleep(this.#retryDelay(attempt), options.signal);
519
+ continue;
520
+ }
521
+ throw cause;
522
+ }
523
+ if (timedOut) {
524
+ if (attempt < this.#maxAttempts) {
525
+ await this.#sleep(this.#retryDelay(attempt), options.signal);
526
+ continue;
527
+ }
528
+ throw new QlooClientError("QLOO_TIMEOUT", "Qloo API request timed out.", {
529
+ cause,
530
+ retryable: true
531
+ });
532
+ }
533
+ if (attempt < this.#maxAttempts) {
534
+ await this.#sleep(this.#retryDelay(attempt), options.signal);
535
+ continue;
536
+ }
537
+ const failure = classifyQlooNetworkFailure(cause);
538
+ throw new QlooClientError(failure.code, failure.message, {
539
+ cause,
540
+ retryable: failure.retryable,
541
+ details: {
542
+ kind: failure.kind,
543
+ attempts: this.#maxAttempts,
544
+ ...failure.systemCode ? { systemCode: failure.systemCode } : {}
545
+ }
546
+ });
547
+ } finally {
548
+ attemptSignal.dispose();
549
+ }
550
+ if (response.ok)
551
+ return body;
552
+ const retryable = RETRYABLE_STATUS_CODES.has(response.status);
553
+ if (retryable && attempt < this.#maxAttempts) {
554
+ const retryAfter = retryAfterMilliseconds(response);
555
+ await this.#sleep(Math.min(retryAfter ?? this.#retryDelay(attempt), this.#maxRetryDelayMs), options.signal);
556
+ continue;
557
+ }
558
+ throw new QlooClientError("QLOO_HTTP_ERROR", responseMessage(body, response), {
559
+ retryable,
560
+ details: body,
561
+ ...responseErrorMetadata(response)
562
+ });
563
+ }
564
+ throw new QlooClientError("QLOO_NETWORK_ERROR", "Unable to reach the Qloo API.", {
565
+ retryable: true
566
+ });
567
+ }
568
+ apiInformation(options) {
569
+ return this.get("/", {}, options);
570
+ }
571
+ insights(query, options) {
572
+ return this.get("/v2/insights", query, options);
573
+ }
574
+ searchEntities(query, options) {
575
+ return this.get("/search", query, options);
576
+ }
577
+ entities(entityIds, options) {
578
+ return this.get("/entities", { entity_ids: entityIds }, options);
579
+ }
580
+ audiences(query = {}, options) {
581
+ return this.get("/v2/audiences", query, options);
582
+ }
583
+ audienceTypes(query = {}, options) {
584
+ return this.get("/v2/audiences/types", query, options);
585
+ }
586
+ tags(query = {}, options) {
587
+ return this.get("/v2/tags", query, options);
588
+ }
589
+ tagTypes(query = {}, options) {
590
+ return this.get("/v2/tags/types", query, options);
591
+ }
592
+ compare(query, options) {
593
+ return this.get("/v2/analysis/compare", query, options);
594
+ }
595
+ trending(query, options) {
596
+ return this.get("/v2/trending", query, options);
597
+ }
598
+ #retryDelay(attempt) {
599
+ const exponential = 250 * 2 ** (attempt - 1);
600
+ const jittered = exponential * (0.75 + this.#random() * 0.5);
601
+ return Math.min(Math.round(jittered), this.#maxRetryDelayMs);
602
+ }
603
+ };
604
+
605
+ // packages/qloo-client-ts/dist/diagnostics.js
606
+ var DEFAULT_PROBE_TIMEOUT_MS = 5e3;
607
+ function elapsed(startedAt, now) {
608
+ return Math.max(0, Math.round(now() - startedAt));
609
+ }
610
+ function configurationFailure(message, durationMs) {
611
+ return {
612
+ ok: false,
613
+ reachable: false,
614
+ kind: "configuration",
615
+ message,
616
+ durationMs,
617
+ retryable: false
618
+ };
619
+ }
620
+ function normalizedProbeUrl(value) {
621
+ const url = new URL(value);
622
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
623
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
624
+ throw new Error("Qloo base URL must use HTTPS.");
625
+ }
626
+ if (url.username || url.password || url.search || url.hash) {
627
+ throw new Error("Qloo base URL contains unsupported URL components.");
628
+ }
629
+ url.pathname = url.pathname.replace(/\/+$/u, "") || "/";
630
+ return url;
631
+ }
632
+ function httpFailure(status, durationMs, requestId) {
633
+ const metadata = {
634
+ durationMs,
635
+ reachable: true,
636
+ status,
637
+ ...requestId ? { requestId } : {}
638
+ };
639
+ if (status === 401 || status === 403) {
640
+ return {
641
+ ...metadata,
642
+ ok: false,
643
+ kind: "authentication",
644
+ message: `Qloo API is reachable but rejected the configured credential (HTTP ${status}).`,
645
+ retryable: false
646
+ };
647
+ }
648
+ if (status === 429) {
649
+ return {
650
+ ...metadata,
651
+ ok: false,
652
+ kind: "rate-limit",
653
+ message: "Qloo API is reachable but the credential is currently rate limited.",
654
+ retryable: true
655
+ };
656
+ }
657
+ if (status >= 500) {
658
+ return {
659
+ ...metadata,
660
+ ok: false,
661
+ kind: "service",
662
+ message: `Qloo API is reachable but returned a service error (HTTP ${status}).`,
663
+ retryable: true
664
+ };
665
+ }
666
+ return {
667
+ ...metadata,
668
+ ok: false,
669
+ kind: "http",
670
+ message: `Qloo API is reachable but the readiness request failed (HTTP ${status}).`,
671
+ retryable: false
672
+ };
673
+ }
674
+ function clientFailure(error, durationMs) {
675
+ if (error.code === "QLOO_HTTP_ERROR" && error.status !== void 0) {
676
+ return httpFailure(error.status, durationMs, error.requestId);
677
+ }
678
+ if (error.code === "QLOO_TIMEOUT") {
679
+ return {
680
+ ok: false,
681
+ reachable: false,
682
+ kind: "timeout",
683
+ message: "Timed out while connecting to the Qloo API. Check VPN routing and firewall configuration.",
684
+ durationMs,
685
+ retryable: true
686
+ };
687
+ }
688
+ if (error.code === "QLOO_CONFIGURATION_ERROR") {
689
+ return configurationFailure(error.message, durationMs);
690
+ }
691
+ const details = error.details && typeof error.details === "object" ? error.details : {};
692
+ const systemCode = typeof details.systemCode === "string" ? details.systemCode : void 0;
693
+ const kind = error.code === "QLOO_DNS_ERROR" ? "dns" : error.code === "QLOO_TLS_ERROR" ? "tls" : error.code === "QLOO_CONNECTION_ERROR" || error.code === "QLOO_PROXY_ERROR" ? "connection" : error.code === "QLOO_ABORTED" ? "cancelled" : "connection";
694
+ return {
695
+ ok: false,
696
+ reachable: false,
697
+ kind,
698
+ message: error.message,
699
+ durationMs,
700
+ retryable: error.retryable,
701
+ ...systemCode ? { systemCode } : {}
702
+ };
703
+ }
704
+ async function probeQlooConnectivity(options = {}) {
705
+ const now = options.now ?? Date.now;
706
+ const startedAt = now();
707
+ const baseUrl = options.baseUrl ?? QLOO_PRODUCTION_BASE_URL;
708
+ const timeoutMs = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
709
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
710
+ return configurationFailure("Qloo connectivity timeout must be a positive integer.", elapsed(startedAt, now));
711
+ }
712
+ if (!isOfficialQlooBaseUrl(baseUrl) && !options.allowCustomBaseUrl) {
713
+ return configurationFailure("Custom Qloo endpoint is not trusted; connectivity probe was not sent.", elapsed(startedAt, now));
714
+ }
715
+ if (options.apiKey?.trim()) {
716
+ try {
717
+ const client = new QlooClient({
718
+ apiKey: options.apiKey,
719
+ baseUrl,
720
+ ...options.allowCustomBaseUrl === void 0 ? {} : { allowCustomBaseUrl: options.allowCustomBaseUrl },
721
+ timeoutMs,
722
+ maxAttempts: 1,
723
+ ...options.fetch ? { fetch: options.fetch } : {}
724
+ });
725
+ await client.apiInformation();
726
+ return {
727
+ ok: true,
728
+ reachable: true,
729
+ kind: "ready",
730
+ message: "Qloo API is reachable and accepted the configured credential.",
731
+ durationMs: elapsed(startedAt, now),
732
+ retryable: false
733
+ };
734
+ } catch (error) {
735
+ if (error instanceof QlooClientError)
736
+ return clientFailure(error, elapsed(startedAt, now));
737
+ const failure = classifyQlooNetworkFailure(error);
738
+ return {
739
+ ok: false,
740
+ reachable: false,
741
+ kind: failure.kind === "aborted" ? "cancelled" : failure.kind === "network" ? "connection" : failure.kind,
742
+ message: failure.message,
743
+ durationMs: elapsed(startedAt, now),
744
+ retryable: failure.retryable,
745
+ ...failure.systemCode ? { systemCode: failure.systemCode } : {}
746
+ };
747
+ }
748
+ }
749
+ let url;
750
+ try {
751
+ url = normalizedProbeUrl(baseUrl);
752
+ } catch {
753
+ return configurationFailure("Qloo base URL is invalid.", elapsed(startedAt, now));
754
+ }
755
+ const controller = new AbortController();
756
+ let timedOut = false;
757
+ const timeout = setTimeout(() => {
758
+ timedOut = true;
759
+ controller.abort();
760
+ }, timeoutMs);
761
+ timeout.unref?.();
762
+ try {
763
+ const response = await (options.fetch ?? globalThis.fetch)(url, {
764
+ method: "GET",
765
+ headers: {
766
+ Accept: "application/json",
767
+ "User-Agent": "agentic-qloo-doctor/0.1.0"
768
+ },
769
+ redirect: "manual",
770
+ signal: controller.signal
771
+ });
772
+ await response.body?.cancel();
773
+ return {
774
+ ok: true,
775
+ reachable: true,
776
+ kind: "reachable",
777
+ message: `Qloo API endpoint is reachable (HTTP ${response.status}); authentication was not tested.`,
778
+ durationMs: elapsed(startedAt, now),
779
+ retryable: false,
780
+ status: response.status,
781
+ ...(() => {
782
+ const requestId = response.headers.get("x-request-id");
783
+ return requestId ? { requestId } : {};
784
+ })()
785
+ };
786
+ } catch (error) {
787
+ if (timedOut) {
788
+ return {
789
+ ok: false,
790
+ reachable: false,
791
+ kind: "timeout",
792
+ message: "Timed out while connecting to the Qloo API. Check VPN routing and firewall configuration.",
793
+ durationMs: elapsed(startedAt, now),
794
+ retryable: true
795
+ };
796
+ }
797
+ const failure = classifyQlooNetworkFailure(error);
798
+ return {
799
+ ok: false,
800
+ reachable: false,
801
+ kind: failure.kind === "aborted" ? "cancelled" : failure.kind === "network" ? "connection" : failure.kind,
802
+ message: failure.message,
803
+ durationMs: elapsed(startedAt, now),
804
+ retryable: failure.retryable,
805
+ ...failure.systemCode ? { systemCode: failure.systemCode } : {}
806
+ };
807
+ } finally {
808
+ clearTimeout(timeout);
809
+ }
810
+ }
811
+
812
+ // apps/qloo-harness/dist/setup.js
813
+ import { ensureQlooStateDirectories } from "./paths.js";
814
+ var QLOO_SETUP_SCHEMA_VERSION = "1.0";
815
+ var QLOO_NO_GUIDED_START_ENVIRONMENT_VARIABLE = "QLOO_NO_GUIDED_START";
816
+ var QLOO_SETUP_HELP = `Configure Qloo chat
817
+
818
+ Usage:
819
+ qloo setup Configure missing Qloo and model authentication
820
+ qloo setup --qloo Configure or replace the Qloo API credential
821
+ qloo setup --model Sign in to a model provider
822
+ qloo setup --status Show readiness without changing configuration
823
+ qloo setup --status --json Emit machine-readable readiness
824
+
825
+ Interactive secret values are hidden and are never printed in the setup report.`;
826
+ var SetupCancelledError = class extends Error {
827
+ constructor() {
828
+ super("Qloo setup was cancelled.");
829
+ this.name = "SetupCancelledError";
830
+ }
831
+ };
832
+ function cleanLine(value) {
833
+ return String(value ?? "").replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/\s+/gu, " ").trim();
834
+ }
835
+ async function lineQuestion(input, output, message) {
836
+ const reader = createInterface({ input, output, terminal: true });
837
+ try {
838
+ return (await reader.question(message)).trim() || void 0;
839
+ } catch {
840
+ return void 0;
841
+ } finally {
842
+ reader.close();
843
+ }
844
+ }
845
+ async function secretQuestion(input, output, message) {
846
+ if (!input.isTTY || typeof input.setRawMode !== "function")
847
+ return void 0;
848
+ output.write(`${message}: `);
849
+ const previousRawMode = input.isRaw;
850
+ const wasPaused = input.isPaused();
851
+ input.setRawMode(true);
852
+ input.resume();
853
+ return new Promise((resolve2) => {
854
+ let value = "";
855
+ const finish = (result) => {
856
+ input.off("data", onData);
857
+ input.setRawMode(previousRawMode);
858
+ if (wasPaused)
859
+ input.pause();
860
+ output.write("\n");
861
+ resolve2(result);
862
+ };
863
+ const onData = (chunk) => {
864
+ for (const character of String(chunk)) {
865
+ if (character === "" || character === "") {
866
+ finish(void 0);
867
+ return;
868
+ }
869
+ if (character === "\r" || character === "\n") {
870
+ finish(value.trim() || void 0);
871
+ return;
872
+ }
873
+ if (character === "\x7F" || character === "\b") {
874
+ if (value.length > 0) {
875
+ value = value.slice(0, -1);
876
+ output.write("\b \b");
877
+ }
878
+ continue;
879
+ }
880
+ if (character >= " ") {
881
+ value += character;
882
+ output.write("\u2022");
883
+ }
884
+ }
885
+ };
886
+ input.on("data", onData);
887
+ });
888
+ }
889
+ function createTerminalSetupIO(input = process.stdin, output = process.stdout) {
890
+ return {
891
+ interactive: input.isTTY === true && output.isTTY === true,
892
+ async select(title, choices) {
893
+ output.write(`
894
+ ${title}
895
+ `);
896
+ choices.forEach((choice, index) => {
897
+ output.write(` ${index + 1}. ${choice.label}${choice.description ? ` \u2014 ${choice.description}` : ""}
898
+ `);
899
+ });
900
+ while (true) {
901
+ const answer = await lineQuestion(input, output, `Choose 1\u2013${choices.length}: `);
902
+ if (answer === void 0)
903
+ return void 0;
904
+ const index = Number(answer) - 1;
905
+ if (Number.isSafeInteger(index) && index >= 0 && index < choices.length) {
906
+ return choices[index]?.id;
907
+ }
908
+ output.write("Please enter one of the listed numbers.\n");
909
+ }
910
+ },
911
+ text: (message, placeholder) => lineQuestion(input, output, `${message}${placeholder ? ` (${placeholder})` : ""}: `),
912
+ secret: (message) => secretQuestion(input, output, message),
913
+ write: (message) => output.write(message.endsWith("\n") ? message : `${message}
914
+ `)
915
+ };
916
+ }
917
+ async function defaultModelRuntime(paths) {
918
+ const { ModelRuntime } = await import("@earendil-works/pi-coding-agent");
919
+ return ModelRuntime.create({
920
+ authPath: paths.authFile,
921
+ modelsPath: paths.modelsFile,
922
+ modelsStorePath: paths.modelsStoreFile,
923
+ allowModelNetwork: false
924
+ });
925
+ }
926
+ async function inspectSetup(options, modelRuntime, changes = [], learningMode = false) {
927
+ const env = options.env ?? process.env;
928
+ const configuration = resolveQlooClientConfiguration({ env, configPath: options.paths.configFile });
929
+ const availableModels = await modelRuntime.getAvailable();
930
+ const qlooReady = configuration.apiKeySource !== "missing";
931
+ return {
932
+ schemaVersion: QLOO_SETUP_SCHEMA_VERSION,
933
+ readyForChat: availableModels.length > 0 && (qlooReady || learningMode || options.allowLearningMode !== false),
934
+ qloo: {
935
+ ready: qlooReady,
936
+ source: configuration.apiKeySource,
937
+ mode: qlooReady ? "live" : "learning"
938
+ },
939
+ model: {
940
+ ready: availableModels.length > 0,
941
+ availableModels: availableModels.length
942
+ },
943
+ changes: [...changes]
944
+ };
945
+ }
946
+ function providerPriority(provider) {
947
+ const priorities = ["anthropic", "openai-codex", "openai", "github-copilot", "google"];
948
+ const index = priorities.indexOf(provider.id);
949
+ return index < 0 ? priorities.length : index;
950
+ }
951
+ function loginProviders(runtime) {
952
+ return [...runtime.getProviders()].filter((provider) => provider.auth.apiKey?.login || provider.auth.oauth?.login).sort((left, right) => providerPriority(left) - providerPriority(right) || left.name.localeCompare(right.name));
953
+ }
954
+ function authEventText(event) {
955
+ if (event.type === "auth_url" && typeof event.url === "string") {
956
+ return [
957
+ typeof event.instructions === "string" ? event.instructions : "Open this URL to continue sign-in:",
958
+ event.url
959
+ ].join("\n");
960
+ }
961
+ if (event.type === "device_code") {
962
+ return [
963
+ typeof event.verificationUri === "string" ? `Open ${event.verificationUri}` : "Open the provider's device-login page.",
964
+ typeof event.userCode === "string" ? `Enter code: ${event.userCode}` : ""
965
+ ].filter(Boolean).join("\n");
966
+ }
967
+ return typeof event.message === "string" ? cleanLine(event.message) : void 0;
968
+ }
969
+ async function loginModelProvider(runtime, io) {
970
+ const providers = loginProviders(runtime);
971
+ if (providers.length === 0) {
972
+ io.write("No interactive model-provider login is available. Set a supported provider credential and rerun qloo setup.");
973
+ return false;
974
+ }
975
+ const selectedProviderId = await io.select("Choose a model provider", providers.map((provider2) => ({
976
+ id: provider2.id,
977
+ label: provider2.name,
978
+ description: provider2.id
979
+ })));
980
+ if (!selectedProviderId)
981
+ return false;
982
+ const provider = providers.find(({ id }) => id === selectedProviderId);
983
+ if (!provider)
984
+ return false;
985
+ const methods = [
986
+ ...provider.auth.oauth?.login ? [{ id: "oauth", label: "Subscription or account sign-in", description: "Use the provider's supported login flow" }] : [],
987
+ ...provider.auth.apiKey?.login ? [{ id: "api_key", label: provider.auth.apiKey.name ?? "API key", description: "Store the credential privately for Qloo" }] : []
988
+ ];
989
+ const selectedMethod = methods.length === 1 ? methods[0]?.id : await io.select(`Authenticate with ${provider.name}`, methods);
990
+ if (selectedMethod !== "api_key" && selectedMethod !== "oauth")
991
+ return false;
992
+ try {
993
+ await runtime.login(provider.id, selectedMethod, {
994
+ async prompt(prompt) {
995
+ if (prompt.signal?.aborted)
996
+ throw new SetupCancelledError();
997
+ const answer = prompt.type === "secret" ? await io.secret(prompt.message) : prompt.type === "select" ? await io.select(prompt.message, prompt.options ?? []) : await io.text(prompt.message, prompt.placeholder);
998
+ if (answer === void 0)
999
+ throw new SetupCancelledError();
1000
+ return answer;
1001
+ },
1002
+ notify(event) {
1003
+ const message = authEventText(event);
1004
+ if (message)
1005
+ io.write(message);
1006
+ }
1007
+ });
1008
+ const available = await runtime.getAvailable(provider.id);
1009
+ if (available.length === 0) {
1010
+ io.write(`${provider.name} accepted the login, but no usable model is currently available. Run qloo doctor for details.`);
1011
+ return false;
1012
+ }
1013
+ io.write(`${provider.name} is ready for Qloo chat.`);
1014
+ return true;
1015
+ } catch (error) {
1016
+ if (error instanceof SetupCancelledError)
1017
+ return false;
1018
+ io.write(`${provider.name} sign-in did not complete. No credential value was displayed; retry or run qloo doctor.`);
1019
+ return false;
1020
+ }
1021
+ }
1022
+ async function configureQlooCredential(options, io) {
1023
+ const env = options.env ?? process.env;
1024
+ const configuration = resolveQlooClientConfiguration({ env, configPath: options.paths.configFile });
1025
+ const choice = await io.select("Connect Qloo data", [
1026
+ { id: "configure", label: "Enter a Qloo API key", description: "Validate it without displaying it, then store it privately" },
1027
+ ...options.allowLearningMode === false ? [] : [{ id: "learning", label: "Continue in learning mode", description: "Explore capabilities and integration guidance without live results" }],
1028
+ ...options.allowLearningMode === false ? [{ id: "cancel", label: "Cancel setup" }] : []
1029
+ ]);
1030
+ if (choice === "learning")
1031
+ return "learning";
1032
+ if (choice !== "configure")
1033
+ return "cancelled";
1034
+ while (true) {
1035
+ const apiKey = await io.secret("Qloo API key (input is hidden)");
1036
+ if (!apiKey)
1037
+ return "cancelled";
1038
+ io.write("Checking Qloo connectivity and authentication\u2026");
1039
+ const probe = await (options.networkProbe ?? probeQlooConnectivity)({
1040
+ apiKey,
1041
+ baseUrl: configuration.baseUrl ?? QLOO_PRODUCTION_BASE_URL,
1042
+ allowCustomBaseUrl: configuration.baseUrlTrusted,
1043
+ timeoutMs: 5e3
1044
+ });
1045
+ if (probe.ok) {
1046
+ (options.saveApiKey ?? ((value, path) => saveQlooConfigValue("api_key", value, path)))(apiKey, options.paths.configFile);
1047
+ io.write("Qloo is connected. The API key was stored in the private Qloo config.");
1048
+ return "configured";
1049
+ }
1050
+ if (probe.kind === "authentication") {
1051
+ io.write("Qloo is reachable, but it rejected that credential. Check the key and try again.");
1052
+ continue;
1053
+ }
1054
+ io.write(probe.message);
1055
+ const recovery = await io.select("The key could not be validated from this network", [
1056
+ { id: "save", label: "Save it and diagnose later", description: "Useful when VPN or routing is temporarily unavailable" },
1057
+ { id: "retry", label: "Try another key" },
1058
+ { id: "cancel", label: "Cancel setup" }
1059
+ ]);
1060
+ if (recovery === "save") {
1061
+ (options.saveApiKey ?? ((value, path) => saveQlooConfigValue("api_key", value, path)))(apiKey, options.paths.configFile);
1062
+ io.write("The key was stored. Run qloo doctor --network after connecting the required network or VPN.");
1063
+ return "configured";
1064
+ }
1065
+ if (recovery !== "retry")
1066
+ return "cancelled";
1067
+ }
1068
+ }
1069
+ async function setupQloo(options) {
1070
+ const io = options.io ?? createTerminalSetupIO();
1071
+ await ensureQlooStateDirectories(options.paths);
1072
+ const modelRuntime = options.modelRuntime ?? await (options.createModelRuntime ?? defaultModelRuntime)(options.paths);
1073
+ let report = await inspectSetup(options, modelRuntime);
1074
+ const changes = [];
1075
+ let learningMode = !report.qloo.ready && options.allowLearningMode !== false;
1076
+ if ((options.configureQloo ?? !report.qloo.ready) && io.interactive) {
1077
+ const result = await configureQlooCredential(options, io);
1078
+ if (result === "configured") {
1079
+ changes.push("qloo-auth");
1080
+ learningMode = false;
1081
+ } else if (result === "learning") {
1082
+ learningMode = true;
1083
+ }
1084
+ }
1085
+ report = await inspectSetup(options, modelRuntime, changes, learningMode);
1086
+ if ((options.configureModel ?? !report.model.ready) && io.interactive) {
1087
+ if (await loginModelProvider(modelRuntime, io))
1088
+ changes.push("model-auth");
1089
+ }
1090
+ return inspectSetup(options, modelRuntime, changes, learningMode);
1091
+ }
1092
+ function formatQlooSetupReport(report) {
1093
+ return [
1094
+ "Qloo setup",
1095
+ ` Qloo data: ${report.qloo.ready ? `ready (${report.qloo.source})` : "learning mode; no API credential"}`,
1096
+ ` Model: ${report.model.ready ? `ready (${report.model.availableModels} available)` : "not configured"}`,
1097
+ ` Chat: ${report.readyForChat ? "ready" : "requires model-provider sign-in"}`,
1098
+ report.changes.length > 0 ? ` Updated: ${report.changes.join(", ")}` : " Updated: nothing"
1099
+ ].join("\n") + "\n";
1100
+ }
1101
+ async function runQlooSetup(argv, options) {
1102
+ if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) {
1103
+ (options.writeOut ?? ((text) => process.stdout.write(text)))(`${QLOO_SETUP_HELP}
1104
+ `);
1105
+ return 0;
1106
+ }
1107
+ const supported = /* @__PURE__ */ new Set(["--status", "--json", "--qloo", "--model"]);
1108
+ const unknown = argv.filter((argument) => !supported.has(argument));
1109
+ if (unknown.length > 0) {
1110
+ (options.writeError ?? ((text) => process.stderr.write(text)))("qloo setup: supported options are --status, --json, --qloo, and --model\n");
1111
+ return 2;
1112
+ }
1113
+ const statusOnly = argv.includes("--status");
1114
+ const io = options.io ?? createTerminalSetupIO();
1115
+ if (!statusOnly && !io.interactive) {
1116
+ (options.writeError ?? ((text) => process.stderr.write(text)))("qloo setup: interactive setup requires a TTY. Use qloo setup --status --json for automation.\n");
1117
+ return 2;
1118
+ }
1119
+ const report = await setupQloo({
1120
+ ...options,
1121
+ io: statusOnly ? { ...io, interactive: false } : io,
1122
+ ...argv.includes("--qloo") ? { configureQloo: true } : {},
1123
+ ...argv.includes("--model") ? { configureModel: true } : {}
1124
+ });
1125
+ const output = argv.includes("--json") ? `${JSON.stringify(report)}
1126
+ ` : formatQlooSetupReport(report);
1127
+ (options.writeOut ?? ((text) => process.stdout.write(text)))(output);
1128
+ return report.readyForChat ? 0 : 1;
1129
+ }
1130
+ export {
1131
+ QLOO_NO_GUIDED_START_ENVIRONMENT_VARIABLE,
1132
+ QLOO_SETUP_HELP,
1133
+ QLOO_SETUP_SCHEMA_VERSION,
1134
+ createTerminalSetupIO,
1135
+ formatQlooSetupReport,
1136
+ runQlooSetup,
1137
+ setupQloo
1138
+ };