pi-provider-litellm 2.0.5 → 2.1.0

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
@@ -46,17 +46,17 @@ You'll be prompted for the base URL and API key. Credentials are persisted to `~
46
46
 
47
47
  #### Enterprise SSO login
48
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:
49
+ If your LiteLLM proxy requires SSO/OAuth authentication (enterprise deployments), you can authenticate through LiteLLM's CLI SSO flow:
50
50
 
51
51
  1. Run `/login litellm` inside pi and select `Sign in with LiteLLM SSO`
52
52
  2. Enter the proxy URL
53
- 3. 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
54
- 4. 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)
55
- 5. When prompted to generate a virtual key, press Enter to accept (recommended) or enter `n` to use the JWT directly
53
+ 3. Pi displays a verification code and opens the LiteLLM SSO login URL automatically
54
+ 4. Authenticate in the browser and confirm the displayed code
55
+ 5. If your account belongs to multiple teams, select the team for this session in Pi
56
56
 
57
- 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.
57
+ Pi polls the proxy until authentication completes and stores the returned short-lived CLI token. Newer proxies advertise the token lifetime; for older proxies, Pi uses `LITELLM_CLI_JWT_EXPIRATION_HOURS` or LiteLLM's 24-hour default. On LiteLLM versions that gate CLI SSO, start the proxy with `EXPERIMENTAL_UI_LOGIN=True`.
58
58
 
59
- 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.
59
+ Older proxies without `/sso/cli/start` fall back to the legacy browser flow: copy the token from the LiteLLM UI, paste it into Pi, and optionally exchange it for a virtual key. Pi reads JWT expiry claims and prompts you to run `/login litellm` again when re-authentication is required.
60
60
 
61
61
  ### Option B — environment variables
62
62
 
@@ -137,6 +137,8 @@ LiteLLM Skills and MCP integration are enabled by default. Disable either featur
137
137
 
138
138
  Setting `skills.enabled` to `false` disables the Skills Gateway management tools, skill fetching, and system-prompt injection. Setting `mcp.enabled` to `false` disables LiteLLM MCP discovery and tool registration. Restart Pi after changing these settings so previously registered tools are removed.
139
139
 
140
+ Treat the configured LiteLLM proxy as trusted: Skills can add instructions to the system prompt, and MCP can expose tools the agent may call. Disable these integrations when the proxy, its administrators, or its configured content are not fully trusted.
141
+
140
142
  ## Use
141
143
 
