Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Scripts/lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ ensure_tools() {
"${ROOT_DIR}/Scripts/install_lint_tools.sh"
}

check_codex_parser_hash() {
"${ROOT_DIR}/Scripts/regenerate-codex-parser-hash.sh" --check
}

cmd="${1:-lint}"

case "$cmd" in
lint)
check_codex_parser_hash
ensure_tools
"${BIN_DIR}/swiftformat" Sources Tests --lint
"${BIN_DIR}/swiftlint" --strict
Expand Down
83 changes: 83 additions & 0 deletions Scripts/regenerate-codex-parser-hash.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env bash

set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SOURCE_DIR="${ROOT_DIR}/Sources/CodexBarCore/Vendored/CostUsage"
OUTPUT_FILE="${ROOT_DIR}/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift"
MODE="${1:-write}"

case "$MODE" in
write | --write)
CHECK_ONLY=0
;;
check | --check)
CHECK_ONLY=1
;;
*)
echo "Usage: $0 [write|--write|check|--check]" >&2
exit 2
;;
esac

if command -v shasum >/dev/null 2>&1; then
HASH_CMD=(shasum -a 256)
elif command -v sha256sum >/dev/null 2>&1; then
HASH_CMD=(sha256sum)
else
echo "error: shasum or sha256sum is required" >&2
exit 1
fi

FILE_LIST="$(mktemp)"
trap 'rm -f "$FILE_LIST"' EXIT

find "$SOURCE_DIR" \
-type f \
-name '*.swift' \
! -name '*Claude*' \
-print |
sed "s#^${ROOT_DIR}/##" |
LC_ALL=C sort >"$FILE_LIST"

HASH="$(
while IFS= read -r file; do
printf '== %s ==\n' "$file"
cat "${ROOT_DIR}/${file}"
printf '\n'
done <"$FILE_LIST" | "${HASH_CMD[@]}"
)"
HASH="${HASH%% *}"
SHORT_HASH="${HASH:0:16}"

render_generated() {
cat <<SWIFT
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.

enum CodexParserHash {
static let value = "$SHORT_HASH"
}
SWIFT
}

if [[ "$CHECK_ONLY" -eq 1 ]]; then
EXPECTED_FILE="$(mktemp)"
trap 'rm -f "$FILE_LIST" "$EXPECTED_FILE"' EXIT
render_generated >"$EXPECTED_FILE"
if ! cmp -s "$EXPECTED_FILE" "$OUTPUT_FILE"; then
echo "error: ${OUTPUT_FILE#${ROOT_DIR}/} is stale. Run Scripts/regenerate-codex-parser-hash.sh and commit the result." >&2
if [[ -f "$OUTPUT_FILE" ]]; then
diff -u "$OUTPUT_FILE" "$EXPECTED_FILE" >&2 || true
else
diff -u /dev/null "$EXPECTED_FILE" >&2 || true
fi
exit 1
fi
echo "Codex parser hash is current (${SHORT_HASH})"
exit 0
fi

mkdir -p "$(dirname "$OUTPUT_FILE")"
render_generated >"$OUTPUT_FILE"

echo "Updated ${OUTPUT_FILE#${ROOT_DIR}/} to ${SHORT_HASH}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.

enum CodexParserHash {
static let value = "e8e8a42de095aa72"
}
33 changes: 29 additions & 4 deletions Sources/CodexBarCore/Vendored/CostUsage/CostUsageCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,41 @@ enum CostUsageCacheIO {
.appendingPathComponent("\(provider.rawValue)-v\(artifactVersion).json", isDirectory: false)
}

