@narumitw/pi-usage 0.65.0 → 0.65.1
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 +11 -1
- package/dist/index.ts +67 -39
- package/dist/index.ts.map +2 -2
- package/docs/providers.md +7 -7
- package/package.json +1 -1
- package/src/format.ts +96 -34
- package/src/usage.ts +14 -12
package/README.md
CHANGED
|
@@ -147,7 +147,7 @@ This preference changes only Codex statusline formatting, not provider data or t
|
|
|
147
147
|
### Codex statusline reset countdown
|
|
148
148
|
|
|
149
149
|
The `codexStatusResetCountdown` preference defaults to `true`. It replaces the window labels with the time remaining until each returned limit resets.
|
|
150
|
-
Turn **Codex reset countdown** Off in the TUI Settings screen, or set it to `false` in `pi-usage.json` and run `/reload`, to
|
|
150
|
+
Turn **Codex reset countdown** Off in the TUI Settings screen, or set it to `false` in `pi-usage.json` and run `/reload`, to show static labels such as `codex 5h:59% · wk:61%`:
|
|
151
151
|
|
|
152
152
|
```json
|
|
153
153
|
{
|
|
@@ -197,6 +197,16 @@ After the active runtime credential changes, the next command, turn, or schedule
|
|
|
197
197
|
## 📊 Statusline behavior
|
|
198
198
|
|
|
199
199
|
The `usage` status item is active only for selected providers that publish statusline usage.
|
|
200
|
+
Codex, Command Code, Kimi, MiniMax Token Plan, Z.AI, and OpenCode Go separate quota windows with ` · ` and place static labels before values, for example `cmd 5h:100% · wk:97% · mo:98%`.
|
|
201
|
+
Countdown segments retain their spacing without window labels, for example `codex 89% ↻ 2h30m · 97% ↻ 5d17h`; a missing reset time falls back to a static label, such as `codex 89% ↻ 2h30m · wk:97%`.
|
|
202
|
+
Single windows have no separator. These presentation changes do not alter the reported values; consumers parsing the old status text must adapt to the new format. ChatGPT companion formatting is unchanged.
|
|
203
|
+
|
|
204
|
+
Kimi, MiniMax Token Plan Global/China, Z.AI Global/China, Command Code, OpenCode Go, and finite GitHub Copilot quotas show countdowns automatically when a displayed quota has a valid future reset time.
|
|
205
|
+
For example, `kimi 77% ↻ 2h52m · mo:87%` mixes a countdown with a static fallback; Copilot preserves its counts, percentage, and any overage, such as `copilot 245/300 82% ↻ 6d`.
|
|
206
|
+
These providers fall back to their existing static output when reset times are missing, invalid, or expired; unlimited quotas and balance/spend reports do not show countdowns.
|
|
207
|
+
Their countdowns are not controlled by the Codex-only settings. Codex and ChatGPT companion retain their existing reset behavior, including `↻ 0m` for expired returned resets.
|
|
208
|
+
Countdowns repaint locally every minute while a displayed eligible reset remains in the future, without additional provider requests; hidden windows and unselected model groups do not activate this repaint for the newly supported providers.
|
|
209
|
+
|
|
200
210
|
It refreshes every five minutes while the session remains on such a provider and is cleared when the model changes to an unsupported or menu-only provider.
|
|
201
211
|
DeepSeek publishes each returned currency as a separate exact balance segment and reports when the API is unavailable.
|
|
202
212
|
Fireworks publishes exact per-currency rated spend totals and reports when no rated usage exists.
|
package/dist/index.ts
CHANGED
|
@@ -421,6 +421,14 @@ function formatUsageReport(report, displayState) {
|
|
|
421
421
|
return lines.join("\n").trimEnd();
|
|
422
422
|
}
|
|
423
423
|
function formatUsageStatusline(report, model, now = Date.now(), showCodexResetCountdown = true, codexStatusPercentage = "remaining") {
|
|
424
|
+
return formatUsageStatuslineWithCountdown(report, model, now, showCodexResetCountdown, codexStatusPercentage).value;
|
|
425
|
+
}
|
|
426
|
+
function formatUsageStatuslineWithCountdown(report, model, now = Date.now(), showCodexResetCountdown = true, codexStatusPercentage = "remaining") {
|
|
427
|
+
const countdown = { now, active: false };
|
|
428
|
+
const value = formatStatusline(report, model, now, showCodexResetCountdown, codexStatusPercentage, countdown);
|
|
429
|
+
return { value, hasCountdown: countdown.active };
|
|
430
|
+
}
|
|
431
|
+
function formatStatusline(report, model, now, showCodexResetCountdown, codexStatusPercentage, countdown) {
|
|
424
432
|
if (report.providerId === "openai" && report.source === "openai-chatgpt-auth") return "chatgpt usage: web only";
|
|
425
433
|
if (report.source === "openai-chatgpt-companion") {
|
|
426
434
|
const windows = report.buckets.filter((bucket) => bucket.groupId === "chatgpt-plan").map((bucket) => {
|
|
@@ -437,24 +445,24 @@ function formatUsageStatusline(report, model, now = Date.now(), showCodexResetCo
|
|
|
437
445
|
if (report.providerId === "deepseek") return formatDeepSeekStatusline(report);
|
|
438
446
|
if (report.providerId === "fireworks") return formatFireworksStatusline(report);
|
|
439
447
|
if (report.providerId === "vercel-ai-gateway") return formatVercelAIGatewayStatusline(report);
|
|
440
|
-
if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report);
|
|
448
|
+
if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report, countdown);
|
|
441
449
|
if (report.providerId === "openrouter") {
|
|
442
450
|
const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
|
|
443
451
|
if (limit?.remaining !== void 0) return `openrouter ${formatUsd(limit.remaining)} left`;
|
|
444
452
|
const total = report.metrics.find((metric) => metric.id === "usage-total");
|
|
445
453
|
if (typeof total?.value === "number") return `openrouter ${formatUsd(total.value)} used`;
|
|
446
454
|
}
|
|
447
|
-
if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
|
|
448
|
-
if (report.providerId === "command-code") return formatCommandCodeStatusline(report);
|
|
449
|
-
if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report);
|
|
455
|
+
if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report, countdown);
|
|
456
|
+
if (report.providerId === "command-code") return formatCommandCodeStatusline(report, countdown);
|
|
457
|
+
if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report, countdown);
|
|
450
458
|
if (report.providerId === "moonshotai" || report.providerId === "moonshotai-cn") {
|
|
451
459
|
return formatMoonshotStatusline(report);
|
|
452
460
|
}
|
|
453
461
|
if (report.providerId === "minimax" || report.providerId === "minimax-cn") {
|
|
454
|
-
return formatMiniMaxStatusline(report, model);
|
|
462
|
+
return formatMiniMaxStatusline(report, countdown, model);
|
|
455
463
|
}
|
|
456
464
|
if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
|
|
457
|
-
return formatZaiStatusline(report);
|
|
465
|
+
return formatZaiStatusline(report, countdown);
|
|
458
466
|
}
|
|
459
467
|
return void 0;
|
|
460
468
|
}
|
|
@@ -572,7 +580,7 @@ function formatGitHubCopilotReport(lines, report) {
|
|
|
572
580
|
lines.push(`${"Additional usage:".padEnd(VALUE_COLUMN)}${overage.value} ${quota.label}`);
|
|
573
581
|
}
|
|
574
582
|
}
|
|
575
|
-
function formatGitHubCopilotStatusline(report) {
|
|
583
|
+
function formatGitHubCopilotStatusline(report, countdown) {
|
|
576
584
|
const quota = findGitHubCopilotQuota(report);
|
|
577
585
|
const kind = compactGitHubCopilotQuotaKind(quota);
|
|
578
586
|
if (!quota || quota.limit === void 0 || quota.remaining === void 0) {
|
|
@@ -580,7 +588,8 @@ function formatGitHubCopilotStatusline(report) {
|
|
|
580
588
|
}
|
|
581
589
|
const overage = report.metrics.find((metric) => metric.id === "overage-used");
|
|
582
590
|
const overageSuffix = typeof overage?.value === "number" && overage.value > 0 ? ` +${overage.value} over` : "";
|
|
583
|
-
|
|
591
|
+
const reset = formatQuotaCountdown(quota, countdown);
|
|
592
|
+
return `copilot ${kind === "premium" ? "" : `${kind} `}${quota.remaining}/${quota.limit} ${percentRemaining(quota)}%${overageSuffix}${reset ? ` \u21BB ${reset}` : ""}`;
|
|
584
593
|
}
|
|
585
594
|
function findGitHubCopilotQuota(report) {
|
|
586
595
|
return report.buckets.find((bucket) => ["ai-credits", "premium-requests", "chat-requests"].includes(bucket.id));
|
|
@@ -616,14 +625,14 @@ function formatOpenCodeZenReport(lines, report) {
|
|
|
616
625
|
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${used}% used${reset}`);
|
|
617
626
|
}
|
|
618
627
|
}
|
|
619
|
-
function formatOpenCodeZenStatusline(report) {
|
|
620
|
-
const parts = [
|
|
628
|
+
function formatOpenCodeZenStatusline(report, countdown) {
|
|
629
|
+
const parts = [];
|
|
621
630
|
for (const bucket of report.buckets) {
|
|
622
631
|
if (bucket.used === void 0) continue;
|
|
623
632
|
const compact = bucket.id === "rolling" ? "r" : bucket.id === "weekly" ? "w" : "m";
|
|
624
|
-
parts.push(`${clampPercent(bucket.used).toFixed(0)}
|
|
633
|
+
parts.push(formatQuotaWindow(bucket, `${clampPercent(bucket.used).toFixed(0)}%`, compact, countdown));
|
|
625
634
|
}
|
|
626
|
-
return parts.length >
|
|
635
|
+
return parts.length > 0 ? `zen ${parts.join(" \xB7 ")}` : void 0;
|
|
627
636
|
}
|
|
628
637
|
function formatCommandCodeReport(lines, report) {
|
|
629
638
|
for (const bucket of report.buckets) {
|
|
@@ -640,8 +649,8 @@ function formatCommandCodeReport(lines, report) {
|
|
|
640
649
|
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`);
|
|
641
650
|
}
|
|
642
651
|
}
|
|
643
|
-
function formatCommandCodeStatusline(report) {
|
|
644
|
-
const parts = [
|
|
652
|
+
function formatCommandCodeStatusline(report, countdown) {
|
|
653
|
+
const parts = [];
|
|
645
654
|
for (const [id, label] of [
|
|
646
655
|
["five-hour", "5h"],
|
|
647
656
|
["weekly", "wk"],
|
|
@@ -649,9 +658,9 @@ function formatCommandCodeStatusline(report) {
|
|
|
649
658
|
]) {
|
|
650
659
|
const bucket = report.buckets.find((candidate) => candidate.id === id);
|
|
651
660
|
if (bucket?.unit !== "usd" || bucket.limit === void 0 || bucket.remaining === void 0) continue;
|
|
652
|
-
parts.push(`${percentRemaining(bucket)}
|
|
661
|
+
parts.push(formatQuotaWindow(bucket, `${percentRemaining(bucket)}%`, label, countdown));
|
|
653
662
|
}
|
|
654
|
-
return parts.length >
|
|
663
|
+
return parts.length > 0 ? `cmd ${parts.join(" \xB7 ")}` : void 0;
|
|
655
664
|
}
|
|
656
665
|
function formatKimiCodingReport(lines, report) {
|
|
657
666
|
for (const bucket of report.buckets) {
|
|
@@ -687,7 +696,7 @@ function formatKimiCodingReport(lines, report) {
|
|
|
687
696
|
lines.push(`${"Monthly limit:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(monthlyLimit)}`);
|
|
688
697
|
}
|
|
689
698
|
}
|
|
690
|
-
function formatKimiCodingStatusline(report) {
|
|
699
|
+
function formatKimiCodingStatusline(report, countdown) {
|
|
691
700
|
const fiveHour = report.buckets.find((bucket) => bucket.id === "five-hour");
|
|
692
701
|
const weekly = report.buckets.find((bucket) => bucket.id === "weekly");
|
|
693
702
|
const monthly = report.buckets.find((bucket) => bucket.id === "monthly");
|
|
@@ -695,15 +704,15 @@ function formatKimiCodingStatusline(report) {
|
|
|
695
704
|
const selected = [subWindow, weekly, monthly].filter(
|
|
696
705
|
(bucket, index, buckets) => bucket !== void 0 && buckets.indexOf(bucket) === index
|
|
697
706
|
);
|
|
698
|
-
const parts = [
|
|
707
|
+
const parts = [];
|
|
699
708
|
for (const bucket of selected) {
|
|
700
709
|
if (bucket.remaining === void 0 || bucket.unit !== "percent" && !bucket.limit) continue;
|
|
701
710
|
const fallback = bucket.id === "weekly" ? "weekly" : "5h";
|
|
702
711
|
const window = bucket.id === "monthly" ? "mo" : formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
703
712
|
const remaining = bucket.unit === "percent" ? Math.round(clampPercent(bucket.remaining)) : percentRemaining(bucket);
|
|
704
|
-
parts.push(`${remaining}
|
|
713
|
+
parts.push(formatQuotaWindow(bucket, `${remaining}%`, window, countdown));
|
|
705
714
|
}
|
|
706
|
-
return parts.length >
|
|
715
|
+
return parts.length > 0 ? `kimi ${parts.join(" \xB7 ")}` : void 0;
|
|
707
716
|
}
|
|
708
717
|
function formatMoonshotReport(lines, report) {
|
|
709
718
|
for (const metric of report.metrics) {
|
|
@@ -731,7 +740,7 @@ function formatMiniMaxReport(lines, report) {
|
|
|
731
740
|
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${value}`);
|
|
732
741
|
}
|
|
733
742
|
}
|
|
734
|
-
function formatMiniMaxStatusline(report, model) {
|
|
743
|
+
function formatMiniMaxStatusline(report, countdown, model) {
|
|
735
744
|
const prefix = report.providerId === "minimax-cn" ? "minimax cn" : "minimax";
|
|
736
745
|
if (report.source === "minimax-account-balance") {
|
|
737
746
|
const available = report.metrics.find((metric) => metric.id === "available-balance");
|
|
@@ -740,22 +749,22 @@ function formatMiniMaxStatusline(report, model) {
|
|
|
740
749
|
const selectedGroup = selectMiniMaxGroup(report, model);
|
|
741
750
|
if (!selectedGroup) return void 0;
|
|
742
751
|
const selected = report.buckets.filter((bucket) => bucket.groupId === selectedGroup);
|
|
743
|
-
const parts = [
|
|
752
|
+
const parts = [];
|
|
744
753
|
for (const bucket of selected) {
|
|
745
754
|
const fallback = bucket.id.endsWith(":weekly") ? "weekly" : "5h";
|
|
746
755
|
const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
747
756
|
if (bucket.period === "unlimited") {
|
|
748
|
-
parts.push(
|
|
757
|
+
parts.push(`${window}:unlimited`);
|
|
749
758
|
continue;
|
|
750
759
|
}
|
|
751
760
|
if (bucket.unit === "percent" && bucket.remaining !== void 0) {
|
|
752
|
-
parts.push(`${bucket.remaining}
|
|
761
|
+
parts.push(formatQuotaWindow(bucket, `${bucket.remaining}%`, window, countdown));
|
|
753
762
|
continue;
|
|
754
763
|
}
|
|
755
764
|
if (bucket.limit === void 0 || bucket.remaining === void 0) continue;
|
|
756
|
-
parts.push(`${percentRemaining(bucket)}
|
|
765
|
+
parts.push(formatQuotaWindow(bucket, `${percentRemaining(bucket)}%`, window, countdown));
|
|
757
766
|
}
|
|
758
|
-
return parts.length >
|
|
767
|
+
return parts.length > 0 ? `${prefix} ${parts.join(" \xB7 ")}` : void 0;
|
|
759
768
|
}
|
|
760
769
|
function selectMiniMaxGroup(report, model) {
|
|
761
770
|
const groups = [
|
|
@@ -797,18 +806,25 @@ function wildcardKeyMatches(pattern, value) {
|
|
|
797
806
|
const last = segments.at(-1);
|
|
798
807
|
return pattern.endsWith("*") || last !== void 0 && value.endsWith(last);
|
|
799
808
|
}
|
|
800
|
-
function formatZaiStatusline(report) {
|
|
809
|
+
function formatZaiStatusline(report, countdown) {
|
|
801
810
|
const selected = [
|
|
802
811
|
report.buckets.find((bucket) => bucket.id === "five-hour"),
|
|
803
812
|
report.buckets.find((bucket) => bucket.id === "weekly")
|
|
804
813
|
];
|
|
805
|
-
const parts = [
|
|
814
|
+
const parts = [];
|
|
806
815
|
for (const bucket of selected) {
|
|
807
816
|
if (!bucket?.limit || bucket.remaining === void 0) continue;
|
|
808
817
|
const fallback = bucket.id === "weekly" ? "weekly" : "5h";
|
|
809
|
-
parts.push(
|
|
818
|
+
parts.push(
|
|
819
|
+
formatQuotaWindow(
|
|
820
|
+
bucket,
|
|
821
|
+
`${percentRemaining(bucket)}%`,
|
|
822
|
+
formatWindowLabel(bucket.windowMinutes, fallback, true),
|
|
823
|
+
countdown
|
|
824
|
+
)
|
|
825
|
+
);
|
|
810
826
|
}
|
|
811
|
-
return parts.length >
|
|
827
|
+
return parts.length > 0 ? `zai ${parts.join(" \xB7 ")}` : void 0;
|
|
812
828
|
}
|
|
813
829
|
function formatCurrencyMetric(metric) {
|
|
814
830
|
if (typeof metric.value !== "number") return String(metric.value);
|
|
@@ -891,7 +907,8 @@ function formatCodexStatusline(report, model, now = Date.now(), showResetCountdo
|
|
|
891
907
|
if (!group) return formatCodexCreditsStatus(report);
|
|
892
908
|
const buckets = report.buckets.filter((bucket) => (bucket.groupId ?? bucket.id) === group);
|
|
893
909
|
const labelBucket = buckets[0];
|
|
894
|
-
const
|
|
910
|
+
const prefix = group === "codex" ? "codex" : `codex ${compactLimitLabel(labelBucket?.groupLabel ?? group)}`;
|
|
911
|
+
const parts = [];
|
|
895
912
|
for (const bucket of buckets) {
|
|
896
913
|
if (bucket.remaining === void 0) continue;
|
|
897
914
|
const displayedPercentage = percentage === "used" ? bucket.used ?? 100 - bucket.remaining : bucket.remaining;
|
|
@@ -899,13 +916,13 @@ function formatCodexStatusline(report, model, now = Date.now(), showResetCountdo
|
|
|
899
916
|
const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
|
|
900
917
|
const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
901
918
|
if (!showResetCountdown) {
|
|
902
|
-
parts.push(`${
|
|
919
|
+
parts.push(`${window}:${percent}`);
|
|
903
920
|
continue;
|
|
904
921
|
}
|
|
905
922
|
const reset = formatResetCountdown(bucket.resetsAt, now);
|
|
906
|
-
parts.push(`${percent}
|
|
923
|
+
parts.push(reset ? `${percent} \u21BB ${reset}` : `${window}:${percent}`);
|
|
907
924
|
}
|
|
908
|
-
return parts.length >
|
|
925
|
+
return parts.length > 0 ? `${prefix} ${parts.join(" \xB7 ")}` : formatCodexCreditsStatus(report);
|
|
909
926
|
}
|
|
910
927
|
function formatCodexCreditsStatus(report) {
|
|
911
928
|
const credits = report.metrics.find((metric) => metric.id === "credits");
|
|
@@ -984,6 +1001,17 @@ function formatWindowLabel(minutes, fallback, compact) {
|
|
|
984
1001
|
if (minutes % 60 === 0) return `${minutes / 60}h`;
|
|
985
1002
|
return `${minutes}m`;
|
|
986
1003
|
}
|
|
1004
|
+
function formatQuotaWindow(bucket, percent, window, countdown) {
|
|
1005
|
+
const reset = formatQuotaCountdown(bucket, countdown);
|
|
1006
|
+
return reset ? `${percent} \u21BB ${reset}` : `${window}:${percent}`;
|
|
1007
|
+
}
|
|
1008
|
+
function formatQuotaCountdown(bucket, countdown) {
|
|
1009
|
+
if (bucket.period === "unlimited" || bucket.resetsAt === void 0 || !Number.isFinite(bucket.resetsAt * 1e3) || !Number.isFinite(countdown.now) || bucket.resetsAt * 1e3 <= countdown.now)
|
|
1010
|
+
return void 0;
|
|
1011
|
+
const reset = formatResetCountdown(bucket.resetsAt, countdown.now);
|
|
1012
|
+
if (reset !== void 0) countdown.active = true;
|
|
1013
|
+
return reset;
|
|
1014
|
+
}
|
|
987
1015
|
function formatResetCountdown(resetsAt, now) {
|
|
988
1016
|
if (resetsAt === void 0 || !Number.isFinite(resetsAt) || !Number.isFinite(now)) return void 0;
|
|
989
1017
|
const totalMinutes = Math.max(0, Math.ceil((resetsAt * 1e3 - now) / 6e4));
|
|
@@ -1942,23 +1970,23 @@ function usageExtension(pi, dependencies = {}) {
|
|
|
1942
1970
|
const showCodexResetCountdown = outcome.state.report.providerId === "openai-codex" && settings.codexStatusResetCountdown;
|
|
1943
1971
|
const isCompanionReport = outcome.state.report.source === "openai-chatgpt-companion";
|
|
1944
1972
|
const now = Date.now();
|
|
1945
|
-
const
|
|
1973
|
+
const status = formatUsageStatuslineWithCountdown(
|
|
1946
1974
|
outcome.state.report,
|
|
1947
1975
|
model,
|
|
1948
1976
|
now,
|
|
1949
1977
|
showCodexResetCountdown,
|
|
1950
1978
|
settings.codexStatusPercentage
|
|
1951
1979
|
);
|
|
1952
|
-
const value =
|
|
1980
|
+
const value = status.value ? fastRuntime.decorateStatus(model, status.value) : void 0;
|
|
1953
1981
|
if (!safeSetStatus(ctx, value)) return;
|
|
1954
1982
|
if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
|
|
1955
|
-
if (sessionActive && (showCodexResetCountdown || isCompanionReport) && outcome.state.report.buckets.some(
|
|
1983
|
+
if (sessionActive && (status.hasCountdown || (showCodexResetCountdown || isCompanionReport) && outcome.state.report.buckets.some(
|
|
1956
1984
|
(bucket) => (!isCompanionReport || bucket.groupId === "chatgpt-plan") && bucket.resetsAt !== void 0 && Number.isFinite(bucket.resetsAt) && bucket.resetsAt * 1e3 > now
|
|
1957
|
-
)) {
|
|
1985
|
+
))) {
|
|
1958
1986
|
const generation = statusGeneration;
|
|
1959
1987
|
statusCountdownTimer = setTimeout(() => {
|
|
1960
|
-
statusCountdownTimer = void 0;
|
|
1961
1988
|
if (!sessionActive || generation !== statusGeneration) return;
|
|
1989
|
+
statusCountdownTimer = void 0;
|
|
1962
1990
|
publishStatus(ctx, outcome, model, false);
|
|
1963
1991
|
}, STATUS_COUNTDOWN_REFRESH_MS);
|
|
1964
1992
|
statusCountdownTimer.unref?.();
|