@pi-unipi/unipi 2.3.0 → 2.4.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.
@@ -36307,11 +36307,25 @@ function listRegistryImageGenModels(registry3) {
36307
36307
  provider: candidate.provider,
36308
36308
  name: candidate.name,
36309
36309
  api: candidate.api ?? "",
36310
+ // Carry the endpoint through. The generic images adapter POSTs to
36311
+ // `{baseUrl}/images/generations`, and this is the only place the
36312
+ // registry's baseUrl is available — dropping it here surfaces later as
36313
+ // "No baseUrl for image model ..." once generation is attempted.
36314
+ ...candidate.baseUrl ? { baseUrl: candidate.baseUrl } : {},
36310
36315
  ...candidate.output ? { output: candidate.output } : {}
36311
36316
  });
36312
36317
  }
36313
36318
  return out;
36314
36319
  }
36320
+ function findProviderBaseUrl(registry3, provider) {
36321
+ if (!registry3) return void 0;
36322
+ try {
36323
+ const models = registry3.getAvailable?.() ?? registry3.getAll();
36324
+ return models.find((m) => m?.provider === provider && m.baseUrl)?.baseUrl;
36325
+ } catch {
36326
+ return void 0;
36327
+ }
36328
+ }
36315
36329
  async function listAllImageGenModels(registry3) {
36316
36330
  const builtin = await listImageGenModels();
36317
36331
  const fromRegistry = registry3 ? listRegistryImageGenModels(registry3) : [];
@@ -36745,6 +36759,240 @@ var ImageModelSelectorOverlay = class {
36745
36759
  }
36746
36760
  };
36747
36761
 
