@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/doctor.js ADDED
@@ -0,0 +1,1019 @@
1
+ import { createRequire as __qlooCreateRequire } from "node:module";
2
+ const require = __qlooCreateRequire(import.meta.url);
3
+
4
+ // apps/qloo-harness/dist/doctor.js
5
+ import { access, stat } from "node:fs/promises";
6
+ import { constants, readFileSync as readFileSync2 } from "node:fs";
7
+
8
+ // packages/qloo-client-ts/dist/config.js
9
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { dirname, isAbsolute, join, parse, resolve } from "node:path";
12
+ var QLOO_PRODUCTION_BASE_URL = "https://api.qloo.com";
13
+ function nonEmptyString(value) {
14
+ if (typeof value !== "string")
15
+ return void 0;
16
+ const trimmed = value.trim();
17
+ return trimmed || void 0;
18
+ }
19
+ function selectFileConfig(value) {
20
+ if (value === null || typeof value !== "object" || Array.isArray(value))
21
+ return {};
22
+ const record = value;
23
+ const apiKey = nonEmptyString(record.api_key);
24
+ const baseUrl = nonEmptyString(record.base_url);
25
+ const trustedBaseUrl = nonEmptyString(record.trusted_base_url);
26
+ const tasteResolverUrl = nonEmptyString(record.taste_resolver_url);
27
+ return {
28
+ ...apiKey ? { api_key: apiKey } : {},
29
+ ...baseUrl ? { base_url: baseUrl } : {},
30
+ ...trustedBaseUrl ? { trusted_base_url: trustedBaseUrl } : {},
31
+ ...tasteResolverUrl ? { taste_resolver_url: tasteResolverUrl } : {}
32
+ };
33
+ }
34
+ function comparableBaseUrl(value) {
35
+ try {
36
+ const url = new URL(value);
37
+ if (url.username || url.password || url.search || url.hash)
38
+ return void 0;
39
+ url.pathname = url.pathname.replace(/\/+$/u, "") || "/";
40
+ return url.toString();
41
+ } catch {
42
+ return void 0;
43
+ }
44
+ }
45
+ function areEquivalentQlooBaseUrls(left, right) {
46
+ const normalizedLeft = comparableBaseUrl(left);
47
+ return normalizedLeft !== void 0 && normalizedLeft === comparableBaseUrl(right);
48
+ }
49
+ function isOfficialQlooBaseUrl(value) {
50
+ return areEquivalentQlooBaseUrls(value, QLOO_PRODUCTION_BASE_URL);
51
+ }
52
+ function parseQlooConfig(content) {
53
+ const trimmed = content.trim();
54
+ if (!trimmed)
55
+ return {};
56
+ if (trimmed.startsWith("{"))
57
+ return selectFileConfig(JSON.parse(trimmed));
58
+ const values = {};
59
+ for (const line of content.split(/\r?\n/u)) {
60
+ const candidate = line.trim();
61
+ if (!candidate || candidate.startsWith("#"))
62
+ continue;
63
+ const separator = candidate.indexOf("=");
64
+ if (separator < 0)
65
+ continue;
66
+ const key = candidate.slice(0, separator).trim();
67
+ let value = candidate.slice(separator + 1).trim();
68
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
69
+ value = value.slice(1, -1);
70
+ }
71
+ values[key] = value;
72
+ }
73
+ return selectFileConfig(values);
74
+ }
75
+ function loadQlooConfig(configPath, readFile) {
76
+ try {
77
+ const content = readFile ? readFile(configPath) : readFileSync(configPath, "utf8");
78
+ return parseQlooConfig(content);
79
+ } catch {
80
+ return {};
81
+ }
82
+ }
83
+ function resolveQlooConfigPath(options = {}) {
84
+ const env = options.env ?? process.env;
85
+ const homeDirectory = resolve(options.homeDirectory ?? homedir());
86
+ const configuredRoot = nonEmptyString(env.QLOO_HOME);
87
+ if (!configuredRoot)
88
+ return join(homeDirectory, ".qloo", "config");
89
+ const root = configuredRoot === "~" ? homeDirectory : configuredRoot.startsWith("~/") || configuredRoot.startsWith("~\\") ? join(homeDirectory, configuredRoot.slice(2)) : isAbsolute(configuredRoot) ? resolve(configuredRoot) : resolve(configuredRoot);
90
+ if (root === homeDirectory || root === parse(root).root) {
91
+ throw new Error("QLOO_HOME must name a dedicated subdirectory, not a home or filesystem root.");
92
+ }
93
+ return join(root, "config");
94
+ }
95
+ function resolveQlooClientConfiguration(options = {}) {
96
+ const env = options.env ?? process.env;
97
+ const configPath = options.configPath ?? resolveQlooConfigPath({ env });
98
+ const fileConfig = loadQlooConfig(configPath, options.readFile);
99
+ const environmentApiKey = nonEmptyString(env.QLOO_API_KEY);
100
+ const environmentBaseUrl = nonEmptyString(env.QLOO_BASE_URL);
101
+ const environmentTrustedBaseUrl = nonEmptyString(env.QLOO_TRUSTED_BASE_URL);
102
+ const apiKey = environmentApiKey ?? fileConfig.api_key;
103
+ const baseUrl = environmentBaseUrl ?? fileConfig.base_url;
104
+ const effectiveBaseUrl = baseUrl ?? QLOO_PRODUCTION_BASE_URL;
105
+ const baseUrlTrustSource = isOfficialQlooBaseUrl(effectiveBaseUrl) ? "official" : environmentTrustedBaseUrl && areEquivalentQlooBaseUrls(effectiveBaseUrl, environmentTrustedBaseUrl) ? "environment" : fileConfig.trusted_base_url && areEquivalentQlooBaseUrls(effectiveBaseUrl, fileConfig.trusted_base_url) ? "config" : "missing";
106
+ return {
107
+ ...apiKey ? { apiKey } : {},
108
+ apiKeySource: environmentApiKey ? "environment" : fileConfig.api_key ? "config" : "missing",
109
+ ...baseUrl ? { baseUrl } : {},
110
+ baseUrlSource: environmentBaseUrl ? "environment" : fileConfig.base_url ? "config" : "default",
111
+ baseUrlTrusted: baseUrlTrustSource !== "missing",
112
+ baseUrlTrustSource,
113
+ fileConfig
114
+ };
115
+ }
116
+
117
+ // packages/qloo-client-ts/dist/errors.js
118
+ var QlooClientError = class extends Error {
119
+ code;
120
+ retryable;
121
+ status;
122
+ requestId;
123
+ details;
124
+ constructor(code, message, options = {}) {
125
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
126
+ this.name = "QlooClientError";
127
+ this.code = code;
128
+ this.retryable = options.retryable ?? false;
129
+ if (options.status !== void 0)
130
+ this.status = options.status;
131
+ if (options.requestId !== void 0)
132
+ this.requestId = options.requestId;
133
+ if (options.details !== void 0)
134
+ this.details = options.details;
135
+ }
136
+ toJSON() {
137
+ return {
138
+ name: this.name,
139
+ code: this.code,
140
+ message: this.message,
141
+ retryable: this.retryable,
142
+ ...this.status === void 0 ? {} : { status: this.status },
143
+ ...this.requestId === void 0 ? {} : { requestId: this.requestId },
144
+ ...this.details === void 0 ? {} : { details: this.details }
145
+ };
146
+ }
147
+ };
148
+ var DNS_CODES = /* @__PURE__ */ new Set([
149
+ "EAI_AGAIN",
150
+ "EAI_FAIL",
151
+ "ENODATA",
152
+ "ENOTFOUND",
153
+ "ENOTIMP",
154
+ "ENOTINITIALIZED",
155
+ "ESERVFAIL"
156
+ ]);
157
+ var CONNECTION_CODES = /* @__PURE__ */ new Set([
158
+ "ECONNABORTED",
159
+ "ECONNREFUSED",
160
+ "ECONNRESET",
161
+ "EHOSTDOWN",
162
+ "EHOSTUNREACH",
163
+ "ENETDOWN",
164
+ "ENETUNREACH",
165
+ "EPIPE",
166
+ "ETIMEDOUT",
167
+ "UND_ERR_CONNECT_TIMEOUT",
168
+ "UND_ERR_SOCKET"
169
+ ]);
170
+ var TLS_CODES = /* @__PURE__ */ new Set([
171
+ "CERT_HAS_EXPIRED",
172
+ "CERT_NOT_YET_VALID",
173
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
174
+ "ERR_TLS_CERT_ALTNAME_INVALID",
175
+ "SELF_SIGNED_CERT_IN_CHAIN",
176
+ "UNABLE_TO_GET_ISSUER_CERT",
177
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
178
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE"
179
+ ]);
180
+ var PROXY_CODES = /* @__PURE__ */ new Set([
181
+ "ERR_PROXY_AUTH_UNSUPPORTED",
182
+ "UND_ERR_PRX"
183
+ ]);
184
+ function isRecord(value) {
185
+ return typeof value === "object" && value !== null;
186
+ }
187
+ function safeErrorCode(value) {
188
+ if (!isRecord(value))
189
+ return void 0;
190
+ const code = value.code;
191
+ return typeof code === "string" && /^[A-Z][A-Z0-9_]{1,63}$/u.test(code) ? code : void 0;
192
+ }
193
+ function errorChain(value) {
194
+ const chain = [];
195
+ const seen = /* @__PURE__ */ new Set();
196
+ let current = value;
197
+ for (let depth = 0; depth < 8 && current !== void 0 && !seen.has(current); depth += 1) {
198
+ chain.push(current);
199
+ seen.add(current);
200
+ if (!isRecord(current))
201
+ break;
202
+ if (Array.isArray(current.errors) && current.errors.length > 0) {
203
+ chain.push(...current.errors.slice(0, 4));
204
+ }
205
+ current = current.cause;
206
+ }
207
+ return chain;
208
+ }
209
+ function classifyQlooNetworkFailure(cause) {
210
+ const chain = errorChain(cause);
211
+ const systemCode = chain.map(safeErrorCode).find((value) => value !== void 0);
212
+ const names = chain.map((value) => isRecord(value) && typeof value.name === "string" ? value.name : void 0).filter((value) => value !== void 0);
213
+ if (names.includes("AbortError") || systemCode === "ABORT_ERR") {
214
+ return {
215
+ code: "QLOO_ABORTED",
216
+ kind: "aborted",
217
+ message: "Qloo API request was cancelled.",
218
+ retryable: false,
219
+ ...systemCode ? { systemCode } : {}
220
+ };
221
+ }
222
+ if (systemCode && DNS_CODES.has(systemCode)) {
223
+ return {
224
+ code: "QLOO_DNS_ERROR",
225
+ kind: "dns",
226
+ message: "Could not resolve the Qloo API hostname. Check DNS and VPN configuration.",
227
+ retryable: true,
228
+ systemCode
229
+ };
230
+ }
231
+ if (systemCode && (TLS_CODES.has(systemCode) || systemCode.startsWith("ERR_TLS_") || systemCode.startsWith("ERR_SSL_"))) {
232
+ return {
233
+ code: "QLOO_TLS_ERROR",
234
+ kind: "tls",
235
+ message: "Could not establish a trusted TLS connection to the Qloo API.",
236
+ retryable: false,
237
+ systemCode
238
+ };
239
+ }
240
+ if (systemCode && PROXY_CODES.has(systemCode)) {
241
+ return {
242
+ code: "QLOO_PROXY_ERROR",
243
+ kind: "proxy",
244
+ message: "The configured network proxy could not reach the Qloo API.",
245
+ retryable: true,
246
+ systemCode
247
+ };
248
+ }
249
+ if (systemCode && CONNECTION_CODES.has(systemCode)) {
250
+ return {
251
+ code: "QLOO_CONNECTION_ERROR",
252
+ kind: "connection",
253
+ message: "Could not open a connection to the Qloo API. Check VPN, routing, and firewall configuration.",
254
+ retryable: true,
255
+ systemCode
256
+ };
257
+ }
258
+ return {
259
+ code: "QLOO_NETWORK_ERROR",
260
+ kind: "network",
261
+ message: "Unable to reach the Qloo API because of an unclassified network failure.",
262
+ retryable: true,
263
+ ...systemCode ? { systemCode } : {}
264
+ };
265
+ }
266
+
267
+ // packages/qloo-client-ts/dist/client.js
268
+ var DEFAULT_TIMEOUT_MS = 2e4;
269
+ var DEFAULT_MAX_ATTEMPTS = 3;
270
+ var DEFAULT_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
271
+ var DEFAULT_MAX_RETRY_DELAY_MS = 3e4;
272
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
273
+ function validatePositiveInteger(name, value) {
274
+ if (!Number.isSafeInteger(value) || value < 1) {
275
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", `${name} must be a positive integer.`);
276
+ }
277
+ }
278
+ function normalizeBaseUrl(value) {
279
+ let url;
280
+ try {
281
+ url = new URL(value);
282
+ } catch (cause) {
283
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", "Qloo base URL is invalid.", {
284
+ cause
285
+ });
286
+ }
287
+ const isLoopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
288
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) {
289
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", "Qloo base URL must use HTTPS (HTTP is allowed only for a loopback test server).");
290
+ }
291
+ if (url.username || url.password || url.search || url.hash) {
292
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", "Qloo base URL cannot contain credentials, query parameters, or a fragment.");
293
+ }
294
+ url.pathname = url.pathname.replace(/\/+$/, "") + "/";
295
+ return url;
296
+ }
297
+ function appendQuery(url, query) {
298
+ for (const [key, value] of Object.entries(query)) {
299
+ if (value === void 0 || value === null || value === "")
300
+ continue;
301
+ const serialized = Array.isArray(value) ? value.join(",") : String(value);
302
+ if (serialized !== "")
303
+ url.searchParams.set(key, serialized);
304
+ }
305
+ }
306
+ function requestIdFrom(response) {
307
+ return response.headers.get("x-qloo-request-id") ?? response.headers.get("x-request-id") ?? response.headers.get("request-id") ?? void 0;
308
+ }
309
+ function responseErrorMetadata(response) {
310
+ const requestId = requestIdFrom(response);
311
+ return {
312
+ status: response.status,
313
+ ...requestId ? { requestId } : {}
314
+ };
315
+ }
316
+ function retryAfterMilliseconds(response, now = Date.now()) {
317
+ const value = response.headers.get("retry-after");
318
+ if (!value)
319
+ return void 0;
320
+ const seconds = Number(value);
321
+ if (Number.isFinite(seconds) && seconds >= 0)
322
+ return seconds * 1e3;
323
+ const date = Date.parse(value);
324
+ if (!Number.isNaN(date))
325
+ return Math.max(0, date - now);
326
+ return void 0;
327
+ }
328
+ function responseMessage(body, response) {
329
+ if (body && typeof body === "object") {
330
+ for (const key of ["message", "reason", "error"]) {
331
+ const value = Reflect.get(body, key);
332
+ if (typeof value === "string" && value.trim())
333
+ return value;
334
+ }
335
+ }
336
+ return `Qloo API request failed with ${response.status} ${response.statusText || "HTTP error"}.`;
337
+ }
338
+ function createAttemptSignal(parent, timeoutMs) {
339
+ const controller = new AbortController();
340
+ let timedOut = false;
341
+ const timeout = setTimeout(() => {
342
+ timedOut = true;
343
+ controller.abort(new Error("Qloo request timed out."));
344
+ }, timeoutMs);
345
+ timeout.unref?.();
346
+ const onParentAbort = () => controller.abort(parent?.reason);
347
+ if (parent?.aborted)
348
+ onParentAbort();
349
+ else
350
+ parent?.addEventListener("abort", onParentAbort, { once: true });
351
+ return {
352
+ signal: controller.signal,
353
+ didTimeout: () => timedOut,
354
+ dispose: () => {
355
+ clearTimeout(timeout);
356
+ parent?.removeEventListener("abort", onParentAbort);
357
+ }
358
+ };
359
+ }
360
+ async function defaultSleep(milliseconds, signal) {
361
+ if (signal?.aborted)
362
+ throw signal.reason;
363
+ await new Promise((resolve2, reject) => {
364
+ const cleanup = () => signal?.removeEventListener("abort", onAbort);
365
+ const timeout = setTimeout(() => {
366
+ cleanup();
367
+ resolve2();
368
+ }, milliseconds);
369
+ const onAbort = () => {
370
+ clearTimeout(timeout);
371
+ cleanup();
372
+ reject(signal?.reason);
373
+ };
374
+ signal?.addEventListener("abort", onAbort, { once: true });
375
+ });
376
+ }
377
+ async function readBoundedBody(response, maxBytes) {
378
+ const contentLength = response.headers.get("content-length");
379
+ if (contentLength !== null && Number(contentLength) > maxBytes) {
380
+ await response.body?.cancel();
381
+ throw new QlooClientError("QLOO_RESPONSE_TOO_LARGE", `Qloo API response exceeded the ${maxBytes}-byte limit.`, responseErrorMetadata(response));
382
+ }
383
+ if (!response.body)
384
+ return "";
385
+ const reader = response.body.getReader();
386
+ const chunks = [];
387
+ let total = 0;
388
+ while (true) {
389
+ const { done, value } = await reader.read();
390
+ if (done)
391
+ break;
392
+ if (!value)
393
+ continue;
394
+ total += value.byteLength;
395
+ if (total > maxBytes) {
396
+ await reader.cancel();
397
+ throw new QlooClientError("QLOO_RESPONSE_TOO_LARGE", `Qloo API response exceeded the ${maxBytes}-byte limit.`, responseErrorMetadata(response));
398
+ }
399
+ chunks.push(value);
400
+ }
401
+ const body = new Uint8Array(total);
402
+ let offset = 0;
403
+ for (const chunk of chunks) {
404
+ body.set(chunk, offset);
405
+ offset += chunk.byteLength;
406
+ }
407
+ return new TextDecoder().decode(body);
408
+ }
409
+ function parseJsonBody(text, response) {
410
+ if (!text.trim())
411
+ return {};
412
+ try {
413
+ return JSON.parse(text);
414
+ } catch (cause) {
415
+ throw new QlooClientError("QLOO_RESPONSE_ERROR", "Qloo API returned invalid JSON.", {
416
+ cause,
417
+ retryable: response.status >= 500,
418
+ ...responseErrorMetadata(response)
419
+ });
420
+ }
421
+ }
422
+ var QlooClient = class {
423
+ baseUrl;
424
+ #apiKey;
425
+ #baseUrl;
426
+ #timeoutMs;
427
+ #maxAttempts;
428
+ #maxResponseBytes;
429
+ #maxRetryDelayMs;
430
+ #userAgent;
431
+ #fetch;
432
+ #sleep;
433
+ #random;
434
+ constructor(options) {
435
+ if (!options.apiKey?.trim()) {
436
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", "Qloo API key is required.");
437
+ }
438
+ this.#apiKey = options.apiKey;
439
+ this.#baseUrl = normalizeBaseUrl(options.baseUrl ?? QLOO_PRODUCTION_BASE_URL);
440
+ if (!isOfficialQlooBaseUrl(this.#baseUrl.toString()) && !options.allowCustomBaseUrl) {
441
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", "Custom Qloo base URL is not trusted. Approve the exact endpoint before sending credentials.");
442
+ }
443
+ this.baseUrl = this.#baseUrl.toString().replace(/\/$/, "");
444
+ this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
445
+ this.#maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
446
+ this.#maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
447
+ this.#maxRetryDelayMs = options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
448
+ this.#userAgent = options.userAgent ?? "agentic-qloo-client/0.1.0";
449
+ this.#fetch = options.fetch ?? globalThis.fetch;
450
+ this.#sleep = options.sleep ?? defaultSleep;
451
+ this.#random = options.random ?? Math.random;
452
+ validatePositiveInteger("timeoutMs", this.#timeoutMs);
453
+ validatePositiveInteger("maxAttempts", this.#maxAttempts);
454
+ validatePositiveInteger("maxResponseBytes", this.#maxResponseBytes);
455
+ validatePositiveInteger("maxRetryDelayMs", this.#maxRetryDelayMs);
456
+ }
457
+ async get(path, query = {}, options = {}) {
458
+ if (path.includes("\\") || path.startsWith("//") || /^[A-Za-z][A-Za-z\d+.-]*:/u.test(path) || /[?#\u0000-\u001f\u007f]/u.test(path)) {
459
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", "Qloo request path must be a relative API path without a query or fragment.");
460
+ }
461
+ const relativePath = path.replace(/^\/+/, "");
462
+ const url = new URL(relativePath, this.#baseUrl);
463
+ if (url.origin !== this.#baseUrl.origin || !url.pathname.startsWith(this.#baseUrl.pathname)) {
464
+ throw new QlooClientError("QLOO_CONFIGURATION_ERROR", "Qloo request path cannot escape the trusted API base URL.");
465
+ }
466
+ appendQuery(url, query);
467
+ for (let attempt = 1; attempt <= this.#maxAttempts; attempt += 1) {
468
+ if (options.signal?.aborted)
469
+ throw options.signal.reason;
470
+ const attemptSignal = createAttemptSignal(options.signal, this.#timeoutMs);
471
+ let response;
472
+ let body;
473
+ try {
474
+ response = await this.#fetch(url, {
475
+ method: "GET",
476
+ headers: {
477
+ Accept: "application/json",
478
+ "X-Api-Key": this.#apiKey,
479
+ "User-Agent": this.#userAgent,
480
+ ...options.correlationId ? { "X-Correlation-Id": options.correlationId } : {}
481
+ },
482
+ signal: attemptSignal.signal
483
+ });
484
+ const text = await readBoundedBody(response, this.#maxResponseBytes);
485
+ body = parseJsonBody(text, response);
486
+ } catch (cause) {
487
+ const timedOut = attemptSignal.didTimeout();
488
+ attemptSignal.dispose();
489
+ if (options.signal?.aborted)
490
+ throw options.signal.reason;
491
+ if (cause instanceof QlooClientError) {
492
+ if (cause.retryable && attempt < this.#maxAttempts) {
493
+ await this.#sleep(this.#retryDelay(attempt), options.signal);
494
+ continue;
495
+ }
496
+ throw cause;
497
+ }
498
+ if (timedOut) {
499
+ if (attempt < this.#maxAttempts) {
500
+ await this.#sleep(this.#retryDelay(attempt), options.signal);
501
+ continue;
502
+ }
503
+ throw new QlooClientError("QLOO_TIMEOUT", "Qloo API request timed out.", {
504
+ cause,
505
+ retryable: true
506
+ });
507
+ }
508
+ if (attempt < this.#maxAttempts) {
509
+ await this.#sleep(this.#retryDelay(attempt), options.signal);
510
+ continue;
511
+ }
512
+ const failure = classifyQlooNetworkFailure(cause);
513
+ throw new QlooClientError(failure.code, failure.message, {
514
+ cause,
515
+ retryable: failure.retryable,
516
+ details: {
517
+ kind: failure.kind,
518
+ attempts: this.#maxAttempts,
519
+ ...failure.systemCode ? { systemCode: failure.systemCode } : {}
520
+ }
521
+ });
522
+ } finally {
523
+ attemptSignal.dispose();
524
+ }
525
+ if (response.ok)
526
+ return body;
527
+ const retryable = RETRYABLE_STATUS_CODES.has(response.status);
528
+ if (retryable && attempt < this.#maxAttempts) {
529
+ const retryAfter = retryAfterMilliseconds(response);
530
+ await this.#sleep(Math.min(retryAfter ?? this.#retryDelay(attempt), this.#maxRetryDelayMs), options.signal);
531
+ continue;
532
+ }
533
+ throw new QlooClientError("QLOO_HTTP_ERROR", responseMessage(body, response), {
534
+ retryable,
535
+ details: body,
536
+ ...responseErrorMetadata(response)
537
+ });
538
+ }
539
+ throw new QlooClientError("QLOO_NETWORK_ERROR", "Unable to reach the Qloo API.", {
540
+ retryable: true
541
+ });
542
+ }
543
+ apiInformation(options) {
544
+ return this.get("/", {}, options);
545
+ }
546
+ insights(query, options) {
547
+ return this.get("/v2/insights", query, options);
548
+ }
549
+ searchEntities(query, options) {
550
+ return this.get("/search", query, options);
551
+ }
552
+ entities(entityIds, options) {
553
+ return this.get("/entities", { entity_ids: entityIds }, options);
554
+ }
555
+ audiences(query = {}, options) {
556
+ return this.get("/v2/audiences", query, options);
557
+ }
558
+ audienceTypes(query = {}, options) {
559
+ return this.get("/v2/audiences/types", query, options);
560
+ }
561
+ tags(query = {}, options) {
562
+ return this.get("/v2/tags", query, options);
563
+ }
564
+ tagTypes(query = {}, options) {
565
+ return this.get("/v2/tags/types", query, options);
566
+ }
567
+ compare(query, options) {
568
+ return this.get("/v2/analysis/compare", query, options);
569
+ }
570
+ trending(query, options) {
571
+ return this.get("/v2/trending", query, options);
572
+ }
573
+ #retryDelay(attempt) {
574
+ const exponential = 250 * 2 ** (attempt - 1);
575
+ const jittered = exponential * (0.75 + this.#random() * 0.5);
576
+ return Math.min(Math.round(jittered), this.#maxRetryDelayMs);
577
+ }
578
+ };
579
+
580
+ // packages/qloo-client-ts/dist/diagnostics.js
581
+ var DEFAULT_PROBE_TIMEOUT_MS = 5e3;
582
+ function elapsed(startedAt, now) {
583
+ return Math.max(0, Math.round(now() - startedAt));
584
+ }
585
+ function configurationFailure(message, durationMs) {
586
+ return {
587
+ ok: false,
588
+ reachable: false,
589
+ kind: "configuration",
590
+ message,
591
+ durationMs,
592
+ retryable: false
593
+ };
594
+ }
595
+ function normalizedProbeUrl(value) {
596
+ const url = new URL(value);
597
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
598
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
599
+ throw new Error("Qloo base URL must use HTTPS.");
600
+ }
601
+ if (url.username || url.password || url.search || url.hash) {
602
+ throw new Error("Qloo base URL contains unsupported URL components.");
603
+ }
604
+ url.pathname = url.pathname.replace(/\/+$/u, "") || "/";
605
+ return url;
606
+ }
607
+ function httpFailure(status, durationMs, requestId) {
608
+ const metadata = {
609
+ durationMs,
610
+ reachable: true,
611
+ status,
612
+ ...requestId ? { requestId } : {}
613
+ };
614
+ if (status === 401 || status === 403) {
615
+ return {
616
+ ...metadata,
617
+ ok: false,
618
+ kind: "authentication",
619
+ message: `Qloo API is reachable but rejected the configured credential (HTTP ${status}).`,
620
+ retryable: false
621
+ };
622
+ }
623
+ if (status === 429) {
624
+ return {
625
+ ...metadata,
626
+ ok: false,
627
+ kind: "rate-limit",
628
+ message: "Qloo API is reachable but the credential is currently rate limited.",
629
+ retryable: true
630
+ };
631
+ }
632
+ if (status >= 500) {
633
+ return {
634
+ ...metadata,
635
+ ok: false,
636
+ kind: "service",
637
+ message: `Qloo API is reachable but returned a service error (HTTP ${status}).`,
638
+ retryable: true
639
+ };
640
+ }
641
+ return {
642
+ ...metadata,
643
+ ok: false,
644
+ kind: "http",
645
+ message: `Qloo API is reachable but the readiness request failed (HTTP ${status}).`,
646
+ retryable: false
647
+ };
648
+ }
649
+ function clientFailure(error, durationMs) {
650
+ if (error.code === "QLOO_HTTP_ERROR" && error.status !== void 0) {
651
+ return httpFailure(error.status, durationMs, error.requestId);
652
+ }
653
+ if (error.code === "QLOO_TIMEOUT") {
654
+ return {
655
+ ok: false,
656
+ reachable: false,
657
+ kind: "timeout",
658
+ message: "Timed out while connecting to the Qloo API. Check VPN routing and firewall configuration.",
659
+ durationMs,
660
+ retryable: true
661
+ };
662
+ }
663
+ if (error.code === "QLOO_CONFIGURATION_ERROR") {
664
+ return configurationFailure(error.message, durationMs);
665
+ }
666
+ const details = error.details && typeof error.details === "object" ? error.details : {};
667
+ const systemCode = typeof details.systemCode === "string" ? details.systemCode : void 0;
668
+ 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";
669
+ return {
670
+ ok: false,
671
+ reachable: false,
672
+ kind,
673
+ message: error.message,
674
+ durationMs,
675
+ retryable: error.retryable,
676
+ ...systemCode ? { systemCode } : {}
677
+ };
678
+ }
679
+ async function probeQlooConnectivity(options = {}) {
680
+ const now = options.now ?? Date.now;
681
+ const startedAt = now();
682
+ const baseUrl = options.baseUrl ?? QLOO_PRODUCTION_BASE_URL;
683
+ const timeoutMs = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
684
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
685
+ return configurationFailure("Qloo connectivity timeout must be a positive integer.", elapsed(startedAt, now));
686
+ }
687
+ if (!isOfficialQlooBaseUrl(baseUrl) && !options.allowCustomBaseUrl) {
688
+ return configurationFailure("Custom Qloo endpoint is not trusted; connectivity probe was not sent.", elapsed(startedAt, now));
689
+ }
690
+ if (options.apiKey?.trim()) {
691
+ try {
692
+ const client = new QlooClient({
693
+ apiKey: options.apiKey,
694
+ baseUrl,
695
+ ...options.allowCustomBaseUrl === void 0 ? {} : { allowCustomBaseUrl: options.allowCustomBaseUrl },
696
+ timeoutMs,
697
+ maxAttempts: 1,
698
+ ...options.fetch ? { fetch: options.fetch } : {}
699
+ });
700
+ await client.apiInformation();
701
+ return {
702
+ ok: true,
703
+ reachable: true,
704
+ kind: "ready",
705
+ message: "Qloo API is reachable and accepted the configured credential.",
706
+ durationMs: elapsed(startedAt, now),
707
+ retryable: false
708
+ };
709
+ } catch (error) {
710
+ if (error instanceof QlooClientError)
711
+ return clientFailure(error, elapsed(startedAt, now));
712
+ const failure = classifyQlooNetworkFailure(error);
713
+ return {
714
+ ok: false,
715
+ reachable: false,
716
+ kind: failure.kind === "aborted" ? "cancelled" : failure.kind === "network" ? "connection" : failure.kind,
717
+ message: failure.message,
718
+ durationMs: elapsed(startedAt, now),
719
+ retryable: failure.retryable,
720
+ ...failure.systemCode ? { systemCode: failure.systemCode } : {}
721
+ };
722
+ }
723
+ }
724
+ let url;
725
+ try {
726
+ url = normalizedProbeUrl(baseUrl);
727
+ } catch {
728
+ return configurationFailure("Qloo base URL is invalid.", elapsed(startedAt, now));
729
+ }
730
+ const controller = new AbortController();
731
+ let timedOut = false;
732
+ const timeout = setTimeout(() => {
733
+ timedOut = true;
734
+ controller.abort();
735
+ }, timeoutMs);
736
+ timeout.unref?.();
737
+ try {
738
+ const response = await (options.fetch ?? globalThis.fetch)(url, {
739
+ method: "GET",
740
+ headers: {
741
+ Accept: "application/json",
742
+ "User-Agent": "agentic-qloo-doctor/0.1.0"
743
+ },
744
+ redirect: "manual",
745
+ signal: controller.signal
746
+ });
747
+ await response.body?.cancel();
748
+ return {
749
+ ok: true,
750
+ reachable: true,
751
+ kind: "reachable",
752
+ message: `Qloo API endpoint is reachable (HTTP ${response.status}); authentication was not tested.`,
753
+ durationMs: elapsed(startedAt, now),
754
+ retryable: false,
755
+ status: response.status,
756
+ ...(() => {
757
+ const requestId = response.headers.get("x-request-id");
758
+ return requestId ? { requestId } : {};
759
+ })()
760
+ };
761
+ } catch (error) {
762
+ if (timedOut) {
763
+ return {
764
+ ok: false,
765
+ reachable: false,
766
+ kind: "timeout",
767
+ message: "Timed out while connecting to the Qloo API. Check VPN routing and firewall configuration.",
768
+ durationMs: elapsed(startedAt, now),
769
+ retryable: true
770
+ };
771
+ }
772
+ const failure = classifyQlooNetworkFailure(error);
773
+ return {
774
+ ok: false,
775
+ reachable: false,
776
+ kind: failure.kind === "aborted" ? "cancelled" : failure.kind === "network" ? "connection" : failure.kind,
777
+ message: failure.message,
778
+ durationMs: elapsed(startedAt, now),
779
+ retryable: failure.retryable,
780
+ ...failure.systemCode ? { systemCode: failure.systemCode } : {}
781
+ };
782
+ } finally {
783
+ clearTimeout(timeout);
784
+ }
785
+ }
786
+
787
+ // apps/qloo-harness/dist/doctor.js
788
+ import { resolveQlooPaths } from "./paths.js";
789
+ import { inspectQlooResolutionProviderConfiguration, probeTasteResolverHealth } from "./resolution-provider.js";
790
+ var PROVIDER_ENVIRONMENT_VARIABLES = [
791
+ "ANTHROPIC_API_KEY",
792
+ "OPENAI_API_KEY",
793
+ "GEMINI_API_KEY",
794
+ "GOOGLE_GENERATIVE_AI_API_KEY",
795
+ "MISTRAL_API_KEY"
796
+ ];
797
+ function nonEmptyString2(value) {
798
+ return typeof value === "string" && value.trim().length > 0;
799
+ }
800
+ function isRecord2(value) {
801
+ return typeof value === "object" && value !== null && !Array.isArray(value);
802
+ }
803
+ function safeEndpointLabel(value) {
804
+ try {
805
+ const endpoint = new URL(value);
806
+ if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
807
+ return "the configured custom endpoint";
808
+ }
809
+ return endpoint.toString();
810
+ } catch {
811
+ return "the configured custom endpoint";
812
+ }
813
+ }
814
+ function isUsablePiCredential(value) {
815
+ if (!isRecord2(value))
816
+ return false;
817
+ if (value.type === "api_key") {
818
+ if (nonEmptyString2(value.key))
819
+ return true;
820
+ if (!isRecord2(value.env))
821
+ return false;
822
+ const environmentValues = Object.values(value.env);
823
+ return environmentValues.length > 0 && environmentValues.every(nonEmptyString2);
824
+ }
825
+ return value.type === "oauth" && nonEmptyString2(value.access) && nonEmptyString2(value.refresh) && typeof value.expires === "number" && Number.isFinite(value.expires);
826
+ }
827
+ function inspectPiAuthFile(content) {
828
+ let parsed;
829
+ try {
830
+ parsed = JSON.parse(content);
831
+ } catch {
832
+ return { status: "invalid" };
833
+ }
834
+ if (!isRecord2(parsed))
835
+ return { status: "invalid" };
836
+ const entries = Object.entries(parsed);
837
+ if (entries.length === 0)
838
+ return { status: "empty" };
839
+ const credentialCount = entries.filter(([provider, credential]) => provider.trim().length > 0 && isUsablePiCredential(credential)).length;
840
+ return credentialCount > 0 ? { status: "usable", credentialCount } : { status: "invalid" };
841
+ }
842
+ async function defaultFileExists(path) {
843
+ try {
844
+ await access(path, constants.R_OK);
845
+ return true;
846
+ } catch {
847
+ return false;
848
+ }
849
+ }
850
+ async function defaultDirectoryMode(path) {
851
+ try {
852
+ return (await stat(path)).mode & 511;
853
+ } catch {
854
+ return void 0;
855
+ }
856
+ }
857
+ async function runDoctor(options) {
858
+ const env = options.env ?? process.env;
859
+ const paths = options.paths ?? resolveQlooPaths({ env });
860
+ const fileExists = options.fileExists ?? defaultFileExists;
861
+ const directoryMode = options.directoryMode ?? defaultDirectoryMode;
862
+ const fileMode = options.fileMode ?? defaultDirectoryMode;
863
+ const readAuthFile = options.readAuthFile ?? ((path) => readFileSync2(path, "utf8"));
864
+ const checks = [];
865
+ const qlooConfiguration = resolveQlooClientConfiguration({
866
+ env,
867
+ configPath: paths.configFile,
868
+ ...options.readConfigFile ? { readFile: options.readConfigFile } : {}
869
+ });
870
+ checks.push(qlooConfiguration.apiKeySource !== "missing" ? {
871
+ id: "qloo-auth",
872
+ status: "ok",
873
+ message: qlooConfiguration.apiKeySource === "environment" ? "Qloo API credential is present in the environment." : `Qloo API credential is configured in ${paths.configFile}.`
874
+ } : {
875
+ id: "qloo-auth",
876
+ status: "warning",
877
+ message: "Qloo API credential is missing. Run `qloo setup` for guided configuration, or set QLOO_API_KEY to use live Qloo tools."
878
+ });
879
+ const effectiveBaseUrl = qlooConfiguration.baseUrl ?? QLOO_PRODUCTION_BASE_URL;
880
+ const endpointLabel = safeEndpointLabel(effectiveBaseUrl);
881
+ checks.push(qlooConfiguration.baseUrlTrusted ? {
882
+ id: "qloo-endpoint-trust",
883
+ status: "ok",
884
+ message: qlooConfiguration.baseUrlTrustSource === "official" ? `Qloo API endpoint is official: ${endpointLabel}` : `Qloo API endpoint is explicitly trusted via ${qlooConfiguration.baseUrlTrustSource === "environment" ? "QLOO_TRUSTED_BASE_URL" : paths.configFile}: ${endpointLabel}`
885
+ } : {
886
+ id: "qloo-endpoint-trust",
887
+ status: "warning",
888
+ message: `Custom Qloo API endpoint ${endpointLabel} is not trusted. Run \`qloo config set base-url <url>\` or set QLOO_TRUSTED_BASE_URL to the exact endpoint.`
889
+ });
890
+ const resolutionConfiguration = inspectQlooResolutionProviderConfiguration({
891
+ env,
892
+ fileConfig: qlooConfiguration.fileConfig
893
+ });
894
+ checks.push({
895
+ id: "qloo-resolution",
896
+ status: resolutionConfiguration.valid ? "ok" : "warning",
897
+ message: resolutionConfiguration.valid ? resolutionConfiguration.message : `${resolutionConfiguration.message} ${resolutionConfiguration.recovery ?? "Correct the resolver configuration."}`
898
+ });
899
+ if (options.network) {
900
+ const networkProbe = options.networkProbe ?? probeQlooConnectivity;
901
+ const probe = await networkProbe({
902
+ ...qlooConfiguration.apiKey ? { apiKey: qlooConfiguration.apiKey } : {},
903
+ baseUrl: effectiveBaseUrl,
904
+ allowCustomBaseUrl: qlooConfiguration.baseUrlTrusted,
905
+ timeoutMs: options.networkTimeoutMs ?? 5e3
906
+ });
907
+ checks.push({
908
+ id: "qloo-network",
909
+ status: probe.ok ? "ok" : "warning",
910
+ message: probe.message,
911
+ category: probe.kind,
912
+ durationMs: probe.durationMs,
913
+ retryable: probe.retryable,
914
+ ...probe.status === void 0 ? {} : { statusCode: probe.status },
915
+ ...probe.requestId === void 0 ? {} : { requestId: probe.requestId }
916
+ });
917
+ if (resolutionConfiguration.valid && resolutionConfiguration.providerId === "taste_resolver_assisted" && resolutionConfiguration.endpoint) {
918
+ const resolverProbe = await (options.resolverNetworkProbe ?? probeTasteResolverHealth)({
919
+ baseUrl: resolutionConfiguration.endpoint,
920
+ trustedBaseUrl: resolutionConfiguration.endpoint,
921
+ timeoutMs: options.networkTimeoutMs ?? 5e3
922
+ });
923
+ checks.push({
924
+ id: "qloo-resolution-network",
925
+ status: resolverProbe.ok ? "ok" : "warning",
926
+ message: resolverProbe.message,
927
+ category: resolverProbe.kind,
928
+ durationMs: resolverProbe.durationMs,
929
+ retryable: resolverProbe.retryable,
930
+ ...resolverProbe.status === void 0 ? {} : { statusCode: resolverProbe.status }
931
+ });
932
+ }
933
+ }
934
+ if (qlooConfiguration.apiKeySource === "config") {
935
+ const mode2 = await fileMode(paths.configFile);
936
+ const exposed = mode2 === void 0 || (mode2 & 63) !== 0;
937
+ checks.push(exposed ? {
938
+ id: "qloo-config-permissions",
939
+ status: "warning",
940
+ message: mode2 === void 0 ? `Could not verify permissions for ${paths.configFile}; use 600 for a plaintext API key.` : `Qloo config permissions are ${mode2.toString(8)}; use 600 for a plaintext API key.`
941
+ } : {
942
+ id: "qloo-config-permissions",
943
+ status: "ok",
944
+ message: "Qloo config is private to the current user."
945
+ });
946
+ }
947
+ const providerEnvironment = PROVIDER_ENVIRONMENT_VARIABLES.find((name) => env[name]?.trim());
948
+ const authFileExists = await fileExists(paths.authFile);
949
+ let authFileInspection;
950
+ if (!providerEnvironment && authFileExists) {
951
+ try {
952
+ authFileInspection = inspectPiAuthFile(readAuthFile(paths.authFile));
953
+ } catch {
954
+ authFileInspection = { status: "invalid" };
955
+ }
956
+ }
957
+ checks.push(providerEnvironment ? {
958
+ id: "model-auth",
959
+ status: "ok",
960
+ message: `Model-provider credential is present via ${providerEnvironment}.`
961
+ } : authFileInspection?.status === "usable" ? {
962
+ id: "model-auth",
963
+ status: "ok",
964
+ message: `${paths.authFile} contains ${authFileInspection.credentialCount} structurally usable Pi provider credential${authFileInspection.credentialCount === 1 ? "" : "s"}.`
965
+ } : {
966
+ id: "model-auth",
967
+ status: "warning",
968
+ message: authFileInspection?.status === "invalid" ? `${paths.authFile} does not contain a usable Pi provider credential. Run \`qloo setup --model\` to sign in, then use \`qloo doctor\` to verify it.` : authFileInspection?.status === "empty" ? `${paths.authFile} is empty. Run \`qloo setup --model\` to sign in, then use \`qloo doctor\` to verify it.` : "No model-provider credential was detected. Run `qloo setup --model` to sign in, then use `qloo doctor` to verify it."
969
+ });
970
+ const mode = await directoryMode(paths.agentDir);
971
+ if (mode !== void 0) {
972
+ const exposed = mode & 63;
973
+ checks.push(exposed === 0 ? { id: "state-permissions", status: "ok", message: "Qloo agent state is private to the current user." } : {
974
+ id: "state-permissions",
975
+ status: "warning",
976
+ message: `Qloo agent state permissions are ${mode.toString(8)}; use 700 to keep sessions private.`
977
+ });
978
+ } else {
979
+ checks.push({
980
+ id: "state-permissions",
981
+ status: "ok",
982
+ message: "Qloo agent state has not been created yet; first run will create it with private permissions."
983
+ });
984
+ }
985
+ return {
986
+ schemaVersion: "1.0",
987
+ status: checks.some((check) => check.status === "warning") ? "warning" : "ok",
988
+ versions: {
989
+ harness: options.harnessVersion,
990
+ node: process.versions.node,
991
+ pi: options.piVersion
992
+ },
993
+ paths: {
994
+ qlooHome: paths.qlooDir,
995
+ agentDirectory: paths.agentDir
996
+ },
997
+ checks
998
+ };
999
+ }
1000
+ function formatDoctorReport(report) {
1001
+ const lines = [
1002
+ `Qloo doctor: ${report.status}`,
1003
+ `Harness ${report.versions.harness} | Pi ${report.versions.pi} | Node ${report.versions.node}`,
1004
+ `State: ${report.paths.agentDirectory}`,
1005
+ "",
1006
+ ...report.checks.map((check) => {
1007
+ const label = check.category ? `${check.id}/${check.category}` : check.id;
1008
+ const duration = check.durationMs === void 0 ? "" : ` (${check.durationMs} ms)`;
1009
+ return `${check.status === "ok" ? "ok" : "warn"} ${label} \u2014 ${check.message}${duration}`;
1010
+ })
1011
+ ];
1012
+ return `${lines.join("\n")}
1013
+ `;
1014
+ }
1015
+ export {
1016
+ formatDoctorReport,
1017
+ inspectPiAuthFile,
1018
+ runDoctor
1019
+ };