Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,11 @@ jobs:
- name: Test
run: dotnet test AppLensDesktop.sln --configuration Release --no-restore

- name: Test Windows initial snapshot
shell: powershell
run: .\tests\Script.Tests\Test-AppLensSnapshot.ps1

- name: Check Store configuration without packaging
shell: powershell
run: .\tests\Script.Tests\Test-StoreConfiguration.ps1

18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ artifacts/
src/AppLens.Desktop/BundleArtifacts/
.vs/
TestResults/
.env
.env.*
!.env.example
!.env.template
.claude/
.claude/settings.local.json
.vercel/
.wrangler/
*.log
*.db
*.db-journal
*.sqlite
*.sqlite3
*.user
*.suo
desktop.ini

# Local report exports
/AppLens-*.md
/AppLens-*.json
/AppLens-*.html
71 changes: 71 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# AppLens Agent Instructions

Installed (formerly AppLens) v1 is a lightweight Windows client for guided screen-share sessions:
scan -> review/filter apps and storage -> approve supported uninstalls -> verify
results -> save a local report. Keep communication concise and plain-English.

The Store product is Installed (9PBPFLL6JVVV). Keep AppLens repository, namespaces,
legacy storage paths and DPAPI purpose identifiers stable; renaming those can
break history compatibility. AppLens-Tune remains a separate deferred component.

## Product scope

- Target Windows 11 x64 and Microsoft Store distribution. Users should install
from the AppLens website/Store and open the installed app from an icon, including
a desktop shortcut, without developer tools or manual runtime/certificate setup.
- Follow [Product Vision](docs/AppLens-Product-Vision.md). Keep the silver/metallic
interface focused on the v1 flow; scan, table, history and reports share one model.
- Preserve deferred Tune in `future/AppLens-Tune/`. Core v1 must have no build,
runtime or background-probe dependency on it; keep needed shared backend services.
- Scans are read-only. System changes require explicit confirmation, supported
mechanisms and recorded outcomes. Preserve Windows approval/policy controls;
do not add automatic remediation, forced deletion or automatic restarts.
- Keep the approved local-only privacy design: no accounts, telemetry or uploads;
Windows current-user protection for new history without plaintext duplicates;
readable reports redacted by default. Preserve legacy evidence unchanged.

## Working rules

- One agent; no subagents. Keep changes within the agreed v1 scope.
- Default to under 120 words, lead with the result, and use plain English.
Summarize verification once. Create reports, plans, handoffs or documentation
files only when explicitly requested.
- Active working copy: `C:\Users\codyl\Desktop\csiOS\Projects\AppLens-clean`,
branch `codex/applens-clean`. The original checkout and worktrees are preserved.
- The [handoff](docs/AppLens-v1-Handoff.md) contains historical release evidence;
its worktree path and issue status are historical. Verify current state before
release work. Remote:
`https://github.com/CSI-Platform/AppLens.git` (main product, not SSH or LLM).
- Honor recorded approvals; do not request them again. Keep native Windows
approvals with the owner. Local builds do not authorize hosting changes, merge,
Store submission or publication; consult the handoff for each action's scope.
- Reuse recorded validation; rerun checks for changed behavior/builds, changed
environments or unresolved failures. Preserve raw evidence and saved reports.
- Keep current status and evidence in Linear and the handoff. This file holds
durable rules. Distinguish local tests from installed, clean-PC and Store results.

## Shared engineering standards

Read the relevant shared standards before acting; keep project-specific scope and
approval rules above intact. Flag inaccessible references rather than guessing.

- [Testing](C:/Users/codyl/Desktop/csiOS/STANDARDS/rules/testing.md): behavioral coverage and evidence.
- [Safety](C:/Users/codyl/Desktop/csiOS/STANDARDS/rules/safety.md): data, access, secrets and recovery.
- [Quality](C:/Users/codyl/Desktop/csiOS/STANDARDS/rules/quality.md): acceptance, builds, usability and review.
- [Release](C:/Users/codyl/Desktop/csiOS/STANDARDS/rules/release.md): CI and authorized delivery.

## Development and release references

```powershell
dotnet restore AppLensDesktop.sln
dotnet build AppLensDesktop.sln -c Release
dotnet test AppLensDesktop.sln -c Release --no-build
```

Use [desktop build guidance](docs/AppLensDesktop-Build.md) for launch commands and
focused checks. `tools/Build-StoreCandidate.ps1` builds a local unsigned candidate;
use the handoff's recorded packaging authorization. A candidate is not a release.

