@agent-finops/core 0.8.1 → 0.9.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.
Files changed (44) hide show
  1. package/README.md +5 -3
  2. package/dist/actionPlanner.d.ts +140 -0
  3. package/dist/actionPlanner.js +938 -0
  4. package/dist/actionVerification.d.ts +1240 -0
  5. package/dist/actionVerification.js +1028 -0
  6. package/dist/activitySnapshot.d.ts +142 -50
  7. package/dist/activitySnapshot.js +145 -6
  8. package/dist/activitySnapshotCache.d.ts +8 -1
  9. package/dist/activitySnapshotCache.js +103 -7
  10. package/dist/agentEconomicsReceipt.d.ts +74 -74
  11. package/dist/glance.d.ts +27 -1
  12. package/dist/glance.js +151 -12
  13. package/dist/index.d.ts +11 -2
  14. package/dist/index.js +10 -1
  15. package/dist/localAgentFormats/gemini.js +2 -2
  16. package/dist/localAgentFormats/registry.js +6 -2
  17. package/dist/localAgentFormats/runtimeRegistry.js +5 -2
  18. package/dist/localAgentFormats/types.d.ts +2 -1
  19. package/dist/localAgentLogs.d.ts +362 -3
  20. package/dist/localAgentLogs.js +1964 -165
  21. package/dist/modelPricing.d.ts +1 -1
  22. package/dist/modelPricing.js +1 -1
  23. package/dist/projectEconomics.d.ts +617 -0
  24. package/dist/projectEconomics.js +620 -0
  25. package/dist/projectEconomicsBuilder.d.ts +89 -0
  26. package/dist/projectEconomicsBuilder.js +473 -0
  27. package/dist/projectIndexStore.d.ts +545 -0
  28. package/dist/projectIndexStore.js +606 -0
  29. package/dist/providerConnectors.d.ts +59 -1
  30. package/dist/providerConnectors.js +175 -11
  31. package/dist/qualitativeIndexCache.d.ts +494 -0
  32. package/dist/qualitativeIndexCache.js +930 -0
  33. package/dist/resultCard.d.ts +350 -0
  34. package/dist/resultCard.js +604 -0
  35. package/dist/runtimeCommands.d.ts +21 -0
  36. package/dist/runtimeCommands.js +27 -0
  37. package/dist/scanGuard.d.ts +3 -1
  38. package/dist/scanGuard.js +164 -4
  39. package/dist/schema.d.ts +31 -31
  40. package/dist/sessionVitals.d.ts +145 -0
  41. package/dist/sessionVitals.js +521 -0
  42. package/dist/toolInvocations.d.ts +40 -1
  43. package/dist/toolInvocations.js +101 -20
  44. package/package.json +1 -1
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { aggregateCalls, dedupeCumulativeSessionCalls } from "./localAgentLogs.js";
3
3
  import { localAgentFormatDescriptors } from "./localAgentFormats/registry.js";
4
4
  import { canPriceTokenUsageAtScope, estimateTokenCostUsd, PRICING_TABLE_AS_OF } from "./modelPricing.js";
5
+ import { subscriptionPlans } from "./planMath.js";
5
6
  import { isBundledSampleUsage } from "./schema.js";
6
7
  import { sourceValidationCoverageValues } from "./sourceStatus.js";
7
8
  const DAY_MS = 24 * 60 * 60 * 1_000;
@@ -163,6 +164,8 @@ export const activitySnapshotSubscriptionAgentSchema = z.object({
163
164
  agent: agentSchema,
164
165
  billing: z.literal("subscription"),
165
166
  planId: planIdSchema,
167
+ /** C-lane §2.1: detected-plan list price; null when the plan is unpriced. */
168
+ committedUsdPerMonth: usdSchema.nullable(),
166
169
  apiEquivalent: activitySnapshotApiEquivalentWindowsSchema,
167
170
  limits: z.array(activitySnapshotLimitSchema).max(2),
168
171
  pressure: z.enum(["extra_usage_credits_exhausted"]).nullable()
@@ -215,6 +218,41 @@ const activitySnapshotUnresolvedSchema = z.object({
215
218
  });
216
219
  }
217
220
  });
