@pi-unipi/unipi 2.3.0 → 2.4.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/package.json +21 -21
  3. package/packages/ask-user/package.json +2 -2
  4. package/packages/autocomplete/package.json +1 -1
  5. package/packages/btw/package.json +2 -2
  6. package/packages/cocoindex/package.json +2 -2
  7. package/packages/compactor/package.json +3 -3
  8. package/packages/compactor/src/compaction/auto-trigger.ts +0 -0
  9. package/packages/compactor/src/config/schema.ts +0 -0
  10. package/packages/core/package.json +1 -1
  11. package/packages/footer/package.json +2 -2
  12. package/packages/image/package.json +2 -2
  13. package/packages/image/src/generate.ts +34 -15
  14. package/packages/image/src/index.ts +8 -0
  15. package/packages/image/src/models.ts +28 -0
  16. package/packages/image/src/openai-images-api.ts +282 -0
  17. package/packages/image/src/register-providers.ts +220 -0
  18. package/packages/image/src/tools.ts +37 -6
  19. package/packages/image/src/tui/settings-dialog.ts +6 -1
  20. package/packages/info-screen/package.json +2 -2
  21. package/packages/input-shortcuts/package.json +2 -2
  22. package/packages/kanboard/package.json +2 -2
  23. package/packages/mcp/package.json +2 -2
  24. package/packages/memory/package.json +3 -3
  25. package/packages/milestone/hooks.ts +0 -0
  26. package/packages/milestone/package.json +2 -2
  27. package/packages/notify/package.json +2 -2
  28. package/packages/ralph/index.ts +10 -5
  29. package/packages/ralph/package.json +3 -3
  30. package/packages/subagents/package.json +4 -4
  31. package/packages/unipi/bundled.js +298 -21
  32. package/packages/updater/package.json +2 -2
  33. package/packages/utility/package.json +2 -2
  34. package/packages/web-api/package.json +2 -2
  35. package/packages/workflow/package.json +2 -2
@@ -7743,7 +7743,7 @@ function ralph_default(pi) {
7743
7743
  );
7744
7744
  }
7745
7745
  });
7746
- pi.on("before_agent_start", async (event, ctx) => {
7746
+ pi.on("before_agent_start", async (_event, ctx) => {
7747
7747
  const mgr = getManager(ctx, pi);
7748
7748
  const currentLoop = mgr.getCurrentLoop();
7749
7749
  if (!currentLoop) return;
@@ -7762,10 +7762,13 @@ function ralph_default(pi) {
7762
7762
  `;
7763
7763
  instructions += `- Otherwise, call ralph_done tool to proceed to next iteration`;
7764
7764
  return {
7765
- systemPrompt: event.systemPrompt + `
7766
- [RALPH LOOP - ${state.name} - Iteration ${iterStr}]
7765
+ message: {
7766
+ customType: "unipi-ralph-loop-reminder",
7767
+ content: `[RALPH LOOP - ${state.name} - Iteration ${iterStr}]
7767
7768
 
7768
- ${instructions}`
7769
+ ${instructions}`,
7770
+ display: false
7771
+ }
7769
7772
  };
7770
7773
  });
7771
7774
  pi.on("session_shutdown", async (_event, ctx) => {
@@ -36307,11 +36310,25 @@ function listRegistryImageGenModels(registry3) {
36307
36310
  provider: candidate.provider,
36308
36311
  name: candidate.name,
36309
36312
  api: candidate.api ?? "",
36313
+ // Carry the endpoint through. The generic images adapter POSTs to
36314
+ // `{baseUrl}/images/generations`, and this is the only place the
36315
+ // registry's baseUrl is available — dropping it here surfaces later as
36316
+ // "No baseUrl for image model ..." once generation is attempted.
36317
+ ...candidate.baseUrl ? { baseUrl: candidate.baseUrl } : {},
36310
36318
  ...candidate.output ? { output: candidate.output } : {}
36311
36319
  });
36312
36320
  }
36313
36321
  return out;
36314
36322
  }
36323
+ function findProviderBaseUrl(registry3, provider) {
36324
+ if (!registry3) return void 0;
36325
+ try {
36326
+ const models = registry3.getAvailable?.() ?? registry3.getAll();
36327
+ return models.find((m) => m?.provider === provider && m.baseUrl)?.baseUrl;
36328
+ } catch {
36329
+ return void 0;
36330
+ }
36331
+ }
36315
36332
  async function listAllImageGenModels(registry3) {
36316
36333
  const builtin = await listImageGenModels();
36317
36334
  const fromRegistry = registry3 ? listRegistryImageGenModels(registry3) : [];
@@ -36745,6 +36762,240 @@ var ImageModelSelectorOverlay = class {
36745
36762
  }
36746
36763
  };
36747
36764
 
36765
+ // packages/image/src/openai-images-api.ts
36766
+ var openai_images_api_exports = {};
36767
+ __export(openai_images_api_exports, {
36768
+ generateImages: () => generateImages,
36769
+ normalizeImageItem: () => normalizeImageItem,
36770
+ normalizeModelId: () => normalizeModelId
36771
+ });
36772
+ var DEFAULT_TIMEOUT_MS2 = 24e4;
36773
+ function asString(value) {
36774
+ return typeof value === "string" && value.length > 0 ? value : void 0;
36775
+ }
36776
+ function normalizeImageItem(item) {
36777
+ const declared = asString(item.media_type) ?? asString(item.mime_type) ?? asString(item.mimeType);
36778
+ const b64 = asString(item.b64_json);
36779
+ if (b64) return { data: b64, mimeType: declared ?? "image/png" };
36780
+ const url = asString(item.url);
36781
+ if (!url) return null;
36782
+ const dataUrl = /^data:([^;,]+)(?:;[^,]*)*,(.*)$/s.exec(url);
36783
+ if (dataUrl) {
36784
+ const [, mime, payload] = dataUrl;
36785
+ if (payload) return { data: payload, mimeType: declared ?? mime ?? "image/png" };
36786
+ return null;
36787
+ }
36788
+ return { text: `Image available at: ${url}` };
36789
+ }
36790
+ function joinUrl(baseUrl, suffix) {
36791
+ return `${baseUrl.replace(/\/+$/, "")}/${suffix.replace(/^\/+/, "")}`;
36792
+ }
36793
+ function normalizeModelId(id) {
36794
+ return id;
36795
+ }
36796
+ function buildHeaders(apiKey, extra) {
36797
+ const headers = {
36798
+ Authorization: `Bearer ${apiKey}`,
36799
+ "Content-Type": "application/json"
36800
+ };
36801
+ for (const [key, value] of Object.entries(extra ?? {})) {
36802
+ if (value === null) delete headers[key];
36803
+ else headers[key] = value;
36804
+ }
36805
+ return headers;
36806
+ }
36807
+ function describeError(status2, statusText, body) {
36808
+ let detail = body.slice(0, 300);
36809
+ try {
36810
+ const parsed = JSON.parse(body);
36811
+ if (typeof parsed.error === "string") detail = parsed.error;
36812
+ else if (parsed.error?.message) detail = parsed.error.message;
36813
+ } catch {
36814
+ }
36815
+ return `${status2} ${statusText}${detail ? `: ${detail}` : ""}`;
36816
+ }
36817
+ function annotateModelError(message, model) {
36818
+ if (/invalid image model|not found|unknown model/i.test(message)) {
36819
+ return `${message}
36820
+ \u2192 Model id sent: "${model.id}" (provider "${model.provider}").
36821
+ \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.`;
36822
+ }
36823
+ return message;
36824
+ }
36825
+ async function generateImages(model, context, options) {
36826
+ const result = {
36827
+ api: model.api || "openai-images",
36828
+ provider: model.provider,
36829
+ model: model.id,
36830
+ output: [],
36831
+ stopReason: "stop",
36832
+ timestamp: Date.now()
36833
+ };
36834
+ const fetchImpl = options?.fetchImpl ?? fetch;
36835
+ const controller = new AbortController();
36836
+ const timer = setTimeout(
36837
+ () => controller.abort(),
36838
+ options?.timeoutMs ?? DEFAULT_TIMEOUT_MS2
36839
+ );
36840
+ const onAbort = () => controller.abort();
36841
+ options?.signal?.addEventListener("abort", onAbort, { once: true });
36842
+ try {
36843
+ const apiKey = options?.apiKey;
36844
+ if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);
36845
+ if (!model.baseUrl) {
36846
+ throw new Error(`No baseUrl for image model ${model.provider}/${model.id}`);
36847
+ }
36848
+ const prompt = context.input.filter((part) => part.type === "text" && part.text).map((part) => part.text).join("\n").trim();
36849
+ const images = context.input.filter((part) => part.type === "image" && part.data).map((part) => `data:${part.mimeType || "image/png"};base64,${part.data}`);
36850
+ if (!prompt) throw new Error("A non-empty prompt is required.");
36851
+ const body = {
36852
+ model: normalizeModelId(model.id),
36853
+ prompt
36854
+ };
36855
+ if (images.length > 0) body.image = images;
36856
+ const response = await fetchImpl(joinUrl(model.baseUrl, "images/generations"), {
36857
+ method: "POST",
36858
+ headers: buildHeaders(apiKey, options?.headers),
36859
+ body: JSON.stringify(body),
36860
+ signal: controller.signal
36861
+ });
36862
+ if (!response.ok) {
36863
+ throw new Error(
36864
+ describeError(response.status, response.statusText, await response.text())
36865
+ );
36866
+ }
36867
+ const payload = await response.json();
36868
+ if (payload.error?.message) throw new Error(payload.error.message);
36869
+ for (const item of payload.data ?? []) {
36870
+ const normalized = normalizeImageItem(item);
36871
+ if (!normalized) continue;
36872
+ if ("text" in normalized) {
36873
+ result.output.push({ type: "text", text: normalized.text });
36874
+ } else {
36875
+ result.output.push({
36876
+ type: "image",
36877
+ data: normalized.data,
36878
+ mimeType: normalized.mimeType
36879
+ });
36880
+ }
36881
+ const revised = asString(item.revised_prompt);
36882
+ if (revised && revised !== prompt) {
36883
+ result.output.push({ type: "text", text: `Revised prompt: ${revised}` });
36884
+ }
36885
+ }
36886
+ if (result.output.every((part) => part.type !== "image")) {
36887
+ throw new Error("The provider returned no image data.");
36888
+ }
36889
+ return result;
36890
+ } catch (error) {
36891
+ const aborted = options?.signal?.aborted || controller.signal.aborted;
36892
+ result.stopReason = options?.signal?.aborted ? "aborted" : "error";
36893
+ result.errorMessage = aborted && !options?.signal?.aborted ? "Image request timed out." : annotateModelError(
36894
+ error instanceof Error ? error.message : String(error),
36895
+ model
36896
+ );
36897
+ return result;
36898
+ } finally {
36899
+ clearTimeout(timer);
36900
+ options?.signal?.removeEventListener("abort", onAbort);
36901
+ }
36902
+ }
36903
+
36904
+ // packages/image/src/register-providers.ts
36905
+ var registered = false;
36906
+ function groupModelsByProvider(models, baseUrlFor) {
36907
+ const grouped = /* @__PURE__ */ new Map();
36908
+ for (const model of models) {
36909
+ const baseUrl = model.baseUrl ?? baseUrlFor(model.provider);
36910
+ if (!baseUrl) continue;
36911
+ let entry = grouped.get(model.provider);
36912
+ if (!entry) {
36913
+ entry = { baseUrl, models: [] };
36914
+ grouped.set(model.provider, entry);
36915
+ }
36916
+ entry.models.push({ ...model, baseUrl });
36917
+ }
36918
+ return grouped;
36919
+ }
36920
+ function providerBaseUrlLookup(registry3) {
36921
+ const cache = /* @__PURE__ */ new Map();
36922
+ return (provider) => {
36923
+ if (cache.has(provider)) return cache.get(provider);
36924
+ let baseUrl;
36925
+ try {
36926
+ const models = registry3.getAvailable?.() ?? registry3.getAll();
36927
+ baseUrl = models.find((m) => m?.provider === provider && m.baseUrl)?.baseUrl;
36928
+ } catch {
36929
+ baseUrl = void 0;
36930
+ }
36931
+ cache.set(provider, baseUrl);
36932
+ return baseUrl;
36933
+ };
36934
+ }
36935
+ async function registerRegistryImageProviders(registry3, options) {
36936
+ if (!registry3) return [];
36937
+ if (registered && !options?.force) return [];
36938
+ const images = await getImagesModels();
36939
+ if (!images) return [];
36940
+ const mutable = images;
36941
+ if (typeof mutable.setProvider !== "function") return [];
36942
+ let createImagesProvider;
36943
+ try {
36944
+ const mod = await import("@earendil-works/pi-ai");
36945
+ if (typeof mod.createImagesProvider !== "function") return [];
36946
+ createImagesProvider = mod.createImagesProvider;
36947
+ } catch {
36948
+ return [];
36949
+ }
36950
+ const discovered = listRegistryImageGenModels(registry3);
36951
+ if (discovered.length === 0) {
36952
+ registered = true;
36953
+ return [];
36954
+ }
36955
+ const grouped = groupModelsByProvider(discovered, providerBaseUrlLookup(registry3));
36956
+ const added = [];
36957
+ for (const [providerId, { models }] of grouped) {
36958
+ try {
36959
+ if (mutable.getProvider?.(providerId)) continue;
36960
+ } catch {
36961
+ }
36962
+ try {
36963
+ const provider = createImagesProvider({
36964
+ id: providerId,
36965
+ name: providerId,
36966
+ models,
36967
+ api: openai_images_api_exports,
36968
+ auth: {
36969
+ apiKey: {
36970
+ name: `${providerId} API key`,
36971
+ // Resolve through pi's own auth storage so the user never logs in
36972
+ // twice. `resolve` MUST return an AuthResult (`{ auth: {...} }`);
36973
+ // returning a bare key fails silently at request time.
36974
+ resolve: async () => {
36975
+ const key = await resolveProviderKey(registry3, providerId);
36976
+ return key ? { auth: { apiKey: key }, source: `pi:${providerId}` } : void 0;
36977
+ }
36978
+ }
36979
+ }
36980
+ });
36981
+ mutable.setProvider(provider);
36982
+ added.push(providerId);
36983
+ } catch {
36984
+ }
36985
+ }
36986
+ registered = true;
36987
+ return added;
36988
+ }
36989
+ async function resolveProviderKey(registry3, provider) {
36990
+ try {
36991
+ const key = await registry3.getApiKeyForProvider?.(provider);
36992
+ if (key) return key;
36993
+ } catch {
36994
+ }
36995
+ const envName = `${provider.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
36996
+ return process.env[envName] || void 0;
36997
+ }
36998
+
36748
36999
  // packages/image/src/tui/settings-dialog.ts
36749
37000
  var EXIT = "__exit__";
36750
37001
  function describe(config) {
@@ -36911,6 +37162,7 @@ async function pickModel(ctx, kind, config) {
36911
37162
  async function collectModels(ctx, kind) {
36912
37163
  const registry3 = ctx.modelRegistry;
36913
37164
  if (kind === "generate") {
37165
+ await registerRegistryImageProviders(registry3);
36914
37166
  const models = await listAllImageGenModels(registry3);
36915
37167
  const generating = await getGeneratingProviders();
36916
37168
  return models.map((m) => ({
@@ -36920,7 +37172,7 @@ async function collectModels(ctx, kind) {
36920
37172
  // pi-ai's images collection has its own provider set. A chat provider's
36921
37173
  // image models are listed for reference but cannot actually generate,
36922
37174
  // 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
37175
+ unavailable: generating.length > 0 && !generating.includes(m.provider) ? "no image route" : void 0
36924
37176
  }));
36925
37177
  }
36926
37178
  if (!registry3) return [];
@@ -36996,7 +37248,7 @@ function providerCanGenerate(imagesApi, provider) {
36996
37248
  return providers.includes(provider);
36997
37249
  }
36998
37250
  async function generateImage(options) {
36999
- const { prompt, model, signal, outputDir, now: now2 } = options;
37251
+ const { prompt, model, signal, outputDir, now: now2, inputImage } = options;
37000
37252
  if (!prompt.trim()) {
37001
37253
  throw new Error("A non-empty prompt is required.");
37002
37254
  }
@@ -37009,28 +37261,37 @@ async function generateImage(options) {
37009
37261
  if (!providerCanGenerate(imagesApi, model.provider)) {
37010
37262
  const supported = supportedProviders(imagesApi);
37011
37263
  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.`
37264
+ `Provider "${model.provider}" has no image-generation route.
37265
+ \u2192 Available: ${supported.join(", ") || "openrouter"}.
37266
+ \u2192 Providers are bridged from pi automatically; one without a baseUrl or an API key cannot be used.
37267
+ \u2192 Pick another with /unipi:image-settings.`
37016
37268
  );
37017
37269
  }
37018
37270
  let apiKey;
37019
37271
  try {
37020
- apiKey = (await imagesApi.getAuth(model))?.apiKey;
37272
+ const resolvedAuth = await imagesApi.getAuth(model);
37273
+ apiKey = resolvedAuth?.auth?.apiKey ?? resolvedAuth?.apiKey;
37021
37274
  } catch {
37022
37275
  }
37023
37276
  apiKey ||= options.apiKey;
37024
37277
  if (!apiKey) {
37025
37278
  throw new Error(
37026
37279
  `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`
37280
+ \u2192 Sign in with /login, or set the provider's API key environment variable.
37281
+ \u2192 Expected environment variable: ${model.provider.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`
37029
37282
  );
37030
37283
  }
37284
+ const input = [{ type: "text", text: prompt }];
37285
+ if (inputImage) {
37286
+ input.push({
37287
+ type: "image",
37288
+ data: inputImage.data,
37289
+ mimeType: inputImage.mimeType
37290
+ });
37291
+ }
37031
37292
  const result = await imagesApi.generateImages(
37032
37293
  model,
37033
- { input: [{ type: "text", text: prompt }] },
37294
+ { input },
37034
37295
  { apiKey, ...signal ? { signal } : {} }
37035
37296
  );
37036
37297
  if (result.stopReason === "error") {
@@ -37194,7 +37455,7 @@ function truncate(value, max = 60) {
37194
37455
  }
37195
37456
 
37196
37457
  // packages/image/src/recognize.ts
37197
- var DEFAULT_TIMEOUT_MS2 = 12e4;
37458
+ var DEFAULT_TIMEOUT_MS3 = 12e4;
37198
37459
  var DEFAULT_MAX_TOKENS = 2048;
37199
37460
  function createSignal(timeoutMs, external) {
37200
37461
  const controller = new AbortController();
@@ -37233,7 +37494,7 @@ async function callAnthropic2(options) {
37233
37494
  apiKey,
37234
37495
  baseUrl,
37235
37496
  modelId,
37236
- timeoutMs = DEFAULT_TIMEOUT_MS2,
37497
+ timeoutMs = DEFAULT_TIMEOUT_MS3,
37237
37498
  maxTokens = DEFAULT_MAX_TOKENS,
37238
37499
  signal: external,
37239
37500
  fetchImpl = fetch
@@ -37340,7 +37601,7 @@ async function callOpenAICompatible2(options) {
37340
37601
  apiKey,
37341
37602
  baseUrl,
37342
37603
  modelId,
37343
- timeoutMs = DEFAULT_TIMEOUT_MS2,
37604
+ timeoutMs = DEFAULT_TIMEOUT_MS3,
37344
37605
  maxTokens = DEFAULT_MAX_TOKENS,
37345
37606
  signal: external,
37346
37607
  fetchImpl = fetch
@@ -37447,11 +37708,14 @@ function registerGenerateTool(pi) {
37447
37708
  pi.registerTool({
37448
37709
  name: IMAGE_TOOLS.GENERATE,
37449
37710
  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.",
37711
+ 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
37712
  promptSnippet: "Generate an image from a text prompt.",
37452
37713
  promptGuidelines: [
37453
37714
  "Use image_generate to create images from a text description.",
37454
37715
  "Write a detailed prompt \u2014 subject, style, composition and lighting all help.",
37716
+ "Pass `image` to edit an existing image instead of generating a new one.",
37717
+ "Editing regenerates the whole image, so unmentioned details may change.",
37718
+ "Describe what you DO want; negation is unreliable in image models.",
37455
37719
  "Omit model to use the one configured in /unipi:image-settings.",
37456
37720
  "Generated images cost money per call; do not regenerate without being asked."
37457
37721
  ],
@@ -37459,6 +37723,11 @@ function registerGenerateTool(pi) {
37459
37723
  prompt: Type9.String({
37460
37724
  description: "Description of the image to generate. Be specific."
37461
37725
  }),
37726
+ image: Type9.Optional(
37727
+ Type9.String({
37728
+ 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."
37729
+ })
37730
+ ),
37462
37731
  model: Type9.Optional(
37463
37732
  Type9.String({
37464
37733
  description: 'Image model override, e.g. "flux.2-pro" or "openrouter/google/gemini-3-pro-image". Omit to use the configured default.'
@@ -37469,21 +37738,26 @@ function registerGenerateTool(pi) {
37469
37738
  try {
37470
37739
  const config = loadConfig7();
37471
37740
  const registry3 = getRegistry2(ctx);
37741
+ await registerRegistryImageProviders(registry3);
37472
37742
  const models = await listAllImageGenModels(registry3);
37473
37743
  const requested = params.model?.trim() || config.generate.model;
37474
- const resolved = resolveImageGenModel(requested, models);
37475
- if (typeof resolved === "string") return errorResult(resolved);
37744
+ const maybeResolved = resolveImageGenModel(requested, models);
37745
+ if (typeof maybeResolved === "string") return errorResult(maybeResolved);
37746
+ const registryBaseUrl = maybeResolved.baseUrl ? void 0 : findProviderBaseUrl(registry3, maybeResolved.provider);
37747
+ const resolved = registryBaseUrl ? { ...maybeResolved, baseUrl: registryBaseUrl } : maybeResolved;
37476
37748
  const fallbackKey = await resolveApiKey(registry3, resolved.provider);
37749
+ const sourceImage = params.image?.trim() ? loadImage(params.image, ctx.cwd ?? process.cwd()) : void 0;
37477
37750
  const result = await generateImage({
37478
37751
  prompt: params.prompt,
37479
37752
  model: resolved,
37480
37753
  ...fallbackKey ? { apiKey: fallbackKey } : {},
37754
+ ...sourceImage ? { inputImage: sourceImage } : {},
37481
37755
  signal,
37482
37756
  outputDir: config.generate.saveToDisk ? getOutputDir(config) : void 0
37483
37757
  });
37484
37758
  const saved = result.images.map((image) => image.path).filter((path28) => Boolean(path28));
37485
37759
  const summary = [
37486
- `Generated ${result.images.length} image${result.images.length === 1 ? "" : "s"} with ${formatModelRef(resolved)}.`,
37760
+ `${sourceImage ? "Edited" : "Generated"} ${result.images.length} image${result.images.length === 1 ? "" : "s"} with ${formatModelRef(resolved)}.`,
37487
37761
  saved.length > 0 ? `Saved to:
37488
37762
  ${saved.map((p) => ` ${p}`).join("\n")}` : "",
37489
37763
  config.generate.saveToDisk && saved.length === 0 ? "Could not write to the output directory \u2014 returning the image inline only." : "",
@@ -37625,6 +37899,9 @@ function src_default5(pi) {
37625
37899
  registerImageCommands(pi);
37626
37900
  pi.on("session_start", async (_event, ctx) => {
37627
37901
  const config = loadConfig7();
37902
+ void registerRegistryImageProviders(
37903
+ ctx.modelRegistry
37904
+ ).catch(() => void 0);
37628
37905
  const tools = [];
37629
37906
  if (config.generate.enabled) tools.push(IMAGE_TOOLS.GENERATE);
37630
37907
  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",