Before release work, read [validation](docs/AppLens-Release-Validation.md),
[Store readiness](docs/Store-Readiness-Checklist.md) and the
[submission plan](docs/AppLens-Store-Submission-Plan.md).
135 changes: 127 additions & 8 deletions AppLens.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
.SYNOPSIS
AppLens — Pre-Audit App Scanner for CSI AI Workflow Audits
.DESCRIPTION
Scans installed desktop apps (Win32) and Microsoft Store apps without
requiring admin rights. Outputs a clean, categorized text file suitable
for pasting into an AI prompt alongside a workflow audit transcript.
Captures hardware, RAM usage, local disk utilization,
installed desktop apps (Win32), and Microsoft Store apps without admin rights.
Read-only: writes a new local text report without changing system settings.
#>

[CmdletBinding()]
param([string]$OutputPath)

# ── Configuration ──────────────────────────────────────────────────────────────

function Get-DesktopFilePath {
Expand All @@ -28,7 +31,12 @@ function Get-DesktopFilePath {
return Join-Path $desktopPath $FileName
}

$OutputPath = Get-DesktopFilePath -FileName "AppLens_Results_$env:COMPUTERNAME.txt"
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$OutputPath = Get-DesktopFilePath -FileName "AppLens_Results_$($env:COMPUTERNAME)_$(Get-Date -Format 'yyyyMMdd-HHmmss-fff').txt"
}
if (Test-Path -LiteralPath $OutputPath) {
throw 'The report path already exists. Choose a new OutputPath to preserve previous evidence.'
}

