feat: Wave 5 — sysinfo speaks, smart reminders, outlook COM, time tracker, WOL, conversation tools
Some checks are pending
Rust CI / cargo test (jarvis-core) (push) Waiting to run
Rust CI / cargo clippy (push) Waiting to run
Rust CI / cargo check (workspace) (push) Waiting to run

Voice sysinfo overhaul (6 scripts: battery/cpu/ram/disk/time/all):
- Now SPEAK the result via jarvis.cmd.ok instead of notify-only. Toast
  still fires for visual reference, but real Jarvis tells you out loud.
- `all` joins lines with commas so TTS doesn't over-pause on linebreaks.

Smart reminders (`reminders/set.lua` rewrite):
- Was: detached `Start-Sleep` PowerShell subprocesses that died on
  jarvis-app restart and spawned a hidden process per timer.
- Now: routes through `jarvis.scheduler.add(... type=speak ...)`. Persists
  to schedule.json, survives restart, no zombie processes. Trade-off:
  sub-minute timers round up to 1 min (rare in voice UX).
- Drops UA triggers from earlier UA-removal sweep.

Translation polish (`translate/translate.lua` + `clipboard.lua`):
- Drops UA target language; adds Italian, Polish, Turkish, French ru
  triggers + English target detectors.
- `language_iso()` helper picks the SAPI voice locale for the TARGET
  language (German translation → German SAPI voice if installed).

LLM system prompt (config.rs):
- New LLM_SYSTEM_PROMPT_EN counterpart. Both languages now mention the
  assistant's tooling (memory / scheduler / profiles / macros / vision /
  clipboard) so the LLM knows it can DO things, not suggest the user
  install something.
- `get_llm_system_prompt(lang)` picks EN/RU.

Conversation pack (NEW `conversation/`):
- 2 commands. `conversation.summary` ("о чём мы говорили") pulls the
  last 12 turns from persisted history, asks LLM for a 1-3 sentence
  recap in Jarvis tone. Falls back gracefully when offline.
- `conversation.repeat` ("повтори") re-speaks the last assistant message.
- New Lua API `jarvis.llm_history()` returns array of {role, content}
  tables, excluding the system prompt.

WOL pack (NEW `wol/`):
- Voice "разбуди сервер" / "wake server" fires a magic packet at a MAC
  stored in long-term memory under `wol_<alias>` (default alias "server").
- Setup via voice: "запомни wol_server AA:BB:CC:DD:EE:FF". Falls back to
  `JARVIS_WOL_TARGET` env if memory empty. Handles every standard MAC
  format (colons / dashes / dots / bare). Broadcast on UDP 9 via
  PowerShell System.Net.Sockets.UdpClient.

Outlook COM pack (NEW `outlook/`, via subagent):
- 4 commands: unread_count, latest, send_clipboard, summarize_inbox.
- PowerShell COM bridge `_outlook.ps1` with tab-delimited line protocol.
- summarize_inbox calls LLM for a one-paragraph summary. Graceful
  failure when Outlook isn't running.

Time tracker pack (NEW `time_tracker/`, via subagent):
- 5 commands: start, stop, today, week, reset.
- Persists via `jarvis.state` (structured object: current_session_start
  + sessions array of {start, end}).
- Local-calendar "today" boundary, full Russian pluralisation
  (час/часа/часов, минута/минуты/минут, сессия/сессии/сессий).

Tests: 139 rust (no regression), 81 python (60 → 81, +14 time_tracker
+7 outlook). Release build green for jarvis-app and jarvis-cli.

Pack count: 88 → 92 rust packs.
This commit is contained in:
Bossiara13 2026-05-16 14:34:29 +03:00
parent 919d565879
commit df799b6cd2
29 changed files with 1383 additions and 106 deletions

View file

@ -1,3 +1,10 @@
-- 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()
@ -5,9 +12,8 @@ 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)
@ -19,19 +25,20 @@ 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,
["секунд"] = 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,
["один"] = 1, ["одну"] = 1, ["одна"] = 1,
["полторы"] = 1, ["полтора"] = 1,
["два"] = 2, ["две"] = 2, ["три"] = 3, ["четыре"] = 4, ["пять"] = 5,
["шесть"] = 6, ["семь"] = 7, ["восемь"] = 8, ["девять"] = 9, ["десять"] = 10,
["пятнадцать"] = 15, ["двадцать"] = 20, ["тридцать"] = 30, ["сорок"] = 40,
["пятьдесят"] = 50, ["час"] = 60, -- "через час" without number
["пятьдесят"] = 50,
}
local number, unit, body = nil, nil, nil
@ -85,35 +92,38 @@ local function human_time(sec)
return string.format("%d ч %d мин", math.floor(sec/3600), math.floor((sec%3600)/60))
end
local title_esc = (lang == "ru" and "Напоминание J.A.R.V.I.S." or "J.A.R.V.I.S. reminder"):gsub("'", "''")
local body_esc = body:gsub("'", "''")
local sapi_text = ((lang == "ru" and "Сэр, напоминаю: " or "Reminder: ") .. body):gsub("'", "''"):gsub("\r?\n", " ")
-- 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 ps = string.format(
[[Start-Sleep -Seconds %d; ]]
.. [[$null = New-Object -ComObject Shell.Application; ]]
.. [[try { ]]
.. [[ Import-Module BurntToast -ErrorAction SilentlyContinue; ]]
.. [[ New-BurntToastNotification -Text '%s', '%s' -ErrorAction SilentlyContinue ]]
.. [[} catch {}; ]]
.. [[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'); ]]
.. [[$wsh = New-Object -ComObject WScript.Shell; $wsh.Popup('%s', 30, '%s', 64) | Out-Null]],
total_seconds, title_esc, body_esc, sapi_text, body_esc, title_esc
)
local cmd = string.format(
'powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -Command "%s"',
ps:gsub('"', '\\"')
)
local speak_text = (lang == "ru" and "Сэр, напоминаю: " or "Reminder: ") .. body
jarvis.system.exec("start /b " .. cmd)
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)
string.format(lang == "ru" and "Через %s: %s" or "In %s: %s",
human_time(total_seconds), body)
)
jarvis.log("info", string.format("reminder in %ds: %s", total_seconds, body))
jarvis.audio.play_ok()
return { chain = false }
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)
))