omnius 1.0.198 → 1.0.200

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/dist/index.js CHANGED
@@ -23044,6 +23044,13 @@ function deleteRepoFromCache(repo) {
23044
23044
  }
23045
23045
  return removed;
23046
23046
  }
23047
+ function deleteCachedModel(repo) {
23048
+ const bytesFreed = deleteRepoFromCache(repo);
23049
+ const meta = readMeta();
23050
+ meta.entries = meta.entries.filter((entry) => entry.repo !== repo);
23051
+ writeMeta(meta);
23052
+ return { repo, bytesFreed };
23053
+ }
23047
23054
  function evictModelsToFreeSpace(args) {
23048
23055
  const safetyMargin = args.safetyMarginBytes ?? 1 * 1024 ** 3;
23049
23056
  const target = args.neededBytes + safetyMargin;
@@ -260097,6 +260104,31 @@ function approxImageDownloadBytes(preset) {
260097
260104
  }
260098
260105
  return gigabytesToBytes(4);
260099
260106
  }
260107
+ function imagePresetDependencyModels(preset, selectedModel) {
260108
+ const models = [
260109
+ selectedModel,
260110
+ preset?.diffusersBaseModel,
260111
+ preset?.textEncoderModel,
260112
+ ...preset?.loraAdapters?.map((adapter) => adapter.repoId) ?? []
260113
+ ].filter((value2) => Boolean(value2 && value2.trim()));
260114
+ return [...new Set(models)];
260115
+ }
260116
+ function diffusersRunnerModelArgs(model) {
260117
+ const preset = getImageGenerationPreset(model);
260118
+ const runnerModel = preset?.diffusersBaseModel ?? model;
260119
+ const argv = ["--model", runnerModel];
260120
+ if (runnerModel !== model)
260121
+ argv.push("--display-model", model);
260122
+ if (preset?.textEncoderModel) {
260123
+ argv.push("--text-encoder", preset.textEncoderModel);
260124
+ if (preset.textEncoderTarget)
260125
+ argv.push("--text-encoder-target", preset.textEncoderTarget);
260126
+ }
260127
+ for (const adapter of preset?.loraAdapters ?? []) {
260128
+ argv.push("--lora", JSON.stringify(adapter));
260129
+ }
260130
+ return argv;
260131
+ }
260100
260132
  async function ensureImageGenerationCacheDirs(repoRoot) {
260101
260133
  const env2 = imageGenerationPythonEnv(repoRoot);
260102
260134
  await Promise.all([
@@ -260228,7 +260260,7 @@ function parseRunnerJson(stdout) {
260228
260260
  }
260229
260261
  return null;
260230
260262
  }
260231
- var DEFAULT_DIFFUSERS_IMAGE_MODEL, DEFAULT_OLLAMA_IMAGE_MODEL, LEGACY_SDXL_TURBO_MODEL, SANA_1_5_1_6B_MODEL, SANA_1_5_4_8B_MODEL, SANA_1_6B_MULTILING_MODEL, SANA_1_6B_2K_MODEL, SANA_1_6B_4K_MODEL, SANA_SPRINT_0_6B_MODEL, SECONDARY_FLUX_DEV_MODEL, SECONDARY_FLUX_DEV_MIRROR_MODEL, SECONDARY_FLUX_DEV_COMFY_MODEL, SECONDARY_FLUX_FILL_MODEL, SECONDARY_FLUX_FILL_FP8_MODEL, SECONDARY_FLUX2_MODEL, OFFICIAL_BFL_ORG, IMAGE_GENERATION_MODEL_REPLACEMENTS, DIFFUSERS_PYTHON_PACKAGES, SDCPP_PYTHON_PACKAGES, IMAGE_GENERATION_MODEL_PRESETS, IMAGE_GENERATION_QUALITY_LADDER, OLLAMA_IMAGE_MODELS, DIFFUSERS_RUNNER, SDCPP_RUNNER, ImageGenerateTool;
260263
+ var DEFAULT_DIFFUSERS_IMAGE_MODEL, DEFAULT_OLLAMA_IMAGE_MODEL, LEGACY_SDXL_TURBO_MODEL, SANA_1_5_1_6B_MODEL, SANA_1_5_4_8B_MODEL, SANA_1_6B_MULTILING_MODEL, SANA_1_6B_2K_MODEL, SANA_1_6B_4K_MODEL, SANA_SPRINT_0_6B_MODEL, SECONDARY_FLUX_DEV_MODEL, SECONDARY_FLUX_DEV_MIRROR_MODEL, SECONDARY_FLUX_DEV_COMFY_MODEL, SECONDARY_FLUX_FILL_MODEL, SECONDARY_FLUX_FILL_FP8_MODEL, SECONDARY_FLUX2_MODEL, OFFICIAL_FLUX1_DEV_MODEL, OFFICIAL_FLUX2_KLEIN_9B_MODEL, PONPOKE_FLUX2_UNCENSORED_TEXT_ENCODER_MODEL, LUSTLY_FLUX_UNCENSORED_LORA_MODEL, KENERATE_FLUX_UNCENSORED_LORA_MODEL, OFFICIAL_BFL_ORG, IMAGE_GENERATION_MODEL_REPLACEMENTS, DIFFUSERS_PYTHON_PACKAGES, SDCPP_PYTHON_PACKAGES, IMAGE_GENERATION_MODEL_PRESETS, IMAGE_GENERATION_QUALITY_LADDER, OLLAMA_IMAGE_MODELS, DIFFUSERS_RUNNER, SDCPP_RUNNER, ImageGenerateTool;
260232
260264
  var init_image_generate = __esm({
260233
260265
  "packages/execution/dist/tools/image-generate.js"() {
260234
260266
  "use strict";
@@ -260252,6 +260284,11 @@ var init_image_generate = __esm({
260252
260284
  SECONDARY_FLUX_FILL_MODEL = "diffusers/FLUX.1-Fill-dev-nf4";
260253
260285
  SECONDARY_FLUX_FILL_FP8_MODEL = "boricuapab/flux1-fill-dev-fp8";
260254
260286
  SECONDARY_FLUX2_MODEL = "x/flux2-klein";
260287
+ OFFICIAL_FLUX1_DEV_MODEL = "black-forest-labs/FLUX.1-dev";
260288
+ OFFICIAL_FLUX2_KLEIN_9B_MODEL = "black-forest-labs/FLUX.2-klein-9B";
260289
+ PONPOKE_FLUX2_UNCENSORED_TEXT_ENCODER_MODEL = "ponpoke/flux2-klein-9b-uncensored-text-encoder";
260290
+ LUSTLY_FLUX_UNCENSORED_LORA_MODEL = "lustlyai/Flux_Lustly.ai_Uncensored_nsfw_v1";
260291
+ KENERATE_FLUX_UNCENSORED_LORA_MODEL = "kenerateai/Flux-uncensored";
260255
260292
  OFFICIAL_BFL_ORG = "black-forest-labs";
260256
260293
  IMAGE_GENERATION_MODEL_REPLACEMENTS = /* @__PURE__ */ new Map([
260257
260294
  [officialBflModel("FLUX.1-dev"), SECONDARY_FLUX_DEV_MODEL],
@@ -260274,6 +260311,7 @@ var init_image_generate = __esm({
260274
260311
  "transformers",
260275
260312
  "accelerate",
260276
260313
  "safetensors",
260314
+ "peft",
260277
260315
  "pillow",
260278
260316
  "sentencepiece",
260279
260317
  "protobuf"
@@ -260387,6 +260425,76 @@ var init_image_generate = __esm({
260387
260425
  fallbackFor: [SECONDARY_FLUX_FILL_MODEL],
260388
260426
  note: "Traceable FP8 fallback for FLUX.1 Fill dev from the research package."
260389
260427
  },
260428
+ {
260429
+ id: PONPOKE_FLUX2_UNCENSORED_TEXT_ENCODER_MODEL,
260430
+ label: "FLUX.2 Klein 9B uncensored text encoder",
260431
+ backend: "diffusers",
260432
+ install: 'python3 .omnius/image-gen/diffusers_text2image.py --model black-forest-labs/FLUX.2-klein-9B --text-encoder ponpoke/flux2-klein-9b-uncensored-text-encoder --steps 4 --guidance 1 --width 1024 --height 1024 --prompt "..." --output .omnius/images/out.png',
260433
+ category: "Adult-capable FLUX adapter",
260434
+ sizeClass: "FLUX.2 Klein 9B text-encoder override",
260435
+ quality: "Uncensored FLUX.2 Klein text-encoder override; uses the official FLUX.2 Klein 9B base pipeline and replaces the prompt encoder when the installed Diffusers stack supports that component layout.",
260436
+ minVramGB: 24,
260437
+ recommendedVramGB: 32,
260438
+ deployment: "Diffusers Flux2KleinPipeline over black-forest-labs/FLUX.2-klein-9B with a replacement text encoder. Requires Hugging Face access/license acceptance for the BFL base and the text-encoder repo.",
260439
+ steps: 4,
260440
+ guidance: 1,
260441
+ width: 1024,
260442
+ height: 1024,
260443
+ diffusersBaseModel: OFFICIAL_FLUX2_KLEIN_9B_MODEL,
260444
+ textEncoderModel: PONPOKE_FLUX2_UNCENSORED_TEXT_ENCODER_MODEL,
260445
+ textEncoderTarget: "auto",
260446
+ approxDownloadGB: 36,
260447
+ note: "Adapter-style preset: loads the gated FLUX.2 Klein 9B base and swaps in the ponpoke text encoder. Not in the automatic fallback ladder."
260448
+ },
260449
+ {
260450
+ id: LUSTLY_FLUX_UNCENSORED_LORA_MODEL,
260451
+ label: "Flux Lustly uncensored LoRA",
260452
+ backend: "diffusers",
260453
+ install: `python3 .omnius/image-gen/diffusers_text2image.py --model black-forest-labs/FLUX.1-dev --lora '{"repoId":"lustlyai/Flux_Lustly.ai_Uncensored_nsfw_v1","weightName":"flux_lustly-ai_v1.safetensors","adapterName":"v1","adapterWeight":1}' --steps 20 --guidance 4 --width 768 --height 768 --prompt "..." --output .omnius/images/out.png`,
260454
+ category: "Adult-capable FLUX adapter",
260455
+ sizeClass: "FLUX.1-dev LoRA adapter",
260456
+ quality: "Adult-capable FLUX.1-dev LoRA tested by its publisher on full FLUX dev and schnell. Listed as an explicit opt-in model, not part of automatic fallback.",
260457
+ minVramGB: 16,
260458
+ recommendedVramGB: 24,
260459
+ deployment: "Diffusers LoRA over black-forest-labs/FLUX.1-dev. Requires Hugging Face access/license acceptance for the gated BFL base model and the adapter terms.",
260460
+ steps: 20,
260461
+ guidance: 4,
260462
+ width: 768,
260463
+ height: 768,
260464
+ diffusersBaseModel: OFFICIAL_FLUX1_DEV_MODEL,
260465
+ loraAdapters: [{
260466
+ repoId: LUSTLY_FLUX_UNCENSORED_LORA_MODEL,
260467
+ weightName: "flux_lustly-ai_v1.safetensors",
260468
+ adapterName: "v1",
260469
+ adapterWeight: 1
260470
+ }],
260471
+ approxDownloadGB: 24,
260472
+ note: "Adapter-style preset: loads FLUX.1-dev then applies the Lustly LoRA. Explicit selection only."
260473
+ },
260474
+ {
260475
+ id: KENERATE_FLUX_UNCENSORED_LORA_MODEL,
260476
+ label: "Kenerate Flux uncensored LoRA",
260477
+ backend: "diffusers",
260478
+ install: `python3 .omnius/image-gen/diffusers_text2image.py --model black-forest-labs/FLUX.1-dev --lora '{"repoId":"kenerateai/Flux-uncensored","adapterName":"kenerate","adapterWeight":1}' --steps 20 --guidance 3.5 --width 1024 --height 1024 --prompt "..." --output .omnius/images/out.png`,
260479
+ category: "Adult-capable FLUX adapter",
260480
+ sizeClass: "FLUX.1-dev LoRA adapter",
260481
+ quality: "Community FLUX.1-dev LoRA. Hugging Face currently marks the model card as removed, so availability may fail at download time; kept as a selectable explicit model because the repo still advertises a Diffusers LoRA load path.",
260482
+ minVramGB: 16,
260483
+ recommendedVramGB: 24,
260484
+ deployment: "Diffusers LoRA over black-forest-labs/FLUX.1-dev. Requires Hugging Face access/license acceptance for the gated BFL base model; adapter availability may vary.",
260485
+ steps: 20,
260486
+ guidance: 3.5,
260487
+ width: 1024,
260488
+ height: 1024,
260489
+ diffusersBaseModel: OFFICIAL_FLUX1_DEV_MODEL,
260490
+ loraAdapters: [{
260491
+ repoId: KENERATE_FLUX_UNCENSORED_LORA_MODEL,
260492
+ adapterName: "kenerate",
260493
+ adapterWeight: 1
260494
+ }],
260495
+ approxDownloadGB: 24,
260496
+ note: "Adapter-style preset: loads FLUX.1-dev then applies the Kenerate LoRA. The upstream card says the LoRA has been removed, so failures should be surfaced directly."
260497
+ },
260390
260498
  {
260391
260499
  id: "stabilityai/stable-diffusion-3.5-large",
260392
260500
  label: "Stable Diffusion 3.5 Large",
@@ -260723,6 +260831,13 @@ def _device():
260723
260831
 
260724
260832
  def _pipeline_class(model):
260725
260833
  lowered = model.lower()
260834
+ if "flux.2" in lowered or "flux2" in lowered or "flux-2" in lowered:
260835
+ try:
260836
+ from diffusers import Flux2KleinPipeline
260837
+ return Flux2KleinPipeline
260838
+ except Exception:
260839
+ from diffusers import DiffusionPipeline
260840
+ return DiffusionPipeline
260726
260841
  if "flux" in lowered:
260727
260842
  from diffusers import FluxPipeline
260728
260843
  return FluxPipeline
@@ -260749,13 +260864,85 @@ def _pipeline_class(model):
260749
260864
  def _large_model(model):
260750
260865
  lowered = model.lower()
260751
260866
  return any(token in lowered for token in [
260752
- "flux.1", "flux.2", "stable-diffusion-3.5", "hunyuan", "janus",
260867
+ "flux.1", "flux.2", "flux2", "stable-diffusion-3.5", "hunyuan", "janus",
260753
260868
  "sana1.5_4.8b", "sana_1600m_2kpx", "sana_1600m_4kpx",
260754
260869
  ])
260755
260870
 
260871
+ def _parse_lora_specs(raw_specs):
260872
+ specs = []
260873
+ for raw in raw_specs or []:
260874
+ if not raw:
260875
+ continue
260876
+ try:
260877
+ parsed = json.loads(raw)
260878
+ if isinstance(parsed, str):
260879
+ parsed = {"repoId": parsed}
260880
+ if isinstance(parsed, dict) and parsed.get("repoId"):
260881
+ specs.append(parsed)
260882
+ except Exception as exc:
260883
+ raise ValueError(f"Invalid --lora JSON {raw!r}: {exc}")
260884
+ return specs
260885
+
260886
+ def _load_lora_adapters(pipe, specs):
260887
+ adapter_names = []
260888
+ adapter_weights = []
260889
+ for index, spec in enumerate(specs):
260890
+ repo_id = str(spec.get("repoId") or "").strip()
260891
+ if not repo_id:
260892
+ continue
260893
+ kwargs = {}
260894
+ weight_name = str(spec.get("weightName") or "").strip()
260895
+ adapter_name = str(spec.get("adapterName") or f"adapter_{index}").strip()
260896
+ if weight_name:
260897
+ kwargs["weight_name"] = weight_name
260898
+ if adapter_name:
260899
+ kwargs["adapter_name"] = adapter_name
260900
+ _progress("load", f"loading LoRA adapter {repo_id}")
260901
+ pipe.load_lora_weights(repo_id, **kwargs)
260902
+ if adapter_name:
260903
+ adapter_names.append(adapter_name)
260904
+ adapter_weights.append(float(spec.get("adapterWeight", 1.0)))
260905
+ if adapter_names and hasattr(pipe, "set_adapters"):
260906
+ _progress("load", f"activating {len(adapter_names)} LoRA adapter(s)")
260907
+ pipe.set_adapters(adapter_names, adapter_weights=adapter_weights)
260908
+
260909
+ def _target_text_encoder_attrs(pipe, target, repo_id):
260910
+ explicit = str(target or "").strip()
260911
+ if explicit and explicit != "auto":
260912
+ return [explicit]
260913
+ lowered = repo_id.lower()
260914
+ preferred = ["text_encoder_3", "text_encoder_2", "text_encoder"] if ("qwen" in lowered or "flux2" in lowered or "klein" in lowered) else ["text_encoder", "text_encoder_2", "text_encoder_3"]
260915
+ return [name for name in preferred if hasattr(pipe, name)]
260916
+
260917
+ def _tokenizer_attr_for_text_encoder(attr):
260918
+ if attr == "text_encoder":
260919
+ return "tokenizer"
260920
+ if attr.startswith("text_encoder_"):
260921
+ return "tokenizer_" + attr.split("_")[-1]
260922
+ return "tokenizer"
260923
+
260924
+ def _replace_text_encoder(pipe, repo_id, target, dtype):
260925
+ if not repo_id:
260926
+ return
260927
+ from transformers import AutoModel, AutoTokenizer
260928
+ attrs = _target_text_encoder_attrs(pipe, target, repo_id)
260929
+ if not attrs:
260930
+ raise ValueError(f"Pipeline has no text_encoder component compatible with text encoder override {repo_id}")
260931
+ attr = attrs[0]
260932
+ tokenizer_attr = _tokenizer_attr_for_text_encoder(attr)
260933
+ _progress("load", f"loading replacement text encoder {repo_id} into {attr}")
260934
+ tokenizer = AutoTokenizer.from_pretrained(repo_id, trust_remote_code=True)
260935
+ text_encoder = AutoModel.from_pretrained(repo_id, torch_dtype=dtype, trust_remote_code=True)
260936
+ setattr(pipe, attr, text_encoder)
260937
+ if hasattr(pipe, tokenizer_attr):
260938
+ setattr(pipe, tokenizer_attr, tokenizer)
260939
+ else:
260940
+ _progress("load", f"pipeline has no {tokenizer_attr}; replaced {attr} only")
260941
+
260756
260942
  def main():
260757
260943
  parser = argparse.ArgumentParser()
260758
260944
  parser.add_argument("--model", required=True)
260945
+ parser.add_argument("--display-model", default="")
260759
260946
  parser.add_argument("--prompt", required=True)
260760
260947
  parser.add_argument("--output", required=True)
260761
260948
  parser.add_argument("--width", type=int, default=512)
@@ -260765,6 +260952,9 @@ def main():
260765
260952
  parser.add_argument("--seed", type=int, default=None)
260766
260953
  parser.add_argument("--device", default="auto")
260767
260954
  parser.add_argument("--variant", default="")
260955
+ parser.add_argument("--lora", action="append", default=[])
260956
+ parser.add_argument("--text-encoder", default="")
260957
+ parser.add_argument("--text-encoder-target", default="auto")
260768
260958
  parser.add_argument("--prewarm", action="store_true")
260769
260959
  args = parser.parse_args()
260770
260960
 
@@ -260793,12 +260983,19 @@ def main():
260793
260983
  pipe = pipeline_cls.from_pretrained(args.model, **kwargs)
260794
260984
  _progress("load", f"model loaded on {device}")
260795
260985
 
260986
+ if args.text_encoder:
260987
+ _replace_text_encoder(pipe, args.text_encoder, args.text_encoder_target, dtype)
260988
+
260796
260989
  if "sana" in lowered_model and hasattr(pipe, "text_encoder") and pipe.text_encoder is not None:
260797
260990
  try:
260798
260991
  pipe.text_encoder.to(torch.bfloat16)
260799
260992
  except Exception:
260800
260993
  pass
260801
260994
 
260995
+ lora_specs = _parse_lora_specs(args.lora)
260996
+ if lora_specs:
260997
+ _load_lora_adapters(pipe, lora_specs)
260998
+
260802
260999
  if hasattr(pipe, "enable_attention_slicing"):
260803
261000
  try:
260804
261001
  pipe.enable_attention_slicing()
@@ -260817,7 +261014,10 @@ def main():
260817
261014
  print(json.dumps({
260818
261015
  "ok": True,
260819
261016
  "path": "",
260820
- "model": args.model,
261017
+ "model": args.display_model or args.model,
261018
+ "base_model": args.model,
261019
+ "lora_adapters": [spec.get("repoId") for spec in lora_specs],
261020
+ "text_encoder": args.text_encoder or None,
260821
261021
  "backend": "diffusers",
260822
261022
  "device": device,
260823
261023
  "prewarm": True,
@@ -260853,7 +261053,10 @@ def main():
260853
261053
  print(json.dumps({
260854
261054
  "ok": True,
260855
261055
  "path": str(out),
260856
- "model": args.model,
261056
+ "model": args.display_model or args.model,
261057
+ "base_model": args.model,
261058
+ "lora_adapters": [spec.get("repoId") for spec in lora_specs],
261059
+ "text_encoder": args.text_encoder or None,
260857
261060
  "backend": "diffusers",
260858
261061
  "device": device,
260859
261062
  "seconds": round(time.perf_counter() - t0, 3),
@@ -260913,7 +261116,7 @@ if __name__ == "__main__":
260913
261116
  `;
260914
261117
  ImageGenerateTool = class {
260915
261118
  name = "generate_image";
260916
- description = `Generate an image from a text prompt using a local image-generation backend. Supports Ollama image models (x/flux2-klein), Python Diffusers models (Sana 1.5 1.6B default, Sana 1.5 4.8B, Sana multilingual/2K/4K, FLUX.1 dev, SD3.5 Large, SDXL Turbo, Tiny-SD, LCM, Sana Sprint), and stable-diffusion.cpp local checkpoints/GGUF. When fallback is enabled, auto generation tries ranked high-quality candidates first (Sana 1.5 above FLUX so we avoid HF gating), including community FLUX mirrors, and then falls back to smaller models if setup, download, or generation fails. Aspect ratio and resolution are model-controllable: pass aspect_ratio (e.g. "16:9", "9:16", "4:3", "3:4", "1:1", "21:9", "2:3", "3:2") to derive width/height around the selected model's preferred base resolution, or pass explicit width/height (in pixels, both rounded to a multiple of 8) when a specific size is required. A preliminary prompt-expansion stage rewrites the user's prompt into a richer, model-tuned version before generation when an LLM expander is wired; pass expand_prompt=false to skip. Saves a PNG under .omnius/images and returns the file path.`;
261119
+ description = `Generate an image from a text prompt using a local image-generation backend. Supports Ollama image models (x/flux2-klein), Python Diffusers models (Sana 1.5 1.6B default, Sana 1.5 4.8B, Sana multilingual/2K/4K, FLUX.1 dev, FLUX adapter/LoRA presets, SD3.5 Large, SDXL Turbo, Tiny-SD, LCM, Sana Sprint), and stable-diffusion.cpp local checkpoints/GGUF. When fallback is enabled, auto generation tries ranked high-quality candidates first (Sana 1.5 above FLUX so we avoid HF gating), including community FLUX mirrors, and then falls back to smaller models if setup, download, or generation fails. Aspect ratio and resolution are model-controllable: pass aspect_ratio (e.g. "16:9", "9:16", "4:3", "3:4", "1:1", "21:9", "2:3", "3:2") to derive width/height around the selected model's preferred base resolution, or pass explicit width/height (in pixels, both rounded to a multiple of 8) when a specific size is required. A preliminary prompt-expansion stage rewrites the user's prompt into a richer, model-tuned version before generation when an LLM expander is wired; pass expand_prompt=false to skip. Saves a PNG under .omnius/images and returns the file path.`;
260917
261120
  parameters = {
260918
261121
  type: "object",
260919
261122
  properties: {
@@ -261407,7 +261610,7 @@ ${errText.slice(0, 1200)}`,
261407
261610
  try {
261408
261611
  const space = ensureDiskSpaceForDownload({
261409
261612
  approxDownloadBytes: approxBytes,
261410
- keepRepos: [args.model]
261613
+ keepRepos: imagePresetDependencyModels(preset, args.model)
261411
261614
  });
261412
261615
  if (space.evicted.length > 0) {
261413
261616
  this.emitProgress({
@@ -261442,8 +261645,7 @@ ${errText.slice(0, 1200)}`,
261442
261645
  }
261443
261646
  const result = await runProcess2(python.command, [
261444
261647
  runner,
261445
- "--model",
261446
- args.model,
261648
+ ...diffusersRunnerModelArgs(args.model),
261447
261649
  "--prompt",
261448
261650
  "omnius prewarm",
261449
261651
  "--output",
@@ -261617,8 +261819,7 @@ ${errText.slice(0, 800)}`,
261617
261819
  }
261618
261820
  const argv = [
261619
261821
  runner,
261620
- "--model",
261621
- args.model,
261822
+ ...diffusersRunnerModelArgs(args.model),
261622
261823
  "--prompt",
261623
261824
  args.prompt,
261624
261825
  "--output",
@@ -261639,7 +261840,7 @@ ${errText.slice(0, 800)}`,
261639
261840
  try {
261640
261841
  const space = ensureDiskSpaceForDownload({
261641
261842
  approxDownloadBytes: approxBytes,
261642
- keepRepos: [args.model]
261843
+ keepRepos: imagePresetDependencyModels(preset, args.model)
261643
261844
  });
261644
261845
  if (space.evicted.length > 0) {
261645
261846
  this.emitProgress({
@@ -534669,6 +534870,7 @@ __export(dist_exports, {
534669
534870
  createWorktree: () => createWorktree2,
534670
534871
  defaultExposureForTool: () => defaultExposureForTool,
534671
534872
  defaultExtensionForMime: () => defaultExtensionForMime,
534873
+ deleteCachedModel: () => deleteCachedModel,
534672
534874
  deleteMediaModelAdapter: () => deleteMediaModelAdapter,
534673
534875
  deleteTodos: () => deleteTodos,
534674
534876
  detectCudaDevices: () => detectCudaDevices,
@@ -534733,6 +534935,7 @@ __export(dist_exports, {
534733
534935
  imageGenerationDir: () => imageGenerationDir,
534734
534936
  imageGenerationModelPresets: () => imageGenerationModelPresets,
534735
534937
  imageGenerationSetupPlan: () => imageGenerationSetupPlan,
534938
+ imagePresetDependencyModels: () => imagePresetDependencyModels,
534736
534939
  inferAudioGenerationBackend: () => inferAudioGenerationBackend,
534737
534940
  inferImageGenerationBackend: () => inferImageGenerationBackend,
534738
534941
  inferMediaBackend: () => inferMediaBackend,
@@ -586916,9 +587119,9 @@ var init_profiles = __esm({
586916
587119
  encrypted: false,
586917
587120
  created: "2026-03-31T00:00:00Z"
586918
587121
  },
586919
- "cygnus-regi-tracking": {
586920
- name: "cygnus-regi-tracking",
586921
- description: "REGI bookkeeping only — todos, working notes, and completion markers. No filesystem, search, shell, network, or model-generation tools.",
587122
+ "bookkeeping-tracking": {
587123
+ name: "bookkeeping-tracking",
587124
+ description: "Bookkeeping only — todos, working notes, and completion markers. No filesystem, search, shell, network, or model-generation tools.",
586922
587125
  tools: {
586923
587126
  allow: ["todo_write", "todo_read", "working_notes", "task_complete"],
586924
587127
  deny: [
@@ -616212,7 +616415,13 @@ function ollamaModelDiskStats(model, sizes) {
616212
616415
  }
616213
616416
  function imageModelDiskStats(ctx3, preset, ollamaSizes) {
616214
616417
  if (preset.backend === "ollama") return ollamaModelDiskStats(preset.id, ollamaSizes);
616215
- if (preset.backend === "diffusers") return cachedModelDiskStats(imageGenerationDir(ctx3.repoRoot), preset.id);
616418
+ if (preset.backend === "diffusers") {
616419
+ const root = imageGenerationDir(ctx3.repoRoot);
616420
+ const parts = imagePresetDependencyModels(preset, preset.id).map((model) => cachedModelDiskStats(root, model));
616421
+ const paths = [...new Set(parts.flatMap((part) => part.paths))];
616422
+ const bytes = parts.reduce((sum, part) => sum + part.bytes, 0);
616423
+ return { downloaded: paths.length > 0, bytes, paths };
616424
+ }
616216
616425
  return { downloaded: false, bytes: 0, paths: [] };
616217
616426
  }
616218
616427
  function audioModelDiskStats(ctx3, preset) {
@@ -616238,7 +616447,8 @@ async function deleteImageModelWeights(ctx3, preset) {
616238
616447
  if (preset.backend === "ollama") {
616239
616448
  messages2.push(await deleteOllamaWeights(ctx3, preset.id));
616240
616449
  } else if (preset.backend === "diffusers") {
616241
- const removed = removeCachedModelPaths(imageGenerationDir(ctx3.repoRoot), preset.id);
616450
+ const root = imageGenerationDir(ctx3.repoRoot);
616451
+ const removed = imagePresetDependencyModels(preset, preset.id).flatMap((model) => removeCachedModelPaths(root, model));
616242
616452
  messages2.push(removed.length > 0 ? `Deleted ${removed.length} cached image model path(s) for ${preset.id}.` : `No cached image weights found for ${preset.id}.`);
616243
616453
  } else {
616244
616454
  messages2.push("stable-diffusion.cpp uses explicit local checkpoint paths; remove the chosen checkpoint file directly if needed.");
@@ -616261,7 +616471,7 @@ async function showImageModelsMenu(ctx3, hasLocal) {
616261
616471
  };
616262
616472
  const items = [
616263
616473
  { key: "setup:ollama", label: "Setup Ollama", detail: "Pull x/z-image-turbo or x/flux2-klein" },
616264
- { key: "setup:diffusers", label: "Setup Diffusers", detail: "Auto-installs SDXL Turbo under .omnius/image-gen/.venv" },
616474
+ { key: "setup:diffusers", label: "Setup Diffusers", detail: "Auto-installs the shared image runtime and selected Diffusers model" },
616265
616475
  { key: "setup:sdcpp", label: "Setup stable-diffusion.cpp", detail: "CPU/GGUF/checkpoint route" },
616266
616476
  { key: "hdr:models", label: selectColors.dim("─── Models ───") },
616267
616477
  ...imageGenerationModelPresets().map(buildModelItem)
@@ -630623,9 +630833,14 @@ function buildTelegramCommandMenuItems(scope) {
630623
630833
  seen.add(cmd.name);
630624
630834
  items.push({
630625
630835
  label: `/${cmd.name}`,
630626
- command: `/${cmd.name}`,
630627
630836
  description: cmd.signatures[0]?.description ?? signature,
630628
- adminOnly: scope === "admin"
630837
+ adminOnly: scope === "admin",
630838
+ action: {
630839
+ type: "command_detail",
630840
+ command: `/${cmd.name}`,
630841
+ label: `/${cmd.name}`,
630842
+ description: cmd.signatures.map((sig) => `${sig.signature} - ${sig.description}`).join("\n")
630843
+ }
630629
630844
  });
630630
630845
  }
630631
630846
  return items.sort((a2, b) => a2.label.localeCompare(b.label));
@@ -630633,10 +630848,18 @@ function buildTelegramCommandMenuItems(scope) {
630633
630848
  function buildTelegramGenerativeMenuItems(commandName) {
630634
630849
  const name10 = commandName.replace(/^\//, "").toLowerCase();
630635
630850
  if (!GENERATIVE_COMMANDS.has(name10)) return [];
630851
+ if (name10 === "models") {
630852
+ return [
630853
+ { label: "CAD models", description: "Browse text-to-CAD adapters.", action: { type: "models", generation: "cad" } },
630854
+ { label: "3D models", description: "Browse 3D mesh/reconstruction adapters.", action: { type: "models", generation: "model3d" } },
630855
+ { label: "Model store", command: "/models", description: "Show unified model store status.", action: { type: "command", command: "/models" } }
630856
+ ];
630857
+ }
630636
630858
  const title = name10[0].toUpperCase() + name10.slice(1);
630859
+ const generation = name10 === "sound" ? "sound" : name10 === "music" ? "music" : name10;
630637
630860
  return [
630638
- { label: `${title} models`, command: `/${name10} list`, description: `List available ${name10} models and hardware fit.` },
630639
- { label: `${title} setup`, command: `/${name10} setup`, description: `Show setup commands for the ${name10} backend.` }
630861
+ { label: `${title} models`, description: `Browse selectable ${name10} models, metadata, cache state, and actions.`, action: { type: "models", generation } },
630862
+ { label: `${title} setup`, description: `Show setup commands for the ${name10} backend.`, action: { type: "setup_generation", generation } }
630640
630863
  ];
630641
630864
  }
630642
630865
  function encodeTelegramCommandMenuCallback(action, value2) {
@@ -630646,43 +630869,85 @@ function encodeTelegramCommandMenuCallback(action, value2) {
630646
630869
  function decodeTelegramCommandMenuCallback(data) {
630647
630870
  const parts = data.split(":");
630648
630871
  if (parts.length !== 3 || parts[0] !== CALLBACK_PREFIX2) return null;
630649
- const action = parts[1] === "p" ? "page" : parts[1] === "r" ? "run" : parts[1] === "c" ? "close" : null;
630872
+ const action = parts[1] === "p" ? "page" : parts[1] === "r" ? "run" : parts[1] === "c" ? "close" : parts[1] === "b" ? "back" : null;
630650
630873
  if (!action) return null;
630651
630874
  return { action, value: parts[2] ?? "" };
630652
630875
  }
630876
+ function pushTelegramCommandMenuState(state, next) {
630877
+ const crumb = {
630878
+ kind: state.kind,
630879
+ title: state.title,
630880
+ subtitle: state.subtitle,
630881
+ body: state.body,
630882
+ page: state.page,
630883
+ items: state.items
630884
+ };
630885
+ return {
630886
+ ...state,
630887
+ kind: next.kind ?? state.kind,
630888
+ title: next.title,
630889
+ subtitle: next.subtitle,
630890
+ body: next.body,
630891
+ page: next.page ?? 0,
630892
+ items: next.items,
630893
+ breadcrumbs: [...state.breadcrumbs ?? [], crumb]
630894
+ };
630895
+ }
630896
+ function popTelegramCommandMenuState(state) {
630897
+ const stack = state.breadcrumbs ?? [];
630898
+ const previous = stack.at(-1);
630899
+ if (!previous) return null;
630900
+ return {
630901
+ ...state,
630902
+ kind: previous.kind,
630903
+ title: previous.title,
630904
+ subtitle: previous.subtitle,
630905
+ body: previous.body,
630906
+ page: previous.page,
630907
+ items: previous.items,
630908
+ breadcrumbs: stack.slice(0, -1)
630909
+ };
630910
+ }
630653
630911
  function renderTelegramCommandMenu(state) {
630654
630912
  const totalPages = Math.max(1, Math.ceil(state.items.length / PAGE_SIZE2));
630655
630913
  const page2 = Math.max(0, Math.min(state.page, totalPages - 1));
630656
630914
  const start2 = page2 * PAGE_SIZE2;
630657
630915
  const visible = state.items.slice(start2, start2 + PAGE_SIZE2);
630658
- const title = state.kind === "generative" ? "Generative command" : "Commands";
630916
+ const title = state.title || (state.kind === "generative" ? "Generative command" : state.kind === "models" ? "Models" : "Commands");
630659
630917
  const scope = state.scope === "admin" ? "admin" : "public";
630660
630918
  const lines = [
630661
630919
  `<b>${escapeHTML3(title)}</b>`,
630662
630920
  `<i>${escapeHTML3(scope)} scope - page ${page2 + 1}/${totalPages}</i>`,
630921
+ state.subtitle ? escapeHTML3(state.subtitle) : "",
630922
+ state.body ? `<blockquote expandable>${escapeHTML3(state.body)}</blockquote>` : "",
630663
630923
  "",
630664
630924
  ...visible.flatMap((item) => [
630665
- `<code>${escapeHTML3(item.command)}</code>`,
630925
+ item.command ? `<code>${escapeHTML3(item.command)}</code>` : `<b>${escapeHTML3(item.label)}</b>`,
630666
630926
  escapeHTML3(item.description)
630667
630927
  ])
630668
- ];
630928
+ ].filter((line) => line !== "");
630669
630929
  const keyboard = visible.map((item, offset) => [{
630670
630930
  text: item.label.slice(0, 32),
630671
630931
  callback_data: encodeTelegramCommandMenuCallback("run", start2 + offset)
630672
630932
  }]);
630673
630933
  const nav = [];
630674
630934
  nav.push({ text: "Close", callback_data: encodeTelegramCommandMenuCallback("close", 0) });
630935
+ if ((state.breadcrumbs ?? []).length > 0) nav.push({ text: "Back", callback_data: encodeTelegramCommandMenuCallback("back", 0) });
630675
630936
  if (page2 > 0) nav.push({ text: "Prev", callback_data: encodeTelegramCommandMenuCallback("page", page2 - 1) });
630676
630937
  nav.push({ text: `${page2 + 1}/${totalPages}`, callback_data: encodeTelegramCommandMenuCallback("page", page2) });
630677
630938
  if (page2 < totalPages - 1) nav.push({ text: "Next", callback_data: encodeTelegramCommandMenuCallback("page", page2 + 1) });
630678
630939
  keyboard.push(nav);
630679
- return { text: lines.join("\n"), reply_markup: { inline_keyboard: keyboard } };
630940
+ return { text: truncateTelegramMenuText(lines.join("\n")), reply_markup: { inline_keyboard: keyboard } };
630680
630941
  }
630681
630942
  function handleTelegramCommandMenuCallback(data, state, now = Date.now()) {
630682
630943
  const decoded = decodeTelegramCommandMenuCallback(data);
630683
630944
  if (!decoded) return null;
630684
630945
  if (state.expiresAt <= now) return null;
630685
630946
  if (decoded.action === "close") return { close: true };
630947
+ if (decoded.action === "back") {
630948
+ const newState = popTelegramCommandMenuState(state);
630949
+ return newState ? { newState, render: renderTelegramCommandMenu(newState) } : null;
630950
+ }
630686
630951
  if (decoded.action === "page") {
630687
630952
  const totalPages = Math.max(1, Math.ceil(state.items.length / PAGE_SIZE2));
630688
630953
  const page2 = Math.max(0, Math.min(Number.parseInt(decoded.value, 10) || 0, totalPages - 1));
@@ -630691,21 +630956,28 @@ function handleTelegramCommandMenuCallback(data, state, now = Date.now()) {
630691
630956
  }
630692
630957
  const index = Number.parseInt(decoded.value, 10);
630693
630958
  const item = Number.isFinite(index) ? state.items[index] : void 0;
630694
- return item ? { command: item.command } : null;
630959
+ if (!item) return null;
630960
+ return item.command ? { command: item.command, item } : { item };
630695
630961
  }
630696
630962
  function escapeHTML3(text) {
630697
630963
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
630698
630964
  }
630965
+ function truncateTelegramMenuText(text) {
630966
+ if (text.length <= 3900) return text;
630967
+ return `${text.slice(0, 3820)}
630968
+
630969
+ <i>... menu page clipped; use buttons to narrow this view.</i>`;
630970
+ }
630699
630971
  var CALLBACK_PREFIX2, PAGE_SIZE2, TTL_MS, MAX_CALLBACK_DATA_BYTES, GENERATIVE_COMMANDS, TelegramCommandMenuStateStore;
630700
630972
  var init_telegram_command_menu = __esm({
630701
630973
  "packages/cli/src/tui/telegram-command-menu.ts"() {
630702
630974
  "use strict";
630703
630975
  init_command_registry();
630704
630976
  CALLBACK_PREFIX2 = "ocm";
630705
- PAGE_SIZE2 = 8;
630977
+ PAGE_SIZE2 = 7;
630706
630978
  TTL_MS = 10 * 60 * 1e3;
630707
630979
  MAX_CALLBACK_DATA_BYTES = 64;
630708
- GENERATIVE_COMMANDS = /* @__PURE__ */ new Set(["image", "video", "sound", "music"]);
630980
+ GENERATIVE_COMMANDS = /* @__PURE__ */ new Set(["image", "video", "sound", "music", "models"]);
630709
630981
  TelegramCommandMenuStateStore = class {
630710
630982
  states = /* @__PURE__ */ new Map();
630711
630983
  key(chatId, messageId) {
@@ -630714,6 +630986,7 @@ var init_telegram_command_menu = __esm({
630714
630986
  create(input, now = Date.now()) {
630715
630987
  return {
630716
630988
  ...input,
630989
+ breadcrumbs: input.breadcrumbs ?? [],
630717
630990
  createdAt: now,
630718
630991
  expiresAt: now + TTL_MS
630719
630992
  };
@@ -634165,6 +634438,17 @@ import { join as join140, resolve as resolve53, basename as basename35, relative
634165
634438
  import { homedir as homedir44 } from "node:os";
634166
634439
  import { writeFile as writeFileAsync } from "node:fs/promises";
634167
634440
  import { createHash as createHash33, randomBytes as randomBytes25, randomInt } from "node:crypto";
634441
+ function formatModelBytes(bytes) {
634442
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
634443
+ const units = ["B", "KB", "MB", "GB", "TB"];
634444
+ let value2 = bytes;
634445
+ let unit = 0;
634446
+ while (value2 >= 1024 && unit < units.length - 1) {
634447
+ value2 /= 1024;
634448
+ unit++;
634449
+ }
634450
+ return `${value2 >= 10 || unit === 0 ? value2.toFixed(0) : value2.toFixed(1)} ${units[unit]}`;
634451
+ }
634168
634452
  function cleanTelegramDecisionNote(value2, maxLength = 260) {
634169
634453
  if (typeof value2 !== "string") return void 0;
634170
634454
  const clean5 = stripTelegramHiddenThinking(value2).replace(/\s+/g, " ").trim();
@@ -636853,6 +637137,8 @@ Telegram link integrity contract:
636853
637137
  statsMenuPruneTimer = null;
636854
637138
  /** Telegram-native command and generative command menus */
636855
637139
  telegramCommandMenuStates = new TelegramCommandMenuStateStore();
637140
+ /** One-shot Telegram prompt intents created by model-menu "Generate" buttons. */
637141
+ telegramGenerationPromptIntents = /* @__PURE__ */ new Map();
636856
637142
  /** Command handler for admin DM slash commands (wired from interactive.ts) */
636857
637143
  commandHandler = null;
636858
637144
  /** Callback fired after a Telegram user completes the TUI-only admin auth challenge */
@@ -637790,7 +638076,7 @@ ${message2}`)
637790
638076
  return;
637791
638077
  }
637792
638078
  if (msg.guestQueryId || !isAdmin) {
637793
- const lines = items.slice(0, 24).map((item) => `${item.command} - ${item.description}`);
638079
+ const lines = items.slice(0, 24).map((item) => `${item.command ?? item.label} - ${item.description}`);
637794
638080
  const text = ["Available commands:", "", ...lines].join("\n");
637795
638081
  if (msg.guestQueryId) {
637796
638082
  await this.answerGuestQuery(msg.guestQueryId, text);
@@ -637806,9 +638092,14 @@ ${message2}`)
637806
638092
  fromUserId: msg.fromUserId ?? 0,
637807
638093
  scope,
637808
638094
  kind,
638095
+ title: kind === "generative" ? `/${commandName ?? "models"}` : void 0,
638096
+ subtitle: kind === "generative" ? "Telegram-native menu; buttons render data and tools directly here." : void 0,
637809
638097
  page: 0,
637810
638098
  items
637811
638099
  });
638100
+ await this.sendTelegramCommandMenuState(msg, previewState);
638101
+ }
638102
+ async sendTelegramCommandMenuState(msg, previewState) {
637812
638103
  const menu = renderTelegramCommandMenu(previewState);
637813
638104
  const sent = await this.apiCall("sendMessage", {
637814
638105
  chat_id: msg.chatId,
@@ -637825,6 +638116,643 @@ ${message2}`)
637825
638116
  });
637826
638117
  }
637827
638118
  }
638119
+ async editTelegramCommandMenuState(chatId, messageId, state) {
638120
+ const render2 = renderTelegramCommandMenu(state);
638121
+ this.telegramCommandMenuStates.set(state);
638122
+ await this.apiCall("editMessageText", {
638123
+ chat_id: chatId,
638124
+ message_id: messageId,
638125
+ text: render2.text,
638126
+ parse_mode: "HTML",
638127
+ reply_markup: JSON.stringify(render2.reply_markup)
638128
+ });
638129
+ }
638130
+ telegramGenerationIntentKey(chatId, fromUserId) {
638131
+ return `${String(chatId)}:${fromUserId}`;
638132
+ }
638133
+ pruneTelegramGenerationPromptIntents(now = Date.now()) {
638134
+ for (const [key, intent] of this.telegramGenerationPromptIntents) {
638135
+ if (intent.expiresAt <= now) this.telegramGenerationPromptIntents.delete(key);
638136
+ }
638137
+ }
638138
+ telegramGenerationLabel(generation) {
638139
+ if (generation === "model3d") return "3D model";
638140
+ if (generation === "cad") return "CAD";
638141
+ return generation;
638142
+ }
638143
+ telegramModelCachedBytes(dependencies) {
638144
+ return dependencies.reduce((sum, dep) => sum + measureRepoCacheBytes(dep), 0);
638145
+ }
638146
+ telegramImageModelDescriptors() {
638147
+ return imageGenerationModelPresets().map((preset) => {
638148
+ const dependencies = imagePresetDependencyModels(preset, preset.id);
638149
+ return {
638150
+ generation: "image",
638151
+ id: preset.id,
638152
+ label: preset.label,
638153
+ backend: preset.backend,
638154
+ category: preset.category ?? "Image",
638155
+ sizeClass: preset.sizeClass ?? "",
638156
+ quality: preset.quality ?? preset.note,
638157
+ note: preset.note,
638158
+ install: preset.install,
638159
+ minVramGB: preset.minVramGB,
638160
+ recommendedVramGB: preset.recommendedVramGB,
638161
+ approxDownloadGB: preset.approxDownloadGB,
638162
+ dependencies,
638163
+ cachedBytes: this.telegramModelCachedBytes(dependencies)
638164
+ };
638165
+ });
638166
+ }
638167
+ telegramAudioModelDescriptors(kind) {
638168
+ return audioGenerationModelPresets(kind).map((preset) => {
638169
+ const dependencies = [preset.id];
638170
+ return {
638171
+ generation: kind,
638172
+ id: preset.id,
638173
+ label: preset.label,
638174
+ backend: preset.backend,
638175
+ category: preset.category,
638176
+ sizeClass: preset.sizeClass,
638177
+ quality: preset.quality,
638178
+ note: preset.note,
638179
+ install: preset.install,
638180
+ minVramGB: preset.minVramGB,
638181
+ recommendedVramGB: preset.recommendedVramGB,
638182
+ approxDownloadGB: preset.approxDownloadGB,
638183
+ dependencies,
638184
+ cachedBytes: this.telegramModelCachedBytes(dependencies)
638185
+ };
638186
+ });
638187
+ }
638188
+ telegramVideoModelDescriptors() {
638189
+ return videoGenerationModelPresets().map((preset) => {
638190
+ const dependencies = [preset.id];
638191
+ return {
638192
+ generation: "video",
638193
+ id: preset.id,
638194
+ label: preset.label,
638195
+ backend: preset.backend,
638196
+ category: preset.category,
638197
+ sizeClass: preset.sizeClass,
638198
+ quality: preset.quality,
638199
+ note: preset.note,
638200
+ install: preset.install,
638201
+ minVramGB: preset.minVramGB,
638202
+ recommendedVramGB: preset.recommendedVramGB,
638203
+ approxDownloadGB: preset.approxDownloadGB,
638204
+ dependencies,
638205
+ cachedBytes: this.telegramModelCachedBytes(dependencies),
638206
+ unavailableReason: preset.gated ? "Requires Hugging Face token/license acceptance." : void 0
638207
+ };
638208
+ });
638209
+ }
638210
+ telegramCad3dModelDescriptors(generation) {
638211
+ const modality = generation === "cad" ? "cad" : "3d";
638212
+ return listMediaModelCatalog(modality).map((entry) => {
638213
+ const spec = entry.spec;
638214
+ const dependencies = [spec.repoId];
638215
+ const notes = spec.deployment.notes.join(" ");
638216
+ return {
638217
+ generation,
638218
+ id: spec.id,
638219
+ label: spec.label || spec.repoId,
638220
+ backend: spec.backend,
638221
+ category: `${spec.modality}/${spec.status}`,
638222
+ sizeClass: spec.resources.approxDownloadGB ? `~${spec.resources.approxDownloadGB}GB download` : spec.runner,
638223
+ quality: spec.hf.cardExcerpt || notes || spec.runner,
638224
+ note: notes || spec.deployment.install,
638225
+ install: spec.deployment.install,
638226
+ minVramGB: spec.resources.minVramGB,
638227
+ recommendedVramGB: spec.resources.recommendedVramGB,
638228
+ approxDownloadGB: spec.resources.approxDownloadGB,
638229
+ dependencies,
638230
+ cachedBytes: this.telegramModelCachedBytes(dependencies),
638231
+ unavailableReason: spec.deployment.runtimeCompatible ? void 0 : "Catalog metadata exists, but the runtime adapter is not wired for artifact creation."
638232
+ };
638233
+ });
638234
+ }
638235
+ telegramGenerationModelDescriptors(generation) {
638236
+ if (generation === "image") return this.telegramImageModelDescriptors();
638237
+ if (generation === "sound" || generation === "music") return this.telegramAudioModelDescriptors(generation);
638238
+ if (generation === "video") return this.telegramVideoModelDescriptors();
638239
+ return this.telegramCad3dModelDescriptors(generation);
638240
+ }
638241
+ telegramModelDescriptor(generation, id) {
638242
+ return this.telegramGenerationModelDescriptors(generation).find((model) => model.id === id);
638243
+ }
638244
+ telegramModelMenuItems(generation) {
638245
+ return this.telegramGenerationModelDescriptors(generation).map((model) => {
638246
+ const cached = model.cachedBytes > 0 ? `cached ${formatModelBytes(model.cachedBytes)}` : "not downloaded";
638247
+ const fit3 = model.recommendedVramGB ? `${model.recommendedVramGB}GB VRAM rec` : model.minVramGB ? `${model.minVramGB}GB VRAM min` : "VRAM n/a";
638248
+ return {
638249
+ label: model.label,
638250
+ description: `${model.backend} - ${model.category} - ${fit3} - ${cached}`,
638251
+ action: {
638252
+ type: "model_detail",
638253
+ generation,
638254
+ model: model.id
638255
+ }
638256
+ };
638257
+ });
638258
+ }
638259
+ telegramModelDetailBody(model) {
638260
+ const cached = model.cachedBytes > 0 ? `${formatModelBytes(model.cachedBytes)} cached` : "not downloaded";
638261
+ const deps = model.dependencies.length > 0 ? model.dependencies.join(", ") : model.id;
638262
+ return [
638263
+ `id: ${model.id}`,
638264
+ `backend: ${model.backend}`,
638265
+ `category: ${model.category}`,
638266
+ `size: ${model.sizeClass || "unknown"}`,
638267
+ `download: ${model.approxDownloadGB ? `~${model.approxDownloadGB}GB` : "unknown"}; cache: ${cached}`,
638268
+ `vram: min ${model.minVramGB ?? "?"}GB; recommended ${model.recommendedVramGB ?? "?"}GB`,
638269
+ `dependencies: ${deps}`,
638270
+ model.unavailableReason ? `runtime note: ${model.unavailableReason}` : "",
638271
+ "",
638272
+ `quality: ${model.quality}`,
638273
+ "",
638274
+ `note: ${model.note}`,
638275
+ model.install ? `
638276
+ install/prewarm: ${model.install}` : ""
638277
+ ].filter(Boolean).join("\n");
638278
+ }
638279
+ telegramModelDetailItems(model) {
638280
+ return [
638281
+ {
638282
+ label: "Generate",
638283
+ description: "Use this model for the next Telegram message you type.",
638284
+ action: { type: "await_prompt", generation: model.generation, model: model.id, backend: model.backend, label: model.label }
638285
+ },
638286
+ {
638287
+ label: "Load",
638288
+ description: "Download/prewarm or check this model in the generation runtime.",
638289
+ action: { type: "prewarm_model", generation: model.generation, model: model.id, backend: model.backend, label: model.label }
638290
+ },
638291
+ {
638292
+ label: "Delete",
638293
+ description: "Remove cached weights for this model and its adapter/base dependencies.",
638294
+ action: { type: "delete_model", generation: model.generation, model: model.id, backend: model.backend, label: model.label }
638295
+ }
638296
+ ];
638297
+ }
638298
+ async replyWithTelegramModelBrowser(msg, isAdmin, generation) {
638299
+ if (!isAdmin) {
638300
+ await this.replyToTelegramMessage(msg, "Model menus require Telegram admin authentication.");
638301
+ return;
638302
+ }
638303
+ const scope = "admin";
638304
+ const title = `${this.telegramGenerationLabel(generation)} models`;
638305
+ const cachedCount = listCachedModels().length;
638306
+ const previewState = this.telegramCommandMenuStates.create({
638307
+ chatId: msg.chatId,
638308
+ messageId: 0,
638309
+ invokerMessageId: msg.messageId,
638310
+ fromUserId: msg.fromUserId ?? 0,
638311
+ scope,
638312
+ kind: "models",
638313
+ title,
638314
+ subtitle: `${cachedCount} cached model record(s). Select a model for metadata, load, delete, or generate.`,
638315
+ page: 0,
638316
+ items: this.telegramModelMenuItems(generation)
638317
+ });
638318
+ await this.sendTelegramCommandMenuState(msg, previewState);
638319
+ }
638320
+ parseTelegramGenerationMenuKind(value2) {
638321
+ const raw = String(value2 ?? "").toLowerCase();
638322
+ if (raw === "image" || raw === "sound" || raw === "music" || raw === "video" || raw === "cad") return raw;
638323
+ if (raw === "model3d" || raw === "3d" || raw === "model") return "model3d";
638324
+ return null;
638325
+ }
638326
+ async handleTelegramCommandMenuAction(callback, state, action) {
638327
+ if (!action) return { handled: false };
638328
+ const chatId = callback.chatId;
638329
+ const messageId = callback.messageId;
638330
+ if (!chatId || !messageId) return { handled: true, answerText: "Cannot identify menu message.", alert: true };
638331
+ if (action.type === "command_detail") {
638332
+ const command = typeof action.command === "string" ? action.command : "";
638333
+ if (!command) return { handled: true, answerText: "Command metadata is missing.", alert: true };
638334
+ const body = [
638335
+ `command: ${command}`,
638336
+ "",
638337
+ typeof action.description === "string" ? action.description : "No command details available."
638338
+ ].join("\n");
638339
+ const nextState = pushTelegramCommandMenuState(state, {
638340
+ kind: "detail",
638341
+ title: command,
638342
+ subtitle: "Telegram command",
638343
+ body,
638344
+ items: [{
638345
+ label: "Run",
638346
+ command,
638347
+ description: `Execute ${command}.`,
638348
+ action: { type: "command", command }
638349
+ }]
638350
+ });
638351
+ await this.editTelegramCommandMenuState(chatId, messageId, nextState);
638352
+ return { handled: true };
638353
+ }
638354
+ if (action.type === "models") {
638355
+ const generation = this.parseTelegramGenerationMenuKind(action.generation);
638356
+ if (!generation) return { handled: true, answerText: "Unknown model menu.", alert: true };
638357
+ const nextState = pushTelegramCommandMenuState(state, {
638358
+ kind: "models",
638359
+ title: `${this.telegramGenerationLabel(generation)} models`,
638360
+ subtitle: "Select a model for metadata, load, delete, or generate.",
638361
+ items: this.telegramModelMenuItems(generation)
638362
+ });
638363
+ await this.editTelegramCommandMenuState(chatId, messageId, nextState);
638364
+ return { handled: true };
638365
+ }
638366
+ if (action.type === "setup_generation") {
638367
+ const generation = this.parseTelegramGenerationMenuKind(action.generation);
638368
+ if (!generation) return { handled: true, answerText: "Unknown setup menu.", alert: true };
638369
+ await this.answerCallbackQuery(callback.id, `Preparing ${this.telegramGenerationLabel(generation)} setup...`).catch(() => false);
638370
+ await this.replyWithTelegramGenerationSetup({
638371
+ chatId,
638372
+ text: "",
638373
+ username: callback.username,
638374
+ firstName: callback.firstName,
638375
+ messageId,
638376
+ fromUserId: callback.fromUserId,
638377
+ chatType: "private"
638378
+ }, generation);
638379
+ return { handled: true, answered: true };
638380
+ }
638381
+ if (action.type === "model_detail") {
638382
+ const generation = this.parseTelegramGenerationMenuKind(action.generation);
638383
+ const modelId = typeof action.model === "string" ? action.model : "";
638384
+ const descriptor = generation ? this.telegramModelDescriptor(generation, modelId) : void 0;
638385
+ if (!generation || !descriptor) return { handled: true, answerText: "Model metadata is no longer available.", alert: true };
638386
+ const nextState = pushTelegramCommandMenuState(state, {
638387
+ kind: "detail",
638388
+ title: descriptor.label,
638389
+ subtitle: `${this.telegramGenerationLabel(generation)} model`,
638390
+ body: this.telegramModelDetailBody(descriptor),
638391
+ items: this.telegramModelDetailItems(descriptor)
638392
+ });
638393
+ await this.editTelegramCommandMenuState(chatId, messageId, nextState);
638394
+ return { handled: true };
638395
+ }
638396
+ if (action.type === "await_prompt") {
638397
+ const generation = this.parseTelegramGenerationMenuKind(action.generation);
638398
+ const modelId = typeof action.model === "string" ? action.model : "";
638399
+ const descriptor = generation ? this.telegramModelDescriptor(generation, modelId) : void 0;
638400
+ if (!generation || !descriptor) return { handled: true, answerText: "Model metadata is no longer available.", alert: true };
638401
+ const now = Date.now();
638402
+ this.pruneTelegramGenerationPromptIntents(now);
638403
+ this.telegramGenerationPromptIntents.set(this.telegramGenerationIntentKey(chatId, callback.fromUserId), {
638404
+ chatId,
638405
+ chatType: "private",
638406
+ fromUserId: callback.fromUserId,
638407
+ username: callback.username,
638408
+ generation,
638409
+ model: descriptor.id,
638410
+ backend: descriptor.backend,
638411
+ label: descriptor.label,
638412
+ createdAt: now,
638413
+ expiresAt: now + 15 * 6e4,
638414
+ menuMessageId: messageId
638415
+ });
638416
+ const nextState = {
638417
+ ...state,
638418
+ subtitle: "Waiting for your next Telegram message.",
638419
+ body: [
638420
+ `Selected: ${descriptor.label}`,
638421
+ `Model: ${descriptor.id}`,
638422
+ "",
638423
+ `Send the prompt you want to generate. Omnius will expand it first, then run the ${this.telegramGenerationLabel(generation)} pipeline with this model.`,
638424
+ "Send /cancel to clear this pending generation."
638425
+ ].join("\n")
638426
+ };
638427
+ await this.editTelegramCommandMenuState(chatId, messageId, nextState);
638428
+ return { handled: true, answerText: "Send the prompt as your next Telegram message." };
638429
+ }
638430
+ if (action.type === "prewarm_model") {
638431
+ const generation = this.parseTelegramGenerationMenuKind(action.generation);
638432
+ const modelId = typeof action.model === "string" ? action.model : "";
638433
+ const descriptor = generation ? this.telegramModelDescriptor(generation, modelId) : void 0;
638434
+ if (!generation || !descriptor) return { handled: true, answerText: "Model metadata is no longer available.", alert: true };
638435
+ await this.answerCallbackQuery(callback.id, `Loading ${descriptor.label}...`).catch(() => false);
638436
+ await this.runTelegramGenerationPrewarm(chatId, descriptor);
638437
+ return { handled: true, answered: true };
638438
+ }
638439
+ if (action.type === "delete_model") {
638440
+ const generation = this.parseTelegramGenerationMenuKind(action.generation);
638441
+ const modelId = typeof action.model === "string" ? action.model : "";
638442
+ const descriptor = generation ? this.telegramModelDescriptor(generation, modelId) : void 0;
638443
+ if (!generation || !descriptor) return { handled: true, answerText: "Model metadata is no longer available.", alert: true };
638444
+ const message2 = await this.deleteTelegramGenerationModelWeights(descriptor);
638445
+ await this.sendMessageHTML(chatId, convertMarkdownToTelegramHTML(message2));
638446
+ return { handled: true, answerText: "Delete complete." };
638447
+ }
638448
+ return { handled: false };
638449
+ }
638450
+ async deleteTelegramGenerationModelWeights(model) {
638451
+ if (model.backend === "ollama" || model.id.startsWith("x/")) {
638452
+ const base3 = String(this.agentConfig?.backendUrl || "http://localhost:11434").replace(/\/v1\/?$/, "").replace(/\/$/, "");
638453
+ const resp = await fetch(`${base3}/api/delete`, {
638454
+ method: "DELETE",
638455
+ headers: { "Content-Type": "application/json" },
638456
+ body: JSON.stringify({ name: model.id }),
638457
+ signal: AbortSignal.timeout(3e4)
638458
+ }).catch((err) => {
638459
+ throw new Error(`Ollama delete failed: ${err instanceof Error ? err.message : String(err)}`);
638460
+ });
638461
+ if (resp.status === 404) return `No Ollama weights found for ${model.id}.`;
638462
+ if (!resp.ok) {
638463
+ const text = await resp.text().catch(() => "");
638464
+ throw new Error(`Ollama delete failed for ${model.id}: HTTP ${resp.status}${text ? ` - ${text.slice(0, 300)}` : ""}`);
638465
+ }
638466
+ return `Deleted Ollama weights for ${model.id}.`;
638467
+ }
638468
+ const results = model.dependencies.map((repo) => deleteCachedModel(repo));
638469
+ const freed = results.reduce((sum, item) => sum + item.bytesFreed, 0);
638470
+ const lines = results.map((item) => `- ${item.repo}: ${item.bytesFreed > 0 ? formatModelBytes(item.bytesFreed) : "not cached"}`);
638471
+ return [`Deleted cached model weights for ${model.label}.`, `Freed: ${formatModelBytes(freed)}`, ...lines].join("\n");
638472
+ }
638473
+ telegramCreativeRootForChat(chatId) {
638474
+ return telegramCreativeWorkspaceRoot(this.repoRoot || process.cwd(), chatId);
638475
+ }
638476
+ buildTelegramGenerationTool(root, model) {
638477
+ if (model.generation === "image") {
638478
+ return {
638479
+ tool: new ImageGenerateTool(root, this.agentConfig?.backendUrl, {
638480
+ model: model.id,
638481
+ backend: model.backend
638482
+ }),
638483
+ args: { model: model.id, backend: model.backend }
638484
+ };
638485
+ }
638486
+ if (model.generation === "sound" || model.generation === "music") {
638487
+ return {
638488
+ tool: new AudioGenerateTool(root, model.generation === "music" ? { musicModel: model.id, musicBackend: model.backend } : { soundModel: model.id, soundBackend: model.backend }),
638489
+ args: { kind: model.generation, model: model.id, backend: model.backend }
638490
+ };
638491
+ }
638492
+ if (model.generation === "video") {
638493
+ return {
638494
+ tool: new VideoGenerateTool(root, {
638495
+ model: model.id,
638496
+ backend: model.backend,
638497
+ defaultKind: "t2v"
638498
+ }),
638499
+ args: { model: model.id, backend: model.backend, kind: "t2v" }
638500
+ };
638501
+ }
638502
+ const kind = model.generation === "cad" ? "cad" : "3d";
638503
+ return {
638504
+ tool: new ModelGenerateTool(root, kind === "cad" ? { cadModel: model.id, cadBackend: model.backend } : { model3dModel: model.id, model3dBackend: model.backend }),
638505
+ args: { kind, model: model.id, backend: model.backend }
638506
+ };
638507
+ }
638508
+ attachTelegramGenerationProgress(chatId, messageId, heading, model, tool) {
638509
+ if (!messageId) return;
638510
+ const setProgress = tool.setProgressCallback;
638511
+ if (typeof setProgress !== "function") return;
638512
+ let lastEdit = 0;
638513
+ setProgress.call(tool, (event) => {
638514
+ const now = Date.now();
638515
+ if (now - lastEdit < 2500 && event.percent === void 0) return;
638516
+ lastEdit = now;
638517
+ const percent = typeof event.percent === "number" ? ` ${Math.round(event.percent)}%` : "";
638518
+ const elapsed = typeof event.elapsedMs === "number" ? `
638519
+ Elapsed: ${Math.round(event.elapsedMs / 1e3)}s` : "";
638520
+ const html = [
638521
+ `<b>${escapeTelegramHTML(heading)}</b>`,
638522
+ `Model: <code>${escapeTelegramHTML(model.id)}</code>`,
638523
+ `Stage: <code>${escapeTelegramHTML(String(event.stage || "process"))}</code>${percent}`,
638524
+ escapeTelegramHTML(String(event.message || "")),
638525
+ elapsed
638526
+ ].filter(Boolean).join("\n");
638527
+ void this.editLiveMessage(chatId, messageId, html).catch(() => false);
638528
+ });
638529
+ }
638530
+ async runTelegramGenerationPrewarm(chatId, model) {
638531
+ const root = this.telegramCreativeRootForChat(chatId);
638532
+ const { tool, args } = this.buildTelegramGenerationTool(root, model);
638533
+ const liveId = await this.sendMessageHTML(
638534
+ chatId,
638535
+ `<b>Loading ${escapeTelegramHTML(model.label)}</b>
638536
+ Model: <code>${escapeTelegramHTML(model.id)}</code>`
638537
+ );
638538
+ this.attachTelegramGenerationProgress(chatId, liveId, `Loading ${model.label}`, model, tool);
638539
+ const action = model.generation === "model3d" || model.generation === "cad" ? "check" : "prewarm";
638540
+ const result = await tool.execute({ ...args, action });
638541
+ const html = result.success ? `<b>Model load/check complete</b>
638542
+ <blockquote expandable>${escapeTelegramHTML(result.output || "Ready.")}</blockquote>` : `<b>Model load/check failed</b>
638543
+ <blockquote expandable>${escapeTelegramHTML(result.error || result.output || "Unknown failure.")}</blockquote>`;
638544
+ if (liveId) await this.editLiveMessage(chatId, liveId, html).catch(() => false);
638545
+ else await this.sendMessageHTML(chatId, html);
638546
+ }
638547
+ async expandTelegramGenerationPrompt(intent, prompt, modelId = intent.model, backend = intent.backend ?? "auto") {
638548
+ const raw = prompt.trim();
638549
+ if (!raw || !this.agentConfig) return raw;
638550
+ try {
638551
+ const llm = new OllamaAgenticBackend(
638552
+ this.agentConfig.backendUrl,
638553
+ this.agentConfig.model,
638554
+ this.agentConfig.apiKey
638555
+ );
638556
+ const system = [
638557
+ "You rewrite a user's short media-generation request into a stronger production prompt.",
638558
+ "Preserve the user's subject and intent. Add concrete sensory, compositional, timing, material, and style details only where useful.",
638559
+ "Do not add policy commentary, labels, quotes, or explanation. Output only the expanded prompt."
638560
+ ].join(" ");
638561
+ const user = [
638562
+ `Generation type: ${intent.generation}`,
638563
+ `Target model: ${modelId}`,
638564
+ `Backend: ${backend}`,
638565
+ "",
638566
+ "User prompt:",
638567
+ raw
638568
+ ].join("\n");
638569
+ const result = await this.telegramObservableInference(
638570
+ llm,
638571
+ telegramThinkSuppressedRequest({
638572
+ messages: [
638573
+ { role: "system", content: system },
638574
+ { role: "user", content: user }
638575
+ ],
638576
+ tools: [],
638577
+ temperature: 0.4,
638578
+ maxTokens: 700,
638579
+ timeoutMs: Math.min(Math.max(this.agentConfig.timeoutMs ?? 3e4, 5e3), 3e4)
638580
+ }),
638581
+ "chat-fast-path",
638582
+ `generation-prompt:${String(intent.chatId)}:${intent.fromUserId}`
638583
+ );
638584
+ const text = result.choices[0]?.message?.content;
638585
+ if (typeof text !== "string") return raw;
638586
+ const cleaned = text.replace(/^["'`]+|["'`]+$/g, "").replace(/^(?:expanded prompt|prompt|output)\s*:\s*/i, "").trim();
638587
+ return cleaned || raw;
638588
+ } catch {
638589
+ return raw;
638590
+ }
638591
+ }
638592
+ installTelegramPromptExpander(tool, intent) {
638593
+ const setExpander = tool.setPromptExpander;
638594
+ if (typeof setExpander !== "function") return;
638595
+ setExpander.call(tool, async (ctx3) => this.expandTelegramGenerationPrompt(intent, ctx3.originalPrompt, ctx3.model, ctx3.backend));
638596
+ }
638597
+ async sendTelegramGenerationArtifacts(msg, root, result) {
638598
+ const paths = /* @__PURE__ */ new Set();
638599
+ for (const path12 of result.mutatedFiles ?? []) {
638600
+ if (typeof path12 === "string" && path12.trim()) paths.add(resolve53(path12));
638601
+ }
638602
+ for (const path12 of collectGeneratedArtifactPathsFromText(result.output || "", root)) {
638603
+ paths.add(resolve53(path12));
638604
+ }
638605
+ let sent = 0;
638606
+ for (const path12 of paths) {
638607
+ if (!existsSync127(path12) || !statSync46(path12).isFile()) continue;
638608
+ const kind = classifyMedia(path12) ?? "document";
638609
+ const messageId = await this.sendMediaReference(msg.chatId, {
638610
+ original: path12,
638611
+ value: path12,
638612
+ kind,
638613
+ source: "file",
638614
+ audioAsVoice: kind === "voice"
638615
+ }, {
638616
+ replyToMessageId: msg.messageId
638617
+ }).catch(() => null);
638618
+ if (messageId !== null) sent++;
638619
+ }
638620
+ return sent;
638621
+ }
638622
+ async executeTelegramGenerationPrompt(msg, intent) {
638623
+ const descriptor = this.telegramModelDescriptor(intent.generation, intent.model) ?? {
638624
+ generation: intent.generation,
638625
+ id: intent.model,
638626
+ label: intent.label,
638627
+ backend: intent.backend || "auto",
638628
+ category: "",
638629
+ sizeClass: "",
638630
+ quality: "",
638631
+ note: "",
638632
+ cachedBytes: 0,
638633
+ dependencies: [intent.model]
638634
+ };
638635
+ const root = this.telegramCreativeRootForChat(msg.chatId);
638636
+ const { tool, args } = this.buildTelegramGenerationTool(root, descriptor);
638637
+ this.installTelegramPromptExpander(tool, intent);
638638
+ const liveId = await this.sendMessageHTML(
638639
+ msg.chatId,
638640
+ [
638641
+ `<b>${escapeTelegramHTML(this.telegramGenerationLabel(intent.generation))} generation</b>`,
638642
+ `Model: <code>${escapeTelegramHTML(intent.model)}</code>`,
638643
+ "Expanding prompt and preparing runtime..."
638644
+ ].join("\n"),
638645
+ msg.messageId
638646
+ );
638647
+ this.attachTelegramGenerationProgress(msg.chatId, liveId, `${this.telegramGenerationLabel(intent.generation)} generation`, descriptor, tool);
638648
+ const rawPrompt = msg.text.trim();
638649
+ const prompt = intent.generation === "image" || intent.generation === "video" ? rawPrompt : await this.expandTelegramGenerationPrompt(intent, rawPrompt, descriptor.id, descriptor.backend);
638650
+ const result = await tool.execute({
638651
+ ...args,
638652
+ action: "generate",
638653
+ prompt,
638654
+ expand_prompt: true
638655
+ });
638656
+ const artifactCount = result.success ? await this.sendTelegramGenerationArtifacts(msg, root, result) : 0;
638657
+ const summary = result.success ? [
638658
+ `<b>Generation complete</b>`,
638659
+ artifactCount > 0 ? `Sent ${artifactCount} artifact${artifactCount === 1 ? "" : "s"}.` : "No artifact file was detected in the tool result.",
638660
+ `<blockquote expandable>${escapeTelegramHTML(result.output || result.llmContent || "Done.")}</blockquote>`
638661
+ ].join("\n") : [
638662
+ `<b>Generation failed</b>`,
638663
+ `<blockquote expandable>${escapeTelegramHTML(result.error || result.output || "Unknown failure.")}</blockquote>`
638664
+ ].join("\n");
638665
+ if (liveId) await this.editLiveMessage(msg.chatId, liveId, summary).catch(() => false);
638666
+ else await this.sendMessageHTML(msg.chatId, summary, msg.messageId);
638667
+ }
638668
+ async maybeHandleTelegramGenerationPrompt(msg) {
638669
+ this.pruneTelegramGenerationPromptIntents();
638670
+ const key = this.telegramGenerationIntentKey(msg.chatId, msg.fromUserId);
638671
+ const intent = this.telegramGenerationPromptIntents.get(key);
638672
+ if (!intent) return false;
638673
+ if (msg.text.trim().toLowerCase() === "/cancel") {
638674
+ this.telegramGenerationPromptIntents.delete(key);
638675
+ await this.replyToTelegramMessage(msg, "Pending generation cleared.");
638676
+ return true;
638677
+ }
638678
+ if (msg.text.trim().startsWith("/")) return false;
638679
+ this.telegramGenerationPromptIntents.delete(key);
638680
+ await this.executeTelegramGenerationPrompt(msg, intent);
638681
+ return true;
638682
+ }
638683
+ telegramSlashGenerationKind(name10) {
638684
+ if (name10 === "image") return "image";
638685
+ if (name10 === "sound") return "sound";
638686
+ if (name10 === "music") return "music";
638687
+ if (name10 === "video") return "video";
638688
+ return null;
638689
+ }
638690
+ async replyWithTelegramGenerationSetup(msg, generation) {
638691
+ const root = this.telegramCreativeRootForChat(msg.chatId);
638692
+ const settings = resolveSettings(this.repoRoot || process.cwd());
638693
+ let tool;
638694
+ let args;
638695
+ if (generation === "image") {
638696
+ tool = new ImageGenerateTool(root, this.agentConfig?.backendUrl, this.imageGenerationDefaultsForRepo(this.repoRoot || process.cwd()));
638697
+ args = { action: "setup", model: settings.imageModel, backend: settings.imageBackend };
638698
+ } else if (generation === "sound" || generation === "music") {
638699
+ tool = new AudioGenerateTool(root, this.audioGenerationDefaultsForRepo(this.repoRoot || process.cwd()));
638700
+ args = {
638701
+ action: "setup",
638702
+ kind: generation,
638703
+ model: generation === "music" ? settings.musicModel : settings.soundModel,
638704
+ backend: generation === "music" ? settings.musicBackend : settings.soundBackend
638705
+ };
638706
+ } else if (generation === "video") {
638707
+ tool = new VideoGenerateTool(root, this.videoGenerationDefaultsForRepo(this.repoRoot || process.cwd()));
638708
+ args = { action: "setup", model: settings.videoModel, backend: settings.videoBackend };
638709
+ } else {
638710
+ tool = new ModelGenerateTool(root, this.modelGenerationDefaultsForRepo(this.repoRoot || process.cwd()));
638711
+ args = { action: "list_models", kind: generation === "cad" ? "cad" : "3d" };
638712
+ }
638713
+ const result = await tool.execute(args);
638714
+ const html = result.success ? `<b>${escapeTelegramHTML(this.telegramGenerationLabel(generation))} setup</b>
638715
+ <blockquote expandable>${escapeTelegramHTML(result.output || "No setup output.")}</blockquote>` : `<b>${escapeTelegramHTML(this.telegramGenerationLabel(generation))} setup failed</b>
638716
+ <blockquote expandable>${escapeTelegramHTML(result.error || result.output || "Unknown failure.")}</blockquote>`;
638717
+ await this.replyToTelegramMessage(msg, html, { html: true });
638718
+ }
638719
+ async handleTelegramGenerationSlashCommand(msg, isAdmin, normalizedCommandText) {
638720
+ const trimmed = normalizedCommandText.trim();
638721
+ if (!trimmed.startsWith("/")) return false;
638722
+ const [rawName = "", rawSub = ""] = trimmed.split(/\s+/);
638723
+ const name10 = rawName.slice(1).split("@")[0]?.toLowerCase() ?? "";
638724
+ const sub = rawSub.toLowerCase();
638725
+ if (name10 === "models") {
638726
+ if (!sub) {
638727
+ await this.replyWithTelegramCommandMenu(msg, isAdmin, "generative", "models");
638728
+ return true;
638729
+ }
638730
+ if (sub === "3d" || sub === "cad") {
638731
+ await this.replyWithTelegramModelBrowser(msg, isAdmin, sub === "cad" ? "cad" : "model3d");
638732
+ return true;
638733
+ }
638734
+ return false;
638735
+ }
638736
+ const generation = this.telegramSlashGenerationKind(name10);
638737
+ if (!generation) return false;
638738
+ if (!sub) {
638739
+ await this.replyWithTelegramCommandMenu(msg, isAdmin, "generative", name10);
638740
+ return true;
638741
+ }
638742
+ if (sub === "list" || sub === "models" || sub === "menu") {
638743
+ await this.replyWithTelegramModelBrowser(msg, isAdmin, generation);
638744
+ return true;
638745
+ }
638746
+ if (sub === "setup") {
638747
+ if (!isAdmin) {
638748
+ await this.replyToTelegramMessage(msg, "Generation setup requires Telegram admin authentication.");
638749
+ return true;
638750
+ }
638751
+ await this.replyWithTelegramGenerationSetup(msg, generation);
638752
+ return true;
638753
+ }
638754
+ return false;
638755
+ }
637828
638756
  collectSessionMetricsSnapshot() {
637829
638757
  if (this._metricsProvider) {
637830
638758
  try {
@@ -643417,6 +644345,9 @@ ${summary}` : ""
643417
644345
  return;
643418
644346
  }
643419
644347
  const isAdmin = this.isAdminUser(msg);
644348
+ if (await this.maybeHandleTelegramGenerationPrompt(msg)) {
644349
+ return;
644350
+ }
643420
644351
  if (msg.text.trim().startsWith("/") && this.isTelegramCommandsMenuCommand(normalizedCommandText)) {
643421
644352
  await this.replyWithTelegramCommandMenu(msg, isAdmin, "commands");
643422
644353
  return;
@@ -643430,6 +644361,9 @@ ${summary}` : ""
643430
644361
  return;
643431
644362
  }
643432
644363
  const telegramSlash = this.telegramSlashName(normalizedCommandText);
644364
+ if (msg.text.trim().startsWith("/") && await this.handleTelegramGenerationSlashCommand(msg, isAdmin, normalizedCommandText)) {
644365
+ return;
644366
+ }
643433
644367
  if (msg.text.trim().startsWith("/") && TELEGRAM_REFLECTION_SLASH_COMMANDS.has(telegramSlash)) {
643434
644368
  await this.handleTelegramReflectionSlash(msg, normalizedCommandText);
643435
644369
  return;
@@ -646293,6 +647227,15 @@ Scoped workspace: ${scopedRoot}`,
646293
647227
  });
646294
647228
  return;
646295
647229
  }
647230
+ if (result.item?.action && result.item.action.type !== "command") {
647231
+ const actionResult = await this.handleTelegramCommandMenuAction(callback, menuState, result.item.action);
647232
+ if (actionResult.handled) {
647233
+ if (actionResult.answered) answered = true;
647234
+ if (actionResult.answerText) answerText2 = actionResult.answerText;
647235
+ if (actionResult.alert) alert2 = true;
647236
+ return;
647237
+ }
647238
+ }
646296
647239
  if (result.command) {
646297
647240
  if (!this.commandHandler) {
646298
647241
  answerText2 = "No command handler is available.";
@@ -667079,7 +668022,7 @@ function handleHelp(req2, res) {
667079
668022
  override: "Operators can override per-tool classification via OMNIUS_TOOL_OVERRIDES env var (JSON map of name → partial security info).",
667080
668023
  filters: "GET /v1/tools supports ?category=, ?scope=, ?risk=, ?off_device=true|false (Q9). E.g. /v1/tools?scope=read&off_device=true returns the safe-to-expose set.",
667081
668024
  profiles: "Tool profiles are enforced before tool exposure and again at execution. Resolution order: preset, working_directory/.omnius/profiles, ~/.omnius/profiles. Missing named profiles fail closed.",
667082
- bookkeeping: "For deterministic tracking, direct-call todo_write, todo_read, working_notes, and task_complete with profile:'cygnus-regi-tracking' and a stable session_id. Do not use /v1/run for bookkeeping-only mutations."
668025
+ bookkeeping: "For deterministic tracking, direct-call todo_write, todo_read, working_notes, and task_complete with profile:'bookkeeping-tracking' and a stable session_id. Do not use /v1/run for bookkeeping-only mutations."
667083
668026
  },
667084
668027
  runtime_keys: {
667085
668028
  "GET /v1/keys": "List runtime keys (admin scope). Secrets masked.",