UNPKG

mdfind-node

Version:

Node.js bindings for macOS Spotlight search (mdfind, mdls, mdutil)

2,792 lines 77.1 kB
// src/mdfind.ts
import { spawn as spawn2 } from "node:child_process";
import process2 from "node:process";

// src/schemas/core/spotlight.ts
import { z } from "zod";
var SpotlightContentTypeSchema = z.enum([
  "public.audio",
  "public.image",
  "public.movie",
  "public.pdf",
  "public.plain-text",
  "public.rtf",
  "public.html",
  "public.font"
]).or(z.string());
var SpotlightAttributeSchema = z.enum([
  // General attributes
  "kMDItemDisplayName",
  "kMDItemFSName",
  "kMDItemPath",
  "kMDItemContentType",
  "kMDItemContentTypeTree",
  "kMDItemKind",
  "kMDItemLastUsedDate",
  "kMDItemContentCreationDate",
  "kMDItemContentModificationDate",
  // Document attributes
  "kMDItemTitle",
  "kMDItemAuthors",
  "kMDItemComment",
  "kMDItemCopyright",
  "kMDItemKeywords",
  "kMDItemNumberOfPages",
  "kMDItemLanguages",
  // Media attributes
  "kMDItemDurationSeconds",
  "kMDItemCodecs",
  "kMDItemPixelHeight",
  "kMDItemPixelWidth",
  "kMDItemAudioBitRate",
  "kMDItemAudioChannelCount",
  "kMDItemTotalBitRate",
  // Image specific
  "kMDItemOrientation",
  "kMDItemFlashOnOff",
  "kMDItemFocalLength",
  "kMDItemAcquisitionMake",
  "kMDItemAcquisitionModel",
  "kMDItemISOSpeed",
  "kMDItemExposureTimeSeconds",
  // Location attributes
  "kMDItemLatitude",
  "kMDItemLongitude",
  "kMDItemAltitude",
  "kMDItemCity",
  "kMDItemStateOrProvince",
  "kMDItemCountry"
]).or(z.string());
var MetadataValueSchema = z.union([
  z.string(),
  z.number(),
  z.boolean(),
  z.date(),
  z.array(z.string()),
  z.null()
]);
var MetadataResultSchema = z.record(z.string(), MetadataValueSchema);

// src/schemas/core/events.ts
import { z as z2 } from "zod";
var MdfindErrorSchema = z2.object({
  name: z2.literal("MdfindError"),
  message: z2.string(),
  stderr: z2.string()
}).strict();
var LiveSearchEventsSchema = z2.object({
  onResult: z2.function().args(z2.array(z2.string())).returns(z2.void()),
  onError: z2.function().args(MdfindErrorSchema).returns(z2.void()),
  onEnd: z2.function().returns(z2.void()).optional()
}).strict();

// src/schemas/core/index-status.ts
import { z as z3 } from "zod";
var IndexingStateSchema = z3.enum(["enabled", "disabled", "unknown", "error"]);
var IndexStatusSchema = z3.object({
  /**
   * The current state of indexing
   */
  state: IndexingStateSchema,
  /**
   * Whether indexing is enabled (maintained for backward compatibility)
   */
  enabled: z3.boolean(),
  /**
   * The raw status message from mdutil
   */
  status: z3.string(),
  /**
   * The last time the volume was scanned
   */
  scanBaseTime: z3.date().nullable().optional(),
  /**
   * The reason for the current state
   */
  reasoning: z3.string().nullable().optional(),
  /**
   * The volume path that was checked
   */
  volumePath: z3.string(),
  /**
   * Whether this volume is a system volume
   */
  isSystemVolume: z3.boolean()
}).strict();

// src/schemas/metadata/index.ts
import { z as z8 } from "zod";

// src/schemas/metadata/basic.ts
import { z as z5 } from "zod";

// src/schemas/core/date.ts
import { z as z4 } from "zod";
var handleRelativeDate = (str) => {
  const date = /* @__PURE__ */ new Date();
  const lowerStr = str.toLowerCase();
  if (lowerStr === "yesterday") {
    date.setDate(date.getDate() - 1);
    return date;
  }
  if (lowerStr === "tomorrow") {
    date.setDate(date.getDate() + 1);
    return date;
  }
  const relativeMatch = lowerStr.match(/^(\d+)\s+(days?|weeks?|months?|years?)\s+ago$/i);
  if (!relativeMatch?.[1] || !relativeMatch[2]) return new Date(str);
  const amount = relativeMatch[1];
  const unit = relativeMatch[2];
  const num = parseInt(amount, 10);
  const unitHandlers = {
    day: () => date.setDate(date.getDate() - num),
    week: () => date.setDate(date.getDate() - num * 7),
    month: () => date.setMonth(date.getMonth() - num),
    year: () => date.setFullYear(date.getFullYear() - num)
  };
  const baseUnit = unit.replace(/s$/, "");
  unitHandlers[baseUnit]();
  return date;
};
var handleTimestamp = (num) => new Date(num < 1e12 ? num * 1e3 : num);
var handleArrayItem = (item) => {
  if (item instanceof Date) return item;
  if (typeof item === "string") return handleRelativeDate(item);
  return handleTimestamp(item);
};
var validateDate = (date) => date instanceof Date && !isNaN(date.getTime()) ? date : null;
var DateCoerceSchema = z4.union([
  z4.date(),
  z4.string().transform(handleRelativeDate),
  z4.number().transform(handleTimestamp),
  z4.array(z4.union([z4.date(), z4.string(), z4.number()])).transform((arr) => arr.map(handleArrayItem)[0])
]).nullable().optional().transform(validateDate);
var StrictDateSchema = z4.date().nullable().optional();
var DateStringSchema = DateCoerceSchema.transform((date) => date?.toISOString() ?? null);
var TimestampSchema = DateCoerceSchema.transform((date) => date?.getTime() ?? null);

// src/schemas/metadata/basic.ts
var BasicMetadataSchema = z5.object({
  name: z5.string(),
  contentType: z5.string().nullable().optional(),
  kind: z5.string().nullable().optional(),
  size: z5.number().optional(),
  created: DateCoerceSchema,
  modified: DateCoerceSchema,
  lastOpened: DateCoerceSchema
});

// src/schemas/metadata/exif.ts
import { z as z6 } from "zod";
var ExifDataSchema = z6.object({
  make: z6.string().nullable().optional(),
  model: z6.string().nullable().optional(),
  lens: z6.string().nullable().optional(),
  exposureTime: z6.number().nullable().optional(),
  fNumber: z6.number().nullable().optional(),
  isoSpeedRatings: z6.number().nullable().optional(),
  focalLength: z6.number().nullable().optional(),
  gpsLatitude: z6.number().nullable().optional(),
  gpsLongitude: z6.number().nullable().optional(),
  gpsAltitude: z6.number().nullable().optional(),
  dateTimeOriginal: DateCoerceSchema,
  dateTimeDigitized: DateCoerceSchema
});

// src/schemas/metadata/xmp.ts
import { z as z7 } from "zod";
var XMPDataSchema = z7.object({
  title: z7.string().nullable().optional(),
  description: z7.string().nullable().optional(),
  creator: z7.string().nullable().optional(),
  subject: z7.array(z7.string()).nullable().optional(),
  createDate: DateCoerceSchema,
  modifyDate: DateCoerceSchema,
  metadataDate: DateCoerceSchema,
  copyrightNotice: z7.string().nullable().optional(),
  rights: z7.string().nullable().optional(),
  webStatement: z7.string().nullable().optional()
});

// src/schemas/metadata/index.ts
var ExtendedMetadataSchema = z8.object({
  basic: BasicMetadataSchema,
  exif: ExifDataSchema.optional(),
  xmp: XMPDataSchema.optional(),
  spotlight: MetadataResultSchema
});

// src/schemas/options.ts
import { z as z9 } from "zod";
var CoreOptionsSchema = z9.object({
  maxBuffer: z9.number().default(1024 * 512),
  literal: z9.boolean().default(false),
  interpret: z9.boolean().default(false)
});
var SearchOptionsSchema = CoreOptionsSchema.extend({
  live: z9.boolean().default(false),
  /**
   * Automatically stop live search after specified duration (in milliseconds).
   * Only applies to live searches.
   * When timeout is reached:
   * - Search will be stopped
   * - onComplete callback will be called
   * - Resources will be cleaned up
   * @example
   * ```typescript
   * // Stop after 5 seconds
   * const options = { live: true, timeout: 5000 }
   * ```
   */
  timeout: z9.number().optional(),
  operator: z9.enum(["&&", "||"]).default("&&"),
  count: z9.boolean().default(false),
  reprint: z9.boolean().default(false),
  nullSeparator: z9.boolean().default(false)
});
var FilterOptionsSchema = CoreOptionsSchema.extend({
  name: z9.string().optional(),
  names: z9.array(z9.string()).optional().default([]),
  onlyIn: z9.string().optional(),
  onlyInDirectory: z9.string().optional(),
  attr: z9.string().optional(),
  attributes: z9.array(z9.string()).optional().default([]),
  smartFolder: z9.string().optional()
});
var MdfindOptionsSchema = SearchOptionsSchema.merge(FilterOptionsSchema);
var MdfindOptionsInputSchema = MdfindOptionsSchema.partial();
var MdfindOptionsOutputSchema = MdfindOptionsSchema.transform((opts) => {
  const transformed = {
    ...opts,
    names: [],
    attributes: []
  };
  transformed.onlyInDirectory = opts.onlyIn ?? opts.onlyInDirectory;
  transformed.name = opts.name;
  transformed.names = opts.names ?? [];
  transformed.attr = opts.attr;
  transformed.attributes = opts.attributes ?? [];
  return transformed;
});
var MdlsOptionsSchema = CoreOptionsSchema.extend({
  attributes: z9.array(z9.string()).default([]),
  raw: z9.boolean().default(false),
  nullMarker: z9.string().default("(null)"),
  structured: z9.boolean().default(false)
});
var MdutilOptionsSchema = CoreOptionsSchema.extend({
  volume: z9.string().optional(),
  verbose: z9.boolean().default(false),
  excludeSystemVolumes: z9.boolean().default(false),
  excludeUnknownState: z9.boolean().default(false)
});
var MdimportOptionsSchema = CoreOptionsSchema.extend({
  recursive: z9.boolean().default(false),
  remove: z9.boolean().default(false),
  update: z9.boolean().default(false),
  scanNow: z9.boolean().default(false),
  importerInfo: z9.boolean().default(false),
  attributeInfo: z9.boolean().default(false)
});

