@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/LICENSE +21 -0
- package/README.md +394 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/app.js +48 -0
- package/dist/bin.js +14 -0
- package/dist/build.js +117 -0
- package/dist/cli.js +95 -0
- package/dist/doctor.js +1019 -0
- package/dist/exec.js +782 -0
- package/dist/guided-journey.js +223 -0
- package/dist/index.d.ts +1321 -0
- package/dist/index.js +156 -0
- package/dist/integration-plan.js +1097 -0
- package/dist/mcp.js +909 -0
- package/dist/observability.js +115 -0
- package/dist/paths.js +77 -0
- package/dist/plan.js +163 -0
- package/dist/profiles.js +63 -0
- package/dist/project-context.js +364 -0
- package/dist/qloo-presentation.js +358 -0
- package/dist/qloo-tools.js +2195 -0
- package/dist/resolution-provider.js +1380 -0
- package/dist/router.js +6061 -0
- package/dist/runtime/explore-policy.js +666 -0
- package/dist/runtime/pi-adapter.js +259 -0
- package/dist/runtime/pi-command-policy.js +74 -0
- package/dist/runtime/qloo-header.js +173 -0
- package/dist/runtime/resources.js +37 -0
- package/dist/setup.js +1138 -0
- package/dist/update-manager.js +906 -0
- package/dist/workflow-executor.js +976 -0
- package/package.json +72 -0
- package/resources/BUILD.md +24 -0
- package/resources/EXPLORE.md +34 -0
- package/resources/INTEGRATE.md +28 -0
- package/resources/PLAN.md +23 -0
- package/resources/SYSTEM.md +49 -0
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
|
+
};
|