diff --git a/resources/commands/backup/command.toml b/resources/commands/backup/command.toml new file mode 100644 index 0000000..c4d3145 --- /dev/null +++ b/resources/commands/backup/command.toml @@ -0,0 +1,19 @@ +# 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"] +ua = ["зроби бекап", "експортуй налаштування"] diff --git a/resources/commands/backup/export.lua b/resources/commands/backup/export.lua new file mode 100644 index 0000000..5dfa49d --- /dev/null +++ b/resources/commands/backup/export.lua @@ -0,0 +1,46 @@ +-- 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/clip_history/command.toml b/resources/commands/clip_history/command.toml new file mode 100644 index 0000000..549b2a0 --- /dev/null +++ b/resources/commands/clip_history/command.toml @@ -0,0 +1,55 @@ +# 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"] +ua = ["запам'ятай буфер"] + + +[[commands]] +id = "cliphist.list" +type = "lua" +script = "list.lua" +sandbox = "minimal" +timeout = 3000 + +[commands.phrases] +ru = [ + "что я копировал", + "история буфера", + "последние буферы", + "что было в буфере", +] +en = ["clipboard history", "what did I copy"] +ua = ["що я копіював"] + + +[[commands]] +id = "cliphist.restore" +type = "lua" +script = "restore.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = [ + "верни первый буфер", + "верни второй буфер", + "верни третий буфер", + "восстанови буфер", + "вставь старый буфер", +] +en = ["restore clipboard", "paste old clipboard"] +ua = ["поверни буфер"] diff --git a/resources/commands/clip_history/list.lua b/resources/commands/clip_history/list.lua new file mode 100644 index 0000000..08f1c37 --- /dev/null +++ b/resources/commands/clip_history/list.lua @@ -0,0 +1,21 @@ +-- 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 new file mode 100644 index 0000000..52e5bef --- /dev/null +++ b/resources/commands/clip_history/restore.lua @@ -0,0 +1,21 @@ +-- "Верни первый/второй/третий буфер" — 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 new file mode 100644 index 0000000..2f1039b --- /dev/null +++ b/resources/commands/clip_history/save.lua @@ -0,0 +1,24 @@ +-- 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/github_issues/command.toml b/resources/commands/github_issues/command.toml new file mode 100644 index 0000000..adb707f --- /dev/null +++ b/resources/commands/github_issues/command.toml @@ -0,0 +1,37 @@ +# 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"] +ua = ["які тікети", "відкриті 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"] +ua = ["мої тікети"] diff --git a/resources/commands/github_issues/list.lua b/resources/commands/github_issues/list.lua new file mode 100644 index 0000000..5f57f6a --- /dev/null +++ b/resources/commands/github_issues/list.lua @@ -0,0 +1,34 @@ +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 new file mode 100644 index 0000000..7c9478c --- /dev/null +++ b/resources/commands/github_issues/mine.lua @@ -0,0 +1,25 @@ +-- 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/habit_streaks/checkin.lua b/resources/commands/habit_streaks/checkin.lua new file mode 100644 index 0000000..799a87b --- /dev/null +++ b/resources/commands/habit_streaks/checkin.lua @@ -0,0 +1,24 @@ +-- 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 new file mode 100644 index 0000000..15b6c40 --- /dev/null +++ b/resources/commands/habit_streaks/command.toml @@ -0,0 +1,41 @@ +# 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"] +ua = ["моя статистика звичок"] + + +[[commands]] +id = "habits.checkin" +type = "lua" +script = "checkin.lua" +sandbox = "standard" +timeout = 3000 + +[commands.phrases] +ru = [ + "отметь привычку", + "выполнил привычку", + "я попил воды", + "я размялся", + "я отдохнул глазам", +] +en = ["check in habit", "I did the habit"] +ua = ["я виконав звичку"] diff --git a/resources/commands/habit_streaks/streak.lua b/resources/commands/habit_streaks/streak.lua new file mode 100644 index 0000000..5a2ac2a --- /dev/null +++ b/resources/commands/habit_streaks/streak.lua @@ -0,0 +1,66 @@ +-- 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/ics_event/command.toml b/resources/commands/ics_event/command.toml new file mode 100644 index 0000000..1b6d3e4 --- /dev/null +++ b/resources/commands/ics_event/command.toml @@ -0,0 +1,19 @@ +# 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"] +ua = ["додай зустріч"] diff --git a/resources/commands/ics_event/create.lua b/resources/commands/ics_event/create.lua new file mode 100644 index 0000000..22c6181 --- /dev/null +++ b/resources/commands/ics_event/create.lua @@ -0,0 +1,77 @@ +-- "Добавь встречу завтра в 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/magic_8ball/ask.lua b/resources/commands/magic_8ball/ask.lua new file mode 100644 index 0000000..04e670c --- /dev/null +++ b/resources/commands/magic_8ball/ask.lua @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000..12dcbf5 --- /dev/null +++ b/resources/commands/magic_8ball/command.toml @@ -0,0 +1,20 @@ +# 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"] +ua = ["скажи так чи ні"] diff --git a/resources/commands/notif_queue/clear.lua b/resources/commands/notif_queue/clear.lua new file mode 100644 index 0000000..2ac0a1b --- /dev/null +++ b/resources/commands/notif_queue/clear.lua @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000..4d3a693 --- /dev/null +++ b/resources/commands/notif_queue/command.toml @@ -0,0 +1,37 @@ +# 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"] +ua = ["що я пропустив"] + + +[[commands]] +id = "notif.clear" +type = "lua" +script = "clear.lua" +sandbox = "minimal" +timeout = 2000 + +[commands.phrases] +ru = [ + "очисти уведомления", + "забудь уведомления", + "сбрось пропущенное", +] +en = ["clear notifications"] +ua = ["очисти сповіщення"] diff --git a/resources/commands/notif_queue/missed.lua b/resources/commands/notif_queue/missed.lua new file mode 100644 index 0000000..fca579e --- /dev/null +++ b/resources/commands/notif_queue/missed.lua @@ -0,0 +1,24 @@ +-- 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/password_vault/command.toml b/resources/commands/password_vault/command.toml new file mode 100644 index 0000000..3b95697 --- /dev/null +++ b/resources/commands/password_vault/command.toml @@ -0,0 +1,52 @@ +# 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"] +ua = ["запам'ятай пароль від"] + + +[[commands]] +id = "vault.get" +type = "lua" +script = "get.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "пароль от", + "дай пароль", + "покажи пароль от", + "достань пароль", +] +en = ["password for", "get password"] +ua = ["пароль від"] + + +[[commands]] +id = "vault.list" +type = "lua" +script = "list.lua" +sandbox = "minimal" +timeout = 3000 + +[commands.phrases] +ru = [ + "какие у меня пароли", + "список паролей", + "что в хранилище паролей", +] +en = ["list passwords", "what's in the vault"] +ua = ["список паролів"] diff --git a/resources/commands/password_vault/get.lua b/resources/commands/password_vault/get.lua new file mode 100644 index 0000000..3455d5f --- /dev/null +++ b/resources/commands/password_vault/get.lua @@ -0,0 +1,67 @@ +-- "Пароль от 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 new file mode 100644 index 0000000..d869b05 --- /dev/null +++ b/resources/commands/password_vault/list.lua @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..378236c --- /dev/null +++ b/resources/commands/password_vault/save.lua @@ -0,0 +1,48 @@ +-- "Сохрани пароль от 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/rss_reader/add.lua b/resources/commands/rss_reader/add.lua new file mode 100644 index 0000000..8517e84 --- /dev/null +++ b/resources/commands/rss_reader/add.lua @@ -0,0 +1,34 @@ +-- "Добавь 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 new file mode 100644 index 0000000..48122d5 --- /dev/null +++ b/resources/commands/rss_reader/command.toml @@ -0,0 +1,54 @@ +# 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"] +ua = ["додай rss"] + + +[[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"] +ua = ["що в стрічці"] + + +[[commands]] +id = "rss.list" +type = "lua" +script = "list.lua" +sandbox = "minimal" +timeout = 3000 + +[commands.phrases] +ru = [ + "какие у меня rss", + "список лент", + "мои подписки", +] +en = ["list feeds", "my feeds"] +ua = ["мої стрічки"] diff --git a/resources/commands/rss_reader/list.lua b/resources/commands/rss_reader/list.lua new file mode 100644 index 0000000..9c1604f --- /dev/null +++ b/resources/commands/rss_reader/list.lua @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000..3974f52 --- /dev/null +++ b/resources/commands/rss_reader/read.lua @@ -0,0 +1,41 @@ +-- 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/weather_extended/command.toml b/resources/commands/weather_extended/command.toml new file mode 100644 index 0000000..5aff963 --- /dev/null +++ b/resources/commands/weather_extended/command.toml @@ -0,0 +1,37 @@ +# 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"] +ua = ["погода завтра"] + + +[[commands]] +id = "weather.week" +type = "lua" +script = "week.lua" +sandbox = "full" +timeout = 20000 + +[commands.phrases] +ru = [ + "прогноз на неделю", + "погода на неделю", + "что с погодой на неделю", + "недельный прогноз", +] +en = ["weather this week", "weekly forecast"] +ua = ["прогноз на тиждень"] diff --git a/resources/commands/weather_extended/tomorrow.lua b/resources/commands/weather_extended/tomorrow.lua new file mode 100644 index 0000000..e724d84 --- /dev/null +++ b/resources/commands/weather_extended/tomorrow.lua @@ -0,0 +1,57 @@ +-- 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 new file mode 100644 index 0000000..b503071 --- /dev/null +++ b/resources/commands/weather_extended/week.lua @@ -0,0 +1,42 @@ +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)