feat(commands): tier-1 — translate, math, theme toggle, project opener, sound panel

Five more portable Lua command packs. Bumps the total from 16 to 21
packs. cargo test -p jarvis-core --lib commands::tests still 3/3.

translate/ — translate.lua. Picks the target language from triggers
like "переведи на английский" / "translate to russian"; falls back to
English when only "переведи X" is heard. Sends to Groq with a strict
"translation only" system prompt at temp 0.3. Drops the result into
the clipboard, fires a notify, and speaks via PowerShell SAPI in the
matching ISO voice (ru-RU / uk-UA / en-US / de-DE etc.) so a German
translation actually sounds German.

math/ — math.lua. Strips the trigger ("посчитай" / "calculate" /
"сколько будет" / "what is" / ...), sends the remainder to Groq with
temp=0.0 and max_tokens=64, asking ONLY for the numeric result. If
the model replies "нет" (the system-prompt sentinel for non-math),
plays not_found instead of speaking nonsense. Otherwise speaks
"Получилось X" via SAPI.

theme/ — set.lua dispatched by command_id (theme_dark / theme_light).
Flips AppsUseLightTheme + SystemUsesLightTheme under
HKCU\...\Themes\Personalize via Set-ItemProperty. No reboot needed,
Windows shell re-themes immediately.

projects/ — open.lua + list.lua. Reads/creates
%USERPROFILE%\Documents\jarvis-projects.json (a flat
[{"name": ..., "path": ...}] list). open.lua strips the trigger,
exact-matches then substring-matches the project name, opens the path
in VS Code if `code.cmd` is on PATH, otherwise in Explorer. First
call creates a sample config with jarvis-rust / jarvis-python /
dietpi entries pointing under USERPROFILE so it works on any machine.
list.lua just notifies the configured project names.

sound_panel/ — open.lua. Two-liner: rundll32 mmsys.cpl,,1 (Recording
tab). Useful both as a standalone "открой настройки звука" and as
the place to fix the mic-disabled state that prevents jarvis-app
from starting.

Portability: USERPROFILE / SystemDrive / PATH lookup throughout.
This commit is contained in:
Bossiara13 2026-05-15 12:06:30 +03:00
parent 01ea46c091
commit 100db82bd6
11 changed files with 515 additions and 0 deletions

View file

@ -0,0 +1,27 @@
[[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",
]
ua = [
"порахуй",
"скільки буде",
"обчисли",
]

View file

@ -0,0 +1,87 @@
local lang = jarvis.context.language
local phrase = (jarvis.context.phrase or ""):lower()
local triggers = {
"сколько будет", "сколько это", "посчитай", "вычисли", "реши",
"how much is", "calculate", "compute", "solve", "what is",
"скільки буде", "порахуй", "обчисли",
}
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("Math", lang == "ru" and "Что посчитать?" or "What to compute?")
jarvis.audio.play_error()
return { chain = false }
end
local token = jarvis.system.env("GROQ_TOKEN")
if not token or token == "" then
jarvis.system.notify("Math", "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 sys = "Ты калькулятор. Решай арифметику, простые уравнения, конверсии единиц, проценты. "
.. "Отвечай ТОЛЬКО результатом числом или короткой строкой (для уравнений — значения корней). "
.. "Если запрос — не математика, ответь одним словом «нет»."
local payload = {
model = model,
messages = {
{ role = "system", content = sys },
{ role = "user", content = query },
},
max_tokens = 64,
temperature = 0.0,
}
jarvis.log("info", "math query: " .. query)
local res = jarvis.http.post_json(base .. "/chat/completions", payload, { Authorization = "Bearer " .. token })
if not res.ok then
jarvis.log("error", "math api failed: " .. tostring(res.status))
jarvis.system.notify("Math", "Ошибка API")
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.system.notify("Math", "Не смог распарсить ответ")
jarvis.audio.play_error()
return { chain = false }
end
content = content:gsub('\\n', '\n'):gsub('\\"', '"'):gsub('\\\\', '\\'):gsub('\\t', '\t')
content = content:gsub("^%s+", ""):gsub("%s+$", "")
if content:lower() == "нет" or content == "" then
jarvis.system.notify("Math", lang == "ru" and "Это не математика" or "Not math")
jarvis.audio.play_not_found()
return { chain = false }
end
jarvis.system.notify(query, content)
jarvis.log("info", "math result: " .. content)
local sapi_text = ((lang == "ru" and "Получилось " or "Result: ") .. content):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_text
)
jarvis.system.exec(string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', ps:gsub('"', '\\"')))
jarvis.audio.play_ok()
return { chain = false }

View file

@ -0,0 +1,36 @@
[[commands]]
id = "open_project"
type = "lua"
script = "open.lua"
sandbox = "full"
timeout = 5000
[commands.phrases]
ru = [
"открой проект",
"запусти проект",
"проект",
"открой папку проекта",
]
en = [
"open project",
"launch project",
"project",
]
ua = [
"відкрий проект",
"запусти проект",
]
[[commands]]
id = "list_projects"
type = "lua"
script = "list.lua"
sandbox = "full"
timeout = 5000
[commands.phrases]
ru = ["какие у меня проекты", "список проектов", "покажи проекты"]
en = ["list projects", "show projects"]
ua = ["які в мене проекти", "перелік проектів"]

View file

@ -0,0 +1,34 @@
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 }

View file

@ -0,0 +1,97 @@
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 }

