PromtGarageMac / Sources /PromptGarageCore /Import /ClaudeJSONLParser.swift
smlflg's picture
Initial public upload from Projekte/PromtGarageMac
4977db2 verified
Raw History Blame Contribute Delete
4.29 kB
import Foundation
struct ClaudeJSONLParser: ImportParser {
private static let agentPrefixes = [
"RESEARCH ONLY", "You are working", "Work in ~/", "Explore the",
"Search system", "Check these", "Read ALL", "Find ALL",
"do NOT modify", "Make ", "Run ", "Execute ",
]
func parse(sourceURL: URL, dedupIndex: DedupIndex) throws -> ImportParseResult {
let files = try ImportFiles.recursiveJSONLFiles(at: sourceURL, excluding: "subagents")
var accumulator = ImportAccumulator(
minimumLength: 20,
maximumLength: 10_000,
dedupIndex: dedupIndex
)
for fileURL in files {
let project = projectTag(from: fileURL)
for line in try ImportFiles.lines(from: fileURL) {
let root: Any
do {
root = try ImportJSON.object(fromLine: line)
} catch {
accumulator.skipMalformed()
continue
}
guard let object = root as? [String: Any] else {
accumulator.skipMalformed()
continue
}
// Claude transcripts also contain valid system/snapshot events
// without a message payload. They are not malformed prompts.
guard let message = object["message"] as? [String: Any] else { continue }
guard message["role"] as? String == "user" else { continue }
// The legacy importer defaults a missing content field to an
// empty string, so it counts as scanned + skipped_short.
let text: String
if message["content"] == nil {
text = ""
} else if let extracted = userText(from: message["content"]) {
text = extracted
} else {
continue
}
let trimmed = ImportText.trimmed(text)
if Self.agentPrefixes.contains(where: { trimmed.hasPrefix($0) }) {
accumulator.skipAgent()
continue
}
accumulator.accept(
trimmed,
sourcePrefix: "claude-raw",
tags: "claude,\(project)",
promptedAt: ImportTimestamp.normalize(object["timestamp"])
)
}
}
return accumulator.result()
}
private func userText(from content: Any?) -> String? {
if let text = content as? String {
return text
}
guard let blocks = content as? [Any] else { return nil }
let textParts = blocks.compactMap { block -> String? in
if let text = block as? String {
return text
}
guard
let object = block as? [String: Any],
object["type"] as? String == "text"
else {
return nil
}
return object["text"] as? String ?? ""
}
return textParts.joined(separator: " ")
}
private func projectTag(from fileURL: URL) -> String {
for component in fileURL.pathComponents {
if let project = decodedProjectComponent(component, homeMarker: "-home-") {
return project
}
if let project = decodedProjectComponent(component, homeMarker: "-Users-") {
return project
}
}
return "claude"
}
/// Claude encodes `/home/name/project` and `/Users/name/project` as one
/// dash-prefixed path component. Strip the home marker and user name, as
/// the original Linux importer did for `-home-smlflg-`.
private func decodedProjectComponent(_ component: String, homeMarker: String) -> String? {
guard component.hasPrefix(homeMarker) else { return nil }
let remainder = component.dropFirst(homeMarker.count)
guard let firstDash = remainder.firstIndex(of: "-") else { return nil }
let projectStart = remainder.index(after: firstDash)
guard projectStart < remainder.endIndex else { return nil }
return remainder[projectStart...].lowercased()
}
}