static func load(provider: UsageProvider, cacheRoot: URL? = nil) -> CostUsageCache {
static func load(
provider: UsageProvider,
cacheRoot: URL? = nil,
producerKey: String? = nil) -> CostUsageCache
{
let url = self.cacheFileURL(provider: provider, cacheRoot: cacheRoot)
if let decoded = self.loadCache(at: url) { return decoded }
let expectedProducerKey = producerKey ?? self.currentProducerKey(provider: provider)
if let decoded = self.loadCache(at: url, expectedProducerKey: expectedProducerKey) { return decoded }
return CostUsageCache()
}

private static func loadCache(at url: URL) -> CostUsageCache? {
private static func loadCache(at url: URL, expectedProducerKey: String?) -> CostUsageCache? {
guard let data = try? Data(contentsOf: url) else { return nil }
guard let decoded = try? JSONDecoder().decode(CostUsageCache.self, from: data)
else { return nil }
guard decoded.version == 1 else { return nil }
if let expectedProducerKey {
guard decoded.producerKey == expectedProducerKey else { return nil }
}
return decoded
}

static func save(provider: UsageProvider, cache: CostUsageCache, cacheRoot: URL? = nil) {
static func save(
provider: UsageProvider,
cache: CostUsageCache,
cacheRoot: URL? = nil,
producerKey: String? = nil)
{
let url = self.cacheFileURL(provider: provider, cacheRoot: cacheRoot)
let dir = url.deletingLastPathComponent()
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)

var cache = cache
cache.producerKey = producerKey ?? self.currentProducerKey(provider: provider)

let tmp = dir.appendingPathComponent(".tmp-\(UUID().uuidString).json", isDirectory: false)
let data = (try? JSONEncoder().encode(cache)) ?? Data()
do {
Expand All @@ -57,10 +73,19 @@ enum CostUsageCacheIO {
try? FileManager.default.removeItem(at: tmp)
}
}

static func currentProducerKey(
provider: UsageProvider,
parserHash: String = CodexParserHash.value) -> String?
{
guard provider == .codex else { return nil }
return "\(provider.rawValue):cu:p\(parserHash)"
}
}

struct CostUsageCache: Codable {
var version: Int = 1
var producerKey: String?
var lastScanUnixMs: Int64 = 0
var scanSinceKey: String?
var scanUntilKey: String?
Expand Down
121 changes: 121 additions & 0 deletions Tests/CodexBarTests/CostUsageCacheTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,125 @@ struct CostUsageCacheTests {
#expect(codexURL.lastPathComponent == "codex-v8.json")
#expect(claudeURL.lastPathComponent == "claude-v2.json")
}

@Test
func `cache load requires matching producer key`() throws {
let root = try self.makeTemporaryCacheRoot()
defer { try? FileManager.default.removeItem(at: root) }

var cache = CostUsageCache()
cache.lastScanUnixMs = 123
cache.days = ["2026-05-18": ["gpt-5.5": [1, 2, 3]]]

CostUsageCacheIO.save(
provider: .codex,
cache: cache,
cacheRoot: root,
producerKey: "codex:cu:p1111111111111111")

let loaded = CostUsageCacheIO.load(
provider: .codex,
cacheRoot: root,
producerKey: "codex:cu:p1111111111111111")
#expect(loaded.producerKey == "codex:cu:p1111111111111111")
#expect(loaded.lastScanUnixMs == 123)
#expect(loaded.days["2026-05-18"]?["gpt-5.5"] == [1, 2, 3])

let stale = CostUsageCacheIO.load(
provider: .codex,
cacheRoot: root,
producerKey: "codex:cu:p2222222222222222")
#expect(stale.lastScanUnixMs == 0)
#expect(stale.files.isEmpty)
#expect(stale.days.isEmpty)
}

@Test
func `legacy cache without producer key is ignored`() throws {
let root = try self.makeTemporaryCacheRoot()
defer { try? FileManager.default.removeItem(at: root) }

let url = CostUsageCacheIO.cacheFileURL(provider: .codex, cacheRoot: root)
try FileManager.default.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true)
let legacy = """
{
"version": 1,
"lastScanUnixMs": 999,
"files": {},
"days": {
"2026-05-18": {
"gpt-5": [1, 0, 0]
}
}
}
"""
try legacy.write(to: url, atomically: false, encoding: .utf8)

let loaded = CostUsageCacheIO.load(
provider: .codex,
cacheRoot: root,
producerKey: "codex:cu:p1111111111111111")

#expect(loaded.lastScanUnixMs == 0)
#expect(loaded.days.isEmpty)
}

@Test
func `non codex cache does not require producer key`() throws {
let root = try self.makeTemporaryCacheRoot()
defer { try? FileManager.default.removeItem(at: root) }

let url = CostUsageCacheIO.cacheFileURL(provider: .claude, cacheRoot: root)
try FileManager.default.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true)
let legacy = """
{
"version": 1,
"lastScanUnixMs": 999,
"files": {},
"days": {
"2026-05-18": {
"claude-sonnet-4-5": [1, 0, 0]
}
}
}
"""
try legacy.write(to: url, atomically: false, encoding: .utf8)

let loaded = CostUsageCacheIO.load(provider: .claude, cacheRoot: root)

#expect(loaded.lastScanUnixMs == 999)
#expect(loaded.days["2026-05-18"]?["claude-sonnet-4-5"] == [1, 0, 0])
}

@Test
func `current producer key uses generated parser hash for codex only`() {
let codexKey = CostUsageCacheIO.currentProducerKey(
provider: .codex,
parserHash: "abc1234567890def")
let standaloneKey = CostUsageCacheIO.currentProducerKey(
provider: .claude,
parserHash: "abc1234567890def")

#expect(codexKey == "codex:cu:pabc1234567890def")
#expect(standaloneKey == nil)
}

@Test
func `generated parser hash is stable short lowercase hex`() {
let hash = CodexParserHash.value

#expect(hash.range(of: #"^[0-9a-f]{16}$"#, options: .regularExpression) != nil)
#expect(CostUsageCacheIO.currentProducerKey(provider: .codex) == "codex:cu:p\(hash)")
}

private func makeTemporaryCacheRoot() throws -> URL {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-cost-cache-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
return root
}
}