Skip to content

Commit 1adcc5f

Browse files
committed
lint
1 parent a1fe5e1 commit 1adcc5f

7 files changed

Lines changed: 123 additions & 43 deletions

File tree

Sources/AgentTally/Presentation/MenuRowsBuilder.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ public enum MenuRowsBuilder {
4747

4848
if spending.isInstalled, state.lastError == nil {
4949
rows.append(.section("\(spending.name) spending"))
50-
rows.append(.disabled("Today: $\(StatusPresenter.displayDollarAmount(for: spending.todayCost))"))
50+
rows.append(
51+
.disabled("Today: $\(StatusPresenter.displayDollarAmount(for: spending.todayCost))"))
5152
rows.append(
5253
.disabled(
5354
"Month: $\(StatusPresenter.displayDollarAmount(for: spending.monthCost)) (\(state.businessDays) biz days)"

Sources/AgentTally/Usage/UsageDataScanner.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,8 @@ enum UsageDataScanner {
226226

227227
let candidateDirectories: [URL]
228228
if !envPaths.isEmpty {
229-
candidateDirectories = envPaths
229+
candidateDirectories =
230+
envPaths
230231
.split(separator: ",")
231232
.map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) }
232233
.filter { !$0.isEmpty }
@@ -245,7 +246,8 @@ enum UsageDataScanner {
245246
for directory in candidateDirectories {
246247
let normalizedDirectory = directory.standardizedFileURL
247248
let normalizedPath = normalizedDirectory.path
248-
let projectsDirectory = normalizedDirectory
249+
let projectsDirectory =
250+
normalizedDirectory
249251
.appendingPathComponent(claudeProjectsDirectoryName)
250252
.standardizedFileURL
251253

Tests/MenuRowsHarness.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ func testMenuRowsBuilder() throws {
1818
avgPerDay: 10.0,
1919
lastUsageDetectedAt: now.addingTimeInterval(-15)
2020
)
21-
let codexNotInstalled = AgentSpending(name: "Codex", isInstalled: false, todayCost: 0, monthCost: 0, avgPerDay: 0)
21+
let codexNotInstalled = AgentSpending(
22+
name: "Codex", isInstalled: false, todayCost: 0, monthCost: 0, avgPerDay: 0)
2223

2324
let rows = MenuRowsBuilder.rows(
2425
for: AppState(
@@ -39,7 +40,8 @@ func testMenuRowsBuilder() throws {
3940
)
4041
let headerIndex = rows.firstIndex(of: .disabled("AgentTally v0.1"))!
4142
if case .disabled(let label) = rows[headerIndex + 1] {
42-
try expect(label.hasPrefix("Last refreshed:"), "last updated should appear directly below the header")
43+
try expect(
44+
label.hasPrefix("Last refreshed:"), "last updated should appear directly below the header")
4345
} else {
4446
throw TestFailure(description: "row after header should be 'Last refreshed:' disabled item")
4547
}

Tests/StatusPresenterHarness.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import Foundation
22

33
func testStatusPresenter() throws {
4-
let claudeSpending = AgentSpending(name: "Claude Code", isInstalled: true, todayCost: 48.35, monthCost: 0, avgPerDay: 0)
5-
let codexSpending = AgentSpending(name: "Codex", isInstalled: true, todayCost: 12.0, monthCost: 0, avgPerDay: 0)
6-
let codexNotInstalled = AgentSpending(name: "Codex", isInstalled: false, todayCost: 0, monthCost: 0, avgPerDay: 0)
7-
let fractionalSpending = AgentSpending(name: "Claude Code", isInstalled: true, todayCost: 0.01, monthCost: 0, avgPerDay: 0)
4+
let claudeSpending = AgentSpending(
5+
name: "Claude Code", isInstalled: true, todayCost: 48.35, monthCost: 0, avgPerDay: 0)
6+
let codexSpending = AgentSpending(
7+
name: "Codex", isInstalled: true, todayCost: 12.0, monthCost: 0, avgPerDay: 0)
8+
let codexNotInstalled = AgentSpending(
9+
name: "Codex", isInstalled: false, todayCost: 0, monthCost: 0, avgPerDay: 0)
10+
let fractionalSpending = AgentSpending(
11+
name: "Claude Code", isInstalled: true, todayCost: 0.01, monthCost: 0, avgPerDay: 0)
812

913
let freshState = AppState(
1014
isRefreshing: true,
@@ -41,7 +45,8 @@ func testStatusPresenter() throws {
4145
let codexOnlyState = AppState(
4246
isRefreshing: false,
4347
agentSpendings: [
44-
AgentSpending(name: "Claude Code", isInstalled: false, todayCost: 0, monthCost: 0, avgPerDay: 0),
48+
AgentSpending(
49+
name: "Claude Code", isInstalled: false, todayCost: 0, monthCost: 0, avgPerDay: 0),
4550
codexSpending,
4651
],
4752
businessDays: 0,

Tests/UsageDataScannerHarness.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ private func testClaudeFingerprintIgnoresNonUsageFiles() throws {
1010
let homeDirectory = try makeTemporaryDirectory()
1111
defer { try? fileManager.removeItem(at: homeDirectory) }
1212

13-
let usageFile = homeDirectory
13+
let usageFile =
14+
homeDirectory
1415
.appendingPathComponent(".claude")
1516
.appendingPathComponent("projects")
1617
.appendingPathComponent("sample-project")
1718
.appendingPathComponent("session.jsonl")
18-
let settingsFile = homeDirectory
19+
let settingsFile =
20+
homeDirectory
1921
.appendingPathComponent(".claude")
2022
.appendingPathComponent("settings.json")
2123

@@ -53,13 +55,15 @@ private func testCodexFingerprintScopesToCurrentMonthSessions() throws {
5355
defer { try? fileManager.removeItem(at: homeDirectory) }
5456

5557
let codexHome = homeDirectory.appendingPathComponent("codex-home")
56-
let previousMonthFile = codexHome
58+
let previousMonthFile =
59+
codexHome
5760
.appendingPathComponent("sessions")
5861
.appendingPathComponent("2026")
5962
.appendingPathComponent("04")
6063
.appendingPathComponent("30")
6164
.appendingPathComponent("old.jsonl")
62-
let currentMonthFile = codexHome
65+
let currentMonthFile =
66+
codexHome
6367
.appendingPathComponent("sessions")
6468
.appendingPathComponent("2026")
6569
.appendingPathComponent("05")

Tests/UsageRefreshControllerHarness.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,8 @@ private func testApplySuccess() throws {
188188

189189
private func testApplyFailure() throws {
190190
let now = Date(timeIntervalSinceReferenceDate: 3_000)
191-
let spending = AgentSpending(name: "Claude Code", isInstalled: true, todayCost: 48.35, monthCost: 208.12, avgPerDay: 52.03)
191+
let spending = AgentSpending(
192+
name: "Claude Code", isInstalled: true, todayCost: 48.35, monthCost: 208.12, avgPerDay: 52.03)
192193
let state = AppState(
193194
isRefreshing: true,
194195
agentSpendings: [spending],

tooling/usage-helper.ts

Lines changed: 93 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
import { loadDailyUsageData } from "ccusage/data-loader";
2-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
2+
import {
3+
existsSync,
4+
mkdirSync,
5+
readdirSync,
6+
readFileSync,
7+
writeFileSync,
8+
} from "fs";
39
import os from "os";
410
import path from "path";
511

6-
type AgentEntry = { name: string; found: boolean; today: number; month: number };
12+
type AgentEntry = {
13+
name: string;
14+
found: boolean;
15+
today: number;
16+
month: number;
17+
};
718
type UsagePayload = { agents: AgentEntry[] };
819

920
function formatLocalDay(date: Date): string {
@@ -17,7 +28,10 @@ function formatLocalDay(date: Date): string {
1728

1829
// ─── Claude Code ────────────────────────────────────────────────────────────
1930

20-
async function loadClaudeData(since: string, offline: boolean): Promise<AgentEntry> {
31+
async function loadClaudeData(
32+
since: string,
33+
offline: boolean,
34+
): Promise<AgentEntry> {
2135
const dailyData = await loadDailyUsageData({ since, offline });
2236
const today = formatLocalDay(new Date());
2337

@@ -42,14 +56,24 @@ const CODEX_MODEL_ALIASES: Record<string, string> = {
4256
"gpt-5.3-codex": "gpt-5.2-codex",
4357
};
4458

45-
const CODEX_PROVIDER_PREFIXES = ["openai/", "azure/openai/", "azure/", "openrouter/openai/"];
59+
const CODEX_PROVIDER_PREFIXES = [
60+
"openai/",
61+
"azure/openai/",
62+
"azure/",
63+
"openrouter/openai/",
64+
];
4665

4766
// ─── Pricing cache ───────────────────────────────────────────────────────────
4867

4968
const LITELLM_URL =
5069
"https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
5170
const PRICING_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
52-
const PRICING_CACHE_PATH = path.join(os.homedir(), ".cache", "agenttally", "codex-pricing.json");
71+
const PRICING_CACHE_PATH = path.join(
72+
os.homedir(),
73+
".cache",
74+
"agenttally",
75+
"codex-pricing.json",
76+
);
5377

5478
type PricingCache = {
5579
fetchedAt: number;
@@ -59,31 +83,45 @@ type PricingCache = {
5983
function parseLiteLLMEntry(data: unknown): ModelPricing | null {
6084
if (typeof data !== "object" || data == null) return null;
6185
const d = data as Record<string, unknown>;
62-
const input = typeof d.input_cost_per_token === "number" ? d.input_cost_per_token : null;
63-
const output = typeof d.output_cost_per_token === "number" ? d.output_cost_per_token : null;
86+
const input =
87+
typeof d.input_cost_per_token === "number" ? d.input_cost_per_token : null;
88+
const output =
89+
typeof d.output_cost_per_token === "number"
90+
? d.output_cost_per_token
91+
: null;
6492
if (input == null || output == null) return null;
6593
return {
6694
input_cost_per_token: input,
6795
output_cost_per_token: output,
6896
cache_read_input_token_cost:
69-
typeof d.cache_read_input_token_cost === "number" ? d.cache_read_input_token_cost : undefined,
97+
typeof d.cache_read_input_token_cost === "number"
98+
? d.cache_read_input_token_cost
99+
: undefined,
70100
};
71101
}
72102

73-
async function loadCodexPricing(offline: boolean): Promise<Record<string, ModelPricing>> {
103+
async function loadCodexPricing(
104+
offline: boolean,
105+
): Promise<Record<string, ModelPricing>> {
74106
try {
75-
const cached = JSON.parse(readFileSync(PRICING_CACHE_PATH, "utf8")) as PricingCache;
107+
const cached = JSON.parse(
108+
readFileSync(PRICING_CACHE_PATH, "utf8"),
109+
) as PricingCache;
76110
if (Date.now() - cached.fetchedAt < PRICING_CACHE_TTL_MS) {
77111
return cached.pricing;
78112
}
79113
} catch {
80114
// cache miss or corrupt
81115
}
82116

83-
if (offline) throw new Error("Codex pricing unavailable: cache stale and in offline mode");
117+
if (offline)
118+
throw new Error(
119+
"Codex pricing unavailable: cache stale and in offline mode",
120+
);
84121

85122
const response = await fetch(LITELLM_URL);
86-
if (!response.ok) throw new Error(`Failed to fetch Codex pricing: HTTP ${response.status}`);
123+
if (!response.ok)
124+
throw new Error(`Failed to fetch Codex pricing: HTTP ${response.status}`);
87125
const raw = (await response.json()) as Record<string, unknown>;
88126

89127
const pricing: Record<string, ModelPricing> = {};
@@ -93,18 +131,22 @@ async function loadCodexPricing(offline: boolean): Promise<Record<string, ModelP
93131
}
94132

95133
mkdirSync(path.dirname(PRICING_CACHE_PATH), { recursive: true });
96-
writeFileSync(PRICING_CACHE_PATH, JSON.stringify({ fetchedAt: Date.now(), pricing }));
134+
writeFileSync(
135+
PRICING_CACHE_PATH,
136+
JSON.stringify({ fetchedAt: Date.now(), pricing }),
137+
);
97138
return pricing;
98139
}
99140

100141
function lookupCodexPricing(
101142
modelName: string,
102-
pricing: Record<string, ModelPricing>
143+
pricing: Record<string, ModelPricing>,
103144
): ModelPricing | null {
104145
// Build candidates: bare name + all provider-prefixed variants
105146
const candidates = [modelName];
106147
for (const prefix of CODEX_PROVIDER_PREFIXES) {
107-
if (modelName.startsWith(prefix)) candidates.push(modelName.slice(prefix.length));
148+
if (modelName.startsWith(prefix))
149+
candidates.push(modelName.slice(prefix.length));
108150
else candidates.push(`${prefix}${modelName}`);
109151
}
110152

@@ -118,7 +160,8 @@ function lookupCodexPricing(
118160
// Fuzzy fallback: substring match
119161
const lower = modelName.toLowerCase();
120162
for (const [key, val] of Object.entries(pricing)) {
121-
if (key.toLowerCase().includes(lower) || lower.includes(key.toLowerCase())) return val;
163+
if (key.toLowerCase().includes(lower) || lower.includes(key.toLowerCase()))
164+
return val;
122165
}
123166

124167
return null;
@@ -133,27 +176,34 @@ type TokenUsage = {
133176
function calcCost(usage: TokenUsage, pricing: ModelPricing): number {
134177
const cachedInput = Math.min(usage.cached_input_tokens, usage.input_tokens);
135178
const nonCachedInput = usage.input_tokens - cachedInput;
136-
const cacheRate = pricing.cache_read_input_token_cost ?? pricing.input_cost_per_token;
179+
const cacheRate =
180+
pricing.cache_read_input_token_cost ?? pricing.input_cost_per_token;
137181
return (
138182
nonCachedInput * pricing.input_cost_per_token +
139183
cachedInput * cacheRate +
140184
usage.output_tokens * pricing.output_cost_per_token
141185
);
142186
}
143187

144-
function subtractTokenUsage(total: TokenUsage, prev: TokenUsage | null): TokenUsage {
188+
function subtractTokenUsage(
189+
total: TokenUsage,
190+
prev: TokenUsage | null,
191+
): TokenUsage {
145192
if (!prev) return total;
146193
return {
147194
input_tokens: Math.max(0, total.input_tokens - prev.input_tokens),
148-
cached_input_tokens: Math.max(0, total.cached_input_tokens - prev.cached_input_tokens),
195+
cached_input_tokens: Math.max(
196+
0,
197+
total.cached_input_tokens - prev.cached_input_tokens,
198+
),
149199
output_tokens: Math.max(0, total.output_tokens - prev.output_tokens),
150200
};
151201
}
152202

153203
function parseCodexSession(
154204
filePath: string,
155205
pricing: Record<string, ModelPricing>,
156-
costsByDate: Map<string, number>
206+
costsByDate: Map<string, number>,
157207
) {
158208
let content: string;
159209
try {
@@ -184,9 +234,13 @@ function parseCodexSession(
184234

185235
if (entry.type !== "event_msg") continue;
186236
const payload = entry.payload as Record<string, unknown> | null;
187-
if ((payload as Record<string, unknown> | null)?.type !== "token_count") continue;
237+
if ((payload as Record<string, unknown> | null)?.type !== "token_count")
238+
continue;
188239

189-
const info = (payload as Record<string, unknown>).info as Record<string, unknown> | null;
240+
const info = (payload as Record<string, unknown>).info as Record<
241+
string,
242+
unknown
243+
> | null;
190244
const lastUsage = info?.last_token_usage as TokenUsage | null;
191245
const totalUsage = info?.total_token_usage as TokenUsage | null;
192246

@@ -199,7 +253,9 @@ function parseCodexSession(
199253
if (totalUsage?.input_tokens != null) prevTotals = totalUsage;
200254
if (!delta) continue;
201255

202-
const modelPricing = currentModel ? lookupCodexPricing(currentModel, pricing) : null;
256+
const modelPricing = currentModel
257+
? lookupCodexPricing(currentModel, pricing)
258+
: null;
203259
if (!modelPricing) continue;
204260

205261
const cost = calcCost(delta, modelPricing);
@@ -210,8 +266,12 @@ function parseCodexSession(
210266
}
211267
}
212268

213-
async function loadCodexData(since: string, offline: boolean): Promise<AgentEntry> {
214-
const codexHome = process.env["CODEX_HOME"] ?? path.join(os.homedir(), ".codex");
269+
async function loadCodexData(
270+
since: string,
271+
offline: boolean,
272+
): Promise<AgentEntry> {
273+
const codexHome =
274+
process.env["CODEX_HOME"] ?? path.join(os.homedir(), ".codex");
215275
const sessionsDir = path.join(codexHome, "sessions");
216276
if (!existsSync(sessionsDir)) {
217277
return { name: "Codex", found: false, today: 0, month: 0 };
@@ -250,7 +310,10 @@ async function loadCodexData(since: string, offline: boolean): Promise<AgentEntr
250310
const AGENT_LOADERS = {
251311
claude: loadClaudeData,
252312
codex: loadCodexData,
253-
} as const satisfies Record<string, (since: string, offline: boolean) => Promise<AgentEntry>>;
313+
} as const satisfies Record<
314+
string,
315+
(since: string, offline: boolean) => Promise<AgentEntry>
316+
>;
254317

255318
type AgentID = keyof typeof AGENT_LOADERS;
256319

@@ -287,7 +350,9 @@ function parseRequestedAgents(args: string[]): AgentID[] {
287350
}
288351
}
289352

290-
return requested.size > 0 ? ALL_AGENTS.filter((agent) => requested.has(agent)) : ALL_AGENTS;
353+
return requested.size > 0
354+
? ALL_AGENTS.filter((agent) => requested.has(agent))
355+
: ALL_AGENTS;
291356
}
292357

293358
async function main() {
@@ -299,7 +364,7 @@ async function main() {
299364
const requestedAgents = parseRequestedAgents(process.argv.slice(3));
300365

301366
const agents = await Promise.all(
302-
requestedAgents.map((agent) => AGENT_LOADERS[agent](since, offline))
367+
requestedAgents.map((agent) => AGENT_LOADERS[agent](since, offline)),
303368
);
304369

305370
const payload: UsagePayload = {

0 commit comments

Comments
 (0)