@loreai/gateway 0.13.4 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +49694 -3155
- package/package.json +14 -6
- package/src/batch-queue.ts +21 -1
- package/src/cache-analytics.ts +344 -0
- package/src/cli/agents.ts +107 -0
- package/src/cli/bin.ts +11 -0
- package/src/cli/help.ts +55 -0
- package/src/cli/lib/binary.ts +353 -0
- package/src/cli/lib/bspatch.ts +306 -0
- package/src/cli/lib/delta-upgrade.ts +790 -0
- package/src/cli/lib/errors.ts +48 -0
- package/src/cli/lib/ghcr.ts +389 -0
- package/src/cli/lib/patch-cache.ts +342 -0
- package/src/cli/lib/upgrade.ts +454 -0
- package/src/cli/lib/version-check.ts +385 -0
- package/src/cli/main.ts +152 -0
- package/src/cli/run.ts +181 -0
- package/src/cli/start.ts +82 -0
- package/src/cli/upgrade.ts +311 -0
- package/src/cli/version.ts +22 -0
- package/src/idle.ts +0 -6
- package/src/index.ts +27 -27
- package/src/llm-adapter.ts +100 -28
- package/src/pipeline.ts +254 -177
- package/src/recall.ts +223 -91
- package/src/temporal-adapter.ts +3 -0
- package/src/translate/anthropic.ts +50 -6
- package/src/translate/types.ts +54 -9
- package/dist/index.js.map +0 -7
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upgrade Error Types
|
|
3
|
+
*
|
|
4
|
+
* Slim error hierarchy for the upgrade system. No Sentry SDK dependency,
|
|
5
|
+
* no complex exit code mapping — just typed reasons for programmatic handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type UpgradeErrorReason =
|
|
9
|
+
| "network_error"
|
|
10
|
+
| "execution_failed"
|
|
11
|
+
| "version_not_found"
|
|
12
|
+
| "offline_cache_miss";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Upgrade-related errors with typed reasons for programmatic handling.
|
|
16
|
+
*/
|
|
17
|
+
export class UpgradeError extends Error {
|
|
18
|
+
readonly reason: UpgradeErrorReason;
|
|
19
|
+
|
|
20
|
+
constructor(reason: UpgradeErrorReason, message?: string) {
|
|
21
|
+
const defaultMessages: Record<UpgradeErrorReason, string> = {
|
|
22
|
+
network_error: "Failed to fetch version information.",
|
|
23
|
+
execution_failed: "Upgrade command failed.",
|
|
24
|
+
version_not_found: "The specified version was not found.",
|
|
25
|
+
offline_cache_miss:
|
|
26
|
+
"Cannot upgrade offline — no pre-downloaded update is available.",
|
|
27
|
+
};
|
|
28
|
+
super(message ?? defaultMessages[reason]);
|
|
29
|
+
this.name = "UpgradeError";
|
|
30
|
+
this.reason = reason;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convert an unknown value to a human-readable string.
|
|
36
|
+
*/
|
|
37
|
+
export function stringifyUnknown(value: unknown): string {
|
|
38
|
+
if (typeof value === "string") return value;
|
|
39
|
+
if (value instanceof Error) return value.message;
|
|
40
|
+
if (value && typeof value === "object") {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.stringify(value);
|
|
43
|
+
} catch {
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return String(value);
|
|
48
|
+
}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHCR (GitHub Container Registry) Client
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates the OCI download protocol for fetching nightly CLI binaries
|
|
5
|
+
* from ghcr.io/BYK/loreai. Nightly builds are pushed as OCI artifacts
|
|
6
|
+
* via ORAS with the version baked into the manifest annotation.
|
|
7
|
+
*
|
|
8
|
+
* Key design decisions:
|
|
9
|
+
* - Anonymous access: nightly package is public; no token needed beyond the
|
|
10
|
+
* standard ghcr.io anonymous token exchange.
|
|
11
|
+
* - Version discovery from manifest annotation: `annotations.version` in the
|
|
12
|
+
* OCI manifest holds the nightly version.
|
|
13
|
+
* - Redirect quirk: ghcr.io blob downloads return 307 to Azure Blob Storage.
|
|
14
|
+
* Using `fetch` with `redirect: "follow"` would forward the Authorization
|
|
15
|
+
* header to Azure, which returns 404. Must follow the redirect manually
|
|
16
|
+
* without the auth header.
|
|
17
|
+
*
|
|
18
|
+
* Adapted from Sentry CLI's ghcr.ts for Lore.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { getUserAgent } from "./binary";
|
|
22
|
+
import { UpgradeError } from "./errors";
|
|
23
|
+
|
|
24
|
+
/** Default timeout for GHCR HTTP requests (10 seconds) */
|
|
25
|
+
const GHCR_REQUEST_TIMEOUT = 10_000;
|
|
26
|
+
|
|
27
|
+
/** Maximum number of retry attempts for transient failures */
|
|
28
|
+
const GHCR_MAX_RETRIES = 1;
|
|
29
|
+
|
|
30
|
+
/** Timeout for large blob downloads (30 seconds) */
|
|
31
|
+
const GHCR_BLOB_TIMEOUT = 30_000;
|
|
32
|
+
|
|
33
|
+
function isRetryableError(error: Error): boolean {
|
|
34
|
+
if (error.name === "TimeoutError" || error.name === "AbortError") {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
const msg = error.message.toLowerCase();
|
|
38
|
+
return (
|
|
39
|
+
msg.includes("timeout") ||
|
|
40
|
+
msg.includes("econnreset") ||
|
|
41
|
+
msg.includes("econnrefused") ||
|
|
42
|
+
msg.includes("network") ||
|
|
43
|
+
msg.includes("fetch failed")
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildSignal(
|
|
48
|
+
timeout: number,
|
|
49
|
+
externalSignal?: AbortSignal,
|
|
50
|
+
): AbortSignal {
|
|
51
|
+
const timeoutSignal = AbortSignal.timeout(timeout);
|
|
52
|
+
return externalSignal
|
|
53
|
+
? AbortSignal.any([timeoutSignal, externalSignal])
|
|
54
|
+
: timeoutSignal;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isExternalAbort(error: Error, externalSignal?: AbortSignal): boolean {
|
|
58
|
+
return Boolean(externalSignal?.aborted && error.name === "AbortError");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type RetryOptions = {
|
|
62
|
+
timeout?: number;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
async function fetchWithRetry(
|
|
67
|
+
url: string,
|
|
68
|
+
init: RequestInit,
|
|
69
|
+
context: string,
|
|
70
|
+
options?: RetryOptions,
|
|
71
|
+
): Promise<Response> {
|
|
72
|
+
const timeout = options?.timeout ?? GHCR_REQUEST_TIMEOUT;
|
|
73
|
+
const externalSignal = options?.signal;
|
|
74
|
+
let lastError: Error | undefined;
|
|
75
|
+
|
|
76
|
+
for (let attempt = 0; attempt <= GHCR_MAX_RETRIES; attempt++) {
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(url, {
|
|
79
|
+
...init,
|
|
80
|
+
signal: buildSignal(timeout, externalSignal),
|
|
81
|
+
});
|
|
82
|
+
return response;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
85
|
+
if (isExternalAbort(lastError, externalSignal)) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (attempt >= GHCR_MAX_RETRIES || !isRetryableError(lastError)) {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
throw new UpgradeError(
|
|
95
|
+
"network_error",
|
|
96
|
+
`${context}: ${lastError?.message ?? "unknown error"}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** GHCR repository for Lore distribution */
|
|
101
|
+
export const GHCR_REPO = "BYK/loreai";
|
|
102
|
+
|
|
103
|
+
/** OCI tag for nightly builds */
|
|
104
|
+
export const GHCR_TAG = "nightly";
|
|
105
|
+
|
|
106
|
+
/** Base URL for GHCR registry API */
|
|
107
|
+
const GHCR_REGISTRY = "https://ghcr.io";
|
|
108
|
+
|
|
109
|
+
/** OCI manifest media type */
|
|
110
|
+
const OCI_MANIFEST_TYPE = "application/vnd.oci.image.manifest.v1+json";
|
|
111
|
+
|
|
112
|
+
/** A single layer entry from an OCI manifest. */
|
|
113
|
+
export type OciLayer = {
|
|
114
|
+
digest: string;
|
|
115
|
+
mediaType: string;
|
|
116
|
+
size: number;
|
|
117
|
+
annotations?: Record<string, string>;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** OCI image manifest returned by the registry. */
|
|
121
|
+
export type OciManifest = {
|
|
122
|
+
schemaVersion: number;
|
|
123
|
+
mediaType?: string;
|
|
124
|
+
config?: OciLayer;
|
|
125
|
+
layers: OciLayer[];
|
|
126
|
+
annotations?: Record<string, string>;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Fetch a short-lived anonymous bearer token for read-only access to the
|
|
131
|
+
* public ghcr.io/BYK/loreai package.
|
|
132
|
+
*/
|
|
133
|
+
export async function getAnonymousToken(
|
|
134
|
+
signal?: AbortSignal,
|
|
135
|
+
): Promise<string> {
|
|
136
|
+
const url = `${GHCR_REGISTRY}/token?scope=repository:${GHCR_REPO}:pull`;
|
|
137
|
+
const response = await fetchWithRetry(
|
|
138
|
+
url,
|
|
139
|
+
{ headers: { "User-Agent": getUserAgent() } },
|
|
140
|
+
"Failed to connect to GHCR",
|
|
141
|
+
{ signal },
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
throw new UpgradeError(
|
|
146
|
+
"network_error",
|
|
147
|
+
`GHCR token exchange failed: HTTP ${response.status}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const data = (await response.json()) as { token?: string };
|
|
152
|
+
if (!data.token) {
|
|
153
|
+
throw new UpgradeError(
|
|
154
|
+
"network_error",
|
|
155
|
+
"GHCR token exchange returned no token",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return data.token;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Fetch the OCI manifest for an arbitrary tag from GHCR.
|
|
164
|
+
*/
|
|
165
|
+
export async function fetchManifest(
|
|
166
|
+
token: string,
|
|
167
|
+
tag: string,
|
|
168
|
+
signal?: AbortSignal,
|
|
169
|
+
): Promise<OciManifest> {
|
|
170
|
+
const url = `${GHCR_REGISTRY}/v2/${GHCR_REPO}/manifests/${tag}`;
|
|
171
|
+
const response = await fetchWithRetry(
|
|
172
|
+
url,
|
|
173
|
+
{
|
|
174
|
+
headers: {
|
|
175
|
+
Authorization: `Bearer ${token}`,
|
|
176
|
+
Accept: OCI_MANIFEST_TYPE,
|
|
177
|
+
"User-Agent": getUserAgent(),
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
`Failed to fetch manifest for tag "${tag}"`,
|
|
181
|
+
{ signal },
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
throw new UpgradeError(
|
|
186
|
+
"network_error",
|
|
187
|
+
`Failed to fetch manifest for tag "${tag}": HTTP ${response.status}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return (await response.json()) as OciManifest;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Fetch the OCI manifest for the `:nightly` tag.
|
|
196
|
+
*/
|
|
197
|
+
export async function fetchNightlyManifest(
|
|
198
|
+
token: string,
|
|
199
|
+
): Promise<OciManifest> {
|
|
200
|
+
return await fetchManifest(token, GHCR_TAG);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Extract the nightly version string from a manifest's annotations.
|
|
205
|
+
*/
|
|
206
|
+
export function getNightlyVersion(manifest: OciManifest): string {
|
|
207
|
+
const version = manifest.annotations?.version;
|
|
208
|
+
if (!version) {
|
|
209
|
+
throw new UpgradeError(
|
|
210
|
+
"network_error",
|
|
211
|
+
"Nightly manifest has no version annotation",
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return version;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Find the layer matching a given filename in an OCI manifest.
|
|
219
|
+
*/
|
|
220
|
+
export function findLayerByFilename(
|
|
221
|
+
manifest: OciManifest,
|
|
222
|
+
filename: string,
|
|
223
|
+
): OciLayer {
|
|
224
|
+
const layer = manifest.layers.find(
|
|
225
|
+
(l) => l.annotations?.["org.opencontainers.image.title"] === filename,
|
|
226
|
+
);
|
|
227
|
+
if (!layer) {
|
|
228
|
+
throw new UpgradeError(
|
|
229
|
+
"version_not_found",
|
|
230
|
+
`No nightly build found for ${filename}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return layer;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Download a nightly binary blob from GHCR.
|
|
238
|
+
*
|
|
239
|
+
* The blob endpoint returns a 307 redirect to Azure Blob Storage.
|
|
240
|
+
* Must follow the redirect manually without the Authorization header.
|
|
241
|
+
*/
|
|
242
|
+
export async function downloadNightlyBlob(
|
|
243
|
+
token: string,
|
|
244
|
+
digest: string,
|
|
245
|
+
signal?: AbortSignal,
|
|
246
|
+
): Promise<Response> {
|
|
247
|
+
const blobUrl = `${GHCR_REGISTRY}/v2/${GHCR_REPO}/blobs/${digest}`;
|
|
248
|
+
|
|
249
|
+
let blobResponse: Response;
|
|
250
|
+
try {
|
|
251
|
+
blobResponse = await fetch(blobUrl, {
|
|
252
|
+
headers: {
|
|
253
|
+
Authorization: `Bearer ${token}`,
|
|
254
|
+
"User-Agent": getUserAgent(),
|
|
255
|
+
},
|
|
256
|
+
redirect: "manual",
|
|
257
|
+
signal: buildSignal(GHCR_BLOB_TIMEOUT, signal),
|
|
258
|
+
});
|
|
259
|
+
} catch (error) {
|
|
260
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
261
|
+
throw new UpgradeError(
|
|
262
|
+
"network_error",
|
|
263
|
+
`Failed to connect to GHCR: ${msg}`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (blobResponse.status === 200) {
|
|
268
|
+
return blobResponse;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (
|
|
272
|
+
blobResponse.status === 301 ||
|
|
273
|
+
blobResponse.status === 302 ||
|
|
274
|
+
blobResponse.status === 307 ||
|
|
275
|
+
blobResponse.status === 308
|
|
276
|
+
) {
|
|
277
|
+
const redirectUrl = blobResponse.headers.get("location");
|
|
278
|
+
if (!redirectUrl) {
|
|
279
|
+
throw new UpgradeError(
|
|
280
|
+
"network_error",
|
|
281
|
+
`GHCR blob redirect (${blobResponse.status}) had no Location header`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let redirectResponse: Response;
|
|
286
|
+
try {
|
|
287
|
+
redirectResponse = await fetch(redirectUrl, {
|
|
288
|
+
headers: { "User-Agent": getUserAgent() },
|
|
289
|
+
signal,
|
|
290
|
+
});
|
|
291
|
+
} catch (error) {
|
|
292
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
293
|
+
throw new UpgradeError(
|
|
294
|
+
"network_error",
|
|
295
|
+
`Failed to download from blob storage: ${msg}`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!redirectResponse.ok) {
|
|
300
|
+
throw new UpgradeError(
|
|
301
|
+
"network_error",
|
|
302
|
+
`Blob storage download failed: HTTP ${redirectResponse.status}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return redirectResponse;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
throw new UpgradeError(
|
|
310
|
+
"network_error",
|
|
311
|
+
`Unexpected GHCR blob response: HTTP ${blobResponse.status}`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Page size for tag listing pagination */
|
|
316
|
+
const TAGS_PAGE_SIZE = 100;
|
|
317
|
+
|
|
318
|
+
async function fetchTagPage(
|
|
319
|
+
token: string,
|
|
320
|
+
lastTag?: string,
|
|
321
|
+
signal?: AbortSignal,
|
|
322
|
+
): Promise<string[]> {
|
|
323
|
+
let url = `${GHCR_REGISTRY}/v2/${GHCR_REPO}/tags/list?n=${TAGS_PAGE_SIZE}`;
|
|
324
|
+
if (lastTag) {
|
|
325
|
+
url += `&last=${encodeURIComponent(lastTag)}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const response = await fetchWithRetry(
|
|
329
|
+
url,
|
|
330
|
+
{
|
|
331
|
+
headers: {
|
|
332
|
+
Authorization: `Bearer ${token}`,
|
|
333
|
+
"User-Agent": getUserAgent(),
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
"Failed to list GHCR tags",
|
|
337
|
+
{ signal },
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
if (!response.ok) {
|
|
341
|
+
throw new UpgradeError(
|
|
342
|
+
"network_error",
|
|
343
|
+
`Failed to list GHCR tags: HTTP ${response.status}`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const data = (await response.json()) as { tags?: string[] };
|
|
348
|
+
return data.tags ?? [];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* List tags in the GHCR repository, optionally filtered by prefix.
|
|
353
|
+
*/
|
|
354
|
+
export async function listTags(
|
|
355
|
+
token: string,
|
|
356
|
+
prefix?: string,
|
|
357
|
+
signal?: AbortSignal,
|
|
358
|
+
): Promise<string[]> {
|
|
359
|
+
const allTags: string[] = [];
|
|
360
|
+
let lastTag: string | undefined;
|
|
361
|
+
|
|
362
|
+
for (;;) {
|
|
363
|
+
const tags = await fetchTagPage(token, lastTag, signal);
|
|
364
|
+
if (tags.length === 0) break;
|
|
365
|
+
|
|
366
|
+
for (const tag of tags) {
|
|
367
|
+
if (!prefix || tag.startsWith(prefix)) {
|
|
368
|
+
allTags.push(tag);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (tags.length < TAGS_PAGE_SIZE) break;
|
|
373
|
+
lastTag = tags.at(-1);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return allTags;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Download an OCI layer blob as an ArrayBuffer.
|
|
381
|
+
*/
|
|
382
|
+
export async function downloadLayerBlob(
|
|
383
|
+
token: string,
|
|
384
|
+
digest: string,
|
|
385
|
+
signal?: AbortSignal,
|
|
386
|
+
): Promise<ArrayBuffer> {
|
|
387
|
+
const response = await downloadNightlyBlob(token, digest, signal);
|
|
388
|
+
return response.arrayBuffer();
|
|
389
|
+
}
|