// src/schemas/core/mdimport.ts
import { z as z10 } from "zod";
var MdimportDebugLevelSchema = z10.enum(["1", "2", "3"]);
var MdimportOptionsSchema2 = z10.object({
  /**
   * Test import without storing in index
   * @default false
   */
  test: z10.boolean().default(false),
  /**
   * Debug level (requires test mode)
   * - 1: Print summary of test import
   * - 2: Print summary and all attributes (except kMDItemTextContent)
   * - 3: Print summary and all attributes (including kMDItemTextContent)
   */
  debugLevel: MdimportDebugLevelSchema.optional(),
  /**
   * Output file for test results (requires test mode)
   */
  outputFile: z10.string().optional(),
  /**
   * Show performance information (requires test mode)
   * @default false
   */
  showPerformance: z10.boolean().default(false),
  /**
   * Maximum buffer size for output
   * @default 512KB
   */
  maxBuffer: z10.number().default(1024 * 512)
}).strict();
var MdimportResultSchema = z10.object({
  /**
   * Raw output from mdimport command
   */
  output: z10.string(),
  /**
   * Performance metrics (only available with showPerformance option)
   */
  performance: z10.object({
    totalTime: z10.number(),
    importTime: z10.number(),
    fileCount: z10.number()
  }).optional(),
  /**
   * Debug information (only available with debugLevel option)
   */
  debug: z10.object({
    level: MdimportDebugLevelSchema,
    summary: z10.string(),
    attributes: z10.record(z10.string(), z10.unknown()).optional()
  }).optional()
});
var ImporterInfoSchema = z10.object({
  /**
   * Path to the importer bundle
   */
  path: z10.string(),
  /**
   * Name of the importer
   */
  name: z10.string(),
  /**
   * Version of the importer
   */
  version: z10.string().optional(),
  /**
   * UTIs handled by this importer
   */
  supportedTypes: z10.array(z10.string()).optional()
});
var AttributeInfoSchema = z10.object({
  /**
   * Attribute name (e.g., kMDItemDisplayName)
   */
  name: z10.string(),
  /**
   * Localized description
   */
  description: z10.string(),
  /**
   * Attribute type (e.g., string, date, number)
   */
  type: z10.string().optional(),
  /**
   * Whether the attribute is localized
   */
  isLocalized: z10.boolean().optional()
});

// src/utils/path.ts
import { homedir } from "node:os";
function expandPath(path) {
  return path.replace(/^~/, homedir());
}

// src/validation.ts
var validateInput = (query, options) => {
  if (options.live && options.count) {
    throw new Error("Cannot use live and count options together");
  }
  if (options.literal && options.interpret) {
    throw new Error("Cannot use literal and interpret options together");
  }
  if (options.timeout !== void 0 && !options.live) {
    throw new Error("Timeout option can only be used with live searches");
  }
  const hasNameOption = Boolean(options.name) || (options.names?.length ?? 0) > 0;
  if (hasNameOption && !query.trim()) {
    return;
  }
  if (!query.trim()) {
    throw new Error("Query cannot be empty unless using -name option");
  }
};

// src/live-search.ts
import { spawn } from "node:child_process";
import process from "node:process";
var DEFAULT_OPTIONS = {
  live: false,
  count: false,
  nullSeparator: false,
  maxBuffer: 1024 * 1024,
  reprint: false,
  literal: false,
  interpret: false,
  names: [],
  attributes: [],
  onlyInDirectory: void 0,
  smartFolder: void 0
};
function mdfindLive(query, options = {}, events) {
  const validatedOptions = MdfindOptionsSchema.parse({ ...DEFAULT_OPTIONS, ...options, live: true });
  const validatedEvents = LiveSearchEventsSchema.parse(events);
  validateInput(query, validatedOptions);
  const args = ["-live"];
  if (validatedOptions.onlyInDirectory) {
    args.push("-onlyin", expandPath(validatedOptions.onlyInDirectory));
  }
  if (validatedOptions.names.length > 0) {
    for (const name of validatedOptions.names) {
      args.push("-name", name);
    }
  }
  if (validatedOptions.attributes.length > 0) {
    for (const attr of validatedOptions.attributes) {
      args.push("-attr", attr);
    }
  }
  if (validatedOptions.smartFolder) {
    args.push("-s", validatedOptions.smartFolder);
  }
  if (validatedOptions.nullSeparator) {
    args.push("-0");
  }
  if (validatedOptions.reprint) {
    args.push("-reprint");
  }
  if (validatedOptions.literal) {
    args.push("-literal");
  }
  if (validatedOptions.interpret) {
    args.push("-interpret");
  }
  const trimmedQuery = query.trim();
  if (trimmedQuery) {
    args.push(trimmedQuery);
  }
  const child = spawn("mdfind", args, { env: process.env });
  let buffer = "";
  child.stdout.on("data", (data) => {
    buffer += data.toString();
    const separator = validatedOptions.nullSeparator ? "\0" : "\n";
    const lines = buffer.split(separator);
    buffer = lines.pop() ?? "";
    const paths = lines.filter((line) => line.length > 0);
    if (paths.length > 0) {
      validatedEvents.onResult(paths);
    }
  });
  child.stderr.on("data", (data) => {
    const stderr = data.toString();
    if (!stderr.includes("[UserQueryParser] Loading keywords")) {
      validatedEvents.onError(new MdfindError("mdfind command failed", stderr));
    }
  });
  child.on("close", () => {
    if (buffer) {
      const separator = validatedOptions.nullSeparator ? "\0" : "\n";
      const paths = buffer.split(separator).filter(Boolean);
      if (paths.length > 0) {
        validatedEvents.onResult(paths);
      }
    }
    validatedEvents.onEnd?.();
  });
  return child;
}

// src/mdfind.ts
function buildMdfindArgs(query, options) {
  const args = [];
  if (options.name) {
    args.push("-name", options.name);
  }
  for (const name of options.names ?? []) {
    args.push("-name", name);
  }
  if (options.onlyIn) {
    args.push("-onlyin", expandPath(options.onlyIn));
  } else if (options.onlyInDirectory) {
    args.push("-onlyin", expandPath(options.onlyInDirectory));
  }
  if (options.attr) {
    args.push("-attr", options.attr);
  }
  for (const attr of options.attributes ?? []) {
    args.push("-attr", attr);
  }
  if (options.smartFolder) {
    args.push("-s", options.smartFolder);
  }
  if (options.nullSeparator) {
    args.push("-0");
  }
  if (options.reprint) {
    args.push("-reprint");
  }
  if (options.literal) {
    args.push("-literal");
  }
  if (options.interpret) {
    args.push("-interpret");
  }
  if (options.count) {
    args.push("-count");
  }
  if (options.live) {
    args.push("-live");
  }
  const trimmedQuery = query.trim();
  if (trimmedQuery) {
    args.push(trimmedQuery);
  }
  return args;
}
var MdfindError = class extends Error {
  constructor(message, stderr) {
    super(message);
    this.stderr = stderr;
  }
  name = "MdfindError";
};
var DEFAULT_OPTIONS2 = {
  live: false,
  count: false,
  nullSeparator: false,
  maxBuffer: 1024 * 512,
  reprint: false,
  literal: false,
  interpret: false,
  names: [],
  attributes: []
};
function mdfind(query, options = {}) {
  return new Promise((resolve2, reject) => {
    const validatedOptions = MdfindOptionsOutputSchema.parse({ ...DEFAULT_OPTIONS2, ...options });
    validateInput(query, validatedOptions);
    const args = buildMdfindArgs(query, validatedOptions);
    const child = spawn2("mdfind", args, { env: process2.env });
    let buffer = "";
    child.stdout.on("data", (data) => {
      buffer += data.toString();
    });
    child.stderr.on("data", (data) => {
      const stderr = data.toString();
      if (!stderr.includes("[UserQueryParser] Loading keywords")) {
        reject(new MdfindError("mdfind command failed", stderr));
      }
    });
    child.on("close", () => {
      const separator = validatedOptions.nullSeparator ? "\0" : "\n";
      const paths = buffer.split(separator).filter(Boolean);
      resolve2(paths);
    });
  });
}
function mdfindCount(query, options = {}) {
  return new Promise((resolve2, reject) => {
    const validatedOptions = MdfindOptionsOutputSchema.parse({
      ...DEFAULT_OPTIONS2,
      ...options,
      count: true
    });
    validateInput(query, validatedOptions);
    const args = buildMdfindArgs(query, validatedOptions);
    const child = spawn2("mdfind", args, { env: process2.env });
    let buffer = "";
    child.stdout.on("data", (data) => {
      buffer += data.toString();
    });
    child.stderr.on("data", (data) => {
      const stderr = data.toString();
      if (!stderr.includes("[UserQueryParser] Loading keywords")) {
        reject(new MdfindError("mdfind command failed", stderr));
      }
    });
    child.on("close", () => {
      const count = parseInt(buffer.trim(), 10);
      resolve2(isNaN(count) ? 0 : count);
    });
  });
}

