Files
steipete--codexbar/Sources/CodexBarCore/Providers/Claude/ClaudeProbeSessionArtifactCleaner.swift
2026-07-13 12:22:33 +08:00

106 lines
4.2 KiB
Swift

import Foundation
enum ClaudeProbeSessionArtifactCleaner {
private static let log = CodexBarLog.logger(LogCategories.claudeProbe)
private static let maxProjectDirectoryNameLength = 200
@discardableResult
static func cleanupProbeSessionArtifacts(
probeDirectory: URL = ClaudeStatusProbe.probeWorkingDirectoryURL(),
environment: [String: String] = ProcessInfo.processInfo.environment,
fileManager fm: FileManager = .default) -> [URL]
{
let projectDirectoryName = self.claudeProjectDirectoryName(for: probeDirectory)
var visitedDirectories = Set<String>()
var removedFiles: [URL] = []
for root in self.claudeConfigRoots(environment: environment, fileManager: fm) {
let projectsRoot = root.appendingPathComponent("projects", isDirectory: true)
let directories = [projectsRoot.appendingPathComponent(projectDirectoryName, isDirectory: true)]
for directory in directories where visitedDirectories.insert(directory.path).inserted {
guard let entries = try? fm.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles])
else { continue }
for entry in entries where entry.pathExtension == "jsonl" {
let values = try? entry.resourceValues(forKeys: [.isRegularFileKey])
guard values?.isRegularFile == true else { continue }
do {
try fm.removeItem(at: entry)
removedFiles.append(entry)
} catch {
Self.log.debug(
"Claude probe session artifact cleanup skipped file",
metadata: ["error": error.localizedDescription])
}
}
if (try? fm.contentsOfDirectory(atPath: directory.path).isEmpty) == true {
try? fm.removeItem(at: directory)
}
}
}
return removedFiles
}
static func claudeProjectDirectoryName(for directory: URL) -> String {
let path = directory.path.precomposedStringWithCanonicalMapping
let sanitized = String(path.utf16.map { codeUnit in
switch codeUnit {
case 48...57, 65...90, 97...122:
Character(UnicodeScalar(codeUnit)!)
default:
"-"
}
})
guard sanitized.count > self.maxProjectDirectoryNameLength else { return sanitized }
return "\(sanitized.prefix(self.maxProjectDirectoryNameLength))-\(self.javaScriptHashBase36(path))"
}
private static func javaScriptHashBase36(_ string: String) -> String {
var hash: Int32 = 0
for codeUnit in string.utf16 {
hash = hash &* 31 &+ Int32(truncatingIfNeeded: codeUnit)
}
let magnitude = hash < 0 ? -Int64(hash) : Int64(hash)
return String(magnitude, radix: 36)
}
private static func claudeConfigRoots(
environment: [String: String],
fileManager fm: FileManager) -> [URL]
{
var roots: [URL] = []
var seen = Set<String>()
func append(_ url: URL) {
let standardized = url.standardizedFileURL
guard seen.insert(standardized.path).inserted else { return }
roots.append(standardized)
}
if let raw = environment["CLAUDE_CONFIG_DIR"] {
for part in raw.split(separator: ",") {
let path = part.trimmingCharacters(in: .whitespacesAndNewlines)
guard !path.isEmpty else { continue }
append(URL(fileURLWithPath: path))
}
}
let home = environment["HOME"].flatMap { $0.isEmpty ? nil : $0 } ?? NSHomeDirectory()
append(URL(fileURLWithPath: home).appendingPathComponent(".claude", isDirectory: true))
append(URL(fileURLWithPath: home).appendingPathComponent(".config/claude", isDirectory: true))
if roots.isEmpty {
append(fm.homeDirectoryForCurrentUser.appendingPathComponent(".claude", isDirectory: true))
}
return roots
}
}