# get_app_cache_info

Reports per-application cache directory sizes on macOS (~/Library/Caches) or Windows (%LOCALAPPDATA% / %TEMP%) without deleting anything. Read-only counterpart to clear_app_cache.

## Metadata

#### Name

get_app_cache_info

#### Updated

3 weeks ago

#### Source

[GitHub source](https://github.com/idemeum/skills/blob/main/getAppCacheInfo.ts)

#### Risk

Low

#### Requires consent

false

#### Affected scope

user

## Code

```typescript
/**
 * mcp/skills/getAppCacheInfo.ts — get_app_cache_info skill
 *
 * Reports application cache directory sizes (per app) without modifying
 * anything.  Read-only counterpart to `clear_app_cache` — designed for
 * synthesis-then-confirm skills (disk-cleanup) that need a diagnostic
 * size feed for the consolidated present_preview card.
 *
 * Platform strategy
 * -----------------
 * darwin  Scans ~/Library/Caches — subdirectories per application.
 * win32   Scans %LOCALAPPDATA% and %TEMP% for app cache folders.
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/getAppCacheInfo.ts
 *
 * NOTE: enumeration logic is duplicated from `clearAppCache.ts` — keep
 * in sync if cache-directory roots or sizing approach change.
 */

import * as fs       from "fs/promises";
import * as os       from "os";
import * as nodePath from "path";

import { isDarwin, isWin32 } from "./_shared/platform";
import { getDirSizeBytes as getDirSizeBytesShared } from "./_shared/dirSize";
import { formatBytes } from "./_shared/formatBytes";
import { DARWIN_BROWSER_CACHE_DIR_NAMES, isWin32BrowserVendorDir } from "./_shared/browserCaches";
import { isDevCacheDir } from "./_shared/devCaches";

// -- Meta ---------------------------------------------------------------------

export const meta = {
  name: "get_app_cache_info",
  description:
    "Reports per-application cache directory sizes on macOS (~/Library/Caches) " +
    "or Windows (%LOCALAPPDATA% / %TEMP%) without deleting anything. Read-only " +
    "counterpart to clear_app_cache.",
  riskLevel:       "low",
  destructive:     false,
  requiresConsent: false,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   false,
  tccCategories:   ["FullDiskAccess"],
  timeoutMs:       180_000,
  schema:          {},
} as const;

// -- Types --------------------------------------------------------------------

export interface AppCacheEntry {
  name:      string;
  path:      string;
  sizeBytes: number;
}

export interface GetAppCacheInfoResult {
  platform:   NodeJS.Platform;
  caches:     AppCacheEntry[];
  totalBytes: number;
  totalHuman: string;
  errors?:    Array<{ scope: string; message: string }>; 
}

// -- Helpers ------------------------------------------------------------------

// -- darwin -------------------------------------------------------------------

async function getAppCacheInfoDarwin(deadlineMs: number): Promise<GetAppCacheInfoResult> {
  const cacheRoot = nodePath.join(os.homedir(), "Library", "Caches");
  const errors: GetAppCacheInfoResult["errors"] = [];

let dirents: import("fs").Dirent[];
  try {
    dirents = await fs.readdir(cacheRoot, { withFileTypes: true });
  } catch (err) {
    errors.push({ scope: "cache-root", message: (err as Error).message });
    return { platform: "darwin", caches: [], totalBytes: 0, totalHuman: formatBytes(0), errors };
  }

const subdirs = dirents.filter(
    (d) => d.isDirectory()
      && !DARWIN_BROWSER_CACHE_DIR_NAMES.has(d.name)
      && !isDevCacheDir(d.name),
  );
  let anyPartial = false;
  const caches: AppCacheEntry[] = await Promise.all(
    subdirs.map(async (d) => {
      const full = nodePath.join(cacheRoot, d.name);
      const { sizeBytes, partial } = await getDirSizeBytesShared(full, deadlineMs);
      if (partial) anyPartial = true;
      return { name: d.name, path: full, sizeBytes };
    }),
  );

caches.sort((a, b) => b.sizeBytes - a.sizeBytes);
  const totalBytes = caches.reduce((s, c) => s + c.sizeBytes, 0);

if (anyPartial) {
    errors.push({
      scope:   "deadline",
      message: "Cache scan exceeded the per-tool deadline; sizes are partial.",
    });
  }

return {
    platform: "darwin",
    caches,
    totalBytes,
    totalHuman: formatBytes(totalBytes),
    ...(errors.length > 0 ? { errors } : {}),
  };
}

// -- win32 --------------------------------------------------------------------

async function getAppCacheInfoWin32(deadlineMs: number): Promise<GetAppCacheInfoResult> {
  const localAppData = process.env["LOCALAPPDATA"] ?? nodePath.join(os.homedir(), "AppData", "Local");
  const tempDir      = process.env["TEMP"]         ?? nodePath.join(localAppData, "Temp");

const roots = [localAppData, tempDir];
  const seen  = new Set<string>();
  const errors: GetAppCacheInfoResult["errors"] = [];
  const all:  { name: string; fullPath: string }[] = [];

for (const root of roots) {
    let dirents: import("fs").Dirent[];
    try {
      dirents = await fs.readdir(root, { withFileTypes: true });
    } catch (err) {
      errors.push({ scope: root, message: (err as Error).message });
      continue;
    }
    for (const d of dirents) {
      if (!d.isDirectory() || seen.has(d.name.toLowerCase())) continue;
      if (isWin32BrowserVendorDir(d.name) || isDevCacheDir(d.name)) continue;
      seen.add(d.name.toLowerCase());
      all.push({ name: d.name, fullPath: nodePath.join(root, d.name) });
    }
  }

let anyPartial = false;
  const caches: AppCacheEntry[] = await Promise.all(
    all.map(async ({ name, fullPath }) => {
      const { sizeBytes, partial } = await getDirSizeBytesShared(fullPath, deadlineMs);
      if (partial) anyPartial = true;
      return { name, path: fullPath, sizeBytes };
    }),
  );

caches.sort((a, b) => b.sizeBytes - a.sizeBytes);
  const totalBytes = caches.reduce((s, c) => s + c.sizeBytes, 0);

if (anyPartial) {
    errors.push({
      scope:   "deadline",
      message: "Cache scan exceeded the per-tool deadline; sizes are partial.",
    });
  }

return {
    platform: "win32",
    caches,
    totalBytes,
    totalHuman: formatBytes(totalBytes),
    ...(errors.length > 0 ? { errors } : {}),
  };
}

// -- Exported run -------------------------------------------------------------

interface RunCtx { deadlineMs?: number }

export async function run(
  _args: Record<string, never> = {},
  ctx?:  RunCtx,
): Promise<GetAppCacheInfoResult> {
  const ceilingMs    = ctx?.deadlineMs ?? (Date.now() + 60_000);
  const remainingMs  = Math.max(0, ceilingMs - Date.now());
  const internalDeadlineMs = Date.now() + Math.floor(remainingMs * 0.9);

if (isDarwin()) return getAppCacheInfoDarwin(internalDeadlineMs);
  if (isWin32())  return getAppCacheInfoWin32(internalDeadlineMs);
  return {
    platform: os.platform(),
    caches:   [],
    totalBytes: 0,
    totalHuman: formatBytes(0),
    errors:   [{ scope: "platform", message: `unsupported platform: ${os.platform()}` }],
  };
}

// -- CLI smoke test -----------------------------------------------------------

if (require.main === module) {
  run()
    .then((r) => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}