36762
+ // packages/image/src/openai-images-api.ts
36763
+ var openai_images_api_exports = {};
36764
+ __export(openai_images_api_exports, {
36765
+ generateImages: () => generateImages,
36766
+ normalizeImageItem: () => normalizeImageItem,
36767
+ normalizeModelId: () => normalizeModelId
36768
+ });
36769
+ var DEFAULT_TIMEOUT_MS2 = 24e4;
36770
+ function asString(value) {
36771
+ return typeof value === "string" && value.length > 0 ? value : void 0;
36772
+ }
36773
+ function normalizeImageItem(item) {
36774
+ const declared = asString(item.media_type) ?? asString(item.mime_type) ?? asString(item.mimeType);
36775
+ const b64 = asString(item.b64_json);
36776
+ if (b64) return { data: b64, mimeType: declared ?? "image/png" };
36777
+ const url = asString(item.url);
36778
+ if (!url) return null;
36779
+ const dataUrl = /^data:([^;,]+)(?:;[^,]*)*,(.*)$/s.exec(url);
36780
+ if (dataUrl) {
36781
+ const [, mime, payload] = dataUrl;
36782
+ if (payload) return { data: payload, mimeType: declared ?? mime ?? "image/png" };
36783
+ return null;
36784
+ }
36785
+ return { text: `Image available at: ${url}` };
36786
+ }
36787
+ function joinUrl(baseUrl, suffix) {
36788
+ return `${baseUrl.replace(/\/+$/, "")}/${suffix.replace(/^\/+/, "")}`;
36789
+ }
36790
+ function normalizeModelId(id) {
36791
+ return id;
36792
+ }
36793
+ function buildHeaders(apiKey, extra) {
36794
+ const headers = {
36795
+ Authorization: `Bearer ${apiKey}`,
36796
+ "Content-Type": "application/json"
36797
+ };
36798
+ for (const [key, value] of Object.entries(extra ?? {})) {
36799
+ if (value === null) delete headers[key];
36800
+ else headers[key] = value;
36801
+ }
36802
+ return headers;
36803
+ }
36804
+ function describeError(status2, statusText, body) {
36805
+ let detail = body.slice(0, 300);
36806
+ try {
36807
+ const parsed = JSON.parse(body);
36808
+ if (typeof parsed.error === "string") detail = parsed.error;
36809
+ else if (parsed.error?.message) detail = parsed.error.message;
36810
+ } catch {
36811
+ }
36812
+ return `${status2} ${statusText}${detail ? `: ${detail}` : ""}`;
36813
+ }
36814
+ function annotateModelError(message, model) {
36815
+ if (/invalid image model|not found|unknown model/i.test(message)) {
36816
+ return `${message}
36817
+ \u2192 Model id sent: "${model.id}" (provider "${model.provider}").
36818
+ \u2192 Some gateways list aliases they cannot serve. Try the id exactly as it appears in the provider's own catalog, or pick another with /unipi:image-settings.`;
36819
+ }
36820
+ return message;
36821
+ }
36822
+ async function generateImages(model, context, options) {
36823
+ const result = {
36824
+ api: model.api || "openai-images",
36825
+ provider: model.provider,
36826
+ model: model.id,
36827
+ output: [],
36828
+ stopReason: "stop",
36829
+ timestamp: Date.now()
36830
+ };
36831
+ const fetchImpl = options?.fetchImpl ?? fetch;
36832
+ const controller = new AbortController();
36833
+ const timer = setTimeout(
36834
+ () => controller.abort(),
36835
+ options?.timeoutMs ?? DEFAULT_TIMEOUT_MS2
36836
+ );
36837
+ const onAbort = () => controller.abort();
36838
+ options?.signal?.addEventListener("abort", onAbort, { once: true });
36839
+ try {
36840
+ const apiKey = options?.apiKey;
36841
+ if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);
36842
+ if (!model.baseUrl) {
36843
+ throw new Error(`No baseUrl for image model ${model.provider}/${model.id}`);
36844
+ }
36845
+ const prompt = context.input.filter((part) => part.type === "text" && part.text).map((part) => part.text).join("\n").trim();
36846
+ const images = context.input.filter((part) => part.type === "image" && part.data).map((part) => `data:${part.mimeType || "image/png"};base64,${part.data}`);
36847
+ if (!prompt) throw new Error("A non-empty prompt is required.");
36848
+ const body = {
36849
+ model: normalizeModelId(model.id),
36850
+ prompt
36851
+ };
36852
+ if (images.length > 0) body.image = images;
36853
+ const response = await fetchImpl(joinUrl(model.baseUrl, "images/generations"), {
36854
+ method: "POST",
36855
+ headers: buildHeaders(apiKey, options?.headers),
36856
+ body: JSON.stringify(body),
36857
+ signal: controller.signal
36858
+ });
36859
+ if (!response.ok) {
36860
+ throw new Error(
36861
+ describeError(response.status, response.statusText, await response.text())
36862
+ );
36863
+ }
36864
+ const payload = await response.json();
36865
+ if (payload.error?.message) throw new Error(payload.error.message);
36866
+ for (const item of payload.data ?? []) {
36867
+ const normalized = normalizeImageItem(item);
36868
+ if (!normalized) continue;
36869
+ if ("text" in normalized) {
36870
+ result.output.push({ type: "text", text: normalized.text });
36871
+ } else {
36872
+ result.output.push({
36873
+ type: "image",
36874
+ data: normalized.data,
36875
+ mimeType: normalized.mimeType
36876
+ });
36877
+ }
36878
+ const revised = asString(item.revised_prompt);
36879
+ if (revised && revised !== prompt) {
36880
+ result.output.push({ type: "text", text: `Revised prompt: ${revised}` });
36881
+ }
36882
+ }
36883
+ if (result.output.every((part) => part.type !== "image")) {
36884
+ throw new Error("The provider returned no image data.");
36885
+ }
36886
+ return result;
36887
+ } catch (error) {
36888
+ const aborted = options?.signal?.aborted || controller.signal.aborted;
36889
+ result.stopReason = options?.signal?.aborted ? "aborted" : "error";
36890
+ result.errorMessage = aborted && !options?.signal?.aborted ? "Image request timed out." : annotateModelError(
36891
+ error instanceof Error ? error.message : String(error),
36892
+ model
36893
+ );
36894
+ return result;
36895
+ } finally {
36896
+ clearTimeout(timer);
36897
+ options?.signal?.removeEventListener("abort", onAbort);
36898
+ }
36899
+ }
36900
+
36901
+ // packages/image/src/register-providers.ts
36902
+ var registered = false;
36903
+ function groupModelsByProvider(models, baseUrlFor) {
36904
+ const grouped = /* @__PURE__ */ new Map();
36905
+ for (const model of models) {
36906
+ const baseUrl = model.baseUrl ?? baseUrlFor(model.provider);
36907
+ if (!baseUrl) continue;
36908
+ let entry = grouped.get(model.provider);
36909
+ if (!entry) {
36910
+ entry = { baseUrl, models: [] };
36911
+ grouped.set(model.provider, entry);
36912
+ }
36913
+ entry.models.push({ ...model, baseUrl });
36914
+ }
36915
+ return grouped;
36916
+ }
36917
+ function providerBaseUrlLookup(registry3) {
36918
+ const cache = /* @__PURE__ */ new Map();
36919
+ return (provider) => {
36920
+ if (cache.has(provider)) return cache.get(provider);
36921
+ let baseUrl;
36922
+ try {
36923
+ const models = registry3.getAvailable?.() ?? registry3.getAll();
36924
+ baseUrl = models.find((m) => m?.provider === provider && m.baseUrl)?.baseUrl;
36925
+ } catch {
36926
+ baseUrl = void 0;
36927
+ }
36928
+ cache.set(provider, baseUrl);
36929
+ return baseUrl;
36930
+ };
36931
+ }
36932
+ async function registerRegistryImageProviders(registry3, options) {
36933
+ if (!registry3) return [];
36934
+ if (registered && !options?.force) return [];
36935
+ const images = await getImagesModels();
36936
+ if (!images) return [];
36937
+ const mutable = images;
36938
+ if (typeof mutable.setProvider !== "function") return [];
36939
+ let createImagesProvider;
36940
+ try {
36941
+ const mod = await import("@earendil-works/pi-ai");
36942
+ if (typeof mod.createImagesProvider !== "function") return [];
36943
+ createImagesProvider = mod.createImagesProvider;
36944
+ } catch {
36945
+ return [];
36946
+ }
36947
+ const discovered = listRegistryImageGenModels(registry3);
36948
+ if (discovered.length === 0) {
36949
+ registered = true;
36950
+ return [];
36951
+ }
36952
+ const grouped = groupModelsByProvider(discovered, providerBaseUrlLookup(registry3));
36953
+ const added = [];
36954
+ for (const [providerId, { models }] of grouped) {
36955
+ try {
36956
+ if (mutable.getProvider?.(providerId)) continue;
36957
+ } catch {
36958
+ }
36959
+ try {
36960
+ const provider = createImagesProvider({
36961
+ id: providerId,
36962
+ name: providerId,
36963
+ models,
36964
+ api: openai_images_api_exports,
36965
+ auth: {
36966
+ apiKey: {
36967
+ name: `${providerId} API key`,
36968
+ // Resolve through pi's own auth storage so the user never logs in
36969
+ // twice. `resolve` MUST return an AuthResult (`{ auth: {...} }`);
36970
+ // returning a bare key fails silently at request time.
36971
+ resolve: async () => {
36972
+ const key = await resolveProviderKey(registry3, providerId);
36973
+ return key ? { auth: { apiKey: key }, source: `pi:${providerId}` } : void 0;
36974
+ }
36975
+ }
36976
+ }
36977
+ });
36978
+ mutable.setProvider(provider);
36979
+ added.push(providerId);
36980
+ } catch {
36981
+ }
36982
+ }
36983
+ registered = true;
36984
+ return added;
36985
+ }
36986
+ async function resolveProviderKey(registry3, provider) {
36987
+ try {
36988
+ const key = await registry3.getApiKeyForProvider?.(provider);
36989
+ if (key) return key;
36990
+ } catch {
36991
+ }
36992
+ const envName = `${provider.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
36993
+ return process.env[envName] || void 0;
36994
+ }
36995
+
36748
36996
  // packages/image/src/tui/settings-dialog.ts
36749
36997
  var EXIT = "__exit__";
36750
36998
  function describe(config) {
@@ -36911,6 +37159,7 @@ async function pickModel(ctx, kind, config) {
36911
37159
  async function collectModels(ctx, kind) {
36912
37160
  const registry3 = ctx.modelRegistry;
36913
37161
  if (kind === "generate") {
37162
+ await registerRegistryImageProviders(registry3);
36914
37163
  const models = await listAllImageGenModels(registry3);
36915
37164
  const generating = await getGeneratingProviders();
36916
37165
  return models.map((m) => ({
@@ -36920,7 +37169,7 @@ async function collectModels(ctx, kind) {
36920
37169
  // pi-ai's images collection has its own provider set. A chat provider's
36921
37170
  // image models are listed for reference but cannot actually generate,
36922
37171
  // so flag them rather than letting the user pick a dead option.
36923
- unavailable: generating.length > 0 && !generating.includes(m.provider) ? "cannot generate" : void 0
37172
+ unavailable: generating.length > 0 && !generating.includes(m.provider) ? "no image route" : void 0
36924
37173
  }));
36925
37174
  }
36926
37175
  if (!registry3) return [];
@@ -36996,7 +37245,7 @@ function providerCanGenerate(imagesApi, provider) {
36996
37245
  return providers.includes(provider);
36997
37246
  }
36998
37247
  async function generateImage(options) {
36999
- const { prompt, model, signal, outputDir, now: now2 } = options;
37248
+ const { prompt, model, signal, outputDir, now: now2, inputImage } = options;
37000
37249
  if (!prompt.trim()) {
37001
37250
  throw new Error("A non-empty prompt is required.");
37002
37251
  }
@@ -37009,28 +37258,37 @@ async function generateImage(options) {
37009
37258
  if (!providerCanGenerate(imagesApi, model.provider)) {
37010
37259
  const supported = supportedProviders(imagesApi);
37011
37260
  throw new Error(
37012
- `Provider "${model.provider}" cannot generate images.
37013
- \u2192 Image generation is served by: ${supported.join(", ") || "openrouter"}.
37014
- \u2192 "${model.provider}" is a chat provider; its image models are listed for recognition and reference, but generation must go through a supported provider.
37015
- \u2192 Pick one with /unipi:image-settings.`
37261
+ `Provider "${model.provider}" has no image-generation route.
37262
+ \u2192 Available: ${supported.join(", ") || "openrouter"}.
37263
+ \u2192 Providers are bridged from pi automatically; one without a baseUrl or an API key cannot be used.
37264
+ \u2192 Pick another with /unipi:image-settings.`
37016
37265
  );
37017
37266
  }
37018
37267
  let apiKey;
37019
37268
  try {
37020
- apiKey = (await imagesApi.getAuth(model))?.apiKey;
37269
+ const resolvedAuth = await imagesApi.getAuth(model);
37270
+ apiKey = resolvedAuth?.auth?.apiKey ?? resolvedAuth?.apiKey;
37021
37271
  } catch {
37022
37272
  }
37023
37273
  apiKey ||= options.apiKey;
37024
37274
  if (!apiKey) {
37025
37275
  throw new Error(
37026
37276
  `No API key for provider "${model.provider}".
37027
- \u2192 Add one with /login, or set the provider's API key environment variable.
37028
- \u2192 Image models are served through OpenRouter: https://openrouter.ai/keys`
37277
+ \u2192 Sign in with /login, or set the provider's API key environment variable.
37278
+ \u2192 Expected environment variable: ${model.provider.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`
37029
37279
  );
37030
37280
  }
37281
+ const input = [{ type: "text", text: prompt }];
37282
+ if (inputImage) {
37283
+ input.push({
37284
+ type: "image",
37285
+ data: inputImage.data,
37286
+ mimeType: inputImage.mimeType
37287
+ });
37288
+ }
37031
37289
  const result = await imagesApi.generateImages(
37032
37290
  model,
37033
- { input: [{ type: "text", text: prompt }] },
37291
+ { input },
37034
37292
  { apiKey, ...signal ? { signal } : {} }
37035
37293
  );
37036
37294
  if (result.stopReason === "error") {
@@ -37194,7 +37452,7 @@ function truncate(value, max = 60) {
37194
37452
  }
37195
37453
 
37196
37454
  // packages/image/src/recognize.ts
37197
- var DEFAULT_TIMEOUT_MS2 = 12e4;
37455
+ var DEFAULT_TIMEOUT_MS3 = 12e4;
37198
37456
  var DEFAULT_MAX_TOKENS = 2048;
37199
37457
  function createSignal(timeoutMs, external) {
37200
37458
  const controller = new AbortController();
@@ -37233,7 +37491,7 @@ async function callAnthropic2(options) {
37233
37491
  apiKey,
37234
37492
  baseUrl,
37235
37493
  modelId,
37236
- timeoutMs = DEFAULT_TIMEOUT_MS2,
37494
+ timeoutMs = DEFAULT_TIMEOUT_MS3,
37237
37495
  maxTokens = DEFAULT_MAX_TOKENS,
37238
37496
  signal: external,
37239
37497
  fetchImpl = fetch
@@ -37340,7 +37598,7 @@ async function callOpenAICompatible2(options) {
37340
37598
  apiKey,
37341
37599
  baseUrl,
37342
37600
  modelId,
37343
- timeoutMs = DEFAULT_TIMEOUT_MS2,
37601
+ timeoutMs = DEFAULT_TIMEOUT_MS3,
37344
37602
  maxTokens = DEFAULT_MAX_TOKENS,
37345
37603
  signal: external,
37346
37604
  fetchImpl = fetch
@@ -37447,11 +37705,14 @@ function registerGenerateTool(pi) {
37447
37705
  pi.registerTool({
37448
37706
  name: IMAGE_TOOLS.GENERATE,
37449
37707
  label: "Generate Image",
37450
- description: "Generate an image from a text prompt using an image model. The image is returned inline and, when enabled, saved to disk.",
37708
+ description: "Generate an image from a text prompt, or edit an existing image by passing `image`. The result is returned inline and, when enabled, saved to disk.",
37451
37709
  promptSnippet: "Generate an image from a text prompt.",
37452
37710
  promptGuidelines: [
37453
37711
  "Use image_generate to create images from a text description.",
37454
37712
  "Write a detailed prompt \u2014 subject, style, composition and lighting all help.",
37713
+ "Pass `image` to edit an existing image instead of generating a new one.",
37714
+ "Editing regenerates the whole image, so unmentioned details may change.",
37715
+ "Describe what you DO want; negation is unreliable in image models.",
37455
37716
  "Omit model to use the one configured in /unipi:image-settings.",
37456
37717
  "Generated images cost money per call; do not regenerate without being asked."
37457
37718
  ],
@@ -37459,6 +37720,11 @@ function registerGenerateTool(pi) {
37459
37720
  prompt: Type9.String({
37460
37721
  description: "Description of the image to generate. Be specific."
37461
37722
  }),
37723
+ image: Type9.Optional(
37724
+ Type9.String({
37725
+ description: "Source image to edit: a local file path, data: URL, or base64 data. When set, the model edits this image instead of generating from scratch."
37726
+ })
37727
+ ),
37462
37728
  model: Type9.Optional(
37463
37729
  Type9.String({
37464
37730
  description: 'Image model override, e.g. "flux.2-pro" or "openrouter/google/gemini-3-pro-image". Omit to use the configured default.'
@@ -37469,21 +37735,26 @@ function registerGenerateTool(pi) {
37469
37735
  try {
37470
37736
  const config = loadConfig7();
37471
37737
  const registry3 = getRegistry2(ctx);
37738
+ await registerRegistryImageProviders(registry3);
37472
37739
  const models = await listAllImageGenModels(registry3);
37473
37740
  const requested = params.model?.trim() || config.generate.model;
37474
- const resolved = resolveImageGenModel(requested, models);
37475
- if (typeof resolved === "string") return errorResult(resolved);
37741
+ const maybeResolved = resolveImageGenModel(requested, models);
37742
+ if (typeof maybeResolved === "string") return errorResult(maybeResolved);
37743
+ const registryBaseUrl = maybeResolved.baseUrl ? void 0 : findProviderBaseUrl(registry3, maybeResolved.provider);
37744
+ const resolved = registryBaseUrl ? { ...maybeResolved, baseUrl: registryBaseUrl } : maybeResolved;
37476
37745
  const fallbackKey = await resolveApiKey(registry3, resolved.provider);
37746
+ const sourceImage = params.image?.trim() ? loadImage(params.image, ctx.cwd ?? process.cwd()) : void 0;
37477
37747
  const result = await generateImage({
37478
37748
  prompt: params.prompt,
37479
37749
  model: resolved,
37480
37750
  ...fallbackKey ? { apiKey: fallbackKey } : {},
37751
+ ...sourceImage ? { inputImage: sourceImage } : {},
37481
37752
  signal,
37482
37753
  outputDir: config.generate.saveToDisk ? getOutputDir(config) : void 0
37483
37754
  });
37484
37755
  const saved = result.images.map((image) => image.path).filter((path28) => Boolean(path28));
37485
37756
  const summary = [
37486
- `Generated ${result.images.length} image${result.images.length === 1 ? "" : "s"} with ${formatModelRef(resolved)}.`,
37757
+ `${sourceImage ? "Edited" : "Generated"} ${result.images.length} image${result.images.length === 1 ? "" : "s"} with ${formatModelRef(resolved)}.`,
37487
37758
  saved.length > 0 ? `Saved to:
37488
37759
  ${saved.map((p) => ` ${p}`).join("\n")}` : "",
37489
37760
  config.generate.saveToDisk && saved.length === 0 ? "Could not write to the output directory \u2014 returning the image inline only." : "",
@@ -37625,6 +37896,9 @@ function src_default5(pi) {
37625
37896
  registerImageCommands(pi);
37626
37897
  pi.on("session_start", async (_event, ctx) => {
37627
37898
  const config = loadConfig7();
37899
+ void registerRegistryImageProviders(
37900
+ ctx.modelRegistry
37901
+ ).catch(() => void 0);
37628
37902
  const tools = [];
37629
37903
  if (config.generate.enabled) tools.push(IMAGE_TOOLS.GENERATE);
37630
37904
  if (config.recognize.enabled) tools.push(IMAGE_TOOLS.RECOGNIZE);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/updater",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Auto-updater, changelog browser, and readme browser for Unipi — checks npm registry, renders CHANGELOG.md and README.md files in TUI overlays",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "2.3.0"
36
+ "@pi-unipi/core": "2.4.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/utility",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Utility commands and tools for Pi coding agent — lifecycle, diagnostics, cache, analytics, display, batch execution",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@pi-unipi/core": "2.3.0",
37
+ "@pi-unipi/core": "2.4.0",
38
38
  "diff": "^7.0.0",
39
39
  "@shikijs/cli": "^4.0.2"
40
40
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/web-api",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Web search, read, and summarize tools with provider-based backend selection for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -31,7 +31,7 @@
31
31
  "README.md"
32
32
  ],
33
33
  "dependencies": {
34
- "@pi-unipi/core": "2.3.0",
34
+ "@pi-unipi/core": "2.4.0",
35
35
  "defuddle": "^0.18.1",
36
36
  "linkedom": "^0.18.12",
37
37
  "lodash": "^4.17.21",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/workflow",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Structured development workflow commands for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -26,7 +26,7 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@pi-unipi/core": "2.3.0"
29
+ "@pi-unipi/core": "2.4.0"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@earendil-works/pi-ai": "^0.80.0",