View file

@ -0,0 +1,23 @@
[[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",
]
ua = [
"відкрий налаштування звуку",
"панель звуку",
]

View file

@ -0,0 +1,3 @@
jarvis.system.exec('rundll32.exe shell32.dll,Control_RunDLL mmsys.cpl,,1')
jarvis.audio.play_ok()
return { chain = false }

View file

@ -0,0 +1,24 @@
[[commands]]
id = "theme_dark"
type = "lua"
script = "set.lua"
sandbox = "full"
timeout = 3000
[commands.phrases]
ru = ["тёмная тема", "темная тема", "включи тёмную тему", "тёмный режим", "темный режим"]
en = ["dark theme", "dark mode", "enable dark mode"]
ua = ["темна тема", "темний режим"]
[[commands]]
id = "theme_light"
type = "lua"
script = "set.lua"
sandbox = "full"
timeout = 3000
[commands.phrases]
ru = ["светлая тема", "включи светлую тему", "светлый режим"]
en = ["light theme", "light mode", "enable light mode"]
ua = ["світла тема", "світлий режим"]

View file

@ -0,0 +1,26 @@
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 }

View file

@ -0,0 +1,34 @@
[[commands]]
id = "translate"
type = "lua"
script = "translate.lua"
sandbox = "full"
timeout = 20000
[commands.phrases]
ru = [
"переведи на английский",
"переведи на русский",
"переведи на украинский",
"переведи на немецкий",
"переведи на французский",
"переведи на испанский",
"переведи на китайский",
"переведи на японский",
"переведи",
"как по английски",
"как по русски",
"как по немецки",
]
en = [
"translate to english",
"translate to russian",
"translate to ukrainian",
"translate",
"how to say in",
]
ua = [
"переклади на англійську",
"переклади на російську",
"переклади",
]

View file

@ -0,0 +1,124 @@
local lang = jarvis.context.language
local phrase = (jarvis.context.phrase or ""):lower()
local target_languages = {
["английский"] = "English", ["english"] = "English",
["русский"] = "Russian", ["russian"] = "Russian",
["украинский"] = "Ukrainian", ["ukrainian"] = "Ukrainian", ["українську"] = "Ukrainian",
["немецкий"] = "German", ["german"] = "German", ["німецьку"] = "German",
["французский"]= "French", ["french"] = "French",
["испанский"] = "Spanish", ["spanish"] = "Spanish",
["итальянский"]= "Italian",
["китайский"] = "Chinese", ["chinese"] = "Chinese",
["японский"] = "Japanese", ["japanese"] = "Japanese",
["польский"] = "Polish",
["турецкий"] = "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)
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')]],
(target_label == "Russian" and "ru") or (target_label == "Ukrainian" and "uk") or "en",
sapi_text
)
jarvis.system.exec(string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', ps:gsub('"', '\\"')))
jarvis.audio.play_ok()
return { chain = false }