File size: 4,528 Bytes
4977db2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import Foundation

public enum ImportProvider: String, CaseIterable, Codable, Identifiable, Sendable {
    case claude
    case codex
    case hermes
    case chatGPT
    case gemini

    public var id: String { rawValue }

    public var displayName: String {
        switch self {
        case .claude: "Claude"
        case .codex: "Codex"
        case .hermes: "Hermes"
        case .chatGPT: "ChatGPT"
        case .gemini: "Gemini"
        }
    }

    public var formats: [ImportFormat] {
        ImportFormat.allCases.filter { $0.provider == self }
    }
}

public enum ImportFormat: String, CaseIterable, Codable, Identifiable, Sendable {
    case claudeJSONL
    case codexHistoryJSONL
    case hermesStateDatabase
    case chatGPTAnalysisJSON
    case chatGPTRawConversationsJSON
    case geminiMarkdown
    case geminiJSONL

    public var id: String { rawValue }

    public var provider: ImportProvider {
        switch self {
        case .claudeJSONL: .claude
        case .codexHistoryJSONL: .codex
        case .hermesStateDatabase: .hermes
        case .chatGPTAnalysisJSON, .chatGPTRawConversationsJSON: .chatGPT
        case .geminiMarkdown, .geminiJSONL: .gemini
        }
    }

    public var displayName: String {
        switch self {
        case .claudeJSONL: "Claude Code JSONL"
        case .codexHistoryJSONL: "Codex history.jsonl"
        case .hermesStateDatabase: "Hermes state.db"
        case .chatGPTAnalysisJSON: "ChatGPT Analysis JSON"
        case .chatGPTRawConversationsJSON: "ChatGPT Conversations JSON"
        case .geminiMarkdown: "Gemini Markdown"
        case .geminiJSONL: "Gemini JSONL"
        }
    }

    public var allowsDirectory: Bool {
        switch self {
        case .hermesStateDatabase: true
        default: true
        }
    }
}

public struct ImportCandidate: Hashable, Sendable {
    public let title: String
    public let content: String
    public let source: String
    public let tags: String?
    public let model: String?
    public let promptedAt: String?

    public init(
        title: String,
        content: String,
        source: String,
        tags: String?,
        model: String?,
        promptedAt: String? = nil
    ) {
        self.title = title
        self.content = content
        self.source = source
        self.tags = tags
        self.model = model
        self.promptedAt = promptedAt
    }
}

public struct ImportStatistics: Codable, Equatable, Sendable {
    public var scanned = 0
    public var imported = 0
    public var skippedShort = 0
    public var skippedLong = 0
    public var skippedDuplicate = 0
    public var skippedAgent = 0
    public var skippedSystem = 0
    public var skippedMalformed = 0

    public init() {}
}

public struct ImportPreview: Sendable {
    public let format: ImportFormat
    public let sourceURL: URL
    public let candidates: [ImportCandidate]
    public let statistics: ImportStatistics

    public init(
        format: ImportFormat,
        sourceURL: URL,
        candidates: [ImportCandidate],
        statistics: ImportStatistics
    ) {
        self.format = format
        self.sourceURL = sourceURL
        self.candidates = candidates
        self.statistics = statistics
    }
}

public struct ImportReport: Codable, Sendable {
    public let provider: String
    public let format: String
    public let sourceName: String
    public let completedAt: Date
    public let statistics: ImportStatistics

    public init(preview: ImportPreview, statistics: ImportStatistics, completedAt: Date = Date()) {
        provider = preview.format.provider.rawValue
        format = preview.format.rawValue
        sourceName = preview.sourceURL.lastPathComponent
        self.completedAt = completedAt
        self.statistics = statistics
    }
}

public struct DedupIndex: Sendable {
    public var hashes: Set<String>
    public let existingContents: [String]

    public init(hashes: Set<String>, existingContents: [String]) {
        // Legacy imports encode their 12-character content hash in `source`.
        // Manual and other pre-native rows may not. Hash every exact stored
        // content as well so preview and commit use the same dedup identity
        // without rewriting any existing row.
        self.hashes = hashes.union(existingContents.map {
            ImportText.contentHash(ImportText.trimmed($0))
        })
        self.existingContents = existingContents
    }

    public func contains(_ hash: String) -> Bool {
        hashes.contains(hash)
    }
}