@wukazis/euphony 0.1.45 → 0.1.46
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 +10 -2
- package/dist/assets/{main-CmldcHcT.js → main-DS3CAMrw.js} +662 -486
- package/dist/index.html +1 -1
- package/lib/components/app/app.d.ts +23 -4
- package/lib/types/common-types.d.ts +15 -2
- package/lib/utils/api-manager.d.ts +6 -2
- package/package.json +1 -1
- package/server-dist/node-main.js +352 -17
package/dist/index.html
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<title>Euphony: Visualize Chat Data in Your Browser</title>
|
|
14
14
|
|
|
15
15
|
<link rel="stylesheet" href="/global.css" />
|
|
16
|
-
<script type="module" crossorigin src="/assets/main-
|
|
16
|
+
<script type="module" crossorigin src="/assets/main-DS3CAMrw.js"></script>
|
|
17
17
|
</head>
|
|
18
18
|
|
|
19
19
|
<body>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LitElement, PropertyValues } from 'lit';
|
|
2
|
-
import { CodexSessionSummary } from '../../types/common-types';
|
|
2
|
+
import { CodexSessionSummary, CodexUsageDay } from '../../types/common-types';
|
|
3
3
|
import { Conversation } from '../../types/harmony-types';
|
|
4
4
|
import { APIManager, BrowserAPIManager } from '../../utils/api-manager';
|
|
5
5
|
import { EuphonyCodex } from '../codex/codex';
|
|
@@ -21,10 +21,13 @@ declare enum DataType {
|
|
|
21
21
|
CONVERSATION = "conversation",
|
|
22
22
|
CODEX = "codex",
|
|
23
23
|
CODEX_SESSION_INDEX = "codex-session-index",
|
|
24
|
+
CODEX_USAGE = "codex-usage",
|
|
24
25
|
JSON = "json"
|
|
25
26
|
}
|
|
26
|
-
type MenuItems = 'Load without cache' | 'Load Codex sessions' | 'Load from clipboard' | 'Load local file' | 'Editor mode' | 'Leave editor mode' | 'Filter data' | 'Preferences' | 'Code';
|
|
27
|
+
type MenuItems = 'Load without cache' | 'Load Codex sessions' | 'Usage' | 'Load from clipboard' | 'Load local file' | 'Editor mode' | 'Leave editor mode' | 'Filter data' | 'Preferences' | 'Code';
|
|
28
|
+
type CodexUsageRange = '7d' | '30d' | '1y';
|
|
27
29
|
type ConversationViewerElement = EuphonyConversation | EuphonyCodex;
|
|
30
|
+
export declare const isCodexUsageRange: (value: string) => value is CodexUsageRange;
|
|
28
31
|
/**
|
|
29
32
|
* App element.
|
|
30
33
|
*
|
|
@@ -35,12 +38,17 @@ export declare class EuphonyApp extends LitElement {
|
|
|
35
38
|
JSONData: Record<string, unknown>[];
|
|
36
39
|
codexSessionData: unknown[][];
|
|
37
40
|
codexSessionSummaries: CodexSessionSummary[];
|
|
41
|
+
codexUsageDays: CodexUsageDay[];
|
|
42
|
+
codexUsageChartDays: CodexUsageDay[];
|
|
43
|
+
codexUsageRange: CodexUsageRange;
|
|
38
44
|
dataType: DataType;
|
|
39
45
|
isLoadingData: boolean;
|
|
40
46
|
curPage: number;
|
|
41
47
|
globalIsShowingMetadata: boolean;
|
|
42
48
|
globalShouldRenderMarkdown: boolean;
|
|
43
49
|
jmespathQuery: string;
|
|
50
|
+
codexSearchQuery: string;
|
|
51
|
+
codexSearchInputValue: string;
|
|
44
52
|
focusModeAuthor: string[];
|
|
45
53
|
focusModeRecipient: string[];
|
|
46
54
|
focusModeContentType: string[];
|
|
@@ -114,6 +122,7 @@ export declare class EuphonyApp extends LitElement {
|
|
|
114
122
|
selectAllButtonClicked(): void;
|
|
115
123
|
updatePageNumber(newPageNumber: number, scrollToTop: boolean): Promise<void>;
|
|
116
124
|
loadCodexSessionsClicked(): Promise<void>;
|
|
125
|
+
loadCodexUsageClicked(): Promise<void>;
|
|
117
126
|
openCodexSessionSummary(summary: CodexSessionSummary): Promise<void>;
|
|
118
127
|
pageClicked(e: CustomEvent<number>): void;
|
|
119
128
|
itemsPerPageChanged(e: CustomEvent<number>): void;
|
|
@@ -133,6 +142,10 @@ export declare class EuphonyApp extends LitElement {
|
|
|
133
142
|
preferenceWindowTranslateAllClicked(): void;
|
|
134
143
|
preferenceWindowFocusModeSettingsChanged(e: CustomEvent<FocusModeSettings>): void;
|
|
135
144
|
searchWindowQuerySubmitted(e: CustomEvent<string>): Promise<void>;
|
|
145
|
+
codexSessionSearchInputChanged(e: Event): void;
|
|
146
|
+
codexSessionSearchSubmitted(e: Event): Promise<void>;
|
|
147
|
+
clearCodexSessionSearch(): Promise<void>;
|
|
148
|
+
codexUsageRangeChanged(e: Event): Promise<void>;
|
|
136
149
|
/**
|
|
137
150
|
* Show the token window when user clicks on the harmony render button
|
|
138
151
|
* @param e CustomEvent<string> - The custom event containing the conversation string
|
|
@@ -165,21 +178,27 @@ export declare class EuphonyApp extends LitElement {
|
|
|
165
178
|
loadDataFromFile: (sourceFile: File) => Promise<void>;
|
|
166
179
|
localDataWorkerMessageHandler(e: MessageEvent<LocalDataWorkerMessage>): void;
|
|
167
180
|
localFileInputChanged(e: Event): void;
|
|
168
|
-
loadData: ({ blobURL, offset, limit, showSuccessToast, noCache, jmespathQuery }: {
|
|
181
|
+
loadData: ({ blobURL, offset, limit, showSuccessToast, noCache, jmespathQuery, codexSearchQuery, codexUsageRange }: {
|
|
169
182
|
blobURL: string;
|
|
170
183
|
offset: number;
|
|
171
184
|
limit: number;
|
|
172
185
|
showSuccessToast?: boolean;
|
|
173
186
|
noCache?: boolean;
|
|
174
187
|
jmespathQuery?: string;
|
|
188
|
+
codexSearchQuery?: string;
|
|
189
|
+
codexUsageRange?: CodexUsageRange;
|
|
175
190
|
}) => Promise<{
|
|
176
191
|
isLoadDataSuccessful: boolean;
|
|
177
192
|
loadDataMessage: string;
|
|
178
193
|
loadedURL: string;
|
|
179
194
|
}>;
|
|
180
|
-
resetFilter: (filter: "jmespath" | "concept") => Promise<void>;
|
|
195
|
+
resetFilter: (filter: "jmespath" | "codexSearch" | "concept") => Promise<void>;
|
|
181
196
|
resetHash: () => void;
|
|
182
197
|
buildEuphonyStyle(styleConfig: Record<string, string>): string;
|
|
198
|
+
getCodexUsageBucketKey(date: Date, range: CodexUsageRange): string;
|
|
199
|
+
getCodexUsageBucketLabel(bucketKey: string, range?: CodexUsageRange): string;
|
|
200
|
+
buildCodexUsageChartDays(): CodexUsageDay[];
|
|
201
|
+
renderCodexUsageChart(): import('lit').TemplateResult<1>;
|
|
183
202
|
getConversationViewerElements(): ConversationViewerElement[];
|
|
184
203
|
render(): import('lit').TemplateResult<1>;
|
|
185
204
|
static styles: import('lit').CSSResult[];
|
|
@@ -49,10 +49,23 @@ export interface CodexSessionSummary {
|
|
|
49
49
|
timestamp: string | null;
|
|
50
50
|
age_seconds: number | null;
|
|
51
51
|
event_count: number;
|
|
52
|
+
token_usage: CodexTokenUsage | null;
|
|
53
|
+
usage_timestamp: string | null;
|
|
54
|
+
}
|
|
55
|
+
export interface CodexTokenUsage {
|
|
56
|
+
input_tokens: number;
|
|
57
|
+
cached_input_tokens: number;
|
|
58
|
+
output_tokens: number;
|
|
59
|
+
reasoning_output_tokens: number;
|
|
60
|
+
total_tokens: number;
|
|
61
|
+
}
|
|
62
|
+
export interface CodexUsageDay extends CodexTokenUsage {
|
|
63
|
+
date: string;
|
|
64
|
+
session_count: number;
|
|
52
65
|
}
|
|
53
66
|
export interface BlobJSONLPayload {
|
|
54
67
|
total: number;
|
|
55
|
-
data: string[] | Conversation[] | Record<string, unknown>[] | unknown[][] | CodexSessionSummary[];
|
|
68
|
+
data: string[] | Conversation[] | Record<string, unknown>[] | unknown[][] | CodexSessionSummary[] | CodexUsageDay[];
|
|
56
69
|
isFiltered: boolean;
|
|
57
70
|
matchedCount: number;
|
|
58
71
|
resolvedURL: string;
|
|
@@ -61,7 +74,7 @@ export interface BlobJSONLResponse {
|
|
|
61
74
|
offset: number;
|
|
62
75
|
limit: number;
|
|
63
76
|
total: number;
|
|
64
|
-
data: string[] | Conversation[] | Record<string, Conversation | string>[] | unknown[][] | CodexSessionSummary[];
|
|
77
|
+
data: string[] | Conversation[] | Record<string, Conversation | string>[] | unknown[][] | CodexSessionSummary[] | CodexUsageDay[];
|
|
65
78
|
isFiltered: boolean;
|
|
66
79
|
matchedCount: number;
|
|
67
80
|
resolvedURL: string;
|
|
@@ -5,23 +5,27 @@ export declare const extractConversationFromJSONL: (data: unknown[]) => Conversa
|
|
|
5
5
|
export declare class APIManager {
|
|
6
6
|
apiBaseURL: string;
|
|
7
7
|
constructor(apiBaseURL: string);
|
|
8
|
-
getJSONL: ({ blobURL, offset, limit, noCache, jmespathQuery }: {
|
|
8
|
+
getJSONL: ({ blobURL, offset, limit, noCache, jmespathQuery, codexSearchQuery, codexUsageRange }: {
|
|
9
9
|
blobURL: string;
|
|
10
10
|
offset: number;
|
|
11
11
|
limit: number;
|
|
12
12
|
noCache: boolean;
|
|
13
13
|
jmespathQuery: string;
|
|
14
|
+
codexSearchQuery?: string;
|
|
15
|
+
codexUsageRange?: string;
|
|
14
16
|
}) => Promise<BlobJSONLPayload>;
|
|
15
17
|
refreshRendererList: () => Promise<string[]>;
|
|
16
18
|
harmonyRender: (conversation: string, renderer: string) => Promise<HarmonyRenderResponse>;
|
|
17
19
|
}
|
|
18
20
|
export declare class BrowserAPIManager {
|
|
19
|
-
getJSONL: ({ blobURL, offset, limit, noCache, jmespathQuery }: {
|
|
21
|
+
getJSONL: ({ blobURL, offset, limit, noCache, jmespathQuery, codexSearchQuery, codexUsageRange }: {
|
|
20
22
|
blobURL: string;
|
|
21
23
|
offset: number;
|
|
22
24
|
limit: number;
|
|
23
25
|
noCache: boolean;
|
|
24
26
|
jmespathQuery: string;
|
|
27
|
+
codexSearchQuery?: string;
|
|
28
|
+
codexUsageRange?: string;
|
|
25
29
|
}) => Promise<BlobJSONLPayload>;
|
|
26
30
|
validateOpenAIAPIKey(apiKey: string): Promise<boolean>;
|
|
27
31
|
/**
|
package/package.json
CHANGED
package/server-dist/node-main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
|
-
import { stat,
|
|
3
|
+
import { stat, readdir, open, readFile } from "node:fs/promises";
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
import { dirname, resolve, join, extname, basename, sep } from "node:path";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -434,12 +434,17 @@ const SERVER_DIR = dirname(fileURLToPath(import.meta.url));
|
|
|
434
434
|
const PACKAGE_ROOT = resolve(SERVER_DIR, "..");
|
|
435
435
|
const DIST_DIR = resolve(PACKAGE_ROOT, "dist");
|
|
436
436
|
const CODEX_SESSIONS_URL = "codex:sessions";
|
|
437
|
+
const CODEX_USAGE_URL = "codex:usage";
|
|
437
438
|
const CODEX_SESSION_URL_PREFIX = "codex:session:";
|
|
438
439
|
const CODEX_SESSIONS_DIR = resolve(
|
|
439
440
|
process.env.CODEX_SESSIONS_DIR ?? join(process.env.HOME ?? process.cwd(), ".codex", "sessions")
|
|
440
441
|
);
|
|
441
442
|
const MAX_PUBLIC_JSON_BYTES = 25 * 1024 * 1024;
|
|
442
443
|
const MAX_CODEX_SESSION_BYTES = 25 * 1024 * 1024;
|
|
444
|
+
const MAX_CODEX_SEARCH_PREVIEW_BYTES = 256 * 1024;
|
|
445
|
+
const CODEX_USAGE_TAIL_CHUNK_BYTES = 64 * 1024;
|
|
446
|
+
const MAX_CODEX_USAGE_TAIL_BYTES = 2 * 1024 * 1024;
|
|
447
|
+
const CODEX_USAGE_CACHE_TTL_MS = 60 * 1e3;
|
|
443
448
|
const MAX_JSON_BODY_BYTES = 2 * 1024 * 1024;
|
|
444
449
|
const TRANSLATION_CACHE_TTL_MS = 5 * 60 * 60 * 1e3;
|
|
445
450
|
const TRANSLATION_MAX_CONCURRENCY = 1024;
|
|
@@ -453,6 +458,8 @@ class HttpError extends Error {
|
|
|
453
458
|
}
|
|
454
459
|
const translationCache = /* @__PURE__ */ new Map();
|
|
455
460
|
const inflightTranslations = /* @__PURE__ */ new Map();
|
|
461
|
+
const codexUsageCache = /* @__PURE__ */ new Map();
|
|
462
|
+
const inflightCodexUsage = /* @__PURE__ */ new Map();
|
|
456
463
|
let activeTranslations = 0;
|
|
457
464
|
const translationWaiters = [];
|
|
458
465
|
const normalizeLimit = (value, fallback) => {
|
|
@@ -463,6 +470,24 @@ const normalizeOffset = (value) => {
|
|
|
463
470
|
const parsed = Number.parseInt(value ?? "", 10);
|
|
464
471
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
465
472
|
};
|
|
473
|
+
const normalizeCodexUsageRange = (value) => {
|
|
474
|
+
if (value === "7d" || value === "30d" || value === "1y") {
|
|
475
|
+
return value;
|
|
476
|
+
}
|
|
477
|
+
return "30d";
|
|
478
|
+
};
|
|
479
|
+
const formatNumberPart = (value, length = 2) => value.toString().padStart(length, "0");
|
|
480
|
+
const formatLocalDateBucket = (timestamp) => `${timestamp.getFullYear()}-${formatNumberPart(timestamp.getMonth() + 1)}-${formatNumberPart(timestamp.getDate())}`;
|
|
481
|
+
const codexUsageRangeStart = (range) => {
|
|
482
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
483
|
+
const start = /* @__PURE__ */ new Date();
|
|
484
|
+
const daysBack = range === "7d" ? 6 : range === "30d" ? 29 : 364;
|
|
485
|
+
start.setHours(0, 0, 0, 0);
|
|
486
|
+
return new Date(start.getTime() - daysBack * dayMs);
|
|
487
|
+
};
|
|
488
|
+
const codexUsageBucketKey = (timestamp, range) => {
|
|
489
|
+
return formatLocalDateBucket(timestamp);
|
|
490
|
+
};
|
|
466
491
|
const isInsideDirectory = (root, candidate) => {
|
|
467
492
|
const relative = resolve(candidate).slice(resolve(root).length);
|
|
468
493
|
return candidate === root || relative.startsWith(sep) && !relative.slice(1).startsWith(`..${sep}`);
|
|
@@ -498,13 +523,15 @@ const parseJsonOrJsonl = (text) => {
|
|
|
498
523
|
return parseJsonl(text);
|
|
499
524
|
}
|
|
500
525
|
};
|
|
501
|
-
const
|
|
526
|
+
const loadJsonlText = async (path) => {
|
|
502
527
|
const fileStat = await stat(path);
|
|
503
528
|
if (fileStat.size > MAX_CODEX_SESSION_BYTES) {
|
|
504
529
|
throw new Error(`Codex session file is too large: ${path}`);
|
|
505
530
|
}
|
|
506
|
-
|
|
507
|
-
|
|
531
|
+
return stripBom(await readFile(path, "utf8"));
|
|
532
|
+
};
|
|
533
|
+
const loadJsonlEvents = async (path) => {
|
|
534
|
+
return parseJsonl(await loadJsonlText(path));
|
|
508
535
|
};
|
|
509
536
|
const resolveCodexSessionPath = (sessionRef) => {
|
|
510
537
|
const relativePath = decodeURIComponent(sessionRef);
|
|
@@ -522,6 +549,77 @@ const parseTimestamp = (timestamp) => {
|
|
|
522
549
|
const parsed = new Date(normalized);
|
|
523
550
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
524
551
|
};
|
|
552
|
+
const numberFromUsageField = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
553
|
+
const emptyCodexTokenUsage = () => ({
|
|
554
|
+
input_tokens: 0,
|
|
555
|
+
cached_input_tokens: 0,
|
|
556
|
+
output_tokens: 0,
|
|
557
|
+
reasoning_output_tokens: 0,
|
|
558
|
+
total_tokens: 0
|
|
559
|
+
});
|
|
560
|
+
const addCodexTokenUsage = (target, source) => {
|
|
561
|
+
target.input_tokens += source.input_tokens;
|
|
562
|
+
target.cached_input_tokens += source.cached_input_tokens;
|
|
563
|
+
target.output_tokens += source.output_tokens;
|
|
564
|
+
target.reasoning_output_tokens += source.reasoning_output_tokens;
|
|
565
|
+
target.total_tokens += source.total_tokens;
|
|
566
|
+
};
|
|
567
|
+
const extractCodexTokenUsage = (value) => {
|
|
568
|
+
if (!value || typeof value !== "object") {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
const record = value;
|
|
572
|
+
const usage = {
|
|
573
|
+
input_tokens: numberFromUsageField(record.input_tokens),
|
|
574
|
+
cached_input_tokens: numberFromUsageField(
|
|
575
|
+
record.cached_input_tokens ?? record.cached_tokens
|
|
576
|
+
),
|
|
577
|
+
output_tokens: numberFromUsageField(record.output_tokens),
|
|
578
|
+
reasoning_output_tokens: numberFromUsageField(
|
|
579
|
+
record.reasoning_output_tokens
|
|
580
|
+
),
|
|
581
|
+
total_tokens: numberFromUsageField(record.total_tokens)
|
|
582
|
+
};
|
|
583
|
+
if (usage.total_tokens === 0 && (usage.input_tokens > 0 || usage.output_tokens > 0)) {
|
|
584
|
+
usage.total_tokens = usage.input_tokens + usage.output_tokens;
|
|
585
|
+
}
|
|
586
|
+
return usage.total_tokens > 0 ? usage : null;
|
|
587
|
+
};
|
|
588
|
+
const extractCodexUsageSnapshotFromEvent = (event) => {
|
|
589
|
+
if (!event || typeof event !== "object") {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
const record = event;
|
|
593
|
+
const payload = record.payload;
|
|
594
|
+
if (record.type !== "event_msg" || !payload || typeof payload !== "object") {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
const payloadRecord = payload;
|
|
598
|
+
if (payloadRecord.type !== "token_count") {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
const info = payloadRecord.info;
|
|
602
|
+
if (!info || typeof info !== "object") {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
const usage = extractCodexTokenUsage(
|
|
606
|
+
info.total_token_usage
|
|
607
|
+
);
|
|
608
|
+
if (!usage) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
usage,
|
|
613
|
+
timestamp: parseTimestamp(record.timestamp)
|
|
614
|
+
};
|
|
615
|
+
};
|
|
616
|
+
const extractCodexUsageSnapshotFromEvents = (events) => {
|
|
617
|
+
let snapshot = null;
|
|
618
|
+
for (const event of events) {
|
|
619
|
+
snapshot = extractCodexUsageSnapshotFromEvent(event) ?? snapshot;
|
|
620
|
+
}
|
|
621
|
+
return snapshot;
|
|
622
|
+
};
|
|
525
623
|
const extractTextFromContent = (content) => {
|
|
526
624
|
if (typeof content === "string") {
|
|
527
625
|
const stripped = content.trim();
|
|
@@ -567,12 +665,140 @@ const cleanSummaryText = (text, maxLength = 240) => {
|
|
|
567
665
|
return cleaned.length <= maxLength ? cleaned : `${cleaned.slice(0, maxLength - 3).trimEnd()}...`;
|
|
568
666
|
};
|
|
569
667
|
const relativeCodexPath = (path) => resolve(path).slice(resolve(CODEX_SESSIONS_DIR).length + 1).split(sep).join("/");
|
|
668
|
+
const normalizeCodexSearchQuery = (query) => query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
669
|
+
const extractDisplayableUserMessage = (record) => {
|
|
670
|
+
const payload = record.payload;
|
|
671
|
+
if (!payload || typeof payload !== "object") {
|
|
672
|
+
return "";
|
|
673
|
+
}
|
|
674
|
+
const payloadRecord = payload;
|
|
675
|
+
let candidate = "";
|
|
676
|
+
if (record.type === "response_item" && payloadRecord.role === "user") {
|
|
677
|
+
candidate = extractTextFromContent(payloadRecord.content) ?? "";
|
|
678
|
+
} else if (record.type === "event_msg" && payloadRecord.type === "user_message") {
|
|
679
|
+
candidate = extractTextFromContent(payloadRecord) ?? "";
|
|
680
|
+
}
|
|
681
|
+
return candidate.length > 0 && isDisplayableFirstMessage(candidate) ? candidate : "";
|
|
682
|
+
};
|
|
683
|
+
const readCodexSessionFirstMessage = async (path) => {
|
|
684
|
+
const fileStat = await stat(path);
|
|
685
|
+
const previewByteLength = Math.min(
|
|
686
|
+
fileStat.size,
|
|
687
|
+
MAX_CODEX_SEARCH_PREVIEW_BYTES
|
|
688
|
+
);
|
|
689
|
+
const fileHandle = await open(path, "r");
|
|
690
|
+
let text = "";
|
|
691
|
+
try {
|
|
692
|
+
const buffer = Buffer.alloc(previewByteLength);
|
|
693
|
+
const { bytesRead } = await fileHandle.read(
|
|
694
|
+
buffer,
|
|
695
|
+
0,
|
|
696
|
+
previewByteLength,
|
|
697
|
+
0
|
|
698
|
+
);
|
|
699
|
+
text = stripBom(buffer.subarray(0, bytesRead).toString("utf8"));
|
|
700
|
+
} finally {
|
|
701
|
+
await fileHandle.close();
|
|
702
|
+
}
|
|
703
|
+
const lines = text.split(/\r?\n/);
|
|
704
|
+
if (fileStat.size > previewByteLength) {
|
|
705
|
+
lines.pop();
|
|
706
|
+
}
|
|
707
|
+
for (const line of lines) {
|
|
708
|
+
const stripped = line.trim();
|
|
709
|
+
if (stripped.length === 0) {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
let event;
|
|
713
|
+
try {
|
|
714
|
+
event = JSON.parse(stripped);
|
|
715
|
+
} catch {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (!event || typeof event !== "object") {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const candidate = extractDisplayableUserMessage(
|
|
722
|
+
event
|
|
723
|
+
);
|
|
724
|
+
if (candidate.length > 0) {
|
|
725
|
+
return cleanSummaryText(candidate, 1e3);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return "";
|
|
729
|
+
};
|
|
730
|
+
const readCodexUsageSnapshot = async (path) => {
|
|
731
|
+
const fileStat = await stat(path);
|
|
732
|
+
if (fileStat.size === 0) {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
const fileHandle = await open(path, "r");
|
|
736
|
+
let cursor = fileStat.size;
|
|
737
|
+
let scannedBytes = 0;
|
|
738
|
+
let tailText = "";
|
|
739
|
+
try {
|
|
740
|
+
while (cursor > 0 && scannedBytes < MAX_CODEX_USAGE_TAIL_BYTES) {
|
|
741
|
+
const chunkSize = Math.min(
|
|
742
|
+
CODEX_USAGE_TAIL_CHUNK_BYTES,
|
|
743
|
+
cursor,
|
|
744
|
+
MAX_CODEX_USAGE_TAIL_BYTES - scannedBytes
|
|
745
|
+
);
|
|
746
|
+
cursor -= chunkSize;
|
|
747
|
+
const buffer = Buffer.alloc(chunkSize);
|
|
748
|
+
const { bytesRead } = await fileHandle.read(
|
|
749
|
+
buffer,
|
|
750
|
+
0,
|
|
751
|
+
chunkSize,
|
|
752
|
+
cursor
|
|
753
|
+
);
|
|
754
|
+
if (bytesRead === 0) {
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
scannedBytes += bytesRead;
|
|
758
|
+
tailText = buffer.subarray(0, bytesRead).toString("utf8") + tailText;
|
|
759
|
+
const lines = tailText.split(/\r?\n/);
|
|
760
|
+
const firstCompleteLineIndex = cursor > 0 ? 1 : 0;
|
|
761
|
+
for (let i = lines.length - 1; i >= firstCompleteLineIndex; i--) {
|
|
762
|
+
const stripped = stripBom(lines[i]).trim();
|
|
763
|
+
if (stripped.length === 0 || !stripped.includes('"token_count"')) {
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
let event;
|
|
767
|
+
try {
|
|
768
|
+
event = JSON.parse(stripped);
|
|
769
|
+
} catch {
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const snapshot = extractCodexUsageSnapshotFromEvent(event);
|
|
773
|
+
if (snapshot) {
|
|
774
|
+
return snapshot;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
} finally {
|
|
779
|
+
await fileHandle.close();
|
|
780
|
+
}
|
|
781
|
+
return null;
|
|
782
|
+
};
|
|
783
|
+
const codexSessionMatchesSearch = async (path, searchTerms) => {
|
|
784
|
+
if (searchTerms.length === 0) {
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
const relativePath = relativeCodexPath(path).toLowerCase();
|
|
788
|
+
if (searchTerms.every((term) => relativePath.includes(term))) {
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
const searchableText = `${relativePath}
|
|
792
|
+
${await readCodexSessionFirstMessage(path)}`.toLowerCase();
|
|
793
|
+
return searchTerms.every((term) => searchableText.includes(term));
|
|
794
|
+
};
|
|
570
795
|
const summarizeCodexSession = async (path) => {
|
|
571
796
|
const events = await loadJsonlEvents(path);
|
|
572
797
|
const relativePath = relativeCodexPath(path);
|
|
573
798
|
const encodedPath = encodeURIComponent(relativePath);
|
|
574
799
|
let sessionTimestamp = null;
|
|
575
800
|
let firstMessage = "";
|
|
801
|
+
const usageSnapshot = extractCodexUsageSnapshotFromEvents(events);
|
|
576
802
|
for (const event of events) {
|
|
577
803
|
if (!event || typeof event !== "object") {
|
|
578
804
|
continue;
|
|
@@ -596,12 +822,7 @@ const summarizeCodexSession = async (path) => {
|
|
|
596
822
|
if (firstMessage.length > 0) {
|
|
597
823
|
continue;
|
|
598
824
|
}
|
|
599
|
-
|
|
600
|
-
if (record.type === "response_item" && payloadRecord.role === "user") {
|
|
601
|
-
candidate = extractTextFromContent(payloadRecord.content) ?? "";
|
|
602
|
-
} else if (record.type === "event_msg" && payloadRecord.type === "user_message") {
|
|
603
|
-
candidate = extractTextFromContent(payloadRecord) ?? "";
|
|
604
|
-
}
|
|
825
|
+
const candidate = extractDisplayableUserMessage(record);
|
|
605
826
|
if (candidate.length > 0 && isDisplayableFirstMessage(candidate)) {
|
|
606
827
|
firstMessage = cleanSummaryText(candidate);
|
|
607
828
|
}
|
|
@@ -623,7 +844,9 @@ const summarizeCodexSession = async (path) => {
|
|
|
623
844
|
first_message: firstMessage,
|
|
624
845
|
timestamp: sessionTimestamp.toISOString(),
|
|
625
846
|
age_seconds: ageSeconds,
|
|
626
|
-
event_count: events.length
|
|
847
|
+
event_count: events.length,
|
|
848
|
+
token_usage: usageSnapshot?.usage ?? null,
|
|
849
|
+
usage_timestamp: usageSnapshot?.timestamp?.toISOString() ?? null
|
|
627
850
|
};
|
|
628
851
|
};
|
|
629
852
|
const findJsonlFiles = async (root) => {
|
|
@@ -658,7 +881,8 @@ const findJsonlFiles = async (root) => {
|
|
|
658
881
|
withStats.sort((a, b) => b.mtime - a.mtime || b.path.localeCompare(a.path));
|
|
659
882
|
return withStats.map((item) => item.path);
|
|
660
883
|
};
|
|
661
|
-
const getCodexSessions = async (offset, limit) => {
|
|
884
|
+
const getCodexSessions = async (offset, limit, searchQuery = "") => {
|
|
885
|
+
const searchTerms = normalizeCodexSearchQuery(searchQuery);
|
|
662
886
|
try {
|
|
663
887
|
const rootStat = await stat(CODEX_SESSIONS_DIR);
|
|
664
888
|
if (!rootStat.isDirectory()) {
|
|
@@ -670,13 +894,27 @@ const getCodexSessions = async (offset, limit) => {
|
|
|
670
894
|
offset,
|
|
671
895
|
limit,
|
|
672
896
|
total: 0,
|
|
673
|
-
isFiltered:
|
|
897
|
+
isFiltered: searchTerms.length > 0,
|
|
674
898
|
matchedCount: 0,
|
|
675
899
|
resolvedURL: CODEX_SESSIONS_URL
|
|
676
900
|
};
|
|
677
901
|
}
|
|
678
902
|
const files = await findJsonlFiles(CODEX_SESSIONS_DIR);
|
|
679
|
-
const
|
|
903
|
+
const matchedFiles = [];
|
|
904
|
+
if (searchTerms.length === 0) {
|
|
905
|
+
matchedFiles.push(...files);
|
|
906
|
+
} else {
|
|
907
|
+
for (const path of files) {
|
|
908
|
+
try {
|
|
909
|
+
if (await codexSessionMatchesSearch(path, searchTerms)) {
|
|
910
|
+
matchedFiles.push(path);
|
|
911
|
+
}
|
|
912
|
+
} catch (error) {
|
|
913
|
+
console.error(`Failed to search Codex session file: ${path}`, error);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const pageFiles = matchedFiles.slice(offset, offset + limit);
|
|
680
918
|
const summaries = [];
|
|
681
919
|
for (const path of pageFiles) {
|
|
682
920
|
try {
|
|
@@ -690,11 +928,89 @@ const getCodexSessions = async (offset, limit) => {
|
|
|
690
928
|
offset,
|
|
691
929
|
limit,
|
|
692
930
|
total: files.length,
|
|
693
|
-
isFiltered:
|
|
694
|
-
matchedCount:
|
|
931
|
+
isFiltered: searchTerms.length > 0,
|
|
932
|
+
matchedCount: matchedFiles.length,
|
|
695
933
|
resolvedURL: CODEX_SESSIONS_URL
|
|
696
934
|
};
|
|
697
935
|
};
|
|
936
|
+
const buildCodexUsageDays = async (range) => {
|
|
937
|
+
try {
|
|
938
|
+
const rootStat = await stat(CODEX_SESSIONS_DIR);
|
|
939
|
+
if (!rootStat.isDirectory()) {
|
|
940
|
+
throw new Error("Not a directory");
|
|
941
|
+
}
|
|
942
|
+
} catch {
|
|
943
|
+
return [];
|
|
944
|
+
}
|
|
945
|
+
const files = await findJsonlFiles(CODEX_SESSIONS_DIR);
|
|
946
|
+
const usageByDay = /* @__PURE__ */ new Map();
|
|
947
|
+
const rangeStart = codexUsageRangeStart(range);
|
|
948
|
+
const now = /* @__PURE__ */ new Date();
|
|
949
|
+
for (const path of files) {
|
|
950
|
+
try {
|
|
951
|
+
const snapshot = await readCodexUsageSnapshot(path);
|
|
952
|
+
if (!snapshot) {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
const fileStat = await stat(path);
|
|
956
|
+
const timestamp = snapshot.timestamp ?? new Date(fileStat.mtimeMs);
|
|
957
|
+
if (timestamp < rangeStart || timestamp > now) {
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
const date = codexUsageBucketKey(timestamp, range);
|
|
961
|
+
const dayUsage = usageByDay.get(date) ?? {
|
|
962
|
+
date,
|
|
963
|
+
session_count: 0,
|
|
964
|
+
...emptyCodexTokenUsage()
|
|
965
|
+
};
|
|
966
|
+
dayUsage.session_count += 1;
|
|
967
|
+
addCodexTokenUsage(dayUsage, snapshot.usage);
|
|
968
|
+
usageByDay.set(date, dayUsage);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
console.error(`Failed to read Codex usage from file: ${path}`, error);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
const days = [...usageByDay.values()].sort(
|
|
974
|
+
(a, b) => b.date.localeCompare(a.date)
|
|
975
|
+
);
|
|
976
|
+
return days;
|
|
977
|
+
};
|
|
978
|
+
const getCodexUsageDays = async (range, noCache) => {
|
|
979
|
+
if (noCache) {
|
|
980
|
+
codexUsageCache.delete(range);
|
|
981
|
+
}
|
|
982
|
+
const cached = codexUsageCache.get(range);
|
|
983
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
984
|
+
return cached.days;
|
|
985
|
+
}
|
|
986
|
+
const inflight = inflightCodexUsage.get(range);
|
|
987
|
+
if (inflight) {
|
|
988
|
+
return inflight;
|
|
989
|
+
}
|
|
990
|
+
const promise = buildCodexUsageDays(range).then((days) => {
|
|
991
|
+
codexUsageCache.set(range, {
|
|
992
|
+
expiresAt: Date.now() + CODEX_USAGE_CACHE_TTL_MS,
|
|
993
|
+
days
|
|
994
|
+
});
|
|
995
|
+
return days;
|
|
996
|
+
}).finally(() => {
|
|
997
|
+
inflightCodexUsage.delete(range);
|
|
998
|
+
});
|
|
999
|
+
inflightCodexUsage.set(range, promise);
|
|
1000
|
+
return promise;
|
|
1001
|
+
};
|
|
1002
|
+
const getCodexUsage = async (offset, limit, range, noCache) => {
|
|
1003
|
+
const days = await getCodexUsageDays(range, noCache);
|
|
1004
|
+
return {
|
|
1005
|
+
data: days.slice(offset, offset + limit),
|
|
1006
|
+
offset,
|
|
1007
|
+
limit,
|
|
1008
|
+
total: days.length,
|
|
1009
|
+
isFiltered: false,
|
|
1010
|
+
matchedCount: days.length,
|
|
1011
|
+
resolvedURL: CODEX_USAGE_URL
|
|
1012
|
+
};
|
|
1013
|
+
};
|
|
698
1014
|
const getCodexSession = async (sessionRef, offset, limit) => {
|
|
699
1015
|
const path = resolveCodexSessionPath(sessionRef);
|
|
700
1016
|
try {
|
|
@@ -1146,8 +1462,26 @@ const handleBlobJsonl = async (req, res, url) => {
|
|
|
1146
1462
|
const limit = normalizeLimit(url.searchParams.get("limit"), 10);
|
|
1147
1463
|
const noCache = url.searchParams.get("noCache") === "true";
|
|
1148
1464
|
const jmespathQuery = url.searchParams.get("jmespathQuery") ?? "";
|
|
1465
|
+
const codexSearchQuery = url.searchParams.get("codexSearchQuery") ?? "";
|
|
1466
|
+
const codexUsageRange = normalizeCodexUsageRange(
|
|
1467
|
+
url.searchParams.get("codexUsageRange")
|
|
1468
|
+
);
|
|
1149
1469
|
if (blobURL === CODEX_SESSIONS_URL) {
|
|
1150
|
-
await sendJson(
|
|
1470
|
+
await sendJson(
|
|
1471
|
+
req,
|
|
1472
|
+
res,
|
|
1473
|
+
200,
|
|
1474
|
+
await getCodexSessions(offset, limit, codexSearchQuery)
|
|
1475
|
+
);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
if (blobURL === CODEX_USAGE_URL) {
|
|
1479
|
+
await sendJson(
|
|
1480
|
+
req,
|
|
1481
|
+
res,
|
|
1482
|
+
200,
|
|
1483
|
+
await getCodexUsage(offset, limit, codexUsageRange, noCache)
|
|
1484
|
+
);
|
|
1151
1485
|
return;
|
|
1152
1486
|
}
|
|
1153
1487
|
if (blobURL.startsWith(CODEX_SESSION_URL_PREFIX)) {
|
|
@@ -1262,6 +1596,7 @@ const startServer = (options = {}) => {
|
|
|
1262
1596
|
console.log(
|
|
1263
1597
|
`Local Codex sessions: http://${host}:${port}/?path=codex%3Asessions`
|
|
1264
1598
|
);
|
|
1599
|
+
console.log(`Codex usage: http://${host}:${port}/?path=codex%3Ausage`);
|
|
1265
1600
|
});
|
|
1266
1601
|
return server;
|
|
1267
1602
|
};
|