@wukazis/euphony 0.1.45 → 0.1.47
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 +28 -2
- package/dist/assets/local-data-worker-DTkq_bcN.js +2 -0
- package/dist/assets/{main-CmldcHcT.js → main-BMLAkPsC.js} +676 -498
- package/dist/index.html +1 -1
- package/lib/components/app/app.d.ts +24 -4
- package/lib/components/codex/codex.js +5 -5
- package/lib/euphony.js +1 -1
- package/lib/types/common-types.d.ts +15 -2
- package/lib/utils/api-manager.d.ts +6 -2
- package/lib/utils/codex-session.d.ts +4 -0
- package/package.json +1 -1
- package/server-dist/node-main.js +659 -17
- package/dist/assets/local-data-worker-CHLGzNeW.js +0 -2
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,22 @@ 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:";
|
|
439
|
+
const CLAUDE_SESSIONS_URL = "claude:sessions";
|
|
440
|
+
const CLAUDE_SESSION_URL_PREFIX = "claude:session:";
|
|
438
441
|
const CODEX_SESSIONS_DIR = resolve(
|
|
439
442
|
process.env.CODEX_SESSIONS_DIR ?? join(process.env.HOME ?? process.cwd(), ".codex", "sessions")
|
|
440
443
|
);
|
|
444
|
+
const CLAUDE_PROJECTS_DIR = resolve(
|
|
445
|
+
process.env.CLAUDE_PROJECTS_DIR ?? join(process.env.HOME ?? process.cwd(), ".claude", "projects")
|
|
446
|
+
);
|
|
441
447
|
const MAX_PUBLIC_JSON_BYTES = 25 * 1024 * 1024;
|
|
442
448
|
const MAX_CODEX_SESSION_BYTES = 25 * 1024 * 1024;
|
|
449
|
+
const MAX_CODEX_SEARCH_PREVIEW_BYTES = 256 * 1024;
|
|
450
|
+
const CODEX_USAGE_TAIL_CHUNK_BYTES = 64 * 1024;
|
|
451
|
+
const MAX_CODEX_USAGE_TAIL_BYTES = 2 * 1024 * 1024;
|
|
452
|
+
const CODEX_USAGE_CACHE_TTL_MS = 60 * 1e3;
|
|
443
453
|
const MAX_JSON_BODY_BYTES = 2 * 1024 * 1024;
|
|
444
454
|
const TRANSLATION_CACHE_TTL_MS = 5 * 60 * 60 * 1e3;
|
|
445
455
|
const TRANSLATION_MAX_CONCURRENCY = 1024;
|
|
@@ -453,6 +463,8 @@ class HttpError extends Error {
|
|
|
453
463
|
}
|
|
454
464
|
const translationCache = /* @__PURE__ */ new Map();
|
|
455
465
|
const inflightTranslations = /* @__PURE__ */ new Map();
|
|
466
|
+
const codexUsageCache = /* @__PURE__ */ new Map();
|
|
467
|
+
const inflightCodexUsage = /* @__PURE__ */ new Map();
|
|
456
468
|
let activeTranslations = 0;
|
|
457
469
|
const translationWaiters = [];
|
|
458
470
|
const normalizeLimit = (value, fallback) => {
|
|
@@ -463,6 +475,24 @@ const normalizeOffset = (value) => {
|
|
|
463
475
|
const parsed = Number.parseInt(value ?? "", 10);
|
|
464
476
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
465
477
|
};
|
|
478
|
+
const normalizeCodexUsageRange = (value) => {
|
|
479
|
+
if (value === "7d" || value === "30d" || value === "1y") {
|
|
480
|
+
return value;
|
|
481
|
+
}
|
|
482
|
+
return "30d";
|
|
483
|
+
};
|
|
484
|
+
const formatNumberPart = (value, length = 2) => value.toString().padStart(length, "0");
|
|
485
|
+
const formatLocalDateBucket = (timestamp) => `${timestamp.getFullYear()}-${formatNumberPart(timestamp.getMonth() + 1)}-${formatNumberPart(timestamp.getDate())}`;
|
|
486
|
+
const codexUsageRangeStart = (range) => {
|
|
487
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
488
|
+
const start = /* @__PURE__ */ new Date();
|
|
489
|
+
const daysBack = range === "7d" ? 6 : range === "30d" ? 29 : 364;
|
|
490
|
+
start.setHours(0, 0, 0, 0);
|
|
491
|
+
return new Date(start.getTime() - daysBack * dayMs);
|
|
492
|
+
};
|
|
493
|
+
const codexUsageBucketKey = (timestamp, range) => {
|
|
494
|
+
return formatLocalDateBucket(timestamp);
|
|
495
|
+
};
|
|
466
496
|
const isInsideDirectory = (root, candidate) => {
|
|
467
497
|
const relative = resolve(candidate).slice(resolve(root).length);
|
|
468
498
|
return candidate === root || relative.startsWith(sep) && !relative.slice(1).startsWith(`..${sep}`);
|
|
@@ -486,6 +516,21 @@ const parseJsonl = (text) => {
|
|
|
486
516
|
}
|
|
487
517
|
return events;
|
|
488
518
|
};
|
|
519
|
+
const parseJsonlTolerant = (text) => {
|
|
520
|
+
const events = [];
|
|
521
|
+
for (const line of text.split(/\r?\n/)) {
|
|
522
|
+
const stripped = line.trim();
|
|
523
|
+
if (stripped.length === 0) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
events.push(JSON.parse(stripped));
|
|
528
|
+
} catch {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return events;
|
|
533
|
+
};
|
|
489
534
|
const parseJsonOrJsonl = (text) => {
|
|
490
535
|
const stripped = text.trim();
|
|
491
536
|
if (stripped.length === 0) {
|
|
@@ -498,13 +543,18 @@ const parseJsonOrJsonl = (text) => {
|
|
|
498
543
|
return parseJsonl(text);
|
|
499
544
|
}
|
|
500
545
|
};
|
|
501
|
-
const
|
|
546
|
+
const loadJsonlText = async (path) => {
|
|
502
547
|
const fileStat = await stat(path);
|
|
503
548
|
if (fileStat.size > MAX_CODEX_SESSION_BYTES) {
|
|
504
549
|
throw new Error(`Codex session file is too large: ${path}`);
|
|
505
550
|
}
|
|
506
|
-
|
|
507
|
-
|
|
551
|
+
return stripBom(await readFile(path, "utf8"));
|
|
552
|
+
};
|
|
553
|
+
const loadJsonlEvents = async (path) => {
|
|
554
|
+
return parseJsonl(await loadJsonlText(path));
|
|
555
|
+
};
|
|
556
|
+
const loadJsonlEventsTolerant = async (path) => {
|
|
557
|
+
return parseJsonlTolerant(await loadJsonlText(path));
|
|
508
558
|
};
|
|
509
559
|
const resolveCodexSessionPath = (sessionRef) => {
|
|
510
560
|
const relativePath = decodeURIComponent(sessionRef);
|
|
@@ -514,6 +564,14 @@ const resolveCodexSessionPath = (sessionRef) => {
|
|
|
514
564
|
}
|
|
515
565
|
return candidate;
|
|
516
566
|
};
|
|
567
|
+
const resolveClaudeSessionPath = (sessionRef) => {
|
|
568
|
+
const relativePath = decodeURIComponent(sessionRef);
|
|
569
|
+
const candidate = resolve(CLAUDE_PROJECTS_DIR, relativePath);
|
|
570
|
+
if (!isInsideDirectory(CLAUDE_PROJECTS_DIR, candidate) || extname(candidate) !== ".jsonl") {
|
|
571
|
+
throw new HttpError(404, "Claude session not found");
|
|
572
|
+
}
|
|
573
|
+
return candidate;
|
|
574
|
+
};
|
|
517
575
|
const parseTimestamp = (timestamp) => {
|
|
518
576
|
if (typeof timestamp !== "string" || timestamp.length === 0) {
|
|
519
577
|
return null;
|
|
@@ -522,6 +580,122 @@ const parseTimestamp = (timestamp) => {
|
|
|
522
580
|
const parsed = new Date(normalized);
|
|
523
581
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
524
582
|
};
|
|
583
|
+
const numberFromUsageField = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
584
|
+
const emptyCodexTokenUsage = () => ({
|
|
585
|
+
input_tokens: 0,
|
|
586
|
+
cached_input_tokens: 0,
|
|
587
|
+
output_tokens: 0,
|
|
588
|
+
reasoning_output_tokens: 0,
|
|
589
|
+
total_tokens: 0
|
|
590
|
+
});
|
|
591
|
+
const addCodexTokenUsage = (target, source) => {
|
|
592
|
+
target.input_tokens += source.input_tokens;
|
|
593
|
+
target.cached_input_tokens += source.cached_input_tokens;
|
|
594
|
+
target.output_tokens += source.output_tokens;
|
|
595
|
+
target.reasoning_output_tokens += source.reasoning_output_tokens;
|
|
596
|
+
target.total_tokens += source.total_tokens;
|
|
597
|
+
};
|
|
598
|
+
const extractCodexTokenUsage = (value) => {
|
|
599
|
+
if (!value || typeof value !== "object") {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
const record = value;
|
|
603
|
+
const usage = {
|
|
604
|
+
input_tokens: numberFromUsageField(record.input_tokens),
|
|
605
|
+
cached_input_tokens: numberFromUsageField(
|
|
606
|
+
record.cached_input_tokens ?? record.cached_tokens
|
|
607
|
+
),
|
|
608
|
+
output_tokens: numberFromUsageField(record.output_tokens),
|
|
609
|
+
reasoning_output_tokens: numberFromUsageField(
|
|
610
|
+
record.reasoning_output_tokens
|
|
611
|
+
),
|
|
612
|
+
total_tokens: numberFromUsageField(record.total_tokens)
|
|
613
|
+
};
|
|
614
|
+
if (usage.total_tokens === 0 && (usage.input_tokens > 0 || usage.output_tokens > 0)) {
|
|
615
|
+
usage.total_tokens = usage.input_tokens + usage.output_tokens;
|
|
616
|
+
}
|
|
617
|
+
return usage.total_tokens > 0 ? usage : null;
|
|
618
|
+
};
|
|
619
|
+
const extractCodexUsageSnapshotFromEvent = (event) => {
|
|
620
|
+
if (!event || typeof event !== "object") {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
const record = event;
|
|
624
|
+
const payload = record.payload;
|
|
625
|
+
if (record.type !== "event_msg" || !payload || typeof payload !== "object") {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
const payloadRecord = payload;
|
|
629
|
+
if (payloadRecord.type !== "token_count") {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
const info = payloadRecord.info;
|
|
633
|
+
if (!info || typeof info !== "object") {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
const usage = extractCodexTokenUsage(
|
|
637
|
+
info.total_token_usage
|
|
638
|
+
);
|
|
639
|
+
if (!usage) {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
usage,
|
|
644
|
+
timestamp: parseTimestamp(record.timestamp)
|
|
645
|
+
};
|
|
646
|
+
};
|
|
647
|
+
const extractCodexUsageSnapshotFromEvents = (events) => {
|
|
648
|
+
let snapshot = null;
|
|
649
|
+
for (const event of events) {
|
|
650
|
+
snapshot = extractCodexUsageSnapshotFromEvent(event) ?? snapshot;
|
|
651
|
+
}
|
|
652
|
+
return snapshot;
|
|
653
|
+
};
|
|
654
|
+
const extractClaudeUsageSnapshotFromEvents = (events) => {
|
|
655
|
+
const usage = emptyCodexTokenUsage();
|
|
656
|
+
const seenUsageIds = /* @__PURE__ */ new Set();
|
|
657
|
+
let timestamp = null;
|
|
658
|
+
for (const event of events) {
|
|
659
|
+
if (!event || typeof event !== "object") {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const record = event;
|
|
663
|
+
if (record.type !== "assistant") {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
const message = record.message;
|
|
667
|
+
if (!message || typeof message !== "object") {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
const messageRecord = message;
|
|
671
|
+
const rawUsage = messageRecord.usage;
|
|
672
|
+
if (!rawUsage || typeof rawUsage !== "object") {
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const usageId = typeof messageRecord.id === "string" ? messageRecord.id : typeof record.requestId === "string" ? record.requestId : typeof record.uuid === "string" ? record.uuid : null;
|
|
676
|
+
if (usageId && seenUsageIds.has(usageId)) {
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (usageId) {
|
|
680
|
+
seenUsageIds.add(usageId);
|
|
681
|
+
}
|
|
682
|
+
const usageRecord = rawUsage;
|
|
683
|
+
const inputTokens = numberFromUsageField(usageRecord.input_tokens);
|
|
684
|
+
const cacheCreationTokens = numberFromUsageField(
|
|
685
|
+
usageRecord.cache_creation_input_tokens
|
|
686
|
+
);
|
|
687
|
+
const cacheReadTokens = numberFromUsageField(
|
|
688
|
+
usageRecord.cache_read_input_tokens
|
|
689
|
+
);
|
|
690
|
+
const outputTokens = numberFromUsageField(usageRecord.output_tokens);
|
|
691
|
+
usage.input_tokens += inputTokens;
|
|
692
|
+
usage.cached_input_tokens += cacheCreationTokens + cacheReadTokens;
|
|
693
|
+
usage.output_tokens += outputTokens;
|
|
694
|
+
usage.total_tokens += inputTokens + cacheCreationTokens + cacheReadTokens + outputTokens;
|
|
695
|
+
timestamp = parseTimestamp(record.timestamp) ?? timestamp;
|
|
696
|
+
}
|
|
697
|
+
return usage.total_tokens > 0 ? { usage, timestamp } : null;
|
|
698
|
+
};
|
|
525
699
|
const extractTextFromContent = (content) => {
|
|
526
700
|
if (typeof content === "string") {
|
|
527
701
|
const stripped = content.trim();
|
|
@@ -557,6 +731,12 @@ const isDisplayableFirstMessage = (text) => {
|
|
|
557
731
|
"<collaboration_mode>",
|
|
558
732
|
"<model_switch>",
|
|
559
733
|
"<environment_context>",
|
|
734
|
+
"<local-command-caveat>",
|
|
735
|
+
"<local-command-stdout>",
|
|
736
|
+
"<local-command-stderr>",
|
|
737
|
+
"<command-name>",
|
|
738
|
+
"<command-message>",
|
|
739
|
+
"<command-args>",
|
|
560
740
|
"# AGENTS.md instructions"
|
|
561
741
|
].some((prefix) => stripped.startsWith(prefix));
|
|
562
742
|
};
|
|
@@ -567,12 +747,215 @@ const cleanSummaryText = (text, maxLength = 240) => {
|
|
|
567
747
|
return cleaned.length <= maxLength ? cleaned : `${cleaned.slice(0, maxLength - 3).trimEnd()}...`;
|
|
568
748
|
};
|
|
569
749
|
const relativeCodexPath = (path) => resolve(path).slice(resolve(CODEX_SESSIONS_DIR).length + 1).split(sep).join("/");
|
|
750
|
+
const relativeClaudePath = (path) => resolve(path).slice(resolve(CLAUDE_PROJECTS_DIR).length + 1).split(sep).join("/");
|
|
751
|
+
const normalizeCodexSearchQuery = (query) => query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
752
|
+
const extractDisplayableUserMessage = (record) => {
|
|
753
|
+
const payload = record.payload;
|
|
754
|
+
if (!payload || typeof payload !== "object") {
|
|
755
|
+
return "";
|
|
756
|
+
}
|
|
757
|
+
const payloadRecord = payload;
|
|
758
|
+
let candidate = "";
|
|
759
|
+
if (record.type === "response_item" && payloadRecord.role === "user") {
|
|
760
|
+
candidate = extractTextFromContent(payloadRecord.content) ?? "";
|
|
761
|
+
} else if (record.type === "event_msg" && payloadRecord.type === "user_message") {
|
|
762
|
+
candidate = extractTextFromContent(payloadRecord) ?? "";
|
|
763
|
+
}
|
|
764
|
+
return candidate.length > 0 && isDisplayableFirstMessage(candidate) ? candidate : "";
|
|
765
|
+
};
|
|
766
|
+
const extractClaudeDisplayableUserMessage = (record) => {
|
|
767
|
+
if (record.type !== "user" || record.isMeta === true) {
|
|
768
|
+
return "";
|
|
769
|
+
}
|
|
770
|
+
const message = record.message;
|
|
771
|
+
if (!message || typeof message !== "object") {
|
|
772
|
+
return "";
|
|
773
|
+
}
|
|
774
|
+
const messageRecord = message;
|
|
775
|
+
if (messageRecord.role !== "user") {
|
|
776
|
+
return "";
|
|
777
|
+
}
|
|
778
|
+
const candidate = extractTextFromContent(messageRecord.content) ?? "";
|
|
779
|
+
return candidate.length > 0 && isDisplayableFirstMessage(candidate) ? candidate : "";
|
|
780
|
+
};
|
|
781
|
+
const readCodexSessionFirstMessage = async (path) => {
|
|
782
|
+
const fileStat = await stat(path);
|
|
783
|
+
const previewByteLength = Math.min(
|
|
784
|
+
fileStat.size,
|
|
785
|
+
MAX_CODEX_SEARCH_PREVIEW_BYTES
|
|
786
|
+
);
|
|
787
|
+
const fileHandle = await open(path, "r");
|
|
788
|
+
let text = "";
|
|
789
|
+
try {
|
|
790
|
+
const buffer = Buffer.alloc(previewByteLength);
|
|
791
|
+
const { bytesRead } = await fileHandle.read(
|
|
792
|
+
buffer,
|
|
793
|
+
0,
|
|
794
|
+
previewByteLength,
|
|
795
|
+
0
|
|
796
|
+
);
|
|
797
|
+
text = stripBom(buffer.subarray(0, bytesRead).toString("utf8"));
|
|
798
|
+
} finally {
|
|
799
|
+
await fileHandle.close();
|
|
800
|
+
}
|
|
801
|
+
const lines = text.split(/\r?\n/);
|
|
802
|
+
if (fileStat.size > previewByteLength) {
|
|
803
|
+
lines.pop();
|
|
804
|
+
}
|
|
805
|
+
for (const line of lines) {
|
|
806
|
+
const stripped = line.trim();
|
|
807
|
+
if (stripped.length === 0) {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
let event;
|
|
811
|
+
try {
|
|
812
|
+
event = JSON.parse(stripped);
|
|
813
|
+
} catch {
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (!event || typeof event !== "object") {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
const candidate = extractDisplayableUserMessage(
|
|
820
|
+
event
|
|
821
|
+
);
|
|
822
|
+
if (candidate.length > 0) {
|
|
823
|
+
return cleanSummaryText(candidate, 1e3);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return "";
|
|
827
|
+
};
|
|
828
|
+
const readClaudeSessionFirstMessage = async (path) => {
|
|
829
|
+
const fileStat = await stat(path);
|
|
830
|
+
const previewByteLength = Math.min(
|
|
831
|
+
fileStat.size,
|
|
832
|
+
MAX_CODEX_SEARCH_PREVIEW_BYTES
|
|
833
|
+
);
|
|
834
|
+
const fileHandle = await open(path, "r");
|
|
835
|
+
let text = "";
|
|
836
|
+
try {
|
|
837
|
+
const buffer = Buffer.alloc(previewByteLength);
|
|
838
|
+
const { bytesRead } = await fileHandle.read(
|
|
839
|
+
buffer,
|
|
840
|
+
0,
|
|
841
|
+
previewByteLength,
|
|
842
|
+
0
|
|
843
|
+
);
|
|
844
|
+
text = stripBom(buffer.subarray(0, bytesRead).toString("utf8"));
|
|
845
|
+
} finally {
|
|
846
|
+
await fileHandle.close();
|
|
847
|
+
}
|
|
848
|
+
const lines = text.split(/\r?\n/);
|
|
849
|
+
if (fileStat.size > previewByteLength) {
|
|
850
|
+
lines.pop();
|
|
851
|
+
}
|
|
852
|
+
for (const line of lines) {
|
|
853
|
+
const stripped = line.trim();
|
|
854
|
+
if (stripped.length === 0) {
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
let event;
|
|
858
|
+
try {
|
|
859
|
+
event = JSON.parse(stripped);
|
|
860
|
+
} catch {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (!event || typeof event !== "object") {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
const candidate = extractClaudeDisplayableUserMessage(
|
|
867
|
+
event
|
|
868
|
+
);
|
|
869
|
+
if (candidate.length > 0) {
|
|
870
|
+
return cleanSummaryText(candidate, 1e3);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return "";
|
|
874
|
+
};
|
|
875
|
+
const readCodexUsageSnapshot = async (path) => {
|
|
876
|
+
const fileStat = await stat(path);
|
|
877
|
+
if (fileStat.size === 0) {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
const fileHandle = await open(path, "r");
|
|
881
|
+
let cursor = fileStat.size;
|
|
882
|
+
let scannedBytes = 0;
|
|
883
|
+
let tailText = "";
|
|
884
|
+
try {
|
|
885
|
+
while (cursor > 0 && scannedBytes < MAX_CODEX_USAGE_TAIL_BYTES) {
|
|
886
|
+
const chunkSize = Math.min(
|
|
887
|
+
CODEX_USAGE_TAIL_CHUNK_BYTES,
|
|
888
|
+
cursor,
|
|
889
|
+
MAX_CODEX_USAGE_TAIL_BYTES - scannedBytes
|
|
890
|
+
);
|
|
891
|
+
cursor -= chunkSize;
|
|
892
|
+
const buffer = Buffer.alloc(chunkSize);
|
|
893
|
+
const { bytesRead } = await fileHandle.read(
|
|
894
|
+
buffer,
|
|
895
|
+
0,
|
|
896
|
+
chunkSize,
|
|
897
|
+
cursor
|
|
898
|
+
);
|
|
899
|
+
if (bytesRead === 0) {
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
scannedBytes += bytesRead;
|
|
903
|
+
tailText = buffer.subarray(0, bytesRead).toString("utf8") + tailText;
|
|
904
|
+
const lines = tailText.split(/\r?\n/);
|
|
905
|
+
const firstCompleteLineIndex = cursor > 0 ? 1 : 0;
|
|
906
|
+
for (let i = lines.length - 1; i >= firstCompleteLineIndex; i--) {
|
|
907
|
+
const stripped = stripBom(lines[i]).trim();
|
|
908
|
+
if (stripped.length === 0 || !stripped.includes('"token_count"')) {
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
let event;
|
|
912
|
+
try {
|
|
913
|
+
event = JSON.parse(stripped);
|
|
914
|
+
} catch {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
const snapshot = extractCodexUsageSnapshotFromEvent(event);
|
|
918
|
+
if (snapshot) {
|
|
919
|
+
return snapshot;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
} finally {
|
|
924
|
+
await fileHandle.close();
|
|
925
|
+
}
|
|
926
|
+
return null;
|
|
927
|
+
};
|
|
928
|
+
const codexSessionMatchesSearch = async (path, searchTerms) => {
|
|
929
|
+
if (searchTerms.length === 0) {
|
|
930
|
+
return true;
|
|
931
|
+
}
|
|
932
|
+
const relativePath = relativeCodexPath(path).toLowerCase();
|
|
933
|
+
if (searchTerms.every((term) => relativePath.includes(term))) {
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
const searchableText = `${relativePath}
|
|
937
|
+
${await readCodexSessionFirstMessage(path)}`.toLowerCase();
|
|
938
|
+
return searchTerms.every((term) => searchableText.includes(term));
|
|
939
|
+
};
|
|
940
|
+
const claudeSessionMatchesSearch = async (path, searchTerms) => {
|
|
941
|
+
if (searchTerms.length === 0) {
|
|
942
|
+
return true;
|
|
943
|
+
}
|
|
944
|
+
const relativePath = relativeClaudePath(path).toLowerCase();
|
|
945
|
+
if (searchTerms.every((term) => relativePath.includes(term))) {
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
const searchableText = `${relativePath}
|
|
949
|
+
${await readClaudeSessionFirstMessage(path)}`.toLowerCase();
|
|
950
|
+
return searchTerms.every((term) => searchableText.includes(term));
|
|
951
|
+
};
|
|
570
952
|
const summarizeCodexSession = async (path) => {
|
|
571
953
|
const events = await loadJsonlEvents(path);
|
|
572
954
|
const relativePath = relativeCodexPath(path);
|
|
573
955
|
const encodedPath = encodeURIComponent(relativePath);
|
|
574
956
|
let sessionTimestamp = null;
|
|
575
957
|
let firstMessage = "";
|
|
958
|
+
const usageSnapshot = extractCodexUsageSnapshotFromEvents(events);
|
|
576
959
|
for (const event of events) {
|
|
577
960
|
if (!event || typeof event !== "object") {
|
|
578
961
|
continue;
|
|
@@ -596,12 +979,7 @@ const summarizeCodexSession = async (path) => {
|
|
|
596
979
|
if (firstMessage.length > 0) {
|
|
597
980
|
continue;
|
|
598
981
|
}
|
|
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
|
-
}
|
|
982
|
+
const candidate = extractDisplayableUserMessage(record);
|
|
605
983
|
if (candidate.length > 0 && isDisplayableFirstMessage(candidate)) {
|
|
606
984
|
firstMessage = cleanSummaryText(candidate);
|
|
607
985
|
}
|
|
@@ -623,7 +1001,59 @@ const summarizeCodexSession = async (path) => {
|
|
|
623
1001
|
first_message: firstMessage,
|
|
624
1002
|
timestamp: sessionTimestamp.toISOString(),
|
|
625
1003
|
age_seconds: ageSeconds,
|
|
626
|
-
event_count: events.length
|
|
1004
|
+
event_count: events.length,
|
|
1005
|
+
token_usage: usageSnapshot?.usage ?? null,
|
|
1006
|
+
usage_timestamp: usageSnapshot?.timestamp?.toISOString() ?? null
|
|
1007
|
+
};
|
|
1008
|
+
};
|
|
1009
|
+
const summarizeClaudeSession = async (path) => {
|
|
1010
|
+
const events = await loadJsonlEventsTolerant(path);
|
|
1011
|
+
const relativePath = relativeClaudePath(path);
|
|
1012
|
+
const encodedPath = encodeURIComponent(relativePath);
|
|
1013
|
+
let sessionTimestamp = null;
|
|
1014
|
+
let firstMessage = "";
|
|
1015
|
+
let aiTitle = "";
|
|
1016
|
+
const usageSnapshot = extractClaudeUsageSnapshotFromEvents(events);
|
|
1017
|
+
for (const event of events) {
|
|
1018
|
+
if (!event || typeof event !== "object") {
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
const record = event;
|
|
1022
|
+
const eventTimestamp = parseTimestamp(record.timestamp) ?? (record.snapshot && typeof record.snapshot === "object" ? parseTimestamp(record.snapshot.timestamp) : null);
|
|
1023
|
+
if (sessionTimestamp === null && eventTimestamp !== null) {
|
|
1024
|
+
sessionTimestamp = eventTimestamp;
|
|
1025
|
+
}
|
|
1026
|
+
if (record.type === "ai-title" && typeof record.aiTitle === "string" && aiTitle.length === 0) {
|
|
1027
|
+
aiTitle = cleanSummaryText(record.aiTitle);
|
|
1028
|
+
}
|
|
1029
|
+
if (firstMessage.length > 0) {
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
const candidate = extractClaudeDisplayableUserMessage(record);
|
|
1033
|
+
if (candidate.length > 0 && isDisplayableFirstMessage(candidate)) {
|
|
1034
|
+
firstMessage = cleanSummaryText(candidate);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (firstMessage.length === 0) {
|
|
1038
|
+
firstMessage = aiTitle || "(no user message)";
|
|
1039
|
+
}
|
|
1040
|
+
if (sessionTimestamp === null) {
|
|
1041
|
+
const fileStat = await stat(path);
|
|
1042
|
+
sessionTimestamp = new Date(fileStat.mtimeMs);
|
|
1043
|
+
}
|
|
1044
|
+
const ageSeconds = Math.max(
|
|
1045
|
+
0,
|
|
1046
|
+
Math.floor((Date.now() - sessionTimestamp.getTime()) / 1e3)
|
|
1047
|
+
);
|
|
1048
|
+
return {
|
|
1049
|
+
path: relativePath,
|
|
1050
|
+
load_url: `${CLAUDE_SESSION_URL_PREFIX}${encodedPath}`,
|
|
1051
|
+
first_message: firstMessage,
|
|
1052
|
+
timestamp: sessionTimestamp.toISOString(),
|
|
1053
|
+
age_seconds: ageSeconds,
|
|
1054
|
+
event_count: events.length,
|
|
1055
|
+
token_usage: usageSnapshot?.usage ?? null,
|
|
1056
|
+
usage_timestamp: usageSnapshot?.timestamp?.toISOString() ?? null
|
|
627
1057
|
};
|
|
628
1058
|
};
|
|
629
1059
|
const findJsonlFiles = async (root) => {
|
|
@@ -658,7 +1088,8 @@ const findJsonlFiles = async (root) => {
|
|
|
658
1088
|
withStats.sort((a, b) => b.mtime - a.mtime || b.path.localeCompare(a.path));
|
|
659
1089
|
return withStats.map((item) => item.path);
|
|
660
1090
|
};
|
|
661
|
-
const getCodexSessions = async (offset, limit) => {
|
|
1091
|
+
const getCodexSessions = async (offset, limit, searchQuery = "") => {
|
|
1092
|
+
const searchTerms = normalizeCodexSearchQuery(searchQuery);
|
|
662
1093
|
try {
|
|
663
1094
|
const rootStat = await stat(CODEX_SESSIONS_DIR);
|
|
664
1095
|
if (!rootStat.isDirectory()) {
|
|
@@ -670,13 +1101,27 @@ const getCodexSessions = async (offset, limit) => {
|
|
|
670
1101
|
offset,
|
|
671
1102
|
limit,
|
|
672
1103
|
total: 0,
|
|
673
|
-
isFiltered:
|
|
1104
|
+
isFiltered: searchTerms.length > 0,
|
|
674
1105
|
matchedCount: 0,
|
|
675
1106
|
resolvedURL: CODEX_SESSIONS_URL
|
|
676
1107
|
};
|
|
677
1108
|
}
|
|
678
1109
|
const files = await findJsonlFiles(CODEX_SESSIONS_DIR);
|
|
679
|
-
const
|
|
1110
|
+
const matchedFiles = [];
|
|
1111
|
+
if (searchTerms.length === 0) {
|
|
1112
|
+
matchedFiles.push(...files);
|
|
1113
|
+
} else {
|
|
1114
|
+
for (const path of files) {
|
|
1115
|
+
try {
|
|
1116
|
+
if (await codexSessionMatchesSearch(path, searchTerms)) {
|
|
1117
|
+
matchedFiles.push(path);
|
|
1118
|
+
}
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
console.error(`Failed to search Codex session file: ${path}`, error);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const pageFiles = matchedFiles.slice(offset, offset + limit);
|
|
680
1125
|
const summaries = [];
|
|
681
1126
|
for (const path of pageFiles) {
|
|
682
1127
|
try {
|
|
@@ -690,11 +1135,141 @@ const getCodexSessions = async (offset, limit) => {
|
|
|
690
1135
|
offset,
|
|
691
1136
|
limit,
|
|
692
1137
|
total: files.length,
|
|
693
|
-
isFiltered:
|
|
694
|
-
matchedCount:
|
|
1138
|
+
isFiltered: searchTerms.length > 0,
|
|
1139
|
+
matchedCount: matchedFiles.length,
|
|
695
1140
|
resolvedURL: CODEX_SESSIONS_URL
|
|
696
1141
|
};
|
|
697
1142
|
};
|
|
1143
|
+
const getClaudeSessions = async (offset, limit, searchQuery = "") => {
|
|
1144
|
+
const searchTerms = normalizeCodexSearchQuery(searchQuery);
|
|
1145
|
+
try {
|
|
1146
|
+
const rootStat = await stat(CLAUDE_PROJECTS_DIR);
|
|
1147
|
+
if (!rootStat.isDirectory()) {
|
|
1148
|
+
throw new Error("Not a directory");
|
|
1149
|
+
}
|
|
1150
|
+
} catch {
|
|
1151
|
+
return {
|
|
1152
|
+
data: [],
|
|
1153
|
+
offset,
|
|
1154
|
+
limit,
|
|
1155
|
+
total: 0,
|
|
1156
|
+
isFiltered: searchTerms.length > 0,
|
|
1157
|
+
matchedCount: 0,
|
|
1158
|
+
resolvedURL: CLAUDE_SESSIONS_URL
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
const files = await findJsonlFiles(CLAUDE_PROJECTS_DIR);
|
|
1162
|
+
const matchedFiles = [];
|
|
1163
|
+
if (searchTerms.length === 0) {
|
|
1164
|
+
matchedFiles.push(...files);
|
|
1165
|
+
} else {
|
|
1166
|
+
for (const path of files) {
|
|
1167
|
+
try {
|
|
1168
|
+
if (await claudeSessionMatchesSearch(path, searchTerms)) {
|
|
1169
|
+
matchedFiles.push(path);
|
|
1170
|
+
}
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
console.error(`Failed to search Claude session file: ${path}`, error);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
const pageFiles = matchedFiles.slice(offset, offset + limit);
|
|
1177
|
+
const summaries = [];
|
|
1178
|
+
for (const path of pageFiles) {
|
|
1179
|
+
try {
|
|
1180
|
+
summaries.push(await summarizeClaudeSession(path));
|
|
1181
|
+
} catch (error) {
|
|
1182
|
+
console.error(`Failed to load Claude session file: ${path}`, error);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return {
|
|
1186
|
+
data: summaries,
|
|
1187
|
+
offset,
|
|
1188
|
+
limit,
|
|
1189
|
+
total: files.length,
|
|
1190
|
+
isFiltered: searchTerms.length > 0,
|
|
1191
|
+
matchedCount: matchedFiles.length,
|
|
1192
|
+
resolvedURL: CLAUDE_SESSIONS_URL
|
|
1193
|
+
};
|
|
1194
|
+
};
|
|
1195
|
+
const buildCodexUsageDays = async (range) => {
|
|
1196
|
+
try {
|
|
1197
|
+
const rootStat = await stat(CODEX_SESSIONS_DIR);
|
|
1198
|
+
if (!rootStat.isDirectory()) {
|
|
1199
|
+
throw new Error("Not a directory");
|
|
1200
|
+
}
|
|
1201
|
+
} catch {
|
|
1202
|
+
return [];
|
|
1203
|
+
}
|
|
1204
|
+
const files = await findJsonlFiles(CODEX_SESSIONS_DIR);
|
|
1205
|
+
const usageByDay = /* @__PURE__ */ new Map();
|
|
1206
|
+
const rangeStart = codexUsageRangeStart(range);
|
|
1207
|
+
const now = /* @__PURE__ */ new Date();
|
|
1208
|
+
for (const path of files) {
|
|
1209
|
+
try {
|
|
1210
|
+
const snapshot = await readCodexUsageSnapshot(path);
|
|
1211
|
+
if (!snapshot) {
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
const fileStat = await stat(path);
|
|
1215
|
+
const timestamp = snapshot.timestamp ?? new Date(fileStat.mtimeMs);
|
|
1216
|
+
if (timestamp < rangeStart || timestamp > now) {
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
const date = codexUsageBucketKey(timestamp, range);
|
|
1220
|
+
const dayUsage = usageByDay.get(date) ?? {
|
|
1221
|
+
date,
|
|
1222
|
+
session_count: 0,
|
|
1223
|
+
...emptyCodexTokenUsage()
|
|
1224
|
+
};
|
|
1225
|
+
dayUsage.session_count += 1;
|
|
1226
|
+
addCodexTokenUsage(dayUsage, snapshot.usage);
|
|
1227
|
+
usageByDay.set(date, dayUsage);
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
console.error(`Failed to read Codex usage from file: ${path}`, error);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
const days = [...usageByDay.values()].sort(
|
|
1233
|
+
(a, b) => b.date.localeCompare(a.date)
|
|
1234
|
+
);
|
|
1235
|
+
return days;
|
|
1236
|
+
};
|
|
1237
|
+
const getCodexUsageDays = async (range, noCache) => {
|
|
1238
|
+
if (noCache) {
|
|
1239
|
+
codexUsageCache.delete(range);
|
|
1240
|
+
}
|
|
1241
|
+
const cached = codexUsageCache.get(range);
|
|
1242
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
1243
|
+
return cached.days;
|
|
1244
|
+
}
|
|
1245
|
+
const inflight = inflightCodexUsage.get(range);
|
|
1246
|
+
if (inflight) {
|
|
1247
|
+
return inflight;
|
|
1248
|
+
}
|
|
1249
|
+
const promise = buildCodexUsageDays(range).then((days) => {
|
|
1250
|
+
codexUsageCache.set(range, {
|
|
1251
|
+
expiresAt: Date.now() + CODEX_USAGE_CACHE_TTL_MS,
|
|
1252
|
+
days
|
|
1253
|
+
});
|
|
1254
|
+
return days;
|
|
1255
|
+
}).finally(() => {
|
|
1256
|
+
inflightCodexUsage.delete(range);
|
|
1257
|
+
});
|
|
1258
|
+
inflightCodexUsage.set(range, promise);
|
|
1259
|
+
return promise;
|
|
1260
|
+
};
|
|
1261
|
+
const getCodexUsage = async (offset, limit, range, noCache) => {
|
|
1262
|
+
const days = await getCodexUsageDays(range, noCache);
|
|
1263
|
+
return {
|
|
1264
|
+
data: days.slice(offset, offset + limit),
|
|
1265
|
+
offset,
|
|
1266
|
+
limit,
|
|
1267
|
+
total: days.length,
|
|
1268
|
+
isFiltered: false,
|
|
1269
|
+
matchedCount: days.length,
|
|
1270
|
+
resolvedURL: CODEX_USAGE_URL
|
|
1271
|
+
};
|
|
1272
|
+
};
|
|
698
1273
|
const getCodexSession = async (sessionRef, offset, limit) => {
|
|
699
1274
|
const path = resolveCodexSessionPath(sessionRef);
|
|
700
1275
|
try {
|
|
@@ -718,6 +1293,29 @@ const getCodexSession = async (sessionRef, offset, limit) => {
|
|
|
718
1293
|
resolvedURL
|
|
719
1294
|
};
|
|
720
1295
|
};
|
|
1296
|
+
const getClaudeSession = async (sessionRef, offset, limit) => {
|
|
1297
|
+
const path = resolveClaudeSessionPath(sessionRef);
|
|
1298
|
+
try {
|
|
1299
|
+
const fileStat = await stat(path);
|
|
1300
|
+
if (!fileStat.isFile()) {
|
|
1301
|
+
throw new Error("Not a file");
|
|
1302
|
+
}
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
throw new HttpError(404, "Claude session not found");
|
|
1305
|
+
}
|
|
1306
|
+
const events = await loadJsonlEventsTolerant(path);
|
|
1307
|
+
const relativePath = relativeClaudePath(path);
|
|
1308
|
+
const resolvedURL = `${CLAUDE_SESSION_URL_PREFIX}${encodeURIComponent(relativePath)}`;
|
|
1309
|
+
return {
|
|
1310
|
+
data: events.slice(offset, offset + limit),
|
|
1311
|
+
offset,
|
|
1312
|
+
limit,
|
|
1313
|
+
total: events.length,
|
|
1314
|
+
isFiltered: false,
|
|
1315
|
+
matchedCount: events.length,
|
|
1316
|
+
resolvedURL
|
|
1317
|
+
};
|
|
1318
|
+
};
|
|
721
1319
|
const readRemoteResponseBody = async (response) => {
|
|
722
1320
|
const chunks = [];
|
|
723
1321
|
let totalBytes = 0;
|
|
@@ -1146,8 +1744,35 @@ const handleBlobJsonl = async (req, res, url) => {
|
|
|
1146
1744
|
const limit = normalizeLimit(url.searchParams.get("limit"), 10);
|
|
1147
1745
|
const noCache = url.searchParams.get("noCache") === "true";
|
|
1148
1746
|
const jmespathQuery = url.searchParams.get("jmespathQuery") ?? "";
|
|
1747
|
+
const codexSearchQuery = url.searchParams.get("codexSearchQuery") ?? "";
|
|
1748
|
+
const codexUsageRange = normalizeCodexUsageRange(
|
|
1749
|
+
url.searchParams.get("codexUsageRange")
|
|
1750
|
+
);
|
|
1149
1751
|
if (blobURL === CODEX_SESSIONS_URL) {
|
|
1150
|
-
await sendJson(
|
|
1752
|
+
await sendJson(
|
|
1753
|
+
req,
|
|
1754
|
+
res,
|
|
1755
|
+
200,
|
|
1756
|
+
await getCodexSessions(offset, limit, codexSearchQuery)
|
|
1757
|
+
);
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
if (blobURL === CLAUDE_SESSIONS_URL) {
|
|
1761
|
+
await sendJson(
|
|
1762
|
+
req,
|
|
1763
|
+
res,
|
|
1764
|
+
200,
|
|
1765
|
+
await getClaudeSessions(offset, limit, codexSearchQuery)
|
|
1766
|
+
);
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
if (blobURL === CODEX_USAGE_URL) {
|
|
1770
|
+
await sendJson(
|
|
1771
|
+
req,
|
|
1772
|
+
res,
|
|
1773
|
+
200,
|
|
1774
|
+
await getCodexUsage(offset, limit, codexUsageRange, noCache)
|
|
1775
|
+
);
|
|
1151
1776
|
return;
|
|
1152
1777
|
}
|
|
1153
1778
|
if (blobURL.startsWith(CODEX_SESSION_URL_PREFIX)) {
|
|
@@ -1163,6 +1788,19 @@ const handleBlobJsonl = async (req, res, url) => {
|
|
|
1163
1788
|
);
|
|
1164
1789
|
return;
|
|
1165
1790
|
}
|
|
1791
|
+
if (blobURL.startsWith(CLAUDE_SESSION_URL_PREFIX)) {
|
|
1792
|
+
await sendJson(
|
|
1793
|
+
req,
|
|
1794
|
+
res,
|
|
1795
|
+
200,
|
|
1796
|
+
await getClaudeSession(
|
|
1797
|
+
blobURL.slice(CLAUDE_SESSION_URL_PREFIX.length),
|
|
1798
|
+
offset,
|
|
1799
|
+
limit
|
|
1800
|
+
)
|
|
1801
|
+
);
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1166
1804
|
await sendJson(
|
|
1167
1805
|
req,
|
|
1168
1806
|
res,
|
|
@@ -1262,6 +1900,10 @@ const startServer = (options = {}) => {
|
|
|
1262
1900
|
console.log(
|
|
1263
1901
|
`Local Codex sessions: http://${host}:${port}/?path=codex%3Asessions`
|
|
1264
1902
|
);
|
|
1903
|
+
console.log(
|
|
1904
|
+
`Local Claude sessions: http://${host}:${port}/?path=claude%3Asessions`
|
|
1905
|
+
);
|
|
1906
|
+
console.log(`Codex usage: http://${host}:${port}/?path=codex%3Ausage`);
|
|
1265
1907
|
});
|
|
1266
1908
|
return server;
|
|
1267
1909
|
};
|