claude-duo 2.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.
- package/ClaudeDuo.bat +2 -0
- package/ClaudeDuo.ps1 +375 -0
- package/LICENSE +21 -0
- package/Launch-ClaudeDuo.vbs +7 -0
- package/README.md +62 -0
- package/Run-Claude-A.cmd +14 -0
- package/Run-Claude-B.cmd +16 -0
- package/bin/claude-duo.js +22 -0
- package/package.json +38 -0
package/ClaudeDuo.bat
ADDED
package/ClaudeDuo.ps1
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
Claude Duo — run two Claude Code sessions in one Windows Terminal window.
|
|
5
|
+
#>
|
|
6
|
+
[CmdletBinding()]
|
|
7
|
+
param(
|
|
8
|
+
[string]$Left,
|
|
9
|
+
[string]$Right,
|
|
10
|
+
[ValidateSet('vertical', 'horizontal')]
|
|
11
|
+
[string]$Split = 'vertical',
|
|
12
|
+
[switch]$Maximized,
|
|
13
|
+
[switch]$NoGui
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
$ErrorActionPreference = 'Stop'
|
|
17
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
18
|
+
Add-Type -AssemblyName System.Drawing
|
|
19
|
+
[System.Windows.Forms.Application]::EnableVisualStyles()
|
|
20
|
+
|
|
21
|
+
$AppName = 'Claude Duo'
|
|
22
|
+
$ConfigDir = Join-Path $env:APPDATA 'ClaudeDuo'
|
|
23
|
+
$ConfigPath = Join-Path $ConfigDir 'config.json'
|
|
24
|
+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
25
|
+
|
|
26
|
+
function Find-ClaudeCmd {
|
|
27
|
+
$candidates = @(
|
|
28
|
+
(Get-Command claude.cmd -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source),
|
|
29
|
+
'C:\nodejs\claude.cmd',
|
|
30
|
+
(Join-Path $env:APPDATA 'npm\claude.cmd')
|
|
31
|
+
) | Where-Object { $_ }
|
|
32
|
+
|
|
33
|
+
foreach ($path in $candidates) {
|
|
34
|
+
if ((Test-Path -LiteralPath $path) -and ($path -match '\.(cmd|exe)$')) { return $path }
|
|
35
|
+
}
|
|
36
|
+
return $null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Find-WindowsTerminal {
|
|
40
|
+
$cmd = Get-Command wt.exe -ErrorAction SilentlyContinue
|
|
41
|
+
if ($cmd) { return $cmd.Source }
|
|
42
|
+
$store = Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps\wt.exe'
|
|
43
|
+
if (Test-Path -LiteralPath $store) { return $store }
|
|
44
|
+
return $null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function Read-Config {
|
|
48
|
+
$defaults = [ordered]@{
|
|
49
|
+
LeftFolder = [Environment]::GetFolderPath('MyDocuments')
|
|
50
|
+
RightFolder = [Environment]::GetFolderPath('MyDocuments')
|
|
51
|
+
SameFolder = $true
|
|
52
|
+
Split = 'vertical'
|
|
53
|
+
Maximized = $true
|
|
54
|
+
SecondAccount = $true
|
|
55
|
+
}
|
|
56
|
+
if (-not (Test-Path -LiteralPath $ConfigPath)) { return $defaults }
|
|
57
|
+
try {
|
|
58
|
+
$raw = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
59
|
+
foreach ($key in @($defaults.Keys)) {
|
|
60
|
+
if ($null -ne $raw.$key) { $defaults[$key] = $raw.$key }
|
|
61
|
+
}
|
|
62
|
+
} catch { }
|
|
63
|
+
return $defaults
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function Save-Config {
|
|
67
|
+
param([hashtable]$Config)
|
|
68
|
+
if (-not (Test-Path -LiteralPath $ConfigDir)) {
|
|
69
|
+
New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null
|
|
70
|
+
}
|
|
71
|
+
($Config | ConvertTo-Json) | Set-Content -LiteralPath $ConfigPath -Encoding UTF8
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function Show-Alert {
|
|
75
|
+
param([string]$Text, [string]$Title = $AppName)
|
|
76
|
+
[System.Windows.Forms.MessageBox]::Show(
|
|
77
|
+
$Text, $Title,
|
|
78
|
+
[System.Windows.Forms.MessageBoxButtons]::OK,
|
|
79
|
+
[System.Windows.Forms.MessageBoxIcon]::Warning
|
|
80
|
+
) | Out-Null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function ConvertTo-WtQuoted {
|
|
84
|
+
param([string]$Value)
|
|
85
|
+
if ([string]::IsNullOrWhiteSpace($Value)) { return '""' }
|
|
86
|
+
if ($Value -notmatch '[\s"]') { return $Value }
|
|
87
|
+
return '"' + ($Value -replace '"', '\"') + '"'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function Start-ClaudeDuo {
|
|
91
|
+
param(
|
|
92
|
+
[string]$LeftFolder,
|
|
93
|
+
[string]$RightFolder,
|
|
94
|
+
[string]$SplitMode,
|
|
95
|
+
[bool]$MaximizeWindow,
|
|
96
|
+
[bool]$SecondAccount
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
$wt = Find-WindowsTerminal
|
|
100
|
+
$claude = Find-ClaudeCmd
|
|
101
|
+
|
|
102
|
+
if (-not $wt) {
|
|
103
|
+
throw "Windows Terminal was not found. Install it from the Microsoft Store (search 'Windows Terminal'), then try again."
|
|
104
|
+
}
|
|
105
|
+
if (-not $claude) {
|
|
106
|
+
throw "Claude Code was not found. Install it, or add claude.cmd to PATH."
|
|
107
|
+
}
|
|
108
|
+
if (-not (Test-Path -LiteralPath $LeftFolder)) {
|
|
109
|
+
throw "Left folder does not exist:`n$LeftFolder"
|
|
110
|
+
}
|
|
111
|
+
if (-not (Test-Path -LiteralPath $RightFolder)) {
|
|
112
|
+
throw "Right folder does not exist:`n$RightFolder"
|
|
113
|
+
}
|
|
114
|
+
|
|
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.
|
|
117
|
+
$splitFlag = if ($SplitMode -eq 'horizontal') { '-H' } else { '-V' }
|
|
118
|
+
$leftDir = ConvertTo-WtQuoted $LeftFolder
|
|
119
|
+
$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)
|
|
123
|
+
|
|
124
|
+
$parts = New-Object System.Collections.Generic.List[string]
|
|
125
|
+
$parts.Add('--window new')
|
|
126
|
+
if ($MaximizeWindow) { $parts.Add('--maximized') }
|
|
127
|
+
$parts.Add("new-tab --title Claude-A --suppressApplicationTitle -d $leftDir cmd.exe /k $leftCmd")
|
|
128
|
+
$parts.Add(';')
|
|
129
|
+
$parts.Add("split-pane $splitFlag -s 0.5 --title Claude-B --suppressApplicationTitle -d $rightDir cmd.exe /k $rightCmd")
|
|
130
|
+
$arguments = ($parts -join ' ')
|
|
131
|
+
|
|
132
|
+
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
133
|
+
$psi.FileName = $wt
|
|
134
|
+
$psi.Arguments = $arguments
|
|
135
|
+
$psi.UseShellExecute = $true
|
|
136
|
+
[void][System.Diagnostics.Process]::Start($psi)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function New-DesktopShortcut {
|
|
140
|
+
$desktop = [Environment]::GetFolderPath('Desktop')
|
|
141
|
+
$lnkPath = Join-Path $desktop 'Claude Duo.lnk'
|
|
142
|
+
$target = Join-Path $ScriptDir 'Launch-ClaudeDuo.vbs'
|
|
143
|
+
$wt = Find-WindowsTerminal
|
|
144
|
+
|
|
145
|
+
$shell = New-Object -ComObject WScript.Shell
|
|
146
|
+
$shortcut = $shell.CreateShortcut($lnkPath)
|
|
147
|
+
$shortcut.TargetPath = $target
|
|
148
|
+
$shortcut.WorkingDirectory = $ScriptDir
|
|
149
|
+
$shortcut.Description = 'Open two Claude Code sessions in one window'
|
|
150
|
+
if ($wt) { $shortcut.IconLocation = "$wt,0" }
|
|
151
|
+
$shortcut.Save()
|
|
152
|
+
return $lnkPath
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function New-Label {
|
|
156
|
+
param($Text, $X, $Y, $Width = 460, $Height = 22, $Color = '#E8E4DF', $Size = 9, $Bold = $false)
|
|
157
|
+
$label = New-Object System.Windows.Forms.Label
|
|
158
|
+
$label.Text = $Text
|
|
159
|
+
$label.Location = New-Object System.Drawing.Point($X, $Y)
|
|
160
|
+
$label.Size = New-Object System.Drawing.Size($Width, $Height)
|
|
161
|
+
$label.ForeColor = [System.Drawing.ColorTranslator]::FromHtml($Color)
|
|
162
|
+
$label.BackColor = [System.Drawing.Color]::Transparent
|
|
163
|
+
$fontStyle = if ($Bold) { [System.Drawing.FontStyle]::Bold } else { [System.Drawing.FontStyle]::Regular }
|
|
164
|
+
$label.Font = New-Object System.Drawing.Font('Segoe UI', $Size, $fontStyle)
|
|
165
|
+
return $label
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function New-TextBox {
|
|
169
|
+
param($X, $Y, $Width = 372)
|
|
170
|
+
$box = New-Object System.Windows.Forms.TextBox
|
|
171
|
+
$box.Location = New-Object System.Drawing.Point($X, $Y)
|
|
172
|
+
$box.Size = New-Object System.Drawing.Size($Width, 28)
|
|
173
|
+
$box.Font = New-Object System.Drawing.Font('Segoe UI', 9)
|
|
174
|
+
$box.BorderStyle = 'FixedSingle'
|
|
175
|
+
$box.BackColor = [System.Drawing.ColorTranslator]::FromHtml('#2A2622')
|
|
176
|
+
$box.ForeColor = [System.Drawing.ColorTranslator]::FromHtml('#F4EFE8')
|
|
177
|
+
return $box
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function New-Button {
|
|
181
|
+
param($Text, $X, $Y, $Width, $Height, $Back, $Fore)
|
|
182
|
+
$btn = New-Object System.Windows.Forms.Button
|
|
183
|
+
$btn.Text = $Text
|
|
184
|
+
$btn.Location = New-Object System.Drawing.Point($X, $Y)
|
|
185
|
+
$btn.Size = New-Object System.Drawing.Size($Width, $Height)
|
|
186
|
+
$btn.FlatStyle = 'Flat'
|
|
187
|
+
$btn.FlatAppearance.BorderSize = 0
|
|
188
|
+
$btn.BackColor = [System.Drawing.ColorTranslator]::FromHtml($Back)
|
|
189
|
+
$btn.ForeColor = [System.Drawing.ColorTranslator]::FromHtml($Fore)
|
|
190
|
+
$btn.Font = New-Object System.Drawing.Font('Segoe UI', 9, [System.Drawing.FontStyle]::Bold)
|
|
191
|
+
$btn.Cursor = [System.Windows.Forms.Cursors]::Hand
|
|
192
|
+
return $btn
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function Show-Gui {
|
|
196
|
+
$config = Read-Config
|
|
197
|
+
$bg = [System.Drawing.ColorTranslator]::FromHtml('#161412')
|
|
198
|
+
$panel = [System.Drawing.ColorTranslator]::FromHtml('#1F1C19')
|
|
199
|
+
|
|
200
|
+
$form = New-Object System.Windows.Forms.Form
|
|
201
|
+
$form.Text = $AppName
|
|
202
|
+
$form.Size = New-Object System.Drawing.Size(520, 540)
|
|
203
|
+
$form.StartPosition = 'CenterScreen'
|
|
204
|
+
$form.FormBorderStyle = 'FixedSingle'
|
|
205
|
+
$form.MaximizeBox = $false
|
|
206
|
+
$form.BackColor = $bg
|
|
207
|
+
$form.ForeColor = [System.Drawing.ColorTranslator]::FromHtml('#F4EFE8')
|
|
208
|
+
$form.Font = New-Object System.Drawing.Font('Segoe UI', 9)
|
|
209
|
+
|
|
210
|
+
$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))
|
|
212
|
+
|
|
213
|
+
$folderPanel = New-Object System.Windows.Forms.Panel
|
|
214
|
+
$folderPanel.Location = New-Object System.Drawing.Point(20, 88)
|
|
215
|
+
$folderPanel.Size = New-Object System.Drawing.Size(464, 168)
|
|
216
|
+
$folderPanel.BackColor = $panel
|
|
217
|
+
$form.Controls.Add($folderPanel)
|
|
218
|
+
|
|
219
|
+
$folderPanel.Controls.Add((New-Label 'Folder A (left / top)' 16 12 300 20 '#B7A99A' 8))
|
|
220
|
+
$txtLeft = New-TextBox 16 34 348
|
|
221
|
+
$txtLeft.Text = [string]$config.LeftFolder
|
|
222
|
+
$folderPanel.Controls.Add($txtLeft)
|
|
223
|
+
$btnBrowseA = New-Button 'Browse' 372 32 76 28 '#3A342E' '#F4EFE8'
|
|
224
|
+
$folderPanel.Controls.Add($btnBrowseA)
|
|
225
|
+
|
|
226
|
+
$folderPanel.Controls.Add((New-Label 'Folder B (right / bottom)' 16 72 300 20 '#B7A99A' 8))
|
|
227
|
+
$txtRight = New-TextBox 16 94 348
|
|
228
|
+
$txtRight.Text = [string]$config.RightFolder
|
|
229
|
+
$folderPanel.Controls.Add($txtRight)
|
|
230
|
+
$btnBrowseB = New-Button 'Browse' 372 92 76 28 '#3A342E' '#F4EFE8'
|
|
231
|
+
$folderPanel.Controls.Add($btnBrowseB)
|
|
232
|
+
|
|
233
|
+
$chkSame = New-Object System.Windows.Forms.CheckBox
|
|
234
|
+
$chkSame.Text = 'Use the same folder for both'
|
|
235
|
+
$chkSame.Location = New-Object System.Drawing.Point(16, 132)
|
|
236
|
+
$chkSame.Size = New-Object System.Drawing.Size(420, 22)
|
|
237
|
+
$chkSame.ForeColor = [System.Drawing.ColorTranslator]::FromHtml('#E8E4DF')
|
|
238
|
+
$chkSame.Checked = [bool]$config.SameFolder
|
|
239
|
+
$folderPanel.Controls.Add($chkSame)
|
|
240
|
+
|
|
241
|
+
$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)
|
|
244
|
+
$optPanel.BackColor = $panel
|
|
245
|
+
$form.Controls.Add($optPanel)
|
|
246
|
+
|
|
247
|
+
$radioSide = New-Object System.Windows.Forms.RadioButton
|
|
248
|
+
$radioSide.Text = 'Side by side'
|
|
249
|
+
$radioSide.Location = New-Object System.Drawing.Point(16, 18)
|
|
250
|
+
$radioSide.Size = New-Object System.Drawing.Size(140, 24)
|
|
251
|
+
$radioSide.ForeColor = [System.Drawing.ColorTranslator]::FromHtml('#F4EFE8')
|
|
252
|
+
$radioSide.Checked = ([string]$config.Split -ne 'horizontal')
|
|
253
|
+
$optPanel.Controls.Add($radioSide)
|
|
254
|
+
|
|
255
|
+
$radioStack = New-Object System.Windows.Forms.RadioButton
|
|
256
|
+
$radioStack.Text = 'One above the other'
|
|
257
|
+
$radioStack.Location = New-Object System.Drawing.Point(170, 18)
|
|
258
|
+
$radioStack.Size = New-Object System.Drawing.Size(170, 24)
|
|
259
|
+
$radioStack.ForeColor = [System.Drawing.ColorTranslator]::FromHtml('#F4EFE8')
|
|
260
|
+
$radioStack.Checked = ([string]$config.Split -eq 'horizontal')
|
|
261
|
+
$optPanel.Controls.Add($radioStack)
|
|
262
|
+
|
|
263
|
+
$chkMax = New-Object System.Windows.Forms.CheckBox
|
|
264
|
+
$chkMax.Text = 'Maximize'
|
|
265
|
+
$chkMax.Location = New-Object System.Drawing.Point(350, 18)
|
|
266
|
+
$chkMax.Size = New-Object System.Drawing.Size(100, 24)
|
|
267
|
+
$chkMax.ForeColor = [System.Drawing.ColorTranslator]::FromHtml('#F4EFE8')
|
|
268
|
+
$chkMax.Checked = [bool]$config.Maximized
|
|
269
|
+
$optPanel.Controls.Add($chkMax)
|
|
270
|
+
|
|
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)
|
|
278
|
+
|
|
279
|
+
$btnLaunch = New-Button 'Open 2 Claude Code' 20 388 300 44 '#D97757' '#1A120E'
|
|
280
|
+
$btnLaunch.Font = New-Object System.Drawing.Font('Segoe UI', 11, [System.Drawing.FontStyle]::Bold)
|
|
281
|
+
$form.Controls.Add($btnLaunch)
|
|
282
|
+
|
|
283
|
+
$btnShortcut = New-Button 'Pin to Desktop' 328 388 156 44 '#3A342E' '#F4EFE8'
|
|
284
|
+
$form.Controls.Add($btnShortcut)
|
|
285
|
+
|
|
286
|
+
$claudePath = Find-ClaudeCmd
|
|
287
|
+
$wtPath = Find-WindowsTerminal
|
|
288
|
+
$claudeOk = if ($claudePath) { 'Claude Code found' } else { 'Claude Code NOT found' }
|
|
289
|
+
$wtOk = if ($wtPath) { 'Windows Terminal found' } else { 'Windows Terminal NOT found' }
|
|
290
|
+
$statusColor = if ($claudePath -and $wtPath) { '#8FBF8A' } else { '#E08A6A' }
|
|
291
|
+
$status = New-Label "$claudeOk · $wtOk" 24 444 460 22 $statusColor 8
|
|
292
|
+
$form.Controls.Add($status)
|
|
293
|
+
|
|
294
|
+
$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
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
$syncRight = {
|
|
306
|
+
if ($ui.Same.Checked) {
|
|
307
|
+
$ui.Right.Text = $ui.Left.Text
|
|
308
|
+
$ui.Right.Enabled = $false
|
|
309
|
+
$ui.BrowseB.Enabled = $false
|
|
310
|
+
} else {
|
|
311
|
+
$ui.Right.Enabled = $true
|
|
312
|
+
$ui.BrowseB.Enabled = $true
|
|
313
|
+
}
|
|
314
|
+
}.GetNewClosure()
|
|
315
|
+
|
|
316
|
+
$pickFolder = {
|
|
317
|
+
param($box)
|
|
318
|
+
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
|
|
319
|
+
$dialog.Description = 'Choose a project folder for Claude Code'
|
|
320
|
+
$dialog.ShowNewFolderButton = $true
|
|
321
|
+
if ($box.Text -and (Test-Path -LiteralPath $box.Text)) { $dialog.SelectedPath = $box.Text }
|
|
322
|
+
if ($dialog.ShowDialog() -eq 'OK') {
|
|
323
|
+
$box.Text = $dialog.SelectedPath
|
|
324
|
+
& $syncRight
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
$btnBrowseA.Add_Click({ & $pickFolder $ui.Left }.GetNewClosure())
|
|
329
|
+
$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())
|
|
332
|
+
& $syncRight
|
|
333
|
+
|
|
334
|
+
$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
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
Start-ClaudeDuo -LeftFolder $cfg.LeftFolder -RightFolder $cfg.RightFolder -SplitMode $cfg.Split -MaximizeWindow $cfg.Maximized -SecondAccount $cfg.SecondAccount
|
|
346
|
+
Save-Config $cfg
|
|
347
|
+
} catch {
|
|
348
|
+
Show-Alert $_.Exception.Message
|
|
349
|
+
}
|
|
350
|
+
}.GetNewClosure())
|
|
351
|
+
|
|
352
|
+
$btnShortcut.Add_Click({
|
|
353
|
+
try {
|
|
354
|
+
$path = New-DesktopShortcut
|
|
355
|
+
Show-Alert "Shortcut created:`n$path"
|
|
356
|
+
} catch {
|
|
357
|
+
Show-Alert $_.Exception.Message
|
|
358
|
+
}
|
|
359
|
+
}.GetNewClosure())
|
|
360
|
+
|
|
361
|
+
$form.ShowDialog() | Out-Null
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
# --- entry ---
|
|
365
|
+
if ($NoGui) {
|
|
366
|
+
$cfg = Read-Config
|
|
367
|
+
$leftFolder = if ($Left) { $Left } else { [string]$cfg.LeftFolder }
|
|
368
|
+
$rightFolder = if ($Right) { $Right } else { if ([bool]$cfg.SameFolder) { $leftFolder } else { [string]$cfg.RightFolder } }
|
|
369
|
+
$splitMode = if ($PSBoundParameters.ContainsKey('Split')) { $Split } else { [string]$cfg.Split }
|
|
370
|
+
$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
|
|
373
|
+
} else {
|
|
374
|
+
Show-Gui
|
|
375
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Muhammad Ferasat Ali
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
' Silent launcher — no PowerShell console flash
|
|
2
|
+
Set fso = CreateObject("Scripting.FileSystemObject")
|
|
3
|
+
folder = fso.GetParentFolderName(WScript.ScriptFullName)
|
|
4
|
+
ps1 = folder & "\ClaudeDuo.ps1"
|
|
5
|
+
cmd = "powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File """ & ps1 & """"
|
|
6
|
+
Set sh = CreateObject("Wscript.Shell")
|
|
7
|
+
sh.Run cmd, 0, False
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Claude Duo
|
|
2
|
+
|
|
3
|
+
Open-source Windows tool that opens **two [Claude Code](https://docs.anthropic.com/en/docs/claude-code) sessions in one window**.
|
|
4
|
+
|
|
5
|
+
Optional: left pane = your current account, right pane = a **second Claude account** (separate login).
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Windows 10 or 11
|
|
10
|
+
- [Windows Terminal](https://aka.ms/terminal)
|
|
11
|
+
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`npm install -g @anthropic-ai/claude-code`)
|
|
12
|
+
|
|
13
|
+
## Install (npm)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g claude-duo
|
|
17
|
+
claude-duo
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
One-shot, no install:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx claude-duo
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Needs Windows 10/11, [Windows Terminal](https://aka.ms/terminal), and [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
|
|
27
|
+
|
|
28
|
+
### From GitHub (no npm)
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/mferasatali/claude-duo.git
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Double-click **`ClaudeDuo.bat`**, or **Pin to Desktop** in the app.
|
|
35
|
+
|
|
36
|
+
## Use
|
|
37
|
+
|
|
38
|
+
1. Pick the project folder(s).
|
|
39
|
+
2. Keep **Right pane: second Claude account** checked if you want two logins.
|
|
40
|
+
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.
|
|
43
|
+
|
|
44
|
+
Do not type `claude` in a blank Command Prompt. The tool starts it for you.
|
|
45
|
+
|
|
46
|
+
## Command line
|
|
47
|
+
|
|
48
|
+
```powershell
|
|
49
|
+
powershell -ExecutionPolicy Bypass -File .\ClaudeDuo.ps1 -NoGui -Left "C:\projA" -Right "C:\projB" -Split vertical -Maximized
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`-Split` is `vertical` (side by side) or `horizontal` (stacked).
|
|
53
|
+
|
|
54
|
+
## How it works
|
|
55
|
+
|
|
56
|
+
- 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.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
[MIT](LICENSE)
|
package/Run-Claude-A.cmd
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
|
package/Run-Claude-B.cmd
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const { spawn } = require('child_process')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
|
|
7
|
+
if (process.platform !== 'win32') {
|
|
8
|
+
console.error('claude-duo is a Windows tool (Windows Terminal + Claude Code).')
|
|
9
|
+
process.exit(1)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ps1 = path.join(__dirname, '..', 'ClaudeDuo.ps1')
|
|
13
|
+
const child = spawn(
|
|
14
|
+
'powershell.exe',
|
|
15
|
+
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ps1, ...process.argv.slice(2)],
|
|
16
|
+
{ stdio: 'inherit', windowsHide: false }
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
child.on('exit', (code, signal) => {
|
|
20
|
+
if (signal) process.kill(process.pid, signal)
|
|
21
|
+
process.exit(code == null ? 1 : code)
|
|
22
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-duo",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Open two Claude Code sessions in one Windows Terminal window, with optional second-account login.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"claude-duo": "bin/claude-duo.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"ClaudeDuo.ps1",
|
|
11
|
+
"ClaudeDuo.bat",
|
|
12
|
+
"Launch-ClaudeDuo.vbs",
|
|
13
|
+
"Run-Claude-A.cmd",
|
|
14
|
+
"Run-Claude-B.cmd",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"os": ["win32"],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/mferasatali/claude-duo.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/mferasatali/claude-duo/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/mferasatali/claude-duo#readme",
|
|
30
|
+
"keywords": [
|
|
31
|
+
"claude",
|
|
32
|
+
"claude-code",
|
|
33
|
+
"windows-terminal",
|
|
34
|
+
"split-pane"
|
|
35
|
+
],
|
|
36
|
+
"author": "Muhammad Ferasat Ali",
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|