@dinasor/mnemo-cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +263 -0
  4. package/VERSION +1 -0
  5. package/bin/mnemo.js +139 -0
  6. package/memory.ps1 +178 -0
  7. package/memory_mac.sh +2447 -0
  8. package/package.json +36 -0
  9. package/scripts/memory/installer/bootstrap.ps1 +21 -0
  10. package/scripts/memory/installer/core/bridge.ps1 +285 -0
  11. package/scripts/memory/installer/core/io.ps1 +110 -0
  12. package/scripts/memory/installer/core/paths.ps1 +83 -0
  13. package/scripts/memory/installer/features/gitignore_setup.ps1 +80 -0
  14. package/scripts/memory/installer/features/hooks_setup.ps1 +157 -0
  15. package/scripts/memory/installer/features/mcp_setup.ps1 +87 -0
  16. package/scripts/memory/installer/features/memory_scaffold.ps1 +541 -0
  17. package/scripts/memory/installer/features/vector_setup.ps1 +103 -0
  18. package/scripts/memory/installer/templates/add-journal-entry.ps1 +122 -0
  19. package/scripts/memory/installer/templates/add-lesson.ps1 +151 -0
  20. package/scripts/memory/installer/templates/autonomy/__init__.py +6 -0
  21. package/scripts/memory/installer/templates/autonomy/context_safety.py +181 -0
  22. package/scripts/memory/installer/templates/autonomy/entity_resolver.py +215 -0
  23. package/scripts/memory/installer/templates/autonomy/ingest_pipeline.py +252 -0
  24. package/scripts/memory/installer/templates/autonomy/lifecycle_engine.py +254 -0
  25. package/scripts/memory/installer/templates/autonomy/policies.yaml +59 -0
  26. package/scripts/memory/installer/templates/autonomy/reranker.py +220 -0
  27. package/scripts/memory/installer/templates/autonomy/retrieval_router.py +148 -0
  28. package/scripts/memory/installer/templates/autonomy/runner.py +272 -0
  29. package/scripts/memory/installer/templates/autonomy/schema.py +150 -0
  30. package/scripts/memory/installer/templates/autonomy/vault_policy.py +205 -0
  31. package/scripts/memory/installer/templates/build-memory-sqlite.py +111 -0
  32. package/scripts/memory/installer/templates/clear-active.ps1 +55 -0
  33. package/scripts/memory/installer/templates/customization.md +84 -0
  34. package/scripts/memory/installer/templates/lint-memory.ps1 +217 -0
  35. package/scripts/memory/installer/templates/mnemo_vector.py +556 -0
  36. package/scripts/memory/installer/templates/query-memory-sqlite.py +95 -0
  37. package/scripts/memory/installer/templates/query-memory.ps1 +122 -0
  38. package/scripts/memory/installer/templates/rebuild-memory-index.ps1 +293 -0