142
144
  ```
@@ -153,9 +155,12 @@ Setting `skills.enabled` to `false` disables the Skills Gateway management tools
153
155
  | `GOOGLE_APPLICATION_CREDENTIALS` | Google default ADC path | Optional path to an ADC JSON file used by `LITELLM_GCLOUD_TOKEN_AUTH`. If unset, the extension checks the default gcloud ADC locations. |
154
156
  | `LITELLM_OFFLINE` | unset | If `1`, disable all model and MCP discovery, including post-login discovery; use cached models only |
155
157
  | `LITELLM_DISCOVERY_TIMEOUT_MS` | `5000` | Background and explicit discovery fetch timeout in ms; `0` disables automatic discovery |
158
+ | `LITELLM_CLI_JWT_EXPIRATION_HOURS` | `24` | CLI SSO token lifetime fallback for older proxies whose poll response omits `expires_in`; mirror a non-default proxy setting locally |
156
159
  | `LITELLM_VERBOSE_DISCOVERY` | unset | If `1`, enable progress messages during model and MCP discovery (login, refresh, startup); discovery is silent by default |
157
160
  | `LITELLM_MODELS_DEV` | enabled | Set to `0` to disable models.dev metadata enrichment, including its cache and network request; `/v1/models` still uses Pi catalog metadata and defaults |
158
161
 
162
+ Only use a trusted `LITELLM_API_KEY_HELPER` or `!command`. Prefer an absolute executable path, keep secrets out of command arguments, and print only the token to stdout without logging it to stderr.
163
+
159
164
  `LITELLM_DISCOVERY_TIMEOUT_MS=0` disables automatic and explicit refresh model discovery. It does not replace the base URL or API key settings required to send requests when you are not using `/login litellm`.
160
165
 
161
166
  Models.dev metadata is cached in `litellm-models-dev.json` under the Pi agent directory for 28 days. Fresh data avoids the public request; stale data is used immediately while one background refresh updates the cache. Set `LITELLM_MODELS_DEV=0` when your LiteLLM metadata is authoritative and no external enrichment is wanted.
@@ -238,6 +243,8 @@ Opening `/model` refreshes configured provider catalogs in the background using
238
243
  | Discovery times out | Increase `LITELLM_DISCOVERY_TIMEOUT_MS` or set `LITELLM_OFFLINE=1` to fall back on cached models |
239
244
  | `401 Token expired` | Set `LITELLM_API_KEY_HELPER`. |
240
245
  | 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 |
246
+ | Enterprise SSO waits for token insertion | The proxy returned 404/405 for `/sso/cli/start`, so Pi used the legacy flow — upgrade LiteLLM or paste the UI token |
247
+ | Enterprise CLI SSO start/poll fails | Check the proxy logs and verify `/sso/cli/start` and `/sso/cli/poll/{login_id}` are reachable; only 404/405 falls back to legacy login |
241
248
  | 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 |
242
249
  | Enterprise SSO token prompt fails with "SSO token is required" | The token field was left empty — paste the token copied from the LiteLLM UI |
243
250
  | MCP tools not showing | Verify the proxy exposes `/mcp-rest/tools/list` and open `/model` after fixing the proxy |
package/dist/discover.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { isIP } from "node:net";
2
3
  import { getModels, getProviders } from "@earendil-works/pi-ai/compat";
3
4
  import { writeJsonAtomic } from "./cache.js";
4
5
  const DEFAULT_TIMEOUT_MS = 5000;
@@ -10,6 +11,12 @@ const MODELS_DEV_CACHE_TTL_MS = 28 * 24 * 60 * 60 * 1000;
10
11
  const modelsDevCaches = new Map();
11
12
  const modelsDevRefreshes = new Map();
12
13
  export function normalizeBaseUrl(input) {
14
+ const url = new URL(input);
15
+ const hostname = url.hostname.toLowerCase();
16
+ const loopback = hostname === "localhost" || hostname === "[::1]" || (isIP(hostname) === 4 && hostname.startsWith("127."));
17
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
18
+ throw new Error("LiteLLM base URL must use HTTPS except for loopback hosts");
19
+ }
13
20
  return input.replace(/\/+$/, "").replace(/\/v1\/?$/i, "");
14
21
  }
15
22
  const RESPONSES_MODE_PATTERN = /^responses?$/i;
@@ -355,6 +362,21 @@ function mapModelsDevMetadata(model) {
355
362
  }
356
363
  return metadata;
357
364
  }
365
+ function mapReasoningEfforts(info) {
366
+ const flags = [
367
+ ["off", "none", info.supports_none_reasoning_effort],
368
+ ["minimal", "minimal", info.supports_minimal_reasoning_effort],
369
+ ["low", "low", info.supports_low_reasoning_effort],
370
+ ["medium", "medium", info.supports_medium_reasoning_effort],
371
+ ["high", "high", info.supports_high_reasoning_effort],
372
+ ["xhigh", "xhigh", info.supports_xhigh_reasoning_effort],
373
+ ["max", "max", info.supports_max_reasoning_effort],
374
+ ];
375
+ const map = Object.fromEntries(flags
376
+ .filter(([, , supported]) => supported !== undefined)
377
+ .map(([level, value, supported]) => [level, supported ? value : null]));
378
+ return Object.keys(map).length > 0 ? map : undefined;
379
+ }
358
380
  function mapFromModelInfo(entry) {
359
381
  const id = entry.model_name;
360
382
  if (!id)
@@ -364,11 +386,15 @@ function mapFromModelInfo(entry) {
364
386
  return undefined;
365
387
  const responsesMode = isResponsesMode(info.mode);
366
388
  const catalogModel = findCatalogModel(id);
389
+ const reasoningEffortMap = mapReasoningEfforts(info);
390
+ const thinkingLevelMap = catalogModel?.thinkingLevelMap || reasoningEffortMap
391
+ ? { ...catalogModel?.thinkingLevelMap, ...reasoningEffortMap }
392
+ : undefined;
367
393
  return {
368
394
  id,
369
395
  name: id,
370
396
  reasoning: info.supports_reasoning ?? false,
371
- ...(catalogModel?.thinkingLevelMap ? { thinkingLevelMap: catalogModel.thinkingLevelMap } : {}),
397
+ ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
372
398
  input: info.supports_vision ? ["text", "image"] : ["text"],
373
399
  cost: mapModelInfoCost(info, catalogModel?.cost),
374
400
  contextWindow: info.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW,
@@ -85,7 +85,7 @@ async function exchangeRefreshToken(credentials) {
85
85
  signal: AbortSignal.timeout(10_000),
86
86
  });
87
87
  if (!response.ok) {
88
- console.warn(`LiteLLM gcloud auth: Token exchange failed (${response.status}): ${await response.text()}`);
88
+ console.warn(`LiteLLM gcloud auth: Token exchange failed (${response.status})`);
89
89
  return null;
90
90
  }
91
91
  const data = (await response.json());
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
+ import { setTimeout as delay } from "node:timers/promises";
4
5
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
6
  import { setupLiteLLMCostTracking } from "./cost.js";
6
7
  import { discoverModels, isGpt55Model, isMoonshotModel, normalizeBaseUrl, shouldSuppressReasoningContent, } from "./discover.js";
@@ -16,11 +17,15 @@ const ENV_API_KEY = "LITELLM_API_KEY";
16
17
  const ENV_API_KEY_HELPER = "LITELLM_API_KEY_HELPER";
17
18
  const ENV_HEADERS = "LITELLM_HEADERS";
18
19
  const ENV_TIMEOUT = "LITELLM_DISCOVERY_TIMEOUT_MS";
20
+ const ENV_CLI_JWT_EXPIRATION_HOURS = "LITELLM_CLI_JWT_EXPIRATION_HOURS";
19
21
  const ENV_OFFLINE = "LITELLM_OFFLINE";
20
22
  const ENV_VERBOSE_DISCOVERY = "LITELLM_VERBOSE_DISCOVERY";
21
23
  const ENV_MODELS_DEV = "LITELLM_MODELS_DEV";
22
24
  const DEFAULT_TIMEOUT_MS = 5000;
23
25
  const LOGIN_TIMEOUT_MS = 10_000;
26
+ const CLI_SSO_POLL_INTERVAL_MS = 2_000;
27
+ const CLI_SSO_EXPIRES_IN_SECONDS = 600;
28
+ const DEFAULT_CLI_JWT_EXPIRATION_HOURS = 24;
24
29
  const MODELS_DEV_CACHE_FILENAME = "litellm-models-dev.json";
25
30
  const TOKEN_REFRESH_LEAD_MS = 5 * 60 * 1000;
26
31
  const PERMANENT_TOKEN_EXPIRES_AT = Number.MAX_SAFE_INTEGER;
@@ -125,10 +130,17 @@ function tokenExpiresAt(apiKey, opaqueFallback = PERMANENT_TOKEN_EXPIRES_AT) {
125
130
  return opaqueFallback;
126
131
  }
127
132
  }
128
- async function generateVirtualKey(baseUrl, userToken, signal, headers) {
129
- const boundedSignal = signal
133
+ function configuredCliJwtExpiresAt() {
134
+ const configured = Number(process.env[ENV_CLI_JWT_EXPIRATION_HOURS]);
135
+ const hours = Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_CLI_JWT_EXPIRATION_HOURS;
136
+ return Date.now() + hours * 60 * 60 * 1_000;
137
+ }
138
+ function boundedLoginSignal(signal) {
139
+ return signal
130
140
  ? AbortSignal.any([signal, AbortSignal.timeout(LOGIN_TIMEOUT_MS)])
131
141
  : AbortSignal.timeout(LOGIN_TIMEOUT_MS);
142
+ }
143
+ async function generateVirtualKey(baseUrl, userToken, signal, headers) {
132
144
  const response = await fetch(`${baseUrl}/key/generate`, {
133
145
  method: "POST",
134
146
  headers: {
@@ -137,11 +149,10 @@ async function generateVirtualKey(baseUrl, userToken, signal, headers) {
137
149
  "Content-Type": "application/json",
138
150
  },
139
151
  body: JSON.stringify({}),
140
- signal: boundedSignal,
152
+ signal: boundedLoginSignal(signal),
141
153
  });
142
154
  if (!response.ok) {
143
- const text = await response.text().catch(() => "");
144
- throw new Error(`Virtual key generation failed (${response.status}): ${text}`);
155
+ throw new Error(`Virtual key generation failed (${response.status})`);
145
156
  }
146
157
  const data = (await response.json());
147
158
  if (typeof data.key !== "string" || !data.key)
@@ -401,15 +412,108 @@ async function loginApiKey(interaction) {
401
412
  throw new Error("Both base URL and API key are required");
402
413
  return { type: "api_key", key, env: { [ENV_BASE_URL]: normalizeBaseUrl(rawBaseUrl) } };
403
414
  }
404
- async function loginOAuth(interaction, headers) {
405
- const rawBaseUrl = (await interaction.prompt({
406
- type: "text",
407
- message: "Enter LiteLLM proxy URL (no trailing /v1):",
408
- placeholder: "https://litellm.example.com",
409
- })).trim();
410
- if (!rawBaseUrl)
411
- throw new Error("Base URL is required");
412
- const baseUrl = normalizeBaseUrl(rawBaseUrl);
415
+ async function startCliSso(baseUrl, signal, headers) {
416
+ const response = await fetch(`${baseUrl}/sso/cli/start`, {
417
+ method: "POST",
418
+ headers,
419
+ signal: boundedLoginSignal(signal),
420
+ });
421
+ if (response.status === 404 || response.status === 405)
422
+ return undefined;
423
+ if (!response.ok)
424
+ throw new Error(`LiteLLM CLI SSO start failed (HTTP ${response.status})`);
425
+ const data = (await response.json());
426
+ if (typeof data.login_id !== "string" ||
427
+ !data.login_id ||
428
+ typeof data.poll_secret !== "string" ||
429
+ !data.poll_secret ||
430
+ typeof data.user_code !== "string" ||
431
+ !data.user_code) {
432
+ throw new Error("LiteLLM CLI SSO start returned an invalid response");
433
+ }
434
+ return {
435
+ loginId: data.login_id,
436
+ pollSecret: data.poll_secret,
437
+ userCode: data.user_code,
438
+ expiresInSeconds: typeof data.expires_in === "number" && Number.isFinite(data.expires_in) && data.expires_in > 0
439
+ ? data.expires_in
440
+ : CLI_SSO_EXPIRES_IN_SECONDS,
441
+ };
442
+ }
443
+ async function waitForNextCliSsoPoll(interaction) {
444
+ try {
445
+ await delay(CLI_SSO_POLL_INTERVAL_MS, undefined, interaction.signal ? { signal: interaction.signal } : undefined);
446
+ }
447
+ catch (error) {
448
+ if (interaction.signal?.aborted)
449
+ throw interaction.signal.reason;
450
+ throw error;
451
+ }
452
+ }
453
+ async function pollCliSso(baseUrl, start, interaction, headers) {
454
+ const deadline = Date.now() + start.expiresInSeconds * 1_000;
455
+ const pollUrl = new URL(`${baseUrl}/sso/cli/poll/${encodeURIComponent(start.loginId)}`);
456
+ let teamId;
457
+ while (Date.now() < deadline) {
458
+ if (teamId)
459
+ pollUrl.searchParams.set("team_id", teamId);
460
+ let response;
461
+ try {
462
+ response = await fetch(pollUrl, {
463
+ headers: { ...headers, "x-litellm-cli-poll-secret": start.pollSecret },
464
+ signal: boundedLoginSignal(interaction.signal),
465
+ });
466
+ }
467
+ catch {
468
+ if (interaction.signal?.aborted)
469
+ throw interaction.signal.reason;
470
+ await waitForNextCliSsoPoll(interaction);
471
+ continue;
472
+ }
473
+ if (response.status === 429 || response.status >= 500) {
474
+ await waitForNextCliSsoPoll(interaction);
475
+ continue;
476
+ }
477
+ if (response.status === 400)
478
+ throw new Error("LiteLLM CLI SSO login expired or is invalid");
479
+ if (!response.ok)
480
+ throw new Error(`LiteLLM CLI SSO polling failed (HTTP ${response.status})`);
481
+ const data = (await response.json());
482
+ if (data.status === "ready" && typeof data.key === "string" && data.key) {
483
+ return {
484
+ access: data.key,
485
+ expiresInSeconds: typeof data.expires_in === "number" && Number.isFinite(data.expires_in) && data.expires_in > 0
486
+ ? data.expires_in
487
+ : undefined,
488
+ };
489
+ }
490
+ if (data.status === "ready" && data.requires_team_selection === true && !teamId) {
491
+ const details = Array.isArray(data.team_details) ? data.team_details : [];
492
+ const teams = details.flatMap((team) => {
493
+ if (!team || typeof team !== "object")
494
+ return [];
495
+ const record = team;
496
+ const id = typeof record.team_id === "string" ? record.team_id : record.id;
497
+ const alias = record.team_alias;
498
+ return typeof id === "string" && id ? [{ id, label: typeof alias === "string" && alias ? alias : id }] : [];
499
+ });
500
+ if (teams.length === 0 && Array.isArray(data.teams))
501
+ teams.push(...data.teams.flatMap((id) => (typeof id === "string" && id ? [{ id, label: id }] : [])));
502
+ if (teams.length === 0)
503
+ throw new Error("LiteLLM CLI SSO requested team selection without any teams");
504
+ const selected = await interaction.prompt({ type: "select", message: "Select a LiteLLM team:", options: teams });
505
+ if (!teams.some((team) => team.id === selected))
506
+ throw new Error("Invalid LiteLLM team selection");
507
+ teamId = selected;
508
+ continue;
509
+ }
510
+ if (data.status !== "pending")
511
+ throw new Error("LiteLLM CLI SSO polling returned an invalid response");
512
+ await waitForNextCliSsoPoll(interaction);
513
+ }
514
+ throw new Error("LiteLLM CLI SSO login expired");
515
+ }
516
+ async function loginWithPastedToken(interaction, baseUrl, headers) {
413
517
  interaction.notify({
414
518
  type: "auth_url",
415
519
  url: `${baseUrl}/sso/key/generate`,
@@ -449,6 +553,35 @@ async function loginOAuth(interaction, headers) {
449
553
  }
450
554
  return { type: "oauth", access, refresh: "", expires, baseUrl };
451
555
  }
556
+ async function loginOAuth(interaction, headers) {
557
+ const rawBaseUrl = (await interaction.prompt({
558
+ type: "text",
559
+ message: "Enter LiteLLM proxy URL (no trailing /v1):",
560
+ placeholder: "https://litellm.example.com",
561
+ })).trim();
562
+ if (!rawBaseUrl)
563
+ throw new Error("Base URL is required");
564
+ const baseUrl = normalizeBaseUrl(rawBaseUrl);
565
+ const cliSso = await startCliSso(baseUrl, interaction.signal, headers);
566
+ if (!cliSso)
567
+ return loginWithPastedToken(interaction, baseUrl, headers);
568
+ interaction.notify({
569
+ type: "device_code",
570
+ userCode: cliSso.userCode,
571
+ verificationUri: `${baseUrl}/sso/key/generate?${new URLSearchParams({ source: "litellm-cli", key: cliSso.loginId })}`,
572
+ expiresInSeconds: cliSso.expiresInSeconds,
573
+ });
574
+ const result = await pollCliSso(baseUrl, cliSso, interaction, headers);
575
+ return {
576
+ type: "oauth",
577
+ access: result.access,
578
+ refresh: "",
579
+ expires: result.expiresInSeconds
580
+ ? Date.now() + result.expiresInSeconds * 1_000
581
+ : tokenExpiresAt(result.access, configuredCliJwtExpiresAt()),
582
+ baseUrl,
583
+ };
584
+ }
452
585
  async function refreshLiteLLM(credentials, _signal) {
453
586
  if (!credentials.refresh.startsWith("!")) {
454
587
  if (credentials.expires < PERMANENT_TOKEN_EXPIRES_AT) {
@@ -948,11 +1081,7 @@ export default async function (pi) {
948
1081
  context.credential) {
949
1082
  const auth = await authForCredential(definition, context.credential);
950
1083
  defaultRuntimeAuth = auth;
951
- const registration = registerMcpTools(auth, context.signal);
952
- if (context.signal)
953
- await registration;
954
- else
955
- void registration.catch(() => undefined);
1084
+ void registerMcpTools(auth, context.signal).catch(() => undefined);
956
1085
  }
957
1086
  }
958
1087
  };
package/dist/provider.js CHANGED
@@ -34,14 +34,7 @@ export function createLiteLLMProvider(options) {
34
34
  ...provider,
35
35
  refreshModels: (context) => refreshModels({
36
36
  ...context,
37
- store: {
38
- async read() {
39
- const entry = await context.store.read();
40
- return entry && { ...entry, models: entry.models.map(enrichCachedModel) };
41
- },
42
- write: (entry) => context.store.write(entry),
43
- delete: () => context.store.delete(),
44
- },
37
+ stored: context.stored && { ...context.stored, models: context.stored.models.map(enrichCachedModel) },
45
38
  }),
46
39
  };
47
40
  }
package/dist/types.d.ts CHANGED
@@ -31,6 +31,13 @@ export interface ModelInfoEntry {
31
31
  max_input_tokens?: number;
32
32
  max_output_tokens?: number;
33
33
  supports_reasoning?: boolean;
34
+ supports_none_reasoning_effort?: boolean;
35
+ supports_minimal_reasoning_effort?: boolean;
36
+ supports_low_reasoning_effort?: boolean;
37
+ supports_medium_reasoning_effort?: boolean;
38
+ supports_high_reasoning_effort?: boolean;
39
+ supports_xhigh_reasoning_effort?: boolean;
40
+ supports_max_reasoning_effort?: boolean;
34
41
  supports_vision?: boolean;
35
42
  };
36
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-litellm",
3
- "version": "2.0.5",
3
+ "version": "2.1.0",
4
4
  "description": "LiteLLM proxy provider extension for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -68,13 +68,14 @@
68
68
  },
69
69
  "overrides": {
70
70
  "basic-ftp": "6.0.1",
71
+ "nanoid": "3.3.18",
71
72
  "protobufjs": "8.7.1",
72
73
  "undici": "8.9.0"
73
74
  },
74
75
  "devDependencies": {
75
76
  "@biomejs/biome": "^2.4.15",
76
- "@earendil-works/pi-ai": "^0.83.0",
77
- "@earendil-works/pi-coding-agent": "^0.83.0",
77
+ "@earendil-works/pi-ai": "^0.84.2",
78
+ "@earendil-works/pi-coding-agent": "^0.84.2",
78
79
  "@kitlangton/terminal-control": "0.6.0",
79
80
  "@types/node": "^26.0.0",
80
81
  "@typescript/native-preview": "7.0.0-dev.20260707.2",