sift-light 1.1.0-1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -176,6 +176,42 @@ function isRawSiftLightConfig(value) {
176
176
  function isRawSemanticJudgeConfig(value) {
177
177
  return typeof value === "object" && value !== null && !Array.isArray(value);
178
178
  }
179
+ function isRawMediaConfig(value) {
180
+ return typeof value === "object" && value !== null && !Array.isArray(value);
181
+ }
182
+ function parseEndpoint(value, path, field) {
183
+ if (typeof value !== "string" || value.length === 0 || value.length > 2048) {
184
+ throw new Error(`Invalid sift-light config at ${path}: ${field} must be a nonempty URL`);
185
+ }
186
+ let parsedEndpoint;
187
+ try {
188
+ parsedEndpoint = new URL(value);
189
+ } catch (error) {
190
+ throw new Error(`Invalid sift-light config at ${path}: ${field} must be a URL`, {
191
+ cause: error
192
+ });
193
+ }
194
+ const loopbackHosts = new Set(["localhost", "127.0.0.1", "[::1]"]);
195
+ const secureEndpoint = parsedEndpoint.protocol === "https:" || parsedEndpoint.protocol === "http:" && loopbackHosts.has(parsedEndpoint.hostname);
196
+ if (!secureEndpoint || parsedEndpoint.username || parsedEndpoint.password) {
197
+ throw new Error(`Invalid sift-light config at ${path}: ${field} must use HTTPS, except that HTTP is allowed for localhost loopback development; URL credentials are not allowed`);
198
+ }
199
+ return value;
200
+ }
201
+ function parseApiKeyEnv(value, path, field) {
202
+ if (value === undefined)
203
+ return;
204
+ if (typeof value !== "string" || !/^[A-Z][A-Z0-9_]{0,127}$/u.test(value)) {
205
+ throw new Error(`Invalid sift-light config at ${path}: ${field} must be an uppercase environment variable name`);
206
+ }
207
+ return value;
208
+ }
209
+ function parseBoundedModel(value, path, field) {
210
+ if (typeof value !== "string" || value.length === 0 || value.length > 128 || /[\r\n\0]/u.test(value)) {
211
+ throw new Error(`Invalid sift-light config at ${path}: ${field} must be bounded single-line text`);
212
+ }
213
+ return value;
214
+ }
179
215
  function boundedInteger(value, fallback, minimum, maximum, field) {
180
216
  const candidate = value ?? fallback;
181
217
  if (typeof candidate !== "number" || !Number.isSafeInteger(candidate) || candidate < minimum || candidate > maximum) {
@@ -207,45 +243,61 @@ function parseSemanticJudge(value, path) {
207
243
  throw new Error(`Invalid sift-light config at ${path}: semanticJudge.enabled must be a boolean`);
208
244
  }
209
245
  const provider = value.provider ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.provider;
210
- if (provider !== "jev") {
211
- throw new Error(`Invalid sift-light config at ${path}: semanticJudge.provider must be "jev"`);
212
- }
213
- const endpoint = value.endpoint ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.endpoint;
214
- if (typeof endpoint !== "string" || endpoint.length === 0 || endpoint.length > 2048) {
215
- throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a nonempty URL`);
216
- }
217
- let parsedEndpoint;
218
- try {
219
- parsedEndpoint = new URL(endpoint);
220
- } catch (error) {
221
- throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a URL`, {
222
- cause: error
223
- });
224
- }
225
- const loopbackHosts = new Set(["localhost", "127.0.0.1", "[::1]"]);
226
- const secureEndpoint = parsedEndpoint.protocol === "https:" || parsedEndpoint.protocol === "http:" && loopbackHosts.has(parsedEndpoint.hostname);
227
- if (!secureEndpoint || parsedEndpoint.username || parsedEndpoint.password) {
228
- throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must use HTTPS, except that HTTP is allowed for localhost loopback development; URL credentials are not allowed`);
229
- }
230
- const apiKeyEnv = value.apiKeyEnv ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.apiKeyEnv;
231
- if (typeof apiKeyEnv !== "string" || !/^[A-Z][A-Z0-9_]{0,127}$/u.test(apiKeyEnv)) {
232
- throw new Error(`Invalid sift-light config at ${path}: semanticJudge.apiKeyEnv must be an uppercase environment variable name`);
233
- }
234
- const model = value.model ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.model;
235
- if (typeof model !== "string" || model.length === 0 || model.length > 128 || /[\r\n\0]/u.test(model)) {
236
- throw new Error(`Invalid sift-light config at ${path}: semanticJudge.model must be bounded single-line text`);
246
+ if (provider !== "jev" && provider !== "http-json") {
247
+ throw new Error(`Invalid sift-light config at ${path}: semanticJudge.provider must be "jev" or "http-json"`);
237
248
  }
249
+ const endpoint = parseEndpoint(value.endpoint ?? (provider === "jev" ? DEFAULT_SEMANTIC_JUDGE_CONFIG.endpoint : undefined), path, "semanticJudge.endpoint");
250
+ const apiKeyEnv = parseApiKeyEnv(value.apiKeyEnv, path, "semanticJudge.apiKeyEnv") ?? (provider === "jev" ? DEFAULT_SEMANTIC_JUDGE_CONFIG.apiKeyEnv : undefined);
251
+ const model = parseBoundedModel(value.model ?? (provider === "jev" ? DEFAULT_SEMANTIC_JUDGE_CONFIG.model : undefined), path, "semanticJudge.model");
238
252
  return {
239
253
  enabled,
240
254
  provider,
241
255
  endpoint,
242
- apiKeyEnv,
256
+ ...apiKeyEnv === undefined ? {} : { apiKeyEnv },
243
257
  model,
244
258
  timeoutMs: boundedInteger(value.timeoutMs, DEFAULT_SEMANTIC_JUDGE_CONFIG.timeoutMs, 1000, 1200000, `config at ${path}: semanticJudge.timeoutMs`),
245
259
  maxCandidates: boundedInteger(value.maxCandidates, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxCandidates, 1, 20, `config at ${path}: semanticJudge.maxCandidates`),
246
260
  maxRetries: boundedInteger(value.maxRetries, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxRetries, 0, 5, `config at ${path}: semanticJudge.maxRetries`)
247
261
  };
248
262
  }
263
+ function parseMedia(value, path) {
264
+ if (value === undefined)
265
+ return;
266
+ if (!isRawMediaConfig(value)) {
267
+ throw new Error(`Invalid sift-light config at ${path}: media must be an object`);
268
+ }
269
+ const unknown = Object.keys(value).filter((key) => ![
270
+ "provider",
271
+ "endpoint",
272
+ "apiKeyEnv",
273
+ "model",
274
+ "timeoutMs",
275
+ "maxRetries",
276
+ "maxRequestBytes",
277
+ "renderer"
278
+ ].includes(key));
279
+ if (unknown.length > 0) {
280
+ throw new Error(`Invalid sift-light config at ${path}: unsupported media fields; accepted fields are provider, endpoint, apiKeyEnv, model, timeoutMs, maxRetries, maxRequestBytes and renderer`);
281
+ }
282
+ const provider = value.provider ?? "http-json";
283
+ if (provider !== "http-json") {
284
+ throw new Error(`Invalid sift-light config at ${path}: media.provider must be "http-json"`);
285
+ }
286
+ const endpoint = parseEndpoint(value.endpoint, path, "media.endpoint");
287
+ const apiKeyEnv = parseApiKeyEnv(value.apiKeyEnv, path, "media.apiKeyEnv");
288
+ const model = parseBoundedModel(value.model, path, "media.model");
289
+ const renderer = parseNativeTextConfig(value.renderer, "media.renderer");
290
+ return {
291
+ provider,
292
+ endpoint,
293
+ ...apiKeyEnv === undefined ? {} : { apiKeyEnv },
294
+ model,
295
+ timeoutMs: boundedInteger(value.timeoutMs, 120000, 1000, 1200000, `config at ${path}: media.timeoutMs`),
296
+ maxRetries: boundedInteger(value.maxRetries, 2, 0, 5, `config at ${path}: media.maxRetries`),
297
+ maxRequestBytes: boundedInteger(value.maxRequestBytes, 32 * 1024 * 1024, 1 * 1024 * 1024, 64 * 1024 * 1024, `config at ${path}: media.maxRequestBytes`),
298
+ ...renderer === undefined ? {} : { renderer }
299
+ };
300
+ }
249
301
  function parseConfig(value, path) {
250
302
  if (!isRawSiftLightConfig(value)) {
251
303
  throw new Error(`Invalid sift-light config at ${path}: expected a JSON object`);
@@ -257,10 +309,11 @@ function parseConfig(value, path) {
257
309
  "semanticJudge",
258
310
  "dataSources",
259
311
  "nativeText",
260
- "nativeMedia"
312
+ "nativeMedia",
313
+ "media"
261
314
  ].includes(key));
262
315
  if (unknown.length > 0) {
263
- throw new Error(`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch, vectorSearchEnabled, semanticJudge, dataSources, nativeText and nativeMedia are accepted`);
316
+ throw new Error(`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch, vectorSearchEnabled, semanticJudge, dataSources, nativeText, nativeMedia and media are accepted`);
264
317
  }
265
318
  const { locale, enforceSearch, vectorSearchEnabled } = value;
266
319
  if (locale !== undefined && locale !== "en" && locale !== "zh-CN") {
@@ -272,11 +325,15 @@ function parseConfig(value, path) {
272
325
  const enforcement = normalizeSearchEnforcement(enforceSearch, `config at ${path}`);
273
326
  const nativeText = parseNativeTextConfig(value.nativeText);
274
327
  const nativeMedia = parseNativeTextConfig(value.nativeMedia, "nativeMedia");
328
+ const media = parseMedia(value.media, `${path}: media`);
329
+ if (nativeMedia && media)
330
+ throw new Error(`Invalid sift-light config at ${path}: choose nativeMedia or media, not both`);
275
331
  if (nativeText && vectorSearchEnabled !== true)
276
332
  throw new Error("nativeText requires vectorSearchEnabled:true");
277
333
  return {
278
334
  ...nativeText ? { nativeText } : {},
279
335
  ...nativeMedia ? { nativeMedia } : {},
336
+ ...media ? { media } : {},
280
337
  locale: locale ?? DEFAULT_SIFT_LIGHT_CONFIG.locale,
281
338
  enforceSearch: enforcement,
282
339
  vectorSearchEnabled: vectorSearchEnabled ?? DEFAULT_SIFT_LIGHT_CONFIG.vectorSearchEnabled ?? false,
@@ -2060,7 +2117,7 @@ function createCtagsStructureProvider(options = {}) {
2060
2117
  // package.json
2061
2118
  var package_default = {
2062
2119
  name: "sift-light",
2063
- version: "1.1.0-1",
2120
+ version: "1.1.0",
2064
2121
  description: "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
2065
2122
  keywords: [
2066
2123
  "ai-agent",
@@ -2676,10 +2733,10 @@ class OperationLifecycle {
2676
2733
  }
2677
2734
 
2678
2735
  // src/media-search.ts
2679
- import { createHash, randomUUID as randomUUID3 } from "crypto";
2680
- import { mkdtemp, mkdir, readFile as readFile3, realpath as realpath3, rm, stat as stat4, writeFile } from "fs/promises";
2736
+ import { createHash, randomUUID as randomUUID4 } from "crypto";
2737
+ import { mkdtemp, mkdir, readFile as readFile4, realpath as realpath3, rm, stat as stat5, writeFile } from "fs/promises";
2681
2738
  import { tmpdir } from "os";
2682
- import { basename as basename2, dirname as dirname2, extname, join as join3, resolve as resolve8 } from "path";
2739
+ import { basename as basename2, dirname as dirname2, extname as extname2, join as join3, resolve as resolve8 } from "path";
2683
2740
 
2684
2741
  // node_modules/zod/v4/core/util.js
2685
2742
  function getEnumValues(entries) {
@@ -8295,6 +8352,278 @@ class ResidentWorkerClient {
8295
8352
  }
8296
8353
  }
8297
8354
 
8355
+ // src/remote-media-client.ts
8356
+ import { randomUUID as randomUUID3 } from "crypto";
8357
+ import { readFile as readFile3, stat as stat4 } from "fs/promises";
8358
+ import { extname } from "path";
8359
+ var MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
8360
+ var MAX_FILE_ID_LENGTH = 128;
8361
+
8362
+ class RemoteMediaRequestError extends SiftLightError {
8363
+ retryable;
8364
+ constructor(message, retryable, options) {
8365
+ super(message, options);
8366
+ this.name = "RemoteMediaRequestError";
8367
+ this.retryable = retryable;
8368
+ }
8369
+ }
8370
+ var remoteResponseSchema = object({
8371
+ version: literal(1),
8372
+ indexedFiles: number2().int().nonnegative(),
8373
+ hits: array(object({
8374
+ fileId: string2().min(1).max(MAX_FILE_ID_LENGTH),
8375
+ score: number2().finite(),
8376
+ kind: _enum(["image", "scan"]),
8377
+ page: number2().int().positive().nullable().optional(),
8378
+ chunkCount: number2().int().positive(),
8379
+ width: number2().int().nonnegative(),
8380
+ height: number2().int().nonnegative()
8381
+ }).strict()).max(10)
8382
+ }).strict();
8383
+ function mimeType(path) {
8384
+ switch (extname(path).toLowerCase()) {
8385
+ case ".png":
8386
+ return "image/png";
8387
+ case ".jpg":
8388
+ case ".jpeg":
8389
+ return "image/jpeg";
8390
+ case ".gif":
8391
+ return "image/gif";
8392
+ case ".bmp":
8393
+ return "image/bmp";
8394
+ case ".tif":
8395
+ case ".tiff":
8396
+ return "image/tiff";
8397
+ case ".heic":
8398
+ return "image/heic";
8399
+ case ".heif":
8400
+ return "image/heif";
8401
+ case ".webp":
8402
+ return "image/webp";
8403
+ case ".pdf":
8404
+ return "application/pdf";
8405
+ default:
8406
+ return "application/octet-stream";
8407
+ }
8408
+ }
8409
+ async function boundedResponseText(response) {
8410
+ const contentLength = response.headers.get("content-length");
8411
+ if (contentLength !== null && Number(contentLength) > MAX_RESPONSE_BYTES)
8412
+ throw new SiftLightError("Remote media response exceeded the 4 MiB limit");
8413
+ if (!response.body) {
8414
+ const text = await response.text();
8415
+ if (new TextEncoder().encode(text).byteLength > MAX_RESPONSE_BYTES)
8416
+ throw new SiftLightError("Remote media response exceeded the 4 MiB limit");
8417
+ return text;
8418
+ }
8419
+ const reader = response.body.getReader();
8420
+ const chunks = [];
8421
+ let bytes = 0;
8422
+ try {
8423
+ while (true) {
8424
+ const result = await reader.read();
8425
+ if (result.done)
8426
+ break;
8427
+ bytes += result.value.byteLength;
8428
+ if (bytes > MAX_RESPONSE_BYTES) {
8429
+ await reader.cancel();
8430
+ throw new SiftLightError("Remote media response exceeded the 4 MiB limit");
8431
+ }
8432
+ chunks.push(result.value);
8433
+ }
8434
+ } finally {
8435
+ reader.releaseLock();
8436
+ }
8437
+ const all = new Uint8Array(bytes);
8438
+ let offset = 0;
8439
+ for (const chunk of chunks) {
8440
+ all.set(chunk, offset);
8441
+ offset += chunk.byteLength;
8442
+ }
8443
+ return new TextDecoder("utf-8", { fatal: true }).decode(all);
8444
+ }
8445
+ function retryableStatus(status) {
8446
+ return status === 408 || status === 429 || status === 529 || status >= 500;
8447
+ }
8448
+ function retryDelay(attempt) {
8449
+ return Math.min(20000, 400 * 2 ** attempt);
8450
+ }
8451
+ async function waitForRetry(delayMs, signal) {
8452
+ if (signal?.aborted)
8453
+ throw signal.reason instanceof Error ? signal.reason : new Error("Operation aborted");
8454
+ await new Promise((resolve, reject) => {
8455
+ const timer = setTimeout(() => {
8456
+ signal?.removeEventListener("abort", onAbort);
8457
+ resolve();
8458
+ }, delayMs);
8459
+ const onAbort = () => {
8460
+ clearTimeout(timer);
8461
+ signal?.removeEventListener("abort", onAbort);
8462
+ reject(signal?.reason instanceof Error ? signal.reason : new Error("Operation aborted"));
8463
+ };
8464
+ signal?.addEventListener("abort", onAbort, { once: true });
8465
+ });
8466
+ }
8467
+ function base642(bytes) {
8468
+ return bytes.toString("base64");
8469
+ }
8470
+ function base64Length(bytes) {
8471
+ return Math.ceil(bytes / 3) * 4;
8472
+ }
8473
+
8474
+ class RemoteMediaClient {
8475
+ #config;
8476
+ #key;
8477
+ #fetcher;
8478
+ constructor(config, environment = process.env, fetcher = fetch) {
8479
+ this.#config = config;
8480
+ this.#key = config.apiKeyEnv ? environment[config.apiKeyEnv]?.trim() : undefined;
8481
+ if (config.apiKeyEnv && !this.#key) {
8482
+ throw new SiftLightError(`Media provider is configured with ${config.apiKeyEnv}, but that environment variable is missing or empty`);
8483
+ }
8484
+ this.#fetcher = fetcher;
8485
+ }
8486
+ async search(request) {
8487
+ const id = randomUUID3();
8488
+ const paths = new Map;
8489
+ const files = [];
8490
+ const fileSizes = [];
8491
+ for (const file of request.files) {
8492
+ if (file.id.length === 0 || file.id.length > MAX_FILE_ID_LENGTH || /[\r\n\0]/u.test(file.id) || paths.has(file.id))
8493
+ throw new SiftLightError("Remote media file ids must be unique bounded text");
8494
+ paths.set(file.id, file.path);
8495
+ const metadata2 = await stat4(file.path);
8496
+ if (!metadata2.isFile())
8497
+ throw new SiftLightError("Remote media files must be regular files");
8498
+ fileSizes.push(metadata2.size);
8499
+ }
8500
+ let imageSize;
8501
+ if (request.imagePath !== undefined) {
8502
+ const metadata2 = await stat4(request.imagePath);
8503
+ if (!metadata2.isFile())
8504
+ throw new SiftLightError("Remote media query image must be a regular file");
8505
+ imageSize = metadata2.size;
8506
+ }
8507
+ const requestSkeleton = JSON.stringify({
8508
+ version: 1,
8509
+ model: this.#config.model,
8510
+ ...request.query === undefined ? {} : { query: request.query },
8511
+ ...request.imagePath === undefined ? {} : { image: { mimeType: mimeType(request.imagePath), data: "" } },
8512
+ kind: request.kind,
8513
+ expectedFiles: request.expectedFiles,
8514
+ files: []
8515
+ });
8516
+ let estimatedBytes = new TextEncoder().encode(requestSkeleton).byteLength;
8517
+ if (imageSize !== undefined)
8518
+ estimatedBytes += base64Length(imageSize);
8519
+ for (let index = 0;index < request.files.length; index += 1) {
8520
+ const file = request.files[index];
8521
+ const fileSkeleton = JSON.stringify({
8522
+ id: file?.id,
8523
+ mimeType: file ? mimeType(file.path) : "application/octet-stream",
8524
+ data: ""
8525
+ });
8526
+ estimatedBytes += new TextEncoder().encode(fileSkeleton).byteLength;
8527
+ estimatedBytes += base64Length(fileSizes[index] ?? 0);
8528
+ }
8529
+ if (request.files.length > 1)
8530
+ estimatedBytes += request.files.length - 1;
8531
+ if (estimatedBytes > this.#config.maxRequestBytes)
8532
+ throw new SiftLightError(`Remote media request exceeded the configured ${String(this.#config.maxRequestBytes)} byte limit`);
8533
+ for (const file of request.files) {
8534
+ files.push({
8535
+ id: file.id,
8536
+ mimeType: mimeType(file.path),
8537
+ data: base642(await readFile3(file.path, { signal: request.signal }))
8538
+ });
8539
+ }
8540
+ const image = request.imagePath === undefined ? undefined : {
8541
+ mimeType: mimeType(request.imagePath),
8542
+ data: base642(await readFile3(request.imagePath, { signal: request.signal }))
8543
+ };
8544
+ const body2 = JSON.stringify({
8545
+ version: 1,
8546
+ model: this.#config.model,
8547
+ ...request.query === undefined ? {} : { query: request.query },
8548
+ ...image === undefined ? {} : { image },
8549
+ kind: request.kind,
8550
+ expectedFiles: request.expectedFiles,
8551
+ files
8552
+ });
8553
+ if (new TextEncoder().encode(body2).byteLength > this.#config.maxRequestBytes)
8554
+ throw new SiftLightError(`Remote media request exceeded the configured ${String(this.#config.maxRequestBytes)} byte limit`);
8555
+ for (let attempt = 0;attempt <= this.#config.maxRetries; attempt += 1) {
8556
+ const controller = new AbortController;
8557
+ const onParentAbort = () => controller.abort(request.signal?.reason);
8558
+ request.signal?.addEventListener("abort", onParentAbort, { once: true });
8559
+ const timeout = setTimeout(() => controller.abort(new Error("Remote media request timed out")), this.#config.timeoutMs);
8560
+ try {
8561
+ const headers = { "content-type": "application/json" };
8562
+ if (this.#key)
8563
+ headers.authorization = `Bearer ${this.#key}`;
8564
+ const response = await this.#fetcher(this.#config.endpoint, {
8565
+ method: "POST",
8566
+ headers,
8567
+ body: body2,
8568
+ signal: controller.signal
8569
+ });
8570
+ if (!response.ok) {
8571
+ throw new RemoteMediaRequestError(`Remote media provider returned HTTP ${String(response.status)}`, retryableStatus(response.status));
8572
+ }
8573
+ let parsed;
8574
+ try {
8575
+ const raw = JSON.parse(await boundedResponseText(response));
8576
+ parsed = remoteResponseSchema.parse(raw);
8577
+ } catch (error) {
8578
+ throw new SiftLightError("Remote media provider returned an invalid response", {
8579
+ cause: error
8580
+ });
8581
+ }
8582
+ if (parsed.indexedFiles !== request.expectedFiles)
8583
+ throw new SiftLightError("Remote media provider indexed an unexpected file count");
8584
+ return {
8585
+ version: 1,
8586
+ id,
8587
+ indexedFiles: parsed.indexedFiles,
8588
+ modelLoads: 0,
8589
+ modelReused: true,
8590
+ processId: process.pid,
8591
+ hits: parsed.hits.map((hit) => {
8592
+ const path = paths.get(hit.fileId);
8593
+ if (!path)
8594
+ throw new SiftLightError("Remote media provider returned an unknown file id");
8595
+ return {
8596
+ path,
8597
+ score: hit.score,
8598
+ kind: hit.kind,
8599
+ page: hit.page ?? null,
8600
+ chunkCount: hit.chunkCount,
8601
+ width: hit.width,
8602
+ height: hit.height
8603
+ };
8604
+ })
8605
+ };
8606
+ } catch (error) {
8607
+ if (request.signal?.aborted)
8608
+ throw error;
8609
+ const retryable = error instanceof RemoteMediaRequestError ? error.retryable : !(error instanceof SiftLightError);
8610
+ if (!retryable || attempt >= this.#config.maxRetries)
8611
+ throw error;
8612
+ await waitForRetry(retryDelay(attempt), request.signal);
8613
+ } finally {
8614
+ clearTimeout(timeout);
8615
+ request.signal?.removeEventListener("abort", onParentAbort);
8616
+ }
8617
+ }
8618
+ throw new SiftLightError("Remote media request failed");
8619
+ }
8620
+ async close() {}
8621
+ }
8622
+ function mediaMimeType(path) {
8623
+ const type = mimeType(path);
8624
+ return type === "application/octet-stream" ? undefined : type;
8625
+ }
8626
+
8298
8627
  // src/workspace-files.ts
8299
8628
  import { relative as relative3, resolve as resolve7, sep as sep2 } from "path";
8300
8629
  class EnumerationLimit extends Error {
@@ -8417,7 +8746,7 @@ var responseSchema = object({
8417
8746
  version: literal(1),
8418
8747
  id: string2(),
8419
8748
  indexedFiles: number2().int().nonnegative(),
8420
- modelLoads: literal(1),
8749
+ modelLoads: number2().int().nonnegative(),
8421
8750
  modelReused: boolean2(),
8422
8751
  processId: number2().int().positive(),
8423
8752
  hits: array(object({
@@ -8453,11 +8782,11 @@ async function sourceBytes(cwd, path, signal) {
8453
8782
  const canonical = await policy.resolveExistingPath(path);
8454
8783
  if (!canonical)
8455
8784
  throw new SiftLightError(`Media source unavailable: ${path}`);
8456
- const before = await stat4(canonical);
8785
+ const before = await stat5(canonical);
8457
8786
  if (!before.isFile() || before.size > maxFileBytes)
8458
8787
  throw new SiftLightError(`Media source must be a regular file at most ${String(maxFileBytes)} bytes: ${path}`);
8459
- const bytes = await readFile3(canonical, { signal });
8460
- const after = await stat4(canonical);
8788
+ const bytes = await readFile4(canonical, { signal });
8789
+ const after = await stat5(canonical);
8461
8790
  if (bytes.length > maxFileBytes || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || await policy.resolveExistingPath(path) !== canonical)
8462
8791
  throw new SiftLightError(`Media source changed during read: ${path}`);
8463
8792
  return { canonical, bytes };
@@ -8465,6 +8794,7 @@ async function sourceBytes(cwd, path, signal) {
8465
8794
 
8466
8795
  class MediaSearch {
8467
8796
  #config;
8797
+ #remoteConfig;
8468
8798
  #operations = new OperationLifecycle({ deadlineMs: 300000 });
8469
8799
  #queue = new OwnedTaskQueue;
8470
8800
  #lifecycle = new AbortController;
@@ -8472,9 +8802,13 @@ class MediaSearch {
8472
8802
  #known = new Map;
8473
8803
  #directory;
8474
8804
  #worker;
8805
+ #remote;
8475
8806
  #closed = false;
8476
8807
  constructor(config) {
8477
- this.#config = config;
8808
+ if ("provider" in config)
8809
+ this.#remoteConfig = config;
8810
+ else
8811
+ this.#config = config;
8478
8812
  }
8479
8813
  async search(value, cwd, signal) {
8480
8814
  const request = mediaRequestSchema.parse(value);
@@ -8530,7 +8864,7 @@ class MediaSearch {
8530
8864
  const reasons = [...inventory.reasons];
8531
8865
  let total = 0;
8532
8866
  for (const path of inventory.paths) {
8533
- if (!extensions.has(extname(path).toLowerCase()))
8867
+ if (!extensions.has(extname2(path).toLowerCase()))
8534
8868
  continue;
8535
8869
  if (signal.aborted)
8536
8870
  throw abortError();
@@ -8568,20 +8902,37 @@ class MediaSearch {
8568
8902
  let imageSource;
8569
8903
  let queryImage;
8570
8904
  if (r.image) {
8571
- if (!extensions.has(extname(r.image).toLowerCase()) || extname(r.image).toLowerCase() === ".pdf")
8905
+ if (!extensions.has(extname2(r.image).toLowerCase()) || extname2(r.image).toLowerCase() === ".pdf")
8572
8906
  throw new SiftLightError("image must be a supported local image file");
8573
8907
  imageSource = await sourceBytes(cwd, r.image, signal);
8574
- queryImage = join3(root, "query" + extname(r.image).toLowerCase());
8908
+ queryImage = join3(root, "query" + extname2(r.image).toLowerCase());
8575
8909
  await writeFile(queryImage, imageSource.bytes, { mode: 384 });
8576
8910
  }
8577
8911
  let response;
8578
8912
  if (admitted.size) {
8579
- this.#worker ??= new ResidentWorkerClient(this.#config, root, corpus, join3(root, "index.sqlite"), { parse: (value) => responseSchema.parse(value), maxResponseBytes: 128 * 1024 });
8580
- response = await this.#worker.search({
8581
- ...queryImage ? { image: queryImage } : { query: r.query },
8582
- kind: r.kind,
8583
- expectedFiles: admitted.size
8584
- }, signal);
8913
+ if (this.#config) {
8914
+ this.#worker ??= new ResidentWorkerClient(this.#config, root, corpus, join3(root, "index.sqlite"), { parse: (value) => responseSchema.parse(value), maxResponseBytes: 128 * 1024 });
8915
+ response = await this.#worker.search({
8916
+ ...queryImage ? { image: queryImage } : { query: r.query },
8917
+ kind: r.kind,
8918
+ expectedFiles: admitted.size
8919
+ }, signal);
8920
+ } else if (this.#remoteConfig) {
8921
+ this.#remote ??= new RemoteMediaClient(this.#remoteConfig);
8922
+ const remoteFiles = [...admitted.values()].map((entry) => ({
8923
+ id: createHash("sha256").update(entry.staged).digest("hex").slice(0, 32),
8924
+ path: entry.staged
8925
+ }));
8926
+ response = await this.#remote.search({
8927
+ ...queryImage ? { imagePath: queryImage } : { query: r.query },
8928
+ kind: r.kind,
8929
+ expectedFiles: admitted.size,
8930
+ files: remoteFiles,
8931
+ signal
8932
+ });
8933
+ }
8934
+ if (!response)
8935
+ throw new SiftLightError("Media provider is not configured");
8585
8936
  if (response.indexedFiles !== admitted.size)
8586
8937
  throw new SiftLightError("Media index inventory mismatch");
8587
8938
  }
@@ -8589,7 +8940,7 @@ class MediaSearch {
8589
8940
  const seen = new Set;
8590
8941
  for (const hit of response?.hits ?? []) {
8591
8942
  const source = admitted.get(resolve8(hit.path));
8592
- if (!source || hit.kind === "scan" !== (extname(source.path).toLowerCase() === ".pdf") || seen.has(hit.path) || hit.kind === "scan" !== (hit.page !== null) || hit.page !== null && hit.page > hit.chunkCount)
8943
+ if (!source || hit.kind === "scan" !== (extname2(source.path).toLowerCase() === ".pdf") || seen.has(hit.path) || hit.kind === "scan" !== (hit.page !== null) || hit.page !== null && hit.page > hit.chunkCount)
8593
8944
  throw new SiftLightError("Media result source or page is inconsistent");
8594
8945
  seen.add(hit.path);
8595
8946
  saved.push({ hit, source });
@@ -8609,7 +8960,7 @@ class MediaSearch {
8609
8960
  }
8610
8961
  if (signal.aborted)
8611
8962
  throw abortError();
8612
- const cursor = randomUUID3();
8963
+ const cursor = randomUUID4();
8613
8964
  for (const [id, snapshot] of this.#snapshots)
8614
8965
  if (Date.now() - snapshot.createdAt > 600000)
8615
8966
  this.#snapshots.delete(id);
@@ -8672,11 +9023,23 @@ ${JSON.stringify(details)}`,
8672
9023
  throw new SiftLightError("Media source changed; search again before inspection");
8673
9024
  const folder = await mkdtemp(join3(await this.#root(), "inspect-"));
8674
9025
  try {
8675
- const input = join3(folder, "source" + extname(item.source.path));
9026
+ const input = join3(folder, "source" + extname2(item.source.path));
8676
9027
  const output = join3(folder, "preview.png");
8677
9028
  await writeFile(input, current.bytes, { mode: 384 });
9029
+ const renderer = this.#config ?? this.#remoteConfig?.renderer;
9030
+ if (!renderer) {
9031
+ const mimeType = mediaMimeType(item.source.path);
9032
+ if (mimeType?.startsWith("image/") && mimeType === "image/png") {
9033
+ return {
9034
+ text: JSON.stringify(details),
9035
+ details,
9036
+ image: { data: current.bytes.toString("base64"), mimeType }
9037
+ };
9038
+ }
9039
+ throw new SiftLightError("Media preview requires nativeMedia or media.renderer; validate still works without a local renderer");
9040
+ }
8678
9041
  const result = await runOwnedProcess({
8679
- executable: this.#config.executable,
9042
+ executable: renderer.executable,
8680
9043
  args: ["--render", input, String(item.hit.page ?? 0), output],
8681
9044
  cwd: folder,
8682
9045
  signal
@@ -8687,7 +9050,7 @@ ${JSON.stringify(details)}`,
8687
9050
  });
8688
9051
  if (result.code !== 0)
8689
9052
  throw new SiftLightError(`Media preview failed: ${result.stderr}`);
8690
- const preview = await readFile3(output);
9053
+ const preview = await readFile4(output);
8691
9054
  if (preview.length > 4 * 1024 * 1024)
8692
9055
  throw new SiftLightError("Media preview exceeds 4 MiB");
8693
9056
  return {
@@ -8712,6 +9075,7 @@ ${JSON.stringify(details)}`,
8712
9075
  await this.#queue.run(async () => {
8713
9076
  try {
8714
9077
  await this.#worker?.close();
9078
+ await this.#remote?.close();
8715
9079
  } finally {
8716
9080
  this.#snapshots.clear();
8717
9081
  if (this.#directory)
@@ -8730,7 +9094,7 @@ class DataSourceError extends Error {
8730
9094
  this.code = code;
8731
9095
  }
8732
9096
  }
8733
- var MAX_RESPONSE_BYTES = 1024 * 1024;
9097
+ var MAX_RESPONSE_BYTES2 = 1024 * 1024;
8734
9098
  async function credential(config) {
8735
9099
  let value;
8736
9100
  if (config.tokenEnv)
@@ -8785,7 +9149,7 @@ async function sourcePost(config, path, payload, signal) {
8785
9149
  if (part.done)
8786
9150
  break;
8787
9151
  size += part.value.byteLength;
8788
- if (size > MAX_RESPONSE_BYTES) {
9152
+ if (size > MAX_RESPONSE_BYTES2) {
8789
9153
  await reader.cancel();
8790
9154
  throw new DataSourceError("protocol");
8791
9155
  }
@@ -9035,14 +9399,14 @@ class DataSources {
9035
9399
  import { createHash as createHash7 } from "crypto";
9036
9400
 
9037
9401
  // src/concept-worker-client.ts
9038
- import { randomUUID as randomUUID4 } from "crypto";
9402
+ import { randomUUID as randomUUID5 } from "crypto";
9039
9403
  import { mkdir as mkdir2, rm as rm2 } from "fs/promises";
9040
9404
  import { dirname as dirname3, join as join5 } from "path";
9041
9405
  import { fileURLToPath } from "url";
9042
9406
  import { StringDecoder as StringDecoder2 } from "string_decoder";
9043
9407
 
9044
9408
  // src/concept-model.ts
9045
- import { stat as stat5 } from "fs/promises";
9409
+ import { stat as stat6 } from "fs/promises";
9046
9410
  import { homedir as homedir2 } from "os";
9047
9411
  import { join as join4, resolve as resolve9 } from "path";
9048
9412
  var CONCEPT_MODEL = "Xenova/multilingual-e5-small";
@@ -9085,7 +9449,7 @@ function conceptModelDirectory() {
9085
9449
  async function inspectConceptModel(directory = conceptModelDirectory()) {
9086
9450
  for (const asset of CONCEPT_ASSETS) {
9087
9451
  try {
9088
- const information = await stat5(join4(directory, asset.path));
9452
+ const information = await stat6(join4(directory, asset.path));
9089
9453
  if (!information.isFile() || information.size !== asset.bytes)
9090
9454
  return {
9091
9455
  installed: false,
@@ -9181,7 +9545,7 @@ class ConceptWorkerClient {
9181
9545
  throw new SiftLightError("Concept worker idle timeout must be 1\u2013300000 ms");
9182
9546
  }
9183
9547
  async#start() {
9184
- const stagingRoot = join5(conceptCacheDirectory(), ".staging", randomUUID4());
9548
+ const stagingRoot = join5(conceptCacheDirectory(), ".staging", randomUUID5());
9185
9549
  await mkdir2(stagingRoot, { recursive: true });
9186
9550
  const ready = Promise.withResolvers();
9187
9551
  ready.promise.catch(() => {
@@ -9315,7 +9679,7 @@ class ConceptWorkerClient {
9315
9679
  const input = await session.ready;
9316
9680
  if (combined.aborted)
9317
9681
  throw abortError();
9318
- const id = randomUUID4();
9682
+ const id = randomUUID5();
9319
9683
  const body2 = JSON.stringify({
9320
9684
  id,
9321
9685
  query,
@@ -10109,7 +10473,7 @@ function deriveSyntaxFacts(tree, language, text) {
10109
10473
  }
10110
10474
 
10111
10475
  // src/language-capability-definitions.ts
10112
- import { extname as extname2 } from "path";
10476
+ import { extname as extname3 } from "path";
10113
10477
  var TYPESCRIPT_LANGUAGE_CAPABILITIES = [
10114
10478
  {
10115
10479
  id: "ast-grep.outline",
@@ -10258,14 +10622,14 @@ var DEFAULT_NEUTRAL_CAPABILITIES = [
10258
10622
  }
10259
10623
  ];
10260
10624
  function descriptorForPath(descriptors, path) {
10261
- const extension = extname2(path).toLowerCase();
10625
+ const extension = extname3(path).toLowerCase();
10262
10626
  return descriptors.find((item) => item.extensions.includes(extension));
10263
10627
  }
10264
10628
  function languageForPath(descriptors, path) {
10265
10629
  return descriptorForPath(descriptors, path)?.language;
10266
10630
  }
10267
10631
  function outlineExtension(path) {
10268
- const extension = extname2(path).toLowerCase();
10632
+ const extension = extname3(path).toLowerCase();
10269
10633
  return extension || undefined;
10270
10634
  }
10271
10635
 
@@ -10441,7 +10805,7 @@ async function parseSyntax(path, text, signal, pattern) {
10441
10805
  import { createHash as createHash4 } from "crypto";
10442
10806
 
10443
10807
  // src/source-access.ts
10444
- import { extname as extname3, resolve as resolve13 } from "path";
10808
+ import { extname as extname4, resolve as resolve13 } from "path";
10445
10809
 
10446
10810
  // src/historical-paths.ts
10447
10811
  import { lstat, mkdir as mkdir3, mkdtemp as mkdtemp2, open as open3, rm as rm3, writeFile as writeFile2 } from "fs/promises";
@@ -11574,7 +11938,7 @@ class SyntaxQueue {
11574
11938
  throw new SourceDocumentError("encoding", "Syntax requires lossless UTF-8 source");
11575
11939
  const origin = document2.reference.origin;
11576
11940
  const revision = origin.kind === "worktree" ? origin.contentHash : origin.blob;
11577
- const key = `${extname3(document2.path).toLowerCase()}\x00${revision}\x00${pattern ?? ""}`;
11941
+ const key = `${extname4(document2.path).toLowerCase()}\x00${revision}\x00${pattern ?? ""}`;
11578
11942
  const cached = this.#cache.get(key);
11579
11943
  if (cached) {
11580
11944
  this.#cache.delete(key);
@@ -12353,7 +12717,7 @@ async function structuralSearch(input, access) {
12353
12717
 
12354
12718
  // src/native-text-search.ts
12355
12719
  import { createHash as createHash6 } from "crypto";
12356
- import { mkdtemp as mkdtemp3, mkdir as mkdir4, readFile as readFile4, realpath as realpath5, rm as rm4, writeFile as writeFile3 } from "fs/promises";
12720
+ import { mkdtemp as mkdtemp3, mkdir as mkdir4, readFile as readFile5, realpath as realpath5, rm as rm4, writeFile as writeFile3 } from "fs/promises";
12357
12721
  import { tmpdir as tmpdir3 } from "os";
12358
12722
  import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute8, join as join7, resolve as resolve15 } from "path";
12359
12723
 
@@ -12477,7 +12841,7 @@ function createNativeTextSearch(config, search) {
12477
12841
  if (!entry || seen.has(entry.staged))
12478
12842
  throw new SiftLightError("Native text returned an unknown or duplicate source");
12479
12843
  seen.add(entry.staged);
12480
- const content = await readFile4(entry.staged);
12844
+ const content = await readFile5(entry.staged);
12481
12845
  if (createHash6("sha256").update(content).digest("hex") !== entry.hash)
12482
12846
  throw new SiftLightError("Native staged source changed");
12483
12847
  const document2 = new SourceDocument(entry.source, content);
@@ -12558,10 +12922,10 @@ function rankRetrievalFiles(current, native) {
12558
12922
  }
12559
12923
 
12560
12924
  // src/evidence-service.ts
12561
- import { dirname as dirname7, extname as extname4, resolve as resolve24 } from "path";
12925
+ import { dirname as dirname7, extname as extname5, resolve as resolve24 } from "path";
12562
12926
 
12563
12927
  // src/analysis-store.ts
12564
- import { randomUUID as randomUUID5 } from "crypto";
12928
+ import { randomUUID as randomUUID6 } from "crypto";
12565
12929
 
12566
12930
  // src/analysis-types.ts
12567
12931
  var SEMANTIC_JUDGE_CLASSIFICATIONS = [
@@ -12946,7 +13310,7 @@ class AnalysisStore {
12946
13310
  }
12947
13311
  create(result, summarize) {
12948
13312
  this.#expire();
12949
- const id = randomUUID5();
13313
+ const id = randomUUID6();
12950
13314
  const bounded = {
12951
13315
  ...result,
12952
13316
  ...result.retrievalLanes ? {
@@ -15191,7 +15555,7 @@ async function discoverFiles(input, cwd, signal) {
15191
15555
  }
15192
15556
 
15193
15557
  // src/source-continuations.ts
15194
- import { randomUUID as randomUUID6 } from "crypto";
15558
+ import { randomUUID as randomUUID7 } from "crypto";
15195
15559
 
15196
15560
  // src/source-pages.ts
15197
15561
  function mergeByteRanges(ranges) {
@@ -15295,7 +15659,7 @@ class SourceContinuations {
15295
15659
  create(source, target, gaps, boundary) {
15296
15660
  this.#sweep();
15297
15661
  const item = {
15298
- id: randomUUID6(),
15662
+ id: randomUUID7(),
15299
15663
  source: structuredClone(source),
15300
15664
  target: mergeByteRanges(target),
15301
15665
  gaps: mergeByteRanges(gaps),
@@ -15891,7 +16255,7 @@ async function continueSource(cursor, access, continuations, expectedPath) {
15891
16255
  }
15892
16256
 
15893
16257
  // src/evidence-validation.ts
15894
- import { realpath as realpath7, stat as stat6 } from "fs/promises";
16258
+ import { realpath as realpath7, stat as stat7 } from "fs/promises";
15895
16259
  import { resolve as resolve22 } from "path";
15896
16260
 
15897
16261
  // src/evidence-validity.ts
@@ -15929,9 +16293,9 @@ async function confirmWorktreeState(path, cwd, signal) {
15929
16293
  const policy = new SearchPathPolicy(cwd);
15930
16294
  try {
15931
16295
  policy.assertPath(absolute);
15932
- const before = await stat6(absolute);
16296
+ const before = await stat7(absolute);
15933
16297
  const beforeCanonical = await realpath7(absolute);
15934
- const after = await stat6(absolute);
16298
+ const after = await stat7(absolute);
15935
16299
  const afterCanonical = await realpath7(absolute);
15936
16300
  if (!before.isFile() || !after.isFile()) {
15937
16301
  return { status: "stale", reason: "Source is no longer a regular file" };
@@ -16073,9 +16437,9 @@ async function validateSnapshotTarget(target, cwd, policy, signal) {
16073
16437
  const result = await confirmWorktreeState(target.path, cwd, signal);
16074
16438
  return { path: target.path, role: "source", ...result };
16075
16439
  }
16076
- const before = await stat6(absolute);
16440
+ const before = await stat7(absolute);
16077
16441
  const beforeCanonical = canonical;
16078
- const after = await stat6(absolute);
16442
+ const after = await stat7(absolute);
16079
16443
  const afterCanonical = await realpath7(absolute);
16080
16444
  if (!before.isFile() || !after.isFile()) {
16081
16445
  return {
@@ -17029,7 +17393,7 @@ async function judgeSemanticCandidateBatches(runner, query, batches, signal) {
17029
17393
 
17030
17394
  // src/semantic-judge.ts
17031
17395
  var MAX_CANDIDATE_CHARS = 4000;
17032
- var MAX_RESPONSE_BYTES2 = 4 * 1024 * 1024;
17396
+ var MAX_RESPONSE_BYTES3 = 4 * 1024 * 1024;
17033
17397
  var MAX_ERROR_CHARS = 512;
17034
17398
  var CLASSIFICATION_PRIORITY = {
17035
17399
  "implementation-candidate": 0,
@@ -17119,14 +17483,22 @@ function requestBody(query, candidates, model) {
17119
17483
  questions
17120
17484
  };
17121
17485
  }
17486
+ function httpJsonRequestBody(query, candidates, model) {
17487
+ return {
17488
+ version: 1,
17489
+ model,
17490
+ query,
17491
+ candidates: candidates.map(({ id, excerpt }) => ({ id, excerpt }))
17492
+ };
17493
+ }
17122
17494
  async function responseText(response) {
17123
17495
  if (!response.body) {
17124
17496
  const contentLength = response.headers.get("content-length");
17125
- if (contentLength !== null && Number(contentLength) > MAX_RESPONSE_BYTES2) {
17497
+ if (contentLength !== null && Number(contentLength) > MAX_RESPONSE_BYTES3) {
17126
17498
  throw new SemanticJudgeRequestError("Semantic judge response exceeded the 4 MiB limit", false);
17127
17499
  }
17128
17500
  const text = await response.text();
17129
- if (new TextEncoder().encode(text).byteLength > MAX_RESPONSE_BYTES2) {
17501
+ if (new TextEncoder().encode(text).byteLength > MAX_RESPONSE_BYTES3) {
17130
17502
  throw new SemanticJudgeRequestError("Semantic judge response exceeded the 4 MiB limit", false);
17131
17503
  }
17132
17504
  return text;
@@ -17141,7 +17513,7 @@ async function responseText(response) {
17141
17513
  break;
17142
17514
  const chunk = result.value;
17143
17515
  bytes += chunk.byteLength;
17144
- if (bytes > MAX_RESPONSE_BYTES2) {
17516
+ if (bytes > MAX_RESPONSE_BYTES3) {
17145
17517
  await reader.cancel();
17146
17518
  throw new SemanticJudgeRequestError("Semantic judge response exceeded the 4 MiB limit", false);
17147
17519
  }
@@ -17158,10 +17530,10 @@ async function responseText(response) {
17158
17530
  }
17159
17531
  return new TextDecoder("utf-8", { fatal: true }).decode(all);
17160
17532
  }
17161
- function retryDelay(attempt) {
17533
+ function retryDelay2(attempt) {
17162
17534
  return Math.min(20000, 400 * 2 ** attempt);
17163
17535
  }
17164
- async function waitForRetry(delayMs, signal) {
17536
+ async function waitForRetry2(delayMs, signal) {
17165
17537
  if (signal?.aborted)
17166
17538
  throw signal.reason instanceof Error ? signal.reason : new Error("Operation aborted");
17167
17539
  await new Promise((resolve, reject) => {
@@ -17179,68 +17551,127 @@ async function waitForRetry(delayMs, signal) {
17179
17551
  signal?.addEventListener("abort", onAbort, { once: true });
17180
17552
  });
17181
17553
  }
17554
+ async function fetchSemanticResponse(config, key, fetcher, body2, parentSignal) {
17555
+ if (new TextEncoder().encode(body2).byteLength > MAX_SEMANTIC_JUDGE_REQUEST_BYTES) {
17556
+ throw new SemanticJudgePayloadTooLargeError("Semantic judge request exceeded the 64 KiB request limit");
17557
+ }
17558
+ for (let attempt = 0;attempt <= config.maxRetries; attempt += 1) {
17559
+ const controller = new AbortController;
17560
+ const onParentAbort = () => controller.abort(parentSignal?.reason);
17561
+ parentSignal?.addEventListener("abort", onParentAbort, { once: true });
17562
+ const timeout = setTimeout(() => controller.abort(new Error("Semantic judge request timed out")), config.timeoutMs);
17563
+ try {
17564
+ const headers = { "content-type": "application/json" };
17565
+ if (key)
17566
+ headers.authorization = `Bearer ${key}`;
17567
+ const response = await fetcher(config.endpoint, {
17568
+ method: "POST",
17569
+ headers,
17570
+ body: body2,
17571
+ signal: controller.signal
17572
+ });
17573
+ if (!response.ok) {
17574
+ if (response.status === 413) {
17575
+ throw new SemanticJudgePayloadTooLargeError("Semantic judge rejected an oversized request with HTTP 413");
17576
+ }
17577
+ const retryable = response.status === 408 || response.status === 429 || response.status === 529 || response.status >= 500;
17578
+ throw new SemanticJudgeRequestError(`Semantic judge returned HTTP ${String(response.status)}`, retryable);
17579
+ }
17580
+ const raw = JSON.parse(await responseText(response));
17581
+ if (!isRecord3(raw)) {
17582
+ throw new SemanticJudgeRequestError("Semantic judge response must be an object", false);
17583
+ }
17584
+ return raw;
17585
+ } catch (error) {
17586
+ if (parentSignal?.aborted)
17587
+ throw error;
17588
+ const retryable = error instanceof SemanticJudgePayloadTooLargeError ? false : error instanceof SemanticJudgeRequestError ? error.retryable : true;
17589
+ if (!retryable || attempt >= config.maxRetries)
17590
+ throw error;
17591
+ await waitForRetry2(retryDelay2(attempt), parentSignal);
17592
+ } finally {
17593
+ clearTimeout(timeout);
17594
+ parentSignal?.removeEventListener("abort", onParentAbort);
17595
+ }
17596
+ }
17597
+ throw new SemanticJudgeRequestError("Semantic judge request failed", false);
17598
+ }
17599
+ function responseModel(raw, config) {
17600
+ const model = raw.model ?? config.model;
17601
+ if (typeof model !== "string" || model.length === 0 || model.length > 128 || /[\r\n\0]/u.test(model)) {
17602
+ throw new SemanticJudgeRequestError("Semantic judge response model must be bounded single-line text", false);
17603
+ }
17604
+ return model;
17605
+ }
17606
+ function responseUsage(raw) {
17607
+ const usage = isRecord3(raw.usage) ? raw.usage : undefined;
17608
+ const inputTokens = typeof usage?.input_tokens === "number" && Number.isSafeInteger(usage.input_tokens) && usage.input_tokens >= 0 ? usage.input_tokens : undefined;
17609
+ const outputTokens = typeof usage?.output_tokens === "number" && Number.isSafeInteger(usage.output_tokens) && usage.output_tokens >= 0 ? usage.output_tokens : undefined;
17610
+ return {
17611
+ ...inputTokens === undefined ? {} : { inputTokens },
17612
+ ...outputTokens === undefined ? {} : { outputTokens }
17613
+ };
17614
+ }
17615
+ function parseHttpJsonResult(raw, candidates, config, elapsedMs) {
17616
+ if (raw.version !== 1 || !Array.isArray(raw.judgments)) {
17617
+ throw new SemanticJudgeRequestError("HTTP JSON semantic judge response must contain version 1 judgments", false);
17618
+ }
17619
+ const candidateIndexes = new Map(candidates.map((candidate, index) => [candidate.id, index]));
17620
+ const judgments = new Map;
17621
+ for (const value of raw.judgments) {
17622
+ if (!isRecord3(value) || typeof value.id !== "string") {
17623
+ throw new SemanticJudgeRequestError("HTTP JSON semantic judge judgment has an invalid id", false);
17624
+ }
17625
+ const candidateIndex = candidateIndexes.get(value.id);
17626
+ if (candidateIndex === undefined || judgments.has(candidateIndex)) {
17627
+ throw new SemanticJudgeRequestError("HTTP JSON semantic judge response contains an unknown or duplicate candidate", false);
17628
+ }
17629
+ if (!isClassification(value.choice)) {
17630
+ throw new SemanticJudgeRequestError(`HTTP JSON semantic judge response has an invalid choice for ${value.id}`, false);
17631
+ }
17632
+ const confidence = value.confidence === undefined ? undefined : probability(value.confidence, "confidence");
17633
+ judgments.set(candidateIndex, {
17634
+ candidateIndex,
17635
+ classification: value.choice,
17636
+ probability: probability(value.probability, "probability"),
17637
+ ...confidence === undefined ? {} : { confidence }
17638
+ });
17639
+ }
17640
+ if (judgments.size !== candidates.length) {
17641
+ throw new SemanticJudgeRequestError(`HTTP JSON semantic judge response judged ${String(judgments.size)} of ${String(candidates.length)} candidates`, false);
17642
+ }
17643
+ const usage = responseUsage(raw);
17644
+ return {
17645
+ model: responseModel(raw, config),
17646
+ judgments: candidates.map((_candidate, index) => judgments.get(index)),
17647
+ ...usage,
17648
+ elapsedMs
17649
+ };
17650
+ }
17182
17651
  function createJevRunner(config, key, fetcher) {
17183
17652
  return async (query, candidates, parentSignal) => {
17184
17653
  const startedAt = performance.now();
17185
17654
  const body2 = JSON.stringify(requestBody(query, candidates, config.model));
17186
- if (new TextEncoder().encode(body2).byteLength > MAX_SEMANTIC_JUDGE_REQUEST_BYTES) {
17187
- throw new SemanticJudgePayloadTooLargeError("Semantic judge request exceeded the 64 KiB request limit");
17188
- }
17189
- for (let attempt = 0;attempt <= config.maxRetries; attempt += 1) {
17190
- const controller = new AbortController;
17191
- const onParentAbort = () => controller.abort(parentSignal?.reason);
17192
- parentSignal?.addEventListener("abort", onParentAbort, { once: true });
17193
- const timeout = setTimeout(() => controller.abort(new Error("Semantic judge request timed out")), config.timeoutMs);
17194
- try {
17195
- const response = await fetcher(config.endpoint, {
17196
- method: "POST",
17197
- headers: {
17198
- "content-type": "application/json",
17199
- authorization: `Bearer ${key}`
17200
- },
17201
- body: body2,
17202
- signal: controller.signal
17203
- });
17204
- if (!response.ok) {
17205
- if (response.status === 413) {
17206
- throw new SemanticJudgePayloadTooLargeError("Semantic judge rejected an oversized request with HTTP 413");
17207
- }
17208
- const retryable = response.status === 408 || response.status === 429 || response.status === 529 || response.status >= 500;
17209
- throw new SemanticJudgeRequestError(`Semantic judge returned HTTP ${String(response.status)}`, retryable);
17210
- }
17211
- const raw = JSON.parse(await responseText(response));
17212
- if (!isRecord3(raw) || !isRecord3(raw.answers)) {
17213
- throw new SemanticJudgeRequestError("Semantic judge response did not contain answers", false);
17214
- }
17215
- const answers = raw.answers;
17216
- const judgments = candidates.map((candidate, index) => parseJudgment(answers[candidate.id], index));
17217
- const model = raw.model ?? config.model;
17218
- if (typeof model !== "string" || model.length === 0 || model.length > 128 || /[\r\n\0]/u.test(model)) {
17219
- throw new SemanticJudgeRequestError("Semantic judge response model must be bounded single-line text", false);
17220
- }
17221
- const usage = isRecord3(raw.usage) ? raw.usage : undefined;
17222
- const inputTokens = typeof usage?.input_tokens === "number" && Number.isSafeInteger(usage.input_tokens) && usage.input_tokens >= 0 ? usage.input_tokens : undefined;
17223
- const outputTokens = typeof usage?.output_tokens === "number" && Number.isSafeInteger(usage.output_tokens) && usage.output_tokens >= 0 ? usage.output_tokens : undefined;
17224
- return {
17225
- model,
17226
- judgments,
17227
- ...inputTokens === undefined ? {} : { inputTokens },
17228
- ...outputTokens === undefined ? {} : { outputTokens },
17229
- elapsedMs: Math.round(performance.now() - startedAt)
17230
- };
17231
- } catch (error) {
17232
- if (parentSignal?.aborted)
17233
- throw error;
17234
- const retryable = error instanceof SemanticJudgePayloadTooLargeError ? false : error instanceof SemanticJudgeRequestError ? error.retryable : true;
17235
- if (!retryable || attempt >= config.maxRetries)
17236
- throw error;
17237
- await waitForRetry(retryDelay(attempt), parentSignal);
17238
- } finally {
17239
- clearTimeout(timeout);
17240
- parentSignal?.removeEventListener("abort", onParentAbort);
17241
- }
17655
+ const raw = await fetchSemanticResponse(config, key, fetcher, body2, parentSignal);
17656
+ if (!isRecord3(raw.answers)) {
17657
+ throw new SemanticJudgeRequestError("Semantic judge response did not contain answers", false);
17242
17658
  }
17243
- throw new SemanticJudgeRequestError("Semantic judge request failed", false);
17659
+ const answers = raw.answers;
17660
+ const judgments = candidates.map((candidate, index) => parseJudgment(answers[candidate.id], index));
17661
+ const usage = responseUsage(raw);
17662
+ return {
17663
+ model: responseModel(raw, config),
17664
+ judgments,
17665
+ ...usage,
17666
+ elapsedMs: Math.round(performance.now() - startedAt)
17667
+ };
17668
+ };
17669
+ }
17670
+ function createHttpJsonRunner(config, key, fetcher) {
17671
+ return async (query, candidates, parentSignal) => {
17672
+ const startedAt = performance.now();
17673
+ const raw = await fetchSemanticResponse(config, key, fetcher, JSON.stringify(httpJsonRequestBody(query, candidates, config.model)), parentSignal);
17674
+ return parseHttpJsonResult(raw, candidates, config, Math.round(performance.now() - startedAt));
17244
17675
  };
17245
17676
  }
17246
17677
  function createDisabledSemanticJudgeIntegration(config) {
@@ -17249,14 +17680,16 @@ function createDisabledSemanticJudgeIntegration(config) {
17249
17680
  function createSemanticJudgeIntegration(config, environment = process.env, fetcher = fetch) {
17250
17681
  if (!config.enabled)
17251
17682
  return createDisabledSemanticJudgeIntegration(config);
17252
- const key = environment[config.apiKeyEnv]?.trim();
17253
- if (!key) {
17254
- throw new SemanticJudgeConfigurationError(`Semantic judge is enabled, but environment variable ${config.apiKeyEnv} is missing or empty`);
17683
+ const key = config.apiKeyEnv ? environment[config.apiKeyEnv]?.trim() : undefined;
17684
+ if (config.provider === "jev" && !key) {
17685
+ throw new SemanticJudgeConfigurationError(`Semantic judge is enabled, but environment variable ${config.apiKeyEnv ?? "TYPESAFE_API_KEY"} is missing or empty`);
17255
17686
  }
17256
- if (config.provider !== "jev") {
17257
- throw new SemanticJudgeConfigurationError(`Unsupported semantic judge provider: ${String(config.provider)}`);
17687
+ if (config.provider === "http-json" && config.apiKeyEnv && !key) {
17688
+ throw new SemanticJudgeConfigurationError(`Semantic judge is enabled, but environment variable ${config.apiKeyEnv} is missing or empty`);
17258
17689
  }
17259
- return { config, runner: createJevRunner(config, key, fetcher) };
17690
+ if (config.provider === "jev")
17691
+ return { config, runner: createJevRunner(config, key, fetcher) };
17692
+ return { config, runner: createHttpJsonRunner(config, key, fetcher) };
17260
17693
  }
17261
17694
  function createConfiguredSemanticJudgeIntegration(config, environment = process.env) {
17262
17695
  const judge = config.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG;
@@ -19078,7 +19511,7 @@ class EvidenceService {
19078
19511
  throw new SiftLightError(`No outline provider is registered for ${document2.path}; use mode=capabilities to inspect available language modes`);
19079
19512
  }
19080
19513
  if (!language && !isPython || language === "go") {
19081
- const extension = extname4(document2.path) || "extensionless source";
19514
+ const extension = extname5(document2.path) || "extensionless source";
19082
19515
  throw new SiftLightError(`${input.mode} is unavailable for ${extension}; choose a language capability from mode=capabilities or use ordinary content search`);
19083
19516
  }
19084
19517
  if (input.mode === "outline") {
@@ -19271,7 +19704,7 @@ function normalizeRequestAliases(request) {
19271
19704
  }
19272
19705
 
19273
19706
  // src/format.ts
19274
- import { readFile as readFile5 } from "fs/promises";
19707
+ import { readFile as readFile6 } from "fs/promises";
19275
19708
  var RESULT_METADATA_RESERVE_BYTES = 1024;
19276
19709
  var RESULT_METADATA_RESERVE_CHARACTERS = 512;
19277
19710
 
@@ -19405,7 +19838,7 @@ async function loadContextLines(match, expectedRevision, cache, signal) {
19405
19838
  cache.set(match.absolutePath, changed);
19406
19839
  return changed;
19407
19840
  }
19408
- const content = await readFile5(match.absolutePath, { encoding: "utf8", signal });
19841
+ const content = await readFile6(match.absolutePath, { encoding: "utf8", signal });
19409
19842
  const afterRevision = await getSourceRevision(match.absolutePath);
19410
19843
  if (!afterRevision || !sameSourceRevision(expectedRevision, afterRevision)) {
19411
19844
  const changed = { status: "changed" };
@@ -19730,7 +20163,7 @@ ${lineRows.join(`
19730
20163
  }
19731
20164
 
19732
20165
  // src/snapshot-store.ts
19733
- import { randomUUID as randomUUID7 } from "crypto";
20166
+ import { randomUUID as randomUUID8 } from "crypto";
19734
20167
  class SnapshotStore {
19735
20168
  #snapshots = new Map;
19736
20169
  #expired = new Set;
@@ -19755,7 +20188,7 @@ class SnapshotStore {
19755
20188
  const now = this.#now();
19756
20189
  const snapshot = {
19757
20190
  ...scan,
19758
- id: randomUUID7(),
20191
+ id: randomUUID8(),
19759
20192
  createdAt: now,
19760
20193
  lastAccessedAt: now
19761
20194
  };
@@ -20525,7 +20958,7 @@ class SiftLightService {
20525
20958
  }
20526
20959
  mediaSearch(value, cwd, signal) {
20527
20960
  if (!this.#media)
20528
- throw new SiftLightError("Visual search is disabled; configure nativeMedia and restart the host");
20961
+ throw new SiftLightError("Visual search is disabled; configure nativeMedia or media and restart the host");
20529
20962
  return this.#media.search(value, cwd, signal);
20530
20963
  }
20531
20964
  #dataSources;
@@ -20541,7 +20974,8 @@ class SiftLightService {
20541
20974
  #operations;
20542
20975
  #reusableSummarySnapshots = new WeakSet;
20543
20976
  constructor(options) {
20544
- this.#media = options.nativeMedia ? new MediaSearch(options.nativeMedia) : undefined;
20977
+ const mediaConfig = options.nativeMedia ?? options.media;
20978
+ this.#media = mediaConfig ? new MediaSearch(mediaConfig) : undefined;
20545
20979
  this.#dataSources = new DataSources(options.dataSources);
20546
20980
  this.#runRipgrep = options.runRipgrep;
20547
20981
  this.#snapshots = options.snapshots ?? new SnapshotStore;