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 +219 -48
- package/README.md +17 -6
- package/Run-Claude-A.cmd +2 -13
- package/Run-Claude-Account.cmd +51 -0
- package/Run-Claude-B.cmd +2 -15
- package/package.json +2 -1
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
|
|
52
|
-
Split
|
|
53
|
-
Maximized
|
|
54
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
116
|
-
|
|
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 =
|
|
121
|
-
$
|
|
122
|
-
$
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
243
|
-
$optPanel.Size = New-Object System.Drawing.Size(464,
|
|
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
|
-
$
|
|
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
|
|
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
|
|
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
|
|
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
|
|
296
|
-
Right
|
|
297
|
-
BrowseB
|
|
298
|
-
Same
|
|
299
|
-
Side
|
|
300
|
-
Stack
|
|
301
|
-
Max
|
|
302
|
-
|
|
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
|
-
$
|
|
336
|
-
|
|
337
|
-
LeftFolder
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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
|
-
$
|
|
372
|
-
|
|
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
|
-
###
|
|
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.
|
|
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
|
|
42
|
-
5.
|
|
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
|
-
-
|
|
58
|
-
-
|
|
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
|
-
|
|
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
|
|
3
|
-
|
|
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.
|
|
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
|
],
|