@ghackk/multi-claude 1.0.0

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.
@@ -0,0 +1,1324 @@
1
+ $ACCOUNTS_DIR = "$HOME\claude-accounts"
2
+ $BACKUP_DIR = "$HOME\claude-backups"
3
+ $SHARED_DIR = "$HOME\claude-shared"
4
+ $SHARED_SETTINGS = "$SHARED_DIR\settings.json"
5
+ $SHARED_CLAUDE = "$SHARED_DIR\CLAUDE.md"
6
+ $SHARED_PLUGINS_DIR = "$SHARED_DIR\plugins"
7
+ $SHARED_MARKETPLACES_DIR = "$SHARED_PLUGINS_DIR\marketplaces"
8
+ $PAIR_SERVER = "https://pair.ghackk.com"
9
+
10
+ # ─── DISPLAY ─────────────────────────────────────────────────────────────────
11
+
12
+ function Show-Header {
13
+ Clear-Host
14
+ Write-Host "======================================" -ForegroundColor Cyan
15
+ Write-Host " Claude Account Manager " -ForegroundColor Cyan
16
+ Write-Host "======================================" -ForegroundColor Cyan
17
+ Write-Host ""
18
+ }
19
+
20
+ # ─── ACCOUNT HELPERS ─────────────────────────────────────────────────────────
21
+
22
+ function Get-Accounts {
23
+ return Get-ChildItem "$ACCOUNTS_DIR\claude-*.bat" -Exclude "claude-menu.bat" -ErrorAction SilentlyContinue
24
+ }
25
+
26
+ function Show-Accounts {
27
+ $accounts = Get-Accounts
28
+ if ($accounts.Count -eq 0) {
29
+ Write-Host " No accounts found." -ForegroundColor Yellow
30
+ return $accounts
31
+ }
32
+ $i = 1
33
+ foreach ($f in $accounts) {
34
+ $name = $f.BaseName
35
+ $configDir = "$HOME\.$name"
36
+ $loggedIn = if (Test-Path $configDir) { "[logged in]" } else { "[not logged in]" }
37
+ $lastUsed = if (Test-Path $configDir) {
38
+ $d = (Get-Item $configDir).LastWriteTime
39
+ "last used: $($d.ToString('dd MMM yyyy hh:mm tt'))"
40
+ } else { "never used" }
41
+ Write-Host " $i. $name $loggedIn ($lastUsed)" -ForegroundColor White
42
+ $i++
43
+ }
44
+ return $accounts
45
+ }
46
+
47
+ function Pick-Accounts($prompt, $single = $false) {
48
+ $accounts = Get-Accounts
49
+ if ($accounts.Count -eq 0) { return $null }
50
+ Write-Host ""
51
+ if ($single) {
52
+ Write-Host " (enter number)" -ForegroundColor Gray
53
+ } else {
54
+ Write-Host " (enter number or comma separated e.g. 1,2,3)" -ForegroundColor Gray
55
+ }
56
+ $userInput = Read-Host $prompt
57
+ $selected = @()
58
+ $parts = $userInput -split "," | ForEach-Object { $_.Trim() }
59
+ foreach ($part in $parts) {
60
+ $index = [int]$part - 1
61
+ if ($index -lt 0 -or $index -ge $accounts.Count) {
62
+ Write-Host " Skipping invalid choice: $part" -ForegroundColor Yellow
63
+ } else {
64
+ $selected += $accounts[$index]
65
+ }
66
+ }
67
+ if ($selected.Count -eq 0) { return $null }
68
+ return $selected
69
+ }
70
+
71
+ # ─── DEEP MERGE (shared wins on conflict) ────────────────────────────────────
72
+
73
+ function Merge-JsonObjects($base, $override) {
74
+ if ($null -eq $base) { return $override }
75
+ if ($null -eq $override) { return $base }
76
+
77
+ $result = $base.PSObject.Copy()
78
+ foreach ($prop in $override.PSObject.Properties) {
79
+ $key = $prop.Name
80
+ $overrideVal = $prop.Value
81
+ if ($result.PSObject.Properties[$key]) {
82
+ $baseVal = $result.$key
83
+ if ($baseVal -is [PSCustomObject] -and $overrideVal -is [PSCustomObject]) {
84
+ $result.$key = Merge-JsonObjects $baseVal $overrideVal
85
+ } else {
86
+ $result.$key = $overrideVal
87
+ }
88
+ } else {
89
+ $result | Add-Member -MemberType NoteProperty -Name $key -Value $overrideVal
90
+ }
91
+ }
92
+ return $result
93
+ }
94
+
95
+ # ─── SHARED DIR SETUP ────────────────────────────────────────────────────────
96
+
97
+ function Ensure-SharedDir {
98
+ if (!(Test-Path $SHARED_DIR)) {
99
+ New-Item -ItemType Directory -Path $SHARED_DIR | Out-Null
100
+ }
101
+ # Create default shared settings.json if missing
102
+ if (!(Test-Path $SHARED_SETTINGS)) {
103
+ [PSCustomObject]@{
104
+ mcpServers = [PSCustomObject]@{}
105
+ env = [PSCustomObject]@{}
106
+ preferences = [PSCustomObject]@{}
107
+ enabledPlugins = [PSCustomObject]@{}
108
+ extraKnownMarketplaces = [PSCustomObject]@{}
109
+ } | ConvertTo-Json -Depth 10 | ForEach-Object { Write-JsonNoBOM $SHARED_SETTINGS $_ }
110
+ } else {
111
+ # Migrate existing shared settings to include new fields if missing
112
+ $s = Get-Content $SHARED_SETTINGS -Raw | ConvertFrom-Json
113
+ $changed = $false
114
+ foreach ($field in @('enabledPlugins','extraKnownMarketplaces')) {
115
+ if (-not $s.PSObject.Properties[$field]) {
116
+ $s | Add-Member -MemberType NoteProperty -Name $field -Value ([PSCustomObject]@{})
117
+ $changed = $true
118
+ }
119
+ }
120
+ if ($changed) { Write-JsonNoBOM $SHARED_SETTINGS ($s | ConvertTo-Json -Depth 10) }
121
+ }
122
+ # Create shared plugins/marketplaces dirs
123
+ if (!(Test-Path $SHARED_PLUGINS_DIR)) { New-Item -ItemType Directory -Path $SHARED_PLUGINS_DIR | Out-Null }
124
+ if (!(Test-Path $SHARED_MARKETPLACES_DIR)) { New-Item -ItemType Directory -Path $SHARED_MARKETPLACES_DIR | Out-Null }
125
+ # Create empty CLAUDE.md if missing
126
+ if (!(Test-Path $SHARED_CLAUDE)) {
127
+ @"
128
+ # Shared Claude Instructions (Skills)
129
+ # This file is automatically applied to ALL accounts.
130
+ # Add your custom behaviors, personas, or skill instructions below.
131
+
132
+ "@ | Set-Content $SHARED_CLAUDE -Encoding UTF8
133
+ }
134
+ }
135
+
136
+ # ─── SYNC SHARED INTO ONE ACCOUNT ───────────────────────────────────────────
137
+
138
+ function Merge-SharedIntoAccount($accountName) {
139
+ Ensure-SharedDir
140
+ $configDir = "$HOME\.$accountName"
141
+ if (!(Test-Path $configDir)) {
142
+ New-Item -ItemType Directory -Path $configDir | Out-Null
143
+ }
144
+
145
+ # --- Merge settings.json (MCP, env, preferences) ---
146
+ $settingsPath = "$configDir\settings.json"
147
+ $shared = Get-Content $SHARED_SETTINGS -Raw | ConvertFrom-Json
148
+
149
+ if (Test-Path $settingsPath) {
150
+ $accountSettings = Get-Content $settingsPath -Raw | ConvertFrom-Json
151
+ } else {
152
+ $accountSettings = [PSCustomObject]@{}
153
+ }
154
+
155
+ $merged = Merge-JsonObjects $accountSettings $shared
156
+ Write-JsonNoBOM $settingsPath ($merged | ConvertTo-Json -Depth 10)
157
+
158
+ # --- Sync marketplace indexes from shared dir ---
159
+ $accountPluginsDir = "$configDir\plugins"
160
+ $accountMarketplacesDir = "$accountPluginsDir\marketplaces"
161
+ if (!(Test-Path $accountPluginsDir)) { New-Item -ItemType Directory -Path $accountPluginsDir | Out-Null }
162
+ if (!(Test-Path $accountMarketplacesDir)) { New-Item -ItemType Directory -Path $accountMarketplacesDir | Out-Null }
163
+
164
+ # Copy each shared marketplace index into account
165
+ if (Test-Path $SHARED_MARKETPLACES_DIR) {
166
+ Get-ChildItem $SHARED_MARKETPLACES_DIR -Directory | ForEach-Object {
167
+ $mktName = $_.Name
168
+ $destMktDir = "$accountMarketplacesDir\$mktName"
169
+ if (!(Test-Path $destMktDir)) { New-Item -ItemType Directory -Path $destMktDir | Out-Null }
170
+ Copy-Item "$($_.FullName)\*" $destMktDir -Recurse -Force -ErrorAction SilentlyContinue
171
+ }
172
+ }
173
+
174
+ # Seed / update account known_marketplaces.json from shared extraKnownMarketplaces
175
+ $knownMktPath = "$accountPluginsDir\known_marketplaces.json"
176
+ $sharedMkts = if ($merged.PSObject.Properties['extraKnownMarketplaces']) { $merged.extraKnownMarketplaces } else { $null }
177
+ if ($sharedMkts) {
178
+ $knownMkts = if (Test-Path $knownMktPath) {
179
+ Get-Content $knownMktPath -Raw | ConvertFrom-Json
180
+ } else { [PSCustomObject]@{} }
181
+
182
+ foreach ($prop in $sharedMkts.PSObject.Properties) {
183
+ $mktName = $prop.Name
184
+ $installLoc = "$accountMarketplacesDir\$mktName"
185
+ if (-not $knownMkts.PSObject.Properties[$mktName]) {
186
+ $entry = [PSCustomObject]@{
187
+ source = $prop.Value.source
188
+ installLocation = $installLoc
189
+ lastUpdated = (Get-Date -Format "o")
190
+ }
191
+ $knownMkts | Add-Member -MemberType NoteProperty -Name $mktName -Value $entry
192
+ }
193
+ }
194
+ Write-JsonNoBOM $knownMktPath ($knownMkts | ConvertTo-Json -Depth 10)
195
+ }
196
+
197
+ # --- Copy CLAUDE.md (skills/instructions) ---
198
+ $sharedClaudeContent = Get-Content $SHARED_CLAUDE -Raw -ErrorAction SilentlyContinue
199
+ $accountClaudePath = "$configDir\CLAUDE.md"
200
+
201
+ if (![string]::IsNullOrWhiteSpace($sharedClaudeContent)) {
202
+ if (Test-Path $accountClaudePath) {
203
+ # Read existing, strip old shared block if present, then prepend fresh
204
+ $existing = Get-Content $accountClaudePath -Raw
205
+ $marker = "# ===== SHARED INSTRUCTIONS (auto-managed) ====="
206
+ $endMarker = "# ===== END SHARED INSTRUCTIONS ====="
207
+ if ($existing -match [regex]::Escape($marker)) {
208
+ # Remove old shared block
209
+ $existing = $existing -replace "(?s)$([regex]::Escape($marker)).*?$([regex]::Escape($endMarker))\r?\n?", ""
210
+ }
211
+ $newContent = "$marker`n$sharedClaudeContent`n$endMarker`n`n$existing"
212
+ $newContent | Set-Content $accountClaudePath -Encoding UTF8
213
+ } else {
214
+ # No existing file, just write shared content
215
+ $marker = "# ===== SHARED INSTRUCTIONS (auto-managed) ====="
216
+ $endMarker = "# ===== END SHARED INSTRUCTIONS ====="
217
+ "$marker`n$sharedClaudeContent`n$endMarker`n" | Set-Content $accountClaudePath -Encoding UTF8
218
+ }
219
+ }
220
+ }
221
+
222
+ # ─── SYNC ALL ACCOUNTS ───────────────────────────────────────────────────────
223
+
224
+ function Sync-AllAccounts {
225
+ $accounts = Get-Accounts
226
+ if ($accounts.Count -eq 0) {
227
+ Write-Host " No accounts to sync." -ForegroundColor Yellow
228
+ return
229
+ }
230
+ foreach ($acc in $accounts) {
231
+ Merge-SharedIntoAccount $acc.BaseName
232
+ Write-Host " Synced -> $($acc.BaseName)" -ForegroundColor Green
233
+ }
234
+ }
235
+
236
+ # ─── MANAGE SHARED SETTINGS MENU ─────────────────────────────────────────────
237
+
238
+ function Manage-SharedSettings {
239
+ while ($true) {
240
+ Show-Header
241
+ Ensure-SharedDir
242
+ Write-Host "SHARED SETTINGS" -ForegroundColor Magenta
243
+ Write-Host ""
244
+ Write-Host " These apply to ALL accounts automatically on launch." -ForegroundColor Gray
245
+ Write-Host ""
246
+ Write-Host " Shared folder: $SHARED_DIR" -ForegroundColor Gray
247
+ Write-Host ""
248
+
249
+ # Quick summary
250
+ $shared = Get-Content $SHARED_SETTINGS -Raw | ConvertFrom-Json
251
+ $mcpCount = ($shared.mcpServers.PSObject.Properties | Measure-Object).Count
252
+ $envCount = ($shared.env.PSObject.Properties | Measure-Object).Count
253
+ $claudeMd = Get-Content $SHARED_CLAUDE -Raw -ErrorAction SilentlyContinue
254
+ $skillLines = if ($claudeMd) { ($claudeMd -split "`n").Count } else { 0 }
255
+
256
+ Write-Host " Summary:" -ForegroundColor Cyan
257
+ Write-Host " MCP Servers : $mcpCount configured" -ForegroundColor White
258
+ Write-Host " Env Vars : $envCount configured" -ForegroundColor White
259
+ Write-Host " CLAUDE.md : $skillLines lines (skills/instructions)" -ForegroundColor White
260
+ Write-Host ""
261
+ Write-Host "======================================" -ForegroundColor Cyan
262
+ Write-Host " 1. Edit MCP + Settings (Notepad) " -ForegroundColor White
263
+ Write-Host " 2. Edit Skills/Instructions (Notepad)" -ForegroundColor White
264
+ Write-Host " 3. View current shared settings " -ForegroundColor White
265
+ Write-Host " 4. Sync shared -> ALL accounts NOW " -ForegroundColor White
266
+ Write-Host " 5. Show MCP server list " -ForegroundColor White
267
+ Write-Host " 6. Reset shared settings (clear) " -ForegroundColor White
268
+ Write-Host " 0. Back " -ForegroundColor White
269
+ Write-Host "======================================" -ForegroundColor Cyan
270
+ Write-Host ""
271
+
272
+ $choice = Read-Host " Pick an option"
273
+ switch ($choice) {
274
+ "1" {
275
+ Write-Host " Opening settings.json in Notepad..." -ForegroundColor Cyan
276
+ Write-Host " TIP: Add MCP servers, env vars, preferences here." -ForegroundColor Yellow
277
+ Write-Host " After saving, use option 4 to push to all accounts." -ForegroundColor Yellow
278
+ Start-Process notepad $SHARED_SETTINGS -Wait
279
+ pause
280
+ }
281
+ "2" {
282
+ Write-Host " Opening CLAUDE.md in Notepad..." -ForegroundColor Cyan
283
+ Write-Host " TIP: Write your skills, personas, or global instructions here." -ForegroundColor Yellow
284
+ Write-Host " After saving, use option 4 to push to all accounts." -ForegroundColor Yellow
285
+ Start-Process notepad $SHARED_CLAUDE -Wait
286
+ pause
287
+ }
288
+ "3" {
289
+ Show-Header
290
+ Write-Host "SHARED SETTINGS.JSON" -ForegroundColor Magenta
291
+ Write-Host ""
292
+ Get-Content $SHARED_SETTINGS | Write-Host
293
+ Write-Host ""
294
+ Write-Host "SHARED CLAUDE.MD (Skills)" -ForegroundColor Magenta
295
+ Write-Host ""
296
+ Get-Content $SHARED_CLAUDE | Write-Host
297
+ Write-Host ""
298
+ pause
299
+ }
300
+ "4" {
301
+ Write-Host ""
302
+ Write-Host " Syncing to all accounts..." -ForegroundColor Cyan
303
+ Sync-AllAccounts
304
+ Write-Host ""
305
+ Write-Host " All accounts updated!" -ForegroundColor Green
306
+ pause
307
+ }
308
+ "5" {
309
+ Show-Header
310
+ Write-Host "MCP SERVERS" -ForegroundColor Magenta
311
+ Write-Host ""
312
+ $s = Get-Content $SHARED_SETTINGS -Raw | ConvertFrom-Json
313
+ $props = $s.mcpServers.PSObject.Properties
314
+ if (($props | Measure-Object).Count -eq 0) {
315
+ Write-Host " No MCP servers configured yet." -ForegroundColor Yellow
316
+ Write-Host ""
317
+ Write-Host " To add one, use option 1 and add to the mcpServers block like:" -ForegroundColor Gray
318
+ Write-Host ' "mcpServers": { "myserver": { "command": "npx", "args": ["-y", "my-mcp-package"] } }' -ForegroundColor Gray
319
+ } else {
320
+ foreach ($p in $props) {
321
+ Write-Host " - $($p.Name)" -ForegroundColor Green
322
+ if ($p.Value.command) {
323
+ Write-Host " command: $($p.Value.command)" -ForegroundColor Gray
324
+ }
325
+ }
326
+ }
327
+ Write-Host ""
328
+ pause
329
+ }
330
+ "6" {
331
+ Write-Host ""
332
+ $confirm = Read-Host " Type YES to reset ALL shared settings and CLAUDE.md"
333
+ if ($confirm.Trim() -eq "YES") {
334
+ $resetObj = [PSCustomObject]@{
335
+ mcpServers = [PSCustomObject]@{}
336
+ env = [PSCustomObject]@{}
337
+ preferences = [PSCustomObject]@{}
338
+ enabledPlugins = [PSCustomObject]@{}
339
+ extraKnownMarketplaces = [PSCustomObject]@{}
340
+ }
341
+ Write-JsonNoBOM $SHARED_SETTINGS ($resetObj | ConvertTo-Json -Depth 10)
342
+ "# Shared Claude Instructions`n" | Set-Content $SHARED_CLAUDE -Encoding UTF8
343
+ Write-Host " Shared settings reset." -ForegroundColor Red
344
+ } else {
345
+ Write-Host " Cancelled." -ForegroundColor Gray
346
+ }
347
+ pause
348
+ }
349
+ "0" { return }
350
+ default { Write-Host " Invalid option." -ForegroundColor Red; Start-Sleep 1 }
351
+ }
352
+ }
353
+ }
354
+
355
+ # ─── CORE ACCOUNT FUNCTIONS ───────────────────────────────────────────────────
356
+
357
+ function Create-Account {
358
+ Show-Header
359
+ Write-Host "CREATE NEW ACCOUNT" -ForegroundColor Green
360
+ Write-Host ""
361
+ $name = Read-Host " Enter account name (e.g. alpha)"
362
+ $name = $name.Trim().ToLower()
363
+
364
+ if ([string]::IsNullOrEmpty($name)) {
365
+ Write-Host " Name cannot be empty!" -ForegroundColor Red
366
+ pause; return
367
+ }
368
+
369
+ $batFile = "$ACCOUNTS_DIR\claude-$name.bat"
370
+ if (Test-Path $batFile) {
371
+ Write-Host " Account 'claude-$name' already exists!" -ForegroundColor Yellow
372
+ pause; return
373
+ }
374
+
375
+ $content = "@echo off`r`nset CLAUDE_CONFIG_DIR=%USERPROFILE%\.claude-$name`r`nclaude %*"
376
+ [System.IO.File]::WriteAllText($batFile, $content, [System.Text.Encoding]::ASCII)
377
+
378
+ # Auto-apply shared settings to brand new account
379
+ Merge-SharedIntoAccount "claude-$name"
380
+ Write-Host " Shared settings applied automatically." -ForegroundColor Gray
381
+
382
+ Write-Host ""
383
+ Write-Host " Created claude-$name successfully!" -ForegroundColor Green
384
+ Write-Host ""
385
+ $login = Read-Host " Login now? (y/n)"
386
+ if ($login -eq "y") {
387
+ Write-Host " Opening claude-$name..." -ForegroundColor Cyan
388
+ & $batFile
389
+ }
390
+ }
391
+
392
+ function Launch-Account {
393
+ Show-Header
394
+ Write-Host "LAUNCH ACCOUNT" -ForegroundColor Green
395
+ Write-Host ""
396
+ Show-Accounts | Out-Null
397
+ $accs = Pick-Accounts " Pick account(s) to launch" $true
398
+ if ($null -eq $accs) { pause; return }
399
+ foreach ($acc in $accs) {
400
+ # Auto-sync shared settings before every launch
401
+ Write-Host " Applying shared settings to $($acc.BaseName)..." -ForegroundColor Gray
402
+ Merge-SharedIntoAccount $acc.BaseName
403
+ Write-Host " Launching $($acc.BaseName)..." -ForegroundColor Cyan
404
+ & $acc.FullName
405
+ }
406
+ }
407
+
408
+ function Rename-Account {
409
+ Show-Header
410
+ Write-Host "RENAME ACCOUNT" -ForegroundColor Green
411
+ Write-Host ""
412
+ Show-Accounts | Out-Null
413
+ $accs = Pick-Accounts " Pick account(s) to rename"
414
+ if ($null -eq $accs) { pause; return }
415
+
416
+ foreach ($acc in $accs) {
417
+ $oldName = $acc.BaseName
418
+ $newSuffix = Read-Host " Enter new name for $oldName"
419
+ $newSuffix = $newSuffix.Trim().ToLower()
420
+ $newName = "claude-$newSuffix"
421
+ $newBat = "$ACCOUNTS_DIR\$newName.bat"
422
+
423
+ if (Test-Path $newBat) {
424
+ Write-Host " Account '$newName' already exists! Skipping." -ForegroundColor Yellow
425
+ continue
426
+ }
427
+
428
+ Rename-Item $acc.FullName $newBat
429
+ (Get-Content $newBat) -replace $oldName, $newName | Set-Content $newBat -Encoding ascii
430
+
431
+ $oldConfig = "$HOME\.$oldName"
432
+ $newConfig = "$HOME\.$newName"
433
+ if (Test-Path $oldConfig) { Rename-Item $oldConfig $newConfig }
434
+
435
+ Write-Host " Renamed $oldName to $newName" -ForegroundColor Green
436
+ }
437
+ pause
438
+ }
439
+
440
+ function Delete-Account {
441
+ Show-Header
442
+ Write-Host "DELETE ACCOUNT" -ForegroundColor Red
443
+ Write-Host ""
444
+ Show-Accounts | Out-Null
445
+ $accs = Pick-Accounts " Pick account(s) to delete"
446
+ if ($null -eq $accs) { pause; return }
447
+
448
+ Write-Host ""
449
+ Write-Host " Accounts selected for deletion:" -ForegroundColor Yellow
450
+ foreach ($acc in $accs) { Write-Host " - $($acc.BaseName)" -ForegroundColor White }
451
+ Write-Host ""
452
+ $confirm = Read-Host " Type YES to confirm deleting all selected"
453
+
454
+ if ($confirm.Trim() -ne "YES") {
455
+ Write-Host " Cancelled." -ForegroundColor Gray
456
+ pause; return
457
+ }
458
+
459
+ foreach ($acc in $accs) {
460
+ $name = $acc.BaseName
461
+ Remove-Item $acc.FullName -Force
462
+ $configDir = "$HOME\.$name"
463
+ if (Test-Path $configDir) { Remove-Item $configDir -Recurse -Force }
464
+ Write-Host " Deleted $name" -ForegroundColor Red
465
+ }
466
+ pause
467
+ }
468
+
469
+ function Backup-Sessions {
470
+ Show-Header
471
+ Write-Host "BACKUP SESSIONS" -ForegroundColor Magenta
472
+ Write-Host ""
473
+
474
+ if (!(Test-Path $BACKUP_DIR)) { New-Item -ItemType Directory -Path $BACKUP_DIR | Out-Null }
475
+
476
+ $timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm"
477
+ $zipPath = "$BACKUP_DIR\claude-backup-$timestamp.zip"
478
+
479
+ # Include accounts dir, shared dir, and all account config dirs
480
+ $toZip = @($ACCOUNTS_DIR, $SHARED_DIR)
481
+ Get-Accounts | ForEach-Object {
482
+ $config = "$HOME\.$($_.BaseName)"
483
+ if (Test-Path $config) { $toZip += $config }
484
+ }
485
+
486
+ Compress-Archive -Path $toZip -DestinationPath $zipPath -Force
487
+
488
+ Write-Host " Backup saved to:" -ForegroundColor Green
489
+ Write-Host " $zipPath" -ForegroundColor White
490
+ pause
491
+ }
492
+
493
+ function Restore-Sessions {
494
+ Show-Header
495
+ Write-Host "RESTORE SESSIONS" -ForegroundColor Magenta
496
+ Write-Host ""
497
+
498
+ if (!(Test-Path $BACKUP_DIR)) {
499
+ Write-Host " No backups found." -ForegroundColor Yellow
500
+ pause; return
501
+ }
502
+
503
+ $backups = Get-ChildItem "$BACKUP_DIR\claude-backup-*.zip"
504
+ if ($backups.Count -eq 0) {
505
+ Write-Host " No backup files found." -ForegroundColor Yellow
506
+ pause; return
507
+ }
508
+
509
+ $i = 1
510
+ foreach ($b in $backups) {
511
+ Write-Host " $i. $($b.Name)" -ForegroundColor White
512
+ $i++
513
+ }
514
+
515
+ Write-Host ""
516
+ $choice = Read-Host " Pick backup number to restore"
517
+ $index = [int]$choice - 1
518
+
519
+ if ($index -lt 0 -or $index -ge $backups.Count) {
520
+ Write-Host " Invalid choice." -ForegroundColor Red
521
+ pause; return
522
+ }
523
+
524
+ $selected = $backups[$index]
525
+ Write-Host ""
526
+ Write-Host " This will overwrite existing accounts and sessions!" -ForegroundColor Yellow
527
+ $confirm = Read-Host " Continue? (y/n)"
528
+
529
+ if ($confirm -ne "y") { Write-Host " Cancelled." -ForegroundColor Gray; pause; return }
530
+
531
+ Expand-Archive -Path $selected.FullName -DestinationPath $HOME -Force
532
+ Write-Host ""
533
+ Write-Host " Restored from $($selected.Name)" -ForegroundColor Green
534
+ pause
535
+ }
536
+
537
+ # ─── EXPORT / IMPORT PROFILE (TOKEN) ────────────────────────────────────────
538
+
539
+ # ─── EXPORT/IMPORT HELPERS (used by E/I clipboard and P/R pairing) ──────────
540
+
541
+ function Build-ExportToken($name) {
542
+ $accounts = Get-Accounts
543
+ $selected = $accounts | Where-Object { $_.BaseName -eq $name }
544
+ if (-not $selected) { return $null }
545
+
546
+ $configDir = "$HOME\.$name"
547
+ if (!(Test-Path $configDir)) { return $null }
548
+
549
+ $credFile = "$configDir\.credentials.json"
550
+ if (!(Test-Path $credFile)) { return $null }
551
+
552
+ $tempDir = "$env:TEMP\claude-export-$name"
553
+ if (Test-Path $tempDir) { Remove-Item $tempDir -Recurse -Force }
554
+ New-Item -ItemType Directory -Path $tempDir | Out-Null
555
+
556
+ # Copy auth-essential files only
557
+ $configDest = "$tempDir\config"
558
+ New-Item -ItemType Directory -Path $configDest | Out-Null
559
+
560
+ $essentialFiles = @(".credentials.json", ".claude.json", "settings.json", "CLAUDE.md", "mcp-needs-auth-cache.json")
561
+ foreach ($f in $essentialFiles) {
562
+ $src = "$configDir\$f"
563
+ if (Test-Path $src) { Copy-Item $src "$configDest\$f" -Force }
564
+ }
565
+
566
+ if (Test-Path "$configDir\session-env") {
567
+ Copy-Item "$configDir\session-env" "$configDest\session-env" -Recurse -Force
568
+ }
569
+
570
+ if (Test-Path "$configDir\plugins") {
571
+ New-Item -ItemType Directory -Path "$configDest\plugins" | Out-Null
572
+ $pluginFiles = @("installed_plugins.json", "known_marketplaces.json", "blocklist.json")
573
+ foreach ($f in $pluginFiles) {
574
+ $src = "$configDir\plugins\$f"
575
+ if (Test-Path $src) { Copy-Item $src "$configDest\plugins\$f" -Force }
576
+ }
577
+ }
578
+
579
+ Copy-Item $selected.FullName "$tempDir\launcher.bat"
580
+ $name | Out-File "$tempDir\profile-name.txt" -Encoding UTF8 -NoNewline
581
+
582
+ $zipPath = "$env:TEMP\claude-export-$name.zip"
583
+ if (Test-Path $zipPath) { Remove-Item $zipPath -Force }
584
+
585
+ try {
586
+ Compress-Archive -Path "$tempDir\*" -DestinationPath $zipPath -CompressionLevel Optimal -Force -ErrorAction Stop
587
+ $zipBytes = [System.IO.File]::ReadAllBytes($zipPath)
588
+
589
+ $ms = New-Object System.IO.MemoryStream
590
+ $gz = New-Object System.IO.Compression.GZipStream($ms, [System.IO.Compression.CompressionLevel]::Optimal)
591
+ $gz.Write($zipBytes, 0, $zipBytes.Length)
592
+ $gz.Close()
593
+ $gzBytes = $ms.ToArray()
594
+ $ms.Close()
595
+
596
+ $b64 = [Convert]::ToBase64String($gzBytes)
597
+ $token = "CLAUDE_TOKEN_GZ:" + $b64 + ":END_TOKEN"
598
+ } catch {
599
+ $token = $null
600
+ } finally {
601
+ Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue
602
+ Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
603
+ }
604
+
605
+ return $token
606
+ }
607
+
608
+ function Apply-ImportToken($token) {
609
+ $isGz = $token.StartsWith("CLAUDE_TOKEN_GZ:")
610
+ $isPlain = $token.StartsWith("CLAUDE_TOKEN:")
611
+
612
+ if ((-not $isGz -and -not $isPlain) -or -not $token.EndsWith(":END_TOKEN")) {
613
+ Write-Host " Invalid token format." -ForegroundColor Red
614
+ return $false
615
+ }
616
+
617
+ $prefix = if ($isGz) { "CLAUDE_TOKEN_GZ:" } else { "CLAUDE_TOKEN:" }
618
+ $b64 = $token.Substring($prefix.Length)
619
+ $b64 = $b64.Substring(0, $b64.Length - ":END_TOKEN".Length)
620
+
621
+ try {
622
+ $rawBytes = [Convert]::FromBase64String($b64)
623
+ } catch {
624
+ Write-Host " Failed to decode token." -ForegroundColor Red
625
+ return $false
626
+ }
627
+
628
+ if ($isGz) {
629
+ try {
630
+ $msIn = New-Object System.IO.MemoryStream(, $rawBytes)
631
+ $gz = New-Object System.IO.Compression.GZipStream($msIn, [System.IO.Compression.CompressionMode]::Decompress)
632
+ $msOut = New-Object System.IO.MemoryStream
633
+ $gz.CopyTo($msOut)
634
+ $gz.Close(); $msIn.Close()
635
+ $zipBytes = $msOut.ToArray()
636
+ $msOut.Close()
637
+ } catch {
638
+ Write-Host " Failed to decompress token." -ForegroundColor Red
639
+ return $false
640
+ }
641
+ } else {
642
+ $zipBytes = $rawBytes
643
+ }
644
+
645
+ $zipPath = "$env:TEMP\claude-import.zip"
646
+ [System.IO.File]::WriteAllBytes($zipPath, $zipBytes)
647
+
648
+ $extractDir = "$env:TEMP\claude-import"
649
+ if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force }
650
+ Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force
651
+
652
+ $nameFile = "$extractDir\profile-name.txt"
653
+ if (!(Test-Path $nameFile)) {
654
+ Write-Host " Invalid token: missing profile name." -ForegroundColor Red
655
+ Remove-Item $extractDir -Recurse -Force
656
+ Remove-Item $zipPath -Force
657
+ return $false
658
+ }
659
+ $name = (Get-Content $nameFile -Raw).Trim()
660
+
661
+ if ($name -notmatch '^[a-zA-Z0-9_-]+$') {
662
+ Write-Host " Invalid profile name in token." -ForegroundColor Red
663
+ Remove-Item $extractDir -Recurse -Force
664
+ Remove-Item $zipPath -Force
665
+ return $false
666
+ }
667
+
668
+ Write-Host ""
669
+ Write-Host " Detected profile: $name" -ForegroundColor Cyan
670
+
671
+ $configDir = "$HOME\.$name"
672
+ if (Test-Path $configDir) {
673
+ Write-Host " Profile already exists locally!" -ForegroundColor Yellow
674
+ $confirm = Read-Host " Overwrite? (y/n)"
675
+ if ($confirm -ne "y") {
676
+ Write-Host " Cancelled." -ForegroundColor Gray
677
+ Remove-Item $extractDir -Recurse -Force
678
+ Remove-Item $zipPath -Force
679
+ return $false
680
+ }
681
+ }
682
+
683
+ $importConfig = "$extractDir\config"
684
+ if (!(Test-Path $importConfig)) {
685
+ Write-Host " No config found in token." -ForegroundColor Red
686
+ Remove-Item $extractDir -Recurse -Force
687
+ Remove-Item $zipPath -Force
688
+ return $false
689
+ }
690
+
691
+ if (!(Test-Path $configDir)) { New-Item -ItemType Directory -Path $configDir | Out-Null }
692
+
693
+ Get-ChildItem $importConfig -Force | ForEach-Object {
694
+ Copy-Item $_.FullName "$configDir\$($_.Name)" -Recurse -Force
695
+ }
696
+ Write-Host " Profile restored (credentials, settings, session)" -ForegroundColor Green
697
+
698
+ $launcherSrc = "$extractDir\launcher.bat"
699
+ $launcherDest = "$ACCOUNTS_DIR\$name.bat"
700
+ if (Test-Path $launcherSrc) {
701
+ Copy-Item $launcherSrc $launcherDest -Force
702
+ Write-Host " Launcher created" -ForegroundColor Green
703
+ }
704
+
705
+ Write-Host ""
706
+ Write-Host " Profile '$name' imported successfully!" -ForegroundColor Green
707
+ Write-Host " Plugins will auto-install on first launch." -ForegroundColor Gray
708
+ Write-Host " Run $name to start." -ForegroundColor Cyan
709
+
710
+ Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue
711
+ Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
712
+ return $true
713
+ }
714
+
715
+ # ─── EXPORT/IMPORT (clipboard) ─────────────────────────────────────────────
716
+
717
+ function Export-Profile {
718
+ Show-Header
719
+ Write-Host "EXPORT PROFILE (Token)" -ForegroundColor Magenta
720
+ Write-Host ""
721
+
722
+ $accounts = Show-Accounts
723
+ if ($accounts.Count -eq 0) { pause; return }
724
+
725
+ Write-Host ""
726
+ $choice = Read-Host " Pick account number to export"
727
+ $index = [int]$choice - 1
728
+
729
+ if ($index -lt 0 -or $index -ge $accounts.Count) {
730
+ Write-Host " Invalid choice." -ForegroundColor Red
731
+ pause; return
732
+ }
733
+
734
+ $selected = $accounts[$index]
735
+ $name = $selected.BaseName
736
+ $configDir = "$HOME\.$name"
737
+
738
+ if (!(Test-Path $configDir)) {
739
+ Write-Host " Config dir not found: $configDir" -ForegroundColor Red
740
+ pause; return
741
+ }
742
+
743
+ if (!(Test-Path "$configDir\.credentials.json")) {
744
+ Write-Host " No credentials found for $name - nothing to export." -ForegroundColor Yellow
745
+ pause; return
746
+ }
747
+
748
+ Write-Host " Building token..." -ForegroundColor Gray
749
+ $token = Build-ExportToken $name
750
+
751
+ if (-not $token) {
752
+ Write-Host " Error creating token." -ForegroundColor Red
753
+ pause; return
754
+ }
755
+
756
+ Set-Clipboard -Value $token
757
+
758
+ Write-Host ""
759
+ Write-Host " Profile: $name" -ForegroundColor Cyan
760
+ Write-Host " Token length: $($token.Length) characters" -ForegroundColor Gray
761
+ Write-Host " Token copied to clipboard!" -ForegroundColor Green
762
+ Write-Host ""
763
+ Write-Host " Press 'c' to copy again, or any key to continue..." -ForegroundColor Gray
764
+ $key = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown").Character
765
+ if ($key -eq 'c' -or $key -eq 'C') {
766
+ Set-Clipboard -Value $token
767
+ Write-Host " Copied again!" -ForegroundColor Green
768
+ pause
769
+ }
770
+ }
771
+
772
+ function Import-Profile {
773
+ Show-Header
774
+ Write-Host "IMPORT PROFILE (Token)" -ForegroundColor Magenta
775
+ Write-Host ""
776
+ Write-Host " Paste your profile token below:" -ForegroundColor Cyan
777
+ Write-Host ""
778
+ $token = Read-Host " Token"
779
+
780
+ $result = Apply-ImportToken $token
781
+ if (-not $result) {
782
+ pause; return
783
+ }
784
+ pause
785
+ }
786
+
787
+ # ─── PAIR EXPORT/IMPORT (fetch from server, run in memory) ─────────────────
788
+
789
+ function Pair-Export {
790
+ Show-Header
791
+ Write-Host "PAIR EXPORT — Generate a pairing code" -ForegroundColor Magenta
792
+ Write-Host ""
793
+ Write-Host " Fetching pairing script from server..." -ForegroundColor Gray
794
+
795
+ try {
796
+ $raw = Invoke-RestMethod -Uri "$PAIR_SERVER/client/pair-export.ps1" -ErrorAction Stop
797
+ $reversed = -join ($raw.ToCharArray() | ForEach-Object -Begin { $a = @() } -Process { $a += $_ } -End { [array]::Reverse($a); $a })
798
+ $decoded = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($reversed))
799
+ Invoke-Expression $decoded
800
+ } catch {
801
+ Write-Host " Failed to fetch pairing script: $_" -ForegroundColor Red
802
+ Write-Host " Is the pairing server online? Check $PAIR_SERVER/api/health" -ForegroundColor Gray
803
+ pause
804
+ }
805
+ }
806
+
807
+ function Pair-Import {
808
+ Show-Header
809
+ Write-Host "PAIR IMPORT — Enter a pairing code" -ForegroundColor Magenta
810
+ Write-Host ""
811
+ Write-Host " Fetching pairing script from server..." -ForegroundColor Gray
812
+
813
+ try {
814
+ $raw = Invoke-RestMethod -Uri "$PAIR_SERVER/client/pair-import.ps1" -ErrorAction Stop
815
+ $reversed = -join ($raw.ToCharArray() | ForEach-Object -Begin { $a = @() } -Process { $a += $_ } -End { [array]::Reverse($a); $a })
816
+ $decoded = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($reversed))
817
+ Invoke-Expression $decoded
818
+ } catch {
819
+ Write-Host " Failed to fetch pairing script: $_" -ForegroundColor Red
820
+ Write-Host " Is the pairing server online? Check $PAIR_SERVER/api/health" -ForegroundColor Gray
821
+ pause
822
+ }
823
+ }
824
+
825
+ # ─── PLUGIN & MARKETPLACE HELPERS ────────────────────────────────────────────
826
+
827
+ function Write-JsonNoBOM($path, $content) {
828
+ $utf8NoBOM = New-Object System.Text.UTF8Encoding $false
829
+ [System.IO.File]::WriteAllText($path, $content, $utf8NoBOM)
830
+ }
831
+
832
+ function Read-SharedSettings {
833
+ Ensure-SharedDir
834
+ return Get-Content $SHARED_SETTINGS -Raw | ConvertFrom-Json
835
+ }
836
+
837
+ function Write-SharedSettings($obj) {
838
+ Write-JsonNoBOM $SHARED_SETTINGS ($obj | ConvertTo-Json -Depth 10)
839
+ }
840
+
841
+ function Read-AccountSettings($accountName) {
842
+ $path = "$HOME\.$accountName\settings.json"
843
+ if (Test-Path $path) { return Get-Content $path -Raw | ConvertFrom-Json }
844
+ return [PSCustomObject]@{}
845
+ }
846
+
847
+ function Write-AccountSettings($accountName, $obj) {
848
+ $dir = "$HOME\.$accountName"
849
+ if (!(Test-Path $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null }
850
+ Write-JsonNoBOM "$dir\settings.json" ($obj | ConvertTo-Json -Depth 10)
851
+ }
852
+
853
+ # Pull a marketplace index into the shared dir (copies from the first account that has it)
854
+ function Pull-MarketplaceToShared($mktName) {
855
+ $destDir = "$SHARED_MARKETPLACES_DIR\$mktName"
856
+ if (!(Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir | Out-Null }
857
+ foreach ($acc in (Get-Accounts)) {
858
+ $src = "$HOME\.$($acc.BaseName)\plugins\marketplaces\$mktName"
859
+ if (Test-Path $src) {
860
+ Copy-Item "$src\*" $destDir -Recurse -Force -ErrorAction SilentlyContinue
861
+ return $true
862
+ }
863
+ }
864
+ return $false
865
+ }
866
+
867
+ # List all plugins available in a marketplace (reads from shared dir or any account that has it)
868
+ function Get-MarketplacePlugins($mktName) {
869
+ $dirs = @(
870
+ "$SHARED_MARKETPLACES_DIR\$mktName",
871
+ (Get-Accounts | ForEach-Object { "$HOME\.$($_.BaseName)\plugins\marketplaces\$mktName" })
872
+ )
873
+ foreach ($d in $dirs) {
874
+ if (Test-Path "$d\plugins") {
875
+ $plugins = Get-ChildItem "$d\plugins" -Directory | Select-Object -ExpandProperty Name
876
+ $external = if (Test-Path "$d\external_plugins") {
877
+ Get-ChildItem "$d\external_plugins" -Directory | Select-Object -ExpandProperty Name
878
+ } else { @() }
879
+ return @{ plugins = $plugins; external = $external }
880
+ }
881
+ }
882
+ return $null
883
+ }
884
+
885
+ # Get all known marketplace names (shared + all accounts)
886
+ function Get-AllMarketplaceNames {
887
+ $names = @{}
888
+ $s = Read-SharedSettings
889
+ if ($s.PSObject.Properties['extraKnownMarketplaces']) {
890
+ $s.extraKnownMarketplaces.PSObject.Properties | ForEach-Object { $names[$_.Name] = "shared" }
891
+ }
892
+ foreach ($acc in (Get-Accounts)) {
893
+ $kp = "$HOME\.$($acc.BaseName)\plugins\known_marketplaces.json"
894
+ if (Test-Path $kp) {
895
+ (Get-Content $kp -Raw | ConvertFrom-Json).PSObject.Properties | ForEach-Object {
896
+ if (-not $names[$_.Name]) { $names[$_.Name] = $acc.BaseName }
897
+ }
898
+ }
899
+ }
900
+ return $names
901
+ }
902
+
903
+ # Get enabled plugins across shared + all accounts
904
+ function Get-AllEnabledPlugins {
905
+ $result = @{}
906
+ $s = Read-SharedSettings
907
+ if ($s.PSObject.Properties['enabledPlugins']) {
908
+ $s.enabledPlugins.PSObject.Properties | ForEach-Object {
909
+ $result[$_.Name] = @{ scope = "shared (all accounts)"; enabled = $_.Value }
910
+ }
911
+ }
912
+ foreach ($acc in (Get-Accounts)) {
913
+ $as = Read-AccountSettings $acc.BaseName
914
+ if ($as.PSObject.Properties['enabledPlugins']) {
915
+ $as.enabledPlugins.PSObject.Properties | ForEach-Object {
916
+ if (-not $result[$_.Name]) {
917
+ $result[$_.Name] = @{ scope = $acc.BaseName; enabled = $_.Value }
918
+ } elseif ($result[$_.Name].scope -eq "shared (all accounts)") {
919
+ # already shared, skip
920
+ } else {
921
+ $result[$_.Name].scope += ", $($acc.BaseName)"
922
+ }
923
+ }
924
+ }
925
+ }
926
+ return $result
927
+ }
928
+
929
+ # ─── MARKETPLACE MANAGEMENT ───────────────────────────────────────────────────
930
+
931
+ function Manage-Marketplaces {
932
+ while ($true) {
933
+ Show-Header
934
+ Write-Host "MARKETPLACE MANAGEMENT" -ForegroundColor Magenta
935
+ Write-Host ""
936
+ $allMkts = Get-AllMarketplaceNames
937
+ if ($allMkts.Count -eq 0) {
938
+ Write-Host " No marketplaces found." -ForegroundColor Yellow
939
+ } else {
940
+ Write-Host " Known Marketplaces:" -ForegroundColor Cyan
941
+ foreach ($kv in $allMkts.GetEnumerator()) {
942
+ $tag = if ($kv.Value -eq "shared") { "[ALL ACCOUNTS]" } else { "[$($kv.Value)]" }
943
+ Write-Host " - $($kv.Key) $tag" -ForegroundColor White
944
+ }
945
+ }
946
+ Write-Host ""
947
+ Write-Host "======================================" -ForegroundColor Cyan
948
+ Write-Host " 1. Add marketplace globally " -ForegroundColor White
949
+ Write-Host " 2. Add marketplace to one account " -ForegroundColor White
950
+ Write-Host " 3. Remove global marketplace " -ForegroundColor White
951
+ Write-Host " 4. Sync marketplace indexes now " -ForegroundColor White
952
+ Write-Host " 5. Pull indexes from accounts " -ForegroundColor White
953
+ Write-Host " 0. Back " -ForegroundColor White
954
+ Write-Host "======================================" -ForegroundColor Cyan
955
+ Write-Host ""
956
+
957
+ $choice = Read-Host " Pick an option"
958
+ switch ($choice) {
959
+ "1" {
960
+ Show-Header
961
+ Write-Host "ADD GLOBAL MARKETPLACE" -ForegroundColor Green
962
+ Write-Host ""
963
+ Write-Host " This marketplace will be available to ALL accounts." -ForegroundColor Gray
964
+ Write-Host ""
965
+ $mktName = (Read-Host " Marketplace name (e.g. my-plugins)").Trim()
966
+ if ([string]::IsNullOrEmpty($mktName)) { Write-Host " Cancelled." -ForegroundColor Gray; pause; break }
967
+ Write-Host " Source type: [1] GitHub repo [2] URL" -ForegroundColor Gray
968
+ $srcChoice = Read-Host " Pick"
969
+ $s = Read-SharedSettings
970
+ if (-not $s.PSObject.Properties['extraKnownMarketplaces']) {
971
+ $s | Add-Member -MemberType NoteProperty -Name 'extraKnownMarketplaces' -Value ([PSCustomObject]@{})
972
+ }
973
+ if ($srcChoice -eq "1") {
974
+ $repo = (Read-Host " GitHub repo (owner/repo)").Trim()
975
+ if ([string]::IsNullOrEmpty($repo)) { Write-Host " Cancelled." -ForegroundColor Gray; pause; break }
976
+ $entry = [PSCustomObject]@{ source = [PSCustomObject]@{ source = "github"; repo = $repo } }
977
+ $s.extraKnownMarketplaces | Add-Member -MemberType NoteProperty -Name $mktName -Value $entry -Force
978
+ } elseif ($srcChoice -eq "2") {
979
+ $url = (Read-Host " URL").Trim()
980
+ if ([string]::IsNullOrEmpty($url)) { Write-Host " Cancelled." -ForegroundColor Gray; pause; break }
981
+ $entry = [PSCustomObject]@{ source = [PSCustomObject]@{ source = "url"; url = $url } }
982
+ $s.extraKnownMarketplaces | Add-Member -MemberType NoteProperty -Name $mktName -Value $entry -Force
983
+ } else { Write-Host " Invalid." -ForegroundColor Red; pause; break }
984
+ Write-SharedSettings $s
985
+ Write-Host ""
986
+ Write-Host " Added '$mktName' globally. Run sync to push to all accounts." -ForegroundColor Green
987
+ pause
988
+ }
989
+ "2" {
990
+ Show-Header
991
+ Write-Host "ADD MARKETPLACE TO ONE ACCOUNT" -ForegroundColor Green
992
+ Write-Host ""
993
+ Show-Accounts | Out-Null
994
+ $accs = Pick-Accounts " Pick account" $true
995
+ if ($null -eq $accs) { pause; break }
996
+ $acc = $accs[0]
997
+ $mktName = (Read-Host " Marketplace name").Trim()
998
+ Write-Host " Source: [1] GitHub [2] URL" -ForegroundColor Gray
999
+ $srcChoice = Read-Host " Pick"
1000
+ $as = Read-AccountSettings $acc.BaseName
1001
+ if (-not $as.PSObject.Properties['extraKnownMarketplaces']) {
1002
+ $as | Add-Member -MemberType NoteProperty -Name 'extraKnownMarketplaces' -Value ([PSCustomObject]@{})
1003
+ }
1004
+ if ($srcChoice -eq "1") {
1005
+ $repo = (Read-Host " GitHub repo (owner/repo)").Trim()
1006
+ $entry = [PSCustomObject]@{ source = [PSCustomObject]@{ source = "github"; repo = $repo } }
1007
+ $as.extraKnownMarketplaces | Add-Member -MemberType NoteProperty -Name $mktName -Value $entry -Force
1008
+ } elseif ($srcChoice -eq "2") {
1009
+ $url = (Read-Host " URL").Trim()
1010
+ $entry = [PSCustomObject]@{ source = [PSCustomObject]@{ source = "url"; url = $url } }
1011
+ $as.extraKnownMarketplaces | Add-Member -MemberType NoteProperty -Name $mktName -Value $entry -Force
1012
+ } else { Write-Host " Invalid." -ForegroundColor Red; pause; break }
1013
+ Write-AccountSettings $acc.BaseName $as
1014
+ Write-Host " Added '$mktName' to $($acc.BaseName)." -ForegroundColor Green
1015
+ pause
1016
+ }
1017
+ "3" {
1018
+ Show-Header
1019
+ Write-Host "REMOVE GLOBAL MARKETPLACE" -ForegroundColor Red
1020
+ Write-Host ""
1021
+ $s = Read-SharedSettings
1022
+ if (-not $s.PSObject.Properties['extraKnownMarketplaces'] -or
1023
+ ($s.extraKnownMarketplaces.PSObject.Properties | Measure-Object).Count -eq 0) {
1024
+ Write-Host " No global marketplaces configured." -ForegroundColor Yellow
1025
+ pause; break
1026
+ }
1027
+ $mkts = $s.extraKnownMarketplaces.PSObject.Properties | ForEach-Object { $_.Name }
1028
+ $i = 1; $mkts | ForEach-Object { Write-Host " $i. $_" -ForegroundColor White; $i++ }
1029
+ Write-Host ""
1030
+ $pick = [int](Read-Host " Pick number") - 1
1031
+ if ($pick -lt 0 -or $pick -ge $mkts.Count) { Write-Host " Invalid." -ForegroundColor Red; pause; break }
1032
+ $toRemove = $mkts[$pick]
1033
+ $s.extraKnownMarketplaces.PSObject.Properties.Remove($toRemove)
1034
+ Write-SharedSettings $s
1035
+ Write-Host " Removed '$toRemove' from global marketplaces." -ForegroundColor Green
1036
+ pause
1037
+ }
1038
+ "4" {
1039
+ Write-Host ""
1040
+ Write-Host " Syncing marketplace indexes to all accounts..." -ForegroundColor Cyan
1041
+ Sync-AllAccounts
1042
+ Write-Host " Done." -ForegroundColor Green
1043
+ pause
1044
+ }
1045
+ "5" {
1046
+ Show-Header
1047
+ Write-Host "PULL MARKETPLACE INDEXES FROM ACCOUNTS" -ForegroundColor Cyan
1048
+ Write-Host ""
1049
+ Write-Host " This copies downloaded marketplace indexes into the shared dir" -ForegroundColor Gray
1050
+ Write-Host " so they can be distributed to other accounts without re-downloading." -ForegroundColor Gray
1051
+ Write-Host ""
1052
+ $pulled = 0
1053
+ (Get-AllMarketplaceNames).Keys | ForEach-Object {
1054
+ $ok = Pull-MarketplaceToShared $_
1055
+ if ($ok) { Write-Host " Pulled: $_" -ForegroundColor Green; $pulled++ }
1056
+ else { Write-Host " Not found locally: $_" -ForegroundColor Yellow }
1057
+ }
1058
+ if ($pulled -eq 0) { Write-Host " Nothing pulled." -ForegroundColor Yellow }
1059
+ pause
1060
+ }
1061
+ "0" { return }
1062
+ default { Write-Host " Invalid option." -ForegroundColor Red; Start-Sleep 1 }
1063
+ }
1064
+ }
1065
+ }
1066
+
1067
+ # ─── PLUGIN MANAGEMENT ────────────────────────────────────────────────────────
1068
+
1069
+ function Manage-Plugins {
1070
+ while ($true) {
1071
+ Show-Header
1072
+ Write-Host "PLUGINS & MARKETPLACE" -ForegroundColor Magenta
1073
+ Write-Host ""
1074
+
1075
+ # Show summary
1076
+ $allPlugins = Get-AllEnabledPlugins
1077
+ $sharedCount = ($allPlugins.Values | Where-Object { $_.scope -eq "shared (all accounts)" } | Measure-Object).Count
1078
+ $specificCount = $allPlugins.Count - $sharedCount
1079
+ Write-Host " Enabled Plugins:" -ForegroundColor Cyan
1080
+ Write-Host " Universal (all accounts) : $sharedCount" -ForegroundColor White
1081
+ Write-Host " Account-specific : $specificCount" -ForegroundColor White
1082
+ Write-Host ""
1083
+ if ($allPlugins.Count -gt 0) {
1084
+ foreach ($kv in ($allPlugins.GetEnumerator() | Sort-Object Key)) {
1085
+ $tag = if ($kv.Value.scope -eq "shared (all accounts)") { "[ALL]" } else { "[$($kv.Value.scope)]" }
1086
+ $color = if ($kv.Value.scope -eq "shared (all accounts)") { "Green" } else { "White" }
1087
+ Write-Host " $($kv.Key) $tag" -ForegroundColor $color
1088
+ }
1089
+ Write-Host ""
1090
+ }
1091
+
1092
+ Write-Host "======================================" -ForegroundColor Cyan
1093
+ Write-Host " 1. Enable plugin for ALL accounts " -ForegroundColor Green
1094
+ Write-Host " 2. Enable plugin for one account " -ForegroundColor White
1095
+ Write-Host " 3. Disable plugin (shared) " -ForegroundColor White
1096
+ Write-Host " 4. Disable plugin (one account) " -ForegroundColor White
1097
+ Write-Host " 5. Browse marketplace plugins " -ForegroundColor Cyan
1098
+ Write-Host " 6. Marketplace Management " -ForegroundColor Yellow
1099
+ Write-Host " 0. Back " -ForegroundColor White
1100
+ Write-Host "======================================" -ForegroundColor Cyan
1101
+ Write-Host ""
1102
+
1103
+ $choice = Read-Host " Pick an option"
1104
+ switch ($choice) {
1105
+ "1" {
1106
+ Show-Header
1107
+ Write-Host "ENABLE PLUGIN FOR ALL ACCOUNTS" -ForegroundColor Green
1108
+ Write-Host ""
1109
+ Write-Host " Format: plugin-name@marketplace-name" -ForegroundColor Gray
1110
+ Write-Host " Example: frontend-design@claude-plugins-official" -ForegroundColor Gray
1111
+ Write-Host ""
1112
+
1113
+ # Offer to browse if marketplaces available
1114
+ $allMkts = Get-AllMarketplaceNames
1115
+ if ($allMkts.Count -gt 0) {
1116
+ Write-Host " Or pick from marketplace:" -ForegroundColor Cyan
1117
+ $mktList = @($allMkts.Keys)
1118
+ $i = 1; $mktList | ForEach-Object { Write-Host " $i. $_" -ForegroundColor White; $i++ }
1119
+ Write-Host " 0. Enter manually" -ForegroundColor Gray
1120
+ Write-Host ""
1121
+ $mktPick = Read-Host " Marketplace (0 to enter manually)"
1122
+ if ($mktPick -ne "0" -and $mktPick -match '^\d+$') {
1123
+ $mktIdx = [int]$mktPick - 1
1124
+ if ($mktIdx -ge 0 -and $mktIdx -lt $mktList.Count) {
1125
+ $selectedMkt = $mktList[$mktIdx]
1126
+ $available = Get-MarketplacePlugins $selectedMkt
1127
+ if ($available) {
1128
+ Show-Header
1129
+ Write-Host "PLUGINS IN $selectedMkt" -ForegroundColor Cyan
1130
+ Write-Host ""
1131
+ $allAvail = @()
1132
+ if ($available.plugins.Count -gt 0) {
1133
+ Write-Host " Official:" -ForegroundColor Green
1134
+ $available.plugins | ForEach-Object { Write-Host " - $_" -ForegroundColor White; $allAvail += "$_@$selectedMkt" }
1135
+ }
1136
+ if ($available.external.Count -gt 0) {
1137
+ Write-Host " External:" -ForegroundColor Yellow
1138
+ $available.external | ForEach-Object { Write-Host " - $_" -ForegroundColor White; $allAvail += "$_@$selectedMkt" }
1139
+ }
1140
+ Write-Host ""
1141
+ $pName = (Read-Host " Enter plugin name (without @marketplace)").Trim()
1142
+ if ([string]::IsNullOrEmpty($pName)) { pause; break }
1143
+ $pluginKey = "$pName@$selectedMkt"
1144
+ } else {
1145
+ Write-Host " Marketplace index not downloaded yet. Enter manually." -ForegroundColor Yellow
1146
+ $pluginKey = (Read-Host " Plugin key (name@marketplace)").Trim()
1147
+ }
1148
+ } else { $pluginKey = (Read-Host " Plugin key (name@marketplace)").Trim() }
1149
+ } else { $pluginKey = (Read-Host " Plugin key (name@marketplace)").Trim() }
1150
+ } else {
1151
+ $pluginKey = (Read-Host " Plugin key (name@marketplace)").Trim()
1152
+ }
1153
+
1154
+ if ([string]::IsNullOrEmpty($pluginKey)) { Write-Host " Cancelled." -ForegroundColor Gray; pause; break }
1155
+ $s = Read-SharedSettings
1156
+ if (-not $s.PSObject.Properties['enabledPlugins']) {
1157
+ $s | Add-Member -MemberType NoteProperty -Name 'enabledPlugins' -Value ([PSCustomObject]@{})
1158
+ }
1159
+ $s.enabledPlugins | Add-Member -MemberType NoteProperty -Name $pluginKey -Value $true -Force
1160
+ Write-SharedSettings $s
1161
+ Write-Host ""
1162
+ Write-Host " '$pluginKey' enabled for ALL accounts." -ForegroundColor Green
1163
+ Write-Host " Syncing to all accounts now..." -ForegroundColor Cyan
1164
+ Sync-AllAccounts
1165
+ Write-Host " Done. Launch any account to activate." -ForegroundColor Green
1166
+ pause
1167
+ }
1168
+ "2" {
1169
+ Show-Header
1170
+ Write-Host "ENABLE PLUGIN FOR ONE ACCOUNT" -ForegroundColor Green
1171
+ Write-Host ""
1172
+ Show-Accounts | Out-Null
1173
+ $accs = Pick-Accounts " Pick account" $true
1174
+ if ($null -eq $accs) { pause; break }
1175
+ $acc = $accs[0]
1176
+ Write-Host ""
1177
+ $pluginKey = (Read-Host " Plugin key (name@marketplace)").Trim()
1178
+ if ([string]::IsNullOrEmpty($pluginKey)) { Write-Host " Cancelled." -ForegroundColor Gray; pause; break }
1179
+ $as = Read-AccountSettings $acc.BaseName
1180
+ if (-not $as.PSObject.Properties['enabledPlugins']) {
1181
+ $as | Add-Member -MemberType NoteProperty -Name 'enabledPlugins' -Value ([PSCustomObject]@{})
1182
+ }
1183
+ $as.enabledPlugins | Add-Member -MemberType NoteProperty -Name $pluginKey -Value $true -Force
1184
+ Write-AccountSettings $acc.BaseName $as
1185
+ Write-Host " '$pluginKey' enabled for $($acc.BaseName)." -ForegroundColor Green
1186
+ pause
1187
+ }
1188
+ "3" {
1189
+ Show-Header
1190
+ Write-Host "DISABLE PLUGIN (SHARED)" -ForegroundColor Red
1191
+ Write-Host ""
1192
+ $s = Read-SharedSettings
1193
+ if (-not $s.PSObject.Properties['enabledPlugins'] -or
1194
+ ($s.enabledPlugins.PSObject.Properties | Measure-Object).Count -eq 0) {
1195
+ Write-Host " No shared plugins configured." -ForegroundColor Yellow
1196
+ pause; break
1197
+ }
1198
+ $keys = @($s.enabledPlugins.PSObject.Properties | ForEach-Object { $_.Name })
1199
+ $i = 1; $keys | ForEach-Object { Write-Host " $i. $_" -ForegroundColor White; $i++ }
1200
+ Write-Host ""
1201
+ $pick = [int](Read-Host " Pick number to disable") - 1
1202
+ if ($pick -lt 0 -or $pick -ge $keys.Count) { Write-Host " Invalid." -ForegroundColor Red; pause; break }
1203
+ $toRemove = $keys[$pick]
1204
+ $s.enabledPlugins.PSObject.Properties.Remove($toRemove)
1205
+ Write-SharedSettings $s
1206
+ Write-Host " '$toRemove' removed from shared. Sync to propagate." -ForegroundColor Green
1207
+ $doSync = Read-Host " Sync to all accounts now? (y/n)"
1208
+ if ($doSync -eq "y") { Sync-AllAccounts; Write-Host " Synced." -ForegroundColor Green }
1209
+ pause
1210
+ }
1211
+ "4" {
1212
+ Show-Header
1213
+ Write-Host "DISABLE PLUGIN (ONE ACCOUNT)" -ForegroundColor Red
1214
+ Write-Host ""
1215
+ Show-Accounts | Out-Null
1216
+ $accs = Pick-Accounts " Pick account" $true
1217
+ if ($null -eq $accs) { pause; break }
1218
+ $acc = $accs[0]
1219
+ $as = Read-AccountSettings $acc.BaseName
1220
+ if (-not $as.PSObject.Properties['enabledPlugins'] -or
1221
+ ($as.enabledPlugins.PSObject.Properties | Measure-Object).Count -eq 0) {
1222
+ Write-Host " No plugins configured for $($acc.BaseName)." -ForegroundColor Yellow
1223
+ pause; break
1224
+ }
1225
+ $keys = @($as.enabledPlugins.PSObject.Properties | ForEach-Object { $_.Name })
1226
+ $i = 1; $keys | ForEach-Object { Write-Host " $i. $_" -ForegroundColor White; $i++ }
1227
+ Write-Host ""
1228
+ $pick = [int](Read-Host " Pick number") - 1
1229
+ if ($pick -lt 0 -or $pick -ge $keys.Count) { Write-Host " Invalid." -ForegroundColor Red; pause; break }
1230
+ $toRemove = $keys[$pick]
1231
+ $as.enabledPlugins.PSObject.Properties.Remove($toRemove)
1232
+ Write-AccountSettings $acc.BaseName $as
1233
+ Write-Host " '$toRemove' disabled for $($acc.BaseName)." -ForegroundColor Green
1234
+ pause
1235
+ }
1236
+ "5" {
1237
+ Show-Header
1238
+ Write-Host "BROWSE MARKETPLACE PLUGINS" -ForegroundColor Cyan
1239
+ Write-Host ""
1240
+ $allMkts = Get-AllMarketplaceNames
1241
+ if ($allMkts.Count -eq 0) {
1242
+ Write-Host " No marketplaces found. Add one via Marketplace Management." -ForegroundColor Yellow
1243
+ pause; break
1244
+ }
1245
+ $mktList = @($allMkts.Keys)
1246
+ $i = 1; $mktList | ForEach-Object { Write-Host " $i. $_" -ForegroundColor White; $i++ }
1247
+ Write-Host ""
1248
+ $pick = [int](Read-Host " Pick marketplace") - 1
1249
+ if ($pick -lt 0 -or $pick -ge $mktList.Count) { Write-Host " Invalid." -ForegroundColor Red; pause; break }
1250
+ $selectedMkt = $mktList[$pick]
1251
+ $available = Get-MarketplacePlugins $selectedMkt
1252
+ Show-Header
1253
+ Write-Host "PLUGINS IN $selectedMkt" -ForegroundColor Cyan
1254
+ Write-Host ""
1255
+ if ($null -eq $available) {
1256
+ Write-Host " Index not downloaded. Launch an account with this marketplace configured to download it," -ForegroundColor Yellow
1257
+ Write-Host " then use option 5 in Marketplace Management to pull indexes to shared." -ForegroundColor Yellow
1258
+ } else {
1259
+ if ($available.plugins.Count -gt 0) {
1260
+ Write-Host " Official plugins:" -ForegroundColor Green
1261
+ $available.plugins | ForEach-Object { Write-Host " - $_" -ForegroundColor White }
1262
+ }
1263
+ if ($available.external.Count -gt 0) {
1264
+ Write-Host " External/3rd-party:" -ForegroundColor Yellow
1265
+ $available.external | ForEach-Object { Write-Host " - $_" -ForegroundColor White }
1266
+ }
1267
+ }
1268
+ Write-Host ""
1269
+ pause
1270
+ }
1271
+ "6" { Manage-Marketplaces }
1272
+ "0" { return }
1273
+ default { Write-Host " Invalid option." -ForegroundColor Red; Start-Sleep 1 }
1274
+ }
1275
+ }
1276
+ }
1277
+
1278
+ # ─── MAIN MENU ───────────────────────────────────────────────────────────────
1279
+
1280
+ function Show-Menu {
1281
+ Show-Header
1282
+ Write-Host " Current Accounts:" -ForegroundColor Cyan
1283
+ Write-Host ""
1284
+ Show-Accounts | Out-Null
1285
+ Write-Host ""
1286
+ Write-Host "======================================" -ForegroundColor Cyan
1287
+ Write-Host " 1. List Accounts " -ForegroundColor White
1288
+ Write-Host " 2. Create New Account " -ForegroundColor White
1289
+ Write-Host " 3. Launch Account " -ForegroundColor White
1290
+ Write-Host " 4. Rename Account " -ForegroundColor White
1291
+ Write-Host " 5. Delete Account " -ForegroundColor White
1292
+ Write-Host " 6. Backup Sessions " -ForegroundColor White
1293
+ Write-Host " 7. Restore Sessions " -ForegroundColor White
1294
+ Write-Host " 8. Shared Settings (MCP/Skills) " -ForegroundColor Yellow
1295
+ Write-Host " 9. Plugins & Marketplace " -ForegroundColor Magenta
1296
+ Write-Host " E. Export Profile (Pair Code) " -ForegroundColor Green
1297
+ Write-Host " I. Import Profile (Pair Code) " -ForegroundColor Green
1298
+ Write-Host " 0. Exit " -ForegroundColor White
1299
+ Write-Host "======================================" -ForegroundColor Cyan
1300
+ Write-Host ""
1301
+ }
1302
+
1303
+ while ($true) {
1304
+ Show-Menu
1305
+ $choice = Read-Host " Pick an option"
1306
+ switch ($choice) {
1307
+ "1" { Show-Header; Write-Host "All Accounts:`n" -ForegroundColor Cyan; Show-Accounts | Out-Null; Write-Host ""; pause }
1308
+ "2" { Create-Account }
1309
+ "3" { Launch-Account }
1310
+ "4" { Rename-Account }
1311
+ "5" { Delete-Account }
1312
+ "6" { Backup-Sessions }
1313
+ "7" { Restore-Sessions }
1314
+ "8" { Manage-SharedSettings }
1315
+ "9" { Manage-Plugins }
1316
+ "e" { Pair-Export }
1317
+ "E" { Pair-Export }
1318
+ "i" { Pair-Import }
1319
+ "I" { Pair-Import }
1320
+ "0" { Clear-Host; Write-Host "Bye!" -ForegroundColor Cyan; break }
1321
+ default { Write-Host " Invalid option." -ForegroundColor Red; Start-Sleep 1 }
1322
+ }
1323
+ if ($choice -eq "0") { break }
1324
+ }