omnius 1.0.21 → 1.0.22

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
@@ -1474,7 +1474,7 @@ var init_security_classifier = __esm({
1474
1474
  // ── Network reads (safe)
1475
1475
  { match: /^(web_search|web_fetch)$/, info: NETWORK_READ },
1476
1476
  // ── Network outbound (mutating or remote inference)
1477
- { match: /^(image_generate|generate_image|vision|video_understand)$/, info: NETWORK_OUTBOUND },
1477
+ { match: /^(image_generate|generate_image|generate_audio|generate_tts|create_audio_file|vision|video_understand|telegram_send_file)$/, info: NETWORK_OUTBOUND },
1478
1478
  { match: /^(transcribe_file|transcribe_url|youtube_download)$/, info: NETWORK_OUTBOUND },
1479
1479
  { match: /^(fortemi_bridge)$/, info: NETWORK_OUTBOUND },
1480
1480
  // ── Memory tools
@@ -1491,7 +1491,7 @@ var init_security_classifier = __esm({
1491
1491
  { match: /^(file_read|file_explore|list_directory|grep_search|glob_find|find_files)$/, info: LOCAL_READ },
1492
1492
  { match: /^(image_read|ocr|ocr_pdf|ocr_image_advanced|pdf_to_text|structured_read|read_structured_file)$/, info: LOCAL_READ },
1493
1493
  { match: /^(symbol_search|impact_analysis|code_neighbors|repo_map|codebase_map|semantic_map|import_graph)$/, info: LOCAL_READ },
1494
- { match: /^(diagnostic|git_info|environment_snapshot|process_health|todo_read|explore_tools)$/, info: LOCAL_READ },
1494
+ { match: /^(diagnostic|git_info|environment_snapshot|process_health|todo_read|explore_tools|telegram_media_recent)$/, info: LOCAL_READ },
1495
1495
  { match: /^(log_explore|log_packet|change_log|phase_recall|code_graph)$/, info: LOCAL_READ },
1496
1496
  { match: /^skill_(list|execute|read)$/, info: LOCAL_READ },
1497
1497
  // ── Task completion (neutral signal)
@@ -5733,13 +5733,20 @@ var init_explore_tools = __esm({
5733
5733
  diagnostic: "Run project diagnostics (build, test, lint)",
5734
5734
  image_read: "Read and describe image contents",
5735
5735
  screenshot: "Capture a screenshot of the desktop",
5736
+ ocr: "Extract text from images via OCR",
5736
5737
  ocr_image: "Extract text from images via OCR",
5738
+ ocr_image_advanced: "Advanced OCR for images with layout-aware extraction",
5737
5739
  ocr_pdf: "Extract text from PDF pages via OCR",
5738
5740
  pdf_to_text: "Convert PDF to plain text",
5739
5741
  vision: "Describe what's on screen using Moondream",
5742
+ video_understand: "Analyze a video file with transcription and keyframe understanding",
5743
+ audio_analyze: "Classify sounds, detect speech, inspect spectrum, or analyze audio files",
5740
5744
  desktop_click: "Click at coordinates on the desktop",
5741
5745
  desktop_describe: "Describe a region of the desktop",
5742
5746
  transcribe_file: "Transcribe audio/video files to text",
5747
+ telegram_media_recent: "List recent Telegram media available in the current chat scope",
5748
+ generate_audio: "Generate sound effects or music with local model backends",
5749
+ generate_tts: "Generate speech from text with configured voice/TTS backends",
5743
5750
  create_tool: "Create a new custom tool from a workflow",
5744
5751
  manage_tools: "List, inspect, or remove custom tools",
5745
5752
  skill_list: "List available AIWG skills",
@@ -84452,7 +84459,7 @@ var require_mime_types = __commonJS({
84452
84459
  "../node_modules/mime-types/index.js"(exports) {
84453
84460
  "use strict";
84454
84461
  var db = require_mime_db();
84455
- var extname16 = __require("path").extname;
84462
+ var extname17 = __require("path").extname;
84456
84463
  var EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/;
84457
84464
  var TEXT_TYPE_REGEXP = /^text\//i;
84458
84465
  exports.charset = charset;
@@ -84506,7 +84513,7 @@ var require_mime_types = __commonJS({
84506
84513
  if (!path11 || typeof path11 !== "string") {
84507
84514
  return false;
84508
84515
  }
84509
- var extension4 = extname16("x." + path11).toLowerCase().substr(1);
84516
+ var extension4 = extname17("x." + path11).toLowerCase().substr(1);
84510
84517
  if (!extension4) {
84511
84518
  return false;
84512
84519
  }
@@ -250402,6 +250409,11 @@ function getImageGenerationPreset(model) {
250402
250409
  function imageGenerationQualityLadder() {
250403
250410
  return IMAGE_GENERATION_QUALITY_LADDER.map((id) => getImageGenerationPreset(id)).filter((preset) => Boolean(preset));
250404
250411
  }
250412
+ function imageGenerationFallbackAlternates(model) {
250413
+ if (!model)
250414
+ return [];
250415
+ return IMAGE_GENERATION_MODEL_PRESETS.filter((preset) => preset.fallbackFor?.includes(model));
250416
+ }
250405
250417
  function inferImageGenerationBackend(model, requested) {
250406
250418
  if (requested && isBackend(requested))
250407
250419
  return requested;
@@ -250438,6 +250450,8 @@ function imageGenerationFallbackCandidates(requestedModel, requestedBackend, all
250438
250450
  };
250439
250451
  if (requestedModel) {
250440
250452
  add2(imageCandidateFor(requestedModel, requestedBackend));
250453
+ for (const alternate of imageGenerationFallbackAlternates(requestedModel))
250454
+ add2(imageCandidateFor(alternate.id));
250441
250455
  } else if (requestedBackend && requestedBackend !== "auto") {
250442
250456
  const firstForBackend = ladder.find((preset) => preset.backend === requestedBackend);
250443
250457
  add2(imageCandidateFor(firstForBackend?.id ?? (requestedBackend === "ollama" ? DEFAULT_OLLAMA_IMAGE_MODEL : DEFAULT_DIFFUSERS_IMAGE_MODEL), requestedBackend));
@@ -250448,8 +250462,11 @@ function imageGenerationFallbackCandidates(requestedModel, requestedBackend, all
250448
250462
  return candidates.length ? candidates : [imageCandidateFor(DEFAULT_DIFFUSERS_IMAGE_MODEL, requestedBackend)];
250449
250463
  const primaryIndex = requestedModel ? ladder.findIndex((preset) => preset.id === requestedModel) : requestedBackend && requestedBackend !== "auto" ? ladder.findIndex((preset) => preset.backend === requestedBackend) : 0;
250450
250464
  const fallbackTail = primaryIndex >= 0 ? ladder.slice(primaryIndex) : ladder;
250451
- for (const preset of fallbackTail)
250465
+ for (const preset of fallbackTail) {
250452
250466
  add2(imageCandidateFor(preset.id));
250467
+ for (const alternate of imageGenerationFallbackAlternates(preset.id))
250468
+ add2(imageCandidateFor(alternate.id));
250469
+ }
250453
250470
  return candidates;
250454
250471
  }
250455
250472
  function imageGenerationDir(repoRoot = ".") {
@@ -250817,6 +250834,78 @@ var init_image_generate = __esm({
250817
250834
  height: 1024,
250818
250835
  note: "Primary serious-generation baseline for maximum photorealism."
250819
250836
  },
250837
+ {
250838
+ id: "black-forest-labs/FLUX.1-dev-FP8",
250839
+ label: "FLUX.1 dev FP8",
250840
+ backend: "diffusers",
250841
+ install: 'python3 .omnius/image-gen/diffusers_text2image.py --model black-forest-labs/FLUX.1-dev-FP8 --steps 28 --guidance 3.5 --width 1024 --height 1024 --prompt "..." --output .omnius/images/out.png',
250842
+ category: "Official FLUX fallback",
250843
+ sizeClass: "12B FLUX.1 dev FP8",
250844
+ quality: "Official lower-precision FLUX.1 dev route; best first fallback when full FLUX.1 dev is unavailable or too heavy.",
250845
+ minVramGB: 16,
250846
+ recommendedVramGB: 24,
250847
+ deployment: "Prefer this before third-party mirrors when loader support is available.",
250848
+ steps: 28,
250849
+ guidance: 3.5,
250850
+ width: 1024,
250851
+ height: 1024,
250852
+ fallbackFor: ["black-forest-labs/FLUX.1-dev"],
250853
+ note: "Official BFL FP8 fallback for FLUX.1 dev."
250854
+ },
250855
+ {
250856
+ id: "black-forest-labs/FLUX.1-Krea-dev",
250857
+ label: "FLUX.1 Krea dev",
250858
+ backend: "diffusers",
250859
+ install: 'python3 .omnius/image-gen/diffusers_text2image.py --model black-forest-labs/FLUX.1-Krea-dev --steps 28 --guidance 3.5 --width 1024 --height 1024 --prompt "..." --output .omnius/images/out.png',
250860
+ category: "Official FLUX fallback",
250861
+ sizeClass: "12B FLUX.1 dev-family",
250862
+ quality: "Official FLUX.1 dev-family aesthetic variant; useful when the base dev repo is unavailable and the requested task tolerates an opinionated realism bias.",
250863
+ minVramGB: 24,
250864
+ recommendedVramGB: 48,
250865
+ deployment: "Heavy Diffusers/ComfyUI route with FLUX.1 dev-family license considerations.",
250866
+ steps: 28,
250867
+ guidance: 3.5,
250868
+ width: 1024,
250869
+ height: 1024,
250870
+ fallbackFor: ["black-forest-labs/FLUX.1-dev"],
250871
+ note: "Official aesthetic FLUX.1 fallback."
250872
+ },
250873
+ {
250874
+ id: "lllyasviel/flux1-dev-bnb-nf4",
250875
+ label: "FLUX.1 dev BNB NF4",
250876
+ backend: "diffusers",
250877
+ install: 'python3 .omnius/image-gen/diffusers_text2image.py --model lllyasviel/flux1-dev-bnb-nf4 --steps 28 --guidance 3.5 --width 1024 --height 1024 --prompt "..." --output .omnius/images/out.png',
250878
+ category: "Traceable FLUX fallback",
250879
+ sizeClass: "12B FLUX.1 dev NF4",
250880
+ quality: "Lower-memory community quantization; useful after official BFL sources, with some possible quality loss and loader brittleness.",
250881
+ minVramGB: 12,
250882
+ recommendedVramGB: 16,
250883
+ deployment: "Best with BNB-aware Diffusers/Forge-style runtimes. Falls through cleanly if the current runner cannot load it.",
250884
+ steps: 28,
250885
+ guidance: 3.5,
250886
+ width: 1024,
250887
+ height: 1024,
250888
+ fallbackFor: ["black-forest-labs/FLUX.1-dev", "black-forest-labs/FLUX.1-dev-FP8"],
250889
+ note: "Traceable low-VRAM NF4 fallback for FLUX.1 dev."
250890
+ },
250891
+ {
250892
+ id: "ChuckMcSneed/FLUX.1-dev",
250893
+ label: "FLUX.1 dev mirror",
250894
+ backend: "diffusers",
250895
+ install: 'python3 .omnius/image-gen/diffusers_text2image.py --model ChuckMcSneed/FLUX.1-dev --steps 28 --guidance 3.5 --width 1024 --height 1024 --prompt "..." --output .omnius/images/out.png',
250896
+ category: "Traceable FLUX fallback",
250897
+ sizeClass: "12B FLUX.1 dev mirror",
250898
+ quality: "Lower-priority mirror fallback for FLUX.1 dev. Use only after official and reputable quantized options fail.",
250899
+ minVramGB: 24,
250900
+ recommendedVramGB: 48,
250901
+ deployment: "Treat as lower-trust than official BFL and well-known quantized conversions; verify provenance and license before relying on it.",
250902
+ steps: 28,
250903
+ guidance: 3.5,
250904
+ width: 1024,
250905
+ height: 1024,
250906
+ fallbackFor: ["black-forest-labs/FLUX.1-dev", "black-forest-labs/FLUX.1-dev-FP8"],
250907
+ note: "Traceable mirror fallback for FLUX.1 dev."
250908
+ },
250820
250909
  {
250821
250910
  id: "stabilityai/stable-diffusion-3.5-large",
250822
250911
  label: "Stable Diffusion 3.5 Large",
@@ -250917,6 +251006,40 @@ var init_image_generate = __esm({
250917
251006
  height: 1024,
250918
251007
  note: "More deployable compact FLUX-family model."
250919
251008
  },
251009
+ {
251010
+ id: "black-forest-labs/FLUX.2-klein-4b-fp8",
251011
+ label: "FLUX.2 Klein 4B FP8",
251012
+ backend: "diffusers",
251013
+ install: 'python3 .omnius/image-gen/diffusers_text2image.py --model black-forest-labs/FLUX.2-klein-4b-fp8 --steps 8 --width 1024 --height 1024 --prompt "..." --output .omnius/images/out.png',
251014
+ category: "Official FLUX fallback",
251015
+ sizeClass: "4B compact FLUX-family FP8",
251016
+ quality: "Official lower-precision FLUX.2 Klein route with better deployment fit than full-precision 4B.",
251017
+ minVramGB: 8,
251018
+ recommendedVramGB: 12,
251019
+ deployment: "Preferred lower-memory official FLUX.2 fallback when compatible with the current loader.",
251020
+ steps: 8,
251021
+ width: 1024,
251022
+ height: 1024,
251023
+ fallbackFor: ["black-forest-labs/FLUX.2-klein-4B", "x/flux2-klein"],
251024
+ note: "Official FP8 fallback for FLUX.2 Klein."
251025
+ },
251026
+ {
251027
+ id: "black-forest-labs/FLUX.2-klein-4b-nvfp4",
251028
+ label: "FLUX.2 Klein 4B NVFP4",
251029
+ backend: "diffusers",
251030
+ install: 'python3 .omnius/image-gen/diffusers_text2image.py --model black-forest-labs/FLUX.2-klein-4b-nvfp4 --steps 8 --width 1024 --height 1024 --prompt "..." --output .omnius/images/out.png',
251031
+ category: "Official FLUX fallback",
251032
+ sizeClass: "4B compact FLUX-family NVFP4",
251033
+ quality: "Official NVIDIA-oriented low-precision FLUX.2 Klein fallback.",
251034
+ minVramGB: 8,
251035
+ recommendedVramGB: 12,
251036
+ deployment: "Use when the runtime/GPU supports the NVFP4 path; otherwise the fallback ladder continues.",
251037
+ steps: 8,
251038
+ width: 1024,
251039
+ height: 1024,
251040
+ fallbackFor: ["black-forest-labs/FLUX.2-klein-4B", "x/flux2-klein", "black-forest-labs/FLUX.2-klein-4b-fp8"],
251041
+ note: "Official NVFP4 fallback for FLUX.2 Klein."
251042
+ },
250920
251043
  {
250921
251044
  id: "deepseek-ai/Janus-Pro-7B",
250922
251045
  label: "Janus-Pro-7B",
@@ -251265,7 +251388,7 @@ if __name__ == "__main__":
251265
251388
  `;
251266
251389
  ImageGenerateTool = class {
251267
251390
  name = "generate_image";
251268
- description = "Generate an image from a text prompt using a local image-generation backend. Supports Ollama image models (x/z-image-turbo, x/flux2-klein), Python Diffusers models (SDXL Turbo default, FLUX.1 dev, SD3.5 Large, Tiny-SD, LCM, Sana Sprint), and stable-diffusion.cpp local checkpoints/GGUF. When fallback is enabled, auto generation tries ranked high-quality candidates first and falls back to smaller models if setup, download, or generation fails. Saves a PNG under .omnius/images and returns the file path.";
251391
+ description = "Generate an image from a text prompt using a local image-generation backend. Supports Ollama image models (x/z-image-turbo, x/flux2-klein), Python Diffusers models (SDXL Turbo default, FLUX.1 dev, SD3.5 Large, Tiny-SD, LCM, Sana Sprint), and stable-diffusion.cpp local checkpoints/GGUF. When fallback is enabled, auto generation tries ranked high-quality candidates first, including official/traceable FLUX fallbacks for Black Forest Labs models, and then falls back to smaller models if setup, download, or generation fails. Saves a PNG under .omnius/images and returns the file path.";
251269
251392
  parameters = {
251270
251393
  type: "object",
251271
251394
  properties: {
@@ -251929,7 +252052,7 @@ ${errText.slice(0, 800)}`,
251929
252052
  });
251930
252053
 
251931
252054
  // packages/execution/dist/tools/audio-generate.js
251932
- import { spawn as spawn10 } from "node:child_process";
252055
+ import { execFileSync as execFileSync2, spawn as spawn10 } from "node:child_process";
251933
252056
  import { existsSync as existsSync24, readdirSync as readdirSync10, statSync as statSync9 } from "node:fs";
251934
252057
  import { chmod as chmod4, mkdir as mkdir12, writeFile as writeFile17 } from "node:fs/promises";
251935
252058
  import { join as join37 } from "node:path";
@@ -251953,6 +252076,56 @@ function backendPackages(backend) {
251953
252076
  return TANGOFLUX_PACKAGES;
251954
252077
  return DIFFUSERS_AUDIO_PACKAGES;
251955
252078
  }
252079
+ function detectLegacyCudaComputeCapability() {
252080
+ try {
252081
+ const out = execFileSync2("nvidia-smi", ["--query-gpu=compute_cap,name", "--format=csv,noheader,nounits"], {
252082
+ encoding: "utf8",
252083
+ timeout: 5e3,
252084
+ stdio: ["ignore", "pipe", "ignore"]
252085
+ }).trim();
252086
+ const first2 = out.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
252087
+ const match = first2?.match(/^(\d+)\.(\d+)\s*,?\s*(.*)$/);
252088
+ if (!match)
252089
+ return null;
252090
+ const major = Number(match[1]);
252091
+ const minor = Number(match[2]);
252092
+ if (!Number.isFinite(major) || !Number.isFinite(minor))
252093
+ return null;
252094
+ return { major, minor, name: match[3]?.trim() || void 0 };
252095
+ } catch {
252096
+ return null;
252097
+ }
252098
+ }
252099
+ function isLegacyCudaCapability(major, minor) {
252100
+ return major < 7 || major === 7 && minor < 5;
252101
+ }
252102
+ function torchInstallPlan(forceLegacyCuda = false) {
252103
+ if (process.env["OMNIUS_AUDIO_TORCH_INDEX_URL"]) {
252104
+ return {
252105
+ args: ["torch", "torchaudio", "--index-url", process.env["OMNIUS_AUDIO_TORCH_INDEX_URL"]],
252106
+ description: `env override ${process.env["OMNIUS_AUDIO_TORCH_INDEX_URL"]}`
252107
+ };
252108
+ }
252109
+ if (forceLegacyCuda) {
252110
+ return {
252111
+ args: ["torch==2.3.1", "torchaudio==2.3.1", "--index-url", "https://download.pytorch.org/whl/cu118"],
252112
+ description: "runtime-detected legacy CUDA GPU; using PyTorch 2.3.1 cu118 to avoid cuDNN 9 incompatibility"
252113
+ };
252114
+ }
252115
+ if (process.platform === "linux" && process.arch === "x64") {
252116
+ const gpu = detectLegacyCudaComputeCapability();
252117
+ if (gpu && isLegacyCudaCapability(gpu.major, gpu.minor)) {
252118
+ return {
252119
+ args: ["torch==2.3.1", "torchaudio==2.3.1", "--index-url", "https://download.pytorch.org/whl/cu118"],
252120
+ description: `CUDA legacy GPU SM ${gpu.major}.${gpu.minor}${gpu.name ? ` ${gpu.name}` : ""}; using PyTorch 2.3.1 cu118 to avoid cuDNN 9 incompatibility`
252121
+ };
252122
+ }
252123
+ }
252124
+ return { args: ["torch", "torchaudio"], description: "default PyTorch wheel selection" };
252125
+ }
252126
+ function withoutTorchPackages(packages) {
252127
+ return packages.filter((pkg) => pkg !== "torch" && pkg !== "torchaudio");
252128
+ }
251956
252129
  function backendImportCheck(backend) {
251957
252130
  if (backend === "transformers")
251958
252131
  return "import torch, torchaudio, transformers, scipy\nfrom transformers import AutoProcessor, MusicgenForConditionalGeneration\n";
@@ -252151,6 +252324,69 @@ async function pythonCanImport2(command, code8, repoRoot, env2) {
252151
252324
  async function pythonImportResult(command, code8, repoRoot, env2) {
252152
252325
  return await runProcess3(command, ["-c", code8], { cwd: repoRoot, timeoutMs: 6e4, env: env2 });
252153
252326
  }
252327
+ async function torchRuntimeCompatibilityResult(command, repoRoot, env2) {
252328
+ const code8 = [
252329
+ "import json, sys",
252330
+ "import torch",
252331
+ "payload={'torch': getattr(torch, '__version__', '?'), 'cuda_available': bool(torch.cuda.is_available())}",
252332
+ "if torch.cuda.is_available():",
252333
+ " cap=torch.cuda.get_device_capability(0)",
252334
+ " cudnn=torch.backends.cudnn.version() or 0",
252335
+ " payload.update({'capability': list(cap), 'cudnn': int(cudnn), 'device': torch.cuda.get_device_name(0)})",
252336
+ " if int(cudnn) >= 90000 and tuple(cap) < (7, 5):",
252337
+ " print(json.dumps(payload))",
252338
+ " raise SystemExit(42)",
252339
+ "print(json.dumps(payload))"
252340
+ ].join("\n");
252341
+ return await runProcess3(command, ["-c", code8], { cwd: repoRoot, timeoutMs: 6e4, env: env2 });
252342
+ }
252343
+ async function repairTorchRuntime(command, repoRoot, env2, forceLegacyCuda = false, onProgress) {
252344
+ const plan = torchInstallPlan(forceLegacyCuda);
252345
+ onProgress?.({ stage: "setup", message: `Installing PyTorch runtime: ${plan.description}` });
252346
+ const result = await runProcess3(command, [
252347
+ "-m",
252348
+ "pip",
252349
+ "install",
252350
+ "--progress-bar",
252351
+ "on",
252352
+ "--prefer-binary",
252353
+ "--force-reinstall",
252354
+ ...plan.args
252355
+ ], {
252356
+ cwd: repoRoot,
252357
+ timeoutMs: 18e5,
252358
+ env: env2,
252359
+ progressLabel: `Installing PyTorch runtime (${plan.description})`,
252360
+ onProgress
252361
+ });
252362
+ if (result.code !== 0) {
252363
+ throw new Error(`Failed to install compatible PyTorch runtime (${plan.description}).
252364
+ ${trimProcessText2(result.stderr || result.stdout)}`);
252365
+ }
252366
+ }
252367
+ async function ensureCompatibleTorchRuntime(command, repoRoot, env2, onProgress) {
252368
+ const existing = await torchRuntimeCompatibilityResult(command, repoRoot, env2);
252369
+ if (existing.code === 0)
252370
+ return;
252371
+ if (existing.code === 42) {
252372
+ await repairTorchRuntime(command, repoRoot, env2, true, onProgress);
252373
+ } else {
252374
+ await repairTorchRuntime(command, repoRoot, env2, false, onProgress);
252375
+ }
252376
+ const installed = await torchRuntimeCompatibilityResult(command, repoRoot, env2);
252377
+ if (installed.code === 0)
252378
+ return;
252379
+ if (installed.code === 42) {
252380
+ await repairTorchRuntime(command, repoRoot, env2, true, onProgress);
252381
+ const repaired = await torchRuntimeCompatibilityResult(command, repoRoot, env2);
252382
+ if (repaired.code === 0)
252383
+ return;
252384
+ throw new Error(`Audio-generation PyTorch runtime remains incompatible after cu118 repair.
252385
+ ${trimProcessText2(repaired.stderr || repaired.stdout)}`);
252386
+ }
252387
+ throw new Error(`Audio-generation PyTorch runtime could not be prepared.
252388
+ ${trimProcessText2(installed.stderr || installed.stdout)}`);
252389
+ }
252154
252390
  function formatAudioSetupFailure(backend, text) {
252155
252391
  const body = trimProcessText2(text);
252156
252392
  const lowered = text.toLowerCase();
@@ -252161,6 +252397,9 @@ function formatAudioSetupFailure(backend, text) {
252161
252397
  if (lowered.includes("cuda") && lowered.includes("not available")) {
252162
252398
  notes2.push("CUDA was not available to the selected Python environment; install a Torch build matching this machine's CUDA runtime or use CPU-compatible settings.");
252163
252399
  }
252400
+ if (lowered.includes("cudnn version") && lowered.includes("sm < 7.5")) {
252401
+ notes2.push("The installed PyTorch wheel uses cuDNN 9 on a legacy CUDA GPU. Omnius now repairs audio-generation venvs by reinstalling PyTorch 2.3.1 from the cu118 index for SM < 7.5 hardware.");
252402
+ }
252164
252403
  return [body, ...notes2.map((note) => `
252165
252404
  ${note}`)].filter(Boolean).join("");
252166
252405
  }
@@ -252189,9 +252428,13 @@ ${trimProcessText2(created.stderr || created.stdout)}`);
252189
252428
  }
252190
252429
  }
252191
252430
  if (await pythonCanImport2(command, backendImportCheck(backend), repoRoot, pythonEnv)) {
252192
- return { command, env: pythonEnv };
252431
+ await ensureCompatibleTorchRuntime(command, repoRoot, pythonEnv, onProgress);
252432
+ if (await pythonCanImport2(command, backendImportCheck(backend), repoRoot, pythonEnv)) {
252433
+ return { command, env: pythonEnv };
252434
+ }
252193
252435
  }
252194
252436
  const packages = backendPackages(backend);
252437
+ await ensureCompatibleTorchRuntime(command, repoRoot, pythonEnv, onProgress);
252195
252438
  onProgress?.({ stage: "setup", message: `Installing ${backend} audio-generation Python packages` });
252196
252439
  const pipArgs = [
252197
252440
  "-m",
@@ -252203,7 +252446,7 @@ ${trimProcessText2(created.stderr || created.stdout)}`);
252203
252446
  ...backend === "audiocraft" ? ["--only-binary", "av"] : [],
252204
252447
  "-U",
252205
252448
  "pip",
252206
- ...packages
252449
+ ...withoutTorchPackages(packages)
252207
252450
  ];
252208
252451
  const pip = await runProcess3(command, pipArgs, {
252209
252452
  cwd: repoRoot,
@@ -252220,6 +252463,12 @@ ${formatAudioSetupFailure(backend, pip.stderr || pip.stdout)}`);
252220
252463
  if (importCheck.code !== 0) {
252221
252464
  throw new Error(`Audio-generation Python environment at ${venvDir} was created, but required ${backend} imports still fail.
252222
252465
  ${formatAudioSetupFailure(backend, importCheck.stderr || importCheck.stdout)}`);
252466
+ }
252467
+ await ensureCompatibleTorchRuntime(command, repoRoot, pythonEnv, onProgress);
252468
+ if (!await pythonCanImport2(command, backendImportCheck(backend), repoRoot, pythonEnv)) {
252469
+ const retry = await pythonImportResult(command, backendImportCheck(backend), repoRoot, pythonEnv);
252470
+ throw new Error(`Audio-generation Python environment at ${venvDir} lost required ${backend} imports after PyTorch repair.
252471
+ ${formatAudioSetupFailure(backend, retry.stderr || retry.stdout)}`);
252223
252472
  }
252224
252473
  return { command, env: pythonEnv };
252225
252474
  }
@@ -252911,6 +253160,10 @@ def _snapshot_model(repo_id):
252911
253160
  def _device():
252912
253161
  import torch
252913
253162
  if torch.cuda.is_available():
253163
+ cap = torch.cuda.get_device_capability(0)
253164
+ cudnn = torch.backends.cudnn.version() or 0
253165
+ if int(cudnn) >= 90000 and tuple(cap) < (7, 5):
253166
+ raise RuntimeError(f"PyTorch cuDNN {cudnn} is incompatible with CUDA device {torch.cuda.get_device_name(0)} SM {cap[0]}.{cap[1]}; recreate the audio venv or let Omnius repair it with a cu118-compatible Torch wheel")
252914
253167
  return "cuda"
252915
253168
  if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
252916
253169
  return "mps"
@@ -253103,6 +253356,10 @@ def _snapshot_model(repo_id):
253103
253356
  def _device():
253104
253357
  import torch
253105
253358
  if torch.cuda.is_available():
253359
+ cap = torch.cuda.get_device_capability(0)
253360
+ cudnn = torch.backends.cudnn.version() or 0
253361
+ if int(cudnn) >= 90000 and tuple(cap) < (7, 5):
253362
+ raise RuntimeError(f"PyTorch cuDNN {cudnn} is incompatible with CUDA device {torch.cuda.get_device_name(0)} SM {cap[0]}.{cap[1]}; recreate the audio venv or let Omnius repair it with a cu118-compatible Torch wheel")
253106
253363
  return "cuda"
253107
253364
  if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
253108
253365
  return "mps"
@@ -477268,7 +477525,7 @@ var require_path_browserify = __commonJS({
477268
477525
  return path11.slice(start2, end);
477269
477526
  }
477270
477527
  },
477271
- extname: function extname16(path11) {
477528
+ extname: function extname17(path11) {
477272
477529
  assertPath(path11);
477273
477530
  var startDot = -1;
477274
477531
  var startPart = 0;
@@ -507429,22 +507686,22 @@ Saved to: ${tempFile}`,
507429
507686
  });
507430
507687
 
507431
507688
  // packages/execution/dist/tools/audio-playback.js
507432
- import { execFileSync as execFileSync2, execSync as execSync29, spawn as spawn16 } from "node:child_process";
507689
+ import { execFileSync as execFileSync3, execSync as execSync29, spawn as spawn16 } from "node:child_process";
507433
507690
  import { copyFileSync as copyFileSync2, existsSync as existsSync40, statSync as statSync18, writeFileSync as writeFileSync16, mkdirSync as mkdirSync16, readdirSync as readdirSync14 } from "node:fs";
507434
507691
  import { basename as basename12, extname as extname10, isAbsolute, join as join58 } from "node:path";
507435
507692
  import { homedir as homedir14, tmpdir as tmpdir11 } from "node:os";
507436
507693
  function hasCommand3(command) {
507437
507694
  try {
507438
507695
  if (process.platform === "win32") {
507439
- execFileSync2("where", [command], { stdio: "ignore", timeout: 2e3 });
507696
+ execFileSync3("where", [command], { stdio: "ignore", timeout: 2e3 });
507440
507697
  } else {
507441
- execFileSync2("command", ["-v", command], { stdio: "ignore", timeout: 2e3 });
507698
+ execFileSync3("command", ["-v", command], { stdio: "ignore", timeout: 2e3 });
507442
507699
  }
507443
507700
  return true;
507444
507701
  } catch {
507445
507702
  if (process.platform !== "win32") {
507446
507703
  try {
507447
- execFileSync2("which", [command], { stdio: "ignore", timeout: 2e3 });
507704
+ execFileSync3("which", [command], { stdio: "ignore", timeout: 2e3 });
507448
507705
  return true;
507449
507706
  } catch {
507450
507707
  return false;
@@ -507499,7 +507756,7 @@ function playSoundFile(file, opts = {}) {
507499
507756
  };
507500
507757
  }
507501
507758
  try {
507502
- execFileSync2(command.command, command.args, { timeout: opts.timeoutMs ?? 3e5, stdio: "pipe" });
507759
+ execFileSync3(command.command, command.args, { timeout: opts.timeoutMs ?? 3e5, stdio: "pipe" });
507503
507760
  return { ok: true, player: command.label };
507504
507761
  } catch (err) {
507505
507762
  return { ok: false, error: `Playback via ${command.label} failed: ${err instanceof Error ? err.message.slice(0, 300) : String(err).slice(0, 300)}` };
@@ -507646,13 +507903,13 @@ function ensureSupertonicInstalled() {
507646
507903
  const py = findPython32();
507647
507904
  if (!py)
507648
507905
  throw new Error("python3 is required to set up Supertonic TTS.");
507649
- execFileSync2(py, ["-m", "venv", join58(voiceDir(), "supertonic3-venv")], { stdio: "pipe", timeout: 18e4 });
507906
+ execFileSync3(py, ["-m", "venv", join58(voiceDir(), "supertonic3-venv")], { stdio: "pipe", timeout: 18e4 });
507650
507907
  }
507651
507908
  try {
507652
- execFileSync2(venvPy, ["-c", "import supertonic"], { stdio: "pipe", timeout: 1e4 });
507909
+ execFileSync3(venvPy, ["-c", "import supertonic"], { stdio: "pipe", timeout: 1e4 });
507653
507910
  } catch {
507654
- execFileSync2(venvPy, ["-m", "pip", "install", "--quiet", "--upgrade", "pip"], { stdio: "pipe", timeout: 12e4 });
507655
- execFileSync2(venvPy, ["-m", "pip", "install", "--quiet", "supertonic"], { stdio: "pipe", timeout: 6e5 });
507911
+ execFileSync3(venvPy, ["-m", "pip", "install", "--quiet", "--upgrade", "pip"], { stdio: "pipe", timeout: 12e4 });
507912
+ execFileSync3(venvPy, ["-m", "pip", "install", "--quiet", "supertonic"], { stdio: "pipe", timeout: 6e5 });
507656
507913
  }
507657
507914
  mkdirSync16(voiceDir(), { recursive: true });
507658
507915
  writeFileSync16(supertonicInferScript(), SUPERTONIC_INFER_PY, "utf-8");
@@ -507667,19 +507924,19 @@ function ensureMlxInstalled() {
507667
507924
  const py = findPython32();
507668
507925
  if (!py)
507669
507926
  throw new Error("python3 is required to set up MLX Audio.");
507670
- execFileSync2(py, ["-m", "venv", join58(voiceDir(), "mlx-venv")], { stdio: "pipe", timeout: 18e4 });
507927
+ execFileSync3(py, ["-m", "venv", join58(voiceDir(), "mlx-venv")], { stdio: "pipe", timeout: 18e4 });
507671
507928
  }
507672
507929
  try {
507673
- execFileSync2(venvPy, ["-c", "import mlx_audio"], { stdio: "pipe", timeout: 1e4 });
507930
+ execFileSync3(venvPy, ["-c", "import mlx_audio"], { stdio: "pipe", timeout: 1e4 });
507674
507931
  } catch {
507675
- execFileSync2(venvPy, ["-m", "pip", "install", "--quiet", "--upgrade", "pip"], { stdio: "pipe", timeout: 12e4 });
507676
- execFileSync2(venvPy, ["-m", "pip", "install", "--quiet", "mlx-audio"], { stdio: "pipe", timeout: 6e5 });
507932
+ execFileSync3(venvPy, ["-m", "pip", "install", "--quiet", "--upgrade", "pip"], { stdio: "pipe", timeout: 12e4 });
507933
+ execFileSync3(venvPy, ["-m", "pip", "install", "--quiet", "mlx-audio"], { stdio: "pipe", timeout: 6e5 });
507677
507934
  }
507678
507935
  return venvPy;
507679
507936
  }
507680
507937
  function pythonCanImportLuxTts(venvPy) {
507681
507938
  try {
507682
- execFileSync2(venvPy, [
507939
+ execFileSync3(venvPy, [
507683
507940
  "-c",
507684
507941
  "import sys, os; sys.path.insert(0, os.environ['LUXTTS_REPO_PATH']); from zipvoice.luxvoice import LuxTTS; print('ok')"
507685
507942
  ], {
@@ -507693,7 +507950,7 @@ function pythonCanImportLuxTts(venvPy) {
507693
507950
  }
507694
507951
  }
507695
507952
  function pipInstall(venvPy, packages, timeout2 = 9e5) {
507696
- execFileSync2(venvPy, ["-m", "pip", "install", "--prefer-binary", ...packages], {
507953
+ execFileSync3(venvPy, ["-m", "pip", "install", "--prefer-binary", ...packages], {
507697
507954
  stdio: "pipe",
507698
507955
  timeout: timeout2,
507699
507956
  env: process.env
@@ -507711,9 +507968,9 @@ function ensureLuxttsInstalled() {
507711
507968
  if (!py)
507712
507969
  throw new Error("python3 is required to set up LuxTTS voice cloning.");
507713
507970
  if (!existsSync40(venvPy)) {
507714
- execFileSync2(py, ["-m", "venv", luxttsVenvDir()], { stdio: "pipe", timeout: 18e4 });
507971
+ execFileSync3(py, ["-m", "venv", luxttsVenvDir()], { stdio: "pipe", timeout: 18e4 });
507715
507972
  }
507716
- execFileSync2(venvPy, ["-m", "pip", "install", "--upgrade", "pip", "wheel", "setuptools<81"], {
507973
+ execFileSync3(venvPy, ["-m", "pip", "install", "--upgrade", "pip", "wheel", "setuptools<81"], {
507717
507974
  stdio: "pipe",
507718
507975
  timeout: 3e5
507719
507976
  });
@@ -507721,7 +507978,7 @@ function ensureLuxttsInstalled() {
507721
507978
  if (!existsSync40(join58(repoDir, "zipvoice", "luxvoice.py"))) {
507722
507979
  if (!hasCommand3("git"))
507723
507980
  throw new Error("git is required to set up LuxTTS voice cloning.");
507724
- execFileSync2("git", ["clone", "--depth", "1", "https://github.com/ysharma3501/LuxTTS.git", repoDir], {
507981
+ execFileSync3("git", ["clone", "--depth", "1", "https://github.com/ysharma3501/LuxTTS.git", repoDir], {
507725
507982
  stdio: "pipe",
507726
507983
  timeout: 3e5
507727
507984
  });
@@ -507761,10 +508018,10 @@ function ensurePiperInstalled() {
507761
508018
  if (!py)
507762
508019
  throw new Error("python3 is required to set up Piper TTS.");
507763
508020
  mkdirSync16(voiceDir(), { recursive: true });
507764
- execFileSync2(py, ["-m", "venv", piperVenvDir()], { stdio: "pipe", timeout: 18e4 });
508021
+ execFileSync3(py, ["-m", "venv", piperVenvDir()], { stdio: "pipe", timeout: 18e4 });
507765
508022
  const venvPy = process.platform === "win32" ? join58(piperVenvDir(), "Scripts", "python.exe") : join58(piperVenvDir(), "bin", "python3");
507766
- execFileSync2(venvPy, ["-m", "pip", "install", "--quiet", "--upgrade", "pip"], { stdio: "pipe", timeout: 12e4 });
507767
- execFileSync2(venvPy, ["-m", "pip", "install", "--quiet", "piper-tts"], { stdio: "pipe", timeout: 6e5 });
508023
+ execFileSync3(venvPy, ["-m", "pip", "install", "--quiet", "--upgrade", "pip"], { stdio: "pipe", timeout: 12e4 });
508024
+ execFileSync3(venvPy, ["-m", "pip", "install", "--quiet", "piper-tts"], { stdio: "pipe", timeout: 6e5 });
507768
508025
  }
507769
508026
  if (!existsSync40(bin)) {
507770
508027
  throw new Error("Piper TTS installed but the piper executable was not found in the managed venv.");
@@ -508294,7 +508551,7 @@ ${tried.map((line) => `- ${line}`).join("\n")}`,
508294
508551
  "d=(np.clip(wav.cpu().numpy().squeeze(), -1, 1)*32767).astype(np.int16)",
508295
508552
  "f=wave.open(args['output'], 'wb'); f.setnchannels(1); f.setsampwidth(2); f.setframerate(48000); f.writeframes(d.tobytes()); f.close()"
508296
508553
  ].join("; ");
508297
- execFileSync2(venvPy, ["-c", pyScript, JSON.stringify({ text, output: outputPath2, clone_ref: cloneRef, repo: repoDir, speed })], {
508554
+ execFileSync3(venvPy, ["-c", pyScript, JSON.stringify({ text, output: outputPath2, clone_ref: cloneRef, repo: repoDir, speed })], {
508298
508555
  stdio: "pipe",
508299
508556
  timeout: 12e4,
508300
508557
  env: { ...process.env, LUXTTS_REPO_PATH: repoDir }
@@ -508307,7 +508564,7 @@ ${tried.map((line) => `- ${line}`).join("\n")}`,
508307
508564
  const lang = typeof args["lang"] === "string" ? args["lang"] : "en";
508308
508565
  const speed = numberArg3(args["speed"], 1.05);
508309
508566
  const totalStep = Math.round(numberArg3(args["total_step"], 8));
508310
- const stdout = execFileSync2(venvPy, [supertonicInferScript()], {
508567
+ const stdout = execFileSync3(venvPy, [supertonicInferScript()], {
508311
508568
  input: JSON.stringify({ text, output_path: outputPath2, voice_name: voice, lang, speed, total_step: totalStep }),
508312
508569
  encoding: "utf8",
508313
508570
  stdio: ["pipe", "pipe", "pipe"],
@@ -508330,7 +508587,7 @@ ${tried.map((line) => `- ${line}`).join("\n")}`,
508330
508587
  "args=json.loads(sys.argv[1])",
508331
508588
  "tts_gen.main(['--model', args['model'], '--text', args['text'], '--voice', args['voice'], '--lang_code', args['lang'], '--audio_path', args['output']])"
508332
508589
  ].join("; ");
508333
- execFileSync2(py, ["-c", pyScript, JSON.stringify({ text, model, voice, lang, output: outputPath2 })], {
508590
+ execFileSync3(py, ["-c", pyScript, JSON.stringify({ text, model, voice, lang, output: outputPath2 })], {
508334
508591
  stdio: "pipe",
508335
508592
  timeout: 18e4,
508336
508593
  cwd: tmpdir11()
@@ -508351,7 +508608,7 @@ ${tried.map((line) => `- ${line}`).join("\n")}`,
508351
508608
  } else {
508352
508609
  throw new Error(`${requireModel ? "Raw ONNX" : "Piper"} TTS requires model=<path.onnx> or voice=<path.onnx>.`);
508353
508610
  }
508354
- execFileSync2(piper, argv, { input: text, stdio: ["pipe", "pipe", "pipe"], timeout: 12e4 });
508611
+ execFileSync3(piper, argv, { input: text, stdio: ["pipe", "pipe", "pipe"], timeout: 12e4 });
508355
508612
  return summary;
508356
508613
  }
508357
508614
  synthesizeEspeak(text, outputPath2, args) {
@@ -508359,7 +508616,7 @@ ${tried.map((line) => `- ${line}`).join("\n")}`,
508359
508616
  throw new Error("Local fallback TTS command not found.");
508360
508617
  const voice = typeof args["voice"] === "string" ? args["voice"] : "en";
508361
508618
  const speed = Math.round(numberArg3(args["speed"], 160));
508362
- execFileSync2("espeak-ng", ["-v", voice, "-s", String(speed), "-w", outputPath2, text], {
508619
+ execFileSync3("espeak-ng", ["-v", voice, "-s", String(speed), "-w", outputPath2, text], {
508363
508620
  stdio: "pipe",
508364
508621
  timeout: 6e4
508365
508622
  });
@@ -575505,7 +575762,7 @@ __export(image_ascii_preview_exports, {
575505
575762
  extractSavedImagePath: () => extractSavedImagePath,
575506
575763
  formatImageAsciiContext: () => formatImageAsciiContext
575507
575764
  });
575508
- import { execFileSync as execFileSync3 } from "node:child_process";
575765
+ import { execFileSync as execFileSync4 } from "node:child_process";
575509
575766
  import { createRequire as createRequire5 } from "node:module";
575510
575767
  import { existsSync as existsSync94, readFileSync as readFileSync75, statSync as statSync32 } from "node:fs";
575511
575768
  import { resolve as resolve37 } from "node:path";
@@ -575642,7 +575899,7 @@ function convertWithFfmpeg(imagePath, width, height, timeoutMs) {
575642
575899
  `scale=${width}:${height}`,
575643
575900
  "format=gray"
575644
575901
  ].join(",");
575645
- const raw = execFileSync3(
575902
+ const raw = execFileSync4(
575646
575903
  "ffmpeg",
575647
575904
  [
575648
575905
  "-hide_banner",
@@ -596827,6 +597084,17 @@ var init_tool_policy = __esm({
596827
597084
  "todo_write",
596828
597085
  "web_search",
596829
597086
  "web_fetch",
597087
+ "image_read",
597088
+ "ocr",
597089
+ "ocr_image_advanced",
597090
+ "ocr_pdf",
597091
+ "pdf_to_text",
597092
+ "vision",
597093
+ "transcribe_file",
597094
+ "video_understand",
597095
+ "audio_analyze",
597096
+ "explore_tools",
597097
+ "telegram_media_recent",
596830
597098
  "generate_image",
596831
597099
  "generate_audio",
596832
597100
  "generate_tts",
@@ -596843,6 +597111,17 @@ var init_tool_policy = __esm({
596843
597111
  "web_search",
596844
597112
  "web_fetch",
596845
597113
  "web_crawl",
597114
+ "image_read",
597115
+ "ocr",
597116
+ "ocr_image_advanced",
597117
+ "ocr_pdf",
597118
+ "pdf_to_text",
597119
+ "vision",
597120
+ "transcribe_file",
597121
+ "video_understand",
597122
+ "audio_analyze",
597123
+ "explore_tools",
597124
+ "telegram_media_recent",
596846
597125
  "generate_image",
596847
597126
  "generate_audio",
596848
597127
  "generate_tts",
@@ -597417,12 +597696,12 @@ __export(vision_ingress_exports, {
597417
597696
  queryVisionModel: () => queryVisionModel,
597418
597697
  runVisionIngress: () => runVisionIngress
597419
597698
  });
597420
- import { execFileSync as execFileSync4 } from "node:child_process";
597699
+ import { execFileSync as execFileSync5 } from "node:child_process";
597421
597700
  import { existsSync as existsSync105, readFileSync as readFileSync86, unlinkSync as unlinkSync20 } from "node:fs";
597422
597701
  import { join as join120 } from "node:path";
597423
597702
  function isTesseractAvailable() {
597424
597703
  try {
597425
- execFileSync4("tesseract", ["--version"], { stdio: "ignore", timeout: 3e3 });
597704
+ execFileSync5("tesseract", ["--version"], { stdio: "ignore", timeout: 3e3 });
597426
597705
  return true;
597427
597706
  } catch {
597428
597707
  return false;
@@ -597463,7 +597742,7 @@ function advancedOcr(imagePath) {
597463
597742
  for (const psm of psmModes) {
597464
597743
  const outFile = `${tmpBase}_psm${psm}`;
597465
597744
  try {
597466
- execFileSync4("tesseract", [
597745
+ execFileSync5("tesseract", [
597467
597746
  imagePath,
597468
597747
  outFile,
597469
597748
  "--psm",
@@ -597562,7 +597841,7 @@ var init_vision_ingress = __esm({
597562
597841
 
597563
597842
  // packages/cli/src/tui/telegram-bridge.ts
597564
597843
  import { mkdirSync as mkdirSync60, existsSync as existsSync106, unlinkSync as unlinkSync21, readdirSync as readdirSync36, statSync as statSync36, readFileSync as readFileSync87, writeFileSync as writeFileSync57 } from "node:fs";
597565
- import { join as join121, resolve as resolve39, basename as basename23, relative as relative13, isAbsolute as isAbsolute7 } from "node:path";
597844
+ import { join as join121, resolve as resolve39, basename as basename23, relative as relative13, isAbsolute as isAbsolute7, extname as extname15 } from "node:path";
597566
597845
  import { writeFile as writeFileAsync } from "node:fs/promises";
597567
597846
  import { createHash as createHash19, randomInt } from "node:crypto";
597568
597847
  function parseTelegramInteractionDecision(text, forcedRoute, options2 = {}) {
@@ -597760,6 +598039,19 @@ function summarizeTelegramMessageAttachments(msg) {
597760
598039
  parts.push(`caption: ${truncateTelegramContextLine(msg.media.caption, 180)}`);
597761
598040
  }
597762
598041
  }
598042
+ if (msg.replyToMedia) {
598043
+ const details = [
598044
+ msg.replyToMedia.type,
598045
+ msg.replyToMedia.mimeType,
598046
+ msg.replyToMedia.fileName,
598047
+ msg.replyToMedia.duration ? `${msg.replyToMedia.duration}s` : "",
598048
+ msg.replyToMedia.fileSize ? `${msg.replyToMedia.fileSize} bytes` : ""
598049
+ ].filter(Boolean).join(", ");
598050
+ parts.push(`replied-to media: ${details}`);
598051
+ if (msg.replyToMedia.caption) {
598052
+ parts.push(`replied-to caption: ${truncateTelegramContextLine(msg.replyToMedia.caption, 180)}`);
598053
+ }
598054
+ }
597763
598055
  if (msg.poll) {
597764
598056
  parts.push(`poll: ${truncateTelegramContextLine(msg.poll.question, 180)}`);
597765
598057
  }
@@ -598133,6 +598425,25 @@ function telegramImageMime(media) {
598133
598425
  if (ext === ".tif" || ext === ".tiff") return "image/tiff";
598134
598426
  return "image/jpeg";
598135
598427
  }
598428
+ function telegramCachedMediaIsImage(entry) {
598429
+ if (entry.mediaType === "photo") return true;
598430
+ if (entry.mimeType?.toLowerCase().startsWith("image/")) return true;
598431
+ return TELEGRAM_IMAGE_EXTENSIONS.has(extname15(entry.localPath).toLowerCase());
598432
+ }
598433
+ function telegramCachedMediaIsPdf(entry) {
598434
+ if (entry.mimeType?.toLowerCase() === "application/pdf") return true;
598435
+ return extname15(entry.localPath).toLowerCase() === ".pdf";
598436
+ }
598437
+ function telegramCachedMediaIsAudio(entry) {
598438
+ if (entry.mediaType === "audio" || entry.mediaType === "voice") return true;
598439
+ if (entry.mimeType?.toLowerCase().startsWith("audio/")) return true;
598440
+ return [".wav", ".mp3", ".flac", ".aac", ".m4a", ".ogg", ".opus"].includes(extname15(entry.localPath).toLowerCase());
598441
+ }
598442
+ function telegramCachedMediaIsVideo(entry) {
598443
+ if (entry.mediaType === "video" || entry.mediaType === "video_note" || entry.mediaType === "live_photo") return true;
598444
+ if (entry.mimeType?.toLowerCase().startsWith("video/")) return true;
598445
+ return [".mp4", ".mkv", ".avi", ".mov", ".webm"].includes(extname15(entry.localPath).toLowerCase());
598446
+ }
598136
598447
  function isPathInside(root, path11) {
598137
598448
  const rel = relative13(resolve39(root), resolve39(path11));
598138
598449
  return rel === "" || Boolean(rel) && !rel.startsWith("..") && !isAbsolute7(rel);
@@ -598166,6 +598477,10 @@ function normalizeTelegramUpdate(update2) {
598166
598477
  const username = message2.from?.username ?? message2.sender_chat?.username ?? "";
598167
598478
  const chatType = message2.chat?.type ?? "private";
598168
598479
  const media = normalizeTelegramMedia(message2);
598480
+ const replyTo = message2.reply_to_message && typeof message2.reply_to_message === "object" ? message2.reply_to_message : void 0;
598481
+ const replyToMedia = replyTo ? normalizeTelegramMedia(replyTo) : void 0;
598482
+ const replyToPoll = replyTo ? normalizeTelegramPoll(replyTo.poll) : void 0;
598483
+ const replyToText = replyTo ? replyTo.text || replyTo.caption || (replyToPoll ? formatTelegramPollSummary(replyToPoll) : "") : "";
598169
598484
  const poll = normalizeTelegramPoll(message2.poll);
598170
598485
  const livePhoto = normalizeTelegramLivePhoto(message2.live_photo);
598171
598486
  const text = message2.text || message2.caption || (poll ? formatTelegramPollSummary(poll) : "");
@@ -598180,6 +598495,8 @@ function normalizeTelegramUpdate(update2) {
598180
598495
  chatType,
598181
598496
  chatTitle: message2.chat?.title,
598182
598497
  media,
598498
+ replyToMedia,
598499
+ replyToText: replyToText || void 0,
598183
598500
  poll,
598184
598501
  livePhoto,
598185
598502
  guestQueryId: typeof message2.guest_query_id === "string" ? message2.guest_query_id : void 0,
@@ -598188,9 +598505,9 @@ function normalizeTelegramUpdate(update2) {
598188
598505
  isGuestMessage: sourceUpdateType === "guest_message",
598189
598506
  isDirectMessages: Boolean(message2.chat?.is_direct_messages),
598190
598507
  parentChatId: message2.chat?.parent_chat?.id ?? message2.direct_messages_topic?.parent_topic?.id,
598191
- replyToMessageId: message2.reply_to_message?.message_id,
598192
- replyToUsername: message2.reply_to_message?.from?.username ?? message2.reply_to_message?.sender_chat?.username,
598193
- replyToBot: Boolean(message2.reply_to_message?.from?.is_bot),
598508
+ replyToMessageId: replyTo?.message_id,
598509
+ replyToUsername: replyTo?.from?.username ?? replyTo?.sender_chat?.username,
598510
+ replyToBot: Boolean(replyTo?.from?.is_bot),
598194
598511
  mentionedUsernames: extractTelegramMentionedUsernames(message2, text),
598195
598512
  sourceUpdateType
598196
598513
  };
@@ -598337,7 +598654,7 @@ function renderTelegramSubAgentError(username, error) {
598337
598654
  process.stdout.write(` ${c3.dim("⎿")} ${c3.red("✘")} @${username}: ${c3.dim(preview)}
598338
598655
  `);
598339
598656
  }
598340
- var TELEGRAM_SAFETY_PROMPT, ADMIN_DM_PROMPT, ADMIN_GROUP_PROMPT, TELEGRAM_PUBLIC_SOUL_PROFILE, TELEGRAM_PUBLIC_ORCHESTRATOR_CONTRACT, TELEGRAM_PUBLIC_MEMORY_SCOPE_CONTRACT, GROUP_REPLY_DISCRETION_PROMPT, TELEGRAM_CHAT_MODE_PROMPT, ADMIN_CHAT_PROFILE_PROMPT, TELEGRAM_ACTION_RESPONSE_CONTRACT, TELEGRAM_CHAT_HISTORY_LIMIT, TELEGRAM_CONTEXT_RECENT_DEFAULT, TELEGRAM_CONTEXT_LINE_LIMIT, TELEGRAM_CONTEXT_SAMPLE_LIMIT, TELEGRAM_MEMORY_CARD_LIMIT, TELEGRAM_MEMORY_NOTE_LIMIT, TELEGRAM_MEMORY_STOPWORDS, TELEGRAM_PUBLIC_HELP_COMMANDS, MEDIA_CACHE_TTL_MS, TelegramBridge;
598657
+ var TELEGRAM_SAFETY_PROMPT, ADMIN_DM_PROMPT, ADMIN_GROUP_PROMPT, TELEGRAM_PUBLIC_SOUL_PROFILE, TELEGRAM_PUBLIC_ORCHESTRATOR_CONTRACT, TELEGRAM_PUBLIC_MEMORY_SCOPE_CONTRACT, GROUP_REPLY_DISCRETION_PROMPT, TELEGRAM_CHAT_MODE_PROMPT, ADMIN_CHAT_PROFILE_PROMPT, TELEGRAM_ACTION_RESPONSE_CONTRACT, TELEGRAM_CHAT_HISTORY_LIMIT, TELEGRAM_CONTEXT_RECENT_DEFAULT, TELEGRAM_CONTEXT_LINE_LIMIT, TELEGRAM_CONTEXT_SAMPLE_LIMIT, TELEGRAM_MEMORY_CARD_LIMIT, TELEGRAM_MEMORY_NOTE_LIMIT, TELEGRAM_MEMORY_STOPWORDS, TELEGRAM_PUBLIC_HELP_COMMANDS, TELEGRAM_IMAGE_EXTENSIONS, MEDIA_CACHE_TTL_MS, TelegramBridge;
598341
598658
  var init_telegram_bridge = __esm({
598342
598659
  "packages/cli/src/tui/telegram-bridge.ts"() {
598343
598660
  "use strict";
@@ -598533,6 +598850,7 @@ Telegram response contract:
598533
598850
  "your"
598534
598851
  ]);
598535
598852
  TELEGRAM_PUBLIC_HELP_COMMANDS = /* @__PURE__ */ new Set(["help", "start", "auth", "call"]);
598853
+ TELEGRAM_IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".tif", ".svg"]);
598536
598854
  MEDIA_CACHE_TTL_MS = 30 * 60 * 1e3;
598537
598855
  TelegramBridge = class {
598538
598856
  constructor(botToken, onMessage, agentConfig, repoRoot, toolPolicyConfig) {
@@ -598944,6 +599262,80 @@ Telegram response contract:
598944
599262
  }
598945
599263
  }
598946
599264
  }
599265
+ updateLastTelegramUserMessageText(msg, text) {
599266
+ const sessionKey = this.sessionKeyForMessage(msg);
599267
+ const history = this.chatHistory.get(sessionKey);
599268
+ if (!history || !text.trim()) return;
599269
+ for (let i2 = history.length - 1; i2 >= 0; i2--) {
599270
+ const entry = history[i2];
599271
+ if (entry.role !== "user") continue;
599272
+ if (entry.messageId === msg.messageId || !entry.messageId && entry.text === msg.text) {
599273
+ entry.text = text.trim();
599274
+ entry.mediaSummary = summarizeTelegramMessageAttachments(msg) || entry.mediaSummary;
599275
+ this.updateTelegramMemoryCards(sessionKey, entry);
599276
+ this.saveTelegramConversationState(sessionKey);
599277
+ return;
599278
+ }
599279
+ }
599280
+ }
599281
+ recentTelegramMediaEntries(chatId, limit = 12) {
599282
+ const now = Date.now();
599283
+ return [...this.mediaCache.values()].filter((entry) => {
599284
+ if (chatId !== void 0 && String(entry.chatId) !== String(chatId)) return false;
599285
+ return now - entry.cachedAt <= MEDIA_CACHE_TTL_MS;
599286
+ }).sort((a2, b) => b.cachedAt - a2.cachedAt).slice(0, limit);
599287
+ }
599288
+ telegramMediaEntryMatchesKind(entry, kind) {
599289
+ if (kind === "image") return telegramCachedMediaIsImage(entry);
599290
+ if (kind === "pdf") return telegramCachedMediaIsPdf(entry);
599291
+ if (kind === "audio") return telegramCachedMediaIsAudio(entry);
599292
+ if (kind === "video") return telegramCachedMediaIsVideo(entry);
599293
+ if (kind === "transcribable") {
599294
+ return telegramCachedMediaIsAudio(entry) || telegramCachedMediaIsVideo(entry);
599295
+ }
599296
+ return true;
599297
+ }
599298
+ resolveTelegramScopedMediaPath(rawValue, chatId, currentMsg, kind) {
599299
+ const raw = String(rawValue ?? "").trim();
599300
+ const repoRoot = this.repoRoot || ".";
599301
+ const creativeRoot = telegramCreativeWorkspaceRoot(repoRoot, chatId);
599302
+ const mediaEntries = this.recentTelegramMediaEntries(chatId, 60).filter((entry) => this.telegramMediaEntryMatchesKind(entry, kind));
599303
+ const aliases = /* @__PURE__ */ new Set(["", "latest", "last", "current", "this", "that", "it", "reply", "replied", "replied-to", "replied_to"]);
599304
+ if (aliases.has(raw.toLowerCase())) {
599305
+ const replied = currentMsg?.replyToMessageId ? mediaEntries.find((entry2) => entry2.messageId === currentMsg.replyToMessageId) : void 0;
599306
+ const entry = replied ?? mediaEntries[0];
599307
+ if (!entry) {
599308
+ return { ok: false, error: `No recent ${kind} media is available in this Telegram chat scope.` };
599309
+ }
599310
+ return { ok: true, path: entry.localPath };
599311
+ }
599312
+ const matchingEntry = mediaEntries.find((entry) => {
599313
+ if (resolve39(entry.localPath) === resolve39(raw)) return true;
599314
+ if (basename23(entry.localPath) === raw) return true;
599315
+ if (entry.fileUniqueId === raw || entry.fileId === raw) return true;
599316
+ if (entry.messageId && String(entry.messageId) === raw) return true;
599317
+ return false;
599318
+ });
599319
+ if (matchingEntry) return { ok: true, path: matchingEntry.localPath };
599320
+ const creativeCandidate = isAbsolute7(raw) ? resolve39(raw) : resolve39(creativeRoot, raw);
599321
+ if (isPathInside(creativeRoot, creativeCandidate) && existsSync106(creativeCandidate)) {
599322
+ return { ok: true, path: creativeCandidate };
599323
+ }
599324
+ return {
599325
+ ok: false,
599326
+ error: `Path is outside this Telegram chat's media/workspace scope or does not exist: ${raw || "(empty)"}`
599327
+ };
599328
+ }
599329
+ resolveTelegramScopedOutputPath(rawValue, chatId, fallbackName) {
599330
+ const repoRoot = this.repoRoot || ".";
599331
+ const creativeRoot = telegramCreativeWorkspaceRoot(repoRoot, chatId);
599332
+ const raw = String(rawValue || fallbackName).trim() || fallbackName;
599333
+ const outputPath2 = isAbsolute7(raw) ? resolve39(raw) : resolve39(creativeRoot, raw);
599334
+ if (!isPathInside(creativeRoot, outputPath2)) {
599335
+ return { ok: false, error: `Output path must stay inside this Telegram chat's creative workspace: ${raw}` };
599336
+ }
599337
+ return { ok: true, path: outputPath2 };
599338
+ }
598947
599339
  updateTelegramParticipantProfile(sessionKey, msg, text) {
598948
599340
  const participantKey = String(msg.fromUserId || msg.username || msg.firstName || "unknown");
598949
599341
  const participants = this.chatParticipants.get(sessionKey) ?? /* @__PURE__ */ new Map();
@@ -599118,6 +599510,22 @@ ${notes2}`;
599118
599510
  sections.push(`### Zettelkasten Memory Recall
599119
599511
  ${cardLines.join("\n")}`);
599120
599512
  }
599513
+ const recentMedia = this.recentTelegramMediaEntries(msg.chatId, 10);
599514
+ if (recentMedia.length > 0) {
599515
+ const mediaLines = recentMedia.map((entry) => {
599516
+ const kind = telegramCachedMediaIsImage(entry) ? "image" : entry.mediaType;
599517
+ const replyMark = msg.replyToMessageId && entry.messageId === msg.replyToMessageId ? " replied-to" : "";
599518
+ const caption = entry.caption ? ` caption:${truncateTelegramContextLine(entry.caption, 120)}` : "";
599519
+ const extracted = entry.extractedContent ? `
599520
+ ${truncateTelegramContextLine(entry.extractedContent.replace(/\s+/g, " "), 220)}` : "";
599521
+ return `- message_id ${entry.messageId}${replyMark}: ${kind}; path ${entry.localPath}; file ${basename23(entry.localPath)}${caption}${extracted}`;
599522
+ });
599523
+ sections.push([
599524
+ "### Recent Chat Media",
599525
+ "Use these paths only as tool inputs when the user asks about media in this chat. Do not quote local paths in the visible Telegram reply.",
599526
+ mediaLines.join("\n")
599527
+ ].join("\n"));
599528
+ }
599121
599529
  if (olderCount > 0) {
599122
599530
  const older = history.slice(0, olderCount);
599123
599531
  const bySpeaker = /* @__PURE__ */ new Map();
@@ -599814,8 +600222,8 @@ Join: ${newUrl}`);
599814
600222
  }
599815
600223
  }
599816
600224
  let steeringText = msg.text;
599817
- if (msg.media) {
599818
- const mediaContext = await this.processMedia(msg);
600225
+ if (msg.media || msg.replyToMedia) {
600226
+ const mediaContext = await this.processMediaContextForMessage(msg);
599819
600227
  if (mediaContext) {
599820
600228
  steeringText += `
599821
600229
 
@@ -599889,8 +600297,8 @@ ${mediaContext}`;
599889
600297
  this.tuiWrite(() => renderTelegramSubAgentStart(msg.username, msg.text, isAdminDM));
599890
600298
  try {
599891
600299
  let mediaContext = "";
599892
- if (msg.media) {
599893
- mediaContext = await this.processMedia(msg);
600300
+ if (msg.media || msg.replyToMedia) {
600301
+ mediaContext = await this.processMediaContextForMessage(msg);
599894
600302
  }
599895
600303
  const result = await this.runSubAgent(msg, subAgent, mediaContext);
599896
600304
  if (subAgent.typingInterval) {
@@ -599992,8 +600400,8 @@ ${mediaContext}`;
599992
600400
  this.tuiWrite(() => renderTelegramSubAgentEvent(msg.username, `admin chat with full context/tools (${this.interactionMode})`));
599993
600401
  try {
599994
600402
  let mediaContext = "";
599995
- if (msg.media) {
599996
- mediaContext = await this.processMedia(msg);
600403
+ if (msg.media || msg.replyToMedia) {
600404
+ mediaContext = await this.processMediaContextForMessage(msg);
599997
600405
  }
599998
600406
  const result = await this.runSubAgent(msg, subAgent, mediaContext, "chat");
599999
600407
  if (subAgent.typingInterval) {
@@ -600076,7 +600484,7 @@ ${mediaContext}`;
600076
600484
  }
600077
600485
  this.tuiWrite(() => renderTelegramSubAgentEvent(msg.username, `live inference: chat reply (${this.interactionMode})`));
600078
600486
  try {
600079
- const mediaContext = msg.media || msg.livePhoto ? "Attachment received. Quick-chat mode does not inspect media; use action mode for media analysis." : "";
600487
+ const mediaContext = msg.media || msg.replyToMedia || msg.livePhoto ? await this.processMediaContextForMessage(msg) : "";
600080
600488
  const finalText = await this.runTelegramChatCompletion(
600081
600489
  msg,
600082
600490
  toolContext,
@@ -600569,6 +600977,128 @@ ${lines.join("\n\n")}` };
600569
600977
  }
600570
600978
  };
600571
600979
  }
600980
+ if (tool.name === "image_read") {
600981
+ return {
600982
+ ...tool,
600983
+ description: "Read only images from this Telegram chat's media cache or creative workspace. Use path='reply' for the replied-to image or path='latest' for the most recent chat image.",
600984
+ execute: async (args) => {
600985
+ const resolved = this.resolveTelegramScopedMediaPath(args["path"], chatId, currentMsg, "image");
600986
+ if (!resolved.ok) return { success: false, output: "", error: resolved.error };
600987
+ return tool.execute({ ...args, path: resolved.path });
600988
+ }
600989
+ };
600990
+ }
600991
+ if (tool.name === "ocr") {
600992
+ return {
600993
+ ...tool,
600994
+ description: "Extract text only from images in this Telegram chat's media cache or creative workspace. Use path='reply' or path='latest' for chat media references.",
600995
+ execute: async (args) => {
600996
+ const resolved = this.resolveTelegramScopedMediaPath(args["path"], chatId, currentMsg, "image");
600997
+ if (!resolved.ok) return { success: false, output: "", error: resolved.error };
600998
+ return tool.execute({ ...args, path: resolved.path });
600999
+ }
601000
+ };
601001
+ }
601002
+ if (tool.name === "vision") {
601003
+ return {
601004
+ ...tool,
601005
+ description: "Analyze only images from this Telegram chat's media cache or creative workspace. Use image='reply' for the replied-to image or image='latest' for the most recent chat image.",
601006
+ execute: async (args) => {
601007
+ const resolved = this.resolveTelegramScopedMediaPath(args["image"], chatId, currentMsg, "image");
601008
+ if (!resolved.ok) return { success: false, output: "", error: resolved.error };
601009
+ return tool.execute({ ...args, image: resolved.path });
601010
+ }
601011
+ };
601012
+ }
601013
+ if (tool.name === "ocr_image_advanced") {
601014
+ return {
601015
+ ...tool,
601016
+ description: "Advanced OCR only for images in this Telegram chat's media cache or creative workspace. Batch directory mode is disabled in public Telegram scope.",
601017
+ execute: async (args) => {
601018
+ if (args["batch"] === true) return { success: false, output: "", error: "Batch directory OCR is not available in public Telegram scope." };
601019
+ const resolved = this.resolveTelegramScopedMediaPath(args["image"], chatId, currentMsg, "image");
601020
+ if (!resolved.ok) return { success: false, output: "", error: resolved.error };
601021
+ const next = { ...args, image: resolved.path };
601022
+ if (typeof next["output_dir"] === "string" && next["output_dir"].trim()) {
601023
+ const output = this.resolveTelegramScopedOutputPath(next["output_dir"], chatId, "ocr-output");
601024
+ if (!output.ok) return { success: false, output: "", error: output.error };
601025
+ next["output_dir"] = output.path;
601026
+ }
601027
+ return tool.execute(next);
601028
+ }
601029
+ };
601030
+ }
601031
+ if (tool.name === "transcribe_file") {
601032
+ return {
601033
+ ...tool,
601034
+ description: "Transcribe only audio/video files from this Telegram chat's media cache or creative workspace. Use path='reply' or path='latest' for chat media references.",
601035
+ execute: async (args) => {
601036
+ const resolved = this.resolveTelegramScopedMediaPath(args["path"], chatId, currentMsg, "transcribable");
601037
+ if (!resolved.ok) return { success: false, output: "", error: resolved.error };
601038
+ return tool.execute({ ...args, path: resolved.path });
601039
+ }
601040
+ };
601041
+ }
601042
+ if (tool.name === "pdf_to_text") {
601043
+ return {
601044
+ ...tool,
601045
+ description: "Extract text only from PDFs in this Telegram chat's media cache or creative workspace. Use path='reply' or path='latest' for chat document references.",
601046
+ execute: async (args) => {
601047
+ const resolved = this.resolveTelegramScopedMediaPath(args["path"], chatId, currentMsg, "pdf");
601048
+ if (!resolved.ok) return { success: false, output: "", error: resolved.error };
601049
+ return tool.execute({ ...args, path: resolved.path });
601050
+ }
601051
+ };
601052
+ }
601053
+ if (tool.name === "ocr_pdf") {
601054
+ return {
601055
+ ...tool,
601056
+ description: "OCR only PDFs from this Telegram chat's media cache or creative workspace. Output, when requested, is forced into this chat's creative workspace.",
601057
+ execute: async (args) => {
601058
+ const input = this.resolveTelegramScopedMediaPath(args["input"], chatId, currentMsg, "pdf");
601059
+ if (!input.ok) return { success: false, output: "", error: input.error };
601060
+ const next = { ...args, input: input.path };
601061
+ if (typeof next["output"] === "string" && next["output"].trim()) {
601062
+ const output = this.resolveTelegramScopedOutputPath(next["output"], chatId, `ocr-${Date.now()}.pdf`);
601063
+ if (!output.ok) return { success: false, output: "", error: output.error };
601064
+ next["output"] = output.path;
601065
+ }
601066
+ return tool.execute(next);
601067
+ }
601068
+ };
601069
+ }
601070
+ if (tool.name === "video_understand") {
601071
+ return {
601072
+ ...tool,
601073
+ description: "Analyze only video files from this Telegram chat's media cache or creative workspace. URL download is disabled in public Telegram scope; use path='reply' or path='latest'.",
601074
+ execute: async (args) => {
601075
+ if (args["url"]) return { success: false, output: "", error: "URL video analysis is not available in public Telegram scope. Use a video posted in this chat." };
601076
+ const resolved = this.resolveTelegramScopedMediaPath(args["path"], chatId, currentMsg, "video");
601077
+ if (!resolved.ok) return { success: false, output: "", error: resolved.error };
601078
+ return tool.execute({ ...args, path: resolved.path });
601079
+ }
601080
+ };
601081
+ }
601082
+ if (tool.name === "audio_analyze") {
601083
+ return {
601084
+ ...tool,
601085
+ description: "Analyze only audio files from this Telegram chat's media cache or creative workspace. Microphone/listen mode is disabled in public Telegram scope.",
601086
+ execute: async (args) => {
601087
+ if (String(args["action"] || "").toLowerCase() === "listen") {
601088
+ return { success: false, output: "", error: "Continuous microphone listening is not available in Telegram public scope." };
601089
+ }
601090
+ const resolved = this.resolveTelegramScopedMediaPath(args["file"] ?? args["path"], chatId, currentMsg, "audio");
601091
+ if (!resolved.ok) return { success: false, output: "", error: resolved.error };
601092
+ return tool.execute({ ...args, file: resolved.path, path: resolved.path });
601093
+ }
601094
+ };
601095
+ }
601096
+ if (tool.name === "explore_tools") {
601097
+ return {
601098
+ ...tool,
601099
+ description: "List and explain the tools available in this Telegram public/group scope. Do not invent unavailable tool names."
601100
+ };
601101
+ }
600572
601102
  return tool;
600573
601103
  });
600574
601104
  }
@@ -600732,11 +601262,16 @@ Scoped workspace: ${scopedRoot}`,
600732
601262
  new ImageReadTool(repoRoot),
600733
601263
  new OCRTool(repoRoot),
600734
601264
  new VisionTool(repoRoot),
601265
+ new OcrImageAdvancedTool(repoRoot),
600735
601266
  new OcrPdfTool(repoRoot),
600736
601267
  new PdfToTextTool(repoRoot),
600737
601268
  // Transcription tools
600738
601269
  new TranscribeFileTool(repoRoot),
600739
- new TranscribeUrlTool(repoRoot)
601270
+ new TranscribeUrlTool(repoRoot),
601271
+ new VideoUnderstandTool(repoRoot),
601272
+ new AudioAnalyzeTool(),
601273
+ new ExploreToolsTool(),
601274
+ this.buildTelegramMediaRecentTool(chatId, msg)
600740
601275
  ];
600741
601276
  const adminTools = [
600742
601277
  new ShellTool(repoRoot),
@@ -600839,6 +601374,55 @@ Scoped workspace: ${scopedRoot}`,
600839
601374
  ]);
600840
601375
  return tools.filter((tool) => !blocked.has(tool.name));
600841
601376
  }
601377
+ buildTelegramMediaRecentTool(chatId, currentMsg) {
601378
+ const bridge = this;
601379
+ return {
601380
+ name: "telegram_media_recent",
601381
+ description: "List recent media files available in this Telegram chat scope, including safe aliases for image_read, ocr, vision, transcribe_file, pdf_to_text, video_understand, and audio_analyze.",
601382
+ parameters: {
601383
+ type: "object",
601384
+ properties: {
601385
+ kind: {
601386
+ type: "string",
601387
+ enum: ["media", "image", "audio", "video", "pdf", "transcribable"],
601388
+ description: "Filter by media kind. Defaults to all recent chat media."
601389
+ },
601390
+ limit: { type: "number", description: "Maximum entries to return, 1-20. Default: 10." }
601391
+ }
601392
+ },
601393
+ async execute(args) {
601394
+ const start2 = performance.now();
601395
+ const kind = String(args["kind"] || "media").toLowerCase();
601396
+ const limit = typeof args["limit"] === "number" && Number.isFinite(args["limit"]) ? Math.max(1, Math.min(20, Math.floor(args["limit"]))) : 10;
601397
+ const entries = bridge.recentTelegramMediaEntries(chatId, 60).filter((entry) => bridge.telegramMediaEntryMatchesKind(entry, kind)).slice(0, limit);
601398
+ if (entries.length === 0) {
601399
+ return { success: true, output: `No recent ${kind} media is available in this Telegram chat scope.`, durationMs: performance.now() - start2 };
601400
+ }
601401
+ const lines = entries.map((entry, index) => {
601402
+ const parts = [
601403
+ `${index + 1}. message_id ${entry.messageId || "unknown"}`,
601404
+ currentMsg?.replyToMessageId === entry.messageId ? "replied-to" : "",
601405
+ telegramCachedMediaIsImage(entry) ? "image" : telegramCachedMediaIsPdf(entry) ? "pdf" : telegramCachedMediaIsAudio(entry) ? "audio" : telegramCachedMediaIsVideo(entry) ? "video" : entry.mediaType,
601406
+ `file=${basename23(entry.localPath)}`,
601407
+ `path=${entry.localPath}`,
601408
+ entry.caption ? `caption=${truncateTelegramContextLine(entry.caption, 140)}` : ""
601409
+ ].filter(Boolean);
601410
+ const extracted = entry.extractedContent ? `
601411
+ context: ${truncateTelegramContextLine(entry.extractedContent.replace(/\s+/g, " "), 240)}` : "";
601412
+ return `${parts.join("; ")}${extracted}`;
601413
+ });
601414
+ return {
601415
+ success: true,
601416
+ output: [
601417
+ "Recent scoped Telegram media:",
601418
+ "Use path='reply' for replied-to media, path='latest' for the most recent matching item, or one of the listed paths.",
601419
+ lines.join("\n")
601420
+ ].join("\n"),
601421
+ durationMs: performance.now() - start2
601422
+ };
601423
+ }
601424
+ };
601425
+ }
600842
601426
  imageGenerationDefaultsForRepo(repoRoot) {
600843
601427
  const settings = resolveSettings(repoRoot);
600844
601428
  return {
@@ -601056,30 +601640,36 @@ ${knownList}` : "Private-user telegram_send_file target must be this DM or a kno
601056
601640
  * Downloads the file, runs it through the appropriate pipeline,
601057
601641
  * caches it, and returns a text description for the agent.
601058
601642
  */
601059
- async processMedia(msg) {
601060
- if (!msg.media) return "";
601061
- const { type, fileId, fileUniqueId, mimeType, caption } = msg.media;
601062
- const isImageMedia = telegramMediaIsImage(msg.media);
601643
+ async processMedia(msg, source = "message") {
601644
+ const media = source === "reply" ? msg.replyToMedia : msg.media;
601645
+ if (!media) return "";
601646
+ const { type, fileId, fileUniqueId, mimeType, caption } = media;
601647
+ const isImageMedia = telegramMediaIsImage(media);
601648
+ const sourceMessageId = source === "reply" ? msg.replyToMessageId : msg.messageId;
601649
+ const sourceLabel = source === "reply" ? "replied-to " : "";
601063
601650
  let ext = ".bin";
601064
- if (isImageMedia) ext = telegramImageExtension(msg.media);
601651
+ if (isImageMedia) ext = telegramImageExtension(media);
601065
601652
  else if (type === "audio" || type === "voice") ext = ".ogg";
601066
601653
  else if (type === "video" || type === "video_note" || type === "live_photo") ext = ".mp4";
601067
- else if (msg.media.fileName) {
601068
- const dotIdx = msg.media.fileName.lastIndexOf(".");
601069
- if (dotIdx >= 0) ext = msg.media.fileName.slice(dotIdx);
601654
+ else if (media.fileName) {
601655
+ const dotIdx = media.fileName.lastIndexOf(".");
601656
+ if (dotIdx >= 0) ext = media.fileName.slice(dotIdx);
601070
601657
  }
601071
601658
  const localPath = await this.downloadTelegramFile(fileId, ext);
601072
601659
  if (!localPath) return `[Media: ${type} — failed to download]`;
601073
601660
  const cacheEntry = {
601074
601661
  localPath,
601075
601662
  fileId,
601663
+ fileUniqueId,
601076
601664
  chatId: msg.chatId,
601665
+ messageId: sourceMessageId ?? 0,
601077
601666
  username: msg.username,
601078
601667
  mediaType: type,
601079
601668
  mimeType,
601669
+ caption,
601080
601670
  cachedAt: Date.now()
601081
601671
  };
601082
- this.mediaCache.set(fileUniqueId, cacheEntry);
601672
+ this.mediaCache.set(`${String(msg.chatId)}:${String(sourceMessageId ?? 0)}:${fileUniqueId}`, cacheEntry);
601083
601673
  const metadataKey = String(msg.chatId);
601084
601674
  if (!this.mediaMetadata.has(metadataKey)) {
601085
601675
  this.mediaMetadata.set(metadataKey, []);
@@ -601100,7 +601690,7 @@ ${knownList}` : "Private-user telegram_send_file target must be this DM or a kno
601100
601690
  {
601101
601691
  path: localPath,
601102
601692
  buffer: readFileSync87(localPath),
601103
- mime: telegramImageMime(msg.media)
601693
+ mime: telegramImageMime(media)
601104
601694
  },
601105
601695
  this.agentConfig?.model ?? ""
601106
601696
  );
@@ -601109,10 +601699,10 @@ ${knownList}` : "Private-user telegram_send_file target must be this DM or a kno
601109
601699
  } catch {
601110
601700
  }
601111
601701
  if (visionContext) {
601112
- description = `[Image received: ${localPath}${caption ? ` — caption: "${caption}"` : ""}
601702
+ description = `[${sourceLabel}image received: ${localPath}${caption ? ` — caption: "${caption}"` : ""}
601113
601703
  ${visionContext}]`;
601114
601704
  } else {
601115
- description = `[Image received and saved to ${localPath}${caption ? ` — caption: "${caption}"` : ""}. You can use image_read or vision tools to analyze it if available.]`;
601705
+ description = `[${sourceLabel}image received and saved to ${localPath}${caption ? ` — caption: "${caption}"` : ""}. You can use image_read, ocr, or vision tools to analyze it.]`;
601116
601706
  }
601117
601707
  try {
601118
601708
  await fetch("http://127.0.0.1:11435/v1/memory/ingest", {
@@ -601136,9 +601726,9 @@ ${visionContext}]`;
601136
601726
  } catch {
601137
601727
  }
601138
601728
  if (transcription) {
601139
- description = `[Voice message transcribed: "${transcription}"${caption ? ` — caption: "${caption}"` : ""}]`;
601729
+ description = `[${sourceLabel}voice message transcribed: "${transcription}"${caption ? ` — caption: "${caption}"` : ""}]`;
601140
601730
  } else {
601141
- description = `[Audio/voice message received and saved to ${localPath}${caption ? ` — caption: "${caption}"` : ""}. You can use transcribe_file to transcribe it if available.]`;
601731
+ description = `[${sourceLabel}audio/voice message received and saved to ${localPath}${caption ? ` — caption: "${caption}"` : ""}. You can use transcribe_file to transcribe it.]`;
601142
601732
  }
601143
601733
  try {
601144
601734
  await fetch("http://127.0.0.1:11435/v1/memory/ingest", {
@@ -601151,13 +601741,30 @@ ${visionContext}]`;
601151
601741
  }
601152
601742
  } else if (type === "video" || type === "video_note" || type === "live_photo") {
601153
601743
  const label = type === "live_photo" ? "Live photo" : "Video";
601154
- description = `[${label} received and saved to ${localPath}${caption ? ` — caption: "${caption}"` : ""}.]`;
601744
+ description = `[${sourceLabel}${label.toLowerCase()} received and saved to ${localPath}${caption ? ` — caption: "${caption}"` : ""}. You can use video_understand or transcribe_file to analyze it.]`;
601155
601745
  } else if (type === "document") {
601156
- description = `[Document received: ${msg.media.fileName || "unnamed"}${mimeType ? ` (${mimeType})` : ""}, saved to ${localPath}${caption ? ` — caption: "${caption}"` : ""}.]`;
601746
+ description = `[${sourceLabel}document received: ${media.fileName || "unnamed"}${mimeType ? ` (${mimeType})` : ""}, saved to ${localPath}${caption ? ` — caption: "${caption}"` : ""}.]`;
601157
601747
  }
601158
601748
  cacheEntry.extractedContent = description;
601159
601749
  return description;
601160
601750
  }
601751
+ async processMediaContextForMessage(msg) {
601752
+ const parts = [];
601753
+ if (msg.media) {
601754
+ const current = await this.processMedia(msg, "message");
601755
+ if (current) parts.push(current);
601756
+ }
601757
+ if (msg.replyToMedia) {
601758
+ const replied = await this.processMedia(msg, "reply");
601759
+ if (replied) parts.push(replied);
601760
+ }
601761
+ const text = parts.join("\n\n");
601762
+ if (text) this.updateLastTelegramUserMessageText(msg, `${msg.text}
601763
+
601764
+ [Media context]
601765
+ ${text}`.trim());
601766
+ return text;
601767
+ }
601161
601768
  /** Clean up expired media cache entries (older than 30 minutes) */
601162
601769
  cleanupMediaCache() {
601163
601770
  const now = Date.now();
@@ -625743,7 +626350,7 @@ var clipboard_media_exports = {};
625743
626350
  __export(clipboard_media_exports, {
625744
626351
  pasteClipboardImageToFile: () => pasteClipboardImageToFile
625745
626352
  });
625746
- import { execFileSync as execFileSync5, execSync as execSync58 } from "node:child_process";
626353
+ import { execFileSync as execFileSync6, execSync as execSync58 } from "node:child_process";
625747
626354
  import { mkdirSync as mkdirSync72, readFileSync as readFileSync99, rmSync as rmSync5, writeFileSync as writeFileSync67 } from "node:fs";
625748
626355
  import { join as join136 } from "node:path";
625749
626356
  function pasteClipboardImageToFile(repoRoot) {
@@ -625760,7 +626367,7 @@ function readClipboardImage() {
625760
626367
  try {
625761
626368
  execSync58("command -v pngpaste", { stdio: "ignore", timeout: 1e3 });
625762
626369
  const tmp = `/tmp/omnius-clipboard-${Date.now()}.png`;
625763
- execFileSync5("pngpaste", [tmp], { timeout: 3e3 });
626370
+ execFileSync6("pngpaste", [tmp], { timeout: 3e3 });
625764
626371
  const buffer2 = readFileSync99(tmp);
625765
626372
  try {
625766
626373
  rmSync5(tmp);
@@ -625780,7 +626387,7 @@ function readClipboardImage() {
625780
626387
  ];
625781
626388
  for (const attempt of attempts) {
625782
626389
  try {
625783
- const buffer2 = execFileSync5(attempt.cmd, attempt.args, { timeout: 3e3, maxBuffer: 25 * 1024 * 1024 });
626390
+ const buffer2 = execFileSync6(attempt.cmd, attempt.args, { timeout: 3e3, maxBuffer: 25 * 1024 * 1024 });
625784
626391
  if (buffer2.length > 0) return { buffer: buffer2, mime: attempt.mime, ext: attempt.ext };
625785
626392
  } catch {
625786
626393
  continue;
@@ -625797,7 +626404,7 @@ function readClipboardImage() {
625797
626404
  "$img.Save($ms,[Drawing.Imaging.ImageFormat]::Png);",
625798
626405
  "[Console]::OpenStandardOutput().Write($ms.ToArray(),0,$ms.Length)"
625799
626406
  ].join("");
625800
- const buffer2 = execFileSync5("powershell.exe", ["-NoProfile", "-Command", ps], {
626407
+ const buffer2 = execFileSync6("powershell.exe", ["-NoProfile", "-Command", ps], {
625801
626408
  timeout: 5e3,
625802
626409
  maxBuffer: 25 * 1024 * 1024
625803
626410
  });
@@ -625816,7 +626423,7 @@ var init_clipboard_media = __esm({
625816
626423
 
625817
626424
  // packages/cli/src/tui/interactive.ts
625818
626425
  import { cwd } from "node:process";
625819
- import { resolve as resolve44, join as join137, dirname as dirname38, extname as extname15, relative as relative14 } from "node:path";
626426
+ import { resolve as resolve44, join as join137, dirname as dirname38, extname as extname16, relative as relative14 } from "node:path";
625820
626427
  import { createRequire as createRequire8 } from "node:module";
625821
626428
  import { fileURLToPath as fileURLToPath18 } from "node:url";
625822
626429
  import {
@@ -633118,7 +633725,7 @@ Execute this skill now. Follow the behavioral guidance above.`;
633118
633725
  const imgPath = resolve44(repoRoot, cleanPath);
633119
633726
  const imgBuffer = readFileSync100(imgPath);
633120
633727
  const base642 = imgBuffer.toString("base64");
633121
- const ext = extname15(cleanPath).toLowerCase();
633728
+ const ext = extname16(cleanPath).toLowerCase();
633122
633729
  const mime = ext === ".png" ? "image/png" : ext === ".gif" ? "image/gif" : ext === ".webp" ? "image/webp" : "image/jpeg";
633123
633730
  const asciiContext = await renderAsciiPreviewForImage(
633124
633731
  imgPath,
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "omnius",
9
- "version": "1.0.21",
9
+ "version": "1.0.22",
10
10
  "bundleDependencies": [
11
11
  "image-to-ascii"
12
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnius",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",