@@ -0,0 +1,122 @@
1
+ <#
2
+ query-memory.ps1
3
+ Search memory quickly.
4
+
5
+ Default: grep (Select-String)
6
+ Optional: SQLite FTS (requires python + memory.sqlite), via -UseSqlite
7
+
8
+ Examples:
9
+ powershell -File scripts/memory/query-memory.ps1 -Query "IL2CPP"
10
+ powershell -File scripts/memory/query-memory.ps1 -Query "IL2CPP" -UseSqlite
11
+ powershell -File scripts/memory/query-memory.ps1 -Query "bind" -Format AI
12
+ #>
13
+
14
+ [CmdletBinding()]
15
+ param(
16
+ [Parameter(Mandatory=$true)][string]$Query,
17
+ [ValidateSet("All","HotRules","Active","Memo","Lessons","Journal","Digests")][string]$Area = "All",
18
+ [ValidateSet("Human","AI")][string]$Format = "Human",
19
+ [switch]$UseSqlite
20
+ )
21
+
22
+ Set-StrictMode -Version Latest
23
+ $ErrorActionPreference = "Stop"
24
+
25
+ if ($PSScriptRoot) {
26
+ $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
27
+ } else {
28
+ $RepoRoot = (Get-Location).Path
29
+ }
30
+
31
+ function Resolve-MnemoMemoryDir([string]$Root) {
32
+ $candidates = @(
33
+ (Join-Path $Root ".mnemo\memory"),
34
+ (Join-Path $Root ".cursor\memory")
35
+ )
36
+ foreach ($candidate in $candidates) {
37
+ if (Test-Path -LiteralPath $candidate) { return $candidate }
38
+ }
39
+ return $candidates[0]
40
+ }
41
+
42
+ $MemoryDir = Resolve-MnemoMemoryDir -Root $RepoRoot
43
+ $LessonsDir = Join-Path $MemoryDir "lessons"
44
+ $SqlitePath = Join-Path $MemoryDir "memory.sqlite"
45
+
46
+ if ($UseSqlite) {
47
+ $pyPath = $null
48
+ foreach ($candidate in @("python","py","python3")) {
49
+ $cmd = Get-Command $candidate -ErrorAction SilentlyContinue
50
+ if ($null -eq $cmd) { continue }
51
+ try {
52
+ & $cmd.Source -c "import sys; print(sys.version)" 1>$null 2>$null
53
+ if ($LASTEXITCODE -eq 0) { $pyPath = $cmd.Source; break }
54
+ } catch {}
55
+ }
56
+ $py = Join-Path $RepoRoot "scripts\memory\query-memory-sqlite.py"
57
+ if ($null -ne $pyPath -and (Test-Path $SqlitePath) -and (Test-Path $py)) {
58
+ Write-Host "Using SQLite FTS search..." -ForegroundColor Cyan
59
+ & $pyPath $py --repo $RepoRoot --q $Query --area $Area --format $Format
60
+ exit $LASTEXITCODE
61
+ }
62
+ Write-Host "SQLite mode unavailable. Falling back to grep." -ForegroundColor DarkYellow
63
+ }
64
+
65
+ Write-Host "Using file-based search..." -ForegroundColor Cyan
66
+
67
+ $targets = @()
68
+ switch ($Area) {
69
+ "HotRules" { $targets += (Join-Path $MemoryDir "hot-rules.md") }
70
+ "Active" { $targets += (Join-Path $MemoryDir "active-context.md") }
71
+ "Memo" { $targets += (Join-Path $MemoryDir "memo.md") }
72
+ "Lessons" {
73
+ $targets += (Join-Path $LessonsDir "index.md")
74
+ $lessonFiles = Get-ChildItem -Path $LessonsDir -Filter "L-*.md" -ErrorAction SilentlyContinue
75
+ foreach ($lf in $lessonFiles) { $targets += $lf.FullName }
76
+ }
77
+ "Journal" { $targets += (Join-Path $MemoryDir "journal-index.md") }
78
+ "Digests" { $targets += (Join-Path $MemoryDir "digests\*.digest.md") }
79
+ "All" {
80
+ $targets += (Join-Path $MemoryDir "hot-rules.md")
81
+ $targets += (Join-Path $MemoryDir "active-context.md")
82
+ $targets += (Join-Path $MemoryDir "memo.md")
83
+ $targets += (Join-Path $LessonsDir "index.md")
84
+ $targets += (Join-Path $MemoryDir "journal-index.md")
85
+ $targets += (Join-Path $MemoryDir "digests\*.digest.md")
86
+ }
87
+ }
88
+
89
+ $allMatches = @()
90
+ foreach ($t in $targets) {
91
+ $results = Select-String -Path $t -Pattern $Query -SimpleMatch -ErrorAction SilentlyContinue
92
+ if ($results) { $allMatches += $results }
93
+ }
94
+
95
+ $matchCount = @($allMatches).Count
96
+
97
+ if ($Format -eq "AI") {
98
+ if ($matchCount -eq 0) {
99
+ Write-Host "No matches found for: $Query"
100
+ } else {
101
+ $uniqueFiles = $allMatches | ForEach-Object { $_.Path } | Sort-Object -Unique
102
+ Write-Host "Files to read:"
103
+ foreach ($f in $uniqueFiles) {
104
+ $relative = $f.Replace($RepoRoot, "").TrimStart('\','/')
105
+ Write-Host " @$relative"
106
+ }
107
+ }
108
+ } else {
109
+ Write-Host "Searching: $Query" -ForegroundColor Cyan
110
+ Write-Host "Area: $Area" -ForegroundColor Cyan
111
+ Write-Host ""
112
+ if ($matchCount -eq 0) {
113
+ Write-Host "No matches found." -ForegroundColor Yellow
114
+ } else {
115
+ $grouped = $allMatches | Group-Object Path
116
+ foreach ($g in $grouped) {
117
+ Write-Host "==> $($g.Name)" -ForegroundColor Green
118
+ foreach ($m in $g.Group) { Write-Host " $($m.LineNumber): $($m.Line.Trim())" }
119
+ Write-Host ""
120
+ }
121
+ }
122
+ }
@@ -0,0 +1,293 @@
1
+ <#
2
+ rebuild-memory-index.ps1
3
+ Generates:
4
+ - lessons/index.md + lessons-index.json
5
+ - journal-index.md + journal-index.json
6
+ - digests/YYYY-MM.digest.md
7
+ Optionally:
8
+ - memory.sqlite (if Python exists)
9
+
10
+ Works on PowerShell 5.1+ (no external deps).
11
+ BOM-tolerant parsing.
12
+ #>
13
+
14
+ [CmdletBinding()]
15
+ param([string]$RepoRoot = "")
16
+
17
+ Set-StrictMode -Version Latest
18
+ $ErrorActionPreference = "Stop"
19
+
20
+ if ([string]::IsNullOrWhiteSpace($RepoRoot)) {
21
+ if ($PSScriptRoot) {
22
+ $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
23
+ } else {
24
+ $RepoRoot = (Get-Location).Path
25
+ }
26
+ }
27
+
28
+ function Write-Utf8NoBom([string]$FilePath, [string]$Content) {
29
+ $dir = Split-Path -Parent $FilePath
30
+ if ($dir -and !(Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
31
+ $enc = New-Object System.Text.UTF8Encoding($false)
32
+ $normalized = ($Content -replace "`r?`n", "`r`n")
33
+ [System.IO.File]::WriteAllText($FilePath, $normalized, $enc)
34
+ }
35
+
36
+ function Read-Utf8([string]$Path) {
37
+ $raw = Get-Content -Raw -Encoding UTF8 $Path
38
+ if ($raw.Length -gt 0 -and [int]$raw[0] -eq 0xFEFF) { $raw = $raw.Substring(1) }
39
+ return $raw
40
+ }
41
+
42
+ function Parse-Frontmatter([string]$FilePath) {
43
+ $raw = Read-Utf8 $FilePath
44
+ $result = @{}
45
+ $lines = $raw -split "`r?`n"
46
+
47
+ if ($lines.Count -lt 3 -or $lines[0].Trim() -ne "---") { return $null }
48
+
49
+ $i = 1
50
+ $currentListKey = $null
51
+ while ($i -lt $lines.Count) {
52
+ $line = $lines[$i]
53
+ if ($line.Trim() -eq "---") { break }
54
+ if ([string]::IsNullOrWhiteSpace($line) -or $line.TrimStart().StartsWith("#")) { $i++; continue }
55
+
56
+ if ($line -match '^\s*-\s+(.+)$' -and $currentListKey) {
57
+ if (-not ($result.ContainsKey($currentListKey))) { $result[$currentListKey] = @() }
58
+ $result[$currentListKey] += $Matches[1].Trim()
59
+ $i++; continue
60
+ }
61
+
62
+ if ($line -match '^\s*([A-Za-z0-9_]+)\s*:\s*(.*)\s*$') {
63
+ $key = $Matches[1].Trim().ToLower()
64
+ $val = $Matches[2]
65
+ if ([string]::IsNullOrWhiteSpace($val)) {
66
+ $result[$key] = @()
67
+ $currentListKey = $key
68
+ $i++; continue
69
+ }
70
+ $currentListKey = $null
71
+ $v = $val.Trim()
72
+ if (($v.StartsWith('"') -and $v.EndsWith('"')) -or ($v.StartsWith("'") -and $v.EndsWith("'"))) {
73
+ $v = $v.Substring(1, $v.Length-2)
74
+ }
75
+ if ($v -match '^\[(.*)\]$') {
76
+ $inner = $Matches[1]
77
+ if ([string]::IsNullOrWhiteSpace($inner)) {
78
+ $result[$key] = @()
79
+ } else {
80
+ $items = $inner -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }
81
+ $result[$key] = @($items)
82
+ }
83
+ } else {
84
+ $result[$key] = $v
85
+ }
86
+ $i++; continue
87
+ }
88
+ $i++
89
+ }
90
+ return $result
91
+ }
92
+
93
+ function Resolve-MnemoMemoryDir([string]$Root) {
94
+ $candidates = @(
95
+ (Join-Path $Root ".mnemo\memory"),
96
+ (Join-Path $Root ".cursor\memory")
97
+ )
98
+ foreach ($candidate in $candidates) {
99
+ if (Test-Path -LiteralPath $candidate) { return $candidate }
100
+ }
101
+ return $candidates[0]
102
+ }
103
+
104
+ $MemoryDir = Resolve-MnemoMemoryDir -Root $RepoRoot
105
+ $LessonsDir = Join-Path $MemoryDir "lessons"
106
+ $JournalDir = Join-Path $MemoryDir "journal"
107
+ $DigestsDir = Join-Path $MemoryDir "digests"
108
+
109
+ if (!(Test-Path $LessonsDir)) { New-Item -ItemType Directory -Force -Path $LessonsDir | Out-Null }
110
+ if (!(Test-Path $JournalDir)) { throw "Missing: $JournalDir" }
111
+ if (!(Test-Path $DigestsDir)) { New-Item -ItemType Directory -Force -Path $DigestsDir | Out-Null }
112
+
113
+ $lessonFiles = Get-ChildItem -Path $LessonsDir -File -Filter "L-*.md" -ErrorAction SilentlyContinue | Sort-Object Name
114
+ $lessons = @()
115
+
116
+ foreach ($lf in $lessonFiles) {
117
+ $yaml = Parse-Frontmatter $lf.FullName
118
+ if ($null -eq $yaml) { continue }
119
+ if (-not $yaml.ContainsKey("id")) { continue }
120
+
121
+ $id = [string]$yaml["id"]
122
+ $num = 0
123
+ if ($id -match 'L-(\d+)') { $num = [int]$Matches[1] }
124
+
125
+ $title = if ($yaml.ContainsKey("title")) { [string]$yaml["title"] } else { $lf.BaseName }
126
+ $status = if ($yaml.ContainsKey("status")) { [string]$yaml["status"] } else { "Active" }
127
+ $introduced = if ($yaml.ContainsKey("introduced")) { [string]$yaml["introduced"] } else { "" }
128
+ $tags = @()
129
+ if ($yaml.ContainsKey("tags")) { $tags = @($yaml["tags"]) }
130
+ $applies = @()
131
+ if ($yaml.ContainsKey("applies_to")) { $applies = @($yaml["applies_to"]) }
132
+ $rule = ""
133
+ if ($yaml.ContainsKey("rule")) { $rule = [string]$yaml["rule"] } else { $rule = $title }
134
+
135
+ $lessons += [pscustomobject]@{
136
+ Id = $id; Num = $num; Title = $title; Status = $status; Introduced = $introduced
137
+ Tags = $tags; AppliesTo = $applies; Rule = $rule; File = $lf.Name
138
+ }
139
+ }
140
+
141
+ $lessons = $lessons | Sort-Object Num
142
+ $gen = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssK")
143
+ $idx = @()
144
+ $idx += "# Lessons Index (generated)"; $idx += ""
145
+ $idx += "Generated: $gen"; $idx += ""
146
+ $idx += "Format: ID | [Tags] | AppliesTo | Rule | File"; $idx += ""
147
+
148
+ if (-not $lessons -or @($lessons).Count -eq 0) {
149
+ $idx += "(No lessons yet.)"
150
+ } else {
151
+ foreach ($l in $lessons) {
152
+ $tagText = ($l.Tags | ForEach-Object { "[$_]" }) -join ""
153
+ $appliesText = "(any)"
154
+ if ($l.AppliesTo -and @($l.AppliesTo).Count -gt 0) { $appliesText = ($l.AppliesTo -join ", ") }
155
+ $idx += "$($l.Id) | $tagText | $appliesText | $($l.Rule) | ``$($l.File)``"
156
+ }
157
+ }
158
+
159
+ Write-Utf8NoBom (Join-Path $LessonsDir "index.md") ($idx -join "`n")
160
+ $lessonsJson = if (-not $lessons -or @($lessons).Count -eq 0) { "[]" } else { ConvertTo-Json -InputObject @($lessons) -Depth 6 }
161
+ Write-Utf8NoBom (Join-Path $MemoryDir "lessons-index.json") $lessonsJson
162
+
163
+ $journalFiles = Get-ChildItem -Path $JournalDir -File | Where-Object {
164
+ $_.Name -match '^\d{4}-\d{2}\.md$' -and $_.Name -ne 'README.md'
165
+ } | Sort-Object Name
166
+
167
+ $journalEntries = New-Object System.Collections.Generic.List[object]
168
+
169
+ foreach ($jf in $journalFiles) {
170
+ $text = Read-Utf8 $jf.FullName
171
+ $datePattern = '(?m)^##\s+(\d{4}-\d{2}-\d{2}).*$'
172
+ $dateMatches = [regex]::Matches($text, $datePattern)
173
+
174
+ for ($i=0; $i -lt $dateMatches.Count; $i++) {
175
+ $date = $dateMatches[$i].Groups[1].Value
176
+ $start = $dateMatches[$i].Index
177
+ $end = if ($i -lt $dateMatches.Count - 1) { $dateMatches[$i+1].Index } else { $text.Length }
178
+ $block = $text.Substring($start, $end - $start)
179
+
180
+ $entryPattern = '(?m)^-\s+(\[[^\]]+\]){1,}.*$'
181
+ $entryMatches = [regex]::Matches($block, $entryPattern)
182
+
183
+ for ($j=0; $j -lt $entryMatches.Count; $j++) {
184
+ $eStart = $entryMatches[$j].Index
185
+ $eEnd = if ($j -lt $entryMatches.Count - 1) { $entryMatches[$j+1].Index } else { $block.Length }
186
+ $eBlock = $block.Substring($eStart, $eEnd - $eStart).Trim()
187
+ $firstLine = ($eBlock -split "`r?`n")[0].Trim()
188
+
189
+ $tags = @()
190
+ foreach ($tm in [regex]::Matches($firstLine, '\[([^\]]+)\]')) { $tags += $tm.Groups[1].Value.Trim() }
191
+ $title = ($firstLine -replace '^-\s+(\[[^\]]+\])+\s*', '').Trim()
192
+
193
+ $files = @()
194
+ foreach ($fm in [regex]::Matches($eBlock, '`([^`]+)`')) {
195
+ $v = $fm.Groups[1].Value.Trim()
196
+ if ($v -match '[/\\]' -or $v -match '\.(cs|md|mdx|yml|yaml|csproj|ps1|ts|tsx|json|py)$') {
197
+ $files += $v
198
+ }
199
+ }
200
+ $files = $files | Select-Object -Unique
201
+
202
+ $journalEntries.Add([pscustomobject]@{
203
+ MonthFile = $jf.Name; Date = $date; Tags = $tags; Title = $title; Files = $files
204
+ })
205
+ }
206
+ }
207
+
208
+ $monthName = [System.IO.Path]::GetFileNameWithoutExtension($jf.Name)
209
+ $digestPath = Join-Path $DigestsDir "$monthName.digest.md"
210
+
211
+ $digest = @()
212
+ $digest += "# Monthly Digest - $monthName (generated)"; $digest += ""
213
+ $digest += "Generated: $gen"; $digest += ""
214
+ $digest += "Token-cheap summary. See ``.mnemo/memory/journal/$($jf.Name)`` for details."; $digest += ""
215
+
216
+ $dates = [regex]::Matches($text, '(?m)^##\s+(\d{4}-\d{2}-\d{2}).*$') | ForEach-Object { $_.Groups[1].Value }
217
+ $uniqueDates = $dates | Select-Object -Unique
218
+ foreach ($d in $uniqueDates) {
219
+ $digest += "## $d"; $digest += ""
220
+ $entriesForDay = $journalEntries | Where-Object { $_.MonthFile -eq $jf.Name -and $_.Date -eq $d }
221
+ foreach ($e in $entriesForDay) {
222
+ $tagText = ($e.Tags | ForEach-Object { "[$_]" }) -join ""
223
+ $digest += "- $tagText $($e.Title)"
224
+ }
225
+ $digest += ""
226
+ }
227
+ Write-Utf8NoBom $digestPath ($digest -join "`n")
228
+ }
229
+
230
+ $ji = @()
231
+ $ji += "# Journal Index (generated)"; $ji += ""
232
+ $ji += "Generated: $gen"; $ji += ""
233
+ $ji += "Format: YYYY-MM-DD | [Tags] | Title | Files"; $ji += ""
234
+
235
+ foreach ($e in ($journalEntries | Sort-Object Date, Title)) {
236
+ $tagText = ($e.Tags | ForEach-Object { "[$_]" }) -join ""
237
+ $fileText = "-"
238
+ if ($e.Files -and @($e.Files).Count -gt 0) { $fileText = ($e.Files -join ", ") }
239
+ $ji += "$($e.Date) | $tagText | $($e.Title) | $fileText"
240
+ }
241
+
242
+ Write-Utf8NoBom (Join-Path $MemoryDir "journal-index.md") ($ji -join "`n")
243
+ $journalJson = if (-not $journalEntries -or $journalEntries.Count -eq 0) { "[]" } else { ConvertTo-Json -InputObject @($journalEntries.ToArray()) -Depth 6 }
244
+ Write-Utf8NoBom (Join-Path $MemoryDir "journal-index.json") $journalJson
245
+
246
+ function Resolve-PythonCommand {
247
+ $candidates = @(
248
+ @{ Kind = "python"; Args = @() },
249
+ @{ Kind = "py"; Args = @("-3") },
250
+ @{ Kind = "py"; Args = @() },
251
+ @{ Kind = "python3"; Args = @() }
252
+ )
253
+ foreach ($c in $candidates) {
254
+ $cmd = Get-Command $c.Kind -ErrorAction SilentlyContinue
255
+ if ($null -eq $cmd) { continue }
256
+ try {
257
+ & $cmd.Source @($c.Args) -c "import sys; print(sys.version)" 1>$null 2>$null
258
+ if ($LASTEXITCODE -eq 0) { return @{ Path = $cmd.Source; Args = @($c.Args) } }
259
+ } catch {}
260
+ }
261
+ return $null
262
+ }
263
+
264
+ $py = Resolve-PythonCommand
265
+ $pyScript = Join-Path $RepoRoot "scripts\memory\build-memory-sqlite.py"
266
+ if ($null -ne $py -and (Test-Path $pyScript)) {
267
+ Write-Host "Python detected; building SQLite FTS index..." -ForegroundColor Cyan
268
+ & $py.Path @($py.Args) $pyScript --repo $RepoRoot | Out-Host
269
+ } else {
270
+ Write-Host "Python not found (or not runnable); skipping SQLite build." -ForegroundColor DarkYellow
271
+ }
272
+
273
+ $hotFiles = @(
274
+ (Join-Path $MemoryDir "hot-rules.md"),
275
+ (Join-Path $MemoryDir "active-context.md"),
276
+ (Join-Path $MemoryDir "memo.md")
277
+ )
278
+ $totalChars = 0
279
+ foreach ($hf in $hotFiles) {
280
+ if (Test-Path $hf) {
281
+ $t = Get-Content -Raw -ErrorAction SilentlyContinue $hf
282
+ if ($t) { $totalChars += $t.Length }
283
+ }
284
+ }
285
+ $estimatedTokens = [math]::Round($totalChars / 4)
286
+ Write-Host ""
287
+ if ($totalChars -gt 8000) {
288
+ Write-Host "WARNING: Always-read layer is $totalChars chars (~$estimatedTokens tokens)" -ForegroundColor Yellow
289
+ } else {
290
+ Write-Host "Always-read layer: $totalChars chars (~$estimatedTokens tokens) - Healthy" -ForegroundColor Green
291
+ }
292
+ Write-Host ""
293
+ Write-Host "Rebuild complete." -ForegroundColor Green