// src/mdls.ts
import { exec } from "node:child_process";
import { promisify } from "node:util";

// src/schemas/metadata/transform.ts
function transformBasicMetadata(metadata) {
  const transformed = {
    name: metadata.kMDItemDisplayName ?? metadata.kMDItemFSName ?? "",
    contentType: metadata.kMDItemContentType,
    kind: metadata.kMDItemKind,
    size: metadata.kMDItemFSSize,
    created: DateCoerceSchema.parse(metadata.kMDItemContentCreationDate),
    modified: DateCoerceSchema.parse(metadata.kMDItemContentModificationDate),
    lastOpened: DateCoerceSchema.parse(metadata.kMDItemLastUsedDate)
  };
  return BasicMetadataSchema.parse(transformed);
}
function transformExifMetadata(metadata) {
  const transformed = {
    make: metadata.kMDItemAcquisitionMake,
    model: metadata.kMDItemAcquisitionModel,
    lens: metadata.kMDItemLensModel,
    exposureTime: metadata.kMDItemExposureTimeSeconds,
    fNumber: metadata.kMDItemFNumber,
    isoSpeedRatings: metadata.kMDItemISOSpeed,
    focalLength: metadata.kMDItemFocalLength,
    gpsLatitude: metadata.kMDItemLatitude,
    gpsLongitude: metadata.kMDItemLongitude,
    gpsAltitude: metadata.kMDItemAltitude,
    dateTimeOriginal: DateCoerceSchema.parse(metadata.kMDItemContentCreationDate),
    dateTimeDigitized: DateCoerceSchema.parse(metadata.kMDItemDateAdded)
  };
  return ExifDataSchema.parse(transformed);
}
function transformXMPMetadata(metadata) {
  const transformed = {
    title: metadata.kMDItemTitle,
    description: metadata.kMDItemDescription,
    creator: Array.isArray(metadata.kMDItemAuthors) ? metadata.kMDItemAuthors[0] : metadata.kMDItemAuthors,
    subject: metadata.kMDItemKeywords,
    createDate: DateCoerceSchema.parse(metadata.kMDItemContentCreationDate),
    modifyDate: DateCoerceSchema.parse(metadata.kMDItemContentModificationDate),
    metadataDate: DateCoerceSchema.parse(metadata.kMDItemAttributeChangeDate),
    copyrightNotice: metadata.kMDItemCopyright,
    rights: metadata.kMDItemRights,
    webStatement: metadata.kMDItemURL
  };
  return XMPDataSchema.parse(transformed);
}

// src/mdls.ts
var execAsync = promisify(exec);
function coerceDate(value, key) {
  if (!key.includes("Date") && !key.includes("date")) return null;
  try {
    const date = new Date(value);
    if (!isNaN(date.getTime())) {
      return date;
    }
  } catch {
  }
  return null;
}
function coerceNumber(value, key) {
  if (!key.includes("Size") && !key.includes("Count") && !key.includes("Number") && !key.includes("BitRate") && !key.includes("Duration") && !key.includes("Height") && !key.includes("Width") && !key.includes("Length") && !key.includes("Speed") && !key.includes("Time")) {
    return null;
  }
  const num = Number(value);
  return isNaN(num) ? null : num;
}
function coerceValue(value, key) {
  const cleanValue = value.replace(/^"(.*)"$/, "$1");
  const dateValue = coerceDate(cleanValue, key);
  if (dateValue) return dateValue;
  const numValue = coerceNumber(cleanValue, key);
  if (numValue !== null) return numValue;
  if (cleanValue === "true" || cleanValue === "false") {
    return cleanValue === "true";
  }
  return cleanValue;
}
var parseRawMetadata = (output, attributes) => {
  const result = {};
  const values = output.trim().split("\0");
  for (let i = 0; i < attributes.length && i < values.length; i++) {
    const value = values[i];
    const attr = attributes[i];
    if (!value || !attr) continue;
    if (value === "(null)") {
      result[attr] = null;
    } else if (value.startsWith("(") && value.endsWith(")")) {
      const content = value.slice(1, -1).trim();
      result[attr] = content ? content.split(",").map((s) => s.trim().replace(/^"(.*)"$/, "$1")) : [];
    } else {
      result[attr] = coerceValue(value, attr);
    }
  }
  return MetadataResultSchema.parse(result);
};
var parseFormattedMetadata = (output) => {
  const result = {};
  const lines = output.split("\n");
  for (const line of lines) {
    const match = line.match(/^([^=]+)=\s*(.*)$/);
    if (!match) continue;
    const [, key, rawValue] = match;
    if (!key || rawValue === void 0) continue;
    const cleanKey = key.trim();
    const cleanValue = rawValue.trim();
    if (cleanValue === "(null)") {
      result[cleanKey] = null;
    } else if (cleanValue.startsWith("(") && cleanValue.endsWith(")")) {
      const content = cleanValue.slice(1, -1).trim();
      result[cleanKey] = content ? content.split(",").map((s) => s.trim().replace(/^"(.*)"$/, "$1")) : [];
    } else {
      result[cleanKey] = coerceValue(cleanValue, cleanKey);
    }
  }
  return MetadataResultSchema.parse(result);
};
var getMetadata = async (filePath, options = {}) => {
  const validatedOptions = MdlsOptionsSchema.parse(options);
  const args = [];
  if (validatedOptions.attributes.length > 0) {
    for (const attr of validatedOptions.attributes) {
      args.push("-name", attr);
    }
  }
  if (validatedOptions.raw) {
    args.push("-raw");
    if (validatedOptions.nullMarker) {
      args.push("-nullMarker", validatedOptions.nullMarker);
    }
  }
  args.push(filePath);
  try {
    const { stdout } = await execAsync(`mdls ${args.map((arg) => `"${arg}"`).join(" ")}`);
    const rawMetadata = validatedOptions.raw ? parseRawMetadata(stdout, validatedOptions.attributes) : parseFormattedMetadata(stdout);
    if (validatedOptions.structured) {
      return ExtendedMetadataSchema.parse({
        basic: transformBasicMetadata(rawMetadata),
        exif: transformExifMetadata(rawMetadata),
        xmp: transformXMPMetadata(rawMetadata),
        spotlight: rawMetadata
      });
    }
    return rawMetadata;
  } catch (error) {
    if (error instanceof Error) {
      throw new Error(`Failed to get metadata: ${error.message}`);
    }
    throw error;
  }
};