# Patterns for apps that should be filtered into the "Runtimes & Frameworks" section
$RuntimePatterns = @(
Expand Down Expand Up @@ -387,13 +395,116 @@ function Format-AppLine {
return $line
}

function Get-WorkstationSnapshot {
$snapshot = @{ Issues = @() }
$queries = [ordered]@{
Win32_ComputerSystem = @('Manufacturer', 'Model')
Win32_OperatingSystem = @('Caption', 'Version', 'BuildNumber', 'OSArchitecture', 'TotalVisibleMemorySize', 'FreePhysicalMemory', 'LastBootUpTime')
Win32_Processor = @('Name', 'NumberOfCores', 'NumberOfLogicalProcessors')
Win32_VideoController = @('Name', 'DriverVersion')
Win32_LogicalDisk = @('DeviceID', 'FileSystem', 'Size', 'FreeSpace')
}
foreach ($className in $queries.Keys) {
$parameters = @{
ClassName = $className
Property = $queries[$className]
OperationTimeoutSec = 5
ErrorAction = 'Stop'
}
if ($className -eq 'Win32_LogicalDisk') { $parameters.Filter = 'DriveType = 3' }
try {
$snapshot[$className] = @(Get-CimInstance @parameters)
if ($snapshot[$className].Count -eq 0) { $snapshot.Issues += "${className}: Unavailable (no data returned)." }
} catch {
$snapshot[$className] = @()
$snapshot.Issues += "${className}: Unavailable (query failed or timed out)."
}
}
return $snapshot
}

function Format-WorkstationSnapshot {
param([Parameter(Mandatory = $true)]$Snapshot)

$lines = @('', '--- Workstation Summary ---')
$attention = @()
$system = $Snapshot.Win32_ComputerSystem | Select-Object -First 1
$os = $Snapshot.Win32_OperatingSystem | Select-Object -First 1
if ($system) { $lines += "Machine: $($system.Manufacturer) $($system.Model)" }
else { $lines += 'Machine: Unavailable' }
if ($os) {
$lines += "OS: $($os.Caption) $($os.OSArchitecture) (version $($os.Version), build $($os.BuildNumber))"
if ($os.LastBootUpTime) {
$uptime = (Get-Date) - $os.LastBootUpTime
$lines += "Uptime: $($uptime.Days)d $($uptime.Hours)h $($uptime.Minutes)m (since last Windows boot)"
} else { $lines += 'Uptime: Unavailable' }
} else { $lines += 'OS: Unavailable'; $lines += 'Uptime: Unavailable' }
$processors = @($Snapshot.Win32_Processor)
if ($processors.Count -gt 0) {
foreach ($cpu in $processors) {
$lines += "CPU: $($cpu.Name) | $($cpu.NumberOfCores) cores / $($cpu.NumberOfLogicalProcessors) logical processors"
}
} else { $lines += 'CPU: Unavailable' }
$gpus = @($Snapshot.Win32_VideoController)
if ($gpus.Count -gt 0) {
foreach ($gpu in $gpus) { $lines += "GPU: $($gpu.Name) | Driver: $($gpu.DriverVersion)" }
} else { $lines += 'GPU: Unavailable' }

if ($os -and $os.TotalVisibleMemorySize -gt 0 -and $null -ne $os.FreePhysicalMemory -and
$os.FreePhysicalMemory -ge 0 -and $os.FreePhysicalMemory -le $os.TotalVisibleMemorySize) {
$usedMemory = $os.TotalVisibleMemorySize - $os.FreePhysicalMemory
$memoryPercent = 100.0 * $usedMemory / $os.TotalVisibleMemorySize
$lines += 'RAM: {0:N1} / {1:N1} GiB used ({2:N1}%); {3:N1} GiB available to Windows' -f
($usedMemory / 1MB), ($os.TotalVisibleMemorySize / 1MB), $memoryPercent, ($os.FreePhysicalMemory / 1MB)
if ($memoryPercent -ge 85) { $attention += '- High RAM usage: {0:N1}% used at capture time (threshold: 85%).' -f $memoryPercent }
} else { $lines += 'RAM: Unavailable' }

$lines += '', '--- Local Disk Utilization ---'
$lines += 'Drive | File system | Total GiB | Used GiB | Free GiB | Used % | Free %'
$disks = @($Snapshot.Win32_LogicalDisk | Sort-Object DeviceID)
if ($disks.Count -eq 0) { $lines += 'Unavailable (no local fixed-disk readings).' }
foreach ($disk in $disks) {
if ($disk.Size -le 0 -or $null -eq $disk.FreeSpace -or $disk.FreeSpace -lt 0 -or $disk.FreeSpace -gt $disk.Size) {
$lines += "$($disk.DeviceID) | Unavailable (capacity or free space not reported)."
continue
}
$used = $disk.Size - $disk.FreeSpace
$freePercent = 100.0 * $disk.FreeSpace / $disk.Size
$lines += '{0} | {1} | {2:N1} | {3:N1} | {4:N1} | {5:N1}% | {6:N1}%' -f
$disk.DeviceID, $disk.FileSystem, ($disk.Size / 1GB), ($used / 1GB), ($disk.FreeSpace / 1GB), (100 - $freePercent), $freePercent
if ($freePercent -lt 10) {
$attention += '- Low disk space on {0} - {1:N1} GiB free ({2:N1}%; threshold: below 10%).' -f
$disk.DeviceID, ($disk.FreeSpace / 1GB), $freePercent
}
}
$lines += 'Scope: mounted local fixed disks with drive letters. GiB = 1,073,741,824 bytes.'

$lines += '', '--- Attention Items ---'
if ($attention.Count) { $lines += $attention }
else { $lines += 'No low-disk/high-RAM thresholds triggered in the available readings.' }
$lines += '', '--- Snapshot Collection Notes ---'
if ($Snapshot.Issues.Count) { $lines += $Snapshot.Issues }
else { $lines += 'All workstation probes returned data.' }
$lines += 'Point-in-time resource snapshot; no folder-size traversal or Tune runtime probes.'
return $lines
}

function Save-ScanReport {
param([Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$Text)
$Text | Out-File -LiteralPath $Path -Encoding UTF8 -NoClobber -ErrorAction Stop
}

# ── Main ───────────────────────────────────────────────────────────────────────

Write-Host ""
Write-Host "AppLens - Pre-Audit App Scanner" -ForegroundColor Cyan
Write-Host "Scanning installed applications..." -ForegroundColor Gray
Write-Host "Capturing workstation resources and installed applications..." -ForegroundColor Gray
Write-Host ""

$scanStarted = Get-Date
$scanTimer = [Diagnostics.Stopwatch]::StartNew()
$workstation = Get-WorkstationSnapshot

# Collect Win32 apps
$win32Apps = Get-Win32Apps

Expand Down Expand Up @@ -421,14 +532,22 @@ foreach ($app in $win32Apps) {
}

# Collect Store apps
$storeApps = Get-StoreApps
$storeApps = @(Get-StoreApps)
$scanTimer.Stop()

# Build output
$output = @()
$output += "=== AppLens Scan Results ==="
$output += "Computer: $env:COMPUTERNAME"
$output += "User: $env:USERNAME"
$output += "Scan Date: $(Get-Date -Format 'yyyy-MM-dd')"
$output += "Scan Date: $($scanStarted.ToString('yyyy-MM-dd HH:mm:ss zzz'))"
$output += 'Mode: Audit (read-only)'
$output += 'Collection duration: {0:N1} seconds' -f $scanTimer.Elapsed.TotalSeconds
$output += Format-WorkstationSnapshot -Snapshot $workstation
$output += ''
$output += '--- App Inventory Summary ---'
$output += "Desktop entries: $($desktopApps.Count + $m365Apps.Count) | Store entries: $($storeApps.Count) | Runtimes/frameworks: $($runtimes.Count)"
$output += 'Scope: registered desktop apps and current-user Store apps after filtering; portable/unregistered apps may be absent.'
$output += ""
$output += "--- Desktop Applications ---"

Expand Down Expand Up @@ -473,7 +592,7 @@ if ($runtimes.Count -eq 0) {

# Write to file
$outputText = $output -join "`r`n"
$outputText | Out-File -FilePath $OutputPath -Encoding UTF8
Save-ScanReport -Path $OutputPath -Text $outputText

# Display results
Write-Host $outputText
Expand Down
Loading
Loading