claude-duo 2.0.0 → 2.1.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.
package/ClaudeDuo.ps1 CHANGED
@@ -44,14 +44,40 @@ function Find-WindowsTerminal {
44
44
  return $null
45
45
  }
46
46
 
47
+ function Get-AccountCatalog {
48
+ return [ordered]@{
49
+ default = @{ Label = 'Personal (default ~/.claude)'; Dir = 'default' }
50
+ account2 = @{ Label = 'Work (account 2)'; Dir = (Join-Path $env:USERPROFILE '.claude-account-2') }
51
+ account3 = @{ Label = 'Other (account 3)'; Dir = (Join-Path $env:USERPROFILE '.claude-account-3') }
52
+ }
53
+ }
54
+
55
+ function Resolve-AccountDir {
56
+ param([string]$AccountId)
57
+ $catalog = Get-AccountCatalog
58
+ if (-not $catalog.Contains($AccountId)) { return 'default' }
59
+ return [string]$catalog[$AccountId].Dir
60
+ }
61
+
62
+ function Resolve-AccountLabel {
63
+ param([string]$AccountId)
64
+ $catalog = Get-AccountCatalog
65
+ if (-not $catalog.Contains($AccountId)) { return 'Personal' }
66
+ $label = [string]$catalog[$AccountId].Label
67
+ if ($label -match '^([^(]+)') { return $matches[1].Trim() }
68
+ return $label
69
+ }
70
+
47
71
  function Read-Config {
48
72
  $defaults = [ordered]@{
49
73
  LeftFolder = [Environment]::GetFolderPath('MyDocuments')
50
74
  RightFolder = [Environment]::GetFolderPath('MyDocuments')
51
- SameFolder = $true
52
- Split = 'vertical'
53
- Maximized = $true
54
- SecondAccount = $true
75
+ SameFolder = $true
76
+ Split = 'vertical'
77
+ Maximized = $true
78
+ LeftAccount = 'default'
79
+ RightAccount = 'account2'
80
+ SecondAccount = $true
55
81
  }
56
82
  if (-not (Test-Path -LiteralPath $ConfigPath)) { return $defaults }
57
83
  try {
@@ -59,6 +85,10 @@ function Read-Config {
59
85
  foreach ($key in @($defaults.Keys)) {
60
86
  if ($null -ne $raw.$key) { $defaults[$key] = $raw.$key }
61
87
  }
88
+ if ($null -eq $raw.LeftAccount -and $null -ne $raw.SecondAccount) {
89
+ $defaults.LeftAccount = 'default'
90
+ $defaults.RightAccount = if ([bool]$raw.SecondAccount) { 'account2' } else { 'default' }
91
+ }
62
92
  } catch { }
63
93
  return $defaults
64
94
  }
@@ -68,7 +98,47 @@ function Save-Config {
68
98
  if (-not (Test-Path -LiteralPath $ConfigDir)) {
69
99
  New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null
70
100
  }
71
- ($Config | ConvertTo-Json) | Set-Content -LiteralPath $ConfigPath -Encoding UTF8
101
+ ($Config | ConvertTo-Json -Depth 4) | Set-Content -LiteralPath $ConfigPath -Encoding UTF8
102
+ }
103
+
104
+ function Get-GuiConfigSnapshot {
105
+ param($Ui)
106
+ $leftAccount = if ($Ui.LeftAccount.SelectedItem) { [string]$Ui.LeftAccount.SelectedItem.Tag } else { 'default' }
107
+ $rightAccount = if ($Ui.RightAccount.SelectedItem) { [string]$Ui.RightAccount.SelectedItem.Tag } else { 'account2' }
108
+ return @{
109
+ LeftFolder = $Ui.Left.Text.Trim()
110
+ RightFolder = $Ui.Right.Text.Trim()
111
+ SameFolder = [bool]$Ui.Same.Checked
112
+ Split = if ($Ui.Stack.Checked) { 'horizontal' } else { 'vertical' }
113
+ Maximized = [bool]$Ui.Max.Checked
114
+ LeftAccount = $leftAccount
115
+ RightAccount = $rightAccount
116
+ SecondAccount = ($rightAccount -ne 'default')
117
+ }
118
+ }
119
+
120
+ function Select-AccountComboItem {
121
+ param($Combo, [string]$AccountId)
122
+ foreach ($item in $Combo.Items) {
123
+ if ([string]$item.Tag -eq $AccountId) {
124
+ $Combo.SelectedItem = $item
125
+ return
126
+ }
127
+ }
128
+ if ($Combo.Items.Count -gt 0) { $Combo.SelectedIndex = 0 }
129
+ }
130
+
131
+ function Build-AccountLaunchCmd {
132
+ param([string]$AccountId)
133
+ $launcher = Join-Path $ScriptDir 'Run-Claude-Account.cmd'
134
+ $accountDir = Resolve-AccountDir $AccountId
135
+ $label = (Resolve-AccountLabel $AccountId) -replace '\s+', '-'
136
+ $quotedLauncher = ConvertTo-WtQuoted $launcher
137
+ if ($accountDir -eq 'default') {
138
+ return "cmd.exe /k $quotedLauncher default $label"
139
+ }
140
+ $quotedDir = ConvertTo-WtQuoted $accountDir
141
+ return "cmd.exe /k $quotedLauncher $quotedDir $label"
72
142
  }
73
143
 
74
144
  function Show-Alert {
@@ -93,7 +163,8 @@ function Start-ClaudeDuo {
93
163
  [string]$RightFolder,
94
164
  [string]$SplitMode,
95
165
  [bool]$MaximizeWindow,
96
- [bool]$SecondAccount
166
+ [string]$LeftAccount = 'default',
167
+ [string]$RightAccount = 'account2'
97
168
  )
98
169
 
99
170
  $wt = Find-WindowsTerminal
@@ -112,21 +183,52 @@ function Start-ClaudeDuo {
112
183
  throw "Right folder does not exist:`n$RightFolder"
113
184
  }
114
185
 
115
- # Start-Process joins -ArgumentList with spaces and does not quote.
116
- # Titles must not contain spaces, and -d paths with spaces must be quoted.
186
+ if ($LeftAccount -eq $RightAccount) {
187
+ throw "Left and right panes use the same account.`nPick different accounts in the dropdowns, or use 'Open one account' below."
188
+ }
189
+
117
190
  $splitFlag = if ($SplitMode -eq 'horizontal') { '-H' } else { '-V' }
118
191
  $leftDir = ConvertTo-WtQuoted $LeftFolder
119
192
  $rightDir = ConvertTo-WtQuoted $RightFolder
120
- $leftCmd = ConvertTo-WtQuoted (Join-Path $ScriptDir 'Run-Claude-A.cmd')
121
- $rightLauncher = if ($SecondAccount) { 'Run-Claude-B.cmd' } else { 'Run-Claude-A.cmd' }
122
- $rightCmd = ConvertTo-WtQuoted (Join-Path $ScriptDir $rightLauncher)
193
+ $leftCmd = Build-AccountLaunchCmd $LeftAccount
194
+ $rightCmd = Build-AccountLaunchCmd $RightAccount
195
+ $leftTitle = (Resolve-AccountLabel $LeftAccount) -replace '\s+', '-'
196
+ $rightTitle = (Resolve-AccountLabel $RightAccount) -replace '\s+', '-'
123
197
 
124
198
  $parts = New-Object System.Collections.Generic.List[string]
125
199
  $parts.Add('--window new')
126
200
  if ($MaximizeWindow) { $parts.Add('--maximized') }
127
- $parts.Add("new-tab --title Claude-A --suppressApplicationTitle -d $leftDir cmd.exe /k $leftCmd")
201
+ $parts.Add("new-tab --title Claude-$leftTitle --suppressApplicationTitle -d $leftDir $leftCmd")
128
202
  $parts.Add(';')
129
- $parts.Add("split-pane $splitFlag -s 0.5 --title Claude-B --suppressApplicationTitle -d $rightDir cmd.exe /k $rightCmd")
203
+ $parts.Add("split-pane $splitFlag -s 0.5 --title Claude-$rightTitle --suppressApplicationTitle -d $rightDir $rightCmd")
204
+ $arguments = ($parts -join ' ')
205
+
206
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
207
+ $psi.FileName = $wt
208
+ $psi.Arguments = $arguments
209
+ $psi.UseShellExecute = $true
210
+ [void][System.Diagnostics.Process]::Start($psi)
211
+ }
212
+
213
+ function Start-SingleClaudeAccount {
214
+ param(
215
+ [string]$Folder,
216
+ [string]$AccountId,
217
+ [bool]$MaximizeWindow = $true
218
+ )
219
+
220
+ $wt = Find-WindowsTerminal
221
+ $claude = Find-ClaudeCmd
222
+ if (-not $wt) { throw "Windows Terminal was not found." }
223
+ if (-not $claude) { throw "Claude Code was not found." }
224
+ if (-not (Test-Path -LiteralPath $Folder)) { throw "Folder does not exist:`n$Folder" }
225
+
226
+ $dir = ConvertTo-WtQuoted $Folder
227
+ $cmd = Build-AccountLaunchCmd $AccountId
228
+ $title = (Resolve-AccountLabel $AccountId) -replace '\s+', '-'
229
+ $parts = @('--window new')
230
+ if ($MaximizeWindow) { $parts += '--maximized' }
231
+ $parts += "new-tab --title Claude-$title --suppressApplicationTitle -d $dir $cmd"
130
232
  $arguments = ($parts -join ' ')
131
233
 
132
234
  $psi = New-Object System.Diagnostics.ProcessStartInfo
@@ -192,6 +294,28 @@ function New-Button {
192
294
  return $btn
193
295
  }
194
296
 
297
+ function New-ComboBox {
298
+ param($X, $Y, $Width = 420)
299
+ $combo = New-Object System.Windows.Forms.ComboBox
300
+ $combo.Location = New-Object System.Drawing.Point($X, $Y)
301
+ $combo.Size = New-Object System.Drawing.Size($Width, 28)
302
+ $combo.Font = New-Object System.Drawing.Font('Segoe UI', 9)
303
+ $combo.DropDownStyle = 'DropDownList'
304
+ $combo.BackColor = [System.Drawing.ColorTranslator]::FromHtml('#2A2622')
305
+ $combo.ForeColor = [System.Drawing.ColorTranslator]::FromHtml('#F4EFE8')
306
+ $combo.FlatStyle = 'Flat'
307
+ return $combo
308
+ }
309
+
310
+ function Populate-AccountCombo {
311
+ param($Combo)
312
+ $Combo.Items.Clear()
313
+ foreach ($entry in (Get-AccountCatalog).GetEnumerator()) {
314
+ [void]$Combo.Items.Add([PSCustomObject]@{ Tag = $entry.Key; Text = $entry.Value.Label })
315
+ }
316
+ $Combo.DisplayMember = 'Text'
317
+ }
318
+
195
319
  function Show-Gui {
196
320
  $config = Read-Config
197
321
  $bg = [System.Drawing.ColorTranslator]::FromHtml('#161412')
@@ -199,7 +323,7 @@ function Show-Gui {
199
323
 
200
324
  $form = New-Object System.Windows.Forms.Form
201
325
  $form.Text = $AppName
202
- $form.Size = New-Object System.Drawing.Size(520, 540)
326
+ $form.Size = New-Object System.Drawing.Size(520, 640)
203
327
  $form.StartPosition = 'CenterScreen'
204
328
  $form.FormBorderStyle = 'FixedSingle'
205
329
  $form.MaximizeBox = $false
@@ -208,7 +332,7 @@ function Show-Gui {
208
332
  $form.Font = New-Object System.Drawing.Font('Segoe UI', 9)
209
333
 
210
334
  $form.Controls.Add((New-Label $AppName 24 18 460 32 '#F4EFE8' 18 $true))
211
- $form.Controls.Add((New-Label 'Two Claude Code sessions in one window.' 24 52 460 22 '#B7A99A' 9))
335
+ $form.Controls.Add((New-Label 'Two Claude Code sessions pick a different account per pane.' 24 52 460 22 '#B7A99A' 9))
212
336
 
213
337
  $folderPanel = New-Object System.Windows.Forms.Panel
214
338
  $folderPanel.Location = New-Object System.Drawing.Point(20, 88)
@@ -238,9 +362,28 @@ function Show-Gui {
238
362
  $chkSame.Checked = [bool]$config.SameFolder
239
363
  $folderPanel.Controls.Add($chkSame)
240
364
 
365
+ $accountPanel = New-Object System.Windows.Forms.Panel
366
+ $accountPanel.Location = New-Object System.Drawing.Point(20, 268)
367
+ $accountPanel.Size = New-Object System.Drawing.Size(464, 108)
368
+ $accountPanel.BackColor = $panel
369
+ $form.Controls.Add($accountPanel)
370
+
371
+ $accountPanel.Controls.Add((New-Label 'Left pane account' 16 12 300 20 '#B7A99A' 8))
372
+ $cmbLeftAccount = New-ComboBox 16 32 420
373
+ Populate-AccountCombo $cmbLeftAccount
374
+ Select-AccountComboItem $cmbLeftAccount ([string]$config.LeftAccount)
375
+ $accountPanel.Controls.Add($cmbLeftAccount)
376
+
377
+ $accountPanel.Controls.Add((New-Label 'Right pane account' 16 58 300 20 '#B7A99A' 8))
378
+ $cmbRightAccount = New-ComboBox 16 78 420
379
+ Populate-AccountCombo $cmbRightAccount
380
+ $rightAccountId = if ($config.RightAccount) { [string]$config.RightAccount } elseif ([bool]$config.SecondAccount) { 'account2' } else { 'default' }
381
+ Select-AccountComboItem $cmbRightAccount $rightAccountId
382
+ $accountPanel.Controls.Add($cmbRightAccount)
383
+
241
384
  $optPanel = New-Object System.Windows.Forms.Panel
242
- $optPanel.Location = New-Object System.Drawing.Point(20, 268)
243
- $optPanel.Size = New-Object System.Drawing.Size(464, 92)
385
+ $optPanel.Location = New-Object System.Drawing.Point(20, 388)
386
+ $optPanel.Size = New-Object System.Drawing.Size(464, 72)
244
387
  $optPanel.BackColor = $panel
245
388
  $form.Controls.Add($optPanel)
246
389
 
@@ -268,40 +411,45 @@ function Show-Gui {
268
411
  $chkMax.Checked = [bool]$config.Maximized
269
412
  $optPanel.Controls.Add($chkMax)
270
413
 
271
- $chkSecond = New-Object System.Windows.Forms.CheckBox
272
- $chkSecond.Text = 'Right pane: second Claude account (login once)'
273
- $chkSecond.Location = New-Object System.Drawing.Point(16, 50)
274
- $chkSecond.Size = New-Object System.Drawing.Size(430, 24)
275
- $chkSecond.ForeColor = [System.Drawing.ColorTranslator]::FromHtml('#F4EFE8')
276
- if ($null -eq $config.SecondAccount) { $chkSecond.Checked = $true } else { $chkSecond.Checked = [bool]$config.SecondAccount }
277
- $optPanel.Controls.Add($chkSecond)
414
+ $optPanel.Controls.Add((New-Label 'Each account keeps its own /login. First open: type /login in that pane once.' 16 44 430 22 '#B7A99A' 8))
278
415
 
279
- $btnLaunch = New-Button 'Open 2 Claude Code' 20 388 300 44 '#D97757' '#1A120E'
416
+ $btnLaunch = New-Button 'Open 2 Claude Code' 20 476 300 44 '#D97757' '#1A120E'
280
417
  $btnLaunch.Font = New-Object System.Drawing.Font('Segoe UI', 11, [System.Drawing.FontStyle]::Bold)
281
418
  $form.Controls.Add($btnLaunch)
282
419
 
283
- $btnShortcut = New-Button 'Pin to Desktop' 328 388 156 44 '#3A342E' '#F4EFE8'
420
+ $btnShortcut = New-Button 'Pin to Desktop' 328 476 156 44 '#3A342E' '#F4EFE8'
284
421
  $form.Controls.Add($btnShortcut)
285
422
 
423
+ $btnOpenLeft = New-Button 'Open left account only' 20 532 220 36 '#3A342E' '#F4EFE8'
424
+ $form.Controls.Add($btnOpenLeft)
425
+
426
+ $btnOpenRight = New-Button 'Open right account only' 264 532 220 36 '#3A342E' '#F4EFE8'
427
+ $form.Controls.Add($btnOpenRight)
428
+
286
429
  $claudePath = Find-ClaudeCmd
287
430
  $wtPath = Find-WindowsTerminal
288
431
  $claudeOk = if ($claudePath) { 'Claude Code found' } else { 'Claude Code NOT found' }
289
432
  $wtOk = if ($wtPath) { 'Windows Terminal found' } else { 'Windows Terminal NOT found' }
290
433
  $statusColor = if ($claudePath -and $wtPath) { '#8FBF8A' } else { '#E08A6A' }
291
- $status = New-Label "$claudeOk · $wtOk" 24 444 460 22 $statusColor 8
434
+ $status = New-Label "$claudeOk · $wtOk · Settings auto-save" 24 580 460 22 $statusColor 8
292
435
  $form.Controls.Add($status)
293
436
 
294
437
  $ui = @{
295
- Left = $txtLeft
296
- Right = $txtRight
297
- BrowseB = $btnBrowseB
298
- Same = $chkSame
299
- Side = $radioSide
300
- Stack = $radioStack
301
- Max = $chkMax
302
- Second = $chkSecond
438
+ Left = $txtLeft
439
+ Right = $txtRight
440
+ BrowseB = $btnBrowseB
441
+ Same = $chkSame
442
+ Side = $radioSide
443
+ Stack = $radioStack
444
+ Max = $chkMax
445
+ LeftAccount = $cmbLeftAccount
446
+ RightAccount = $cmbRightAccount
303
447
  }
304
448
 
449
+ $persistConfig = {
450
+ try { Save-Config (Get-GuiConfigSnapshot $ui) } catch { }
451
+ }.GetNewClosure()
452
+
305
453
  $syncRight = {
306
454
  if ($ui.Same.Checked) {
307
455
  $ui.Right.Text = $ui.Left.Text
@@ -322,27 +470,49 @@ function Show-Gui {
322
470
  if ($dialog.ShowDialog() -eq 'OK') {
323
471
  $box.Text = $dialog.SelectedPath
324
472
  & $syncRight
473
+ & $persistConfig
325
474
  }
326
475
  }
327
476
 
328
477
  $btnBrowseA.Add_Click({ & $pickFolder $ui.Left }.GetNewClosure())
329
478
  $btnBrowseB.Add_Click({ & $pickFolder $ui.Right }.GetNewClosure())
330
- $chkSame.Add_CheckedChanged({ & $syncRight }.GetNewClosure())
331
- $txtLeft.Add_TextChanged({ if ($ui.Same.Checked) { $ui.Right.Text = $ui.Left.Text } }.GetNewClosure())
479
+ $chkSame.Add_CheckedChanged({ & $syncRight; & $persistConfig }.GetNewClosure())
480
+ $txtLeft.Add_TextChanged({ if ($ui.Same.Checked) { $ui.Right.Text = $ui.Left.Text }; & $persistConfig }.GetNewClosure())
481
+ $txtRight.Add_TextChanged({ & $persistConfig }.GetNewClosure())
482
+ $radioSide.Add_CheckedChanged({ if ($radioSide.Checked) { & $persistConfig } }.GetNewClosure())
483
+ $radioStack.Add_CheckedChanged({ if ($radioStack.Checked) { & $persistConfig } }.GetNewClosure())
484
+ $chkMax.Add_CheckedChanged({ & $persistConfig }.GetNewClosure())
485
+ $cmbLeftAccount.Add_SelectedIndexChanged({ & $persistConfig }.GetNewClosure())
486
+ $cmbRightAccount.Add_SelectedIndexChanged({ & $persistConfig }.GetNewClosure())
487
+ $form.Add_FormClosing({ & $persistConfig }.GetNewClosure())
332
488
  & $syncRight
489
+ & $persistConfig
333
490
 
334
491
  $btnLaunch.Add_Click({
335
- $splitMode = if ($ui.Stack.Checked) { 'horizontal' } else { 'vertical' }
336
- $cfg = @{
337
- LeftFolder = $ui.Left.Text.Trim()
338
- RightFolder = $ui.Right.Text.Trim()
339
- SameFolder = [bool]$ui.Same.Checked
340
- Split = $splitMode
341
- Maximized = [bool]$ui.Max.Checked
342
- SecondAccount = [bool]$ui.Second.Checked
492
+ $cfg = Get-GuiConfigSnapshot $ui
493
+ try {
494
+ Start-ClaudeDuo -LeftFolder $cfg.LeftFolder -RightFolder $cfg.RightFolder -SplitMode $cfg.Split -MaximizeWindow $cfg.Maximized -LeftAccount $cfg.LeftAccount -RightAccount $cfg.RightAccount
495
+ Save-Config $cfg
496
+ } catch {
497
+ Show-Alert $_.Exception.Message
498
+ }
499
+ }.GetNewClosure())
500
+
501
+ $btnOpenLeft.Add_Click({
502
+ $cfg = Get-GuiConfigSnapshot $ui
503
+ try {
504
+ Start-SingleClaudeAccount -Folder $cfg.LeftFolder -AccountId $cfg.LeftAccount -MaximizeWindow $cfg.Maximized
505
+ Save-Config $cfg
506
+ } catch {
507
+ Show-Alert $_.Exception.Message
343
508
  }
509
+ }.GetNewClosure())
510
+
511
+ $btnOpenRight.Add_Click({
512
+ $cfg = Get-GuiConfigSnapshot $ui
344
513
  try {
345
- Start-ClaudeDuo -LeftFolder $cfg.LeftFolder -RightFolder $cfg.RightFolder -SplitMode $cfg.Split -MaximizeWindow $cfg.Maximized -SecondAccount $cfg.SecondAccount
514
+ $folder = if ($cfg.SameFolder) { $cfg.LeftFolder } else { $cfg.RightFolder }
515
+ Start-SingleClaudeAccount -Folder $folder -AccountId $cfg.RightAccount -MaximizeWindow $cfg.Maximized
346
516
  Save-Config $cfg
347
517
  } catch {
348
518
  Show-Alert $_.Exception.Message
@@ -368,8 +538,9 @@ if ($NoGui) {
368
538
  $rightFolder = if ($Right) { $Right } else { if ([bool]$cfg.SameFolder) { $leftFolder } else { [string]$cfg.RightFolder } }
369
539
  $splitMode = if ($PSBoundParameters.ContainsKey('Split')) { $Split } else { [string]$cfg.Split }
370
540
  $max = if ($PSBoundParameters.ContainsKey('Maximized')) { [bool]$Maximized } else { [bool]$cfg.Maximized }
371
- $second = if ($null -eq $cfg.SecondAccount) { $true } else { [bool]$cfg.SecondAccount }
372
- Start-ClaudeDuo -LeftFolder $leftFolder -RightFolder $rightFolder -SplitMode $splitMode -MaximizeWindow $max -SecondAccount $second
541
+ $leftAccount = if ($cfg.LeftAccount) { [string]$cfg.LeftAccount } else { 'default' }
542
+ $rightAccount = if ($cfg.RightAccount) { [string]$cfg.RightAccount } elseif ([bool]$cfg.SecondAccount) { 'account2' } else { 'default' }
543
+ Start-ClaudeDuo -LeftFolder $leftFolder -RightFolder $rightFolder -SplitMode $splitMode -MaximizeWindow $max -LeftAccount $leftAccount -RightAccount $rightAccount
373
544
  } else {
374
545
  Show-Gui
375
546
  }
package/README.md CHANGED
@@ -25,7 +25,17 @@ npx claude-duo
25
25
 
26
26
  Needs Windows 10/11, [Windows Terminal](https://aka.ms/terminal), and [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
27
27
 
28
- ### From GitHub (no npm)
28
+ ### GitHub Packages
29
+
30
+ Shows on the profile **Packages** tab:
31
+
32
+ ```bash
33
+ npm install -g @mferasatali/claude-duo --registry=https://npm.pkg.github.com
34
+ ```
35
+
36
+ Most people should still use `npx claude-duo` from [npmjs](https://www.npmjs.com/package/claude-duo).
37
+
38
+ ### From GitHub (source, no registry)
29
39
 
30
40
  ```bash
31
41
  git clone https://github.com/mferasatali/claude-duo.git
@@ -36,10 +46,11 @@ Double-click **`ClaudeDuo.bat`**, or **Pin to Desktop** in the app.
36
46
  ## Use
37
47
 
38
48
  1. Pick the project folder(s).
39
- 2. Keep **Right pane: second Claude account** checked if you want two logins.
49
+ 2. Choose **Left pane account** and **Right pane account** (must be different).
40
50
  3. Click **Open 2 Claude Code**.
41
- 4. Left pane uses your default Claude login. Right pane asks you to sign in once with the second account (saved under `%USERPROFILE%\.claude-account-2`).
42
- 5. Click a pane, type a task, press Enter.
51
+ 4. Left pane uses `~/.claude` (Personal). Right pane uses its own folder (e.g. `~/.claude-account-2` for Work).
52
+ 5. **First time per account:** in that pane type `/login` once. Logins stay saved in that account's config folder.
53
+ 6. Use **Open left/right account only** to switch to a single account in a new window.
43
54
 
44
55
  Do not type `claude` in a blank Command Prompt. The tool starts it for you.
45
56
 
@@ -54,8 +65,8 @@ powershell -ExecutionPolicy Bypass -File .\ClaudeDuo.ps1 -NoGui -Left "C:\projA"
54
65
  ## How it works
55
66
 
56
67
  - Uses Windows Terminal split panes (`wt.exe`).
57
- - Account A runs with your normal Claude config.
58
- - Account B sets `CLAUDE_CONFIG_DIR` so logins do not overwrite each other.
68
+ - Each pane sets `CLAUDE_CONFIG_DIR` to an isolated folder so logins, history, and settings never mix.
69
+ - Settings (folders, accounts, layout) auto-save to `%APPDATA%\ClaudeDuo\config.json`.
59
70
 
60
71
  ## License
61
72
 
package/Run-Claude-A.cmd CHANGED
@@ -1,14 +1,3 @@
1
1
  @echo off
2
- REM Account A: default Claude login
3
- set "PATH=C:\nodejs;%APPDATA%\npm;%PATH%"
4
- where claude.cmd >nul 2>&1
5
- if %ERRORLEVEL%==0 (
6
- claude.cmd %*
7
- goto :eof
8
- )
9
- if exist "C:\nodejs\claude.cmd" (
10
- "C:\nodejs\claude.cmd" %*
11
- goto :eof
12
- )
13
- echo Claude Code not found. Install: npm install -g @anthropic-ai/claude-code
14
- pause
2
+ REM Account A: default Claude login (~/.claude)
3
+ call "%~dp0Run-Claude-Account.cmd" default Personal
@@ -0,0 +1,51 @@
1
+ @echo off
2
+ REM Universal Claude Code launcher with isolated config directory per account.
3
+ REM Usage: Run-Claude-Account.cmd [configDir|default] [label]
4
+ setlocal EnableDelayedExpansion
5
+
6
+ set "PATH=C:\nodejs;%APPDATA%\npm;%PATH%"
7
+ set "ACCOUNT_DIR=%~1"
8
+ set "ACCOUNT_LABEL=%~2"
9
+ if "%ACCOUNT_LABEL%"=="" set "ACCOUNT_LABEL=Claude"
10
+
11
+ if /I "%ACCOUNT_DIR%"=="default" (
12
+ set "CLAUDE_CONFIG_DIR="
13
+ ) else if not "%ACCOUNT_DIR%"=="" (
14
+ set "CLAUDE_CONFIG_DIR=%ACCOUNT_DIR%"
15
+ if not exist "!CLAUDE_CONFIG_DIR!" mkdir "!CLAUDE_CONFIG_DIR!"
16
+ ) else (
17
+ set "CLAUDE_CONFIG_DIR="
18
+ )
19
+
20
+ title Claude-!ACCOUNT_LABEL!
21
+ echo.
22
+ echo Claude Duo - Account: !ACCOUNT_LABEL!
23
+ if defined CLAUDE_CONFIG_DIR (
24
+ echo Config dir: !CLAUDE_CONFIG_DIR!
25
+ ) else (
26
+ echo Config dir: %USERPROFILE%\.claude ^(default^)
27
+ )
28
+ echo First time on this account? Type /login inside Claude Code.
29
+ echo.
30
+
31
+ where claude.cmd >nul 2>&1
32
+ if %ERRORLEVEL%==0 (
33
+ call claude.cmd
34
+ goto :done
35
+ )
36
+
37
+ if exist "C:\nodejs\claude.cmd" (
38
+ call "C:\nodejs\claude.cmd"
39
+ goto :done
40
+ )
41
+
42
+ if exist "%APPDATA%\npm\claude.cmd" (
43
+ call "%APPDATA%\npm\claude.cmd"
44
+ goto :done
45
+ )
46
+
47
+ echo Claude Code not found. Install: npm install -g @anthropic-ai/claude-code
48
+ pause
49
+
50
+ :done
51
+ endlocal
package/Run-Claude-B.cmd CHANGED
@@ -1,16 +1,3 @@
1
1
  @echo off
2
- REM Account B: separate Claude login. First run asks you to sign in.
3
- set "PATH=C:\nodejs;%APPDATA%\npm;%PATH%"
4
- set "CLAUDE_CONFIG_DIR=%USERPROFILE%\.claude-account-2"
5
- if not exist "%CLAUDE_CONFIG_DIR%" mkdir "%CLAUDE_CONFIG_DIR%"
6
- where claude.cmd >nul 2>&1
7
- if %ERRORLEVEL%==0 (
8
- claude.cmd %*
9
- goto :eof
10
- )
11
- if exist "C:\nodejs\claude.cmd" (
12
- "C:\nodejs\claude.cmd" %*
13
- goto :eof
14
- )
15
- echo Claude Code not found. Install: npm install -g @anthropic-ai/claude-code
16
- pause
2
+ REM Account B: separate Claude login (~/.claude-account-2)
3
+ call "%~dp0Run-Claude-Account.cmd" "%USERPROFILE%\.claude-account-2" Work
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-duo",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Open two Claude Code sessions in one Windows Terminal window, with optional second-account login.",
5
5
  "bin": {
6
6
  "claude-duo": "bin/claude-duo.js"
@@ -12,6 +12,7 @@
12
12
  "Launch-ClaudeDuo.vbs",
13
13
  "Run-Claude-A.cmd",
14
14
  "Run-Claude-B.cmd",
15
+ "Run-Claude-Account.cmd",
15
16
  "README.md",
16
17
  "LICENSE"
17
18
  ],