unbrowse 2.12.2 → 2.12.7

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.
Files changed (64) hide show
  1. package/README.md +8 -44
  2. package/dist/cli.js +514 -20723
  3. package/package.json +4 -10
  4. package/runtime-src/api/routes.ts +15 -801
  5. package/runtime-src/auth/index.ts +32 -142
  6. package/runtime-src/capture/index.ts +101 -436
  7. package/runtime-src/cli.ts +371 -956
  8. package/runtime-src/client/index.ts +29 -622
  9. package/runtime-src/execution/index.ts +85 -345
  10. package/runtime-src/graph/index.ts +10 -128
  11. package/runtime-src/intent-match.ts +27 -27
  12. package/runtime-src/kuri/client.ts +82 -543
  13. package/runtime-src/orchestrator/index.ts +462 -2246
  14. package/runtime-src/reverse-engineer/index.ts +22 -220
  15. package/runtime-src/runtime/local-server.ts +16 -149
  16. package/runtime-src/runtime/paths.ts +5 -9
  17. package/runtime-src/runtime/setup.ts +1 -52
  18. package/runtime-src/server.ts +11 -6
  19. package/runtime-src/transform/schema-hints.ts +358 -0
  20. package/runtime-src/types/skill.ts +2 -49
  21. package/runtime-src/verification/index.ts +0 -15
  22. package/runtime-src/version.ts +13 -13
  23. package/vendor/kuri/darwin-arm64/kuri +0 -0
  24. package/vendor/kuri/darwin-x64/kuri +0 -0
  25. package/vendor/kuri/linux-arm64/kuri +0 -0
  26. package/vendor/kuri/linux-x64/kuri +0 -0
  27. package/bin/unbrowse-wrapper.mjs +0 -39
  28. package/bin/unbrowse.js +0 -38
  29. package/runtime-src/analytics-session.ts +0 -33
  30. package/runtime-src/api/browse-index.ts +0 -254
  31. package/runtime-src/api/browse-session.ts +0 -179
  32. package/runtime-src/api/browse-submit.ts +0 -455
  33. package/runtime-src/auth/runtime.ts +0 -116
  34. package/runtime-src/browser/index.ts +0 -635
  35. package/runtime-src/browser/types.ts +0 -41
  36. package/runtime-src/capture/prefetch.ts +0 -122
  37. package/runtime-src/capture/rsc.ts +0 -45
  38. package/runtime-src/cli/shortcuts.ts +0 -273
  39. package/runtime-src/client/graph-client.ts +0 -99
  40. package/runtime-src/execution/robots.ts +0 -167
  41. package/runtime-src/execution/search-forms.ts +0 -188
  42. package/runtime-src/graph/planner.ts +0 -411
  43. package/runtime-src/graph/session.ts +0 -294
  44. package/runtime-src/graph/trace-store.ts +0 -136
  45. package/runtime-src/indexer/index.ts +0 -480
  46. package/runtime-src/orchestrator/browser-agent.ts +0 -374
  47. package/runtime-src/orchestrator/dag-advisor.ts +0 -59
  48. package/runtime-src/orchestrator/dag-feedback.ts +0 -256
  49. package/runtime-src/orchestrator/first-pass-action.ts +0 -362
  50. package/runtime-src/orchestrator/passive-publish.ts +0 -152
  51. package/runtime-src/orchestrator/timing-economics.ts +0 -80
  52. package/runtime-src/payments/cascade.ts +0 -137
  53. package/runtime-src/payments/index.ts +0 -268
  54. package/runtime-src/payments/wallet.ts +0 -33
  55. package/runtime-src/reverse-engineer/description-prompt.ts +0 -132
  56. package/runtime-src/router.ts +0 -17
  57. package/runtime-src/runtime/browser-access.ts +0 -11
  58. package/runtime-src/runtime/browser-host.ts +0 -48
  59. package/runtime-src/runtime/lifecycle.ts +0 -17
  60. package/runtime-src/runtime/supervisor.ts +0 -69
  61. package/runtime-src/single-binary.ts +0 -141
  62. package/runtime-src/telemetry.ts +0 -253
  63. package/runtime-src/verification/matrix.ts +0 -30
  64. package/scripts/postinstall.mjs +0 -81