221
+ /**
222
+ * C-lane §2.1: a provider-billed subscription with no local agent (cursor
223
+ * today). The writer includes a billed30d amount ONLY when its financial
224
+ * evidence is "verified"; the renderer independently drops anything else
225
+ * (double lock: estimated/unverified provider dollars can never reach a
226
+ * statusline segment).
227
+ */
228
+ export const activitySnapshotProviderSubscriptionSchema = z.object({
229
+ provider: z.enum(activitySnapshotProviderValues),
230
+ billing: z.literal("subscription"),
231
+ planLabel: z.string().min(1).max(64).nullable(),
232
+ committedUsdPerMonth: usdSchema.nullable(),
233
+ billed30d: activitySnapshotBilledWindowSchema
234
+ }).strict();
235
+ /** C-lane §2.1: the committed $/mo total across every detected subscription. */
236
+ export const activitySnapshotCommittedTotalSchema = z.object({
237
+ amountUsd: usdSchema.nullable(),
238
+ pricedSubs: countSchema,
239
+ totalSubs: countSchema
240
+ }).strict().superRefine((total, context) => {
241
+ if (total.pricedSubs > total.totalSubs) {
242
+ context.addIssue({
243
+ code: "custom",
244
+ path: ["pricedSubs"],
245
+ message: "Priced subscriptions cannot exceed total subscriptions."
246
+ });
247
+ }
248
+ if ((total.amountUsd === null) !== (total.pricedSubs === 0)) {
249
+ context.addIssue({
250
+ code: "custom",
251
+ path: ["amountUsd"],
252
+ message: "A committed total exists exactly when at least one subscription is priced."
253
+ });
254
+ }
255
+ });
218
256
  export const activitySnapshotOverageSchema = z.object({
219
257
  amountUsd: z.number().finite().positive(),
220
258
  currency: z.literal("USD"),
@@ -330,7 +368,10 @@ export const activitySnapshotCoverageSchema = z.object({
330
368
  });
331
369
  export const activitySnapshotSchema = z.object({
332
370
  kind: z.literal("aibill.activity_snapshot"),
333
- schemaVersion: z.literal(1),
371
+ // v2 (C-lane §2.1): adds per-agent committedUsdPerMonth, provider-billed
372
+ // subscriptions, and the committed total. The writer dual-writes a v1
373
+ // payload for already-installed v1 runners during the deprecation window.
374
+ schemaVersion: z.literal(2),
334
375
  currency: z.literal("USD"),
335
376
  asOf: isoTimestampSchema,
336
377
  generatedAt: isoTimestampSchema,
@@ -347,6 +388,8 @@ export const activitySnapshotSchema = z.object({
347
388
  subscription: activitySnapshotSubscriptionSchema.nullable(),
348
389
  metered: activitySnapshotMeteredSchema.nullable(),
349
390
  unresolved: activitySnapshotUnresolvedSchema.nullable(),
391
+ providers: z.array(activitySnapshotProviderSubscriptionSchema).max(5).nullable(),
392
+ committedTotal: activitySnapshotCommittedTotalSchema,
350
393
  overage: activitySnapshotOverageSchema.nullable(),
351
394
  coverage: activitySnapshotCoverageSchema,
352
395
  networkUploaded: z.literal(false)
@@ -372,6 +415,20 @@ export const activitySnapshotSchema = z.object({
372
415
  (snapshot.subscription || snapshot.metered || snapshot.unresolved || snapshot.overage)) {
373
416
  invalid("Empty and error snapshots cannot carry financial cohorts.", ["mode"]);
374
417
  }
418
+ if ((snapshot.mode === "empty" || snapshot.mode === "error") &&
419
+ (snapshot.providers || snapshot.committedTotal.amountUsd !== null ||
420
+ snapshot.committedTotal.totalSubs !== 0)) {
421
+ invalid("Empty and error snapshots cannot carry subscription pricing.", ["committedTotal"]);
422
+ }
423
+ if (snapshot.providers &&
424
+ new Set(snapshot.providers.map((provider) => provider.provider)).size !== snapshot.providers.length) {
425
+ invalid("A provider subscription may appear only once.", ["providers"]);
426
+ }
427
+ const expectedTotalSubs = (snapshot.subscription?.agents.length ?? 0) +
428
+ (snapshot.providers?.length ?? 0);
429
+ if (snapshot.committedTotal.totalSubs !== expectedTotalSubs) {
430
+ invalid("The committed total must count every subscription row exactly once.", ["committedTotal"]);
431
+ }
375
432
  if (snapshot.mode === "error" && snapshot.refresh.status !== "error") {
376
433
  invalid("Error mode requires an error refresh state.", ["refresh"]);
377
434
  }
@@ -559,9 +616,32 @@ export function buildActivitySnapshot(input) {
559
616
  } : null;
560
617
  const overage = buildOverage(meteredBilledRecords, overageIds, asOfMs);
561
618
  const coverage = buildCoverage(classified.map((entry) => entry.record), scans, providers, input.pricingAsOf ?? PRICING_TABLE_AS_OF, deduplicated.conflictingIds);
619
+ const finalSubscription = mode === "metered" || mode === "unresolved" || mode === "empty"
620
+ ? null
621
+ : subscription;
622
+ // Writer-side lock (C-lane §2.1): a provider-billed amount survives only
623
+ // when its supplied window is verified; anything else degrades to missing.
624
+ const providerSubscriptionRows = mode === "empty"
625
+ ? []
626
+ : (input.providerSubscriptions ?? []).map((row) => ({
627
+ provider: row.provider,
628
+ billing: "subscription",
629
+ planLabel: row.planLabel,
630
+ committedUsdPerMonth: row.committedUsdPerMonth,
631
+ billed30d: row.billed30d &&
632
+ row.billed30d.financialEvidence === "verified" &&
633
+ row.billed30d.amountUsd !== null
634
+ ? row.billed30d
635
+ : missingBilledWindow()
636
+ }));
637
+ const committedRows = [
638
+ ...(finalSubscription?.agents ?? []).map((agent) => agent.committedUsdPerMonth),
639
+ ...providerSubscriptionRows.map((row) => row.committedUsdPerMonth)
640
+ ];
641
+ const pricedRows = committedRows.filter((amount) => amount !== null);
562
642
  return activitySnapshotSchema.parse({
563
643
  kind: "aibill.activity_snapshot",
564
- schemaVersion: 1,
644
+ schemaVersion: 2,
565
645
  currency: "USD",
566
646
  asOf: new Date(asOfMs).toISOString(),
567
647
  generatedAt,
@@ -569,24 +649,80 @@ export function buildActivitySnapshot(input) {
569
649
  lastSuccessAt: generatedAt,
570
650
  refresh: { status: "ok" },
571
651
  mode,
572
- subscription: mode === "metered" || mode === "unresolved" || mode === "empty"
573
- ? null
574
- : subscription,
652
+ subscription: finalSubscription,
575
653
  metered: mode === "subscription" || mode === "unresolved" || mode === "empty"
576
654
  ? null
577
655
  : metered,
578
656
  unresolved,
657
+ providers: providerSubscriptionRows.length > 0 ? providerSubscriptionRows : null,
658
+ committedTotal: {
659
+ amountUsd: pricedRows.length > 0
660
+ ? roundUsd(pricedRows.reduce((total, amount) => total + amount, 0))
661
+ : null,
662
+ pricedSubs: pricedRows.length,
663
+ totalSubs: committedRows.length
664
+ },
579
665
  overage: mode === "metered" || mode === "mixed" ? overage : null,
580
666
  coverage,
581
667
  networkUploaded: false
582
668
  });
583
669
  }
670
+ /** Detected-plan list price (subscriptionPlans); null when unpriced. */
671
+ function committedPriceForPlanId(planId) {
672
+ if (!planId)
673
+ return null;
674
+ return subscriptionPlans.find((plan) => plan.id === planId)?.monthlyUsd ?? null;
675
+ }
676
+ function missingBilledWindow() {
677
+ return {
678
+ amountUsd: null,
679
+ recordCount: 0,
680
+ basis: "provider_billed",
681
+ financialEvidence: "missing",
682
+ coverage: "missing"
683
+ };
684
+ }
685
+ /**
686
+ * The v1 dual-write payload (C-lane §2.1 fleet back-compat): today's fields
687
+ * only, so an already-installed v1 runner keeps rendering fresh data instead
688
+ * of decaying into permanent staleness. Exact v1 key set; no v2 fields.
689
+ */
690
+ export function activitySnapshotV1Payload(snapshot) {
691
+ return {
692
+ kind: snapshot.kind,
693
+ schemaVersion: 1,
694
+ currency: snapshot.currency,
695
+ asOf: snapshot.asOf,
696
+ generatedAt: snapshot.generatedAt,
697
+ lastAttemptAt: snapshot.lastAttemptAt,
698
+ lastSuccessAt: snapshot.lastSuccessAt,
699
+ refresh: snapshot.refresh,
700
+ mode: snapshot.mode,
701
+ subscription: snapshot.subscription
702
+ ? {
703
+ agents: snapshot.subscription.agents.map((agent) => ({
704
+ agent: agent.agent,
705
+ billing: agent.billing,
706
+ planId: agent.planId,
707
+ apiEquivalent: agent.apiEquivalent,
708
+ limits: agent.limits,
709
+ pressure: agent.pressure
710
+ }))
711
+ }
712
+ : null,
713
+ metered: snapshot.metered,
714
+ unresolved: snapshot.unresolved,
715
+ overage: snapshot.overage,
716
+ coverage: snapshot.coverage,
717
+ networkUploaded: snapshot.networkUploaded
718
+ };
719
+ }
584
720
  /** A bounded no-evidence state for an initial failed refresh. */
585
721
  export function createActivitySnapshotError(attemptedAt, errorCode) {
586
722
  const timestamp = new Date(parseTimestamp(attemptedAt, "attemptedAt")).toISOString();
587
723
  return activitySnapshotSchema.parse({
588
724
  kind: "aibill.activity_snapshot",
589
- schemaVersion: 1,
725
+ schemaVersion: 2,
590
726
  currency: "USD",
591
727
  asOf: timestamp,
592
728
  generatedAt: timestamp,
@@ -597,6 +733,8 @@ export function createActivitySnapshotError(attemptedAt, errorCode) {
597
733
  subscription: null,
598
734
  metered: null,
599
735
  unresolved: null,
736
+ providers: null,
737
+ committedTotal: { amountUsd: null, pricedSubs: 0, totalSubs: 0 },
600
738
  overage: null,
601
739
  coverage: {
602
740
  agents: [],
@@ -650,6 +788,7 @@ function activitySubscriptionAgents(records, calls, allCalls, plans, scans, trus
650
788
  agent,
651
789
  billing: "subscription",
652
790
  planId: isKnownPlanId(plan.planId) ? plan.planId : null,
791
+ committedUsdPerMonth: committedPriceForPlanId(plan.planId),
653
792
  apiEquivalent: buildApiWindows(agentRecords, allCalls.filter((call) => call.agent === agent), trustedProviderIds, asOfMs, localCoverageForAgent(agent, scans)),
654
793
  limits: latestReportedLimits(agentCalls, asOfMs),
655
794
  pressure: plan.limitSignal === "extra-usage credits exhausted"
@@ -1,6 +1,13 @@
1
1
  import { type ActivitySnapshot, type ActivitySnapshotRefreshErrorCode } from "./activitySnapshot.js";
2
2
  export declare const activitySnapshotCacheEnvironmentVariable = "AIBILL_CACHE_DIR";
3
- export declare const activitySnapshotCacheFileName = "statusline-v1.json";
3
+ /** The v2 snapshot cache (C-lane §2.1). */
4
+ export declare const activitySnapshotCacheFileName = "statusline-v2.json";
5
+ /**
6
+ * Deprecation-window dual-write target: installed v1 runners are frozen
7
+ * copies that read only this file, so every v2 write also refreshes a v1
8
+ * payload here (today's fields only) instead of stranding them stale.
9
+ */
10
+ export declare const activitySnapshotLegacyCacheFileName = "statusline-v1.json";
4
11
  export declare const activitySnapshotCacheMaxBytes: number;
5
12
  export type ActivitySnapshotCacheOptions = {
6
13
  /** Test/embedding override. Production defaults to ~/.aibill/cache. */
@@ -1,18 +1,30 @@
1
1
  import { constants } from "node:fs";
2
+ import { execFile as execFileCallback } from "node:child_process";
2
3
  import { open, lstat, mkdir, chmod, realpath, rename, unlink } from "node:fs/promises";
3
4
  import { homedir } from "node:os";
4
- import { join, resolve } from "node:path";
5
+ import { dirname, join, relative, resolve } from "node:path";
5
6
  import { randomUUID } from "node:crypto";
6
7
  import { setTimeout as delay } from "node:timers/promises";
7
- import { activitySnapshotSchema, createActivitySnapshotError } from "./activitySnapshot.js";
8
+ import { promisify } from "node:util";
9
+ import { activitySnapshotSchema, activitySnapshotV1Payload, createActivitySnapshotError } from "./activitySnapshot.js";
8
10
  export const activitySnapshotCacheEnvironmentVariable = "AIBILL_CACHE_DIR";
9
- export const activitySnapshotCacheFileName = "statusline-v1.json";
11
+ /** The v2 snapshot cache (C-lane §2.1). */
12
+ export const activitySnapshotCacheFileName = "statusline-v2.json";
13
+ /**
14
+ * Deprecation-window dual-write target: installed v1 runners are frozen
15
+ * copies that read only this file, so every v2 write also refreshes a v1
16
+ * payload here (today's fields only) instead of stranding them stale.
17
+ */
18
+ export const activitySnapshotLegacyCacheFileName = "statusline-v1.json";
10
19
  export const activitySnapshotCacheMaxBytes = 64 * 1_024;
20
+ // The lock name is shared with pre-v2 writers on purpose: during the fleet
21
+ // deprecation window both CLI generations serialize through one lock.
11
22
  const lockFileName = ".statusline-v1.lock";
12
23
  const defaultLockTimeoutMs = 2_000;
13
24
  const staleLockMs = 15_000;
14
25
  const lockPollMs = 20;
15
26
  const lockMetadataMaxBytes = 512;
27
+ const execFile = promisify(execFileCallback);
16
28
  export class ActivitySnapshotCacheError extends Error {
17
29
  code;
18
30
  constructor(code, message) {
@@ -207,7 +219,7 @@ async function readSnapshotFile(directory) {
207
219
  catch {
208
220
  return { status: "error", code: "malformed" };
209
221
  }
210
- if (isRecord(value) && value.schemaVersion !== undefined && value.schemaVersion !== 1) {
222
+ if (isRecord(value) && value.schemaVersion !== undefined && value.schemaVersion !== 2) {
211
223
  return { status: "error", code: "unsupported_version" };
212
224
  }
213
225
  const parsed = activitySnapshotSchema.safeParse(value);
@@ -225,11 +237,17 @@ async function readSnapshotFile(directory) {
225
237
  }
226
238
  }
227
239
  async function atomicWriteSnapshot(directory, snapshot) {
228
- const contents = `${JSON.stringify(snapshot)}\n`;
240
+ await atomicWriteCacheFile(directory, activitySnapshotCacheFileName, `${JSON.stringify(snapshot)}\n`);
241
+ // C-lane §2.1 fleet back-compat: dual-write the v1 payload so an
242
+ // already-installed v1 runner keeps rendering fresh data during the
243
+ // deprecation window instead of decaying into permanent staleness.
244
+ await atomicWriteCacheFile(directory, activitySnapshotLegacyCacheFileName, `${JSON.stringify(activitySnapshotV1Payload(snapshot))}\n`);
245
+ }
246
+ async function atomicWriteCacheFile(directory, fileName, contents) {
229
247
  if (Buffer.byteLength(contents, "utf8") > activitySnapshotCacheMaxBytes) {
230
248
  throw new ActivitySnapshotCacheError("invalid_snapshot", "Activity snapshot exceeds the 64 KiB cache limit.");
231
249
  }
232
- const filePath = join(directory, activitySnapshotCacheFileName);
250
+ const filePath = join(directory, fileName);
233
251
  const existing = await lstat(filePath).catch((error) => {
234
252
  if (isNodeError(error, "ENOENT"))
235
253
  return undefined;
@@ -238,7 +256,7 @@ async function atomicWriteSnapshot(directory, snapshot) {
238
256
  if (existing?.isSymbolicLink() || (existing && !existing.isFile())) {
239
257
  throw new ActivitySnapshotCacheError("unsafe_file", "Activity snapshot cache path is not a regular file.");
240
258
  }
241
- const temporaryPath = join(directory, `.${activitySnapshotCacheFileName}.${process.pid}.${randomUUID()}.tmp`);
259
+ const temporaryPath = join(directory, `.${fileName}.${process.pid}.${randomUUID()}.tmp`);
242
260
  let handle;
243
261
  try {
244
262
  handle = await open(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollowFlag(), 0o600);
@@ -329,6 +347,84 @@ async function ensureDefaultParent(homeDirectory, create) {
329
347
  }
330
348
  if (create)
331
349
  await chmod(parent, 0o700);
350
+ await ensureDefaultCacheGitPrivacy(parent, create);
351
+ }
352
+ /**
353
+ * When a synthetic or real HOME is itself inside a Git worktree, protect the
354
+ * complete top-level private state directory before any cache child is
355
+ * created. Explicit AIBILL_CACHE_DIR/cacheDirectory overrides intentionally
356
+ * remain caller-owned and never receive repository files from this helper.
357
+ */
358
+ async function ensureDefaultCacheGitPrivacy(aibillDirectory, create) {
359
+ const gitRoot = await findEnclosingGitRoot(aibillDirectory);
360
+ if (!gitRoot)
361
+ return;
362
+ const marker = join(aibillDirectory, ".gitignore");
363
+ let handle;
364
+ try {
365
+ handle = await open(marker, constants.O_RDONLY | noFollowFlag());
366
+ }
367
+ catch (error) {
368
+ if (!isNodeError(error, "ENOENT"))
369
+ throw error;
370
+ if (!create) {
371
+ const missing = new Error("Private aibill Git privacy marker does not exist.");
372
+ missing.code = "ENOENT";
373
+ throw missing;
374
+ }
375
+ handle = await open(marker, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollowFlag(), 0o600);
376
+ await handle.writeFile("*\n", "utf8");
377
+ await handle.sync();
378
+ await handle.close();
379
+ handle = await open(marker, constants.O_RDONLY | noFollowFlag());
380
+ }
381
+ try {
382
+ const info = await handle.stat();
383
+ if (!info.isFile() || !hasPrivatePermissions(info.mode) || info.size !== 2) {
384
+ throw new ActivitySnapshotCacheError("unsafe_directory", "Private aibill Git privacy marker is unsafe.");
385
+ }
386
+ const buffer = Buffer.alloc(2);
387
+ const { bytesRead } = await handle.read(buffer, 0, 2, 0);
388
+ if (bytesRead !== 2 || buffer.toString("utf8") !== "*\n") {
389
+ throw new ActivitySnapshotCacheError("unsafe_directory", "Private aibill Git privacy marker is invalid.");
390
+ }
391
+ }
392
+ finally {
393
+ await handle.close().catch(() => undefined);
394
+ }
395
+ const relativeDirectory = relative(gitRoot, aibillDirectory);
396
+ const tracked = await execFile("git", ["-C", gitRoot, "ls-files", "--", relativeDirectory], {
397
+ encoding: "utf8",
398
+ maxBuffer: 64 * 1024
399
+ }).then(({ stdout }) => stdout.trim()).catch(() => {
400
+ throw new ActivitySnapshotCacheError("unsafe_directory", "Private aibill cache tracking status could not be verified.");
401
+ });
402
+ if (tracked) {
403
+ throw new ActivitySnapshotCacheError("unsafe_directory", "Private aibill cache is already tracked by Git.");
404
+ }
405
+ const ignored = await execFile("git", [
406
+ "-C", gitRoot, "check-ignore", "--quiet", "--no-index", "--",
407
+ join(relativeDirectory, "cache", "privacy-probe.json")
408
+ ]).then(() => true).catch(() => false);
409
+ if (!ignored) {
410
+ throw new ActivitySnapshotCacheError("unsafe_directory", "Private aibill cache is not proven ignored by Git.");
411
+ }
412
+ }
413
+ async function findEnclosingGitRoot(path) {
414
+ let current = resolve(path);
415
+ while (true) {
416
+ const gitEntry = await lstat(join(current, ".git")).catch((error) => {
417
+ if (isNodeError(error, "ENOENT") || isNodeError(error, "ENOTDIR"))
418
+ return undefined;
419
+ throw error;
420
+ });
421
+ if (gitEntry)
422
+ return current;
423
+ const parent = dirname(current);
424
+ if (parent === current)
425
+ return undefined;
426
+ current = parent;
427
+ }
332
428
  }
333
429
  function configuredCacheDirectory(options) {
334
430
  const configured = options.cacheDirectory?.trim() ||