customer-map-codex-bridge 0.5.2 → 0.5.3
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/README.md +22 -10
- package/authorization-macos.applescript +35 -0
- package/authorization-windows.ps1 +300 -0
- package/index.mjs +94 -45
- package/install-protocol-macos.sh +85 -0
- package/install-protocol-windows.ps1 +43 -0
- package/package.json +7 -2
- package/protocol-install.mjs +35 -0
package/README.md
CHANGED
|
@@ -13,11 +13,23 @@ The settings panel reports **Connected** only after both the local Codex app-ser
|
|
|
13
13
|
|
|
14
14
|
## Run
|
|
15
15
|
|
|
16
|
-
```bash
|
|
17
|
-
npx -y customer-map-codex-bridge@0.5.
|
|
18
|
-
--site https://your-customer-map.example \
|
|
19
|
-
--code CMAP-CODEX-XXXXXXXXXXXX
|
|
20
|
-
```
|
|
16
|
+
```bash
|
|
17
|
+
npx -y customer-map-codex-bridge@0.5.3 \
|
|
18
|
+
--site https://your-customer-map.example \
|
|
19
|
+
--code CMAP-CODEX-XXXXXXXXXXXX
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Click-to-authorize (Windows and macOS)
|
|
23
|
+
|
|
24
|
+
The Customer Map button can open the local authorization window directly after the protocol handler is registered once. Run this one-time command in a terminal on the computer where Codex is installed:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx -y customer-map-codex-bridge@0.5.3 --install-protocol
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The installer registers the `customer-map-codex://` URL scheme for the current user and stores the local Codex path. It does not upload credentials or start a Bridge. Return to Customer Map and click **Open Codex authorization**; the native authorization window will appear on Windows or macOS.
|
|
31
|
+
|
|
32
|
+
If the browser still does not open the window, use the **fallback command** shown in Agent settings. This is also the supported path on Linux, where click-to-authorize is not currently registered.
|
|
21
33
|
|
|
22
34
|
Starting a newly authorized Bridge automatically replaces the older Bridge for the same Customer Map account. Clicking **Remove connection** in Customer Map disconnects the running Bridge, so users do not need to find or kill local processes.
|
|
23
35
|
|
|
@@ -29,17 +41,17 @@ When the user explicitly chooses **Save Gmail draft** or **Send email** in Custo
|
|
|
29
41
|
|
|
30
42
|
If the protocol handler cannot find `codex`, pass its absolute path explicitly:
|
|
31
43
|
|
|
32
|
-
```bash
|
|
33
|
-
npx -y customer-map-codex-bridge@0.5.
|
|
44
|
+
```bash
|
|
45
|
+
npx -y customer-map-codex-bridge@0.5.3 \
|
|
34
46
|
--site https://your-customer-map.example \
|
|
35
47
|
--code CMAP-CODEX-XXXXXXXXXXXX \
|
|
36
48
|
--codex /absolute/path/to/codex
|
|
37
|
-
```
|
|
49
|
+
```
|
|
38
50
|
|
|
39
51
|
If `gog` is outside the standard Homebrew/system paths, pass it explicitly:
|
|
40
52
|
|
|
41
|
-
```bash
|
|
42
|
-
npx -y customer-map-codex-bridge@0.5.
|
|
53
|
+
```bash
|
|
54
|
+
npx -y customer-map-codex-bridge@0.5.3 \
|
|
43
55
|
--site https://your-customer-map.example \
|
|
44
56
|
--code CMAP-CODEX-XXXXXXXXXXXX \
|
|
45
57
|
--gog /absolute/path/to/gog
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
use framework "Cocoa"
|
|
2
|
+
use scripting additions
|
|
3
|
+
|
|
4
|
+
on run argv
|
|
5
|
+
if (count of argv) is less than 1 then return "ERROR"
|
|
6
|
+
|
|
7
|
+
set siteUrl to item 1 of argv
|
|
8
|
+
set alert to current application's NSAlert's alloc()'s init()
|
|
9
|
+
alert's setMessageText:"Connect Customer Map to Codex"
|
|
10
|
+
alert's setInformativeText:("Use the Codex already signed in on this Mac." & return & return & ¬
|
|
11
|
+
"Read-only Codex conversations and public-web search are available for your sales tasks." & return & ¬
|
|
12
|
+
"File writes and model-generated command networking remain blocked." & return & ¬
|
|
13
|
+
"Only explicit Gmail draft or send actions may use your local gog installation.")
|
|
14
|
+
alert's setAlertStyle:(current application's NSInformationalAlertStyle)
|
|
15
|
+
|
|
16
|
+
set siteField to current application's NSTextField's alloc()'s initWithFrame:{{0, 0}, {360, 28}}
|
|
17
|
+
siteField's setStringValue:siteUrl
|
|
18
|
+
siteField's setEditable:false
|
|
19
|
+
siteField's setSelectable:true
|
|
20
|
+
siteField's setBezeled:true
|
|
21
|
+
siteField's setDrawsBackground:true
|
|
22
|
+
siteField's setBackgroundColor:(current application's NSColor's colorWithCalibratedWhite:0.96 alpha:1.0)
|
|
23
|
+
siteField's setTextColor:(current application's NSColor's colorWithCalibratedWhite:0.22 alpha:1.0)
|
|
24
|
+
siteField's setFont:(current application's NSFont's monospacedSystemFontOfSize:11 weight:(current application's NSFontWeightRegular))
|
|
25
|
+
siteField's setLineBreakMode:(current application's NSLineBreakByTruncatingMiddle)
|
|
26
|
+
alert's setAccessoryView:siteField
|
|
27
|
+
|
|
28
|
+
alert's addButtonWithTitle:"Cancel"
|
|
29
|
+
alert's addButtonWithTitle:"Allow connection"
|
|
30
|
+
|
|
31
|
+
current application's NSApp's activateIgnoringOtherApps:true
|
|
32
|
+
set response to alert's runModal()
|
|
33
|
+
if response is (current application's NSAlertSecondButtonReturn) then return "ALLOW"
|
|
34
|
+
return "CANCEL"
|
|
35
|
+
end run
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory = $true)]
|
|
3
|
+
[string]$Site,
|
|
4
|
+
[switch]$TestMode,
|
|
5
|
+
[string]$RenderPath = ''
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
$ErrorActionPreference = 'Stop'
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
Add-Type -AssemblyName PresentationFramework
|
|
12
|
+
Add-Type -AssemblyName PresentationCore
|
|
13
|
+
Add-Type -AssemblyName WindowsBase
|
|
14
|
+
|
|
15
|
+
[xml]$xaml = @'
|
|
16
|
+
<Window
|
|
17
|
+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
18
|
+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
19
|
+
Title="Customer Map - Codex authorization"
|
|
20
|
+
Width="560"
|
|
21
|
+
Height="540"
|
|
22
|
+
WindowStartupLocation="CenterScreen"
|
|
23
|
+
ResizeMode="NoResize"
|
|
24
|
+
ShowActivated="True"
|
|
25
|
+
Topmost="True"
|
|
26
|
+
Background="#F6F5F2"
|
|
27
|
+
FontFamily="Segoe UI"
|
|
28
|
+
UseLayoutRounding="True"
|
|
29
|
+
SnapsToDevicePixels="True">
|
|
30
|
+
<Window.Resources>
|
|
31
|
+
<Style x:Key="QuietButton" TargetType="Button">
|
|
32
|
+
<Setter Property="MinWidth" Value="104"/>
|
|
33
|
+
<Setter Property="Height" Value="38"/>
|
|
34
|
+
<Setter Property="Padding" Value="18,0"/>
|
|
35
|
+
<Setter Property="FontSize" Value="13"/>
|
|
36
|
+
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
37
|
+
<Setter Property="Cursor" Value="Hand"/>
|
|
38
|
+
<Setter Property="Template">
|
|
39
|
+
<Setter.Value>
|
|
40
|
+
<ControlTemplate TargetType="Button">
|
|
41
|
+
<Border
|
|
42
|
+
x:Name="ButtonSurface"
|
|
43
|
+
Background="{TemplateBinding Background}"
|
|
44
|
+
BorderBrush="{TemplateBinding BorderBrush}"
|
|
45
|
+
BorderThickness="1"
|
|
46
|
+
CornerRadius="6"
|
|
47
|
+
RenderTransformOrigin="0.5,0.5">
|
|
48
|
+
<Border.RenderTransform>
|
|
49
|
+
<ScaleTransform ScaleX="1" ScaleY="1"/>
|
|
50
|
+
</Border.RenderTransform>
|
|
51
|
+
<ContentPresenter
|
|
52
|
+
HorizontalAlignment="Center"
|
|
53
|
+
VerticalAlignment="Center"
|
|
54
|
+
RecognizesAccessKey="True"/>
|
|
55
|
+
</Border>
|
|
56
|
+
<ControlTemplate.Triggers>
|
|
57
|
+
<Trigger Property="IsMouseOver" Value="True">
|
|
58
|
+
<Setter TargetName="ButtonSurface" Property="Opacity" Value="0.9"/>
|
|
59
|
+
</Trigger>
|
|
60
|
+
<Trigger Property="IsPressed" Value="True">
|
|
61
|
+
<Setter TargetName="ButtonSurface" Property="Opacity" Value="0.82"/>
|
|
62
|
+
<Setter TargetName="ButtonSurface" Property="RenderTransform">
|
|
63
|
+
<Setter.Value>
|
|
64
|
+
<ScaleTransform ScaleX="0.98" ScaleY="0.98"/>
|
|
65
|
+
</Setter.Value>
|
|
66
|
+
</Setter>
|
|
67
|
+
</Trigger>
|
|
68
|
+
<Trigger Property="IsKeyboardFocused" Value="True">
|
|
69
|
+
<Setter TargetName="ButtonSurface" Property="BorderBrush" Value="#315F7D"/>
|
|
70
|
+
<Setter TargetName="ButtonSurface" Property="BorderThickness" Value="2"/>
|
|
71
|
+
</Trigger>
|
|
72
|
+
<Trigger Property="IsEnabled" Value="False">
|
|
73
|
+
<Setter TargetName="ButtonSurface" Property="Opacity" Value="0.45"/>
|
|
74
|
+
</Trigger>
|
|
75
|
+
</ControlTemplate.Triggers>
|
|
76
|
+
</ControlTemplate>
|
|
77
|
+
</Setter.Value>
|
|
78
|
+
</Setter>
|
|
79
|
+
</Style>
|
|
80
|
+
<Style x:Key="PrimaryButton" TargetType="Button" BasedOn="{StaticResource QuietButton}">
|
|
81
|
+
<Setter Property="MinWidth" Value="124"/>
|
|
82
|
+
<Setter Property="Background" Value="#1F2328"/>
|
|
83
|
+
<Setter Property="BorderBrush" Value="#1F2328"/>
|
|
84
|
+
<Setter Property="Foreground" Value="#FFFFFF"/>
|
|
85
|
+
</Style>
|
|
86
|
+
<Style x:Key="SecondaryButton" TargetType="Button" BasedOn="{StaticResource QuietButton}">
|
|
87
|
+
<Setter Property="Background" Value="#FFFFFF"/>
|
|
88
|
+
<Setter Property="BorderBrush" Value="#B7C8D1"/>
|
|
89
|
+
<Setter Property="Foreground" Value="#1F2328"/>
|
|
90
|
+
</Style>
|
|
91
|
+
</Window.Resources>
|
|
92
|
+
|
|
93
|
+
<Grid Margin="12">
|
|
94
|
+
<Border
|
|
95
|
+
Background="#FFFFFF"
|
|
96
|
+
BorderBrush="#E2E8F0"
|
|
97
|
+
BorderThickness="1"
|
|
98
|
+
CornerRadius="8">
|
|
99
|
+
<Grid>
|
|
100
|
+
<Grid.RowDefinitions>
|
|
101
|
+
<RowDefinition Height="3"/>
|
|
102
|
+
<RowDefinition Height="Auto"/>
|
|
103
|
+
<RowDefinition Height="*"/>
|
|
104
|
+
<RowDefinition Height="68"/>
|
|
105
|
+
</Grid.RowDefinitions>
|
|
106
|
+
|
|
107
|
+
<Border Grid.Row="0" Background="#315F7D" CornerRadius="8,8,0,0"/>
|
|
108
|
+
|
|
109
|
+
<Grid Grid.Row="1" Margin="22,18,22,14">
|
|
110
|
+
<Grid.ColumnDefinitions>
|
|
111
|
+
<ColumnDefinition Width="42"/>
|
|
112
|
+
<ColumnDefinition Width="*"/>
|
|
113
|
+
</Grid.ColumnDefinitions>
|
|
114
|
+
<Border
|
|
115
|
+
Width="38"
|
|
116
|
+
Height="38"
|
|
117
|
+
HorizontalAlignment="Left"
|
|
118
|
+
VerticalAlignment="Top"
|
|
119
|
+
Background="#DBE8F0"
|
|
120
|
+
CornerRadius="6">
|
|
121
|
+
<TextBlock
|
|
122
|
+
Text="CM"
|
|
123
|
+
HorizontalAlignment="Center"
|
|
124
|
+
VerticalAlignment="Center"
|
|
125
|
+
Foreground="#1F2328"
|
|
126
|
+
FontSize="12"
|
|
127
|
+
FontWeight="Bold"/>
|
|
128
|
+
</Border>
|
|
129
|
+
<StackPanel Grid.Column="1" Margin="12,0,0,0">
|
|
130
|
+
<TextBlock
|
|
131
|
+
Text="Connect Customer Map to Codex"
|
|
132
|
+
Foreground="#1F2328"
|
|
133
|
+
FontSize="16"
|
|
134
|
+
FontWeight="SemiBold"/>
|
|
135
|
+
<TextBlock
|
|
136
|
+
Margin="0,4,0,0"
|
|
137
|
+
Text="Use the Codex already signed in on this computer."
|
|
138
|
+
Foreground="#667085"
|
|
139
|
+
FontSize="12.5"/>
|
|
140
|
+
</StackPanel>
|
|
141
|
+
</Grid>
|
|
142
|
+
|
|
143
|
+
<StackPanel Grid.Row="2" Margin="22,0,22,18">
|
|
144
|
+
<TextBlock
|
|
145
|
+
Text="REQUESTING WEBSITE"
|
|
146
|
+
Foreground="#667085"
|
|
147
|
+
FontSize="10.5"
|
|
148
|
+
FontWeight="SemiBold"/>
|
|
149
|
+
<Border
|
|
150
|
+
Margin="0,7,0,16"
|
|
151
|
+
Padding="12,10"
|
|
152
|
+
Background="#FBFAF8"
|
|
153
|
+
BorderBrush="#E2E8F0"
|
|
154
|
+
BorderThickness="1"
|
|
155
|
+
CornerRadius="6">
|
|
156
|
+
<TextBlock
|
|
157
|
+
x:Name="SiteText"
|
|
158
|
+
Foreground="#1F2328"
|
|
159
|
+
FontFamily="Consolas"
|
|
160
|
+
FontSize="12"
|
|
161
|
+
TextTrimming="CharacterEllipsis"/>
|
|
162
|
+
</Border>
|
|
163
|
+
|
|
164
|
+
<TextBlock
|
|
165
|
+
Text="WHAT THIS ALLOWS"
|
|
166
|
+
Foreground="#667085"
|
|
167
|
+
FontSize="10.5"
|
|
168
|
+
FontWeight="SemiBold"/>
|
|
169
|
+
|
|
170
|
+
<Grid Margin="0,9,0,0">
|
|
171
|
+
<Grid.ColumnDefinitions>
|
|
172
|
+
<ColumnDefinition Width="18"/>
|
|
173
|
+
<ColumnDefinition Width="*"/>
|
|
174
|
+
</Grid.ColumnDefinitions>
|
|
175
|
+
<Ellipse Width="7" Height="7" Fill="#315F7D" VerticalAlignment="Top" Margin="2,5,0,0"/>
|
|
176
|
+
<StackPanel Grid.Column="1">
|
|
177
|
+
<TextBlock Text="Read-only Codex conversations" Foreground="#1F2328" FontSize="12.5" FontWeight="SemiBold"/>
|
|
178
|
+
<TextBlock Text="File writes and model-generated command networking stay blocked." Foreground="#667085" FontSize="11.5" Margin="0,2,0,0"/>
|
|
179
|
+
</StackPanel>
|
|
180
|
+
</Grid>
|
|
181
|
+
|
|
182
|
+
<Grid Margin="0,11,0,0">
|
|
183
|
+
<Grid.ColumnDefinitions>
|
|
184
|
+
<ColumnDefinition Width="18"/>
|
|
185
|
+
<ColumnDefinition Width="*"/>
|
|
186
|
+
</Grid.ColumnDefinitions>
|
|
187
|
+
<Ellipse Width="7" Height="7" Fill="#315F7D" VerticalAlignment="Top" Margin="2,5,0,0"/>
|
|
188
|
+
<StackPanel Grid.Column="1">
|
|
189
|
+
<TextBlock Text="Live public-web search" Foreground="#1F2328" FontSize="12.5" FontWeight="SemiBold"/>
|
|
190
|
+
<TextBlock Text="Codex may search and read public websites for your sales tasks." Foreground="#667085" FontSize="11.5" Margin="0,2,0,0"/>
|
|
191
|
+
</StackPanel>
|
|
192
|
+
</Grid>
|
|
193
|
+
|
|
194
|
+
<Grid Margin="0,11,0,0">
|
|
195
|
+
<Grid.ColumnDefinitions>
|
|
196
|
+
<ColumnDefinition Width="18"/>
|
|
197
|
+
<ColumnDefinition Width="*"/>
|
|
198
|
+
</Grid.ColumnDefinitions>
|
|
199
|
+
<Ellipse Width="7" Height="7" Fill="#315F7D" VerticalAlignment="Top" Margin="2,5,0,0"/>
|
|
200
|
+
<StackPanel Grid.Column="1">
|
|
201
|
+
<TextBlock Text="Local credentials stay local" Foreground="#1F2328" FontSize="12.5" FontWeight="SemiBold"/>
|
|
202
|
+
<TextBlock Text="Customer Map never receives your Codex login or API keys." Foreground="#667085" FontSize="11.5" Margin="0,2,0,0"/>
|
|
203
|
+
</StackPanel>
|
|
204
|
+
</Grid>
|
|
205
|
+
|
|
206
|
+
<Border
|
|
207
|
+
Margin="0,16,0,0"
|
|
208
|
+
Padding="11,9"
|
|
209
|
+
Background="#E4EBE2"
|
|
210
|
+
CornerRadius="6">
|
|
211
|
+
<TextBlock
|
|
212
|
+
Text="Only explicit Gmail draft or send actions may use your local gog installation."
|
|
213
|
+
Foreground="#33413A"
|
|
214
|
+
FontSize="11.5"
|
|
215
|
+
TextWrapping="Wrap"/>
|
|
216
|
+
</Border>
|
|
217
|
+
</StackPanel>
|
|
218
|
+
|
|
219
|
+
<Border
|
|
220
|
+
Grid.Row="3"
|
|
221
|
+
Background="#FBFAF8"
|
|
222
|
+
BorderBrush="#E2E8F0"
|
|
223
|
+
BorderThickness="0,1,0,0"
|
|
224
|
+
CornerRadius="0,0,8,8">
|
|
225
|
+
<Grid Margin="22,14">
|
|
226
|
+
<Grid.ColumnDefinitions>
|
|
227
|
+
<ColumnDefinition Width="*"/>
|
|
228
|
+
<ColumnDefinition Width="Auto"/>
|
|
229
|
+
<ColumnDefinition Width="10"/>
|
|
230
|
+
<ColumnDefinition Width="Auto"/>
|
|
231
|
+
</Grid.ColumnDefinitions>
|
|
232
|
+
<TextBlock
|
|
233
|
+
VerticalAlignment="Center"
|
|
234
|
+
Text="Manage this connection in Customer Map settings."
|
|
235
|
+
TextTrimming="CharacterEllipsis"
|
|
236
|
+
Foreground="#667085"
|
|
237
|
+
FontSize="10.5"/>
|
|
238
|
+
<Button
|
|
239
|
+
x:Name="CancelButton"
|
|
240
|
+
Grid.Column="1"
|
|
241
|
+
Content="Cancel"
|
|
242
|
+
IsCancel="True"
|
|
243
|
+
Style="{StaticResource SecondaryButton}"/>
|
|
244
|
+
<Button
|
|
245
|
+
x:Name="AllowButton"
|
|
246
|
+
Grid.Column="3"
|
|
247
|
+
Content="Allow connection"
|
|
248
|
+
IsDefault="True"
|
|
249
|
+
Style="{StaticResource PrimaryButton}"/>
|
|
250
|
+
</Grid>
|
|
251
|
+
</Border>
|
|
252
|
+
</Grid>
|
|
253
|
+
</Border>
|
|
254
|
+
</Grid>
|
|
255
|
+
</Window>
|
|
256
|
+
'@
|
|
257
|
+
|
|
258
|
+
$reader = New-Object System.Xml.XmlNodeReader $xaml
|
|
259
|
+
$window = [Windows.Markup.XamlReader]::Load($reader)
|
|
260
|
+
$window.FindName('SiteText').Text = $Site
|
|
261
|
+
|
|
262
|
+
if ($TestMode) {
|
|
263
|
+
if ($RenderPath) {
|
|
264
|
+
$window.WindowStartupLocation = 'Manual'
|
|
265
|
+
$window.Left = -10000
|
|
266
|
+
$window.Top = -10000
|
|
267
|
+
$window.ShowInTaskbar = $false
|
|
268
|
+
$window.Show()
|
|
269
|
+
$window.UpdateLayout()
|
|
270
|
+
|
|
271
|
+
$width = [Math]::Max(1, [int][Math]::Ceiling($window.ActualWidth))
|
|
272
|
+
$height = [Math]::Max(1, [int][Math]::Ceiling($window.ActualHeight))
|
|
273
|
+
$bitmap = New-Object Windows.Media.Imaging.RenderTargetBitmap($width, $height, 96, 96, [Windows.Media.PixelFormats]::Pbgra32)
|
|
274
|
+
$bitmap.Render($window)
|
|
275
|
+
$encoder = New-Object Windows.Media.Imaging.PngBitmapEncoder
|
|
276
|
+
$encoder.Frames.Add([Windows.Media.Imaging.BitmapFrame]::Create($bitmap))
|
|
277
|
+
$stream = [IO.File]::Open($RenderPath, [IO.FileMode]::Create)
|
|
278
|
+
try { $encoder.Save($stream) } finally { $stream.Dispose() }
|
|
279
|
+
$window.Close()
|
|
280
|
+
}
|
|
281
|
+
exit 0
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
$script:approved = $false
|
|
285
|
+
[void]$window.FindName('AllowButton').Focus()
|
|
286
|
+
$window.FindName('AllowButton').Add_Click({
|
|
287
|
+
$script:approved = $true
|
|
288
|
+
$window.Close()
|
|
289
|
+
})
|
|
290
|
+
$window.FindName('CancelButton').Add_Click({
|
|
291
|
+
$window.Close()
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
[void]$window.ShowDialog()
|
|
295
|
+
if ($script:approved) { exit 0 }
|
|
296
|
+
exit 1
|
|
297
|
+
} catch {
|
|
298
|
+
Write-Error $_
|
|
299
|
+
exit 2
|
|
300
|
+
}
|
package/index.mjs
CHANGED
|
@@ -2,23 +2,37 @@
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
5
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
6
|
-
import { homedir } from 'node:os';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import process from 'node:process';
|
|
5
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
9
10
|
import WebSocket from 'ws';
|
|
10
11
|
import { executeCustomerMapMailAction } from './mail-action.mjs';
|
|
11
12
|
import { startCodexAppServer } from './codex-process.mjs';
|
|
13
|
+
import { installCodexProtocolHandler } from './protocol-install.mjs';
|
|
12
14
|
|
|
13
|
-
const BRIDGE_VERSION = '0.5.
|
|
14
|
-
const RELAY_RECONNECT_BASE_MS = 1_000;
|
|
15
|
-
const RELAY_RECONNECT_MAX_MS = 30_000;
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
const BRIDGE_VERSION = '0.5.3';
|
|
16
|
+
const RELAY_RECONNECT_BASE_MS = 1_000;
|
|
17
|
+
const RELAY_RECONNECT_MAX_MS = 30_000;
|
|
18
|
+
const BRIDGE_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const WINDOWS_AUTHORIZATION_SCRIPT = join(BRIDGE_DIRECTORY, 'authorization-windows.ps1');
|
|
20
|
+
const MACOS_AUTHORIZATION_SCRIPT = join(BRIDGE_DIRECTORY, 'authorization-macos.applescript');
|
|
21
|
+
|
|
22
|
+
const args = parseArgs(process.argv.slice(2));
|
|
23
|
+
if (args.help) {
|
|
24
|
+
console.log('Usage: customer-map-codex-bridge [--install-protocol] | --site <Customer Map URL> --code <one-time code> [--cwd <folder>] [--codex <path>] [--gog <path>]');
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
if (args['install-protocol']) {
|
|
28
|
+
try {
|
|
29
|
+
installCodexProtocolHandler(BRIDGE_VERSION);
|
|
30
|
+
process.exit(0);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(error instanceof Error ? error.message : String(error || 'Protocol installation failed'));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
22
36
|
|
|
23
37
|
const uri = args.uri || process.argv.slice(2).find(value => String(value).startsWith('customer-map-codex://'));
|
|
24
38
|
const uriValues = uri ? parseConnectUri(uri) : {};
|
|
@@ -611,36 +625,18 @@ function wait(delayMs) {
|
|
|
611
625
|
});
|
|
612
626
|
}
|
|
613
627
|
|
|
614
|
-
async function requestAuthorization(siteUrl) {
|
|
615
|
-
if (process.platform === 'win32') {
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}
|
|
627
|
-
if (process.platform === 'darwin') {
|
|
628
|
-
const dialog = spawnSync('osascript', ['-e', [
|
|
629
|
-
'on run argv',
|
|
630
|
-
' set siteUrl to item 1 of argv',
|
|
631
|
-
' set promptText to "Customer Map wants to connect to the Codex installed on this Mac." & return & return & "Site: " & siteUrl & return & return & "Codex chat stays read-only. Live web search may read public sites; shell network and file writes stay blocked. Only explicit Gmail draft/send actions may run local gog. Credentials stay local."',
|
|
632
|
-
' display dialog promptText with title "Customer Map - Codex authorization" buttons {"Cancel", "Allow"} default button "Cancel"',
|
|
633
|
-
' if button returned of result is "Allow" then return 0',
|
|
634
|
-
' return 1',
|
|
635
|
-
'end run',
|
|
636
|
-
].join('\n'), siteUrl], { stdio: 'ignore' });
|
|
637
|
-
if (!dialog.error) {
|
|
638
|
-
if (dialog.status === 0) return;
|
|
639
|
-
console.log('Codex connection cancelled.');
|
|
640
|
-
process.exit(0);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
console.log(`\nCustomer Map wants to connect to the Codex installed on this computer.`);
|
|
628
|
+
async function requestAuthorization(siteUrl) {
|
|
629
|
+
if (process.platform === 'win32') {
|
|
630
|
+
const decision = runWindowsAuthorization(siteUrl);
|
|
631
|
+
if (decision === true) return;
|
|
632
|
+
if (decision === false) cancelAuthorization();
|
|
633
|
+
}
|
|
634
|
+
if (process.platform === 'darwin') {
|
|
635
|
+
const decision = runMacAuthorization(siteUrl);
|
|
636
|
+
if (decision === true) return;
|
|
637
|
+
if (decision === false) cancelAuthorization();
|
|
638
|
+
}
|
|
639
|
+
console.log(`\nCustomer Map wants to connect to the Codex installed on this computer.`);
|
|
644
640
|
console.log(`Site: ${siteUrl}`);
|
|
645
641
|
console.log('Codex chat stays read-only. Live web search may read public sites; shell network and file writes stay blocked. Only explicit Gmail draft/send actions may run local gog. Credentials stay local.');
|
|
646
642
|
process.stdout.write('Allow this connection? [y/N] ');
|
|
@@ -650,5 +646,58 @@ async function requestAuthorization(siteUrl) {
|
|
|
650
646
|
if (String(answer).trim().toLowerCase() !== 'y') {
|
|
651
647
|
console.log('Codex connection cancelled.');
|
|
652
648
|
process.exit(0);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function runWindowsAuthorization(siteUrl) {
|
|
653
|
+
if (existsSync(WINDOWS_AUTHORIZATION_SCRIPT)) {
|
|
654
|
+
const result = authorizationDecision(spawnSync('powershell.exe', [
|
|
655
|
+
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
|
656
|
+
'-WindowStyle', 'Hidden', '-File', WINDOWS_AUTHORIZATION_SCRIPT, '-Site', siteUrl,
|
|
657
|
+
], { stdio: 'ignore', windowsHide: true }));
|
|
658
|
+
if (result !== null) return result;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return authorizationDecision(spawnSync('powershell.exe', [
|
|
662
|
+
'-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
|
|
663
|
+
'-WindowStyle', 'Hidden', '-Command',
|
|
664
|
+
"$ErrorActionPreference='Stop'; Add-Type -AssemblyName PresentationFramework; $site = $env:CUSTOMER_MAP_CODEX_SITE; $message = \"Customer Map wants to connect to the Codex installed on this computer.`n`nSite: $site`n`nCodex chat stays read-only. Live web search may read public sites; shell network and file writes stay blocked. Only explicit Gmail draft/send actions may run local gog. Credentials stay local.\"; $result = [System.Windows.MessageBox]::Show($message, 'Customer Map - Codex authorization', 'YesNo', 'Question', 'No'); if ($result -eq 'Yes') { exit 0 }; exit 1",
|
|
665
|
+
], { env: { ...process.env, CUSTOMER_MAP_CODEX_SITE: siteUrl }, stdio: 'ignore', windowsHide: true }));
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function runMacAuthorization(siteUrl) {
|
|
669
|
+
if (existsSync(MACOS_AUTHORIZATION_SCRIPT)) {
|
|
670
|
+
const result = macAuthorizationDecision(spawnSync('osascript', [MACOS_AUTHORIZATION_SCRIPT, siteUrl], { encoding: 'utf8' }));
|
|
671
|
+
if (result !== null) return result;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return macAuthorizationDecision(spawnSync('osascript', ['-e', [
|
|
675
|
+
'on run argv',
|
|
676
|
+
' set siteUrl to item 1 of argv',
|
|
677
|
+
' set promptText to "Customer Map wants to connect to the Codex installed on this Mac." & return & return & "Site: " & siteUrl & return & return & "Codex chat stays read-only. Live web search may read public sites; shell network and file writes stay blocked. Only explicit Gmail draft/send actions may run local gog. Credentials stay local."',
|
|
678
|
+
' display dialog promptText with title "Customer Map - Codex authorization" buttons {"Cancel", "Allow"} default button "Cancel"',
|
|
679
|
+
' if button returned of result is "Allow" then return "ALLOW"',
|
|
680
|
+
' return "CANCEL"',
|
|
681
|
+
'end run',
|
|
682
|
+
].join('\n'), siteUrl], { encoding: 'utf8' }));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function authorizationDecision(result) {
|
|
686
|
+
if (result.error || result.signal || result.status === null) return null;
|
|
687
|
+
if (result.status === 0) return true;
|
|
688
|
+
if (result.status === 1) return false;
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function macAuthorizationDecision(result) {
|
|
693
|
+
if (result.error || result.signal || result.status !== 0) return null;
|
|
694
|
+
const answer = String(result.stdout || '').trim().toUpperCase();
|
|
695
|
+
if (answer === 'ALLOW') return true;
|
|
696
|
+
if (answer === 'CANCEL') return false;
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function cancelAuthorization() {
|
|
701
|
+
console.log('Codex connection cancelled.');
|
|
702
|
+
process.exit(0);
|
|
703
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
VERSION=${1:-}
|
|
5
|
+
case "$VERSION" in
|
|
6
|
+
''|*[!0-9.]*)
|
|
7
|
+
printf '%s\n' 'Invalid Codex Bridge version.' >&2
|
|
8
|
+
exit 1
|
|
9
|
+
;;
|
|
10
|
+
esac
|
|
11
|
+
|
|
12
|
+
NPX_PATH=$(command -v npx || true)
|
|
13
|
+
if [ -z "$NPX_PATH" ]; then
|
|
14
|
+
printf '%s\n' 'Could not find npx on PATH. Install Node.js 18+ first.' >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
CODEX_PATH=$(command -v codex || true)
|
|
19
|
+
if [ -z "$CODEX_PATH" ]; then
|
|
20
|
+
printf '%s\n' 'Could not find codex on PATH. Install and sign in to Codex CLI first.' >&2
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
APP_DIR="$HOME/Applications"
|
|
25
|
+
APP_PATH="$APP_DIR/Customer Map Codex Bridge.app"
|
|
26
|
+
SOURCE_PATH=$(mktemp "${TMPDIR:-/tmp}/customer-map-codex-bridge.XXXXXX")
|
|
27
|
+
LOG_DIR="$HOME/Library/Logs/CustomerMapCodexBridge"
|
|
28
|
+
LOG_PATH="$LOG_DIR/bridge.log"
|
|
29
|
+
PACKAGE="customer-map-codex-bridge@$VERSION"
|
|
30
|
+
trap 'rm -f "$SOURCE_PATH"' EXIT
|
|
31
|
+
|
|
32
|
+
escape_applescript_string() {
|
|
33
|
+
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
NPX_APPLESCRIPT=$(escape_applescript_string "$NPX_PATH")
|
|
37
|
+
CODEX_APPLESCRIPT=$(escape_applescript_string "$CODEX_PATH")
|
|
38
|
+
PACKAGE_APPLESCRIPT=$(escape_applescript_string "$PACKAGE")
|
|
39
|
+
LOG_APPLESCRIPT=$(escape_applescript_string "$LOG_PATH")
|
|
40
|
+
|
|
41
|
+
mkdir -p "$APP_DIR" "$LOG_DIR"
|
|
42
|
+
if [ -e "$APP_PATH" ]; then
|
|
43
|
+
osascript -e 'tell application id "com.customermap.codexbridge" to quit' >/dev/null 2>&1 || true
|
|
44
|
+
BACKUP_PATH="$APP_DIR/Customer Map Codex Bridge.backup.$(date +%Y%m%d%H%M%S).app"
|
|
45
|
+
mv "$APP_PATH" "$BACKUP_PATH"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
cat > "$SOURCE_PATH" <<EOF
|
|
49
|
+
property npxPath : "$NPX_APPLESCRIPT"
|
|
50
|
+
property codexPath : "$CODEX_APPLESCRIPT"
|
|
51
|
+
property bridgePackage : "$PACKAGE_APPLESCRIPT"
|
|
52
|
+
property logPath : "$LOG_APPLESCRIPT"
|
|
53
|
+
|
|
54
|
+
on run
|
|
55
|
+
end run
|
|
56
|
+
|
|
57
|
+
on open location theUrl
|
|
58
|
+
set commandText to "nohup " & quoted form of npxPath & " -y " & quoted form of bridgePackage & " --codex " & quoted form of codexPath & " --uri " & quoted form of theUrl & " >> " & quoted form of logPath & " 2>&1 < /dev/null &"
|
|
59
|
+
do shell script commandText
|
|
60
|
+
end open location
|
|
61
|
+
|
|
62
|
+
on idle
|
|
63
|
+
return 30
|
|
64
|
+
end idle
|
|
65
|
+
EOF
|
|
66
|
+
|
|
67
|
+
osacompile -s -o "$APP_PATH" "$SOURCE_PATH"
|
|
68
|
+
PLIST="$APP_PATH/Contents/Info.plist"
|
|
69
|
+
/usr/libexec/PlistBuddy -c "Add :CFBundleIdentifier string com.customermap.codexbridge" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier com.customermap.codexbridge" "$PLIST"
|
|
70
|
+
/usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string Customer Map Codex Bridge" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName Customer Map Codex Bridge" "$PLIST"
|
|
71
|
+
/usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string $VERSION" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$PLIST"
|
|
72
|
+
/usr/libexec/PlistBuddy -c "Add :CFBundleVersion string $VERSION" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $VERSION" "$PLIST"
|
|
73
|
+
/usr/libexec/PlistBuddy -c "Add :LSUIElement bool true" "$PLIST" 2>/dev/null || /usr/libexec/PlistBuddy -c "Set :LSUIElement true" "$PLIST"
|
|
74
|
+
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes array" "$PLIST"
|
|
75
|
+
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0 dict" "$PLIST"
|
|
76
|
+
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLName string com.customermap.codexbridge" "$PLIST"
|
|
77
|
+
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes array" "$PLIST"
|
|
78
|
+
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string customer-map-codex" "$PLIST"
|
|
79
|
+
|
|
80
|
+
LSREGISTER='/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister'
|
|
81
|
+
"$LSREGISTER" -f "$APP_PATH"
|
|
82
|
+
open -gj "$APP_PATH"
|
|
83
|
+
|
|
84
|
+
printf '%s\n' 'Customer Map Codex click-to-authorize is installed for this macOS account.'
|
|
85
|
+
printf '%s\n' 'Return to Customer Map and click Open Codex authorization again.'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory = $true)]
|
|
3
|
+
[ValidatePattern('^\d+\.\d+\.\d+$')]
|
|
4
|
+
[string]$Version
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
$ErrorActionPreference = 'Stop'
|
|
8
|
+
|
|
9
|
+
$npxPath = (Get-Command npx.cmd -ErrorAction Stop).Source
|
|
10
|
+
$codexPath = (Get-Command codex.cmd -ErrorAction Stop).Source
|
|
11
|
+
$powershellPath = (Get-Command powershell.exe -ErrorAction Stop).Source
|
|
12
|
+
$installDirectory = Join-Path $env:LOCALAPPDATA 'CustomerMapCodexBridge'
|
|
13
|
+
$launcherPath = Join-Path $installDirectory 'launch.ps1'
|
|
14
|
+
$logPath = Join-Path $installDirectory 'bridge.log'
|
|
15
|
+
$protocolKey = 'HKCU:\Software\Classes\customer-map-codex'
|
|
16
|
+
$commandKey = Join-Path $protocolKey 'shell\open\command'
|
|
17
|
+
$package = "customer-map-codex-bridge@$Version"
|
|
18
|
+
|
|
19
|
+
New-Item -ItemType Directory -Path $installDirectory -Force | Out-Null
|
|
20
|
+
|
|
21
|
+
$launcher = @"
|
|
22
|
+
param(
|
|
23
|
+
[Parameter(Mandatory = `$true, Position = 0)]
|
|
24
|
+
[string]`$Uri
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
`$ErrorActionPreference = 'Stop'
|
|
28
|
+
`$logDirectory = '$($installDirectory.Replace("'", "''"))'
|
|
29
|
+
`$logPath = '$($logPath.Replace("'", "''"))'
|
|
30
|
+
New-Item -ItemType Directory -Path `$logDirectory -Force | Out-Null
|
|
31
|
+
& '$($npxPath.Replace("'", "''"))' '-y' '$package' '--codex' '$($codexPath.Replace("'", "''"))' '--uri' `$Uri *>> `$logPath
|
|
32
|
+
exit `$LASTEXITCODE
|
|
33
|
+
"@
|
|
34
|
+
|
|
35
|
+
Set-Content -LiteralPath $launcherPath -Value $launcher -Encoding UTF8
|
|
36
|
+
|
|
37
|
+
New-Item -Path $commandKey -Force | Out-Null
|
|
38
|
+
Set-ItemProperty -Path $protocolKey -Name '(default)' -Value 'URL:Customer Map Codex Bridge'
|
|
39
|
+
Set-ItemProperty -Path $protocolKey -Name 'URL Protocol' -Value ''
|
|
40
|
+
Set-ItemProperty -Path $commandKey -Name '(default)' -Value ('"{0}" -NoLogo -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "{1}" "%1"' -f $powershellPath, $launcherPath)
|
|
41
|
+
|
|
42
|
+
Write-Host 'Customer Map Codex click-to-authorize is installed for this Windows account.'
|
|
43
|
+
Write-Host 'Return to Customer Map and click Open Codex authorization again.'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "customer-map-codex-bridge",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Connect a locally authenticated Codex CLI to Customer Map.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
"index.mjs",
|
|
11
11
|
"codex-process.mjs",
|
|
12
12
|
"mail-action.mjs",
|
|
13
|
-
"
|
|
13
|
+
"protocol-install.mjs",
|
|
14
|
+
"install-protocol-windows.ps1",
|
|
15
|
+
"install-protocol-macos.sh",
|
|
16
|
+
"authorization-windows.ps1",
|
|
17
|
+
"authorization-macos.applescript",
|
|
18
|
+
"README.md"
|
|
14
19
|
],
|
|
15
20
|
"engines": {
|
|
16
21
|
"node": ">=18"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const BRIDGE_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
export function installCodexProtocolHandler(version) {
|
|
9
|
+
if (!/^\d+\.\d+\.\d+$/.test(String(version || ''))) {
|
|
10
|
+
throw new Error('Invalid Codex Bridge version for protocol installation');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (process.platform === 'win32') {
|
|
14
|
+
runInstaller('powershell.exe', [
|
|
15
|
+
'-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass',
|
|
16
|
+
'-File', join(BRIDGE_DIRECTORY, 'install-protocol-windows.ps1'),
|
|
17
|
+
'-Version', version,
|
|
18
|
+
]);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (process.platform === 'darwin') {
|
|
23
|
+
runInstaller('/bin/sh', [join(BRIDGE_DIRECTORY, 'install-protocol-macos.sh'), version]);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
throw new Error('Click-to-authorize installation currently supports Windows and macOS.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function runInstaller(command, args) {
|
|
31
|
+
const result = spawnSync(command, args, { stdio: 'inherit', windowsHide: false });
|
|
32
|
+
if (result.error) throw result.error;
|
|
33
|
+
if (result.signal) throw new Error(`Protocol installer stopped by ${result.signal}`);
|
|
34
|
+
if (result.status !== 0) throw new Error(`Protocol installer exited (${result.status ?? 'unknown'})`);
|
|
35
|
+
}
|