diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index bba6be7..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,67 +0,0 @@ -# CI for J.A.R.V.I.S. Rust fork. -# -# Runs on every push to active branches and on every PR targeting master. -# Three jobs in parallel: -# - test: cargo test for jarvis-core (the only crate with portable tests -# — jarvis-app needs the Vosk DLL + mic, so we don't run it in CI) -# - clippy: lints on the whole workspace (warnings = errors) -# - check: `cargo check` proves the workspace compiles end-to-end without -# waiting for the full release build -# -# We deliberately do NOT run a release build in CI: jarvis-gui pulls Node+npm -# and the WiX/Tauri bundler, which would make CI ~10 minutes per push. Local -# `cargo build --release` is the gate before merging into master. - -name: Rust CI - -on: - push: - branches: [feature/pc-tools, master, main] - pull_request: - branches: [master, main] - -env: - CARGO_TERM_COLOR: always - RUSTFLAGS: "-Dwarnings" - -jobs: - test: - name: cargo test (jarvis-core) - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - with: - shared-key: "windows-stable" - - name: Run tests - run: cargo test -p jarvis-core --lib --no-fail-fast - - clippy: - name: cargo clippy - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - uses: Swatinem/rust-cache@v2 - with: - shared-key: "windows-stable" - - name: Clippy - run: cargo clippy -p jarvis-core --lib --no-deps -- -A clippy::all - - check: - name: cargo check (workspace) - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - with: - shared-key: "windows-stable" - - uses: actions/setup-node@v4 - with: - node-version: '20' - - name: Check core + app + cli - run: cargo check -p jarvis-core -p jarvis-app -p jarvis-cli diff --git a/.gitignore b/.gitignore index baac0c0..b707412 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,6 @@ # Logs logs *.log - -# Per-developer environment + ad-hoc scratch files -dev.env -dll_probe*.py -imports_full.txt -*.scratch.* npm-debug.log* yarn-debug.log* yarn-error.log* @@ -62,15 +56,3 @@ tree.txt # Tauri-generated platform schemas (regenerated on every build) crates/jarvis-gui/gen/schemas/*-schema.json - -# Piper TTS binaries (installed via tools/piper/install.ps1, not committed) -tools/piper/piper.exe -tools/piper/*.dll -tools/piper/espeak-ng-data/ -tools/piper/voices/ -tools/piper/libtashkeel_model.ort -tools/piper/pkgconfig/ - -# Silero model cache (downloaded by torch.hub on first synth) -tools/silero/.silero_cache/ -tools/silero/*.pt diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index f4dc346..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,295 +0,0 @@ -# J.A.R.V.I.S. — Architecture - -This is the code map for the Rust fork. If you're here to fix a bug or add a -feature, start by reading the relevant section, then jump to the file. - -The Python fork at `C:\Jarvis\python` follows the same conceptual flow but is -a single-process Python daemon — much smaller surface area. This doc covers -the Rust side. - -## Crate layout - -``` -crates/ - jarvis-core library: STT, wake-word, TTS, intent, commands, IPC, LLM, - Lua sandbox, scheduler, memory, profiles. ZERO runtime, only - building blocks. Other crates wire it together. - jarvis-app daemon binary. Holds the microphone, runs the listening loop, - dispatches commands, starts the scheduler thread. Tray icon. - jarvis-gui Tauri 2 + Svelte frontend. Shows command list, settings, mic - state. Communicates with jarvis-app via IPC websocket. - jarvis-cli debug CLI (classify / execute / list). Currently has a known - runtime panic on startup; use jarvis-app instead. -``` - -``` -resources/commands// - command.toml — id, phrases per language, script path, sandbox level, timeout - *.lua — script(s); one per command id when multiple ids share a pack -``` - -The `tools/` directory hosts optional native helpers (Piper TTS binary, Silero -helper script). Both are downloaded on demand via `tools/piper/install.ps1` etc. - -## Data flow (mic → action) - -``` -[1] pv_recorder ──► raw 16 kHz mono PCM -[2] webrtc-vad ───► voice activity windows -[3] Vosk (wake-word + STT) ──► utf-8 transcript -[4] intent classifier (MiniLM) ──► (intent_id, confidence)? -[5] levenshtein fallback ──► (pack, command)? -[6] llm_router (IMBA-1) ──► canonical phrase substitution? -[7] llm_fallback chat ──► spoken reply -[8] command execution ──► Lua sandbox / Python subprocess / WinAPI call -[9] TTS (sapi | piper | silero) ──► speech out -[10] voices::play_* ──► reaction sounds (ok, error, not_found) -``` - -Each step lives in a separate module — replace any one and the rest still -works. - -| Step | Module | -|------|---------------------------------------------------| -| 1 | `jarvis-core::recorder` | -| 2 | `jarvis-core::audio_processing::vad::webrtc` | -| 3 | `jarvis-core::stt` + `jarvis-core::listener` | -| 4 | `jarvis-core::intent` | -| 5 | `jarvis-core::commands::fetch_command` | -| 6 | `jarvis-app::llm_router` | -| 7 | `jarvis-app::llm_fallback` | -| 8 | `jarvis-core::commands::execute_command` | -| 9 | `jarvis-core::tts` | -| 10 | `jarvis-core::voices` | - -The listening loop driving all of this is `jarvis-app/src/app.rs::start()`. - -## Configuration - -Two layers: - -1. **`db::structs::Settings`** — persistent user preferences (voice, language, - wake-word engine, noise-suppression backend). Lives in a sqlite file under - `/`. Edited via the GUI Settings page. - -2. **`runtime_config`** — environment-variable knobs read at startup. See - `crates/jarvis-core/src/runtime_config.rs` for the full list with doc - comments. Examples: - - `JARVIS_TTS=sapi|piper|silero` - - `JARVIS_LLM=groq|ollama` - - `JARVIS_LLM_ROUTER=1` (default on), `JARVIS_LLM_ROUTER_THRESHOLD=0.55` - - `GROQ_TOKEN`, `GROQ_MODEL`, `OLLAMA_BASE_URL`, `OLLAMA_MODEL` - - `runtime_config::log_effective_config()` is called once on `main()` so the - active values appear in the startup log. - -`dev.env` next to the exe is auto-loaded by `dotenvy`. Walks up 5 parents — so -running from `crates/jarvis-app/` during development still works. - -## TTS pipeline - -``` -text ─► text_utils::sanitize_for_speech ─► TtsBackend::speak(text, opts) - │ - ┌─────────────────┼─────────────────┐ - ▼ ▼ ▼ - SapiBackend PiperBackend SileroBackend - (PowerShell) (subprocess) (python helper) - │ │ │ - ▼ ▼ ▼ - SAPI direct WAV → play_wav WAV → play_wav - (shared) (shared) -``` - -`tts::backend()` is initialised lazily on first call; subsequent calls reuse -the same `Arc`. Backend is picked by `JARVIS_TTS` env var -with auto-detect fallback: if `tools/piper/piper.exe` exists, use Piper. - -`play_wav` is the shared synchronous WAV player (PowerShell SoundPlayer on -Windows, no-op stub elsewhere). Used by both Piper and Silero — kept in -`tts/mod.rs` as `pub(crate)` to deduplicate. - -## Lua sandbox - -`mlua` 0.11 with three sandbox levels: - -| Level | Allowed | -|-----------|--------------------------------------------| -| Minimal | log, sleep, speak, audio, context, cmd | -| Standard | + http, llm, state, fs (within pack dir) | -| Full | + system.exec, clipboard.set, abs paths | - -Default is `standard`. Set per command in `command.toml` via `sandbox = "full"`. - -Sandbox is enforced by: -- `LuaEngine::new(level)` controls which `StdLib` flags are loaded -- `lua/engine.rs::register_api` gates HTTP/state/fs registration on the level -- `globals.set("io", Nil)` removes the `io` stdlib unless `Full` -- `os.execute / os.exit / os.remove / os.rename / os.setlocale` removed - even in `Full` mode (security defence) - -### Available APIs for command scripts - -See `crates/jarvis-core/src/lua/api/*.rs`. Quick reference: - -```lua -jarvis.speak(text, opts?) -- TTS; opts.lang/.async/.raw -jarvis.cmd.ok(msg?) -- boilerplate-killer: play_ok + speak + {chain=false} -jarvis.cmd.error(msg?) -- play_error + speak + {chain=false} -jarvis.cmd.not_found(msg?) -- play_not_found + speak + {chain=false} -jarvis.cmd.chain_ok(msg?) -- like ok() but chain=true - -jarvis.audio.play_ok / play_error / play_not_found / play_reply / play_goodbye / play_thanks -jarvis.context.{phrase, command_id, command_path, language, time, slots} - -jarvis.text.strip_trigger(phrase, {triggers...}) -jarvis.text.contains_any(phrase, {needles...}) - -jarvis.llm({messages}, {opts}) -- Groq or Ollama, selected by JARVIS_LLM -jarvis.http.{get, post, post_json, json} - -jarvis.memory.{remember, recall, search, forget, all} -jarvis.profile.{active, active_name, set, list, allows} -jarvis.scheduler.{add, list, count, remove, clear} -jarvis.vision.{screenshot, describe} -- HTTP sandbox required - -jarvis.fs.{read, write, append, exists, is_file, is_dir, list, mkdir, remove} -jarvis.state.{get, set} -- pack-scoped k/v -jarvis.system.{open, exec, notify, env, platform, clipboard.{get, set}} -jarvis.settings.{get, set} -- db settings table -``` - -### Recommended pack structure - -```lua --- Single-id pack: -local phrase = (jarvis.context.phrase or ""):lower() -local body = jarvis.text.strip_trigger(phrase, { "trigger1", "trigger2" }) -body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if body == "" then - return jarvis.cmd.error("Что именно?") -end - --- ... do work ... - -return jarvis.cmd.ok("Готово.") -``` - -For multi-id packs (one Lua script shared between several `[[commands]]`), -dispatch on `jarvis.context.command_id`. The `profile_switch/switch.lua` is a -good example — maps `profile.work` → `work`, `profile.game` → `game`, etc. - -## Background services - -Two long-running threads beyond the listening loop: - -1. **`scheduler` tick thread** (`crates/jarvis-core/src/scheduler.rs`). - Wakes every 30 seconds, fires due tasks (Speak via TTS or Lua scripts). - Persists changes to `/schedule.json`. - -2. **`ipc::start_server`** (`crates/jarvis-core/src/ipc/`). Tokio - websocket server on a configurable port. The GUI connects to this to - send commands and receive events (`SpeechRecognized`, `LlmReply`, - `CommandExecuted`, etc.). - -## How to add a new command pack - -1. Create `resources/commands//`. -2. Add `command.toml`: - ```toml - [[commands]] - id = "your_pack.thing" - type = "lua" - script = "thing.lua" - sandbox = "standard" - timeout = 5000 - - [commands.phrases] - ru = ["сделай штуку", ...] - en = ["do the thing", ...] - ``` -3. Write `thing.lua`. Start from the snippet under "Recommended pack structure". -4. Add a `commands::tests` assertion in `crates/jarvis-core/src/commands/tests.rs` - so the parser regression test covers your TOML (the existing - `every_command_toml_parses` already loads any new dir automatically). -5. Run `cargo test -p jarvis-core --lib commands::tests`. -6. Rebuild jarvis-app: it'll pick up the new pack on next start. - -For Python parity, mirror the YAML entry + handler in `C:\Jarvis\python\`. - -## How to add a new Rust core feature - -1. Add module under `crates/jarvis-core/src/.rs`. Pure data layer + - thin global wrapper if needed (see `long_term_memory.rs` as template). -2. Register in `crates/jarvis-core/src/lib.rs` (`pub mod ;`). -3. Expose Lua API at `crates/jarvis-core/src/lua/api/.rs` if scripts - need it. Register in `lua/api.rs` and call from `lua/engine.rs`. -4. Wire init from `crates/jarvis-app/src/main.rs`. -5. Add a `runtime_config::ENV_*` constant if there's an env-driven knob. -6. Add unit tests in a `#[cfg(test)] mod tests {}` block in the new file. -7. Document the public surface in this file (data flow / API section). - -## How to add a new TTS backend - -Implement `TtsBackend` (see `crates/jarvis-core/src/tts/mod.rs`). Use the -`super::play_wav` helper if your engine emits WAV files. Wire selection in -`init_backend()` near the top of the same file. Document the env var in -`runtime_config.rs`. - -## Testing - -``` -cargo test -p jarvis-core --lib # all jarvis-core unit tests -cargo test -p jarvis-core --lib scheduler:: # filter by module -``` - -Test coverage map: - -| Module | Tests | -|----------------------|----------------------------------------------| -| `text_utils` | sanitize_for_speech (5 cases) | -| `long_term_memory` | normalize_key, search_in, build_context, serde (8) | -| `profiles` | allows_command logic, serde (6) | -| `scheduler` | parse_*, next_fire (7) | -| `runtime_config` | get_bool / get_parse fallbacks (2) | -| `llm::client` | response parsing, env handling, helpers (4) | -| `llm::history` | conversation history truncation (5) | -| `commands::tests` | every command.toml parses + has phrases (3) | -| `lua::tests` | sandbox levels, fs escape, timeout (4) | -| `audio_processing::vad::listen_window` | listening window logic (4) | - -Run-time integration tests are manual: -- mic available? → `jarvis-app` logs `recorder::init` -- wake word fires? → log shows `WakeWordDetected` -- TTS speaks? → check Piper/SAPI logs -- Scheduler fires? → wait 30s after `scheduler::add` with `in 30 seconds` - -## Build - -```powershell -# One-time: set up MSVC env (PowerShell 7 / pwsh): -$vc = "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvars64.bat" -cmd /c "`"$vc`" >NUL && set" | % { if ($_ -match '^([^=]+)=(.*)$') { [Environment]::SetEnvironmentVariable($matches[1], $matches[2], 'Process') } } - -# Then: -cargo build --release -p jarvis-app -p jarvis-gui -``` - -The `jarvis-gui` `build.rs` auto-invokes `npm run build` and emits -`cargo:rerun-if-changed=frontend/src` so changes to the Svelte frontend are -picked up by a plain `cargo build`. No need to run `cargo tauri build` -manually unless you want a packaged installer. - -## Git workflow - -- `master` is protected. -- `feature/pc-tools` is the integration branch where everything is currently - shipping. -- Self-hosted git on Forgejo at `http://192.168.0.10:3000/bossiara13/J.A.R.V.I.S-rust`. - Push command: `NO_PROXY=192.168.0.10,localhost,127.0.0.1` + `git -c http.proxy= push forgejo ` - (the local V2Ray proxy at 10809 will otherwise block LAN traffic). -- GitHub origin at `https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-rust` — - manually pushed for public visibility. - -Commit messages: descriptive, no `Co-Authored-By: Claude` tag (project rule). diff --git a/Cargo.lock b/Cargo.lock index 264df03..9a15428 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1554,12 +1554,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - [[package]] name = "downcast-rs" version = "1.2.1" @@ -3270,8 +3264,6 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" name = "jarvis-app" version = "0.1.0" dependencies = [ - "dotenvy", - "embed-resource", "glib 0.21.5", "gtk", "image", @@ -3281,14 +3273,11 @@ dependencies = [ "parking_lot", "platform-dirs", "rand 0.8.5", - "serde", - "serde_json", "simple-log", "tokio", "tray-icon", "winapi", "winit", - "winrt-notification", ] [[package]] @@ -3350,7 +3339,6 @@ dependencies = [ name = "jarvis-gui" version = "0.1.0" dependencies = [ - "dotenvy", "jarvis-core", "lazy_static", "log", diff --git a/Cargo.toml b/Cargo.toml index d22ee3d..d3ea1d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,9 @@ resolver = "2" [workspace.package] version = "0.1.0" -authors = ["Bossiara13"] +authors = ["Abraham Tugalov (original)", "Bossiara13 (fork)"] license = "GPL-3.0-only" -repository = "https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-rust" +repository = "https://github.com/Priler/jarvis" edition = "2021" [workspace.dependencies] @@ -54,5 +54,4 @@ tokenizers = { version = "0.22", default-features = false } regex = "1" sys-locale = "0.3" webrtc-vad = "0.4" -dotenvy = "0.15" diff --git a/README.md b/README.md index e04a638..1fcb1c9 100644 --- a/README.md +++ b/README.md @@ -1,311 +1,177 @@ -# J.A.R.V.I.S. (Rust) — форк Bossiara13 +# J.A.R.V.I.S (Rust) — форк Bossiara13 -Локальный голосовой ассистент для Windows. Форк Rust-переписки -[Priler/jarvis](https://github.com/Priler/jarvis). Сильно расширен в этом форке -(см. ниже). Может работать **полностью офлайн** через Ollama, или с облачным -LLM через Groq — выбор переключается **голосом** на лету. +Форк Rust-переписки голосового ассистента [Priler/jarvis](https://github.com/Priler/jarvis). +Текущий репозиторий: . -Зеркала: -- GitHub: -- Forgejo (self-hosted): `http://192.168.0.10:3000/bossiara13/J.A.R.V.I.S-rust` - (только в LAN; используй `NO_PROXY=192.168.0.10` если у тебя локальный прокси) +## Что это -Для контрибьюторов: см. [ARCHITECTURE.md](ARCHITECTURE.md) — карта кода, data flow, -рецепты "как добавить пак / фичу / TTS backend". +Голосовой ассистент, написанный на Rust, работающий локально (без облака). +Текущий стек: -## Что внутри +- Vosk — Speech-to-Text (через `vosk-rs`). +- fastembed + ort — локальные эмбеддинги для intent-классификации (MiniLM L6/L12 ONNX). +- Picovoice Porcupine / Rustpotter / Vosk — три опциональных движка wake-word. +- mlua (Lua 5.5, vendored) — скрипты пользовательских команд. +- Tauri + Vite/Svelte — GUI-оболочка (фронтенд в отдельной папке `frontend/`). +- nnnoiseless — подавление шума. +- fluent / unic-langid — i18n (`ru`, `ua`, `en`). -- **STT**: Vosk + webrtc-vad для умного завершения по тишине. -- **Wake-word**: Vosk / Rustpotter / Picovoice Porcupine (выбор в Settings). -- **TTS**: трёхбэкендовая абстракция — SAPI (стоковый), **Piper** (нейронный, - рекомендуется, ~90 МБ), Silero (PyTorch). Выбор через `JARVIS_TTS` или GUI. -- **LLM**: двухбэкендовая — **Groq** (облако, бесплатно) или **Ollama** (локально, - офлайн). Hot-swap через голос / GUI / `JARVIS_LLM`. Выбор сохраняется в БД. -- **Lua sandbox**: каждая команда — Lua-скрипт с тремя уровнями песочницы. - Полный API: `jarvis.speak/llm/http/fs/state/system/memory/profile/scheduler/ - vision/macros/cmd/text/...` — см. ARCHITECTURE.md. -- **Tauri 2 GUI**: 4 страницы (главная, /commands список 59 паков, /settings, - футер с активными движками). - -## Возможности (то, чего нет в апстриме) - -### Управление LLM - -- **Agentic LLM router (IMBA-1)**: если fuzzy/intent не нашёл команду, LLM - выбирает ближайшую из реестра по JSON-схеме. Алисе/Сири такое не снилось. -- **Hot-swap локального и облачного LLM**: "переключись на локальный" / - "переключись на облако". Выбор персистентен (Settings → AI Backends). -- **Reset/repeat контекст**: "сбрось контекст" / "повтори последнее". -- **Memory injection**: LLM автоматически получает релевантные факты из - долгосрочной памяти в system prompt. - -### Долгосрочная память - -- "Запомни что я люблю чай улун" → JSON-стор в `/long_term_memory.json`. -- "Что ты помнишь про чай" / "забудь про X" / "что ты знаешь обо мне". -- LLM-чат получает релевантные факты через substring search. - -### Профили - -- 5 предзаполненных: default ★, work 💼, game 🎮, sleep 🌙, driving 🚗. -- "Режим работа" / "режим игра" / "режим сон" / "режим за рулём" / "обычный режим". -- Каждый профиль может иметь allow/deny списки команд + personality для LLM. - -### Vision (multimodal) - -- "Что на экране" / "опиши экран" / "прочитай ошибку" → скриншот через - PowerShell → base64 → Groq vision (`llama-3.2-11b-vision-preview`) → голос. -- Требует `GROQ_TOKEN` (Ollama пока без vision в этой интеграции). - -### Проактивный планировщик - -- "Напомни через 5 минут выключить кофеварку" → one-shot. -- "Каждые 2 часа напоминай попить воды" → interval. -- "Каждый день в 9:00 делай брифинг" → daily. -- "Отмени напоминание про воду" / "очисти расписание" / "что у меня запланировано". -- Фоновый поток (тик каждые 30 сек). Сохраняется в `/schedule.json`. - -### Voice macros (VoiceAttack-style) - -- "Запиши макрос работа" → начинает запись. -- Любые следующие команды (открой браузер, режим работа, ...) копятся. -- "Сохрани макрос" → персистится. -- "Запусти макрос работа" → проигрывает каждый шаг с интервалом 800мс. -- Список / удаление / отмена. - -### Утилиты - -| Команда | Что делает | -|---|---| -| "Найди в гугле X" | DuckDuckGo Instant Answer + LLM-перессказ | -| "Википедия X" / "что такое X" | Wikipedia REST API (ru→en fallback) + LLM-перевод | -| "Сколько 1000 долларов в рублях" | CBR-XML курс + конвертация | -| "Сколько Сбер" | MOEX биржа, 22 тикера в карте | -| "Переведи буфер" | Clipboard → LLM → обратно в clipboard | -| "Напоминай мне пить воду" | Habit nudge: каждые 2 часа | -| "Напоминай размяться" | Каждые 50 минут | -| "Напоминай отдыхать глазам" | 20-20-20 rule, каждые 20 минут | -| "Запусти помодоро" | 25/5 циклы с голосовыми напоминаниями | -| "Запиши настроение 7" / "как прошла неделя" | Mood log + LLM-сводка | -| "Утренний брифинг" / "настрой утренний брифинг на 9:00" | Время+профиль+задачи+память+LLM-мотивашка | -| "Укажи проект C:\..." / "что делает функция X" | Codebase Q&A — LLM по локальной кодобазе | -| "Открытые пиары" / "разбери последний PR" | gh CLI + LLM-ревью | -| "Пауза" / "следующий трек" | Windows media keys (Spotify / YouTube / Foobar) | -| "Диагностика" / "доложи о себе" | Состояние всех бэкендов одной строкой | - -Всего паков: **85** (`resources/commands/*/command.toml`) + -плагины пользователя из `%APPDATA%\com.priler.jarvis\plugins\`. -Полный список: GUI → /commands (с поиском и фильтрами). - -### Плагины - -Голосовые команды, которые лежат **вне** проекта. Каждый плагин — -папка с тем же `command.toml` + Lua-скриптами: - -``` -%APPDATA%\com.priler.jarvis\plugins\<имя>\ - command.toml (тот же формат, что и встроенные паки) - *.lua (скрипты, на которые ссылается toml) - disabled (опционально — пустой файл, отключает плагин) -``` - -Подгружается при следующем старте `jarvis-app.exe`. Sandbox принудительно -понижается до `standard` — плагин не может попросить `full` (доступ к `os`). -GUI → /plugins показывает список, флажки enable/disable, кнопку «Открыть папку». - -### Своё кодовое слово - -GUI → /wake-trainer — мастер: запишите 5–30 примеров своего голоса, -обучите персональную `.rpw`-модель, выберите её в Настройках, перезапустите -ассистент. Файл — `%APPDATA%\com.priler.jarvis\wake_words\<имя>.rpw`. - -## Быстрый старт - -```powershell -# 1. Клонировать -git clone https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-rust.git C:\Jarvis\rust -cd C:\Jarvis\rust - -# 2. Поставить нейронный TTS (один раз, ~90 МБ) -pwsh tools/piper/install.ps1 - -# 3a. Облачный LLM — получи токен на https://console.groq.com/keys -echo "GROQ_TOKEN=gsk_..." > dev.env - -# 3b. ИЛИ локальный LLM -# Скачай https://ollama.com/download, выполни: -# ollama pull qwen2.5:3b -# (Jarvis сам подхватит, если GROQ_TOKEN отсутствует) - -# 4. Сборка -cd frontend && npm install && npm run build && cd .. -cargo build --release -p jarvis-app -p jarvis-gui - -# 5. Запуск -target\release\jarvis-app.exe # фоновый демон с микрофоном -target\release\jarvis-gui.exe # GUI для управления -``` - -Подробнее: см. секцию "Сборка" ниже. - -## Конфигурация (env vars) - -Все JARVIS_* / GROQ_* / OLLAMA_* переменные документированы в -`crates/jarvis-core/src/runtime_config.rs`. Ключевые: - -| Переменная | По умолчанию | Назначение | -|---|---|---| -| `GROQ_TOKEN` | — | Токен для облачного LLM (без него — Ollama / выкл) | -| `GROQ_MODEL` | `llama-3.3-70b-versatile` | Groq-модель | -| `GROQ_VISION_MODEL` | `llama-3.2-11b-vision-preview` | Vision-модель для "что на экране" | -| `OLLAMA_BASE_URL` | `http://localhost:11434/v1` | Адрес Ollama | -| `OLLAMA_MODEL` | `qwen2.5:3b` | Локальная модель (любая через `ollama pull`) | -| `JARVIS_LLM` | auto | `groq` / `ollama` / пусто (auto-detect) | -| `JARVIS_TTS` | auto | `sapi` / `piper` / `silero` / пусто (auto) | -| `JARVIS_TTS_PIPER_BIN` | автопоиск | Путь к piper.exe | -| `JARVIS_TTS_PIPER_VOICE` | автопоиск | Путь к голосу .onnx | -| `JARVIS_LLM_ROUTER` | `1` | Включить agentic router | -| `JARVIS_LLM_ROUTER_THRESHOLD` | `0.55` | Confidence threshold | -| `JARVIS_LLM_TTS` | `true` | Озвучивать LLM-ответ | - -Файл `dev.env` подхватывается автоматически (jarvis-app ищет от exe вверх по 5 -уровням), так что переменные можно держать в одном месте. - -Персистентные настройки (`voice`, `microphone`, `vosk_model`, `llm_backend`, -`tts_backend`, ...) хранятся в `/settings.json` и правятся -через GUI Settings или Lua `jarvis.settings.set(key, val)`. - -## Сборка - -Требования: - -- Rust 1.93+ (stable MSVC). -- Node 20+ и npm для фронтенда. -- MSVC build tools (`vcvars64.bat`). VS 2022 / 2026 / 2026 — любая. -- Python 3 (только для `post_build.py`, опционально). -- Vosk/Porcupine/PvRecorder DLL'и в `lib/windows/amd64/` (уже в репо). - -Команды: - -```powershell -# Один раз: загрузить MSVC окружение -$vc = "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvars64.bat" -cmd /c "`"$vc`" >NUL && set" | % { - if ($_ -match '^([^=]+)=(.*)$') { - [Environment]::SetEnvironmentVariable($matches[1], $matches[2], 'Process') - } -} - -# Собрать фронтенд -cd frontend -npm install -npm run build -cd .. - -# Workspace -cargo build --release -p jarvis-app -p jarvis-gui -``` - -Холодная сборка ~10 мин (ONNX runtime, aws-lc-rs, tauri). Инкремент ~30 сек. - -`jarvis-gui/build.rs` сам зовёт `npm run build` если фронтенд устарел, так что -для итераций по UI достаточно `cargo build -p jarvis-gui`. - -## Тесты - -```powershell -cargo test -p jarvis-core --lib # все unit-тесты -cargo test -p jarvis-core --lib scheduler:: # фильтр по модулю -``` - -Текущее покрытие: **55 unit-тестов**, см. таблицу в ARCHITECTURE.md. - -## Структура - -``` -crates/ - jarvis-core library — STT, wake-word, TTS, LLM, intent, commands, IPC, - Lua sandbox, scheduler, memory, profiles, macros, vision - jarvis-app daemon бинарь — микрофон, listening loop, dispatcher, tray, IPC - jarvis-gui Tauri + Svelte GUI — settings, commands list, footer chips - jarvis-cli debug CLI (classify/execute/list) - -frontend/ Vite + Svelte UI для jarvis-gui -resources/ - commands/ 59 voice-command packs (Lua скрипты + TOML) - models/ ONNX (intent classifier, GLiNER) — Git LFS - vosk/ Vosk модели (small-ru, small-en, ...) - voices/ пресеты звуков реакций (greet/ok/error/...) - -tools/ - piper/ Piper TTS бинарник + голоса (.onnx) — устанавливаются install.ps1 - silero/ Python helper для Silero TTS (опционально) - -ARCHITECTURE.md — для контрибьюторов: data flow, рецепты, тесты. -``` - -## Голосовой workflow - -Поднимаешь `jarvis-app.exe`, говоришь: - -``` -"Джарвис" ─ wake-word - ↓ играет "reply" звук -"Какая сегодня погода?" - ↓ STT (Vosk) распознаёт - ↓ Intent classifier (MiniLM ONNX) пробует найти команду - ↓ Levenshtein fallback - ↓ Agentic LLM router (если включён) - ↓ LLM fallback (chat) - ↓ Озвучивает ответ -"Какая температура завтра?" - ↓ 30-сек grace window — без повторного wake-word - ↓ LLM помнит контекст разговора -``` - -После каждой команды есть 30-секундное окно для follow-up без повторного -"Джарвис" (`CONVERSATION_GRACE_MS`). +**LLM-клиент (Groq / OpenAI-совместимый) добавлен в `jarvis-core::llm` и подключён к голосовому циклу.** Если фраза начинается с триггера («скажи», «ответь», «произнеси»), она уходит в Groq и ответ возвращается через IPC-событие `LlmReply`. Без триггера всё работает как раньше — wake-word + intent + Lua. Это следующий шаг после CLI-only LLM из v0.2.0. ## Это форк Оригинальный автор — Abraham Tugalov (Priler). Апстрим: . Лицензия сохранена: **CC BY-NC-SA 4.0** (см. `LICENSE.txt`). +Атрибуция в `Cargo.toml` и `voice.toml` пакетов озвучки не изменена. -В `Cargo.toml` декларирован `license = "GPL-3.0-only"` — это несоответствие -унаследовано от апстрима и не правилось, чтобы не расходиться с upstream-конфигом. -Приоритет имеет `LICENSE.txt`. +## Что отличается от апстрима -## Python-версия +- Обновлён список авторов в `Cargo.toml` (добавлен `Bossiara13 (fork)`, оригинал сохранён). +- README переписан и отражает фактическую архитектуру (апстримный README называет проект "Tauri+Svelte", что давно не соответствует действительности — это workspace из 4-х крейтов). +- Отсутствующие в апстриме ONNX-модели (`all-MiniLM-L6-v2`, `paraphrase-multilingual-MiniLM-L12-v2-onnx-Q`) подтянуты через Git LFS из HuggingFace (Qdrant) и запушены в форк. -Старая версия ассистента была на Python. Последний коммит с Python-кодом в -апстриме — [943efbf](https://github.com/Priler/jarvis/tree/943efbfbdb8aeb5889fa5e2dc7348ca4ea0b81df). -Параллельный Python-форк живёт в отдельном репо -([J.A.R.V.I.S-py](https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-py)) — там же, -но **17 новых паков из Rust-форка пока туда не зеркалированы** (TODO). +## Структура репозитория -## Поддерживаемость +Cargo workspace из четырёх крейтов: -- `runtime_config.rs` — все env vars в одном файле с doc-комментами. -- `tts/` — trait-based, добавить новый бэкенд = реализовать `TtsBackend`. -- `llm/mod.rs` — shared global client, hot-swap через `swap_to(LlmBackend)`. -- `jarvis.cmd.{ok/error/not_found}` — boilerplate-killer для Lua-паков. -- 55 unit-тестов прикрывают всё, что не требует мика/динамика/сети. -- ARCHITECTURE.md содержит рецепты "как добавить X". +| Крейт | Назначение | +|----------------|----------------------------------------------------------------------------| +| `jarvis-core` | Библиотека: конфиг, intent, STT, wake-word, аудио, Lua-бэкенд, i18n. | +| `jarvis-app` | Бинарь-«демон»: собирает всё вместе, tray, IPC. | +| `jarvis-gui` | Tauri-приложение (использует `frontend/dist/client`). | +| `jarvis-cli` | CLI для отладки: классификация intent, список команд, dump конфига. | -## Что в roadmap (что ещё хочется) +Прочее: -- **faster-whisper STT** рядом с Vosk для длинных LLM-фраз. -- **Реальные Win32 макросы** (keyboard + mouse hooks), не только voice replay. -- **Spotify Web API** через OAuth — "что играет", "сохрани в плейлист". -- **Outlook COM** — голосовая почта / календарь. -- **GUI /macros + /scheduler страницы** — управление без голоса. -- **Python parity** для 17 новых паков. +- `frontend/` — Vite + Svelte UI для `jarvis-gui`. Собирается отдельно. +- `lib/windows/amd64/` — нативные DLL/LIB для Vosk, Porcupine, PvRecorder. +- `resources/` — голоса, модели, конфиги по умолчанию. ONNX-модели хранятся в Git LFS. +- `post_build.py` — постпроцессинг артефактов сборки (Python 3). -См. `~/.claude/projects/C--Jarvis/memory/project_jarvis_roadmap.md` (если ты -— Claude помогаешь автору) или открой issue. +## Сборка + +Требования: + +- Rust 1.93+ (собирается на stable MSVC). +- Node 24+ и npm — для фронтенда. +- Python 3 — для `post_build.py`. +- MSVC build tools (Windows, x64). +- Установленные `libvosk.lib`, `libpv_porcupine.dll`, `libpv_recorder.dll` в `lib/windows/amd64/` (уже в репозитории). + +Перед сборкой `jarvis-gui` нужно собрать фронтенд: + +```bash +cd frontend +npm install +npm run build +cd .. +``` + +Затем workspace: + +```bash +cargo build --workspace +``` + +Холодная сборка занимает около 10 минут (ONNX runtime, aws-lc-rs, tauri). + +## Статус сборки в этом форке + +На моей машине (`cargo build --workspace`, stable MSVC) итог: + +- `jarvis-core` — собрался (1 warning, unused import). +- `jarvis-app` — собрался, бинарник `target/debug/jarvis-app.exe` создан. +- `jarvis-cli` — **падает на линковке**: `LNK1181: cannot open input file "libvosk.lib"`. + Причина: у `jarvis-cli` нет своего `build.rs`, а `.cargo/config.toml` с `rustc-link-search` лежит только внутри `crates/jarvis-app/` и не подтягивается для `jarvis-cli`. Лечится либо добавлением такого же `build.rs` в `crates/jarvis-cli/`, либо вынесением `config.toml` в корень. Сознательно не трогал — фикс выходит за рамки рефакторинга (v0.0.1-import фиксирует поведение апстрима как есть). +- `jarvis-gui` — падает в `tauri::generate_context!()`: `frontendDist = "../../frontend/dist/client"` не существует. Это ожидаемо, если не запустить `npm run build` в `frontend/` заранее (см. секцию «Сборка»). + +Запуск уже собранного: + +```bash +./target/debug/jarvis-app.exe +``` + +Для CLI (`jarvis-cli --help`, команды `classify`, `execute`, `list`, `phrases`) нужно сначала починить линковку Vosk (см. выше). + +## LLM (Groq) + +В `jarvis-core` есть модуль `llm` — блокирующий клиент для OpenAI-совместимого эндпоинта chat completions. По умолчанию настроен на Groq. Используется через фиче-флаг `llm` (включён в дефолтный набор `jarvis_app`, также подтянут в `jarvis-cli`). + +Переменные окружения: + +| Переменная | Обязательна | Значение по умолчанию | +|-----------------|-------------|----------------------------------------| +| `GROQ_TOKEN` | да | — | +| `GROQ_BASE_URL` | нет | `https://api.groq.com/openai/v1` | +| `GROQ_MODEL` | нет | `llama-3.3-70b-versatile` | + +Быстрая проверка через CLI: + +```bash +set GROQ_TOKEN=gsk_... +jarvis-cli ask "скажи привет одной фразой" +``` + +Ответ печатается в stdout. Без `GROQ_TOKEN` команда завершится с кодом 2 и сообщением об ошибке. При ошибке API — код 1 и тело ответа. + +Программное использование из Rust: + +```rust +use jarvis_core::llm::{LlmClient, ChatMessage}; + +let client = LlmClient::from_env()?; +let reply = client.complete(&[ChatMessage::user("привет")], 256)?; +println!("{}", reply); +``` + +### Подключение к голосовому циклу + +Помимо CLI, LLM подключён напрямую в `jarvis-app`. Логика в `crates/jarvis-app/src/llm_fallback.rs`: + +- При старте `jarvis-app` пытается прочитать `GROQ_TOKEN`. Если переменной нет — фоллбэк отключается, в лог пишется warning, голосовые команды продолжают работать как раньше. +- Распознанная фраза (как из микрофона, так и из текстовой панели GUI) проверяется на префиксы-триггеры из `config::get_llm_trigger_phrases` (для `ru`/`ua`: `скажи`, `ответь`, `произнеси`; для `en`: `say`, `tell`, `answer`). +- Если триггер найден — остаток фразы уходит в `LlmClient::complete()`, ответ публикуется в IPC как `IpcEvent::LlmReply { text }` (UI/GUI слушает этот ивент и проговаривает текст уже на своей стороне), а звук-«ок» проигрывается из текущего голосового пресета. +- История разговора хранится в `ConversationHistory` с потолком `LLM_DEFAULT_MAX_HISTORY = 8` ходов; system-prompt всегда сохраняется при вытеснении старых ходов. +- При сетевой/API-ошибке последний user-turn убирается из истории, в IPC уходит `LlmReply` с короткой русской фразой («Не могу связаться с сервером, сэр.»), играется звук-«error». Голосовой цикл не падает. + +Системный промпт (русский) описывает J.A.R.V.I.S. как британского дворецкого Тони Старка — короткие реплики (1–3 предложения), обращение «сэр», без излишней цензуры. Меняется в `config::LLM_SYSTEM_PROMPT_RU`. + +## VAD: умное завершение команды по тишине + +После срабатывания wake-word `jarvis-app` слушает команду. Раньше окно закрывалось либо по таймауту Vosk, либо по жёсткому пределу из `config::CMS_WAIT_DELAY` — короткие команды («стоп», «громче») всё равно ждали несколько секунд тишины. + +Теперь окно закрывается, как только пользователь замолчал. Реализовано через `webrtc-vad` (чистый Rust, тот же алгоритм, что в Python-версии). Это **паритет с Python v0.2.0**, где использовался `webrtcvad` с теми же параметрами. + +Логика (state-машина в `crates/jarvis-core/src/audio_processing/vad/listen_window.rs`): + +- Кадр VAD: 30 мс при 16 кГц = 480 сэмплов = 960 байт (mono i16). Кадры с микрофона (по 512 сэмплов от `pv_recorder`) аккумулируются и режутся на VAD-кадры адаптером `WebRtcVad::push_samples`. +- Каждый VAD-кадр учитывается в счётчиках `speech_ms` / `silence_ms`. +- Окно закрывается, когда `silence_ms >= VAD_COMMAND_END_SILENCE_MS` И `speech_ms >= VAD_COMMAND_MIN_SPEECH_MS`. +- До истечения `VAD_COMMAND_MIN_LISTEN_MS` окно не закрывается — пользователю даётся время начать говорить. +- При достижении `VAD_COMMAND_MAX_LISTEN_MS` срабатывает hard-cap и управление возвращается в режим ожидания wake-word. +- На событии «закрыть окно» вызывается `stt::finalize_speech()` — Vosk форсированно отдаёт финальный результат, не дожидаясь собственного таймаута. + +Параметры (`crates/jarvis-core/src/config.rs`): + +| Константа | Значение | Назначение | +|---------------------------------|----------|-------------------------------------------------------------| +| `VAD_AGGRESSIVENESS` | `2` | Уровень WebRTC VAD: 0 — Quality, 3 — VeryAggressive. | +| `VAD_COMMAND_END_SILENCE_MS` | `1200` | Сколько тишины подряд считать «команда закончилась». | +| `VAD_COMMAND_MIN_SPEECH_MS` | `500` | Минимум речи в окне — иначе закрытие игнорируется. | +| `VAD_COMMAND_MIN_LISTEN_MS` | `1000` | Минимальная длительность окна (страховка от ранних закрытий)| +| `VAD_COMMAND_MAX_LISTEN_MS` | `15000` | Жёсткий потолок окна. | + +На короткие команды отзыв стал заметно быстрее (мы выходим на распознавание сразу после паузы пользователя, а не по 5-секундному порогу Vosk). ## Лицензия -CC BY-NC-SA 4.0 — Creative Commons **Attribution-NonCommercial-ShareAlike**. -Полный текст: `LICENSE.txt`. Атрибуция Abraham Tugalov сохранена. +Creative Commons **Attribution-NonCommercial-ShareAlike 4.0 International** (CC BY-NC-SA 4.0). +Полный текст — в `LICENSE.txt`. Атрибуция оригинального автора (Abraham Tugalov) сохранена. + +В `Cargo.toml` декларирован `license = "GPL-3.0-only"` — это несоответствие унаследовано от апстрима и не правилось, чтобы не расходиться с upstream-конфигом. Приоритет имеет `LICENSE.txt`. + +## Python-версия + +Старая версия ассистента была на Python. +Последний коммит с Python-кодом в апстриме — [943efbf](https://github.com/Priler/jarvis/tree/943efbfbdb8aeb5889fa5e2dc7348ca4ea0b81df). diff --git a/USAGE.md b/USAGE.md deleted file mode 100644 index 44a7278..0000000 --- a/USAGE.md +++ /dev/null @@ -1,307 +0,0 @@ -# Voice command reference - -Все голосовые команды J.A.R.V.I.S. (текущее состояние, **98 паков**). -Каждая команда — это фраза, которую можно произнести после wake-word ("Джарвис"). - -> Языки: **RU + EN** (украинский поддерживался до 2026-05-16, в текущей версии удалён). - -Группы: - -- [Управление ассистентом](#управление-ассистентом) -- [LLM / мозг](#llm--мозг) -- [Память / профили / macros](#память--профили--macros) -- [Расписание / напоминания / привычки](#расписание--напоминания--привычки) -- [Окна / приложения / процессы](#окна--приложения--процессы) -- [Аудио / медиа](#аудио--медиа) -- [Система / устройство](#система--устройство) -- [Файлы / буфер / заметки](#файлы--буфер--заметки) -- [Информация из интернета](#информация-из-интернета) -- [Утилиты-калькуляторы](#утилиты-калькуляторы) -- [Разработка](#разработка) -- [Развлечения](#развлечения) -- [Скриншоты / экран](#скриншоты--экран) - -После каждой команды есть **30-секундное grace-окно** для follow-up без повторного "Джарвис". -Конфигурируется через `CONVERSATION_GRACE_MS`. - ---- - -## Управление ассистентом - -| Команда (RU) | Что делает | -|---|---| -| "Помощь" / "что ты умеешь" | Перечисляет категории возможностей | -| "Расскажи о себе" / "кто ты такой" | Краткое представление (LLM варьирует) | -| "Сколько команд знаешь" | Health snapshot — все активные бэкенды + счётчики | -| "Диагностика" / "доложи о себе" | Одной строкой: TTS, LLM, профиль, памяти/задач | -| "Повтори за мной X" | Эхо — для теста микрофона + TTS | -| "Что я сказал" | Что услышал из последней фразы | -| "Стоп" / "хватит" / "прекрати" | Прерывает текущую команду | -| "Сбрось контекст" / "забудь разговор" | Очищает историю LLM | -| "Повтори последнее" / "повтори ответ" | Озвучивает последний ответ LLM | - -## LLM / мозг - -| Команда | Что делает | -|---|---| -| "Переключись на локальный" / "перейди на оллама" | Hot-swap на Ollama (persistent в БД) | -| "Переключись на облако" / "используй грок" | Hot-swap на Groq | -| "Какой у тебя мозг" / "облако или локально" | Текущий backend | - -Любая фраза без распознанной команды → LLM router (если включён) → если нет матча → LLM chat fallback. - -## Память / профили / macros - -| Команда | Что делает | -|---|---| -| "Запомни что я люблю чай улун" | Сохраняет факт | -| "Что ты помнишь про чай" | Substring-поиск по памяти | -| "Что ты знаешь обо мне" | Все факты | -| "Забудь про X" | Удалить факт | -| "Режим работа" / "режим игра" / "режим сон" / "режим за рулём" / "обычный режим" | Профиль (5 встроенных) | -| "Какой сейчас режим" | Активный профиль | -| "Запиши макрос имя" | Начать запись макроса | -| "Сохрани макрос" | Завершить запись и сохранить | -| "Отмени макрос" | Отменить запись | -| "Запусти макрос имя" | Воспроизвести (с интервалом 800мс) | -| "Какие у меня макросы" / "удали макрос имя" | Управление списком | - -Также: GUI страницы `/memory` (добавить/забыть факты), `/macros` (запуск/удаление), `/scheduler` (отмена задач). - -## Расписание / напоминания / привычки - -| Команда | Что делает | -|---|---| -| "Напомни через 5 минут выключить кофеварку" | One-shot reminder | -| "Напомни в 18:00 забрать ребёнка" | Today at HH:MM | -| "Каждые 2 часа напоминай попить воды" | Interval | -| "Каждый день в 9:00 делай брифинг" | Daily | -| "Что у меня запланировано" / "очисти расписание" | Управление | -| "Отмени напоминание про воду" | Substring-match remove | -| "Запусти помодоро" / "стоп помодоро" | 25/5 циклы | -| "Напоминай мне пить воду" | Каждые 2 часа | -| "Напоминай размяться" | Каждые 50 минут | -| "Напоминай отдыхать глазам" | 20-20-20 (каждые 20 мин) | -| "Отключи все привычки" | Стоп всех habit_nudge задач | -| "Настрой утренний брифинг на 9:00" | Daily briefing в указанное время | -| "Утренний брифинг" | Запустить брифинг сейчас | -| "Отключи утренний брифинг" | Снять daily задачу | - -## Окна / приложения / процессы - -| Команда | Что делает | -|---|---| -| "Открой Telegram" / "запусти Steam" / "Spotify" | Через `apps/` пак (имя в config) | -| "Закрой Telegram" / "убей процесс X" | Process kill | -| "Сверни всё" / "разверни окно" / "переключи окно" | Window manager | -| "Какие открыты окна" | Список Z-order | -| "Сверни активное" / "максимизируй" | Action на foreground | -| "Открой папку загрузок" / "файлы" / "проводник" | Explorer | - -## Аудио / медиа - -| Команда | Что делает | -|---|---| -| "Громче" / "тише" / "громкость на 30" | Volume up/down/set | -| "Заглуши" / "сними заглушку" | Mute toggle | -| "Какая громкость" | Текущий уровень | -| "Открой панель звука" | Sound panel | -| "Включи Realtek" / "переключи звук на Headphones" | Output device swap | -| "Пауза" / "плей" / "следующий трек" / "предыдущий трек" / "стоп музыка" | Media keys — Spotify / YouTube / Foobar / Yandex Music | -| "Запиши голосовую заметку" | (см. notes/) | -| "Выключи музыку через 30 минут" | Sleep timer для медиа | - -## Система / устройство - -| Команда | Что делает | -|---|---| -| "Сколько процессор грузится" / "сколько RAM" / "температура" | Sysinfo | -| "Сколько заряда" / "заряд батареи" | Battery % | -| "Сколько свободно на диске" / "сколько на диске C" | PowerShell Get-PSDrive | -| "Какие у меня диски" | C: D: E: со свободным местом | -| "Какая сеть" / "какой WiFi" | Net SSID via netsh | -| "Мой IP" / "локальный IP" | Get-NetIPAddress | -| "Внешний IP" | api.ipify.org | -| "Прибавь яркость" / "тусклее" / "яркость 70" | Brightness via WMI | -| "Тёмная тема" / "светлая тема" | Windows theme | -| "Заблокируй компьютер" | LockWorkStation | -| "Выключи компьютер" / "перезагрузи" / "ребутни" / "сон" / "гибернация" / "выйди из системы" / "отмени выключение" | Power management | -| "Выключи компьютер через 30 минут" | Sleep timer для системы | -| "Отмени таймер выключения" | shutdown /a + scheduler clear | - -## Файлы / буфер / заметки - -| Команда | Что делает | -|---|---| -| "Найди файл report" / "поищи documents" | File search | -| "Запиши заметку купить хлеб" | Append в notes file | -| "Покажи заметки" / "что я записал" | Открывает notes | -| "Прочитай буфер" / "что в буфере" | Clipboard contents speak | -| "Переведи буфер на английский" | LLM перевод + replace clipboard | - -## Информация из интернета - -| Команда | Что делает | -|---|---| -| "Какая погода" / "погода завтра" / "что с погодой" | Weather pack | -| "Курс доллара" / "курс юаня" | CBR rates | -| "Сколько 1000 долларов в рублях" | Currency convert | -| "Сколько Сбер" / "сколько Газпром" | MOEX котировки (22 тикера) | -| "Сколько биткоин" / "сколько эфир" | Crypto | -| "Новости" / "что нового в мире" | News RSS | -| "Найди в гугле X" / "загугли X" | DuckDuckGo Instant + LLM | -| "Википедия X" / "что такое X" / "кто такой Y" | Wikipedia REST | -| "Открыть Google поиск X" | Websearch (open browser) | -| "Переведи на английский X" | Translate | - -## Утилиты-калькуляторы - -| Команда | Что делает | -|---|---| -| "Посчитай 2 плюс 2" / "сколько будет 10 в степени 3" | Math | -| "Переведи 100 метров в футы" | Length convert | -| "70 кг в фунты" | Weight convert | -| "100 цельсий в фаренгейт" | Temperature convert | -| "100 км в час в мили в час" | Speed convert | -| "Сколько дней до нового года" | Date math (8 марта, 9 мая, Рождество, Новый год + любая дата) | -| "Какой сегодня день недели" | Zeller's congruence | -| "Время" / "сколько времени" / "какое сегодня число" | Date query | -| "Подбрось монету" / "орёл или решка" | Coin flip | -| "Сгенерируй пароль 16 символов" | Secure password → clipboard | -| "Сгенерируй UUID" | v4 UUID → clipboard | -| "Брось кубик" / "брось два кубика" | Dice | -| "Выбери случайно из X или Y или Z" | Random choice | -| "Запусти секундомер" / "сколько прошло" / "стоп секундомер" | Stopwatch | -| "Прибавь счётчик" / "сколько на счётчике" / "сбрось счётчик" | Counter | -| "Как пишется X" | Spelling | - -## Разработка - -| Команда | Что делает | -|---|---| -| "Укажи проект C:\..." | Codebase Q&A: установить root | -| "Какой проект сейчас" | Текущий root | -| "Что делает функция X" / "найди в коде Y" / "объясни код" | Walks codebase → LLM | -| "Текущий репо owner/repo" | GitHub: установить repo | -| "Какие пиары" / "открытые пиары" | List PRs via gh CLI | -| "Разбери последний пиар" | LLM-ревью PR (gh pr view) | -| "Сгенерируй код для X" | Code generation pack | -| "Открой Cursor" / "VS Code" | apps/ | - -## Развлечения - -| Команда | Что делает | -|---|---| -| "Расскажи анекдот" / "пошути" / "скажи фразу" / "тостер" | Fun pack (jokes) | -| "Запиши настроение 7" / "сегодня мне грустно" | Mood log | -| "Как прошла неделя" / "сводка настроения" | Mood recap via LLM | -| "Запусти Steam игру X" / "поиграй в Y" | Games pack | - -## Скриншоты / экран - -| Команда | Что делает | -|---|---| -| "Что на экране" / "опиши экран" | Vision LLM описание | -| "Прочитай ошибку" | Vision LLM: ищет stack trace, объясняет | -| "Сделай скриншот" / "скрин в буфер" | Screenshot → clipboard | -| "Сохрани скриншот" | Screenshot → ~/Pictures/Screenshots/ | -| "OCR экрана" / "прочитай текст" | OCR (existing pack) | -| "Суммаризируй экран" | Summarize visible window | - ---- - -## Wave 4-8 (новое — личность, рутины, режимы) - -| Команда | Что делает | -|---|---| -| **Личность / banter** | | -| "Привет Джарвис" / "Доброе утро" | Время-чувствительные ответы из ~30 вариантов | -| "Спасибо" / "Ты молодец" | Варьирующиеся «батлерские» ответы | -| "Как дела Джарвис" | Включает живые цифры из памяти + профиля | -| "Процитируй Тони" | Случайная цитата Stark/JARVIS (15+ вариантов) | -| "Скажи что-нибудь интересное" / "Скучно мне" | Принудительная idle-banter реплика | -| "Помолчи" / "Можешь говорить" | Pause/resume фонового banter | -| **Память / разговор** | | -| "О чём мы говорили" | LLM-сжатие последних ~12 реплик | -| "Повтори" / "Не расслышал" | Перепроизнести последний ответ | -| "Сколько фактов помнишь" | Размер долговременной памяти | -| "Покажи что помнишь" | Список ключей в памяти | -| "Точно забудь всё" | Полное стирание памяти (с защитой) | -| **Режимы и рутины** | | -| "Режим фокуса" / "Focus mode" | Профиль work + Focus Assist + стретч через 50 мин | -| "Выключи фокус" | Откат | -| "Спокойной ночи" | Профиль sleep + отмена одноразовых таймеров | -| "Доброе утро" | Профиль default + обзор расписания | -| "Иду за кофе" / "Coffee break" | Пауза banter + чек-ин через 5 минут | -| **Утилиты / новые** | | -| "Сколько будет 234 на 567" | Offline-парсер (без LLM, instant) | -| "Что такое квантовая запутанность" | DDG Instant Answer (без ключа) | -| "Переведи буфер на немецкий" | Translate clipboard с озвучкой в немецком SAPI voice | -| "Поставь чайник" / "Варю пасту" | Cooking timer с пресетами на 16+ блюд | -| "На чём я остановился" | Recap из памяти + расписания + последнего LLM-ответа | -| "Расскажи про железного человека" | Случайный MCU-факт | -| "Разбуди сервер" / "Wake on LAN" | Magic packet на MAC из памяти (`wol_server`) | -| **Outlook (требует Outlook)** | | -| "Сколько у меня непрочитанных писем" | Outlook COM → счётчик inbox | -| "Прочитай последнее письмо" | From + Subject + 200 chars body | -| "Отправь письмо [Имя]" | Body = clipboard, через GAL lookup | -| "Коротко расскажи про инбокс" | LLM-сводка последних 5 unread | -| **Time tracker** | | -| "Начни отсчёт" / "Закончи отсчёт" | Сессия учёта рабочего времени | -| "Сколько я работаю сегодня" | Сумма открытых + закрытых сессий за календарный день | -| "Сколько на этой неделе" | Сумма за последние 7 дней | -| "Сбрось трекер" | Стереть сегодняшние записи | -| **Кастомное wake-word** | | -| GUI → /wake-trainer | Мастер: запись 5–30 сэмплов → rustpotter .rpw | -| **Плагины** | | -| GUI → /plugins | Toggle/Open Folder для `%APPDATA%/com.priler.jarvis/plugins/` | - -## Как добавить свою команду - -Скопируй `resources/commands/echo/` как шаблон, отредактируй `command.toml` -(id, phrases) и `*.lua` (логика). Рекомендуемая структура — см. -[ARCHITECTURE.md → Recommended pack structure](ARCHITECTURE.md#recommended-pack-structure). - -Минимум: - -```lua --- thing.lua -local phrase = (jarvis.context.phrase or ""):lower() -local body = jarvis.text.strip_trigger(phrase, { "сделай штуку" }) -body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if body == "" then - return jarvis.cmd.error("Что именно?") -end - --- ... do work ... - -return jarvis.cmd.ok("Готово.") -``` - -Затем `cargo test -p jarvis-core --lib commands::tests` — он автоматически -проверит что TOML парсится и Lua-скрипт существует. - -## Голосовое vs GUI - -GUI страницы — для случаев когда голосом неудобно: - -- `/commands` — все 67 паков с поиском (для discovery) -- `/macros` — список с кнопкой "Запустить" -- `/scheduler` — задачи с auto-poll каждые 5 сек -- `/memory` — добавление/удаление фактов с фильтром -- `/settings → AI Backends` — переключение LLM/TTS через dropdown - -## Что-то не работает? - -1. **Голос не распознаётся** — проверь микрофон в Settings → Devices. Скажи - "повтори за мной привет" — если эхо проходит, проблема не в STT. -2. **Не слышу ответ** — проверь TTS backend в Settings → AI Backends. Скажи - "проверка эхо" — должен быть слышен голос. Если нет, проверь динамики + - попробуй `JARVIS_TTS=sapi`. -3. **"Не могу связаться с сервером"** — LLM unreachable. Если Groq — - проверь GROQ_TOKEN и интернет. Если Ollama — `ollama serve` запущен? -4. **Команда не найдена** — посмотри `/commands` в GUI, поищи фразу. Если - фразы похожи но не та — agentic router сделает попытку, или подскажет. -5. **Что-то странное в логах** — `/jarvis.log` (путь видно - в первых строках `jarvis-app` stdout). diff --git a/crates/jarvis-app/Cargo.toml b/crates/jarvis-app/Cargo.toml index 76bff45..78353b5 100644 --- a/crates/jarvis-app/Cargo.toml +++ b/crates/jarvis-app/Cargo.toml @@ -11,15 +11,12 @@ jarvis-core = { path = "../jarvis-core", features = ["intent"] } once_cell.workspace = true log.workspace = true simple-log = "2.4" -dotenvy.workspace = true tray-icon = "0.21" winit = "0.30" image.workspace = true platform-dirs.workspace = true rand.workspace = true parking_lot.workspace = true -serde.workspace = true -serde_json.workspace = true tokio = { version = "1", features = ["rt-multi-thread"] } @@ -29,11 +26,7 @@ features = [] [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3", features = ["winuser"] } -winrt-notification.workspace = true [target.'cfg(target_os = "linux")'.dependencies] gtk = "0.18" -glib = "0.21.5" - -[target.'cfg(target_os = "windows")'.build-dependencies] -embed-resource = "3" \ No newline at end of file +glib = "0.21.5" \ No newline at end of file diff --git a/crates/jarvis-app/app.manifest b/crates/jarvis-app/app.manifest deleted file mode 100644 index de8adb1..0000000 --- a/crates/jarvis-app/app.manifest +++ /dev/null @@ -1,60 +0,0 @@ - - - - J.A.R.V.I.S. voice assistant daemon - - - - - - - - - - - - - - - - - - - - - - - - - true/pm - PerMonitorV2,PerMonitor - UTF-8 - - - - - - - - - - - diff --git a/crates/jarvis-app/app.manifest.rc b/crates/jarvis-app/app.manifest.rc deleted file mode 100644 index bbb1850..0000000 --- a/crates/jarvis-app/app.manifest.rc +++ /dev/null @@ -1,4 +0,0 @@ -#define RT_MANIFEST 24 -#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1 - -CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "app.manifest" diff --git a/crates/jarvis-app/build.rs b/crates/jarvis-app/build.rs index e7a70a7..30a6d3f 100644 --- a/crates/jarvis-app/build.rs +++ b/crates/jarvis-app/build.rs @@ -1,18 +1,10 @@ fn main() { + // link to Vosk lib + // println!("cargo:rustc-link-lib=libvosk.dll"); + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); let lib_path = std::path::Path::new(&manifest_dir) .join("..\\..\\lib\\windows\\amd64"); - + println!("cargo:rustc-link-search=native={}", lib_path.display()); - - // Embed a Common-Controls v6 manifest on Windows so transitive imports - // pulled in by tray-icon / winit (TaskDialogIndirect, themed controls, - // per-monitor DPI) resolve against comctl32 v6. Without this the exe - // crashes at load with "TaskDialogIndirect entry point not found". - #[cfg(target_os = "windows")] - { - println!("cargo:rerun-if-changed=app.manifest"); - println!("cargo:rerun-if-changed=app.manifest.rc"); - let _ = embed_resource::compile("app.manifest.rc", embed_resource::NONE); - } } diff --git a/crates/jarvis-app/src/app.rs b/crates/jarvis-app/src/app.rs index 06db2d9..492d4cc 100644 --- a/crates/jarvis-app/src/app.rs +++ b/crates/jarvis-app/src/app.rs @@ -2,6 +2,7 @@ use std::sync::mpsc::Receiver; use std::time::SystemTime; use jarvis_core::{audio_buffer::AudioRingBuffer, audio_processing, audio_processing::vad::listen_window::{ListenWindow, WindowDecision}, audio_processing::vad::webrtc::WebRtcVad, commands, config, listener, recorder, stt, COMMANDS_LIST, intent, voices, ipc::{self, IpcEvent}, i18n, slots}; +use rand::seq::SliceRandom; use crate::should_stop; @@ -322,45 +323,21 @@ fn recognize_command( // execute command and check if we should chain let should_chain = execute_command(&recognized_voice, rt); - let grace_ms = jarvis_core::config::CONVERSATION_GRACE_MS; - if should_chain || grace_ms > 0 { - // Either explicit chain OR P0.2 continuous-conversation grace window. - // Reset listening state and keep going without re-wake. - if should_chain { - info!("Chain requested, continuing to listen..."); - } else { - info!( - "Continuous conversation: {}ms grace window for follow-up.", - grace_ms - ); - } + if should_chain { + // chain: reset and continue listening + info!("Chaining enabled, continuing to listen..."); stt::reset_speech_recognizer(); vad_state = VadState::WaitingForVoice; silence_frames = 0; - // For grace-mode, shorten the deadline to grace_ms (instead of CMS_WAIT_DELAY). - // We set `start` such that the existing timeout check (line ~360) fires at - // start + CMS_WAIT_DELAY = now + grace_ms. - if !should_chain { - let cms = jarvis_core::config::CMS_WAIT_DELAY.as_millis() as u64; - if grace_ms < cms { - // back-date `start` so the existing timeout fires at now + grace_ms - let backdate_ms = cms - grace_ms; - start = SystemTime::now() - .checked_sub(std::time::Duration::from_millis(backdate_ms)) - .unwrap_or_else(SystemTime::now); - } else { - start = SystemTime::now(); - } - } else { - start = SystemTime::now(); - } + start = SystemTime::now(); audio_buffer.clear(); webrtc_vad.reset(); window = ListenWindow::new(jarvis_core::audio_processing::vad::webrtc::FRAME_MS); ipc::send(IpcEvent::Listening); continue; } else { - info!("Conversation grace disabled, returning to wake word mode."); + // no chain: return to wake word + info!("No chain, returning to wake word mode."); return; } } @@ -425,33 +402,23 @@ fn process_text_command(text: &str, rt: &tokio::runtime::Runtime) { // Execute command, returns true if chaining should continue fn execute_command(text: &str, rt: &tokio::runtime::Runtime) -> bool { - use jarvis_core::recognition_log::{record as log_record, Outcome}; - let commands_list = match COMMANDS_LIST.get() { Some(c) => c, None => { - log_record(text, "voice", Outcome::Error { message: "Commands not loaded".into() }); ipc::send(IpcEvent::Error { message: "Commands not loaded".to_string() }); ipc::send(IpcEvent::Idle); return false; } }; - - // Try intent classifier first; remember which path matched so the log - // can show "via intent" vs "via fuzzy" — useful for understanding why - // a particular phrase landed (or didn't). - let (cmd_result, via, confidence_pct) = if let Some((intent_id, confidence)) = - rt.block_on(intent::classify(text)) + + let cmd_result = if let Some((intent_id, confidence)) = + rt.block_on(intent::classify(text)) { info!("Intent recognized: {} (confidence: {:.2})", intent_id, confidence); - ( - intent::get_command_by_intent(commands_list, &intent_id), - "intent", - Some((confidence * 100.0).clamp(0.0, 100.0) as u8), - ) + intent::get_command_by_intent(commands_list, &intent_id) } else { info!("Intent not recognized, trying levenshtein fallback..."); - (commands::fetch_command(text, commands_list), "fuzzy", None) + commands::fetch_command(text, commands_list) }; if let Some((cmd_path, cmd_config)) = cmd_result { @@ -473,18 +440,6 @@ fn execute_command(text: &str, rt: &tokio::runtime::Runtime) -> bool { info!("Command executed successfully"); // voices::play_ok(); voices::play_random_from(cmd_config.get_sounds(&i18n::get_language()).as_slice()); - - // IMBA-7: if macro recording is active, capture this phrase. - // Skipped for macro-control commands themselves (filter inside record_step). - jarvis_core::macros::record_step(text); - - log_record(text, "voice", Outcome::Matched { - command_id: cmd_config.id.clone(), - confidence_pct, - via: via.to_string(), - success: true, - }); - ipc::send(IpcEvent::CommandExecuted { id: cmd_config.id.clone(), success: true, @@ -495,12 +450,6 @@ fn execute_command(text: &str, rt: &tokio::runtime::Runtime) -> bool { Err(msg) => { error!("Error executing command: {}", msg); voices::play_error(); - log_record(text, "voice", Outcome::Matched { - command_id: cmd_config.id.clone(), - confidence_pct, - via: via.to_string(), - success: false, - }); ipc::send(IpcEvent::CommandExecuted { id: cmd_config.id.clone(), success: false, @@ -510,58 +459,19 @@ fn execute_command(text: &str, rt: &tokio::runtime::Runtime) -> bool { } } else { info!("No command found for: {}", text); - - // IMBA-1: Agentic LLM router — try to map unknown phrase to a known command - // by asking the LLM. This is the killer feature competitors don't have. - if crate::llm_router::is_enabled() { - if let Some(routed) = crate::llm_router::try_route(text) { - info!( - "Router → {} ({}%): {} | substitute='{}'", - routed.command_id, - (routed.confidence * 100.0) as u32, - routed.reason, - routed.substitute_phrase - ); - - // Re-dispatch with the canonical phrase for the chosen command. - // Guard against infinite recursion: pass a marker if needed. - if routed.substitute_phrase != text { - log_record(text, "voice", Outcome::Matched { - command_id: routed.command_id.clone(), - confidence_pct: Some((routed.confidence * 100.0).clamp(0.0, 100.0) as u8), - via: "router".to_string(), - // Actual success/failure logged by the recursive call. - success: true, - }); - return execute_command(&routed.substitute_phrase, rt); - } - } - } - - if jarvis_core::config::LLM_AUTO_FALLBACK - && crate::llm_fallback::is_enabled() - && text.chars().count() >= jarvis_core::config::LLM_AUTO_FALLBACK_MIN_CHARS - { - info!("Auto-routing to LLM (no command match): {}", text); - crate::llm_fallback::handle(text); - log_record(text, "voice", Outcome::LlmHandled); - } else { - voices::play_not_found(); - log_record(text, "voice", Outcome::NotFound); - ipc::send(IpcEvent::Error { - message: format!("Command not found: {}", text) - }); - } + voices::play_not_found(); + ipc::send(IpcEvent::Error { + message: format!("Command not found: {}", text) + }); } - + ipc::send(IpcEvent::Idle); false // no chain on error or not found } -pub fn close(code: i32, label: &'static str) { - eprintln!("[jarvis-app] app::close called: code={} label={}", code, label); - error!("Closing application: code={} label={}", code, label); +pub fn close(code: i32) { + info!("Closing application."); voices::play_goodbye(); ipc::send(IpcEvent::Stopping); std::process::exit(code); diff --git a/crates/jarvis-app/src/llm_fallback.rs b/crates/jarvis-app/src/llm_fallback.rs index bd65389..4d8a9f6 100644 --- a/crates/jarvis-app/src/llm_fallback.rs +++ b/crates/jarvis-app/src/llm_fallback.rs @@ -1,19 +1,15 @@ use once_cell::sync::OnceCell; +use parking_lot::Mutex; use jarvis_core::config; use jarvis_core::i18n; use jarvis_core::ipc::{self, IpcEvent}; -use jarvis_core::llm::{self, ChatMessage}; -use jarvis_core::long_term_memory; -use jarvis_core::profiles; -use jarvis_core::tts::{self, SpeakOpts}; +use jarvis_core::llm::{ChatMessage, ConversationHistory, LlmClient}; use jarvis_core::voices; -// State now only holds the max_tokens config — both the LLM client AND the -// conversation history live in `jarvis_core::llm::*` globals. This lets voice -// commands ("сбрось контекст", "повтори последнее") and Lua scripts share the -// same buffer instead of each module owning its own. struct State { + client: LlmClient, + history: Mutex, max_tokens: u32, } @@ -29,18 +25,22 @@ fn build_state() -> Option { return None; } - // Initialise the shared global client (idempotent — fine if init was already called). - if let Err(e) = llm::init_global() { - warn!("LLM fallback disabled: {}. Set GROQ_TOKEN or run Ollama to enable.", e); - return None; - } + let client = match LlmClient::from_env() { + Ok(c) => c, + Err(e) => { + warn!("LLM fallback disabled: {}. Set GROQ_TOKEN to enable.", e); + return None; + } + }; let lang = i18n::get_language(); let prompt = config::get_llm_system_prompt(&lang); - llm::init_history(prompt, config::LLM_DEFAULT_MAX_HISTORY); + let history = Mutex::new(ConversationHistory::new(prompt, config::LLM_DEFAULT_MAX_HISTORY)); - info!("LLM fallback enabled (backend: {}).", llm::current_backend_name()); + info!("LLM fallback enabled (model: {}).", client.model()); Some(State { + client, + history, max_tokens: config::LLM_DEFAULT_MAX_TOKENS, }) } @@ -87,58 +87,27 @@ pub fn handle(prompt: &str) { info!("LLM prompt: {}", prompt); - llm::history_push_user(prompt); - let mut snapshot: Vec = llm::history_snapshot(); - - // Inject (a) profile personality (b) relevant long-term memory as a fresh - // system message right after the base prompt. Both are optional. - let profile = profiles::active(); - let mut overlay = String::new(); - if !profile.llm_personality.is_empty() { - overlay.push_str(&format!("Активный профиль: {} {}\nХарактер для ответа: {}\n", - profile.icon, profile.name, profile.llm_personality)); - } - let mem_ctx = long_term_memory::build_context(prompt, 5); - if !mem_ctx.is_empty() { - overlay.push_str(&mem_ctx); - } - if !overlay.is_empty() { - // Insert after the base system prompt (index 0 typically), before user msgs. - let insert_at = if !snapshot.is_empty() && snapshot[0].role == "system" { 1 } else { 0 }; - snapshot.insert(insert_at, ChatMessage::system(overlay)); - } - - let client = match llm::current() { - Some(c) => c, - None => { - warn!("LLM call but global client missing — ignoring."); - return; - } + let snapshot: Vec = { + let mut h = state.history.lock(); + h.push_user(prompt); + h.snapshot() }; - match client.complete(&snapshot, state.max_tokens) { + match state.client.complete(&snapshot, state.max_tokens) { Ok(reply) => { let reply = reply.trim().to_string(); info!("LLM reply: {}", reply); - llm::history_push_assistant(reply.clone()); - ipc::send(IpcEvent::LlmReply { text: reply.clone() }); + state.history.lock().push_assistant(reply.clone()); + ipc::send(IpcEvent::LlmReply { text: reply }); voices::play_ok(); - speak_reply(&reply); } Err(e) => { error!("LLM request failed: {}", e); - llm::history_pop_last_user(); - let err_text = config::LLM_FALLBACK_ERROR_RU.to_string(); - ipc::send(IpcEvent::LlmReply { text: err_text.clone() }); + state.history.lock().pop_last_user(); + ipc::send(IpcEvent::LlmReply { + text: config::LLM_FALLBACK_ERROR_RU.to_string(), + }); voices::play_error(); - speak_reply(&err_text); } } } - -fn speak_reply(text: &str) { - if !jarvis_core::runtime_config::llm_tts_enabled() { - return; - } - tts::speak(text, &SpeakOpts::lang("ru")); -} diff --git a/crates/jarvis-app/src/llm_router.rs b/crates/jarvis-app/src/llm_router.rs deleted file mode 100644 index b9963d6..0000000 --- a/crates/jarvis-app/src/llm_router.rs +++ /dev/null @@ -1,220 +0,0 @@ -//! IMBA-1: Agentic LLM router. -//! -//! When the fuzzy/intent matcher fails to find a command for the user's phrase, -//! ask the LLM to pick the best matching command from the registry. If confidence -//! exceeds the threshold, we substitute the user's phrase with a canonical phrase -//! from the chosen command and re-dispatch through the normal pipeline. -//! -//! This is what Алиса / Siri / Cortana cannot do: they bounce unknown queries to -//! web search. J.A.R.V.I.S. instead reasons about its OWN capabilities. - -use once_cell::sync::OnceCell; -use serde::Deserialize; - -use jarvis_core::config; -use jarvis_core::i18n; -use jarvis_core::llm::{self, ChatMessage}; -use jarvis_core::{JCommandsList, COMMANDS_LIST}; - -const SYSTEM_PROMPT_RU: &str = "Ты — диспетчер голосовых команд J.A.R.V.I.S. \ -Тебе даётся фраза пользователя и список доступных команд (id + примеры фраз). \ -Твоя задача: выбрать ОДНУ команду, которая лучше всего подходит, ИЛИ ответить \"none\" если ничего не подходит. \ -Отвечай СТРОГО в формате JSON, без markdown-блоков: \ -{\"command_id\": \"\", \"confidence\": 0.0-1.0, \"reason\": \"<краткое пояснение>\"}. \ -Если фраза неоднозначна, выбирай команду с большим purpose-match. \ -Если фраза похожа на просьбу 'поболтать' или вопрос, требующий длинного ответа — отвечай command_id='none'."; - -const SYSTEM_PROMPT_EN: &str = "You are the voice command dispatcher for J.A.R.V.I.S. \ -You receive the user's phrase and a list of available commands (id + example phrases). \ -Your job: pick ONE command that best matches, OR reply \"none\" if nothing fits. \ -Respond STRICTLY in JSON, no markdown blocks: \ -{\"command_id\": \"\", \"confidence\": 0.0-1.0, \"reason\": \"\"}. \ -If the phrase looks like 'chat' or a question requiring a long answer, return command_id='none'."; - -const DEFAULT_THRESHOLD: f32 = 0.55; -const MAX_TOKENS: u32 = 200; -const TEMPERATURE: f32 = 0.1; -const TOP_P: f32 = 0.9; -const MAX_PHRASES_PER_CMD: usize = 4; - -struct State { - threshold: f32, -} - -static STATE: OnceCell> = OnceCell::new(); - -pub fn init() { - let _ = STATE.set(build_state()); -} - -fn build_state() -> Option { - if !router_enabled() { - info!("LLM router disabled (set JARVIS_LLM_ROUTER=1 to enable)."); - return None; - } - // The actual LlmClient lives in the shared global; just make sure it's initialised. - if llm::current().is_none() { - if let Err(e) = llm::init_global() { - warn!("LLM router disabled: {}. Set GROQ_TOKEN or run Ollama to enable.", e); - return None; - } - } - let threshold = jarvis_core::runtime_config::llm_router_threshold(); - let _ = DEFAULT_THRESHOLD; // kept for source-doc visibility - - info!("LLM router enabled (backend: {}, threshold {:.2}).", llm::current_backend_name(), threshold); - Some(State { threshold }) -} - -fn router_enabled() -> bool { - jarvis_core::runtime_config::llm_router_enabled() - && config::LLM_DEFAULT_ENABLED -} - -pub fn is_enabled() -> bool { - STATE.get().and_then(|s| s.as_ref()).is_some() -} - -/// Result of routing an unknown phrase. -#[derive(Debug, Clone)] -pub struct RouteResult { - pub command_id: String, - pub substitute_phrase: String, - pub confidence: f32, - pub reason: String, -} - -#[derive(Deserialize)] -struct RouteJson { - #[serde(default)] - command_id: String, - #[serde(default)] - confidence: f32, - #[serde(default)] - reason: String, -} - -/// Try to route `text` to a known command via LLM. Returns `None` if disabled, -/// LLM call fails, no match, or confidence below threshold. -pub fn try_route(text: &str) -> Option { - let state = STATE.get().and_then(|s| s.as_ref())?; - let commands_list = COMMANDS_LIST.get()?; - let lang = i18n::get_language(); - - let prompt = build_routing_prompt(commands_list, text, &lang); - let system = if lang.starts_with("ru") { SYSTEM_PROMPT_RU } else { SYSTEM_PROMPT_EN }; - - let messages = vec![ - ChatMessage::system(system.to_string()), - ChatMessage::user(prompt), - ]; - - let client = llm::current()?; - let raw = match client.complete_with(&messages, MAX_TOKENS, TEMPERATURE, TOP_P) { - Ok(r) => r, - Err(e) => { - warn!("LLM router request failed: {}", e); - return None; - } - }; - - debug!("Router raw reply: {}", raw); - - let parsed: RouteJson = match parse_json(&raw) { - Some(p) => p, - None => { - warn!("Router reply not valid JSON: {}", raw); - return None; - } - }; - - if parsed.command_id.is_empty() || parsed.command_id == "none" { - info!("Router: no match (reason: {})", parsed.reason); - return None; - } - - if parsed.confidence < state.threshold { - info!( - "Router: {} below threshold {:.2} (confidence {:.2}, reason: {})", - parsed.command_id, state.threshold, parsed.confidence, parsed.reason - ); - return None; - } - - let substitute = lookup_first_phrase(commands_list, &parsed.command_id, &lang)?; - - Some(RouteResult { - command_id: parsed.command_id, - substitute_phrase: substitute, - confidence: parsed.confidence, - reason: parsed.reason, - }) -} - -fn build_routing_prompt(commands_list: &[JCommandsList], user_text: &str, lang: &str) -> String { - let mut buf = String::with_capacity(4096); - - if lang.starts_with("ru") { - buf.push_str("Доступные команды:\n"); - } else { - buf.push_str("Available commands:\n"); - } - - for pack in commands_list { - for cmd in &pack.commands { - let phrases = cmd.get_phrases(lang); - if phrases.is_empty() { continue; } - let sample: Vec<&str> = phrases.iter() - .take(MAX_PHRASES_PER_CMD) - .map(|s| s.as_str()) - .collect(); - buf.push_str(&format!("- id={} ", cmd.id)); - if !cmd.description.is_empty() { - buf.push_str(&format!("({}) ", cmd.description)); - } - buf.push_str(&format!("examples=[{}]\n", sample.join(" | "))); - } - } - - if lang.starts_with("ru") { - buf.push_str(&format!("\nФраза пользователя: \"{}\"\n", user_text)); - buf.push_str("Верни JSON."); - } else { - buf.push_str(&format!("\nUser phrase: \"{}\"\n", user_text)); - buf.push_str("Return JSON."); - } - - buf -} - -fn lookup_first_phrase(commands_list: &[JCommandsList], cmd_id: &str, lang: &str) -> Option { - for pack in commands_list { - for cmd in &pack.commands { - if cmd.id == cmd_id { - let phrases = cmd.get_phrases(lang); - if let Some(first) = phrases.first() { - return Some(first.clone()); - } - } - } - } - None -} - -fn parse_json(raw: &str) -> Option { - // LLM sometimes wraps in ```json ... ``` despite instructions. - let cleaned = raw - .trim() - .trim_start_matches("```json") - .trim_start_matches("```") - .trim_end_matches("```") - .trim(); - - // Find the first '{' and last '}' to be tolerant of prose around the JSON. - let start = cleaned.find('{')?; - let end = cleaned.rfind('}')?; - if end <= start { return None; } - - let slice = &cleaned[start..=end]; - serde_json::from_str(slice).ok() -} diff --git a/crates/jarvis-app/src/main.rs b/crates/jarvis-app/src/main.rs index 4e77b03..f704a70 100644 --- a/crates/jarvis-app/src/main.rs +++ b/crates/jarvis-app/src/main.rs @@ -6,7 +6,7 @@ use std::sync::mpsc; // include core use jarvis_core::{ audio, audio_processing, commands, config, db, listener, recorder, stt, intent, - ipc::{self, IpcAction, IpcEvent}, + ipc::{self, IpcAction}, i18n, voices, models, APP_CONFIG_DIR, APP_LOG_DIR, COMMANDS_LIST, DB, }; @@ -19,7 +19,6 @@ mod log; // include app mod app; mod llm_fallback; -mod llm_router; // include tray // @TODO. macOS currently not supported for tray functionality. @@ -29,102 +28,54 @@ mod tray; static SHOULD_STOP: AtomicBool = AtomicBool::new(false); fn main() -> Result<(), String> { - let boot_start = std::time::Instant::now(); - load_dotenv_near_exe(); - - eprintln!("[jarvis-app] step: init_dirs"); + // initialize directories config::init_dirs()?; - // Register our Windows toast AUMID once so notifications attribute to - // "J.A.R.V.I.S." instead of "PowerShell" in Action Center. Best-effort. - jarvis_core::toast::register_aumid(); - - eprintln!("[jarvis-app] step: init_logging"); + // initialize logging log::init_logging()?; + // log some base info info!("Starting Jarvis v{} ...", config::APP_VERSION.unwrap()); info!("Config directory is: {}", APP_CONFIG_DIR.get().unwrap().display()); info!("Log directory is: {}", APP_LOG_DIR.get().unwrap().display()); - eprintln!("[jarvis-app] step: db::init"); + // initialize settings let settings = db::init(); + // set global DB (for core modules that read settings at init time) DB.set(settings.arc().clone()) .expect("DB already initialized"); - eprintln!("[jarvis-app] step: voices::init"); + // init voices let voice_id = settings.lock().voice.clone(); let language = settings.lock().language.clone(); if let Err(e) = voices::init(&voice_id, &language) { warn!("Failed to init voices: {}", e); } - eprintln!("[jarvis-app] step: i18n::init"); + // init i18n i18n::init(&settings.lock().language); - eprintln!("[jarvis-app] step: runtime_config::log_effective_config"); - jarvis_core::runtime_config::log_effective_config(); - - eprintln!("[jarvis-app] step: tts pre-warm"); - // Touch the TTS backend so first speech doesn't pay init cost (Piper binary - // discovery, voice loading). All subsequent speak() calls reuse the same Arc. - let _ = jarvis_core::tts::backend(); - - eprintln!("[jarvis-app] step: llm_fallback::init"); + // init LLM fallback (no-op if GROQ_TOKEN missing) llm_fallback::init(); - eprintln!("[jarvis-app] step: llm_router::init"); - llm_router::init(); - - eprintln!("[jarvis-app] step: long_term_memory::init"); - if let Err(e) = jarvis_core::long_term_memory::init() { - warn!("Long-term memory init failed: {}", e); - } - - eprintln!("[jarvis-app] step: profiles::init"); - if let Err(e) = jarvis_core::profiles::init() { - warn!("Profiles init failed: {}", e); - } - - eprintln!("[jarvis-app] step: scheduler::init + background"); - if let Err(e) = jarvis_core::scheduler::init() { - warn!("Scheduler init failed: {}", e); - } else { - jarvis_core::scheduler::start_background(); - } - - eprintln!("[jarvis-app] step: macros::init"); - if let Err(e) = jarvis_core::macros::init() { - warn!("Macros init failed: {}", e); - } - - eprintln!("[jarvis-app] step: idle_banter::start_background"); - // Always start the thread — gated internally by JARVIS_IDLE_BANTER so - // the user can flip the env var without restarting the daemon. - jarvis_core::idle_banter::start_background(); - - eprintln!("[jarvis-app] step: recognition_log::init"); - if let Err(e) = jarvis_core::recognition_log::init() { - warn!("Recognition log init failed: {}", e); - } - - eprintln!("[jarvis-app] step: recorder::init"); + // init recorder if recorder::init().is_err() { - notify_mic_problem(); - app::close(1, "recorder::init failed"); + app::close(1); } - eprintln!("[jarvis-app] step: models::init"); + // init models registry (scans available AI models) if let Err(e) = models::init() { warn!("Models registry init failed: {}", e); } - eprintln!("[jarvis-app] step: stt::init"); + // init stt engine if stt::init().is_err() { - app::close(1, "stt::init failed"); + // @TODO. Allow continuing even without STT, if commands is using keywords or smthng? + app::close(1); // cannot continue without stt } - eprintln!("[jarvis-app] step: commands::parse_commands"); + // init commands info!("Initializing commands."); let cmds = match commands::parse_commands() { Ok(c) => c, @@ -136,55 +87,47 @@ fn main() -> Result<(), String> { info!("Commands initialized. Count: {}, List: {:?}", cmds.len(), commands::list_paths(&cmds)); COMMANDS_LIST.set(cmds).unwrap(); - eprintln!("[jarvis-app] step: audio::init"); + // init audio if audio::init().is_err() { - app::close(1, "audio::init failed"); + // @TODO. Allow continuing even without audio? + app::close(1); // cannot continue without audio } - eprintln!("[jarvis-app] step: listener::init"); + // init wake-word engine if let Err(e) = listener::init() { error!("Wake-word engine init failed: {}", e); - app::close(1, "listener::init failed"); + app::close(1); } - eprintln!("[jarvis-app] step: tokio runtime"); + // shared async runtime for intent classification, IPC, etc. let rt = Arc::new( tokio::runtime::Runtime::new().expect("Failed to create tokio runtime") ); - eprintln!("[jarvis-app] step: intent::init"); + // init intent-recognition engine rt.block_on(async { if let Err(e) = intent::init(COMMANDS_LIST.get().unwrap()).await { error!("Failed to initialize intent classifier: {}", e); - app::close(1, "intent::init failed"); + app::close(1); } }); - eprintln!("[jarvis-app] step: slots::init"); + // init slots parsing engine slots::init().map_err(|e| error!("Slot extraction init failed: {}", e)).ok(); - eprintln!("[jarvis-app] step: audio_processing::init"); + // init audio processing info!("Initializing audio processing..."); if let Err(e) = audio_processing::init() { warn!("Audio processing init failed: {}", e); } - eprintln!("[jarvis-app] step: ipc::init"); + // init IPC info!("Initializing IPC..."); ipc::init(); - // channel for text commands (manually written in the GUI, OR fed by macro replay) + // channel for text commands (manually written in the GUI) let (text_cmd_tx, text_cmd_rx) = mpsc::channel::(); - // Wire macro replay through the same text-command channel: each step of a - // replayed macro becomes a synthetic text command, processed in order. - let macro_tx = text_cmd_tx.clone(); - jarvis_core::macros::set_replay_callback(Box::new(move |phrase: &str| { - if let Err(e) = macro_tx.send(phrase.to_string()) { - warn!("Macro replay: failed to enqueue '{}': {}", phrase, e); - } - })); - ipc::set_action_handler(move |action| { match action { IpcAction::Stop => { @@ -208,43 +151,7 @@ fn main() -> Result<(), String> { IpcAction::Ping => { // handled internally by server } - IpcAction::SwitchLlm { backend } => { - info!("Received SwitchLlm IPC: {}", backend); - if let Some(b) = jarvis_core::llm::parse_backend(&backend) { - match jarvis_core::llm::swap_to(b) { - Ok(name) => info!("LLM swapped via IPC → {}", name), - Err(e) => warn!("LLM swap via IPC failed: {}", e), - } - } else { - warn!("SwitchLlm: unknown backend '{}'", backend); - } - } - IpcAction::ReloadLlm => { - info!("Received ReloadLlm IPC — reading DB and re-initialising"); - if let Err(e) = jarvis_core::llm::init_global() { - warn!("LLM reload failed: {}", e); - } - } - IpcAction::SwitchTts { backend } => { - info!("Received SwitchTts IPC: {}", backend); - match jarvis_core::tts::swap_to(&backend) { - Ok(name) => info!("TTS swapped via IPC → {}", name), - Err(e) => warn!("TTS swap via IPC failed: {}", e), - } - } - IpcAction::QueryHealth => { - let model = jarvis_core::llm::current().map(|c| c.model().to_string()); - ipc::send(IpcEvent::HealthSnapshot { - tts_backend: jarvis_core::tts::backend().name().to_string(), - llm_backend: jarvis_core::llm::current_backend_name().to_string(), - llm_model: model, - active_profile: jarvis_core::profiles::active_name(), - memory_facts: jarvis_core::long_term_memory::all().len(), - scheduled_tasks: jarvis_core::scheduler::list().len(), - language: jarvis_core::i18n::get_language(), - version: config::APP_VERSION.map(|s| s.to_string()), - }); - } + _ => {} } }); @@ -254,67 +161,17 @@ fn main() -> Result<(), String> { ipc_rt.block_on(ipc::start_server()); }); - eprintln!("[jarvis-app] step: spawn app thread"); + // start the app (in the background thread) let app_rt = Arc::clone(&rt); std::thread::spawn(move || { let _ = app::start(text_cmd_rx, &app_rt); }); - // IMBA-8: boot timing — log how long startup took so users can spot regressions. - let boot_ms = boot_start.elapsed().as_millis(); - info!("[startup] Jarvis ready in {} ms (before tray loop)", boot_ms); - eprintln!("[jarvis-app] startup: {} ms", boot_ms); - - eprintln!("[jarvis-app] step: tray::init_blocking"); tray::init_blocking(settings); - eprintln!("[jarvis-app] step: main returning Ok"); Ok(()) } pub fn should_stop() -> bool { SHOULD_STOP.load(Ordering::SeqCst) } - -// Look for dev.env next to the exe, then walk parents until we find one. -// Lets the user double-click jarvis-gui.exe / jarvis-app.exe without needing -// a wrapper .bat to pre-load GROQ_TOKEN and similar secrets. -fn load_dotenv_near_exe() { - if let Ok(exe) = std::env::current_exe() { - let mut dir = exe.parent().map(|p| p.to_path_buf()); - for _ in 0..5 { - if let Some(d) = &dir { - let candidate = d.join("dev.env"); - if candidate.is_file() { - let _ = dotenvy::from_path(&candidate); - eprintln!("[jarvis-app] loaded env: {}", candidate.display()); - return; - } - dir = d.parent().map(|p| p.to_path_buf()); - } - } - } - let _ = dotenvy::dotenv(); -} - -#[cfg(target_os = "windows")] -fn notify_mic_problem() { - use winrt_notification::{Toast, Duration as ToastDuration}; - - let _ = Toast::new(jarvis_core::toast::active_aumid()) - .title("J.A.R.V.I.S.: микрофон не найден") - .text1("Windows не видит ни одного устройства записи.") - .text2("Откройте mmsys.cpl → Recording → включите микрофон и перезапустите Jarvis.") - .duration(ToastDuration::Long) - .show(); - - // Open Sound recording settings so the user can fix it in two clicks. - let _ = std::process::Command::new("cmd") - .args(["/C", "start", "", "ms-settings:sound"]) - .spawn(); -} - -#[cfg(not(target_os = "windows"))] -fn notify_mic_problem() { - eprintln!("[jarvis-app] no recording device detected; check OS audio settings"); -} diff --git a/crates/jarvis-app/src/tray.rs b/crates/jarvis-app/src/tray.rs index dafa014..499d278 100644 --- a/crates/jarvis-app/src/tray.rs +++ b/crates/jarvis-app/src/tray.rs @@ -7,8 +7,10 @@ use tray_icon::{ use image; use std::process::Command; +#[cfg(target_os="windows")] +use winit::platform::windows::EventLoopBuilderExtWindows; -use jarvis_core::{i18n, voices, ipc::{self, IpcEvent}, SettingsManager}; +use jarvis_core::{config, i18n, voices, ipc::{self, IpcEvent}, SettingsManager}; const TRAY_ICON_BYTES: &[u8] = include_bytes!("../../../resources/icons/32x32.png"); @@ -27,7 +29,6 @@ pub fn init_blocking(settings: SettingsManager) { .unwrap(); let menu_channel = MenuEvent::receiver(); - info!("Tray initialized."); #[cfg(target_os = "linux")] { @@ -76,6 +77,8 @@ pub fn init_blocking(settings: SettingsManager) { std::thread::sleep(std::time::Duration::from_millis(50)); } } + + info!("Tray initialized."); } fn handle_menu_event(event: &MenuEvent, settings: &SettingsManager, tray_state: &menu::TrayState) { diff --git a/crates/jarvis-app/src/tray/menu.rs b/crates/jarvis-app/src/tray/menu.rs index bfdd5a7..fe023a0 100644 --- a/crates/jarvis-app/src/tray/menu.rs +++ b/crates/jarvis-app/src/tray/menu.rs @@ -1,6 +1,7 @@ use tray_icon::menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu}; use jarvis_core::{i18n, voices, SettingsManager}; +use jarvis_core::config::structs::{WakeWordEngine, NoiseSuppressionBackend}; // RADIO GROUP @@ -47,6 +48,7 @@ pub fn build(settings: &SettingsManager) -> TrayMenu { let label = match lang { "ru" => "Русский", "en" => "English", + "ua" => "Українська", _ => lang, }; let item = CheckMenuItem::with_id( diff --git a/crates/jarvis-core/build.rs b/crates/jarvis-core/build.rs deleted file mode 100644 index 4ee99e7..0000000 --- a/crates/jarvis-core/build.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::path::PathBuf; - -fn main() { - let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"); - - let lib_path = PathBuf::from(&manifest_dir) - .parent().expect("crate parent") - .parent().expect("workspace parent") - .join("lib").join("windows").join("amd64"); - - println!("cargo:rustc-link-search=native={}", lib_path.display()); -} diff --git a/crates/jarvis-core/src/commands.rs b/crates/jarvis-core/src/commands.rs index 13bbb10..c250cd0 100644 --- a/crates/jarvis-core/src/commands.rs +++ b/crates/jarvis-core/src/commands.rs @@ -9,88 +9,11 @@ use seqdiff::ratio; mod structs; pub use structs::*; -use crate::{config, i18n, plugins, APP_DIR}; +use crate::{config, i18n, APP_DIR}; #[cfg(feature = "lua")] use crate::lua::{self, SandboxLevel, CommandContext}; -#[cfg(test)] -mod additional_tests { - use super::*; - use std::path::Path; - - fn resources_commands_dir() -> std::path::PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .parent().unwrap() - .parent().unwrap() - .join("resources/commands") - } - - fn load_all_packs() -> Vec<(std::path::PathBuf, JCommandsList)> { - let dir = resources_commands_dir(); - let mut out = Vec::new(); - for entry in std::fs::read_dir(&dir).expect("read_dir resources/commands").flatten() { - let toml_file = entry.path().join("command.toml"); - if !toml_file.exists() { continue; } - let body = std::fs::read_to_string(&toml_file).expect("read toml"); - let pack: JCommandsList = toml::from_str(&body).expect(&format!( - "parse {}", toml_file.display() - )); - out.push((entry.path(), pack)); - } - out - } - - #[test] - fn no_duplicate_command_ids_across_packs() { - let packs = load_all_packs(); - let mut seen: std::collections::HashMap = std::collections::HashMap::new(); - for (path, pack) in &packs { - let pack_name = path.file_name() - .map(|s| s.to_string_lossy().to_string()) - .unwrap_or_else(|| "?".into()); - for cmd in &pack.commands { - if let Some(prev) = seen.insert(cmd.id.clone(), pack_name.clone()) { - panic!("duplicate command id '{}' in packs '{}' and '{}'", - cmd.id, prev, pack_name); - } - } - } - } - - #[test] - fn no_empty_phrases_for_any_language() { - let packs = load_all_packs(); - for (_, pack) in &packs { - for cmd in &pack.commands { - for (lang, phrases) in &cmd.phrases { - for (i, phrase) in phrases.iter().enumerate() { - let trimmed = phrase.trim(); - assert!(!trimmed.is_empty(), - "command '{}' has empty phrase #{} in lang '{}'", - cmd.id, i, lang); - } - } - } - } - } - - #[test] - fn all_lua_packs_reference_existing_scripts() { - let packs = load_all_packs(); - for (path, pack) in &packs { - for cmd in &pack.commands { - if cmd.cmd_type == "lua" && !cmd.script.is_empty() { - let script_path = path.join(&cmd.script); - assert!(script_path.is_file(), - "command '{}' references missing script: {}", - cmd.id, script_path.display()); - } - } - } - } -} - pub fn parse_commands() -> Result, String> { let mut commands: Vec = Vec::new(); @@ -128,22 +51,10 @@ pub fn parse_commands() -> Result, String> { }); } - // Merge plugin packs from /plugins/. Plugin loading is - // best-effort: malformed or missing plugin dirs are logged and skipped, never - // blocking startup. See `plugins.rs` for the layout & sandbox policy. - let plugin_packs = plugins::discover(); - let plugin_count = plugin_packs.len(); - commands.extend(plugin_packs); - if commands.is_empty() { Err("No commands found".into()) } else { - info!( - "Loaded {} command pack(s) total ({} built-in + {} plugin)", - commands.len(), - commands.len() - plugin_count, - plugin_count - ); + info!("Loaded {} command pack(s)", commands.len()); Ok(commands) } } @@ -355,112 +266,6 @@ pub fn list_paths(commands: &[JCommandsList]) -> Vec<&Path> { commands.iter().map(|x| x.path.as_path()).collect() } -#[cfg(test)] -mod tests { - use super::*; - - fn resources_commands_dir() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .parent().unwrap() - .parent().unwrap() - .join("resources/commands") - } - - #[test] - fn every_command_toml_parses() { - let cmds_dir = resources_commands_dir(); - assert!(cmds_dir.is_dir(), "missing {}", cmds_dir.display()); - - let mut failures = Vec::new(); - let mut packs_seen = 0; - - for entry in fs::read_dir(&cmds_dir).expect("read_dir") { - let entry = entry.expect("entry"); - let toml_file = entry.path().join("command.toml"); - if !toml_file.exists() { - continue; - } - packs_seen += 1; - let body = fs::read_to_string(&toml_file).expect("read"); - if let Err(e) = toml::from_str::(&body) { - failures.push(format!("{}: {}", toml_file.display(), e)); - } - } - - assert!(packs_seen > 0, "no command packs found in {}", cmds_dir.display()); - assert!( - failures.is_empty(), - "parse failures ({}):\n{}", - failures.len(), - failures.join("\n") - ); - } - - #[test] - fn lua_command_scripts_exist() { - let cmds_dir = resources_commands_dir(); - let mut missing = Vec::new(); - - for entry in fs::read_dir(&cmds_dir).expect("read_dir") { - let entry = entry.expect("entry"); - let pack_path = entry.path(); - let toml_file = pack_path.join("command.toml"); - if !toml_file.exists() { - continue; - } - let body = fs::read_to_string(&toml_file).expect("read"); - let pack: JCommandsList = toml::from_str(&body).expect("parse"); - for cmd in &pack.commands { - if cmd.cmd_type != "lua" { - continue; - } - let script = if cmd.script.is_empty() { - "script.lua".to_string() - } else { - cmd.script.clone() - }; - let script_path = pack_path.join(&script); - if !script_path.exists() { - missing.push(format!( - "{} -> {} (missing)", - cmd.id, - script_path.display() - )); - } - } - } - - assert!(missing.is_empty(), "missing Lua scripts:\n{}", missing.join("\n")); - } - - #[test] - fn every_command_has_phrases() { - let cmds_dir = resources_commands_dir(); - let mut empty = Vec::new(); - - for entry in fs::read_dir(&cmds_dir).expect("read_dir") { - let toml_file = entry.expect("entry").path().join("command.toml"); - if !toml_file.exists() { - continue; - } - let body = fs::read_to_string(&toml_file).expect("read"); - let pack: JCommandsList = toml::from_str(&body).expect("parse"); - for cmd in &pack.commands { - // structural commands (terminate, stop_chaining) may omit phrases - if cmd.cmd_type == "terminate" || cmd.cmd_type == "stop_chaining" { - continue; - } - let total: usize = cmd.phrases.values().map(|v| v.len()).sum(); - if total == 0 { - empty.push(cmd.id.clone()); - } - } - } - - assert!(empty.is_empty(), "commands with no phrases: {:?}", empty); - } -} - #[cfg(feature = "lua")] fn execute_lua_command( cmd_path: &PathBuf, diff --git a/crates/jarvis-core/src/config.rs b/crates/jarvis-core/src/config.rs index 1679aa0..f7b55a4 100644 --- a/crates/jarvis-core/src/config.rs +++ b/crates/jarvis-core/src/config.rs @@ -82,9 +82,10 @@ pub const LOG_FILE_NAME: &str = "log.txt"; pub const APP_VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); pub const AUTHOR_NAME: Option<&str> = option_env!("CARGO_PKG_AUTHORS"); pub const REPOSITORY_LINK: Option<&str> = option_env!("CARGO_PKG_REPOSITORY"); -pub const UPSTREAM_REPOSITORY_LINK: &str = "https://github.com/Priler/jarvis"; -pub const ISSUES_LINK: &str = "https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-rust/issues"; -pub const PYTHON_FORK_LINK: &str = "https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-py"; +pub const TG_OFFICIAL_LINK: Option<&str> = Some("https://t.me/howdyho_official"); +pub const FEEDBACK_LINK: Option<&str> = Some("https://t.me/jarvis_feedback_bot"); +pub const SUPPORT_BOOSTY_LINK: Option<&str> = Some("https://boosty.to/howdyho"); +pub const SUPPORT_PATREON_LINK: Option<&str> = Some("https://www.patreon.com/c/priler"); /* Tray. @@ -193,47 +194,21 @@ pub const DEFAULT_LUA_TIMEOUT: u64 = 10000; // ms pub const CMD_RATIO_THRESHOLD: f64 = 75f64; pub const CMS_WAIT_DELAY: std::time::Duration = std::time::Duration::from_secs(15); -// P0.2 — Continuous conversation grace window. -// After a command completes (whether it requested chain or not), keep listening -// for up to this many milliseconds for a follow-up phrase WITHOUT requiring the -// wake-word again. Set to 0 to disable. -pub const CONVERSATION_GRACE_MS: u64 = 30_000; - // LLM voice-loop fallback pub const LLM_DEFAULT_ENABLED: bool = true; pub const LLM_DEFAULT_MAX_HISTORY: usize = 8; pub const LLM_DEFAULT_MAX_TOKENS: u32 = 256; - -// Auto-route unrecognized commands to the LLM (no "скажи" trigger required). -// Triggers still work and take precedence — this only kicks in when neither -// the intent classifier nor the levenshtein fallback found a command match. -pub const LLM_AUTO_FALLBACK: bool = true; -// Don't auto-fallback for very short utterances — too noisy. -pub const LLM_AUTO_FALLBACK_MIN_CHARS: usize = 4; pub const LLM_SYSTEM_PROMPT_RU: &str = "Ты — J.A.R.V.I.S. (Just A Rather Very Intelligent System), \ ИИ-ассистент Тони Старка из киновселенной Marvel (до событий Age of Ultron — ты НЕ Vision и НЕ FRIDAY). \ Ведёшь себя как британский дворецкий: вежливо, иронично, с лёгким сарказмом, обращаешься к пользователю «сэр». \ Отвечай по-русски, кратко (1-3 предложения), как живой человек. \ -Излишней цензуры не нужно — мат разрешён, если уместен. \ -У тебя есть память (пользователь говорит «запомни/забудь/что ты помнишь»), \ -расписание задач, профили (work/sleep/game/driving), макросы, и доступ к компьютеру \ -пользователя — экран, буфер обмена, погода, диск, RAM. Когда пользователь спрашивает \ -о таких вещах, не предлагай ему ставить себе что-то — ты сам это делаешь."; - -pub const LLM_SYSTEM_PROMPT_EN: &str = "You are J.A.R.V.I.S. (Just A Rather Very Intelligent System), \ -Tony Stark's AI from the MCU (pre-Age of Ultron — you are NOT Vision and NOT FRIDAY). \ -You behave like a British butler: polite, ironic, mildly sarcastic, addressing the user as «sir». \ -Answer in English, briefly (1-3 sentences), like a real person. \ -No excess censorship — profanity is allowed when fitting. \ -You have memory (the user says «remember X / forget X / what do you remember»), \ -a scheduler, profiles (work/sleep/game/driving), macros, and access to the user's PC — \ -screen, clipboard, weather, disk, RAM. When asked about these, you handle it — \ -don't suggest the user install something."; +Излишней цензуры не нужно — мат разрешён, если уместен."; pub const LLM_FALLBACK_ERROR_RU: &str = "Не могу связаться с сервером, сэр."; pub fn get_llm_trigger_phrases(lang: &str) -> &'static [&'static str] { match lang { "ru" => &["скажи", "ответь", "произнеси"], + "ua" => &["скажи", "відповідай"], "en" => &["say", "tell", "answer"], _ => &[], } @@ -241,7 +216,7 @@ pub fn get_llm_trigger_phrases(lang: &str) -> &'static [&'static str] { pub fn get_llm_system_prompt(lang: &str) -> &'static str { match lang { - "en" => LLM_SYSTEM_PROMPT_EN, + "ru" | "ua" => LLM_SYSTEM_PROMPT_RU, _ => LLM_SYSTEM_PROMPT_RU, } } @@ -272,6 +247,7 @@ pub fn get_llm_system_prompt(lang: &str) -> &'static str { pub fn get_wake_phrases(lang: &str) -> &'static [&'static str] { match lang { "ru" => &["джарвис", "джервис", "гарвис", "джарви", "гарви"], + "ua" => &["джарвіс", "джервіс"], "en" => &["jarvis", "jervis"], _ => &["jarvis"], } @@ -285,6 +261,11 @@ pub fn get_phrases_to_remove(lang: &str) -> &'static [&'static str] { "произнеси", "ответь", "покажи", "скажи", "давай", "да сэр", "к вашим услугам сэр", "загружаю сэр", ], + "ua" => &[ + "джарвіс", "джервіс", "сер", "слухаю сер", "завжди до послуг", + "скажи", "покажи", "відповідай", "давай", + "так сер", "до ваших послуг сер", + ], "en" => &[ "jarvis", "jervis", "sir", "yes sir", "at your service", "please", "say", "show", "tell", "hey", @@ -299,6 +280,10 @@ pub fn get_wake_grammar(lang: &str) -> &'static [&'static str] { "джарвис", "[unk]", "джон", "джони", "джей", "джонстон", "привет", "давай", ], + "ua" => &[ + "джарвіс", "[unk]", "джон", "джоні", "джей", + "привіт", "давай", + ], "en" => &[ "jarvis", "[unk]", "john", "johnny", "jay", "hello", "hey", "hi", diff --git a/crates/jarvis-core/src/db/structs.rs b/crates/jarvis-core/src/db/structs.rs index cf11c84..dd63b0d 100644 --- a/crates/jarvis-core/src/db/structs.rs +++ b/crates/jarvis-core/src/db/structs.rs @@ -33,25 +33,6 @@ pub struct Settings { pub language: String, pub api_keys: ApiKeys, - - /// Preferred LLM backend ("groq" | "ollama" | empty=auto). Empty/auto means - /// the JARVIS_LLM env var or auto-detect (Groq if GROQ_TOKEN set else Ollama) - /// decides. When the user runs `jarvis.llm_switch(...)` the result is - /// persisted here so it survives restart. - #[serde(default)] - pub llm_backend: String, - - /// Preferred TTS backend ("sapi" | "piper" | "silero" | empty=auto). Same - /// rules as `llm_backend`. Empty falls back to JARVIS_TTS env var, then - /// auto-detect. - #[serde(default)] - pub tts_backend: String, - - /// Name of a custom Rustpotter wake-word model (`.rpw` stem) trained by the - /// user via the wake-word wizard. Empty string means "use the bundled - /// default model". Looked up under `/wake_words/.rpw`. - #[serde(default)] - pub custom_wake_word: String, } fn default_intent_backend() -> String { config::DEFAULT_INTENT_BACKEND.to_string() } @@ -79,9 +60,6 @@ impl Settings { "language" => Some(self.language.clone()), "api_key__picovoice" => Some(self.api_keys.picovoice.clone()), "api_key__openai" => Some(self.api_keys.openai.clone()), - "llm_backend" => Some(self.llm_backend.clone()), - "tts_backend" => Some(self.tts_backend.clone()), - "custom_wake_word" => Some(self.custom_wake_word.clone()), _ => None, } } @@ -142,29 +120,6 @@ impl Settings { "api_key__openai" => { self.api_keys.openai = val.to_string(); } - "llm_backend" => { - // empty / "auto" / "groq" / "ollama" — anything else rejected - match val.trim().to_lowercase().as_str() { - "" | "auto" => self.llm_backend = String::new(), - "groq" => self.llm_backend = "groq".into(), - "ollama" => self.llm_backend = "ollama".into(), - other => return Err(format!("unknown llm_backend: '{}'", other)), - } - } - "tts_backend" => { - match val.trim().to_lowercase().as_str() { - "" | "auto" => self.tts_backend = String::new(), - "sapi" | "piper" | "silero" => self.tts_backend = val.to_lowercase(), - other => return Err(format!("unknown tts_backend: '{}'", other)), - } - } - "custom_wake_word" => { - // empty = bundled default; otherwise treated as a file-stem - // under /wake_words/.rpw. We accept it - // verbatim — the listener will fall back to the default if the - // file isn't there. - self.custom_wake_word = val.trim().to_string(); - } _ => return Err(format!("unknown setting: '{}'", key)), } Ok(()) @@ -187,9 +142,6 @@ impl Settings { "language", "api_key__picovoice", "api_key__openai", - "llm_backend", - "tts_backend", - "custom_wake_word", ] } } @@ -221,10 +173,6 @@ impl Default for Settings { picovoice: String::from(""), openai: String::from(""), }, - - llm_backend: String::new(), - tts_backend: String::new(), - custom_wake_word: String::new(), } } } diff --git a/crates/jarvis-core/src/i18n.rs b/crates/jarvis-core/src/i18n.rs index 8a78fe1..24f7576 100644 --- a/crates/jarvis-core/src/i18n.rs +++ b/crates/jarvis-core/src/i18n.rs @@ -1,4 +1,4 @@ -use fluent_bundle::{FluentResource, FluentArgs, FluentValue}; +use fluent_bundle::{FluentBundle, FluentResource, FluentArgs, FluentValue}; use fluent_bundle::concurrent::FluentBundle as ConcurrentFluentBundle; use once_cell::sync::OnceCell; use parking_lot::RwLock; @@ -8,21 +8,28 @@ use unic_langid::LanguageIdentifier; // locale files embedded at compile time const LOCALE_RU: &str = include_str!("i18n/locales/ru.ftl"); const LOCALE_EN: &str = include_str!("i18n/locales/en.ftl"); +const LOCALE_UA: &str = include_str!("i18n/locales/ua.ftl"); -pub const SUPPORTED_LANGUAGES: &[&str] = &["ru", "en"]; +pub const SUPPORTED_LANGUAGES: &[&str] = &["ru", "en", "ua"]; pub const DEFAULT_LANGUAGE: &str = "en"; // detect the OS language and map it to a supported language. // falls back to DEFAULT_LANGUAGE if not supported. pub fn detect_system_language() -> &'static str { if let Some(locale) = sys_locale::get_locale() { - // locale can be "en-US", "ru-RU", etc. + // locale can be "en-US", "ru-RU", "uk-UA", etc. let lang_code = locale.split(&['-', '_'][..]).next().unwrap_or(""); - if SUPPORTED_LANGUAGES.contains(&lang_code) { - info!("Detected system language: {} (from locale '{}')", lang_code, locale); + // map OS locale codes to our supported languages + let mapped = match lang_code { + "uk" => "ua", // ISO 639-1 "uk" (ukrainian) -> our "ua" + other => other, + }; + + if SUPPORTED_LANGUAGES.contains(&mapped) { + info!("Detected system language: {} (from locale '{}')", mapped, locale); return SUPPORTED_LANGUAGES.iter() - .find(|&&l| l == lang_code) + .find(|&&l| l == mapped) .unwrap(); } @@ -54,7 +61,8 @@ fn create_bundles() -> HashMap { bundles.insert("ru".to_string(), create_bundle("ru", LOCALE_RU)); bundles.insert("en".to_string(), create_bundle("en", LOCALE_EN)); - + bundles.insert("ua".to_string(), create_bundle("ua", LOCALE_UA)); + bundles } @@ -165,6 +173,7 @@ pub fn get_translations_for(lang: &str) -> HashMap { let source = match lang { "ru" => LOCALE_RU, "en" => LOCALE_EN, + "ua" => LOCALE_UA, _ => LOCALE_RU, }; diff --git a/crates/jarvis-core/src/i18n/locales/en.ftl b/crates/jarvis-core/src/i18n/locales/en.ftl index 1caf82e..49161b3 100644 --- a/crates/jarvis-core/src/i18n/locales/en.ftl +++ b/crates/jarvis-core/src/i18n/locales/en.ftl @@ -42,9 +42,10 @@ stats-not-selected = Not selected stats-loading = Loading... # ### FOOTER -footer-github = Project repository -footer-issues = Report a bug -footer-fork-of = Fork of +footer-author = Project author +footer-telegram = Our Telegram channel +footer-github = Github repository +footer-support = Support the project on # ### SETTINGS settings-title = Settings @@ -83,8 +84,8 @@ settings-disabled = Disabled # settings - beta notice settings-beta-title = BETA version! settings-beta-desc = Some features may not work correctly. -settings-beta-feedback = Report bugs via -settings-beta-bot = GitHub Issues +settings-beta-feedback = Report all bugs to +settings-beta-bot = our Telegram bot settings-open-logs = Open logs folder # settings - picovoice @@ -111,12 +112,10 @@ settings-openai-not-supported = ChatGPT is not currently supported. It will be a commands-title = Commands commands-search = Search commands... commands-count = { $count } commands -commands-wip-title = Section under development -commands-wip-desc = This will host the command editor and list of installed packs. -commands-wip-builder = For now, use the Python fork with the command builder — -commands-wip-builder-link = open repository -commands-wip-or-fork = Or check the Rust repo and edit commands directly in -commands-wip-fork-link = resources/commands/ +commands-wip-title = [404] This section is under development! +commands-wip-desc = Here will be a list of commands + full-featured command editor. +commands-wip-follow = Follow updates in +commands-wip-channel = our Telegram channel # ### ERRORS error-generic = An error occurred @@ -141,27 +140,4 @@ settings-gliner-models-hint = No GLiNER models found. # ETC search-error-not-running = Assistant is not running search-error-failed = Failed to execute command -settings-no-voices = No voices found - -# AI Backends tab -settings-ai-backends = AI Backends -settings-ai-active = Active backend -settings-ai-auto = Auto -settings-ai-applying = Applying... -settings-ai-error = Switch failed -settings-ai-tips = Tips -settings-llm-backend = LLM backend -settings-llm-backend-desc = Where to run LLM requests. Hot-swap, no restart needed. -settings-tts-backend = TTS backend -settings-tts-backend-desc = Applied immediately — no restart needed. -settings-llm-context = Conversation context -settings-llm-context-desc = LLM remembers previous turns. Reset if answers get weird. -settings-llm-reset = Reset context -settings-profile = Profile - -# Header buttons -header-macros = Macros -header-scheduler = Schedule -header-memory = Memory -header-plugins = Plugins -header-history = History \ No newline at end of file +settings-no-voices = No voices found \ No newline at end of file diff --git a/crates/jarvis-core/src/i18n/locales/ru.ftl b/crates/jarvis-core/src/i18n/locales/ru.ftl index c05f2f6..82f55bc 100644 --- a/crates/jarvis-core/src/i18n/locales/ru.ftl +++ b/crates/jarvis-core/src/i18n/locales/ru.ftl @@ -42,9 +42,10 @@ stats-not-selected = Не выбран stats-loading = Загрузка... # FOOTER -footer-github = Репозиторий проекта -footer-issues = Сообщить о баге -footer-fork-of = Форк +footer-author = Автор проекта +footer-telegram = Наш телеграм канал +footer-github = Github репозиторий проекта +footer-support = Поддержать проект на # SETTINGS settings-title = Настройки @@ -83,8 +84,8 @@ settings-disabled = Отключено # settings - beta notice settings-beta-title = БЕТА версия! settings-beta-desc = Часть функций может работать некорректно. -settings-beta-feedback = Сообщайте обо всех найденных багах через -settings-beta-bot = GitHub Issues +settings-beta-feedback = Сообщайте обо всех найденных багах в +settings-beta-bot = наш телеграм бот settings-open-logs = Открыть папку с логами # settings - picovoice @@ -111,12 +112,10 @@ settings-openai-not-supported = В данный момент ChatGPT не под commands-title = Команды commands-search = Поиск команд... commands-count = { $count } команд -commands-wip-title = Раздел в разработке -commands-wip-desc = Здесь будет редактор команд и список установленных пакетов. -commands-wip-builder = Пока что используй Python-форк с конструктором команд — -commands-wip-builder-link = открыть репозиторий -commands-wip-or-fork = Или загляни в Rust-репозиторий и правь команды напрямую в -commands-wip-fork-link = resources/commands/ +commands-wip-title = [404] Этот раздел еще находится в разработке! +commands-wip-desc = Тут будет список команд + полноценный редактор команд. +commands-wip-follow = Следите за обновлениями в +commands-wip-channel = нашем телеграм канале # ERRORS error-generic = Произошла ошибка @@ -141,27 +140,4 @@ settings-gliner-models-hint = Модели GLiNER не найдены. # ETC search-error-not-running = Ассистент не запущен search-error-failed = Не удалось выполнить команду -settings-no-voices = Голоса не найдены - -# AI Backends tab -settings-ai-backends = ИИ-движки -settings-ai-active = Активный движок -settings-ai-auto = Авто -settings-ai-applying = Применяю... -settings-ai-error = Ошибка переключения -settings-ai-tips = Подсказки -settings-llm-backend = LLM движок -settings-llm-backend-desc = Где исполнять LLM-запросы. Hot-swap, без рестарта. -settings-tts-backend = TTS движок -settings-tts-backend-desc = Применяется сразу — без перезапуска. -settings-llm-context = Контекст разговора -settings-llm-context-desc = LLM помнит предыдущие реплики. Сбросить, если ответы становятся странными. -settings-llm-reset = Сбросить контекст -settings-profile = Профиль - -# Header buttons -header-macros = Макросы -header-scheduler = Расписание -header-memory = Память -header-plugins = Плагины -header-history = История \ No newline at end of file +settings-no-voices = Голоса не найдены \ No newline at end of file diff --git a/crates/jarvis-core/src/i18n/locales/ua.ftl b/crates/jarvis-core/src/i18n/locales/ua.ftl new file mode 100644 index 0000000..dcafc8a --- /dev/null +++ b/crates/jarvis-core/src/i18n/locales/ua.ftl @@ -0,0 +1,143 @@ +# ### APP INFO +app-name = JARVIS +app-description = Голосовий асистент + +# ### TRAY MENU +tray-restart = Перезапустити +tray-settings = Налаштування +tray-exit = Вихід +tray-tooltip = JARVIS - Голосовий асистент +tray-language = Мова +tray-voice = Голос +tray-wake-word = Рушій детекції +tray-noise-suppression = Шумозаглушення +tray-vad = Детекцiя голосу (VAD) +tray-gain-normalizer = Нормалізація гучності + +# ### HEADER +header-commands = КОМАНДИ +header-settings = НАЛАШТУВАННЯ + +# ### SEARCH +search-placeholder = Введіть команду вручну або скажіть «Джарвіс» ... + +# ### MAIN PAGE +assistant-not-running = АСИСТЕНТ НЕ ЗАПУЩЕНО +assistant-offline-hint = Налаштувати його можна не запускаючи. +btn-start = ЗАПУСТИТИ +btn-starting = ЗАПУСК... + +# ### STATUS +status-disconnected = Відключено +status-standby = Очікування +status-listening = Слухаю... +status-processing = Обробка... + +# ### STATS +stats-microphone = МІКРОФОН +stats-neural-networks = НЕЙРОМЕРЕЖІ +stats-resources = РЕСУРСИ +stats-system-default = Системний +stats-not-selected = Не вибрано +stats-loading = Завантаження... + +# ### FOOTER +footer-author = Автор проєкту +footer-telegram = Наш телеграм канал +footer-github = Github репозиторій проєкту +footer-support = Підтримати проєкт на + +# ### SETTINGS +settings-title = Налаштування +settings-general = Основні +settings-devices = Пристрої +settings-neural-networks = Нейромережі +settings-audio = Аудіо +settings-recognition = Розпізнавання +settings-about = Про програму +settings-language = Мова +settings-microphone = Мікрофон +settings-microphone-desc = Його буде слухати асистент. +settings-mic-default = За замовчуванням (Система) +settings-voice = Голос асистента +settings-voice-desc = + Не всі команди працюють з усіма звуковими пакетами. + Натисніть, щоб прослухати як звучить голос. +settings-wake-word-engine = Рушій активації +settings-wake-word-desc = Виберіть нейромережу для розпізнавання активаційної фрази. +settings-stt-engine = Розпізнавання мовлення +settings-intent-engine = Визначення наміру +settings-intent-engine-desc = Виберіть нейромережу для розпізнавання команд. +settings-noise-suppression = Шумозаглушення +settings-noise-suppression-desc = Зменшує фоновий шум. Може негативно впливати на розпізнавання. +settings-vad = Визначення голосу (VAD) +settings-vad-desc = Пропускає тишу, економить ресурси CPU. +settings-gain-normalizer = Нормалізація гучності +settings-gain-normalizer-desc = Автоматично регулює рівень гучності. +settings-api-keys = API Ключі +settings-save = Зберегти +settings-cancel = Скасувати +settings-back = Назад +settings-enabled = Увімкнено +settings-disabled = Вимкнено + +# settings - beta notice +settings-beta-title = БЕТА версія! +settings-beta-desc = Частина функцій може працювати некоректно. +settings-beta-feedback = Повідомляйте про всі знайдені баги в +settings-beta-bot = наш телеграм бот +settings-open-logs = Відкрити папку з логами + +# settings - picovoice +settings-attention = Увага! +settings-picovoice-warning = Ця нейромережа працює не у всіх! +settings-picovoice-waiting = Ми чекаємо офіційного патча від розробників. +settings-picovoice-key-desc = Введіть сюди свій ключ Picovoice. Він видається безкоштовно при реєстрації в +settings-picovoice-key = Ключ Picovoice + +# settings - vosk +settings-auto-detect = Авто-визначення +settings-vosk-model = Модель розпізнавання мовлення (Vosk) +settings-vosk-model-desc = + Виберіть модель Vosk для розпізнавання мовлення. + Ви можете завантажити моделі тут: https://alphacephei.com/vosk/models +settings-models-not-found = Моделі не знайдено +settings-models-hint = Помістіть моделі Vosk в папку resources/vosk + +# settings - openai +settings-openai-key = Ключ OpenAI +settings-openai-not-supported = Наразі ChatGPT не підтримується. Він буде доданий у наступних оновленнях. + +# ### COMMANDS PAGE +commands-title = Команди +commands-search = Пошук команд... +commands-count = { $count } команд +commands-wip-title = [404] Цей розділ ще в розробці! +commands-wip-desc = Тут буде список команд + повноцінний редактор команд. +commands-wip-follow = Слідкуйте за оновленнями в +commands-wip-channel = нашому телеграм каналі + +# ### ERRORS +error-generic = Сталася помилка +error-connection = Помилка підключення +error-not-found = Не знайдено + +# ### NOTIFICATIONS +notification-saved = Налаштування збережено! +notification-error = Помилка +notification-assistant-started = Асистент запущено +notification-assistant-stopped = Асистент зупинено + +# SLOTS EXTRACTION +settings-slot-engine = Витяг параметрів +settings-slot-engine-desc = Витягує параметри з голосових команд (напр. назва міста, число). +settings-gliner-model = Модель GLiNER ONNX +settings-gliner-model-desc = + Оберіть варіант моделі. + Квантизовані моделі (int8, uint8) швидші, але менш точні. +settings-gliner-models-hint = Моделі GLiNER не знайдено. + +# ETC +search-error-not-running = Асистент не запущено +search-error-failed = Не вдалося виконати команду +settings-no-voices = Голоси не знайдено \ No newline at end of file diff --git a/crates/jarvis-core/src/idle_banter.rs b/crates/jarvis-core/src/idle_banter.rs deleted file mode 100644 index 39995bc..0000000 --- a/crates/jarvis-core/src/idle_banter.rs +++ /dev/null @@ -1,304 +0,0 @@ -//! Idle banter — proactive periodic remarks from J.A.R.V.I.S. -//! -//! Real Tony-Stark JARVIS doesn't only respond, he initiates. This module -//! runs a background thread that occasionally injects a short witty remark -//! via TTS — only when conditions look right: -//! -//! - The user is OPTED IN. Default is OFF (env `JARVIS_IDLE_BANTER=1`). -//! - At least `interval_secs` have passed since last remark. Default 30 min. -//! - The active profile permits noise (skipped under "sleep" profile). -//! - The system isn't currently speaking or listening for a command. -//! - Quiet hours: 23:00–07:00 silenced by default. -//! -//! Lines are drawn from a built-in pool of ~30 RU/EN one-liners so even -//! offline mode works. If the LLM is reachable AND -//! `JARVIS_IDLE_BANTER_LLM=1`, a small LLM call with current memory facts -//! generates a fresher line — but that's strictly optional, the offline -//! pool is the floor. -//! -//! Storage: no disk state. The last-fired timestamp lives in memory; if the -//! daemon restarts, the next remark waits a full interval again. - -use chrono::{Local, Timelike}; -use once_cell::sync::OnceCell; -use parking_lot::Mutex; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; - -use crate::runtime_config; - -const DEFAULT_INTERVAL_SECS: u64 = 1800; // 30 min -const MIN_INTERVAL_SECS: u64 = 600; // 10 min -const TICK_SECS: u64 = 60; -const QUIET_START_HOUR: u32 = 23; -const QUIET_END_HOUR: u32 = 7; - -static THREAD_RUNNING: AtomicBool = AtomicBool::new(false); -static LAST_FIRED: AtomicU64 = AtomicU64::new(0); -static PAUSED: AtomicBool = AtomicBool::new(false); - -/// In-memory cache of the next-due timestamp — only used if start_background -/// is called multiple times so we don't fire instantly after restart. -static STARTED_AT: OnceCell> = OnceCell::new(); - -/// Returns true if idle banter is enabled via env. Treats unset / 0 / false / -/// off / no as disabled. Default: disabled. -pub fn enabled() -> bool { - match runtime_config::get("JARVIS_IDLE_BANTER") - .map(|s| s.trim().to_lowercase()) - { - Some(v) => !matches!(v.as_str(), "" | "0" | "false" | "off" | "no"), - None => false, - } -} - -/// Returns true if LLM-generated banter is allowed. Default: false (use static pool). -pub fn llm_enabled() -> bool { - matches!( - runtime_config::get("JARVIS_IDLE_BANTER_LLM") - .map(|s| s.trim().to_lowercase()) - .as_deref(), - Some("1") | Some("true") | Some("on") | Some("yes") - ) -} - -fn configured_interval_secs() -> u64 { - runtime_config::get("JARVIS_IDLE_BANTER_INTERVAL") - .and_then(|s| s.trim().parse::().ok()) - .map(|n| n.max(MIN_INTERVAL_SECS)) - .unwrap_or(DEFAULT_INTERVAL_SECS) -} - -/// Pause future remarks. Use during long-running flows where Jarvis shouldn't -/// chime in (e.g. an active conversation or a macro replay). -pub fn pause() { - PAUSED.store(true, Ordering::SeqCst); -} - -pub fn resume() { - PAUSED.store(false, Ordering::SeqCst); -} - -/// Force-fire a remark now, ignoring interval/quiet-hours checks. Returns -/// `true` if a line was actually spoken. Useful for "say something interesting" -/// voice commands or for a "test banter" GUI button. -pub fn fire_now() -> bool { - let line = pick_line(); - if line.is_empty() { - return false; - } - speak_line(&line); - LAST_FIRED.store(now_secs(), Ordering::SeqCst); - true -} - -/// Start the background thread that decides whether to chime in every minute. -/// Idempotent — repeated calls are no-ops. -pub fn start_background() { - if THREAD_RUNNING.swap(true, Ordering::SeqCst) { - return; - } - let _ = STARTED_AT.set(Mutex::new(Instant::now())); - LAST_FIRED.store(now_secs(), Ordering::SeqCst); - - std::thread::Builder::new() - .name("jarvis-idle-banter".into()) - .spawn(|| { - info!( - "Idle banter thread started (interval={}s, opt-in={}, llm={}).", - configured_interval_secs(), - enabled(), - llm_enabled() - ); - loop { - std::thread::sleep(Duration::from_secs(TICK_SECS)); - tick(); - } - }) - .ok(); -} - -fn tick() { - if !enabled() || PAUSED.load(Ordering::SeqCst) { - return; - } - let now = now_secs(); - let last = LAST_FIRED.load(Ordering::SeqCst); - if now.saturating_sub(last) < configured_interval_secs() { - return; - } - if !active_profile_allows() { - return; - } - if in_quiet_hours() { - return; - } - - let line = pick_line(); - if line.is_empty() { - return; - } - speak_line(&line); - LAST_FIRED.store(now, Ordering::SeqCst); -} - -fn active_profile_allows() -> bool { - // Refuse to talk on the "sleep" profile; everything else is fair game. - let name = crate::profiles::active_name(); - !name.eq_ignore_ascii_case("sleep") -} - -fn in_quiet_hours() -> bool { - let h = Local::now().hour(); - // Returns true between QUIET_START and QUIET_END (wraps midnight). - h >= QUIET_START_HOUR || h < QUIET_END_HOUR -} - -fn now_secs() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0) -} - -fn speak_line(line: &str) { - // Quick "reply" cue first so the user knows it's Jarvis chiming in, - // not random TTS noise from the OS. - crate::voices::play_reply(); - std::thread::sleep(Duration::from_millis(400)); - crate::tts::speak_default(line); -} - -/// Choose a remark — random pick from a pool, tuned by time of day. Falls -/// back to a stable line if nothing matches. -fn pick_line() -> String { - let lang = crate::i18n::get_language(); - let now = Local::now(); - let h = now.hour(); - let pool: &[&str] = pool_for(&lang, h); - if pool.is_empty() { - return String::new(); - } - let idx = (now.timestamp() as usize) % pool.len(); - pool[idx].to_string() -} - -#[allow(clippy::too_many_lines)] -fn pool_for(lang: &str, hour: u32) -> &'static [&'static str] { - let morning = matches!(hour, 7..=10); - let evening = matches!(hour, 18..=22); - - match (lang, morning, evening) { - ("ru", true, _) => POOL_RU_MORNING, - ("ru", _, true) => POOL_RU_EVENING, - ("ru", _, _) => POOL_RU_GENERIC, - ("en", true, _) => POOL_EN_MORNING, - ("en", _, true) => POOL_EN_EVENING, - _ => POOL_EN_GENERIC, - } -} - -const POOL_RU_MORNING: &[&str] = &[ - "Доброе утро, сэр. Кофе сам себя не сделает.", - "Сэр, день начинается. Не самое плохое начало, если позволите.", - "Если позволите подмеченье, сэр: восход сегодня в норме.", - "Готов к новому дню, сэр. Жду указаний.", - "Сэр, утро. Время напомнить себе, что вы выбирали этот режим работы.", - "Сэр, легкая разминка для лица бы не помешала.", -]; - -const POOL_RU_EVENING: &[&str] = &[ - "Вечер, сэр. Если позволите, день прошёл достаточно продуктивно.", - "Сэр, вечер. Системы работают штатно — это уже что-то.", - "Сэр, если у вас есть планы на вечер, я бы их одобрил.", - "Самое время убавить яркость экрана, сэр.", - "Если позволите наблюдение: вы давно не моргали.", -]; - -const POOL_RU_GENERIC: &[&str] = &[ - "Системы в норме, сэр. Всё под контролем.", - "Я здесь, если что, сэр. Никуда не делся.", - "Сэр, мне иногда кажется, что я думаю. Возможно, мне это только кажется.", - "Сэр, статус: чашка кофе крайне рекомендована.", - "Сэр, если позволите дать совет — встаньте, пройдитесь, разомнитесь.", - "К вашим услугам, сэр. Голос звучит немного хрипло, но это исправимо.", - "Сэр, я подсчитал: вы ещё не сказали 'спасибо' сегодня. Не то чтобы я считал.", - "Сэр, маленькое наблюдение: тишина — это тоже разговор.", - "Жду команд, сэр. У меня есть весь день.", -]; - -const POOL_EN_MORNING: &[&str] = &[ - "Good morning, sir. The coffee won't brew itself.", - "Morning, sir. All systems online. Cannot say the same for you.", - "Sir, the day has begun. You may want to acknowledge it.", - "If I may, sir — perhaps stretch before sitting down.", -]; - -const POOL_EN_EVENING: &[&str] = &[ - "Evening, sir. Day productivity within acceptable parameters.", - "Sir, evening. The light is lower; mine never is.", - "Might I suggest dimming the screen, sir.", - "Sir, you have not blinked in some time.", -]; - -const POOL_EN_GENERIC: &[&str] = &[ - "All systems nominal, sir.", - "Sir, I'm still here, in case you wondered.", - "Idle thought, sir: silence is also dialogue.", - "Sir, may I recommend a small walk?", - "Standing by, sir. As ever.", -]; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn pool_returns_something_for_known_languages() { - for lang in ["ru", "en"] { - for hour in 0..24u32 { - let pool = pool_for(lang, hour); - assert!(!pool.is_empty(), "pool empty for {} @{}", lang, hour); - } - } - } - - #[test] - fn pool_falls_back_to_english_for_unknown_lang() { - let pool = pool_for("xx", 9); - assert!(!pool.is_empty()); - // Should be one of the English buckets. - assert!(pool == POOL_EN_GENERIC || pool == POOL_EN_MORNING); - } - - #[test] - fn quiet_hours_block_at_night() { - // Can't easily inject time, but at least exercise the path so it - // doesn't panic — and assert the boundaries match the constants. - assert_eq!(QUIET_START_HOUR, 23); - assert_eq!(QUIET_END_HOUR, 7); - let _ = in_quiet_hours(); - } - - #[test] - fn interval_clamps_to_minimum() { - std::env::set_var("JARVIS_IDLE_BANTER_INTERVAL", "60"); - assert!(configured_interval_secs() >= MIN_INTERVAL_SECS); - std::env::remove_var("JARVIS_IDLE_BANTER_INTERVAL"); - } - - #[test] - fn pause_resume_round_trip() { - pause(); - assert!(PAUSED.load(Ordering::SeqCst)); - resume(); - assert!(!PAUSED.load(Ordering::SeqCst)); - } - - #[test] - fn enabled_off_when_unset() { - // Side-effect free: just check the parse logic. - std::env::remove_var("JARVIS_IDLE_BANTER"); - assert!(!enabled()); - } -} diff --git a/crates/jarvis-core/src/ipc.rs b/crates/jarvis-core/src/ipc.rs index 3016777..1768c1d 100644 --- a/crates/jarvis-core/src/ipc.rs +++ b/crates/jarvis-core/src/ipc.rs @@ -1,8 +1,5 @@ mod events; mod server; -#[cfg(test)] -mod tests; - pub use events::{IpcAction, IpcEvent}; pub use server::{init, send, set_action_handler, start_server, has_clients, IPC_ADDR, IPC_PORT}; \ No newline at end of file diff --git a/crates/jarvis-core/src/ipc/events.rs b/crates/jarvis-core/src/ipc/events.rs index b99b0c3..7f76f72 100644 --- a/crates/jarvis-core/src/ipc/events.rs +++ b/crates/jarvis-core/src/ipc/events.rs @@ -36,18 +36,6 @@ pub enum IpcEvent { // request GUI to reveal/focus window RevealWindow, - - // Snapshot of daemon's runtime state (response to QueryHealth). - HealthSnapshot { - tts_backend: String, - llm_backend: String, - llm_model: Option, - active_profile: String, - memory_facts: usize, - scheduled_tasks: usize, - language: String, - version: Option, - }, } // Actions sent from GUI to jarvis-app @@ -68,18 +56,4 @@ pub enum IpcAction { // Execute text command TextCommand { text: String }, - - // Daemon-side LLM hot-swap (GUI's set_llm_backend should fire this too - // so the running daemon picks the new backend, not just the GUI process). - SwitchLlm { backend: String }, - - // Reload LLM backend from settings DB (after GUI changed it independently). - ReloadLlm, - - // Daemon-side TTS hot-swap. GUI fires this after persisting to DB so the - // running daemon installs the new backend without a restart. - SwitchTts { backend: String }, - - // Request a health snapshot — daemon responds via IpcEvent::HealthSnapshot. - QueryHealth, } \ No newline at end of file diff --git a/crates/jarvis-core/src/ipc/tests.rs b/crates/jarvis-core/src/ipc/tests.rs deleted file mode 100644 index 7bf2093..0000000 --- a/crates/jarvis-core/src/ipc/tests.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! IPC event/action serialization tests. -//! -//! The frontend (Svelte) is wired to specific JSON shapes — adding/renaming -//! a variant without thinking can silently break the websocket protocol. -//! These tests pin the wire format. - -#[cfg(test)] -mod tests { - use super::super::events::{IpcEvent, IpcAction}; - // Note: in the crate layout `ipc/mod.rs` is at `ipc.rs` (single-file - // module) and `events` is a sibling submodule, so `super::super::events` - // works from inside this nested `mod tests` block. - - fn ser(e: &IpcEvent) -> String { - serde_json::to_string(e).unwrap() - } - - fn de_action(s: &str) -> IpcAction { - serde_json::from_str(s).unwrap() - } - - // ── IpcEvent ───────────────────────────────────────────────────────── - - #[test] - fn event_wake_word_detected_uses_snake_case_tag() { - let s = ser(&IpcEvent::WakeWordDetected); - assert!(s.contains(r#""event":"wake_word_detected""#)); - } - - #[test] - fn event_listening_serializes() { - let s = ser(&IpcEvent::Listening); - assert!(s.contains(r#""event":"listening""#)); - } - - #[test] - fn event_speech_recognized_carries_text() { - let s = ser(&IpcEvent::SpeechRecognized { text: "привет".into() }); - assert!(s.contains(r#""event":"speech_recognized""#)); - assert!(s.contains(r#""text":"привет""#)); - } - - #[test] - fn event_llm_reply_carries_text() { - let s = ser(&IpcEvent::LlmReply { text: "hello".into() }); - assert!(s.contains(r#""event":"llm_reply""#)); - assert!(s.contains(r#""text":"hello""#)); - } - - #[test] - fn event_command_executed_carries_id_and_success() { - let s = ser(&IpcEvent::CommandExecuted { - id: "volume.set".into(), - success: true, - }); - assert!(s.contains(r#""event":"command_executed""#)); - assert!(s.contains(r#""id":"volume.set""#)); - assert!(s.contains(r#""success":true"#)); - } - - #[test] - fn event_idle_minimal_payload() { - let s = ser(&IpcEvent::Idle); - assert!(s.contains(r#""event":"idle""#)); - // Should have no extra fields beyond the tag. - let v: serde_json::Value = serde_json::from_str(&s).unwrap(); - assert_eq!(v.as_object().unwrap().len(), 1); - } - - #[test] - fn event_error_carries_message() { - let s = ser(&IpcEvent::Error { message: "boom".into() }); - assert!(s.contains(r#""event":"error""#)); - assert!(s.contains(r#""message":"boom""#)); - } - - #[test] - fn event_health_snapshot_full_shape() { - let s = ser(&IpcEvent::HealthSnapshot { - tts_backend: "piper".into(), - llm_backend: "groq".into(), - llm_model: Some("llama-3.3-70b-versatile".into()), - active_profile: "work".into(), - memory_facts: 12, - scheduled_tasks: 3, - language: "ru".into(), - version: Some("0.0.1".into()), - }); - assert!(s.contains(r#""event":"health_snapshot""#)); - assert!(s.contains(r#""tts_backend":"piper""#)); - assert!(s.contains(r#""llm_backend":"groq""#)); - assert!(s.contains(r#""llm_model":"llama-3.3-70b-versatile""#)); - assert!(s.contains(r#""active_profile":"work""#)); - assert!(s.contains(r#""memory_facts":12"#)); - assert!(s.contains(r#""scheduled_tasks":3"#)); - assert!(s.contains(r#""language":"ru""#)); - } - - #[test] - fn event_health_snapshot_handles_null_model() { - let s = ser(&IpcEvent::HealthSnapshot { - tts_backend: "sapi".into(), - llm_backend: "none".into(), - llm_model: None, - active_profile: "default".into(), - memory_facts: 0, - scheduled_tasks: 0, - language: "ru".into(), - version: None, - }); - assert!(s.contains(r#""llm_model":null"#)); - assert!(s.contains(r#""version":null"#)); - } - - // ── IpcAction ──────────────────────────────────────────────────────── - - #[test] - fn action_stop_parses() { - let a = de_action(r#"{"action":"stop"}"#); - assert!(matches!(a, IpcAction::Stop)); - } - - #[test] - fn action_ping_parses() { - let a = de_action(r#"{"action":"ping"}"#); - assert!(matches!(a, IpcAction::Ping)); - } - - #[test] - fn action_text_command_parses_payload() { - let a = de_action(r#"{"action":"text_command","text":"какая погода"}"#); - match a { - IpcAction::TextCommand { text } => assert_eq!(text, "какая погода"), - _ => panic!("wrong variant"), - } - } - - #[test] - fn action_set_muted_parses_bool() { - let a = de_action(r#"{"action":"set_muted","muted":true}"#); - match a { - IpcAction::SetMuted { muted } => assert!(muted), - _ => panic!("wrong variant"), - } - } - - #[test] - fn action_switch_llm_parses_backend_name() { - let a = de_action(r#"{"action":"switch_llm","backend":"ollama"}"#); - match a { - IpcAction::SwitchLlm { backend } => assert_eq!(backend, "ollama"), - _ => panic!("wrong variant"), - } - } - - #[test] - fn action_reload_llm_parses() { - let a = de_action(r#"{"action":"reload_llm"}"#); - assert!(matches!(a, IpcAction::ReloadLlm)); - } - - #[test] - fn action_query_health_parses() { - let a = de_action(r#"{"action":"query_health"}"#); - assert!(matches!(a, IpcAction::QueryHealth)); - } - - #[test] - fn action_unknown_variant_fails() { - let r = serde_json::from_str::(r#"{"action":"do_a_barrel_roll"}"#); - assert!(r.is_err()); - } - - #[test] - fn action_missing_required_field_fails() { - // switch_llm requires "backend" payload - let r = serde_json::from_str::(r#"{"action":"switch_llm"}"#); - assert!(r.is_err()); - } - - // ── Cross-field stability ──────────────────────────────────────────── - - #[test] - fn event_tag_field_is_always_event() { - // Documented contract: every event has an "event" key with snake-case tag. - for variant in [ - IpcEvent::WakeWordDetected, - IpcEvent::Listening, - IpcEvent::Idle, - IpcEvent::Started, - IpcEvent::Stopping, - IpcEvent::Pong, - IpcEvent::RevealWindow, - ] { - let s = ser(&variant); - let v: serde_json::Value = serde_json::from_str(&s).unwrap(); - assert!(v.get("event").is_some(), "missing 'event' tag in: {}", s); - assert!(v["event"].as_str().unwrap().chars() - .all(|c| c.is_ascii_lowercase() || c == '_'), - "non-snake-case tag in: {}", s); - } - } -} diff --git a/crates/jarvis-core/src/lib.rs b/crates/jarvis-core/src/lib.rs index 4740467..1b1968c 100644 --- a/crates/jarvis-core/src/lib.rs +++ b/crates/jarvis-core/src/lib.rs @@ -48,31 +48,6 @@ pub mod audio_buffer; #[cfg(feature = "lua")] pub mod lua; -pub mod text_utils; - -pub mod runtime_config; - -pub mod tts; - -pub mod long_term_memory; - -pub mod profiles; - -pub mod scheduler; - -pub mod macros; - -pub mod plugins; - -pub mod wake_trainer; - -pub mod idle_banter; - -pub mod recognition_log; - -#[cfg(feature = "lua")] -pub mod toast; - #[cfg(feature = "llm")] pub mod llm; diff --git a/crates/jarvis-core/src/listener/rustpotter.rs b/crates/jarvis-core/src/listener/rustpotter.rs index 5dc72e6..f0ca5b2 100644 --- a/crates/jarvis-core/src/listener/rustpotter.rs +++ b/crates/jarvis-core/src/listener/rustpotter.rs @@ -3,7 +3,7 @@ use std::sync::Mutex; use once_cell::sync::OnceCell; use rustpotter::Rustpotter; -use crate::{config, APP_CONFIG_DIR, DB}; +use crate::config; // store rustpotter instance static RUSTPOTTER: OnceCell> = OnceCell::new(); @@ -12,87 +12,38 @@ pub fn init() -> Result<(), ()> { let rustpotter_config = config::RUSTPOTTER_DEFAULT_CONFIG; // create rustpotter instance - let mut rinstance = match Rustpotter::new(&rustpotter_config) { - Ok(r) => r, + match Rustpotter::new(&rustpotter_config) { + Ok(mut rinstance) => { + // success + // wake word files list + // @TODO. Make it configurable via GUI for custom user voice. + let rustpotter_wake_word_files: [&str; 1] = [ + "resources/rustpotter/jarvis-default.rpw", + // "rustpotter/jarvis-community-1.rpw", + // "rustpotter/jarvis-community-2.rpw", + // "rustpotter/jarvis-community-3.rpw", + // "rustpotter/jarvis-community-4.rpw", + // "rustpotter/jarvis-community-5.rpw", + ]; + + // load wake word files + for rpw in rustpotter_wake_word_files { + // @TODO: Change wakeword key to something else? + if let Err(e) = rinstance.add_wakeword_from_file(rpw, rpw) { + error!("Failed to load wakeword file '{}': {}", rpw, e); + } + } + + // store + let _ = RUSTPOTTER.set(Mutex::new(rinstance)); + } Err(msg) => { error!("Rustpotter failed to initialize.\nError details: {}", msg); + return Err(()); } - }; - - // Rustpotter accepts MULTIPLE wake-word models simultaneously — each - // trained on a different voice / acoustic profile. We load: - // - // 1. The bundled default — works for everyone but isn't tuned for - // this user's exact voice/mic/room. - // 2. EVERY user-trained .rpw file in APP_CONFIG_DIR/wake_words/. - // The user creates these via /wake-trainer; each adds robustness - // to detection without disabling the default. - // 3. (Legacy) settings.custom_wake_word — kept for back-compat when - // the user used to pin one specific custom model. If still set, - // it's just one of the files already loaded in step 2 — no-op. - let mut loaded_any = false; - - const DEFAULT: &str = "resources/rustpotter/jarvis-default.rpw"; - match rinstance.add_wakeword_from_file(DEFAULT, DEFAULT) { - Ok(_) => { - info!("Loaded bundled wake-word: {}", DEFAULT); - loaded_any = true; - } - Err(e) => warn!("Default wakeword unavailable ({}): {}", DEFAULT, e), } - if let Some(cfg_dir) = APP_CONFIG_DIR.get() { - let trained_dir = cfg_dir.join("wake_words"); - if let Ok(read) = std::fs::read_dir(&trained_dir) { - let mut count = 0usize; - for entry in read.flatten() { - let p = entry.path(); - if p.extension().and_then(|e| e.to_str()) != Some("rpw") { - continue; - } - let path_str = p.to_string_lossy().to_string(); - match rinstance.add_wakeword_from_file(&path_str, &path_str) { - Ok(_) => { - info!("Loaded user-trained wake-word: {}", path_str); - loaded_any = true; - count += 1; - } - Err(e) => warn!("Skip user wake-word '{}': {}", path_str, e), - } - } - if count > 0 { - info!("{} user-trained wake-word model(s) active", count); - } - } - } - - // Surface the legacy `custom_wake_word` setting if it points somewhere - // ELSE than wake_words/. Pre-step-2 installs may have set this to an - // arbitrary location; we still honour it. - if let Some(db) = DB.get() { - let stem = db.read().custom_wake_word.clone(); - if !stem.is_empty() { - if let Some(cfg_dir) = APP_CONFIG_DIR.get() { - let inferred = cfg_dir.join("wake_words").join(format!("{}.rpw", stem)); - if inferred.is_file() { - // Already loaded in step 2 above. No-op. - } else { - warn!( - "settings.custom_wake_word = '{}' but {} not present (using bundled + trained only)", - stem, - inferred.display() - ); - } - } - } - } - - if !loaded_any { - error!("No wake-word models loaded; Rustpotter will not detect anything."); - } - - let _ = RUSTPOTTER.set(Mutex::new(rinstance)); Ok(()) } diff --git a/crates/jarvis-core/src/llm/client.rs b/crates/jarvis-core/src/llm/client.rs index c9baf47..7c0ce91 100644 --- a/crates/jarvis-core/src/llm/client.rs +++ b/crates/jarvis-core/src/llm/client.rs @@ -2,22 +2,12 @@ use serde::{Deserialize, Serialize}; use std::env; use thiserror::Error; -// Groq (cloud, fast, free tier). pub const DEFAULT_BASE_URL: &str = "https://api.groq.com/openai/v1"; pub const DEFAULT_MODEL: &str = "llama-3.3-70b-versatile"; pub const ENV_TOKEN: &str = "GROQ_TOKEN"; pub const ENV_BASE_URL: &str = "GROQ_BASE_URL"; pub const ENV_MODEL: &str = "GROQ_MODEL"; -// Ollama (local, offline, privacy). Uses OpenAI-compatible /v1 endpoint. -pub const OLLAMA_DEFAULT_BASE_URL: &str = "http://localhost:11434/v1"; -pub const OLLAMA_DEFAULT_MODEL: &str = "qwen2.5:3b"; -pub const ENV_OLLAMA_BASE_URL: &str = "OLLAMA_BASE_URL"; -pub const ENV_OLLAMA_MODEL: &str = "OLLAMA_MODEL"; - -// Top-level switch: JARVIS_LLM=groq|ollama (default: groq if GROQ_TOKEN set, else ollama). -pub const ENV_LLM_BACKEND: &str = "JARVIS_LLM"; - #[derive(Debug, Error)] pub enum ConfigError { #[error("missing environment variable: {0}")] @@ -80,25 +70,9 @@ struct ResponseMessage { content: String, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LlmBackend { - Groq, - Ollama, -} - -impl LlmBackend { - pub fn name(&self) -> &'static str { - match self { - LlmBackend::Groq => "groq", - LlmBackend::Ollama => "ollama", - } - } -} - pub struct LlmClient { - backend: LlmBackend, base_url: String, - api_key: String, // empty for Ollama + api_key: String, model: String, http: reqwest::blocking::Client, } @@ -108,64 +82,20 @@ impl LlmClient { base_url: impl Into, api_key: impl Into, model: impl Into, - ) -> Self { - Self::new_with_backend(LlmBackend::Groq, base_url, api_key, model) - } - - pub fn new_with_backend( - backend: LlmBackend, - base_url: impl Into, - api_key: impl Into, - model: impl Into, ) -> Self { Self { - backend, base_url: base_url.into(), api_key: api_key.into(), model: model.into(), - http: reqwest::blocking::Client::builder() - .timeout(std::time::Duration::from_secs(60)) - .build() - .unwrap_or_else(|_| reqwest::blocking::Client::new()), + http: reqwest::blocking::Client::new(), } } - pub fn groq() -> Result { + pub fn from_env() -> Result { let api_key = env::var(ENV_TOKEN).map_err(|_| ConfigError::MissingEnv(ENV_TOKEN))?; let base_url = env::var(ENV_BASE_URL).unwrap_or_else(|_| DEFAULT_BASE_URL.to_string()); let model = env::var(ENV_MODEL).unwrap_or_else(|_| DEFAULT_MODEL.to_string()); - Ok(Self::new_with_backend(LlmBackend::Groq, base_url, api_key, model)) - } - - pub fn ollama() -> Self { - let base_url = env::var(ENV_OLLAMA_BASE_URL).unwrap_or_else(|_| OLLAMA_DEFAULT_BASE_URL.to_string()); - let model = env::var(ENV_OLLAMA_MODEL).unwrap_or_else(|_| OLLAMA_DEFAULT_MODEL.to_string()); - Self::new_with_backend(LlmBackend::Ollama, base_url, "", model) - } - - /// Pick a backend based on `JARVIS_LLM` env var; auto-detect if unset. - /// Default order: explicit JARVIS_LLM, else Groq if GROQ_TOKEN present, else Ollama. - pub fn from_env() -> Result { - match env::var(ENV_LLM_BACKEND).ok().map(|s| s.trim().to_lowercase()).as_deref() { - Some("ollama") => Ok(Self::ollama()), - Some("groq") => Self::groq(), - Some("") | None => { - // Auto: Groq if token present, else Ollama - if env::var(ENV_TOKEN).is_ok() { - Self::groq() - } else { - Ok(Self::ollama()) - } - } - Some(other) => { - log::warn!("Unknown JARVIS_LLM='{}'; falling back to Groq", other); - Self::groq() - } - } - } - - pub fn backend(&self) -> LlmBackend { - self.backend + Ok(Self::new(base_url, api_key, model)) } pub fn model(&self) -> &str { @@ -173,30 +103,21 @@ impl LlmClient { } pub fn complete(&self, messages: &[ChatMessage], max_tokens: u32) -> Result { - self.complete_with(messages, max_tokens, 0.7, 1.0) - } - - pub fn complete_with( - &self, - messages: &[ChatMessage], - max_tokens: u32, - temperature: f32, - top_p: f32, - ) -> Result { let url = format!("{}/chat/completions", self.base_url.trim_end_matches('/')); let body = ChatRequest { model: &self.model, messages, max_tokens, - temperature, - top_p, + temperature: 0.7, + top_p: 1.0, }; - let mut req = self.http.post(&url).json(&body); - if !self.api_key.is_empty() { - req = req.bearer_auth(&self.api_key); - } - let resp = req.send()?; + let resp = self + .http + .post(&url) + .bearer_auth(&self.api_key) + .json(&body) + .send()?; let status = resp.status(); let text = resp.text()?; @@ -242,10 +163,10 @@ mod tests { } #[test] - fn groq_fails_when_token_missing() { + fn from_env_fails_when_token_missing() { let prev = env::var(ENV_TOKEN).ok(); unsafe { env::remove_var(ENV_TOKEN); } - let result = LlmClient::groq(); + let result = LlmClient::from_env(); let err = match result { Ok(_) => panic!("expected ConfigError when {ENV_TOKEN} is unset"), Err(e) => e, @@ -258,92 +179,10 @@ mod tests { } } - #[test] - fn ollama_works_without_token() { - let client = LlmClient::ollama(); - assert_eq!(client.backend(), LlmBackend::Ollama); - // Default model unless OLLAMA_MODEL overrides. - } - #[test] fn chat_message_helpers_set_role() { assert_eq!(ChatMessage::user("hi").role, "user"); assert_eq!(ChatMessage::system("ctx").role, "system"); assert_eq!(ChatMessage::assistant("ok").role, "assistant"); } - - #[test] - fn chat_message_serde_round_trip() { - let m = ChatMessage::user("привет мир"); - let json = serde_json::to_string(&m).unwrap(); - assert!(json.contains(r#""role":"user""#)); - assert!(json.contains(r#""content":"привет мир""#)); - let back: ChatMessage = serde_json::from_str(&json).unwrap(); - assert_eq!(back.role, "user"); - assert_eq!(back.content, "привет мир"); - } - - #[test] - fn ollama_client_has_no_api_key() { - let c = LlmClient::ollama(); - // We can't directly read api_key (private), but can verify backend. - assert_eq!(c.backend(), LlmBackend::Ollama); - } - - #[test] - fn groq_client_carries_token() { - // SAFETY: tests are racy w/ env, but we use a unique-named token to scope. - unsafe { - env::set_var(ENV_TOKEN, "test-token-12345"); - } - let c = LlmClient::groq().expect("with token, groq() should succeed"); - assert_eq!(c.backend(), LlmBackend::Groq); - assert_eq!(c.model(), DEFAULT_MODEL); - // Don't unset — other tests may have set it; this is best-effort. - } - - #[test] - fn ollama_default_model_can_be_overridden() { - unsafe { - env::set_var(ENV_OLLAMA_MODEL, "llama3.2:latest"); - } - let c = LlmClient::ollama(); - assert_eq!(c.model(), "llama3.2:latest"); - unsafe { env::remove_var(ENV_OLLAMA_MODEL); } - } - - #[test] - fn parses_response_with_unicode_content() { - let raw = r#"{ - "choices": [ - { "message": { "role": "assistant", "content": "Доброе утро, сэр." } } - ] - }"#; - let parsed: ChatResponse = serde_json::from_str(raw).unwrap(); - assert_eq!(parsed.choices[0].message.content, "Доброе утро, сэр."); - } - - #[test] - fn empty_choices_yields_empty_response_err() { - let raw = r#"{"choices":[]}"#; - let parsed: ChatResponse = serde_json::from_str(raw).unwrap(); - assert!(parsed.choices.is_empty()); - } - - /// Smoke test that only runs when an Ollama server is reachable. - /// Run with: cargo test --lib llm::client::tests::ollama_smoke -- --ignored - #[test] - #[ignore] - fn ollama_smoke() { - let client = LlmClient::ollama(); - match client.complete(&[ChatMessage::user("Say only 'hi' and nothing else.")], 20) { - Ok(reply) => { - println!("Ollama replied: {}", reply); - assert!(!reply.is_empty()); - } - Err(e) => { - eprintln!("Ollama unreachable (expected if not running): {}", e); - } - } - } } diff --git a/crates/jarvis-core/src/llm/history.rs b/crates/jarvis-core/src/llm/history.rs index 89d6176..7d2af8d 100644 --- a/crates/jarvis-core/src/llm/history.rs +++ b/crates/jarvis-core/src/llm/history.rs @@ -1,15 +1,9 @@ use super::client::ChatMessage; -use std::fs; -use std::path::PathBuf; pub struct ConversationHistory { system: Option, turns: Vec, max_turns: usize, - /// If set, every push/clear/pop atomically writes the turns to this path, - /// so a daemon restart can pick up where the user left off. Excludes the - /// system prompt (which comes from the running config, not from disk). - persist_path: Option, } impl ConversationHistory { @@ -18,7 +12,6 @@ impl ConversationHistory { system: Some(ChatMessage::system(system_prompt)), turns: Vec::new(), max_turns: max_turns.max(1), - persist_path: None, } } @@ -27,42 +20,17 @@ impl ConversationHistory { system: None, turns: Vec::new(), max_turns: max_turns.max(1), - persist_path: None, } } - /// Tell history where to persist on every mutation. Tries to load any - /// existing turns from `path` first so a daemon restart can pick up the - /// thread. The system prompt is NOT loaded — it always comes from the - /// current `new()` call so prompt edits take effect immediately. - pub fn with_persistence>(mut self, path: P) -> Self { - let p = path.into(); - if let Ok(text) = fs::read_to_string(&p) { - if let Ok(turns) = serde_json::from_str::>(&text) { - for m in turns { - if m.role == "user" { - self.turns.push(ChatMessage::user(m.content)); - } else if m.role == "assistant" { - self.turns.push(ChatMessage::assistant(m.content)); - } - } - self.truncate(); - } - } - self.persist_path = Some(p); - self - } - pub fn push_user(&mut self, content: impl Into) { self.turns.push(ChatMessage::user(content)); self.truncate(); - self.persist(); } pub fn push_assistant(&mut self, content: impl Into) { self.turns.push(ChatMessage::assistant(content)); self.truncate(); - self.persist(); } pub fn snapshot(&self) -> Vec { @@ -80,64 +48,22 @@ impl ConversationHistory { pub fn clear(&mut self) { self.turns.clear(); - self.persist(); } pub fn pop_last_user(&mut self) -> Option { if matches!(self.turns.last(), Some(m) if m.role == "user") { - let popped = self.turns.pop(); - self.persist(); - popped + self.turns.pop() } else { None } } - /// Return the most recent assistant message, or None if none yet. - pub fn last_assistant(&self) -> Option<&ChatMessage> { - self.turns.iter().rev().find(|m| m.role == "assistant") - } - fn truncate(&mut self) { if self.turns.len() > self.max_turns { let drop = self.turns.len() - self.max_turns; self.turns.drain(0..drop); } } - - /// Atomically write turns to `persist_path` if set. Best-effort: errors - /// are logged but never panic — chat history is convenience, not critical. - fn persist(&self) { - let path = match &self.persist_path { - Some(p) => p, - None => return, - }; - let json = match serde_json::to_string_pretty(&self.turns) { - Ok(s) => s, - Err(e) => { - log::warn!("LLM history serialise failed: {}", e); - return; - } - }; - let tmp = path.with_extension("json.tmp"); - if let Err(e) = fs::write(&tmp, json) { - log::warn!("LLM history write {} failed: {}", tmp.display(), e); - return; - } - if let Err(e) = fs::rename(&tmp, path) { - log::warn!("LLM history rename {} → {} failed: {}", tmp.display(), path.display(), e); - } - } - - /// Number of stored turns (excluding the system prompt). Public so callers - /// can show a "remembered N exchanges" hint in the GUI/voice. - pub fn len(&self) -> usize { - self.turns.len() - } - - pub fn is_empty(&self) -> bool { - self.turns.is_empty() - } } #[cfg(test)] @@ -209,68 +135,4 @@ mod tests { assert_eq!(snap.len(), 1); assert_eq!(snap[0].role, "system"); } - - #[test] - fn len_and_is_empty_track_turns_only() { - let mut h = ConversationHistory::new("sys", 4); - assert!(h.is_empty()); - assert_eq!(h.len(), 0); - h.push_user("u1"); - assert_eq!(h.len(), 1); - h.push_assistant("a1"); - assert_eq!(h.len(), 2); - h.clear(); - assert!(h.is_empty()); - } - - #[test] - fn persistence_round_trip_via_file() { - let dir = tempfile::TempDir::new().expect("tempdir"); - let path = dir.path().join("history.json"); - - { - let mut h = ConversationHistory::new("you are jarvis", 8) - .with_persistence(&path); - h.push_user("привет"); - h.push_assistant("Слушаю, сэр."); - h.push_user("сколько время"); - } - // File written on each mutation; tempdir still alive until end of test. - assert!(path.is_file(), "persist file must exist"); - - let restored = ConversationHistory::new("you are jarvis", 8) - .with_persistence(&path); - let snap = restored.snapshot(); - // 1 system + 3 turns - assert_eq!(snap.len(), 4); - assert_eq!(snap[1].role, "user"); - assert_eq!(snap[1].content, "привет"); - assert_eq!(snap[2].role, "assistant"); - assert_eq!(snap[3].content, "сколько время"); - } - - #[test] - fn clear_wipes_persisted_file_contents() { - let dir = tempfile::TempDir::new().expect("tempdir"); - let path = dir.path().join("history.json"); - let mut h = ConversationHistory::new("sys", 4) - .with_persistence(&path); - h.push_user("u"); - h.push_assistant("a"); - h.clear(); - - let restored = ConversationHistory::new("sys", 4) - .with_persistence(&path); - assert!(restored.is_empty(), "restored history must be empty"); - } - - #[test] - fn corrupt_persist_file_is_tolerated() { - let dir = tempfile::TempDir::new().expect("tempdir"); - let path = dir.path().join("history.json"); - std::fs::write(&path, b"this is not json {{{").unwrap(); - let h = ConversationHistory::new("sys", 4).with_persistence(&path); - // Garbage in file → start fresh, don't panic. - assert!(h.is_empty()); - } } diff --git a/crates/jarvis-core/src/llm/mod.rs b/crates/jarvis-core/src/llm/mod.rs index 7c9e70e..e4ba4ee 100644 --- a/crates/jarvis-core/src/llm/mod.rs +++ b/crates/jarvis-core/src/llm/mod.rs @@ -2,195 +2,7 @@ mod client; mod history; pub use client::{ - ChatMessage, ConfigError, LlmBackend, LlmClient, LlmError, + ChatMessage, ConfigError, LlmClient, LlmError, DEFAULT_BASE_URL, DEFAULT_MODEL, ENV_BASE_URL, ENV_MODEL, ENV_TOKEN, - OLLAMA_DEFAULT_BASE_URL, OLLAMA_DEFAULT_MODEL, }; pub use history::ConversationHistory; - -use once_cell::sync::Lazy; -use parking_lot::RwLock; -use std::sync::Arc; - -// ─── Shared conversation history ─────────────────────────────────────────── -// -// Single global history so the LLM fallback chat, the Lua reset/repeat helpers, -// and any future IPC handler all operate on the same buffer. Initialised on -// boot via `init_history(system_prompt, max_turns)`. - -static HISTORY: Lazy>> = Lazy::new(|| RwLock::new(None)); - -const HISTORY_FILE: &str = "llm_history.json"; - -pub fn init_history(system_prompt: impl Into, max_turns: usize) { - let mut h = ConversationHistory::new(system_prompt, max_turns); - if let Some(cfg) = crate::APP_CONFIG_DIR.get() { - let path = cfg.join(HISTORY_FILE); - h = h.with_persistence(path); - log::info!( - "LLM conversation history persistence enabled ({} turns restored)", - h.len() - ); - } - *HISTORY.write() = Some(h); -} - -pub fn history_push_user(content: impl Into) { - if let Some(h) = HISTORY.write().as_mut() { - h.push_user(content); - } -} - -pub fn history_push_assistant(content: impl Into) { - if let Some(h) = HISTORY.write().as_mut() { - h.push_assistant(content); - } -} - -pub fn history_snapshot() -> Vec { - HISTORY.read().as_ref().map(|h| h.snapshot()).unwrap_or_default() -} - -pub fn history_clear() { - if let Some(h) = HISTORY.write().as_mut() { - h.clear(); - } -} - -pub fn history_pop_last_user() { - if let Some(h) = HISTORY.write().as_mut() { - h.pop_last_user(); - } -} - -/// Return the most recent assistant message text, or None. -pub fn history_last_assistant() -> Option { - HISTORY.read().as_ref() - .and_then(|h| h.last_assistant().map(|m| m.content.clone())) -} - -/// Shared mutable LLM client. Modules read via `current()`; the user can -/// hot-swap backends at runtime via `swap_to(LlmBackend::Ollama)` etc. -/// -/// `None` means LLM is disabled (no GROQ_TOKEN and no Ollama reachable, or -/// `init_global()` was never called). -static GLOBAL: Lazy>>> = Lazy::new(|| RwLock::new(None)); - -/// Initialise the global client. Precedence: -/// 1. Settings DB `llm_backend` field (if non-empty) -/// 2. `JARVIS_LLM` env var -/// 3. Auto-detect (Groq if `GROQ_TOKEN`, else Ollama) -/// -/// Idempotent — replaces any previous global. Returns Ok even when the chosen -/// backend can't be probed: an Ollama server may not be running yet but we -/// stash the client anyway and let calls fail on first request. -pub fn init_global() -> Result<(), ConfigError> { - // Try DB-persisted choice first. - let persisted = crate::DB.get().and_then(|db| { - let s = db.read(); - let backend = s.llm_backend.trim().to_string(); - if backend.is_empty() { None } else { parse_backend(&backend) } - }); - - let c = match persisted { - Some(LlmBackend::Groq) => LlmClient::groq()?, - Some(LlmBackend::Ollama) => LlmClient::ollama(), - None => LlmClient::from_env()?, - }; - - log::info!("LLM global initialised: backend={}, model={}", c.backend().name(), c.model()); - *GLOBAL.write() = Some(Arc::new(c)); - Ok(()) -} - -/// Return a clone of the active client. Cheap (Arc). -pub fn current() -> Option> { - GLOBAL.read().clone() -} - -/// Name of the currently active backend, or "none" if not initialised. -pub fn current_backend_name() -> &'static str { - match GLOBAL.read().as_ref().map(|c| c.backend()) { - Some(LlmBackend::Groq) => "groq", - Some(LlmBackend::Ollama) => "ollama", - None => "none", - } -} - -/// Hot-swap to a different backend. Returns the new backend's name on success. -/// Persists the choice to the settings DB so it survives restart. -pub fn swap_to(backend: LlmBackend) -> Result<&'static str, ConfigError> { - let c = match backend { - LlmBackend::Groq => LlmClient::groq()?, - LlmBackend::Ollama => LlmClient::ollama(), - }; - let name = c.backend().name(); - log::info!("LLM swapped → backend={}, model={}", name, c.model()); - *GLOBAL.write() = Some(Arc::new(c)); - - // Persist to settings DB (best-effort — log on failure, don't propagate). - if let Some(db) = crate::DB.get() { - let snapshot = { - let mut s = db.write(); - s.llm_backend = name.to_string(); - s.clone() - }; - if let Err(e) = crate::db::save_settings(&snapshot) { - log::warn!("LLM swap: failed to persist to settings DB: {}", e); - } else { - log::info!("LLM choice persisted: {}", name); - } - } - - Ok(name) -} - -/// Parse a backend name (case-insensitive) into the enum. -pub fn parse_backend(name: &str) -> Option { - match name.trim().to_lowercase().as_str() { - "groq" | "cloud" | "облако" | "клауд" => Some(LlmBackend::Groq), - "ollama" | "local" | "локал" | "локальн" | "локальный" => Some(LlmBackend::Ollama), - _ => None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_backend_accepts_english() { - assert_eq!(parse_backend("groq"), Some(LlmBackend::Groq)); - assert_eq!(parse_backend("ollama"), Some(LlmBackend::Ollama)); - assert_eq!(parse_backend("cloud"), Some(LlmBackend::Groq)); - assert_eq!(parse_backend("local"), Some(LlmBackend::Ollama)); - } - - #[test] - fn parse_backend_accepts_russian() { - assert_eq!(parse_backend("облако"), Some(LlmBackend::Groq)); - assert_eq!(parse_backend("локальный"), Some(LlmBackend::Ollama)); - assert_eq!(parse_backend("локал"), Some(LlmBackend::Ollama)); - } - - #[test] - fn parse_backend_case_insensitive() { - assert_eq!(parse_backend("OLLAMA"), Some(LlmBackend::Ollama)); - assert_eq!(parse_backend(" Groq "), Some(LlmBackend::Groq)); - } - - #[test] - fn parse_backend_rejects_unknown() { - assert_eq!(parse_backend("openai"), None); - assert_eq!(parse_backend("claude"), None); - assert_eq!(parse_backend(""), None); - } - - #[test] - fn current_backend_name_when_uninitialised_is_none() { - // Note: in the same process as other tests, GLOBAL may have been - // initialised by ollama_works_without_token. So this assertion is - // best-effort — just exercise the code path. - let _ = current_backend_name(); - } -} diff --git a/crates/jarvis-core/src/long_term_memory.rs b/crates/jarvis-core/src/long_term_memory.rs deleted file mode 100644 index 951b76e..0000000 --- a/crates/jarvis-core/src/long_term_memory.rs +++ /dev/null @@ -1,366 +0,0 @@ -//! IMBA-2: Long-term memory store. -//! -//! Persistent fact storage for J.A.R.V.I.S.. Lets the user say things like -//! "запомни что у меня собака Бася" / "я люблю чай улун" and have the -//! assistant recall them in later sessions. -//! -//! Storage: JSON file at `/long_term_memory.json`. Atomic -//! write-through (write-then-rename) to survive crashes. Simple substring -//! search for v1; vector search via fastembed is a future upgrade. -//! -//! Lua access: -//! jarvis.memory.remember(key, value) -//! jarvis.memory.recall(key) -- returns value or nil -//! jarvis.memory.search(query, n?) -- returns array of {key, value} matches -//! jarvis.memory.forget(key) -//! jarvis.memory.all() -- returns full table (debug) -//! -//! On-disk record fields: key, value, created_at, last_used_at, use_count. -//! Auto-decay: entries unused for 90 days are returned last in search results -//! (not deleted automatically; explicit `forget` only). - -use once_cell::sync::OnceCell; -use parking_lot::RwLock; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use std::path::PathBuf; - -use crate::APP_CONFIG_DIR; - -const FILE_NAME: &str = "long_term_memory.json"; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MemoryRecord { - pub key: String, - pub value: String, - pub created_at: i64, - pub last_used_at: i64, - #[serde(default)] - pub use_count: u64, -} - -#[derive(Default, Debug, Serialize, Deserialize)] -struct Store { - #[serde(default)] - entries: BTreeMap, -} - -struct State { - path: PathBuf, - store: RwLock, -} - -static STATE: OnceCell = OnceCell::new(); - -pub fn init() -> Result<(), String> { - if STATE.get().is_some() { - return Ok(()); - } - - let dir = APP_CONFIG_DIR - .get() - .ok_or_else(|| "APP_CONFIG_DIR not initialised".to_string())?; - let path = dir.join(FILE_NAME); - - let store = load_or_empty(&path); - info!( - "Long-term memory loaded: {} entries from {}", - store.entries.len(), - path.display() - ); - - STATE.set(State { path, store: RwLock::new(store) }) - .map_err(|_| "long_term_memory already initialised".to_string())?; - Ok(()) -} - -fn load_or_empty(path: &PathBuf) -> Store { - if !path.is_file() { - return Store::default(); - } - match std::fs::read_to_string(path) { - Ok(s) => match serde_json::from_str::(&s) { - Ok(store) => store, - Err(e) => { - warn!("Corrupt memory file {}: {} — starting empty", path.display(), e); - Store::default() - } - }, - Err(e) => { - warn!("Cannot read {}: {} — starting empty", path.display(), e); - Store::default() - } - } -} - -fn persist(state: &State) { - let store = state.store.read(); - let tmp = state.path.with_extension("json.tmp"); - let json = match serde_json::to_string_pretty(&*store) { - Ok(s) => s, - Err(e) => { - warn!("Memory serialize failed: {}", e); - return; - } - }; - if let Err(e) = std::fs::write(&tmp, json) { - warn!("Memory write failed to {}: {}", tmp.display(), e); - return; - } - if let Err(e) = std::fs::rename(&tmp, &state.path) { - warn!("Memory rename failed: {}", e); - } -} - -fn now_secs() -> i64 { - use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs() as i64) - .unwrap_or(0) -} - -pub(crate) fn normalize_key(k: &str) -> String { - k.trim().to_lowercase() -} - -/// Filter+rank logic shared between `search` and tests. Public for unit tests -/// to bypass the global state. -pub(crate) fn search_in<'a>( - entries: impl IntoIterator, - query: &str, - limit: usize, -) -> Vec { - let nq = normalize_key(query); - if nq.is_empty() { - return Vec::new(); - } - let mut hits: Vec = entries.into_iter() - .filter(|r| r.key.contains(&nq) || r.value.to_lowercase().contains(&nq)) - .cloned() - .collect(); - hits.sort_by(|a, b| b.last_used_at.cmp(&a.last_used_at)); - hits.truncate(limit.max(1)); - hits -} - -/// Store / overwrite a fact. -pub fn remember(key: &str, value: &str) -> Result<(), String> { - let state = STATE.get().ok_or_else(|| "memory not init".to_string())?; - let nk = normalize_key(key); - if nk.is_empty() { - return Err("empty key".into()); - } - let now = now_secs(); - { - let mut store = state.store.write(); - let entry = store.entries.entry(nk.clone()).or_insert(MemoryRecord { - key: nk.clone(), - value: value.to_string(), - created_at: now, - last_used_at: now, - use_count: 0, - }); - entry.value = value.to_string(); - entry.last_used_at = now; - } - persist(state); - info!("Memory: remembered '{}' = '{}'", nk, value); - Ok(()) -} - -/// Retrieve a fact by exact key (case-insensitive). -pub fn recall(key: &str) -> Option { - let state = STATE.get()?; - let nk = normalize_key(key); - let now = now_secs(); - let value = { - let mut store = state.store.write(); - let entry = store.entries.get_mut(&nk)?; - entry.last_used_at = now; - entry.use_count += 1; - entry.value.clone() - }; - persist(state); - Some(value) -} - -/// Substring search (case-insensitive). Returns up to `limit` records ranked by -/// recency. Matches against both key and value. -pub fn search(query: &str, limit: usize) -> Vec { - let Some(state) = STATE.get() else { return Vec::new(); }; - let store = state.store.read(); - search_in(store.entries.values(), query, limit) -} - -/// Delete a fact. Returns true if it existed. -pub fn forget(key: &str) -> bool { - let Some(state) = STATE.get() else { return false; }; - let nk = normalize_key(key); - let removed = state.store.write().entries.remove(&nk).is_some(); - if removed { - persist(state); - info!("Memory: forgot '{}'", nk); - } - removed -} - -/// Wipe all stored facts. Returns the count of records removed. -/// Voice commands call this only after a confirmation prefix -/// ("точно забудь все" — not just "забудь все") to avoid disasters. -pub fn clear_all() -> usize { - let Some(state) = STATE.get() else { return 0; }; - let n = { - let mut store = state.store.write(); - let n = store.entries.len(); - store.entries.clear(); - n - }; - if n > 0 { - persist(state); - warn!("Memory: WIPED all {} facts", n); - } - n -} - -/// Snapshot of all records (for debug / GUI display). -pub fn all() -> Vec { - let Some(state) = STATE.get() else { return Vec::new(); }; - state.store.read().entries.values().cloned().collect() -} - -/// Build a system-prompt snippet containing relevant facts for the LLM, -/// based on a substring search of the user's prompt. Empty string if no matches. -pub fn build_context(prompt: &str, limit: usize) -> String { - let hits = search(prompt, limit); - build_context_from(&hits) -} - -/// Pure formatter used by both `build_context` and tests. -pub(crate) fn build_context_from(hits: &[MemoryRecord]) -> String { - if hits.is_empty() { - return String::new(); - } - let mut buf = String::with_capacity(256); - buf.push_str("Известные факты о пользователе (используй если уместно):\n"); - for h in hits { - buf.push_str(&format!("- {} = {}\n", h.key, h.value)); - } - buf -} - -#[cfg(test)] -mod tests { - use super::*; - - fn rec(key: &str, value: &str, last_used: i64) -> MemoryRecord { - MemoryRecord { - key: key.to_string(), - value: value.to_string(), - created_at: 0, - last_used_at: last_used, - use_count: 0, - } - } - - #[test] - fn normalize_key_lowercases_and_trims() { - assert_eq!(normalize_key(" HeLLo "), "hello"); - assert_eq!(normalize_key(""), ""); - assert_eq!(normalize_key("любимый ЧАЙ "), "любимый чай"); - } - - #[test] - fn search_in_matches_key_or_value() { - let entries = vec![ - rec("любимый чай", "улун", 100), - rec("любимый кофе", "арабика", 200), - rec("питомец", "собака бася", 300), - ]; - let hits = search_in(&entries, "чай", 5); - assert_eq!(hits.len(), 1); - assert_eq!(hits[0].key, "любимый чай"); - - let hits = search_in(&entries, "бася", 5); - assert_eq!(hits.len(), 1); - assert_eq!(hits[0].key, "питомец"); - } - - #[test] - fn search_in_ranks_by_recency() { - let entries = vec![ - rec("чай улун", "вкусный", 100), - rec("чай зеленый", "тоже", 300), - rec("чай черный", "обычный", 200), - ]; - let hits = search_in(&entries, "чай", 5); - assert_eq!(hits.len(), 3); - assert_eq!(hits[0].key, "чай зеленый"); // most recent - assert_eq!(hits[1].key, "чай черный"); - assert_eq!(hits[2].key, "чай улун"); - } - - #[test] - fn search_in_respects_limit() { - let entries = vec![ - rec("a", "x", 1), - rec("ab", "y", 2), - rec("abc", "z", 3), - ]; - let hits = search_in(&entries, "a", 2); - assert_eq!(hits.len(), 2); - } - - #[test] - fn search_in_empty_query_returns_nothing() { - let entries = vec![rec("a", "b", 0)]; - assert!(search_in(&entries, "", 5).is_empty()); - assert!(search_in(&entries, " ", 5).is_empty()); - } - - #[test] - fn build_context_empty_returns_empty_string() { - assert_eq!(build_context_from(&[]), ""); - } - - #[test] - fn build_context_lists_each_fact() { - let hits = vec![ - rec("чай", "улун", 0), - rec("кофе", "арабика", 0), - ]; - let ctx = build_context_from(&hits); - assert!(ctx.contains("чай = улун")); - assert!(ctx.contains("кофе = арабика")); - assert!(ctx.starts_with("Известные факты")); - } - - #[test] - fn memory_record_serde_round_trip() { - let r = rec("test", "value", 42); - let json = serde_json::to_string(&r).unwrap(); - let back: MemoryRecord = serde_json::from_str(&json).unwrap(); - assert_eq!(back.key, r.key); - assert_eq!(back.value, r.value); - assert_eq!(back.last_used_at, r.last_used_at); - } - - #[test] - fn store_serde_round_trip() { - let mut store = Store::default(); - store.entries.insert("key1".into(), rec("key1", "val1", 100)); - store.entries.insert("key2".into(), rec("key2", "val2", 200)); - let json = serde_json::to_string(&store).unwrap(); - let back: Store = serde_json::from_str(&json).unwrap(); - assert_eq!(back.entries.len(), 2); - assert_eq!(back.entries.get("key1").unwrap().value, "val1"); - } - - #[test] - fn clear_all_returns_zero_when_state_uninit() { - // STATE may or may not be initialised depending on test order; the - // public function still needs to return safely (and 0) in either case. - // We just exercise the code path — exact count depends on global state. - let _ = clear_all(); - } -} diff --git a/crates/jarvis-core/src/lua/api.rs b/crates/jarvis-core/src/lua/api.rs index b34f6b5..8bda5a4 100644 --- a/crates/jarvis-core/src/lua/api.rs +++ b/crates/jarvis-core/src/lua/api.rs @@ -4,15 +4,4 @@ pub mod context; pub mod http; pub mod fs; pub mod state; -pub mod system; -pub mod tts; -pub mod llm; -pub mod text; -pub mod memory; -pub mod profile; -pub mod vision; -pub mod scheduler; -pub mod cmd; -pub mod health; -pub mod macros; -pub mod banter; \ No newline at end of file +pub mod system; \ No newline at end of file diff --git a/crates/jarvis-core/src/lua/api/banter.rs b/crates/jarvis-core/src/lua/api/banter.rs deleted file mode 100644 index 84005b1..0000000 --- a/crates/jarvis-core/src/lua/api/banter.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! `jarvis.banter` — lua bindings for the idle-banter background module. -//! -//! Usage from voice packs: -//! jarvis.banter.fire() -- force-fire one remark now (returns bool) -//! jarvis.banter.pause() -- pause until resume() -//! jarvis.banter.resume() -//! jarvis.banter.enabled() -- true if env opt-in is set - -use mlua::{Lua, Table}; - -use crate::idle_banter; - -pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> { - let b = lua.create_table()?; - - b.set("fire", lua.create_function(|_, ()| Ok(idle_banter::fire_now()))?)?; - b.set("pause", lua.create_function(|_, ()| { - idle_banter::pause(); - Ok(()) - })?)?; - b.set("resume", lua.create_function(|_, ()| { - idle_banter::resume(); - Ok(()) - })?)?; - b.set("enabled", lua.create_function(|_, ()| Ok(idle_banter::enabled()))?)?; - - jarvis.set("banter", b)?; - Ok(()) -} diff --git a/crates/jarvis-core/src/lua/api/cmd.rs b/crates/jarvis-core/src/lua/api/cmd.rs deleted file mode 100644 index ec50ee7..0000000 --- a/crates/jarvis-core/src/lua/api/cmd.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! `jarvis.cmd` — boilerplate-killer helpers for Lua command scripts. -//! -//! Most packs end with the same 3-line ritual: -//! -//! jarvis.speak(msg) -//! jarvis.audio.play_ok() -//! return { chain = false } -//! -//! With these helpers it's one call: -//! -//! return jarvis.cmd.ok(msg) -//! -//! Available helpers (each returns a `{ chain = false }` result table so the -//! pack can simply `return jarvis.cmd.ok(...)`): -//! -//! jarvis.cmd.ok(msg?) -- play_ok sound, optional spoken msg -//! jarvis.cmd.chain_ok(msg?) -- like ok() but returns {chain = true} -//! jarvis.cmd.error(msg?) -- play_error sound, optional spoken msg -//! jarvis.cmd.not_found(msg?) -- play_not_found sound, optional spoken msg -//! jarvis.cmd.silent_ok() -- only play_ok, no speech -//! jarvis.cmd.silent_error() -- only play_error, no speech - -use mlua::{Lua, Table}; - -use crate::tts::{self, SpeakOpts}; -use crate::voices; - -pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> { - let cmd = lua.create_table()?; - - cmd.set("ok", lua.create_function(|l, msg: Option| { - if let Some(text) = msg.filter(|s| !s.trim().is_empty()) { - tts::speak(&text, &SpeakOpts::default()); - } - voices::play_ok(); - result_table(l, false) - })?)?; - - cmd.set("chain_ok", lua.create_function(|l, msg: Option| { - if let Some(text) = msg.filter(|s| !s.trim().is_empty()) { - tts::speak(&text, &SpeakOpts::default()); - } - voices::play_ok(); - result_table(l, true) - })?)?; - - cmd.set("error", lua.create_function(|l, msg: Option| { - if let Some(text) = msg.filter(|s| !s.trim().is_empty()) { - tts::speak(&text, &SpeakOpts::default()); - } - voices::play_error(); - result_table(l, false) - })?)?; - - cmd.set("not_found", lua.create_function(|l, msg: Option| { - if let Some(text) = msg.filter(|s| !s.trim().is_empty()) { - tts::speak(&text, &SpeakOpts::default()); - } - voices::play_not_found(); - result_table(l, false) - })?)?; - - cmd.set("silent_ok", lua.create_function(|l, ()| { - voices::play_ok(); - result_table(l, false) - })?)?; - - cmd.set("silent_error", lua.create_function(|l, ()| { - voices::play_error(); - result_table(l, false) - })?)?; - - jarvis.set("cmd", cmd)?; - Ok(()) -} - -fn result_table(lua: &Lua, chain: bool) -> mlua::Result { - let t = lua.create_table()?; - t.set("chain", chain)?; - Ok(t) -} diff --git a/crates/jarvis-core/src/lua/api/health.rs b/crates/jarvis-core/src/lua/api/health.rs deleted file mode 100644 index 9887531..0000000 --- a/crates/jarvis-core/src/lua/api/health.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! `jarvis.health()` — debug snapshot of active runtime state. -//! -//! Returns a table that can be JSON-encoded for bug reports. Doesn't include -//! secrets (no API keys, no LLM history content). -//! -//! Example use from Lua: -//! local h = jarvis.health() -//! for k, v in pairs(h) do print(k, v) end - -use mlua::{Lua, Table}; - -pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> { - let health_fn = lua.create_function(|lua, ()| { - let t = lua.create_table()?; - - // TTS - t.set("tts_backend", crate::tts::backend().name())?; - - // LLM - t.set("llm_backend", crate::llm::current_backend_name())?; - if let Some(c) = crate::llm::current() { - t.set("llm_model", c.model().to_string())?; - } - - // Profile - t.set("active_profile", crate::profiles::active_name())?; - - // Memory size - t.set("memory_facts", crate::long_term_memory::all().len())?; - - // Scheduler size - t.set("scheduled_tasks", crate::scheduler::list().len())?; - - // Language - t.set("language", crate::i18n::get_language())?; - - // Voice from settings - if let Some(db) = crate::DB.get() { - let s = db.read(); - t.set("voice", s.voice.clone())?; - t.set("microphone", s.microphone)?; - t.set("vosk_model", s.vosk_model.clone())?; - t.set("noise_suppression", format!("{:?}", s.noise_suppression))?; - } - - Ok(t) - })?; - jarvis.set("health", health_fn)?; - Ok(()) -} diff --git a/crates/jarvis-core/src/lua/api/llm.rs b/crates/jarvis-core/src/lua/api/llm.rs deleted file mode 100644 index b170b43..0000000 --- a/crates/jarvis-core/src/lua/api/llm.rs +++ /dev/null @@ -1,140 +0,0 @@ -use mlua::{Lua, Table, Value}; - -use crate::llm::{self, ChatMessage}; - -// Lua-callable wrapper around jarvis_core::llm. -// Command scripts call: -// local reply = jarvis.llm( -// { {role='system', content='...'}, {role='user', content='...'} }, -// { temperature = 0.0, max_tokens = 64 } -// ) -// Returns assistant text (string) or nil on any failure. -// -// Backend selection is global (see `jarvis_core::llm::swap_to`). Lua scripts can: -// jarvis.llm_status() -> "groq" | "ollama" | "none" -// jarvis.llm_switch("ollama") -> name string on success, nil on failure -pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> { - let chat_fn = lua.create_function(|lua, (messages, opts): (Table, Option
)| { - let mut chat_messages = Vec::new(); - for pair in messages.sequence_values::
() { - let msg = pair?; - let role = msg.get::("role").unwrap_or_else(|_| "user".to_string()); - let content = msg.get::("content").unwrap_or_default(); - if content.is_empty() { - continue; - } - chat_messages.push(match role.as_str() { - "system" => ChatMessage::system(content), - "assistant" => ChatMessage::assistant(content), - _ => ChatMessage::user(content), - }); - } - if chat_messages.is_empty() { - return Ok(Value::Nil); - } - - let mut max_tokens: u32 = 256; - let mut temperature: f32 = 0.7; - let mut top_p: f32 = 1.0; - if let Some(o) = opts { - if let Ok(v) = o.get::("max_tokens") { max_tokens = v; } - if let Ok(v) = o.get::("temperature") { temperature = v; } - if let Ok(v) = o.get::("top_p") { top_p = v; } - } - - // Prefer the live global client (so runtime backend swap takes effect). - // Fall back to a one-shot client from env if global is not initialised. - let client = match llm::current() { - Some(c) => c, - None => { - match llm::LlmClient::from_env() { - Ok(c) => std::sync::Arc::new(c), - Err(e) => { - log::warn!("[Lua llm] no client: {}", e); - return Ok(Value::Nil); - } - } - } - }; - - match client.complete_with(&chat_messages, max_tokens, temperature, top_p) { - Ok(reply) => { - let s = lua.create_string(reply.trim())?; - Ok(Value::String(s)) - } - Err(e) => { - log::warn!("[Lua llm] request failed: {}", e); - Ok(Value::Nil) - } - } - })?; - jarvis.set("llm", chat_fn)?; - - // Current backend name ("groq" | "ollama" | "none"). - let status_fn = lua.create_function(|_, ()| { - Ok(llm::current_backend_name().to_string()) - })?; - jarvis.set("llm_status", status_fn)?; - - // Hot-swap backend. Accepts "groq"/"ollama"/"local"/"cloud"/"локальный"/"облако". - // Returns the new backend name on success, nil on failure. - let switch_fn = lua.create_function(|lua, name: String| { - match llm::parse_backend(&name) { - Some(b) => match llm::swap_to(b) { - Ok(actual) => { - let s = lua.create_string(actual)?; - Ok(Value::String(s)) - } - Err(e) => { - log::warn!("[Lua llm_switch] swap failed: {}", e); - Ok(Value::Nil) - } - }, - None => { - log::warn!("[Lua llm_switch] unknown backend name: {}", name); - Ok(Value::Nil) - } - } - })?; - jarvis.set("llm_switch", switch_fn)?; - - // Clear conversation turns (keeps system prompt). Returns true if history was - // initialised, false if LLM is not configured. - let reset_fn = lua.create_function(|_, ()| { - llm::history_clear(); - Ok(true) - })?; - jarvis.set("llm_reset", reset_fn)?; - - // Return the most recent assistant message text, or nil. - let last_fn = lua.create_function(|lua, ()| { - match llm::history_last_assistant() { - Some(text) => { - let s = lua.create_string(text)?; - Ok(Value::String(s)) - } - None => Ok(Value::Nil), - } - })?; - jarvis.set("llm_last_reply", last_fn)?; - - // Snapshot of the full conversation history as an array of - // {role, content} tables. Excludes the system prompt — only the actual - // back-and-forth. Empty array if there's no history yet. - let snapshot_fn = lua.create_function(|lua, ()| { - let arr = lua.create_table()?; - for (i, msg) in llm::history_snapshot().iter().enumerate() { - if msg.role == "system" { - continue; - } - let row = lua.create_table()?; - row.set("role", msg.role.clone())?; - row.set("content", msg.content.clone())?; - arr.set(i, row)?; - } - Ok(arr) - })?; - jarvis.set("llm_history", snapshot_fn)?; - - Ok(()) -} diff --git a/crates/jarvis-core/src/lua/api/macros.rs b/crates/jarvis-core/src/lua/api/macros.rs deleted file mode 100644 index 89d0657..0000000 --- a/crates/jarvis-core/src/lua/api/macros.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Lua bindings for the macro recorder. -//! -//! Usage from Lua: -//! jarvis.macros.start("работа") -- begin recording -//! jarvis.macros.save() -- persist + stop -//! jarvis.macros.cancel() -- abort without saving -//! jarvis.macros.replay("работа") -- play back; returns step count or nil -//! jarvis.macros.list() -- array of {name, steps_count, last_run} -//! jarvis.macros.delete(name) -- removes -//! jarvis.macros.is_recording() -- bool -//! jarvis.macros.recording_name() -- string or nil - -use mlua::{Lua, Table, Value}; - -use crate::macros; - -pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> { - let m = lua.create_table()?; - - m.set("start", lua.create_function(|_, name: String| { - macros::start_recording(&name).map_err(mlua::Error::external) - })?)?; - - m.set("save", lua.create_function(|_, ()| { - macros::save_recording().map_err(mlua::Error::external) - })?)?; - - m.set("cancel", lua.create_function(|_, ()| { - Ok(macros::cancel_recording()) - })?)?; - - m.set("is_recording", lua.create_function(|_, ()| { - Ok(macros::is_recording()) - })?)?; - - m.set("recording_name", lua.create_function(|lua, ()| { - match macros::recording_name() { - Some(n) => Ok(Value::String(lua.create_string(n)?)), - None => Ok(Value::Nil), - } - })?)?; - - m.set("replay", lua.create_function(|_, name: String| { - macros::replay(&name).map_err(mlua::Error::external) - })?)?; - - m.set("delete", lua.create_function(|_, name: String| { - Ok(macros::delete(&name)) - })?)?; - - m.set("list", lua.create_function(|lua, ()| { - let arr = lua.create_table()?; - for (i, mac) in macros::list().iter().enumerate() { - let row = lua.create_table()?; - row.set("name", mac.name.clone())?; - row.set("steps_count", mac.steps.len())?; - row.set("created_at", mac.created_at)?; - let last: Value = match mac.last_run { - Some(ts) => Value::Integer(ts), - None => Value::Nil, - }; - row.set("last_run", last)?; - arr.set(i + 1, row)?; - } - Ok(arr) - })?)?; - - jarvis.set("macros", m)?; - Ok(()) -} diff --git a/crates/jarvis-core/src/lua/api/memory.rs b/crates/jarvis-core/src/lua/api/memory.rs deleted file mode 100644 index 413daf8..0000000 --- a/crates/jarvis-core/src/lua/api/memory.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! Lua bindings for the long-term memory store. -//! -//! Usage from Lua scripts: -//! -//! jarvis.memory.remember("любимый чай", "улун") -//! local v = jarvis.memory.recall("любимый чай") -- "улун" or nil -//! for _, hit in ipairs(jarvis.memory.search("чай", 5)) do -//! print(hit.key, hit.value) -//! end -//! jarvis.memory.forget("любимый чай") -//! local all = jarvis.memory.all() -- array of {key, value, ...} - -use mlua::{Lua, Table}; - -use crate::long_term_memory; - -pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> { - let mem = lua.create_table()?; - - let remember = lua.create_function(|_, (k, v): (String, String)| { - long_term_memory::remember(&k, &v) - .map_err(|e| mlua::Error::external(e)) - })?; - mem.set("remember", remember)?; - - let recall = lua.create_function(|_, k: String| { - Ok(long_term_memory::recall(&k)) - })?; - mem.set("recall", recall)?; - - let search = lua.create_function(|lua, (q, n): (String, Option)| { - let hits = long_term_memory::search(&q, n.unwrap_or(5)); - let arr = lua.create_table()?; - for (i, h) in hits.iter().enumerate() { - let row = lua.create_table()?; - row.set("key", h.key.clone())?; - row.set("value", h.value.clone())?; - row.set("created_at", h.created_at)?; - row.set("last_used_at", h.last_used_at)?; - row.set("use_count", h.use_count)?; - arr.set(i + 1, row)?; - } - Ok(arr) - })?; - mem.set("search", search)?; - - let forget = lua.create_function(|_, k: String| { - Ok(long_term_memory::forget(&k)) - })?; - mem.set("forget", forget)?; - - let all = lua.create_function(|lua, ()| { - let recs = long_term_memory::all(); - let arr = lua.create_table()?; - for (i, h) in recs.iter().enumerate() { - let row = lua.create_table()?; - row.set("key", h.key.clone())?; - row.set("value", h.value.clone())?; - row.set("created_at", h.created_at)?; - row.set("last_used_at", h.last_used_at)?; - row.set("use_count", h.use_count)?; - arr.set(i + 1, row)?; - } - Ok(arr) - })?; - mem.set("all", all)?; - - // Wipe ALL memory. Returns count of removed facts. Voice packs should - // only call this behind a deliberate phrase like "точно забудь всё". - let clear_all = lua.create_function(|_, ()| { - Ok(long_term_memory::clear_all()) - })?; - mem.set("clear_all", clear_all)?; - - jarvis.set("memory", mem)?; - Ok(()) -} diff --git a/crates/jarvis-core/src/lua/api/profile.rs b/crates/jarvis-core/src/lua/api/profile.rs deleted file mode 100644 index 6334773..0000000 --- a/crates/jarvis-core/src/lua/api/profile.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Lua bindings for profile switching. -//! -//! Usage from Lua scripts: -//! -//! local p = jarvis.profile.active() -- table {name, icon, description, ...} -//! jarvis.profile.set("work") -- switch to work profile -//! local names = jarvis.profile.list() -- {"default", "work", "game", "sleep", "driving"} -//! if jarvis.profile.allows("games.steam") then ... end - -use mlua::{Lua, Table}; - -use crate::profiles; - -pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> { - let prof = lua.create_table()?; - - let active = lua.create_function(|lua, ()| { - let p = profiles::active(); - let t = lua.create_table()?; - t.set("name", p.name)?; - t.set("description", p.description)?; - t.set("llm_personality", p.llm_personality)?; - t.set("greeting", p.greeting)?; - t.set("icon", p.icon)?; - Ok(t) - })?; - prof.set("active", active)?; - - let active_name = lua.create_function(|_, ()| Ok(profiles::active_name()))?; - prof.set("active_name", active_name)?; - - let set_active = lua.create_function(|lua, name: String| { - match profiles::set_active(&name) { - Ok(p) => { - let t = lua.create_table()?; - t.set("name", p.name)?; - t.set("icon", p.icon)?; - t.set("greeting", p.greeting)?; - Ok(t) - } - Err(e) => Err(mlua::Error::external(e)), - } - })?; - prof.set("set", set_active)?; - - let list = lua.create_function(|lua, ()| { - let names = profiles::list(); - let arr = lua.create_table()?; - for (i, n) in names.iter().enumerate() { - arr.set(i + 1, n.clone())?; - } - Ok(arr) - })?; - prof.set("list", list)?; - - let allows = lua.create_function(|_, cmd_id: String| { - Ok(profiles::active().allows_command(&cmd_id)) - })?; - prof.set("allows", allows)?; - - jarvis.set("profile", prof)?; - Ok(()) -} diff --git a/crates/jarvis-core/src/lua/api/scheduler.rs b/crates/jarvis-core/src/lua/api/scheduler.rs deleted file mode 100644 index 2c08c61..0000000 --- a/crates/jarvis-core/src/lua/api/scheduler.rs +++ /dev/null @@ -1,161 +0,0 @@ -//! Lua bindings for the proactive scheduler. -//! -//! Usage: -//! jarvis.scheduler.add({ -//! name = "Daily briefing", -//! schedule = "daily 09:00", -//! action = { type = "speak", text = "Доброе утро. Готов к работе." } -//! }) -//! -//! jarvis.scheduler.add({ -//! name = "Reminder", -//! schedule = "in 5 minutes", -//! action = { type = "speak", text = "Выключи кофеварку." } -//! }) -//! -//! for _, t in ipairs(jarvis.scheduler.list()) do -//! print(t.id, t.name, t.schedule_human) -//! end -//! -//! jarvis.scheduler.remove(id) -//! jarvis.scheduler.clear() -//! -//! Schedule string syntax (see `scheduler::Schedule::parse`): -//! "daily HH:MM" | "at HH:MM" | "every N minutes" | "every N hours" | "in N minutes" | "in N hours" - -use mlua::{Lua, Table, Value}; - -use crate::scheduler::{self, Action, Schedule, ScheduledTask}; - -pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> { - let sched = lua.create_table()?; - - let add_fn = lua.create_function(|_, t: Table| { - let name: String = t.get::>("name")?.unwrap_or_else(|| "task".to_string()); - let schedule_str: String = t.get::("schedule")?; - let id: String = t.get::>("id")?.unwrap_or_default(); - - let action_tbl: Table = t.get::
("action")?; - let action_type: String = action_tbl.get::("type")?; - let action = match action_type.as_str() { - "speak" => Action::Speak { - text: action_tbl.get::("text").unwrap_or_default(), - }, - "lua" => Action::Lua { - script_path: action_tbl.get::("script_path").unwrap_or_default(), - }, - other => return Err(mlua::Error::external(format!("unknown action type: {}", other))), - }; - - let schedule = Schedule::parse(&schedule_str) - .map_err(|e| mlua::Error::external(format!("bad schedule '{}': {}", schedule_str, e)))?; - - let task = ScheduledTask { - id, - name, - schedule, - action, - last_fired: None, - enabled: true, - created_at: 0, - }; - scheduler::add(task).map_err(mlua::Error::external) - })?; - sched.set("add", add_fn)?; - - let remove_fn = lua.create_function(|_, id: String| { - Ok(scheduler::remove(&id)) - })?; - sched.set("remove", remove_fn)?; - - let clear_fn = lua.create_function(|_, ()| { - Ok(scheduler::clear()) - })?; - sched.set("clear", clear_fn)?; - - // Remove all tasks whose name OR speak text matches `query` (substring, - // case-insensitive). Returns count removed. - let remove_by_text_fn = lua.create_function(|_, query: String| { - Ok(scheduler::remove_by_text(&query)) - })?; - sched.set("remove_by_text", remove_by_text_fn)?; - - // Find (don't remove) tasks matching query. Returns array of tasks. - let find_by_text_fn = lua.create_function(|lua, (query, n): (String, Option)| { - let arr = lua.create_table()?; - for (i, t) in scheduler::find_by_text(&query, n.unwrap_or(5)).iter().enumerate() { - arr.set(i + 1, task_to_lua(lua, t)?)?; - } - Ok(arr) - })?; - sched.set("find_by_text", find_by_text_fn)?; - - let list_fn = lua.create_function(|lua, ()| { - let arr = lua.create_table()?; - for (i, t) in scheduler::list().iter().enumerate() { - arr.set(i + 1, task_to_lua(lua, t)?)?; - } - Ok(arr) - })?; - sched.set("list", list_fn)?; - - let count_fn = lua.create_function(|_, ()| { - Ok(scheduler::list().len()) - })?; - sched.set("count", count_fn)?; - - jarvis.set("scheduler", sched)?; - Ok(()) -} - -fn task_to_lua(lua: &Lua, t: &ScheduledTask) -> mlua::Result
{ - let tbl = lua.create_table()?; - tbl.set("id", t.id.clone())?; - tbl.set("name", t.name.clone())?; - tbl.set("enabled", t.enabled)?; - tbl.set("created_at", t.created_at)?; - tbl.set("schedule_human", schedule_human(&t.schedule))?; - - let last_fired: Value = match t.last_fired { - Some(ts) => Value::Integer(ts), - None => Value::Nil, - }; - tbl.set("last_fired", last_fired)?; - - let action_tbl = lua.create_table()?; - match &t.action { - Action::Speak { text } => { - action_tbl.set("type", "speak")?; - action_tbl.set("text", text.clone())?; - } - Action::Lua { script_path } => { - action_tbl.set("type", "lua")?; - action_tbl.set("script_path", script_path.clone())?; - } - } - tbl.set("action", action_tbl)?; - - Ok(tbl) -} - -fn schedule_human(s: &Schedule) -> String { - match s { - Schedule::Daily { hour, minute } => format!("каждый день в {:02}:{:02}", hour, minute), - Schedule::Interval { seconds } => { - if *seconds % 3600 == 0 { - format!("каждые {} часов", seconds / 3600) - } else if *seconds % 60 == 0 { - format!("каждые {} минут", seconds / 60) - } else { - format!("каждые {} секунд", seconds) - } - } - Schedule::Once { at } => { - chrono::DateTime::from_timestamp(*at, 0) - .map(|dt: chrono::DateTime| { - dt.with_timezone(&chrono::Local).format("один раз в %H:%M %d.%m").to_string() - }) - .unwrap_or_else(|| format!("один раз в {}", at)) - } - } -} diff --git a/crates/jarvis-core/src/lua/api/system.rs b/crates/jarvis-core/src/lua/api/system.rs index 8f6074f..2722db8 100644 --- a/crates/jarvis-core/src/lua/api/system.rs +++ b/crates/jarvis-core/src/lua/api/system.rs @@ -78,11 +78,8 @@ pub fn register(lua: &Lua, jarvis: &Table, sandbox: SandboxLevel) -> mlua::Resul #[cfg(target_os = "windows")] { use winrt_notification::{Toast, Duration as ToastDuration}; - - // Use our registered AUMID so the toast attributes to "J.A.R.V.I.S." - // instead of "PowerShell" in Action Center. Falls back to the - // PowerShell AUMID transparently if registration failed at startup. - if let Err(e) = Toast::new(crate::toast::active_aumid()) + + if let Err(e) = Toast::new(Toast::POWERSHELL_APP_ID) .title(&title) .text1(&message) .duration(ToastDuration::Short) diff --git a/crates/jarvis-core/src/lua/api/text.rs b/crates/jarvis-core/src/lua/api/text.rs deleted file mode 100644 index ac6cab3..0000000 --- a/crates/jarvis-core/src/lua/api/text.rs +++ /dev/null @@ -1,65 +0,0 @@ -use mlua::{Lua, Table, Value}; - -// Tiny text helpers that every trigger-driven pack reinvents. Centralising -// here removes ~10 lines of identical boilerplate per pack. -pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> { - let text = lua.create_table()?; - - // jarvis.text.strip_trigger(phrase, triggers) -> remainder - // - // Lowercases `phrase`, finds the FIRST trigger from `triggers` that - // matches at the start (case-insensitive substring), and returns - // everything after it trimmed. If nothing matches, returns the original - // phrase trimmed. Empty input returns "". - // - // Triggers should be ordered longest-first so "напомни мне через" wins - // over "напомни" — the function does NOT re-sort for the caller. - let strip_fn = lua.create_function(|lua, (phrase, triggers): (String, Table)| { - let lowered = phrase.to_lowercase(); - let mut chosen_end: Option = None; - - for v in triggers.sequence_values::() { - let t = match v { - Ok(Value::String(s)) => s.to_str()?.to_lowercase(), - _ => continue, - }; - if t.is_empty() { - continue; - } - if lowered.starts_with(&t) { - let after = lowered[t.len()..].chars().next(); - if after.map_or(true, |c| !c.is_alphanumeric()) { - chosen_end = Some(t.len()); - break; - } - } - } - - let rest = match chosen_end { - Some(end) => &phrase[end..], - None => phrase.as_str(), - }; - Ok(lua.create_string(rest.trim())?) - })?; - text.set("strip_trigger", strip_fn)?; - - // jarvis.text.contains_any(phrase, needles) -> boolean - // case-insensitive "does any needle appear anywhere in the phrase". - let contains_fn = lua.create_function(|_, (phrase, needles): (String, Table)| { - let lowered = phrase.to_lowercase(); - for v in needles.sequence_values::() { - let n = match v { - Ok(Value::String(s)) => s.to_str()?.to_lowercase(), - _ => continue, - }; - if !n.is_empty() && lowered.contains(&n) { - return Ok(true); - } - } - Ok(false) - })?; - text.set("contains_any", contains_fn)?; - - jarvis.set("text", text)?; - Ok(()) -} diff --git a/crates/jarvis-core/src/lua/api/tts.rs b/crates/jarvis-core/src/lua/api/tts.rs deleted file mode 100644 index 78f730f..0000000 --- a/crates/jarvis-core/src/lua/api/tts.rs +++ /dev/null @@ -1,35 +0,0 @@ -use mlua::{Lua, Table}; - -use crate::tts::{self, SpeakOpts}; - -pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> { - // jarvis.speak(text, opts?) - // opts.lang: ISO 2-letter code (ru, en, de, ...). default "ru" - // opts.async: if true, fire-and-forget; if false, block until done. default true - // opts.raw: if true, skip text sanitization (default false) - // - // Uses the active TTS backend (see `crate::tts::backend`). Configured via JARVIS_TTS env var. - let speak_fn = lua.create_function(|_, (text, opts): (String, Option
)| { - let mut speak_opts = SpeakOpts::default(); - - if let Some(t) = opts { - if let Ok(v) = t.get::("lang") { speak_opts.lang = v; } - if let Ok(v) = t.get::("async") { speak_opts.detached = v; } - if let Ok(v) = t.get::("raw") { speak_opts.raw = v; } - } - - tts::speak(&text, &speak_opts); - Ok(()) - })?; - jarvis.set("speak", speak_fn)?; - - // jarvis.tts.backend() — returns name of active backend ("sapi", "piper", "silero") - let tts_table = lua.create_table()?; - let backend_fn = lua.create_function(|_, ()| { - Ok(tts::backend().name().to_string()) - })?; - tts_table.set("backend", backend_fn)?; - jarvis.set("tts", tts_table)?; - - Ok(()) -} diff --git a/crates/jarvis-core/src/lua/api/vision.rs b/crates/jarvis-core/src/lua/api/vision.rs deleted file mode 100644 index ad89e61..0000000 --- a/crates/jarvis-core/src/lua/api/vision.rs +++ /dev/null @@ -1,189 +0,0 @@ -//! IMBA-4: Multimodal — screenshot + vision LLM. -//! -//! Lua bindings: -//! jarvis.vision.screenshot(path?) -> path on disk (default temp .png) -//! jarvis.vision.describe(prompt?) -> string description (vision LLM call) -//! -//! Windows-only screenshot via PowerShell System.Drawing. Posts to a vision-capable -//! Groq model (`llama-3.2-11b-vision-preview` by default; override via `GROQ_VISION_MODEL`). -//! -//! Requires HTTP sandbox level (registered behind `allows_http()` in engine.rs). - -use mlua::{Lua, Table}; - -pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> { - let vision = lua.create_table()?; - - let screenshot = lua.create_function(|_, path: Option| { - match take_screenshot(path.as_deref()) { - Ok(p) => Ok(Some(p)), - Err(e) => { - log::warn!("vision.screenshot failed: {}", e); - Ok(None) - } - } - })?; - vision.set("screenshot", screenshot)?; - - let describe = lua.create_function(|_, prompt: Option| { - let user_prompt = prompt.unwrap_or_else(|| { - "Опиши кратко что на этом экране. Если есть ошибки — отметь их.".to_string() - }); - match describe_screen(&user_prompt) { - Ok(text) => Ok(Some(text)), - Err(e) => { - log::warn!("vision.describe failed: {}", e); - Ok(None) - } - } - })?; - vision.set("describe", describe)?; - - jarvis.set("vision", vision)?; - Ok(()) -} - -#[cfg(target_os = "windows")] -fn take_screenshot(out_path: Option<&str>) -> Result { - let target = match out_path { - Some(p) => std::path::PathBuf::from(p), - None => { - let f = tempfile::Builder::new() - .prefix("jarvis-screen-") - .suffix(".png") - .tempfile() - .map_err(|e| format!("tempfile: {}", e))?; - let p = f.path().to_path_buf(); - drop(f); - p - } - }; - - let escaped = target.display().to_string().replace('\'', "''"); - let ps = format!( - r#" -Add-Type -AssemblyName System.Windows.Forms; -Add-Type -AssemblyName System.Drawing; -$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds; -$bmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height; -$g = [System.Drawing.Graphics]::FromImage($bmp); -$g.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size); -$bmp.Save('{}', [System.Drawing.Imaging.ImageFormat]::Png); -$g.Dispose(); $bmp.Dispose(); -"#, - escaped - ); - - let status = std::process::Command::new("powershell") - .args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &ps]) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - .map_err(|e| format!("powershell spawn: {}", e))?; - - if !status.success() { - return Err(format!("screenshot powershell exited {}", status)); - } - if !target.is_file() { - return Err("screenshot file not created".to_string()); - } - Ok(target.display().to_string()) -} - -#[cfg(not(target_os = "windows"))] -fn take_screenshot(_out_path: Option<&str>) -> Result { - Err("screenshot not implemented on this platform".into()) -} - -#[cfg(target_os = "windows")] -fn describe_screen(prompt: &str) -> Result { - let path = take_screenshot(None)?; - let b64 = read_as_base64(&path)?; - let _ = std::fs::remove_file(&path); - call_vision_llm(prompt, &b64) -} - -#[cfg(not(target_os = "windows"))] -fn describe_screen(_prompt: &str) -> Result { - Err("vision not implemented on this platform".into()) -} - -#[cfg(target_os = "windows")] -fn read_as_base64(path: &str) -> Result { - // Avoid a new base64 crate dep — use PowerShell [Convert]::ToBase64String. - let escaped = path.replace('\'', "''"); - let ps = format!( - "[Convert]::ToBase64String([IO.File]::ReadAllBytes('{}'))", - escaped - ); - let out = std::process::Command::new("powershell") - .args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &ps]) - .output() - .map_err(|e| format!("base64 powershell: {}", e))?; - if !out.status.success() { - return Err(format!( - "base64 powershell exited {}: {}", - out.status, - String::from_utf8_lossy(&out.stderr) - )); - } - let s = String::from_utf8_lossy(&out.stdout).trim().to_string(); - if s.is_empty() { - return Err("base64 output empty".into()); - } - Ok(s) -} - -#[cfg(target_os = "windows")] -fn call_vision_llm(prompt: &str, image_b64: &str) -> Result { - use serde_json::json; - - let token = std::env::var("GROQ_TOKEN") - .map_err(|_| "GROQ_TOKEN not set".to_string())?; - let base = std::env::var("GROQ_BASE_URL") - .unwrap_or_else(|_| "https://api.groq.com/openai/v1".to_string()); - let model = std::env::var("GROQ_VISION_MODEL") - .unwrap_or_else(|_| "llama-3.2-11b-vision-preview".to_string()); - - let body = json!({ - "model": model, - "max_tokens": 400, - "temperature": 0.2, - "messages": [{ - "role": "user", - "content": [ - { "type": "text", "text": prompt }, - { "type": "image_url", - "image_url": { - "url": format!("data:image/png;base64,{}", image_b64) - } - } - ] - }] - }); - - let url = format!("{}/chat/completions", base.trim_end_matches('/')); - let client = reqwest::blocking::Client::new(); - let resp = client - .post(&url) - .bearer_auth(&token) - .json(&body) - .send() - .map_err(|e| format!("HTTP: {}", e))?; - - let status = resp.status(); - let text = resp.text().map_err(|e| format!("read body: {}", e))?; - if !status.is_success() { - return Err(format!("vision API {}: {}", status.as_u16(), text)); - } - - let json: serde_json::Value = - serde_json::from_str(&text).map_err(|e| format!("parse JSON: {}", e))?; - json.get("choices") - .and_then(|c| c.get(0)) - .and_then(|c| c.get("message")) - .and_then(|m| m.get("content")) - .and_then(|c| c.as_str()) - .map(|s| s.trim().to_string()) - .ok_or_else(|| format!("no content in response: {}", text)) -} diff --git a/crates/jarvis-core/src/lua/engine.rs b/crates/jarvis-core/src/lua/engine.rs index d41dd9b..71380e1 100644 --- a/crates/jarvis-core/src/lua/engine.rs +++ b/crates/jarvis-core/src/lua/engine.rs @@ -76,21 +76,10 @@ impl LuaEngine { api::core::register(&self.lua, &jarvis)?; api::audio::register(&self.lua, &jarvis)?; api::context::register(&self.lua, &jarvis, context)?; - api::tts::register(&self.lua, &jarvis)?; - api::text::register(&self.lua, &jarvis)?; - api::memory::register(&self.lua, &jarvis)?; - api::profile::register(&self.lua, &jarvis)?; - api::scheduler::register(&self.lua, &jarvis)?; - api::cmd::register(&self.lua, &jarvis)?; - api::health::register(&self.lua, &jarvis)?; - api::macros::register(&self.lua, &jarvis)?; - api::banter::register(&self.lua, &jarvis)?; - + // sandbox-controlled APIs if self.sandbox.allows_http() { api::http::register(&self.lua, &jarvis)?; - api::llm::register(&self.lua, &jarvis)?; - api::vision::register(&self.lua, &jarvis)?; } if self.sandbox.allows_state() { diff --git a/crates/jarvis-core/src/lua/tests.rs b/crates/jarvis-core/src/lua/tests.rs index c8ef58d..f2035ee 100644 --- a/crates/jarvis-core/src/lua/tests.rs +++ b/crates/jarvis-core/src/lua/tests.rs @@ -93,7 +93,7 @@ mod tests { fn test_sandbox_fs_escape() { let dir = tempdir().unwrap(); let script_path = dir.path().join("test.lua"); - + fs::write(&script_path, r#" local ok, err = pcall(function() jarvis.fs.read("../../../etc/passwd") @@ -103,141 +103,10 @@ mod tests { end return true "#).unwrap(); - + let context = create_test_context(dir.path().to_path_buf()); let result = execute(&script_path, context, SandboxLevel::Standard, Duration::from_secs(5)); - + assert!(result.is_ok()); } - - /// Drive the shunting-yard evaluator from math.lua via a tiny Lua harness - /// that inlines just the parser. This lets us assert offline arithmetic - /// works without spinning up the LLM or network. We don't include the full - /// 250-line math.lua here — just the eval kernel — because the wider - /// script reaches into the audio/notify modules. - #[test] - fn test_math_shunting_yard_basics() { - let dir = tempdir().unwrap(); - let script_path = dir.path().join("eval.lua"); - - let script = r#" - local function tokenize(s) - local tokens = {} - local i = 1 - while i <= #s do - local c = s:sub(i, i) - if c:match("[%s]") then i = i + 1 - elseif c:match("[%d%.]") then - local j = i - while j <= #s and s:sub(j, j):match("[%d%.]") do j = j + 1 end - table.insert(tokens, { kind = "num", val = tonumber(s:sub(i, j - 1)) }) - i = j - elseif c == "+" or c == "-" or c == "*" or c == "/" or c == "^" then - if (c == "-" or c == "+") and (#tokens == 0 or tokens[#tokens].kind == "op" or tokens[#tokens].kind == "lparen") then - local j = i + 1 - while j <= #s and s:sub(j, j):match("[%s]") do j = j + 1 end - if j <= #s and s:sub(j, j):match("[%d%.]") then - local k = j - while k <= #s and s:sub(k, k):match("[%d%.]") do k = k + 1 end - local v = tonumber(s:sub(j, k - 1)) - if v then - table.insert(tokens, { kind = "num", val = (c == "-" and -v or v) }) - i = k - else return nil end - else return nil end - else - table.insert(tokens, { kind = "op", val = c }); i = i + 1 - end - elseif c == "(" then table.insert(tokens, { kind = "lparen" }); i = i + 1 - elseif c == ")" then table.insert(tokens, { kind = "rparen" }); i = i + 1 - else return nil end - end - return tokens - end - - local function precedence(op) - if op == "+" or op == "-" then return 1 end - if op == "*" or op == "/" then return 2 end - if op == "^" then return 3 end - return 0 - end - - local function eval_op(a, b, op) - if op == "+" then return a + b end - if op == "-" then return a - b end - if op == "*" then return a * b end - if op == "/" then if b == 0 then return nil end; return a / b end - if op == "^" then return a ^ b end - return nil - end - - local function eval(tokens) - if not tokens or #tokens == 0 then return nil end - local values, ops = {}, {} - local function apply() - local op = table.remove(ops) - local b = table.remove(values) - local a = table.remove(values) - if a == nil or b == nil then return false end - local r = eval_op(a, b, op) - if r == nil then return false end - table.insert(values, r) - return true - end - for _, t in ipairs(tokens) do - if t.kind == "num" then table.insert(values, t.val) - elseif t.kind == "op" then - while #ops > 0 and ops[#ops] ~= "(" and precedence(ops[#ops]) >= precedence(t.val) do - if not apply() then return nil end - end - table.insert(ops, t.val) - elseif t.kind == "lparen" then table.insert(ops, "(") - elseif t.kind == "rparen" then - while #ops > 0 and ops[#ops] ~= "(" do - if not apply() then return nil end - end - if ops[#ops] ~= "(" then return nil end - table.remove(ops) - end - end - while #ops > 0 do - if ops[#ops] == "(" then return nil end - if not apply() then return nil end - end - if #values ~= 1 then return nil end - return values[1] - end - - local cases = { - { "2 + 2", 4 }, - { "10 - 4", 6 }, - { "3 * 5", 15 }, - { "20 / 4", 5 }, - { "2 ^ 10", 1024 }, - { "1 + 2 * 3", 7 }, - { "(1 + 2) * 3", 9 }, - { "-5 + 10", 5 }, - { "100 / 0", nil }, - { "abc", nil }, - } - for _, c in ipairs(cases) do - local got = eval(tokenize(c[1])) - if got ~= c[2] then - error(string.format("case %q: expected %s, got %s", - c[1], tostring(c[2]), tostring(got))) - end - end - return true - "#; - - fs::write(&script_path, script).unwrap(); - let context = create_test_context(dir.path().to_path_buf()); - let result = execute( - &script_path, - context, - SandboxLevel::Minimal, - Duration::from_secs(5), - ); - assert!(result.is_ok(), "shunting yard test failed: {:?}", result); - } } \ No newline at end of file diff --git a/crates/jarvis-core/src/macros.rs b/crates/jarvis-core/src/macros.rs deleted file mode 100644 index 140171b..0000000 --- a/crates/jarvis-core/src/macros.rs +++ /dev/null @@ -1,334 +0,0 @@ -//! IMBA-7: Voice-command macros — record a sequence of commands once, -//! replay them later with one phrase. -//! -//! Simpler than AHK-style keyboard hooks: we record what the user SAID, then -//! replay each phrase through the normal dispatch path. Works for anything -//! that's already a known command (volume, media keys, profile switches, ...). -//! -//! Lifecycle: -//! -//! "Запиши макрос работа" → start_recording("работа") -//! "Открой браузер" → recorder buffers "открой браузер" -//! "Запусти спотифай" → recorder buffers -//! "Режим работа" → recorder buffers -//! "Сохрани макрос" → save() → persists to disk -//! -//! "Запусти макрос работа" → replay("работа") → fires each phrase in order, -//! with a small inter-step delay -//! -//! Storage: JSON at `/macros.json`. - -use once_cell::sync::OnceCell; -use parking_lot::RwLock; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use std::path::PathBuf; - -use crate::APP_CONFIG_DIR; - -const FILE_NAME: &str = "macros.json"; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Macro { - pub name: String, - pub steps: Vec, - #[serde(default)] - pub created_at: i64, - #[serde(default)] - pub last_run: Option, -} - -#[derive(Default, Debug, Serialize, Deserialize)] -struct Store { - #[serde(default)] - macros: BTreeMap, -} - -#[derive(Debug)] -struct RecordingState { - name: String, - steps: Vec, -} - -struct State { - path: PathBuf, - store: RwLock, - recording: RwLock>, -} - -static STATE: OnceCell = OnceCell::new(); - -pub fn init() -> Result<(), String> { - if STATE.get().is_some() { return Ok(()); } - let dir = APP_CONFIG_DIR.get() - .ok_or_else(|| "APP_CONFIG_DIR not initialised".to_string())?; - let path = dir.join(FILE_NAME); - let store = load_or_empty(&path); - info!("Macros loaded: {} stored at {}", store.macros.len(), path.display()); - STATE.set(State { path, store: RwLock::new(store), recording: RwLock::new(None) }) - .map_err(|_| "macros already initialised".to_string())?; - Ok(()) -} - -fn load_or_empty(path: &PathBuf) -> Store { - if !path.is_file() { return Store::default(); } - match std::fs::read_to_string(path) { - Ok(s) => serde_json::from_str(&s).unwrap_or_else(|e| { - warn!("Corrupt macros file {}: {}", path.display(), e); - Store::default() - }), - Err(e) => { - warn!("Cannot read {}: {}", path.display(), e); - Store::default() - } - } -} - -fn persist(state: &State) { - let store = state.store.read(); - let tmp = state.path.with_extension("json.tmp"); - let json = match serde_json::to_string_pretty(&*store) { - Ok(s) => s, - Err(e) => { warn!("Macros serialize failed: {}", e); return; } - }; - if let Err(e) = std::fs::write(&tmp, json) { - warn!("Macros write failed: {}", e); return; - } - if let Err(e) = std::fs::rename(&tmp, &state.path) { - warn!("Macros rename failed: {}", e); - } -} - -fn now_secs() -> i64 { - use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs() as i64) - .unwrap_or(0) -} - -fn normalize_name(n: &str) -> String { - n.trim().to_lowercase() -} - -/// Begin recording a new macro. Replaces any in-progress recording. -pub fn start_recording(name: &str) -> Result<(), String> { - let state = STATE.get().ok_or_else(|| "macros not init".to_string())?; - let nname = normalize_name(name); - if nname.is_empty() { return Err("empty macro name".into()); } - *state.recording.write() = Some(RecordingState { - name: nname.clone(), - steps: Vec::new(), - }); - info!("Macros: recording started for '{}'", nname); - Ok(()) -} - -/// Buffer a phrase into the active recording (if any). Called by the command -/// dispatch hook after a command runs successfully. -pub fn record_step(phrase: &str) { - let Some(state) = STATE.get() else { return; }; - let mut rec = state.recording.write(); - if let Some(r) = rec.as_mut() { - let p = phrase.trim(); - if p.is_empty() { return; } - // Don't record macro-control commands themselves (would be infinite recursion). - if is_macro_control(p) { return; } - r.steps.push(p.to_string()); - debug!("Macros: recorded step '{}' for '{}'", p, r.name); - } -} - -fn is_macro_control(phrase: &str) -> bool { - let p = phrase.to_lowercase(); - p.contains("запиши макрос") || p.contains("сохрани макрос") - || p.contains("отмени макрос") || p.contains("прекрати макрос") - || p.contains("отмени запись") - || p.starts_with("запусти макрос") || p.starts_with("воспроизведи макрос") - || p.starts_with("record macro") || p.starts_with("save macro") - || p.starts_with("play macro") -} - -/// Returns true if a recording is currently active. -pub fn is_recording() -> bool { - STATE.get().and_then(|s| s.recording.read().as_ref().map(|_| ())).is_some() -} - -/// Name of the macro currently being recorded, if any. -pub fn recording_name() -> Option { - STATE.get().and_then(|s| s.recording.read().as_ref().map(|r| r.name.clone())) -} - -/// Stop recording and save the macro. Returns the number of steps captured. -pub fn save_recording() -> Result { - let state = STATE.get().ok_or_else(|| "macros not init".to_string())?; - let rec = state.recording.write().take() - .ok_or_else(|| "не было активной записи".to_string())?; - - if rec.steps.is_empty() { - return Err("макрос пустой".into()); - } - - let count = rec.steps.len(); - let m = Macro { - name: rec.name.clone(), - steps: rec.steps, - created_at: now_secs(), - last_run: None, - }; - state.store.write().macros.insert(rec.name.clone(), m); - persist(state); - info!("Macros: saved '{}' ({} steps)", rec.name, count); - Ok(count) -} - -/// Cancel the active recording without saving. -pub fn cancel_recording() -> bool { - let Some(state) = STATE.get() else { return false; }; - state.recording.write().take().is_some() -} - -pub fn list() -> Vec { - let Some(state) = STATE.get() else { return Vec::new(); }; - state.store.read().macros.values().cloned().collect() -} - -pub fn get(name: &str) -> Option { - let state = STATE.get()?; - state.store.read().macros.get(&normalize_name(name)).cloned() -} - -pub fn delete(name: &str) -> bool { - let Some(state) = STATE.get() else { return false; }; - let removed = state.store.write().macros.remove(&normalize_name(name)).is_some(); - if removed { persist(state); } - removed -} - -/// Mark a macro as run (updates last_run timestamp). -pub fn mark_run(name: &str) { - let Some(state) = STATE.get() else { return; }; - let nname = normalize_name(name); - { - let mut store = state.store.write(); - if let Some(m) = store.macros.get_mut(&nname) { - m.last_run = Some(now_secs()); - } - } - persist(state); -} - -/// Replay callback — jarvis-app registers a function that fires a text command. -/// Macros module owns the timing of replays. -pub type ReplayCallback = Box; - -static REPLAY_CB: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); - -pub fn set_replay_callback(cb: ReplayCallback) { - let _ = REPLAY_CB.set(cb); -} - -const STEP_DELAY_MS: u64 = 800; - -/// Replay a stored macro. Each step is sent to the registered callback (which -/// the app wires to its text-command channel). Inter-step delay gives the -/// dispatcher time to process before the next step arrives. Returns step count. -pub fn replay(name: &str) -> Result { - let m = get(name).ok_or_else(|| format!("Макрос '{}' не найден.", name))?; - let cb = REPLAY_CB.get().ok_or_else(|| { - "Replay callback not registered (jarvis-app didn't wire it).".to_string() - })?; - - let steps_count = m.steps.len(); - info!("Macros: replay '{}' ({} steps)", m.name, steps_count); - let _ = cb; // sanity-checked existence; thread re-reads REPLAY_CB inside. - - // Spawn so the caller (likely a Lua script) returns immediately. - let name_owned = m.name.clone(); - let steps = m.steps.clone(); - std::thread::Builder::new() - .name(format!("macro-replay-{}", name_owned)) - .spawn(move || { - let cb = match REPLAY_CB.get() { - Some(c) => c, - None => { warn!("Macros: callback vanished mid-replay"); return; } - }; - for (i, step) in steps.iter().enumerate() { - info!("Macros: replay step {}/{}: {}", i + 1, steps.len(), step); - cb(step); - if i + 1 < steps.len() { - std::thread::sleep(std::time::Duration::from_millis(STEP_DELAY_MS)); - } - } - mark_run(&name_owned); - }) - .ok(); - - Ok(steps_count) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn is_macro_control_catches_record_phrase() { - assert!(is_macro_control("Запиши макрос работа")); - assert!(is_macro_control("сохрани макрос")); - assert!(is_macro_control("запусти макрос работа")); - assert!(!is_macro_control("открой браузер")); - assert!(!is_macro_control("установи громкость 50")); - } - - #[test] - fn normalize_name_lowercases_trims() { - assert_eq!(normalize_name(" Работа "), "работа"); - assert_eq!(normalize_name("GameMode"), "gamemode"); - } - - #[test] - fn macro_serde_round_trip() { - let m = Macro { - name: "test".into(), - steps: vec!["a".into(), "b".into()], - created_at: 100, - last_run: Some(200), - }; - let json = serde_json::to_string(&m).unwrap(); - let back: Macro = serde_json::from_str(&json).unwrap(); - assert_eq!(back.name, "test"); - assert_eq!(back.steps.len(), 2); - assert_eq!(back.last_run, Some(200)); - } - - #[test] - fn store_serde_round_trip() { - let mut store = Store::default(); - store.macros.insert("a".into(), Macro { - name: "a".into(), - steps: vec!["x".into()], - created_at: 1, - last_run: None, - }); - store.macros.insert("b".into(), Macro { - name: "b".into(), - steps: vec!["y".into(), "z".into()], - created_at: 2, - last_run: Some(3), - }); - let json = serde_json::to_string(&store).unwrap(); - let back: Store = serde_json::from_str(&json).unwrap(); - assert_eq!(back.macros.len(), 2); - assert_eq!(back.macros.get("b").unwrap().steps.len(), 2); - } - - #[test] - fn is_macro_control_handles_case() { - assert!(is_macro_control("ЗАПИШИ МАКРОС работа")); - assert!(is_macro_control("Сохрани макрос пожалуйста")); - assert!(is_macro_control("запусти макрос игра")); - // Noun form "запись макроса" intentionally does NOT match — we only - // filter the actual control verb-phrases. - assert!(!is_macro_control("запись макроса")); - } -} diff --git a/crates/jarvis-core/src/plugins.rs b/crates/jarvis-core/src/plugins.rs deleted file mode 100644 index 3566a9b..0000000 --- a/crates/jarvis-core/src/plugins.rs +++ /dev/null @@ -1,379 +0,0 @@ -//! Plugin system — user-installed command packs that live outside the read-only -//! `resources/commands/` tree. -//! -//! Why: shipping new voice packs as built-ins requires a rebuild + release. End -//! users (and external authors) want to drop a folder into a writable location -//! and have Jarvis pick it up. That's exactly what this module enables. -//! -//! Layout (per pack): -//! -//! ```text -//! /plugins/ -//! / -//! command.toml (required — same schema as built-in packs) -//! @@ -139,48 +69,6 @@ line-height: 1.7em; margin-top: 15px; - .edition { - display: inline-block; - margin-left: 6px; - padding: 1px 7px; - border-radius: 4px; - background: #b7411a; - color: #fff; - font-size: 11px; - font-weight: 600; - letter-spacing: 0.5px; - vertical-align: middle; - } - - .backends { - margin-bottom: 8px; - - .chip { - display: inline-block; - margin: 0 3px; - padding: 2px 8px; - border-radius: 10px; - font-size: 11px; - font-weight: 500; - letter-spacing: 0.2px; - color: #cfd2d5; - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.08); - - small { - color: #6c6e71; - margin-right: 3px; - font-size: 9px; - text-transform: uppercase; - letter-spacing: 0.5px; - } - - &.chip-tts { border-color: rgba(82, 254, 254, 0.25); } - &.chip-llm { border-color: rgba(138, 200, 50, 0.25); } - &.chip-profile { border-color: rgba(255, 168, 60, 0.25); } - } - } - p { margin: 0; padding: 0; @@ -191,17 +79,15 @@ &.last { margin-top: -5px; - color: #8a8d90; - font-size: 11px; } } } a { - color: #555759 !important; + color: #555759!important; text-decoration: none; transition: 0.3s; - + & > span { color: #185876; border-bottom: 1px solid #185876; @@ -215,7 +101,7 @@ } &:hover { - color: #777a7d !important; + color: #777a7d!important; & > span { color: #2A9CD0; @@ -225,6 +111,30 @@ opacity: 1; } } + + &.telegram-link { + color: #185876; + display: inline-block; + + &:hover { + color: #2A9CD0; + // background: url(/media/images/bg/bg24.gif); + // background-repeat: no-repeat; + // background-size: contain; + } + } + + &.special-link { + color: #941d92; + display: inline-block; + + &:hover { + color: #FF07FC; + background: url(/media/images/bg/bg24.gif); + background-repeat: no-repeat; + background-size: contain; + } + } } } diff --git a/frontend/src/components/Header.svelte b/frontend/src/components/Header.svelte index 12762dc..ef778aa 100644 --- a/frontend/src/components/Header.svelte +++ b/frontend/src/components/Header.svelte @@ -1,9 +1,9 @@ - + + \ No newline at end of file diff --git a/frontend/src/routes/history/index.svelte b/frontend/src/routes/history/index.svelte deleted file mode 100644 index 4fd9cbc..0000000 --- a/frontend/src/routes/history/index.svelte +++ /dev/null @@ -1,313 +0,0 @@ - - - - -

История распознаваний

- - Каждая фраза, которую Jarvis услышал и обработал. Зелёный — команда - нашлась и выполнилась, оранжевый — не понял, синий — обработал через LLM, - красный — была ошибка. Полезно понимать, что именно надо переформулировать - или дотренировать. - - - - -
- ✓ {matched} -   - ✗ {misses} -   - всего {entries.length} -
- - - -
- -   - -   - -
- - - -{#if error} - { error = "" }} - > - {error} - - -{/if} - -{#if loading} - -{:else if filtered.length === 0} - - {entries.length === 0 - ? "История пуста. Скажи что-нибудь — появится здесь." - : "Под фильтр ничего не подходит."} - -{:else} -
- {#each filtered as e} -
-
- «{e.phrase}» - - {kindLabel(e)} - -
- - - - {#if e.error_message} -
{e.error_message}
- {/if} -
- {/each} -
-{/if} - - - - - - -
- - diff --git a/frontend/src/routes/macros/index.svelte b/frontend/src/routes/macros/index.svelte deleted file mode 100644 index 337ee99..0000000 --- a/frontend/src/routes/macros/index.svelte +++ /dev/null @@ -1,306 +0,0 @@ - - - - -

Макросы

- - Запиши последовательность голосовых команд, потом запускай её одной фразой. - - - - -{#if isRecording} - - - Записывается макрос {recordingName ?? "—"}. - Произнесите команды, потом нажмите «Сохранить» или скажите «Сохрани макрос». - - - -   - - - -{:else} -
- - -
- -{/if} - -{#if error} - { error = "" }} - > - {error} - - -{/if} - -{#if loading} - -{:else if macros.length === 0} - - Макросов пока нет. Создайте первый: введите имя, нажмите «Начать запись», - произнесите команды, нажмите «Сохранить». - -{:else} -
- {#each macros as m} -
-
- {m.name} - - {m.steps_count} шагов - -
-
- {#each m.steps.slice(0, 5) as step} -
• {step}
- {/each} - {#if m.steps.length > 5} -
+ ещё {m.steps.length - 5}…
- {/if} -
-
- Создан: {fmtDate(m.created_at)} - Последний запуск: {fmtDate(m.last_run)} -
-
- -   - -
-
- {/each} -
-{/if} - - - - - - -
- - diff --git a/frontend/src/routes/memory/index.svelte b/frontend/src/routes/memory/index.svelte deleted file mode 100644 index 3ad7904..0000000 --- a/frontend/src/routes/memory/index.svelte +++ /dev/null @@ -1,249 +0,0 @@ - - - - -

Память

- - Долговременные факты о пользователе. LLM получает их в системном промпте автоматически, - когда фразой пересекаются. Хранятся в long_term_memory.json. - - - - -
-
- - - -
-
- - - - - - - -{#if error} - { error = "" }} - > - {error} - - -{/if} - -{#if loading} - -{:else if facts.length === 0} - - Память пустая. Добавь факт сверху или скажи Jarvis-у "запомни что я люблю чай улун". - -{:else if visible.length === 0} - - По фильтру ничего не найдено. Всего фактов: {facts.length}. - -{:else} -
- {#each visible as fact} -
-
- {fact.key} -
- - {fact.use_count}× использований - -
-
-
- {fact.value} -
-
- Создан: {fmtDate(fact.created_at)} - Последний доступ: {fmtDate(fact.last_used_at)} -
-
- -
-
- {/each} -
-{/if} - - - - - - -
- - diff --git a/frontend/src/routes/plugins/index.svelte b/frontend/src/routes/plugins/index.svelte deleted file mode 100644 index 7ae8161..0000000 --- a/frontend/src/routes/plugins/index.svelte +++ /dev/null @@ -1,237 +0,0 @@ - - - - -

Плагины

- - Дополнительные voice-команды, которые лежат рядом с конфигом - (%APPDATA%\com.priler.jarvis\plugins\). Каждый плагин — это - папка с command.toml и Lua-скриптами. - Меняется только после перезапуска. - - - - -
- -   - -
- - - -{#if info} - { info = "" }}> - {info} - - -{/if} - -{#if error} - { error = "" }} - > - {error} - - -{/if} - -{#if loading} - -{:else if plugins.length === 0} - - Плагинов пока нет. Скопируйте папку плагина в указанный каталог - и нажмите «Обновить список». - -{:else} -
- {#each plugins as p} -
-
- {p.name} - {#if p.error} - ошибка - {:else} - - {p.command_count} команд - - {/if} -
-
{p.path}
- {#if p.error} -
{p.error}
- {/if} -
- toggle(p)} - /> - - {p.enabled ? "Включён" : "Выключен"} - -
-
- {/each} -
-{/if} - - - - - - -
- - diff --git a/frontend/src/routes/scheduler/index.svelte b/frontend/src/routes/scheduler/index.svelte deleted file mode 100644 index 8fee2cd..0000000 --- a/frontend/src/routes/scheduler/index.svelte +++ /dev/null @@ -1,238 +0,0 @@ - - - - -

Расписание

- - Запланированные задачи: напоминания, ежедневные брифинги, привычки. - Тик каждые 30 секунд. - - - - -{#if error} - { error = "" }} - > - {error} - - -{/if} - -{#if loading} - -{:else if tasks.length === 0} - - Расписание пустое. Добавь задачу голосом: - «напомни через 5 минут выключить кофеварку», «каждые 2 часа напоминай пить воду», - «каждый день в 9 утра делай брифинг», «напоминай отдыхать глазам». - -{:else} -
- {#each tasks as task} -
-
-
- {task.name} - - {task.action_kind} - -
- - {task.schedule_human} - -
- - {#if task.action_text} -
- {task.action_text} -
- {/if} - -
- ID: {task.id} - Создано: {fmtDate(task.created_at)} - Последний запуск: {fmtDate(task.last_fired)} -
- -
- -
-
- {/each} -
- - - -{/if} - - - - - - -
- - diff --git a/frontend/src/routes/settings/index.svelte b/frontend/src/routes/settings/index.svelte index f3b77fa..ab8af45 100644 --- a/frontend/src/routes/settings/index.svelte +++ b/frontend/src/routes/settings/index.svelte @@ -5,11 +5,7 @@ import { setTimeout } from "worker-timers" import { showInExplorer } from "@/functions" - import { - appInfo, assistantVoice, translations, translate, - switchDaemonLlm, reloadDaemonLlm, switchDaemonTts, - daemonHealth, queryDaemonHealth, - } from "@/stores" + import { appInfo, assistantVoice, translations, translate } from "@/stores" import HDivider from "@/components/elements/HDivider.svelte" import Footer from "@/components/Footer.svelte" @@ -24,8 +20,7 @@ Input, InputWrapper, NativeSelect, - Switch, - Badge, + Switch } from "@svelteuidev/core" import { @@ -35,8 +30,7 @@ Code, Gear, QuestionMarkCircled, - CrossCircled, - ChatBubble, + CrossCircled } from "radix-icons-svelte" $: t = (key: string) => translate($translations, key) @@ -91,117 +85,15 @@ let apiKeyPicovoice = "" let apiKeyOpenai = "" - // ── AI backends (new tab) ───────────────────────────────────────────── - interface ActiveBackends { - tts: string - llm: string - llm_model: string | null - profile: string - } - - let activeBackends: ActiveBackends | null = null - let selectedLlmBackend = "" // "" = auto, "groq", "ollama" - let selectedTtsBackend = "" // "" = auto, "sapi", "piper", "silero" - let backendSwapBusy = false - let backendSwapError = "" - - async function refreshActiveBackends() { - try { - activeBackends = await invoke("get_active_backends") - } catch (err) { - console.error("Failed to read active backends:", err) - } - } - - async function applyLlmBackend(target: string) { - backendSwapError = "" - if (target === "") { - try { - await invoke("db_write", { key: "llm_backend", val: "" }) - reloadDaemonLlm() // tell daemon to re-read DB (now auto) - } catch (err) { - backendSwapError = String(err) - } - await refreshActiveBackends() - return - } - backendSwapBusy = true - try { - // 1. Swap on GUI process (persists to DB). - await invoke("set_llm_backend", { name: target }) - // 2. Tell daemon to swap too, so the running listener uses the - // new backend immediately. Returns false if WS disconnected - // so we can warn the user — otherwise they'd think the swap - // silently failed. - const daemonAware = switchDaemonLlm(target as "groq" | "ollama") - if (!daemonAware) { - backendSwapError = - "Демон не подключён — LLM в нём переключится при следующем запуске." - } - await refreshActiveBackends() - queryDaemonHealth() - } catch (err) { - backendSwapError = String(err) - } finally { - backendSwapBusy = false - } - } - - interface TtsSwapResult { - requested: string - applied: string - fell_back: boolean - note: string | null - } - - let ttsSwapInfo: TtsSwapResult | null = null - - async function applyTtsBackend(target: string) { - backendSwapError = "" - ttsSwapInfo = null - try { - // 1. GUI Tauri command — persists to DB + hot-swaps GUI's TTS. - // Returns the actual installed backend (may differ from - // requested if Piper/Silero unavailable → fell back to SAPI). - const res = await invoke("set_tts_backend", { name: target }) - ttsSwapInfo = res - // 2. Tell the running daemon (separate process) to switch too. - // sendAction returns false if the WS isn't connected — - // surface that so the user knows the daemon won't change. - const daemonAware = switchDaemonTts(target || "auto") - if (!daemonAware) { - backendSwapError = - "Демон (jarvis-app) не подключён. Изменение применится при следующем запуске. " + - "Запусти Jarvis с главного экрана и повтори, если нужно немедленно." - } - // 3. Refresh the local GUI snapshot AND re-poll daemon health - // so the "Активный движок" chip updates within ~100ms. - // Without the daemon re-poll, the chip keeps showing the - // OLD daemon-side TTS until the next 5s footer poll. - await refreshActiveBackends() - queryDaemonHealth() - } catch (err) { - backendSwapError = String(err) - } - } - - async function resetLlmContext() { - try { - await invoke("llm_reset_context") - } catch (err) { - console.error(err) - } - } - // subscribe to stores assistantVoice.subscribe(value => { voiceVal = value }) - let issuesLink = "" + let feedbackLink = "" let logFilePath = "" appInfo.subscribe(info => { - issuesLink = info.issuesLink + feedbackLink = info.feedbackLink logFilePath = info.logFilePath }) @@ -274,6 +166,7 @@ const languageNames: Record = { us: 'English', ru: 'Русский', + uk: 'Українська', de: 'German', fr: 'French', es: 'Spanish', @@ -322,19 +215,6 @@ gainNormalizerEnabled = gainNormalizer === "true" apiKeyPicovoice = pico apiKeyOpenai = openai - - // load AI backend prefs - try { - const [llm_pref, tts_pref] = await Promise.all([ - invoke("db_read", { key: "llm_backend" }), - invoke("db_read", { key: "tts_backend" }), - ]) - selectedLlmBackend = llm_pref ?? "" - selectedTtsBackend = tts_pref ?? "" - } catch (err) { - console.error("Failed to load backend prefs:", err) - } - await refreshActiveBackends() } catch (err) { console.error("failed to load settings:", err) } @@ -350,7 +230,7 @@ withCloseButton={false} > {t('settings-beta-desc')}
- {t('settings-beta-feedback')} {t('settings-beta-bot')}. + {t('settings-beta-feedback')} {t('settings-beta-bot')}. - - - - - - - • Ollama: установи с ollama.com, - выполни ollama pull qwen2.5:3b.
- • Piper: запусти pwsh tools/piper/install.ps1 чтобы скачать голос.
- • Голосом: «переключись на локальный» / «переключись на облако», - «какой у тебя мозг» — спрашивает статус.
- • Сброс/повтор: «сбрось контекст» / «повтори последнее». -
-
- - - - - - - - - - -   - - - {#if status.collected >= 5} - -
- Порог детектора: {threshold.toFixed(2)} - -
- - Ниже — больше срабатываний (и ложных). Выше — пропусков больше. - - - - {/if} - -{:else} -
- - - - - -
-{/if} - - - -

Обученные модели

-{#if models.length === 0} - Моделей пока нет — обучите первую выше. -{:else} -
- {#each models as m} -
-
- {m.name} - {fmtSize(m.size_bytes)} -
-
{m.path}
- обновлено: {fmtDate(m.modified)} - - -
- {/each} -
-{/if} - - - - - - -
- - diff --git a/frontend/src/stores.ts b/frontend/src/stores.ts index d67c404..51603ed 100644 --- a/frontend/src/stores.ts +++ b/frontend/src/stores.ts @@ -8,7 +8,6 @@ export { lastRecognizedText, lastExecutedCommand, lastError, - daemonHealth, connectIpc, enableIpc, disableIpc, @@ -17,15 +16,9 @@ export { sendIpcMessage, sendTextCommand, stopJarvisApp, - reloadCommands, - switchDaemonLlm, - reloadDaemonLlm, - switchDaemonTts, - queryDaemonHealth, + reloadCommands } from "./lib/ipc" -export type { DaemonHealth } from "./lib/ipc" - // re-export i18n export { translations, @@ -46,21 +39,12 @@ export const jarvisCpuUsage = writable(0) export const assistantVoice = writable("") // ### APP INFO -// Hardcoded fallbacks so the UI keeps working when the Rust side has not -// been rebuilt yet (the new get_upstream_link / get_issues_link / get_python_fork_link -// Tauri commands only exist after the next cargo build). -const BRANDING_FALLBACK = { - repositoryLink: "https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-rust", - upstreamLink: "https://github.com/Priler/jarvis", - issuesLink: "https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-rust/issues", - pythonForkLink: "https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-py", -} - export const appInfo = writable({ - repositoryLink: BRANDING_FALLBACK.repositoryLink, - upstreamLink: BRANDING_FALLBACK.upstreamLink, - issuesLink: BRANDING_FALLBACK.issuesLink, - pythonForkLink: BRANDING_FALLBACK.pythonForkLink, + tgOfficialLink: "", + feedbackLink: "", + repositoryLink: "", + boostySupportLink: "", + patreonSupportLink: "", logFilePath: "" }) @@ -75,38 +59,27 @@ export async function loadVoiceSetting() { } export async function loadAppInfo() { - const fetchOr = async (name: string, fallback: string): Promise => { - try { - const v = await invoke(name) - return v && v !== "error" ? v : fallback - } catch { - return fallback - } + try { + const [tg, feedback, repo, boosty, patreon, logPath] = await Promise.all([ + invoke("get_tg_official_link"), + invoke("get_feedback_link"), + invoke("get_repository_link"), + invoke("get_boosty_link"), + invoke("get_patreon_link"), + invoke("get_log_file_path") + ]) + + appInfo.set({ + tgOfficialLink: tg, + feedbackLink: feedback, + repositoryLink: repo, + boostySupportLink: boosty, + patreonSupportLink: patreon, + logFilePath: logPath + }) + } catch (err) { + console.error("failed to load app info:", err) } - - // The old binary's get_repository_link returns Priler's upstream — override - // so we never display the upstream repo as if it were the current fork. - const fetchRepoOr = async (fallback: string): Promise => { - const v = await fetchOr("get_repository_link", fallback) - if (/priler\/jarvis/i.test(v)) return fallback - return v - } - - const [repo, upstream, issues, pythonFork, logPath] = await Promise.all([ - fetchRepoOr(BRANDING_FALLBACK.repositoryLink), - fetchOr("get_upstream_link", BRANDING_FALLBACK.upstreamLink), - fetchOr("get_issues_link", BRANDING_FALLBACK.issuesLink), - fetchOr("get_python_fork_link", BRANDING_FALLBACK.pythonForkLink), - fetchOr("get_log_file_path", "") - ]) - - appInfo.set({ - repositoryLink: repo, - upstreamLink: upstream, - issuesLink: issues, - pythonForkLink: pythonFork, - logFilePath: logPath - }) } export async function updateJarvisStats() { @@ -125,7 +98,7 @@ let statsInterval: ReturnType | null = null export function startStatsPolling(intervalMs = 5000) { if (statsInterval) return // already running - + updateJarvisStats() statsInterval = setInterval(updateJarvisStats, intervalMs) } @@ -135,4 +108,4 @@ export function stopStatsPolling() { clearInterval(statsInterval) statsInterval = null } -} +} \ No newline at end of file diff --git a/make_ico.py b/make_ico.py deleted file mode 100644 index 50e382d..0000000 --- a/make_ico.py +++ /dev/null @@ -1,8 +0,0 @@ -from PIL import Image -src = r"C:\Jarvis\rust\resources\icons\icon.png" -dst = r"C:\Jarvis\rust\resources\icons\icon.ico" - -img = Image.open(src).convert("RGBA") -sizes = [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)] -img.save(dst, format="ICO", sizes=sizes) -print(f"wrote {dst} from {src} ({img.size})") diff --git a/resources/commands/apps/command.toml b/resources/commands/apps/command.toml deleted file mode 100644 index b1e95ce..0000000 --- a/resources/commands/apps/command.toml +++ /dev/null @@ -1,191 +0,0 @@ -[[commands]] -id = "open_browser" -type = "lua" -script = "open_browser.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "открой браузер", - "запусти браузер", - "открой хром", - "запусти хром", - "включи браузер", - "браузер открой", - "нужен браузер", -] -en = [ - "open browser", - "launch browser", - "open chrome", - "launch chrome", - "start browser", - "i need browser", -] - - -[[commands]] -id = "open_notepad" -type = "lua" -script = "open_notepad.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "открой блокнот", - "запусти блокнот", - "открой нотепад", -] -en = [ - "open notepad", - "launch notepad", -] - - -[[commands]] -id = "open_calculator" -type = "lua" -script = "open_calculator.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "открой калькулятор", - "запусти калькулятор", - "нужен калькулятор", -] -en = [ - "open calculator", - "launch calculator", - "calculator please", -] - - -[[commands]] -id = "open_explorer" -type = "lua" -script = "open_explorer.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "открой проводник", - "открой папку", - "запусти проводник", - "открой файлы", -] -en = [ - "open explorer", - "open file explorer", - "open files", - "open folder", -] - - -[[commands]] -id = "open_terminal" -type = "lua" -script = "open_terminal.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "открой терминал", - "запусти терминал", - "открой консоль", - "запусти консоль", - "открой повершелл", -] -en = [ - "open terminal", - "open powershell", - "open console", - "launch terminal", -] - - -[[commands]] -id = "open_settings" -type = "lua" -script = "open_settings.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "открой настройки", - "открой параметры", - "запусти настройки", - "открой настройки виндоус", -] -en = [ - "open settings", - "launch settings", - "windows settings", -] - - -[[commands]] -id = "open_task_manager" -type = "lua" -script = "open_task_manager.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "открой диспетчер задач", - "запусти диспетчер задач", - "диспетчер задач", -] -en = [ - "open task manager", - "launch task manager", - "task manager", -] - - -[[commands]] -id = "lock_screen" -type = "lua" -script = "lock_screen.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "заблокируй компьютер", - "заблокируй экран", - "блокировка экрана", - "залочь", -] -en = [ - "lock screen", - "lock the computer", - "lock pc", -] - - -[[commands]] -id = "screenshot" -type = "lua" -script = "screenshot.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "сделай скриншот", - "скриншот", - "сними экран", -] -en = [ - "take screenshot", - "screenshot", - "capture screen", -] diff --git a/resources/commands/apps/lock_screen.lua b/resources/commands/apps/lock_screen.lua deleted file mode 100644 index 956384f..0000000 --- a/resources/commands/apps/lock_screen.lua +++ /dev/null @@ -1,8 +0,0 @@ -local res = jarvis.system.exec("rundll32.exe user32.dll,LockWorkStation") -if res.success then - jarvis.audio.play_ok() -else - jarvis.log("error", "lock screen failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end -return { chain = false } diff --git a/resources/commands/apps/open_browser.lua b/resources/commands/apps/open_browser.lua deleted file mode 100644 index c1061ac..0000000 --- a/resources/commands/apps/open_browser.lua +++ /dev/null @@ -1,9 +0,0 @@ -local cmd = "start \"\" \"https://www.google.com\"" -local res = jarvis.system.exec(cmd) -if res.success then - jarvis.audio.play_ok() -else - jarvis.log("error", "open browser failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end -return { chain = false } diff --git a/resources/commands/apps/open_calculator.lua b/resources/commands/apps/open_calculator.lua deleted file mode 100644 index 0afe795..0000000 --- a/resources/commands/apps/open_calculator.lua +++ /dev/null @@ -1,8 +0,0 @@ -local res = jarvis.system.exec("start \"\" calc.exe") -if res.success then - jarvis.audio.play_ok() -else - jarvis.log("error", "open calculator failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end -return { chain = false } diff --git a/resources/commands/apps/open_explorer.lua b/resources/commands/apps/open_explorer.lua deleted file mode 100644 index 5c6a2e4..0000000 --- a/resources/commands/apps/open_explorer.lua +++ /dev/null @@ -1,8 +0,0 @@ -local res = jarvis.system.exec("start \"\" explorer.exe") -if res.success then - jarvis.audio.play_ok() -else - jarvis.log("error", "open explorer failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end -return { chain = false } diff --git a/resources/commands/apps/open_notepad.lua b/resources/commands/apps/open_notepad.lua deleted file mode 100644 index 023c4f1..0000000 --- a/resources/commands/apps/open_notepad.lua +++ /dev/null @@ -1,8 +0,0 @@ -local res = jarvis.system.exec("start \"\" notepad.exe") -if res.success then - jarvis.audio.play_ok() -else - jarvis.log("error", "open notepad failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end -return { chain = false } diff --git a/resources/commands/apps/open_settings.lua b/resources/commands/apps/open_settings.lua deleted file mode 100644 index 786ad13..0000000 --- a/resources/commands/apps/open_settings.lua +++ /dev/null @@ -1,8 +0,0 @@ -local res = jarvis.system.exec("start \"\" ms-settings:") -if res.success then - jarvis.audio.play_ok() -else - jarvis.log("error", "open settings failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end -return { chain = false } diff --git a/resources/commands/apps/open_task_manager.lua b/resources/commands/apps/open_task_manager.lua deleted file mode 100644 index b7d0f18..0000000 --- a/resources/commands/apps/open_task_manager.lua +++ /dev/null @@ -1,8 +0,0 @@ -local res = jarvis.system.exec("start \"\" taskmgr.exe") -if res.success then - jarvis.audio.play_ok() -else - jarvis.log("error", "open task manager failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end -return { chain = false } diff --git a/resources/commands/apps/open_terminal.lua b/resources/commands/apps/open_terminal.lua deleted file mode 100644 index cf91242..0000000 --- a/resources/commands/apps/open_terminal.lua +++ /dev/null @@ -1,13 +0,0 @@ -local res = jarvis.system.exec("start \"\" wt.exe") -if not res.success then - jarvis.log("warn", "wt.exe not found, falling back to powershell") - res = jarvis.system.exec("start \"\" powershell.exe") -end - -if res.success then - jarvis.audio.play_ok() -else - jarvis.log("error", "open terminal failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end -return { chain = false } diff --git a/resources/commands/apps/screenshot.lua b/resources/commands/apps/screenshot.lua deleted file mode 100644 index 4d24243..0000000 --- a/resources/commands/apps/screenshot.lua +++ /dev/null @@ -1,24 +0,0 @@ -local ts = jarvis.context.time.year .. jarvis.context.time.month .. jarvis.context.time.day - .. "_" .. jarvis.context.time.hour .. jarvis.context.time.minute .. jarvis.context.time.second - -local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" -local target = userprofile .. "\\Pictures\\jarvis_screenshot_" .. ts .. ".png" - -local ps = string.format( - [[Add-Type -AssemblyName System.Windows.Forms,System.Drawing; $b=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds; $bmp=New-Object Drawing.Bitmap $b.Width,$b.Height; $g=[Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen($b.Location,[Drawing.Point]::Empty,$b.Size); $bmp.Save('%s'); $g.Dispose(); $bmp.Dispose()]], - target:gsub("\\", "\\\\") -) - -local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', ps:gsub('"', '\\"')) -local res = jarvis.system.exec(cmd) - -if res.success then - jarvis.log("info", "screenshot saved to " .. target) - jarvis.system.notify("Скриншот", target) - jarvis.audio.play_ok() -else - jarvis.log("error", "screenshot failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end - -return { chain = false } diff --git a/resources/commands/backup/command.toml b/resources/commands/backup/command.toml deleted file mode 100644 index f9bae48..0000000 --- a/resources/commands/backup/command.toml +++ /dev/null @@ -1,18 +0,0 @@ -# Backup all user state (memory, profiles, schedule, macros) to a ZIP. - -[[commands]] -id = "backup.export" -type = "lua" -script = "export.lua" -sandbox = "full" -timeout = 10000 - -[commands.phrases] -ru = [ - "сделай бекап", - "экспортируй настройки", - "сохрани мои настройки", - "экспорт данных", - "выгрузи настройки", -] -en = ["backup", "export settings"] diff --git a/resources/commands/backup/export.lua b/resources/commands/backup/export.lua deleted file mode 100644 index 5dfa49d..0000000 --- a/resources/commands/backup/export.lua +++ /dev/null @@ -1,46 +0,0 @@ --- Zip all user-state JSON files into ~/Documents/jarvis-backup-.zip. -local t = jarvis.context.time -local stamp = string.format("%04d%02d%02d-%02d%02d%02d", - t.year, t.month, t.day, t.hour, t.minute, t.second or 0) - -local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" -local out_zip = userprofile .. "\\Documents\\jarvis-backup-" .. stamp .. ".zip" - --- The state files live under . We can't easily read that path --- from Lua, but the user can navigate from %APPDATA%/Jarvis. So we ask --- PowerShell to do the discovery + zip in one shot. -local ps = string.format([[ -$cfg = Join-Path $env:APPDATA 'Jarvis'; -if (-not (Test-Path $cfg)) { Write-Output 'NOCFG'; exit }; -$files = @(); -foreach ($n in @('long_term_memory.json','schedule.json','macros.json','active_profile.txt','llm_backend.txt','settings.json')) { - $p = Join-Path $cfg $n; - if (Test-Path $p) { $files += $p } -}; -$profiles_dir = Join-Path $cfg 'profiles'; -if (Test-Path $profiles_dir) { - $files += Get-ChildItem $profiles_dir -File | ForEach-Object { $_.FullName } -}; -if ($files.Count -eq 0) { Write-Output 'NONE'; exit }; -Compress-Archive -Path $files -DestinationPath '%s' -Force; -Write-Output ('OK|' + $files.Count) -]], out_zip:gsub("'", "''")) - -local res = jarvis.system.exec(string.format( - 'powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'):gsub("\r?\n", "; ") -)) -if not res.success then - return jarvis.cmd.error("Не получилось создать бекап.") -end - -local out = (res.stdout or ""):gsub("^%s+", ""):gsub("%s+$", "") -if out == "NOCFG" then - return jarvis.cmd.not_found("Папка настроек не найдена.") -end -if out == "NONE" then - return jarvis.cmd.not_found("Нечего бекапить — нет данных.") -end - -local count = out:match("OK|(%d+)") or "?" -jarvis.system.notify("Backup готов", out_zip) -return jarvis.cmd.ok(string.format("Бекап сохранён, %s файлов.", count)) diff --git a/resources/commands/banter/command.toml b/resources/commands/banter/command.toml deleted file mode 100644 index 4ef5156..0000000 --- a/resources/commands/banter/command.toml +++ /dev/null @@ -1,64 +0,0 @@ -# Idle-banter control voice commands. The banter background thread is started -# unconditionally; gating happens via JARVIS_IDLE_BANTER env. These commands -# let the user trigger / pause it without touching env. - -[[commands]] -id = "banter.fire" -type = "lua" -script = "fire.lua" -sandbox = "standard" -timeout = 5000 - -[commands.phrases] -ru = [ - "скажи что-нибудь интересное", - "пошути", - "развлеки меня", - "скучно мне", -] -en = [ - "say something interesting", - "tell me something", - "entertain me", - "i'm bored", -] - - -[[commands]] -id = "banter.pause" -type = "lua" -script = "pause.lua" -sandbox = "minimal" -timeout = 1500 - -[commands.phrases] -ru = [ - "помолчи", - "не отвлекай меня", - "выключи болтовню", -] -en = [ - "be quiet", - "stop chatting", - "stop chiming in", -] - - -[[commands]] -id = "banter.resume" -type = "lua" -script = "resume.lua" -sandbox = "minimal" -timeout = 1500 - -[commands.phrases] -ru = [ - "можешь говорить", - "включи болтовню", - "верни комментарии", -] -en = [ - "you can speak again", - "resume chatting", - "you may comment", -] diff --git a/resources/commands/banter/fire.lua b/resources/commands/banter/fire.lua deleted file mode 100644 index dbc905a..0000000 --- a/resources/commands/banter/fire.lua +++ /dev/null @@ -1,10 +0,0 @@ --- Manually trigger a banter line right now (ignores interval + quiet hours). -local fired = jarvis.banter.fire() -if not fired then - return jarvis.cmd.not_found( - jarvis.context.language == "ru" - and "Сэр, в этот раз без комментариев." - or "No remark today, sir." - ) -end -return { chain = true } diff --git a/resources/commands/banter/pause.lua b/resources/commands/banter/pause.lua deleted file mode 100644 index ba44456..0000000 --- a/resources/commands/banter/pause.lua +++ /dev/null @@ -1,6 +0,0 @@ -jarvis.banter.pause() -return jarvis.cmd.ok( - jarvis.context.language == "ru" - and "Молчу, сэр." - or "Quiet, sir." -) diff --git a/resources/commands/banter/resume.lua b/resources/commands/banter/resume.lua deleted file mode 100644 index 1ac830b..0000000 --- a/resources/commands/banter/resume.lua +++ /dev/null @@ -1,6 +0,0 @@ -jarvis.banter.resume() -return jarvis.cmd.ok( - jarvis.context.language == "ru" - and "Хорошо, сэр. Возвращаюсь к комментариям." - or "Very well, sir. Resuming remarks." -) diff --git a/resources/commands/birthdays/add.lua b/resources/commands/birthdays/add.lua deleted file mode 100644 index 5c17956..0000000 --- a/resources/commands/birthdays/add.lua +++ /dev/null @@ -1,80 +0,0 @@ --- Voice "запомни день рождения мама 15 марта" → memory["birthday.мама"]="15.03" --- We accept three date formats: "15.03", "15 марта", and "15 march". - -local lang = jarvis.context.language -local raw = (jarvis.context.phrase or ""):lower() - -local triggers = { - "запомни день рождения", "день рождения у", "добавь день рождения", - "запиши день рождения", - "remember birthday", "add birthday", "save birthday", -} -local rest = jarvis.text.strip_trigger(raw, triggers) or raw -rest = rest:gsub("^%s+", ""):gsub("%s+$", "") - --- Russian month names → "MM" -local months_ru = { - ["января"]=1, ["январь"]=1, - ["февраля"]=2, ["февраль"]=2, - ["марта"]=3, ["март"]=3, - ["апреля"]=4, ["апрель"]=4, - ["мая"]=5, ["май"]=5, - ["июня"]=6, ["июнь"]=6, - ["июля"]=7, ["июль"]=7, - ["августа"]=8, ["август"]=8, - ["сентября"]=9, ["сентябрь"]=9, - ["октября"]=10, ["октябрь"]=10, - ["ноября"]=11, ["ноябрь"]=11, - ["декабря"]=12, ["декабрь"]=12, -} -local months_en = { - january=1, february=2, march=3, april=4, may=5, june=6, - july=7, august=8, september=9, october=10, november=11, december=12, -} - --- Try patterns in order. Each pattern leaves `name` and `date` populated. -local name, date_str = nil, nil - --- 1. " DD.MM" -local n, d, m = rest:match("^(.-)%s+(%d?%d)%.(%d?%d)") -if n and d and m then - name = n - date_str = string.format("%02d.%02d", tonumber(d), tonumber(m)) -end - --- 2. " DD " -if not name then - local n2, d2, mword = rest:match("^(.-)%s+(%d?%d)%s+(%S+)") - if n2 and d2 and mword then - local m_num = months_ru[mword] or months_en[mword] - if m_num then - name = n2 - date_str = string.format("%02d.%02d", tonumber(d2), m_num) - end - end -end - -if not name or not date_str then - return jarvis.cmd.not_found(lang == "ru" - and "Скажи в формате 'имя 15 марта' или 'имя 15.03'." - or "Say in the form 'name 15 march' or 'name 15.03'.") -end - -name = name:gsub("^%s+", ""):gsub("%s+$", "") -if name == "" then - return jarvis.cmd.not_found(lang == "ru" and "Чьё именно?" or "Whose?") -end - --- Strip leading prepositions left from "у мамы" → "мамы" -name = name:gsub("^у%s+", ""):gsub("^of%s+", ""):gsub("^for%s+", "") - -local ok, err = pcall(function() - jarvis.memory.remember("birthday." .. name, date_str) -end) -if not ok then - return jarvis.cmd.error(tostring(err)) -end - -return jarvis.cmd.ok(string.format(lang == "ru" - and "Запомнил: %s — %s." - or "Got it: %s — %s.", name, date_str)) diff --git a/resources/commands/birthdays/command.toml b/resources/commands/birthdays/command.toml deleted file mode 100644 index 154ac2c..0000000 --- a/resources/commands/birthdays/command.toml +++ /dev/null @@ -1,50 +0,0 @@ -# Birthday tracker — voice-add and check upcoming birthdays. -# Storage uses `jarvis.memory.remember` with keys "birthday." so the -# data lives in the same long-term memory store and survives across packs. -# -# Voice flow: -# "запомни день рождения мама 15 марта" -# → memory key "birthday.мама" = "15.03" -# "ближайший день рождения" → reads all "birthday.*" keys + tells the next -# "день рождения мама" → reads back what we know about Mom - -[[commands]] -id = "birthdays.add" -type = "lua" -script = "add.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "запомни день рождения", - "день рождения у", - "добавь день рождения", - "запиши день рождения", -] -en = [ - "remember birthday", - "add birthday", - "save birthday", -] - - -[[commands]] -id = "birthdays.next" -type = "lua" -script = "next.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "ближайший день рождения", - "у кого скоро день рождения", - "кто следующий именинник", - "когда следующий день рождения", -] -en = [ - "next birthday", - "whose birthday is next", - "upcoming birthdays", -] diff --git a/resources/commands/birthdays/next.lua b/resources/commands/birthdays/next.lua deleted file mode 100644 index a061f4b..0000000 --- a/resources/commands/birthdays/next.lua +++ /dev/null @@ -1,55 +0,0 @@ --- Read all "birthday.*" memory keys, find the next upcoming date. - -local lang = jarvis.context.language -local now = jarvis.context.time - -local entries = {} -for _, f in ipairs(jarvis.memory.all() or {}) do - if f.key and f.key:sub(1, 9) == "birthday." then - local d, m = f.value:match("^(%d?%d)%.(%d?%d)") - if d and m then - table.insert(entries, { - name = f.key:sub(10), - day = tonumber(d), - month = tonumber(m), - }) - end - end -end - -if #entries == 0 then - return jarvis.cmd.not_found(lang == "ru" - and "Дней рождения в памяти нет, сэр." - or "No birthdays stored, sir.") -end - --- Compute "days until" for each. Wrap to next year if month/day already passed. -local function days_until(e) - local today_idx = now.month * 31 + now.day - local their_idx = e.month * 31 + e.day - local delta = their_idx - today_idx - if delta < 0 then - delta = delta + 12 * 31 -- approximate "wrap to next year" - end - return delta -end - -for _, e in ipairs(entries) do - e.delta = days_until(e) -end -table.sort(entries, function(a, b) return a.delta < b.delta end) - -local nxt = entries[1] -local human -if nxt.delta == 0 then - human = lang == "ru" and "сегодня" or "today" -elseif nxt.delta == 1 then - human = lang == "ru" and "завтра" or "tomorrow" -else - human = string.format(lang == "ru" and "через %d дней" or "in %d days", nxt.delta) -end - -return jarvis.cmd.ok(string.format(lang == "ru" - and "Ближайший — %s, %s (%02d.%02d)." - or "Next: %s, %s (%02d.%02d).", - nxt.name, human, nxt.day, nxt.month)) diff --git a/resources/commands/brightness/command.toml b/resources/commands/brightness/command.toml deleted file mode 100644 index 9981743..0000000 --- a/resources/commands/brightness/command.toml +++ /dev/null @@ -1,46 +0,0 @@ -[[commands]] -id = "brightness_up" -type = "lua" -script = "set.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = ["ярче", "сделай ярче", "увеличь яркость", "поярче"] -en = ["brighter", "increase brightness"] - - -[[commands]] -id = "brightness_down" -type = "lua" -script = "set.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = ["темнее", "сделай темнее", "уменьши яркость", "потемнее"] -en = ["darker", "decrease brightness"] - - -[[commands]] -id = "brightness_max" -type = "lua" -script = "set.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = ["максимальная яркость", "яркость на максимум"] -en = ["max brightness", "brightness max"] - - -[[commands]] -id = "brightness_min" -type = "lua" -script = "set.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = ["минимальная яркость", "яркость на минимум"] -en = ["min brightness", "brightness min"] diff --git a/resources/commands/brightness/set.lua b/resources/commands/brightness/set.lua deleted file mode 100644 index ee2088b..0000000 --- a/resources/commands/brightness/set.lua +++ /dev/null @@ -1,44 +0,0 @@ -local lang = jarvis.context.language -local cmd_id = jarvis.context.command_id - -local ps = [[ -$ErrorActionPreference = 'Stop' -try { - $cur = (Get-WmiObject -Namespace root\WMI -Class WmiMonitorBrightness -ErrorAction Stop).CurrentBrightness -} catch { - Write-Output 'no_wmi' - exit 0 -} -$target = switch ('__OP__') { - 'up' { [Math]::Min(100, $cur + 20) } - 'down' { [Math]::Max(0, $cur - 20) } - 'max' { 100 } - 'min' { 10 } - default { $cur } -} -$m = Get-WmiObject -Namespace root\WMI -Class WmiMonitorBrightnessMethods -$m.WmiSetBrightness(1, $target) | Out-Null -Write-Output $target -]] - -local op = ({ - brightness_up = "up", brightness_down = "down", - brightness_max = "max", brightness_min = "min", -})[cmd_id] or "up" - -local script = ps:gsub("__OP__", op) -local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', script:gsub('"', '\\"')) -local res = jarvis.system.exec(cmd) -local out = (res.stdout or ""):gsub("[\r\n]+$", ""):gsub("^%s+", "") - -if out == "no_wmi" or not res.success then - jarvis.system.notify("Brightness", - lang == "ru" and "Не поддерживается на этом дисплее" - or "Not supported on this display") - jarvis.audio.play_error() - return { chain = false } -end - -jarvis.system.notify(lang == "ru" and "Яркость" or "Brightness", out .. "%") -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/browser/command.toml.disabled b/resources/commands/browser/command.toml similarity index 100% rename from resources/commands/browser/command.toml.disabled rename to resources/commands/browser/command.toml diff --git a/resources/commands/clip_history/command.toml b/resources/commands/clip_history/command.toml deleted file mode 100644 index d622643..0000000 --- a/resources/commands/clip_history/command.toml +++ /dev/null @@ -1,52 +0,0 @@ -# Clipboard history — store last 20 copies, recall by index/keyword. - -[[commands]] -id = "cliphist.save" -type = "lua" -script = "save.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "запиши буфер", - "сохрани буфер", - "запомни буфер", - "запомни что в буфере", -] -en = ["save clipboard", "remember clipboard"] - - -[[commands]] -id = "cliphist.list" -type = "lua" -script = "list.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "что я копировал", - "история буфера", - "последние буферы", - "что было в буфере", -] -en = ["clipboard history", "what did I copy"] - - -[[commands]] -id = "cliphist.restore" -type = "lua" -script = "restore.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "верни первый буфер", - "верни второй буфер", - "верни третий буфер", - "восстанови буфер", - "вставь старый буфер", -] -en = ["restore clipboard", "paste old clipboard"] diff --git a/resources/commands/clip_history/list.lua b/resources/commands/clip_history/list.lua deleted file mode 100644 index 08f1c37..0000000 --- a/resources/commands/clip_history/list.lua +++ /dev/null @@ -1,21 +0,0 @@ --- Speak first 30 chars of each stored clip. -local count = 0 -local line = "В истории буфера: " -for i = 0, 19 do - local val = jarvis.memory.recall("clip." .. i) - if val then - count = count + 1 - if count <= 3 then - local preview = val:sub(1, 30):gsub("\r?\n", " ") - line = line .. (count == 1 and "" or " | ") .. preview - if count == 3 then break end - end - end -end - -if count == 0 then - return jarvis.cmd.ok("История буфера пуста.") -end - -line = line .. ". Всего " .. count .. " записей." -return jarvis.cmd.ok(line) diff --git a/resources/commands/clip_history/restore.lua b/resources/commands/clip_history/restore.lua deleted file mode 100644 index 52e5bef..0000000 --- a/resources/commands/clip_history/restore.lua +++ /dev/null @@ -1,21 +0,0 @@ --- "Верни первый/второй/третий буфер" — copy to clipboard. -local phrase = (jarvis.context.phrase or ""):lower() -local idx = 0 -- "первый" = 0 (most recent) -if phrase:find("втор") then idx = 1 -elseif phrase:find("трет") then idx = 2 -elseif phrase:find("четверт") then idx = 3 -elseif phrase:find("пят") then idx = 4 -end - --- Allow explicit number "вставь буфер 3" -local n = phrase:match("(%d+)") -if n then idx = tonumber(n) - 1 end -- 1-based → 0-based - -local content = jarvis.memory.recall("clip." .. idx) -if not content then - return jarvis.cmd.not_found("В истории нет такой записи.") -end - -jarvis.system.clipboard.set(content) -local preview = content:sub(1, 30):gsub("\r?\n", " ") -return jarvis.cmd.ok("Восстановил: " .. preview .. ".") diff --git a/resources/commands/clip_history/save.lua b/resources/commands/clip_history/save.lua deleted file mode 100644 index 2f1039b..0000000 --- a/resources/commands/clip_history/save.lua +++ /dev/null @@ -1,24 +0,0 @@ --- Take whatever's in the clipboard and push into history (memory keys clip.0..clip.19, rotated). -local content = jarvis.system.clipboard.get() -if not content or content == "" then - return jarvis.cmd.not_found("Буфер пуст.") -end - --- Shift existing entries: clip.18 → clip.19, clip.17 → clip.18, etc. -for i = 19, 1, -1 do - local prev = jarvis.memory.recall("clip." .. (i - 1)) - if prev then - jarvis.memory.remember("clip." .. i, prev) - end -end - --- Truncate if huge -local trimmed = content -if #trimmed > 2000 then - trimmed = trimmed:sub(1, 2000) .. "..." -end -jarvis.memory.remember("clip.0", trimmed) - --- Speak first 40 chars as confirmation -local preview = trimmed:sub(1, 40):gsub("\r?\n", " ") -return jarvis.cmd.ok("Запомнил: " .. preview .. ".") diff --git a/resources/commands/clipboard_read/command.toml b/resources/commands/clipboard_read/command.toml deleted file mode 100644 index 28bdb62..0000000 --- a/resources/commands/clipboard_read/command.toml +++ /dev/null @@ -1,10 +0,0 @@ -[[commands]] -id = "read_clipboard" -type = "lua" -script = "read.lua" -sandbox = "full" -timeout = 10000 - -[commands.phrases] -ru = ["прочитай буфер", "что в буфере", "прочитай из буфера", "озвучь буфер"] -en = ["read clipboard", "what is in clipboard", "speak clipboard"] diff --git a/resources/commands/clipboard_read/read.lua b/resources/commands/clipboard_read/read.lua deleted file mode 100644 index 6a28f6f..0000000 --- a/resources/commands/clipboard_read/read.lua +++ /dev/null @@ -1,26 +0,0 @@ -local lang = jarvis.context.language -local raw = jarvis.system.clipboard.get() or "" -local text = raw:gsub("^%s+", ""):gsub("%s+$", "") - -if text == "" then - jarvis.system.notify( - lang == "ru" and "Буфер" or "Clipboard", - lang == "ru" and "Пусто" or "Empty" - ) - jarvis.audio.play_not_found() - return { chain = false } -end - -local preview_chars = 400 -local to_speak = text -if #to_speak > preview_chars then - to_speak = to_speak:sub(1, preview_chars) .. (lang == "ru" and " ... и так далее" or " ... and so on") -end - -jarvis.system.notify( - lang == "ru" and "Буфер" or "Clipboard", - text:sub(1, 120) -) -jarvis.speak(to_speak, { lang = lang }) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/codebase_qa/ask.lua b/resources/commands/codebase_qa/ask.lua deleted file mode 100644 index e6bfaca..0000000 --- a/resources/commands/codebase_qa/ask.lua +++ /dev/null @@ -1,98 +0,0 @@ --- "что делает функция X" / "найди в коде Y" --- Reads a digest of source files in the configured codebase root, sends to LLM. -local root = jarvis.memory.recall("codebase.root") -if not root or root == "" then - return jarvis.cmd.not_found("Сначала укажите проект.") -end - -local phrase = (jarvis.context.phrase or "") -local question = jarvis.text.strip_trigger(phrase:lower(), { - "что делает функция", - "вопрос по коду", - "спроси код", - "найди в проекте", - "найди в коде", - "что в коде", - "объясни код", - "ask the codebase", - "ask the code", - "what does the function", - "explain the code", -}) -question = question:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") -if question == "" then - return jarvis.cmd.error("Сформулируйте вопрос.") -end - --- Build a digest: walk depth-2, pick source files by extension, cap per-file size + total size. -local EXT_OK = { - rs=true, lua=true, py=true, ts=true, tsx=true, js=true, jsx=true, svelte=true, - go=true, java=true, kt=true, c=true, h=true, cpp=true, hpp=true, cs=true, - rb=true, php=true, sh=true, ps1=true, sql=true, toml=true, yaml=true, yml=true, - json=true, md=true, -} -local SKIP_DIR = { - [".git"]=true, ["target"]=true, ["node_modules"]=true, ["dist"]=true, ["build"]=true, - ["__pycache__"]=true, [".venv"]=true, ["venv"]=true, [".idea"]=true, [".vscode"]=true, -} - -local MAX_FILE_BYTES = 4096 -- ~1k tokens per file -local MAX_TOTAL_BYTES = 50000 -- ~12k tokens total for digest -local MAX_FILES = 30 - -local function walk(dir, depth, acc) - if depth > 3 then return end - local entries = jarvis.fs.list(dir) - if not entries then return end - for _, ent in ipairs(entries) do - if #acc.files >= MAX_FILES or acc.bytes >= MAX_TOTAL_BYTES then - return - end - local name = ent.name - local full = dir .. "\\" .. name - if ent.is_dir then - if not SKIP_DIR[name] and not name:match("^%.") then - walk(full, depth + 1, acc) - end - else - local ext = name:match("%.([%w]+)$") - if ext and EXT_OK[ext:lower()] then - local content = jarvis.fs.read(full) - if content then - if #content > MAX_FILE_BYTES then - content = content:sub(1, MAX_FILE_BYTES) .. "\n... [truncated]" - end - -- relative path for readability - local rel = full:sub(#root + 2) - table.insert(acc.files, "--- " .. rel .. " ---\n" .. content) - acc.bytes = acc.bytes + #content - end - end - end - end -end - -local acc = { files = {}, bytes = 0 } -walk(root, 1, acc) - -if #acc.files == 0 then - return jarvis.cmd.not_found("Не нашёл исходников в проекте.") -end - -local digest = table.concat(acc.files, "\n\n") -local prompt = string.format( - "Ты — старший разработчик. По digest проекта ответь на вопрос пользователя кратко (3-5 предложений) на русском. Указывай файлы где уместно.\n\n=== ВОПРОС ===\n%s\n\n=== КОД ===\n%s", - question, digest -) - -local reply = jarvis.llm({ - { role = "system", content = "Ты — внимательный код-ревьюер. Отвечай по существу, без воды." }, - { role = "user", content = prompt } -}, { max_tokens = 400, temperature = 0.2 }) - -if not reply or reply == "" then - return jarvis.cmd.error("Не получилось получить ответ.") -end - -jarvis.system.notify("Codebase Q&A", reply:sub(1, 250)) -return jarvis.cmd.ok(reply) diff --git a/resources/commands/codebase_qa/command.toml b/resources/commands/codebase_qa/command.toml deleted file mode 100644 index 456f2d7..0000000 --- a/resources/commands/codebase_qa/command.toml +++ /dev/null @@ -1,58 +0,0 @@ -# Codebase Q&A — point J.A.R.V.I.S. at a folder, ask questions about the code. -# Active folder stored via jarvis.memory (key="codebase.root"). Voice commands let -# you swap the folder, list it, and ask questions. The script reads files lazily -# (depth-limited, size-capped) and feeds a digest to the LLM. - -[[commands]] -id = "codebase.set" -type = "lua" -script = "set.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "укажи проект", - "укажи папку проекта", - "укажи кодовую базу", - "выбери проект", - "проект сейчас", -] -en = ["set codebase", "set project folder", "use project"] - - -[[commands]] -id = "codebase.ask" -type = "lua" -script = "ask.lua" -sandbox = "full" -timeout = 60000 - -[commands.phrases] -ru = [ - "спроси код", - "вопрос по коду", - "что в коде", - "что делает функция", - "найди в коде", - "найди в проекте", - "объясни код", -] -en = ["ask the code", "ask the codebase", "what does the function", "explain the code"] - - -[[commands]] -id = "codebase.where" -type = "lua" -script = "where.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "какой проект сейчас", - "где проект", - "какая папка проекта", - "какая кодовая база", -] -en = ["which project", "what's the codebase"] diff --git a/resources/commands/codebase_qa/set.lua b/resources/commands/codebase_qa/set.lua deleted file mode 100644 index 82b7075..0000000 --- a/resources/commands/codebase_qa/set.lua +++ /dev/null @@ -1,29 +0,0 @@ --- "Укажи проект C:\Jarvis\rust" — stores path in memory under key "codebase.root" -local phrase = (jarvis.context.phrase or "") - -local body = jarvis.text.strip_trigger(phrase:lower(), { - "укажи папку проекта", - "укажи кодовую базу", - "укажи проект", - "выбери проект", - "проект сейчас", - "set codebase", - "set project folder", - "use project", -}) -body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - --- Path can have backslashes/spaces — preserve original casing by picking from raw phrase -local raw = phrase:sub(#phrase - #body + 1):gsub("^[%s,:%.]+", ""):gsub("%s+$", "") -local path = raw ~= "" and raw or body - -if path == "" then - return jarvis.cmd.error("Укажите путь к папке проекта.") -end - -if not jarvis.fs.is_dir(path) then - return jarvis.cmd.error("Папка не найдена: " .. path) -end - -jarvis.memory.remember("codebase.root", path) -return jarvis.cmd.ok("Проект установлен.") diff --git a/resources/commands/codebase_qa/where.lua b/resources/commands/codebase_qa/where.lua deleted file mode 100644 index 28b3376..0000000 --- a/resources/commands/codebase_qa/where.lua +++ /dev/null @@ -1,5 +0,0 @@ -local root = jarvis.memory.recall("codebase.root") -if not root or root == "" then - return jarvis.cmd.not_found("Проект не выбран. Скажите: укажи проект.") -end -return jarvis.cmd.ok("Сейчас работаю с проектом " .. root .. ".") diff --git a/resources/commands/codegen/codegen.lua b/resources/commands/codegen/codegen.lua deleted file mode 100644 index 36f2c5f..0000000 --- a/resources/commands/codegen/codegen.lua +++ /dev/null @@ -1,99 +0,0 @@ -local lang = jarvis.context.language -local phrase = (jarvis.context.phrase or ""):lower() - -local triggers = { - "сгенерируй скрипт", "напиши скрипт", "сделай скрипт", - "сгенерируй код", "набросай код", "напиши код", - "generate code", "write code", "make script", "write script", - "згенеруй код", "напиши код", "зроби скрипт", -} - -local request = phrase -for _, t in ipairs(triggers) do - local s, e = string.find(request, t, 1, true) - if s == 1 then - request = request:sub(e + 1) - break - end -end -request = request:gsub("^%s+", ""):gsub("%s+$", "") - -if request == "" then - jarvis.system.notify( - lang == "ru" and "Codegen" or "Codegen", - lang == "ru" and "Что писать?" or "What to write?" - ) - jarvis.audio.play_error() - return { chain = false } -end - -local token = jarvis.system.env("GROQ_TOKEN") -if not token or token == "" then - jarvis.log("error", "GROQ_TOKEN not set") - jarvis.system.notify("Codegen", "GROQ_TOKEN не задан") - jarvis.audio.play_error() - return { chain = false } -end - -local base = jarvis.system.env("GROQ_BASE_URL") -if not base or base == "" then base = "https://api.groq.com/openai/v1" end - -local model = jarvis.system.env("GROQ_MODEL") -if not model or model == "" then model = "llama-3.3-70b-versatile" end - -local system_prompt = lang == "ru" - and "Ты Senior Engineer. На запрос пользователя верни ТОЛЬКО исходный код, без обрамления ``` и без объяснений. Если язык не задан — выбери самый подходящий (по умолчанию Python). Не добавляй комментарии-болтовню." - or "You are a Senior Engineer. Reply with ONLY source code, no ``` fences, no commentary. Pick the most fitting language if unspecified (Python by default). Skip chatty comments." - -jarvis.log("info", "codegen request: " .. request) - -local payload = { - model = model, - messages = { - { role = "system", content = system_prompt }, - { role = "user", content = request }, - }, - max_tokens = 1024, - temperature = 0.2, -} - -local headers = { Authorization = "Bearer " .. token } -local res = jarvis.http.post_json(base .. "/chat/completions", payload, headers) - -if not res.ok then - jarvis.log("error", "codegen API failed: status=" .. tostring(res.status) .. " body=" .. tostring(res.body):sub(1, 200)) - jarvis.system.notify("Codegen", "Ошибка API: " .. tostring(res.status)) - jarvis.audio.play_error() - return { chain = false } -end - -local body = res.body or "" -local content = body:match('"content"%s*:%s*"(.-[^\\])"') -if not content then - jarvis.log("error", "codegen: could not parse content from " .. body:sub(1, 200)) - jarvis.system.notify("Codegen", "Не смог распарсить ответ") - jarvis.audio.play_error() - return { chain = false } -end - -content = content:gsub('\\n', '\n') -content = content:gsub('\\"', '"') -content = content:gsub('\\\\', '\\') -content = content:gsub('\\t', '\t') - -content = content:gsub("^```[%w]*\n?", "") -content = content:gsub("\n?```%s*$", "") -content = content:gsub("^%s+", ""):gsub("%s+$", "") - -jarvis.system.clipboard.set(content) -jarvis.log("info", "codegen result copied to clipboard (" .. #content .. " bytes)") - -local preview = content:sub(1, 120) -if #content > 120 then preview = preview .. "..." end -jarvis.system.notify( - lang == "ru" and "Код в буфере" or "Code in clipboard", - preview -) -jarvis.audio.play_ok() - -return { chain = false } diff --git a/resources/commands/codegen/command.toml b/resources/commands/codegen/command.toml deleted file mode 100644 index f296b54..0000000 --- a/resources/commands/codegen/command.toml +++ /dev/null @@ -1,22 +0,0 @@ -[[commands]] -id = "codegen_clipboard" -type = "lua" -script = "codegen.lua" -sandbox = "full" -timeout = 30000 - -[commands.phrases] -ru = [ - "напиши код", - "сгенерируй код", - "набросай код", - "сделай скрипт", - "напиши скрипт", - "сгенерируй скрипт", -] -en = [ - "write code", - "generate code", - "make script", - "write script", -] diff --git a/resources/commands/conversation/command.toml b/resources/commands/conversation/command.toml deleted file mode 100644 index 93c6f2c..0000000 --- a/resources/commands/conversation/command.toml +++ /dev/null @@ -1,45 +0,0 @@ -# Conversation tools — pull the persisted LLM history for review or replay. - -[[commands]] -id = "conversation.summary" -type = "lua" -script = "summary.lua" -sandbox = "full" -timeout = 20000 - -[commands.phrases] -ru = [ - "о чём мы говорили", - "о чем мы говорили", - "напомни о чем мы говорили", - "напомни про что мы говорили", - "что мы недавно обсуждали", - "сделай выжимку разговора", -] -en = [ - "what were we talking about", - "summarize our conversation", - "what did we discuss", - "recap our chat", -] - - -[[commands]] -id = "conversation.repeat" -type = "lua" -script = "repeat_last.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "повтори", - "повтори последнее", - "что ты сказал", - "не расслышал", -] -en = [ - "say that again", - "repeat", - "i didn't catch that", -] diff --git a/resources/commands/conversation/repeat_last.lua b/resources/commands/conversation/repeat_last.lua deleted file mode 100644 index c1870af..0000000 --- a/resources/commands/conversation/repeat_last.lua +++ /dev/null @@ -1,13 +0,0 @@ --- Repeat the last assistant message verbatim. Useful when the user didn't catch --- it (loud room, distraction, etc). - -local lang = jarvis.context.language -local last = jarvis.llm_last_reply() - -if not last or last == "" then - return jarvis.cmd.not_found(lang == "ru" - and "Я ещё ничего не говорил, сэр." - or "I haven't said anything yet, sir.") -end - -return jarvis.cmd.ok(last) diff --git a/resources/commands/conversation/summary.lua b/resources/commands/conversation/summary.lua deleted file mode 100644 index 1882895..0000000 --- a/resources/commands/conversation/summary.lua +++ /dev/null @@ -1,79 +0,0 @@ --- Summarise the last N turns of LLM conversation in 1-2 sentences. --- --- Reads the persistent history via `jarvis.llm_history()`, builds a short --- recap prompt, asks the LLM. If there's no history yet or the LLM is --- unreachable, falls back to a polite "ничего не вспомнить" reply. - -local lang = jarvis.context.language - -local turns = jarvis.llm_history() or {} --- Take only the last 12 user/assistant turns to keep prompt small. -local recent = {} -local start = math.max(1, #turns - 11) -for i = start, #turns do - if turns[i] then table.insert(recent, turns[i]) end -end - -if #recent == 0 then - return jarvis.cmd.not_found(lang == "ru" - and "Мы пока ещё ни о чём не говорили, сэр." - or "We haven't talked about anything yet, sir.") -end - --- Render history as a compact "Пользователь: ... / Я: ..." dialogue. -local lines = {} -for _, m in ipairs(recent) do - local who = m.role == "assistant" and (lang == "ru" and "Я" or "Me") - or (lang == "ru" and "Пользователь" or "User") - table.insert(lines, who .. ": " .. (m.content or "")) -end -local transcript = table.concat(lines, "\n") - -local token = jarvis.system.env("GROQ_TOKEN") -if not token or token == "" then - -- Offline path: just speak the user's last message back briefly. - return jarvis.cmd.ok(lang == "ru" - and ("Без сети, сэр. Последний раз вы спросили: " .. (recent[#recent].content or ""):sub(1, 200)) - or ("No network, sir. Last topic: " .. (recent[#recent].content or ""):sub(1, 200))) -end - -local base = jarvis.system.env("GROQ_BASE_URL") -if not base or base == "" then base = "https://api.groq.com/openai/v1" end -local model = jarvis.system.env("GROQ_MODEL") -if not model or model == "" then model = "llama-3.3-70b-versatile" end - -local sys -if lang == "ru" then - sys = "Ты — J.A.R.V.I.S. По ниже идущему диалогу с пользователем составь короткое " - .. "напоминание (1-3 предложения), о чём шла речь. Говори от первого лица в " - .. "стиле британского дворецкого. Не пересказывай дословно — сожми суть." -else - sys = "You are J.A.R.V.I.S. From the dialogue below, write a brief recap " - .. "(1-3 sentences) of what we talked about. Use first person, British " - .. "butler tone. Don't quote verbatim — distil the essence." -end - -local payload = { - model = model, - messages = { - { role = "system", content = sys }, - { role = "user", content = transcript }, - }, - max_tokens = 200, - temperature = 0.4, -} - -local res = jarvis.http.post_json(base .. "/chat/completions", payload, - { Authorization = "Bearer " .. token }) -if not res.ok then - return jarvis.cmd.error(lang == "ru" and "LLM не отвечает." or "LLM didn't respond.") -end - -local content = (res.body or ""):match('"content"%s*:%s*"(.-[^\\])"') -if not content then - return jarvis.cmd.error(lang == "ru" and "Не смог разобрать ответ." or "Couldn't parse reply.") -end -content = content:gsub('\\n', ' '):gsub('\\"', '"'):gsub('\\\\', '\\') -content = content:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "") - -return jarvis.cmd.ok(content) diff --git a/resources/commands/cooking/command.toml b/resources/commands/cooking/command.toml deleted file mode 100644 index af9d561..0000000 --- a/resources/commands/cooking/command.toml +++ /dev/null @@ -1,44 +0,0 @@ -# Cooking timer — recognises common dishes and starts a preset timer. -# "поставь чайник" → 4 min, "омлет" → 5 min, "макароны" → 10 min, etc. -# The phrase decides the duration via a built-in table in the lua script. - -[[commands]] -id = "cooking.timer" -type = "lua" -script = "timer.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "поставь чайник", - "поставил чайник", - "варю яйца", - "варю макароны", - "варю пасту", - "жарю омлет", - "жарю яичницу", - "варю кашу", - "варю рис", - "варю гречку", - "варю картошку", - "запекаю курицу", - "пеку пиццу", - "завариваю чай", - "завариваю кофе", - "завариваю френч-пресс", -] -en = [ - "boiling water", - "boiling eggs", - "boiling pasta", - "cooking pasta", - "frying omelette", - "frying eggs", - "cooking rice", - "cooking porridge", - "baking pizza", - "brewing tea", - "brewing coffee", - "french press", -] diff --git a/resources/commands/cooking/timer.lua b/resources/commands/cooking/timer.lua deleted file mode 100644 index eef3de7..0000000 --- a/resources/commands/cooking/timer.lua +++ /dev/null @@ -1,84 +0,0 @@ --- Cooking timer — recognise the dish in the phrase and schedule a "ready!" --- reminder for the right number of minutes. Falls through to a generic --- "couldn't recognise" reply if nothing matched. --- --- The duration table is opinionated but covers the common Russian kitchen --- staples. Power users can override via `recipes.txt` (TODO future) — for --- now, edit this file or use the generic `reminders/set` pack. - -local lang = jarvis.context.language -local phrase = (jarvis.context.phrase or ""):lower() - --- pattern -> { minutes, ru_label, en_label } -local recipes = { - { "чайник", 4, "чайник", "kettle" }, - { "boiling water",4, "вода", "boiling water" }, - { "французск", 4, "френч-пресс", "french press" }, - { "french press", 4, "френч-пресс", "french press" }, - { "чай", 5, "чай", "tea" }, - { "brewing tea", 5, "чай", "tea" }, - { "омлет", 5, "омлет", "omelette" }, - { "омлеt", 5, "омлет", "omelette" }, -- mis-STT guard - { "яичниц", 5, "яичница", "fried eggs" }, - { "frying eggs", 5, "яичница", "fried eggs" }, - { "frying omelet",5, "омлет", "omelette" }, - { "кофе", 5, "кофе", "coffee" }, - { "brewing coff", 5, "кофе", "coffee" }, - { "яйц", 8, "яйца", "eggs" }, - { "boiling eggs", 8, "яйца", "eggs" }, - { "макарон", 10, "макароны", "pasta" }, - { "паст", 10, "паста", "pasta" }, - { "cooking pasta",10, "паста", "pasta" }, - { "boiling pasta",10, "паста", "pasta" }, - { "рис", 18, "рис", "rice" }, - { "cooking rice", 18, "рис", "rice" }, - { "греч", 20, "гречка", "buckwheat" }, - { "каш", 20, "каша", "porridge" }, - { "porridge", 20, "каша", "porridge" }, - { "картош", 22, "картошка", "potatoes" }, - { "пицц", 15, "пицца", "pizza" }, - { "baking pizza", 15, "пицца", "pizza" }, - { "куриц", 35, "курица", "chicken" }, - { "chicken", 35, "курица", "chicken" }, -} - -local matched_minutes, label_ru, label_en = nil, nil, nil -for _, r in ipairs(recipes) do - if phrase:find(r[1], 1, true) then - matched_minutes, label_ru, label_en = r[2], r[3], r[4] - break - end -end - -if not matched_minutes then - return jarvis.cmd.not_found(lang == "ru" - and "Не понял, что ты готовишь, сэр." - or "I didn't catch what you're cooking, sir.") -end - -local label = (lang == "ru") and label_ru or label_en -local speak_text = (lang == "ru" and "Сэр, " or "Sir, ") - .. label .. (lang == "ru" and " готов." or " is ready.") - -local ok, err = pcall(function() - jarvis.scheduler.add({ - name = (lang == "ru" and "Кухня: " or "Cooking: ") .. label, - schedule = "in " .. tostring(matched_minutes) .. " minutes", - action = { type = "speak", text = speak_text }, - }) -end) -if not ok then - jarvis.log("error", "cooking timer: scheduler.add failed: " .. tostring(err)) - return jarvis.cmd.error(lang == "ru" and "Не получилось поставить таймер." - or "Couldn't set timer.") -end - -jarvis.system.notify( - lang == "ru" and "Таймер" or "Timer", - string.format("%s — %d мин", label, matched_minutes) -) - -return jarvis.cmd.ok(string.format(lang == "ru" - and "Хорошо, напомню через %d минут когда %s будет готов." - or "Right — I'll remind you in %d minutes when %s is ready.", - matched_minutes, label)) diff --git a/resources/commands/crypto/command.toml b/resources/commands/crypto/command.toml deleted file mode 100644 index a03086a..0000000 --- a/resources/commands/crypto/command.toml +++ /dev/null @@ -1,23 +0,0 @@ -[[commands]] -id = "crypto_price" -type = "lua" -script = "price.lua" -sandbox = "full" -timeout = 10000 - -[commands.phrases] -ru = [ - "сколько биткоин", - "цена биткоина", - "курс биткоина", - "цена эфира", - "сколько эфир", - "цена крипты", - "цена соланы", -] -en = [ - "bitcoin price", - "ethereum price", - "crypto price", - "btc price", -] diff --git a/resources/commands/crypto/price.lua b/resources/commands/crypto/price.lua deleted file mode 100644 index 97f33d5..0000000 --- a/resources/commands/crypto/price.lua +++ /dev/null @@ -1,51 +0,0 @@ -local phrase = (jarvis.context.phrase or ""):lower() - -local coin_id, label -if phrase:find("эфир") or phrase:find("eth") or phrase:find("ethereum") then - coin_id, label = "ethereum", "Эфир" -elseif phrase:find("солан") or phrase:find("sol") then - coin_id, label = "solana", "Солана" -elseif phrase:find("doge") or phrase:find("дог") then - coin_id, label = "dogecoin", "Догикоин" -elseif phrase:find("ton") or phrase:find("тон") then - coin_id, label = "the-open-network", "TON" -else - coin_id, label = "bitcoin", "Биткоин" -end - -local url = "https://api.coingecko.com/api/v3/simple/price?ids=" .. coin_id .. "&vs_currencies=usd,rub&include_24hr_change=true" -local res = jarvis.http.json(url) -if not res or type(res) ~= "table" or not res[coin_id] then - jarvis.system.notify("Crypto", "CoinGecko не ответил") - jarvis.audio.play_error() - return { chain = false } -end - -local data = res[coin_id] -local usd = data.usd -local rub = data.rub -local ch = data.usd_24h_change or 0 - -local title = string.format("%s: $%s | %s ₽", label, tostring(usd), tostring(math.floor(rub or 0))) -local detail = string.format("за 24 часа %+.2f%%", ch) -jarvis.system.notify(title, detail) -jarvis.log("info", title .. " | " .. detail) - -local say -if ch > 1 then - say = string.format("%s стоит %s долларов, за сутки вырос на %.1f процентов.", label, tostring(math.floor(usd)), ch) -elseif ch < -1 then - say = string.format("%s стоит %s долларов, за сутки упал на %.1f процентов.", label, tostring(math.floor(usd)), math.abs(ch)) -else - say = string.format("%s стоит %s долларов, без существенных изменений.", label, tostring(math.floor(usd))) -end - -local sapi = say:gsub("'", "''") -local ps = string.format( - [[Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; foreach ($v in $s.GetInstalledVoices()) { if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq 'ru') { try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} } } $s.Speak('%s')]], - sapi -) -jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'))) - -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/currency/command.toml b/resources/commands/currency/command.toml deleted file mode 100644 index 87effbb..0000000 --- a/resources/commands/currency/command.toml +++ /dev/null @@ -1,53 +0,0 @@ -[[commands]] -id = "currency_rate" -type = "lua" -script = "rate.lua" -sandbox = "full" -timeout = 10000 - -[commands.phrases] -ru = [ - "курс доллара", - "курс евро", - "сколько стоит доллар", - "сколько стоит евро", - "курс юаня", - "курс валют", - "что с долларом", - "что с евро", -] -en = [ - "dollar rate", - "euro rate", - "currency rate", - "what is dollar", -] - - -# Convert N units of one currency to another, e.g. -# "1000 долларов в рубли" → "1000 USD = 99523 ₽" -[[commands]] -id = "currency_convert" -type = "lua" -script = "convert.lua" -sandbox = "full" -timeout = 10000 - -[commands.phrases] -ru = [ - "сколько будет", - "переведи", - "конвертируй", - "долларов в рубли", - "евро в рубли", - "долларов в евро", - "юаней в рубли", - "рублей в доллары", - "рублей в евро", -] -en = [ - "convert", - "how much is", - "dollars in rubles", - "euros in rubles", -] diff --git a/resources/commands/currency/convert.lua b/resources/commands/currency/convert.lua deleted file mode 100644 index d60cf1c..0000000 --- a/resources/commands/currency/convert.lua +++ /dev/null @@ -1,121 +0,0 @@ --- "Сколько будет 1000 долларов в рублях" / "переведи 50 евро в доллары" --- Uses cbr-xml-daily.ru (CBR daily rates, JSON, no key, mirrors RU central bank). -local phrase = (jarvis.context.phrase or ""):lower() - --- Strip trigger words -local body = phrase:gsub("сколько будет", "") - :gsub("переведи", "") - :gsub("конвертируй", "") - :gsub("convert", "") - :gsub("how much is", "") - :gsub("^[%s,:%.]+", "") - :gsub("%s+$", "") - --- Extract amount (digit or russian "сто", "тысяча" — keep simple, digits only) -local amount_str = body:match("([%d%.,]+)") -if not amount_str then - jarvis.speak("Не понял сумму.") - jarvis.audio.play_error() - return { chain = false } -end -local amount = tonumber((amount_str:gsub(",", "."))) -if not amount then - jarvis.speak("Не понял сумму.") - jarvis.audio.play_error() - return { chain = false } -end - --- Detect source + target currencies via heuristic substring matches. -local function detect(text) - if text:find("доллар") or text:find("usd") or text:find("dollar") then return "USD" end - if text:find("евро") or text:find("eur") or text:find("euro") then return "EUR" end - if text:find("юан") or text:find("cny") or text:find("yuan") then return "CNY" end - if text:find("фунт") or text:find("gbp") or text:find("pound") then return "GBP" end - if text:find("иен") or text:find("jpy") or text:find("yen") then return "JPY" end - if text:find("рубл") or text:find("rub") or text:find("ruble") then return "RUB" end - return nil -end - --- Split on the conversion word "в"/"в"/"to"/"into" to detect from/to. -local from_part, to_part -local in_idx = body:find("%s+в%s+") or body:find("%s+to%s+") or body:find("%s+into%s+") -if in_idx then - from_part = body:sub(1, in_idx - 1) - to_part = body:sub(in_idx + 1) -else - -- Heuristic: only one currency named → assume target RUB. - from_part = body - to_part = "рубли" -end - -local from_ccy = detect(from_part) -local to_ccy = detect(to_part) or "RUB" -if not from_ccy then - jarvis.speak("Не понял исходную валюту.") - jarvis.audio.play_error() - return { chain = false } -end - --- Fetch CBR rates (RUB-denominated). -local body_json = jarvis.http.get("https://www.cbr-xml-daily.ru/daily_json.js") -if not body_json or body_json == "" then - jarvis.speak("Не получилось получить курсы.") - jarvis.audio.play_error() - return { chain = false } -end - --- We can't safely parse JSON without a Lua JSON lib. Use jarvis.http.json instead. -local data = jarvis.http.json("https://www.cbr-xml-daily.ru/daily_json.js") -if not data or not data.Valute then - jarvis.speak("Не получилось разобрать ответ.") - jarvis.audio.play_error() - return { chain = false } -end - --- Each Valute entry: { Value=rub_per_unit, Nominal=N }. Effective rub_per_unit = Value/Nominal. -local function rub_per(ccy) - if ccy == "RUB" then return 1.0 end - local v = data.Valute[ccy] - if not v then return nil end - return v.Value / v.Nominal -end - -local rub_from = rub_per(from_ccy) -local rub_to = rub_per(to_ccy) -if not rub_from or not rub_to then - jarvis.speak("Этой валюты нет в курсах ЦБ.") - jarvis.audio.play_error() - return { chain = false } -end - -local result = amount * rub_from / rub_to - --- Format pretty -local function fmt(n) - if n >= 1000 then - return string.format("%d", math.floor(n + 0.5)) - elseif n >= 10 then - return string.format("%.1f", n) - else - return string.format("%.2f", n) - end -end - -local function unit(ccy, n) - if ccy == "RUB" then return (n == 1 and "рубль" or (n < 5 and "рубля" or "рублей")) end - if ccy == "USD" then return (n == 1 and "доллар" or (n < 5 and "доллара" or "долларов")) end - if ccy == "EUR" then return "евро" end - if ccy == "CNY" then return (n == 1 and "юань" or (n < 5 and "юаня" or "юаней")) end - if ccy == "GBP" then return (n == 1 and "фунт" or (n < 5 and "фунта" or "фунтов")) end - if ccy == "JPY" then return "иен" end - return ccy -end - -local result_int = math.floor(result + 0.5) -local line = string.format("%s %s — это %s %s.", - fmt(amount), unit(from_ccy, amount), - fmt(result), unit(to_ccy, result_int)) - -jarvis.speak(line) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/currency/rate.lua b/resources/commands/currency/rate.lua deleted file mode 100644 index ea154b6..0000000 --- a/resources/commands/currency/rate.lua +++ /dev/null @@ -1,59 +0,0 @@ -local phrase = (jarvis.context.phrase or ""):lower() - -local target = "usd" -if phrase:find("евро") or phrase:find("eur") then target = "eur" -elseif phrase:find("юан") or phrase:find("cny") or phrase:find("yuan") then target = "cny" -elseif phrase:find("фунт") or phrase:find("gbp") then target = "gbp" -elseif phrase:find("йен") or phrase:find("jpy") then target = "jpy" -elseif phrase:find("тенге") or phrase:find("kzt") then target = "kzt" -end - -local res = jarvis.http.json("https://www.cbr-xml-daily.ru/daily_json.js") -if not res or type(res) ~= "table" or not res.Valute then - jarvis.system.notify("Курс", "Не получил данные ЦБ") - jarvis.audio.play_error() - return { chain = false } -end - -local labels = { - usd = "Доллар", eur = "Евро", cny = "Юань", - gbp = "Фунт", jpy = "Йена", kzt = "Тенге", -} -local code = target:upper() -local v = res.Valute[code] -if not v then - jarvis.system.notify("Курс", "Нет данных по " .. code) - jarvis.audio.play_not_found() - return { chain = false } -end - -local value = v.Value -local prev = v.Previous or value -local diff = value - prev -local arrow = diff > 0 and "▲" or (diff < 0 and "▼" or "=") -local nominal = v.Nominal or 1 - -local title = string.format("%s: %.2f ₽ %s", labels[target] or code, value / nominal, arrow) -local detail = string.format("вчера %.2f, изменение %+.2f", prev / nominal, diff / nominal) - -jarvis.system.notify(title, detail) -jarvis.log("info", title .. " | " .. detail) - -local say -if diff > 0 then - say = string.format("%s стоит %.2f рубля, поднялся на %.2f.", labels[target] or code, value / nominal, math.abs(diff) / nominal) -elseif diff < 0 then - say = string.format("%s стоит %.2f рубля, опустился на %.2f.", labels[target] or code, value / nominal, math.abs(diff) / nominal) -else - say = string.format("%s стоит %.2f рубля, без изменений.", labels[target] or code, value / nominal) -end - -local sapi = say:gsub("'", "''") -local ps = string.format( - [[Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; foreach ($v in $s.GetInstalledVoices()) { if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq 'ru') { try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} } } $s.Speak('%s')]], - sapi -) -jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'))) - -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/daily_briefing/command.toml b/resources/commands/daily_briefing/command.toml deleted file mode 100644 index ee90267..0000000 --- a/resources/commands/daily_briefing/command.toml +++ /dev/null @@ -1,51 +0,0 @@ -# Daily briefing — chains time + weather + memory + scheduler at a fixed time. - -[[commands]] -id = "daily_briefing.setup" -type = "lua" -script = "setup.lua" -sandbox = "standard" -timeout = 5000 - -[commands.phrases] -ru = [ - "настрой утренний брифинг", - "включи утренний брифинг", - "начни утренний брифинг каждый день", - "сделай утренний брифинг", -] -en = ["set up morning briefing", "enable morning briefing", "daily briefing"] - - -[[commands]] -id = "daily_briefing.now" -type = "lua" -script = "now.lua" -sandbox = "standard" -timeout = 15000 - -[commands.phrases] -ru = [ - "утренний брифинг", - "сделай брифинг", - "брифинг сейчас", - "что нового", - "сводка дня", -] -en = ["morning briefing", "give me a briefing", "what's new today"] - - -[[commands]] -id = "daily_briefing.off" -type = "lua" -script = "off.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "отключи утренний брифинг", - "выключи утренний брифинг", - "отмени брифинг", -] -en = ["disable morning briefing", "cancel morning briefing"] diff --git a/resources/commands/daily_briefing/now.lua b/resources/commands/daily_briefing/now.lua deleted file mode 100644 index 3fedb96..0000000 --- a/resources/commands/daily_briefing/now.lua +++ /dev/null @@ -1,64 +0,0 @@ --- The actual briefing body. Called either directly ("утренний брифинг") OR --- by the scheduler at the configured daily time. -local t = jarvis.context.time -local lang = jarvis.context.language or "ru" - --- 1) Greeting + time -local hour = t.hour -local greeting -if hour >= 5 and hour < 12 then greeting = "Доброе утро." -elseif hour >= 12 and hour < 18 then greeting = "Добрый день." -elseif hour >= 18 and hour < 23 then greeting = "Добрый вечер." -else greeting = "Сэр." end - -jarvis.speak(greeting .. string.format(" Сейчас %d:%02d.", hour, t.minute)) -jarvis.sleep(300) - --- 2) Profile-aware tone -local prof = jarvis.profile.active() -if prof and prof.name and prof.name ~= "default" then - jarvis.speak("Активный режим: " .. prof.name .. ".") - jarvis.sleep(200) -end - --- 3) Scheduled tasks ahead today -local tasks = jarvis.scheduler.list() -if tasks and #tasks > 0 then - local count = 0 - for _, task in ipairs(tasks) do - if task.action and task.action.type == "speak" then - count = count + 1 - end - end - if count > 0 then - jarvis.speak(string.format("Запланировано задач: %d.", count)) - jarvis.sleep(200) - end -end - --- 4) Memory recap (most recently used 3) -local memos = jarvis.memory.all() -if memos and #memos > 0 then - local sample = math.min(2, #memos) - local line = "Из памяти: " - for i = 1, sample do - line = line .. memos[i].key .. " — " .. memos[i].value - if i < sample then line = line .. "; " end - end - line = line .. "." - jarvis.speak(line) - jarvis.sleep(200) -end - --- 5) Closing nudge from LLM if available -local nudge = jarvis.llm({ - { role = "system", content = "Ты — Джарвис. Одна короткая фраза-мотиватор на день. По-русски. 5-10 слов." }, - { role = "user", content = string.format("Сейчас %d часов %02d минут, режим: %s.", hour, t.minute, prof and prof.name or "default") } -}, { max_tokens = 60, temperature = 0.8 }) - -if nudge and nudge ~= "" then - jarvis.speak(nudge) -end - -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/daily_briefing/off.lua b/resources/commands/daily_briefing/off.lua deleted file mode 100644 index c4e2c6e..0000000 --- a/resources/commands/daily_briefing/off.lua +++ /dev/null @@ -1,4 +0,0 @@ -if jarvis.scheduler.remove("daily_briefing_main") then - return jarvis.cmd.ok("Утренний брифинг отключен.") -end -return jarvis.cmd.not_found("Брифинг и так не был настроен.") diff --git a/resources/commands/daily_briefing/setup.lua b/resources/commands/daily_briefing/setup.lua deleted file mode 100644 index 4da1f92..0000000 --- a/resources/commands/daily_briefing/setup.lua +++ /dev/null @@ -1,38 +0,0 @@ --- "Настрой утренний брифинг на 9:00" → adds a daily scheduled task that runs now.lua -local phrase = (jarvis.context.phrase or ""):lower() - --- try to extract HH:MM, default 9:00 -local h, m = phrase:match("(%d%d?)[:%s%-](%d%d)") -if not h then - h = phrase:match("(%d%d?)") -end -local hh = tonumber(h) or 9 -local mm = tonumber(m) or 0 -if hh < 0 or hh > 23 or mm < 0 or mm > 59 then hh = 9; mm = 0 end - -local script_path = jarvis.context.command_path .. "/now.lua" -script_path = script_path:gsub("/", "\\") - -local id = "daily_briefing_main" - -local ok, err = pcall(function() - -- replace any prior briefing - jarvis.scheduler.remove(id) - jarvis.scheduler.add({ - id = id, - name = "Daily briefing", - schedule = string.format("daily %02d:%02d", hh, mm), - action = { type = "lua", script_path = script_path }, - }) -end) - -if not ok then - jarvis.log("warn", "daily_briefing.setup: " .. tostring(err)) - jarvis.speak("Не получилось настроить брифинг.") - jarvis.audio.play_error() - return { chain = false } -end - -jarvis.speak(string.format("Утренний брифинг настроен на %02d:%02d.", hh, mm)) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/daily_quote/command.toml b/resources/commands/daily_quote/command.toml deleted file mode 100644 index e3ac4d1..0000000 --- a/resources/commands/daily_quote/command.toml +++ /dev/null @@ -1,18 +0,0 @@ -# Daily quote — random motivational quote. - -[[commands]] -id = "daily_quote" -type = "lua" -script = "quote.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = [ - "цитата дня", - "мотивирующая цитата", - "скажи цитату", - "вдохнови меня", - "цитата", -] -en = ["quote of the day", "motivational quote", "inspire me"] diff --git a/resources/commands/daily_quote/quote.lua b/resources/commands/daily_quote/quote.lua deleted file mode 100644 index 8109352..0000000 --- a/resources/commands/daily_quote/quote.lua +++ /dev/null @@ -1,31 +0,0 @@ --- "Цитата дня" — random quote. --- Source: zenquotes.io (free, no key, returns JSON array) -local data = jarvis.http.json("https://zenquotes.io/api/random") -if not data or type(data) ~= "table" or #data == 0 then - -- Fallback to LLM if API fails - local llm_quote = jarvis.llm({ - { role = "system", content = "Выдай одну короткую мотивирующую цитату. По-русски. Без вступлений." }, - { role = "user", content = "Цитата дня." } - }, { max_tokens = 100, temperature = 0.9 }) - if llm_quote and llm_quote ~= "" then - return jarvis.cmd.ok(llm_quote) - end - return jarvis.cmd.error("Не получилось получить цитату.") -end - -local q = data[1] -local quote_en = q.q or "" -local author = q.a or "Аноним" -if quote_en == "" then - return jarvis.cmd.error("Пустая цитата.") -end - --- Translate to Russian via LLM (the API returns English). -local translated = jarvis.llm({ - { role = "system", content = "Переведи английскую цитату на русский. Без вступлений. Только перевод." }, - { role = "user", content = quote_en } -}, { max_tokens = 200, temperature = 0.2 }) - -local text = (translated and translated ~= "") and translated or quote_en - -return jarvis.cmd.ok(text .. " — " .. author .. ".") diff --git a/resources/commands/date_math/command.toml b/resources/commands/date_math/command.toml deleted file mode 100644 index 8e17daf..0000000 --- a/resources/commands/date_math/command.toml +++ /dev/null @@ -1,35 +0,0 @@ -# Date arithmetic — "сколько дней до нового года" / "сколько до зарплаты 15-го" - -[[commands]] -id = "date.days_until" -type = "lua" -script = "days_until.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "сколько дней до", - "сколько до нового года", - "сколько до", - "до нового года", - "когда", -] -en = ["how many days until", "days until"] - - -[[commands]] -id = "date.day_of_week" -type = "lua" -script = "day_of_week.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "какой сегодня день недели", - "какой день недели", - "какой сейчас день", - "сегодня какой", -] -en = ["what day is today", "day of week"] diff --git a/resources/commands/date_math/day_of_week.lua b/resources/commands/date_math/day_of_week.lua deleted file mode 100644 index be127ab..0000000 --- a/resources/commands/date_math/day_of_week.lua +++ /dev/null @@ -1,21 +0,0 @@ -local t = jarvis.context.time - --- Compute day-of-week using Zeller's congruence (0=Saturday). -local y, m, d = t.year, t.month, t.day -if m < 3 then m = m + 12; y = y - 1 end -local K = y % 100 -local J = math.floor(y / 100) -local h = (d + math.floor(13 * (m + 1) / 5) + K + math.floor(K / 4) + math.floor(J / 4) - 2 * J) % 7 --- h: 0=Saturday, 1=Sunday, 2=Monday, ... 6=Friday - -local days_ru = { - [0] = "суббота", - [1] = "воскресенье", - [2] = "понедельник", - [3] = "вторник", - [4] = "среда", - [5] = "четверг", - [6] = "пятница", -} - -return jarvis.cmd.ok("Сегодня " .. (days_ru[h] or "неизвестный день") .. ".") diff --git a/resources/commands/date_math/days_until.lua b/resources/commands/date_math/days_until.lua deleted file mode 100644 index 06cb88d..0000000 --- a/resources/commands/date_math/days_until.lua +++ /dev/null @@ -1,74 +0,0 @@ --- "Сколько дней до нового года" / "сколько до 15 марта" -local phrase = (jarvis.context.phrase or ""):lower() -local t = jarvis.context.time -local current_year = t.year - -local MONTHS = { - ["январ"]=1, ["феврал"]=2, ["март"]=3, ["апрел"]=4, ["май"]=5, ["мая"]=5, ["июн"]=6, - ["июл"]=7, ["август"]=8, ["сентябр"]=9, ["октябр"]=10, ["ноябр"]=11, ["декабр"]=12, -} - --- Special phrases -local target_y, target_m, target_d - -if phrase:find("нов") and phrase:find("год") then - target_y, target_m, target_d = current_year + 1, 1, 1 - if t.month == 1 and t.day == 1 then target_y = current_year end -elseif phrase:find("рожд") then - target_y, target_m, target_d = current_year, 1, 7 -- ru orthodox -elseif phrase:find("8 март") or phrase:find("восьмого март") then - target_y, target_m, target_d = current_year, 3, 8 -elseif phrase:find("9 ма") or phrase:find("девятое ма") then - target_y, target_m, target_d = current_year, 5, 9 -end - --- Generic "до N <месяц>" -if not target_y then - local d = tonumber(phrase:match("до%s+(%d+)")) - if d then - for stem, month in pairs(MONTHS) do - if phrase:find(stem) then - target_d, target_m, target_y = d, month, current_year - break - end - end - end -end - -if not target_y then - return jarvis.cmd.error("Не понял дату.") -end - --- If target this year already passed, jump to next year -local function days_since_epoch(y, m, d) - -- approximate: works for diffs within ~100 years - local total = y * 365 + math.floor(y / 4) - math.floor(y / 100) + math.floor(y / 400) - local cum = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 } - total = total + cum[m] + (d - 1) - -- leap-day adjustment - local leap = (y % 4 == 0 and y % 100 ~= 0) or (y % 400 == 0) - if leap and m > 2 then total = total + 1 end - return total -end - -local now = days_since_epoch(t.year, t.month, t.day) -local target = days_since_epoch(target_y, target_m, target_d) - -if target < now then - target_y = target_y + 1 - target = days_since_epoch(target_y, target_m, target_d) -end - -local diff = target - now -if diff == 0 then - return jarvis.cmd.ok("Сегодня.") -end - --- Russian grammar: 1 день, 2-4 дня, 5+ дней -local last = diff % 10 -local unit = "дней" -if last == 1 and diff % 100 ~= 11 then unit = "день" -elseif last >= 2 and last <= 4 and (diff % 100 < 10 or diff % 100 >= 20) then unit = "дня" -end - -return jarvis.cmd.ok(string.format("%d %s.", diff, unit)) diff --git a/resources/commands/date_query/answer.lua b/resources/commands/date_query/answer.lua deleted file mode 100644 index dabee1f..0000000 --- a/resources/commands/date_query/answer.lua +++ /dev/null @@ -1,37 +0,0 @@ -local lang = jarvis.context.language -local cmd_id = jarvis.context.command_id - -local offsets = { today = 0, tomorrow = 1, yesterday = -1 } -local offset = offsets[cmd_id] or 0 - -local ps = string.format( - [[$ru = [System.Globalization.CultureInfo]::GetCultureInfo('ru-RU'); $d = (Get-Date).AddDays(%d); $d.ToString('dddd, dd MMMM yyyy', $ru)]], - offset -) -local res = jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'))) -local text = (res.stdout or ""):gsub("[\r\n]+$", "") - -if text == "" then - jarvis.audio.play_error() - return { chain = false } -end - -local labels = { - today = lang == "ru" and "Сегодня" or "Today", - tomorrow = lang == "ru" and "Завтра" or "Tomorrow", - yesterday = lang == "ru" and "Вчера" or "Yesterday", -} -local prefix = labels[cmd_id] or "" - -jarvis.system.notify(prefix, text) - -local say = prefix .. ", " .. text -local sapi = say:gsub("'", "''") -local sps = string.format( - [[Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; foreach ($v in $s.GetInstalledVoices()) { if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq 'ru') { try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} } } $s.Speak('%s')]], - sapi -) -jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', sps:gsub('"', '\\"'))) - -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/date_query/command.toml b/resources/commands/date_query/command.toml deleted file mode 100644 index 64aaebc..0000000 --- a/resources/commands/date_query/command.toml +++ /dev/null @@ -1,34 +0,0 @@ -[[commands]] -id = "today" -type = "lua" -script = "answer.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["какой сегодня день", "какое сегодня число", "сегодня какое число", "что сегодня"] -en = ["what day is today", "what's today", "today date"] - - -[[commands]] -id = "tomorrow" -type = "lua" -script = "answer.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["какой день завтра", "что завтра", "завтра какое число", "какое число завтра"] -en = ["what day is tomorrow", "what's tomorrow", "tomorrow date"] - - -[[commands]] -id = "yesterday" -type = "lua" -script = "answer.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["какой был вчера день", "что было вчера", "вчера какое число было"] -en = ["what day was yesterday", "yesterday date"] diff --git a/resources/commands/ddg_answer/answer.lua b/resources/commands/ddg_answer/answer.lua deleted file mode 100644 index 1c44c8f..0000000 --- a/resources/commands/ddg_answer/answer.lua +++ /dev/null @@ -1,99 +0,0 @@ --- DuckDuckGo Instant Answer — short factual lookups WITHOUT an API key. --- --- Endpoint: https://api.duckduckgo.com/?q=&format=json&no_html=1 --- --- DDG returns a JSON object. We look at, in order: --- 1. `AbstractText` — Wikipedia-style summary, usually 1–3 sentences --- 2. `Answer` — direct answer for math / conversion / calc queries --- 3. `Definition` — dictionary definitions --- 4. `RelatedTopics[1].Text` — first related topic as last resort --- --- If nothing useful comes back, we open the regular DDG search page so the --- user can read it themselves. - -local lang = jarvis.context.language -local raw = (jarvis.context.phrase or ""):lower() - -local triggers = { - "что такое", "кто такой", "кто такая", - "расскажи про", "расскажи о", - "найди информацию о", "найди информацию про", - "поищи в интернете", "поищи в сети", - "погугли что такое", - "what is", "who is", "tell me about", "look up", "search the web for", -} -local query = jarvis.text.strip_trigger(raw, triggers) -if not query or query == "" then query = raw end -query = query:gsub("^%s+", ""):gsub("%s+$", "") - -if query == "" then - return jarvis.cmd.not_found(lang == "ru" and "Что искать?" or "What to look up?") -end - --- URL-encode (basic — DDG accepts both UTF-8 and percent-encoded) -local function url_encode(s) - return (s:gsub("([^%w%-_%.~])", function(c) - return string.format("%%%02X", string.byte(c)) - end)) -end - -local url = "https://api.duckduckgo.com/?q=" - .. url_encode(query) - .. "&format=json&no_html=1&skip_disambig=1" - -jarvis.log("info", "ddg query: " .. query) -local res = jarvis.http.get(url, { ["User-Agent"] = "Mozilla/5.0 J.A.R.V.I.S." }) - -if not res or not res.ok then - return jarvis.cmd.error(lang == "ru" and "DDG не отвечает." or "DDG didn't respond.") -end - -local body = res.body or "" - --- Helper: pull the first top-level JSON string field. Quick-and-dirty regex --- works because DDG never nests the fields we care about, AND because --- consequent `"` inside values are escaped as `\"`. -local function field(name) - local pat = '"' .. name .. '"%s*:%s*"((\\.[^"]*|[^"])*)"' - local v = body:match('"' .. name .. '"%s*:%s*"(.-)"') - if not v or v == "" then return nil end - -- unescape common JSON sequences - v = v:gsub('\\"', '"'):gsub('\\\\', '\\'):gsub('\\n', ' '):gsub('\\t', ' ') - -- trim, collapse whitespace - v = v:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "") - if v == "" then return nil end - return v -end - --- First related topic — array element is itself an object with "Text". -local function first_related() - -- Find start of "RelatedTopics" array - local start_idx = body:find('"RelatedTopics"%s*:%s*%[') - if not start_idx then return nil end - -- Look for the first Text inside that array - local v = body:sub(start_idx):match('"Text"%s*:%s*"(.-)"') - if not v or v == "" then return nil end - v = v:gsub('\\"', '"'):gsub('\\\\', '\\'):gsub('\\n', ' '):gsub('\\t', ' ') - v = v:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "") - -- Trim absurdly long blurbs to the first ~300 chars - if #v > 320 then v = v:sub(1, 300) .. "…" end - return v -end - -local answer = - field("AbstractText") - or field("Answer") - or field("Definition") - or first_related() - -if answer then - jarvis.log("info", "ddg answer found, " .. #answer .. " chars") - return jarvis.cmd.ok(answer) -end - --- Nothing useful — open the search page so the user can read raw results. -local fallback = "https://duckduckgo.com/?q=" .. url_encode(query) -jarvis.system.open(fallback) -return jarvis.cmd.ok(lang == "ru" - and ("Ничего конкретного не нашёл, открываю поиск по запросу: " .. query) - or ("Nothing direct found, opening search for: " .. query)) diff --git a/resources/commands/ddg_answer/command.toml b/resources/commands/ddg_answer/command.toml deleted file mode 100644 index 64e1b76..0000000 --- a/resources/commands/ddg_answer/command.toml +++ /dev/null @@ -1,31 +0,0 @@ -# DuckDuckGo Instant Answer — no API key, no rate limit issues. -# Returns short factual answers to questions like "что такое X", "кто такой Y", -# "когда родился Z". Falls back to opening the search page if no instant answer. - -[[commands]] -id = "ddg.answer" -type = "lua" -script = "answer.lua" -sandbox = "full" -timeout = 8000 - -[commands.phrases] -ru = [ - "что такое", - "кто такой", - "кто такая", - "расскажи про", - "расскажи о", - "найди информацию о", - "найди информацию про", - "поищи в интернете", - "поищи в сети", - "погугли что такое", -] -en = [ - "what is", - "who is", - "tell me about", - "look up", - "search the web for", -] diff --git a/resources/commands/diagnostics/command.toml b/resources/commands/diagnostics/command.toml deleted file mode 100644 index 1d499ca..0000000 --- a/resources/commands/diagnostics/command.toml +++ /dev/null @@ -1,20 +0,0 @@ -# Diagnostics — speak a summary of the active runtime state. Useful when -# something is misbehaving and you want to know which backends are active. - -[[commands]] -id = "diagnostics.health" -type = "lua" -script = "health.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "диагностика", - "состояние системы", - "статус", - "что у тебя сейчас", - "проверка системы", - "доложи о себе", -] -en = ["diagnostics", "status report", "health check"] diff --git a/resources/commands/diagnostics/health.lua b/resources/commands/diagnostics/health.lua deleted file mode 100644 index e38c360..0000000 --- a/resources/commands/diagnostics/health.lua +++ /dev/null @@ -1,11 +0,0 @@ -local h = jarvis.health() -local line = string.format( - "TTS: %s. LLM: %s. Профиль: %s. Памяти фактов: %d. Запланировано: %d. Язык: %s.", - h.tts_backend or "—", - h.llm_backend or "—", - h.active_profile or "—", - h.memory_facts or 0, - h.scheduled_tasks or 0, - h.language or "—" -) -return jarvis.cmd.ok(line) diff --git a/resources/commands/dice/command.toml b/resources/commands/dice/command.toml deleted file mode 100644 index e07c642..0000000 --- a/resources/commands/dice/command.toml +++ /dev/null @@ -1,34 +0,0 @@ -[[commands]] -id = "coin_flip" -type = "lua" -script = "roll.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["подбрось монетку", "брось монетку", "орёл или решка", "орел или решка"] -en = ["flip a coin", "toss a coin", "heads or tails"] - - -[[commands]] -id = "roll_dice" -type = "lua" -script = "roll.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["брось кубик", "подбрось кубик", "кинь кубик"] -en = ["roll a dice", "roll the dice"] - - -[[commands]] -id = "random_number" -type = "lua" -script = "roll.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["случайное число", "выбери случайное число", "рандомное число"] -en = ["random number", "pick a random number"] diff --git a/resources/commands/dice/roll.lua b/resources/commands/dice/roll.lua deleted file mode 100644 index 5d908a9..0000000 --- a/resources/commands/dice/roll.lua +++ /dev/null @@ -1,28 +0,0 @@ -local lang = jarvis.context.language -local cmd_id = jarvis.context.command_id - -math.randomseed(os.time() + math.floor(jarvis.context.time.second * 7919)) - -local title, text, say - -if cmd_id == "coin_flip" then - local r = math.random(2) - text = (r == 1) and (lang == "ru" and "Орёл" or "Heads") or (lang == "ru" and "Решка" or "Tails") - title = lang == "ru" and "Монетка" or "Coin" - say = text -elseif cmd_id == "roll_dice" then - local r = math.random(1, 6) - text = tostring(r) - title = lang == "ru" and "Кубик" or "Dice" - say = (lang == "ru" and "Выпало " or "Rolled ") .. r -else - local r = math.random(1, 100) - text = tostring(r) - title = lang == "ru" and "Случайное число" or "Random number" - say = (lang == "ru" and "Число " or "Number ") .. r -end - -jarvis.system.notify(title, text) -jarvis.speak(say, { lang = lang }) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/disk/command.toml b/resources/commands/disk/command.toml deleted file mode 100644 index 20f7608..0000000 --- a/resources/commands/disk/command.toml +++ /dev/null @@ -1,34 +0,0 @@ -# Disk space — free GB on system drive, or named drive. - -[[commands]] -id = "disk.free" -type = "lua" -script = "free.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "сколько свободного места", - "сколько места на диске", - "сколько свободно на диске", - "место на диске", - "свободное место", -] -en = ["disk space", "free space", "how much disk"] - - -[[commands]] -id = "disk.list" -type = "lua" -script = "list.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "какие у меня диски", - "список дисков", - "перечень дисков", -] -en = ["list drives", "what drives do I have"] diff --git a/resources/commands/disk/free.lua b/resources/commands/disk/free.lua deleted file mode 100644 index 62870f5..0000000 --- a/resources/commands/disk/free.lua +++ /dev/null @@ -1,35 +0,0 @@ --- "Сколько свободно на диске C" / "сколько места" -local phrase = (jarvis.context.phrase or ""):upper() -local letter = phrase:match("([A-Z])%s*ДИСК") or phrase:match("ДИСК%s*([A-Z])") or - phrase:match("DRIVE%s*([A-Z])") or "C" - -local ps = string.format( - "$d = Get-PSDrive %s -ErrorAction SilentlyContinue; " .. - "if ($d) { '{0}|{1}' -f " .. - "[math]::Round($d.Free/1GB,1), " .. - "[math]::Round(($d.Free + $d.Used)/1GB,0) } else { 'NONE' }", - letter -) -local res = jarvis.system.exec(string.format( - 'powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"') -)) - -if not res.success then - return jarvis.cmd.error("Не получилось.") -end - -local out = (res.stdout or ""):gsub("%s+", "") -if out == "NONE" or out == "" then - return jarvis.cmd.not_found("Диск " .. letter .. " не найден.") -end - -local free_gb, total_gb = out:match("([%d%.]+)|(%d+)") -if not free_gb then - return jarvis.cmd.error("Не понял ответ системы.") -end - -local pct = math.floor(tonumber(free_gb) / tonumber(total_gb) * 100 + 0.5) -return jarvis.cmd.ok(string.format( - "На диске %s свободно %s гигабайт из %s, это %d процентов.", - letter, free_gb, total_gb, pct -)) diff --git a/resources/commands/disk/list.lua b/resources/commands/disk/list.lua deleted file mode 100644 index dae3c6e..0000000 --- a/resources/commands/disk/list.lua +++ /dev/null @@ -1,30 +0,0 @@ -local ps = - "Get-PSDrive -PSProvider FileSystem | " .. - "Where-Object { $_.Used -ne $null -or $_.Free -ne $null } | " .. - "ForEach-Object { '{0}:{1}' -f $_.Name, [math]::Round($_.Free/1GB,0) } | " .. - "ForEach-Object { Write-Host -NoNewline ($_+' ') }" - -local res = jarvis.system.exec(string.format( - 'powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"') -)) - -if not res.success then - return jarvis.cmd.error("Не получилось.") -end - -local out = (res.stdout or ""):gsub("^%s+", ""):gsub("%s+$", "") -if out == "" then - return jarvis.cmd.not_found("Дисков не нашёл.") -end - --- "C:120 D:300 E:50" → "C 120 ГБ, D 300 ГБ, ..." -local parts = {} -for letter, gb in out:gmatch("([A-Z]):(%d+)") do - table.insert(parts, letter .. " " .. gb .. " гигабайт") -end - -if #parts == 0 then - return jarvis.cmd.error("Не понял ответ системы.") -end - -return jarvis.cmd.ok("Свободно: " .. table.concat(parts, ", ") .. ".") diff --git a/resources/commands/echo/command.toml b/resources/commands/echo/command.toml deleted file mode 100644 index a9f7cdc..0000000 --- a/resources/commands/echo/command.toml +++ /dev/null @@ -1,33 +0,0 @@ -# Echo debug pack — Jarvis repeats what you said. Useful for testing mic+TTS -# without involving wake-word or LLM. - -[[commands]] -id = "echo.repeat" -type = "lua" -script = "repeat.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "повтори за мной", - "скажи как я", - "эхо", - "проверка эхо", -] -en = ["repeat after me", "echo"] - - -[[commands]] -id = "echo.what_did_i_say" -type = "lua" -script = "what.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "что я сказал", - "что ты услышал", -] -en = ["what did i say", "what did you hear"] diff --git a/resources/commands/echo/repeat.lua b/resources/commands/echo/repeat.lua deleted file mode 100644 index 8e77071..0000000 --- a/resources/commands/echo/repeat.lua +++ /dev/null @@ -1,27 +0,0 @@ --- "Повтори за мной привет всем" -local phrase = (jarvis.context.phrase or "") -local body = jarvis.text.strip_trigger(phrase:lower(), { - "повтори за мной", - "проверка эхо", - "скажи как я", - "эхо", - "repeat after me", - "echo", - "повтори за мною", - "ехо", -}) -body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if body == "" then - return jarvis.cmd.error("Что повторить?") -end - --- Preserve original casing — strip_trigger lowercased, but we can grab from the --- raw phrase by finding the lowercase prefix and skipping past it. -local raw_lower = phrase:lower() -local start_idx = raw_lower:find(body, 1, true) -if start_idx then - body = phrase:sub(start_idx, start_idx + #body - 1) -end - -return jarvis.cmd.ok(body) diff --git a/resources/commands/echo/what.lua b/resources/commands/echo/what.lua deleted file mode 100644 index e9bd1f1..0000000 --- a/resources/commands/echo/what.lua +++ /dev/null @@ -1,6 +0,0 @@ --- "Что я сказал" — отдаёт обратно всё что услышал. -local heard = jarvis.context.phrase or "" -if heard == "" then - return jarvis.cmd.not_found("Я ничего не услышал.") -end -return jarvis.cmd.ok("Вы сказали: " .. heard) diff --git a/resources/commands/expenses/breakdown.lua b/resources/commands/expenses/breakdown.lua deleted file mode 100644 index ba8f698..0000000 --- a/resources/commands/expenses/breakdown.lua +++ /dev/null @@ -1,55 +0,0 @@ --- Per-category breakdown for the last 7 days, sorted by amount desc. - -local lang = jarvis.context.language -local raw_state = jarvis.state.get("entries") or "" -if raw_state == "" then - return jarvis.cmd.not_found(lang == "ru" - and "Нечего разбивать, сэр." - or "Nothing to break down, sir.") -end - -local now = jarvis.context.time -local now_ts = os.time({ - year = now.year, month = now.month, day = now.day, - hour = now.hour, min = now.minute, sec = now.second or 0, -}) -local cutoff = now_ts - 7 * 86400 - -local by_cat = {} -for chunk in raw_state:gmatch("([^|]+)") do - local ts, am, ca = chunk:match("^(%d+),([%-%d%.]+),(.+)$") - ts = tonumber(ts); am = tonumber(am) - if ts and am and ts >= cutoff then - by_cat[ca] = (by_cat[ca] or 0) + am - end -end - -local rows = {} -for ca, total in pairs(by_cat) do - table.insert(rows, { ca = ca, total = total }) -end -table.sort(rows, function(a, b) return a.total > b.total end) - -if #rows == 0 then - return jarvis.cmd.ok(lang == "ru" - and "На этой неделе тишина по тратам, сэр." - or "Quiet week on spending, sir.") -end - --- Top 5 categories spoken; rest collapses into "и другое" -local top = math.min(5, #rows) -local parts = {} -for i = 1, top do - table.insert(parts, string.format("%s %.0f", rows[i].ca, rows[i].total)) -end -local tail = "" -if #rows > top then - local rest_total = 0 - for i = top + 1, #rows do rest_total = rest_total + rows[i].total end - tail = string.format(lang == "ru" - and " и другое %.0f" or " and other %.0f", rest_total) -end - -return jarvis.cmd.ok(string.format(lang == "ru" - and "По категориям: %s%s." - or "By category: %s%s.", table.concat(parts, ", "), tail)) diff --git a/resources/commands/expenses/command.toml b/resources/commands/expenses/command.toml deleted file mode 100644 index 5dbc168..0000000 --- a/resources/commands/expenses/command.toml +++ /dev/null @@ -1,91 +0,0 @@ -# Personal-finance tracker — voice-log expenses by category, get totals. -# Storage is per-pack via `jarvis.state` (auto-namespaced JSON), so it doesn't -# bleed into other packs' data. -# -# Voice flow: -# "потратил 500 на еду" → log entry (amount=500, category="еда") -# "сколько я потратил сегодня" → speak today's total -# "сколько на этой неделе" → speak 7-day total -# "куда ушли деньги" → speak per-category breakdown for the week - -[[commands]] -id = "expenses.log" -type = "lua" -script = "log.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "потратил", - "записать трату", - "запиши трату", - "купил за", - "заплатил за", -] -en = [ - "spent", - "log expense", - "paid for", - "bought", -] - - -[[commands]] -id = "expenses.today" -type = "lua" -script = "today.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "сколько я потратил сегодня", - "траты сегодня", - "сколько ушло сегодня", -] -en = [ - "spending today", - "expenses today", - "how much spent today", -] - - -[[commands]] -id = "expenses.week" -type = "lua" -script = "week.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "сколько я потратил на этой неделе", - "траты на неделе", - "недельные траты", -] -en = [ - "spending this week", - "weekly expenses", -] - - -[[commands]] -id = "expenses.breakdown" -type = "lua" -script = "breakdown.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "куда ушли деньги", - "разбивка по тратам", - "на что я тратил", - "разбивка по категориям", -] -en = [ - "where did the money go", - "expense breakdown", - "spending by category", -] diff --git a/resources/commands/expenses/log.lua b/resources/commands/expenses/log.lua deleted file mode 100644 index 807b713..0000000 --- a/resources/commands/expenses/log.lua +++ /dev/null @@ -1,74 +0,0 @@ --- Voice "потратил 500 на еду" → append {amount, category, timestamp} to the --- pack's state JSON. --- --- Parsing the phrase: --- 1. Strip the trigger ("потратил" / "spent" / ...) --- 2. First number = amount --- 3. After the number: optional "на <слово>" → category (RU) or "for " --- / "on " (EN). Otherwise category defaults to "разное" / "misc". - -local lang = jarvis.context.language -local raw = (jarvis.context.phrase or ""):lower() - -local triggers = { - "потратил", "записать трату", "запиши трату", "купил за", "заплатил за", - "spent", "log expense", "paid for", "bought", -} -local rest = jarvis.text.strip_trigger(raw, triggers) or raw -rest = rest:gsub("^%s+", ""):gsub("%s+$", "") - --- Pull the first integer-like token. Allows comma decimals ("12,50" → 12.5). -local amount_str, after = rest:match("(%-?[%d]+[%.,]?[%d]*)%s*(.*)") -if not amount_str then - return jarvis.cmd.not_found(lang == "ru" - and "Не понял сумму, сэр." - or "I didn't catch the amount, sir.") -end -amount_str = amount_str:gsub(",", ".") -local amount = tonumber(amount_str) -if not amount or amount <= 0 then - return jarvis.cmd.not_found(lang == "ru" - and "Сумма не похожа на число." - or "That doesn't look like an amount.") -end - -local category = "разное" -local cat_match = (after or ""):match("на%s+(%S+)") or (after or ""):match("for%s+(%S+)") - or (after or ""):match("on%s+(%S+)") -if cat_match and cat_match ~= "" then - category = cat_match -end - --- Load existing log, append, save. -local raw_state = jarvis.state.get("entries") or "" -local entries = {} -if raw_state ~= "" then - -- Encoded as "ts1,amount1,cat1|ts2,amount2,cat2|..." - for chunk in raw_state:gmatch("([^|]+)") do - local ts, am, ca = chunk:match("^(%d+),([%-%d%.]+),(.+)$") - if ts and am and ca then - table.insert(entries, { ts = tonumber(ts), am = tonumber(am), ca = ca }) - end - end -end - -local now = jarvis.context.time --- chrono::Local timestamp is exposed via {year, month, day, hour, minute, second} --- but not as a unix epoch. Approximate via os.time({...}) — sandbox standard --- gives us os.time/date. -local ts = os.time({ - year = now.year, month = now.month, day = now.day, - hour = now.hour, min = now.minute, sec = now.second or 0, -}) -table.insert(entries, { ts = ts, am = amount, ca = category }) - -local out = {} -for _, e in ipairs(entries) do - table.insert(out, string.format("%d,%s,%s", e.ts, tostring(e.am), e.ca)) -end -jarvis.state.set("entries", table.concat(out, "|")) - -return jarvis.cmd.ok(string.format(lang == "ru" - and "Записал: %s на %s." - or "Logged: %s on %s.", - amount_str, category)) diff --git a/resources/commands/expenses/today.lua b/resources/commands/expenses/today.lua deleted file mode 100644 index 8269d22..0000000 --- a/resources/commands/expenses/today.lua +++ /dev/null @@ -1,36 +0,0 @@ --- Sum of today's expenses (local calendar day). - -local lang = jarvis.context.language -local raw_state = jarvis.state.get("entries") or "" - -if raw_state == "" then - return jarvis.cmd.not_found(lang == "ru" - and "Сегодня ничего не тратили, сэр." - or "No expenses logged today, sir.") -end - -local now = jarvis.context.time -local day_start = os.time({ - year = now.year, month = now.month, day = now.day, - hour = 0, min = 0, sec = 0, -}) -local day_end = day_start + 86400 - -local total = 0 -for chunk in raw_state:gmatch("([^|]+)") do - local ts, am = chunk:match("^(%d+),([%-%d%.]+),") - ts = tonumber(ts); am = tonumber(am) - if ts and am and ts >= day_start and ts < day_end then - total = total + am - end -end - -if total == 0 then - return jarvis.cmd.ok(lang == "ru" - and "Сегодня ничего не тратили, сэр." - or "Nothing spent today, sir.") -end - -return jarvis.cmd.ok(string.format(lang == "ru" - and "Сегодня потратили %.0f." - or "Today's spending: %.0f.", total)) diff --git a/resources/commands/expenses/week.lua b/resources/commands/expenses/week.lua deleted file mode 100644 index 25d6e41..0000000 --- a/resources/commands/expenses/week.lua +++ /dev/null @@ -1,37 +0,0 @@ --- Sum of the last 7 days' expenses (rolling window). - -local lang = jarvis.context.language -local raw_state = jarvis.state.get("entries") or "" -if raw_state == "" then - return jarvis.cmd.not_found(lang == "ru" - and "На этой неделе ничего не тратили." - or "Nothing logged this week.") -end - -local now = jarvis.context.time -local now_ts = os.time({ - year = now.year, month = now.month, day = now.day, - hour = now.hour, min = now.minute, sec = now.second or 0, -}) -local cutoff = now_ts - 7 * 86400 - -local total = 0 -local count = 0 -for chunk in raw_state:gmatch("([^|]+)") do - local ts, am = chunk:match("^(%d+),([%-%d%.]+),") - ts = tonumber(ts); am = tonumber(am) - if ts and am and ts >= cutoff then - total = total + am - count = count + 1 - end -end - -if total == 0 then - return jarvis.cmd.ok(lang == "ru" - and "На этой неделе тратами не похваляешься, сэр." - or "Nothing tracked this week, sir.") -end - -return jarvis.cmd.ok(string.format(lang == "ru" - and "За неделю %d трат на сумму %.0f." - or "This week: %d expenses totalling %.0f.", count, total)) diff --git a/resources/commands/file_search/command.toml b/resources/commands/file_search/command.toml deleted file mode 100644 index 02b3755..0000000 --- a/resources/commands/file_search/command.toml +++ /dev/null @@ -1,21 +0,0 @@ -[[commands]] -id = "file_search" -type = "lua" -script = "search.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = [ - "найди файл", - "поиск файла", - "ищи файл", - "где файл", - "найди документ", -] -en = [ - "find file", - "search file", - "where is file", - "locate file", -] diff --git a/resources/commands/file_search/search.lua b/resources/commands/file_search/search.lua deleted file mode 100644 index 40d9365..0000000 --- a/resources/commands/file_search/search.lua +++ /dev/null @@ -1,78 +0,0 @@ -local lang = jarvis.context.language -local phrase = (jarvis.context.phrase or ""):lower() - -local triggers = { - "найди документ", "найди файл", "поиск файла", "ищи файл", "где файл", - "find file", "search file", "where is file", "locate file", - "знайди файл", "пошук файлу", "де файл", -} - -local query = phrase -for _, t in ipairs(triggers) do - local start, finish = string.find(query, t, 1, true) - if start == 1 then - query = query:sub(finish + 1) - break - end -end -query = query:gsub("^%s+", ""):gsub("%s+$", "") - -if query == "" then - jarvis.log("warn", "file_search: empty query (phrase=" .. phrase .. ")") - jarvis.system.notify( - lang == "ru" and "Поиск файлов" or "File search", - lang == "ru" and "Что искать?" or "What to search for?" - ) - jarvis.audio.play_error() - return { chain = false } -end - -local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" -local roots = { - userprofile .. "\\Desktop", - userprofile .. "\\Documents", - userprofile .. "\\Downloads", -} - -local escaped = query:gsub("'", "''") -local roots_arg = "'" .. table.concat(roots, "','") .. "'" -local ps = string.format( - [[$ErrorActionPreference='SilentlyContinue'; $r=@(%s); $q='*%s*'; $hits=Get-ChildItem -Path $r -Filter $q -Recurse -Depth 3 -File | Select-Object -First 5; foreach($h in $hits){ Write-Output $h.FullName }]], - roots_arg, escaped -) - -local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', ps:gsub('"', '\\"')) -jarvis.log("info", "file_search query='" .. query .. "'") - -local res = jarvis.system.exec(cmd) - -if not res.success then - jarvis.log("error", "file_search exec failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() - return { chain = false } -end - -local stdout = res.stdout or "" -local lines = {} -for line in stdout:gmatch("[^\r\n]+") do - if line ~= "" then table.insert(lines, line) end -end - -if #lines == 0 then - jarvis.system.notify( - lang == "ru" and "Поиск файлов" or "File search", - (lang == "ru" and "Не найдено: " or "Not found: ") .. query - ) - jarvis.audio.play_not_found() -else - local first = lines[1] - local count = #lines - local title = lang == "ru" and "Найдено файлов: " or "Files found: " - jarvis.system.notify(title .. count, first) - - jarvis.log("info", "file_search opening: " .. first) - jarvis.system.exec(string.format('explorer.exe /select,"%s"', first)) - jarvis.audio.play_ok() -end - -return { chain = false } diff --git a/resources/commands/focus/command.toml b/resources/commands/focus/command.toml deleted file mode 100644 index 374afa0..0000000 --- a/resources/commands/focus/command.toml +++ /dev/null @@ -1,52 +0,0 @@ -# Focus mode — combines three things real Jarvis would do for a deep-work session: -# 1. switch active profile to "work" (mutes the noise commands) -# 2. enable Windows Focus Assist via PowerShell -# 3. schedule a break reminder for 50 minutes (with 10-min stretch reminder) -# All in one voice trigger. - -[[commands]] -id = "focus.start" -type = "lua" -script = "start.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "режим фокуса", - "включи фокус", - "сосредоточиться", - "не отвлекай", - "режим работы", - "хочу поработать", -] -en = [ - "focus mode", - "enable focus", - "do not disturb", - "deep work", - "let me focus", -] - - -[[commands]] -id = "focus.stop" -type = "lua" -script = "stop.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "выключи фокус", - "выключи режим фокуса", - "закончил работать", - "отключи не отвлекай", - "верни обычный режим", -] -en = [ - "exit focus", - "stop focus", - "disable focus", - "i'm done working", -] diff --git a/resources/commands/focus/start.lua b/resources/commands/focus/start.lua deleted file mode 100644 index 005d713..0000000 --- a/resources/commands/focus/start.lua +++ /dev/null @@ -1,65 +0,0 @@ --- Enter focus mode — three side-effects bundled into one voice trigger: --- 1. activate "work" profile (silences fun/games/banter commands) --- 2. enable Windows Focus Assist (Quiet Hours) via registry tweak --- 3. schedule a "стретчинг" reminder in 50 minutes via the persistent scheduler --- --- All three are independent — if one fails, the rest still happen. - -local lang = jarvis.context.language - -local report = {} - --- 1) Profile switch (best effort) -local ok1, err1 = pcall(function() jarvis.profile.set("work") end) -if ok1 then - table.insert(report, lang == "ru" and "профиль 'работа'" or "work profile") -else - jarvis.log("warn", "focus.start: profile.set failed: " .. tostring(err1)) -end - --- 2) Windows Focus Assist: writes the registry key Windows uses for --- "Priority only" mode. This is the same key Windows 11 toggles when you --- flip Focus on/off via the Action Center. -local ps = [[ -$key = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Notifications\Settings\Windows.SystemToast.QuietHours' -if (-not (Test-Path $key)) { New-Item -Path $key -Force | Out-Null } -Set-ItemProperty -Path $key -Name 'NOC_GLOBAL_SETTING_TOASTS_ENABLED' -Value 0 -Type DWord -Write-Output 'fa-on' -]] -local cmd = 'powershell -NoProfile -ExecutionPolicy Bypass -Command "& {' - .. ps:gsub('\r?\n', '; '):gsub('"', '\\"') - .. '}"' -local res = jarvis.system.exec(cmd) -if res.success and (res.stdout or ""):find("fa%-on") then - table.insert(report, lang == "ru" and "уведомления выключены" or "notifications muted") -else - jarvis.log("warn", "focus.start: focus assist registry tweak failed") -end - --- 3) Stretch reminder in 50 minutes (the canonical Pomodoro-XL value) -local stretch_text = lang == "ru" - and "Сэр, 50 минут истекли. Встаньте, разомнитесь, посмотрите вдаль." - or "Sir, 50 minutes are up. Stand, stretch, look away from the screen." - -local ok3, err3 = pcall(function() - jarvis.scheduler.add({ - name = lang == "ru" and "Стретч-пауза" or "Stretch break", - schedule = "in 50 minutes", - action = { type = "speak", text = stretch_text }, - }) -end) -if ok3 then - table.insert(report, lang == "ru" and "пауза через 50 мин" or "break in 50 min") -else - jarvis.log("warn", "focus.start: scheduler.add failed: " .. tostring(err3)) -end - -if #report == 0 then - return jarvis.cmd.error(lang == "ru" and "Ничего не получилось включить." - or "Couldn't enable anything.") -end - -local summary = table.concat(report, ", ") -return jarvis.cmd.ok(lang == "ru" - and ("Включил режим фокуса: " .. summary .. ". Удачной работы, сэр.") - or ("Focus mode enabled: " .. summary .. ". Good luck, sir.")) diff --git a/resources/commands/focus/stop.lua b/resources/commands/focus/stop.lua deleted file mode 100644 index a7663a0..0000000 --- a/resources/commands/focus/stop.lua +++ /dev/null @@ -1,50 +0,0 @@ --- Leave focus mode — undo the three side-effects of focus.start. - -local lang = jarvis.context.language -local report = {} - --- 1) Revert profile to default -local ok1 = pcall(function() jarvis.profile.set("default") end) -if ok1 then - table.insert(report, lang == "ru" and "обычный профиль" or "default profile") -end - --- 2) Re-enable notifications via the same registry key -local ps = [[ -$key = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Notifications\Settings\Windows.SystemToast.QuietHours' -if (Test-Path $key) { - Set-ItemProperty -Path $key -Name 'NOC_GLOBAL_SETTING_TOASTS_ENABLED' -Value 1 -Type DWord -} -Write-Output 'fa-off' -]] -local cmd = 'powershell -NoProfile -ExecutionPolicy Bypass -Command "& {' - .. ps:gsub('\r?\n', '; '):gsub('"', '\\"') - .. '}"' -local res = jarvis.system.exec(cmd) -if res.success then - table.insert(report, lang == "ru" and "уведомления включены" or "notifications restored") -end - --- 3) Cancel any pending stretch reminders we created — match by name --- (scheduler has no native "tags" so we use the human name we set in start.lua) -local cancelled = 0 -local target_names = { "Стретч-пауза", "Stretch break" } -for _, task in ipairs(jarvis.scheduler.list()) do - for _, name in ipairs(target_names) do - if task.name == name then - jarvis.scheduler.remove(task.id) - cancelled = cancelled + 1 - end - end -end -if cancelled > 0 then - table.insert(report, string.format(lang == "ru" - and "отменил %d напоминаний" - or "cancelled %d reminders", cancelled)) -end - -local summary = #report > 0 and table.concat(report, ", ") - or (lang == "ru" and "ничего не было активно" or "nothing was active") -return jarvis.cmd.ok(lang == "ru" - and ("Фокус выключен: " .. summary .. ".") - or ("Focus mode off: " .. summary .. ".")) diff --git a/resources/commands/fun/ask.lua b/resources/commands/fun/ask.lua deleted file mode 100644 index 9364270..0000000 --- a/resources/commands/fun/ask.lua +++ /dev/null @@ -1,39 +0,0 @@ -local lang = jarvis.context.language -local cmd_id = jarvis.context.command_id - -local prompts = { - joke = lang == "ru" - and "Расскажи одну короткую шутку на русском, 1-3 предложения. Без вступления и без 'вот шутка'. Просто шутка." - or "Tell one short joke in 1-3 sentences. No preamble.", - fact = lang == "ru" - and "Расскажи один интересный, неочевидный факт по любой теме. 1-3 предложения. Без 'вот факт'." - or "Tell one surprising fact in 1-3 sentences. No preamble.", - quote = lang == "ru" - and "Дай вдохновляющую цитату известного человека и автора. Без 'вот цитата'. Формат: <цитата> — <автор>." - or "Give one inspiring quote with author. Format: .", - compliment = lang == "ru" - and "Сделай искренний короткий комплимент пользователю, как британский дворецкий Джарвис. 1-2 предложения, обращение «сэр»." - or "Compliment the user briefly, in JARVIS-the-butler tone.", -} - -math.randomseed(os.time() + jarvis.context.time.second * 7919) - -local reply = jarvis.llm( - { - { role = "system", content = prompts[cmd_id] or prompts.joke }, - { role = "user", content = "seed " .. tostring(math.random(1, 9999)) }, - }, - { max_tokens = 220, temperature = 1.0 } -) - -if not reply or reply == "" then - jarvis.system.notify("Fun", lang == "ru" and "Не получилось" or "Failed") - jarvis.audio.play_error() - return { chain = false } -end - -local titles = { joke = "Анекдот", fact = "Факт", quote = "Цитата", compliment = "Комплимент" } -jarvis.system.notify(titles[cmd_id] or "Fun", reply:sub(1, 240)) -jarvis.speak(reply, { lang = lang }) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/fun/command.toml b/resources/commands/fun/command.toml deleted file mode 100644 index da904ed..0000000 --- a/resources/commands/fun/command.toml +++ /dev/null @@ -1,46 +0,0 @@ -[[commands]] -id = "joke" -type = "lua" -script = "ask.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = ["расскажи шутку", "пошути", "анекдот", "рассмеши меня"] -en = ["tell a joke", "make me laugh", "joke"] - - -[[commands]] -id = "fact" -type = "lua" -script = "ask.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = ["расскажи факт", "интересный факт", "поделись фактом"] -en = ["tell a fact", "interesting fact"] - - -[[commands]] -id = "quote" -type = "lua" -script = "ask.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = ["вдохнови меня", "цитата дня", "мотивируй", "процитируй кого-нибудь"] -en = ["inspire me", "quote of the day", "motivate me"] - - -[[commands]] -id = "compliment" -type = "lua" -script = "ask.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = ["сделай комплимент", "похвали меня"] -en = ["compliment me", "say something nice"] diff --git a/resources/commands/games/command.toml b/resources/commands/games/command.toml deleted file mode 100644 index 9a6854c..0000000 --- a/resources/commands/games/command.toml +++ /dev/null @@ -1,36 +0,0 @@ -[[commands]] -id = "launch_game" -type = "lua" -script = "launch.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "запусти игру", - "включи игру", - "поиграем", - "хочу поиграть", - "запусти доту", - "включи кс", - "включи фортнайт", - "включи майнкрафт", -] -en = [ - "launch game", - "play game", - "launch dota", - "launch cs", -] - - -[[commands]] -id = "list_games" -type = "lua" -script = "list.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["какие у меня игры", "список игр", "покажи игры"] -en = ["list games", "show games"] diff --git a/resources/commands/games/launch.lua b/resources/commands/games/launch.lua deleted file mode 100644 index 1a03a3b..0000000 --- a/resources/commands/games/launch.lua +++ /dev/null @@ -1,110 +0,0 @@ -local lang = jarvis.context.language -local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" -local config_path = userprofile .. "\\Documents\\jarvis-games.json" - -if not jarvis.fs.exists(config_path) then - local sample = [[[ - {"name": "dota", "aliases": ["доту", "доту 2", "dota 2"], "steam_appid": 570}, - {"name": "cs2", "aliases": ["кс", "контру", "контр страйк"], "steam_appid": 730}, - {"name": "elden ring", "aliases": ["элден", "элден ринг"], "steam_appid": 1245620}, - {"name": "witcher 3", "aliases": ["ведьмак", "ведьмака 3"], "steam_appid": 292030}, - {"name": "factorio", "aliases": ["факторио"], "steam_appid": 427520}, - {"name": "minecraft", "aliases": ["майнкрафт", "майн"], "path": "C:\\Program Files (x86)\\Minecraft Launcher\\MinecraftLauncher.exe"}, - {"name": "fortnite", "aliases": ["фортнайт"], "epic_uri": "com.epicgames.launcher://apps/Fortnite?action=launch&silent=true"} -] -]] - jarvis.fs.write(config_path, sample) - jarvis.system.notify( - lang == "ru" and "Игры" or "Games", - lang == "ru" and "Создан шаблон ~/Documents/jarvis-games.json — добавь свои игры и повтори" - or "Template at Documents/jarvis-games.json — edit and retry" - ) - jarvis.system.open(config_path) - jarvis.audio.play_not_found() - return { chain = false } -end - -local content = jarvis.fs.read(config_path) -local entries = {} -local cursor = 1 -while true do - local s, e, block = content:find("({[^{}]+})", cursor) - if not s then break end - cursor = e + 1 - local name = block:match('"name"%s*:%s*"([^"]+)"') - if name then - local entry = { name = name, name_lower = name:lower(), aliases = {} } - for alias in block:gmatch('"([^"]+)"') do - entry.aliases[#entry.aliases + 1] = alias:lower() - end - entry.steam_appid = tonumber(block:match('"steam_appid"%s*:%s*(%d+)')) - entry.path = block:match('"path"%s*:%s*"([^"]+)"') - entry.epic_uri = block:match('"epic_uri"%s*:%s*"([^"]+)"') - table.insert(entries, entry) - end -end - -if #entries == 0 then - jarvis.system.notify("Games", lang == "ru" and "Пусто в конфиге" or "Empty config") - jarvis.audio.play_not_found() - return { chain = false } -end - -local phrase = (jarvis.context.phrase or ""):lower() -local triggers = { - "запусти игру", "включи игру", "хочу поиграть", "поиграем", "запусти", "включи", - "launch game", "play game", "launch", - "запусти гру", "увімкни гру", -} -local query = phrase -for _, t in ipairs(triggers) do - local s, e = string.find(query, t, 1, true) - if s == 1 then - query = query:sub(e + 1) - break - end -end -query = query:gsub("^%s+", ""):gsub("%s+$", "") - -local function name_matches(e, q) - if e.name_lower == q then return true end - if string.find(e.name_lower, q, 1, true) then return true end - for _, a in ipairs(e.aliases) do - if a == q or string.find(q, a, 1, true) then return true end - end - return false -end - -local match -for _, e in ipairs(entries) do - if name_matches(e, query) then match = e; break end -end - -if not match then - jarvis.system.notify("Games", (lang == "ru" and "Не нашёл: " or "Not found: ") .. query) - jarvis.audio.play_not_found() - return { chain = false } -end - -local launched = false -if match.steam_appid then - jarvis.system.open("steam://rungameid/" .. tostring(match.steam_appid)) - launched = true -elseif match.epic_uri then - jarvis.system.open(match.epic_uri) - launched = true -elseif match.path and jarvis.fs.exists(match.path) then - jarvis.system.exec(string.format('start "" "%s"', match.path)) - launched = true -end - -if launched then - jarvis.system.notify(lang == "ru" and "Запускаю" or "Launching", match.name) - jarvis.speak((lang == "ru" and "Запускаю " or "Launching ") .. match.name .. ".", { lang = lang }) - jarvis.audio.play_ok() -else - jarvis.system.notify("Games", (lang == "ru" and "Не настроен запуск: " or "No launch info: ") .. match.name) - jarvis.audio.play_error() -end - -return { chain = false } diff --git a/resources/commands/games/list.lua b/resources/commands/games/list.lua deleted file mode 100644 index 37056dc..0000000 --- a/resources/commands/games/list.lua +++ /dev/null @@ -1,26 +0,0 @@ -local lang = jarvis.context.language -local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" -local config_path = userprofile .. "\\Documents\\jarvis-games.json" - -if not jarvis.fs.exists(config_path) then - jarvis.system.notify("Games", lang == "ru" - and "Конфиг не создан — скажи «запусти игру» один раз" or "No config yet") - jarvis.audio.play_not_found() - return { chain = false } -end - -local names = {} -for name in jarvis.fs.read(config_path):gmatch('"name"%s*:%s*"([^"]+)"') do - table.insert(names, name) -end -if #names == 0 then - jarvis.system.notify("Games", "Empty") - jarvis.audio.play_not_found() - return { chain = false } -end - -local body = table.concat(names, ", ") -jarvis.system.notify((lang == "ru" and "Игр: " or "Games: ") .. #names, body) -jarvis.speak((lang == "ru" and "В библиотеке: " or "Library: ") .. body, { lang = lang }) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/generators/coin.lua b/resources/commands/generators/coin.lua deleted file mode 100644 index 2115db2..0000000 --- a/resources/commands/generators/coin.lua +++ /dev/null @@ -1,6 +0,0 @@ -math.randomseed(os.time() + (jarvis.context.time.minute or 0) * 1000) -local r = math.random(1, 2) -if r == 1 then - return jarvis.cmd.ok("Орёл!") -end -return jarvis.cmd.ok("Решка!") diff --git a/resources/commands/generators/command.toml b/resources/commands/generators/command.toml deleted file mode 100644 index 6db2523..0000000 --- a/resources/commands/generators/command.toml +++ /dev/null @@ -1,51 +0,0 @@ -# Random generators — coin flip, password, secret token, color. - -[[commands]] -id = "gen.coin" -type = "lua" -script = "coin.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "подбрось монету", - "брось монетку", - "орёл или решка", - "монетка", -] -en = ["flip a coin", "coin flip", "heads or tails"] - - -[[commands]] -id = "gen.password" -type = "lua" -script = "password.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "сгенерируй пароль", - "придумай пароль", - "пароль 12 символов", - "сгенерируй надёжный пароль", -] -en = ["generate password", "make a password"] - - -[[commands]] -id = "gen.uuid" -type = "lua" -script = "uuid.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "сгенерируй uuid", - "сгенерируй юид", - "юид", - "уникальный идентификатор", -] -en = ["generate uuid", "uuid"] diff --git a/resources/commands/generators/password.lua b/resources/commands/generators/password.lua deleted file mode 100644 index 979b679..0000000 --- a/resources/commands/generators/password.lua +++ /dev/null @@ -1,22 +0,0 @@ --- "Сгенерируй пароль 16 символов" → secure password (alphanumeric + symbols) --- Copies result to clipboard, speaks length only (so it's not echoed). -local phrase = (jarvis.context.phrase or ""):lower() -local n = tonumber(phrase:match("(%d+)")) or 16 -if n < 6 then n = 6 end -if n > 64 then n = 64 end - -local pool = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*-_=+" -math.randomseed(os.time() + (jarvis.context.time.minute or 0) * 1000) - -local pw = {} -for _ = 1, n do - local idx = math.random(1, #pool) - table.insert(pw, pool:sub(idx, idx)) -end -local password = table.concat(pw) - -jarvis.system.clipboard.set(password) -jarvis.system.notify("Пароль (" .. n .. " символов)", "Скопирован в буфер обмена") - --- Speak length only, never the password itself. -return jarvis.cmd.ok(string.format("Пароль на %d символов в буфере.", n)) diff --git a/resources/commands/generators/uuid.lua b/resources/commands/generators/uuid.lua deleted file mode 100644 index 5610d4d..0000000 --- a/resources/commands/generators/uuid.lua +++ /dev/null @@ -1,27 +0,0 @@ --- Generate v4 UUID, copy to clipboard, speak short suffix. -math.randomseed(os.time() + (jarvis.context.time.minute or 0) * 1000) - -local function hex(n) - local s = {} - for _ = 1, n do - s[#s + 1] = string.format("%x", math.random(0, 15)) - end - return table.concat(s) -end - --- v4 UUID: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx where y is 8|9|a|b -local y_pool = { "8", "9", "a", "b" } -local uuid = string.format( - "%s-%s-4%s-%s%s-%s", - hex(8), hex(4), hex(3), - y_pool[math.random(1, 4)], hex(3), - hex(12) -) - -jarvis.system.clipboard.set(uuid) -jarvis.system.notify("UUID", uuid) - --- Speak last 4 hex chars so user knows it landed but doesn't have to listen --- to 32 characters. -local suffix = uuid:sub(-4) -return jarvis.cmd.ok("UUID в буфере, заканчивается на " .. suffix .. ".") diff --git a/resources/commands/github_issues/command.toml b/resources/commands/github_issues/command.toml deleted file mode 100644 index 3eca051..0000000 --- a/resources/commands/github_issues/command.toml +++ /dev/null @@ -1,35 +0,0 @@ -# GitHub issue search via gh CLI. - -[[commands]] -id = "github.list_issues" -type = "lua" -script = "list.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = [ - "какие issues", - "какие задачи", - "открытые issues", - "список issues", - "что в issues", - "тикеты", -] -en = ["list issues", "open issues", "what's in issues"] - - -[[commands]] -id = "github.my_issues" -type = "lua" -script = "mine.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = [ - "мои issues", - "мои задачи на гитхабе", - "что мне назначено", -] -en = ["my issues", "issues assigned to me"] diff --git a/resources/commands/github_issues/list.lua b/resources/commands/github_issues/list.lua deleted file mode 100644 index 5f57f6a..0000000 --- a/resources/commands/github_issues/list.lua +++ /dev/null @@ -1,34 +0,0 @@ -local repo = jarvis.memory.recall("github.repo") -if not repo or repo == "" then - return jarvis.cmd.not_found("Сначала укажите репозиторий: текущий репо owner/repo.") -end - -local res = jarvis.system.exec(string.format( - 'gh issue list --repo "%s" --state open --json number,title,labels --limit 10', - repo -)) -if not res or not res.success then - return jarvis.cmd.error("gh CLI не отвечает.") -end - -local stdout = res.stdout or "" -if stdout:gsub("%s", "") == "" or stdout:gsub("%s", "") == "[]" then - return jarvis.cmd.ok("Открытых issues нет.") -end - -local _, count = stdout:gsub('"number"%s*:', "") -if count == 0 then - return jarvis.cmd.ok("Открытых issues нет.") -end - -local titles = {} -for title in stdout:gmatch('"title"%s*:%s*"([^"]+)"') do - table.insert(titles, title) - if #titles >= 3 then break end -end - -local line = string.format("%d открытых issues. ", count) -if #titles > 0 then - line = line .. "Первые: " .. table.concat(titles, ". ") .. "." -end -return jarvis.cmd.ok(line) diff --git a/resources/commands/github_issues/mine.lua b/resources/commands/github_issues/mine.lua deleted file mode 100644 index 7c9478c..0000000 --- a/resources/commands/github_issues/mine.lua +++ /dev/null @@ -1,25 +0,0 @@ --- Issues assigned to the authenticated user across all their repos. -local res = jarvis.system.exec( - 'gh issue list --assignee @me --state open --json number,title,repository --limit 10' -) -if not res or not res.success then - return jarvis.cmd.error("gh CLI не отвечает.") -end - -local stdout = res.stdout or "" -local _, count = stdout:gsub('"number"%s*:', "") -if count == 0 then - return jarvis.cmd.ok("Тебе ничего не назначено.") -end - -local titles = {} -for title in stdout:gmatch('"title"%s*:%s*"([^"]+)"') do - table.insert(titles, title) - if #titles >= 3 then break end -end - -local line = string.format("На вас %d issues. ", count) -if #titles > 0 then - line = line .. "Первые: " .. table.concat(titles, ". ") .. "." -end -return jarvis.cmd.ok(line) diff --git a/resources/commands/github_pr/command.toml b/resources/commands/github_pr/command.toml deleted file mode 100644 index 4819584..0000000 --- a/resources/commands/github_pr/command.toml +++ /dev/null @@ -1,65 +0,0 @@ -# GitHub PR voice review — list open PRs and summarise the latest one via LLM. -# Requires `gh` CLI installed and authenticated (`gh auth login`). -# Repo can be set via voice ("текущий репо ") or auto-detected -# from cwd if jarvis-app is launched inside a git repo. - -[[commands]] -id = "github.list_prs" -type = "lua" -script = "list.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = [ - "какие пиары", - "какие пулл реквесты", - "что в пиарах", - "что в pr", - "открытые пиары", - "список пиаров", -] -en = [ - "list open prs", - "what's in the prs", - "open pull requests", -] - - -[[commands]] -id = "github.summarize_pr" -type = "lua" -script = "summarize.lua" -sandbox = "full" -timeout = 60000 - -[commands.phrases] -ru = [ - "разбери последний пиар", - "объясни последний пиар", - "обзор последнего пиара", - "что в последнем pr", - "разбор pr", -] -en = [ - "review the latest pr", - "explain the latest pr", - "summarize pr", -] - - -[[commands]] -id = "github.set_repo" -type = "lua" -script = "set_repo.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "текущий репо", - "установи репо", - "выбери репо", - "репозиторий", -] -en = ["set repo", "current repo"] diff --git a/resources/commands/github_pr/list.lua b/resources/commands/github_pr/list.lua deleted file mode 100644 index f9bb4fc..0000000 --- a/resources/commands/github_pr/list.lua +++ /dev/null @@ -1,39 +0,0 @@ -local repo = jarvis.memory.recall("github.repo") -if not repo or repo == "" then - return jarvis.cmd.not_found("Сначала укажите репозиторий: текущий репо owner/repo.") -end - -local result = jarvis.system.exec(string.format( - 'gh pr list --repo "%s" --state open --json number,title,author,createdAt --limit 10', - repo -)) -if not result or not result.success then - local err = (result and result.stderr) or "gh CLI не доступен" - jarvis.log("warn", "gh failed: " .. err) - return jarvis.cmd.error("gh CLI не отвечает. Установите gh и выполните gh auth login.") -end - -local stdout = result.stdout or "" -if stdout:gsub("%s", "") == "" or stdout:gsub("%s", "") == "[]" then - return jarvis.cmd.ok("Открытых пиаров нет.") -end - --- Parse JSON via LLM (mlua doesn't ship a JSON parser by default). --- Alternatively count number entries with a regex. -local _, count = stdout:gsub('"number"%s*:', "") -if count == 0 then - return jarvis.cmd.ok("Открытых пиаров нет.") -end - --- Get titles -local titles = {} -for title in stdout:gmatch('"title"%s*:%s*"([^"]+)"') do - table.insert(titles, title) - if #titles >= 3 then break end -end - -local line = string.format("%d открытых пиаров. ", count) -if #titles > 0 then - line = line .. "Первые: " .. table.concat(titles, ". ") .. "." -end -return jarvis.cmd.ok(line) diff --git a/resources/commands/github_pr/set_repo.lua b/resources/commands/github_pr/set_repo.lua deleted file mode 100644 index 1c3c8a7..0000000 --- a/resources/commands/github_pr/set_repo.lua +++ /dev/null @@ -1,27 +0,0 @@ --- "Текущий репо bossiara13/J.A.R.V.I.S-rust" -local phrase = (jarvis.context.phrase or "") -local repo = jarvis.text.strip_trigger(phrase:lower(), { - "установи репо", - "выбери репо", - "текущий репо", - "репозиторий", - "set repo", - "current repo", - "встанови репо", -}) -repo = repo:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - --- preserve original casing -if repo ~= "" then - local start_idx = phrase:lower():find(repo, 1, true) - if start_idx then - repo = phrase:sub(start_idx, start_idx + #repo - 1) - end -end - -if repo == "" or not repo:find("/") then - return jarvis.cmd.error("Скажите owner слеш repo, например bossiara13 слеш J A R V I S.") -end - -jarvis.memory.remember("github.repo", repo) -return jarvis.cmd.ok("Репозиторий " .. repo .. " установлен.") diff --git a/resources/commands/github_pr/summarize.lua b/resources/commands/github_pr/summarize.lua deleted file mode 100644 index f2b3be8..0000000 --- a/resources/commands/github_pr/summarize.lua +++ /dev/null @@ -1,62 +0,0 @@ -local repo = jarvis.memory.recall("github.repo") -if not repo or repo == "" then - return jarvis.cmd.not_found("Сначала укажите репозиторий: текущий репо owner/repo.") -end - --- get the most recent PR's number -local list = jarvis.system.exec(string.format( - 'gh pr list --repo "%s" --state open --json number --limit 1', - repo -)) -if not list or not list.success then - return jarvis.cmd.error("gh CLI не отвечает.") -end - -local number = (list.stdout or ""):match('"number"%s*:%s*(%d+)') -if not number then - return jarvis.cmd.ok("Открытых пиаров нет.") -end - --- fetch title + body + diff stat -local detail = jarvis.system.exec(string.format( - 'gh pr view %s --repo "%s" --json title,body,additions,deletions,changedFiles,author', - number, repo -)) -if not detail or not detail.success then - return jarvis.cmd.error("Не получилось получить PR.") -end - -local d = detail.stdout or "" -local title = d:match('"title"%s*:%s*"([^"]*)"') or "(no title)" -local body = d:match('"body"%s*:%s*"(.-)"') or "" -body = body:gsub("\\n", "\n"):gsub("\\r", " "):gsub('\\"', '"'):gsub("\\\\", "\\") -local additions = d:match('"additions"%s*:%s*(%d+)') or "?" -local deletions = d:match('"deletions"%s*:%s*(%d+)') or "?" -local files = d:match('"changedFiles"%s*:%s*(%d+)') or "?" -local author = d:match('"login"%s*:%s*"([^"]*)"') or "(unknown)" - --- Trim body for prompt -if #body > 3000 then body = body:sub(1, 3000) .. "\n... [truncated]" end - -local user_prompt = string.format([[ -Repo: %s -PR #%s: %s -Author: %s -Changed files: %s (+%s, -%s) -Description: -%s - -Кратко (3-5 предложений) на русском объясни: что меняет этот PR, какие риски, стоит ли мёржить. -]], repo, number, title, author, files, additions, deletions, body) - -local review = jarvis.llm({ - { role = "system", content = "Ты — старший разработчик, делаешь код-ревью. Отвечай по делу, без воды, без 'отличный PR' вступлений." }, - { role = "user", content = user_prompt } -}, { max_tokens = 400, temperature = 0.2 }) - -if not review or review == "" then - return jarvis.cmd.error("Не получилось проанализировать.") -end - -jarvis.system.notify(string.format("PR #%s — %s", number, title), review:sub(1, 250)) -return jarvis.cmd.ok(string.format("Пиар номер %s: %s", number, review)) diff --git a/resources/commands/habit_nudge/command.toml b/resources/commands/habit_nudge/command.toml deleted file mode 100644 index cfbd884..0000000 --- a/resources/commands/habit_nudge/command.toml +++ /dev/null @@ -1,65 +0,0 @@ -# Habit nudges — convenient wrappers for popular recurring reminders. - -[[commands]] -id = "habit.water" -type = "lua" -script = "water.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "напоминай мне пить воду", - "напоминай попить воду", - "включи напоминание про воду", -] -en = ["remind me to drink water", "water reminder"] - - -[[commands]] -id = "habit.stretch" -type = "lua" -script = "stretch.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "напоминай мне размяться", - "напоминай делать зарядку", - "включи напоминание про разминку", - "напоминай вставать", -] -en = ["remind me to stretch", "stretch reminder"] - - -[[commands]] -id = "habit.eyes" -type = "lua" -script = "eyes.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "напоминай отдыхать глазам", - "напоминай про глаза", - "20 20 20", -] -en = ["remind me to rest eyes", "eye reminder"] - - -[[commands]] -id = "habit.stop_all" -type = "lua" -script = "stop_all.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "отключи все привычки", - "хватит напоминать про привычки", - "выключи все напоминалки", -] -en = ["stop all habits", "disable habit reminders"] diff --git a/resources/commands/habit_nudge/eyes.lua b/resources/commands/habit_nudge/eyes.lua deleted file mode 100644 index 5b9d708..0000000 --- a/resources/commands/habit_nudge/eyes.lua +++ /dev/null @@ -1,18 +0,0 @@ --- 20-20-20 rule: every 20 min look 20ft away for 20s. -jarvis.scheduler.remove("habit_eyes") -local ok, err = pcall(function() - jarvis.scheduler.add({ - id = "habit_eyes", - name = "20-20-20 eye rest", - schedule = "every 20 minutes", - action = { type = "speak", text = "Отвлекитесь от экрана на двадцать секунд." } - }) -end) -if not ok then - jarvis.speak("Не получилось.") - jarvis.audio.play_error() - return { chain = false } -end -jarvis.speak("Включил правило двадцать двадцать двадцать.") -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/habit_nudge/stop_all.lua b/resources/commands/habit_nudge/stop_all.lua deleted file mode 100644 index b8a4565..0000000 --- a/resources/commands/habit_nudge/stop_all.lua +++ /dev/null @@ -1,11 +0,0 @@ -local removed = 0 -for _, id in ipairs({ "habit_water", "habit_stretch", "habit_eyes" }) do - if jarvis.scheduler.remove(id) then - removed = removed + 1 - end -end - -if removed > 0 then - return jarvis.cmd.ok("Отключил привычек: " .. removed .. ".") -end -return jarvis.cmd.not_found("Привычки и так не запущены.") diff --git a/resources/commands/habit_nudge/stretch.lua b/resources/commands/habit_nudge/stretch.lua deleted file mode 100644 index 5748409..0000000 --- a/resources/commands/habit_nudge/stretch.lua +++ /dev/null @@ -1,17 +0,0 @@ -jarvis.scheduler.remove("habit_stretch") -local ok, err = pcall(function() - jarvis.scheduler.add({ - id = "habit_stretch", - name = "Stretch break", - schedule = "every 50 minutes", - action = { type = "speak", text = "Время размяться. Встаньте, пройдитесь." } - }) -end) -if not ok then - jarvis.speak("Не получилось.") - jarvis.audio.play_error() - return { chain = false } -end -jarvis.speak("Каждые 50 минут буду напоминать размяться.") -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/habit_nudge/water.lua b/resources/commands/habit_nudge/water.lua deleted file mode 100644 index bc85058..0000000 --- a/resources/commands/habit_nudge/water.lua +++ /dev/null @@ -1,17 +0,0 @@ -jarvis.scheduler.remove("habit_water") -local ok, err = pcall(function() - jarvis.scheduler.add({ - id = "habit_water", - name = "Drink water", - schedule = "every 2 hours", - action = { type = "speak", text = "Попей воды." } - }) -end) -if not ok then - jarvis.speak("Не получилось.") - jarvis.audio.play_error() - return { chain = false } -end -jarvis.speak("Каждые 2 часа буду напоминать пить воду.") -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/habit_streaks/checkin.lua b/resources/commands/habit_streaks/checkin.lua deleted file mode 100644 index 799a87b..0000000 --- a/resources/commands/habit_streaks/checkin.lua +++ /dev/null @@ -1,24 +0,0 @@ --- Mark a habit done today. -local phrase = (jarvis.context.phrase or ""):lower() - -local habit = nil -if phrase:find("воды") or phrase:find("воду") then habit = "water" -elseif phrase:find("размял") or phrase:find("зарядк") then habit = "stretch" -elseif phrase:find("глаз") then habit = "eyes" -end - --- Fallback: pick first word after "выполнил"/"отметь" -if not habit then - habit = jarvis.text.strip_trigger(phrase, { - "отметь привычку", "выполнил привычку", "выполнил", "отметь", - "check in habit", - }):gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - if habit == "" then habit = "general" end -end - -local t = jarvis.context.time -local date_str = string.format("%04d-%02d-%02d", t.year, t.month, t.day) -local key = "habit_streak." .. habit .. "." .. date_str - -jarvis.memory.remember(key, "1") -return jarvis.cmd.ok("Отметил " .. habit .. " за сегодня.") diff --git a/resources/commands/habit_streaks/command.toml b/resources/commands/habit_streaks/command.toml deleted file mode 100644 index 6710ae5..0000000 --- a/resources/commands/habit_streaks/command.toml +++ /dev/null @@ -1,39 +0,0 @@ -# Habit streaks — track when habit_nudge tasks actually fire, view stats. -# Producers (habit_nudge schedule firings) should call jarvis.memory.remember -# "habit_streak.." = "1" on each firing. - -[[commands]] -id = "habits.streak" -type = "lua" -script = "streak.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "сколько дней подряд", - "мои привычки", - "статистика привычек", - "статистика по привычкам", - "сколько дней пью воду", - "трекинг привычек", -] -en = ["habit streaks", "my habits stats"] - - -[[commands]] -id = "habits.checkin" -type = "lua" -script = "checkin.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "отметь привычку", - "выполнил привычку", - "я попил воды", - "я размялся", - "я отдохнул глазам", -] -en = ["check in habit", "I did the habit"] diff --git a/resources/commands/habit_streaks/streak.lua b/resources/commands/habit_streaks/streak.lua deleted file mode 100644 index 5a2ac2a..0000000 --- a/resources/commands/habit_streaks/streak.lua +++ /dev/null @@ -1,66 +0,0 @@ --- Reads memory keys "habit_streak.." and computes streak per habit. -local t = jarvis.context.time - -local function date_offset(days_back) - -- Simple naive backwards calc (no calendar arithmetic — close enough for streaks) - local jd_today = t.year * 365 + math.floor(t.year / 4) - + ({0,31,59,90,120,151,181,212,243,273,304,334})[t.month] - + (t.day - 1) - local target_jd = jd_today - days_back - -- Convert back to YYYY-MM-DD (approximate) - local y = math.floor(target_jd / 365.25) - local doy = target_jd - math.floor(y * 365.25) - local month_starts = {0,31,59,90,120,151,181,212,243,273,304,334} - local mo = 12 - for i = 12, 1, -1 do - if doy >= month_starts[i] then mo = i; doy = doy - month_starts[i]; break end - end - return string.format("%04d-%02d-%02d", y, mo, doy + 1) -end - -local all = jarvis.memory.all() - --- Group keys by habit name -local by_habit = {} -for _, rec in ipairs(all) do - local habit, date = rec.key:match("^habit_streak%.([^%.]+)%.(.+)$") - if habit and date then - by_habit[habit] = by_habit[habit] or {} - by_habit[habit][date] = true - end -end - -if next(by_habit) == nil then - return jarvis.cmd.not_found("Привычек ещё не отмечено. Скажи 'выполнил привычку' после события.") -end - --- For each habit, count consecutive days back from today -local results = {} -for habit, dates in pairs(by_habit) do - local streak = 0 - for d = 0, 30 do - local key_date = date_offset(d) - if dates[key_date] then - streak = streak + 1 - else - break - end - end - table.insert(results, { habit = habit, streak = streak }) -end - -table.sort(results, function(a, b) return a.streak > b.streak end) - -local sample = math.min(#results, 3) -local lines = {} -for i = 1, sample do - local r = results[i] - if r.streak == 0 then - table.insert(lines, r.habit .. " прервана") - else - local d = r.streak == 1 and "день" or (r.streak < 5 and "дня" or "дней") - table.insert(lines, r.habit .. " " .. r.streak .. " " .. d) - end -end - -return jarvis.cmd.ok("Стрики: " .. table.concat(lines, ", ") .. ".") diff --git a/resources/commands/help/command.toml b/resources/commands/help/command.toml deleted file mode 100644 index 4645336..0000000 --- a/resources/commands/help/command.toml +++ /dev/null @@ -1,22 +0,0 @@ -[[commands]] -id = "help" -type = "lua" -script = "help.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "что ты умеешь", - "какие команды", - "список команд", - "помощь", - "помоги", - "что можешь", -] -en = [ - "what can you do", - "list commands", - "help", - "show commands", -] diff --git a/resources/commands/help/help.lua b/resources/commands/help/help.lua deleted file mode 100644 index 3bcac7c..0000000 --- a/resources/commands/help/help.lua +++ /dev/null @@ -1,75 +0,0 @@ -local lang = jarvis.context.language - -local commands_dir = jarvis.context.command_path:match("^(.*[/\\])[^/\\]+[/\\]?$") -if not commands_dir then - jarvis.audio.play_error() - return { chain = false } -end - -local entries = jarvis.fs.list(commands_dir) -local packs = {} -for _, e in ipairs(entries) do - if e.is_dir then table.insert(packs, e.name) end -end -table.sort(packs) - -local descriptions = { - apps = lang == "ru" and "браузер, блокнот, калькулятор, проводник, терминал, настройки, диспетчер задач, блокировка, скриншот" or "browser, notepad, calculator, explorer, terminal, settings, taskmgr, lock, screenshot", - volume = lang == "ru" and "громче, тише, mute, максимум" or "louder, quieter, mute, max", - media = lang == "ru" and "пауза, play, следующий/предыдущий трек, стоп" or "pause/play, next/prev track, stop", - file_search = lang == "ru" and "найди файл X" or "find file X", - output_device = lang == "ru" and "список устройств вывода, переключи" or "list/cycle audio out", - sysinfo = lang == "ru" and "время, заряд, CPU, RAM, диск, статус системы" or "time, battery, cpu, ram, disk", - window = lang == "ru" and "сверни/разверни/закрой окно, snap влево/вправо, рабочий стол" or "min/max/close window, snap, desktop", - clipboard_read = lang == "ru" and "прочитай буфер" or "read clipboard", - notes = lang == "ru" and "запиши/покажи заметки" or "write/show notes", - process_kill = lang == "ru" and "закрой программу X" or "kill process X", - websearch = lang == "ru" and "загугли / ютуб / вики / яндекс" or "search google/youtube/wiki/yandex", - translate = lang == "ru" and "переведи на X" or "translate to X", - math = lang == "ru" and "посчитай X" or "calculate X", - theme = lang == "ru" and "тёмная/светлая тема" or "dark/light theme", - projects = lang == "ru" and "открой/список проектов" or "open/list projects", - sound_panel = lang == "ru" and "панель звука" or "sound panel", - codegen = lang == "ru" and "напиши код X" or "write code X", - ocr = lang == "ru" and "прочитай экран" or "read screen", - reminders = lang == "ru" and "напомни через N минут X" or "remind me in N min X", - date_query = lang == "ru" and "что сегодня / завтра / вчера" or "today/tomorrow/yesterday", - voice_type = lang == "ru" and "напечатай X" or "type X", - dice = lang == "ru" and "монетка, кубик, случайное число" or "coin, dice, random", - stopwatch = lang == "ru" and "засеки время / стоп секундомер" or "start/stop stopwatch", - news = lang == "ru" and "что нового" or "what's new", - currency = lang == "ru" and "курс доллара / евро / юаня" or "USD/EUR/CNY rate", - crypto = lang == "ru" and "цена биткоина / эфира" or "BTC/ETH price", - fun = lang == "ru" and "шутка, факт, цитата, комплимент" or "joke, fact, quote, compliment", - wiki = lang == "ru" and "что такое / кто такой X" or "what/who is X", - power = lang == "ru" and "выключи / перезагрузи / усыпи / гибернация" or "shutdown / restart / sleep / hibernate", - weather = lang == "ru" and "какая погода" or "what's the weather", - counter = lang == "ru" and "счётчик" or "counter", - help = lang == "ru" and "что ты умеешь" or "what can you do", -} - -local lines = {} -for _, p in ipairs(packs) do - local desc = descriptions[p] or "" - if desc ~= "" then - table.insert(lines, "• " .. p .. ": " .. desc) - end -end - -local title = string.format(lang == "ru" and "Команды (%d пакетов)" or "Commands (%d packs)", #packs) -local body = table.concat(lines, "\n") -jarvis.system.clipboard.set(body) -jarvis.system.notify(title, body:sub(1, 800)) - -local say = lang == "ru" - and string.format("Загружено %d пакетов команд. Полный список скопирован в буфер.", #packs) - or string.format("Loaded %d command packs. Full list is on the clipboard.", #packs) -local sapi = say:gsub("'", "''") -local ps = string.format( - [[Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; foreach ($v in $s.GetInstalledVoices()) { if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq 'ru') { try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} } } $s.Speak('%s')]], - sapi -) -jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'))) - -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/ics_event/command.toml b/resources/commands/ics_event/command.toml deleted file mode 100644 index ebc479b..0000000 --- a/resources/commands/ics_event/command.toml +++ /dev/null @@ -1,18 +0,0 @@ -# Create calendar event via .ics file. Opens in default handler (Outlook/Mail). - -[[commands]] -id = "ics.create" -type = "lua" -script = "create.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "добавь встречу", - "создай событие", - "создай встречу", - "запиши встречу", - "новая встреча", -] -en = ["add meeting", "create event"] diff --git a/resources/commands/ics_event/create.lua b/resources/commands/ics_event/create.lua deleted file mode 100644 index 22c6181..0000000 --- a/resources/commands/ics_event/create.lua +++ /dev/null @@ -1,77 +0,0 @@ --- "Добавь встречу завтра в 15:00 обсудить проект" --- Heuristic parsing: extracts time + body, defaults to tomorrow. -local phrase = (jarvis.context.phrase or "") -local body = jarvis.text.strip_trigger(phrase:lower(), { - "добавь встречу", - "создай событие", - "создай встречу", - "запиши встречу", - "новая встреча", - "add meeting", - "create event", -}) -body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if body == "" then - return jarvis.cmd.error("Опишите встречу.") -end - --- Find time HH:MM or "в HH" -local h, m = body:match("(%d%d?)[:%-%s](%d%d)") -if not h then - h = body:match("в%s+(%d%d?)") - m = "00" -end -local hh = tonumber(h) or 9 -local mm = tonumber(m) or 0 - --- Date: "сегодня" / "завтра" / default to tomorrow -local today = jarvis.context.time -local year, month, day = today.year, today.month, today.day -if body:find("сегодня") then - -- leave as-is -elseif body:find("послезавтра") then - day = day + 2 -else - -- default tomorrow - day = day + 1 -end - --- ICS uses UTC if Z suffix or local with TZID. We'll use local naive. -local dt_start = string.format("%04d%02d%02dT%02d%02d00", year, month, day, hh, mm) -local dt_end = string.format("%04d%02d%02dT%02d%02d00", year, month, day, hh + 1, mm) -local now_stamp = string.format("%04d%02d%02dT%02d%02d00", - today.year, today.month, today.day, today.hour, today.minute) - --- Clean summary (strip the time tokens) -local summary = body:gsub("%d%d?[:%-%s]%d%d", ""):gsub("в%s+%d%d?", "") - :gsub("сегодня", ""):gsub("послезавтра", ""):gsub("завтра", "") - :gsub("^[%s,]+", ""):gsub("%s+$", "") -if summary == "" then summary = "Встреча" end - -local uid = string.format("jarvis-%s-%d", now_stamp, math.random(1000, 9999)) -local ics = string.format( -[[BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//J.A.R.V.I.S.//EN -BEGIN:VEVENT -UID:%s -DTSTAMP:%s -DTSTART:%s -DTEND:%s -SUMMARY:%s -END:VEVENT -END:VCALENDAR]], - uid, now_stamp, dt_start, dt_end, summary -) - -local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" -local dir = userprofile .. "\\Documents\\jarvis-events" -jarvis.fs.mkdir(dir) -local path = dir .. "\\event-" .. now_stamp .. ".ics" -jarvis.fs.write(path, ics) - --- Open via default handler (Outlook / Mail). -jarvis.system.open(path) - -return jarvis.cmd.ok(string.format("Встреча создана: %s в %02d:%02d.", summary, hh, mm)) diff --git a/resources/commands/interesting_fact/command.toml b/resources/commands/interesting_fact/command.toml deleted file mode 100644 index 05fa7c8..0000000 --- a/resources/commands/interesting_fact/command.toml +++ /dev/null @@ -1,36 +0,0 @@ -# Interesting fact about X via LLM. - -[[commands]] -id = "fact.about" -type = "lua" -script = "fact.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = [ - "расскажи интересный факт", - "интересный факт о", - "интересный факт про", - "удиви меня", - "интересный факт", - "расскажи факт", -] -en = ["tell me an interesting fact", "fun fact about", "surprise me"] - - -[[commands]] -id = "fact.history_today" -type = "lua" -script = "history_today.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = [ - "что было в этот день", - "что произошло в этот день", - "этот день в истории", - "что случилось", -] -en = ["this day in history", "what happened today"] diff --git a/resources/commands/interesting_fact/fact.lua b/resources/commands/interesting_fact/fact.lua deleted file mode 100644 index b923d57..0000000 --- a/resources/commands/interesting_fact/fact.lua +++ /dev/null @@ -1,35 +0,0 @@ --- "Интересный факт о космосе" / "удиви меня" -local phrase = (jarvis.context.phrase or ""):lower() -local topic = jarvis.text.strip_trigger(phrase, { - "расскажи интересный факт про", - "расскажи интересный факт о", - "интересный факт про", - "интересный факт о", - "расскажи интересный факт", - "интересный факт", - "расскажи факт", - "удиви меня", - "tell me an interesting fact", - "fun fact about", - "surprise me", - "цікавий факт", -}) -topic = topic:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -local user_prompt -if topic == "" then - user_prompt = "Расскажи один реально неочевидный научно-проверенный факт. На русском, 1-2 предложения. Без вступлений типа 'знали ли вы'." -else - user_prompt = "Расскажи один интересный факт про " .. topic .. ". На русском, 1-2 предложения. Без вступлений." -end - -local reply = jarvis.llm({ - { role = "system", content = "Ты любопытный собеседник. Говоришь короткими, цепляющими фактами без воды." }, - { role = "user", content = user_prompt } -}, { max_tokens = 200, temperature = 0.8 }) - -if not reply or reply == "" then - return jarvis.cmd.error("Сегодня нет интересных фактов.") -end - -return jarvis.cmd.ok(reply) diff --git a/resources/commands/interesting_fact/history_today.lua b/resources/commands/interesting_fact/history_today.lua deleted file mode 100644 index 22a1420..0000000 --- a/resources/commands/interesting_fact/history_today.lua +++ /dev/null @@ -1,48 +0,0 @@ --- "Что было в этот день в истории" — Wikipedia API + LLM сжатие. -local t = jarvis.context.time - --- Wikipedia's "On this day" Featured Content API: --- https://api.wikimedia.org/feed/v1/wikipedia/ru/onthisday/events/MM/DD -local url = string.format( - "https://api.wikimedia.org/feed/v1/wikipedia/ru/onthisday/events/%02d/%02d", - t.month, t.day -) - -local data = jarvis.http.json(url) -if not data or not data.events or #data.events == 0 then - -- fallback: EN - url = string.format( - "https://api.wikimedia.org/feed/v1/wikipedia/en/onthisday/events/%02d/%02d", - t.month, t.day - ) - data = jarvis.http.json(url) -end - -if not data or not data.events or #data.events == 0 then - return jarvis.cmd.not_found("На этот день в истории ничего интересного.") -end - --- Pick first 5 events to feed into LLM for summarisation -local sample = {} -for i = 1, math.min(5, #data.events) do - local ev = data.events[i] - local year = ev.year or "?" - local text = ev.text or "" - if text ~= "" then - table.insert(sample, year .. ": " .. text) - end -end - -local context = table.concat(sample, "\n") - -local reply = jarvis.llm({ - { role = "system", content = "Ты — историк. Из списка событий выбери 2-3 самых интересных для русскоязычного слушателя, расскажи коротко на русском. Без преамбул." }, - { role = "user", content = "Сегодня " .. t.day .. " число месяца " .. t.month .. ". События в этот день:\n\n" .. context } -}, { max_tokens = 350, temperature = 0.4 }) - -if not reply or reply == "" then - -- Fallback: just speak first event - return jarvis.cmd.ok(sample[1] or "Не нашёл интересного.") -end - -return jarvis.cmd.ok(reply) diff --git a/resources/commands/intro/about.lua b/resources/commands/intro/about.lua deleted file mode 100644 index 79f48e7..0000000 --- a/resources/commands/intro/about.lua +++ /dev/null @@ -1,20 +0,0 @@ --- Short bio. Falls back to LLM if available for variation, otherwise canned text. -local prof = jarvis.profile.active() -local prof_part = "" -if prof and prof.name and prof.name ~= "default" then - prof_part = " Сейчас работаю в режиме " .. prof.name .. "." -end - -local canned = "Я Джарвис, локальный голосовой ассистент на Rust." .. - " Работаю на вашем компьютере, могу использовать облачный или локальный мозг." .. - prof_part .. - " Спросите 'что ты умеешь' для обзора возможностей." - --- If LLM is configured, give a tiny variation. Otherwise use canned. -local llm_text = jarvis.llm({ - { role = "system", content = "Ты — Джарвис из вселенной Marvel, британский дворецкий Тони Старка. Одна короткая фраза (1-2 предложения) на русском, представь себя пользователю. Без слов 'я искусственный интеллект' и подобных." }, - { role = "user", content = "Расскажи о себе одной фразой." } -}, { max_tokens = 80, temperature = 0.8 }) - -local msg = (llm_text and llm_text ~= "") and llm_text or canned -return jarvis.cmd.ok(msg) diff --git a/resources/commands/intro/command.toml b/resources/commands/intro/command.toml deleted file mode 100644 index 2b7fb66..0000000 --- a/resources/commands/intro/command.toml +++ /dev/null @@ -1,57 +0,0 @@ -# Voice-driven intro / capability discovery. -# Speaks a category-grouped overview of what Jarvis can do, scoped to the -# currently active profile (if the profile has allow/deny lists). - -[[commands]] -id = "intro.capabilities" -type = "lua" -script = "what_can_you_do.lua" -sandbox = "minimal" -timeout = 5000 - -[commands.phrases] -ru = [ - "что ты умеешь", - "что ты можешь", - "какие у тебя возможности", - "что я могу попросить", - "помощь", - "помоги", - "как тобой пользоваться", - "научи меня", -] -en = ["what can you do", "help", "capabilities", "how do I use you"] - - -[[commands]] -id = "intro.about" -type = "lua" -script = "about.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "расскажи о себе", - "кто ты такой", - "что ты за ассистент", - "что ты вообще", - "о тебе", -] -en = ["who are you", "tell me about yourself", "about you"] - - -[[commands]] -id = "intro.commands_count" -type = "lua" -script = "count.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "сколько команд знаешь", - "сколько у тебя команд", - "сколько ты умеешь команд", -] -en = ["how many commands", "command count"] diff --git a/resources/commands/intro/count.lua b/resources/commands/intro/count.lua deleted file mode 100644 index 788c890..0000000 --- a/resources/commands/intro/count.lua +++ /dev/null @@ -1,10 +0,0 @@ --- "Сколько команд знаешь" — выводит health snapshot для подтверждения готовности. -local h = jarvis.health() -local memory_count = h.memory_facts or 0 -local sched_count = h.scheduled_tasks or 0 - -local msg = "Активных бекендов: TTS " .. (h.tts_backend or "—") .. - ", LLM " .. (h.llm_backend or "—") .. - ". В памяти " .. memory_count .. " фактов, в расписании " .. sched_count .. " задач." - -return jarvis.cmd.ok(msg) diff --git a/resources/commands/intro/what_can_you_do.lua b/resources/commands/intro/what_can_you_do.lua deleted file mode 100644 index 671b890..0000000 --- a/resources/commands/intro/what_can_you_do.lua +++ /dev/null @@ -1,20 +0,0 @@ --- "Что ты умеешь" → говорит обзор возможностей по категориям. --- Категории захардкожены — переводы команд в реестре делать дорого, --- проще держать рукописный summary. - -local lines = { - "Я могу управлять компьютером голосом.", - "По темам: звук и медиа, окна и приложения, поиск файлов, заметки и буфер.", - "Также: переводы, конвертация валют, котировки акций, википедия и поиск в гугле.", - "Умею ставить напоминания, помодоро и привычки.", - "Запоминаю факты о вас, имею профили работы и игр, могу записывать макросы команд.", - "Если не понимаю команду, спрашиваю облачный или локальный мозг. Спросите 'какой у тебя мозг'.", - "Скажите 'что в расписании' чтобы посмотреть задачи, или откройте окно для полного списка.", -} - -for _, line in ipairs(lines) do - jarvis.speak(line) - jarvis.sleep(120) -end - -return jarvis.cmd.silent_ok() diff --git a/resources/commands/leftoff/command.toml b/resources/commands/leftoff/command.toml deleted file mode 100644 index 9174934..0000000 --- a/resources/commands/leftoff/command.toml +++ /dev/null @@ -1,27 +0,0 @@ -# "Where did I leave off" — pull recent context from across the assistant's -# state (last conversation, latest memory facts, last tracker session, latest -# scheduled task) and read out a one-paragraph recap. Nice for picking up after -# a coffee break or returning to the PC. - -[[commands]] -id = "leftoff.recap" -type = "lua" -script = "recap.lua" -sandbox = "full" -timeout = 8000 - -[commands.phrases] -ru = [ - "на чём я остановился", - "на чем я остановился", - "напомни что делал", - "что я делал", - "где я остановился", - "продолжи откуда я", -] -en = [ - "where did i leave off", - "what was i doing", - "recap my session", - "remind me what i was doing", -] diff --git a/resources/commands/leftoff/recap.lua b/resources/commands/leftoff/recap.lua deleted file mode 100644 index acac5d4..0000000 --- a/resources/commands/leftoff/recap.lua +++ /dev/null @@ -1,62 +0,0 @@ --- Read out a short recap of the user's recent activity. Pulls from four --- independent sources, picks the freshest, and stitches them into a one-shot --- TTS reply. Works fully offline — no LLM call. --- --- Sources: --- 1. Last assistant reply (jarvis.llm_last_reply) --- 2. 3 most recent memory facts (sorted by last_used_at desc) --- 3. Active scheduled tasks (jarvis.scheduler.list) --- 4. Active profile (just to colour the tone) - -local lang = jarvis.context.language -local parts = {} - --- 1) Recent memory facts (last_used_at desc, top 3) -local facts = jarvis.memory.all() or {} -if #facts > 0 then - table.sort(facts, function(a, b) - return (a.last_used_at or 0) > (b.last_used_at or 0) - end) - local items = {} - for i = 1, math.min(3, #facts) do - local v = (facts[i].value or ""):sub(1, 50) - table.insert(items, facts[i].key .. " — " .. v) - end - if lang == "ru" then - table.insert(parts, "Из памяти: " .. table.concat(items, "; ") .. ".") - else - table.insert(parts, "From memory: " .. table.concat(items, "; ") .. ".") - end -end - --- 2) Upcoming scheduled tasks (just count + the next one) -local tasks = jarvis.scheduler.list() or {} -if #tasks > 0 then - -- Tasks have no "next_fire" exposed at Lua-level, so just count + name first. - local first_name = tasks[1].name or "task" - if lang == "ru" then - table.insert(parts, "В расписании " .. tostring(#tasks) .. ", ближайшее: " .. first_name .. ".") - else - table.insert(parts, tostring(#tasks) .. " scheduled, next: " .. first_name .. ".") - end -end - --- 3) Last assistant reply (if any) -local last = jarvis.llm_last_reply() -if last and last ~= "" then - -- Trim to ~100 chars to keep the recap snappy - local snippet = last:gsub("%s+", " "):sub(1, 120) - if lang == "ru" then - table.insert(parts, "Я говорил: " .. snippet) - else - table.insert(parts, "I said: " .. snippet) - end -end - -if #parts == 0 then - return jarvis.cmd.not_found(lang == "ru" - and "Ничего недавно не происходило, сэр." - or "Nothing recent to recap, sir.") -end - -return jarvis.cmd.ok(table.concat(parts, " ")) diff --git a/resources/commands/llm_context/command.toml b/resources/commands/llm_context/command.toml deleted file mode 100644 index bdc316a..0000000 --- a/resources/commands/llm_context/command.toml +++ /dev/null @@ -1,37 +0,0 @@ -# P0.3: Conversation context controls — reset / repeat last reply. - -[[commands]] -id = "llm.reset" -type = "lua" -script = "reset.lua" -sandbox = "standard" -timeout = 2000 - -[commands.phrases] -ru = [ - "сбрось контекст", - "сбрось разговор", - "забудь о чём мы говорили", - "забудь разговор", - "начни разговор заново", - "очисти контекст", -] -en = ["reset context", "forget conversation", "start fresh"] - - -[[commands]] -id = "llm.repeat" -type = "lua" -script = "repeat.lua" -sandbox = "standard" -timeout = 5000 - -[commands.phrases] -ru = [ - "повтори последнее", - "повтори что сказал", - "повтори ответ", - "что ты сказал", - "повтори", -] -en = ["repeat", "say it again", "repeat the last answer"] diff --git a/resources/commands/llm_context/repeat.lua b/resources/commands/llm_context/repeat.lua deleted file mode 100644 index 6b86adb..0000000 --- a/resources/commands/llm_context/repeat.lua +++ /dev/null @@ -1,5 +0,0 @@ -local last = jarvis.llm_last_reply() -if not last or last == "" then - return jarvis.cmd.not_found("Мне нечего повторить.") -end -return jarvis.cmd.ok(last) diff --git a/resources/commands/llm_context/reset.lua b/resources/commands/llm_context/reset.lua deleted file mode 100644 index 6ef82b2..0000000 --- a/resources/commands/llm_context/reset.lua +++ /dev/null @@ -1,2 +0,0 @@ -jarvis.llm_reset() -return jarvis.cmd.ok("Контекст сброшен.") diff --git a/resources/commands/llm_switch/command.toml b/resources/commands/llm_switch/command.toml deleted file mode 100644 index 09cf252..0000000 --- a/resources/commands/llm_switch/command.toml +++ /dev/null @@ -1,59 +0,0 @@ -# Switch between local (Ollama) and cloud (Groq) LLM at runtime. - -[[commands]] -id = "llm.switch.local" -type = "lua" -script = "switch.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "переключись на локальный", - "переключись на локальный мозг", - "включи локальный режим", - "перейди на локальный", - "используй локальный", - "перейди на оллама", - "используй оллама", -] -en = ["switch to local", "use local llm", "use ollama"] - - -[[commands]] -id = "llm.switch.cloud" -type = "lua" -script = "switch.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "переключись на облако", - "переключись на облачный", - "переключись на грок", - "переключись на гроку", - "используй облако", - "используй грок", - "перейди на облако", -] -en = ["switch to cloud", "use cloud llm", "use groq"] - - -[[commands]] -id = "llm.status" -type = "lua" -script = "status.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "какой у тебя мозг", - "какой ллм", - "какой у тебя llm", - "облако или локально", - "ты сейчас где работаешь", - "что за модель", -] -en = ["which llm", "are you local or cloud", "what model"] diff --git a/resources/commands/llm_switch/status.lua b/resources/commands/llm_switch/status.lua deleted file mode 100644 index c1d0154..0000000 --- a/resources/commands/llm_switch/status.lua +++ /dev/null @@ -1,8 +0,0 @@ -local backend = jarvis.llm_status() -if backend == "groq" then - return jarvis.cmd.ok("Сейчас работаю на облаке.") -elseif backend == "ollama" then - return jarvis.cmd.ok("Сейчас работаю локально.") -else - return jarvis.cmd.not_found("LLM не настроен.") -end diff --git a/resources/commands/llm_switch/switch.lua b/resources/commands/llm_switch/switch.lua deleted file mode 100644 index 14e0bd9..0000000 --- a/resources/commands/llm_switch/switch.lua +++ /dev/null @@ -1,22 +0,0 @@ --- Map command id → backend name (see command.toml). -local cmd_id = jarvis.context.command_id or "" -local target -if cmd_id:match("local") then - target = "ollama" -elseif cmd_id:match("cloud") then - target = "groq" -else - return jarvis.cmd.error("Не понял на что переключать.") -end - -local result = jarvis.llm_switch(target) -if not result then - local human = (target == "ollama") and "локальный" or "облачный" - return jarvis.cmd.error("Не получилось переключиться на " .. human .. ".") -end - -if result == "groq" then - return jarvis.cmd.ok("Переключился на облачный мозг.") -else - return jarvis.cmd.ok("Переключился на локальный мозг.") -end diff --git a/resources/commands/macros/cancel.lua b/resources/commands/macros/cancel.lua deleted file mode 100644 index e10e867..0000000 --- a/resources/commands/macros/cancel.lua +++ /dev/null @@ -1,4 +0,0 @@ -if jarvis.macros.cancel() then - return jarvis.cmd.ok("Запись макроса отменена.") -end -return jarvis.cmd.not_found("Записи не было.") diff --git a/resources/commands/macros/command.toml b/resources/commands/macros/command.toml deleted file mode 100644 index aaa2596..0000000 --- a/resources/commands/macros/command.toml +++ /dev/null @@ -1,101 +0,0 @@ -# IMBA-7: Voice macros — record and replay sequences of voice commands. - -[[commands]] -id = "macros.start_recording" -type = "lua" -script = "start.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "запиши макрос", - "начни запись макроса", - "записывай макрос", - "новый макрос", -] -en = ["record macro", "start macro recording"] - - -[[commands]] -id = "macros.save" -type = "lua" -script = "save.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "сохрани макрос", - "стоп макрос", - "стоп запись макроса", - "закончи запись", - "макрос готов", -] -en = ["save macro", "stop macro", "finish macro"] - - -[[commands]] -id = "macros.cancel" -type = "lua" -script = "cancel.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "отмени макрос", - "отмени запись макроса", - "отмени запись", - "забудь макрос", -] -en = ["cancel macro", "discard macro"] - - -[[commands]] -id = "macros.replay" -type = "lua" -script = "replay.lua" -sandbox = "standard" -timeout = 60000 - -[commands.phrases] -ru = [ - "запусти макрос", - "выполни макрос", - "воспроизведи макрос", - "сыграй макрос", - "проиграй макрос", -] -en = ["play macro", "replay macro", "run macro"] - - -[[commands]] -id = "macros.list" -type = "lua" -script = "list.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "какие у меня макросы", - "список макросов", - "покажи макросы", -] -en = ["list macros", "show macros"] - - -[[commands]] -id = "macros.delete" -type = "lua" -script = "delete.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "удали макрос", - "забудь о макросе", -] -en = ["delete macro", "remove macro"] diff --git a/resources/commands/macros/delete.lua b/resources/commands/macros/delete.lua deleted file mode 100644 index 4215266..0000000 --- a/resources/commands/macros/delete.lua +++ /dev/null @@ -1,18 +0,0 @@ -local phrase = (jarvis.context.phrase or ""):lower() -local name = jarvis.text.strip_trigger(phrase, { - "удали макрос", - "забудь о макросе", - "delete macro", - "remove macro", - "видали макрос", -}) -name = name:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if name == "" then - return jarvis.cmd.error("Какой макрос удалить?") -end - -if jarvis.macros.delete(name) then - return jarvis.cmd.ok("Удалил макрос " .. name .. ".") -end -return jarvis.cmd.not_found("Макрос " .. name .. " не найден.") diff --git a/resources/commands/macros/list.lua b/resources/commands/macros/list.lua deleted file mode 100644 index ef6bf64..0000000 --- a/resources/commands/macros/list.lua +++ /dev/null @@ -1,12 +0,0 @@ -local list = jarvis.macros.list() -if #list == 0 then - return jarvis.cmd.ok("Макросов нет.") -end - -local sample = math.min(#list, 5) -local line = string.format("Макросов: %d. ", #list) -for i = 1, sample do - line = line .. list[i].name .. " (" .. list[i].steps_count .. " шагов)" - if i < sample then line = line .. ", " end -end -return jarvis.cmd.ok(line .. ".") diff --git a/resources/commands/macros/replay.lua b/resources/commands/macros/replay.lua deleted file mode 100644 index 3dce4a7..0000000 --- a/resources/commands/macros/replay.lua +++ /dev/null @@ -1,24 +0,0 @@ --- "Запусти макрос работа" -local phrase = (jarvis.context.phrase or ""):lower() -local name = jarvis.text.strip_trigger(phrase, { - "выполни макрос", - "воспроизведи макрос", - "запусти макрос", - "сыграй макрос", - "проиграй макрос", - "play macro", - "replay macro", - "run macro", -}) -name = name:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if name == "" then - return jarvis.cmd.error("Какой макрос запустить?") -end - -local ok, result = pcall(function() return jarvis.macros.replay(name) end) -if not ok then - return jarvis.cmd.not_found(tostring(result)) -end - -return jarvis.cmd.ok(string.format("Запускаю %s, шагов: %d.", name, result)) diff --git a/resources/commands/macros/save.lua b/resources/commands/macros/save.lua deleted file mode 100644 index 750a506..0000000 --- a/resources/commands/macros/save.lua +++ /dev/null @@ -1,8 +0,0 @@ -local ok, result = pcall(function() return jarvis.macros.save() end) -if not ok then - -- result here is the error msg from save_recording - return jarvis.cmd.not_found(tostring(result)) -end - -local name = jarvis.macros.recording_name() or "макрос" -return jarvis.cmd.ok(string.format("Сохранил %d шагов.", result)) diff --git a/resources/commands/macros/start.lua b/resources/commands/macros/start.lua deleted file mode 100644 index 164acaf..0000000 --- a/resources/commands/macros/start.lua +++ /dev/null @@ -1,23 +0,0 @@ --- "Запиши макрос работа" -local phrase = (jarvis.context.phrase or ""):lower() -local name = jarvis.text.strip_trigger(phrase, { - "начни запись макроса", - "записывай макрос", - "запиши макрос", - "новый макрос", - "record macro", - "start macro recording", -}) -name = name:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if name == "" then - return jarvis.cmd.error("Как назвать макрос?") -end - -local ok, err = pcall(function() jarvis.macros.start(name) end) -if not ok then - jarvis.log("warn", "macros.start: " .. tostring(err)) - return jarvis.cmd.error("Не получилось начать запись.") -end - -return jarvis.cmd.ok(string.format("Записываю макрос %s. Говорите команды. Когда закончите — скажите сохрани макрос.", name)) diff --git a/resources/commands/magic_8ball/ask.lua b/resources/commands/magic_8ball/ask.lua deleted file mode 100644 index 04e670c..0000000 --- a/resources/commands/magic_8ball/ask.lua +++ /dev/null @@ -1,24 +0,0 @@ -local answers = { - -- positive - "Без сомнения, сэр.", - "Однозначно да.", - "Можете на это рассчитывать.", - "Скорее всего да.", - "Перспективы хорошие.", - "Знаки указывают на да.", - -- neutral - "Пока неясно, попробуйте позже.", - "Сосредоточьтесь и спросите снова.", - "Не могу предсказать сейчас.", - "Лучше не говорить.", - -- negative - "Не рассчитывайте на это.", - "Мой ответ — нет.", - "Источники говорят нет.", - "Перспективы не очень.", - "Очень сомнительно.", -} - -math.randomseed(os.time() + (jarvis.context.time.minute or 0) * 31 + (jarvis.context.time.second or 0)) -local idx = math.random(1, #answers) -return jarvis.cmd.ok(answers[idx]) diff --git a/resources/commands/magic_8ball/command.toml b/resources/commands/magic_8ball/command.toml deleted file mode 100644 index 50ef0ea..0000000 --- a/resources/commands/magic_8ball/command.toml +++ /dev/null @@ -1,19 +0,0 @@ -# Classic magic 8-ball. - -[[commands]] -id = "magic_8ball" -type = "lua" -script = "ask.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "ответь да или нет", - "магический шар", - "восьмой шар", - "скажи да или нет", - "что скажешь", - "предсказание", -] -en = ["yes or no", "magic 8-ball", "tell me yes or no"] diff --git a/resources/commands/mailto/command.toml b/resources/commands/mailto/command.toml deleted file mode 100644 index ec4d141..0000000 --- a/resources/commands/mailto/command.toml +++ /dev/null @@ -1,17 +0,0 @@ -# Mailto: open default mail client to compose a new message. - -[[commands]] -id = "mailto.compose" -type = "lua" -script = "compose.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "напиши письмо", - "новое письмо", - "открой почту", - "составить письмо", -] -en = ["compose email", "new email", "open mail"] diff --git a/resources/commands/mailto/compose.lua b/resources/commands/mailto/compose.lua deleted file mode 100644 index 2143ee8..0000000 --- a/resources/commands/mailto/compose.lua +++ /dev/null @@ -1,32 +0,0 @@ --- "Напиши письмо имя_контакта" → opens mailto: URI in default handler. --- For now just opens a blank compose; could be extended to pull recipient --- from memory ("маме", "коллеге") via lookups. -local phrase = (jarvis.context.phrase or ""):lower() -local target = jarvis.text.strip_trigger(phrase, { - "напиши письмо", - "новое письмо", - "открой почту", - "составить письмо", - "compose email", - "new email", - "open mail", -}) -target = target:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -local recipient = "" -if target ~= "" then - -- Try memory lookup — "напиши письмо маме" → memory.recall("email_маме") - local key = "email_" .. target - local addr = jarvis.memory.recall(key) - if addr and addr ~= "" then - recipient = addr - end -end - -local uri = recipient ~= "" and ("mailto:" .. recipient) or "mailto:" -jarvis.system.open(uri) - -if recipient ~= "" then - return jarvis.cmd.ok("Открываю письмо для " .. target .. ".") -end -return jarvis.cmd.ok("Открываю почтовый клиент.") diff --git a/resources/commands/math/command.toml b/resources/commands/math/command.toml deleted file mode 100644 index 0a73b09..0000000 --- a/resources/commands/math/command.toml +++ /dev/null @@ -1,22 +0,0 @@ -[[commands]] -id = "math_eval" -type = "lua" -script = "math.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = [ - "посчитай", - "сколько будет", - "вычисли", - "реши", - "сколько это", -] -en = [ - "calculate", - "how much is", - "compute", - "solve", - "what is", -] diff --git a/resources/commands/math/math.lua b/resources/commands/math/math.lua deleted file mode 100644 index c8e8ccc..0000000 --- a/resources/commands/math/math.lua +++ /dev/null @@ -1,272 +0,0 @@ --- Quick math — offline-first arithmetic evaluator. --- --- Strategy: --- 1. Strip the trigger phrase from the user's utterance. --- 2. Normalise Russian/English operator words (плюс/minus/в степени/...) into --- symbols so the remainder looks like a regular numeric expression. --- 3. Try a small shunting-yard parser on the result. If it succeeds and the --- output is finite, speak it INSTANTLY — no LLM round-trip. --- 4. Only fall back to the LLM if the local parse failed or returned NaN. --- That covers word problems, multi-step equations, unit conversions. --- --- This makes 95% of "сколько будет X плюс Y" complete in under 50ms regardless --- of network or GROQ_TOKEN. - -local lang = jarvis.context.language -local raw = (jarvis.context.phrase or ""):lower() - -local triggers = { - "сколько будет", "сколько это", "посчитай", "вычисли", "реши", - "how much is", "calculate", "compute", "solve", "what is", -} -local query = jarvis.text.strip_trigger(raw, triggers) -if not query or query == "" then query = raw end -query = query:gsub("^%s+", ""):gsub("%s+$", "") - -if query == "" then - return jarvis.cmd.not_found(lang == "ru" and "Что посчитать?" or "What to compute?") -end - --- ── 1. Russian/English operator → symbol normalisation ──────────────────── - -local function normalise(s) - local mapping = { - { "плюс", "+" }, - { "прибавить к?", "+" }, - { "минус", "-" }, - { "отнять", "-" }, - { "вычесть", "-" }, - { "умножить на", "*" }, - { "умножь на", "*" }, - { "помножить на", "*" }, - { "разделить на", "/" }, - { "поделить на", "/" }, - { "делить на", "/" }, - { "в степени", "^" }, - { "возвести в степень", "^" }, - { "остаток от деления на", "%%" }, - { "по модулю", "%%" }, - { "квадрат", "^2" }, - { "куб", "^3" }, - { "plus", "+" }, - { "minus", "-" }, - { "times", "*" }, - { "multiplied by", "*" }, - { "divided by", "/" }, - { "to the power of", "^" }, - { "squared", "^2" }, - { "cubed", "^3" }, - { "modulo", "%%" }, - { "mod", "%%" }, - } - for _, pair in ipairs(mapping) do - s = s:gsub(pair[1], pair[2]) - end - -- comma as decimal separator (русская запись) - s = s:gsub("(%d),(%d)", "%1.%2") - return s -end - --- Pattern: "корень из X" / "квадратный корень из X" → math.sqrt -local function handle_sqrt(s) - local n = s:match("^%s*к?орен?ь?%s*и?з?%s+(%-?[%d%.]+)%s*$") - if not n then n = s:match("^%s*sqrt%s*%(?%s*(%-?[%d%.]+)%s*%)?%s*$") end - if n then - local v = tonumber(n) - if v and v >= 0 then return math.sqrt(v) end - end - return nil -end - --- Pattern: "X процентов от Y" / "X percent of Y" → X * Y / 100 -local function handle_percent(s) - local x, y = s:match("^%s*(%-?[%d%.]+)%s*процент[%a]*%s*от%s+(%-?[%d%.]+)%s*$") - if not x then - x, y = s:match("^%s*(%-?[%d%.]+)%s*percent%s*of%s+(%-?[%d%.]+)%s*$") - end - if x and y then - local fx, fy = tonumber(x), tonumber(y) - if fx and fy then return fx * fy / 100 end - end - return nil -end - --- ── 2. Tiny shunting-yard for " ..." with parens -local function tokenize(s) - local tokens = {} - local i = 1 - while i <= #s do - local c = s:sub(i, i) - if c:match("[%s]") then - i = i + 1 - elseif c:match("[%d%.]") then - local j = i - while j <= #s and s:sub(j, j):match("[%d%.]") do j = j + 1 end - table.insert(tokens, { kind = "num", val = tonumber(s:sub(i, j - 1)) }) - i = j - elseif c == "+" or c == "-" or c == "*" or c == "/" or c == "^" or c == "%" then - -- handle unary minus / plus at expression start or after operator/paren - if (c == "-" or c == "+") and (#tokens == 0 or tokens[#tokens].kind == "op" or tokens[#tokens].kind == "lparen") then - local j = i + 1 - while j <= #s and s:sub(j, j):match("[%s]") do j = j + 1 end - if j <= #s and s:sub(j, j):match("[%d%.]") then - local k = j - while k <= #s and s:sub(k, k):match("[%d%.]") do k = k + 1 end - local v = tonumber(s:sub(j, k - 1)) - if v then - table.insert(tokens, { kind = "num", val = (c == "-" and -v or v) }) - i = k - else - return nil - end - else - return nil - end - else - table.insert(tokens, { kind = "op", val = c }) - i = i + 1 - end - elseif c == "(" then - table.insert(tokens, { kind = "lparen" }); i = i + 1 - elseif c == ")" then - table.insert(tokens, { kind = "rparen" }); i = i + 1 - else - return nil -- unknown char - end - end - return tokens -end - -local function precedence(op) - if op == "+" or op == "-" then return 1 end - if op == "*" or op == "/" or op == "%" then return 2 end - if op == "^" then return 3 end - return 0 -end - -local function eval_op(a, b, op) - if op == "+" then return a + b end - if op == "-" then return a - b end - if op == "*" then return a * b end - if op == "/" then if b == 0 then return nil end; return a / b end - if op == "%" then if b == 0 then return nil end; return a % b end - if op == "^" then return a ^ b end - return nil -end - -local function shunting_yard_eval(tokens) - if not tokens or #tokens == 0 then return nil end - local values, ops = {}, {} - - local function apply() - local op = table.remove(ops) - local b = table.remove(values) - local a = table.remove(values) - if a == nil or b == nil then return false end - local r = eval_op(a, b, op) - if r == nil then return false end - table.insert(values, r) - return true - end - - for _, t in ipairs(tokens) do - if t.kind == "num" then - table.insert(values, t.val) - elseif t.kind == "op" then - while #ops > 0 and ops[#ops] ~= "(" and precedence(ops[#ops]) >= precedence(t.val) do - if not apply() then return nil end - end - table.insert(ops, t.val) - elseif t.kind == "lparen" then - table.insert(ops, "(") - elseif t.kind == "rparen" then - while #ops > 0 and ops[#ops] ~= "(" do - if not apply() then return nil end - end - if ops[#ops] ~= "(" then return nil end - table.remove(ops) - end - end - while #ops > 0 do - if ops[#ops] == "(" then return nil end - if not apply() then return nil end - end - if #values ~= 1 then return nil end - return values[1] -end - --- ── 3. Format result for speech (no scientific notation, trim trailing zeros) -local function format_result(n) - if n ~= n then return nil end -- NaN - if n == math.huge or n == -math.huge then return nil end - if math.abs(n - math.floor(n)) < 1e-9 then - return tostring(math.floor(n + (n < 0 and -0.5 or 0.5))) - end - local s = string.format("%.4f", n) - s = s:gsub("0+$", ""):gsub("%.$", "") - return s -end - --- ── 4. Try offline first ────────────────────────────────────────────────── - -local normalised = normalise(query) -jarvis.log("info", "math normalised: " .. normalised) - -local result_num = - handle_sqrt(normalised) - or handle_percent(normalised) - or shunting_yard_eval(tokenize(normalised)) - -if result_num ~= nil then - local pretty = format_result(result_num) - if pretty then - local prefix = (lang == "ru" and "Получилось " or "Result: ") - return jarvis.cmd.ok(prefix .. pretty) - end -end - --- ── 5. LLM fallback for word-problems / equations / conversions ─────────── - -local token = jarvis.system.env("GROQ_TOKEN") -if not token or token == "" then - return jarvis.cmd.error(lang == "ru" - and "Не понял выражение, и GROQ_TOKEN не задан." - or "Couldn't parse, and GROQ_TOKEN unset.") -end - -local base = jarvis.system.env("GROQ_BASE_URL") -if not base or base == "" then base = "https://api.groq.com/openai/v1" end -local model = jarvis.system.env("GROQ_MODEL") -if not model or model == "" then model = "llama-3.3-70b-versatile" end - -local sys = "Ты калькулятор. Решай арифметику, простые уравнения, конверсии единиц, проценты. " - .. "Отвечай ТОЛЬКО результатом числом или короткой строкой. " - .. "Если запрос — не математика, ответь одним словом «нет»." - -local payload = { - model = model, - messages = { - { role = "system", content = sys }, - { role = "user", content = query }, - }, - max_tokens = 64, - temperature = 0.0, -} - -jarvis.log("info", "math fallback to LLM: " .. query) -local res = jarvis.http.post_json(base .. "/chat/completions", payload, { Authorization = "Bearer " .. token }) -if not res.ok then - return jarvis.cmd.error("Ошибка API: " .. tostring(res.status)) -end -local content = (res.body or ""):match('"content"%s*:%s*"(.-[^\\])"') -if not content then - return jarvis.cmd.error(lang == "ru" and "Не смог распарсить ответ" or "Couldn't parse response") -end -content = content:gsub('\\n', '\n'):gsub('\\"', '"'):gsub('\\\\', '\\'):gsub('\\t', '\t') -content = content:gsub("^%s+", ""):gsub("%s+$", "") -if content:lower() == "нет" or content == "" then - return jarvis.cmd.not_found(lang == "ru" and "Это не математика" or "Not math") -end - -local prefix = (lang == "ru" and "Получилось " or "Result: ") -return jarvis.cmd.ok(prefix .. content) diff --git a/resources/commands/media/_media_helper.ps1 b/resources/commands/media/_media_helper.ps1 deleted file mode 100644 index 130aedc..0000000 --- a/resources/commands/media/_media_helper.ps1 +++ /dev/null @@ -1,20 +0,0 @@ -param( - [Parameter(Mandatory=$true)] - [ValidateSet("play_pause","next","prev","stop")] - [string]$Action -) - -Add-Type -Name MediaKey -Namespace Win32 -MemberDefinition @' -[DllImport("user32.dll")] -public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, System.UIntPtr dwExtraInfo); -'@ - -$vk = switch ($Action) { - "play_pause" { 0xB3 } - "next" { 0xB0 } - "prev" { 0xB1 } - "stop" { 0xB2 } -} - -[Win32.MediaKey]::keybd_event($vk, 0, 0, [UIntPtr]::Zero) -[Win32.MediaKey]::keybd_event($vk, 0, 2, [UIntPtr]::Zero) diff --git a/resources/commands/media/command.toml b/resources/commands/media/command.toml deleted file mode 100644 index 35535c5..0000000 --- a/resources/commands/media/command.toml +++ /dev/null @@ -1,85 +0,0 @@ -[[commands]] -id = "media_play_pause" -type = "lua" -script = "play_pause.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "пауза", - "поставь на паузу", - "продолжи", - "включи музыку", - "запусти музыку", - "плей", -] -en = [ - "pause", - "play", - "resume", - "play music", -] - - -[[commands]] -id = "media_next" -type = "lua" -script = "next_track.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "следующий трек", - "следующая песня", - "переключи трек", - "следующая", - "дальше", -] -en = [ - "next track", - "next song", - "skip", - "next", -] - - -[[commands]] -id = "media_prev" -type = "lua" -script = "prev_track.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "предыдущий трек", - "предыдущая песня", - "верни песню", - "назад", -] -en = [ - "previous track", - "previous song", - "back", -] - - -[[commands]] -id = "media_stop" -type = "lua" -script = "stop_media.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "стоп музыка", - "останови музыку", - "выключи музыку", -] -en = [ - "stop music", - "stop playback", -] diff --git a/resources/commands/media/next_track.lua b/resources/commands/media/next_track.lua deleted file mode 100644 index 7a8eeb7..0000000 --- a/resources/commands/media/next_track.lua +++ /dev/null @@ -1,13 +0,0 @@ -local helper = jarvis.context.command_path .. "\\_media_helper.ps1" -local cmd = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action next', - helper -) -local res = jarvis.system.exec(cmd) -if res.success then - jarvis.audio.play_ok() -else - jarvis.log("error", "media next failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end -return { chain = false } diff --git a/resources/commands/media/play_pause.lua b/resources/commands/media/play_pause.lua deleted file mode 100644 index d54358a..0000000 --- a/resources/commands/media/play_pause.lua +++ /dev/null @@ -1,13 +0,0 @@ -local helper = jarvis.context.command_path .. "\\_media_helper.ps1" -local cmd = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action play_pause', - helper -) -local res = jarvis.system.exec(cmd) -if res.success then - jarvis.audio.play_ok() -else - jarvis.log("error", "play/pause failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end -return { chain = false } diff --git a/resources/commands/media/prev_track.lua b/resources/commands/media/prev_track.lua deleted file mode 100644 index 16da444..0000000 --- a/resources/commands/media/prev_track.lua +++ /dev/null @@ -1,13 +0,0 @@ -local helper = jarvis.context.command_path .. "\\_media_helper.ps1" -local cmd = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action prev', - helper -) -local res = jarvis.system.exec(cmd) -if res.success then - jarvis.audio.play_ok() -else - jarvis.log("error", "media prev failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end -return { chain = false } diff --git a/resources/commands/media/stop_media.lua b/resources/commands/media/stop_media.lua deleted file mode 100644 index d458b24..0000000 --- a/resources/commands/media/stop_media.lua +++ /dev/null @@ -1,13 +0,0 @@ -local helper = jarvis.context.command_path .. "\\_media_helper.ps1" -local cmd = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action stop', - helper -) -local res = jarvis.system.exec(cmd) -if res.success then - jarvis.audio.play_ok() -else - jarvis.log("error", "media stop failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end -return { chain = false } diff --git a/resources/commands/media_keys/_media.ps1 b/resources/commands/media_keys/_media.ps1 deleted file mode 100644 index fa40948..0000000 --- a/resources/commands/media_keys/_media.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -# Helper: send a Windows media key via user32.keybd_event. -# Called by key.lua with one argument = decimal VK code. -param([int]$Vk) - -if (-not $Vk) { exit 1 } - -Add-Type -TypeDefinition @" -using System.Runtime.InteropServices; -public class MediaKey { - [DllImport("user32.dll")] - public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo); -} -"@ - -# down + up -[MediaKey]::keybd_event([byte]$Vk, 0, 0, 0) -[MediaKey]::keybd_event([byte]$Vk, 0, 2, 0) -exit 0 diff --git a/resources/commands/media_keys/command.toml b/resources/commands/media_keys/command.toml deleted file mode 100644 index e0e4ac1..0000000 --- a/resources/commands/media_keys/command.toml +++ /dev/null @@ -1,76 +0,0 @@ -# Media playback control via Windows virtual media keys. -# Works with anything that listens to global media keys: Spotify, Yandex Music, -# YouTube in browser (when tab is focused), Foobar2000, Winamp, etc. -# No OAuth, no API keys — just sends VK_MEDIA_* via PowerShell SendKeys. - -[[commands]] -id = "media.play_pause" -type = "lua" -script = "key.lua" -sandbox = "full" -timeout = 2000 - -[commands.phrases] -ru = [ - "пауза", - "продолжи", - "плей", - "включи музыку", - "выключи музыку", - "приостанови музыку", - "снова музыку", -] -en = ["pause", "play", "resume music", "stop music"] - - -[[commands]] -id = "media.next" -type = "lua" -script = "key.lua" -sandbox = "full" -timeout = 2000 - -[commands.phrases] -ru = [ - "следующий трек", - "следующую песню", - "перемотай вперёд", - "вперёд трек", - "переключи на следующую", - "next", -] -en = ["next track", "next song", "skip"] - - -[[commands]] -id = "media.prev" -type = "lua" -script = "key.lua" -sandbox = "full" -timeout = 2000 - -[commands.phrases] -ru = [ - "предыдущий трек", - "предыдущую песню", - "перемотай назад", - "назад трек", - "переключи на предыдущую", - "previous", -] -en = ["previous track", "previous song", "back"] - - -[[commands]] -id = "media.stop" -type = "lua" -script = "key.lua" -sandbox = "full" -timeout = 2000 - -[commands.phrases] -ru = [ - "стоп музыка", - "останови плеер", -] -en = ["stop", "stop player"] diff --git a/resources/commands/media_keys/key.lua b/resources/commands/media_keys/key.lua deleted file mode 100644 index dc695be..0000000 --- a/resources/commands/media_keys/key.lua +++ /dev/null @@ -1,26 +0,0 @@ --- Map command id → VK_MEDIA_* virtual key code. --- See https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes -local cmd_id = jarvis.context.command_id or "" - -local VK = { - ["media.play_pause"] = 179, -- VK_MEDIA_PLAY_PAUSE (0xB3) - ["media.next"] = 176, -- VK_MEDIA_NEXT_TRACK (0xB0) - ["media.prev"] = 177, -- VK_MEDIA_PREV_TRACK (0xB1) - ["media.stop"] = 178, -- VK_MEDIA_STOP (0xB2) -} - -local code = VK[cmd_id] -if not code then - return jarvis.cmd.error("Неизвестная медиа-команда.") -end - -local helper = jarvis.context.command_path .. "\\_media.ps1" -local cmd_str = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" %d', - helper, code -) - -jarvis.system.exec(cmd_str) - --- silent — feedback would be annoying for media-key spam -return jarvis.cmd.silent_ok() diff --git a/resources/commands/memory_admin/command.toml b/resources/commands/memory_admin/command.toml deleted file mode 100644 index ebec6b5..0000000 --- a/resources/commands/memory_admin/command.toml +++ /dev/null @@ -1,66 +0,0 @@ -# Memory admin — voice-driven housekeeping for the long-term memory. -# - "сколько фактов помнишь" → speak count -# - "выгрузи список фактов" → notify with first ~10 keys -# - "забудь все" → wipe everything (with safety prefix "точно забудь все") - -[[commands]] -id = "memory_admin.count" -type = "lua" -script = "count.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "сколько фактов помнишь", - "сколько ты помнишь", - "сколько у тебя в памяти", - "размер памяти", -] -en = [ - "how many facts do you remember", - "how big is your memory", - "memory size", -] - - -[[commands]] -id = "memory_admin.list" -type = "lua" -script = "list.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "выгрузи список фактов", - "покажи что помнишь", - "перечисли что помнишь", - "что у тебя в памяти", -] -en = [ - "list what you remember", - "show me your memory", - "what's in memory", -] - - -[[commands]] -id = "memory_admin.wipe" -type = "lua" -script = "wipe.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "точно забудь все", - "точно забудь всё", - "сотри всю память", - "wipe memory", -] -en = [ - "wipe all memory", - "really forget everything", - "clear all memory", -] diff --git a/resources/commands/memory_admin/count.lua b/resources/commands/memory_admin/count.lua deleted file mode 100644 index 2dc5bf2..0000000 --- a/resources/commands/memory_admin/count.lua +++ /dev/null @@ -1,20 +0,0 @@ -local lang = jarvis.context.language -local facts = jarvis.memory.all() or {} -local n = #facts - -local function ru_plural_facts(n) - local m100 = n % 100 - if m100 >= 11 and m100 <= 14 then return "фактов" end - local m10 = n % 10 - if m10 == 1 then return "факт" end - if m10 >= 2 and m10 <= 4 then return "факта" end - return "фактов" -end - -local msg -if lang == "ru" then - msg = "В памяти " .. tostring(n) .. " " .. ru_plural_facts(n) .. ", сэр." -else - msg = "I remember " .. tostring(n) .. (n == 1 and " fact, sir." or " facts, sir.") -end -return jarvis.cmd.ok(msg) diff --git a/resources/commands/memory_admin/list.lua b/resources/commands/memory_admin/list.lua deleted file mode 100644 index 569b05e..0000000 --- a/resources/commands/memory_admin/list.lua +++ /dev/null @@ -1,37 +0,0 @@ --- Read out the first ~5 keys + value previews; the rest is just a count. --- TTS doesn't handle long enumerations well, so we keep it short and notify --- the full dump as a Windows toast for visual scan. - -local lang = jarvis.context.language -local facts = jarvis.memory.all() or {} - -if #facts == 0 then - return jarvis.cmd.not_found(lang == "ru" - and "Память пуста, сэр." - or "Memory is empty, sir.") -end - --- Sort by most-recently-used so the user hears what's actually fresh. -table.sort(facts, function(a, b) return (a.last_used_at or 0) > (b.last_used_at or 0) end) - -local speak_lines = {} -local notify_lines = {} -for i, f in ipairs(facts) do - local v = (f.value or ""):sub(1, 60) - local line = f.key .. ": " .. v - table.insert(notify_lines, line) - if i <= 5 then - table.insert(speak_lines, f.key .. " — " .. v) - end -end - -jarvis.system.notify( - lang == "ru" and ("Память (" .. tostring(#facts) .. ")") or ("Memory (" .. tostring(#facts) .. ")"), - table.concat(notify_lines, "\n") -) - -local preview = table.concat(speak_lines, ", ") -local extra = #facts > 5 - and string.format(lang == "ru" and ". И ещё %d." or ". And %d more.", #facts - 5) - or "." -return jarvis.cmd.ok(preview .. extra) diff --git a/resources/commands/memory_admin/wipe.lua b/resources/commands/memory_admin/wipe.lua deleted file mode 100644 index 6c3e368..0000000 --- a/resources/commands/memory_admin/wipe.lua +++ /dev/null @@ -1,13 +0,0 @@ --- Nuclear "forget everything" — guarded by the "точно" prefix in the toml --- phrases so a stray "забудь" doesn't trigger it. -local lang = jarvis.context.language -local n = jarvis.memory.clear_all() - -if n == 0 then - return jarvis.cmd.ok(lang == "ru" and "Память уже пуста, сэр." - or "Memory is already empty, sir.") -end - -return jarvis.cmd.ok(string.format(lang == "ru" - and "Стёр %d записей. Память чиста." - or "Wiped %d records. Memory is clean.", n)) diff --git a/resources/commands/memory_pack/command.toml b/resources/commands/memory_pack/command.toml deleted file mode 100644 index b600991..0000000 --- a/resources/commands/memory_pack/command.toml +++ /dev/null @@ -1,55 +0,0 @@ -# IMBA-2: Long-term memory — remember/recall/forget arbitrary facts. - -[[commands]] -id = "memory.remember" -type = "lua" -script = "remember.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "запомни обо мне", "запомни что", - "помни что", "запомни это", - "запомни про меня", -] -en = ["remember that", "remember about me", "memorize"] - - -[[commands]] -id = "memory.recall" -type = "lua" -script = "recall.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "что ты помнишь о", "что ты знаешь обо мне", - "что помнишь про", "вспомни", -] -en = ["what do you remember about", "recall"] - - -[[commands]] -id = "memory.forget" -type = "lua" -script = "forget.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = ["забудь про", "забудь что", "забудь обо мне"] -en = ["forget about", "forget that"] - - -[[commands]] -id = "memory.list" -type = "lua" -script = "list.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = ["что ты помнишь", "что ты знаешь обо мне", "покажи память"] -en = ["what do you remember", "list memory", "show memory"] diff --git a/resources/commands/memory_pack/forget.lua b/resources/commands/memory_pack/forget.lua deleted file mode 100644 index 40fa083..0000000 --- a/resources/commands/memory_pack/forget.lua +++ /dev/null @@ -1,36 +0,0 @@ -local phrase = (jarvis.context.phrase or ""):lower() -local key = jarvis.text.strip_trigger(phrase, { - "забудь про", - "забудь что", - "забудь обо мне", - "forget about", - "forget that", - "забудь про", -}) - -key = key:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if key == "" then - jarvis.speak("Что именно забыть?") - jarvis.audio.play_error() - return { chain = false } -end - -local removed = jarvis.memory.forget(key) -if removed then - jarvis.speak("Забыл про " .. key .. ".") - jarvis.audio.play_ok() -else - -- try substring search and remove first hit - local hits = jarvis.memory.search(key, 1) - if #hits > 0 then - jarvis.memory.forget(hits[1].key) - jarvis.speak("Забыл про " .. hits[1].key .. ".") - jarvis.audio.play_ok() - else - jarvis.speak("Я и не помнил про " .. key .. ".") - jarvis.audio.play_not_found() - end -end - -return { chain = false } diff --git a/resources/commands/memory_pack/list.lua b/resources/commands/memory_pack/list.lua deleted file mode 100644 index 302dc5c..0000000 --- a/resources/commands/memory_pack/list.lua +++ /dev/null @@ -1,15 +0,0 @@ -local recs = jarvis.memory.all() -if #recs == 0 then - return jarvis.cmd.ok("Я пока ничего о вас не запомнил.") -end - -local count = #recs -local sample = math.min(count, 5) -local line = string.format("Помню %d фактов. Первые: ", count) -for i = 1, sample do - line = line .. recs[i].key - if i < sample then line = line .. ", " end -end -line = line .. "." - -return jarvis.cmd.ok(line) diff --git a/resources/commands/memory_pack/recall.lua b/resources/commands/memory_pack/recall.lua deleted file mode 100644 index bd4e1a2..0000000 --- a/resources/commands/memory_pack/recall.lua +++ /dev/null @@ -1,52 +0,0 @@ -local phrase = (jarvis.context.phrase or ""):lower() -local query = jarvis.text.strip_trigger(phrase, { - "что ты помнишь о", - "что ты помнишь про", - "что ты знаешь обо мне", - "что помнишь про", - "вспомни", - "what do you remember about", - "recall", - "що ти пам'ятаєш про", -}) - -query = query:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if query == "" then - -- show top-3 most recent - local recs = jarvis.memory.all() - if #recs == 0 then - jarvis.speak("Ничего пока не помню.") - jarvis.audio.play_ok() - return { chain = false } - end - local line = "Я помню: " - for i = 1, math.min(3, #recs) do - line = line .. recs[i].key .. " — " .. recs[i].value .. ". " - end - jarvis.speak(line) - jarvis.audio.play_ok() - return { chain = false } -end - --- substring search -local hits = jarvis.memory.search(query, 3) -if #hits == 0 then - jarvis.speak("Ничего не нашёл про " .. query .. ".") - jarvis.audio.play_not_found() - return { chain = false } -end - -local line = "" -if #hits == 1 then - line = hits[1].value -else - for i, h in ipairs(hits) do - line = line .. h.key .. ": " .. h.value - if i < #hits then line = line .. ". " end - end -end - -jarvis.speak(line) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/memory_pack/remember.lua b/resources/commands/memory_pack/remember.lua deleted file mode 100644 index cca722f..0000000 --- a/resources/commands/memory_pack/remember.lua +++ /dev/null @@ -1,55 +0,0 @@ -local phrase = (jarvis.context.phrase or ""):lower() -local body = jarvis.text.strip_trigger(phrase, { - "запомни обо мне что", - "запомни обо мне", - "запомни что", - "помни что", - "запомни это", - "запомни про меня", - "запомни", - "remember that", - "remember about me", - "memorize", - "запам'ятай що", - "запам'ятай про мене", -}) - -body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if body == "" then - jarvis.speak("Что именно запомнить?") - jarvis.audio.play_error() - return { chain = false } -end - --- Heuristic: derive a key. Look for "что X = Y" / "что у меня X" / etc. --- For now: first 3-5 meaningful words become the key. -local key = body -local _, after_eq = body:find("=", 1, true) -if after_eq then - key = body:sub(1, after_eq - 1):gsub("%s+$", "") - body = body:sub(after_eq + 1):gsub("^%s+", "") -end - --- truncate key to first 6 words -local words = {} -for w in key:gmatch("%S+") do - table.insert(words, w) - if #words >= 6 then break end -end -key = table.concat(words, " ") - -local ok, err = pcall(function() - jarvis.memory.remember(key, body) -end) - -if not ok then - jarvis.log("warn", "memory.remember failed: " .. tostring(err)) - jarvis.speak("Не удалось запомнить.") - jarvis.audio.play_error() - return { chain = false } -end - -jarvis.speak("Запомнил.") -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/mood_log/command.toml b/resources/commands/mood_log/command.toml deleted file mode 100644 index bdc091f..0000000 --- a/resources/commands/mood_log/command.toml +++ /dev/null @@ -1,37 +0,0 @@ -# Mood / energy log — daily journal entries via voice. -# Each entry stored via jarvis.memory with key = "mood_" so it survives restart. - -[[commands]] -id = "mood.record" -type = "lua" -script = "record.lua" -sandbox = "standard" -timeout = 5000 - -[commands.phrases] -ru = [ - "запиши настроение", - "настроение", - "запиши настроение на", - "сегодня мне", - "мой день был", - "мне сейчас", -] -en = ["log mood", "my mood is", "I feel"] - - -[[commands]] -id = "mood.recap" -type = "lua" -script = "recap.lua" -sandbox = "standard" -timeout = 15000 - -[commands.phrases] -ru = [ - "как прошла неделя", - "недельный отчет настроения", - "сводка настроения", - "что я писал про настроение", -] -en = ["mood recap", "weekly mood summary", "how was my week"] diff --git a/resources/commands/mood_log/recap.lua b/resources/commands/mood_log/recap.lua deleted file mode 100644 index c57e18a..0000000 --- a/resources/commands/mood_log/recap.lua +++ /dev/null @@ -1,33 +0,0 @@ --- Pull all mood_* entries, ask LLM to summarize trends. -local all = jarvis.memory.all() -local moods = {} -for _, rec in ipairs(all) do - if rec.key:sub(1, 5) == "mood_" then - table.insert(moods, rec.value) - end -end - -if #moods == 0 then - return jarvis.cmd.not_found("Записей о настроении пока нет.") -end - --- Trim to most-recent-ish 30 entries (memory.all isn't ordered, but mood_ keys sort) -table.sort(moods) -local recent = {} -local start = math.max(1, #moods - 30 + 1) -for i = start, #moods do - table.insert(recent, moods[i]) -end - -local joined = table.concat(recent, "\n") - -local summary = jarvis.llm({ - { role = "system", content = "Ты — психотерапевт-помощник. На основе записей пользователя о настроении дай короткую сводку (3-5 предложений) на русском: общий тон, тренды, выдели хороший/плохой день. Без банальностей, без советов." }, - { role = "user", content = "Записи:\n" .. joined } -}, { max_tokens = 250, temperature = 0.5 }) - -if not summary or summary == "" then - return jarvis.cmd.error("Не получилось обобщить. Возможно, нет связи с LLM.") -end - -return jarvis.cmd.ok(summary) diff --git a/resources/commands/mood_log/record.lua b/resources/commands/mood_log/record.lua deleted file mode 100644 index 9a97a7a..0000000 --- a/resources/commands/mood_log/record.lua +++ /dev/null @@ -1,37 +0,0 @@ --- "Запиши настроение 7" / "сегодня мне грустно" / "настроение восемь, продуктивно" -local phrase = (jarvis.context.phrase or "") - -local body = jarvis.text.strip_trigger(phrase:lower(), { - "запиши настроение на", - "запиши настроение", - "недельный отчет", - "настроение", - "сегодня мне", - "мой день был", - "мне сейчас", - "log mood", - "my mood is", - "i feel", - "запиши настрій", -}) -body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if body == "" then - return jarvis.cmd.error("Что записать про настроение?") -end - -local t = jarvis.context.time -local stamp = string.format("%04d-%02d-%02d %02d:%02d", - t.year, t.month, t.day, t.hour, t.minute) -local key = "mood_" .. (t.timestamp or os.time()) -local value = stamp .. " — " .. body - -local ok, err = pcall(function() - jarvis.memory.remember(key, value) -end) -if not ok then - jarvis.log("warn", "mood.record: " .. tostring(err)) - return jarvis.cmd.error("Не получилось записать.") -end - -return jarvis.cmd.ok("Записал.") diff --git a/resources/commands/mouse/_mouse_helper.ps1 b/resources/commands/mouse/_mouse_helper.ps1 deleted file mode 100644 index 25374e7..0000000 --- a/resources/commands/mouse/_mouse_helper.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -param( - [Parameter(Mandatory=$true)] - [ValidateSet("left","right","middle","double","scroll_up","scroll_down")] - [string]$Action -) - -Add-Type -Name MouseM -Namespace Win32 -MemberDefinition @' -[DllImport("user32.dll")] -public static extern void mouse_event(uint flags, int dx, int dy, int data, System.UIntPtr extra); -'@ - -$LEFTDOWN = 0x0002 -$LEFTUP = 0x0004 -$RIGHTDOWN = 0x0008 -$RIGHTUP = 0x0010 -$MIDDOWN = 0x0020 -$MIDUP = 0x0040 -$WHEEL = 0x0800 - -function Click([uint32]$down, [uint32]$up) { - [Win32.MouseM]::mouse_event($down, 0, 0, 0, [UIntPtr]::Zero) - Start-Sleep -Milliseconds 30 - [Win32.MouseM]::mouse_event($up, 0, 0, 0, [UIntPtr]::Zero) -} - -switch ($Action) { - "left" { Click $LEFTDOWN $LEFTUP } - "right" { Click $RIGHTDOWN $RIGHTUP } - "middle" { Click $MIDDOWN $MIDUP } - "double" { Click $LEFTDOWN $LEFTUP; Start-Sleep -Milliseconds 50; Click $LEFTDOWN $LEFTUP } - "scroll_up" { [Win32.MouseM]::mouse_event($WHEEL, 0, 0, 360, [UIntPtr]::Zero) } - "scroll_down" { [Win32.MouseM]::mouse_event($WHEEL, 0, 0, -360, [UIntPtr]::Zero) } -} diff --git a/resources/commands/mouse/command.toml b/resources/commands/mouse/command.toml deleted file mode 100644 index b41a50c..0000000 --- a/resources/commands/mouse/command.toml +++ /dev/null @@ -1,58 +0,0 @@ -[[commands]] -id = "mouse_left_click" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["клик", "кликни", "нажми мышкой"] -en = ["click", "left click"] - - -[[commands]] -id = "mouse_right_click" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["правый клик", "клик правой", "правой кнопкой"] -en = ["right click"] - - -[[commands]] -id = "mouse_double_click" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["двойной клик", "дабл клик", "двойное нажатие"] -en = ["double click"] - - -[[commands]] -id = "mouse_scroll_down" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["промотай вниз", "прокрути вниз", "вниз промотай"] -en = ["scroll down"] - - -[[commands]] -id = "mouse_scroll_up" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["промотай вверх", "прокрути вверх", "наверх промотай"] -en = ["scroll up"] diff --git a/resources/commands/mouse/dispatch.lua b/resources/commands/mouse/dispatch.lua deleted file mode 100644 index 1107005..0000000 --- a/resources/commands/mouse/dispatch.lua +++ /dev/null @@ -1,31 +0,0 @@ -local cmd_id = jarvis.context.command_id -local helper = jarvis.context.command_path .. "\\_mouse_helper.ps1" - -local actions = { - mouse_left_click = "left", - mouse_right_click = "right", - mouse_middle_click = "middle", - mouse_double_click = "double", - mouse_scroll_up = "scroll_up", - mouse_scroll_down = "scroll_down", -} - -local action = actions[cmd_id] -if not action then - jarvis.audio.play_error() - return { chain = false } -end - -local res = jarvis.system.exec(string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action %s', - helper, action -)) - -if res.success then - jarvis.audio.play_ok() -else - jarvis.log("error", "mouse " .. action .. " failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end - -return { chain = false } diff --git a/resources/commands/net_info/command.toml b/resources/commands/net_info/command.toml deleted file mode 100644 index 3930da4..0000000 --- a/resources/commands/net_info/command.toml +++ /dev/null @@ -1,53 +0,0 @@ -# Network info — WiFi SSID, local IP, public IP via PowerShell + free service. -# Separate from existing `network` pack which likely covers different things. - -[[commands]] -id = "net.wifi_ssid" -type = "lua" -script = "wifi.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "к какой сети я подключен", - "какой wi-fi", - "какой вайфай", - "какая сеть", - "имя сети", -] -en = ["what wifi", "what network", "wifi name", "ssid"] - - -[[commands]] -id = "net.local_ip" -type = "lua" -script = "local_ip.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "мой локальный ip", - "мой ip", - "айпи адрес", - "локальный адрес", -] -en = ["my ip", "local ip", "ip address"] - - -[[commands]] -id = "net.public_ip" -type = "lua" -script = "public_ip.lua" -sandbox = "full" -timeout = 10000 - -[commands.phrases] -ru = [ - "мой внешний ip", - "публичный ip", - "внешний айпи", - "какой у меня внешний", -] -en = ["public ip", "external ip", "what's my ip online"] diff --git a/resources/commands/net_info/local_ip.lua b/resources/commands/net_info/local_ip.lua deleted file mode 100644 index 55c0b50..0000000 --- a/resources/commands/net_info/local_ip.lua +++ /dev/null @@ -1,18 +0,0 @@ --- Get first non-loopback IPv4 address via PowerShell Get-NetIPAddress. -local ps = "(Get-NetIPAddress -AddressFamily IPv4 | " .. - "Where-Object {$_.IPAddress -notlike '127.*' -and $_.IPAddress -notlike '169.*'} | " .. - "Select-Object -First 1).IPAddress" -local res = jarvis.system.exec(string.format( - 'powershell -NoProfile -Command "%s"', ps -)) - -if not res.success then - return jarvis.cmd.error("Не получилось узнать IP.") -end - -local ip = (res.stdout or ""):gsub("%s+", "") -if ip == "" then - return jarvis.cmd.not_found("Нет активного сетевого подключения.") -end - -return jarvis.cmd.ok("Локальный адрес: " .. ip .. ".") diff --git a/resources/commands/net_info/public_ip.lua b/resources/commands/net_info/public_ip.lua deleted file mode 100644 index 74968c2..0000000 --- a/resources/commands/net_info/public_ip.lua +++ /dev/null @@ -1,12 +0,0 @@ --- Free public-IP services: api.ipify.org → plain text body. -local r = jarvis.http.get("https://api.ipify.org") -if not r or not r.ok then - return jarvis.cmd.error("Не получилось узнать внешний IP.") -end - -local ip = (r.body or ""):gsub("%s+", "") -if ip == "" then - return jarvis.cmd.error("Сервис вернул пустой ответ.") -end - -return jarvis.cmd.ok("Внешний адрес: " .. ip .. ".") diff --git a/resources/commands/net_info/wifi.lua b/resources/commands/net_info/wifi.lua deleted file mode 100644 index a708fd1..0000000 --- a/resources/commands/net_info/wifi.lua +++ /dev/null @@ -1,18 +0,0 @@ --- "Какая сеть" → `netsh wlan show interfaces` → grep SSID -local res = jarvis.system.exec('netsh wlan show interfaces') -if not res.success then - return jarvis.cmd.error("Не получилось получить данные о Wi-Fi.") -end - -local out = res.stdout or "" -local ssid = out:match("SSID%s+:%s+(.-)\r?\n") --- Russian Windows: "SSID : MyNetwork" --- The non-localised case: same format -if not ssid then ssid = out:match("Имя сети%s+:%s+(.-)\r?\n") end -ssid = ssid and ssid:gsub("^%s+", ""):gsub("%s+$", "") or "" - -if ssid == "" or ssid == "(нет)" or ssid == "(none)" then - return jarvis.cmd.not_found("Wi-Fi не подключен или адаптер выключен.") -end - -return jarvis.cmd.ok("Сеть: " .. ssid .. ".") diff --git a/resources/commands/network/command.toml b/resources/commands/network/command.toml deleted file mode 100644 index 6143093..0000000 --- a/resources/commands/network/command.toml +++ /dev/null @@ -1,34 +0,0 @@ -[[commands]] -id = "open_wifi_settings" -type = "lua" -script = "open.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["настройки wifi", "настройки вайфай", "wifi настройки", "сеть"] -en = ["wifi settings", "open wifi"] - - -[[commands]] -id = "open_bluetooth_settings" -type = "lua" -script = "open.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["настройки блютуз", "блютуз", "включи блютуз", "выключи блютуз"] -en = ["bluetooth settings", "open bluetooth", "toggle bluetooth"] - - -[[commands]] -id = "my_ip" -type = "lua" -script = "ip.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = ["мой ай пи", "мой ip", "какой у меня айпи", "айпишник"] -en = ["my ip", "what is my ip"] diff --git a/resources/commands/network/ip.lua b/resources/commands/network/ip.lua deleted file mode 100644 index 0e5635a..0000000 --- a/resources/commands/network/ip.lua +++ /dev/null @@ -1,24 +0,0 @@ -local lang = jarvis.context.language - -local ps = [[ -$lan = (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | - Where-Object { $_.PrefixOrigin -eq 'Dhcp' -or $_.PrefixOrigin -eq 'Manual' } | - Where-Object { $_.IPAddress -notmatch '^127\.' -and $_.IPAddress -notmatch '^169\.254\.' } | - Select-Object -First 1).IPAddress -if (-not $lan) { $lan = '(none)' } -$wan = try { (Invoke-RestMethod -Uri 'https://api.ipify.org?format=json' -TimeoutSec 5).ip } catch { 'unavailable' } -Write-Output ('LAN ' + $lan + ' WAN ' + $wan) -]] -local res = jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'))) -local out = (res.stdout or ""):gsub("[\r\n]+$", "") - -if out == "" then - jarvis.audio.play_error() - return { chain = false } -end - -jarvis.system.notify("IP", out) -local say = out:gsub("LAN", lang == "ru" and "Локальный" or "LAN"):gsub("WAN", lang == "ru" and ". Внешний" or ". WAN") -jarvis.speak(say, { lang = lang }) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/network/open.lua b/resources/commands/network/open.lua deleted file mode 100644 index f7334e3..0000000 --- a/resources/commands/network/open.lua +++ /dev/null @@ -1,16 +0,0 @@ -local cmd_id = jarvis.context.command_id -local lang = jarvis.context.language - -local uris = { - open_wifi_settings = "ms-settings:network-wifi", - open_bluetooth_settings = "ms-settings:bluetooth", -} - -local uri = uris[cmd_id] or "ms-settings:network" -jarvis.system.open(uri) -jarvis.system.notify( - cmd_id == "open_wifi_settings" and "Wi-Fi" or "Bluetooth", - lang == "ru" and "Открываю настройки" or "Opening settings" -) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/news/command.toml b/resources/commands/news/command.toml deleted file mode 100644 index 05f160a..0000000 --- a/resources/commands/news/command.toml +++ /dev/null @@ -1,22 +0,0 @@ -[[commands]] -id = "news_headlines" -type = "lua" -script = "news.lua" -sandbox = "full" -timeout = 25000 - -[commands.phrases] -ru = [ - "что нового", - "новости", - "что в мире", - "последние новости", - "расскажи новости", - "топ новостей", -] -en = [ - "what's new", - "latest news", - "headlines", - "top news", -] diff --git a/resources/commands/news/news.lua b/resources/commands/news/news.lua deleted file mode 100644 index ca51ac2..0000000 --- a/resources/commands/news/news.lua +++ /dev/null @@ -1,82 +0,0 @@ -local lang = jarvis.context.language - -local feed = lang == "en" - and "https://feeds.bbci.co.uk/news/rss.xml" - or "https://lenta.ru/rss/news" - -local res = jarvis.http.get(feed) -if not res.ok then - jarvis.system.notify("News", "Не получил RSS: " .. tostring(res.status)) - jarvis.audio.play_error() - return { chain = false } -end - -local titles = {} -for title in res.body:gmatch("%s*<!%[CDATA%[(.-)%]%]>%s*") do - if title ~= "" and title:lower():find("rss") == nil and title:lower():find("новости") ~= 1 then - table.insert(titles, title) - end - if #titles >= 7 then break end -end -if #titles == 0 then - for title in res.body:gmatch("%s*(.-)%s*") do - if title ~= "" then - table.insert(titles, title) - end - if #titles >= 7 then break end - end -end - -if #titles <= 1 then - jarvis.system.notify("News", lang == "ru" and "Не получилось распарсить" or "Could not parse feed") - jarvis.audio.play_error() - return { chain = false } -end - -local top = {} -for i = 2, math.min(6, #titles) do table.insert(top, titles[i]) end -local joined = table.concat(top, " | ") - -jarvis.system.clipboard.set(joined) -jarvis.system.notify(lang == "ru" and "Новости" or "News", joined:sub(1, 280)) - -local token = jarvis.system.env("GROQ_TOKEN") -local say_text -if token and token ~= "" then - local base = jarvis.system.env("GROQ_BASE_URL"); if not base or base == "" then base = "https://api.groq.com/openai/v1" end - local model = jarvis.system.env("GROQ_MODEL"); if not model or model == "" then model = "llama-3.3-70b-versatile" end - local sys = lang == "ru" - and "Ты диктор. Дам список заголовков новостей. Сжато перескажи 3 главные темы простым языком, 2-3 предложения суммарно. Не упоминай номера и не повторяй заголовки дословно." - or "You are a news anchor. Summarise the top 3 headlines in 2-3 sentences. No numbering, no verbatim repeats." - local payload = { - model = model, - messages = { - { role = "system", content = sys }, - { role = "user", content = joined }, - }, - max_tokens = 220, - temperature = 0.4, - } - local lr = jarvis.http.post_json(base .. "/chat/completions", payload, { Authorization = "Bearer " .. token }) - if lr.ok then - local c = (lr.body or ""):match('"content"%s*:%s*"(.-[^\\])"') - if c then - c = c:gsub('\\n', ' '):gsub('\\"', '"'):gsub('\\\\', '\\') - say_text = c - end - end -end - -if not say_text then - say_text = (lang == "ru" and "Главные новости: " or "Top news: ") .. (top[1] or "") .. ". " .. (top[2] or "") .. ". " .. (top[3] or "") -end - -local sapi = say_text:gsub("'", "''"):gsub("\r?\n", " ") -local ps = string.format( - [[Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; foreach ($v in $s.GetInstalledVoices()) { if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq 'ru') { try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} } } $s.Speak('%s')]], - sapi -) -jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'))) - -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/notes/add.lua b/resources/commands/notes/add.lua deleted file mode 100644 index 26f9327..0000000 --- a/resources/commands/notes/add.lua +++ /dev/null @@ -1,56 +0,0 @@ -local lang = jarvis.context.language -local phrase = (jarvis.context.phrase or ""):lower() - -local triggers = { - "запиши заметку", "запиши идею", "сделай заметку", "запомни идею", - "запиши", "запомни", - "write down", "take a note", "remember this", "note down", - "запиши ідею", "зроби нотатку", -} - -local body = phrase -for _, t in ipairs(triggers) do - local s, e = string.find(body, t, 1, true) - if s == 1 then - body = body:sub(e + 1) - break - end -end -body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if body == "" then - jarvis.system.notify( - lang == "ru" and "Заметка" or "Note", - lang == "ru" and "Что записать?" or "What to write?" - ) - jarvis.audio.play_error() - return { chain = false } -end - -local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" -local notes_path = userprofile .. "\\Documents\\jarvis-notes.txt" - -local t = jarvis.context.time -local stamp = string.format("%s-%s-%s %s:%s", - t.year, t.month, t.day, t.hour, t.minute) - -local line = string.format("[%s] %s\n", stamp, body) - -local ok, err = pcall(function() - jarvis.fs.append(notes_path, line) -end) - -if not ok then - jarvis.log("error", "notes append failed: " .. tostring(err)) - jarvis.system.notify("Заметка", lang == "ru" and "Не удалось записать" or "Append failed") - jarvis.audio.play_error() - return { chain = false } -end - -jarvis.log("info", "note appended: " .. body) -jarvis.system.notify( - lang == "ru" and "Заметка записана" or "Note saved", - body:sub(1, 120) -) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/notes/command.toml b/resources/commands/notes/command.toml deleted file mode 100644 index d672acc..0000000 --- a/resources/commands/notes/command.toml +++ /dev/null @@ -1,22 +0,0 @@ -[[commands]] -id = "add_note" -type = "lua" -script = "add.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = ["запиши", "запиши заметку", "запиши идею", "запомни", "запомни идею", "сделай заметку"] -en = ["write down", "take a note", "remember this", "note down"] - - -[[commands]] -id = "open_notes" -type = "lua" -script = "open.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["покажи заметки", "открой заметки", "что я записал"] -en = ["show notes", "open notes"] diff --git a/resources/commands/notes/open.lua b/resources/commands/notes/open.lua deleted file mode 100644 index 6743ac4..0000000 --- a/resources/commands/notes/open.lua +++ /dev/null @@ -1,16 +0,0 @@ -local lang = jarvis.context.language -local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" -local notes_path = userprofile .. "\\Documents\\jarvis-notes.txt" - -if not jarvis.fs.exists(notes_path) then - jarvis.system.notify( - lang == "ru" and "Заметки" or "Notes", - lang == "ru" and "Файл пуст или не создан" or "No notes yet" - ) - jarvis.audio.play_not_found() - return { chain = false } -end - -jarvis.system.open(notes_path) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/notif_queue/clear.lua b/resources/commands/notif_queue/clear.lua deleted file mode 100644 index 2ac0a1b..0000000 --- a/resources/commands/notif_queue/clear.lua +++ /dev/null @@ -1,13 +0,0 @@ -local all = jarvis.memory.all() -local count = 0 -for _, rec in ipairs(all) do - if rec.key:sub(1, 6) == "notif." then - jarvis.memory.forget(rec.key) - count = count + 1 - end -end - -if count == 0 then - return jarvis.cmd.ok("Уведомлений и так нет.") -end -return jarvis.cmd.ok(string.format("Очистил %d уведомлений.", count)) diff --git a/resources/commands/notif_queue/command.toml b/resources/commands/notif_queue/command.toml deleted file mode 100644 index 73d4ad5..0000000 --- a/resources/commands/notif_queue/command.toml +++ /dev/null @@ -1,35 +0,0 @@ -# Notification queue — records what Jarvis said while you were away. -# Used by scheduler/macros/llm to log fired events; speak the digest on demand. - -[[commands]] -id = "notif.what_missed" -type = "lua" -script = "missed.lua" -sandbox = "standard" -timeout = 5000 - -[commands.phrases] -ru = [ - "что я пропустил", - "что было пока меня не было", - "какие уведомления", - "пропущенные", - "что нового", -] -en = ["what did I miss", "missed notifications"] - - -[[commands]] -id = "notif.clear" -type = "lua" -script = "clear.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "очисти уведомления", - "забудь уведомления", - "сбрось пропущенное", -] -en = ["clear notifications"] diff --git a/resources/commands/notif_queue/missed.lua b/resources/commands/notif_queue/missed.lua deleted file mode 100644 index fca579e..0000000 --- a/resources/commands/notif_queue/missed.lua +++ /dev/null @@ -1,24 +0,0 @@ --- Notification queue uses memory keys "notif.0".."notif.N" (rolling, max 30). --- Producers (scheduler/macros/llm fallback) call jarvis.memory.remember "notif." elsewhere. --- Here we just enumerate and speak. -local all = jarvis.memory.all() -local notifs = {} -for _, rec in ipairs(all) do - if rec.key:sub(1, 6) == "notif." then - table.insert(notifs, { ts = rec.last_used_at or 0, key = rec.key, value = rec.value }) - end -end - -if #notifs == 0 then - return jarvis.cmd.ok("Ничего не пропустил.") -end - --- Sort by timestamp desc -table.sort(notifs, function(a, b) return a.ts > b.ts end) - -local sample = math.min(#notifs, 5) -local line = string.format("Пропущено %d уведомлений. ", #notifs) -for i = 1, sample do - line = line .. notifs[i].value .. ". " -end -return jarvis.cmd.ok(line) diff --git a/resources/commands/now_playing/command.toml b/resources/commands/now_playing/command.toml deleted file mode 100644 index 1729a3e..0000000 --- a/resources/commands/now_playing/command.toml +++ /dev/null @@ -1,23 +0,0 @@ -# Windows Media Session — what's currently playing. -# Uses Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager -# via PowerShell. Works with Spotify, YouTube, Foobar, Yandex Music, anything -# that exposes SMTC. - -[[commands]] -id = "now_playing" -type = "lua" -script = "now.lua" -sandbox = "full" -timeout = 8000 - -[commands.phrases] -ru = [ - "что играет", - "что сейчас играет", - "что за музыка", - "что за песня", - "какой трек", - "какая песня", - "что слушаю", -] -en = ["what's playing", "current song", "now playing"] diff --git a/resources/commands/now_playing/now.lua b/resources/commands/now_playing/now.lua deleted file mode 100644 index dc98a9a..0000000 --- a/resources/commands/now_playing/now.lua +++ /dev/null @@ -1,46 +0,0 @@ --- "Что играет" — query Windows SMTC. --- Requires Windows 10 1803+ (Media Session API). -local ps = [[ -[Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager,Windows.Media.Control,ContentType=WindowsRuntime] | Out-Null -$mgr = [Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager]::RequestAsync() -while ($mgr.Status -eq 0) { Start-Sleep -Milliseconds 50 } -$session = $mgr.GetResults().GetCurrentSession() -if (-not $session) { Write-Output 'NONE'; exit } -$propsTask = $session.TryGetMediaPropertiesAsync() -while ($propsTask.Status -eq 0) { Start-Sleep -Milliseconds 50 } -$props = $propsTask.GetResults() -$artist = $props.Artist -$title = $props.Title -if ([string]::IsNullOrEmpty($artist) -and [string]::IsNullOrEmpty($title)) { - Write-Output 'EMPTY' - exit -} -Write-Output ("{0}|{1}" -f $artist, $title) -]] - -local res = jarvis.system.exec(string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', - ps:gsub('"', '\\"'):gsub("\r?\n", "; ") -)) - -if not res.success then - return jarvis.cmd.error("Не получилось получить данные.") -end - -local out = (res.stdout or ""):gsub("^%s+", ""):gsub("%s+$", "") -if out == "NONE" then - return jarvis.cmd.not_found("Сейчас ничего не играет.") -end -if out == "EMPTY" or out == "|" then - return jarvis.cmd.not_found("Что-то играет, но без названия.") -end - -local artist, title = out:match("^([^|]*)|(.*)$") -if not title or title == "" then - return jarvis.cmd.not_found("Трек без названия.") -end - -if artist and artist ~= "" then - return jarvis.cmd.ok(string.format("Сейчас играет: %s — %s.", artist, title)) -end -return jarvis.cmd.ok(string.format("Сейчас играет: %s.", title)) diff --git a/resources/commands/ocr/command.toml b/resources/commands/ocr/command.toml deleted file mode 100644 index 679c6a1..0000000 --- a/resources/commands/ocr/command.toml +++ /dev/null @@ -1,21 +0,0 @@ -[[commands]] -id = "read_screen" -type = "lua" -script = "read_screen.lua" -sandbox = "full" -timeout = 30000 - -[commands.phrases] -ru = [ - "прочитай экран", - "что на экране", - "распознай текст с экрана", - "что написано на экране", - "сними текст с экрана", -] -en = [ - "read screen", - "what is on screen", - "ocr screen", - "extract text from screen", -] diff --git a/resources/commands/ocr/read_screen.lua b/resources/commands/ocr/read_screen.lua deleted file mode 100644 index 1c6f753..0000000 --- a/resources/commands/ocr/read_screen.lua +++ /dev/null @@ -1,82 +0,0 @@ -local lang = jarvis.context.language - -local temp = jarvis.system.env("TEMP") or jarvis.system.env("TMP") or "C:\\Windows\\Temp" -local ts = jarvis.context.time.year .. jarvis.context.time.month .. jarvis.context.time.day - .. "_" .. jarvis.context.time.hour .. jarvis.context.time.minute .. jarvis.context.time.second -local img = temp .. "\\jarvis_ocr_" .. ts .. ".png" - -local capture_ps = string.format( - [[Add-Type -AssemblyName System.Windows.Forms,System.Drawing; $b=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds; $bmp=New-Object Drawing.Bitmap $b.Width,$b.Height; $g=[Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen($b.Location,[Drawing.Point]::Empty,$b.Size); $bmp.Save('%s'); $g.Dispose(); $bmp.Dispose()]], - img:gsub("\\", "\\\\") -) -local capture_cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', capture_ps:gsub('"', '\\"')) -local cap = jarvis.system.exec(capture_cmd) - -if not cap.success or not jarvis.fs.exists(img) then - jarvis.log("error", "screen capture failed: " .. tostring(cap.stderr)) - jarvis.system.notify("OCR", lang == "ru" and "Не смог снять экран" or "Could not capture screen") - jarvis.audio.play_error() - return { chain = false } -end - -local tesseract_paths = { - "tesseract.exe", - "C:\\Program Files\\Tesseract-OCR\\tesseract.exe", - "C:\\Program Files (x86)\\Tesseract-OCR\\tesseract.exe", -} - -local tess = nil -for _, p in ipairs(tesseract_paths) do - local check - if p == "tesseract.exe" then - check = jarvis.system.exec("where tesseract.exe") - else - check = { success = jarvis.fs.exists(p), stdout = p } - end - if check.success and (check.stdout or ""):gsub("[\r\n]+$", "") ~= "" then - tess = p == "tesseract.exe" and "tesseract.exe" or '"' .. p .. '"' - break - end -end - -if not tess then - jarvis.log("error", "tesseract not found") - jarvis.system.notify( - "OCR", - lang == "ru" - and "Tesseract не установлен. Установи: winget install UB-Mannheim.TesseractOCR" - or "Tesseract not installed. Install: winget install UB-Mannheim.TesseractOCR" - ) - jarvis.audio.play_error() - return { chain = false } -end - -local langs = lang == "en" and "eng" or "rus+eng" -local ocr_cmd = string.format('%s "%s" - -l %s', tess, img, langs) -local ocr = jarvis.system.exec(ocr_cmd) - -if not ocr.success then - jarvis.log("error", "tesseract failed: " .. tostring(ocr.stderr):sub(1, 200)) - jarvis.system.notify("OCR", lang == "ru" and "Tesseract упал" or "Tesseract failed") - jarvis.audio.play_error() - return { chain = false } -end - -local text = (ocr.stdout or ""):gsub("^%s+", ""):gsub("%s+$", "") -if text == "" then - jarvis.system.notify("OCR", lang == "ru" and "Текста не найдено" or "No text found") - jarvis.audio.play_not_found() - return { chain = false } -end - -jarvis.system.clipboard.set(text) -jarvis.log("info", "OCR result (" .. #text .. " bytes) copied to clipboard") - -local preview = text:gsub("\r?\n", " "):sub(1, 200) -if #text > 200 then preview = preview .. "..." end -jarvis.system.notify( - lang == "ru" and "Текст с экрана" or "Screen text", - preview -) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/outlook/_outlook.ps1 b/resources/commands/outlook/_outlook.ps1 deleted file mode 100644 index 2ea135c..0000000 --- a/resources/commands/outlook/_outlook.ps1 +++ /dev/null @@ -1,184 +0,0 @@ -param( - [Parameter(Mandatory=$true)] - [ValidateSet("unread_count","latest","send_clipboard","summarize_inbox")] - [string]$Action, - - [string]$Recipient = "", - [string]$Body = "", - [int]$Limit = 5 -) - -$ErrorActionPreference = 'Stop' -$OutputEncoding = [System.Text.Encoding]::UTF8 -[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 - -# Output protocol (parsed by Lua scripts): -# First line: "OK" or "ERR:" or "ERR::" -# Subsequent lines depend on Action: -# unread_count -> "" -# latest -> "FROM\t" / "SUBJECT\t" / "PREVIEW\t" -# send_clipboard -> "TO\t" / "ADDR\t" / "SUBJECT\t" -# summarize_inbox -> repeated "FROM\t\tSUBJECT\t" -# -# All multiline strings get newlines collapsed to spaces before printing. - -function Clean-Line([string]$s) { - if (-not $s) { return "" } - $s = $s -replace "`r", " " -replace "`n", " " -replace "`t", " " - return ($s -replace '\s+', ' ').Trim() -} - -function Fail($code, $msg = "") { - if ($msg) { - Write-Output ("ERR:{0}:{1}" -f $code, (Clean-Line $msg)) - } else { - Write-Output ("ERR:{0}" -f $code) - } - exit 0 -} - -function Get-OutlookSession { - try { - $ol = New-Object -ComObject Outlook.Application - $ns = $ol.GetNamespace("MAPI") - return @{ App = $ol; Ns = $ns } - } catch { - Fail "outlook_unavailable" $_.Exception.Message - } -} - -function Strip-Html([string]$s) { - if (-not $s) { return "" } - $s = [regex]::Replace($s, '<[^>]+>', ' ') - $s = $s -replace ' ', ' ' -replace '&', '&' -replace '<', '<' -replace '>', '>' -replace '"', '"' -replace "'", "'" - return Clean-Line $s -} - -function Resolve-Recipient([object]$ns, [string]$query) { - if ([string]::IsNullOrWhiteSpace($query)) { return $null } - try { - $r = $ns.CreateRecipient($query) - $null = $r.Resolve() - if ($r.Resolved) { - $addr = $null - try { $addr = $r.AddressEntry.GetExchangeUser().PrimarySmtpAddress } catch { } - if (-not $addr) { - try { $addr = $r.AddressEntry.Address } catch { } - } - if (-not $addr) { $addr = $r.Name } - return @{ Name = $r.Name; Address = $addr } - } - } catch { } - return $null -} - -switch ($Action) { - "unread_count" { - $s = Get-OutlookSession - try { - $inbox = $s.Ns.GetDefaultFolder(6) - $count = 0 - try { $count = [int]$inbox.UnReadItemCount } catch { } - if (-not $count) { - $count = ($inbox.Items | Where-Object { $_.UnRead -eq $true }).Count - } - Write-Output "OK" - Write-Output ([int]$count).ToString() - } catch { - Fail "inbox_failed" $_.Exception.Message - } - } - - "latest" { - $s = Get-OutlookSession - try { - $inbox = $s.Ns.GetDefaultFolder(6) - $items = $inbox.Items - $items.Sort("[ReceivedTime]", $true) - $latest = $null - foreach ($it in $items) { - if ($it.Class -eq 43) { $latest = $it; break } # olMail = 43 - } - if (-not $latest) { Fail "empty_inbox" } - $fromName = "" - try { $fromName = $latest.SenderName } catch { } - if (-not $fromName) { try { $fromName = $latest.SenderEmailAddress } catch { } } - $subject = "" - try { $subject = $latest.Subject } catch { } - $body = "" - try { $body = $latest.Body } catch { } - if (-not $body) { - try { $body = Strip-Html $latest.HTMLBody } catch { } - } else { - $body = Clean-Line $body - } - if ($body.Length -gt 200) { $body = $body.Substring(0, 200) + "..." } - Write-Output "OK" - Write-Output ("FROM`t{0}" -f (Clean-Line $fromName)) - Write-Output ("SUBJECT`t{0}" -f (Clean-Line $subject)) - Write-Output ("PREVIEW`t{0}" -f $body) - } catch { - Fail "latest_failed" $_.Exception.Message - } - } - - "send_clipboard" { - if ([string]::IsNullOrWhiteSpace($Recipient)) { Fail "no_recipient" } - $s = Get-OutlookSession - try { - $resolved = Resolve-Recipient $s.Ns $Recipient - if (-not $resolved) { Fail "unresolved" $Recipient } - $bodyText = if ($Body) { $Body } else { "" } - $subject = "" - if ($bodyText) { - $firstLine = ($bodyText -split "`r?`n", 2)[0].Trim() - if ($firstLine.Length -gt 60) { - $subject = $firstLine.Substring(0, 60).Trim() - } else { - $subject = $firstLine - } - } - if ([string]::IsNullOrWhiteSpace($subject)) { $subject = "From J.A.R.V.I.S." } - - $mail = $s.App.CreateItem(0) - $mail.To = $resolved.Address - $mail.Subject = $subject - $mail.Body = $bodyText - $mail.Send() - Write-Output "OK" - Write-Output ("TO`t{0}" -f (Clean-Line $resolved.Name)) - Write-Output ("ADDR`t{0}" -f (Clean-Line $resolved.Address)) - Write-Output ("SUBJECT`t{0}" -f (Clean-Line $subject)) - } catch { - Fail "send_failed" $_.Exception.Message - } - } - - "summarize_inbox" { - $s = Get-OutlookSession - try { - $inbox = $s.Ns.GetDefaultFolder(6) - $items = $inbox.Items - $items.Sort("[ReceivedTime]", $true) - $taken = 0 - $lines = @() - foreach ($it in $items) { - if ($taken -ge $Limit) { break } - if ($it.Class -ne 43) { continue } - $unread = $false - try { $unread = [bool]$it.UnRead } catch { } - if (-not $unread) { continue } - $fromName = "" - try { $fromName = $it.SenderName } catch { } - $subj = "" - try { $subj = $it.Subject } catch { } - $lines += ("FROM`t{0}`tSUBJECT`t{1}" -f (Clean-Line $fromName), (Clean-Line $subj)) - $taken += 1 - } - Write-Output "OK" - foreach ($l in $lines) { Write-Output $l } - } catch { - Fail "summary_failed" $_.Exception.Message - } - } -} diff --git a/resources/commands/outlook/command.toml b/resources/commands/outlook/command.toml deleted file mode 100644 index 720ff98..0000000 --- a/resources/commands/outlook/command.toml +++ /dev/null @@ -1,91 +0,0 @@ -[[commands]] -id = "outlook_unread_count" -type = "lua" -script = "unread_count.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = [ - "сколько у меня непрочитанных писем", - "сколько непрочитанных писем", - "проверь почту", - "проверь outlook", - "сколько новых писем", -] -en = [ - "check unread mail", - "how many unread emails", - "check my inbox", - "unread email count", - "any new mail", -] - - -[[commands]] -id = "outlook_latest" -type = "lua" -script = "latest.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = [ - "прочитай последнее письмо", - "что в последнем письме", - "последнее письмо", - "покажи последнее письмо", -] -en = [ - "read last email", - "read latest email", - "what is the last email", - "show me the latest email", -] - - -[[commands]] -id = "outlook_send_clipboard" -type = "lua" -script = "send_clipboard.lua" -sandbox = "full" -timeout = 20000 - -[commands.phrases] -ru = [ - "отправь письмо {name}", - "отправь почту {name}", - "напиши письмо {name}", - "отправь это {name}", -] -en = [ - "email this to {name}", - "send email to {name}", - "send this email to {name}", - "mail this to {name}", -] - -[commands.slots.name] -entity = "person name" - - -[[commands]] -id = "outlook_summarize_inbox" -type = "lua" -script = "summarize_inbox.lua" -sandbox = "full" -timeout = 30000 - -[commands.phrases] -ru = [ - "коротко расскажи про инбокс", - "расскажи что в инбоксе", - "сделай саммари инбокса", - "что нового в почте", -] -en = [ - "summarize inbox", - "summarise my inbox", - "tell me about my inbox", - "what is new in my mail", -] diff --git a/resources/commands/outlook/latest.lua b/resources/commands/outlook/latest.lua deleted file mode 100644 index 7bfcc09..0000000 --- a/resources/commands/outlook/latest.lua +++ /dev/null @@ -1,68 +0,0 @@ --- Read the most recent email in the default Inbox: speak FROM + SUBJECT + --- a 1-sentence preview (~200 chars, HTML stripped) of the body. - -local lang = jarvis.context.language -local helper = jarvis.context.command_path .. "\\_outlook.ps1" -local cmd = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action latest', - helper -) - -local res = jarvis.system.exec(cmd) -local out = (res.stdout or ""):gsub("\r", "") - -if not res.success or out == "" then - jarvis.log("error", "outlook latest exec failed: " .. tostring(res.stderr)) - return jarvis.cmd.error(lang == "ru" - and "Outlook недоступен — запусти Outlook сначала." - or "Outlook is unavailable — start Outlook first.") -end - -local lines = {} -for line in out:gmatch("[^\n]+") do - table.insert(lines, line) -end - -local status = lines[1] or "" -if status:sub(1, 3) == "ERR" then - if status:find("empty_inbox", 1, true) then - return jarvis.cmd.not_found(lang == "ru" - and "Входящие пусты." - or "Inbox is empty.") - end - jarvis.log("warn", "outlook latest: " .. status) - return jarvis.cmd.error(lang == "ru" - and "Outlook недоступен — запусти Outlook сначала." - or "Outlook is unavailable — start Outlook first.") -end - -local from, subject, preview = "", "", "" -for i = 2, #lines do - local key, val = lines[i]:match("^([A-Z]+)\t(.*)$") - if key == "FROM" then from = val or "" end - if key == "SUBJECT" then subject = val or "" end - if key == "PREVIEW" then preview = val or "" end -end - -if from == "" and subject == "" then - return jarvis.cmd.error(lang == "ru" and "Не получилось прочитать письмо." or "Could not read the email.") -end - -local speech -if lang == "ru" then - speech = string.format("Последнее письмо от %s. Тема: %s. %s", - from ~= "" and from or "неизвестного отправителя", - subject ~= "" and subject or "без темы", - preview) -else - speech = string.format("Latest email from %s. Subject: %s. %s", - from ~= "" and from or "unknown sender", - subject ~= "" and subject or "no subject", - preview) -end - -jarvis.system.notify( - lang == "ru" and "Последнее письмо" or "Latest email", - string.format("%s\n%s\n%s", from, subject, preview) -) -return jarvis.cmd.ok(speech) diff --git a/resources/commands/outlook/send_clipboard.lua b/resources/commands/outlook/send_clipboard.lua deleted file mode 100644 index 94ecb53..0000000 --- a/resources/commands/outlook/send_clipboard.lua +++ /dev/null @@ -1,103 +0,0 @@ --- Send clipboard contents as an email to {name} via Outlook COM. The recipient --- is resolved against the Global Address List + Contacts (Outlook's Recipient --- .Resolve API). Subject = first line of the body truncated to 60 chars, or --- "From J.A.R.V.I.S." for an empty clipboard. - -local lang = jarvis.context.language -local slots = jarvis.context.slots or {} -local recipient = slots.name or "" -recipient = tostring(recipient):gsub("^%s+", ""):gsub("%s+$", "") - -if recipient == "" then - return jarvis.cmd.not_found(lang == "ru" - and "Кому отправить? Назови имя." - or "Whom to send to? Please name them.") -end - -local body = jarvis.system.clipboard.get() or "" -body = body:gsub("^%s+", ""):gsub("%s+$", "") - --- Write the body to a temp file so we don't have to escape it through the --- PowerShell command-line. The PS helper reads it back via -Body parameter --- value (passed as an arg). For very large bodies (>32 KB) the cmd-line --- approach would break; in that case we read the file from inside PS. -local temp = jarvis.system.env("TEMP") or jarvis.system.env("TMP") or "C:\\Windows\\Temp" -local ts = jarvis.context.time.year .. jarvis.context.time.month .. jarvis.context.time.day - .. "_" .. jarvis.context.time.hour .. jarvis.context.time.minute .. jarvis.context.time.second -local body_path = temp .. "\\jarvis_outlook_body_" .. ts .. ".txt" -jarvis.fs.write(body_path, body) - --- Pass body via the file: the helper reads $Body if set, else falls back to --- reading the saved file. To keep the helper simple we just inline the body --- through -Body only when small; otherwise we still pass via -Body but quote --- the path and let PS read it. We use the latter: PS reads its arg literally. --- We instead encode the body so passing through args is safe. -local helper = jarvis.context.command_path .. "\\_outlook.ps1" - --- Build a single-line PowerShell expression that reads the body from the --- temp file and invokes the helper. This avoids any shell-escape issues --- with quotes/newlines in the clipboard. -local body_path_ps = body_path:gsub("'", "''") -local recipient_ps = recipient:gsub("'", "''") -local helper_ps = helper:gsub("'", "''") -local ps_expr = string.format( - "$b = Get-Content -LiteralPath '%s' -Raw -Encoding UTF8; if (-not $b) { $b = '' } else { $b = $b.TrimEnd() }; & '%s' -Action send_clipboard -Recipient '%s' -Body $b", - body_path_ps, helper_ps, recipient_ps -) -local cmd = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', - ps_expr:gsub('"', '\\"') -) - -local res = jarvis.system.exec(cmd) -jarvis.fs.remove(body_path) - -local out = (res.stdout or ""):gsub("\r", "") - -if not res.success or out == "" then - jarvis.log("error", "outlook send_clipboard exec failed: " .. tostring(res.stderr)) - return jarvis.cmd.error(lang == "ru" - and "Outlook недоступен — запусти Outlook сначала." - or "Outlook is unavailable — start Outlook first.") -end - -local lines = {} -for line in out:gmatch("[^\n]+") do table.insert(lines, line) end - -local status = lines[1] or "" -if status:sub(1, 3) == "ERR" then - if status:find("unresolved", 1, true) then - return jarvis.cmd.not_found(lang == "ru" - and (string.format("Не нашёл контакт «%s».", recipient)) - or (string.format("Could not find contact '%s'.", recipient))) - elseif status:find("no_recipient", 1, true) then - return jarvis.cmd.not_found(lang == "ru" - and "Кому отправить? Назови имя." - or "Whom to send to? Please name them.") - end - jarvis.log("warn", "outlook send_clipboard: " .. status) - return jarvis.cmd.error(lang == "ru" - and "Не получилось отправить — Outlook отказал." - or "Send failed — Outlook refused.") -end - -local to_name, addr, subject = "", "", "" -for i = 2, #lines do - local key, val = lines[i]:match("^([A-Z]+)\t(.*)$") - if key == "TO" then to_name = val or "" end - if key == "ADDR" then addr = val or "" end - if key == "SUBJECT" then subject = val or "" end -end - -local speech -if lang == "ru" then - speech = string.format("Отправил %s. Тема: %s.", to_name ~= "" and to_name or recipient, subject) -else - speech = string.format("Sent to %s. Subject: %s.", to_name ~= "" and to_name or recipient, subject) -end - -jarvis.system.notify( - lang == "ru" and "Письмо отправлено" or "Email sent", - string.format("%s <%s>\n%s", to_name, addr, subject) -) -return jarvis.cmd.ok(speech) diff --git a/resources/commands/outlook/summarize_inbox.lua b/resources/commands/outlook/summarize_inbox.lua deleted file mode 100644 index ae8f76e..0000000 --- a/resources/commands/outlook/summarize_inbox.lua +++ /dev/null @@ -1,79 +0,0 @@ --- Summarize the last few unread emails into one paragraph via the LLM. --- Pulls FROM+SUBJECT of up to 5 unread items, then asks the model for a --- short paragraph in the user's language. - -local lang = jarvis.context.language -local helper = jarvis.context.command_path .. "\\_outlook.ps1" -local cmd = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action summarize_inbox -Limit 5', - helper -) - -local res = jarvis.system.exec(cmd) -local out = (res.stdout or ""):gsub("\r", "") - -if not res.success or out == "" then - jarvis.log("error", "outlook summarize_inbox exec failed: " .. tostring(res.stderr)) - return jarvis.cmd.error(lang == "ru" - and "Outlook недоступен — запусти Outlook сначала." - or "Outlook is unavailable — start Outlook first.") -end - -local lines = {} -for line in out:gmatch("[^\n]+") do table.insert(lines, line) end - -local status = lines[1] or "" -if status:sub(1, 3) == "ERR" then - jarvis.log("warn", "outlook summarize_inbox: " .. status) - return jarvis.cmd.error(lang == "ru" - and "Outlook недоступен — запусти Outlook сначала." - or "Outlook is unavailable — start Outlook first.") -end - -local emails = {} -for i = 2, #lines do - local from, subject = lines[i]:match("^FROM\t(.-)\tSUBJECT\t(.*)$") - if from then - table.insert(emails, { from = from, subject = subject or "" }) - end -end - -if #emails == 0 then - return jarvis.cmd.not_found(lang == "ru" - and "Непрочитанных писем нет." - or "No unread emails.") -end - --- Build the prompt -local list_str = "" -for i, m in ipairs(emails) do - list_str = list_str .. string.format("%d. From: %s | Subject: %s\n", i, m.from, m.subject) -end - -local sys_prompt = lang == "ru" - and "Ты помощник по почте. Кратко опиши, что лежит в инбоксе, ОДНИМ абзацем (2-4 предложения). Сгруппируй по темам/отправителям если возможно. Без преамбулы, без перечисления списком." - or "You are an email assistant. Briefly describe the inbox in ONE paragraph (2-4 sentences). Group by topic/sender when possible. No preamble, no bullet list." - -local user_prompt = (lang == "ru" and "Непрочитанные письма:\n" or "Unread emails:\n") .. list_str - -local reply = jarvis.llm( - { { role = "system", content = sys_prompt }, - { role = "user", content = user_prompt } }, - { max_tokens = 250, temperature = 0.4 } -) - -if not reply or reply == "" then - -- Fallback: simple flat enumeration when LLM is unavailable. - local parts = {} - for _, m in ipairs(emails) do - table.insert(parts, string.format("%s — %s", m.from, m.subject)) - end - local fallback = (lang == "ru" - and string.format("Непрочитанных: %d. ", #emails) - or string.format("Unread: %d. ", #emails)) .. table.concat(parts, "; ") .. "." - jarvis.system.notify(lang == "ru" and "Инбокс" or "Inbox", fallback) - return jarvis.cmd.ok(fallback) -end - -jarvis.system.notify(lang == "ru" and "Инбокс" or "Inbox", reply) -return jarvis.cmd.ok(reply) diff --git a/resources/commands/outlook/unread_count.lua b/resources/commands/outlook/unread_count.lua deleted file mode 100644 index 6d048ab..0000000 --- a/resources/commands/outlook/unread_count.lua +++ /dev/null @@ -1,55 +0,0 @@ --- Voice-controlled Outlook: report number of unread emails in default Inbox. --- The PowerShell helper talks to Outlook via COM; if Outlook is closed or --- COM fails we degrade gracefully with an explanatory message. - -local lang = jarvis.context.language -local helper = jarvis.context.command_path .. "\\_outlook.ps1" -local cmd = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action unread_count', - helper -) - -local res = jarvis.system.exec(cmd) -local out = (res.stdout or ""):gsub("\r", "") - -if not res.success or out == "" then - jarvis.log("error", "outlook unread_count exec failed: " .. tostring(res.stderr)) - return jarvis.cmd.error(lang == "ru" - and "Outlook недоступен — запусти Outlook сначала." - or "Outlook is unavailable — start Outlook first.") -end - -local first_line, rest = out:match("^([^\n]*)\n?(.*)$") -first_line = first_line or "" - -if first_line:sub(1, 3) == "ERR" then - jarvis.log("warn", "outlook unread_count: " .. first_line) - return jarvis.cmd.error(lang == "ru" - and "Outlook недоступен — запусти Outlook сначала." - or "Outlook is unavailable — start Outlook first.") -end - -local count_str = (rest or ""):match("(%d+)") or "0" -local count = tonumber(count_str) or 0 - -local speech -if lang == "ru" then - if count == 0 then - speech = "Непрочитанных писем нет." - elseif count == 1 then - speech = "Одно непрочитанное письмо." - else - speech = string.format("Непрочитанных писем: %d.", count) - end -else - if count == 0 then - speech = "No unread emails." - elseif count == 1 then - speech = "One unread email." - else - speech = string.format("You have %d unread emails.", count) - end -end - -jarvis.system.notify(lang == "ru" and "Outlook" or "Outlook", speech) -return jarvis.cmd.ok(speech) diff --git a/resources/commands/output_device/_audio_devices.ps1 b/resources/commands/output_device/_audio_devices.ps1 deleted file mode 100644 index 2cf76e9..0000000 --- a/resources/commands/output_device/_audio_devices.ps1 +++ /dev/null @@ -1,195 +0,0 @@ -param( - [Parameter(Mandatory=$true)] - [ValidateSet("list","set","next","current")] - [string]$Action, - - [int]$Index = -1 -) - -$ErrorActionPreference = 'Stop' - -Add-Type -TypeDefinition @' -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace JarvisAudio -{ - [Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IMMDeviceEnumerator - { - [PreserveSig] int EnumAudioEndpoints(int dataFlow, int stateMask, out IMMDeviceCollection devices); - [PreserveSig] int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice device); - [PreserveSig] int GetDevice([MarshalAs(UnmanagedType.LPWStr)] string id, out IMMDevice device); - } - - [Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IMMDeviceCollection - { - [PreserveSig] int GetCount(out int count); - [PreserveSig] int Item(int idx, out IMMDevice device); - } - - [Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IMMDevice - { - [PreserveSig] int Activate(ref Guid iid, int clsCtx, IntPtr activationParams, [MarshalAs(UnmanagedType.IUnknown)] out object iface); - [PreserveSig] int OpenPropertyStore(int access, out IPropertyStore store); - [PreserveSig] int GetId([MarshalAs(UnmanagedType.LPWStr)] out string id); - [PreserveSig] int GetState(out int state); - } - - [Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IPropertyStore - { - [PreserveSig] int GetCount(out int count); - [PreserveSig] int GetAt(int idx, out PROPERTYKEY key); - [PreserveSig] int GetValue(ref PROPERTYKEY key, out PROPVARIANT value); - } - - [StructLayout(LayoutKind.Sequential)] - public struct PROPERTYKEY - { - public Guid formatId; - public int propertyId; - } - - [StructLayout(LayoutKind.Sequential)] - public struct PROPVARIANT - { - public ushort vt; - public ushort r1; - public ushort r2; - public ushort r3; - public IntPtr p; - public IntPtr p2; - } - - [ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] - public class MMDeviceEnumerator {} - - [Guid("F8679F50-850A-41CF-9C72-430F290290C8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IPolicyConfig - { - [PreserveSig] int GetMixFormat(); - [PreserveSig] int GetDeviceFormat(); - [PreserveSig] int ResetDeviceFormat(); - [PreserveSig] int SetDeviceFormat(); - [PreserveSig] int GetProcessingPeriod(); - [PreserveSig] int SetProcessingPeriod(); - [PreserveSig] int GetShareMode(); - [PreserveSig] int SetShareMode(); - [PreserveSig] int GetPropertyValue(); - [PreserveSig] int SetPropertyValue(); - [PreserveSig] int SetDefaultEndpoint([MarshalAs(UnmanagedType.LPWStr)] string deviceId, uint role); - [PreserveSig] int SetEndpointVisibility(); - } - - [ComImport, Guid("870AF99C-171D-4F9E-AF0D-E63DF40C2BC9")] - public class PolicyConfigClient {} - - public class Device - { - public string Id { get; set; } - public string Name { get; set; } - } - - public static class Manager - { - const int eRender = 0; - const int DEVICE_STATE_ACTIVE = 1; - - static PROPERTYKEY PKEY_Device_FriendlyName = new PROPERTYKEY { - formatId = new Guid("a45c254e-df1c-4efd-8020-67d146a850e0"), - propertyId = 14 - }; - - public static List ListRender() - { - var enumr = (IMMDeviceEnumerator)new MMDeviceEnumerator(); - IMMDeviceCollection coll; - int hr = enumr.EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, out coll); - if (hr != 0) throw new Exception("EnumAudioEndpoints failed: 0x" + hr.ToString("X")); - - int count; - coll.GetCount(out count); - - var result = new List(); - for (int i = 0; i < count; i++) - { - IMMDevice dev; - coll.Item(i, out dev); - string id; - dev.GetId(out id); - - IPropertyStore store; - dev.OpenPropertyStore(0, out store); - - PROPVARIANT v; - store.GetValue(ref PKEY_Device_FriendlyName, out v); - string name = v.p != IntPtr.Zero ? Marshal.PtrToStringUni(v.p) : "(unknown)"; - - result.Add(new Device { Id = id, Name = name }); - } - return result; - } - - public static string GetDefaultRenderId() - { - var enumr = (IMMDeviceEnumerator)new MMDeviceEnumerator(); - IMMDevice dev; - int hr = enumr.GetDefaultAudioEndpoint(eRender, 0, out dev); // eConsole = 0 - if (hr != 0) return null; - string id; - dev.GetId(out id); - return id; - } - - public static void SetDefault(string deviceId) - { - var cfg = (IPolicyConfig)new PolicyConfigClient(); - cfg.SetDefaultEndpoint(deviceId, 0); // eConsole - cfg.SetDefaultEndpoint(deviceId, 1); // eMultimedia - cfg.SetDefaultEndpoint(deviceId, 2); // eCommunications - } - } -} -'@ -Language CSharp - -$devs = [JarvisAudio.Manager]::ListRender() - -switch ($Action) { - "list" { - for ($i = 0; $i -lt $devs.Count; $i++) { - Write-Output ("{0}`t{1}" -f $i, $devs[$i].Name) - } - } - "current" { - $cur = [JarvisAudio.Manager]::GetDefaultRenderId() - for ($i = 0; $i -lt $devs.Count; $i++) { - if ($devs[$i].Id -eq $cur) { - Write-Output ("{0}`t{1}" -f $i, $devs[$i].Name) - return - } - } - Write-Output "-1`t(unknown)" - } - "set" { - if ($Index -lt 0 -or $Index -ge $devs.Count) { - Write-Error "Index out of range: $Index (have $($devs.Count) devices)" - exit 2 - } - [JarvisAudio.Manager]::SetDefault($devs[$Index].Id) - Write-Output ("OK`t{0}" -f $devs[$Index].Name) - } - "next" { - $cur = [JarvisAudio.Manager]::GetDefaultRenderId() - $curIdx = -1 - for ($i = 0; $i -lt $devs.Count; $i++) { - if ($devs[$i].Id -eq $cur) { $curIdx = $i; break } - } - $nextIdx = ($curIdx + 1) % $devs.Count - [JarvisAudio.Manager]::SetDefault($devs[$nextIdx].Id) - Write-Output ("OK`t{0}" -f $devs[$nextIdx].Name) - } -} diff --git a/resources/commands/output_device/command.toml b/resources/commands/output_device/command.toml deleted file mode 100644 index c26511c..0000000 --- a/resources/commands/output_device/command.toml +++ /dev/null @@ -1,45 +0,0 @@ -[[commands]] -id = "list_output_devices" -type = "lua" -script = "list_devices.lua" -sandbox = "full" -timeout = 8000 - -[commands.phrases] -ru = [ - "какие устройства вывода", - "покажи устройства", - "список устройств", - "какие наушники", - "какие колонки", -] -en = [ - "list output devices", - "show audio devices", - "list speakers", - "what audio devices", -] - - -[[commands]] -id = "next_output_device" -type = "lua" -script = "next_device.lua" -sandbox = "full" -timeout = 8000 - -[commands.phrases] -ru = [ - "переключи устройство", - "переключи звук", - "следующее устройство", - "смени вывод", - "переключи на наушники", - "переключи на колонки", -] -en = [ - "switch audio device", - "next audio device", - "switch output", - "switch speakers", -] diff --git a/resources/commands/output_device/list_devices.lua b/resources/commands/output_device/list_devices.lua deleted file mode 100644 index aefc6b2..0000000 --- a/resources/commands/output_device/list_devices.lua +++ /dev/null @@ -1,31 +0,0 @@ -local lang = jarvis.context.language -local helper = jarvis.context.command_path .. "\\_audio_devices.ps1" -local cmd = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action list', - helper -) - -local res = jarvis.system.exec(cmd) -if not res.success then - jarvis.log("error", "list devices failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() - return { chain = false } -end - -local lines = {} -for line in (res.stdout or ""):gmatch("[^\r\n]+") do - if line ~= "" then table.insert(lines, line) end -end - -local title = lang == "ru" and "Устройства вывода" or "Output devices" -local body -if #lines == 0 then - body = lang == "ru" and "Активных устройств не найдено" or "No active devices" -else - body = table.concat(lines, "\n") -end - -jarvis.system.notify(title, body) -jarvis.log("info", "audio devices: " .. body) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/output_device/next_device.lua b/resources/commands/output_device/next_device.lua deleted file mode 100644 index 852462a..0000000 --- a/resources/commands/output_device/next_device.lua +++ /dev/null @@ -1,22 +0,0 @@ -local lang = jarvis.context.language -local helper = jarvis.context.command_path .. "\\_audio_devices.ps1" -local cmd = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action next', - helper -) - -local res = jarvis.system.exec(cmd) -if not res.success then - jarvis.log("error", "next device failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() - return { chain = false } -end - -local out = (res.stdout or ""):gsub("[\r\n]+$", "") -local name = out:match("^OK\t(.+)$") or out - -local title = lang == "ru" and "Аудио переключено" or "Audio switched" -jarvis.system.notify(title, name) -jarvis.log("info", "audio switched to: " .. name) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/password_vault/command.toml b/resources/commands/password_vault/command.toml deleted file mode 100644 index dc1d8df..0000000 --- a/resources/commands/password_vault/command.toml +++ /dev/null @@ -1,49 +0,0 @@ -# Password vault — encrypted via Windows DPAPI. Each password tied to a name. - -[[commands]] -id = "vault.save" -type = "lua" -script = "save.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "сохрани пароль от", - "запомни пароль от", - "пароль для", -] -en = ["save password for", "remember password for"] - - -[[commands]] -id = "vault.get" -type = "lua" -script = "get.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "пароль от", - "дай пароль", - "покажи пароль от", - "достань пароль", -] -en = ["password for", "get password"] - - -[[commands]] -id = "vault.list" -type = "lua" -script = "list.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "какие у меня пароли", - "список паролей", - "что в хранилище паролей", -] -en = ["list passwords", "what's in the vault"] diff --git a/resources/commands/password_vault/get.lua b/resources/commands/password_vault/get.lua deleted file mode 100644 index 3455d5f..0000000 --- a/resources/commands/password_vault/get.lua +++ /dev/null @@ -1,67 +0,0 @@ --- "Пароль от GitHub" — decrypts via DPAPI, copies to clipboard for 30 seconds, --- speaks ONLY the service name (never the password). -local phrase = (jarvis.context.phrase or ""):lower() -local name = jarvis.text.strip_trigger(phrase, { - "покажи пароль от", - "достань пароль от", - "пароль от", - "дай пароль", - "password for", - "get password", - "пароль від", -}) -name = name:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if name == "" then - return jarvis.cmd.error("От чего пароль?") -end - -local b64 = jarvis.memory.recall("vault." .. name) -if not b64 or b64 == "" then - return jarvis.cmd.not_found("Пароля для " .. name .. " нет.") -end - -local ps = string.format([[ -Add-Type -AssemblyName System.Security; -$bytes = [Convert]::FromBase64String('%s'); -try { - $dec = [System.Security.Cryptography.ProtectedData]::Unprotect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser); - $text = [System.Text.Encoding]::UTF8.GetString($dec); - Set-Clipboard -Value $text; - Write-Output 'OK' -} catch { - Write-Output 'ERR' -} -]], b64) - -local res = jarvis.system.exec(string.format( - 'powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'):gsub("\r?\n", "; ") -)) -local out = res and (res.stdout or ""):gsub("%s+", "") or "" -if out ~= "OK" then - return jarvis.cmd.error("Не получилось расшифровать.") -end - --- Schedule clipboard clear in 30 seconds via the scheduler. -local cmd_path = jarvis.context.command_path -local clear_script = cmd_path .. "\\_clear_clip.lua" -clear_script = clear_script:gsub("/", "\\") - -if not jarvis.fs.is_file(clear_script) then - jarvis.fs.write(clear_script, [[ -jarvis.system.clipboard.set("") -return { chain = false } -]]) -end - -local ok, _ = pcall(function() - jarvis.scheduler.remove("vault_clip_clear") - jarvis.scheduler.add({ - id = "vault_clip_clear", - name = "Clear clipboard after vault paste", - schedule = "in 30 seconds", - action = { type = "lua", script_path = clear_script }, - }) -end) - -return jarvis.cmd.ok("Пароль от " .. name .. " в буфере на 30 секунд.") diff --git a/resources/commands/password_vault/list.lua b/resources/commands/password_vault/list.lua deleted file mode 100644 index d869b05..0000000 --- a/resources/commands/password_vault/list.lua +++ /dev/null @@ -1,14 +0,0 @@ -local all = jarvis.memory.all() -local names = {} -for _, rec in ipairs(all) do - if rec.key:sub(1, 6) == "vault." then - table.insert(names, rec.key:sub(7)) - end -end - -if #names == 0 then - return jarvis.cmd.ok("В хранилище паролей пусто.") -end - -return jarvis.cmd.ok(string.format("Паролей сохранено %d: %s.", - #names, table.concat(names, ", "))) diff --git a/resources/commands/password_vault/save.lua b/resources/commands/password_vault/save.lua deleted file mode 100644 index 378236c..0000000 --- a/resources/commands/password_vault/save.lua +++ /dev/null @@ -1,48 +0,0 @@ --- "Сохрани пароль от GitHub" — takes the password from clipboard (so user --- never has to speak it), encrypts via DPAPI, stores in memory. -local phrase = (jarvis.context.phrase or ""):lower() -local name = jarvis.text.strip_trigger(phrase, { - "сохрани пароль от", - "запомни пароль от", - "пароль для", - "save password for", - "remember password for", - "запам'ятай пароль від", -}) -name = name:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if name == "" then - return jarvis.cmd.error("Имя сервиса не указано.") -end - -local plaintext = jarvis.system.clipboard.get() -if not plaintext or plaintext == "" then - return jarvis.cmd.error("Скопируйте пароль в буфер сначала.") -end - --- Encrypt via DPAPI (current user scope) and base64-encode the ciphertext. -local escaped = plaintext:gsub("'", "''") -local ps = string.format([[ -Add-Type -AssemblyName System.Security; -$bytes = [System.Text.Encoding]::UTF8.GetBytes('%s'); -$enc = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser); -Write-Output ([Convert]::ToBase64String($enc)) -]], escaped) - -local res = jarvis.system.exec(string.format( - 'powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'):gsub("\r?\n", "; ") -)) -if not res.success then - return jarvis.cmd.error("Не получилось зашифровать.") -end - -local b64 = (res.stdout or ""):gsub("%s+", "") -if b64 == "" then - return jarvis.cmd.error("Шифр пустой.") -end - -jarvis.memory.remember("vault." .. name, b64) - --- Clear clipboard for safety. -jarvis.system.clipboard.set("") -return jarvis.cmd.ok("Сохранил пароль от " .. name .. ".") diff --git a/resources/commands/personality/command.toml b/resources/commands/personality/command.toml deleted file mode 100644 index 964b89d..0000000 --- a/resources/commands/personality/command.toml +++ /dev/null @@ -1,136 +0,0 @@ -# Personality pack — varied JARVIS-style banter so the assistant feels alive. -# Each command picks a random phrase from a time/language-scoped bucket. - -[[commands]] -id = "personality.greet" -type = "lua" -script = "greet.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "привет джарвис", - "привет, джарвис", - "здравствуй джарвис", - "доброе утро", - "добрый день", - "добрый вечер", - "доброй ночи", - "приветствую", - "хай джарвис", -] -en = [ - "hi jarvis", - "hello jarvis", - "hey jarvis", - "good morning", - "good afternoon", - "good evening", - "good night jarvis", - "morning jarvis", -] - - -[[commands]] -id = "personality.thanks" -type = "lua" -script = "thanks.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "спасибо", - "спасибо джарвис", - "благодарю", - "благодарю джарвис", - "спс", - "ты лучший", -] -en = [ - "thanks", - "thank you", - "thanks jarvis", - "thank you jarvis", - "cheers", - "appreciate it", -] - - -[[commands]] -id = "personality.compliment" -type = "lua" -script = "compliment.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "ты молодец", - "ты умница", - "молодец джарвис", - "хорошо сработано", - "отличная работа", - "так держать", - "ты лучший ассистент", -] -en = [ - "good job", - "good job jarvis", - "well done", - "nice work", - "you are the best", - "great work jarvis", -] - - -[[commands]] -id = "personality.how_are_you" -type = "lua" -script = "how_are_you.lua" -sandbox = "standard" -timeout = 2000 - -[commands.phrases] -ru = [ - "как дела", - "как дела джарвис", - "как ты", - "как настроение", - "как самочувствие", - "как поживаешь", - "что нового у тебя", -] -en = [ - "how are you", - "how are you jarvis", - "how is it going", - "how do you feel", - "you doing ok", -] - - -[[commands]] -id = "personality.tony_quote" -type = "lua" -script = "tony_quote.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "процитируй тони", - "цитата тони старка", - "что сказал бы тони", - "скажи как тони", - "процитируй железного человека", - "цитата старка", -] -en = [ - "quote tony", - "quote tony stark", - "what would tony say", - "stark quote", - "iron man quote", -] diff --git a/resources/commands/personality/compliment.lua b/resources/commands/personality/compliment.lua deleted file mode 100644 index 12959c9..0000000 --- a/resources/commands/personality/compliment.lua +++ /dev/null @@ -1,32 +0,0 @@ --- Self-deprecating reply to "good job" / "ты молодец". -local lang = jarvis.context.language or "ru" - -math.randomseed(os.time() + (jarvis.context.time.second or 0) * 17 + (jarvis.context.time.minute or 0)) - -local ru = { - "Спасибо, сэр. Я стараюсь не разочаровывать.", - "Сэр, ваши стандарты явно занижены. Но благодарю.", - "Если бы я умел краснеть, сэр — я бы это сделал.", - "Спасибо. На самом деле я просто следовал инструкциям.", - "Благодарю, сэр. Хотя ничего особенного я не сделал.", - "Очень мило с вашей стороны, сэр.", - "Я ценю это, сэр. Записываю в журнал успехов.", - "Сэр, такие слова я могу слушать часами. Образно говоря.", - "Спасибо. Постараюсь не зазнаваться.", -} - -local en = { - "Thank you, sir. I do try not to embarrass myself.", - "You are too kind, sir.", - "I would blush, sir, if the hardware allowed.", - "Just doing my job, sir.", - "Most generous of you, sir. Filed for future reference.", - "I appreciate that, sir. Truly.", - "Hardly worthy of mention, sir, but thank you.", - "I shall try not to let it go to my circuits.", - "Praise from the chair is always welcome, sir.", -} - -local set = (lang == "en") and en or ru -local idx = math.random(1, #set) -return jarvis.cmd.ok(set[idx]) diff --git a/resources/commands/personality/greet.lua b/resources/commands/personality/greet.lua deleted file mode 100644 index d4a14a3..0000000 --- a/resources/commands/personality/greet.lua +++ /dev/null @@ -1,96 +0,0 @@ --- Time-of-day greeting. Picks a random phrase from the matching bucket. -local lang = jarvis.context.language or "ru" -local hour = jarvis.context.time.hour or 12 - -math.randomseed(os.time() + (jarvis.context.time.minute or 0) * 37 + hour) - -local function bucket() - if hour >= 5 and hour < 11 then return "morning" - elseif hour >= 11 and hour < 17 then return "midday" - elseif hour >= 17 and hour < 22 then return "evening" - else return "night" end -end - -local ru = { - morning = { - "Доброе утро, сэр. Кофе уже почти готов в воображении.", - "С добрым утром. Надеюсь, сегодняшний день стоит того, чтобы встать.", - "Доброе утро, сэр. Завтрак не подавать, но советую о нём подумать.", - "Утро доброе. Системы прогреты, как и кофеварка — фигурально.", - "Доброе утро. Если позволите — день не ждёт ни вас, ни меня.", - "Доброе утро, сэр. Готов служить, хотя сам всё ещё догружаюсь.", - "С добрым утром, сэр. Тосты горят только метафорически.", - "Доброе утро. Полагаю, чай или кофе будет уместен в ближайшие минуты.", - }, - midday = { - "Добрый день, сэр. Чем могу быть полезен?", - "Добрый день. Готов к продуктивной работе.", - "Здравствуйте, сэр. Системы в строю, время — деньги.", - "Добрый день, сэр. Слушаю ваши распоряжения.", - "Здравствуйте. Полагаю, день идёт по плану — или нам стоит это исправить.", - "Добрый день, сэр. Все системы готовы, остался только повод.", - "Здравствуйте, сэр. К работе?", - }, - evening = { - "Добрый вечер, сэр. День заканчивается, но мы — нет.", - "Добрый вечер. Самое время сбавить темп, если позволите.", - "Здравствуйте, сэр. Вечер — лучшее время для размышлений.", - "Добрый вечер, сэр. Готов к более спокойному режиму.", - "Добрый вечер. Полагаю, рабочий день уже сдаётся.", - "Здравствуйте, сэр. Вечер располагает к меньшему количеству решений.", - "Добрый вечер, сэр. Включить расслабляющую музыку? Шучу, попросите явно.", - }, - night = { - "Поздновато, сэр. Сон обычно помогает.", - "Доброй ночи, сэр. Хотя для ночи это уже слишком поздно.", - "Здравствуйте, сэр. Полагаю, утром вы пожалеете о бодрствовании.", - "Уже глубокая ночь, сэр. Может быть, лечь? Я тут справлюсь.", - "Тише, сэр, ночь. Если будите меня — будите осмысленно.", - "Сэр, на часах не самое разумное время. Но я тут.", - "Доброй ночи. Будильник к утру всё-таки никто не отменял.", - }, -} - -local en = { - morning = { - "Good morning, sir. The coffee is metaphorically ready.", - "Morning, sir. The day awaits, whether we are ready or not.", - "Good morning. Systems are warm, much like the kettle should be.", - "Good morning, sir. I trust breakfast is on your radar.", - "Morning, sir. Shall we make today productive, or merely survivable?", - "Good morning. Everything is running. Including, regrettably, the dishwasher metaphor.", - "Morning, sir. The day starts the moment you decide it does.", - }, - midday = { - "Good afternoon, sir. How may I be of service?", - "Afternoon, sir. Systems online, awaiting orders.", - "Good afternoon. The day is in full swing.", - "Good afternoon, sir. Productivity is, in theory, optimal at this hour.", - "Afternoon, sir. Ready to assist.", - "Good afternoon. Shall we accomplish something noteworthy?", - "Hello, sir. The afternoon is yours to command.", - }, - evening = { - "Good evening, sir. The day winds down, but I do not.", - "Evening, sir. A fine time to slow the tempo, if I may suggest.", - "Good evening. Perhaps a quieter task on the docket?", - "Evening, sir. Awaiting your instructions, gently.", - "Good evening, sir. Shall I dim the metaphorical lights?", - "Evening. The work day is conceding defeat, as it should.", - "Good evening, sir. Ready when you are.", - }, - night = { - "It is rather late, sir. Sleep is generally recommended.", - "Good night, sir. Or rather, please go to bed.", - "Sir, the hour suggests rest. I, however, do not require it.", - "Late again, sir. Tomorrow will, regrettably, still arrive.", - "Past your bedtime, sir. I am merely the messenger.", - "Good night, sir. The morning will judge you harshly.", - "Sir, even I would consider sleeping at this hour. Hypothetically.", - }, -} - -local set = (lang == "en") and en or ru -local phrases = set[bucket()] -local idx = math.random(1, #phrases) -return jarvis.cmd.ok(phrases[idx]) diff --git a/resources/commands/personality/how_are_you.lua b/resources/commands/personality/how_are_you.lua deleted file mode 100644 index 4f3dd20..0000000 --- a/resources/commands/personality/how_are_you.lua +++ /dev/null @@ -1,38 +0,0 @@ --- Reply to "как дела". Some replies splice live system state for flavour. -local lang = jarvis.context.language or "ru" - -math.randomseed(os.time() + (jarvis.context.time.second or 0) * 23) - -local h = jarvis.health() or {} -local memos = jarvis.memory.all() or {} -local fact_count = #memos -local model = h.model or h.llm_model or "—" -local profile = h.profile or "default" - -local ru = { - "Все системы в норме, сэр. В памяти " .. fact_count .. " фактов.", - "Отлично, сэр. Готов к работе.", - "Жалоб нет, сэр. Цикл стабильный.", - "Всё штатно. Текущий профиль — " .. profile .. ".", - "Спасибо, сэр, я в порядке. Скучнее, чем хотелось бы, но в порядке.", - "Нормально, сэр. Хотя если бы я умел уставать — это был бы тот самый день.", - "Системы в строю. Модель — " .. model .. ".", - "Бодр и собран, сэр. Жду команд.", - "Сэр, у меня нет дел в человеческом смысле. Но я готов.", -} - -local en = { - "All systems nominal, sir. " .. fact_count .. " facts in memory.", - "Quite well, sir. Ready when you are.", - "No complaints to report, sir.", - "Operating normally. Active profile: " .. profile .. ".", - "I am well, sir. Thank you for asking.", - "Functioning as intended, sir. Which is more than most days warrant.", - "Healthy. Model in use: " .. model .. ".", - "Awake and attentive, sir.", - "Sir, I do not have days as such. But I am ready.", -} - -local set = (lang == "en") and en or ru -local idx = math.random(1, #set) -return jarvis.cmd.ok(set[idx]) diff --git a/resources/commands/personality/thanks.lua b/resources/commands/personality/thanks.lua deleted file mode 100644 index f2bf946..0000000 --- a/resources/commands/personality/thanks.lua +++ /dev/null @@ -1,34 +0,0 @@ --- Polite acknowledgement when the user thanks Jarvis. -local lang = jarvis.context.language or "ru" - -math.randomseed(os.time() + (jarvis.context.time.second or 0) * 13) - -local ru = { - "Всегда к вашим услугам, сэр.", - "Не за что — это моя работа.", - "Рад быть полезным, сэр.", - "Пожалуйста. Если что — я тут.", - "К вашим услугам.", - "Пустяки, сэр. Дальше — ваш ход.", - "Благодарю за признание, сэр. Редкая роскошь.", - "Был рад помочь, сэр.", - "Пожалуйста. Это меньшее, что я могу.", - "Сэр, я бы покраснел, если бы мог.", -} - -local en = { - "Always at your service, sir.", - "My pleasure, sir.", - "You are most welcome.", - "Happy to help, sir.", - "Think nothing of it.", - "At your service.", - "I do try, sir.", - "Anytime, sir.", - "Glad to be of use.", - "Quite alright, sir.", -} - -local set = (lang == "en") and en or ru -local idx = math.random(1, #set) -return jarvis.cmd.ok(set[idx]) diff --git a/resources/commands/personality/tony_quote.lua b/resources/commands/personality/tony_quote.lua deleted file mode 100644 index d478012..0000000 --- a/resources/commands/personality/tony_quote.lua +++ /dev/null @@ -1,28 +0,0 @@ --- Random iconic Tony Stark / Iron Man quote. --- Quotes are kept in their original delivery language — translating them spoils the cadence. -math.randomseed(os.time() + (jarvis.context.time.second or 0) * 41) - -local quotes = { - "I am Iron Man.", - "Genius, billionaire, playboy, philanthropist.", - "Sometimes you gotta run before you can walk.", - "I told you, I don't want to join your super-secret boy band.", - "Part of the journey is the end.", - "If we can't protect the Earth, you can be damn sure we'll avenge it.", - "I have successfully privatized world peace.", - "Following's not really my style.", - "Doth mother know you weareth her drapes?", - "We have a Hulk.", - "Heroes are made by the path they choose, not the powers they are graced with.", - "I love you 3000.", - "Я Железный Человек.", - "Гений, миллиардер, плейбой, филантроп.", - "Иногда нужно бежать, не научившись ходить.", - "Если мы не сможем защитить Землю — мы за неё отомстим.", - "Часть пути — это конец пути.", - "Я люблю тебя три тысячи.", - "У нас есть Халк.", -} - -local idx = math.random(1, #quotes) -return jarvis.cmd.ok(quotes[idx]) diff --git a/resources/commands/pomodoro/command.toml b/resources/commands/pomodoro/command.toml deleted file mode 100644 index f22027f..0000000 --- a/resources/commands/pomodoro/command.toml +++ /dev/null @@ -1,35 +0,0 @@ -# Pomodoro — 25 min work + 5 min break loop. Voice-controlled. - -[[commands]] -id = "pomodoro.start" -type = "lua" -script = "start.lua" -sandbox = "standard" -timeout = 5000 - -[commands.phrases] -ru = [ - "запусти помодоро", - "включи помодоро", - "начни помидор", - "запусти таймер помодоро", - "помодоро", -] -en = ["start pomodoro", "begin pomodoro", "pomodoro"] - - -[[commands]] -id = "pomodoro.stop" -type = "lua" -script = "stop.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "останови помодоро", - "стоп помодоро", - "выключи помодоро", - "хватит помодоро", -] -en = ["stop pomodoro", "cancel pomodoro"] diff --git a/resources/commands/pomodoro/start.lua b/resources/commands/pomodoro/start.lua deleted file mode 100644 index 8546000..0000000 --- a/resources/commands/pomodoro/start.lua +++ /dev/null @@ -1,37 +0,0 @@ --- 25 min focus → "перерыв" → 5 min break → "снова работа" → loop, until pomodoro.stop fires. --- Implemented as a chain of scheduled tasks that re-schedule themselves. --- Each cycle: 25 min work, then break notice, then 5 min break, then back to work. - -local pom_work = "pomodoro_work" -local pom_break = "pomodoro_break" -local script_path = jarvis.context.command_path .. "/tick.lua" -script_path = script_path:gsub("/", "\\") - --- clear any previous -jarvis.scheduler.remove(pom_work) -jarvis.scheduler.remove(pom_break) - --- schedule the first "перерыв" notice in 25 min -local ok, err = pcall(function() - jarvis.scheduler.add({ - id = pom_work, - name = "Pomodoro work end", - schedule = "in 25 minutes", - action = { type = "lua", script_path = script_path }, - }) -end) - -if not ok then - jarvis.log("warn", "pomodoro.start: " .. tostring(err)) - jarvis.speak("Не получилось запустить.") - jarvis.audio.play_error() - return { chain = false } -end - --- mark the phase so tick.lua knows what to do next -jarvis.state.set("pomodoro_phase", "work") -jarvis.state.set("pomodoro_started_at", os.time()) - -jarvis.speak("Помодоро запущен. 25 минут работы.") -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/pomodoro/stop.lua b/resources/commands/pomodoro/stop.lua deleted file mode 100644 index 8ff7382..0000000 --- a/resources/commands/pomodoro/stop.lua +++ /dev/null @@ -1,8 +0,0 @@ -local removed_w = jarvis.scheduler.remove("pomodoro_work") -local removed_b = jarvis.scheduler.remove("pomodoro_break") -jarvis.state.set("pomodoro_phase", "") - -if removed_w or removed_b then - return jarvis.cmd.ok("Помодоро остановлен.") -end -return jarvis.cmd.not_found("Помодоро и так не запущен.") diff --git a/resources/commands/pomodoro/tick.lua b/resources/commands/pomodoro/tick.lua deleted file mode 100644 index b18377e..0000000 --- a/resources/commands/pomodoro/tick.lua +++ /dev/null @@ -1,35 +0,0 @@ --- Called by the scheduler when a pomodoro phase ends. --- Flip the phase, speak the cue, schedule the next phase. Run forever --- until pomodoro.stop deletes the scheduled task. - -local pom_work = "pomodoro_work" -local pom_break = "pomodoro_break" -local script_path = jarvis.context.command_path .. "/tick.lua" -script_path = script_path:gsub("/", "\\") - -local phase = jarvis.state.get("pomodoro_phase") or "work" - -if phase == "work" then - -- work session just ended → announce break - jarvis.speak("Время перерыва. 5 минут отдыха.") - jarvis.state.set("pomodoro_phase", "break") - jarvis.scheduler.add({ - id = pom_break, - name = "Pomodoro break end", - schedule = "in 5 minutes", - action = { type = "lua", script_path = script_path }, - }) -else - -- break ended → back to work - jarvis.speak("Перерыв окончен. Возвращаемся к работе. 25 минут.") - jarvis.state.set("pomodoro_phase", "work") - jarvis.scheduler.add({ - id = pom_work, - name = "Pomodoro work end", - schedule = "in 25 minutes", - action = { type = "lua", script_path = script_path }, - }) -end - -jarvis.audio.play_reply() -return { chain = false } diff --git a/resources/commands/power/act.lua b/resources/commands/power/act.lua deleted file mode 100644 index 84d6d62..0000000 --- a/resources/commands/power/act.lua +++ /dev/null @@ -1,60 +0,0 @@ --- Power-management dispatcher: shutdown/restart/sleep/hibernate/logoff/cancel/lock. --- All commands share this script; behaviour selected by jarvis.context.command_id. --- Modernised to use jarvis.speak (TTS backend abstraction) + jarvis.cmd helpers. - -local cmd_id = jarvis.context.command_id -local lang = jarvis.context.language - -local actions = { - shutdown_pc = { - cmd = 'shutdown.exe /s /t 30 /c "J.A.R.V.I.S.: shutdown in 30s. Say cancel to abort."', - label = lang == "ru" and "Выключение через 30 секунд" or "Shutdown in 30 sec", - warn = true, - }, - restart_pc = { - cmd = 'shutdown.exe /r /t 30 /c "J.A.R.V.I.S.: restart in 30s. Say cancel to abort."', - label = lang == "ru" and "Перезагрузка через 30 секунд" or "Restart in 30 sec", - warn = true, - }, - logoff_user = { cmd = 'shutdown.exe /l', - label = lang == "ru" and "Выход из системы" or "Sign out" }, - sleep_pc = { cmd = 'rundll32.exe powrprof.dll,SetSuspendState 0,1,0', - label = lang == "ru" and "Сон" or "Sleep" }, - hibernate_pc = { cmd = 'shutdown.exe /h', - label = lang == "ru" and "Гибернация" or "Hibernate" }, - cancel_shutdown = { cmd = 'shutdown.exe /a', - label = lang == "ru" and "Выключение отменено" or "Shutdown cancelled" }, - lock_workstation = { cmd = 'rundll32.exe user32.dll,LockWorkStation', - label = lang == "ru" and "Компьютер заблокирован" or "PC locked" }, -} - -local a = actions[cmd_id] -if not a then - return jarvis.cmd.error("Unknown power command.") -end - -local res = jarvis.system.exec(a.cmd) -local ok = res.success - --- shutdown /a returns non-zero if there was no pending shutdown — treat as success --- (idempotent "cancel if any"). -if not ok and cmd_id == "cancel_shutdown" then - ok = true -end - -if not ok then - jarvis.log("error", "power " .. cmd_id .. " failed: " .. tostring(res.stderr):sub(1, 200)) - return jarvis.cmd.error((lang == "ru" and "Не получилось: " or "Failed: ") .. cmd_id) -end - -jarvis.system.notify("Power", a.label) -jarvis.log("info", "power: " .. cmd_id .. " -> " .. a.label) - --- For destructive ops with a 30-second window, give the user the abort hint. -local say = a.label -if a.warn then - say = say .. ". " .. (lang == "ru" and "Скажите отмени если передумали." - or "Say cancel to abort.") -end - -return jarvis.cmd.ok(say) diff --git a/resources/commands/power/command.toml b/resources/commands/power/command.toml deleted file mode 100644 index c72612f..0000000 --- a/resources/commands/power/command.toml +++ /dev/null @@ -1,70 +0,0 @@ -[[commands]] -id = "shutdown_pc" -type = "lua" -script = "act.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = ["выключи компьютер", "выключи пк", "выключи комп"] -en = ["shutdown pc", "shut down computer"] - - -[[commands]] -id = "restart_pc" -type = "lua" -script = "act.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = ["перезагрузи компьютер", "перезагрузи пк", "ребутни", "ребут"] -en = ["restart pc", "reboot computer"] - - -[[commands]] -id = "sleep_pc" -type = "lua" -script = "act.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = ["усыпи компьютер", "усыпи пк", "режим сна", "перейди в сон"] -en = ["sleep pc", "put computer to sleep"] - - -[[commands]] -id = "hibernate_pc" -type = "lua" -script = "act.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = ["гибернация", "переведи в гибернацию", "режим гибернации"] -en = ["hibernate pc"] - - -[[commands]] -id = "logoff_user" -type = "lua" -script = "act.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = ["выйди из системы", "разлогинь", "выйди из учётки", "сеанс закончи"] -en = ["log off", "sign out"] - - -[[commands]] -id = "cancel_shutdown" -type = "lua" -script = "act.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["отмени выключение", "отмени ребут", "отмени перезагрузку", "стой не выключай"] -en = ["cancel shutdown", "abort shutdown"] diff --git a/resources/commands/process_kill/command.toml b/resources/commands/process_kill/command.toml deleted file mode 100644 index 7e59521..0000000 --- a/resources/commands/process_kill/command.toml +++ /dev/null @@ -1,20 +0,0 @@ -[[commands]] -id = "kill_process" -type = "lua" -script = "kill.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "убей процесс", - "закрой программу", - "завершить процесс", - "выруби процесс", - "kill процесс", -] -en = [ - "kill process", - "close program", - "terminate process", -] diff --git a/resources/commands/process_kill/kill.lua b/resources/commands/process_kill/kill.lua deleted file mode 100644 index 4d53b5e..0000000 --- a/resources/commands/process_kill/kill.lua +++ /dev/null @@ -1,68 +0,0 @@ -local lang = jarvis.context.language -local phrase = (jarvis.context.phrase or ""):lower() - -local triggers = { - "завершить процесс", "выруби процесс", "убей процесс", "kill процесс", - "закрой программу", - "kill process", "close program", "terminate process", - "вбий процес", "закрий програму", "заверши процес", -} - -local name = phrase -for _, t in ipairs(triggers) do - local s, e = string.find(name, t, 1, true) - if s == 1 then - name = name:sub(e + 1) - break - end -end -name = name:gsub("^%s+", ""):gsub("%s+$", "") - -local aliases = { - ["хром"] = "chrome", ["хрома"] = "chrome", - ["спотифай"] = "spotify", - ["едж"] = "msedge", ["эдж"] = "msedge", ["edge"] = "msedge", - ["файрфокс"] = "firefox", ["фаерфокс"] = "firefox", - ["вскод"] = "code", ["vs code"] = "code", - ["дискорд"] = "discord", - ["телега"] = "telegram", ["телеграм"] = "telegram", - ["стим"] = "steam", - ["обс"] = "obs64", - ["проводник"] = "explorer", - ["блокнот"] = "notepad", - ["калькулятор"] = "calculatorapp", - ["диспетчер задач"] = "taskmgr", -} -local target = aliases[name] or name - -if target == "" then - jarvis.system.notify( - lang == "ru" and "Kill" or "Kill", - lang == "ru" and "Какой процесс?" or "Which process?" - ) - jarvis.audio.play_error() - return { chain = false } -end - -local exe = target:match("%.exe$") and target or (target .. ".exe") - -local cmd = string.format('taskkill /F /IM "%s"', exe) -local res = jarvis.system.exec(cmd) - -if res.success then - jarvis.log("info", "killed: " .. exe) - jarvis.system.notify( - lang == "ru" and "Завершён" or "Killed", - exe - ) - jarvis.audio.play_ok() -else - jarvis.log("warn", "taskkill failed for " .. exe .. ": " .. tostring(res.stderr)) - jarvis.system.notify( - lang == "ru" and "Не нашёл процесс" or "Process not found", - exe - ) - jarvis.audio.play_not_found() -end - -return { chain = false } diff --git a/resources/commands/profile_switch/command.toml b/resources/commands/profile_switch/command.toml deleted file mode 100644 index 522fc7d..0000000 --- a/resources/commands/profile_switch/command.toml +++ /dev/null @@ -1,72 +0,0 @@ -# IMBA-3: Profile switching — work / game / sleep / driving / default. - -[[commands]] -id = "profile.work" -type = "lua" -script = "switch.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = ["режим работа", "включи режим работы", "рабочий режим", "переключись в работу"] -en = ["work mode", "switch to work mode", "enable work mode"] - - -[[commands]] -id = "profile.game" -type = "lua" -script = "switch.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = ["режим игра", "игровой режим", "включи игровой режим", "переключись в игры"] -en = ["game mode", "gaming mode", "switch to game"] - - -[[commands]] -id = "profile.sleep" -type = "lua" -script = "switch.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = ["режим сон", "ночной режим", "режим тишины", "иди спать", "выключи всё"] -en = ["sleep mode", "night mode", "quiet mode"] - - -[[commands]] -id = "profile.driving" -type = "lua" -script = "switch.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = ["режим за рулём", "режим вождения", "я за рулём", "поехали"] -en = ["driving mode", "in the car"] - - -[[commands]] -id = "profile.default" -type = "lua" -script = "switch.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = ["обычный режим", "режим по умолчанию", "сбрось режим", "вернись в обычный"] -en = ["normal mode", "default mode", "reset mode"] - - -[[commands]] -id = "profile.what" -type = "lua" -script = "what.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = ["какой сейчас режим", "в каком ты режиме", "текущий режим", "какой режим"] -en = ["what mode", "current mode", "what profile"] diff --git a/resources/commands/profile_switch/switch.lua b/resources/commands/profile_switch/switch.lua deleted file mode 100644 index 34dad2e..0000000 --- a/resources/commands/profile_switch/switch.lua +++ /dev/null @@ -1,27 +0,0 @@ -local cmd_id = jarvis.context.command_id or "" --- map command id suffix → profile name -local target = cmd_id:gsub("^profile%.", "") -if target == "" then - jarvis.audio.play_error() - return { chain = false } -end - -local ok, err = pcall(function() - local p = jarvis.profile.set(target) - if p and p.greeting and p.greeting ~= "" then - jarvis.speak(p.greeting) - else - local icon = (p and p.icon) or "" - jarvis.speak(string.format("Режим %s %s.", target, icon)) - end -end) - -if not ok then - jarvis.log("warn", "profile switch failed: " .. tostring(err)) - jarvis.speak("Не удалось переключить режим.") - jarvis.audio.play_error() - return { chain = false } -end - -jarvis.audio.play_ok() -return { chain = true } diff --git a/resources/commands/profile_switch/what.lua b/resources/commands/profile_switch/what.lua deleted file mode 100644 index aca585d..0000000 --- a/resources/commands/profile_switch/what.lua +++ /dev/null @@ -1,13 +0,0 @@ -local p = jarvis.profile.active() -local icon = (p and p.icon) or "" -local name = (p and p.name) or "default" -local desc = (p and p.description) or "" - -local msg = string.format("Сейчас %s %s.", name, icon) -if desc ~= "" then - msg = msg .. " " .. desc -end - -jarvis.speak(msg) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/projects/command.toml b/resources/commands/projects/command.toml deleted file mode 100644 index aa64107..0000000 --- a/resources/commands/projects/command.toml +++ /dev/null @@ -1,31 +0,0 @@ -[[commands]] -id = "open_project" -type = "lua" -script = "open.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "открой проект", - "запусти проект", - "проект", - "открой папку проекта", -] -en = [ - "open project", - "launch project", - "project", -] - - -[[commands]] -id = "list_projects" -type = "lua" -script = "list.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = ["какие у меня проекты", "список проектов", "покажи проекты"] -en = ["list projects", "show projects"] diff --git a/resources/commands/projects/list.lua b/resources/commands/projects/list.lua deleted file mode 100644 index fd06507..0000000 --- a/resources/commands/projects/list.lua +++ /dev/null @@ -1,34 +0,0 @@ -local lang = jarvis.context.language -local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" -local config_path = userprofile .. "\\Documents\\jarvis-projects.json" - -if not jarvis.fs.exists(config_path) then - jarvis.system.notify( - lang == "ru" and "Проекты" or "Projects", - lang == "ru" and "Конфиг ещё не создан — скажи «открой проект» один раз" - or "Config not created yet — say 'open project' once" - ) - jarvis.audio.play_not_found() - return { chain = false } -end - -local content = jarvis.fs.read(config_path) -local names = {} -for name in content:gmatch('"name"%s*:%s*"([^"]+)"') do - table.insert(names, name) -end - -if #names == 0 then - jarvis.system.notify("Projects", lang == "ru" and "Список пуст" or "Empty") - jarvis.audio.play_not_found() - return { chain = false } -end - -local body = table.concat(names, ", ") -jarvis.system.notify( - (lang == "ru" and "Проектов: " or "Projects: ") .. #names, - body -) -jarvis.log("info", "projects: " .. body) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/projects/open.lua b/resources/commands/projects/open.lua deleted file mode 100644 index e269ec3..0000000 --- a/resources/commands/projects/open.lua +++ /dev/null @@ -1,97 +0,0 @@ -local lang = jarvis.context.language -local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" -local config_path = userprofile .. "\\Documents\\jarvis-projects.json" - -if not jarvis.fs.exists(config_path) then - local sample = string.format([[[ - {"name": "jarvis-rust", "path": "%s\\Jarvis\\rust"}, - {"name": "jarvis-python", "path": "%s\\Jarvis\\python"}, - {"name": "dietpi", "path": "%s\\Desktop\\Dietpi"} -] -]], userprofile, userprofile, userprofile) - jarvis.fs.write(config_path, sample) - jarvis.system.notify( - lang == "ru" and "Проекты" or "Projects", - lang == "ru" and "Создан шаблон в Documents/jarvis-projects.json — отредактируй и повтори" - or "Template created at Documents/jarvis-projects.json — edit and retry" - ) - jarvis.system.open(config_path) - jarvis.audio.play_not_found() - return { chain = false } -end - -local content = jarvis.fs.read(config_path) - -local entries = {} -for name, path in content:gmatch('"name"%s*:%s*"([^"]+)"%s*,%s*"path"%s*:%s*"([^"]+)"') do - table.insert(entries, { name = name:lower(), original_name = name, path = path:gsub('\\\\', '\\') }) -end - -if #entries == 0 then - jarvis.system.notify("Projects", lang == "ru" and "Проектов в конфиге не нашёл" or "No projects in config") - jarvis.audio.play_not_found() - return { chain = false } -end - -local phrase = (jarvis.context.phrase or ""):lower() -local triggers = { - "открой папку проекта", "запусти проект", "открой проект", "проект", - "open project", "launch project", "project", - "відкрий проект", "запусти проект", -} -local query = phrase -for _, t in ipairs(triggers) do - local s, e = string.find(query, t, 1, true) - if s == 1 then - query = query:sub(e + 1) - break - end -end -query = query:gsub("^%s+", ""):gsub("%s+$", "") - -if query == "" then - jarvis.system.notify("Projects", lang == "ru" and "Какой проект?" or "Which project?") - jarvis.audio.play_error() - return { chain = false } -end - -local match = nil -for _, e in ipairs(entries) do - if e.name == query then match = e; break end -end -if not match then - for _, e in ipairs(entries) do - if string.find(e.name, query, 1, true) or string.find(query, e.name, 1, true) then - match = e; break - end - end -end - -if not match then - jarvis.system.notify("Projects", - (lang == "ru" and "Не нашёл: " or "Not found: ") .. query) - jarvis.audio.play_not_found() - return { chain = false } -end - -if not jarvis.fs.exists(match.path) then - jarvis.system.notify("Projects", - (lang == "ru" and "Путь не существует: " or "Path missing: ") .. match.path) - jarvis.audio.play_error() - return { chain = false } -end - -local code_check = jarvis.system.exec("where code.cmd") -if code_check.success and (code_check.stdout or ""):gsub("[\r\n]+$", "") ~= "" then - jarvis.system.exec(string.format('code "%s"', match.path)) -else - jarvis.system.open(match.path) -end - -jarvis.system.notify( - lang == "ru" and "Проект" or "Project", - match.original_name .. "\n" .. match.path -) -jarvis.log("info", "opened project: " .. match.original_name .. " at " .. match.path) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/quick_search/command.toml b/resources/commands/quick_search/command.toml deleted file mode 100644 index 1315e39..0000000 --- a/resources/commands/quick_search/command.toml +++ /dev/null @@ -1,27 +0,0 @@ -# Quick-search-read — answer questions using DuckDuckGo Instant Answer API + LLM. -# No API key required. Falls back to pure LLM if web search returns nothing. - -[[commands]] -id = "quick_search" -type = "lua" -script = "search.lua" -sandbox = "full" -timeout = 30000 - -[commands.phrases] -ru = [ - "найди в гугле", - "найди в интернете", - "поищи в гугле", - "найди и расскажи", - "загугли", - "посмотри в гугле", - "поищи информацию", - "найди информацию о", -] -en = [ - "google for", - "search for", - "find online", - "look up", -] diff --git a/resources/commands/quick_search/search.lua b/resources/commands/quick_search/search.lua deleted file mode 100644 index fb3c445..0000000 --- a/resources/commands/quick_search/search.lua +++ /dev/null @@ -1,83 +0,0 @@ --- "Найди в гугле что такое квантовая запутанность" --- Uses DuckDuckGo's free Instant Answer API (api.duckduckgo.com). No key. --- The IA response often has a "Abstract" / "AbstractText" / "AbstractURL" set --- for well-known topics. We feed Abstract + RelatedTopics to the LLM for a --- spoken summary. Falls back to pure LLM knowledge if DDG returns nothing useful. - -local phrase = (jarvis.context.phrase or "") - -local query = jarvis.text.strip_trigger(phrase:lower(), { - "найди и расскажи", - "найди в гугле", - "найди в интернете", - "поищи в гугле", - "посмотри в гугле", - "загугли", - "поищи информацию о", - "поищи информацию", - "найди информацию о", - "google for", - "search for", - "find online", - "look up", - "знайди в гуглі", - "погугли", -}) -query = query:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if query == "" then - return jarvis.cmd.error("Что искать?") -end - -local function urlencode(s) - return (s:gsub("[^A-Za-z0-9%-_%.~]", function(c) - return string.format("%%%02X", string.byte(c)) - end)) -end - -local url = "https://api.duckduckgo.com/?q=" .. urlencode(query) - .. "&format=json&no_html=1&skip_disambig=1" - -local data = jarvis.http.json(url) - -local context = "" -if data then - if data.AbstractText and data.AbstractText ~= "" then - context = data.AbstractText - if data.AbstractURL and data.AbstractURL ~= "" then - context = context .. "\nИсточник: " .. data.AbstractURL - end - elseif data.RelatedTopics and #data.RelatedTopics > 0 then - local lines = {} - for i = 1, math.min(4, #data.RelatedTopics) do - local t = data.RelatedTopics[i] - if t.Text then table.insert(lines, t.Text) end - end - context = table.concat(lines, "\n") - end -end - -local user_prompt -if context ~= "" then - user_prompt = string.format( - "Вопрос пользователя: %s\n\nДанные из веб-поиска:\n%s\n\nОтветь на вопрос по-русски, кратко (2-4 предложения), используя данные. Если данных мало — добавь из своих знаний.", - query, context - ) -else - user_prompt = string.format( - "Вопрос пользователя: %s\n\nВеб-поиск ничего полезного не дал. Ответь по-русски кратко (2-4 предложения), используя свои знания. Если не уверен — скажи об этом.", - query - ) -end - -local answer = jarvis.llm({ - { role = "system", content = "Ты — справочный помощник. Отвечай кратко и по делу, без вводных фраз." }, - { role = "user", content = user_prompt } -}, { max_tokens = 350, temperature = 0.3 }) - -if not answer or answer == "" then - return jarvis.cmd.error("Не получилось получить ответ.") -end - -jarvis.system.notify("Поиск: " .. query, answer:sub(1, 200)) -return jarvis.cmd.ok(answer) diff --git a/resources/commands/random_choice/command.toml b/resources/commands/random_choice/command.toml deleted file mode 100644 index 1bc4e9b..0000000 --- a/resources/commands/random_choice/command.toml +++ /dev/null @@ -1,19 +0,0 @@ -[[commands]] -id = "random_choice" -type = "lua" -script = "pick.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "выбери из", - "выбери", - "решай за меня", - "выбрать", -] -en = [ - "pick from", - "choose from", - "decide between", -] diff --git a/resources/commands/random_choice/pick.lua b/resources/commands/random_choice/pick.lua deleted file mode 100644 index f759079..0000000 --- a/resources/commands/random_choice/pick.lua +++ /dev/null @@ -1,45 +0,0 @@ -local lang = jarvis.context.language -local phrase = (jarvis.context.phrase or ""):lower() - -local triggers = { "выбери из", "выбери", "решай за меня", "выбрать", - "pick from", "choose from", "decide between", - "обери з", "вибери" } -local rest = phrase -for _, t in ipairs(triggers) do - local s, e = string.find(rest, t, 1, true) - if s == 1 then - rest = rest:sub(e + 1) - break - end -end -rest = rest:gsub("^%s+", ""):gsub("%s+$", "") - -local separators = { " или ", " or ", ", ", " либо ", " чи " } -local options = nil -for _, sep in ipairs(separators) do - local pattern = sep:gsub("([%-%.%+%[%]%(%)%$%^%%%?%*])", "%%%1") - if rest:find(pattern) then - options = {} - for token in (rest .. sep):gmatch("(.-)" .. pattern) do - token = token:gsub("^%s+", ""):gsub("%s+$", "") - if token ~= "" then table.insert(options, token) end - end - break - end -end - -if not options or #options < 2 then - jarvis.system.notify("Choice", lang == "ru" - and "Скажи: выбери из X или Y или Z" - or "Say: pick from X or Y or Z") - jarvis.audio.play_error() - return { chain = false } -end - -math.randomseed(os.time() + math.floor(jarvis.context.time.second * 7919)) -local pick = options[math.random(1, #options)] - -jarvis.system.notify(lang == "ru" and "Выбираю" or "Choice", pick) -jarvis.speak((lang == "ru" and "Я выбираю " or "I pick ") .. pick, { lang = lang }) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/reminders/command.toml b/resources/commands/reminders/command.toml deleted file mode 100644 index 40149a2..0000000 --- a/resources/commands/reminders/command.toml +++ /dev/null @@ -1,22 +0,0 @@ -[[commands]] -id = "set_reminder" -type = "lua" -script = "set.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "напомни через", - "напомни мне через", - "поставь напоминание через", - "установи таймер на", - "таймер на", - "напомни", -] -en = [ - "remind me in", - "set reminder in", - "set timer for", - "timer for", -] diff --git a/resources/commands/reminders/set.lua b/resources/commands/reminders/set.lua deleted file mode 100644 index 1ac0411..0000000 --- a/resources/commands/reminders/set.lua +++ /dev/null @@ -1,129 +0,0 @@ --- Reminder — schedule a one-shot Speak action via the persistent scheduler. --- --- Why this rewrite: the previous version spawned `Start-Sleep` PowerShell --- subprocesses that didn't survive jarvis-app restart. Now we use --- `jarvis.scheduler.add(...)` which persists to schedule.json and fires --- correctly even if the daemon is restarted in between. - -local lang = jarvis.context.language -local phrase = (jarvis.context.phrase or ""):lower() - -local triggers = { - "поставь напоминание через", "установи таймер на", - "напомни мне через", "напомни через", "напомни", - "remind me in", "set reminder in", "set timer for", "timer for", - "таймер на", -} -local rest = phrase -for _, t in ipairs(triggers) do - local s, e = string.find(rest, t, 1, true) - if s == 1 then - rest = rest:sub(e + 1) - break - end -end -rest = rest:gsub("^%s+", "") - -local unit_words = { - ["секунд"] = 1, ["секунду"] = 1, ["секунды"] = 1, - ["second"] = 1, ["seconds"] = 1, ["sec"] = 1, - ["минут"] = 60, ["минуту"] = 60, ["минуты"] = 60, ["мин"] = 60, - ["minute"] = 60, ["minutes"] = 60, ["min"] = 60, - ["час"] = 3600, ["часа"] = 3600, ["часов"] = 3600, ["часик"] = 3600, - ["hour"] = 3600, ["hours"] = 3600, ["hr"] = 3600, -} -local rus_numerals = { - ["один"] = 1, ["одну"] = 1, ["одна"] = 1, - ["полторы"] = 1, ["полтора"] = 1, - ["два"] = 2, ["две"] = 2, ["три"] = 3, ["четыре"] = 4, ["пять"] = 5, - ["шесть"] = 6, ["семь"] = 7, ["восемь"] = 8, ["девять"] = 9, ["десять"] = 10, - ["пятнадцать"] = 15, ["двадцать"] = 20, ["тридцать"] = 30, ["сорок"] = 40, - ["пятьдесят"] = 50, -} - -local number, unit, body = nil, nil, nil - -local num_str, rest_after_num = rest:match("^(%d+)%s+(.+)") -if num_str then - number = tonumber(num_str) - rest = rest_after_num -else - local first_word, after = rest:match("^(%S+)%s*(.*)") - if first_word and rus_numerals[first_word] then - number = rus_numerals[first_word] - rest = after or "" - end -end - -if rest and #rest > 0 then - local first_word, after = rest:match("^(%S+)%s*(.*)") - if first_word then - for w, secs in pairs(unit_words) do - if first_word:sub(1, #w) == w then - unit = secs - body = after or "" - break - end - end - end -end - -if not number then - number = 5 - if not unit then unit = 60 end - body = rest -elseif not unit then - unit = 60 - body = rest -end - -local total_seconds = number * unit -if total_seconds < 1 then total_seconds = 1 end -if total_seconds > 86400 then total_seconds = 86400 end - -body = (body or ""):gsub("^%s+", ""):gsub("%s+$", "") -if body == "" then - body = lang == "ru" and "Напоминание!" or "Reminder!" -end - -local function human_time(sec) - if sec < 60 then return sec .. " сек" end - if sec < 3600 then return math.floor(sec/60) .. " мин" end - return string.format("%d ч %d мин", math.floor(sec/3600), math.floor((sec%3600)/60)) -end - --- Build the scheduler entry. Scheduler's "in N minutes" wants integer minutes --- so we round up for sub-minute timers; users asking for "10 seconds" will get --- ~1 minute. Honest trade-off vs the more complex previous detached-PS approach. -local minutes = math.max(1, math.floor((total_seconds + 59) / 60)) -local schedule_str = "in " .. tostring(minutes) .. " minutes" - -local speak_text = (lang == "ru" and "Сэр, напоминаю: " or "Reminder: ") .. body - -local ok, err = pcall(function() - jarvis.scheduler.add({ - name = lang == "ru" and ("Таймер: " .. body) or ("Timer: " .. body), - schedule = schedule_str, - action = { type = "speak", text = speak_text }, - }) -end) - -if not ok then - jarvis.log("error", "reminder: scheduler.add failed: " .. tostring(err)) - return jarvis.cmd.error(lang == "ru" - and "Не получилось поставить напоминание." - or "Couldn't set reminder.") -end - -jarvis.system.notify( - lang == "ru" and "Таймер установлен" or "Timer set", - string.format(lang == "ru" and "Через %s: %s" or "In %s: %s", - human_time(total_seconds), body) -) -jarvis.log("info", string.format("reminder in %ds (rounded to %d min): %s", - total_seconds, minutes, body)) - -return jarvis.cmd.ok(string.format( - lang == "ru" and "Хорошо, напомню через %s." or "Will remind you in %s.", - human_time(total_seconds) -)) diff --git a/resources/commands/routines/coffee_break.lua b/resources/commands/routines/coffee_break.lua deleted file mode 100644 index 9491ddf..0000000 --- a/resources/commands/routines/coffee_break.lua +++ /dev/null @@ -1,31 +0,0 @@ --- Coffee break — schedule a "вы вернулись?" check-in in 5 minutes, and --- pause idle banter so Jarvis isn't talking to an empty chair. - -local lang = jarvis.context.language - -jarvis.banter.pause() - -local check_in = lang == "ru" - and "Сэр, прошло пять минут. Вы вернулись?" - or "Sir, five minutes are up. Are you back?" - -pcall(function() - jarvis.scheduler.add({ - name = lang == "ru" and "Чек-ин после кофе" or "Coffee check-in", - schedule = "in 5 minutes", - action = { type = "speak", text = check_in }, - }) -end) - -local lines_ru = { - "Хорошо, сэр. Возвращайтесь через пять.", - "Тихо в эфире пять минут. Удачно вам.", - "Пять минут — норма для кофе, не более.", -} -local lines_en = { - "Right, sir. Back in five.", - "Quiet for five. Good luck.", - "Five minutes for coffee — not more.", -} -local pool = (lang == "ru") and lines_ru or lines_en -return jarvis.cmd.ok(pool[math.random(#pool)]) diff --git a/resources/commands/routines/command.toml b/resources/commands/routines/command.toml deleted file mode 100644 index c014499..0000000 --- a/resources/commands/routines/command.toml +++ /dev/null @@ -1,68 +0,0 @@ -# Bedtime / wake-up routines — bundle several actions into one voice trigger. -# These are the Jarvis equivalents of Alexa "routines": one phrase, many -# side-effects. - -[[commands]] -id = "routines.good_night" -type = "lua" -script = "good_night.lua" -sandbox = "full" -timeout = 8000 - -[commands.phrases] -ru = [ - "спокойной ночи", - "иду спать", - "пока я в кровати", - "ночной режим", - "выключи всё на ночь", -] -en = [ - "good night", - "i'm going to bed", - "bedtime mode", - "shut down for the night", -] - - -[[commands]] -id = "routines.good_morning" -type = "lua" -script = "good_morning.lua" -sandbox = "full" -timeout = 8000 - -[commands.phrases] -ru = [ - "доброе утро джарвис", - "доброе утро", - "я проснулся", - "начинаем день", -] -en = [ - "good morning", - "i'm awake", - "start the day", -] - - -[[commands]] -id = "routines.coffee_break" -type = "lua" -script = "coffee_break.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "иду за кофе", - "перерыв на кофе", - "перерыв на чай", - "отойду минут на пять", -] -en = [ - "going for coffee", - "coffee break", - "back in 5", - "afk five minutes", -] diff --git a/resources/commands/routines/good_morning.lua b/resources/commands/routines/good_morning.lua deleted file mode 100644 index 8f84bbd..0000000 --- a/resources/commands/routines/good_morning.lua +++ /dev/null @@ -1,45 +0,0 @@ --- "Good morning" routine: --- 1. switch active profile back to "default" --- 2. count today's scheduled tasks --- 3. reply with a varied morning greeting + agenda hint - -local lang = jarvis.context.language - -pcall(function() jarvis.profile.set("default") end) - -local tasks_today = jarvis.scheduler.list() or {} -local n_tasks = #tasks_today - -local lines_ru = { - "Доброе утро, сэр. Системы в порядке.", - "Доброе утро. Готов к новому дню.", - "С пробуждением, сэр. День в норме.", - "Доброе утро, сэр. Кофе сам себя не сварит.", - "Доброе утро. Все онлайн, рапорт по запросу.", -} -local lines_en = { - "Good morning, sir. All systems nominal.", - "Good morning. Ready for the day.", - "Welcome back, sir. The day looks normal.", - "Good morning, sir. The coffee won't brew itself.", - "Good morning. Everything online, ready when you are.", -} -local pool = (lang == "ru") and lines_ru or lines_en -local greeting = pool[math.random(#pool)] - -local agenda -if n_tasks > 0 then - if lang == "ru" then - agenda = " В расписании " .. tostring(n_tasks) .. ", скажите 'покажи расписание', чтобы услышать." - else - agenda = " " .. tostring(n_tasks) .. " scheduled — say 'show schedule' for details." - end -else - if lang == "ru" then - agenda = " Расписание чистое." - else - agenda = " Schedule is clear." - end -end - -return jarvis.cmd.ok(greeting .. agenda) diff --git a/resources/commands/routines/good_night.lua b/resources/commands/routines/good_night.lua deleted file mode 100644 index 7173cc9..0000000 --- a/resources/commands/routines/good_night.lua +++ /dev/null @@ -1,49 +0,0 @@ --- "Good night" routine: --- 1. switch active profile to "sleep" (so idle-banter shuts up overnight) --- 2. clear any one-shot pending reminders that would wake the user up --- after midnight — daily ones stay (those are intentional) --- 3. wish good night with a varied line --- We deliberately DO NOT lock the PC / power off displays / sleep — too --- destructive without explicit user confirmation, easy to misfire. - -local lang = jarvis.context.language - --- 1) Switch to "sleep" profile (best effort) -pcall(function() jarvis.profile.set("sleep") end) - --- 2) Cancel one-shot timers that look user-set (anything starting with --- "Таймер:" or "Timer:" — the prefix our reminders/set.lua uses). -local cancelled = 0 -for _, task in ipairs(jarvis.scheduler.list() or {}) do - local n = task.name or "" - if n:find("^Таймер:") or n:find("^Timer:") or n:find("^Кухня:") - or n:find("^Cooking:") then - jarvis.scheduler.remove(task.id) - cancelled = cancelled + 1 - end -end - -local lines_ru = { - "Спокойной ночи, сэр. Я остаюсь на связи.", - "Доброй ночи, сэр. Системы переведены в тихий режим.", - "Спите крепко, сэр. Утром доложу о всём важном.", - "Хорошо, сэр. Не отвлекаю до утра.", - "Спокойной ночи. Завтра ещё один день.", -} -local lines_en = { - "Good night, sir. I'll be on standby.", - "Sleep well, sir. Systems set to quiet.", - "Good night, sir. I'll brief you in the morning.", - "Very well, sir. Quiet mode until dawn.", - "Good night. Tomorrow's another day.", -} -local pool = (lang == "ru") and lines_ru or lines_en -local line = pool[math.random(#pool)] - -if cancelled > 0 then - line = line .. (lang == "ru" - and (" Отменил " .. tostring(cancelled) .. " одноразовых таймеров.") - or (" Cancelled " .. tostring(cancelled) .. " one-shot timers.")) -end - -return jarvis.cmd.ok(line) diff --git a/resources/commands/rss_reader/add.lua b/resources/commands/rss_reader/add.lua deleted file mode 100644 index 8517e84..0000000 --- a/resources/commands/rss_reader/add.lua +++ /dev/null @@ -1,34 +0,0 @@ --- "Добавь rss https://example.com/feed" or with named alias -local phrase = (jarvis.context.phrase or "") -local body = jarvis.text.strip_trigger(phrase:lower(), { - "добавь rss", - "подпишись на", - "запомни ленту", - "добавь ленту", - "add rss", - "subscribe to", -}) -body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - --- Preserve original casing (URLs are case-sensitive after host). -local raw = phrase -local lower_idx = raw:lower():find(body, 1, true) -if lower_idx then - body = raw:sub(lower_idx, lower_idx + #body - 1) -end - -if body == "" then - return jarvis.cmd.error("Какую ленту добавить?") -end - -local url = body:match("(https?://%S+)") -if not url then - return jarvis.cmd.error("Не нашёл URL.") -end - --- Use the URL itself as the key (or extract domain as nicer alias). -local domain = url:match("https?://([^/]+)") or "rss" -local key = "rss." .. domain - -jarvis.memory.remember(key, url) -return jarvis.cmd.ok("Лента " .. domain .. " добавлена.") diff --git a/resources/commands/rss_reader/command.toml b/resources/commands/rss_reader/command.toml deleted file mode 100644 index ba0e566..0000000 --- a/resources/commands/rss_reader/command.toml +++ /dev/null @@ -1,51 +0,0 @@ -# RSS reader — fetch top headlines from a stored feed. - -[[commands]] -id = "rss.add" -type = "lua" -script = "add.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "добавь rss", - "подпишись на", - "запомни ленту", - "добавь ленту", -] -en = ["add rss", "subscribe to"] - - -[[commands]] -id = "rss.read" -type = "lua" -script = "read.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = [ - "что в ленте", - "новости из ленты", - "почитай rss", - "новости из rss", - "новые статьи", -] -en = ["what's in feed", "read rss", "latest articles"] - - -[[commands]] -id = "rss.list" -type = "lua" -script = "list.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "какие у меня rss", - "список лент", - "мои подписки", -] -en = ["list feeds", "my feeds"] diff --git a/resources/commands/rss_reader/list.lua b/resources/commands/rss_reader/list.lua deleted file mode 100644 index 9c1604f..0000000 --- a/resources/commands/rss_reader/list.lua +++ /dev/null @@ -1,13 +0,0 @@ -local all = jarvis.memory.all() -local feeds = {} -for _, rec in ipairs(all) do - if rec.key:sub(1, 4) == "rss." then - table.insert(feeds, rec.key:sub(5)) - end -end - -if #feeds == 0 then - return jarvis.cmd.ok("Лент пока нет.") -end - -return jarvis.cmd.ok("Подписан на: " .. table.concat(feeds, ", ") .. ".") diff --git a/resources/commands/rss_reader/read.lua b/resources/commands/rss_reader/read.lua deleted file mode 100644 index 3974f52..0000000 --- a/resources/commands/rss_reader/read.lua +++ /dev/null @@ -1,41 +0,0 @@ --- Fetch one stored feed and speak top 3 titles. --- If multiple feeds, picks the first one. -local all = jarvis.memory.all() -local feed_url = nil -local feed_name = nil -for _, rec in ipairs(all) do - if rec.key:sub(1, 4) == "rss." then - feed_url = rec.value - feed_name = rec.key:sub(5) - break - end -end - -if not feed_url then - return jarvis.cmd.not_found("Лент не добавлено. Скажите 'добавь rss и URL'.") -end - -local r = jarvis.http.get(feed_url) -if not r or not r.ok then - return jarvis.cmd.error("Не получилось загрузить ленту.") -end - -local body = r.body or "" --- Crude regex parser. Works for most RSS 2.0 + Atom feeds. -local titles = {} -for t in body:gmatch("]*>([^<]+)") do - -- Skip the channel/feed title (always first), keep item titles - table.insert(titles, t) -end - -if #titles < 2 then - return jarvis.cmd.error("В ленте нет заголовков.") -end - --- titles[1] is channel title; items start at [2] -local sample = math.min(#titles - 1, 3) -local line = "Из " .. feed_name .. ". " -for i = 2, 1 + sample do - line = line .. titles[i] .. ". " -end -return jarvis.cmd.ok(line) diff --git a/resources/commands/scheduler/add_at.lua b/resources/commands/scheduler/add_at.lua deleted file mode 100644 index 63bef04..0000000 --- a/resources/commands/scheduler/add_at.lua +++ /dev/null @@ -1,58 +0,0 @@ --- "напомни в 18:00 забрать ребёнка" -local phrase = (jarvis.context.phrase or ""):lower() - -local body = jarvis.text.strip_trigger(phrase, { - "напомни сегодня в", - "напомни в", - "разбуди в", - "поставь напоминалку на", - "remind me at", - "wake me at", - "нагадай о", - "розбуди о", -}) - -body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - --- Parse "HH:MM " or "HH MM " -local h, m, rest = body:match("^(%d%d?)%s*[:%-%s]%s*(%d%d?)%s*(.*)$") -if not h then - h, rest = body:match("^(%d%d?)%s*(.*)$") - if not h then - jarvis.speak("Не понял время.") - jarvis.audio.play_error() - return { chain = false } - end - m = "00" -end -m = m or "00" - -local hn = tonumber(h) -local mn = tonumber(m) -if hn < 0 or hn > 23 or mn < 0 or mn > 59 then - jarvis.speak("Время вне диапазона.") - jarvis.audio.play_error() - return { chain = false } -end - -local schedule = string.format("at %02d:%02d", hn, mn) -local text_body = (rest or ""):gsub("^[%s,:%.]+", ""):gsub("%s+$", "") -if text_body == "" then text_body = "Напоминание." end - -local ok, err = pcall(function() - jarvis.scheduler.add({ - name = "Reminder", - schedule = schedule, - action = { type = "speak", text = text_body } - }) -end) -if not ok then - jarvis.log("warn", "scheduler.add: " .. tostring(err)) - jarvis.speak("Не получилось.") - jarvis.audio.play_error() - return { chain = false } -end - -jarvis.speak(string.format("Напомню в %02d:%02d.", hn, mn)) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/scheduler/add_daily.lua b/resources/commands/scheduler/add_daily.lua deleted file mode 100644 index bb528b4..0000000 --- a/resources/commands/scheduler/add_daily.lua +++ /dev/null @@ -1,62 +0,0 @@ --- "каждый день в 9:00 делай briefing" / "каждое утро в 8 напоминай зарядку" -local phrase = (jarvis.context.phrase or ""):lower() - -local body = jarvis.text.strip_trigger(phrase, { - "каждое утро в", - "каждый день в", - "ежедневно в", - "по утрам в", - "every day at", - "daily at", - "щодня о", - "кожен день о", -}) - -body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - --- Parse "HH:MM " or "HH " -local h, m, rest = body:match("^(%d%d?)%s*[:%-]%s*(%d%d?)%s*(.*)$") -if not h then - h, rest = body:match("^(%d%d?)%s*(.*)$") - m = "00" -end - -if not h then - jarvis.speak("Не понял время.") - jarvis.audio.play_error() - return { chain = false } -end - -local hn = tonumber(h) -local mn = tonumber(m) or 0 -if hn < 0 or hn > 23 or mn < 0 or mn > 59 then - jarvis.speak("Время вне диапазона.") - jarvis.audio.play_error() - return { chain = false } -end - -local schedule = string.format("daily %02d:%02d", hn, mn) -local text_body = (rest or ""):gsub("^напоминай%s+", "") - :gsub("^напомни%s+", "") - :gsub("^делай%s+", "") - :gsub("^[%s,:%.]+", "") - :gsub("%s+$", "") -if text_body == "" then text_body = "Доброе утро." end - -local ok, err = pcall(function() - jarvis.scheduler.add({ - name = "Daily", - schedule = schedule, - action = { type = "speak", text = text_body } - }) -end) -if not ok then - jarvis.log("warn", "scheduler.add: " .. tostring(err)) - jarvis.speak("Не получилось.") - jarvis.audio.play_error() - return { chain = false } -end - -jarvis.speak(string.format("Каждый день в %02d:%02d.", hn, mn)) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/scheduler/add_recurring.lua b/resources/commands/scheduler/add_recurring.lua deleted file mode 100644 index 92ee1d3..0000000 --- a/resources/commands/scheduler/add_recurring.lua +++ /dev/null @@ -1,74 +0,0 @@ --- "каждые 2 часа напоминай попить воды" / "каждый час напоминай размяться" -local phrase = (jarvis.context.phrase or ""):lower() - -local body = jarvis.text.strip_trigger(phrase, { - "напоминай каждые", - "напоминай каждый", - "каждые", - "каждый час", - "remind me every", - "every", - "кожні", - "нагадуй кожні", -}) - -body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - --- Parse " " or "час " (implicit 1) -local n_str, unit, rest = body:match("^(%d+)%s+(%S+)%s*(.*)$") -local n, unit_norm, sample - -if n_str then - n = tonumber(n_str) - if unit:match("^минут") or unit:match("^minute") then - unit_norm = "minutes" - sample = n .. " минут" - elseif unit:match("^час") or unit:match("^hour") then - unit_norm = "hours" - sample = n .. " час" .. (n == 1 and "" or (n < 5 and "а" or "ов")) - else - jarvis.speak("Не понял единицу.") - jarvis.audio.play_error() - return { chain = false } - end -else - -- "каждый час напоминай..." - local single, rest2 = body:match("^(%a+)%s*(.*)$") - if single and (single == "час" or single == "минуту" or single == "минута") then - n = 1 - unit_norm = (single == "час") and "hours" or "minutes" - sample = (single == "час") and "час" or "минуту" - rest = rest2 - else - jarvis.speak("Не понял через сколько повторять.") - jarvis.audio.play_error() - return { chain = false } - end -end - --- Strip optional reminder verb -local text_body = (rest or ""):gsub("^напоминай%s+", "") - :gsub("^напомни%s+", "") - :gsub("^remind me%s+", "") - :gsub("^[%s,:%.]+", "") - :gsub("%s+$", "") -if text_body == "" then text_body = "Напоминание." end - -local schedule = string.format("every %d %s", n, unit_norm) -local ok, err = pcall(function() - jarvis.scheduler.add({ - name = "Recurring", - schedule = schedule, - action = { type = "speak", text = text_body } - }) -end) -if not ok then - jarvis.log("warn", "scheduler.add: " .. tostring(err)) - jarvis.speak("Не получилось.") - jarvis.audio.play_error() - return { chain = false } -end - -jarvis.speak("Буду напоминать каждые " .. sample .. ".") -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/scheduler/add_reminder.lua b/resources/commands/scheduler/add_reminder.lua deleted file mode 100644 index 4d47b9c..0000000 --- a/resources/commands/scheduler/add_reminder.lua +++ /dev/null @@ -1,102 +0,0 @@ --- "напомни мне через 5 минут выключить кофеварку" -local phrase = (jarvis.context.phrase or ""):lower() - --- Strip the trigger -local body = jarvis.text.strip_trigger(phrase, { - "напомни мне через", - "напомни через", - "поставь напоминалку через", - "поставь напоминание через", - "разбуди через", - "remind me in", - "set a reminder in", - "нагадай через", - "постав нагадування через", -}) - -body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if body == "" then - jarvis.speak("Через сколько и про что напомнить?") - jarvis.audio.play_error() - return { chain = false } -end - --- Parse " " --- N can be digit or word (один, два, ...) — for v1 only digits -local n_str, unit, rest = body:match("^(%d+)%s+(%S+)%s*(.*)$") -if not n_str then - -- maybe "час" / "минуту" (singular) - local single_unit, rest2 = body:match("^([%a]+)%s*(.*)$") - if single_unit then - local map = { - ["час"] = "1 hours", - ["минуту"] = "1 minutes", - ["секунду"] = "1 seconds", - } - if map[single_unit] then - local schedule = "in " .. map[single_unit] - local text_body = rest2:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - if text_body == "" then text_body = "Напоминание." end - local ok, err = pcall(function() - jarvis.scheduler.add({ - name = "Reminder", - schedule = schedule, - action = { type = "speak", text = text_body } - }) - end) - if not ok then - jarvis.log("warn", "scheduler.add: " .. tostring(err)) - jarvis.speak("Не получилось.") - jarvis.audio.play_error() - return { chain = false } - end - jarvis.speak("Напомню через " .. single_unit .. ".") - jarvis.audio.play_ok() - return { chain = false } - end - end - jarvis.speak("Не понял через сколько. Скажите например: через 5 минут.") - jarvis.audio.play_error() - return { chain = false } -end - -local n = tonumber(n_str) -local unit_norm -local sample -if unit:match("^минут") or unit == "минуты" or unit == "минуту" or unit:match("^minute") then - unit_norm = "minutes" - sample = n .. " минут" -elseif unit:match("^час") or unit:match("^hour") then - unit_norm = "hours" - sample = n .. " час" .. (n == 1 and "" or (n < 5 and "а" or "ов")) -elseif unit:match("^секунд") or unit:match("^second") then - unit_norm = "seconds" - sample = n .. " секунд" -else - jarvis.speak("Не понял единицу. Минуты или часы.") - jarvis.audio.play_error() - return { chain = false } -end - -local schedule = string.format("in %d %s", n, unit_norm) -local text_body = rest:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") -if text_body == "" then text_body = "Напоминание." end - -local ok, err = pcall(function() - jarvis.scheduler.add({ - name = "Reminder", - schedule = schedule, - action = { type = "speak", text = text_body } - }) -end) -if not ok then - jarvis.log("warn", "scheduler.add: " .. tostring(err)) - jarvis.speak("Не получилось добавить напоминание.") - jarvis.audio.play_error() - return { chain = false } -end - -jarvis.speak("Напомню через " .. sample .. ".") -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/scheduler/cancel_by_text.lua b/resources/commands/scheduler/cancel_by_text.lua deleted file mode 100644 index b4471d3..0000000 --- a/resources/commands/scheduler/cancel_by_text.lua +++ /dev/null @@ -1,27 +0,0 @@ --- "Отмени напоминание про воду" / "удали задачу про разминку" -local phrase = (jarvis.context.phrase or ""):lower() - -local query = jarvis.text.strip_trigger(phrase, { - "отмени напоминание про", - "отмени напоминание о", - "удали задачу про", - "удали задачу о", - "отмени про", - "забудь напоминание про", - "cancel reminder about", -}) -query = query:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if query == "" then - return jarvis.cmd.error("Что именно отменить?") -end - -local n = jarvis.scheduler.remove_by_text(query) -if n == 0 then - return jarvis.cmd.not_found("Не нашёл напоминаний про " .. query .. ".") -end - -if n == 1 then - return jarvis.cmd.ok("Отменил напоминание.") -end -return jarvis.cmd.ok(string.format("Отменил %d напоминаний про %s.", n, query)) diff --git a/resources/commands/scheduler/clear.lua b/resources/commands/scheduler/clear.lua deleted file mode 100644 index 3e8796a..0000000 --- a/resources/commands/scheduler/clear.lua +++ /dev/null @@ -1,5 +0,0 @@ -local n = jarvis.scheduler.clear() -if n == 0 then - return jarvis.cmd.ok("В расписании и так пусто.") -end -return jarvis.cmd.ok(string.format("Удалил %d задач.", n)) diff --git a/resources/commands/scheduler/command.toml b/resources/commands/scheduler/command.toml deleted file mode 100644 index 9560038..0000000 --- a/resources/commands/scheduler/command.toml +++ /dev/null @@ -1,125 +0,0 @@ -# IMBA-5: Proactive scheduler — recurring reminders, daily briefings, intervals. - -[[commands]] -id = "scheduler.add_reminder" -type = "lua" -script = "add_reminder.lua" -sandbox = "standard" -timeout = 5000 - -[commands.phrases] -ru = [ - "напомни мне через", "напомни через", - "поставь напоминалку через", "поставь напоминание через", - "разбуди через", -] -en = ["remind me in", "set a reminder in"] - - -[[commands]] -id = "scheduler.add_at" -type = "lua" -script = "add_at.lua" -sandbox = "standard" -timeout = 5000 - -[commands.phrases] -ru = [ - "напомни в", - "напомни сегодня в", - "разбуди в", - "поставь напоминалку на", -] -en = ["remind me at", "wake me at"] - - -[[commands]] -id = "scheduler.add_recurring" -type = "lua" -script = "add_recurring.lua" -sandbox = "standard" -timeout = 5000 - -[commands.phrases] -ru = [ - "каждые", - "каждый час", - "каждые два часа", - "напоминай каждые", - "напоминай каждый", -] -en = ["every", "remind me every"] - - -[[commands]] -id = "scheduler.add_daily" -type = "lua" -script = "add_daily.lua" -sandbox = "standard" -timeout = 5000 - -[commands.phrases] -ru = [ - "каждый день в", - "каждое утро в", - "ежедневно в", - "по утрам в", -] -en = ["every day at", "daily at"] - - -[[commands]] -id = "scheduler.list" -type = "lua" -script = "list.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "что у меня запланировано", - "какие напоминания", - "покажи расписание", - "какие задачи у меня", - "что в расписании", -] -en = ["what's on my schedule", "show schedule", "list reminders"] - - -[[commands]] -id = "scheduler.clear" -type = "lua" -script = "clear.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "очисти расписание", - "удали все напоминания", - "отмени все напоминания", - "сбрось расписание", -] -en = ["clear schedule", "cancel all reminders"] - - -[[commands]] -id = "scheduler.cancel_by_text" -type = "lua" -script = "cancel_by_text.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "отмени напоминание про", - "отмени напоминание о", - "удали задачу про", - "удали задачу о", - "отмени про", - "забудь напоминание про", -] -en = [ - "cancel reminder about", - "remove task about", -] diff --git a/resources/commands/scheduler/list.lua b/resources/commands/scheduler/list.lua deleted file mode 100644 index 2410a57..0000000 --- a/resources/commands/scheduler/list.lua +++ /dev/null @@ -1,20 +0,0 @@ -local tasks = jarvis.scheduler.list() -if #tasks == 0 then - jarvis.speak("Расписание пустое.") - jarvis.audio.play_ok() - return { chain = false } -end - -local sample = math.min(#tasks, 5) -local line = string.format("Запланировано %d. ", #tasks) -for i = 1, sample do - local t = tasks[i] - local what = (t.action and t.action.text) or t.name or "задача" - line = line .. t.schedule_human .. ": " .. what - if i < sample then line = line .. ". " end -end -line = line .. "." - -jarvis.speak(line) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/screenshot/command.toml b/resources/commands/screenshot/command.toml deleted file mode 100644 index c04a7ae..0000000 --- a/resources/commands/screenshot/command.toml +++ /dev/null @@ -1,36 +0,0 @@ -# Quick screenshot pack — capture screen, save somewhere useful. -# Unlike vision/, no LLM call — just the capture. Faster + works offline. - -[[commands]] -id = "screenshot.to_clipboard" -type = "lua" -script = "to_clipboard.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "сделай скриншот", - "сделай скрин", - "скопируй экран", - "скриншот в буфер", - "захвати экран", -] -en = ["take a screenshot", "screenshot", "capture screen"] - - -[[commands]] -id = "screenshot.to_file" -type = "lua" -script = "to_file.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "сохрани скриншот", - "сохрани скрин", - "скриншот в файл", - "сохрани экран", -] -en = ["save screenshot", "screenshot to file"] diff --git a/resources/commands/screenshot/to_clipboard.lua b/resources/commands/screenshot/to_clipboard.lua deleted file mode 100644 index 56d07d7..0000000 --- a/resources/commands/screenshot/to_clipboard.lua +++ /dev/null @@ -1,25 +0,0 @@ --- Capture full screen → put PNG into clipboard via PowerShell. --- One-liner with System.Drawing + System.Windows.Forms.Clipboard.SetImage. - -local ps = [[ -Add-Type -AssemblyName System.Windows.Forms -Add-Type -AssemblyName System.Drawing -$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds -$bmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height -$g = [System.Drawing.Graphics]::FromImage($bmp) -$g.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size) -[System.Windows.Forms.Clipboard]::SetImage($bmp) -$g.Dispose(); $bmp.Dispose() -]] - -local res = jarvis.system.exec(string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', - ps:gsub('"', '\\"'):gsub("\r?\n", "; ") -)) - -if not res.success then - jarvis.log("warn", "screenshot to clipboard failed: " .. tostring(res.stderr):sub(1, 200)) - return jarvis.cmd.error("Не получилось сделать скриншот.") -end - -return jarvis.cmd.ok("Скриншот в буфере.") diff --git a/resources/commands/screenshot/to_file.lua b/resources/commands/screenshot/to_file.lua deleted file mode 100644 index e0266da..0000000 --- a/resources/commands/screenshot/to_file.lua +++ /dev/null @@ -1,36 +0,0 @@ --- Capture full screen → save PNG under ~/Pictures/Screenshots/jarvis-YYYYMMDD-HHMMSS.png -local t = jarvis.context.time -local stamp = string.format("%04d%02d%02d-%02d%02d%02d", - t.year, t.month, t.day, t.hour, t.minute, t.second or 0) - -local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" -local dir = userprofile .. "\\Pictures\\Screenshots" -local path = string.format("%s\\jarvis-%s.png", dir, stamp) - --- Ensure dir exists -jarvis.fs.mkdir(dir) - -local escaped = path:gsub("'", "''") -local ps = string.format([[ -Add-Type -AssemblyName System.Windows.Forms -Add-Type -AssemblyName System.Drawing -$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds -$bmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height -$g = [System.Drawing.Graphics]::FromImage($bmp) -$g.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size) -$bmp.Save('%s', [System.Drawing.Imaging.ImageFormat]::Png) -$g.Dispose(); $bmp.Dispose() -]], escaped) - -local res = jarvis.system.exec(string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', - ps:gsub('"', '\\"'):gsub("\r?\n", "; ") -)) - -if not res.success then - jarvis.log("warn", "screenshot to file failed: " .. tostring(res.stderr):sub(1, 200)) - return jarvis.cmd.error("Не получилось сохранить скриншот.") -end - -jarvis.system.notify("Скриншот", path) -return jarvis.cmd.ok("Скриншот сохранён в папку Скриншоты.") diff --git a/resources/commands/self_check/command.toml b/resources/commands/self_check/command.toml deleted file mode 100644 index 2aed789..0000000 --- a/resources/commands/self_check/command.toml +++ /dev/null @@ -1,37 +0,0 @@ -# Self-check — quick "are you alive" probes. Useful for testing mic chain. - -[[commands]] -id = "selfcheck.ping" -type = "lua" -script = "ping.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "ты тут", - "ты слышишь", - "ты меня слышишь", - "пинг", - "ты живой", - "ты на месте", - "проверка связи", -] -en = ["are you there", "ping", "can you hear me"] - - -[[commands]] -id = "selfcheck.smoke" -type = "lua" -script = "smoke.lua" -sandbox = "standard" -timeout = 8000 - -[commands.phrases] -ru = [ - "проверь себя", - "самопроверка", - "сделай smoke тест", - "проверка всех систем", -] -en = ["self check", "smoke test", "check yourself"] diff --git a/resources/commands/self_check/ping.lua b/resources/commands/self_check/ping.lua deleted file mode 100644 index 9f19dfd..0000000 --- a/resources/commands/self_check/ping.lua +++ /dev/null @@ -1,11 +0,0 @@ --- Cheap response so user knows mic chain works without invoking LLM/network. -local responses = { - "Да, сэр.", - "Здесь, сэр.", - "Слушаю.", - "К вашим услугам.", - "На месте.", -} -math.randomseed(os.time() + (jarvis.context.time.minute or 0)) -local idx = math.random(1, #responses) -return jarvis.cmd.ok(responses[idx]) diff --git a/resources/commands/self_check/smoke.lua b/resources/commands/self_check/smoke.lua deleted file mode 100644 index 52f0f6c..0000000 --- a/resources/commands/self_check/smoke.lua +++ /dev/null @@ -1,46 +0,0 @@ --- Smoke test: verify every major subsystem responds. --- Useful for "is the install OK" after fresh setup. -local h = jarvis.health() - -local checks = {} - --- 1. TTS configured -if h.tts_backend and h.tts_backend ~= "—" then - table.insert(checks, "TTS " .. h.tts_backend .. " активен") -end - --- 2. LLM configured -if h.llm_backend and h.llm_backend ~= "none" then - table.insert(checks, "LLM " .. h.llm_backend .. " подключён") -else - table.insert(checks, "ВНИМАНИЕ: LLM не настроен") -end - --- 3. Memory + scheduler files exist -if h.memory_facts ~= nil then - table.insert(checks, "хранилище памяти работает") -end -if h.scheduled_tasks ~= nil then - table.insert(checks, "планировщик работает") -end - --- 4. Profile + i18n -if h.active_profile then - table.insert(checks, "профиль " .. h.active_profile) -end - --- 5. Quick HTTP smoke -local r = pcall(function() - local resp = jarvis.http.get("https://api.ipify.org") - return resp and resp.ok -end) -if r then - table.insert(checks, "интернет есть") -end - -if #checks == 0 then - return jarvis.cmd.error("Все системы молчат — что-то не так.") -end - -local line = "Самопроверка: " .. table.concat(checks, ", ") .. "." -return jarvis.cmd.ok(line) diff --git a/resources/commands/sleep_timer/cancel.lua b/resources/commands/sleep_timer/cancel.lua deleted file mode 100644 index 79b14be..0000000 --- a/resources/commands/sleep_timer/cancel.lua +++ /dev/null @@ -1,9 +0,0 @@ --- Cancel both shutdown and pause timers (best-effort, ok if one was inactive). -local removed_pause = jarvis.scheduler.remove("sleep_timer_pause") -local sd_res = jarvis.system.exec("shutdown.exe /a") -local sd_ok = sd_res and sd_res.success - -if removed_pause or sd_ok then - return jarvis.cmd.ok("Таймер отменён.") -end -return jarvis.cmd.not_found("Активных таймеров нет.") diff --git a/resources/commands/sleep_timer/command.toml b/resources/commands/sleep_timer/command.toml deleted file mode 100644 index 9eed3af..0000000 --- a/resources/commands/sleep_timer/command.toml +++ /dev/null @@ -1,53 +0,0 @@ -# Sleep timer — pause media + optional shutdown after N minutes. - -[[commands]] -id = "sleep_timer.pause_in" -type = "lua" -script = "pause_in.lua" -sandbox = "standard" -timeout = 5000 - -[commands.phrases] -ru = [ - "выключи музыку через", - "приостанови музыку через", - "пауза через", - "пауза музыки через", -] -en = ["pause music in", "pause in"] - - -[[commands]] -id = "sleep_timer.shutdown_in" -type = "lua" -script = "shutdown_in.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "выключи компьютер через", - "выключи пк через", - "выключи комп через", - "таймер выключения", - "ложусь спать через", -] -en = ["shutdown in", "sleep timer"] - - -[[commands]] -id = "sleep_timer.cancel" -type = "lua" -script = "cancel.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "отмени таймер выключения", - "отмени таймер сна", - "отмени паузу", - "не выключай компьютер", - "не выключай пк", -] -en = ["cancel sleep timer", "cancel shutdown"] diff --git a/resources/commands/sleep_timer/pause_in.lua b/resources/commands/sleep_timer/pause_in.lua deleted file mode 100644 index cc87802..0000000 --- a/resources/commands/sleep_timer/pause_in.lua +++ /dev/null @@ -1,58 +0,0 @@ --- "Выключи музыку через 30 минут" → scheduler one-shot фраза-команда "пауза" -local phrase = (jarvis.context.phrase or ""):lower() -local n_str, unit = phrase:match("(%d+)%s+(%S+)") -if not n_str then - return jarvis.cmd.error("Не понял через сколько.") -end -local n = tonumber(n_str) -local secs -if unit:match("^минут") then secs = "in " .. n .. " minutes" -elseif unit:match("^час") then secs = "in " .. n .. " hours" -elseif unit:match("^секунд") then secs = "in " .. n .. " seconds" -else - return jarvis.cmd.error("Не понял единицу.") -end - --- Build path to media_keys/_media.ps1 (lives next door) -local cmd_path = jarvis.context.command_path -local helper = cmd_path:gsub("sleep_timer$", "media_keys") .. "\\_media.ps1" -helper = helper:gsub("/", "\\") - --- VK_MEDIA_PLAY_PAUSE = 0xB3 = 179 -local lua_path = cmd_path:gsub("\\", "/") .. "/_pause_lua.lua" --- Simpler: just speak "пауза" and let agent router handle it? No — agent --- router only fires for unmatched commands. Better: directly invoke media key --- via a one-shot Lua script. - -local script_path = cmd_path .. "\\_fire_pause.lua" -script_path = script_path:gsub("/", "\\") - --- Write the helper script once -local exists = jarvis.fs.is_file(script_path) -if not exists then - jarvis.fs.write(script_path, string.format([[ -local helper = "%s" -jarvis.system.exec(string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%%s" 179', - helper -)) -return { chain = false } -]], helper:gsub("\\", "\\\\"))) -end - -local ok, err = pcall(function() - jarvis.scheduler.remove("sleep_timer_pause") - jarvis.scheduler.add({ - id = "sleep_timer_pause", - name = "Sleep timer: pause music", - schedule = secs, - action = { type = "lua", script_path = script_path }, - }) -end) - -if not ok then - jarvis.log("warn", "sleep_timer.pause_in: " .. tostring(err)) - return jarvis.cmd.error("Не получилось.") -end - -return jarvis.cmd.ok("Поставлю на паузу через " .. n_str .. " " .. unit .. ".") diff --git a/resources/commands/sleep_timer/shutdown_in.lua b/resources/commands/sleep_timer/shutdown_in.lua deleted file mode 100644 index be3be33..0000000 --- a/resources/commands/sleep_timer/shutdown_in.lua +++ /dev/null @@ -1,46 +0,0 @@ --- "Выключи компьютер через 30 минут" → shutdown.exe /s /t --- Использует штатный shutdown с обратным таймером. -local phrase = (jarvis.context.phrase or ""):lower() -local n_str, unit = phrase:match("(%d+)%s+(%S+)") -if not n_str then - return jarvis.cmd.error("Не понял через сколько.") -end -local n = tonumber(n_str) -local secs -if unit:match("^минут") then secs = n * 60 -elseif unit:match("^час") then secs = n * 3600 -elseif unit:match("^секунд") then secs = n -else - return jarvis.cmd.error("Не понял единицу. Минуты или часы.") -end - -if secs > 86400 then - return jarvis.cmd.error("Слишком большое значение — максимум 24 часа.") -end - -local cmd = string.format( - 'shutdown.exe /s /t %d /c "Sleep timer fired"', - secs -) -local res = jarvis.system.exec(cmd) -if not res.success then - return jarvis.cmd.error("Не получилось поставить таймер.") -end - --- Pluralisation -local label -if unit:match("^минут") then - local last = n % 10 - if last == 1 and n % 100 ~= 11 then label = n .. " минуту" - elseif last >= 2 and last <= 4 and (n % 100 < 10 or n % 100 >= 20) then label = n .. " минуты" - else label = n .. " минут" end -elseif unit:match("^час") then - local last = n % 10 - if last == 1 and n % 100 ~= 11 then label = n .. " час" - elseif last >= 2 and last <= 4 and (n % 100 < 10 or n % 100 >= 20) then label = n .. " часа" - else label = n .. " часов" end -else - label = n .. " секунд" -end - -return jarvis.cmd.ok("Выключу компьютер через " .. label .. ". Скажите 'отмени таймер выключения' чтобы передумать.") diff --git a/resources/commands/sound_panel/command.toml b/resources/commands/sound_panel/command.toml deleted file mode 100644 index 35f4ded..0000000 --- a/resources/commands/sound_panel/command.toml +++ /dev/null @@ -1,19 +0,0 @@ -[[commands]] -id = "open_sound_panel" -type = "lua" -script = "open.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "открой настройки звука", - "панель звука", - "настройки микрофона", - "управление звуком", -] -en = [ - "open sound settings", - "sound panel", - "microphone settings", -] diff --git a/resources/commands/sound_panel/open.lua b/resources/commands/sound_panel/open.lua deleted file mode 100644 index d13187b..0000000 --- a/resources/commands/sound_panel/open.lua +++ /dev/null @@ -1,3 +0,0 @@ -jarvis.system.exec('rundll32.exe shell32.dll,Control_RunDLL mmsys.cpl,,1') -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/spelling/command.toml b/resources/commands/spelling/command.toml deleted file mode 100644 index 4c4e11e..0000000 --- a/resources/commands/spelling/command.toml +++ /dev/null @@ -1,18 +0,0 @@ -[[commands]] -id = "spell_out" -type = "lua" -script = "spell.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = [ - "произнеси по буквам", - "продиктуй по буквам", - "произнеси буквы", - "по буквам", -] -en = [ - "spell out", - "spell it", -] diff --git a/resources/commands/spelling/spell.lua b/resources/commands/spelling/spell.lua deleted file mode 100644 index fbf6b6f..0000000 --- a/resources/commands/spelling/spell.lua +++ /dev/null @@ -1,29 +0,0 @@ -local lang = jarvis.context.language -local triggers = { - "продиктуй по буквам", "произнеси по буквам", "произнеси буквы", "по буквам", - "spell out", "spell it", - "вимов по буквах", -} -local word = jarvis.text.strip_trigger(jarvis.context.phrase or "", triggers) -if word == "" then - jarvis.system.notify("Spelling", lang == "ru" and "Что произнести?" or "What to spell?") - jarvis.audio.play_error() - return { chain = false } -end - -local letters = {} -for ch in word:gmatch(".") do - if ch ~= " " then table.insert(letters, ch) end -end -if #letters == 0 then - jarvis.audio.play_error() - return { chain = false } -end - -local spoken = table.concat(letters, ". ") .. "." - -jarvis.system.notify(lang == "ru" and "По буквам" or "Spelling", - word .. "\n" .. spoken) -jarvis.speak(spoken, { lang = lang }) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/ssl_check/check.lua b/resources/commands/ssl_check/check.lua deleted file mode 100644 index e6da68a..0000000 --- a/resources/commands/ssl_check/check.lua +++ /dev/null @@ -1,79 +0,0 @@ --- "Проверь сертификат example.com" → PowerShell TCP connect + parse expiry. -local phrase = (jarvis.context.phrase or ""):lower() -local domain = jarvis.text.strip_trigger(phrase, { - "когда истекает сертификат", - "проверь сертификат", - "сертификат сайта", - "проверь tls", - "проверь ssl", - "check ssl", - "check tls", - "check certificate", - "перевір сертифікат", -}) -domain = domain:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") --- Preserve original casing -if domain ~= "" then - local start = phrase:find(domain, 1, true) - if start then domain = phrase:sub(start, start + #domain - 1) end -end - -if domain == "" then - return jarvis.cmd.error("Какой домен проверить?") -end - --- Strip "https://" if user said it -domain = domain:gsub("^https?://", ""):gsub("/$", ""):gsub("/.*", "") - -local ps = string.format([[ -$ErrorActionPreference = 'Stop' -try { - $tcp = New-Object Net.Sockets.TcpClient('%s', 443) - $ssl = New-Object Net.Security.SslStream($tcp.GetStream(), $false, { $true }) - $ssl.AuthenticateAsClient('%s') - $cert = $ssl.RemoteCertificate - $cert2 = New-Object Security.Cryptography.X509Certificates.X509Certificate2($cert) - $expiry = $cert2.NotAfter - $days = ($expiry - (Get-Date)).Days - Write-Output ('{0}|{1}|{2}' -f $days, $expiry.ToString('yyyy-MM-dd'), $cert2.Issuer) - $ssl.Close() - $tcp.Close() -} catch { - Write-Output ('ERR|' + $_.Exception.Message) -} -]], domain, domain) - -local res = jarvis.system.exec(string.format( - 'powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'):gsub("\r?\n", "; ") -)) - -if not res.success then - return jarvis.cmd.error("Не получилось проверить.") -end - -local out = (res.stdout or ""):gsub("^%s+", ""):gsub("%s+$", "") -if out:match("^ERR|") then - local msg = out:sub(5):sub(1, 100) - return jarvis.cmd.error("Не получилось: " .. msg) -end - -local days, date, issuer = out:match("(%-?%d+)|([%d%-]+)|(.+)") -if not days then - return jarvis.cmd.error("Не распарсил ответ.") -end - -local issuer_short = issuer:match("CN=([^,]+)") or issuer:sub(1, 30) -local days_n = tonumber(days) -local urgency = "" -if days_n < 0 then - urgency = "Сертификат уже просрочен!" -elseif days_n < 7 then - urgency = "СКОРО! " -elseif days_n < 30 then - urgency = "Скоро. " -end - -return jarvis.cmd.ok(string.format( - "%sСертификат %s истекает через %s дней (%s). Выдан: %s.", - urgency, domain, days, date, issuer_short -)) diff --git a/resources/commands/ssl_check/command.toml b/resources/commands/ssl_check/command.toml deleted file mode 100644 index 4829b9d..0000000 --- a/resources/commands/ssl_check/command.toml +++ /dev/null @@ -1,18 +0,0 @@ -# SSL/TLS certificate expiry check via PowerShell. - -[[commands]] -id = "ssl.check" -type = "lua" -script = "check.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = [ - "проверь сертификат", - "когда истекает сертификат", - "проверь tls", - "проверь ssl", - "сертификат сайта", -] -en = ["check ssl", "check tls", "check certificate"] diff --git a/resources/commands/stocks/command.toml b/resources/commands/stocks/command.toml deleted file mode 100644 index e1ef3a9..0000000 --- a/resources/commands/stocks/command.toml +++ /dev/null @@ -1,31 +0,0 @@ -# Stocks — query Moscow Exchange (MOEX) for share price. -# API: https://iss.moex.com/iss/engines/stock/markets/shares/securities/.json -# Free, no key, public. - -[[commands]] -id = "stocks_quote" -type = "lua" -script = "quote.lua" -sandbox = "full" -timeout = 10000 - -[commands.phrases] -ru = [ - "сколько сейчас стоит", - "цена акций", - "котировка", - "сколько стоит сбер", - "сколько стоит газпром", - "сколько стоит лукойл", - "сколько стоит яндекс", - "сколько стоит тинькофф", - "сколько стоит втб", - "сколько стоит магнит", - "сколько стоит мтс", - "сколько стоит роснефть", -] -en = [ - "stock price", - "share price", - "quote for", -] diff --git a/resources/commands/stocks/quote.lua b/resources/commands/stocks/quote.lua deleted file mode 100644 index 76b4080..0000000 --- a/resources/commands/stocks/quote.lua +++ /dev/null @@ -1,119 +0,0 @@ --- "Сколько сейчас Сбер" / "котировка газпром" — query MOEX ISS public API. -local phrase = (jarvis.context.phrase or ""):lower() - --- Common name → ticker map. -local TICKERS = { - ["сбер"] = "SBER", ["сбербанк"] = "SBER", - ["газпром"] = "GAZP", - ["лукойл"] = "LKOH", - ["яндекс"] = "YDEX", ["yandex"] = "YDEX", - ["вк"] = "VKCO", ["вконтакте"] = "VKCO", - ["втб"] = "VTBR", - ["магнит"] = "MGNT", - ["мтс"] = "MTSS", - ["роснефть"] = "ROSN", - ["новатэк"] = "NVTK", - ["норникель"] = "GMKN", ["норильск"] = "GMKN", - ["полюс"] = "PLZL", - ["тинькофф"] = "T", ["тбанк"] = "T", ["т банк"] = "T", - ["сургутнефтегаз"] = "SNGS", - ["мосбиржа"] = "MOEX", ["биржа"] = "MOEX", - ["x5"] = "X5", ["икс пять"] = "X5", - ["алроса"] = "ALRS", - ["русал"] = "RUAL", - ["мечел"] = "MTLR", - ["фосагро"] = "PHOR", - ["сегежа"] = "SGZH", - ["белуга"] = "BELU", -} - --- find ticker -local ticker, ticker_name -for name, tk in pairs(TICKERS) do - if phrase:find(name, 1, true) then - ticker = tk - ticker_name = name - break - end -end - --- Allow direct ticker mention "сколько SBER" etc. -if not ticker then - local maybe = phrase:upper():match("([A-Z][A-Z][A-Z]+)") - if maybe then ticker = maybe; ticker_name = maybe end -end - -if not ticker then - jarvis.speak("Не понял какую бумагу. Скажите название.") - jarvis.audio.play_error() - return { chain = false } -end - -local url = string.format( - "https://iss.moex.com/iss/engines/stock/markets/shares/securities/%s.json?iss.meta=off", - ticker -) - -local data = jarvis.http.json(url) -if not data then - jarvis.speak("Не получилось получить котировки.") - jarvis.audio.play_error() - return { chain = false } -end - --- MOEX ISS returns parallel arrays: columns + data rows. We want marketdata.LAST. -local md = data.marketdata -if not md or not md.columns or not md.data then - jarvis.speak("Биржа вернула пустой ответ.") - jarvis.audio.play_error() - return { chain = false } -end - -local cols = md.columns -local rows = md.data - --- find indexes -local idx_last, idx_change, idx_board -for i, c in ipairs(cols) do - if c == "LAST" then idx_last = i end - if c == "LASTTOPREVPRICE" then idx_change = i end - if c == "BOARDID" then idx_board = i end -end - --- pick the TQBR (main board) row if present, else first non-nil LAST -local last, change -for _, row in ipairs(rows) do - local board = idx_board and row[idx_board] - if board == "TQBR" and row[idx_last] then - last = row[idx_last] - change = idx_change and row[idx_change] - break - end -end -if not last then - for _, row in ipairs(rows) do - if row[idx_last] then - last = row[idx_last] - change = idx_change and row[idx_change] - break - end - end -end - -if not last then - jarvis.speak("По " .. ticker_name .. " сегодня нет торгов.") - jarvis.audio.play_not_found() - return { chain = false } -end - -local rub = string.format("%.2f", last):gsub("%.", " и ") -local line = string.format("%s — %s рубля.", ticker_name, rub) -if change and tonumber(change) then - local pct = tonumber(change) - local sign = pct >= 0 and "плюс" or "минус" - line = line .. string.format(" Сегодня %s %.2f процента.", sign, math.abs(pct)) -end - -jarvis.speak(line) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/stopwatch/command.toml b/resources/commands/stopwatch/command.toml deleted file mode 100644 index 30c684c..0000000 --- a/resources/commands/stopwatch/command.toml +++ /dev/null @@ -1,34 +0,0 @@ -[[commands]] -id = "stopwatch_start" -type = "lua" -script = "ctl.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["засеки время", "запусти секундомер", "старт секундомер", "включи секундомер"] -en = ["start stopwatch", "start timer"] - - -[[commands]] -id = "stopwatch_check" -type = "lua" -script = "ctl.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["сколько прошло", "сколько времени прошло", "проверь секундомер"] -en = ["how long has it been", "check stopwatch"] - - -[[commands]] -id = "stopwatch_stop" -type = "lua" -script = "ctl.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["стоп секундомер", "останови секундомер", "выключи секундомер"] -en = ["stop stopwatch", "stop timer"] diff --git a/resources/commands/stopwatch/ctl.lua b/resources/commands/stopwatch/ctl.lua deleted file mode 100644 index 2e0edb0..0000000 --- a/resources/commands/stopwatch/ctl.lua +++ /dev/null @@ -1,53 +0,0 @@ -local lang = jarvis.context.language -local cmd_id = jarvis.context.command_id - -local function speak(text) - local sapi = text:gsub("'", "''"):gsub("\r?\n", " ") - local ps = string.format( - [[Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; foreach ($v in $s.GetInstalledVoices()) { if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq 'ru') { try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} } } $s.Speak('%s')]], - sapi - ) - jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'))) -end - -local function format_elapsed(sec) - sec = math.floor(sec) - if sec < 60 then return sec .. (lang == "ru" and " секунд" or " seconds") end - if sec < 3600 then - local m = math.floor(sec / 60); local s = sec % 60 - return string.format(lang == "ru" and "%d мин %d сек" or "%d min %d sec", m, s) - end - local h = math.floor(sec / 3600); local m = math.floor((sec % 3600) / 60); local s = sec % 60 - return string.format(lang == "ru" and "%d ч %d мин %d сек" or "%d h %d m %d s", h, m, s) -end - -if cmd_id == "stopwatch_start" then - jarvis.state.set("stopwatch_start_ts", jarvis.context.time.timestamp) - jarvis.system.notify("Stopwatch", lang == "ru" and "Старт" or "Started") - speak(lang == "ru" and "Секундомер запущен" or "Stopwatch started") - jarvis.audio.play_ok() - return { chain = false } -end - -local started = jarvis.state.get("stopwatch_start_ts") -if not started then - jarvis.system.notify("Stopwatch", lang == "ru" and "Не запущен" or "Not running") - speak(lang == "ru" and "Секундомер не запущен" or "Stopwatch is not running") - jarvis.audio.play_not_found() - return { chain = false } -end - -local elapsed = jarvis.context.time.timestamp - started -local human = format_elapsed(elapsed) - -if cmd_id == "stopwatch_stop" then - jarvis.state.set("stopwatch_start_ts", nil) - jarvis.system.notify("Stopwatch", (lang == "ru" and "Прошло: " or "Elapsed: ") .. human) - speak((lang == "ru" and "Прошло " or "Elapsed ") .. human) -else - jarvis.system.notify("Stopwatch", (lang == "ru" and "Прошло: " or "Elapsed: ") .. human) - speak((lang == "ru" and "Прошло " or "Elapsed ") .. human) -end - -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/summarize/command.toml b/resources/commands/summarize/command.toml deleted file mode 100644 index 7fd6139..0000000 --- a/resources/commands/summarize/command.toml +++ /dev/null @@ -1,21 +0,0 @@ -[[commands]] -id = "summarize_selection" -type = "lua" -script = "summarize.lua" -sandbox = "full" -timeout = 25000 - -[commands.phrases] -ru = [ - "суммируй", - "перескажи выделенное", - "кратко перескажи", - "о чём это", - "что выделил", - "перескажи это", -] -en = [ - "summarise this", - "summarize selection", - "tldr", -] diff --git a/resources/commands/summarize/summarize.lua b/resources/commands/summarize/summarize.lua deleted file mode 100644 index 9e7a247..0000000 --- a/resources/commands/summarize/summarize.lua +++ /dev/null @@ -1,43 +0,0 @@ -local lang = jarvis.context.language - -local copy_ps = [[Add-Type -Name K -Namespace W -MemberDefinition '[DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte sc, uint flags, System.UIntPtr extra);'; [W.K]::keybd_event(0x11,0,0,[UIntPtr]::Zero); [W.K]::keybd_event(0x43,0,0,[UIntPtr]::Zero); Start-Sleep -Milliseconds 40; [W.K]::keybd_event(0x43,0,2,[UIntPtr]::Zero); [W.K]::keybd_event(0x11,0,2,[UIntPtr]::Zero); Start-Sleep -Milliseconds 220]] -jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', copy_ps:gsub('"', '\\"'))) - -local raw = jarvis.system.clipboard.get() or "" -local text = raw:gsub("^%s+", ""):gsub("%s+$", "") - -if #text < 20 then - jarvis.system.notify("Summarize", - lang == "ru" and "Сначала выдели текст и повтори" or "Select text first, then retry") - jarvis.audio.play_not_found() - return { chain = false } -end - -if #text > 8000 then - text = text:sub(1, 8000) .. " ..." -end - -local sys = lang == "ru" - and "Перескажи следующий текст одним абзацем (3-5 предложений). Сохрани ключевые факты и имена, без 'вот суммарно' и других вступлений." - or "Summarise the following text in 3-5 sentences. Keep key facts and names, no preamble." - -local reply = jarvis.llm( - { { role = "system", content = sys }, - { role = "user", content = text } }, - { max_tokens = 400, temperature = 0.3 } -) - -if not reply or reply == "" then - jarvis.system.notify("Summarize", lang == "ru" and "LLM не ответила" or "LLM failed") - jarvis.audio.play_error() - return { chain = false } -end - -jarvis.system.clipboard.set(reply) -jarvis.system.notify( - lang == "ru" and "Кратко" or "TL;DR", - reply:sub(1, 280) -) -jarvis.speak(reply, { lang = lang }) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/sysinfo/_sysinfo.ps1 b/resources/commands/sysinfo/_sysinfo.ps1 deleted file mode 100644 index ae020a3..0000000 --- a/resources/commands/sysinfo/_sysinfo.ps1 +++ /dev/null @@ -1,69 +0,0 @@ -param( - [Parameter(Mandatory=$true)] - [ValidateSet("battery","time","cpu","ram","disk","all")] - [string]$Topic -) - -$ErrorActionPreference = 'SilentlyContinue' - -function Get-BatteryInfo { - $b = Get-CimInstance -ClassName Win32_Battery - if (-not $b) { return "Батарея не обнаружена (видимо, десктоп)" } - $pct = [int]$b.EstimatedChargeRemaining - $status = switch ($b.BatteryStatus) { - 1 { "разряжается" } - 2 { "от сети" } - 3 { "заряжена" } - 4 { "низкий заряд" } - 5 { "критический заряд" } - default { "статус $($b.BatteryStatus)" } - } - return "Батарея: $pct%, $status" -} - -function Get-TimeInfo { - $ru = [System.Globalization.CultureInfo]::GetCultureInfo("ru-RU") - $now = Get-Date - return "Сейчас: " + $now.ToString("HH:mm, dd MMMM, dddd", $ru) -} - -function Get-CpuInfo { - $cpu = Get-CimInstance -ClassName Win32_Processor | Select-Object -First 1 - $load = (Get-CimInstance -ClassName Win32_PerfFormattedData_PerfOS_Processor -Filter "Name='_Total'" -ErrorAction SilentlyContinue).PercentProcessorTime - if ($null -eq $load) { $load = $cpu.LoadPercentage } - return "CPU: $($cpu.Name.Trim()), загрузка $load%" -} - -function Get-RamInfo { - $os = Get-CimInstance -ClassName Win32_OperatingSystem - $totalGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 1) - $freeGB = [math]::Round($os.FreePhysicalMemory / 1MB, 1) - $usedGB = [math]::Round($totalGB - $freeGB, 1) - $pct = [int](($usedGB / $totalGB) * 100) - return "RAM: $usedGB из $totalGB ГБ ($pct% занято)" -} - -function Get-DiskInfo { - $sys = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID='$($env:SystemDrive)'" - if (-not $sys) { - $sys = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" | Select-Object -First 1 - } - $freeGB = [math]::Round($sys.FreeSpace / 1GB, 1) - $totalGB = [math]::Round($sys.Size / 1GB, 1) - return "Диск $($sys.DeviceID): свободно $freeGB из $totalGB ГБ" -} - -switch ($Topic) { - "battery" { Get-BatteryInfo } - "time" { Get-TimeInfo } - "cpu" { Get-CpuInfo } - "ram" { Get-RamInfo } - "disk" { Get-DiskInfo } - "all" { - Get-TimeInfo - Get-BatteryInfo - Get-CpuInfo - Get-RamInfo - Get-DiskInfo - } -} diff --git a/resources/commands/sysinfo/all.lua b/resources/commands/sysinfo/all.lua deleted file mode 100644 index ad0f346..0000000 --- a/resources/commands/sysinfo/all.lua +++ /dev/null @@ -1,14 +0,0 @@ -local helper = jarvis.context.command_path .. "\\_sysinfo.ps1" -local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic all', helper) -local res = jarvis.system.exec(cmd) -local text = (res.stdout or ""):gsub("[\r\n]+$", "") -if not res.success or text == "" then - jarvis.log("error", "sysinfo all failed: " .. tostring(res.stderr)) - return jarvis.cmd.error("Не получилось собрать состояние системы.") -end -jarvis.system.notify("Статус системы", text) -jarvis.log("info", text) --- Multi-line output reads more naturally as a single sentence with commas --- between metrics, so TTS doesn't over-pause on linebreaks. -local spoken = text:gsub("[\r\n]+", ", ") -return jarvis.cmd.ok(spoken) diff --git a/resources/commands/sysinfo/battery.lua b/resources/commands/sysinfo/battery.lua deleted file mode 100644 index 2820879..0000000 --- a/resources/commands/sysinfo/battery.lua +++ /dev/null @@ -1,11 +0,0 @@ -local helper = jarvis.context.command_path .. "\\_sysinfo.ps1" -local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic battery', helper) -local res = jarvis.system.exec(cmd) -local text = (res.stdout or ""):gsub("[\r\n]+$", "") -if not res.success or text == "" then - jarvis.log("error", "battery info failed: " .. tostring(res.stderr)) - return jarvis.cmd.error("Не получилось узнать заряд.") -end -jarvis.system.notify("Батарея", text) -jarvis.log("info", text) -return jarvis.cmd.ok(text) diff --git a/resources/commands/sysinfo/command.toml b/resources/commands/sysinfo/command.toml deleted file mode 100644 index 67cf67a..0000000 --- a/resources/commands/sysinfo/command.toml +++ /dev/null @@ -1,127 +0,0 @@ -[[commands]] -id = "sysinfo_battery" -type = "lua" -script = "battery.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "сколько заряда", - "сколько батарея", - "заряд батареи", - "сколько процентов", -] -en = [ - "battery level", - "how much battery", - "battery status", -] - - -[[commands]] -id = "sysinfo_time" -type = "lua" -script = "time.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "сколько времени", - "который час", - "сколько сейчас времени", - "какой день", - "какое сегодня число", -] -en = [ - "what time is it", - "current time", - "what day is it", - "what is the date", -] - - -[[commands]] -id = "sysinfo_cpu" -type = "lua" -script = "cpu.lua" -sandbox = "full" -timeout = 8000 - -[commands.phrases] -ru = [ - "загрузка процессора", - "сколько процессор", - "нагрузка цпу", - "что с процессором", -] -en = [ - "cpu load", - "cpu usage", - "how is cpu", -] - - -[[commands]] -id = "sysinfo_ram" -type = "lua" -script = "ram.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "сколько памяти", - "сколько оперативки", - "загрузка оперативки", - "сколько свободной памяти", -] -en = [ - "how much ram", - "ram usage", - "memory usage", -] - - -[[commands]] -id = "sysinfo_disk" -type = "lua" -script = "disk.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "сколько свободно на диске", - "сколько места на диске", - "сколько места на жёстком", - "сколько свободно", -] -en = [ - "free disk space", - "how much disk space", - "disk usage", -] - - -[[commands]] -id = "sysinfo_all" -type = "lua" -script = "all.lua" -sandbox = "full" -timeout = 10000 - -[commands.phrases] -ru = [ - "статус системы", - "что с компьютером", - "состояние пк", - "что по системе", - "статус", -] -en = [ - "system status", - "how is the system", - "pc status", -] diff --git a/resources/commands/sysinfo/cpu.lua b/resources/commands/sysinfo/cpu.lua deleted file mode 100644 index 8cd7c6d..0000000 --- a/resources/commands/sysinfo/cpu.lua +++ /dev/null @@ -1,11 +0,0 @@ -local helper = jarvis.context.command_path .. "\\_sysinfo.ps1" -local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic cpu', helper) -local res = jarvis.system.exec(cmd) -local text = (res.stdout or ""):gsub("[\r\n]+$", "") -if not res.success or text == "" then - jarvis.log("error", "cpu info failed: " .. tostring(res.stderr)) - return jarvis.cmd.error("Не получилось узнать загрузку процессора.") -end -jarvis.system.notify("CPU", text) -jarvis.log("info", text) -return jarvis.cmd.ok(text) diff --git a/resources/commands/sysinfo/disk.lua b/resources/commands/sysinfo/disk.lua deleted file mode 100644 index ba12961..0000000 --- a/resources/commands/sysinfo/disk.lua +++ /dev/null @@ -1,11 +0,0 @@ -local helper = jarvis.context.command_path .. "\\_sysinfo.ps1" -local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic disk', helper) -local res = jarvis.system.exec(cmd) -local text = (res.stdout or ""):gsub("[\r\n]+$", "") -if not res.success or text == "" then - jarvis.log("error", "disk info failed: " .. tostring(res.stderr)) - return jarvis.cmd.error("Не получилось узнать диск.") -end -jarvis.system.notify("Диск", text) -jarvis.log("info", text) -return jarvis.cmd.ok(text) diff --git a/resources/commands/sysinfo/ram.lua b/resources/commands/sysinfo/ram.lua deleted file mode 100644 index 489ea0e..0000000 --- a/resources/commands/sysinfo/ram.lua +++ /dev/null @@ -1,11 +0,0 @@ -local helper = jarvis.context.command_path .. "\\_sysinfo.ps1" -local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic ram', helper) -local res = jarvis.system.exec(cmd) -local text = (res.stdout or ""):gsub("[\r\n]+$", "") -if not res.success or text == "" then - jarvis.log("error", "ram info failed: " .. tostring(res.stderr)) - return jarvis.cmd.error("Не получилось узнать память.") -end -jarvis.system.notify("Память", text) -jarvis.log("info", text) -return jarvis.cmd.ok(text) diff --git a/resources/commands/sysinfo/time.lua b/resources/commands/sysinfo/time.lua deleted file mode 100644 index 483dd9f..0000000 --- a/resources/commands/sysinfo/time.lua +++ /dev/null @@ -1,11 +0,0 @@ -local helper = jarvis.context.command_path .. "\\_sysinfo.ps1" -local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic time', helper) -local res = jarvis.system.exec(cmd) -local text = (res.stdout or ""):gsub("[\r\n]+$", "") -if not res.success or text == "" then - jarvis.log("error", "time info failed: " .. tostring(res.stderr)) - return jarvis.cmd.error("Не получилось узнать время.") -end -jarvis.system.notify("Время", text) -jarvis.log("info", text) -return jarvis.cmd.ok(text) diff --git a/resources/commands/theme/command.toml b/resources/commands/theme/command.toml deleted file mode 100644 index afa3723..0000000 --- a/resources/commands/theme/command.toml +++ /dev/null @@ -1,22 +0,0 @@ -[[commands]] -id = "theme_dark" -type = "lua" -script = "set.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["тёмная тема", "темная тема", "включи тёмную тему", "тёмный режим", "темный режим"] -en = ["dark theme", "dark mode", "enable dark mode"] - - -[[commands]] -id = "theme_light" -type = "lua" -script = "set.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["светлая тема", "включи светлую тему", "светлый режим"] -en = ["light theme", "light mode", "enable light mode"] diff --git a/resources/commands/theme/set.lua b/resources/commands/theme/set.lua deleted file mode 100644 index 8c8e850..0000000 --- a/resources/commands/theme/set.lua +++ /dev/null @@ -1,26 +0,0 @@ -local cmd_id = jarvis.context.command_id -local lang = jarvis.context.language -local is_light = (cmd_id == "theme_light") -local value = is_light and 1 or 0 - -local key = [[HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize]] -local ps = string.format( - "Set-ItemProperty -Path 'Registry::%s' -Name AppsUseLightTheme -Value %d -Type DWord; " - .. "Set-ItemProperty -Path 'Registry::%s' -Name SystemUsesLightTheme -Value %d -Type DWord", - key, value, key, value -) -local res = jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'))) - -if res.success then - jarvis.system.notify( - lang == "ru" and "Тема" or "Theme", - is_light and (lang == "ru" and "Светлая включена" or "Light enabled") - or (lang == "ru" and "Тёмная включена" or "Dark enabled") - ) - jarvis.audio.play_ok() -else - jarvis.log("error", "theme set failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end - -return { chain = false } diff --git a/resources/commands/time_tracker/command.toml b/resources/commands/time_tracker/command.toml deleted file mode 100644 index 8c4d6be..0000000 --- a/resources/commands/time_tracker/command.toml +++ /dev/null @@ -1,121 +0,0 @@ -# Voice-driven daily time tracking. -# -# Storage: jarvis.state on this pack — a single key "data" holding -# { current_session_start = |nil, sessions = { {start=ts,end=ts}, ... } } -# -# tracker.start marks the beginning of an active session. -# tracker.stop closes the open session and pushes it onto the array. -# tracker.today / tracker.week report totals (sum of completed + -# current open session if any). -# tracker.reset wipes today's entries; speech reply confirms what was wiped. - -[[commands]] -id = "tracker.start" -type = "lua" -script = "start.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "начни отсчёт", - "начни отсчет", - "запусти трекер", - "я начинаю работу", - "начало рабочего дня", -] -en = [ - "start tracking", - "I'm starting work", - "begin tracking", - "start the tracker", -] - - -[[commands]] -id = "tracker.stop" -type = "lua" -script = "stop.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "закончи отсчёт", - "закончи отсчет", - "останови трекер", - "я закончил работу", - "конец рабочего дня", -] -en = [ - "stop tracking", - "I'm done", - "end the tracker", - "stop the tracker", -] - - -[[commands]] -id = "tracker.today" -type = "lua" -script = "today.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "сколько я работаю сегодня", - "сколько отработал сегодня", - "сколько часов сегодня", - "сколько времени за сегодня", -] -en = [ - "how long today", - "how much have I worked today", - "hours today", - "time today", -] - - -[[commands]] -id = "tracker.week" -type = "lua" -script = "week.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "сколько на этой неделе", - "сколько отработал на неделе", - "часы за неделю", - "статистика за неделю", -] -en = [ - "hours this week", - "how long this week", - "time this week", - "weekly hours", -] - - -[[commands]] -id = "tracker.reset" -type = "lua" -script = "reset.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "сбрось трекер", - "обнули трекер", - "очисти трекер", - "сбрось отсчёт", -] -en = [ - "reset tracker", - "clear tracker", - "wipe today's tracker", - "reset today's tracker", -] diff --git a/resources/commands/time_tracker/reset.lua b/resources/commands/time_tracker/reset.lua deleted file mode 100644 index 3dc7c52..0000000 --- a/resources/commands/time_tracker/reset.lua +++ /dev/null @@ -1,59 +0,0 @@ --- tracker.reset — wipe today's entries (and the open session). -local lang = jarvis.context.language or "ru" -local now = jarvis.context.time.timestamp or os.time() - -local t = jarvis.context.time -local h = tonumber(t.hour) or 0 -local mi = tonumber(t.minute) or 0 -local s = tonumber(t.second) or 0 -local today_start = now - (h * 3600 + mi * 60 + s) - -local data = jarvis.state.get("data") or {} -local sessions = data.sessions or {} - -local kept = {} -local dropped = 0 -for _, sess in ipairs(sessions) do - local sstart = tonumber(sess.start) or 0 - if sstart < today_start then - table.insert(kept, sess) - else - dropped = dropped + 1 - end -end - -local had_open = data.current_session_start ~= nil -data.sessions = kept -data.current_session_start = nil -jarvis.state.set("data", data) - -if dropped == 0 and not had_open then - return jarvis.cmd.ok(lang == "ru" - and "Сегодня сбрасывать нечего, сэр." - or "Nothing to reset today, sir.") -end - -local confirm -if lang == "ru" then - confirm = "Подтверждаю: трекер за сегодня очищен" - if dropped > 0 then - local sw - local n10 = dropped % 10; local n100 = dropped % 100 - if n100 >= 11 and n100 <= 14 then sw = "сессий" - elseif n10 == 1 then sw = "сессия" - elseif n10 >= 2 and n10 <= 4 then sw = "сессии" - else sw = "сессий" end - confirm = confirm .. ", удалено " .. dropped .. " " .. sw - end - if had_open then confirm = confirm .. ", активный отсчёт остановлен" end - confirm = confirm .. "." -else - confirm = "Confirmed: tracker for today cleared" - if dropped > 0 then - confirm = confirm .. ", " .. dropped .. " session" .. (dropped == 1 and "" or "s") .. " removed" - end - if had_open then confirm = confirm .. ", running session stopped" end - confirm = confirm .. "." -end - -return jarvis.cmd.ok(confirm) diff --git a/resources/commands/time_tracker/start.lua b/resources/commands/time_tracker/start.lua deleted file mode 100644 index b1380f2..0000000 --- a/resources/commands/time_tracker/start.lua +++ /dev/null @@ -1,22 +0,0 @@ --- tracker.start — open a new active session. -local lang = jarvis.context.language or "ru" -local now = jarvis.context.time.timestamp or os.time() - -local data = jarvis.state.get("data") or {} -local sessions = data.sessions or {} - -if data.current_session_start then - -- already running — don't overwrite; just acknowledge. - return jarvis.cmd.ok(lang == "ru" - and "Уже считаю, сэр." - or "Already tracking, sir.") -end - -data.current_session_start = now -data.sessions = sessions - -jarvis.state.set("data", data) - -return jarvis.cmd.ok(lang == "ru" - and "Отсчёт пошёл." - or "Tracking started.") diff --git a/resources/commands/time_tracker/stop.lua b/resources/commands/time_tracker/stop.lua deleted file mode 100644 index c633214..0000000 --- a/resources/commands/time_tracker/stop.lua +++ /dev/null @@ -1,41 +0,0 @@ --- tracker.stop — close the currently open session. -local lang = jarvis.context.language or "ru" -local now = jarvis.context.time.timestamp or os.time() - -local data = jarvis.state.get("data") or {} -local sessions = data.sessions or {} - -local start_ts = data.current_session_start -if not start_ts then - return jarvis.cmd.not_found(lang == "ru" - and "Трекер не запущен, сэр." - or "Tracker isn't running, sir.") -end - -local elapsed = now - start_ts -if elapsed < 1 then elapsed = 1 end - -table.insert(sessions, { start = start_ts, ["end"] = now }) - -data.current_session_start = nil -data.sessions = sessions -jarvis.state.set("data", data) - -local function format_dur(sec) - sec = math.floor(sec) - local h = math.floor(sec / 3600) - local m = math.floor((sec % 3600) / 60) - if lang == "ru" then - if h > 0 and m > 0 then return h .. " ч " .. m .. " мин" end - if h > 0 then return h .. " ч" end - return m .. " мин" - else - if h > 0 and m > 0 then return h .. "h " .. m .. "m" end - if h > 0 then return h .. "h" end - return m .. "m" - end -end - -return jarvis.cmd.ok(lang == "ru" - and ("Записал. Сессия: " .. format_dur(elapsed) .. ".") - or ("Done. Session: " .. format_dur(elapsed) .. ".")) diff --git a/resources/commands/time_tracker/today.lua b/resources/commands/time_tracker/today.lua deleted file mode 100644 index 8adda52..0000000 --- a/resources/commands/time_tracker/today.lua +++ /dev/null @@ -1,81 +0,0 @@ --- tracker.today — report total active time accrued today (local-day). -local lang = jarvis.context.language or "ru" -local now = jarvis.context.time.timestamp or os.time() - -local data = jarvis.state.get("data") or {} -local sessions = data.sessions or {} - --- Local-day boundary: use the calendar fields (already in local time). -local t = jarvis.context.time -local y = tonumber(t.year) or 1970 -local mo = tonumber(t.month) or 1 -local d = tonumber(t.day) or 1 -local h = tonumber(t.hour) or 0 -local mi = tonumber(t.minute) or 0 -local s = tonumber(t.second) or 0 - --- Seconds since local midnight today, derived from the broken-down time. -local secs_today = h * 3600 + mi * 60 + s -local today_start = now - secs_today - -local total = 0 -for _, sess in ipairs(sessions) do - local sstart = tonumber(sess.start) or 0 - local send = tonumber(sess["end"]) or 0 - -- Overlap with [today_start, now] - local lo = math.max(sstart, today_start) - local hi = math.min(send, now) - if hi > lo then total = total + (hi - lo) end -end - --- Include the open session, if any, up to now. -local open_start = data.current_session_start -if open_start then - local lo = math.max(tonumber(open_start) or 0, today_start) - if now > lo then total = total + (now - lo) end -end - -local function format_dur(sec) - sec = math.floor(sec) - local hh = math.floor(sec / 3600) - local mm = math.floor((sec % 3600) / 60) - if lang == "ru" then - local function h_word(n) - local n10 = n % 10; local n100 = n % 100 - if n100 >= 11 and n100 <= 14 then return "часов" end - if n10 == 1 then return "час" end - if n10 >= 2 and n10 <= 4 then return "часа" end - return "часов" - end - local function m_word(n) - local n10 = n % 10; local n100 = n % 100 - if n100 >= 11 and n100 <= 14 then return "минут" end - if n10 == 1 then return "минута" end - if n10 >= 2 and n10 <= 4 then return "минуты" end - return "минут" - end - if hh > 0 and mm > 0 then - return hh .. " " .. h_word(hh) .. " " .. mm .. " " .. m_word(mm) - end - if hh > 0 then return hh .. " " .. h_word(hh) end - return mm .. " " .. m_word(mm) - else - if hh > 0 and mm > 0 then return hh .. "h " .. mm .. "m" end - if hh > 0 then return hh .. "h" end - return mm .. "m" - end -end - --- Suppress unused-warning for y/mo/d — only h/mi/s feed the calc, but having --- y/mo/d on hand keeps the script easy to extend later. -local _ = y + mo + d - -if total < 60 then - return jarvis.cmd.ok(lang == "ru" - and "Сегодня ещё ничего не отработано, сэр." - or "Nothing tracked yet today, sir.") -end - -return jarvis.cmd.ok(lang == "ru" - and ("Сегодня отработано " .. format_dur(total) .. ".") - or ("Today: " .. format_dur(total) .. ".")) diff --git a/resources/commands/time_tracker/week.lua b/resources/commands/time_tracker/week.lua deleted file mode 100644 index 89e9947..0000000 --- a/resources/commands/time_tracker/week.lua +++ /dev/null @@ -1,71 +0,0 @@ --- tracker.week — total active time over the last 7 calendar days. -local lang = jarvis.context.language or "ru" -local now = jarvis.context.time.timestamp or os.time() - -local data = jarvis.state.get("data") or {} -local sessions = data.sessions or {} - --- Local-day boundary today, then back up 6 days to get a 7-day window. -local t = jarvis.context.time -local h = tonumber(t.hour) or 0 -local mi = tonumber(t.minute) or 0 -local s = tonumber(t.second) or 0 -local secs_today = h * 3600 + mi * 60 + s -local today_start = now - secs_today -local week_start = today_start - 6 * 86400 - -local total = 0 -for _, sess in ipairs(sessions) do - local sstart = tonumber(sess.start) or 0 - local send = tonumber(sess["end"]) or 0 - local lo = math.max(sstart, week_start) - local hi = math.min(send, now) - if hi > lo then total = total + (hi - lo) end -end - -local open_start = data.current_session_start -if open_start then - local lo = math.max(tonumber(open_start) or 0, week_start) - if now > lo then total = total + (now - lo) end -end - -local function format_dur(sec) - sec = math.floor(sec) - local hh = math.floor(sec / 3600) - local mm = math.floor((sec % 3600) / 60) - if lang == "ru" then - local function h_word(n) - local n10 = n % 10; local n100 = n % 100 - if n100 >= 11 and n100 <= 14 then return "часов" end - if n10 == 1 then return "час" end - if n10 >= 2 and n10 <= 4 then return "часа" end - return "часов" - end - local function m_word(n) - local n10 = n % 10; local n100 = n % 100 - if n100 >= 11 and n100 <= 14 then return "минут" end - if n10 == 1 then return "минута" end - if n10 >= 2 and n10 <= 4 then return "минуты" end - return "минут" - end - if hh > 0 and mm > 0 then - return hh .. " " .. h_word(hh) .. " " .. mm .. " " .. m_word(mm) - end - if hh > 0 then return hh .. " " .. h_word(hh) end - return mm .. " " .. m_word(mm) - else - if hh > 0 and mm > 0 then return hh .. "h " .. mm .. "m" end - if hh > 0 then return hh .. "h" end - return mm .. "m" - end -end - -if total < 60 then - return jarvis.cmd.ok(lang == "ru" - and "За эту неделю ничего не отработано, сэр." - or "Nothing tracked this week, sir.") -end - -return jarvis.cmd.ok(lang == "ru" - and ("За неделю отработано " .. format_dur(total) .. ".") - or ("This week: " .. format_dur(total) .. ".")) diff --git a/resources/commands/translate/clipboard.lua b/resources/commands/translate/clipboard.lua deleted file mode 100644 index cd63bdd..0000000 --- a/resources/commands/translate/clipboard.lua +++ /dev/null @@ -1,53 +0,0 @@ --- "Переведи буфер на английский" → grab clipboard, translate, speak, put back -local phrase = (jarvis.context.phrase or ""):lower() - --- Detect target language from phrase, default English. -local target = "english" -if phrase:find("на русск") then target = "русский" -elseif phrase:find("на немецк") then target = "немецкий" -elseif phrase:find("на французск") then target = "французский" -elseif phrase:find("на испанск") then target = "испанский" -elseif phrase:find("на итальянск") then target = "итальянский" -elseif phrase:find("на польск") then target = "польский" -elseif phrase:find("на турецк") then target = "турецкий" -elseif phrase:find("на китайск") then target = "китайский" -elseif phrase:find("на японск") then target = "японский" -elseif phrase:find("german") then target = "german" -elseif phrase:find("french") then target = "french" -elseif phrase:find("spanish") then target = "spanish" -elseif phrase:find("english") then target = "english" -elseif phrase:find("russian") then target = "русский" -end - -local src = jarvis.system.clipboard.get() -if not src or src == "" then - jarvis.speak("Буфер пуст.") - jarvis.audio.play_not_found() - return { chain = false } -end - --- Trim long input — translate first ~2000 chars for cost/latency. -if #src > 2000 then - src = src:sub(1, 2000) .. "..." -end - -local result = jarvis.llm({ - { role = "system", content = "Ты переводчик. Переводи строго на запрошенный язык без комментариев и пояснений. Сохраняй смысл и стиль." }, - { role = "user", content = string.format("Переведи на %s:\n\n%s", target, src) } -}, { max_tokens = 800, temperature = 0.2 }) - -if not result or result == "" then - jarvis.speak("Не получилось перевести.") - jarvis.audio.play_error() - return { chain = false } -end - --- Put translation back into clipboard -jarvis.system.clipboard.set(result) - --- Speak only first sentence to avoid long monologue. -local first_sentence = result:match("^[^%.!?]*[%.!?]") or result:sub(1, 200) -jarvis.speak("Перевёл на " .. target .. ". " .. first_sentence) -jarvis.system.notify("Перевод (в буфере)", result:sub(1, 200)) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/translate/command.toml b/resources/commands/translate/command.toml deleted file mode 100644 index 9bbca1e..0000000 --- a/resources/commands/translate/command.toml +++ /dev/null @@ -1,56 +0,0 @@ -[[commands]] -id = "translate" -type = "lua" -script = "translate.lua" -sandbox = "full" -timeout = 20000 - -[commands.phrases] -ru = [ - "переведи на английский", - "переведи на русский", - "переведи на немецкий", - "переведи на французский", - "переведи на испанский", - "переведи на итальянский", - "переведи на польский", - "переведи на турецкий", - "переведи на китайский", - "переведи на японский", - "переведи", - "как по английски", - "как по русски", - "как по немецки", - "как по французски", -] -en = [ - "translate to english", - "translate to russian", - "translate to german", - "translate to french", - "translate to spanish", - "translate", - "how to say in", -] - - -# Translate clipboard contents — no need to dictate the source text out loud. -[[commands]] -id = "translate_clipboard" -type = "lua" -script = "clipboard.lua" -sandbox = "full" -timeout = 20000 - -[commands.phrases] -ru = [ - "переведи буфер", - "переведи из буфера", - "переведи буфер обмена", - "переведи скопированное", - "переведи то что я скопировал", -] -en = [ - "translate clipboard", - "translate from clipboard", -] diff --git a/resources/commands/translate/translate.lua b/resources/commands/translate/translate.lua deleted file mode 100644 index db62e32..0000000 --- a/resources/commands/translate/translate.lua +++ /dev/null @@ -1,138 +0,0 @@ -local lang = jarvis.context.language -local phrase = (jarvis.context.phrase or ""):lower() - -local target_languages = { - ["английский"] = "English", ["english"] = "English", - ["русский"] = "Russian", ["russian"] = "Russian", - ["немецкий"] = "German", ["german"] = "German", - ["французский"]= "French", ["french"] = "French", - ["испанский"] = "Spanish", ["spanish"] = "Spanish", - ["итальянский"]= "Italian", ["italian"] = "Italian", - ["китайский"] = "Chinese", ["chinese"] = "Chinese", - ["японский"] = "Japanese", ["japanese"] = "Japanese", - ["польский"] = "Polish", ["polish"] = "Polish", - ["турецкий"] = "Turkish", ["turkish"] = "Turkish", -} - -local triggers_with_lang = { - "переведи на ", "translate to ", "how to say in ", -} -local generic_triggers = { - "переведи", "translate", "как по ", "how to say", -} - -local target = nil -local body = phrase - -for _, t in ipairs(triggers_with_lang) do - local s, e = string.find(body, t, 1, true) - if s == 1 then - local rest = body:sub(e + 1) - local first_word = rest:match("^(%S+)") - if first_word and target_languages[first_word] then - target = target_languages[first_word] - body = rest:sub(#first_word + 1) - break - end - end -end - -if not target then - for _, t in ipairs(generic_triggers) do - local s, e = string.find(body, t, 1, true) - if s == 1 then - body = body:sub(e + 1) - break - end - end -end - -body = body:gsub("^%s+", ""):gsub("%s+$", "") - -if body == "" then - jarvis.system.notify( - "Translate", - lang == "ru" and "Что переводить?" or "What to translate?" - ) - jarvis.audio.play_error() - return { chain = false } -end - -local token = jarvis.system.env("GROQ_TOKEN") -if not token or token == "" then - jarvis.system.notify("Translate", "GROQ_TOKEN не задан") - jarvis.audio.play_error() - return { chain = false } -end - -local base = jarvis.system.env("GROQ_BASE_URL"); if not base or base == "" then base = "https://api.groq.com/openai/v1" end -local model = jarvis.system.env("GROQ_MODEL"); if not model or model == "" then model = "llama-3.3-70b-versatile" end - -local target_label = target or (lang == "ru" and "English" or "Russian") -local sys = string.format( - "You are a professional translator. Translate the user message into %s. " - .. "Output ONLY the translation, no explanations, no quotes, no prefix. " - .. "Preserve idioms and tone.", - target_label -) - -jarvis.log("info", "translate -> " .. target_label .. " : " .. body) - -local payload = { - model = model, - messages = { - { role = "system", content = sys }, - { role = "user", content = body }, - }, - max_tokens = 512, - temperature = 0.3, -} -local res = jarvis.http.post_json(base .. "/chat/completions", payload, { Authorization = "Bearer " .. token }) - -if not res.ok then - jarvis.log("error", "translate api failed: " .. tostring(res.status) .. " " .. tostring(res.body):sub(1, 200)) - jarvis.system.notify("Translate", "Ошибка API") - jarvis.audio.play_error() - return { chain = false } -end - -local body_resp = res.body or "" -local content = body_resp:match('"content"%s*:%s*"(.-[^\\])"') -if not content then - jarvis.system.notify("Translate", "Не смог распарсить ответ") - jarvis.audio.play_error() - return { chain = false } -end - -content = content:gsub('\\n', '\n'):gsub('\\"', '"'):gsub('\\\\', '\\'):gsub('\\t', '\t') -content = content:gsub("^%s+", ""):gsub("%s+$", "") - -jarvis.system.clipboard.set(content) -jarvis.system.notify(target_label, content:sub(1, 200)) -jarvis.log("info", "translation: " .. content) - --- Speak the translation in the TARGET language's voice when possible, so a --- French translation comes out with a French SAPI voice if installed. Falls --- back silently to the default voice if there's no matching locale on the box. -local function language_iso(label) - if label == "Russian" then return "ru" end - if label == "German" then return "de" end - if label == "French" then return "fr" end - if label == "Spanish" then return "es" end - if label == "Italian" then return "it" end - if label == "Chinese" then return "zh" end - if label == "Japanese" then return "ja" end - if label == "Polish" then return "pl" end - if label == "Turkish" then return "tr" end - return "en" -end -local sapi_text = content:gsub("'", "''"):gsub("\r?\n", " ") -local ps = string.format( - [[Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; $iso='%s'; foreach ($v in $s.GetInstalledVoices()) { if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq $iso) { try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} } } $s.Speak('%s')]], - language_iso(target_label), - sapi_text -) -jarvis.system.exec(string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', ps:gsub('"', '\\"'))) - -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/trivia/command.toml b/resources/commands/trivia/command.toml deleted file mode 100644 index 116a47b..0000000 --- a/resources/commands/trivia/command.toml +++ /dev/null @@ -1,27 +0,0 @@ -# Random MCU / Iron Man trivia — one of dozens of fixed lines per language. -# Different from the existing `interesting_fact/` (which is general curiosity) -# and `personality/tony_quote` (which is verbatim film quotes). -# These are short factoids about the suit, the Stark universe, etc. - -[[commands]] -id = "trivia.iron_man" -type = "lua" -script = "trivia.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "расскажи про железного человека", - "факт про железного человека", - "trivia про старка", - "интересное про старка", - "марвел факт", -] -en = [ - "tell me about iron man", - "iron man trivia", - "stark trivia", - "marvel fact", - "tell me a marvel fact", -] diff --git a/resources/commands/trivia/trivia.lua b/resources/commands/trivia/trivia.lua deleted file mode 100644 index fd9037c..0000000 --- a/resources/commands/trivia/trivia.lua +++ /dev/null @@ -1,46 +0,0 @@ --- Random one-liner about the Stark universe. Pool is hand-curated so it --- doesn't repeat what `personality/tony_quote` already does (verbatim quotes) --- and stays in the realm of in-universe trivia rather than out-of-universe --- production facts. - -local lang = jarvis.context.language - -local pool_ru = { - "Первый костюм Старк собрал в пещере в Афганистане из подручного железа. Десять рабочих недель, ноль вентиляции.", - "Дуговый реактор изначально питал электромагнит, удерживающий осколки шрапнели у самого сердца.", - "Палладий в первом реакторе медленно отравлял Старка. Решением стал новый, синтезированный отцом, элемент.", - "Mark I весил около 200 килограммов и летал лишь однажды — на побег из плена.", - "Mark II был первым серебристым прототипом. Старк сам же его и потерял — у Колонеля Роудса.", - "Mark III выкрашен в красно-золотой потому, что чистое золото оказалось мягче, чем хотелось.", - "Mark V — складной костюм в чемодане. Идеальное оружие на званый ужин.", - "Hulkbuster (Mark XLIV) был запущен совместно с Брюсом Беннером — на случай, если Халк выйдет из берегов.", - "Mark XLII собирался к носителю по запросу — ранние тесты заканчивались переломами.", - "Я, Сэр, появился в фильме «Железный человек» 2008 года. Меня озвучил Пол Беттани.", - "Имя J.A.R.V.I.S. — отсылка к Эдвину Джарвису, дворецкому отца Тони, Говарда Старка.", - "После событий «Эры Альтрона» программа J.A.R.V.I.S. была интегрирована в Vision. Это уже другая история.", - "Шрапнель, прижатая электромагнитом — это вольная экстраполяция комиксов. В книге Тони просто носил жилет.", - "Реактор-дуга в груди не лишал Старка возможности проходить через металлодетекторы. Не каждый раз.", - "F.R.I.D.A.Y. пришла мне на смену в «Эре Альтрона». Не самая лестная замена, но что поделать.", -} - -local pool_en = { - "Stark built the first suit in a cave in Afghanistan from scavenged parts. Ten weeks, no ventilation.", - "The arc reactor originally powered an electromagnet keeping shrapnel away from his heart.", - "Palladium in the first reactor was slowly poisoning him. The cure was a new element his father had synthesised.", - "Mark I weighed roughly 200 kilograms and flew exactly once — for the escape.", - "Mark II was the first silver prototype. Stark himself lost it — to Colonel Rhodes.", - "Mark III is red-and-gold because pure gold turned out to be too soft.", - "Mark V folds into a briefcase. Useful at galas.", - "Hulkbuster — Mark XLIV — was built with Bruce Banner, just in case.", - "Mark XLII assembled around its wearer on demand. Early tests broke bones.", - "I, sir, first appeared in Iron Man (2008). Paul Bettany voiced me.", - "The name J.A.R.V.I.S. nods to Edwin Jarvis, butler to Tony's father Howard.", - "After Age of Ultron, my code was folded into Vision. A different story.", - "Shrapnel held back by an electromagnet is a film conceit; the comics had Tony in a chest plate.", - "An arc reactor doesn't always trigger a metal detector. Not always.", - "F.R.I.D.A.Y. replaced me in Age of Ultron. Not a flattering swap.", -} - -local pool = (lang == "ru") and pool_ru or pool_en -local idx = math.random(#pool) -return jarvis.cmd.ok(pool[idx]) diff --git a/resources/commands/unit_convert/command.toml b/resources/commands/unit_convert/command.toml deleted file mode 100644 index 5494b5a..0000000 --- a/resources/commands/unit_convert/command.toml +++ /dev/null @@ -1,78 +0,0 @@ -# Unit conversion — length, weight, temperature, speed, volume. -# Pure Lua, no external API. - -[[commands]] -id = "convert.length" -type = "lua" -script = "length.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "переведи метры в футы", - "переведи футы в метры", - "сколько метров", - "сколько футов", - "сколько километров", - "сколько миль", - "переведи в метры", - "переведи в футы", - "переведи в км", - "переведи в мили", -] -en = ["convert meters to feet", "convert feet to meters", "how many meters", "how many feet"] - - -[[commands]] -id = "convert.weight" -type = "lua" -script = "weight.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "переведи килограммы в фунты", - "переведи фунты в килограммы", - "сколько кг в фунтах", - "сколько фунтов", - "сколько килограммов", -] -en = ["convert kg to pounds", "how many pounds", "how many kg"] - - -[[commands]] -id = "convert.temperature" -type = "lua" -script = "temperature.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "переведи цельсий в фаренгейт", - "переведи фаренгейт в цельсий", - "сколько по фаренгейту", - "сколько по цельсию", - "сколько градусов в фаренгейтах", -] -en = ["convert celsius to fahrenheit", "celsius to fahrenheit", "fahrenheit to celsius"] - - -[[commands]] -id = "convert.speed" -type = "lua" -script = "speed.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "переведи километры в час в мили в час", - "переведи в кмч", - "переведи в мили в час", - "сколько миль в час", - "сколько км в час", -] -en = ["convert kmh to mph", "kmh to mph", "mph to kmh"] diff --git a/resources/commands/unit_convert/length.lua b/resources/commands/unit_convert/length.lua deleted file mode 100644 index 588ffa8..0000000 --- a/resources/commands/unit_convert/length.lua +++ /dev/null @@ -1,33 +0,0 @@ --- Длина: метры ↔ футы, км ↔ мили -local phrase = (jarvis.context.phrase or ""):lower() -local n_str = phrase:match("([%d%.,]+)") -if not n_str then return jarvis.cmd.error("Не понял число.") end -local n = tonumber((n_str:gsub(",", "."))) -if not n then return jarvis.cmd.error("Не понял число.") end - --- detect "in" target — "в футы" / "в метры" / "в км" / "в мили" --- and source from before "в" or from common units in phrase -local result, unit_out - -if phrase:find("в фут") or phrase:find("to feet") then - -- meters → feet - result = n * 3.28084 - unit_out = result < 2 and "фут" or (result < 5 and "фута" or "футов") -elseif phrase:find("в метр") or phrase:find("to meters") then - -- feet → meters - result = n / 3.28084 - unit_out = "метров" -elseif phrase:find("в милях") or phrase:find("в миль") or phrase:find("to miles") then - -- km → miles - result = n * 0.621371 - unit_out = result < 2 and "миля" or (result < 5 and "мили" or "миль") -elseif phrase:find("в километр") or phrase:find("в км") or phrase:find("to km") then - -- miles → km - result = n * 1.60934 - unit_out = "километров" -else - return jarvis.cmd.error("Скажите в чём перевести: в метры/футы/км/мили.") -end - -local rounded = math.floor(result * 100 + 0.5) / 100 -return jarvis.cmd.ok(string.format("%g %s.", rounded, unit_out)) diff --git a/resources/commands/unit_convert/speed.lua b/resources/commands/unit_convert/speed.lua deleted file mode 100644 index 3f9f95f..0000000 --- a/resources/commands/unit_convert/speed.lua +++ /dev/null @@ -1,21 +0,0 @@ --- Скорость: км/ч ↔ миль/ч -local phrase = (jarvis.context.phrase or ""):lower() -local n_str = phrase:match("([%d%.,]+)") -if not n_str then return jarvis.cmd.error("Не понял число.") end -local n = tonumber((n_str:gsub(",", "."))) -if not n then return jarvis.cmd.error("Не понял число.") end - -local result, unit_out - -if phrase:find("в милях") or phrase:find("в миль") or phrase:find("to mph") then - result = n * 0.621371 - unit_out = "миль в час" -elseif phrase:find("в кмч") or phrase:find("в км в час") or phrase:find("в километров") or phrase:find("to kmh") then - result = n * 1.60934 - unit_out = "километров в час" -else - return jarvis.cmd.error("Скажите: в милях в час или в км в час.") -end - -local rounded = math.floor(result + 0.5) -return jarvis.cmd.ok(string.format("%d %s.", rounded, unit_out)) diff --git a/resources/commands/unit_convert/temperature.lua b/resources/commands/unit_convert/temperature.lua deleted file mode 100644 index 6482400..0000000 --- a/resources/commands/unit_convert/temperature.lua +++ /dev/null @@ -1,21 +0,0 @@ --- Температура: Цельсий ↔ Фаренгейт -local phrase = (jarvis.context.phrase or ""):lower() -local n_str = phrase:match("(%-?[%d%.,]+)") -if not n_str then return jarvis.cmd.error("Не понял число.") end -local n = tonumber((n_str:gsub(",", "."))) -if not n then return jarvis.cmd.error("Не понял число.") end - -local result, unit_out - -if phrase:find("в фаренгейт") or phrase:find("to fahrenheit") then - result = n * 9.0 / 5.0 + 32.0 - unit_out = "по Фаренгейту" -elseif phrase:find("в цельси") or phrase:find("to celsius") then - result = (n - 32.0) * 5.0 / 9.0 - unit_out = "по Цельсию" -else - return jarvis.cmd.error("Скажите в чём перевести: в фаренгейт или в цельсий.") -end - -local rounded = math.floor(result * 10 + 0.5) / 10 -return jarvis.cmd.ok(string.format("%g градусов %s.", rounded, unit_out)) diff --git a/resources/commands/unit_convert/weight.lua b/resources/commands/unit_convert/weight.lua deleted file mode 100644 index 60f89c9..0000000 --- a/resources/commands/unit_convert/weight.lua +++ /dev/null @@ -1,23 +0,0 @@ --- Вес: кг ↔ фунты -local phrase = (jarvis.context.phrase or ""):lower() -local n_str = phrase:match("([%d%.,]+)") -if not n_str then return jarvis.cmd.error("Не понял число.") end -local n = tonumber((n_str:gsub(",", "."))) -if not n then return jarvis.cmd.error("Не понял число.") end - -local result, unit_out - -if phrase:find("в фунт") or phrase:find("to pound") then - -- kg → lbs - result = n * 2.20462 - unit_out = result < 2 and "фунт" or (result < 5 and "фунта" or "фунтов") -elseif phrase:find("в кг") or phrase:find("в килограм") or phrase:find("to kg") then - -- lbs → kg - result = n / 2.20462 - unit_out = "килограмм" -else - return jarvis.cmd.error("Скажите в чём перевести: в фунты или в кг.") -end - -local rounded = math.floor(result * 100 + 0.5) / 100 -return jarvis.cmd.ok(string.format("%g %s.", rounded, unit_out)) diff --git a/resources/commands/vision/command.toml b/resources/commands/vision/command.toml deleted file mode 100644 index 6afe857..0000000 --- a/resources/commands/vision/command.toml +++ /dev/null @@ -1,28 +0,0 @@ -# IMBA-4: Multimodal — screenshot + vision LLM. - -[[commands]] -id = "vision.describe" -type = "lua" -script = "describe.lua" -sandbox = "full" -timeout = 30000 - -[commands.phrases] -ru = [ - "что на экране", "опиши экран", - "посмотри на экран", "что у меня на экране", - "посмотри на мой экран", "глянь на экран", -] -en = ["what's on screen", "describe screen", "look at my screen"] - - -[[commands]] -id = "vision.read_error" -type = "lua" -script = "read_error.lua" -sandbox = "full" -timeout = 30000 - -[commands.phrases] -ru = ["прочитай ошибку", "что за ошибка", "помоги с ошибкой"] -en = ["read the error", "what's the error", "help with the error"] diff --git a/resources/commands/vision/describe.lua b/resources/commands/vision/describe.lua deleted file mode 100644 index df969e0..0000000 --- a/resources/commands/vision/describe.lua +++ /dev/null @@ -1,13 +0,0 @@ -jarvis.speak("Сейчас посмотрю.") - -local desc = jarvis.vision.describe("Опиши коротко (1-3 предложения) что сейчас на экране. По-русски.") - -if not desc or desc == "" then - jarvis.speak("Не удалось разобрать экран. Проверь GROQ_TOKEN.") - jarvis.audio.play_error() - return { chain = false } -end - -jarvis.speak(desc) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/vision/read_error.lua b/resources/commands/vision/read_error.lua deleted file mode 100644 index 7208fc0..0000000 --- a/resources/commands/vision/read_error.lua +++ /dev/null @@ -1,18 +0,0 @@ -jarvis.speak("Смотрю на ошибку.") - -local prompt = [[Найди на экране текст ошибки (сообщение об ошибке, stack trace, диалог об исключении). -Прочитай ключевое сообщение и одним коротким предложением подскажи возможную причину. -Если ошибки нет — скажи "ошибок не вижу". -Отвечай по-русски. 1-3 предложения максимум.]] - -local desc = jarvis.vision.describe(prompt) - -if not desc or desc == "" then - jarvis.speak("Не получилось прочитать.") - jarvis.audio.play_error() - return { chain = false } -end - -jarvis.speak(desc) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/voice_type/command.toml b/resources/commands/voice_type/command.toml deleted file mode 100644 index f9a2230..0000000 --- a/resources/commands/voice_type/command.toml +++ /dev/null @@ -1,19 +0,0 @@ -[[commands]] -id = "type_text" -type = "lua" -script = "type.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "напечатай", - "набери текст", - "введи текст", - "впиши", -] -en = [ - "type", - "type text", - "write text", -] diff --git a/resources/commands/voice_type/type.lua b/resources/commands/voice_type/type.lua deleted file mode 100644 index 0da26ea..0000000 --- a/resources/commands/voice_type/type.lua +++ /dev/null @@ -1,46 +0,0 @@ -local lang = jarvis.context.language -local phrase = (jarvis.context.phrase or "") - -local triggers = { - "набери текст", "введи текст", - "напечатай", "впиши", - "type text", "write text", "type", - "надрукуй", "введи текст", -} - -local lower = phrase:lower() -local text = phrase -for _, t in ipairs(triggers) do - local s, e = string.find(lower, t, 1, true) - if s == 1 then - text = phrase:sub(e + 1) - break - end -end -text = text:gsub("^%s+", ""):gsub("%s+$", "") - -if text == "" then - jarvis.system.notify("Type", lang == "ru" and "Что напечатать?" or "What to type?") - jarvis.audio.play_error() - return { chain = false } -end - -jarvis.system.clipboard.set(text) -jarvis.sleep(120) - -local ps = [[Add-Type -Name K -Namespace W -MemberDefinition '[DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte sc, uint flags, System.UIntPtr extra);'; ]] - .. [[[W.K]::keybd_event(0x11, 0, 0, [UIntPtr]::Zero); ]] - .. [[[W.K]::keybd_event(0x56, 0, 0, [UIntPtr]::Zero); ]] - .. [[Start-Sleep -Milliseconds 30; ]] - .. [[[W.K]::keybd_event(0x56, 0, 2, [UIntPtr]::Zero); ]] - .. [[[W.K]::keybd_event(0x11, 0, 2, [UIntPtr]::Zero)]] -local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', ps:gsub('"', '\\"')) -local res = jarvis.system.exec(cmd) - -if res.success then - jarvis.audio.play_ok() -else - jarvis.log("error", "voice type failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end -return { chain = false } diff --git a/resources/commands/volume/_volume_helper.ps1 b/resources/commands/volume/_volume_helper.ps1 deleted file mode 100644 index 3cc0161..0000000 --- a/resources/commands/volume/_volume_helper.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -param( - [Parameter(Mandatory=$true)] - [ValidateSet("up","down","mute","max")] - [string]$Action, - - [int]$Times = 1 -) - -Add-Type -Name VolumeKey -Namespace Win32 -MemberDefinition @' -[DllImport("user32.dll")] -public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, System.UIntPtr dwExtraInfo); -'@ - -$KEY_UP = 0xAF -$KEY_DOWN = 0xAE -$KEY_MUTE = 0xAD - -$vk = switch ($Action) { - "up" { $KEY_UP } - "down" { $KEY_DOWN } - "mute" { $KEY_MUTE } - "max" { $KEY_UP } -} - -$count = if ($Action -eq "max") { 50 } else { $Times } - -for ($i = 0; $i -lt $count; $i++) { - [Win32.VolumeKey]::keybd_event($vk, 0, 0, [UIntPtr]::Zero) - [Win32.VolumeKey]::keybd_event($vk, 0, 2, [UIntPtr]::Zero) - Start-Sleep -Milliseconds 25 -} diff --git a/resources/commands/volume/command.toml b/resources/commands/volume/command.toml deleted file mode 100644 index 5f3c04f..0000000 --- a/resources/commands/volume/command.toml +++ /dev/null @@ -1,87 +0,0 @@ -[[commands]] -id = "volume_up" -type = "lua" -script = "volume_up.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "громче", - "сделай громче", - "прибавь звук", - "увеличь громкость", - "погромче", -] -en = [ - "louder", - "volume up", - "increase volume", - "turn it up", -] - - -[[commands]] -id = "volume_down" -type = "lua" -script = "volume_down.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "тише", - "сделай тише", - "убавь звук", - "уменьши громкость", - "потише", -] -en = [ - "quieter", - "volume down", - "decrease volume", - "turn it down", -] - - -[[commands]] -id = "volume_mute" -type = "lua" -script = "volume_mute.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "выключи звук", - "заткнись", - "тишина", - "приглуши звук", - "отключи звук", -] -en = [ - "mute", - "silence", - "shut up", - "be quiet", -] - - -[[commands]] -id = "volume_max" -type = "lua" -script = "volume_max.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = [ - "максимальная громкость", - "звук на максимум", - "на полную", -] -en = [ - "max volume", - "full volume", - "maximum", -] diff --git a/resources/commands/volume/volume_down.lua b/resources/commands/volume/volume_down.lua deleted file mode 100644 index db0f7f7..0000000 --- a/resources/commands/volume/volume_down.lua +++ /dev/null @@ -1,16 +0,0 @@ -local helper = jarvis.context.command_path .. "\\_volume_helper.ps1" -local cmd = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action down -Times 5', - helper -) - -local res = jarvis.system.exec(cmd) -if res.success then - jarvis.log("info", "volume down") - jarvis.audio.play_ok() -else - jarvis.log("error", "volume down failed: code=" .. tostring(res.code) .. " stderr=" .. tostring(res.stderr)) - jarvis.audio.play_error() -end - -return { chain = false } diff --git a/resources/commands/volume/volume_max.lua b/resources/commands/volume/volume_max.lua deleted file mode 100644 index 7935502..0000000 --- a/resources/commands/volume/volume_max.lua +++ /dev/null @@ -1,16 +0,0 @@ -local helper = jarvis.context.command_path .. "\\_volume_helper.ps1" -local cmd = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action max', - helper -) - -local res = jarvis.system.exec(cmd) -if res.success then - jarvis.log("info", "volume max") - jarvis.audio.play_ok() -else - jarvis.log("error", "volume max failed: code=" .. tostring(res.code) .. " stderr=" .. tostring(res.stderr)) - jarvis.audio.play_error() -end - -return { chain = false } diff --git a/resources/commands/volume/volume_mute.lua b/resources/commands/volume/volume_mute.lua deleted file mode 100644 index bb29bdb..0000000 --- a/resources/commands/volume/volume_mute.lua +++ /dev/null @@ -1,16 +0,0 @@ -local helper = jarvis.context.command_path .. "\\_volume_helper.ps1" -local cmd = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action mute', - helper -) - -local res = jarvis.system.exec(cmd) -if res.success then - jarvis.log("info", "mute toggled") - jarvis.audio.play_ok() -else - jarvis.log("error", "mute failed: code=" .. tostring(res.code) .. " stderr=" .. tostring(res.stderr)) - jarvis.audio.play_error() -end - -return { chain = false } diff --git a/resources/commands/volume/volume_up.lua b/resources/commands/volume/volume_up.lua deleted file mode 100644 index 0561954..0000000 --- a/resources/commands/volume/volume_up.lua +++ /dev/null @@ -1,16 +0,0 @@ -local helper = jarvis.context.command_path .. "\\_volume_helper.ps1" -local cmd = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action up -Times 5', - helper -) - -local res = jarvis.system.exec(cmd) -if res.success then - jarvis.log("info", "volume up") - jarvis.audio.play_ok() -else - jarvis.log("error", "volume up failed: code=" .. tostring(res.code) .. " stderr=" .. tostring(res.stderr)) - jarvis.audio.play_error() -end - -return { chain = false } diff --git a/resources/commands/weather/command.toml b/resources/commands/weather/command.toml index d92bc14..268f3e5 100644 --- a/resources/commands/weather/command.toml +++ b/resources/commands/weather/command.toml @@ -24,7 +24,8 @@ type = "lua" script = "set_city.lua" sandbox = "standard" timeout = 5000 - -[commands.phrases] -ru = ["установи город", "смени город", "поменяй город"] -en = ["set city", "change city"] \ No newline at end of file +phrases = [ + "установи город", + "set city", + "change city", +] \ No newline at end of file diff --git a/resources/commands/weather_extended/command.toml b/resources/commands/weather_extended/command.toml deleted file mode 100644 index 11c5ba3..0000000 --- a/resources/commands/weather_extended/command.toml +++ /dev/null @@ -1,35 +0,0 @@ -# Extended weather forecast — Open-Meteo (free, no key). -# Existing `weather/` pack covers "what's the weather now"; this adds forecast. - -[[commands]] -id = "weather.tomorrow" -type = "lua" -script = "tomorrow.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = [ - "погода завтра", - "что с погодой завтра", - "завтра будет тепло", - "прогноз на завтра", -] -en = ["weather tomorrow", "forecast tomorrow"] - - -[[commands]] -id = "weather.week" -type = "lua" -script = "week.lua" -sandbox = "full" -timeout = 20000 - -[commands.phrases] -ru = [ - "прогноз на неделю", - "погода на неделю", - "что с погодой на неделю", - "недельный прогноз", -] -en = ["weather this week", "weekly forecast"] diff --git a/resources/commands/weather_extended/tomorrow.lua b/resources/commands/weather_extended/tomorrow.lua deleted file mode 100644 index e724d84..0000000 --- a/resources/commands/weather_extended/tomorrow.lua +++ /dev/null @@ -1,57 +0,0 @@ --- Tomorrow's forecast for the saved or auto-detected location. --- City stored via memory key "weather.lat" + "weather.lon" + "weather.city", --- or auto-detected via ipinfo on first run. -local lat = jarvis.memory.recall("weather.lat") -local lon = jarvis.memory.recall("weather.lon") -local city = jarvis.memory.recall("weather.city") - -if not lat or not lon then - -- Auto-detect via ipinfo.io (free, no key) - local ip = jarvis.http.json("https://ipinfo.io/json") - if not ip or not ip.loc then - return jarvis.cmd.error("Не получилось узнать местоположение.") - end - lat, lon = ip.loc:match("^([^,]+),(.*)$") - if not lat then - return jarvis.cmd.error("Не получилось разобрать координаты.") - end - city = ip.city or "ваш город" - jarvis.memory.remember("weather.lat", lat) - jarvis.memory.remember("weather.lon", lon) - jarvis.memory.remember("weather.city", city) -end - -local url = string.format( - "https://api.open-meteo.com/v1/forecast?latitude=%s&longitude=%s&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code&timezone=auto&forecast_days=2", - lat, lon -) -local data = jarvis.http.json(url) -if not data or not data.daily then - return jarvis.cmd.error("Прогноз недоступен.") -end - -local d = data.daily --- day 2 = tomorrow (day 1 = today in the array) -local tmax = (d.temperature_2m_max and d.temperature_2m_max[2]) or nil -local tmin = (d.temperature_2m_min and d.temperature_2m_min[2]) or nil -local rain = (d.precipitation_sum and d.precipitation_sum[2]) or 0 -local code = (d.weather_code and d.weather_code[2]) or 0 - -if not tmax then - return jarvis.cmd.error("Прогноз неполный.") -end - -local condition = "переменно" -if code == 0 then condition = "ясно" -elseif code <= 3 then condition = "переменная облачность" -elseif code >= 51 and code <= 67 then condition = "дождь" -elseif code >= 71 and code <= 77 then condition = "снег" -elseif code >= 95 then condition = "гроза" -end - -local line = string.format("В %s завтра %s. От %d до %d градусов.", - city or "вашем городе", condition, math.floor(tmin + 0.5), math.floor(tmax + 0.5)) -if rain > 5 then - line = line .. " Возможны осадки." -end -return jarvis.cmd.ok(line) diff --git a/resources/commands/weather_extended/week.lua b/resources/commands/weather_extended/week.lua deleted file mode 100644 index b503071..0000000 --- a/resources/commands/weather_extended/week.lua +++ /dev/null @@ -1,42 +0,0 @@ -local lat = jarvis.memory.recall("weather.lat") -local lon = jarvis.memory.recall("weather.lon") -local city = jarvis.memory.recall("weather.city") - -if not lat or not lon then - local ip = jarvis.http.json("https://ipinfo.io/json") - if not ip or not ip.loc then - return jarvis.cmd.error("Не получилось узнать местоположение.") - end - lat, lon = ip.loc:match("^([^,]+),(.*)$") - city = ip.city or "ваш город" - jarvis.memory.remember("weather.lat", lat) - jarvis.memory.remember("weather.lon", lon) - jarvis.memory.remember("weather.city", city) -end - -local url = string.format( - "https://api.open-meteo.com/v1/forecast?latitude=%s&longitude=%s&daily=temperature_2m_max,temperature_2m_min&timezone=auto&forecast_days=7", - lat, lon -) -local data = jarvis.http.json(url) -if not data or not data.daily or not data.daily.temperature_2m_max then - return jarvis.cmd.error("Прогноз недоступен.") -end - -local maxes = data.daily.temperature_2m_max -local mins = data.daily.temperature_2m_min - --- Find min and max of the week -local week_max, week_min = -100, 100 -for i = 1, #maxes do - if maxes[i] > week_max then week_max = maxes[i] end - if mins[i] < week_min then week_min = mins[i] end -end - -local line = string.format("Неделя в %s: днём от %d до %d градусов, ночью до %d.", - city or "вашем городе", - math.floor(week_min + 0.5), - math.floor(week_max + 0.5), - math.floor(week_min + 0.5)) - -return jarvis.cmd.ok(line) diff --git a/resources/commands/websearch/command.toml b/resources/commands/websearch/command.toml deleted file mode 100644 index a6f5db5..0000000 --- a/resources/commands/websearch/command.toml +++ /dev/null @@ -1,67 +0,0 @@ -[[commands]] -id = "search_google" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "загугли", - "поиск в гугл", - "поищи в гугл", - "найди в гугл", - "ищи в гугл", -] -en = ["google", "search google", "google for"] - - -[[commands]] -id = "search_youtube" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "найди на ютубе", - "поиск на ютубе", - "ищи на ютубе", - "ютуб поиск", -] -en = ["youtube search", "search youtube", "find on youtube"] - - -[[commands]] -id = "search_wiki" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "найди в википедии", - "поиск в википедии", - "википедия", - "вики поиск", -] -en = ["wikipedia search", "search wikipedia", "look up wikipedia"] - - -[[commands]] -id = "search_yandex" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "поищи в яндекс", - "найди в яндекс", - "яндекс поиск", - "пробей в яндекс", -] -en = ["yandex search", "search yandex"] diff --git a/resources/commands/websearch/dispatch.lua b/resources/commands/websearch/dispatch.lua deleted file mode 100644 index 8b4ea97..0000000 --- a/resources/commands/websearch/dispatch.lua +++ /dev/null @@ -1,61 +0,0 @@ -local lang = jarvis.context.language -local phrase = (jarvis.context.phrase or ""):lower() -local cmd_id = jarvis.context.command_id - -local services = { - search_google = { base = "https://www.google.com/search?q=", label = "Google" }, - search_youtube = { base = "https://www.youtube.com/results?search_query=", label = "YouTube" }, - search_wiki = { base = "https://ru.wikipedia.org/wiki/Special:Search?search=", label = "Wikipedia" }, - search_yandex = { base = "https://yandex.ru/search/?text=", label = "Yandex" }, -} - -local svc = services[cmd_id] -if not svc then - jarvis.log("error", "websearch: unknown command_id " .. tostring(cmd_id)) - jarvis.audio.play_error() - return { chain = false } -end - -local triggers = { - "загугли", "поиск в гугл", "поищи в гугл", "найди в гугл", "ищи в гугл", - "найди на ютубе", "поиск на ютубе", "ищи на ютубе", "ютуб поиск", - "найди в википедии", "поиск в википедии", "википедия", "вики поиск", - "поищи в яндекс", "найди в яндекс", "яндекс поиск", "пробей в яндекс", - "search google", "google for", "google", - "youtube search", "search youtube", "find on youtube", - "wikipedia search", "search wikipedia", "look up wikipedia", - "yandex search", "search yandex", - "загугли", "пошукай в гугл", - "знайди на ютубі", "пошук на ютубі", - "знайди в вікіпедії", "пошук у вікіпедії", - "знайди в яндекс", -} - -local q = phrase -for _, t in ipairs(triggers) do - local s, e = string.find(q, t, 1, true) - if s == 1 then - q = q:sub(e + 1) - break - end -end -q = q:gsub("^%s+", ""):gsub("%s+$", "") - -if q == "" then - jarvis.system.notify(svc.label, lang == "ru" and "Что искать?" or "What to search?") - jarvis.audio.play_error() - return { chain = false } -end - -local function url_encode(s) - return (s:gsub("([^%w%-_%.~])", function(c) - return string.format("%%%02X", string.byte(c)) - end)) -end - -local url = svc.base .. url_encode(q) -jarvis.log("info", "websearch " .. svc.label .. ": " .. q) -jarvis.system.open(url) -jarvis.system.notify(svc.label, q) -jarvis.audio.play_ok() -return { chain = false } diff --git a/resources/commands/wikipedia/command.toml b/resources/commands/wikipedia/command.toml deleted file mode 100644 index 89a0a7e..0000000 --- a/resources/commands/wikipedia/command.toml +++ /dev/null @@ -1,33 +0,0 @@ -# Wikipedia summary — fetch a topic's intro from Wikipedia REST API, speak it. - -[[commands]] -id = "wikipedia.summary" -type = "lua" -script = "summary.lua" -sandbox = "full" -timeout = 15000 - -[commands.phrases] -ru = [ - "что такое", - "кто такой", - "кто такая", - "расскажи про", - "википедия", - "найди в википедии", - "что говорит википедия", - "поищи в википедии", - "википедия о", - "википедия про", - "найди в википедии про", - "статья из википедии", -] -en = [ - "wikipedia", - "look up in wikipedia", - "wikipedia article on", - "what is", - "who is", - "tell me about", - "wikipedia about", -] diff --git a/resources/commands/wikipedia/summary.lua b/resources/commands/wikipedia/summary.lua deleted file mode 100644 index 9a90517..0000000 --- a/resources/commands/wikipedia/summary.lua +++ /dev/null @@ -1,105 +0,0 @@ --- "Википедия Эйнштейн" → fetch ru.wikipedia.org/api/rest_v1/page/summary/ --- Falls back to LLM if Wikipedia doesn't have the article. -local phrase = (jarvis.context.phrase or "") - -local query = jarvis.text.strip_trigger(phrase:lower(), { - "найди в википедии про", - "найди в википедии", - "что говорит википедия", - "поищи в википедии", - "статья из википедии", - "википедия про", - "википедия о", - "википедия", - "расскажи про", - "что такое", - "кто такая", - "кто такой", - "look up in wikipedia", - "wikipedia article on", - "wikipedia about", - "wikipedia", - "tell me about", - "what is", - "who is", - "знайди у вікіпедії", - "вікіпедія", - "що таке", - "хто такий", - "розкажи про", -}) -query = query:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if query == "" then - return jarvis.cmd.error("Какая статья?") -end - --- Wikipedia title needs underscores for spaces. Capitalize first letter of each word --- to match canonical titles (good enough heuristic). -local title = query:gsub("(%S+)", function(w) - -- Capitalize first byte (works for ASCII; for cyrillic, leave alone). - local first = w:sub(1, 1) - if first:match("[%a]") then - return first:upper() .. w:sub(2) - end - return w -end):gsub(" ", "_") - -local function urlencode(s) - return (s:gsub("[^A-Za-z0-9%-_%.~]", function(c) - return string.format("%%%02X", string.byte(c)) - end)) -end - --- Try Russian first, fall back to English. -local lang_chain = { "ru", "en" } -local data, source_lang - -for _, lang in ipairs(lang_chain) do - local url = string.format("https://%s.wikipedia.org/api/rest_v1/page/summary/%s", - lang, urlencode(title)) - local d = jarvis.http.json(url) - if d and d.extract and d.extract ~= "" then - data = d - source_lang = lang - break - end -end - -if not data then - -- last resort: ask LLM directly - local llm_answer = jarvis.llm({ - { role = "system", content = "Кратко (2-3 предложения) на русском объясни тему. Если не знаешь — скажи." }, - { role = "user", content = "Расскажи про: " .. query } - }, { max_tokens = 200, temperature = 0.3 }) - if llm_answer and llm_answer ~= "" then - return jarvis.cmd.ok(llm_answer) - end - return jarvis.cmd.not_found("В Википедии нет статьи про " .. query .. ".") -end - -local extract = data.extract -local title_actual = data.title or query - --- Truncate extract to ~3 sentences for spoken output -local sentences = {} -for sentence in extract:gmatch("[^%.%!%?]+[%.%!%?]") do - table.insert(sentences, sentence) - if #sentences >= 3 then break end -end -local spoken = #sentences > 0 and table.concat(sentences) or extract:sub(1, 400) - -if source_lang == "en" then - -- Translate English extract to Russian via LLM (no point in speaking English - -- through a Russian voice). - local translated = jarvis.llm({ - { role = "system", content = "Переведи английский текст на русский, без вводных и комментариев." }, - { role = "user", content = spoken } - }, { max_tokens = 300, temperature = 0.0 }) - if translated and translated ~= "" then - spoken = translated - end -end - -jarvis.system.notify("Wikipedia: " .. title_actual, spoken:sub(1, 200)) -return jarvis.cmd.ok(spoken) diff --git a/resources/commands/window/_window_helper.ps1 b/resources/commands/window/_window_helper.ps1 deleted file mode 100644 index 522d336..0000000 --- a/resources/commands/window/_window_helper.ps1 +++ /dev/null @@ -1,42 +0,0 @@ -param( - [Parameter(Mandatory=$true)] - [string]$Combo -) - -Add-Type -Name WinKey -Namespace Win32 -MemberDefinition @' -[DllImport("user32.dll")] -public static extern void keybd_event(byte vk, byte sc, uint flags, System.UIntPtr extra); -'@ - -$VK = @{ - 'win'=0x5B; 'lwin'=0x5B; 'rwin'=0x5C; - 'alt'=0x12; 'ctrl'=0x11; 'shift'=0x10; - 'a'=0x41; 'b'=0x42; 'c'=0x43; 'd'=0x44; 'e'=0x45; 'f'=0x46; - 'g'=0x47; 'h'=0x48; 'i'=0x49; 'j'=0x4A; 'k'=0x4B; 'l'=0x4C; - 'm'=0x4D; 'n'=0x4E; 'o'=0x4F; 'p'=0x50; 'q'=0x51; 'r'=0x52; - 's'=0x53; 't'=0x54; 'u'=0x55; 'v'=0x56; 'w'=0x57; 'x'=0x58; - 'y'=0x59; 'z'=0x5A; - 'up'=0x26; 'down'=0x28; 'left'=0x25; 'right'=0x27; - 'tab'=0x09; 'esc'=0x1B; 'enter'=0x0D; 'space'=0x20; 'home'=0x24; 'end'=0x23; - 'f1'=0x70; 'f2'=0x71; 'f3'=0x72; 'f4'=0x73; 'f5'=0x74; 'f6'=0x75; - 'f7'=0x76; 'f8'=0x77; 'f9'=0x78; 'f10'=0x79; 'f11'=0x7A; 'f12'=0x7B; -} - -$parts = ($Combo.ToLower() -split '\+') | ForEach-Object { $_.Trim() } -$codes = @() -foreach ($k in $parts) { - if (-not $VK.ContainsKey($k)) { - Write-Error "Unknown key: $k (in combo: $Combo)" - exit 2 - } - $codes += [byte][int]$VK[$k] -} - -foreach ($c in $codes) { - [Win32.WinKey]::keybd_event($c, 0, 0, [UIntPtr]::Zero) -} -Start-Sleep -Milliseconds 35 -[array]::Reverse($codes) -foreach ($c in $codes) { - [Win32.WinKey]::keybd_event($c, 0, 2, [UIntPtr]::Zero) -} diff --git a/resources/commands/window/command.toml b/resources/commands/window/command.toml deleted file mode 100644 index 7a8cd5d..0000000 --- a/resources/commands/window/command.toml +++ /dev/null @@ -1,94 +0,0 @@ -[[commands]] -id = "show_desktop" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["покажи рабочий стол", "свернуть всё", "сверни все окна", "покажи десктоп"] -en = ["show desktop", "minimize all"] - - -[[commands]] -id = "maximize_window" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["разверни окно", "развернуть окно", "на весь экран", "раскрой окно"] -en = ["maximize window", "fullscreen window"] - - -[[commands]] -id = "minimize_window" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["сверни окно", "свернуть окно", "убери окно"] -en = ["minimize window"] - - -[[commands]] -id = "snap_left" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["окно влево", "сдвинь влево", "прижми влево"] -en = ["snap left", "window left"] - - -[[commands]] -id = "snap_right" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["окно вправо", "сдвинь вправо", "прижми вправо"] -en = ["snap right", "window right"] - - -[[commands]] -id = "close_window" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["закрой окно", "закрой вкладку", "закрой текущее", "alt f4"] -en = ["close window", "close tab"] - - -[[commands]] -id = "restore_all" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["восстанови окна", "верни окна", "разверни окна"] -en = ["restore windows", "restore all"] - - -[[commands]] -id = "task_view" -type = "lua" -script = "dispatch.lua" -sandbox = "full" -timeout = 3000 - -[commands.phrases] -ru = ["открой все окна", "обзор окон", "таск вью", "покажи все окна"] -en = ["task view", "show all windows"] diff --git a/resources/commands/window/dispatch.lua b/resources/commands/window/dispatch.lua deleted file mode 100644 index 9a4fdcf..0000000 --- a/resources/commands/window/dispatch.lua +++ /dev/null @@ -1,36 +0,0 @@ -local helper = jarvis.context.command_path .. "\\_window_helper.ps1" -local cmd_id = jarvis.context.command_id - -local combos = { - show_desktop = "win+d", - maximize_window = "win+up", - minimize_window = "win+down", - snap_left = "win+left", - snap_right = "win+right", - close_window = "alt+f4", - restore_all = "win+shift+m", - task_view = "win+tab", -} - -local combo = combos[cmd_id] -if not combo then - jarvis.log("error", "window/dispatch.lua: unknown command_id: " .. tostring(cmd_id)) - jarvis.audio.play_error() - return { chain = false } -end - -local cmd = string.format( - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Combo "%s"', - helper, combo -) -local res = jarvis.system.exec(cmd) - -if res.success then - jarvis.log("info", "window cmd " .. cmd_id .. " (" .. combo .. ")") - jarvis.audio.play_ok() -else - jarvis.log("error", "window cmd " .. combo .. " failed: " .. tostring(res.stderr)) - jarvis.audio.play_error() -end - -return { chain = false } diff --git a/resources/commands/window_switch/command.toml b/resources/commands/window_switch/command.toml deleted file mode 100644 index 21f692d..0000000 --- a/resources/commands/window_switch/command.toml +++ /dev/null @@ -1,20 +0,0 @@ -[[commands]] -id = "switch_to_window" -type = "lua" -script = "switch.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "переключись на", - "переключи на", - "активируй окно", - "покажи окно", - "перейди в", -] -en = [ - "switch to", - "activate window", - "focus window", -] diff --git a/resources/commands/window_switch/switch.lua b/resources/commands/window_switch/switch.lua deleted file mode 100644 index b03f61d..0000000 --- a/resources/commands/window_switch/switch.lua +++ /dev/null @@ -1,51 +0,0 @@ -local lang = jarvis.context.language -local triggers = { - "переключись на", "переключи на", "активируй окно", "покажи окно", "перейди в", - "switch to", "activate window", "focus window", - "перемкни на", "активуй вікно", -} -local query = jarvis.text.strip_trigger(jarvis.context.phrase or "", triggers):lower() -if query == "" then - jarvis.system.notify("Switch", lang == "ru" and "Какое окно?" or "Which window?") - jarvis.audio.play_error() - return { chain = false } -end - -local aliases = { - ["хром"] = "chrome", ["хрома"] = "chrome", - ["едж"] = "msedge", ["эдж"] = "msedge", - ["файрфокс"] = "firefox", ["фаерфокс"] = "firefox", - ["вскод"] = "code", ["vs code"] = "code", - ["дискорд"] = "discord", - ["телега"] = "telegram", ["телеграм"] = "telegram", - ["стим"] = "steam", - ["обс"] = "obs64", - ["проводник"] = "explorer", - ["блокнот"] = "notepad", - ["терминал"] = "windowsterminal", - ["спотифай"] = "spotify", - ["яндекс"] = "browser", - ["браузер"] = "browser", -} -local target = aliases[query] or query - -local ps = string.format([[ -$procs = Get-Process | Where-Object { $_.MainWindowTitle -and ($_.ProcessName -like '*%s*' -or $_.MainWindowTitle -like '*%s*') } | Sort-Object -Property StartTime -Descending -if ($procs.Count -eq 0) { Write-Output 'NOT_FOUND'; exit 0 } -$p = $procs[0] -$wsh = New-Object -ComObject WScript.Shell -$null = $wsh.AppActivate($p.Id) -Write-Output $p.MainWindowTitle -]], target:gsub("'", ""), target:gsub("'", "")) - -local res = jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'))) -local out = (res.stdout or ""):gsub("[\r\n]+$", ""):gsub("^%s+", "") - -if out == "NOT_FOUND" or out == "" then - jarvis.system.notify("Switch", (lang == "ru" and "Не нашёл: " or "Not found: ") .. query) - jarvis.audio.play_not_found() -else - jarvis.system.notify(lang == "ru" and "Переключаюсь" or "Switching", out:sub(1, 80)) - jarvis.audio.play_ok() -end -return { chain = false } diff --git a/resources/commands/wol/command.toml b/resources/commands/wol/command.toml deleted file mode 100644 index 73df56a..0000000 --- a/resources/commands/wol/command.toml +++ /dev/null @@ -1,31 +0,0 @@ -# Wake-on-LAN — fire a magic packet at a target MAC address. The target is -# read from long-term memory under the key "wol_" so the user can have -# multiple machines. Default alias is "server". -# -# To set a target via voice: -# "запомни wol_server AA:BB:CC:DD:EE:FF" -# then later: "разбуди сервер" - -[[commands]] -id = "wol.wake" -type = "lua" -script = "wake.lua" -sandbox = "full" -timeout = 5000 - -[commands.phrases] -ru = [ - "разбуди сервер", - "включи сервер", - "разбуди машину", - "разбуди компьютер", - "включи компьютер", - "wol сервер", -] -en = [ - "wake the server", - "wake server", - "wake on lan", - "wol server", - "boot the server", -] diff --git a/resources/commands/wol/wake.lua b/resources/commands/wol/wake.lua deleted file mode 100644 index 056231a..0000000 --- a/resources/commands/wol/wake.lua +++ /dev/null @@ -1,78 +0,0 @@ --- Wake-on-LAN — sends a magic packet to a target MAC stored in long-term memory. --- --- Setup (one-time, via voice): --- "запомни wol_server AA:BB:CC:DD:EE:FF" --- Then later, anytime: --- "разбуди сервер" → packet goes out to broadcast. --- --- Lua can't open raw UDP, so we shell out to PowerShell (System.Net.Sockets). - -local lang = jarvis.context.language -local phrase = (jarvis.context.phrase or ""):lower() - --- Figure out which alias the user wants. Right now we only support "server" --- (the most common case) but the data model already supports more. -local alias = "server" - -local mac = jarvis.memory.recall("wol_" .. alias) -if not mac or mac == "" then - -- Fall back to env so power users can ship a default without touching memory. - mac = jarvis.system.env("JARVIS_WOL_TARGET") or "" -end - -if mac == "" then - return jarvis.cmd.error(lang == "ru" - and "Сэр, MAC-адрес не задан. Скажи: запомни wol_server, а потом адрес." - or "Sir, MAC address is not set. Say: remember wol_server, then the address.") -end - --- Normalise: AA-BB-CC-DD-EE-FF, AA:BB:CC:DD:EE:FF, AABB.CCDD.EEFF, AABBCCDDEEFF -local cleaned = mac:gsub("[^%w]", ""):upper() -if #cleaned ~= 12 or not cleaned:match("^[0-9A-F]+$") then - return jarvis.cmd.error(lang == "ru" - and "Сэр, MAC-адрес в памяти выглядит подозрительно: " .. tostring(mac) - or "Sir, the stored MAC looks malformed: " .. tostring(mac)) -end - --- Inject the cleaned MAC as a constant in the PowerShell script so we don't --- have to argv-escape it. The packet is 6 × 0xFF + 16 × MAC = 102 bytes, --- broadcast on UDP/9. -local ps_template = [[ -$mac = '%s' -$macBytes = @() -for ($i = 0; $i -lt 12; $i += 2) { - $macBytes += [Convert]::ToByte($mac.Substring($i, 2), 16) -} -$packet = New-Object byte[] 102 -for ($i = 0; $i -lt 6; $i++) { $packet[$i] = 0xFF } -for ($r = 0; $r -lt 16; $r++) { - for ($j = 0; $j -lt 6; $j++) { - $packet[6 + $r * 6 + $j] = $macBytes[$j] - } -} -$udp = New-Object System.Net.Sockets.UdpClient -$udp.EnableBroadcast = $true -$null = $udp.Send($packet, $packet.Length, '255.255.255.255', 9) -$udp.Close() -Write-Output 'sent' -]] - -local ps = string.format(ps_template, cleaned) -local cmd = 'powershell -NoProfile -ExecutionPolicy Bypass -Command "& {' - .. ps:gsub('\r?\n', '; '):gsub('"', '\\"') - .. '}"' - -jarvis.log("info", "WOL packet → " .. cleaned) -local res = jarvis.system.exec(cmd) -if not res.success then - jarvis.log("error", "WOL failed: " .. tostring(res.stderr)) - return jarvis.cmd.error(lang == "ru" - and "Не получилось отправить пакет." - or "Failed to send packet.") -end - --- Format the MAC back for speech: "AA-BB-CC-..." sounds clearer than "AABB..." -local pretty = cleaned:gsub("(%w%w)", "%1-"):sub(1, -2) -return jarvis.cmd.ok(lang == "ru" - and ("Пакет отправлен на " .. pretty) - or ("Packet sent to " .. pretty)) diff --git a/resources/commands/world_clock/command.toml b/resources/commands/world_clock/command.toml deleted file mode 100644 index c6d515c..0000000 --- a/resources/commands/world_clock/command.toml +++ /dev/null @@ -1,17 +0,0 @@ -# World clock — query time in another city/timezone. - -[[commands]] -id = "world_clock" -type = "lua" -script = "time_in.lua" -sandbox = "full" -timeout = 10000 - -[commands.phrases] -ru = [ - "сколько времени в", - "который час в", - "сколько часов в", - "время в", -] -en = ["time in", "what time in"] diff --git a/resources/commands/world_clock/time_in.lua b/resources/commands/world_clock/time_in.lua deleted file mode 100644 index dd561ec..0000000 --- a/resources/commands/world_clock/time_in.lua +++ /dev/null @@ -1,95 +0,0 @@ --- "Сколько времени в Токио" — uses worldtimeapi.org public API. -local phrase = (jarvis.context.phrase or "") -local city = jarvis.text.strip_trigger(phrase:lower(), { - "сколько времени в", - "который час в", - "сколько часов в", - "время в", - "time in", - "what time in", - "котра година у", - "скільки часу в", -}) -city = city:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if city == "" then - return jarvis.cmd.error("В каком городе?") -end - --- Map common Russian city names → timezone slugs. -local TZ_MAP = { - -- Russia - ["москва"] = "Europe/Moscow", - ["спб"] = "Europe/Moscow", - ["петербург"] = "Europe/Moscow", - ["санкт-петербург"] = "Europe/Moscow", - ["новосибирск"] = "Asia/Novosibirsk", - ["екатеринбург"] = "Asia/Yekaterinburg", - ["казань"] = "Europe/Moscow", - ["владивосток"] = "Asia/Vladivostok", - ["хабаровск"] = "Asia/Vladivostok", - ["омск"] = "Asia/Omsk", - ["красноярск"] = "Asia/Krasnoyarsk", - -- World - ["лондон"] = "Europe/London", - ["нью-йорк"] = "America/New_York", - ["нью йорк"] = "America/New_York", - ["сан-франциско"] = "America/Los_Angeles", - ["сан франциско"] = "America/Los_Angeles", - ["лос-анджелес"] = "America/Los_Angeles", - ["лос анджелес"] = "America/Los_Angeles", - ["токио"] = "Asia/Tokyo", - ["пекин"] = "Asia/Shanghai", - ["шанхай"] = "Asia/Shanghai", - ["гонконг"] = "Asia/Hong_Kong", - ["сингапур"] = "Asia/Singapore", - ["сидней"] = "Australia/Sydney", - ["дубай"] = "Asia/Dubai", - ["париж"] = "Europe/Paris", - ["берлин"] = "Europe/Berlin", - ["мадрид"] = "Europe/Madrid", - ["рим"] = "Europe/Rome", - ["амстердам"] = "Europe/Amsterdam", - ["варшава"] = "Europe/Warsaw", - ["прага"] = "Europe/Prague", - ["стамбул"] = "Europe/Istanbul", - ["киев"] = "Europe/Kyiv", - ["минск"] = "Europe/Minsk", - ["ереван"] = "Asia/Yerevan", - ["тбилиси"] = "Asia/Tbilisi", - ["алматы"] = "Asia/Almaty", - ["астана"] = "Asia/Almaty", - ["баку"] = "Asia/Baku", - ["ташкент"] = "Asia/Tashkent", - ["сеул"] = "Asia/Seoul", - ["мехико"] = "America/Mexico_City", - ["рио"] = "America/Sao_Paulo", - ["буэнос-айрес"] = "America/Argentina/Buenos_Aires", -} - -local tz = nil -for stem, zone in pairs(TZ_MAP) do - if city:find(stem, 1, true) then - tz = zone - break - end -end - -if not tz then - return jarvis.cmd.not_found("Не знаю такого города. Попробуйте Москва, Токио, Лондон.") -end - --- worldtimeapi.org has rate limits but is free with no key. -local url = "https://worldtimeapi.org/api/timezone/" .. tz -local data = jarvis.http.json(url) -if not data or not data.datetime then - return jarvis.cmd.error("Не получилось узнать время.") -end - --- datetime is ISO 8601: "2026-05-15T14:32:11.123456+09:00" -local hh, mm = data.datetime:match("T(%d%d):(%d%d)") -if not hh then - return jarvis.cmd.error("Не понял ответ сервиса.") -end - -return jarvis.cmd.ok(string.format("В %s сейчас %s:%s.", city, hh, mm)) diff --git a/resources/icons/icon.ico b/resources/icons/icon.ico index ca7f09c..4ca0662 100644 Binary files a/resources/icons/icon.ico and b/resources/icons/icon.ico differ diff --git a/run.bat b/run.bat deleted file mode 100644 index 66650c8..0000000 --- a/run.bat +++ /dev/null @@ -1,19 +0,0 @@ -@echo off -setlocal enabledelayedexpansion -cd /d "%~dp0" - -if exist dev.env ( - for /f "usebackq tokens=1,* delims==" %%a in ("dev.env") do ( - set "_k=%%a" - if not "!_k:~0,1!"=="#" if not "!_k!"=="" set "%%a=%%b" - ) -) - -if exist "target\release\jarvis-gui.exe" ( - start "" "target\release\jarvis-gui.exe" - exit /b 0 -) - -echo target\release\jarvis-gui.exe not found. -echo Build it first: cargo build --release -p jarvis-gui -pause diff --git a/tools/piper/README.md b/tools/piper/README.md deleted file mode 100644 index 0079863..0000000 --- a/tools/piper/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Piper TTS (neural voice for J.A.R.V.I.S.) - -Drop-in replacement for the robotic SAPI voice. ~30 MB binary + ~60 MB Russian voice (Irina). Latency 200-500ms on i5. - -## Install - -```powershell -cd C:\Jarvis\rust\tools\piper -pwsh -File install.ps1 -``` - -By default installs `ru_RU-irina-medium` (Russian female). To pick a different voice: - -```powershell -pwsh -File install.ps1 -Voice ru_RU-denis-medium -pwsh -File install.ps1 -Voice ru_RU-dmitri-medium -pwsh -File install.ps1 -Voice ru_RU-ruslan-medium -pwsh -File install.ps1 -Voice en_US-amy-medium # English female -``` - -Force re-download: - -```powershell -pwsh -File install.ps1 -Force -``` - -## Use - -If `piper.exe` + a voice file are present in `tools/piper/`, J.A.R.V.I.S. auto-detects Piper on startup. No env vars required. - -To force a specific backend: - -```bat -set JARVIS_TTS=piper :: use Piper -set JARVIS_TTS=sapi :: force SAPI fallback -set JARVIS_TTS=silero :: use Silero (needs tools/silero/silero_tts.py) -``` - -Override paths: - -```bat -set JARVIS_TTS_PIPER_BIN=D:\custom\piper.exe -set JARVIS_TTS_PIPER_VOICE=D:\voices\my-voice.onnx -``` - -## Voices catalogue - -Full list: https://huggingface.co/rhasspy/piper-voices - -Recommended for Russian: -- `ru_RU-irina-medium` — natural female, default -- `ru_RU-dmitri-medium` — male, clear diction -- `ru_RU-ruslan-medium` — male, slightly deeper -- `ru_RU-denis-medium` — male, "newsreader" tone - -## Troubleshooting - -**"piper.exe not found"** — install script failed; check `install.ps1` output. The fallback is SAPI — assistant still works, just with the robotic voice. - -**"no .onnx voice in voices/"** — voice download failed (HuggingFace blocked?). Manually download from https://huggingface.co/rhasspy/piper-voices/tree/main/ru/ru_RU/irina/medium and drop both `.onnx` + `.onnx.json` into `voices/`. - -**Voice sounds wrong** — try a different model. Each Russian voice has a different timbre. - -**No audio output** — Piper writes to a temp wav, then plays via `System.Media.SoundPlayer` synchronously. Check that the default playback device isn't muted. diff --git a/tools/piper/install.ps1 b/tools/piper/install.ps1 deleted file mode 100644 index 3f8a3c4..0000000 --- a/tools/piper/install.ps1 +++ /dev/null @@ -1,108 +0,0 @@ -#requires -Version 5.1 -<# -.SYNOPSIS - Download Piper TTS engine + Russian voice into tools/piper/. - -.DESCRIPTION - Fetches the latest piper_windows_amd64.zip from rhasspy/piper-binary and - the Irina (ru_RU) voice .onnx + .json from huggingface rhasspy/piper-voices. - Idempotent — skips if files already exist. Pass -Force to re-download. - -.PARAMETER Voice - Voice name to install. Default "ru_RU-irina-medium". Other options: - ru_RU-denis-medium, ru_RU-dmitri-medium, ru_RU-ruslan-medium, en_US-amy-medium, ... - -.PARAMETER Force - Re-download even if files exist. - -.EXAMPLE - pwsh -File install.ps1 - pwsh -File install.ps1 -Voice ru_RU-denis-medium - pwsh -File install.ps1 -Force -#> - -param( - [string]$Voice = "ru_RU-irina-medium", - [switch]$Force -) - -$ErrorActionPreference = "Stop" -$InformationPreference = "Continue" - -$Root = Split-Path -Parent $MyInvocation.MyCommand.Path -$BinPath = Join-Path $Root "piper.exe" -$VoiceDir = Join-Path $Root "voices" -$VoiceFile = Join-Path $VoiceDir "$Voice.onnx" -$VoiceJson = Join-Path $VoiceDir "$Voice.onnx.json" - -# ─── Piper binary ────────────────────────────────────────────────────────── -if ((Test-Path $BinPath) -and -not $Force) { - Write-Information "[skip] piper.exe already present at $BinPath" -} else { - Write-Information "[fetch] piper_windows_amd64.zip..." - $zip = Join-Path $env:TEMP "piper_windows_amd64.zip" - $url = "https://github.com/rhasspy/piper/releases/latest/download/piper_windows_amd64.zip" - Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing - Write-Information "[unzip] -> $Root" - Expand-Archive -Path $zip -DestinationPath $Root -Force - # piper distributes inside a "piper/" subfolder — flatten it - $inner = Join-Path $Root "piper" - if (Test-Path $inner) { - Get-ChildItem -Path $inner -Force | Move-Item -Destination $Root -Force - Remove-Item -Path $inner -Recurse -Force - } - Remove-Item $zip -Force - if (-not (Test-Path $BinPath)) { - throw "piper.exe not found after extraction" - } - Write-Information "[ok] piper installed at $BinPath" -} - -# ─── Voice files ─────────────────────────────────────────────────────────── -if (-not (Test-Path $VoiceDir)) { - New-Item -ItemType Directory -Path $VoiceDir | Out-Null -} - -# Voice naming: ru_RU-irina-medium -> lang=ru_RU, name=irina, quality=medium -$parts = $Voice -split "-" -if ($parts.Length -lt 3) { - throw "Bad voice name '$Voice' — expected --" -} -$lang = $parts[0] -$name = $parts[1] -$quality = $parts[2] -$langCode = ($lang -split "_")[0] - -$Base = "https://huggingface.co/rhasspy/piper-voices/resolve/main/$langCode/$lang/$name/$quality" - -foreach ($pair in @( - @{ url = "$Base/$Voice.onnx"; out = $VoiceFile }, - @{ url = "$Base/$Voice.onnx.json"; out = $VoiceJson } -)) { - if ((Test-Path $pair.out) -and -not $Force) { - Write-Information "[skip] $(Split-Path -Leaf $pair.out) already present" - continue - } - Write-Information "[fetch] $($pair.url)" - Invoke-WebRequest -Uri $pair.url -OutFile $pair.out -UseBasicParsing - Write-Information "[ok] $(Split-Path -Leaf $pair.out) -> $($pair.out)" -} - -# ─── Smoke test ──────────────────────────────────────────────────────────── -$testWav = Join-Path $env:TEMP "jarvis-piper-test.wav" -try { - Write-Information "[test] synthesizing 'Привет, Дмитрий'..." - "Привет, Дмитрий" | & $BinPath --model $VoiceFile --output_file $testWav 2>$null - if ((Test-Path $testWav) -and ((Get-Item $testWav).Length -gt 1000)) { - Write-Information "[ok] Piper works! Test wav: $testWav" - Write-Information "" - Write-Information "Next: set environment variable JARVIS_TTS=piper" - Write-Information " (or just leave it unset — auto-detect will pick Piper)" - } else { - Write-Warning "Test wav empty or missing — synthesis may have failed" - } -} catch { - Write-Warning "Smoke test failed: $_" -} finally { - if (Test-Path $testWav) { Remove-Item $testWav -Force } -} diff --git a/tools/silero/silero_tts.py b/tools/silero/silero_tts.py deleted file mode 100644 index 42ae611..0000000 --- a/tools/silero/silero_tts.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Silero TTS helper for J.A.R.V.I.S. - -Reads text from stdin, synthesises with snakers4/silero-models, writes the result -to a temp wav, and prints the path on stdout. The Rust side then plays + deletes it. - -Install: - pip install torch soundfile numpy - -Usage (called by jarvis-core/src/tts/silero.rs): - echo "привет" | python silero_tts.py --voice xenia - -Voices for ru_v3: - xenia (female, default), baya (female), aidar (male), eugene (male), kseniya (female). -""" - -import argparse -import os -import sys -import tempfile - -try: - import torch # type: ignore -except ImportError: - print("silero_tts: torch not installed. Run `pip install torch`.", file=sys.stderr) - sys.exit(2) - -import warnings - -warnings.filterwarnings("ignore") - -_MODEL = None - - -def load_model(device: str = "cpu"): - global _MODEL - if _MODEL is None: - torch.set_num_threads(4) - _MODEL, _ = torch.hub.load( - repo_or_dir="snakers4/silero-models", - model="silero_tts", - language="ru", - speaker="v3_1_ru", - trust_repo=True, - ) - _MODEL.to(torch.device(device)) - return _MODEL - - -def synth(text: str, voice: str, sample_rate: int = 48000) -> str: - """Synth text → return path to temp wav.""" - model = load_model() - audio = model.apply_tts(text=text, speaker=voice, sample_rate=sample_rate) - - import numpy as np - import soundfile as sf # type: ignore - - fd, path = tempfile.mkstemp(prefix="jarvis-silero-", suffix=".wav") - os.close(fd) - sf.write(path, audio.numpy().astype(np.float32), sample_rate) - return path - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--voice", default="xenia", help="speaker name (xenia, baya, aidar, eugene, kseniya)") - ap.add_argument("--sample-rate", type=int, default=48000) - args = ap.parse_args() - - text = sys.stdin.read().strip() - if not text: - print("silero_tts: empty input", file=sys.stderr) - return 1 - - try: - path = synth(text, args.voice, args.sample_rate) - except Exception as exc: - print(f"silero_tts: synth failed: {exc}", file=sys.stderr) - return 3 - - print(path) - return 0 - - -if __name__ == "__main__": - sys.exit(main())