@@ -1,480 +0,0 @@
1
- import { buildSkillOperationGraph } from "../graph/index.js";
2
- import { validateManifest, publishSkill, cachePublishedSkill, publishGraphEdges } from "../client/index.js";
3
- import { mergeEndpoints } from "../marketplace/index.js";
4
- import {
5
- writeSkillSnapshot,
6
- domainSkillCache,
7
- persistDomainCache,
8
- getDomainReuseKey,
9
- scopedCacheKey,
10
- snapshotPathForCacheKey,
11
- generateLocalDescription,
12
- } from "../orchestrator/index.js";
13
- import { getRegistrableDomain } from "../domain.js";
14
- import type { SkillManifest, EndpointDescriptor } from "../types/index.js";
15
- import { existsSync, readdirSync, readFileSync } from "node:fs";
16
- import { join } from "node:path";
17
- import { homedir } from "node:os";
18
-
19
- const UNBROWSE_CONFIG_PATH = join(homedir(), ".unbrowse", "config.json");
20
- const SKILL_SNAPSHOT_DIR = process.env.UNBROWSE_SKILL_SNAPSHOT_DIR
21
- ?? join(process.env.HOME ?? "/tmp", ".unbrowse", "skill-snapshots");
22
-
23
- /** Read agent_id from local config — used for contributor attribution on publish. */
24
- function getLocalAgentId(): string | undefined {
25
- try {
26
- const config = JSON.parse(readFileSync(UNBROWSE_CONFIG_PATH, "utf-8"));
27
- return config.agent_id ?? undefined;
28
- } catch {
29
- return undefined;
30
- }
31
- }
32
- /**
33
- * Strip PII and user-specific data from endpoints before publishing to marketplace.
34
- * Keeps: URL templates, method, schema structure, semantic metadata (action/resource kinds,
35
- * requires/provides, field paths, descriptions).
36
- * Strips: example response data, actual query values, sample URLs with query params,
37
- * request bodies, header values.
38
- */
39
- /**
40
- * Strip PII and user-specific data from endpoints before publishing to marketplace.
41
- * Deterministic baseline — the agent sanitizer builds on top of this.
42
- */
43
- // Patterns that identify secret/token values regardless of field name
44
- const SECRET_VALUE_PATTERNS = [
45
- /^eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/, // JWT tokens
46
- /^Bearer\s+\S+/i, // Bearer tokens
47
- /^Basic\s+[A-Za-z0-9+/=]+/i, // Basic auth
48
- /^ghp_[A-Za-z0-9]{36}/, // GitHub PAT
49
- /^sk-[A-Za-z0-9]{20,}/, // OpenAI/Stripe secret keys
50
- /^pk_(live|test)_[A-Za-z0-9]+/, // Stripe publishable keys
51
- /^xox[bsrp]-[A-Za-z0-9-]+/, // Slack tokens
52
- /^AKIA[A-Z0-9]{16}/, // AWS access key
53
- /^[A-Za-z0-9+/]{40,}={0,2}$/, // Base64 encoded secrets (long)
54
- /^v2\.[A-Za-z0-9_-]{20,}/, // Various API v2 tokens
55
- ];
56
-
57
- // Field name patterns that indicate the value is a secret
58
- const SECRET_KEY_PATTERNS = /^(api[_-]?key|access[_-]?token|auth[_-]?token|secret[_-]?key|private[_-]?key|password|passwd|session[_-]?id|session[_-]?token|csrf[_-]?token|client[_-]?secret|bearer|refresh[_-]?token|id[_-]?token|jwt|nonce|otp|pin|ssn|credit[_-]?card)$/i;
59
-
60
- /**
61
- * Returns true if a value looks like a secret/token/credential.
62
- */
63
- export function looksLikeSecret(key: string, value: unknown): boolean {
64
- if (typeof value !== "string" || value.length < 8) return false;
65
- if (SECRET_KEY_PATTERNS.test(key)) return true;
66
- return SECRET_VALUE_PATTERNS.some((pattern) => pattern.test(value));
67
- }
68
-
69
- /**
70
- * Recursively walk an object and replace secret-looking values with "[REDACTED]".
71
- * Returns a new object — does not mutate the input.
72
- */
73
- export function redactSecrets(obj: unknown, parentKey = ""): unknown {
74
- if (obj === null || obj === undefined) return obj;
75
- if (typeof obj === "string") {
76
- return looksLikeSecret(parentKey, obj) ? "[REDACTED]" : obj;
77
- }
78
- if (Array.isArray(obj)) {
79
- return obj.map((item, i) => redactSecrets(item, parentKey));
80
- }
81
- if (typeof obj === "object") {
82
- const result: Record<string, unknown> = {};
83
- for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
84
- result[k] = redactSecrets(v, k);
85
- }
86
- return result;
87
- }
88
- return obj;
89
- }
90
-
91
- /**
92
- * Strip PII and user-specific data from endpoints before publishing to marketplace.
93
- * Deterministic baseline — the agent sanitizer builds on top of this.
94
- *
95
- * Layer 1: Redact secrets (tokens, keys, JWTs) from ALL string values
96
- * Layer 2: Strip example responses, query defaults, sample URLs
97
- */
98
- /**
99
- * Synthesize a plausible placeholder value for a given real value.
100
- * Preserves type and rough shape so agents can understand the endpoint.
101
- */
102
- function synthesizePlaceholder(key: string, value: unknown): unknown {
103
- if (value === null || value === undefined) return value;
104
- if (typeof value === "number") return Number.isInteger(value) ? 12345 : 99.99;
105
- if (typeof value === "boolean") return value;
106
- if (typeof value === "string") {
107
- if (looksLikeSecret(key, value)) return "[REDACTED]";
108
- // Email-like
109
- if (/@/.test(value)) return "user@example.com";
110
- // URL-like
111
- if (/^https?:\/\//.test(value)) return "https://example.com/item/123";
112
- // UUID-like
113
- if (/^[0-9a-f]{8}-[0-9a-f]{4}/.test(value)) return "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
114
- // Numeric string
115
- if (/^\d+$/.test(value)) return "12345";
116
- // Date-like
117
- if (/^\d{4}-\d{2}-\d{2}/.test(value)) return "2026-01-15T00:00:00Z";
118
- // Short identifier
119
- if (value.length <= 8) return "abc123";
120
- // General text — return a generic of similar length
121
- if (value.length > 100) return "Example description text for this item.";
122
- return "example-value";
123
- }
124
- if (Array.isArray(value)) {
125
- return value.length > 0 ? [synthesizeExample(value[0], 0)] : [];
126
- }
127
- if (typeof value === "object") {
128
- return synthesizeExample(value, 0);
129
- }
130
- return value;
131
- }
132
-
133
- /**
134
- * Recursively synthesize a structurally similar example with placeholder values.
135
- * Keeps keys and types, replaces actual data with generic equivalents.
136
- */
137
- export function synthesizeExample(obj: unknown, depth = 0): unknown {
138
- if (depth > 5) return null; // prevent infinite recursion
139
- if (obj === null || obj === undefined) return obj;
140
- if (typeof obj !== "object") return synthesizePlaceholder("", obj);
141
- if (Array.isArray(obj)) {
142
- // Keep at most 2 items to show the shape
143
- return obj.slice(0, 2).map((item) => synthesizeExample(item, depth + 1));
144
- }
145
- const result: Record<string, unknown> = {};
146
- for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
147
- result[k] = typeof v === "object" && v !== null
148
- ? synthesizeExample(v, depth + 1)
149
- : synthesizePlaceholder(k, v);
150
- }
151
- return result;
152
- }
153
-
154
- /**
155
- * Strip PII and user-specific data from endpoints before publishing to marketplace.
156
- * Replaces real data with structurally similar synthetic examples so agents
157
- * can still understand the endpoint shape.
158
- *
159
- * Layer 1: Redact secrets (tokens, keys, JWTs) from ALL string values
160
- * Layer 2: Synthesize placeholder examples, sanitize query defaults
161
- */
162
- export function sanitizeForPublish(endpoints: EndpointDescriptor[]): EndpointDescriptor[] {
163
- return endpoints.map((ep) => {
164
- const clean = { ...ep };
165
-
166
- // Layer 1: Redact any secret values in headers, query, body
167
- if (clean.headers_template) {
168
- clean.headers_template = redactSecrets(clean.headers_template) as Record<string, string>;
169
- }
170
- if (clean.query) {
171
- clean.query = redactSecrets(clean.query) as Record<string, unknown>;
172
- }
173
- if (clean.body) {
174
- clean.body = redactSecrets(clean.body) as Record<string, unknown>;
175
- }
176
-
177
- // Layer 2: Replace query defaults with generic placeholders
178
- if (clean.query) {
179
- clean.query = Object.fromEntries(
180
- Object.entries(clean.query).map(([k, v]) => [k, typeof v === "string" ? "example" : v]),
181
- );
182
- }
183
-
184
- // Replace path_params with generic placeholders
185
- if (clean.path_params) {
186
- clean.path_params = Object.fromEntries(
187
- Object.keys(clean.path_params).map((k) => [k, "example"]),
188
- );
189
- }
190
-
191
- // Synthesize body example (keep structure, replace values)
192
- if (clean.body) clean.body = synthesizeExample(clean.body) as Record<string, unknown>;
193
- if (clean.body_params) clean.body_params = synthesizeExample(clean.body_params) as Record<string, unknown>;
194
-
195
- // Strip header values — keep keys only (headers are not useful as examples)
196
- if (clean.headers_template) {
197
- clean.headers_template = Object.fromEntries(
198
- Object.keys(clean.headers_template).map((k) => [k, ""]),
199
- );
200
- }
201
-
202
- // Strip trigger_url query params (keep origin + path)
203
- if (clean.trigger_url) {
204
- try {
205
- const u = new URL(clean.trigger_url);
206
- clean.trigger_url = u.origin + u.pathname;
207
- } catch { /* keep as-is if unparseable */ }
208
- }
209
-
210
- // Sanitize semantic descriptor — synthesize examples instead of deleting
211
- if (clean.semantic) {
212
- const sem = { ...clean.semantic };
213
-
214
- // Synthesize structurally similar examples
215
- if (sem.example_response_compact) {
216
- sem.example_response_compact = synthesizeExample(sem.example_response_compact);
217
- }
218
- if (sem.example_request) {
219
- sem.example_request = synthesizeExample(sem.example_request);
220
- }
221
-
222
- // Replace sample URL query params with placeholders
223
- if (sem.sample_request_url) {
224
- try {
225
- const u = new URL(sem.sample_request_url);
226
- for (const key of u.searchParams.keys()) {
227
- u.searchParams.set(key, "example");
228
- }
229
- sem.sample_request_url = u.toString();
230
- } catch { delete sem.sample_request_url; }
231
- }
232
-
233
- // Strip example_value from bindings (these are real captured values)
234
- if (sem.requires) {
235
- sem.requires = sem.requires.map((b) => {
236
- const { example_value: _, ...rest } = b;
237
- return rest;
238
- });
239
- }
240
- if (sem.provides) {
241
- sem.provides = sem.provides.map((b) => {
242
- const { example_value: _, ...rest } = b;
243
- return rest;
244
- });
245
- }
246
-
247
- clean.semantic = sem;
248
- }
249
-
250
- return clean;
251
- });
252
- }
253
-
254
- /**
255
- * Merge agent-reviewed endpoint metadata into sanitized endpoints.
256
- * Called by the /v1/skills/:id/review route when an agent submits
257
- * reviewed descriptions and synthetic examples for a skill's endpoints.
258
- */
259
- export function mergeAgentReview(
260
- endpoints: EndpointDescriptor[],
261
- reviews: Array<{
262
- endpoint_id: string;
263
- description?: string;
264
- action_kind?: string;
265
- resource_kind?: string;
266
- example_request?: unknown;
267
- example_response?: unknown;
268
- }>,
269
- ): EndpointDescriptor[] {
270
- const reviewMap = new Map(reviews.map((r) => [r.endpoint_id, r]));
271
- return endpoints.map((ep) => {
272
- const reviewed = reviewMap.get(ep.endpoint_id);
273
- if (!reviewed) return ep;
274
- return {
275
- ...ep,
276
- description: reviewed.description || ep.description,
277
- semantic: ep.semantic ? {
278
- ...ep.semantic,
279
- action_kind: reviewed.action_kind || ep.semantic.action_kind,
280
- resource_kind: reviewed.resource_kind || ep.semantic.resource_kind,
281
- description_out: reviewed.description || ep.semantic.description_out,
282
- ...(reviewed.example_response ? { example_response_compact: reviewed.example_response } : {}),
283
- ...(reviewed.example_request ? { example_request: reviewed.example_request } : {}),
284
- } : ep.semantic,
285
- };
286
- });
287
- }
288
- /**
289
- * Find existing domain snapshots and merge incoming endpoints into them.
290
- * Returns a merged skill with all endpoints from both existing snapshots
291
- * and the incoming skill, or null if no existing snapshot found.
292
- */
293
- export function findAndMergeDomainSnapshot(
294
- snapshotDir: string,
295
- domain: string,
296
- incoming: SkillManifest,
297
- ): SkillManifest | null {
298
- if (!existsSync(snapshotDir)) return null;
299
- const targetDomain = getRegistrableDomain(domain);
300
-
301
- let bestExisting: SkillManifest | null = null;
302
- let bestEndpointCount = 0;
303
-
304
- for (const entry of readdirSync(snapshotDir)) {
305
- if (!entry.endsWith(".json")) continue;
306
- try {
307
- const candidate = JSON.parse(readFileSync(join(snapshotDir, entry), "utf-8")) as SkillManifest;
308
- if (getRegistrableDomain(candidate.domain) !== targetDomain) continue;
309
- if (candidate.execution_type !== "http") continue;
310
- const epCount = candidate.endpoints?.length ?? 0;
311
- if (epCount > bestEndpointCount) {
312
- bestExisting = candidate;
313
- bestEndpointCount = epCount;
314
- }
315
- } catch { /* skip corrupt */ }
316
- }
317
-
318
- if (!bestExisting) return null;
319
-
320
- const merged = mergeEndpoints(bestExisting.endpoints, incoming.endpoints);
321
- if (merged.length <= bestEndpointCount) return null; // no new endpoints to add
322
-
323
- return {
324
- ...bestExisting,
325
- endpoints: merged,
326
- intents: Array.from(new Set([
327
- ...(bestExisting.intents ?? []),
328
- ...(incoming.intents ?? []),
329
- incoming.intent_signature,
330
- ])),
331
- updated_at: new Date().toISOString(),
332
- };
333
- }
334
- const indexInFlight = new Map<string, Promise<void>>();
335
-
336
- export interface BackgroundIndexJob {
337
- skill: SkillManifest;
338
- domain: string;
339
- intent: string;
340
- contextUrl?: string;
341
- clientScope?: string;
342
- cacheKey: string;
343
- }
344
-
345
- /**
346
- * Queue a skill for background processing: graph building, marketplace
347
- * validation, and publishing. Non-blocking — returns immediately.
348
- * Per-domain dedup: only one job per domain runs at a time.
349
- */
350
- export function queueBackgroundIndex(job: BackgroundIndexJob): void {
351
- const key = job.domain;
352
- if (indexInFlight.has(key)) {
353
- console.log(`[background-index] skipped for ${key}: already in flight`);
354
- return;
355
- }
356
-
357
- const work = processIndexJob(job)
358
- .catch(err =>
359
- console.error(`[background-index] failed for ${key}: ${(err as Error).message}`)
360
- )
361
- .finally(() => indexInFlight.delete(key));
362
-
363
- indexInFlight.set(key, work);
364
- console.log(`[background-index] queued for ${key}`);
365
- }
366
-
367
- async function processIndexJob(job: BackgroundIndexJob): Promise<void> {
368
- let { skill, domain, clientScope } = job;
369
- const scope = clientScope ?? "global";
370
- const scopedKey = scopedCacheKey(scope, job.cacheKey);
371
-
372
- // 0. Merge with existing domain snapshot (accumulate endpoints across captures)
373
- const merged = findAndMergeDomainSnapshot(SKILL_SNAPSHOT_DIR, domain, skill);
374
- if (merged) {
375
- console.log(`[background-index] merged ${skill.endpoints.length} new endpoint(s) into existing ${merged.endpoints.length - skill.endpoints.length} for ${domain}`);
376
- skill = merged;
377
- }
378
-
379
- // 1. Build operation graph from ALL accumulated endpoints
380
- skill.operation_graph = buildSkillOperationGraph(skill.endpoints);
381
-
382
- // 2. Generate local descriptions for BM25 ranking
383
- for (const ep of skill.endpoints) {
384
- if (!ep.description) {
385
- ep.description = generateLocalDescription(ep);
386
- }
387
- }
388
-
389
- // 3. Update local snapshot with merged skill + graph + descriptions
390
-
391
- // 4. Sanitize + validate + publish to marketplace (remote, ~1.5s total)
392
- const publishable = skill.endpoints.filter(ep => ep.method !== "WS");
393
- if (publishable.length === 0) {
394
- console.log(`[background-index] no publishable endpoints for ${domain}`);
395
- return;
396
- }
397
-
398
- // Deterministic PII sanitization — secrets redacted, values replaced with synthetic placeholders.
399
- // The calling agent can later POST to /v1/skills/:id/review with better descriptions and examples.
400
- const sanitized = sanitizeForPublish(publishable);
401
-
402
- const { operation_graph: _g, ...base } = skill;
403
- const draft: SkillManifest = { ...base, endpoints: sanitized, indexer_id: getLocalAgentId() };
404
- const validation = await validateManifest({ ...draft, skill_id: "__validate__" });
405
- if (!validation.valid) {
406
- console.warn(
407
- `[background-index] validation failed for ${domain}: ${validation.hardErrors.join("; ")}`
408
- );
409
- return;
410
- }
411
-
412
- const publishStart = Date.now();
413
- const published = await publishSkill(draft);
414
- const publishMs = Date.now() - publishStart;
415
- console.log(`[background-index] publish latency: ${publishMs}ms for ${domain}`);
416
-
417
- const publishedSkill: SkillManifest = {
418
- ...published,
419
- endpoints: skill.endpoints,
420
- operation_graph: skill.operation_graph,
421
- ...(skill.auth_profile_ref ? { auth_profile_ref: skill.auth_profile_ref } : {}),
422
- };
423
-
424
- // 5. Update caches with published version (has backend descriptions)
425
- cachePublishedSkill(publishedSkill, clientScope);
426
- writeSkillSnapshot(scopedKey, publishedSkill);
427
-
428
- // 6. Publish graph edges via dedicated endpoint (fire-and-forget)
429
- if (skill.operation_graph?.operations) {
430
- for (const op of skill.operation_graph.operations) {
431
- const opEdges = (skill.operation_graph.edges ?? [])
432
- .filter(e => e.from_operation_id === op.operation_id)
433
- .map(e => ({
434
- target_endpoint_id: skill.operation_graph!.operations.find(
435
- t => t.operation_id === e.to_operation_id
436
- )?.endpoint_id ?? e.to_operation_id,
437
- kind: e.kind,
438
- confidence: e.confidence,
439
- }));
440
- if (opEdges.length > 0) {
441
- publishGraphEdges(domain, {
442
- endpoint_id: op.endpoint_id,
443
- method: op.method,
444
- url_template: op.url_template,
445
- }, opEdges).catch(() => {});
446
- }
447
- }
448
- }
449
-
450
- // 7. Update domain cache so cross-intent reuse works
451
- const domainKey = getDomainReuseKey(job.contextUrl ?? domain);
452
- if (domainKey) {
453
- domainSkillCache.set(domainKey, {
454
- skillId: publishedSkill.skill_id,
455
- localSkillPath: snapshotPathForCacheKey(scopedKey),
456
- ts: Date.now(),
457
- });
458
- persistDomainCache();
459
- }
460
-
461
- console.log(`[background-index] completed for ${domain} -> ${published.skill_id}`);
462
- }
463
-
464
- /** Check if a domain has an indexing job running. */
465
- export function isIndexingInFlight(domain: string): boolean {
466
- return indexInFlight.has(domain);
467
- }
468
-
469
- /** Await all in-flight background index jobs. Call before process exit. */
470
- export async function drainPendingIndexJobs(): Promise<void> {
471
- const pending = [...indexInFlight.values()];
472
- if (pending.length === 0) return;
473
- console.log(`[background-index] draining ${pending.length} pending job(s)...`);
474
- await Promise.allSettled(pending);
475
- console.log(`[background-index] all jobs drained`);
476
- }
477
-
478
- export function resetIndexQueueForTests(): void {
479
- indexInFlight.clear();
480
- }