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 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.2 \
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.2 \
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.2 \
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.2';
14
- const RELAY_RECONNECT_BASE_MS = 1_000;
15
- const RELAY_RECONNECT_MAX_MS = 30_000;
16
-
17
- const args = parseArgs(process.argv.slice(2));
18
- if (args.help) {
19
- console.log('Usage: customer-map-codex-bridge --site <Customer Map URL> --code <one-time code> [--cwd <folder>] [--codex <path>] [--gog <path>]');
20
- process.exit(0);
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 dialog = spawnSync('powershell.exe', [
617
- '-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
618
- '-WindowStyle', 'Hidden', '-Command',
619
- "$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",
620
- ], { env: { ...process.env, CUSTOMER_MAP_CODEX_SITE: siteUrl }, stdio: 'ignore', windowsHide: true });
621
- if (!dialog.error) {
622
- if (dialog.status === 0) return;
623
- console.log('Codex connection cancelled.');
624
- process.exit(0);
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.2",
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
- "README.md"
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
+ }