@minhspark/codex-mcp-bridge 1.27.2 → 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.
@@ -109,7 +109,7 @@ const LIVE_CHECKS = {
109
109
  },
110
110
  codex: {
111
111
  senderDestination: "Each Claude delivery separately verifies the Codex sender, the recipient session, its permission class and the accounts. A project grant does not replace these checks. Check claude_bridge_status in the calling task.",
112
- creationTarget: "Creating a new Claude session is confirmed separately through read_claude_creation; this inspection does not open or check one.",
112
+ creationTarget: "The bridge does not create Claude sessions. The user opens a new session in Claude Desktop; this inspection does not open or check one.",
113
113
  },
114
114
  };
115
115
 
@@ -1,102 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { promisify } from "node:util";
5
- import { setTimeout as delay } from "node:timers/promises";
6
-
7
- const execute = promisify(execFile);
8
- const helperPath = fileURLToPath(new URL("./claude-composer-submit.ps1", import.meta.url));
9
-
10
- export async function trustClaudeCodeComposer({ cwd, prompt }, options = {}) {
11
- const platform = options.platform ?? process.platform;
12
- if (platform !== "win32") return { status: "blocked", reason: "native_workspace_trust_unsupported", platform };
13
- if (typeof cwd !== "string" || !path.win32.isAbsolute(cwd)) return { status: "blocked", reason: "absolute_project_path_required" };
14
- const payload = Buffer.from(JSON.stringify({ cwd, prompt }), "utf8").toString("base64");
15
- try {
16
- const result = await (options.run ?? execute)("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", helperPath, "-PayloadBase64", payload, "-Action", "Trust", "-WaitMilliseconds", "8000"], { windowsHide: true, timeout: 12000, encoding: "utf8", maxBuffer: 1024 * 1024 });
17
- const trusted = JSON.parse(result.stdout.trim().replace(/^\uFEFF/, ""));
18
- if (!["trusted", "absent", "blocked", "uncertain"].includes(trusted?.status)) return { status: "uncertain", reason: "native_workspace_trust_outcome_unknown" };
19
- return trusted;
20
- } catch { return { status: "uncertain", reason: "native_workspace_trust_outcome_unknown" }; }
21
- }
22
-
23
- export function assessClaudeCodeComposer(snapshot, { prompt, cwd }) {
24
- const block = (reason) => ({ status: "blocked", reason });
25
- if (!snapshot || !Array.isArray(snapshot.windows) || snapshot.windows.length !== 1) {
26
- return block("desktop_window_ambiguous_or_missing");
27
- }
28
- const window = snapshot.windows[0];
29
- if (window.installedDesktop !== true) return block("desktop_process_not_verified");
30
- if (snapshot.dialogs?.length) return block("desktop_dialog_requires_attention");
31
- if (snapshot.codeSelected !== true) return block("code_mode_not_selected");
32
- if (!Array.isArray(snapshot.composers) || snapshot.composers.length !== 1) {
33
- return block("composer_ambiguous_or_missing");
34
- }
35
- const composerText = (text) => typeof text === "string" ? text.replace(/\r\n/g, "\n").split("\n").filter((line) => line !== "").join("\n") : null;
36
- if (composerText(snapshot.composers[0].text) !== composerText(prompt)) return block("composer_prompt_mismatch");
37
- if (snapshot.worktreeEnabled !== false) return block("original_project_folder_not_verified");
38
- if (snapshot.folderCount !== 1 || snapshot.localMode !== true) return block("project_folder_ambiguous_or_remote");
39
- if (!Array.isArray(snapshot.selectedFolderPaths) || snapshot.selectedFolderPaths.length !== 1) {
40
- return block("selected_project_path_not_verified");
41
- }
42
- const normalize = (value) => path.win32.normalize(value).replace(/[\\/]+$/, "").toLowerCase();
43
- if (typeof snapshot.selectedFolderPaths[0] !== "string" || !path.win32.isAbsolute(snapshot.selectedFolderPaths[0]) || normalize(snapshot.selectedFolderPaths[0]) !== normalize(cwd)) {
44
- return block("selected_project_path_mismatch");
45
- }
46
- if (snapshot.sendButtons?.length !== 1 || snapshot.sendButtons[0].enabled !== true || snapshot.sendButtons[0].invokable !== true) {
47
- return block("send_button_ambiguous_disabled_or_unavailable");
48
- }
49
- return { status: "ready", reason: "exact_composer_and_project_verified", windowId: window.id };
50
- }
51
-
52
- export async function submitClaudeCodeComposer({ prompt, cwd, beforeSubmit }, options = {}) {
53
- const platform = options.platform ?? process.platform;
54
- if (typeof prompt !== "string" || !prompt.length || typeof cwd !== "string" || !cwd.length) {
55
- return { status: "blocked", reason: "prompt_and_project_required" };
56
- }
57
- if (platform !== "win32") {
58
- return { status: "blocked", reason: "native_composer_submission_unsupported", platform };
59
- }
60
- if (!path.win32.isAbsolute(cwd)) return { status: "blocked", reason: "absolute_project_path_required" };
61
- const run = options.run ?? execute;
62
- const requestedBudget = options.timeoutMs ?? 20000;
63
- const budget = Number.isFinite(requestedBudget) ? Math.max(1000, Math.min(20000, requestedBudget)) : 20000;
64
- const started = Date.now();
65
- const payload = Buffer.from(JSON.stringify({ prompt, cwd }), "utf8").toString("base64");
66
- let attempted = false;
67
- try {
68
- const call = async (action, windowId) => {
69
- const remaining = budget - (Date.now() - started);
70
- if (remaining <= 0) throw new Error("Native composer deadline exceeded");
71
- const args = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", helperPath,
72
- "-PayloadBase64", payload, "-Action", action, "-WaitMilliseconds", String(Math.min(8000, remaining - 250))];
73
- if (windowId != null) args.push("-ExpectedWindowId", String(windowId));
74
- const result = await run("powershell.exe", args, {
75
- windowsHide: true, timeout: remaining, maxBuffer: 1024 * 1024, encoding: "utf8",
76
- });
77
- return JSON.parse(result.stdout.trim().replace(/^\uFEFF/, ""));
78
- };
79
- const inspection = await call("Inspect");
80
- if (inspection.status === "blocked") return { ...inspection, submissionAttempted: false };
81
- const assessment = assessClaudeCodeComposer(inspection.snapshot, { prompt, cwd });
82
- if (assessment.status !== "ready") return { ...assessment, submissionAttempted: false };
83
- if (beforeSubmit) await beforeSubmit();
84
- const selection = await call("SelectFolder", assessment.windowId);
85
- if (selection.status !== "selected") return { status: "blocked", reason: selection.reason ?? "native_folder_selection_not_confirmed", submissionAttempted: false };
86
- await (options.sleep ?? delay)(2000);
87
- if (beforeSubmit) await beforeSubmit();
88
- attempted = true;
89
- const result = await call("Submit", assessment.windowId);
90
- if (!["submitted", "blocked", "uncertain"].includes(result.status)) {
91
- return { status: "uncertain", reason: "native_submit_result_invalid", submissionAttempted: true };
92
- }
93
- return { ...result, submissionAttempted: result.status !== "blocked" };
94
- } catch (error) {
95
- return {
96
- status: attempted ? "uncertain" : "blocked",
97
- reason: attempted ? "native_submit_outcome_unknown" : "native_accessibility_unavailable",
98
- submissionAttempted: attempted,
99
- error: error.code ?? error.name ?? "Error",
100
- };
101
- }
102
- }
@@ -1,211 +0,0 @@
1
- param(
2
- [Parameter(Mandatory = $true)][string]$PayloadBase64,
3
- [ValidateSet('Inspect', 'SelectFolder', 'Submit', 'Trust')][string]$Action = 'Inspect',
4
- [int]$WaitMilliseconds = 8000,
5
- [long]$ExpectedWindowId = 0
6
- )
7
-
8
- $ErrorActionPreference = 'Stop'
9
- $ProgressPreference = 'SilentlyContinue'
10
- [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
11
- Add-Type -AssemblyName UIAutomationClient
12
- Add-Type -AssemblyName UIAutomationTypes
13
- $request = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($PayloadBase64)) | ConvertFrom-Json
14
- $deadline = [DateTime]::UtcNow.AddMilliseconds([Math]::Max(0, [Math]::Min(8000, $WaitMilliseconds)))
15
- $invocationStarted = $false
16
-
17
- function Finish([string]$Status, [string]$Reason, $Snapshot = $null) {
18
- $result = @{ status = $Status; reason = $Reason }
19
- if ($null -ne $Snapshot) { $result.snapshot = $Snapshot }
20
- $result | ConvertTo-Json -Depth 8 -Compress
21
- exit 0
22
- }
23
-
24
- function Descendants($Element) {
25
- return $Element.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition)
26
- }
27
-
28
- function Dialogs($Element) {
29
- for ($attempt = 0; $attempt -lt 3; $attempt++) {
30
- try {
31
- @(Descendants $Element | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Window -or $_.GetCurrentPropertyValue([System.Windows.Automation.AutomationElement]::IsDialogProperty) -eq $true })
32
- return
33
- } catch {
34
- if ($attempt -eq 2) { throw }
35
- Start-Sleep -Milliseconds 150
36
- }
37
- }
38
- }
39
-
40
- function TextValue($Element) {
41
- $pattern = $null
42
- if ($Element.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$pattern)) {
43
- return ([System.Windows.Automation.ValuePattern]$pattern).Current.Value
44
- }
45
- if ($Element.TryGetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern, [ref]$pattern)) {
46
- return ([System.Windows.Automation.TextPattern]$pattern).DocumentRange.GetText(-1)
47
- }
48
- return $null
49
- }
50
-
51
- function IsInstalledDesktop($Process) {
52
- try {
53
- $exe = Get-Item -LiteralPath $Process.Path
54
- if ($exe.VersionInfo.ProductName -ne 'Claude' -or $exe.VersionInfo.CompanyName -ne 'Anthropic') { return $false }
55
- $localRoot = Join-Path $env:LOCALAPPDATA 'AnthropicClaude'
56
- $root = [IO.Path]::GetFullPath($localRoot).TrimEnd('\') + '\'
57
- if ($exe.FullName.StartsWith($root, [StringComparison]::OrdinalIgnoreCase) -and $exe.Directory.Name -match '^app-\d+(\.\d+)+$' -and $exe.Name -ieq 'claude.exe') { return $true }
58
- foreach ($package in @(Get-AppxPackage -Name '*Claude*' -ErrorAction SilentlyContinue)) {
59
- if ($package.InstallLocation -and $exe.FullName.StartsWith($package.InstallLocation.TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase)) { return $true }
60
- }
61
- } catch {}
62
- return $false
63
- }
64
-
65
- function PathEqual([string]$First, [string]$Second) {
66
- try {
67
- if ($First -notmatch '^(?:[A-Za-z]:[\\/]|\\\\[^\\]+\\[^\\]+)') { return $false }
68
- return [IO.Path]::GetFullPath($First).TrimEnd('\').Equals([IO.Path]::GetFullPath($Second).TrimEnd('\'), [StringComparison]::OrdinalIgnoreCase)
69
- } catch { return $false }
70
- }
71
-
72
- function PromptEqual([string]$First, [string]$Second) {
73
- $firstLines = @($First.Replace("`r`n", "`n").Split("`n") | Where-Object { $_ -cne '' })
74
- $secondLines = @($Second.Replace("`r`n", "`n").Split("`n") | Where-Object { $_ -cne '' })
75
- return ($firstLines -join "`n") -ceq ($secondLines -join "`n")
76
- }
77
-
78
- function SelectedPaths($Folder, $Root) {
79
- $expander = [System.Windows.Automation.ExpandCollapsePattern]$Folder.GetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern)
80
- try {
81
- if ($expander.Current.ExpandCollapseState -eq [System.Windows.Automation.ExpandCollapseState]::Collapsed) { $expander.Expand() }
82
- Start-Sleep -Milliseconds 150
83
- $menus = @(Descendants $Root | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Menu -and $_.Current.Name -eq $Folder.Current.Name -and -not $_.Current.IsOffscreen })
84
- if ($menus.Count -ne 1) { throw 'Folder menu not verified' }
85
- foreach ($radio in @(Descendants $menus[0] | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::RadioButton })) {
86
- $selection = [System.Windows.Automation.SelectionItemPattern]$radio.GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern)
87
- if ($selection.Current.IsSelected) { $radio.Current.HelpText }
88
- }
89
- } finally {
90
- if ($expander.Current.ExpandCollapseState -eq [System.Windows.Automation.ExpandCollapseState]::Expanded) { $expander.Collapse() }
91
- Start-Sleep -Milliseconds 250
92
- }
93
- }
94
-
95
- try {
96
- do {
97
- $processes = @(Get-Process -Name Claude -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 -and (IsInstalledDesktop $_) })
98
- if ($processes.Count -ne 1) { Finish 'blocked' 'desktop_window_ambiguous_or_missing' }
99
- $process = $processes[0]
100
- $windowId = $process.MainWindowHandle.ToInt64()
101
- if ($ExpectedWindowId -ne 0 -and $ExpectedWindowId -ne $windowId) { Finish 'blocked' 'desktop_window_changed' }
102
- $root = [System.Windows.Automation.AutomationElement]::FromHandle($process.MainWindowHandle)
103
- $elements = Descendants $root
104
- $dialogs = @(Dialogs $root)
105
- if ($Action -eq 'Trust') {
106
- if ($dialogs.Count -gt 0) {
107
- if ($dialogs.Count -ne 1 -or $dialogs[0].Current.Name -cne 'Trust this workspace?') { Finish 'blocked' 'workspace_trust_dialog_not_verified' }
108
- $trustElements = Descendants $dialogs[0]
109
- $trustPaths = @($trustElements | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Text -and (PathEqual $_.Current.Name $request.cwd) })
110
- $trustButtons = @($trustElements | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Button -and $_.Current.Name -ceq 'Trust workspace' -and $_.Current.IsEnabled -and -not $_.Current.IsOffscreen })
111
- if ($trustPaths.Count -ne 1 -or $trustButtons.Count -ne 1) { Finish 'blocked' 'workspace_trust_path_mismatch_or_ambiguous' }
112
- $trustInvoke = $null
113
- if (-not $trustButtons[0].TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern, [ref]$trustInvoke)) { Finish 'blocked' 'workspace_trust_button_unavailable' }
114
- $invocationStarted = $true
115
- ([System.Windows.Automation.InvokePattern]$trustInvoke).Invoke()
116
- do {
117
- Start-Sleep -Milliseconds 150
118
- $remainingTrust = @(Descendants $root | Where-Object { $_.Current.Name -ceq 'Trust this workspace?' -and $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Window })
119
- if ($remainingTrust.Count -eq 0) { Finish 'trusted' 'exact_native_workspace_trust_confirmed' }
120
- } while ([DateTime]::UtcNow -lt $deadline)
121
- Finish 'uncertain' 'native_workspace_trust_outcome_unknown'
122
- }
123
- }
124
- if ($dialogs.Count -gt 0) { Finish 'blocked' 'desktop_dialog_requires_attention' }
125
- $composers = @($elements | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Edit -and $_.Current.Name -eq 'Prompt' -and -not $_.Current.IsOffscreen })
126
- if ($composers.Count -gt 1) { Finish 'blocked' 'composer_ambiguous_or_missing' }
127
- if ($composers.Count -eq 1 -and (PromptEqual (TextValue $composers[0]) $request.prompt)) { break }
128
- if ([DateTime]::UtcNow -ge $deadline) { Finish 'blocked' 'composer_prompt_mismatch' }
129
- Start-Sleep -Milliseconds 150
130
- } while ($true)
131
-
132
- $codeControls = @($elements | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::RadioButton -and $_.Current.Name -eq 'Code' -and -not $_.Current.IsOffscreen })
133
- if ($codeControls.Count -ne 1) { Finish 'blocked' 'code_mode_not_selected' }
134
- $codeSelected = ([System.Windows.Automation.SelectionItemPattern]$codeControls[0].GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern)).Current.IsSelected
135
- if (-not $codeSelected) { Finish 'blocked' 'code_mode_not_selected' }
136
- $pane = $composers[0]
137
- $walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
138
- for ($level = 0; $level -lt 12; $level++) {
139
- $pane = $walker.GetParent($pane)
140
- if ($null -eq $pane) { Finish 'blocked' 'composer_scope_unavailable' }
141
- if ($pane.Current.ClassName -match '(?:^|\s)dframe-pane-primary(?:\s|$)') { break }
142
- }
143
- if ($pane.Current.ClassName -notmatch '(?:^|\s)dframe-pane-primary(?:\s|$)') { Finish 'blocked' 'composer_scope_unavailable' }
144
- $scope = Descendants $pane
145
- $pillButtons = @($scope | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Button -and $_.Current.ClassName -like '*pill-h*' -and -not $_.Current.IsOffscreen })
146
- $localMode = @($pillButtons | Where-Object { $_.Current.Name -eq 'Local' }).Count -eq 1
147
- $folders = @($pillButtons | Where-Object { $_.Current.Name -notin @('Local', 'Add another folder') })
148
- if (-not $localMode -or $folders.Count -ne 1) { Finish 'blocked' 'project_folder_ambiguous_or_remote' }
149
- $worktrees = @($scope | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::CheckBox -and $_.Current.Name -eq 'worktree' })
150
- $worktreeEnabled = $false
151
- if ($worktrees.Count -gt 1) { Finish 'blocked' 'original_project_folder_not_verified' }
152
- if ($worktrees.Count -eq 1) { $worktreeEnabled = ([System.Windows.Automation.TogglePattern]$worktrees[0].GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern)).Current.ToggleState -ne [System.Windows.Automation.ToggleState]::Off }
153
- if ($worktreeEnabled) { Finish 'blocked' 'original_project_folder_not_verified' }
154
-
155
- $folder = $folders[0]
156
- $paths = @(SelectedPaths $folder $root)
157
- if ($paths.Count -ne 1 -or -not (PathEqual $paths[0] $request.cwd)) { Finish 'blocked' 'selected_project_path_mismatch' }
158
- $scope = Descendants $pane
159
- $sends = @($scope | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Button -and $_.Current.Name -eq 'Send' -and -not $_.Current.IsOffscreen })
160
- if ($sends.Count -ne 1 -or -not $sends[0].Current.IsEnabled) { Finish 'blocked' 'send_button_ambiguous_disabled_or_unavailable' }
161
- $invoke = $null
162
- if (-not $sends[0].TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern, [ref]$invoke)) { Finish 'blocked' 'send_button_ambiguous_disabled_or_unavailable' }
163
- $currentPaths = @(SelectedPaths $folder $root)
164
- if ($currentPaths.Count -ne 1 -or -not (PathEqual $currentPaths[0] $request.cwd) -or -not (PromptEqual (TextValue $composers[0]) $request.prompt)) { Finish 'blocked' 'composer_changed_before_submission' }
165
- $currentDialogs = @(Dialogs $root)
166
- if ($currentDialogs.Count -gt 0) { Finish 'blocked' 'desktop_dialog_requires_attention' }
167
- if (-not ([System.Windows.Automation.SelectionItemPattern]$codeControls[0].GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern)).Current.IsSelected) { Finish 'blocked' 'code_mode_not_selected' }
168
- if ($worktrees.Count -eq 1 -and ([System.Windows.Automation.TogglePattern]$worktrees[0].GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern)).Current.ToggleState -ne [System.Windows.Automation.ToggleState]::Off) { Finish 'blocked' 'original_project_folder_not_verified' }
169
- $snapshot = @{
170
- windows = @(@{ id = $windowId; installedDesktop = $true })
171
- dialogs = @()
172
- codeSelected = $true
173
- composers = @(@{ text = (TextValue $composers[0]) })
174
- folderCount = $folders.Count
175
- localMode = $localMode
176
- worktreeEnabled = $worktreeEnabled
177
- selectedFolderPaths = @($paths)
178
- sendButtons = @(@{ enabled = $true; invokable = $true })
179
- }
180
- if ($Action -eq 'Inspect') { Finish 'ready' 'exact_composer_and_project_verified' $snapshot }
181
- if ($Action -eq 'Trust') { Finish 'absent' 'exact_composer_and_project_verified' $snapshot }
182
- if ($Action -eq 'SelectFolder') {
183
- $expander = [System.Windows.Automation.ExpandCollapsePattern]$folder.GetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern)
184
- $selectionInvoked = $false
185
- try {
186
- $expander.Expand()
187
- Start-Sleep -Milliseconds 150
188
- $menus = @(Descendants $root | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Menu -and $_.Current.Name -eq $folder.Current.Name -and -not $_.Current.IsOffscreen })
189
- if ($menus.Count -ne 1) { throw 'Folder menu not verified' }
190
- $choices = @(Descendants $menus[0] | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::RadioButton -and (PathEqual $_.Current.HelpText $request.cwd) -and $_.Current.IsEnabled -and -not $_.Current.IsOffscreen })
191
- if ($choices.Count -ne 1) { throw 'Folder choice not verified' }
192
- $choiceInvoke = $null
193
- if (-not $choices[0].TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern, [ref]$choiceInvoke)) { throw 'Folder choice unavailable' }
194
- if (-not (PromptEqual (TextValue $composers[0]) $request.prompt)) { throw 'Composer changed' }
195
- ([System.Windows.Automation.InvokePattern]$choiceInvoke).Invoke()
196
- $selectionInvoked = $true
197
- } finally {
198
- try {
199
- if ($expander.Current.ExpandCollapseState -eq [System.Windows.Automation.ExpandCollapseState]::Expanded) { $expander.Collapse() }
200
- Start-Sleep -Milliseconds 250
201
- } catch { if (-not $selectionInvoked) { throw } }
202
- }
203
- Finish 'selected' 'exact_native_folder_explicitly_selected'
204
- }
205
- $invocationStarted = $true
206
- ([System.Windows.Automation.InvokePattern]$invoke).Invoke()
207
- Finish 'submitted' 'native_send_invoked_once'
208
- } catch {
209
- if ($invocationStarted) { Finish 'uncertain' 'native_submit_outcome_unknown' }
210
- Finish 'blocked' 'native_accessibility_unavailable'
211
- }
@@ -1,306 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
-
4
- const REQUEST_ID = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{32,128})$/i;
5
- const PENDING = new Set(["awaiting_user", "awaiting_session", "submission_uncertain", "launch_uncertain", "ambiguous", "awaiting_project_confirmation"]);
6
- const STATUSES = new Set([...PENDING, "created", "abandoned"]);
7
- const MAX_REQUESTS = 32;
8
- const MAX_TASKS = 8192;
9
-
10
- function fail(code, message) {
11
- throw Object.assign(new Error(message), { code });
12
- }
13
-
14
- function string(value, max = 1024) {
15
- return typeof value === "string" && value.length > 0 && value.length <= max && !value.includes("\0");
16
- }
17
-
18
- function accountIdentity(account) {
19
- if (account?.status !== "verified" || !string(account.fingerprint) || !string(account.root, 8192)) {
20
- fail("CLAUDE_ACCOUNT_UNVERIFIED", "The current Claude Desktop account must be verified.");
21
- }
22
- return { fingerprint: account.fingerprint, root: account.root };
23
- }
24
-
25
- function sameAccount(left, right) {
26
- return left.fingerprint === right.fingerprint && left.root === right.root;
27
- }
28
-
29
- function augmentedPrompt(prompt, requestId) {
30
- if (!string(prompt, 14000) || !prompt.trim()) fail("CLAUDE_CREATION_PROMPT_INVALID", "Provide a nonempty prompt of at most 14000 characters.");
31
- const submitted = `${prompt.trim()}\n\n[Codex creation request: ${requestId}]`;
32
- if (submitted.length > 14000) fail("CLAUDE_CREATION_PROMPT_TOO_LONG", "The prompt including its correlation marker exceeds 14000 characters.");
33
- return submitted;
34
- }
35
-
36
- function matchesInitialMessage(text, expected) {
37
- if (text === expected) return true;
38
- const normalized = (value) => typeof value === "string" ? value.replace(/\r\n/g, "\n").split("\n").filter((line) => line !== "").join("\n") : null;
39
- if (normalized(text) === normalized(expected)) return true;
40
- if (typeof text !== "string" || !/^<system-reminder>\r?\nThe user started this session without choosing a project folder,/.test(text)) return false;
41
- const closing = text.indexOf("</system-reminder>");
42
- if (closing < 0 || closing > 32768) return false;
43
- const body = text.slice("<system-reminder>".length, closing);
44
- if (body.includes("<system-reminder") || body.includes("</system-reminder")) return false;
45
- const suffix = text.slice(closing + "</system-reminder>".length);
46
- const separator = /^\r?\n\r?\n/.exec(suffix);
47
- return separator !== null && normalized(suffix.slice(separator[0].length)) === normalized(expected);
48
- }
49
-
50
- function receipt(request) {
51
- const submitted = request.status === "created" || request.submissionObserved === true;
52
- return structuredClone({ requestId: request.requestId, status: request.status, cwd: request.cwd,
53
- // false means no verified observation, not proof that Desktop did not submit.
54
- promptSubmitted: submitted, submissionStatus: submitted ? "observed" : "not_observed",
55
- reason: request.reason, ...(request.observedCwd ? { observedCwd: request.observedCwd } : {}), ...(request.sessionId ? { sessionId: request.sessionId, taskId: request.taskId, title: request.title } : {}),
56
- ...(PENDING.has(request.status) ? { directoryVerification: {
57
- required: true, expectedCwd: request.cwd, verified: false,
58
- detail: "The prefilled folder chip is not evidence of the actual session directory. Claude Desktop may create a No folder scratch session. An unobserved submission may be outside the authorized discovery scope; do not expand scope or resend to find it. Inspect Desktop before taking further action. For a fresh setup, prefer the intended project's sidebar New session button, verify the actual cwd with a read-only bootstrap, then dispatch work to the verified task. Do not replace a pending request without explicit cancellation.",
59
- } } : {}) });
60
- }
61
-
62
- export class ClaudeSessionCreation {
63
- constructor({ open, submit, trust, settle = async () => {}, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, resolveProject, realpath = (directory) => {
64
- const canonical = fs.realpathSync.native(directory);
65
- if (!fs.statSync(canonical).isDirectory()) throw new Error("Not a directory");
66
- return canonical;
67
- }, platform = process.platform }) {
68
- for (const [name, dependency] of Object.entries({ open, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, realpath })) {
69
- if (typeof dependency !== "function") throw new TypeError(`${name} must be a function`);
70
- }
71
- Object.assign(this, { open, submit, trust, settle, listTasks, listSessions, readContext, readTranscript, readAccount, assertProcess, resolveProject, realpath, platform });
72
- this.requests = new Map();
73
- this.queue = Promise.resolve();
74
- }
75
-
76
- async canonical(directory) {
77
- const paths = this.platform === "win32" ? path.win32 : path.posix;
78
- if (!string(directory, 8192) || !paths.isAbsolute(directory)) fail("CLAUDE_CREATION_CWD_INVALID", "The project directory must be an existing absolute directory.");
79
- let canonical;
80
- try { canonical = await this.realpath(directory); } catch { fail("CLAUDE_CREATION_CWD_INVALID", "The project directory does not exist or cannot be resolved."); }
81
- if (!string(canonical, 8192) || !paths.isAbsolute(canonical)) fail("CLAUDE_CREATION_CWD_INVALID", "The canonical project directory is invalid.");
82
- return canonical;
83
- }
84
-
85
- sameCwd(left, right) {
86
- if (typeof left !== "string" || typeof right !== "string") return false;
87
- return this.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
88
- }
89
-
90
- async checkAccount(account, expected) {
91
- const provided = accountIdentity(account);
92
- const current = accountIdentity(await this.readAccount());
93
- if (!sameAccount(provided, current) || (expected && !sameAccount(provided, expected))) fail("CLAUDE_CREATION_ACCOUNT_CHANGED", "Claude Desktop account changed; this creation request cannot be used in another account.");
94
- return provided;
95
- }
96
-
97
- start(args) {
98
- const pending = this.queue.then(() => this.startLocked(args));
99
- this.queue = pending.catch(() => {});
100
- return pending;
101
- }
102
-
103
- async startLocked({ requestId, cwd, prompt, account, senderThreadId, beforeOpen, autoSubmit = false, trustProject = false }) {
104
- if (!REQUEST_ID.test(requestId ?? "") || typeof requestId !== "string") fail("CLAUDE_CREATION_REQUEST_INVALID", "requestId must be a UUID or 32–128 hexadecimal characters.");
105
- if (!string(senderThreadId, 256)) fail("CLAUDE_CREATION_SENDER_INVALID", "A verified sender thread identity is required.");
106
- const identity = await this.checkAccount(account);
107
- const canonical = await this.canonical(cwd);
108
- const initialMessage = augmentedPrompt(prompt, requestId);
109
- const prior = this.requests.get(requestId);
110
- if (prior) {
111
- if (!sameAccount(identity, prior.account) || prior.senderThreadId !== senderThreadId || !this.sameCwd(prior.cwd, canonical) || prior.prompt !== prompt) {
112
- fail("CLAUDE_CREATION_REQUEST_CONFLICT", "This requestId is already bound to a different creation payload.");
113
- }
114
- if (autoSubmit && !prior.submissionAttempted && ["awaiting_user", "launch_uncertain"].includes(prior.status)) {
115
- const observed = await this.inspect(requestId, { account, senderThreadId });
116
- if (observed.promptSubmitted) return observed;
117
- return this.completeRequest(prior, { account, senderThreadId, beforeOpen, trustProject });
118
- }
119
- return receipt(prior);
120
- }
121
- if (this.requests.size >= MAX_REQUESTS) fail("CLAUDE_CREATION_LIMIT", "The creation receipt limit has been reached; existing requests remain available for inspection.");
122
- for (const existing of this.requests.values()) {
123
- const composerReleased = existing.status === "awaiting_project_confirmation" && existing.submissionObserved === true;
124
- if (sameAccount(existing.account, identity) && PENDING.has(existing.status) && !composerReleased) fail("CLAUDE_CREATION_PENDING", "A creation request is already pending for this account; inspect or explicitly abandon that request before opening another project in the shared composer.");
125
- }
126
- const tasks = await this.listTasks(account);
127
- const validTasks = value => Array.isArray(value) && value.length <= MAX_TASKS && value.every(task => string(task?.taskId, 256) && (task.fileTaskId === undefined || task.fileTaskId === task.taskId)) && new Set(value.map(task => task.taskId)).size === value.length;
128
- if (!validTasks(tasks)) fail("CLAUDE_CREATION_TASKS_INVALID", "The existing native task baseline cannot be verified.");
129
- const baseline = new Set(tasks.map(task => task.taskId));
130
- const projectCwd = async task => this.resolveProject ? this.resolveProject(task) : this.canonical(task.cwd);
131
- let existingProject;
132
- for (const task of tasks) {
133
- if (task.isArchived !== false) continue;
134
- try { if (this.sameCwd(await projectCwd(task), canonical)) { existingProject = task; break; } } catch {}
135
- }
136
- if (!existingProject) fail("CLAUDE_CREATION_PROJECT_NOT_FOUND", "The canonical directory must already belong to a nonarchived Claude Desktop task.");
137
- if (autoSubmit && typeof this.submit !== "function") fail("CLAUDE_CREATION_SUBMIT_UNAVAILABLE", "Native composer submission is not available.");
138
- if (trustProject && typeof this.trust !== "function") fail("CLAUDE_CREATION_TRUST_UNAVAILABLE", "Authorized project trust preparation is not available.");
139
- const url = `claude://code/new?q=${encodeURIComponent(initialMessage)}&folder=${encodeURIComponent(canonical)}`;
140
- if (url.length > 30000) fail("CLAUDE_CREATION_URL_TOO_LONG", "The encoded Claude Desktop link exceeds 30000 characters.");
141
- await this.checkAccount(account, identity);
142
- if (beforeOpen !== undefined) {
143
- if (typeof beforeOpen !== "function") fail("CLAUDE_CREATION_PREFLIGHT_INVALID", "beforeOpen must be a function.");
144
- await beforeOpen();
145
- }
146
- if (this.resolveProject) {
147
- const currentTasks = await this.listTasks(account);
148
- if (!validTasks(currentTasks)) fail("CLAUDE_CREATION_TASKS_INVALID", "The current native task baseline cannot be verified.");
149
- const matches = currentTasks.filter(task => task.taskId === existingProject.taskId && task.fileTaskId === existingProject.fileTaskId && task.cwd === existingProject.cwd && task.isArchived === false);
150
- if (matches.length !== 1 || !this.sameCwd(await projectCwd(matches[0]), canonical)) {
151
- fail("CLAUDE_CREATION_PROJECT_NOT_FOUND", "The saved project or its authorized current directory changed before opening Desktop.");
152
- }
153
- for (const task of currentTasks) baseline.add(task.taskId);
154
- if (baseline.size > MAX_TASKS) fail("CLAUDE_CREATION_TASKS_INVALID", "The native task baseline exceeds the creation limit.");
155
- if (beforeOpen) await beforeOpen();
156
- }
157
- const projectSource = this.resolveProject ? { taskId: existingProject.taskId, fileTaskId: existingProject.fileTaskId, cwd: existingProject.cwd } : undefined;
158
- const request = { requestId, cwd: canonical, prompt, initialMessage, account: identity, senderThreadId, baseline: [...baseline], ...(projectSource ? { projectSource } : {}), status: "launch_uncertain", reason: "The Desktop launch outcome is uncertain; inspect this request before taking further action." };
159
- await this.revalidateProjectSource(request, account);
160
- this.requests.set(requestId, request);
161
- try {
162
- await this.open(url);
163
- if (autoSubmit) await this.settle();
164
- request.status = "awaiting_user";
165
- request.reason = "Claude Desktop was asked to prefill a new session; the bridge did not press Send. The folder chip alone does not confirm the actual directory. Verify the created task's cwd before dispatching work; absence of a discovered task does not prove that the prompt was not submitted.";
166
- } catch {
167
- request.reason = "The Desktop launcher failed or returned an uncertain outcome. This request will not be launched again; inspect it to check whether the session was created.";
168
- }
169
- if (request.status === "awaiting_user" && autoSubmit) {
170
- return this.completeRequest(request, { account, senderThreadId, beforeOpen, trustProject });
171
- }
172
- return receipt(request);
173
- }
174
-
175
- async revalidateProjectSource(request, account) {
176
- if (!this.resolveProject) return;
177
- const source = request.projectSource;
178
- if (!source) fail("CLAUDE_CREATION_PROJECT_NOT_FOUND", "The original saved project source is unavailable; submission is not authorized.");
179
- const tasks = await this.listTasks(account);
180
- if (!Array.isArray(tasks) || tasks.length > MAX_TASKS) fail("CLAUDE_CREATION_TASKS_INVALID", "The saved project source cannot be verified.");
181
- const matches = tasks.filter(task => task?.taskId === source.taskId);
182
- if (matches.length !== 1 || matches[0].fileTaskId !== source.fileTaskId || matches[0].cwd !== source.cwd || matches[0].isArchived !== false ||
183
- !this.sameCwd(await this.resolveProject(matches[0]), request.cwd)) {
184
- fail("CLAUDE_CREATION_PROJECT_NOT_FOUND", "The original saved project or its authorization changed; submission is not authorized.");
185
- }
186
- }
187
-
188
- async completeRequest(request, { account, senderThreadId, beforeOpen, trustProject }) {
189
- if (typeof this.submit !== "function") fail("CLAUDE_CREATION_SUBMIT_UNAVAILABLE", "Native composer submission is not available.");
190
- if (trustProject && typeof this.trust !== "function") fail("CLAUDE_CREATION_TRUST_UNAVAILABLE", "Authorized project trust preparation is not available.");
191
- await this.checkAccount(account, request.account);
192
- if (beforeOpen) await beforeOpen();
193
- await this.revalidateProjectSource(request, account);
194
- if (trustProject) {
195
- const trusted = await this.trust({ cwd: request.cwd, prompt: request.initialMessage });
196
- if (!["trusted", "absent"].includes(trusted?.status)) {
197
- request.status = "awaiting_user";
198
- request.reason = trusted?.reason ?? "Native workspace trust is not confirmed; inspect the exact project composer.";
199
- return receipt(request);
200
- }
201
- if (trusted.status === "trusted") await this.settle();
202
- }
203
- await this.checkAccount(account, request.account);
204
- if (beforeOpen) await beforeOpen();
205
- await this.revalidateProjectSource(request, account);
206
- request.submissionAttempted = true;
207
- request.status = "submission_uncertain";
208
- request.reason = "Native submission was attempted; inspect this request before retrying. The bridge will not press Send again.";
209
- try {
210
- const submission = await this.submit({ prompt: request.initialMessage, cwd: request.cwd, beforeSubmit: async () => {
211
- await this.checkAccount(account, request.account);
212
- if (beforeOpen) await beforeOpen();
213
- await this.revalidateProjectSource(request, account);
214
- } });
215
- if (submission?.status === "blocked" && submission.submissionAttempted === false) delete request.submissionAttempted;
216
- request.status = submission?.status === "submitted" ? "awaiting_session" : submission?.status === "blocked" ? "awaiting_user" : "submission_uncertain";
217
- request.reason = submission?.reason ?? "Inspect the original native composer and creation receipt; submission has not been verified.";
218
- } catch {}
219
- return this.inspect(request.requestId, { account, senderThreadId });
220
- }
221
-
222
- async inspect(requestId, { account, senderThreadId }) {
223
- const request = this.requests.get(requestId);
224
- if (!request) fail("CLAUDE_CREATION_REQUEST_NOT_FOUND", "No retained creation request has this requestId.");
225
- if (request.senderThreadId !== senderThreadId) fail("CLAUDE_CREATION_SENDER_MISMATCH", "This creation request belongs to another sender thread.");
226
- await this.checkAccount(account, request.account);
227
- if (request.status === "created") return receipt(request);
228
- const sessions = await this.listSessions(account);
229
- if (!Array.isArray(sessions) || sessions.length > MAX_TASKS) fail("CLAUDE_CREATION_SESSIONS_INVALID", "Live native sessions could not be inspected within the limit.");
230
- const matches = new Map();
231
- for (const session of sessions) {
232
- if (!session?.alive || session.entrypoint !== "claude-desktop" || !string(session.sessionId, 256)) continue;
233
- try {
234
- const observedCwd = await this.canonical(session.cwd);
235
- if (await this.assertProcess(session) === false) continue;
236
- const context = await this.readContext(session, account);
237
- if (context?.status !== "matched" || context.isArchived || !string(context.taskId, 256) || request.baseline.includes(context.taskId)) continue;
238
- if (context.fileTaskId !== undefined && context.fileTaskId !== context.taskId) continue;
239
- if (context.accountFingerprint && context.accountFingerprint !== request.account.fingerprint) continue;
240
- if (!this.sameCwd(await this.canonical(context.cwd), observedCwd)) continue;
241
- const transcript = await this.readTranscript(session.sessionId, observedCwd, 1000);
242
- if (transcript?.truncated || transcript?.incomplete) continue;
243
- const firstUser = transcript?.messages?.find((message) => message.role === "user");
244
- if (!matchesInitialMessage(firstUser?.text, request.initialMessage)) continue;
245
- if (await this.assertProcess(session) === false) continue;
246
- matches.set(JSON.stringify([context.taskId, session.sessionId]), { sessionId: session.sessionId, taskId: context.taskId, title: string(context.title, 65536) && context.title.trim() ? context.title.trim().slice(0, 4096) : null, observedCwd });
247
- } catch {}
248
- }
249
- await this.checkAccount(account, request.account);
250
- if (matches.size > 1) {
251
- request.submissionObserved = true;
252
- request.ambiguitySeen = true;
253
- request.status = "ambiguous";
254
- for (const key of ["sessionId", "taskId", "title", "observedCwd"]) delete request[key];
255
- request.reason = "Multiple new native sessions contain the exact creation message; no session was selected.";
256
- } else if (matches.size === 1 && request.status !== "ambiguous" && !request.ambiguitySeen) {
257
- const match = [...matches.values()][0];
258
- const projectConfirmed = this.sameCwd(match.observedCwd, request.cwd);
259
- Object.assign(request, match, { submissionObserved: true, status: projectConfirmed ? "created" : "awaiting_project_confirmation", reason: projectConfirmed ? "A new live native Claude Desktop task has the exact initial user message, verified project, account, and process identity." : "The prompt was submitted in a new native Claude Desktop task, but its actual directory differs from the requested project. Select or adopt the requested project folder in Claude Desktop and inspect this request again; project creation is not confirmed." });
260
- }
261
- return receipt(request);
262
- }
263
-
264
- abandon(requestId, args) {
265
- const pending = this.queue.then(() => this.abandonLocked(requestId, args));
266
- this.queue = pending.catch(() => {});
267
- return pending;
268
- }
269
-
270
- async abandonLocked(requestId, { account, senderThreadId }) {
271
- const request = this.requests.get(requestId);
272
- if (!request) fail("CLAUDE_CREATION_REQUEST_NOT_FOUND", "No retained creation request has this requestId.");
273
- if (request.senderThreadId !== senderThreadId) fail("CLAUDE_CREATION_SENDER_MISMATCH", "This creation request belongs to another sender thread.");
274
- await this.checkAccount(account, request.account);
275
- if (request.status === "created") return receipt(request);
276
- if (request.status === "ambiguous") request.ambiguitySeen = true;
277
- request.status = "abandoned";
278
- request.reason = "The creation request was abandoned and its composer lock released. Claude Desktop and its composer may still be open, and the prompt could still be submitted later. The receipt is retained for inspection and will never relaunch.";
279
- return receipt(request);
280
- }
281
-
282
- exportState() {
283
- return structuredClone({ version: 1, requests: [...this.requests.values()] });
284
- }
285
-
286
- restoreState(state) {
287
- if (this.requests.size || state?.version !== 1 || !Array.isArray(state.requests) || state.requests.length > MAX_REQUESTS) fail("CLAUDE_CREATION_STATE_INVALID", "Creation state cannot be restored.");
288
- const restored = new Map();
289
- const paths = this.platform === "win32" ? path.win32 : path.posix;
290
- for (const request of state.requests) {
291
- if (!request || typeof request.requestId !== "string" || !REQUEST_ID.test(request.requestId) || restored.has(request.requestId) || !string(request.cwd, 8192) || !paths.isAbsolute(request.cwd) || !string(request.senderThreadId, 256) || !string(request.account?.fingerprint) || !string(request.account?.root, 8192) || !STATUSES.has(request.status) || !string(request.reason, 4096) || !Array.isArray(request.baseline) || request.baseline.length > MAX_TASKS || request.baseline.some((id) => !string(id, 256)) || request.initialMessage !== augmentedPrompt(request.prompt, request.requestId)) fail("CLAUDE_CREATION_STATE_INVALID", "A retained creation receipt is invalid.");
292
- const hasSession = ["created", "awaiting_project_confirmation"].includes(request.status) || request.sessionId !== undefined;
293
- if (hasSession && (!string(request.sessionId, 256) || !string(request.taskId, 256) || request.baseline.includes(request.taskId) || (request.title !== null && !string(request.title, 4096)))) fail("CLAUDE_CREATION_STATE_INVALID", "A submitted creation receipt is invalid.");
294
- if (request.submissionObserved !== undefined && typeof request.submissionObserved !== "boolean") fail("CLAUDE_CREATION_STATE_INVALID", "A retained submission marker is invalid.");
295
- if (request.submissionAttempted !== undefined && typeof request.submissionAttempted !== "boolean") fail("CLAUDE_CREATION_STATE_INVALID", "A retained submission attempt is invalid.");
296
- if ((request.status === "awaiting_project_confirmation" || request.observedCwd !== undefined) && (!string(request.observedCwd, 8192) || !paths.isAbsolute(request.observedCwd))) fail("CLAUDE_CREATION_STATE_INVALID", "A retained observed directory is invalid.");
297
- if (request.status === "awaiting_project_confirmation" && (request.submissionObserved !== true || this.sameCwd(request.cwd, request.observedCwd))) fail("CLAUDE_CREATION_STATE_INVALID", "A pending project confirmation is invalid.");
298
- if (request.ambiguitySeen !== undefined && typeof request.ambiguitySeen !== "boolean") fail("CLAUDE_CREATION_STATE_INVALID", "A retained ambiguity marker is invalid.");
299
- if (request.projectSource !== undefined && (!string(request.projectSource?.taskId, 256) || !request.baseline.includes(request.projectSource.taskId) ||
300
- (request.projectSource.fileTaskId !== undefined && request.projectSource.fileTaskId !== request.projectSource.taskId) ||
301
- !string(request.projectSource.cwd, 8192) || !paths.isAbsolute(request.projectSource.cwd))) fail("CLAUDE_CREATION_STATE_INVALID", "A retained project source is invalid.");
302
- restored.set(request.requestId, structuredClone({ requestId: request.requestId, cwd: request.cwd, prompt: request.prompt, initialMessage: request.initialMessage, account: { fingerprint: request.account.fingerprint, root: request.account.root }, senderThreadId: request.senderThreadId, baseline: request.baseline, ...(request.projectSource ? { projectSource: { taskId: request.projectSource.taskId, fileTaskId: request.projectSource.fileTaskId, cwd: request.projectSource.cwd } } : {}), status: request.status, reason: request.reason, ...(request.submissionAttempted ? { submissionAttempted: true } : {}), ...(request.ambiguitySeen || request.status === "ambiguous" ? { ambiguitySeen: true } : {}), ...(request.submissionObserved ? { submissionObserved: true } : {}), ...(request.observedCwd ? { observedCwd: request.observedCwd } : {}), ...(hasSession ? { sessionId: request.sessionId, taskId: request.taskId, title: request.title } : {}) }));
303
- }
304
- this.requests = restored;
305
- }
306
- }