// src/mdutil.ts
import { exec as exec2 } from "node:child_process";
import { resolve } from "node:path";
import { promisify as promisify2 } from "node:util";
var execAsync2 = promisify2(exec2);
function escapeShellPath(path) {
  return path.replace(/([\s'"\[\](){}$&*?|<>^;`\\])/g, "\\$1").replace(/\n/g, "").replace(/\r/g, "").replace(/\t/g, "").replace(/\0/g, "").trim();
}
var MdutilError = class extends Error {
  constructor(message, stderr, requiresRoot = false) {
    super(message);
    this.stderr = stderr;
    this.requiresRoot = requiresRoot;
  }
  name = "MdutilError";
};
var parseIndexingStatus = (output, volumePath) => {
  const resolvedPath = resolve(volumePath);
  const isSystemVolume = resolvedPath.startsWith("/System/Volumes/");
  let state;
  if (output.includes("Indexing enabled")) {
    state = "enabled";
  } else if (output.includes("Indexing disabled")) {
    state = "disabled";
  } else if (output.includes("Error: unknown indexing state")) {
    state = "unknown";
  } else {
    state = "error";
  }
  const scanBaseMatch = output.match(/Scan base time: ([^(]+)/);
  let scanBaseTime = null;
  if (scanBaseMatch?.[1]) {
    try {
      scanBaseTime = new Date(scanBaseMatch[1]);
    } catch {
    }
  }
  const reasoningMatch = output.match(/reasoning: '([^']*)'/);
  const reasoning = reasoningMatch?.[1] ?? null;
  const result = {
    state,
    enabled: state === "enabled",
    status: output.trim(),
    scanBaseTime,
    reasoning,
    volumePath: resolvedPath,
    isSystemVolume
  };
  return IndexStatusSchema.parse(result);
};
var getIndexingStatus = async (volumePath, options = {
  maxBuffer: 1024 * 1024,
  literal: false,
  interpret: true,
  verbose: false,
  excludeSystemVolumes: false,
  excludeUnknownState: false
}) => {
  const validatedOptions = MdutilOptionsSchema.parse(options);
  const args = ["-s"];
  if (validatedOptions.verbose) args.push("-v");
  args.push(volumePath);
  try {
    const { stdout } = await execAsync2(`mdutil ${args.map((arg) => `"${arg}"`).join(" ")}`);
    return parseIndexingStatus(stdout, volumePath);
  } catch (error) {
    if (error instanceof Error) {
      const requiresRoot = error.message.includes("Operation not permitted");
      throw new MdutilError(
        `Failed to get indexing status: ${error.message}`,
        error.message,
        requiresRoot
      );
    }
    throw error;
  }
};
var getAllVolumesStatus = async (options = {
  maxBuffer: 1024 * 1024,
  literal: false,
  interpret: true,
  verbose: false,
  excludeSystemVolumes: false,
  excludeUnknownState: false
}) => {
  const validatedOptions = MdutilOptionsSchema.parse(options);
  const args = ["-s", "-a"];
  if (validatedOptions.verbose) args.push("-v");
  try {
    const { stdout } = await execAsync2(`mdutil ${args.join(" ")}`);
    const volumes = stdout.split("\n\n").filter(Boolean);
    const results = volumes.map((volume) => {
      const pathMatch = volume.match(/^([^:]+):/);
      const volumePath = pathMatch?.[1] ?? "/";
      return parseIndexingStatus(volume, volumePath);
    });
    return results.filter((result) => {
      if (validatedOptions.excludeSystemVolumes && result.isSystemVolume) return false;
      if (validatedOptions.excludeUnknownState && result.state === "unknown") return false;
      return true;
    });
  } catch (error) {
    if (error instanceof Error) {
      const requiresRoot = error.message.includes("Operation not permitted");
      throw new MdutilError(
        `Failed to get all volumes status: ${error.message}`,
        error.message,
        requiresRoot
      );
    }
    throw error;
  }
};
var getIndexedEntries = async (volumePath) => {
  try {
    const resolvedPath = resolve(volumePath);
    const query = `kMDItemPath == "${resolvedPath}"* || kMDItemPath == "${resolvedPath}"`;
    return await mdfind(query);
  } catch (error) {
    if (error instanceof Error) {
      throw new MdutilError(
        `Failed to check indexed entries: ${error.message}`,
        error.message,
        false
      );
    }
    throw error;
  }
};
var setIndexing = async (volumePath, enable) => {
  try {
    const resolvedPath = resolve(volumePath);
    const escapedPath = escapeShellPath(resolvedPath);
    await execAsync2(`mdutil -i ${enable ? "on" : "off"} "${escapedPath}"`);
    const status = await getIndexingStatus(resolvedPath);
    const success = status.enabled === enable;
    const remainingEntries = !enable ? await getIndexedEntries(resolvedPath) : [];
    return { success, remainingEntries };
  } catch (error) {
    if (error instanceof Error) {
      if (error.message.includes("invalid operation")) {
        throw new MdutilError("Operation not permitted on this path", error.message, false);
      }
      if (error.message.includes("unknown indexing state")) {
        throw new MdutilError("Path is not eligible for Spotlight indexing", error.message, false);
      }
      const requiresRoot = error.message.includes("Operation not permitted");
      throw new MdutilError(
        `Failed to ${enable ? "enable" : "disable"} indexing: ${error.message}`,
        error.message,
        requiresRoot
      );
    }
    throw error;
  }
};
var eraseAndRebuildIndex = async (volumePath) => {
  try {
    const resolvedPath = resolve(volumePath);
    const escapedPath = escapeShellPath(resolvedPath);
    await execAsync2(`mdutil -E "${escapedPath}"`);
  } catch (error) {
    if (error instanceof Error) {
      if (error.message.includes("invalid operation")) {
        throw new MdutilError("Operation not permitted on this path", error.message, false);
      }
      const requiresRoot = error.message.includes("Operation not permitted");
      throw new MdutilError(
        `Failed to erase and rebuild index: ${error.message}`,
        error.message,
        requiresRoot
      );
    }
    throw error;
  }
};
var listIndexContents = async (volumePath) => {
  try {
    const resolvedPath = resolve(volumePath);
    const escapedPath = escapeShellPath(resolvedPath);
    const { stdout } = await execAsync2(`mdutil -L "${escapedPath}"`);
    return stdout.trim();
  } catch (error) {
    if (error instanceof Error) {
      const requiresRoot = error.message.includes("Must be root");
      throw new MdutilError(
        `Failed to list index contents: ${error.message}`,
        error.message,
        requiresRoot
      );
    }
    throw error;
  }
};
var getVolumeConfig = async (volumePath) => {
  try {
    const resolvedPath = resolve(volumePath);
    const escapedPath = escapeShellPath(resolvedPath);
    const { stdout } = await execAsync2(`mdutil -P "${escapedPath}"`);
    return stdout.trim();
  } catch (error) {
    if (error instanceof Error) {
      const requiresRoot = error.message.includes("Must be root");
      throw new MdutilError(
        `Failed to get volume config: ${error.message}`,
        error.message,
        requiresRoot
      );
    }
    throw error;
  }
};
var removeIndexDirectory = async (volumePath) => {
  try {
    const resolvedPath = resolve(volumePath);
    const escapedPath = escapeShellPath(resolvedPath);
    await execAsync2(`mdutil -X "${escapedPath}"`);
  } catch (error) {
    if (error instanceof Error) {
      const requiresRoot = error.message.includes("Operation not permitted");
      throw new MdutilError(
        `Failed to remove index directory: ${error.message}`,
        error.message,
        requiresRoot
      );
    }
    throw error;
  }
};
var enableIndexing = async (directory = "/") => {
  await setIndexing(directory, true);
};
var disableIndexing = async (directory = "/") => {
  await setIndexing(directory, false);
};
var eraseIndex = (directory = "/") => eraseAndRebuildIndex(directory);

// src/mdimport.ts
import { execFile } from "node:child_process";
import { promisify as promisify3 } from "node:util";
import { z as z11 } from "zod";
var execFileAsync = promisify3(execFile);
var MdimportError = class extends Error {
  constructor(message, stderr, requiresRoot = false) {
    super(message);
    this.stderr = stderr;
    this.requiresRoot = requiresRoot;
  }
  name = "MdimportError";
};
var MdimportDebugLevel = {
  /** Print summary of test import */
  SUMMARY: 1,
  /** Print summary and all attributes (except kMDItemTextContent) */
  ATTRIBUTES: 2,
  /** Print summary and all attributes (including kMDItemTextContent) */
  FULL: 3
};
var MdimportOptionsSchema3 = z11.object({
  /**
   * Test import without storing in index
   * When true, the import is simulated and attributes are returned without modifying the index
   * @default false
   */
  test: z11.boolean().default(false),
  /**
   * Debug level (requires test mode)
   * - 1: Print summary of test import
   * - 2: Print summary and all attributes (except kMDItemTextContent)
   * - 3: Print summary and all attributes (including kMDItemTextContent)
   */
  debugLevel: z11.enum(["1", "2", "3"]).optional(),
  /**
   * Output file for test results (requires test mode)
   */
  outputFile: z11.string().optional(),
  /**
   * Show performance information (requires test mode)
   * @default false
   */
  showPerformance: z11.boolean().default(false),
  /**
   * Maximum buffer size for output
   * @default 512KB
   */
  maxBuffer: z11.number().default(1024 * 512),
  /**
   * Force immediate indexing
   * Note: This is the default behavior if no other flags are specified
   * @default true
   */
  immediate: z11.boolean().default(true),
  /**
   * Recursively import directories
   * Note: This is always true for directory imports
   * @default true
   */
  recursive: z11.boolean().default(true)
}).strict().refine(
  (data) => {
    if (!data.test) {
      return !data.debugLevel && !data.outputFile && !data.showPerformance;
    }
    return true;
  },
  {
    message: "Debug level, output file, and performance options require test mode (-t)"
  }
);
async function mdimport(paths, options = {}) {
  const opts = MdimportOptionsSchema3.parse(options);
  const args = [];
  if (opts.test) {
    args.push("-t");
    if (opts.debugLevel) {
      args.push("-d", opts.debugLevel);
    }
    if (opts.outputFile) {
      args.push("-o", opts.outputFile);
    }
    if (opts.showPerformance) {
      args.push("-p");
    }
  } else if (opts.immediate) {
    args.push("-i");
  }
  const pathArray = Array.isArray(paths) ? paths : [paths];
  args.push(...pathArray);
  try {
    const { stdout, stderr } = await execFileAsync("mdimport", args, {
      maxBuffer: opts.maxBuffer
    });
    const isError = stderr !== "" && !stderr.includes("Loading keywords");
    if (isError) {
      throw new MdimportError("mdimport command failed", stderr);
    }
    return stdout.trim();
  } catch (error) {
    if (error instanceof Error) {
      const requiresRoot = error.message.includes("Operation not permitted");
      throw new MdimportError(
        `Failed to import: ${error.message}`,
        error instanceof MdimportError ? error.stderr : error.message,
        requiresRoot
      );
    }
    throw error;
  }
}
async function listImporters() {
  try {
    const { stdout, stderr } = await execFileAsync("mdimport", ["-L"]);
    if (stderr) {
      throw new MdimportError("Failed to list importers", stderr);
    }
    return stdout.trim().split("\n").filter((line) => line.length > 0);
  } catch (error) {
    if (error instanceof Error) {
      throw new MdimportError(
        "Failed to list importers",
        error instanceof MdimportError ? error.stderr : error.message
      );
    }
    throw error;
  }
}
async function listAttributes() {
  try {
    const { stdout, stderr } = await execFileAsync("mdimport", ["-A"]);
    if (stderr) {
      throw new MdimportError("Failed to list attributes", stderr);
    }
    return stdout.trim().split("\n").filter((line) => line.length > 0);
  } catch (error) {
    if (error instanceof Error) {
      throw new MdimportError(
        "Failed to list attributes",
        error instanceof MdimportError ? error.stderr : error.message
      );
    }
    throw error;
  }
}
async function getSchema() {
  try {
    const { stdout, stderr } = await execFileAsync("mdimport", ["-X"]);
    if (stderr) {
      throw new MdimportError("Failed to get schema", stderr);
    }
    return stdout.trim();
  } catch (error) {
    if (error instanceof Error) {
      throw new MdimportError(
        "Failed to get schema",
        error instanceof MdimportError ? error.stderr : error.message
      );
    }
    throw error;
  }
}
async function reimportForImporter(importerPath) {
  try {
    const { stdout, stderr } = await execFileAsync("mdimport", ["-r", importerPath]);
    if (stderr) {
      throw new MdimportError("Failed to reimport files", stderr);
    }
    return stdout.trim();
  } catch (error) {
    if (error instanceof Error) {
      const requiresRoot = error.message.includes("Operation not permitted");
      throw new MdimportError(
        "Failed to reimport files",
        error instanceof MdimportError ? error.stderr : error.message,
        requiresRoot
      );
    }
    throw error;
  }
}

// src/query-builder.ts
import { spawn as spawn3 } from "node:child_process";
import { EventEmitter } from "node:events";
import { homedir as homedir2 } from "node:os";
import process3 from "node:process";
import { clearTimeout, setTimeout } from "node:timers";
var QueryBuilder = class {
  query = [];
  options = {
    // Core options
    maxBuffer: 1024 * 512,
    literal: false,
    interpret: false,
    // Search options
    live: false,
    operator: "&&",
    count: false,
    reprint: false,
    nullSeparator: false,
    // Filter options (all optional, so not needed in default)
    names: [],
    attributes: []
  };
  /**
   * Create a new QueryBuilder instance
   */
  constructor(options) {
    const parsedOpts = MdfindOptionsInputSchema.parse(options ?? {});
    this.options = {
      ...this.options,
      ...parsedOpts
    };
  }
  /**
   * Add a raw query condition.
   * Useful for complex conditions or custom metadata attributes.
   *
   * @param {string} condition - Raw Spotlight query condition
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .where('kMDItemPixelHeight > 1080')
   *   .where('kMDItemPixelWidth > 1920')
   *   .execute()
   * ```
   */
  where(condition) {
    this.query.push(condition);
    return this;
  }
  /**
   * Filter by content type (UTI).
   * Common types include:
   * - public.image
   * - public.audio
   * - public.movie
   * - public.pdf
   * - public.plain-text
   * - public.rtf
   * - public.html
   * - public.font
   *
   * @param {string} type - Uniform Type Identifier (UTI)
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const images = await new QueryBuilder()
   *   .contentType('public.image')
   *   .execute()
   * ```
   */
  contentType(type) {
    this.query.push(`kMDItemContentType == "${type}"`);
    return this;
  }
  /**
   * Set the name pattern for file matching
   */
  named(pattern) {
    this.options.name = pattern;
    return this;
  }
  /**
   * Set the directory to search in
   */
  inDirectory(path) {
    this.options.onlyIn = path;
    return this;
  }
  /**
   * Filter by creation date.
   *
   * @param {Date} date - Date to compare against
   * @returns {this} The builder instance for chaining
   */
  createdAfter(date) {
    this.query.push(`kMDItemContentCreationDate > "${date.toISOString()}"`);
    return this;
  }
  /**
   * Filter by creation date.
   *
   * @param {Date} date - Date to compare against
   * @returns {this} The builder instance for chaining
   */
  createdBefore(date) {
    this.query.push(`kMDItemContentCreationDate < "${date.toISOString()}"`);
    return this;
  }
  /**
   * Filter by modification date.
   *
   * @param {Date} date - Date to compare against
   * @returns {this} The builder instance for chaining
   */
  modifiedAfter(date) {
    this.query.push(`kMDItemContentModificationDate > "${date.toISOString()}"`);
    return this;
  }
  /**
   * Filter by modification date.
   *
   * @param {Date} date - Date to compare against
   * @returns {this} The builder instance for chaining
   */
  modifiedBefore(date) {
    this.query.push(`kMDItemContentModificationDate < "${date.toISOString()}"`);
    return this;
  }
  /**
   * Filter by last opened date.
   *
   * @param {Date} date - Date to compare against
   * @returns {this} The builder instance for chaining
   */
  lastOpenedAfter(date) {
    this.query.push(`kMDItemLastUsedDate > "${date.toISOString()}"`);
    return this;
  }
  /**
   * Filter by file size in bytes.
   *
   * @param {number} bytes - Minimum file size in bytes
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .largerThan(1024 * 1024) // 1MB
   *   .execute()
   * ```
   */
  largerThan(bytes) {
    this.query.push(`kMDItemFSSize > ${bytes}`);
    return this;
  }
  /**
   * Filter by file size in bytes.
   *
   * @param {number} bytes - Maximum file size in bytes
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .smallerThan(1024 * 100) // 100KB
   *   .execute()
   * ```
   */
  smallerThan(bytes) {
    this.query.push(`kMDItemFSSize < ${bytes}`);
    return this;
  }
  /**
   * Filter by file extension.
   *
   * @param {string} ext - File extension without dot
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .extension('pdf')
   *   .execute()
   * ```
   */
  extension(ext) {
    this.query.push(`kMDItemFSName ==[c] "*.${ext}"`);
    return this;
  }
  /**
   * Filter by author name.
   *
   * @param {string} name - Author's name
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .author('John Doe')
   *   .execute()
   * ```
   */
  author(name) {
    this.query.push(`kMDItemAuthors == "${name}"`);
    return this;
  }
  /**
   * Filter by author or artist.
   * Alias for author() method with more descriptive name for media files.
   *
   * @param {string} name - Author or artist name
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .contentType('public.audio')
   *   .byAuthor('Radiohead')
   *   .execute()
   * ```
   */
  byAuthor(name) {
    return this.author(name);
  }
  /**
   * Filter by text content.
   *
   * @param {string} text - Text to search for
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .containing('important')
   *   .execute()
   * ```
   */
  containing(text) {
    this.query.push(`kMDItemTextContent == "${text}"w`);
    return this;
  }
  /**
   * Enable natural language query interpretation.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .where('images created today')
   *   .interpret()
   *   .execute()
   * ```
   */
  interpret() {
    this.options.interpret = true;
    return this;
  }
  /**
   * Disable special query interpretation.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .where('kMDItemFSName == "*.txt"')
   *   .literal()
   *   .execute()
   * ```
   */
  literal() {
    this.options.literal = true;
    return this;
  }
  /**
   * Return only the count of matches.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const count = await new QueryBuilder()
   *   .contentType('public.image')
   *   .count()
   *   .execute()
   * ```
   */
  count() {
    this.options.count = true;
    return this;
  }
  /**
   * Return specific metadata attributes.
   *
   * @param {string} name - Spotlight metadata attribute
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const metadata = await new QueryBuilder()
   *   .contentType('public.image')
   *   .attribute('kMDItemPixelHeight')
   *   .execute()
   * ```
   */
  attribute(name) {
    this.options.attr = name;
    return this;
  }
  /**
   * Filter for files that have GPS data.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .contentType('public.image')
   *   .hasGPS()
   *   .execute()
   * ```
   */
  hasGPS() {
    this.query.push("kMDItemLatitude != null && kMDItemLongitude != null");
    return this;
  }
  /**
   * Filter for audio files with minimum quality requirements.
   *
   * @param {number} sampleRate - Minimum sample rate in Hz (e.g., 44100)
   * @param {number} bitRate - Minimum bit rate in bits/second (e.g., 320000)
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .contentType('public.audio')
   *   .minAudioQuality(44100, 320000)
   *   .execute()
   * ```
   */
  minAudioQuality(sampleRate, bitRate) {
    this.query.push(`kMDItemAudioSampleRate >= ${sampleRate} && kMDItemAudioBitRate >= ${bitRate}`);
    return this;
  }
  /**
   * Set the operator for combining conditions
   */
  useOperator(op) {
    this.options.operator = op;
    return this;
  }
  /**
   * Filter by keyword in content or metadata.
   *
   * @param {string} keyword - Keyword to search for
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .hasKeyword('typescript')
   *   .execute()
   * ```
   */
  hasKeyword(keyword) {
    this.query.push(`(kMDItemTextContent == "${keyword}"w || kMDItemKeywords == "${keyword}")`);
    return this;
  }
  /**
   * Filter images by minimum dimensions.
   *
   * @param {number} width - Minimum width in pixels
   * @param {number} height - Minimum height in pixels
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .contentType('public.image')
   *   .minImageDimensions(1920, 1080)
   *   .execute()
   * ```
   */
  minImageDimensions(width, height) {
    this.query.push(`kMDItemPixelWidth >= ${width} && kMDItemPixelHeight >= ${height}`);
    return this;
  }
  /**
   * Filter for application bundles.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const apps = await new QueryBuilder()
   *   .isApplication()
   *   .execute()
   * ```
   */
  isApplication() {
    this.query.push('kMDItemContentType == "com.apple.application-bundle"');
    return this;
  }
  /**
   * Filter for system preference panes.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const prefs = await new QueryBuilder()
   *   .isPreferencePane()
   *   .execute()
   * ```
   */
  isPreferencePane() {
    this.query.push('kMDItemContentType == "com.apple.systempreference"');
    return this;
  }
  /**
   * Filter by Finder label color.
   *
   * @param {number} label - Label index (0-7)
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .hasLabel(2) // Red label
   *   .execute()
   * ```
   */
  hasLabel(label) {
    this.query.push(`kMDItemFSLabel == ${label}`);
    return this;
  }
  /**
   * Filter for invisible files.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .isInvisible()
   *   .execute()
   * ```
   */
  isInvisible() {
    this.query.push("kMDItemFSInvisible == 1");
    return this;
  }
  /**
   * Filter by file owner.
   *
   * @param {number} uid - User ID
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .ownedBy(501) // Standard user ID
   *   .execute()
   * ```
   */
  ownedBy(uid) {
    this.query.push(`kMDItemFSOwnerUserID == ${uid}`);
    return this;
  }
  /**
   * Filter by encoding application.
   *
   * @param {string} appName - Application name
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .encodedBy('Adobe Photoshop')
   *   .execute()
   * ```
   */
  encodedBy(appName) {
    this.query.push(`kMDItemEncodingApplications == "${appName}"`);
    return this;
  }
  /**
   * Filter by musical genre.
   *
   * @param {string} genre - Musical genre
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .contentType('public.audio')
   *   .inGenre('Jazz')
   *   .execute()
   * ```
   */
  inGenre(genre) {
    this.query.push(`kMDItemMusicalGenre == "${genre}"`);
    return this;
  }
  /**
   * Filter by recording year.
   *
   * @param {number} year - Recording year
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .contentType('public.audio')
   *   .recordedIn(2024)
   *   .execute()
   * ```
   */
  recordedIn(year) {
    this.query.push(`kMDItemRecordingYear == ${year}`);
    return this;
  }
  /**
   * Filter by album name.
   *
   * @param {string} name - Album name
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .contentType('public.audio')
   *   .inAlbum('Greatest Hits')
   *   .execute()
   * ```
   */
  inAlbum(name) {
    this.query.push(`kMDItemAlbum == "${name}"`);
    return this;
  }
  /**
   * Filter by composer.
   *
   * @param {string} name - Composer name
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .contentType('public.audio')
   *   .byComposer('Mozart')
   *   .execute()
   * ```
   */
  byComposer(name) {
    this.query.push(`kMDItemComposer == "${name}"`);
    return this;
  }
  /**
   * Filter by camera make.
   *
   * @param {string} make - Camera manufacturer
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .contentType('public.image')
   *   .takenWith('Canon')
   *   .execute()
   * ```
   */
  takenWith(make) {
    this.query.push(`kMDItemAcquisitionMake == "${make}"`);
    return this;
  }
  /**
   * Filter by camera model.
   *
   * @param {string} model - Camera model
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .contentType('public.image')
   *   .usingModel('EOS R5')
   *   .execute()
   * ```
   */
  usingModel(model) {
    this.query.push(`kMDItemAcquisitionModel == "${model}"`);
    return this;
  }
  /**
   * Filter by ISO speed.
   *
   * @param {number} min - Minimum ISO speed
   * @param {number} max - Maximum ISO speed
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .contentType('public.image')
   *   .withISO(100, 400)
   *   .execute()
   * ```
   */
  withISO(min, max) {
    this.query.push(`kMDItemISOSpeed >= ${min} && kMDItemISOSpeed <= ${max}`);
    return this;
  }
  /**
   * Filter by focal length.
   *
   * @param {number} min - Minimum focal length in mm
   * @param {number} max - Maximum focal length in mm
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .contentType('public.image')
   *   .withFocalLength(24, 70)
   *   .execute()
   * ```
   */
  withFocalLength(min, max) {
    this.query.push(`kMDItemFocalLength >= ${min} && kMDItemFocalLength <= ${max}`);
    return this;
  }
  /**
   * Filter by color space.
   *
   * @param {string} colorSpace - Color space name (e.g., 'RGB', 'CMYK')
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .contentType('public.image')
   *   .inColorSpace('RGB')
   *   .execute()
   * ```
   */
  inColorSpace(colorSpace) {
    this.query.push(`kMDItemColorSpace == "${colorSpace}"`);
    return this;
  }
  /**
   * Filter by bits per sample.
   *
   * @param {number} bits - Bits per sample
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .contentType('public.image')
   *   .withBitDepth(16)
   *   .execute()
   * ```
   */
  withBitDepth(bits) {
    this.query.push(`kMDItemBitsPerSample == ${bits}`);
    return this;
  }
  /**
   * Set the maximum buffer size for the search results.
   *
   * @param {number} bytes - Maximum buffer size in bytes
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .maxBuffer(5 * 1024 * 1024) // 5MB buffer
   *   .execute()
   * ```
   */
  maxBuffer(bytes) {
    this.options.maxBuffer = bytes;
    return this;
  }
  /**
   * Filter for text-based content.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .isText()
   *   .execute()
   * ```
   */
  isText() {
    this.query.push('kMDItemContentTypeTree == "public.text"');
    return this;
  }
  /**
   * Filter for composite content.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .isComposite()
   *   .execute()
   * ```
   */
  isComposite() {
    this.query.push('kMDItemContentTypeTree == "public.composite-content"');
    return this;
  }
  /**
   * Filter for audiovisual content.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .isAudiovisual()
   *   .execute()
   * ```
   */
  isAudiovisual() {
    this.query.push('kMDItemContentTypeTree == "public.audiovisual-content"');
    return this;
  }
  /**
   * Filter for bundle content.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .isBundle()
   *   .execute()
   * ```
   */
  isBundle() {
    this.query.push('kMDItemContentTypeTree == "com.apple.bundle"');
    return this;
  }
  /**
   * Filter for Markdown files.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .isMarkdown()
   *   .execute()
   * ```
   */
  isMarkdown() {
    this.query.push('kMDItemContentType == "net.daringfireball.markdown"');
    return this;
  }
  /**
   * Filter for property list files.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .isPlist()
   *   .execute()
   * ```
   */
  isPlist() {
    this.query.push('kMDItemContentType == "com.apple.property-list"');
    return this;
  }
  /**
   * Filter for PDF documents.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .isPDF()
   *   .execute()
   * ```
   */
  isPDF() {
    this.query.push('kMDItemContentType == "com.adobe.pdf"');
    return this;
  }
  /**
   * Filter for JSON files.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .isJSON()
   *   .execute()
   * ```
   */
  isJSON() {
    this.query.push('kMDItemContentType == "public.json"');
    return this;
  }
  /**
   * Filter for YAML files.
   *
   * @returns {this} The builder instance for chaining
   *
   * @example
   * ```typescript
   * const files = await new QueryBuilder()
   *   .isYAML()
   *   .execute()
   * ```
   */
  isYAML() {
    this.query.push('kMDItemContentType == "public.yaml"');
    return this;
  }
  /**
   * Convert the query to a string
   */
  toString() {
    const query = this.query.length ? this.query.join(` ${this.options.operator} `) : "";
    const args = [];
    if (this.options.name) {
      args.push(`kMDItemFSName ==[c] "${this.options.name}"`);
    }
    return args.length ? query ? `${query} ${this.options.operator} ${args.join(" ")}` : args.join(" ") : query;
  }
  /**
   * Execute the search query
   */
  execute() {
    const args = [...this.query];
    const command = "mdfind";
    if (this.options.onlyIn) {
      args.push("-onlyin", this.options.onlyIn.replace(/^~/, homedir2()));
    }
    if (this.options.live) {
      return new Promise((resolve2, reject) => {
        const results = [];
        const emitter = new EventEmitter();
        const child = spawn3(command, ["-live", ...args]);
        let timeoutId;
        if (typeof this.options.timeout === "number") {
          timeoutId = setTimeout(() => {
            child.kill();
            emitter.emit("done");
          }, this.options.timeout);
        }
        child.stdout.setEncoding("utf8");
        child.stdout.on("data", (data) => {
          const lines = data.trim().split("\n");
          for (const line of lines) {
            if (line.length > 0) {
              results.push(line);
              emitter.emit("result", line);
            }
          }
        });
        child.stderr.on("data", (data) => {
          reject(new Error(`mdfind error: ${data}`));
        });
        child.on("close", (code) => {
          if (code !== null && code !== 0) {
            reject(new Error(`mdfind exited with code ${code}`));
          }
          emitter.emit("done");
        });
        emitter.on("done", () => {
          if (timeoutId !== void 0) {
            clearTimeout(timeoutId);
          }
          resolve2(results);
        });
        const cleanup = () => {
          child.kill();
          if (timeoutId !== void 0) {
            clearTimeout(timeoutId);
          }
        };
        process3.on("SIGINT", cleanup);
        process3.on("SIGTERM", cleanup);
        process3.on("exit", cleanup);
      });
    }
    return new Promise((resolve2, reject) => {
      const results = [];
      const child = spawn3(command, args);
      child.stdout.setEncoding("utf8");
      child.stdout.on("data", (data) => {
        const lines = data.trim().split("\n");
        for (const line of lines) {
          if (line.length > 0) {
            results.push(line);
          }
        }
      });
      child.stderr.on("data", (data) => {
        reject(new Error(`mdfind error: ${data}`));
      });
      child.on("close", (code) => {
        if (code !== null && code !== 0) {
          reject(new Error(`mdfind exited with code ${code}`));
        }
        resolve2(results);
      });
    });
  }
  /**
   * Execute the search query with live updates
   * @param onResult Callback function that receives each result as it arrives
   * @param onComplete Optional callback function called when the search completes
   */
  executeLive(onResult, onComplete) {
    const args = [...this.query];
    const command = "mdfind";
    const results = [];
    const emitter = new EventEmitter();
    const child = spawn3(command, ["-live", ...args]);
    let timeoutId;
    if (typeof this.options.timeout === "number") {
      timeoutId = setTimeout(() => {
        child.kill();
        emitter.emit("done");
      }, this.options.timeout);
    }
    child.stdout.setEncoding("utf8");
    child.stdout.on("data", (data) => {
      const lines = data.trim().split("\n");
      for (const line of lines) {
        if (line.length > 0) {
          results.push(line);
          onResult(line);
        }
      }
    });
    child.stderr.on("data", (data) => {
      throw new Error(`mdfind error: ${data}`);
    });
    child.on("close", (code) => {
      if (code !== null && code !== 0) {
        throw new Error(`mdfind exited with code ${code}`);
      }
      emitter.emit("done");
    });
    emitter.on("done", () => {
      if (timeoutId !== void 0) {
        clearTimeout(timeoutId);
      }
      if (onComplete !== void 0) {
        onComplete(results);
      }
    });
    const cleanup = () => {
      child.kill();
      if (timeoutId !== void 0) {
        clearTimeout(timeoutId);
      }
    };
    process3.on("SIGINT", cleanup);
    process3.on("SIGTERM", cleanup);
    process3.on("exit", cleanup);
    return Promise.resolve();
  }
  /**
   * Filter by file types using name patterns
   */
  withFileTypes(types) {
    const pattern = `*.{${types.join(",")}}`;
    return this.named(pattern);
  }
};
var SpotlightQuery = QueryBuilder;

// src/batch.ts
var DEFAULT_BATCH_OPTIONS = {
  live: false,
  count: false,
  nullSeparator: false,
  maxBuffer: 1024 * 1024,
  reprint: false,
  literal: false,
  interpret: false,
  names: [],
  attributes: [],
  onlyInDirectory: void 0,
  smartFolder: void 0
};
async function batchSearch(searches) {
  const results = await Promise.all(
    searches.map(
      ({ query, options = {} }) => mdfind(query, {
        ...DEFAULT_BATCH_OPTIONS,
        ...options
      })
    )
  );
  return results;
}
async function batchSearchSequential(searches) {
  const results = [];
  for (const { query, options = {} } of searches) {
    const result = await mdfind(query, {
      ...DEFAULT_BATCH_OPTIONS,
      ...options
    });
    results.push(result);
  }
  return results;
}

// src/batch-utils.ts
function mdfindMultiDirectory(query, directories, options = {}) {
  const searches = directories.map((dir) => ({
    query,
    options: { ...options, onlyInDirectory: dir }
  }));
  return batchSearch(searches);
}
function mdfindMultiQuery(queries, directory, options = {}) {
  const searches = queries.map((query) => ({
    query,
    options: { ...options, onlyInDirectory: directory }
  }));
  return batchSearch(searches);
}

// src/metadata.ts
async function getBasicMetadata(filePath) {
  const result = await getMetadata(filePath, { structured: true });
  const metadata = result.spotlight;
  return BasicMetadataSchema.parse({
    name: metadata.kMDItemDisplayName ?? metadata.kMDItemFSName ?? "",
    contentType: metadata.kMDItemContentType,
    kind: metadata.kMDItemKind,
    size: metadata.kMDItemFSSize ?? 0,
    created: metadata.kMDItemContentCreationDate,
    modified: metadata.kMDItemContentModificationDate,
    lastOpened: metadata.kMDItemLastUsedDate
  });
}
async function getExifData(filePath) {
  const result = await getMetadata(filePath, { structured: true });
  const metadata = result.spotlight;
  return ExifDataSchema.parse({
    make: metadata.kMDItemAcquisitionMake,
    model: metadata.kMDItemAcquisitionModel,
    lens: metadata.kMDItemLensModel,
    exposureTime: metadata.kMDItemExposureTimeSeconds,
    fNumber: metadata.kMDItemFNumber,
    isoSpeedRatings: metadata.kMDItemISOSpeed,
    focalLength: metadata.kMDItemFocalLength,
    gpsLatitude: metadata.kMDItemLatitude,
    gpsLongitude: metadata.kMDItemLongitude,
    gpsAltitude: metadata.kMDItemAltitude
  });
}
async function getXMPData(filePath) {
  const result = await getMetadata(filePath, { structured: true });
  const metadata = result.spotlight;
  return XMPDataSchema.parse({
    title: metadata.kMDItemTitle,
    description: metadata.kMDItemDescription,
    creator: Array.isArray(metadata.kMDItemAuthors) ? metadata.kMDItemAuthors[0] : metadata.kMDItemAuthors,
    subject: metadata.kMDItemKeywords,
    createDate: metadata.kMDItemContentCreationDate,
    modifyDate: metadata.kMDItemContentModificationDate,
    metadataDate: metadata.kMDItemAttributeChangeDate,
    copyrightNotice: metadata.kMDItemCopyright,
    rights: metadata.kMDItemRights,
    webStatement: metadata.kMDItemURL
  });
}
async function getExtendedMetadata(filePath) {
  const [basic, exif, xmp] = await Promise.all([
    getBasicMetadata(filePath),
    getExifData(filePath).catch(() => ({})),
    getXMPData(filePath).catch(() => ({}))
  ]);
  return {
    basic,
    exif,
    xmp
  };
}

// src/discover.ts
import { execSync } from "node:child_process";

// src/schemas/core/attributes.ts
import { z as z12 } from "zod";
var AttributeDefinitionSchema = z12.object({
  name: z12.string(),
  description: z12.string(),
  type: z12.enum(["string", "number", "date", "boolean", "array"]),
  example: z12.union([z12.string(), z12.number(), z12.boolean(), z12.array(z12.string())]).optional(),
  category: z12.enum(["general", "document", "media", "image", "audio", "location", "system"])
});
var CONTENT_TYPES = {
  "public.item": "Base type for all items",
  "public.content": "Base type for all content",
  "public.data": "Generic data files",
  "public.text": "Text-based content",
  "public.composite-content": "Content with multiple parts",
  // Images
  "public.image": "Image files (JPEG, PNG, etc.)",
  "public.jpeg": "JPEG Image",
  "public.png": "PNG Image",
  "public.heic": "HEIC Image",
  "com.apple.icns": "Apple Icon Image",
  // Audio/Video
  "public.audio": "Audio files (MP3, WAV, etc.)",
  "public.movie": "Video files (MP4, MOV, etc.)",
  "public.audiovisual-content": "Audio/Visual content",
  "public.mp3": "MP3 Audio",
  "public.mp4": "MP4 Video",
  "public.mpeg-4": "MPEG-4 Media",
  "public.mpeg-2-transport-stream": "MPEG-2 Transport Stream",
  "com.apple.quicktime-movie": "QuickTime Movie",
  // Documents
  "public.plain-text": "Plain text files",
  "public.rtf": "Rich Text Format documents",
  "public.html": "HTML documents",
  "public.xml": "XML documents",
  "public.pdf": "PDF documents",
  "com.adobe.pdf": "Adobe PDF Document",
  "net.daringfireball.markdown": "Markdown Document",
  // Code
  "public.source-code": "Source Code File",
  "public.shell-script": "Shell Script",
  "public.swift-source": "Swift Source File",
  "public.python-script": "Python Script",
  "public.json": "JSON File",
  "public.yaml": "YAML File",
  // Bundles and Packages
  "public.directory": "Directory/Folder",
  "public.folder": "Folders/Directories",
  "com.apple.bundle": "Generic Bundle",
  "com.apple.package": "macOS Package Bundle",
  "com.apple.application": "Generic Application",
  "com.apple.application-bundle": "macOS Application Bundle",
  "com.apple.application-file": "macOS Application File",
  "com.apple.localizable-name-bundle": "Bundle with Localizable Name",
  // System
  "public.executable": "Executable files",
  "com.apple.property-list": "Property List (plist)",
  "com.apple.systempreference": "System Preference",
  "com.apple.plugin": "Plugin Bundle",
  "com.apple.framework": "Framework Bundle",
  // Archives and Data
  "public.archive": "Archive files (ZIP, etc.)",
  "public.font": "Font files",
  // Apple iWork
  "com.apple.keynote.key": "Keynote Presentation",
  "com.apple.numbers.numbers": "Numbers Spreadsheet",
  "com.apple.pages.pages": "Pages Document",
  // Mail
  "com.apple.mail.emlx": "Apple Mail Message"
};
var SPOTLIGHT_ATTRIBUTES = [
  // General attributes
  {
    name: "kMDItemContentType",
    description: "The type of content (see CONTENT_TYPES)",
    type: "string",
    example: "public.image",
    category: "general"
  },
  {
    name: "kMDItemDisplayName",
    description: "The display name of the file",
    type: "string",
    example: "example.jpg",
    category: "general"
  },
  {
    name: "kMDItemFSName",
    description: "The filename on disk",
    type: "string",
    example: "example.jpg",
    category: "general"
  },
  {
    name: "kMDItemFSSize",
    description: "File size in bytes",
    type: "number",
    example: 1024,
    category: "general"
  },
  {
    name: "kMDItemContentCreationDate",
    description: "When the file was created",
    type: "date",
    category: "general"
  },
  {
    name: "kMDItemContentModificationDate",
    description: "When the file was last modified",
    type: "date",
    category: "general"
  },
  {
    name: "kMDItemLastUsedDate",
    description: "When the file was last opened",
    type: "date",
    category: "general"
  },
  {
    name: "kMDItemContentTypeTree",
    description: "Hierarchy of content types",
    type: "array",
    example: ["public.image", "public.jpeg"],
    category: "general"
  },
  {
    name: "kMDItemKind",
    description: "Localized description of the file type",
    type: "string",
    example: "JPEG image",
    category: "general"
  },
  // System attributes
  {
    name: "kMDItemFSCreatorCode",
    description: "Classic Mac OS creator code",
    type: "string",
    category: "system"
  },
  {
    name: "kMDItemFSTypeCode",
    description: "Classic Mac OS type code",
    type: "string",
    category: "system"
  },
  {
    name: "kMDItemFSNodeCount",
    description: "Number of items in a folder",
    type: "number",
    category: "system"
  },
  {
    name: "kMDItemFSOwnerUserID",
    description: "User ID of the file owner",
    type: "number",
    category: "system"
  },
  {
    name: "kMDItemFSOwnerGroupID",
    description: "Group ID of the file owner",
    type: "number",
    category: "system"
  },
  {
    name: "kMDItemFSHasCustomIcon",
    description: "Whether the file has a custom icon",
    type: "boolean",
    category: "system"
  },
  {
    name: "kMDItemFSIsStationery",
    description: "Whether the file is a stationery pad",
    type: "boolean",
    category: "system"
  },
  {
    name: "kMDItemFSInvisible",
    description: "Whether the file is invisible",
    type: "boolean",
    category: "system"
  },
  {
    name: "kMDItemFSLabel",
    description: "Finder label (0-7)",
    type: "number",
    category: "system"
  },
  // Document attributes
  {
    name: "kMDItemAuthors",
    description: "Authors of the document",
    type: "array",
    example: ["John Doe", "Jane Smith"],
    category: "document"
  },
  {
    name: "kMDItemTitle",
    description: "Title of the document",
    type: "string",
    example: "My Document",
    category: "document"
  },
  {
    name: "kMDItemKeywords",
    description: "Keywords/tags associated with the file",
    type: "array",
    example: ["vacation", "beach", "2024"],
    category: "document"
  },
  {
    name: "kMDItemTextContent",
    description: "Searchable text content",
    type: "string",
    category: "document"
  },
  {
    name: "kMDItemEncodingApplications",
    description: "Applications that created/modified the file",
    type: "array",
    category: "document"
  },
  {
    name: "kMDItemLanguages",
    description: "Languages used in the content",
    type: "array",
    example: ["en", "fr"],
    category: "document"
  },
  {
    name: "kMDItemCopyright",
    description: "Copyright information",
    type: "string",
    category: "document"
  },
  {
    name: "kMDItemNumberOfPages",
    description: "Number of pages",
    type: "number",
    category: "document"
  },
  // Image attributes
  {
    name: "kMDItemPixelHeight",
    description: "Height of the image in pixels",
    type: "number",
    example: 1080,
    category: "image"
  },
  {
    name: "kMDItemPixelWidth",
    description: "Width of the image in pixels",
    type: "number",
    example: 1920,
    category: "image"
  },
  {
    name: "kMDItemColorSpace",
    description: "Color space of the image",
    type: "string",
    example: "RGB",
    category: "image"
  },
  {
    name: "kMDItemBitsPerSample",
    description: "Bits per color sample",
    type: "number",
    example: 8,
    category: "image"
  },
  {
    name: "kMDItemFlashOnOff",
    description: "Whether flash was used",
    type: "boolean",
    category: "image"
  },
  {
    name: "kMDItemFocalLength",
    description: "Focal length of the lens (mm)",
    type: "number",
    category: "image"
  },
  {
    name: "kMDItemAcquisitionMake",
    description: "Camera manufacturer",
    type: "string",
    category: "image"
  },
  {
    name: "kMDItemAcquisitionModel",
    description: "Camera model",
    type: "string",
    category: "image"
  },
  {
    name: "kMDItemISOSpeed",
    description: "ISO speed rating",
    type: "number",
    category: "image"
  },
  {
    name: "kMDItemOrientation",
    description: "Image orientation (1-8)",
    type: "number",
    category: "image"
  },
  {
    name: "kMDItemLayerNames",
    description: "Names of layers in the image",
    type: "array",
    category: "image"
  },
  // Audio attributes
  {
    name: "kMDItemAudioBitRate",
    description: "Audio bit rate in bits per second",
    type: "number",
    example: 32e4,
    category: "audio"
  },
  {
    name: "kMDItemAudioChannelCount",
    description: "Number of audio channels",
    type: "number",
    example: 2,
    category: "audio"
  },
  {
    name: "kMDItemAudioSampleRate",
    description: "Audio sample rate in Hz",
    type: "number",
    example: 44100,
    category: "audio"
  },
  {
    name: "kMDItemMusicalGenre",
    description: "Musical genre",
    type: "string",
    category: "audio"
  },
  {
    name: "kMDItemRecordingYear",
    description: "Year the audio was recorded",
    type: "number",
    category: "audio"
  },
  {
    name: "kMDItemComposer",
    description: "Music composer",
    type: "string",
    category: "audio"
  },
  {
    name: "kMDItemAlbum",
    description: "Album name",
    type: "string",
    category: "audio"
  },
  {
    name: "kMDItemAudioTrackNumber",
    description: "Track number in album",
    type: "number",
    category: "audio"
  },
  // Location attributes
  {
    name: "kMDItemLatitude",
    description: "GPS latitude where photo/video was taken",
    type: "number",
    example: 37.7749,
    category: "location"
  },
  {
    name: "kMDItemLongitude",
    description: "GPS longitude where photo/video was taken",
    type: "number",
    example: -122.4194,
    category: "location"
  },
  {
    name: "kMDItemAltitude",
    description: "GPS altitude in meters",
    type: "number",
    category: "location"
  },
  {
    name: "kMDItemTimestamp",
    description: "When the location was recorded",
    type: "date",
    category: "location"
  },
  {
    name: "kMDItemSpeed",
    description: "Speed in meters per second",
    type: "number",
    category: "location"
  },
  {
    name: "kMDItemGPSTrack",
    description: "Direction of travel (degrees)",
    type: "number",
    category: "location"
  },
  {
    name: "kMDItemCity",
    description: "City name",
    type: "string",
    category: "location"
  },
  {
    name: "kMDItemStateOrProvince",
    description: "State or province name",
    type: "string",
    category: "location"
  },
  {
    name: "kMDItemCountry",
    description: "Country name",
    type: "string",
    category: "location"
  }
];
var getAttributeDefinition = (name) => {
  return SPOTLIGHT_ATTRIBUTES.find((attr) => attr.name === name);
};
var getAttributesByCategory = (category) => {
  return SPOTLIGHT_ATTRIBUTES.filter((attr) => attr.category === category);
};
var getContentTypeDescription = (contentType) => {
  return CONTENT_TYPES[contentType];
};

// src/discover.ts
var discoverAttributes = (filePath) => {
  try {
    const output = execSync(`mdimport -A "${filePath}"`, { encoding: "utf8" });
    const attributes = {};
    output.split("\n").forEach((line) => {
      const match = line.match(/^\s*([kMD]\w+)\s*=\s*(.+)$/);
      if (match?.[1] && match[2]) {
        const [, name, description] = match;
        attributes[name] = description.trim();
      }
    });
    return attributes;
  } catch (error) {
    if (error instanceof Error) {
      throw new Error(`Failed to discover attributes: ${error.message}`);
    }
    throw error;
  }
};
var getContentTypes = () => CONTENT_TYPES;
var getSpotlightAttributes = () => SPOTLIGHT_ATTRIBUTES;
var searchAttributes = (query) => {
  const lowerQuery = query.toLowerCase();
  return SPOTLIGHT_ATTRIBUTES.filter(
    (attr) => attr.name.toLowerCase().includes(lowerQuery) || attr.description.toLowerCase().includes(lowerQuery)
  );
};
export {
  MdfindError,
  MdimportDebugLevel,
  MdimportError,
  MdimportOptionsSchema3 as MdimportOptionsSchema,
  MdutilError,
  QueryBuilder,
  SpotlightQuery,
  batchSearch,
  batchSearchSequential,
  disableIndexing,
  discoverAttributes,
  enableIndexing,
  eraseAndRebuildIndex,
  eraseIndex,
  getAllVolumesStatus,
  getAttributeDefinition,
  getAttributesByCategory,
  getBasicMetadata,
  getContentTypeDescription,
  getContentTypes,
  getExifData,
  getExtendedMetadata,
  getIndexedEntries,
  getIndexingStatus,
  getMetadata,
  getSchema,
  getSpotlightAttributes,
  getVolumeConfig,
  getXMPData,
  listAttributes,
  listImporters,
  listIndexContents,
  mdfind,
  mdfindCount,
  mdfindLive,
  mdfindMultiDirectory,
  mdfindMultiQuery,
  mdimport,
  reimportForImporter,
  removeIndexDirectory,
  searchAttributes,
  setIndexing
};
//# sourceMappingURL=index.js.map