Initial commit: AVWatch v1.0.0 with .gitignore and CLAUDE.md
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Register (or remove) the two Windows scheduled tasks that make AVWatch
|
||||
run by itself.
|
||||
|
||||
.DESCRIPTION
|
||||
Creates:
|
||||
"AVWatch Scan" mid-week checks. Cheap: it only asks Claude about
|
||||
versions it has never seen before, and reuses cached
|
||||
web lookups. Writes reports\latest.html. No email.
|
||||
"AVWatch Digest" the Monday briefing. Full pass, every outstanding
|
||||
update gets a verdict, then the email goes out.
|
||||
|
||||
Tasks run as the current user so they inherit your environment variables
|
||||
(API key, SMTP password). They are set to run whether or not you are
|
||||
logged in, and to start late if the machine was off at the scheduled time.
|
||||
|
||||
Run this from a normal PowerShell window -- no administrator rights are
|
||||
needed for a per-user task.
|
||||
|
||||
.EXAMPLE
|
||||
.\install_tasks.ps1
|
||||
.\install_tasks.ps1 -ScanDays Tuesday,Thursday,Saturday -ScanTime 06:00 -DigestTime 07:15
|
||||
.\install_tasks.ps1 -Remove
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string[]]$ScanDays = @('Tuesday', 'Thursday', 'Saturday'),
|
||||
[string]$ScanTime = '06:00',
|
||||
[string]$DigestDay = 'Monday',
|
||||
[string]$DigestTime = '07:00',
|
||||
[string]$ProjectRoot,
|
||||
[switch]$Remove
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if (-not $ProjectRoot) {
|
||||
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||
}
|
||||
$runner = Join-Path $ProjectRoot 'scripts\run_avwatch.ps1'
|
||||
|
||||
$scanTaskName = 'AVWatch Scan'
|
||||
$digestTaskName = 'AVWatch Digest'
|
||||
|
||||
if ($Remove) {
|
||||
foreach ($name in $scanTaskName, $digestTaskName) {
|
||||
if (Get-ScheduledTask -TaskName $name -ErrorAction SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName $name -Confirm:$false
|
||||
Write-Output "removed scheduled task: $name"
|
||||
} else {
|
||||
Write-Output "not present: $name"
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (-not (Test-Path $runner)) {
|
||||
throw "runner script not found: $runner"
|
||||
}
|
||||
|
||||
# --- sanity check before we schedule anything ---------------------------
|
||||
Write-Output "Checking the setup first..."
|
||||
Push-Location $ProjectRoot
|
||||
try {
|
||||
$venvPython = Join-Path $ProjectRoot '.venv\Scripts\python.exe'
|
||||
if (Test-Path $venvPython) { $python = $venvPython }
|
||||
else { $python = (Get-Command python).Source }
|
||||
& $python -m avwatch doctor
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning ("'avwatch doctor' reported problems (exit $LASTEXITCODE). " +
|
||||
"Scheduling anyway -- fix them and the tasks will start working.")
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" `
|
||||
-LogonType S4U -RunLevel Limited
|
||||
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-StartWhenAvailable `
|
||||
-DontStopOnIdleEnd `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-MultipleInstances IgnoreNew `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Hours 3)
|
||||
|
||||
function New-AvwatchAction([string]$Command) {
|
||||
# -NoProfile keeps a slow or broken PowerShell profile out of the picture.
|
||||
$argument = ('-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "{0}" ' +
|
||||
'-Command {1} -ProjectRoot "{2}"') -f $runner, $Command, $ProjectRoot
|
||||
New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $argument `
|
||||
-WorkingDirectory $ProjectRoot
|
||||
}
|
||||
|
||||
# --- mid-week scans ------------------------------------------------------
|
||||
$scanTrigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek $ScanDays -At $ScanTime
|
||||
Register-ScheduledTask -TaskName $scanTaskName `
|
||||
-Action (New-AvwatchAction 'scan') `
|
||||
-Trigger $scanTrigger `
|
||||
-Principal $principal `
|
||||
-Settings $settings `
|
||||
-Description ("AVWatch: check the AV fleet for new firmware and software " +
|
||||
"releases and refresh reports\latest.html. No email.") `
|
||||
-Force | Out-Null
|
||||
Write-Output "registered: $scanTaskName ($($ScanDays -join ', ') at $ScanTime)"
|
||||
|
||||
# --- Monday digest -------------------------------------------------------
|
||||
$digestTrigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek $DigestDay -At $DigestTime
|
||||
Register-ScheduledTask -TaskName $digestTaskName `
|
||||
-Action (New-AvwatchAction 'digest') `
|
||||
-Trigger $digestTrigger `
|
||||
-Principal $principal `
|
||||
-Settings $settings `
|
||||
-Description ("AVWatch: full update-and-risk pass, then email the weekly " +
|
||||
"briefing to the recipients in config.toml.") `
|
||||
-Force | Out-Null
|
||||
Write-Output "registered: $digestTaskName ($DigestDay at $DigestTime)"
|
||||
|
||||
Write-Output ""
|
||||
Write-Output "Done. Useful commands:"
|
||||
Write-Output " Start-ScheduledTask -TaskName '$digestTaskName' # run the digest now"
|
||||
Write-Output " Get-ScheduledTaskInfo -TaskName '$digestTaskName' # last run time and result"
|
||||
Write-Output " Get-ChildItem '$ProjectRoot\logs' | Sort LastWriteTime -Desc | Select -First 5"
|
||||
Write-Output ""
|
||||
Write-Output "Secrets must be set as USER environment variables so the tasks"
|
||||
Write-Output "inherit them. For example:"
|
||||
Write-Output ' setx ANTHROPIC_API_KEY "sk-ant-..."'
|
||||
Write-Output ' setx AVWATCH_SMTP_PASSWORD "your-app-password"'
|
||||
Write-Output "Open a new shell after setx before testing."
|
||||
@@ -0,0 +1,79 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Wrapper that Windows Task Scheduler calls. Runs one avwatch command and
|
||||
writes a timestamped log.
|
||||
|
||||
.DESCRIPTION
|
||||
Kept deliberately thin: it resolves the project folder, prefers a local
|
||||
virtual environment if one exists, runs the command, and keeps the last
|
||||
30 logs. Secrets come from the user's environment variables, which the
|
||||
scheduled task inherits because it runs as that user.
|
||||
|
||||
.EXAMPLE
|
||||
.\run_avwatch.ps1 -Command scan
|
||||
.\run_avwatch.ps1 -Command digest
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('scan', 'digest')]
|
||||
[string]$Command,
|
||||
|
||||
[string]$ProjectRoot,
|
||||
|
||||
[string[]]$ExtraArgs = @()
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if (-not $ProjectRoot) {
|
||||
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
||||
}
|
||||
Set-Location $ProjectRoot
|
||||
|
||||
$logDir = Join-Path $ProjectRoot 'logs'
|
||||
if (-not (Test-Path $logDir)) {
|
||||
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
|
||||
}
|
||||
$stamp = Get-Date -Format 'yyyy-MM-dd_HHmm'
|
||||
$logFile = Join-Path $logDir "$Command`_$stamp.log"
|
||||
|
||||
# Prefer a project virtualenv if present, else whatever python is on PATH.
|
||||
$venvPython = Join-Path $ProjectRoot '.venv\Scripts\python.exe'
|
||||
if (Test-Path $venvPython) {
|
||||
$python = $venvPython
|
||||
} else {
|
||||
$cmd = Get-Command python -ErrorAction SilentlyContinue
|
||||
if (-not $cmd) {
|
||||
"FATAL: no python found on PATH and no .venv present" | Out-File $logFile -Encoding utf8
|
||||
exit 1
|
||||
}
|
||||
$python = $cmd.Source
|
||||
}
|
||||
|
||||
"=== avwatch $Command @ $(Get-Date -Format 'u') ===" | Out-File $logFile -Encoding utf8
|
||||
"python: $python" | Out-File $logFile -Append -Encoding utf8
|
||||
"root: $ProjectRoot" | Out-File $logFile -Append -Encoding utf8
|
||||
|
||||
# Report which credentials are visible, without ever logging their values.
|
||||
foreach ($name in 'ANTHROPIC_API_KEY', 'AVWATCH_SMTP_PASSWORD',
|
||||
'AVWATCH_GITHUB_TOKEN', 'AVWATCH_REDDIT_CLIENT_ID') {
|
||||
$present = if ([Environment]::GetEnvironmentVariable($name)) { 'set' } else { 'NOT SET' }
|
||||
"$name`: $present" | Out-File $logFile -Append -Encoding utf8
|
||||
}
|
||||
"" | Out-File $logFile -Append -Encoding utf8
|
||||
|
||||
$argList = @('-m', 'avwatch', $Command) + $ExtraArgs
|
||||
& $python $argList *>&1 | Out-File $logFile -Append -Encoding utf8
|
||||
$code = $LASTEXITCODE
|
||||
|
||||
"" | Out-File $logFile -Append -Encoding utf8
|
||||
"=== exit code $code @ $(Get-Date -Format 'u') ===" | Out-File $logFile -Append -Encoding utf8
|
||||
|
||||
# Keep the last 30 logs so this never fills a booth machine's disk.
|
||||
Get-ChildItem $logDir -Filter '*.log' |
|
||||
Sort-Object LastWriteTime -Descending |
|
||||
Select-Object -Skip 30 |
|
||||
Remove-Item -Force -ErrorAction SilentlyContinue
|
||||
|
||||
exit $code
|
||||
@@ -0,0 +1,108 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Ask for the Anthropic API key and store it as a user environment
|
||||
variable.
|
||||
|
||||
.DESCRIPTION
|
||||
Exists so nobody has to paste a command containing a placeholder. Telling
|
||||
someone to run setx ANTHROPIC_API_KEY "<your-key>" invites the
|
||||
placeholder being pasted verbatim, and the angle brackets and quotes can
|
||||
leave PowerShell sitting at a continuation prompt with nothing working.
|
||||
|
||||
Run it with no arguments:
|
||||
powershell -ExecutionPolicy Bypass -File scripts\set_api_key.ps1
|
||||
|
||||
The key is read here and handed straight to setx. It is not written to
|
||||
any file in this project, not logged, and not echoed to the screen.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$Check
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Test-KeyShape([string]$k) {
|
||||
if ([string]::IsNullOrWhiteSpace($k)) { return 'empty' }
|
||||
if ($k -match '\.\.\.') { return 'placeholder' }
|
||||
if ($k -match '[<>]') { return 'placeholder' }
|
||||
if ($k.Trim().Length -lt 25) { return 'tooshort' }
|
||||
if (-not $k.StartsWith('sk-ant-')) { return 'prefix' }
|
||||
return 'ok'
|
||||
}
|
||||
|
||||
# --- --Check mode: report what is currently set, without revealing it ----
|
||||
if ($Check) {
|
||||
$existing = [Environment]::GetEnvironmentVariable('ANTHROPIC_API_KEY', 'User')
|
||||
if (-not $existing) {
|
||||
$existing = [Environment]::GetEnvironmentVariable('ANTHROPIC_API_KEY', 'Process')
|
||||
}
|
||||
$shape = Test-KeyShape $existing
|
||||
switch ($shape) {
|
||||
'empty' { Write-Output "No ANTHROPIC_API_KEY is set." }
|
||||
'placeholder' { Write-Output "ANTHROPIC_API_KEY is set to a PLACEHOLDER, not a real key. Run this script without -Check to fix it." }
|
||||
'tooshort' { Write-Output "ANTHROPIC_API_KEY is set but is too short to be a real key." }
|
||||
'prefix' { Write-Output "ANTHROPIC_API_KEY is set but does not start with 'sk-ant-'." }
|
||||
'ok' { Write-Output ("ANTHROPIC_API_KEY looks valid (starts {0}, {1} characters)." -f $existing.Substring(0,11), $existing.Trim().Length) }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Write-Output ""
|
||||
Write-Output "Anthropic API key setup"
|
||||
Write-Output "======================="
|
||||
Write-Output ""
|
||||
Write-Output "Get a key from: https://console.anthropic.com/settings/keys"
|
||||
Write-Output " -> Create Key -> copy it"
|
||||
Write-Output ""
|
||||
Write-Output "A real key starts with 'sk-ant-' and is about 100 characters long."
|
||||
Write-Output "Paste it below and press Enter. Nothing is shown as you type."
|
||||
Write-Output ""
|
||||
|
||||
$secure = Read-Host -Prompt 'Paste your API key' -AsSecureString
|
||||
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
|
||||
try {
|
||||
$key = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr).Trim()
|
||||
} finally {
|
||||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
||||
}
|
||||
|
||||
switch (Test-KeyShape $key) {
|
||||
'empty' {
|
||||
Write-Output ""
|
||||
Write-Output "Nothing was entered. No change made."
|
||||
exit 1
|
||||
}
|
||||
'placeholder' {
|
||||
Write-Output ""
|
||||
Write-Output "That looks like example text rather than a key (it contains"
|
||||
Write-Output "'...' or angle brackets). No change made -- copy the real key"
|
||||
Write-Output "from the Anthropic console and run this again."
|
||||
exit 1
|
||||
}
|
||||
'tooshort' {
|
||||
Write-Output ""
|
||||
Write-Output ("That is only {0} characters. A real key is around 100." -f $key.Length)
|
||||
Write-Output "No change made."
|
||||
exit 1
|
||||
}
|
||||
'prefix' {
|
||||
Write-Output ""
|
||||
Write-Output "That does not start with 'sk-ant-', so it is probably not an"
|
||||
Write-Output "Anthropic key. No change made."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
setx ANTHROPIC_API_KEY $key | Out-Null
|
||||
# Also set it for THIS session, so the next command works without reopening.
|
||||
$env:ANTHROPIC_API_KEY = $key
|
||||
|
||||
Write-Output ""
|
||||
Write-Output ("Saved. Key starts {0} and is {1} characters." -f $key.Substring(0,11), $key.Length)
|
||||
Write-Output ""
|
||||
Write-Output "It is active in this window already, and in any new window from now on."
|
||||
Write-Output ""
|
||||
Write-Output "Check it: python -m avwatch doctor"
|
||||
Write-Output "Then run: python -m avwatch digest --dry-run --no-email"
|
||||
Write-Output ""
|
||||
Reference in New Issue
Block a user