pi-provider-litellm 1.2.7 → 1.2.8

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 CHANGED
@@ -44,6 +44,21 @@ You can also run `/login`, select `Use a subscription`, then select `LiteLLM`.
44
44
 
45
45
  You'll be prompted for the base URL and API key. Credentials are persisted to `~/.pi/agent/auth.json`.
46
46
 
47
+ #### Enterprise SSO login
48
+
49
+ If your LiteLLM proxy requires SSO/OAuth authentication (enterprise deployments), you can authenticate via a browser SSO flow and optionally pair the resulting JWT with a stable virtual key:
50
+
51
+ 1. Run `/login litellm` inside pi
52
+ 2. Enter the proxy URL
53
+ 3. At the login method prompt, enter `2` for SSO / Enterprise JWT
54
+ 4. Your default browser opens the LiteLLM SSO login URL (e.g. `https://litellm.your-domain.com/sso/key/generate`) automatically — the URL is also displayed in case it can't be opened. Authenticate via SSO
55
+ 5. Copy your token from the LiteLLM UI and paste it at the prompt (copying a full `Bearer ...` header value is fine — the prefix is stripped automatically)
56
+ 6. When prompted to generate a virtual key, press Enter to accept (recommended) or enter `n` to use the JWT directly
57
+
58
+ When you generate a virtual key, the resulting `sk-...` key is stored as your credential and used for all API requests. If the proxy's key policy attaches an expiry to the generated key, Pi will prompt you to re-authenticate when it nears expiry; otherwise the key is treated as permanent until revoked in LiteLLM.
59
+
60
+ When using a JWT directly, the extension reads its `exp` claim and Pi will prompt you to re-authenticate when the token nears expiry. Run `/login litellm` again to refresh.
61
+
47
62
  ### Option B — environment variables
48
63
 
49
64
  ```bash
@@ -100,39 +115,6 @@ If your LiteLLM proxy exposes `/v1/skills`, enabled skills are fetched before ea
100
115
  - `litellm_skill_create`
101
116
  - `litellm_skill_delete`
102
117
 
103
- ## Mocked LiteLLM smoke workflow
104
-
105
- The `LiteLLM Smoke` GitHub Actions workflow starts VidaiMock and a real LiteLLM proxy on the runner. LiteLLM exposes OpenAI-compatible and Anthropic routes whose upstreams are served by VidaiMock, then this extension's smoke runner discovers those models through LiteLLM and sends `/v1/chat/completions` requests through the proxy.
106
-
107
- This keeps the LiteLLM integration path under test but does not call real LLM APIs. No provider API keys or GitHub Models permission are required. The workflow also runs a non-interactive Pi CLI smoke with `--list-models` and `-p` so extension loading, model discovery, and a real completion path are covered without opening the TUI.
108
-
109
- ## Development
110
-
111
- This package requires Node.js `>=22.19.0`. CI currently uses Node `24.16.0`.
112
-
113
- ```bash
114
- npm ci
115
- npm run check
116
- npm run clean && npm run build
117
- ```
118
-
119
- `npm run check` runs Biome, type checking, and the Vitest suite. Runtime changes must be built before local Pi smoke checks because the extension entrypoint is `./dist/index.js`.
120
-
121
- Before changing package contents or dependency policy, also run:
122
-
123
- ```bash
124
- npm run supply-chain:guard
125
- npm pack --dry-run
126
- ```
127
-
128
- The published npm package should contain only `dist`, `README.md`, and `LICENSE`.
129
-
130
- ## Release
131
-
132
- Releases are driven by semver tags named `v*.*.*`. The GitHub release workflow installs from the lockfile, runs the checks, builds `dist`, verifies the package tarball, publishes to npm with provenance, and creates a GitHub release.
133
-
134
- Before tagging a release, keep `package.json` and `package-lock.json` versions in sync and verify the dry-run package contents.
135
-
136
118
  ## Slash commands
137
119
 
138
120
  - `/litellm-refresh` — force re-fetch the model list, ignoring cache
@@ -153,6 +135,8 @@ If the cache is older than 24 hours, the extension refreshes it in the backgroun
153
135
  | Discovery times out | Increase `LITELLM_DISCOVERY_TIMEOUT_MS` or set `LITELLM_OFFLINE=1` to fall back on cached models |
154
136
  | `401 Token expired` | Set `LITELLM_API_KEY_HELPER`. |
155
137
  | No models with gcloud auth | Verify `gcloud auth application-default login` has been run or set `GOOGLE_APPLICATION_CREDENTIALS` to an `authorized_user` ADC file |
138
+ | Enterprise SSO login shows "virtual key generation failed" | The LiteLLM instance may lack a database (`/key/generate` requires one), your user account may lack key-generation permission, or the request timed out; the JWT is used directly as a fallback |
139
+ | Enterprise SSO token prompt fails with "SSO token is required" | The token field was left empty — paste the token copied from the LiteLLM UI |
156
140
  | MCP tools not showing | Verify the proxy exposes `/mcp-rest/tools/list` and run `/litellm-refresh` after fixing the proxy |
157
141
  | Skills not affecting prompts | Verify the proxy exposes `/v1/skills` and returns enabled skills |
158
142
 
@@ -4,4 +4,8 @@ export declare function normalizeBaseUrl(input: string): string;
4
4
  export declare function isMoonshotModel(modelId: string): boolean;
5
5
  export declare function shouldSuppressReasoningContent(modelId: string): boolean;
6
6
  export declare function buildCompat(modelId: string): ProviderModelConfig["compat"];
7
+ export declare function withTimeout(timeoutMs: number, signal?: AbortSignal): {
8
+ signal: AbortSignal;
9
+ cancel: () => void;
10
+ };
7
11
  export declare function discoverModels(baseUrl: string, apiKey: string, options?: DiscoveryOptions): Promise<DiscoveryResult>;
package/dist/discover.js CHANGED
@@ -45,22 +45,53 @@ function toKnownProvider(provider) {
45
45
  }
46
46
  function findCatalogModel(id, ownedBy) {
47
47
  const prefixProvider = toKnownProvider(id.split("/")[0]);
48
- const candidates = [toKnownProvider(ownedBy), prefixProvider].filter((provider) => provider !== undefined);
48
+ const lookupIds = catalogLookupIds(id);
49
+ const candidates = [toKnownProvider(ownedBy), prefixProvider, lookupIds.length > 1 ? "anthropic" : undefined].filter((provider) => provider !== undefined);
49
50
  for (const provider of candidates) {
50
- const exact = getModels(provider).find((model) => model.id === id);
51
- if (exact)
52
- return exact;
53
- const providerQualified = getModels(provider).find((model) => model.id === `${provider}/${id}`);
54
- if (providerQualified)
55
- return providerQualified;
51
+ const match = findCatalogModelInProvider(provider, lookupIds);
52
+ if (match)
53
+ return match;
56
54
  }
57
55
  for (const provider of getProviders()) {
58
- const exact = getModels(provider).find((model) => model.id === id);
56
+ const match = findCatalogModelInProvider(provider, lookupIds);
57
+ if (match)
58
+ return match;
59
+ }
60
+ return undefined;
61
+ }
62
+ function catalogLookupIds(id) {
63
+ const lookupIds = new Set([id]);
64
+ const unprefixed = id.includes("/") ? id.slice(id.indexOf("/") + 1) : id;
65
+ lookupIds.add(unprefixed);
66
+ const anthropicAlias = unprefixed.toLowerCase().replaceAll(".", "-");
67
+ const match = /^(?:claude-)?(opus|sonnet|haiku)-(\d+)-(\d+)$/.exec(anthropicAlias);
68
+ if (match)
69
+ lookupIds.add(`claude-${match[1]}-${match[2]}-${match[3]}`);
70
+ return [...lookupIds];
71
+ }
72
+ function findCatalogModelInProvider(provider, lookupIds) {
73
+ for (const lookupId of lookupIds) {
74
+ const exact = getModels(provider).find((model) => model.id === lookupId);
59
75
  if (exact)
60
76
  return exact;
77
+ const providerQualified = getModels(provider).find((model) => model.id === `${provider}/${lookupId}`);
78
+ if (providerQualified)
79
+ return providerQualified;
61
80
  }
62
81
  return undefined;
63
82
  }
83
+ function mapModelInfoCost(info, fallback) {
84
+ return {
85
+ input: info.input_cost_per_token !== undefined ? info.input_cost_per_token * 1_000_000 : (fallback?.input ?? 0),
86
+ output: info.output_cost_per_token !== undefined ? info.output_cost_per_token * 1_000_000 : (fallback?.output ?? 0),
87
+ cacheRead: info.cache_read_input_token_cost !== undefined
88
+ ? info.cache_read_input_token_cost * 1_000_000
89
+ : (fallback?.cacheRead ?? 0),
90
+ cacheWrite: info.cache_creation_input_token_cost !== undefined
91
+ ? info.cache_creation_input_token_cost * 1_000_000
92
+ : (fallback?.cacheWrite ?? 0),
93
+ };
94
+ }
64
95
  function getFallbackProviderAndModel(id, ownedBy) {
65
96
  const [prefix, ...rest] = id.split("/");
66
97
  const prefixProvider = toKnownProvider(prefix);
@@ -75,7 +106,7 @@ function findModelsDevModel(catalog, id, ownedBy) {
75
106
  return undefined;
76
107
  return catalog?.[provider]?.models?.[modelId];
77
108
  }
78
- function withTimeout(timeoutMs, signal) {
109
+ export function withTimeout(timeoutMs, signal) {
79
110
  const controller = new AbortController();
80
111
  const onAbort = () => controller.abort(signal?.reason);
81
112
  if (signal) {
@@ -170,17 +201,13 @@ function mapFromModelInfo(entry) {
170
201
  const info = entry.model_info ?? {};
171
202
  if (info.mode && info.mode !== "chat")
172
203
  return undefined;
204
+ const catalogModel = findCatalogModel(id);
173
205
  return {
174
206
  id,
175
207
  name: id,
176
208
  reasoning: info.supports_reasoning ?? false,
177
209
  input: info.supports_vision ? ["text", "image"] : ["text"],
178
- cost: {
179
- input: (info.input_cost_per_token ?? 0) * 1_000_000,
180
- output: (info.output_cost_per_token ?? 0) * 1_000_000,
181
- cacheRead: (info.cache_read_input_token_cost ?? 0) * 1_000_000,
182
- cacheWrite: (info.cache_creation_input_token_cost ?? 0) * 1_000_000,
183
- },
210
+ cost: mapModelInfoCost(info, catalogModel?.cost),
184
211
  contextWindow: info.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW,
185
212
  maxTokens: info.max_output_tokens ?? DEFAULT_MAX_TOKENS,
186
213
  compat: buildCompat(id),
@@ -192,19 +219,16 @@ function mapFromHealthModelInfo(entry, fallbackId) {
192
219
  return model;
193
220
  if (entry.model_info?.mode && entry.model_info.mode !== "chat")
194
221
  return undefined;
222
+ const info = entry.model_info ?? {};
223
+ const catalogModel = findCatalogModel(fallbackId);
195
224
  return {
196
225
  id: fallbackId,
197
226
  name: fallbackId,
198
- reasoning: entry.model_info?.supports_reasoning ?? false,
199
- input: entry.model_info?.supports_vision ? ["text", "image"] : ["text"],
200
- cost: {
201
- input: (entry.model_info?.input_cost_per_token ?? 0) * 1_000_000,
202
- output: (entry.model_info?.output_cost_per_token ?? 0) * 1_000_000,
203
- cacheRead: (entry.model_info?.cache_read_input_token_cost ?? 0) * 1_000_000,
204
- cacheWrite: (entry.model_info?.cache_creation_input_token_cost ?? 0) * 1_000_000,
205
- },
206
- contextWindow: entry.model_info?.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW,
207
- maxTokens: entry.model_info?.max_output_tokens ?? DEFAULT_MAX_TOKENS,
227
+ reasoning: info.supports_reasoning ?? false,
228
+ input: info.supports_vision ? ["text", "image"] : ["text"],
229
+ cost: mapModelInfoCost(info, catalogModel?.cost),
230
+ contextWindow: info.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW,
231
+ maxTokens: info.max_output_tokens ?? DEFAULT_MAX_TOKENS,
208
232
  compat: buildCompat(fallbackId),
209
233
  };
210
234
  }
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { execSync } from "node:child_process";
1
+ import { execSync, spawn } from "node:child_process";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { AuthStorage, getAgentDir } from "@earendil-works/pi-coding-agent";
5
5
  import { fingerprint, readCache, writeCache } from "./cache.js";
6
6
  import { setupLiteLLMCostTracking } from "./cost.js";
7
- import { discoverModels, normalizeBaseUrl, shouldSuppressReasoningContent } from "./discover.js";
7
+ import { discoverModels, normalizeBaseUrl, shouldSuppressReasoningContent, withTimeout } from "./discover.js";
8
8
  import { getGcloudToken, getGcloudTokenCacheKey, getGcloudTokenCommand, isGcloudTokenAuthEnabled, } from "./gcloud-token.js";
9
9
  import { getSessionIdFromFile } from "./litellm.js";
10
10
  import { createMcpToolDefinitions } from "./mcp-tools.js";
@@ -72,6 +72,45 @@ function tokenExpiresAt(apiKey, opaqueFallback = PERMANENT_TOKEN_EXPIRES_AT) {
72
72
  return opaqueFallback;
73
73
  }
74
74
  }
75
+ function openInBrowser(url) {
76
+ // Never invoke a shell here: cmd.exe re-parses metacharacters before `start`
77
+ // runs, which would make URL contents injectable. Launch is best-effort; the
78
+ // URL is also shown to the user, so launcher failures must not crash the process.
79
+ const [cmd, args] = process.platform === "darwin"
80
+ ? ["open", [url]]
81
+ : process.platform === "win32"
82
+ ? ["rundll32", ["url.dll,FileProtocolHandler", url]]
83
+ : ["xdg-open", [url]];
84
+ spawn(cmd, args, { stdio: "ignore", detached: true })
85
+ .on("error", () => undefined)
86
+ .unref();
87
+ }
88
+ async function generateVirtualKey(baseUrl, userToken, signal) {
89
+ const { signal: boundedSignal, cancel } = withTimeout(LOGIN_TIMEOUT_MS, signal);
90
+ try {
91
+ const response = await fetch(`${baseUrl}/key/generate`, {
92
+ method: "POST",
93
+ headers: {
94
+ Authorization: `Bearer ${userToken}`,
95
+ "Content-Type": "application/json",
96
+ },
97
+ body: JSON.stringify({}),
98
+ signal: boundedSignal,
99
+ });
100
+ if (!response.ok) {
101
+ const text = await response.text().catch(() => "");
102
+ throw new Error(`Virtual key generation failed (${response.status}): ${text}`);
103
+ }
104
+ const data = (await response.json());
105
+ if (typeof data.key !== "string" || !data.key)
106
+ throw new Error("No key in response from /key/generate");
107
+ const expiresMs = typeof data.expires === "string" ? Date.parse(data.expires) : Number.NaN;
108
+ return { key: data.key, expiresAt: Number.isNaN(expiresMs) ? undefined : expiresMs };
109
+ }
110
+ finally {
111
+ cancel();
112
+ }
113
+ }
75
114
  function getLiteLLMApiKey(credentials) {
76
115
  return credentials.access;
77
116
  }
@@ -164,12 +203,63 @@ async function loginLiteLLM(callbacks, onCacheWrite) {
164
203
  message: "Enter LiteLLM proxy URL (no trailing /v1):",
165
204
  placeholder: "https://litellm.example.com",
166
205
  })).trim();
167
- const apiKeyInput = (await callbacks.onPrompt({ message: "Enter API key:" })).trim();
168
- if (!rawBaseUrl || !apiKeyInput)
169
- throw new Error("Both base URL and API key are required");
206
+ if (!rawBaseUrl)
207
+ throw new Error("Base URL is required");
170
208
  const baseUrl = normalizeBaseUrl(rawBaseUrl);
171
- const refresh = apiKeyInput.startsWith("!") ? apiKeyInput : "";
172
- const apiKey = refresh ? executeApiKeyCommand(refresh) : apiKeyInput;
209
+ const method = (await callbacks.onPrompt({
210
+ message: "Select login method (1 = API key / !command, 2 = SSO / Enterprise JWT):",
211
+ })).trim();
212
+ let apiKey;
213
+ let refresh;
214
+ let expires;
215
+ if (method === "2") {
216
+ callbacks.onAuth?.({
217
+ url: `${baseUrl}/sso/key/generate`,
218
+ instructions: "Authenticate via SSO, then copy your token from the LiteLLM UI.",
219
+ });
220
+ const rawToken = (await callbacks.onPrompt({ message: "Paste your SSO token from the LiteLLM UI:" }))
221
+ .trim()
222
+ .replace(/^Bearer\s+/i, "")
223
+ .trim();
224
+ if (!rawToken)
225
+ throw new Error("SSO token is required");
226
+ const wantVirtualKey = (await callbacks.onPrompt({ message: "Generate a LiteLLM virtual key from this token? (y/n):" }))
227
+ .trim()
228
+ .toLowerCase();
229
+ if (wantVirtualKey !== "n" && wantVirtualKey !== "no") {
230
+ try {
231
+ callbacks.onProgress?.("Generating virtual key...");
232
+ const generated = await generateVirtualKey(baseUrl, rawToken, callbacks.signal);
233
+ apiKey = generated.key;
234
+ refresh = "";
235
+ expires =
236
+ generated.expiresAt === undefined
237
+ ? PERMANENT_TOKEN_EXPIRES_AT
238
+ : Math.max(Date.now(), generated.expiresAt - TOKEN_REFRESH_LEAD_MS);
239
+ callbacks.onProgress?.("Virtual key generated and will be used for API calls.");
240
+ }
241
+ catch (error) {
242
+ const message = error instanceof Error ? error.message : String(error);
243
+ callbacks.onProgress?.(`LiteLLM: virtual key generation failed (${message}); using SSO token directly.`);
244
+ apiKey = rawToken;
245
+ refresh = "";
246
+ expires = tokenExpiresAt(rawToken, PERMANENT_TOKEN_EXPIRES_AT);
247
+ }
248
+ }
249
+ else {
250
+ apiKey = rawToken;
251
+ refresh = "";
252
+ expires = tokenExpiresAt(rawToken, PERMANENT_TOKEN_EXPIRES_AT);
253
+ }
254
+ }
255
+ else {
256
+ const apiKeyInput = (await callbacks.onPrompt({ message: "Enter API key:" })).trim();
257
+ if (!apiKeyInput)
258
+ throw new Error("Both base URL and API key are required");
259
+ refresh = apiKeyInput.startsWith("!") ? apiKeyInput : "";
260
+ apiKey = refresh ? executeApiKeyCommand(refresh) : apiKeyInput;
261
+ expires = tokenExpiresAt(apiKey, refresh ? EXPIRE_TOKEN_IMMEDIATELY : PERMANENT_TOKEN_EXPIRES_AT);
262
+ }
173
263
  const { models, source } = await discoverModels(baseUrl, apiKey, {
174
264
  timeoutMs: LOGIN_TIMEOUT_MS,
175
265
  signal: callbacks.signal,
@@ -187,13 +277,17 @@ async function loginLiteLLM(callbacks, onCacheWrite) {
187
277
  return {
188
278
  access: apiKey,
189
279
  refresh,
190
- expires: tokenExpiresAt(apiKey, refresh ? EXPIRE_TOKEN_IMMEDIATELY : PERMANENT_TOKEN_EXPIRES_AT),
280
+ expires,
191
281
  baseUrl,
192
282
  };
193
283
  }
194
284
  async function refreshLiteLLM(credentials) {
195
- if (!credentials.refresh.startsWith("!"))
285
+ if (!credentials.refresh.startsWith("!")) {
286
+ if (credentials.expires < PERMANENT_TOKEN_EXPIRES_AT) {
287
+ throw new Error("LiteLLM credential cannot be refreshed; run /login litellm again");
288
+ }
196
289
  return credentials;
290
+ }
197
291
  const access = executeApiKeyCommand(credentials.refresh);
198
292
  return { ...credentials, access, expires: tokenExpiresAt(access, EXPIRE_TOKEN_IMMEDIATELY) };
199
293
  }
@@ -453,7 +547,10 @@ export default async function (pi) {
453
547
  }
454
548
  async function runLogin(ctx) {
455
549
  const credential = await loginLiteLLM({
456
- onAuth: () => undefined,
550
+ onAuth: ({ url, instructions }) => {
551
+ openInBrowser(url);
552
+ ctx.ui.notify(instructions ? `${url} — ${instructions}` : url, "info");
553
+ },
457
554
  onDeviceCode: () => undefined,
458
555
  onPrompt: async ({ message, placeholder }) => {
459
556
  const value = await ctx.ui.input(message, placeholder);
package/dist/mcp-tools.js CHANGED
@@ -92,37 +92,6 @@ function sanitizeName(name) {
92
92
  .replace(/^_+|_+$/g, "");
93
93
  return sanitized || "tool";
94
94
  }
95
- function isMappableSchema(value) {
96
- if (!value || typeof value !== "object")
97
- return false;
98
- const schema = value;
99
- if ("$ref" in schema || "anyOf" in schema || "oneOf" in schema || "allOf" in schema)
100
- return false;
101
- if (["string", "number", "integer", "boolean"].includes(String(schema.type)))
102
- return true;
103
- if (schema.type === "array") {
104
- const items = schema.items;
105
- return items?.type === "string";
106
- }
107
- return false;
108
- }
109
- function mapPropertySchema(name, schema) {
110
- const description = typeof schema.description === "string" ? schema.description : name;
111
- switch (schema.type) {
112
- case "string":
113
- return Type.String({ description });
114
- case "number":
115
- return Type.Number({ description });
116
- case "integer":
117
- return Type.Integer({ description });
118
- case "boolean":
119
- return Type.Boolean({ description });
120
- case "array":
121
- return Type.Array(Type.String(), { description });
122
- default:
123
- return Type.Unknown({ description });
124
- }
125
- }
126
95
  function buildParameters(inputSchema) {
127
96
  const properties = inputSchema.properties;
128
97
  if (!properties || typeof properties !== "object") {
@@ -130,20 +99,7 @@ function buildParameters(inputSchema) {
130
99
  args: Type.Record(Type.String(), Type.Unknown(), { description: "Tool arguments as key-value pairs" }),
131
100
  });
132
101
  }
133
- const required = Array.isArray(inputSchema.required)
134
- ? inputSchema.required.filter((key) => typeof key === "string")
135
- : [];
136
- const mapped = {};
137
- for (const [key, property] of Object.entries(properties)) {
138
- if (!isMappableSchema(property)) {
139
- return Type.Object({
140
- args: Type.Record(Type.String(), Type.Unknown(), { description: "Tool arguments as key-value pairs" }),
141
- });
142
- }
143
- const field = mapPropertySchema(key, property);
144
- mapped[key] = required.includes(key) ? field : Type.Optional(field);
145
- }
146
- return Type.Object(mapped);
102
+ return Type.Unsafe(inputSchema);
147
103
  }
148
104
  export async function createMcpToolDefinitions(baseUrl, getApiKey) {
149
105
  const discoveryApiKey = await getApiKey();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-litellm",
3
- "version": "1.2.7",
3
+ "version": "1.2.8",
4
4
  "description": "LiteLLM proxy provider extension for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,7 +40,7 @@
40
40
  "image": "https://raw.githubusercontent.com/balcsida/pi-provider-litellm/refs/heads/main/assets/pi_litellm_gallery.png"
41
41
  },
42
42
  "scripts": {
43
- "clean": "shx rm -rf dist",
43
+ "clean": "rm -rf dist",
44
44
  "build": "tsgo -p tsconfig.build.json",
45
45
  "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput",
46
46
  "typecheck": "tsgo --noEmit",
@@ -68,10 +68,8 @@
68
68
  "@earendil-works/pi-coding-agent": "^0.79.1",
69
69
  "@biomejs/biome": "^2.4.15",
70
70
  "@types/node": "^25.9.1",
71
- "@typescript/native-preview": "7.0.0-dev.20260610.1",
72
- "shx": "^0.4.0",
71
+ "@typescript/native-preview": "7.0.0-dev.20260617.2",
73
72
  "tsx": "^4.21.0",
74
- "typescript": "^6.0.3",
75
73
  "vitest": "^4.1.7"
76
74
  }
77
75
  }