letmecode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # letmecode
2
+
3
+ Minimal `npx`-first CLI package.
4
+
5
+ ## Local development
6
+
7
+ ```bash
8
+ pnpm install
9
+ pnpm start
10
+ ```
11
+
12
+ The Ink app source lives in `ink-app/`.
13
+
14
+ ## Usage
15
+
16
+ ```bash
17
+ npx letmecode
18
+ ```
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ Promise.resolve(require("../dist/index.js").main()).catch(error => {
6
+ console.error(error);
7
+ process.exitCode = 1;
8
+ });
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.main = main;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_path_1 = require("node:path");
6
+ function main(argv = process.argv.slice(2)) {
7
+ const inkCliPath = (0, node_path_1.join)(__dirname, "..", "ink-app", "dist", "index.js");
8
+ const result = (0, node_child_process_1.spawnSync)(process.execPath, [inkCliPath, ...argv], {
9
+ stdio: "inherit"
10
+ });
11
+ if (result.error) {
12
+ throw result.error;
13
+ }
14
+ if (typeof result.status === "number") {
15
+ process.exitCode = result.status;
16
+ return;
17
+ }
18
+ process.exitCode = 1;
19
+ }
@@ -0,0 +1,142 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React, { useEffect, useState } from "react";
3
+ import { Box, Text, useApp, useInput, render } from "ink";
4
+ import { createProviders } from "./providers/index.js";
5
+ const VERTICAL_TABS = [
6
+ { id: "limit-windows", label: "Limit windows" },
7
+ { id: "usage-by-model", label: "Usage by model" }
8
+ ];
9
+ function App() {
10
+ const { exit } = useApp();
11
+ const providers = React.useState(() => createProviders())[0];
12
+ const [providerStates, setProviderStates] = useState(providers.map((provider) => ({ provider, status: "loading" })));
13
+ const [selectedProviderIndex, setSelectedProviderIndex] = useState(0);
14
+ const [selectedVerticalTabIndex, setSelectedVerticalTabIndex] = useState(0);
15
+ useEffect(() => {
16
+ let cancelled = false;
17
+ for (const provider of providers) {
18
+ void provider
19
+ .getStats()
20
+ .then((stats) => {
21
+ if (cancelled) {
22
+ return;
23
+ }
24
+ setProviderStates((current) => current.map((entry) => entry.provider.id === provider.id
25
+ ? { provider, status: "ready", stats }
26
+ : entry));
27
+ })
28
+ .catch((error) => {
29
+ if (cancelled) {
30
+ return;
31
+ }
32
+ const message = error instanceof Error ? error.message : String(error);
33
+ setProviderStates((current) => current.map((entry) => entry.provider.id === provider.id
34
+ ? { provider, status: "error", errorMessage: message }
35
+ : entry));
36
+ });
37
+ }
38
+ return () => {
39
+ cancelled = true;
40
+ };
41
+ }, [providers]);
42
+ useInput((input, key) => {
43
+ if (input === "q" || key.escape) {
44
+ exit();
45
+ return;
46
+ }
47
+ if ((input === "\t" && !key.shift) || key.rightArrow) {
48
+ setSelectedProviderIndex((current) => (current + 1) % providerStates.length);
49
+ return;
50
+ }
51
+ if ((input === "\t" && key.shift) || key.leftArrow) {
52
+ setSelectedProviderIndex((current) => (current - 1 + providerStates.length) % providerStates.length);
53
+ return;
54
+ }
55
+ if (key.downArrow || input === "j") {
56
+ setSelectedVerticalTabIndex((current) => (current + 1) % VERTICAL_TABS.length);
57
+ return;
58
+ }
59
+ if (key.upArrow || input === "k") {
60
+ setSelectedVerticalTabIndex((current) => (current - 1 + VERTICAL_TABS.length) % VERTICAL_TABS.length);
61
+ }
62
+ });
63
+ const selectedProvider = providerStates[selectedProviderIndex];
64
+ const selectedVerticalTab = VERTICAL_TABS[selectedVerticalTabIndex];
65
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "letmecode usage dashboard" }), _jsx(Text, { color: "gray", children: "tab/shift+tab or left/right to switch providers, j/k or up/down for details, q to quit" }), _jsx(Box, { marginTop: 1, children: providerStates.map((state, index) => (_jsx(ProviderTab, { label: state.provider.label, active: index === selectedProviderIndex, status: state.status }, state.provider.id))) }), _jsx(Box, { marginTop: 1, borderStyle: "round", paddingX: 1, paddingY: 0, flexDirection: "column", children: _jsx(SummarySection, { providerState: selectedProvider }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Box, { flexDirection: "column", width: 22, marginRight: 2, children: VERTICAL_TABS.map((tab, index) => (_jsx(VerticalTab, { label: tab.label, active: index === selectedVerticalTabIndex }, tab.id))) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(ContentPanel, { providerState: selectedProvider, tabId: selectedVerticalTab.id }) })] }), selectedProvider.status === "ready" && selectedProvider.stats.warnings.length > 0 ? (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "Warnings" }), selectedProvider.stats.warnings.map((warning) => (_jsx(Text, { children: warning }, warning)))] })) : null] }));
66
+ }
67
+ function ProviderTab(props) {
68
+ const statusColor = props.status === "error" ? "red" : props.status === "loading" ? "yellow" : "green";
69
+ const tabLabel = props.active ? ` ${props.label} ` : `[${props.label}]`;
70
+ return (_jsx(Box, { marginRight: 1, children: _jsx(Text, { inverse: props.active, color: statusColor, children: tabLabel }) }));
71
+ }
72
+ function VerticalTab(props) {
73
+ return (_jsx(Text, { inverse: props.active, children: props.active ? ` ${props.label} ` : ` ${props.label}` }));
74
+ }
75
+ function SummarySection(props) {
76
+ if (props.providerState.status === "loading") {
77
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: props.providerState.provider.label }), _jsx(Text, { color: "yellow", children: "Loading stats from local sessions..." })] }));
78
+ }
79
+ if (props.providerState.status === "error") {
80
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: props.providerState.provider.label }), _jsxs(Text, { color: "red", children: ["Failed to load provider stats: ", props.providerState.errorMessage] })] }));
81
+ }
82
+ const { summary } = props.providerState.stats;
83
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: props.providerState.stats.providerLabel }), _jsxs(Text, { children: ["root: ", summary.rootLabel, " (", summary.rootPath, ")"] }), _jsxs(Text, { children: ["files: ", formatInteger(summary.filesScanned), " lines: ", formatInteger(summary.linesRead), " token events: ", formatInteger(summary.tokenEvents)] }), _jsxs(Text, { children: ["input: ", formatInteger(summary.totals.inputTokens), " cached: ", formatInteger(summary.totals.cachedInputTokens), " non-cached: ", formatInteger(summary.totals.nonCachedInputTokens)] }), _jsxs(Text, { children: ["output: ", formatInteger(summary.totals.outputTokens), " reasoning: ", formatInteger(summary.totals.reasoningOutputTokens), " total: ", formatInteger(summary.totals.totalTokens)] }), _jsxs(Text, { children: ["estimated credits: ", formatCredits(summary.totals.estimatedCredits), " models: ", summary.distinctModels.join(", ") || "none", " plans: ", summary.distinctPlanTypes.join(", ") || "none"] })] }));
84
+ }
85
+ function ContentPanel(props) {
86
+ if (props.providerState.status === "loading") {
87
+ return _jsxs(Text, { color: "yellow", children: ["Loading ", props.providerState.provider.label, " stats..."] });
88
+ }
89
+ if (props.providerState.status === "error") {
90
+ return _jsxs(Text, { color: "red", children: ["Provider error: ", props.providerState.errorMessage] });
91
+ }
92
+ if (props.tabId === "limit-windows") {
93
+ return _jsx(LimitWindowsPanel, { stats: props.providerState.stats });
94
+ }
95
+ return _jsx(UsageByModelPanel, { stats: props.providerState.stats });
96
+ }
97
+ function LimitWindowsPanel(props) {
98
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Primary" }), _jsx(LimitWindowSection, { windows: props.stats.primaryLimitWindows }), _jsx(Box, { marginTop: 1 }), _jsx(Text, { bold: true, children: "Secondary" }), _jsx(LimitWindowSection, { windows: props.stats.secondaryLimitWindows })] }));
99
+ }
100
+ function LimitWindowSection(props) {
101
+ if (props.windows.length === 0) {
102
+ return _jsx(Text, { color: "gray", children: "No windows found." });
103
+ }
104
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "plan limit window used start end events" }), props.windows.map((window) => {
105
+ const windowLabel = formatWindowMinutes(window.windowMinutes);
106
+ const usedLabel = `${window.minUsedPercent}%->${window.maxUsedPercent}%`;
107
+ return (_jsxs(Text, { children: [pad(window.planType, 10), " ", pad(window.limitId, 10), " ", pad(windowLabel, 8), " ", pad(usedLabel, 12), " ", pad(shortIso(window.startTimeIso), 20), " ", pad(shortIso(window.endTimeIso), 20), " ", formatInteger(window.eventCount)] }, `${window.scope}-${window.planType}-${window.limitId}-${window.endTimeIso}`));
108
+ })] }));
109
+ }
110
+ function UsageByModelPanel(props) {
111
+ if (props.stats.modelUsage.length === 0) {
112
+ return _jsx(Text, { color: "gray", children: "No model usage found." });
113
+ }
114
+ const totals = props.stats.summary.totals;
115
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "model input cached non-cached output credits events" }), props.stats.modelUsage.map((row) => (_jsxs(Text, { children: [pad(row.modelId, 16), " ", pad(formatInteger(row.totals.inputTokens), 12), " ", pad(formatInteger(row.totals.cachedInputTokens), 12), " ", pad(formatInteger(row.totals.nonCachedInputTokens), 12), " ", pad(formatInteger(row.totals.outputTokens), 12), " ", pad(formatCredits(row.totals.estimatedCredits), 12), " ", pad(formatInteger(row.totals.eventCount), 8)] }, row.modelId))), _jsxs(Text, { color: "cyan", children: [pad("TOTAL", 16), " ", pad(formatInteger(totals.inputTokens), 12), " ", pad(formatInteger(totals.cachedInputTokens), 12), " ", pad(formatInteger(totals.nonCachedInputTokens), 12), " ", pad(formatInteger(totals.outputTokens), 12), " ", pad(formatCredits(totals.estimatedCredits), 12), " ", pad(formatInteger(totals.eventCount), 8)] })] }));
116
+ }
117
+ function formatInteger(value) {
118
+ return Math.round(value).toLocaleString("en-US");
119
+ }
120
+ function formatCredits(value) {
121
+ return value.toLocaleString("en-US", {
122
+ minimumFractionDigits: 2,
123
+ maximumFractionDigits: 2
124
+ });
125
+ }
126
+ function formatWindowMinutes(value) {
127
+ const hours = value / 60;
128
+ if (hours >= 24) {
129
+ return `${(hours / 24).toFixed(2)}d`;
130
+ }
131
+ return `${hours.toFixed(2)}h`;
132
+ }
133
+ function shortIso(value) {
134
+ return value.replace(".000Z", "Z").slice(0, 19) + "Z";
135
+ }
136
+ function pad(value, length) {
137
+ return value.length >= length ? value.slice(0, length) : value.padEnd(length);
138
+ }
139
+ export function main() {
140
+ render(_jsx(App, {}));
141
+ }
142
+ main();
@@ -0,0 +1,301 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import readline from "node:readline";
5
+ import { UsageProviderBase, createEmptyUsageTotals } from "./contract.js";
6
+ const RATE_CARD = {
7
+ "gpt-5.5": { input: 125, cachedInput: 12.5, output: 750 },
8
+ "gpt-5.4": { input: 62.5, cachedInput: 6.25, output: 375 },
9
+ "gpt-5.4-mini": { input: 18.75, cachedInput: 1.875, output: 113 }
10
+ };
11
+ export class CodexUsageProvider extends UsageProviderBase {
12
+ constructor(options = {}) {
13
+ super("codex", "Codex");
14
+ this.root = path.resolve(options.root ?? os.homedir());
15
+ }
16
+ async getStats() {
17
+ const sessionsRoot = path.join(this.root, ".codex", "sessions");
18
+ const byModel = new Map();
19
+ const windows = new Map();
20
+ const planTypes = new Set();
21
+ const warnings = [];
22
+ const parseTotals = {
23
+ filesScanned: 0,
24
+ linesRead: 0,
25
+ tokenEvents: 0,
26
+ malformedLines: 0
27
+ };
28
+ for await (const file of walkSessionFiles(sessionsRoot)) {
29
+ parseTotals.filesScanned += 1;
30
+ const fileStats = await parseSessionFile(file, byModel, windows, planTypes);
31
+ parseTotals.linesRead += fileStats.linesRead;
32
+ parseTotals.tokenEvents += fileStats.tokenEvents;
33
+ parseTotals.malformedLines += fileStats.malformedLines;
34
+ }
35
+ if (parseTotals.malformedLines > 0) {
36
+ warnings.push(`Skipped ${parseTotals.malformedLines} malformed JSONL line(s).`);
37
+ }
38
+ const modelUsage = [...byModel.entries()]
39
+ .map(([modelId, totals]) => ({ modelId, totals }))
40
+ .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
41
+ const unknownPricedModels = modelUsage
42
+ .map((row) => row.modelId)
43
+ .filter((modelId) => !RATE_CARD[modelId]);
44
+ if (unknownPricedModels.length > 0) {
45
+ warnings.push(`No credit rate configured for: ${unknownPricedModels.join(", ")}.`);
46
+ }
47
+ if (parseTotals.filesScanned === 0) {
48
+ warnings.push(`No Codex session files found under ${sessionsRoot}.`);
49
+ }
50
+ const summaryTotals = sumUsageTotals(modelUsage.map((row) => row.totals));
51
+ const [primaryLimitWindows, secondaryLimitWindows] = buildWindowLists(windows);
52
+ return {
53
+ providerId: this.id,
54
+ providerLabel: this.label,
55
+ summary: {
56
+ filesScanned: parseTotals.filesScanned,
57
+ linesRead: parseTotals.linesRead,
58
+ tokenEvents: parseTotals.tokenEvents,
59
+ totals: summaryTotals,
60
+ distinctModels: modelUsage.map((row) => row.modelId),
61
+ distinctPlanTypes: [...planTypes].sort(),
62
+ rootLabel: "~/.codex/sessions",
63
+ rootPath: sessionsRoot
64
+ },
65
+ modelUsage,
66
+ primaryLimitWindows,
67
+ secondaryLimitWindows,
68
+ warnings
69
+ };
70
+ }
71
+ }
72
+ function createEmptyRawUsage() {
73
+ return {
74
+ inputTokens: 0,
75
+ cachedInputTokens: 0,
76
+ outputTokens: 0,
77
+ reasoningOutputTokens: 0,
78
+ totalTokens: 0
79
+ };
80
+ }
81
+ function numberOrZero(value) {
82
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
83
+ }
84
+ function normalizeRawUsage(value) {
85
+ const usage = value && typeof value === "object" ? value : {};
86
+ return {
87
+ inputTokens: numberOrZero(usage.input_tokens),
88
+ cachedInputTokens: numberOrZero(usage.cached_input_tokens),
89
+ outputTokens: numberOrZero(usage.output_tokens),
90
+ reasoningOutputTokens: numberOrZero(usage.reasoning_output_tokens),
91
+ totalTokens: numberOrZero(usage.total_tokens)
92
+ };
93
+ }
94
+ function subtractRawUsage(current, previous) {
95
+ return {
96
+ inputTokens: Math.max(0, current.inputTokens - previous.inputTokens),
97
+ cachedInputTokens: Math.max(0, current.cachedInputTokens - previous.cachedInputTokens),
98
+ outputTokens: Math.max(0, current.outputTokens - previous.outputTokens),
99
+ reasoningOutputTokens: Math.max(0, current.reasoningOutputTokens - previous.reasoningOutputTokens),
100
+ totalTokens: Math.max(0, current.totalTokens - previous.totalTokens)
101
+ };
102
+ }
103
+ function creditsFor(modelId, usage) {
104
+ const rate = RATE_CARD[modelId];
105
+ if (!rate) {
106
+ return 0;
107
+ }
108
+ const cachedInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
109
+ const nonCachedInputTokens = Math.max(0, usage.inputTokens - cachedInputTokens);
110
+ return ((nonCachedInputTokens / 1000000) * rate.input +
111
+ (cachedInputTokens / 1000000) * rate.cachedInput +
112
+ (usage.outputTokens / 1000000) * rate.output);
113
+ }
114
+ function rawUsageToTotals(usage) {
115
+ const cachedInputTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
116
+ return {
117
+ inputTokens: usage.inputTokens,
118
+ cachedInputTokens,
119
+ nonCachedInputTokens: Math.max(0, usage.inputTokens - cachedInputTokens),
120
+ outputTokens: usage.outputTokens,
121
+ reasoningOutputTokens: usage.reasoningOutputTokens,
122
+ totalTokens: usage.totalTokens,
123
+ estimatedCredits: 0,
124
+ eventCount: 0
125
+ };
126
+ }
127
+ function addUsageTotals(target, source) {
128
+ target.inputTokens += source.inputTokens;
129
+ target.cachedInputTokens += source.cachedInputTokens;
130
+ target.nonCachedInputTokens += source.nonCachedInputTokens;
131
+ target.outputTokens += source.outputTokens;
132
+ target.reasoningOutputTokens += source.reasoningOutputTokens;
133
+ target.totalTokens += source.totalTokens;
134
+ target.estimatedCredits += source.estimatedCredits;
135
+ target.eventCount += source.eventCount;
136
+ }
137
+ function addModelUsage(byModel, modelId, usage) {
138
+ const resolvedModelId = modelId || "unknown";
139
+ const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
140
+ const deltaTotals = rawUsageToTotals(usage);
141
+ deltaTotals.estimatedCredits = creditsFor(resolvedModelId, usage);
142
+ deltaTotals.eventCount = 1;
143
+ addUsageTotals(totals, deltaTotals);
144
+ byModel.set(resolvedModelId, totals);
145
+ }
146
+ function sumUsageTotals(rows) {
147
+ const totals = createEmptyUsageTotals();
148
+ for (const row of rows) {
149
+ addUsageTotals(totals, row);
150
+ }
151
+ return totals;
152
+ }
153
+ function formatIsoFromSeconds(seconds) {
154
+ return new Date(seconds * 1000).toISOString().replace(".000Z", "Z");
155
+ }
156
+ function formatIsoFromMilliseconds(milliseconds) {
157
+ return new Date(milliseconds).toISOString().replace(".000Z", "Z");
158
+ }
159
+ function makeWindowKey(scope, rateLimits, window) {
160
+ return [
161
+ scope,
162
+ String(rateLimits.limit_id ?? "unknown"),
163
+ String(rateLimits.plan_type ?? "unknown"),
164
+ numberOrZero(window.window_minutes),
165
+ Math.round(numberOrZero(window.resets_at) / 60)
166
+ ].join("|");
167
+ }
168
+ function upsertWindow(windows, scope, rateLimits, window, eventTimeMs) {
169
+ if (!window) {
170
+ return;
171
+ }
172
+ const windowMinutes = numberOrZero(window.window_minutes);
173
+ const resetsAt = numberOrZero(window.resets_at);
174
+ if (!windowMinutes || !resetsAt) {
175
+ return;
176
+ }
177
+ const startsAt = resetsAt - windowMinutes * 60;
178
+ const usedPercent = numberOrZero(window.used_percent);
179
+ const key = makeWindowKey(scope, rateLimits, window);
180
+ const existing = windows.get(key);
181
+ if (!existing) {
182
+ windows.set(key, {
183
+ scope,
184
+ limitId: String(rateLimits.limit_id ?? "unknown"),
185
+ planType: String(rateLimits.plan_type ?? "unknown"),
186
+ windowMinutes,
187
+ minStartsAt: startsAt,
188
+ maxResetsAt: resetsAt,
189
+ firstSeenMs: eventTimeMs,
190
+ lastSeenMs: eventTimeMs,
191
+ minUsedPercent: usedPercent,
192
+ maxUsedPercent: usedPercent,
193
+ eventCount: 1
194
+ });
195
+ return;
196
+ }
197
+ existing.minStartsAt = Math.min(existing.minStartsAt, startsAt);
198
+ existing.maxResetsAt = Math.max(existing.maxResetsAt, resetsAt);
199
+ existing.firstSeenMs = Math.min(existing.firstSeenMs, eventTimeMs);
200
+ existing.lastSeenMs = Math.max(existing.lastSeenMs, eventTimeMs);
201
+ existing.minUsedPercent = Math.min(existing.minUsedPercent, usedPercent);
202
+ existing.maxUsedPercent = Math.max(existing.maxUsedPercent, usedPercent);
203
+ existing.eventCount += 1;
204
+ }
205
+ function buildWindowLists(windows) {
206
+ const rows = [...windows.values()]
207
+ .map((window) => ({
208
+ scope: window.scope,
209
+ planType: window.planType,
210
+ limitId: window.limitId,
211
+ windowMinutes: window.windowMinutes,
212
+ startTimeIso: formatIsoFromSeconds(window.minStartsAt),
213
+ endTimeIso: formatIsoFromSeconds(window.maxResetsAt),
214
+ firstSeenIso: formatIsoFromMilliseconds(window.firstSeenMs),
215
+ lastSeenIso: formatIsoFromMilliseconds(window.lastSeenMs),
216
+ minUsedPercent: window.minUsedPercent,
217
+ maxUsedPercent: window.maxUsedPercent,
218
+ eventCount: window.eventCount
219
+ }))
220
+ .sort((left, right) => right.endTimeIso.localeCompare(left.endTimeIso));
221
+ const primary = rows.filter((row) => row.scope === "primary").slice(0, 5);
222
+ const secondary = rows.filter((row) => row.scope === "secondary").slice(0, 5);
223
+ return [primary, secondary];
224
+ }
225
+ function isSessionFile(filePath) {
226
+ return filePath.endsWith(".jsonl") && filePath.includes(`${path.sep}.codex${path.sep}sessions${path.sep}`);
227
+ }
228
+ async function* walkSessionFiles(directory) {
229
+ let entries;
230
+ try {
231
+ entries = await fs.promises.readdir(directory, { withFileTypes: true });
232
+ }
233
+ catch {
234
+ return;
235
+ }
236
+ for (const entry of entries) {
237
+ const fullPath = path.join(directory, entry.name);
238
+ if (entry.isDirectory()) {
239
+ yield* walkSessionFiles(fullPath);
240
+ }
241
+ else if (entry.isFile() && isSessionFile(fullPath)) {
242
+ yield fullPath;
243
+ }
244
+ }
245
+ }
246
+ async function parseSessionFile(filePath, byModel, windows, planTypes) {
247
+ const stream = fs.createReadStream(filePath, { encoding: "utf8" });
248
+ const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
249
+ let currentModel = "unknown";
250
+ let previousTotal;
251
+ let linesRead = 0;
252
+ let tokenEvents = 0;
253
+ let malformedLines = 0;
254
+ for await (const line of lineReader) {
255
+ linesRead += 1;
256
+ if (!line.trim()) {
257
+ continue;
258
+ }
259
+ let payloadObject;
260
+ try {
261
+ payloadObject = JSON.parse(line);
262
+ }
263
+ catch {
264
+ malformedLines += 1;
265
+ continue;
266
+ }
267
+ if (payloadObject.type === "turn_context") {
268
+ const payload = payloadObject.payload;
269
+ const collaborationMode = payload?.collaboration_mode;
270
+ const settings = collaborationMode?.settings;
271
+ currentModel = String(payload?.model ?? settings?.model ?? currentModel);
272
+ continue;
273
+ }
274
+ if (payloadObject.type !== "event_msg") {
275
+ continue;
276
+ }
277
+ const payload = payloadObject.payload;
278
+ if (payload?.type !== "token_count") {
279
+ continue;
280
+ }
281
+ const info = payload.info;
282
+ const rateLimits = payload.rate_limits;
283
+ const totalUsage = normalizeRawUsage(info?.total_token_usage);
284
+ const lastUsage = info?.last_token_usage;
285
+ const usage = lastUsage ? normalizeRawUsage(lastUsage) : previousTotal ? subtractRawUsage(totalUsage, previousTotal) : totalUsage;
286
+ previousTotal = totalUsage;
287
+ tokenEvents += 1;
288
+ addModelUsage(byModel, currentModel, usage);
289
+ if (typeof rateLimits?.plan_type === "string") {
290
+ planTypes.add(rateLimits.plan_type);
291
+ }
292
+ const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
293
+ const safeEventTimeMs = Number.isFinite(eventTimeMs) ? eventTimeMs : 0;
294
+ upsertWindow(windows, "primary", rateLimits ?? {}, asRecord(rateLimits?.primary), safeEventTimeMs);
295
+ upsertWindow(windows, "secondary", rateLimits ?? {}, asRecord(rateLimits?.secondary), safeEventTimeMs);
296
+ }
297
+ return { linesRead, tokenEvents, malformedLines };
298
+ }
299
+ function asRecord(value) {
300
+ return value && typeof value === "object" ? value : null;
301
+ }
@@ -0,0 +1,18 @@
1
+ export class UsageProviderBase {
2
+ constructor(id, label) {
3
+ this.id = id;
4
+ this.label = label;
5
+ }
6
+ }
7
+ export function createEmptyUsageTotals() {
8
+ return {
9
+ inputTokens: 0,
10
+ cachedInputTokens: 0,
11
+ nonCachedInputTokens: 0,
12
+ outputTokens: 0,
13
+ reasoningOutputTokens: 0,
14
+ totalTokens: 0,
15
+ estimatedCredits: 0,
16
+ eventCount: 0
17
+ };
18
+ }
@@ -0,0 +1,6 @@
1
+ import { CodexUsageProvider } from "./codex.js";
2
+ export function createProviders() {
3
+ return [new CodexUsageProvider()];
4
+ }
5
+ export { CodexUsageProvider } from "./codex.js";
6
+ export { UsageProviderBase } from "./contract.js";
@@ -0,0 +1,4 @@
1
+ {
2
+ "private": true,
3
+ "type": "module"
4
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "letmecode",
3
+ "version": "0.1.0",
4
+ "description": "Provider-based terminal usage dashboard for LetMeCode.",
5
+ "author": "devforth.io",
6
+ "license": "MIT",
7
+ "type": "commonjs",
8
+ "bin": "./bin/letmecode.js",
9
+ "files": [
10
+ "bin",
11
+ "dist",
12
+ "ink-app/dist",
13
+ "ink-app/package.json"
14
+ ],
15
+ "engines": {
16
+ "node": ">=14.16"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "keywords": [
22
+ "cli",
23
+ "ink",
24
+ "npx",
25
+ "typescript"
26
+ ],
27
+ "dependencies": {
28
+ "ink": "4.4.1",
29
+ "react": "18.3.1"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^24.0.7",
33
+ "@types/react": "^18.3.24",
34
+ "typescript": "^5.8.3"
35
+ },
36
+ "scripts": {
37
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
38
+ "build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
39
+ "prestart": "npm run build",
40
+ "start": "node ./bin/letmecode.js",
41
+ "pretest": "npm run build",
42
+ "smoke": "node ./bin/letmecode.js",
43
+ "test": "node --test ink-app/test/*.test.mjs"
44
+ }
45
+ }