From 5f29b9670be2ceb56f74d77a448a241f9109f02d Mon Sep 17 00:00:00 2001 From: Hulkito Date: Mon, 2 Mar 2026 10:40:31 +0100 Subject: [PATCH 1/2] fix: add Windows hooks support (PowerShell script + os.tmpdir state file) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, the hooks mode was silently broken for two reasons: 1. The hook script is a bash `.sh` file — not executable on Windows. 2. The state file path was hardcoded to `/tmp/cursor-office-state.json`, which does not exist on Windows. Changes: - hooks/cursor-office-hook.ps1 (new): PowerShell equivalent of the bash hook script. Reads hook event JSON from stdin via $input, maps tool names to activity states, writes JSON to %TEMP%\cursor-office-state.json. - src/hooksInstaller.ts: detect Windows via os.platform() === 'win32'. - STATE_FILE now uses os.tmpdir() on Windows, /tmp on Unix. - getHookScriptPath() returns .ps1 on Windows, .sh on Unix. - buildHookCommand() uses powershell.exe -NoProfile -ExecutionPolicy Bypass -File on Windows, bash on Unix. - IS_WINDOWS exported for use in cursorWatcher.ts. - src/cursorWatcher.ts: skip fs.watch() on the state file directory on Windows (%TEMP% is too busy and generates excessive noise). The 1s polling interval started by startHooksWatcher() is sufficient on Windows. --- hooks/cursor-office-hook.ps1 | 47 ++++++++++++++++++++++++++++++++++++ src/cursorWatcher.ts | 21 +++++++++------- src/hooksInstaller.ts | 18 +++++++++++--- 3 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 hooks/cursor-office-hook.ps1 diff --git a/hooks/cursor-office-hook.ps1 b/hooks/cursor-office-hook.ps1 new file mode 100644 index 0000000..7068530 --- /dev/null +++ b/hooks/cursor-office-hook.ps1 @@ -0,0 +1,47 @@ +# Cursor Office hook script for Windows +# Reads hook event JSON from stdin, writes activity state to a temp file. +$stateFile = Join-Path $env:TEMP "cursor-office-state.json" +$input_data = $input | Out-String + +function Get-JsonValue { + param([string]$json, [string]$key) + if ($json -match """$key""\s*:\s*""([^""]+)""") { return $Matches[1] } + return "" +} + +$event = Get-JsonValue $input_data "hook_event_name" +$ts = [int][double]::Parse((Get-Date -UFormat %s)) + +switch ($event) { + "preToolUse" { + $tool = Get-JsonValue $input_data "tool_name" + $activity = switch ($tool) { + { $_ -in "Read","Glob","SemanticSearch","Grep" } { "reading" } + { $_ -in "Write","StrReplace","EditNotebook","Delete" } { "editing" } + "Shell" { "running" } + "Task" { "phoning" } + default { "typing" } + } + "{""activity"":""$activity"",""tool"":""$tool"",""ts"":$ts}" | Set-Content $stateFile -Encoding UTF8 + } + "subagentStart" { + "{""activity"":""phoning"",""ts"":$ts}" | Set-Content $stateFile -Encoding UTF8 + } + "subagentStop" { + "{""activity"":""typing"",""ts"":$ts}" | Set-Content $stateFile -Encoding UTF8 + } + "stop" { + $status = Get-JsonValue $input_data "status" + $activity = switch ($status) { + "completed" { "celebrating" } + "error" { "error" } + default { "idle" } + } + "{""activity"":""$activity"",""ts"":$ts}" | Set-Content $stateFile -Encoding UTF8 + } + "beforeSubmitPrompt" { + "{""activity"":""idle"",""ts"":$ts}" | Set-Content $stateFile -Encoding UTF8 + } +} + +exit 0 diff --git a/src/cursorWatcher.ts b/src/cursorWatcher.ts index 1abf84e..2348b0a 100644 --- a/src/cursorWatcher.ts +++ b/src/cursorWatcher.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import * as os from 'os'; import * as vscode from 'vscode'; import { parseTranscriptLine, ParsedStatus } from './transcriptParser'; -import { isHooksInstalled, getStateFilePath } from './hooksInstaller'; +import { isHooksInstalled, getStateFilePath, IS_WINDOWS } from './hooksInstaller'; export class CursorWatcher implements vscode.Disposable { private watchers: fs.FSWatcher[] = []; @@ -105,14 +105,17 @@ export class CursorWatcher implements vscode.Disposable { } }; - try { - this.hooksWatcher = fs.watch(path.dirname(stateFile), { persistent: false }, (_event, filename) => { - if (filename === path.basename(stateFile)) { - pollState(); - } - }); - } catch { - this.log.appendLine('[hooks] fs.watch on /tmp failed, falling back to polling'); + // On Windows %TEMP% is too busy for fs.watch — polling at 1s is sufficient. + if (!IS_WINDOWS) { + try { + this.hooksWatcher = fs.watch(path.dirname(stateFile), { persistent: false }, (_event, filename) => { + if (filename === path.basename(stateFile)) { + pollState(); + } + }); + } catch { + this.log.appendLine('[hooks] fs.watch on state dir failed, falling back to polling'); + } } this.scanInterval = setInterval(pollState, 1000); diff --git a/src/hooksInstaller.ts b/src/hooksInstaller.ts index 298cec0..acf6a0e 100644 --- a/src/hooksInstaller.ts +++ b/src/hooksInstaller.ts @@ -6,7 +6,12 @@ import * as vscode from 'vscode'; const HOOKS_DIR = path.join(os.homedir(), '.cursor'); const HOOKS_JSON = path.join(HOOKS_DIR, 'hooks.json'); const HOOK_MARKER = 'cursor-office-hook'; -const STATE_FILE = '/tmp/cursor-office-state.json'; + +const IS_WINDOWS = os.platform() === 'win32'; + +const STATE_FILE = IS_WINDOWS + ? path.join(os.tmpdir(), 'cursor-office-state.json') + : '/tmp/cursor-office-state.json'; interface HooksConfig { version: number; @@ -14,11 +19,16 @@ interface HooksConfig { } function getHookScriptPath(extensionPath: string): string { - return path.join(extensionPath, 'hooks', 'cursor-office-hook.sh'); + const script = IS_WINDOWS ? 'cursor-office-hook.ps1' : 'cursor-office-hook.sh'; + return path.join(extensionPath, 'hooks', script); } function buildHookCommand(extensionPath: string): string { - return `bash "${getHookScriptPath(extensionPath)}"`; + const scriptPath = getHookScriptPath(extensionPath); + if (IS_WINDOWS) { + return `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}"`; + } + return `bash "${scriptPath}"`; } export function isHooksInstalled(): boolean { @@ -35,6 +45,8 @@ export function getStateFilePath(): string { return STATE_FILE; } +export { IS_WINDOWS }; + export function installHooks(extensionPath: string): { success: boolean; message: string } { const hookCmd = buildHookCommand(extensionPath); From 7103e528ae8f8533076baf5282159d1725507ced Mon Sep 17 00:00:00 2001 From: Hulkito Date: Mon, 2 Mar 2026 10:49:30 +0100 Subject: [PATCH 2/2] refactor: use ConvertFrom-Json/ConvertTo-Json in PS1 hook script Replace the custom regex-based JSON parsing and manual string building with PowerShell's built-in ConvertFrom-Json and ConvertTo-Json cmdlets. Centralizes file-writing via a $state hashtable, making the script more robust and easier to maintain. Suggested by Gemini Code Assist review on PR #2. --- hooks/cursor-office-hook.ps1 | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/hooks/cursor-office-hook.ps1 b/hooks/cursor-office-hook.ps1 index 7068530..d25a72d 100644 --- a/hooks/cursor-office-hook.ps1 +++ b/hooks/cursor-office-hook.ps1 @@ -1,20 +1,15 @@ # Cursor Office hook script for Windows # Reads hook event JSON from stdin, writes activity state to a temp file. $stateFile = Join-Path $env:TEMP "cursor-office-state.json" -$input_data = $input | Out-String - -function Get-JsonValue { - param([string]$json, [string]$key) - if ($json -match """$key""\s*:\s*""([^""]+)""") { return $Matches[1] } - return "" -} - -$event = Get-JsonValue $input_data "hook_event_name" +$data = $input | Out-String | ConvertFrom-Json +$event = $data.hook_event_name $ts = [int][double]::Parse((Get-Date -UFormat %s)) +$state = $null + switch ($event) { "preToolUse" { - $tool = Get-JsonValue $input_data "tool_name" + $tool = $data.tool_name $activity = switch ($tool) { { $_ -in "Read","Glob","SemanticSearch","Grep" } { "reading" } { $_ -in "Write","StrReplace","EditNotebook","Delete" } { "editing" } @@ -22,26 +17,29 @@ switch ($event) { "Task" { "phoning" } default { "typing" } } - "{""activity"":""$activity"",""tool"":""$tool"",""ts"":$ts}" | Set-Content $stateFile -Encoding UTF8 + $state = @{ activity = $activity; tool = $tool; ts = $ts } } "subagentStart" { - "{""activity"":""phoning"",""ts"":$ts}" | Set-Content $stateFile -Encoding UTF8 + $state = @{ activity = "phoning"; ts = $ts } } "subagentStop" { - "{""activity"":""typing"",""ts"":$ts}" | Set-Content $stateFile -Encoding UTF8 + $state = @{ activity = "typing"; ts = $ts } } "stop" { - $status = Get-JsonValue $input_data "status" - $activity = switch ($status) { + $activity = switch ($data.status) { "completed" { "celebrating" } "error" { "error" } default { "idle" } } - "{""activity"":""$activity"",""ts"":$ts}" | Set-Content $stateFile -Encoding UTF8 + $state = @{ activity = $activity; ts = $ts } } "beforeSubmitPrompt" { - "{""activity"":""idle"",""ts"":$ts}" | Set-Content $stateFile -Encoding UTF8 + $state = @{ activity = "idle"; ts = $ts } } } +if ($state) { + $state | ConvertTo-Json -Compress | Set-Content $stateFile -Encoding UTF8 +} + exit 0