feat: Wave 1 — 10 new packs (75 → 85 rust packs)
User picked from the fishki roadmap; this commit ships the 10 easy/medium
ones in one batch. No Rust core changes — all pure Lua + jarvis.* APIs.
Pack catalog
magic_8ball
- "ответь да или нет" / "магический шар" / "что скажешь"
- 15 канонических ответов, random pick. Pure fun.
github_issues (2 cmds)
- "какие issues" / "открытые issues" — gh issue list по сохранённому репо
- "мои issues" / "что мне назначено" — gh issue list --assignee @me
weather_extended (2 cmds, Open-Meteo, no key)
- "погода завтра" — temp min/max + conditions + rain note
- "прогноз на неделю" — общий диапазон температур
- Auto-detects location via ipinfo.io if not in memory; stores lat/lon/city.
rss_reader (3 cmds)
- "добавь rss <url>" — extracts URL, persists via memory под ключом "rss.<domain>"
- "что в ленте" — fetches first feed, parses <title> tags, speaks top 3
- "какие у меня rss" — lists subscribed feed domains
ics_event
- "добавь встречу завтра в 15:00 обсудить проект"
- Parses time (HH:MM или "в HH") + date keywords (сегодня/завтра/послезавтра)
- Writes valid iCalendar v2.0 file to ~/Documents/jarvis-events/
- jarvis.system.open → дефолтный handler (Outlook/Mail/whatever)
backup
- "сделай бекап" — PowerShell Compress-Archive всех state files из APPDATA/Jarvis
- Outputs to ~/Documents/jarvis-backup-<YYYYMMDD-HHMMSS>.zip
- Bundles: long_term_memory, schedule, macros, profiles/, active_profile, llm_backend, settings
clip_history (3 cmds, 20-slot rolling buffer)
- "запиши буфер" — push current clipboard onto memory keys clip.0..clip.19
- "что я копировал" — speak preview of last 3
- "верни первый/второй/третий буфер" — restore clip.N to clipboard
notif_queue (2 cmds)
- "что я пропустил" — enumerate memory keys "notif.*", speak last 5 by recency
- "очисти уведомления" — purge all notif.* keys
- Producers (scheduler/macros/llm) can later push to memory[notif.<ts>] = text
password_vault (3 cmds, Windows DPAPI)
- "сохрани пароль от GitHub" — encrypts current clipboard content via DPAPI
(CurrentUser scope), base64-stored in memory[vault.GitHub]. Clears clipboard.
Password is NEVER spoken or written to disk in plaintext.
- "пароль от GitHub" — decrypts via DPAPI, restores to clipboard for 30 sec,
schedules auto-clear via jarvis.scheduler. Speaks only "Пароль от X в буфере."
- "какие у меня пароли" — list of stored service names.
habit_streaks (2 cmds, integrates with habit_nudge)
- "сколько дней подряд" / "статистика привычек" — reads memory keys
"habit_streak.<habit>.<YYYY-MM-DD>", computes consecutive-day streak per habit.
- "я попил воды" / "отметь привычку" — marks today's check-in.
Maps voice to habit: воды→water, размял/зарядк→stretch, глаз→eyes.
Tests: 6 commands tests pass (auto-validate the 10 new packs).
This commit is contained in:
parent
4d3d664abd
commit
c4b22618f8
30 changed files with 1086 additions and 0 deletions
19
resources/commands/backup/command.toml
Normal file
19
resources/commands/backup/command.toml
Normal file
|
|
@ -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 = ["зроби бекап", "експортуй налаштування"]
|
||||
46
resources/commands/backup/export.lua
Normal file
46
resources/commands/backup/export.lua
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
-- Zip all user-state JSON files into ~/Documents/jarvis-backup-<ts>.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 <APP_CONFIG_DIR>. 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))
|
||||
55
resources/commands/clip_history/command.toml
Normal file
55
resources/commands/clip_history/command.toml
Normal file
|
|
@ -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 = ["поверни буфер"]
|
||||
21
resources/commands/clip_history/list.lua
Normal file
21
resources/commands/clip_history/list.lua
Normal file
|
|
@ -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)
|
||||
21
resources/commands/clip_history/restore.lua
Normal file
21
resources/commands/clip_history/restore.lua
Normal file
|
|
@ -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 .. ".")
|
||||
24
resources/commands/clip_history/save.lua
Normal file
24
resources/commands/clip_history/save.lua
Normal file
|
|
@ -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 .. ".")
|
||||
37
resources/commands/github_issues/command.toml
Normal file
37
resources/commands/github_issues/command.toml
Normal file
|
|
@ -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 = ["мої тікети"]
|
||||
34
resources/commands/github_issues/list.lua
Normal file
34
resources/commands/github_issues/list.lua
Normal file
|
|
@ -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)
|
||||
25
resources/commands/github_issues/mine.lua
Normal file
25
resources/commands/github_issues/mine.lua
Normal file
|
|
@ -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)
|
||||
24
resources/commands/habit_streaks/checkin.lua
Normal file
24
resources/commands/habit_streaks/checkin.lua
Normal file
|
|
@ -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 .. " за сегодня.")
|
||||
41
resources/commands/habit_streaks/command.toml
Normal file
41
resources/commands/habit_streaks/command.toml
Normal file
|
|
@ -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.<habit>.<YYYY-MM-DD>" = "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 = ["я виконав звичку"]
|
||||
66
resources/commands/habit_streaks/streak.lua
Normal file
66
resources/commands/habit_streaks/streak.lua
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
-- Reads memory keys "habit_streak.<habit>.<YYYY-MM-DD>" 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, ", ") .. ".")
|
||||
19
resources/commands/ics_event/command.toml
Normal file
19
resources/commands/ics_event/command.toml
Normal file
|
|
@ -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 = ["додай зустріч"]
|
||||
77
resources/commands/ics_event/create.lua
Normal file
77
resources/commands/ics_event/create.lua
Normal file
|
|
@ -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))
|
||||
24
resources/commands/magic_8ball/ask.lua
Normal file
24
resources/commands/magic_8ball/ask.lua
Normal file
|
|
@ -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])
|
||||
20
resources/commands/magic_8ball/command.toml
Normal file
20
resources/commands/magic_8ball/command.toml
Normal file
|
|
@ -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 = ["скажи так чи ні"]
|
||||
13
resources/commands/notif_queue/clear.lua
Normal file
13
resources/commands/notif_queue/clear.lua
Normal file
|
|
@ -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))
|
||||
37
resources/commands/notif_queue/command.toml
Normal file
37
resources/commands/notif_queue/command.toml
Normal file
|
|
@ -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 = ["очисти сповіщення"]
|
||||
24
resources/commands/notif_queue/missed.lua
Normal file
24
resources/commands/notif_queue/missed.lua
Normal file
|
|
@ -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.<ts>" 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)
|
||||
52
resources/commands/password_vault/command.toml
Normal file
52
resources/commands/password_vault/command.toml
Normal file
|
|
@ -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 = ["список паролів"]
|
||||
67
resources/commands/password_vault/get.lua
Normal file
67
resources/commands/password_vault/get.lua
Normal file
|
|
@ -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 секунд.")
|
||||
14
resources/commands/password_vault/list.lua
Normal file
14
resources/commands/password_vault/list.lua
Normal file
|
|
@ -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, ", ")))
|
||||
48
resources/commands/password_vault/save.lua
Normal file
48
resources/commands/password_vault/save.lua
Normal file
|
|
@ -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 .. ".")
|
||||
34
resources/commands/rss_reader/add.lua
Normal file
34
resources/commands/rss_reader/add.lua
Normal file
|
|
@ -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 .. " добавлена.")
|
||||
54
resources/commands/rss_reader/command.toml
Normal file
54
resources/commands/rss_reader/command.toml
Normal file
|
|
@ -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 = ["мої стрічки"]
|
||||
13
resources/commands/rss_reader/list.lua
Normal file
13
resources/commands/rss_reader/list.lua
Normal file
|
|
@ -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, ", ") .. ".")
|
||||
41
resources/commands/rss_reader/read.lua
Normal file
41
resources/commands/rss_reader/read.lua
Normal file
|
|
@ -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("<title[^>]*>([^<]+)</title>") 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)
|
||||
37
resources/commands/weather_extended/command.toml
Normal file
37
resources/commands/weather_extended/command.toml
Normal file
|
|
@ -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 = ["прогноз на тиждень"]
|
||||
57
resources/commands/weather_extended/tomorrow.lua
Normal file
57
resources/commands/weather_extended/tomorrow.lua
Normal file
|
|
@ -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)
|
||||
42
resources/commands/weather_extended/week.lua
Normal file
42
resources/commands/weather_extended/week.lua
Normal file
|
|
@ -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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue