@henryqw/pi-multi-codex 2.0.2 → 2.0.4
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 +4 -3
- package/extensions/multi-codex.ts +61 -57
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -21,14 +21,14 @@ pi install npm:@henryqw/pi-multi-codex
|
|
|
21
21
|
|
|
22
22
|
## Use
|
|
23
23
|
|
|
24
|
-
Run `/login` and authenticate `OpenAI Codex` for slot 1 first. Run `/codex-add`, then run `/login` and select the new `OpenAI Codex #<n>` provider. Restart Pi or update model scope, then run `/codex-status`. The status lists the new slot with cached quota when available, or `unavailable` until the first successful snapshot.
|
|
24
|
+
Run `/login` and authenticate `OpenAI Codex (legacy)` for slot 1 first. Run `/codex-add`, then run `/login` and select the new `OpenAI Codex #<n>` provider. Restart Pi or update model scope, then run `/codex-status`. The status lists the new slot with cached quota when available, or `unavailable` until the first successful snapshot.
|
|
25
25
|
|
|
26
26
|
| Surface | Type | Purpose |
|
|
27
27
|
| --- | --- | --- |
|
|
28
28
|
| `/codex-add` | command | For people: create the next numbered slot, then authenticate that slot. |
|
|
29
29
|
| `/codex-status` | command | For people: show shared quota snapshots and five-hour blocks; never waits on network. |
|
|
30
30
|
| `/codex-switch` | command | For people: select an authenticated, scope-allowed slot for the current Codex model. |
|
|
31
|
-
| Pi model selector (`OpenAI Codex`, `OpenAI Codex #<n>`) | ui | Choose an authenticated account slot. |
|
|
31
|
+
| Pi model selector (`OpenAI Codex (legacy)`, `OpenAI Codex #<n>`) | ui | Choose an authenticated account slot. |
|
|
32
32
|
| Pi status `pi-multi-codex` | ui | Report the active slot's fresh quota, five-hour limit countdown, or stale/unavailable state; `@henryqw/pi-footer` displays it in the footer. |
|
|
33
33
|
|
|
34
34
|
The footer and `/codex-status` show the five-hour reset countdown for Free, Go, and Plus tiers. They show the seven-day reset countdown for Pro Lite and other tiers. A numbered slot is one Codex account position in Pi.
|
|
@@ -41,6 +41,7 @@ The footer and `/codex-status` show the five-hour reset countdown for Free, Go,
|
|
|
41
41
|
- After HTTP 429, failover can use authenticated, registered, scope-allowed, untried slots with stale or missing quota.
|
|
42
42
|
- Failover skips known active five-hour blocks. It ranks fresh known quota first, then unranked slots by slot number.
|
|
43
43
|
- Routing preserves the model ID.
|
|
44
|
+
- Only physical Codex models are slots. A virtual model listed under `openai-codex`, such as `openai-codex/auto`, is never routed to a slot and shows no quota status.
|
|
44
45
|
- During one agent run, each eligible slot is tried at most once after HTTP 429 responses.
|
|
45
46
|
- Automatic retry stops when no untried eligible slot remains.
|
|
46
47
|
- The footer shows the active slot's fresh quota or five-hour block.
|
|
@@ -61,6 +62,6 @@ The extension maintains a generated, credential-free quota cache at `~/.pi/agent
|
|
|
61
62
|
|
|
62
63
|
## Limits and recovery
|
|
63
64
|
|
|
64
|
-
The extension reads `auth.json
|
|
65
|
+
The extension reads `auth.json` to identify account slots but does not directly edit credentials. When it asks Pi's auth resolver for a token to check quota, Pi may refresh expired OAuth credentials and update its auth store.
|
|
65
66
|
|
|
66
67
|
Scoped sessions can switch only to exact scoped aliases.
|
|
@@ -46,17 +46,21 @@ type SlotIdentity = {
|
|
|
46
46
|
type SlotAuthResolver = (slot: number) => Promise<string | undefined>;
|
|
47
47
|
type QuotaRefresh = { session: AbortController; resolveSlotAuth: SlotAuthResolver };
|
|
48
48
|
|
|
49
|
-
type
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
fetchedAt?: number;
|
|
55
|
-
remaining?: number;
|
|
56
|
-
reset?: number;
|
|
49
|
+
type SnapshotBase = { slot: number; accountHash: string; tier?: string; checkedAt: number };
|
|
50
|
+
type MeasuredSnapshot = SnapshotBase & {
|
|
51
|
+
fetchedAt: number;
|
|
52
|
+
remaining: number;
|
|
53
|
+
reset: number;
|
|
57
54
|
fiveHourReset?: number;
|
|
58
|
-
limitedUntil
|
|
55
|
+
limitedUntil: number | null;
|
|
59
56
|
};
|
|
57
|
+
type UsageSnapshot = MeasuredSnapshot | (SnapshotBase & {
|
|
58
|
+
fetchedAt?: never;
|
|
59
|
+
remaining?: never;
|
|
60
|
+
reset?: never;
|
|
61
|
+
fiveHourReset?: never;
|
|
62
|
+
limitedUntil?: never;
|
|
63
|
+
});
|
|
60
64
|
|
|
61
65
|
type UsageLock = {
|
|
62
66
|
slot: number;
|
|
@@ -76,7 +80,7 @@ type Config = { autoSwitchOn429: boolean };
|
|
|
76
80
|
|
|
77
81
|
const NATIVE_PROVIDER_ID = "openai-codex";
|
|
78
82
|
const CODEX_ALIAS_PATTERN = /^openai-codex-([2-9]|[1-9]\d+)$/;
|
|
79
|
-
const NO_ACCOUNTS_MESSAGE = "No Codex OAuth accounts found. Run /login and select OpenAI Codex.";
|
|
83
|
+
const NO_ACCOUNTS_MESSAGE = "No Codex OAuth accounts found. Run /login and select OpenAI Codex (legacy).";
|
|
80
84
|
const AGENT_STARTED_ENTRY = "pi-multi-codex:agent-started";
|
|
81
85
|
const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
82
86
|
const REFRESH_MS = 5 * 60_000;
|
|
@@ -182,30 +186,27 @@ function validTier(value: unknown): value is string {
|
|
|
182
186
|
|
|
183
187
|
function readSnapshot(value: unknown): UsageSnapshot | undefined {
|
|
184
188
|
if (!isRecord(value) || !validSlot(value.slot) || !validHash(value.accountHash) || !validTime(value.checkedAt)) return undefined;
|
|
185
|
-
const
|
|
186
|
-
if (successful && (
|
|
187
|
-
!validTime(value.fetchedAt) ||
|
|
188
|
-
typeof value.remaining !== "number" ||
|
|
189
|
-
!Number.isFinite(value.remaining) ||
|
|
190
|
-
!validTime(value.reset) ||
|
|
191
|
-
(value.fiveHourReset !== undefined && !validTime(value.fiveHourReset)) ||
|
|
192
|
-
(value.limitedUntil !== null && !validTime(value.limitedUntil))
|
|
193
|
-
)) return undefined;
|
|
194
|
-
if (!successful && (value.fiveHourReset !== undefined || value.limitedUntil !== undefined)) return undefined;
|
|
195
|
-
return {
|
|
189
|
+
const base = {
|
|
196
190
|
slot: value.slot,
|
|
197
191
|
accountHash: value.accountHash,
|
|
198
192
|
...(validTier(value.tier) ? { tier: value.tier } : {}),
|
|
199
193
|
checkedAt: value.checkedAt,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
194
|
+
};
|
|
195
|
+
const successful = value.fetchedAt !== undefined || value.remaining !== undefined || value.reset !== undefined;
|
|
196
|
+
if (!successful) {
|
|
197
|
+
if (value.fiveHourReset !== undefined || value.limitedUntil !== undefined) return undefined;
|
|
198
|
+
return base;
|
|
199
|
+
}
|
|
200
|
+
if (!validTime(value.fetchedAt) || typeof value.remaining !== "number" || !Number.isFinite(value.remaining) ||
|
|
201
|
+
!validTime(value.reset) || (value.fiveHourReset !== undefined && !validTime(value.fiveHourReset)) ||
|
|
202
|
+
(value.limitedUntil !== null && !validTime(value.limitedUntil))) return undefined;
|
|
203
|
+
return {
|
|
204
|
+
...base,
|
|
205
|
+
fetchedAt: value.fetchedAt,
|
|
206
|
+
remaining: Math.max(0, Math.min(100, value.remaining)),
|
|
207
|
+
reset: value.reset,
|
|
208
|
+
...(validTime(value.fiveHourReset) ? { fiveHourReset: value.fiveHourReset } : {}),
|
|
209
|
+
limitedUntil: value.limitedUntil,
|
|
209
210
|
};
|
|
210
211
|
}
|
|
211
212
|
|
|
@@ -299,12 +300,14 @@ async function withCacheMutex<T>(operation: (signal: AbortSignal) => Promise<T>,
|
|
|
299
300
|
throw new Error("Cache mutex acquisition ended unexpectedly.");
|
|
300
301
|
}
|
|
301
302
|
|
|
302
|
-
function
|
|
303
|
+
function isMeasured(snapshot: UsageSnapshot | undefined): snapshot is MeasuredSnapshot {
|
|
304
|
+
return snapshot?.fetchedAt !== undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function isFresh(snapshot: UsageSnapshot | undefined, identity: SlotIdentity, now: number): snapshot is MeasuredSnapshot {
|
|
303
308
|
return Boolean(
|
|
304
|
-
snapshot &&
|
|
309
|
+
isMeasured(snapshot) &&
|
|
305
310
|
snapshot.accountHash === identity.accountHash &&
|
|
306
|
-
validTime(snapshot.fetchedAt) &&
|
|
307
|
-
validTime(snapshot.reset) &&
|
|
308
311
|
snapshot.fetchedAt <= now &&
|
|
309
312
|
snapshot.reset > now &&
|
|
310
313
|
now - snapshot.fetchedAt < REFRESH_MS,
|
|
@@ -312,14 +315,14 @@ function isFresh(snapshot: UsageSnapshot | undefined, identity: SlotIdentity, no
|
|
|
312
315
|
}
|
|
313
316
|
|
|
314
317
|
function isFiveHourLimited(snapshot: UsageSnapshot, now: number): boolean {
|
|
315
|
-
return
|
|
318
|
+
return isMeasured(snapshot) && snapshot.limitedUntil !== null && snapshot.limitedUntil > now;
|
|
316
319
|
}
|
|
317
320
|
|
|
318
|
-
function displayedReset(snapshot:
|
|
319
|
-
if (["free", "go", "plus"].includes(snapshot.tier ?? "") &&
|
|
321
|
+
function displayedReset(snapshot: MeasuredSnapshot, now: number): { label: "5h" | "7d"; time: number } {
|
|
322
|
+
if (["free", "go", "plus"].includes(snapshot.tier ?? "") && snapshot.fiveHourReset !== undefined && snapshot.fiveHourReset > now) {
|
|
320
323
|
return { label: "5h", time: snapshot.fiveHourReset };
|
|
321
324
|
}
|
|
322
|
-
return { label: "7d", time: snapshot.reset
|
|
325
|
+
return { label: "7d", time: snapshot.reset };
|
|
323
326
|
}
|
|
324
327
|
|
|
325
328
|
function checkedRecently(snapshot: UsageSnapshot | undefined, identity: SlotIdentity, now: number): boolean {
|
|
@@ -328,7 +331,7 @@ function checkedRecently(snapshot: UsageSnapshot | undefined, identity: SlotIden
|
|
|
328
331
|
snapshot.accountHash === identity.accountHash &&
|
|
329
332
|
snapshot.checkedAt <= now &&
|
|
330
333
|
now - snapshot.checkedAt < REFRESH_MS &&
|
|
331
|
-
(!
|
|
334
|
+
(!isMeasured(snapshot) || snapshot.reset > now),
|
|
332
335
|
);
|
|
333
336
|
}
|
|
334
337
|
|
|
@@ -491,7 +494,7 @@ class CodexQuotaStatus {
|
|
|
491
494
|
? lock.heartbeatAt + LOCK_STALE_MS + 1
|
|
492
495
|
: undefined;
|
|
493
496
|
const snapshotDue = isFresh(snapshot, identity, now)
|
|
494
|
-
? Math.min(snapshot
|
|
497
|
+
? Math.min(snapshot.fetchedAt + REFRESH_MS, snapshot.reset) + 1
|
|
495
498
|
: checkedRecently(snapshot, identity, now)
|
|
496
499
|
? snapshot!.checkedAt + REFRESH_MS + 1
|
|
497
500
|
: now;
|
|
@@ -614,7 +617,7 @@ class CodexQuotaStatus {
|
|
|
614
617
|
limitedUntil: outcome.limitedUntil && outcome.limitedUntil > checkedAt ? outcome.limitedUntil : null,
|
|
615
618
|
});
|
|
616
619
|
} else {
|
|
617
|
-
state.slots.set(slot, previous?.accountHash === identity.accountHash && (!
|
|
620
|
+
state.slots.set(slot, previous?.accountHash === identity.accountHash && (!isMeasured(previous) || previous.reset > checkedAt)
|
|
618
621
|
? { ...previous, checkedAt }
|
|
619
622
|
: { slot, accountHash: identity.accountHash, checkedAt });
|
|
620
623
|
}
|
|
@@ -685,7 +688,7 @@ class CodexQuotaStatus {
|
|
|
685
688
|
.map(([slot, credential]) => {
|
|
686
689
|
const identity = identityFor(credential);
|
|
687
690
|
const snapshot = state.slots.get(slot);
|
|
688
|
-
if (!identity || !snapshot || snapshot.accountHash !== identity.accountHash
|
|
691
|
+
if (!identity || !isMeasured(snapshot) || snapshot.accountHash !== identity.accountHash) {
|
|
689
692
|
return `Codex slot ${slot}: unavailable`;
|
|
690
693
|
}
|
|
691
694
|
const tier = snapshot.tier ? ` (${snapshot.tier})` : "";
|
|
@@ -815,8 +818,9 @@ function providerForSlot(slot: number): string {
|
|
|
815
818
|
return slot === 1 ? NATIVE_PROVIDER_ID : `${NATIVE_PROVIDER_ID}-${slot}`;
|
|
816
819
|
}
|
|
817
820
|
|
|
818
|
-
|
|
819
|
-
|
|
821
|
+
/** A physical Codex model on a slot provider. Virtual models listed under `openai-codex` route elsewhere and are not slots. */
|
|
822
|
+
function isManagedModel(model: Model<any> | undefined): model is CodexModel {
|
|
823
|
+
return model?.api === "openai-codex-responses" && slotForProvider(model.provider) !== undefined;
|
|
820
824
|
}
|
|
821
825
|
|
|
822
826
|
function sessionHasAgentWork(ctx: ExtensionContext): boolean {
|
|
@@ -871,7 +875,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
871
875
|
if ((slot !== 1 && !registered.has(slot)) || !allowsModel(ctx, model, slot)) return [];
|
|
872
876
|
const identity = identityFor(credential);
|
|
873
877
|
const snapshot = state.slots.get(slot);
|
|
874
|
-
if (!identity || !isFresh(snapshot, identity, now) ||
|
|
878
|
+
if (!identity || !isFresh(snapshot, identity, now) || isFiveHourLimited(snapshot, now)) return [];
|
|
875
879
|
return [{ slot, remaining: snapshot.remaining }];
|
|
876
880
|
});
|
|
877
881
|
};
|
|
@@ -895,7 +899,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
895
899
|
const identity = identityFor(credential);
|
|
896
900
|
const snapshot = state.slots.get(slot);
|
|
897
901
|
if (identity && snapshot?.accountHash === identity.accountHash && isFiveHourLimited(snapshot, now)) return [];
|
|
898
|
-
const remaining = identity && isFresh(snapshot, identity, now) ? snapshot
|
|
902
|
+
const remaining = identity && isFresh(snapshot, identity, now) ? snapshot.remaining : undefined;
|
|
899
903
|
return [{ slot, remaining }];
|
|
900
904
|
})
|
|
901
905
|
.sort((left, right) => (right.remaining ?? -1) - (left.remaining ?? -1) || left.slot - right.slot)
|
|
@@ -904,12 +908,12 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
904
908
|
|
|
905
909
|
const footerText = (ctx: ExtensionContext): string | undefined => {
|
|
906
910
|
const model = ctx.model;
|
|
907
|
-
const slot = slotForProvider(model
|
|
908
|
-
if (!
|
|
911
|
+
const slot = isManagedModel(model) ? slotForProvider(model.provider) : undefined;
|
|
912
|
+
if (!slot) return undefined;
|
|
909
913
|
const identity = currentIdentity(slot);
|
|
910
914
|
const snapshot = quota.snapshot(slot);
|
|
911
915
|
const prefix = `Codex #${slot}`;
|
|
912
|
-
if (!identity || !snapshot || snapshot.accountHash !== identity.accountHash
|
|
916
|
+
if (!identity || !isMeasured(snapshot) || snapshot.accountHash !== identity.accountHash) {
|
|
913
917
|
return `${prefix} · unavailable`;
|
|
914
918
|
}
|
|
915
919
|
const now = Date.now();
|
|
@@ -938,7 +942,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
938
942
|
};
|
|
939
943
|
|
|
940
944
|
const updatePendingCandidate = (ctx: ExtensionContext): void => {
|
|
941
|
-
if (!
|
|
945
|
+
if (!isManagedModel(ctx.model)) return;
|
|
942
946
|
const slot = selectFreshSlot(ctx, ctx.model);
|
|
943
947
|
automaticCandidate = slot ? { ...ctx.model, provider: providerForSlot(slot) } : ctx.model;
|
|
944
948
|
};
|
|
@@ -967,7 +971,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
967
971
|
ctx.ui.notify("Pi Multi Codex config is invalid. Automatic 429 switching is disabled; the file was left unchanged.", "warning");
|
|
968
972
|
}
|
|
969
973
|
}
|
|
970
|
-
automaticOpen =
|
|
974
|
+
automaticOpen = isManagedModel(ctx.model) && !sessionHasAgentWork(ctx);
|
|
971
975
|
automaticCandidate = ctx.model;
|
|
972
976
|
// Cache-only ranking happens before background refresh starts.
|
|
973
977
|
updatePendingCandidate(ctx);
|
|
@@ -980,7 +984,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
980
984
|
triedSlots.clear();
|
|
981
985
|
providerRequest = undefined;
|
|
982
986
|
const model = ctx.model;
|
|
983
|
-
const slot = slotForProvider(model
|
|
987
|
+
const slot = isManagedModel(model) ? slotForProvider(model.provider) : undefined;
|
|
984
988
|
const identity = slot ? currentIdentity(slot) : undefined;
|
|
985
989
|
const snapshot = slot ? quota.snapshot(slot) : undefined;
|
|
986
990
|
if (!automaticOpen && (preserveModelChoice || !identity || snapshot?.accountHash !== identity.accountHash || !isFiveHourLimited(snapshot, Date.now()))) return;
|
|
@@ -988,7 +992,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
988
992
|
// Close before await: no timer, refresh, queued turn, or provider event can route later.
|
|
989
993
|
automaticOpen = false;
|
|
990
994
|
const candidate = automaticCandidate;
|
|
991
|
-
if (!model || !candidate ||
|
|
995
|
+
if (!isManagedModel(model) || !candidate || candidate.id !== model.id || candidate.provider === model.provider) return;
|
|
992
996
|
await setModelAutomatically(candidate);
|
|
993
997
|
updateFooter(ctx);
|
|
994
998
|
});
|
|
@@ -1011,7 +1015,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
1011
1015
|
const model = ctx.model;
|
|
1012
1016
|
const failedSlot = slotForProvider(event.message.provider);
|
|
1013
1017
|
if (
|
|
1014
|
-
!model
|
|
1018
|
+
!isManagedModel(model)
|
|
1015
1019
|
|| !failedSlot
|
|
1016
1020
|
|| request.provider !== event.message.provider
|
|
1017
1021
|
|| model.provider !== event.message.provider
|
|
@@ -1066,7 +1070,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
1066
1070
|
handler: async (_args, ctx) => {
|
|
1067
1071
|
const slots = syncSlots();
|
|
1068
1072
|
if (!slots.has(1)) {
|
|
1069
|
-
ctx.ui.notify("Run /login and select OpenAI Codex for slot 1 first.", "warning");
|
|
1073
|
+
ctx.ui.notify("Run /login and select OpenAI Codex (legacy) for slot 1 first.", "warning");
|
|
1070
1074
|
return;
|
|
1071
1075
|
}
|
|
1072
1076
|
|
|
@@ -1090,7 +1094,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
1090
1094
|
description: "switch current Codex model to another authenticated slot",
|
|
1091
1095
|
handler: async (_args, ctx) => {
|
|
1092
1096
|
const model = ctx.model;
|
|
1093
|
-
if (!
|
|
1097
|
+
if (!isManagedModel(model)) {
|
|
1094
1098
|
ctx.ui.notify("Select an OpenAI Codex model before switching slots.", "warning");
|
|
1095
1099
|
return;
|
|
1096
1100
|
}
|
|
@@ -1105,7 +1109,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
|
|
|
1105
1109
|
ctx.ui.notify(
|
|
1106
1110
|
scopedModels.length > 0
|
|
1107
1111
|
? "No authenticated Codex slot matches this session's model scope. Restart Pi or update scoped models."
|
|
1108
|
-
: "No authenticated Codex slots found. Run /login and select OpenAI Codex.",
|
|
1112
|
+
: "No authenticated Codex slots found. Run /login and select OpenAI Codex (legacy).",
|
|
1109
1113
|
"warning",
|
|
1110
1114
|
);
|
|
1111
1115
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-multi-codex",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "Use multiple ChatGPT Codex OAuth accounts in Pi.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"pack:check": "npm pack --dry-run"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@earendil-works/pi-ai": ">=0.86.0 <0.
|
|
29
|
-
"@earendil-works/pi-coding-agent": ">=0.86.0 <0.
|
|
28
|
+
"@earendil-works/pi-ai": ">=0.86.0 <0.100.0",
|
|
29
|
+
"@earendil-works/pi-coding-agent": ">=0.86.0 <0.100.0"
|
|
30
30
|
},
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"proper-lockfile": "^4.1.2"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-ai": "0.
|
|
54
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
53
|
+
"@earendil-works/pi-ai": "0.99.1",
|
|
54
|
+
"@earendil-works/pi-coding-agent": "0.99.1",
|
|
55
55
|
"@types/proper-lockfile": "^4.1.4"
|
|
56
56
|
}
|
|
57
57
|
}
|