cursor Init 完善多人协同changelog,以及godot相关基础skill和代码规范

This commit is contained in:
Nostars Developer
2026-07-15 14:38:08 +08:00
parent fc8557f2a9
commit 6446261c69
56 changed files with 3523 additions and 1584 deletions

View File

@@ -1,38 +1,61 @@
# Cursor Hooks 说明
## 结构
## 文件
```
```text
hooks/
├── run-hook.ps1 # Windows dispatcher读取 local-env.json 后分发)
├── session-init.ps1 # sessionStart hookPowerShell
├── session-init.sh # sessionStart hookbash
├── check-changelog.ps1 # stop hookPowerShell
├── check-changelog.sh # stop hookbash
└── README.md # 本文件
├── run-hook.ps1 # Windows dispatcher读取 local-env.json 并校验 JSON 输出
├── session-init.ps1 # sessionStartPowerShell
├── session-init.sh # sessionStartBash
├── check-changelog.ps1 # stop / changelog guardPowerShell
├── check-changelog.sh # stop / changelog guardBash
└── README.md
```
## 跨平台适配
## Windows 配置(当前模板)
`hooks.json` 中的 `command` 字段是**平台绑定的**——Windows 用 `powershell`macOS/Linux 用 `bash`
`.cursor/hooks.json` 统一通过 dispatcher 调用:
### Windows当前
- `sessionStart``powershell -ExecutionPolicy Bypass -File .cursor/hooks/run-hook.ps1 session-init`,超时 5 秒。
- `stop``powershell -ExecutionPolicy Bypass -File .cursor/hooks/run-hook.ps1 check-changelog`,超时 10 秒,`loop_limit` 为 1。
hooks.json 使用 `powershell ... run-hook.ps1` 作为入口dispatcher 根据
`.cursor/local-env.json``shell` 字段决定执行 `.ps1` 还是 `.sh` 脚本。
dispatcher 从 `.cursor/local-env.json` 读取 `shell`
### 迁移到 macOS / Linux
- `powershell` / `pwsh`:运行同名 `.ps1`
- `bash` / `zsh` / `sh`:选择同名 `.sh`,并通过 `bash` 执行。
将 hooks.json 的 command 改为直接调用 `.sh` 脚本:
stdin 以 UTF-8 原样转发。子脚本成功时dispatcher 校验并规范化其 JSON 对象;脚本缺失、配置无效、启动失败、非零退出或输出无效时,只向 stderr 写诊断,并向 hook 返回非阻断的 `{}`
## macOS / Linux 切换
macOS/Linux 不需要 Windows dispatcher。将 `.cursor/hooks.json` 改为 Bash 直调:
```json
{
"version": 1,
"hooks": {
"sessionStart": [{ "command": "bash .cursor/hooks/session-init.sh", "timeout": 5 }],
"stop": [{ "command": "bash .cursor/hooks/check-changelog.sh", "timeout": 10, "loop_limit": 1 }]
"sessionStart": [
{
"command": "bash .cursor/hooks/session-init.sh",
"timeout": 5
}
],
"stop": [
{
"command": "bash .cursor/hooks/check-changelog.sh",
"timeout": 10,
"loop_limit": 1
}
]
}
}
```
同时更新 `.cursor/local-env.json``shell` 字段`bash``zsh`
同时把本机 `.cursor/local-env.json``shell` `bash`(或实际使用的 `zsh`。Bash 脚本不依赖 `jq`;优先用 `python3`Windows Git Bash 可回退 `python`)安全解析/生成 JSON解释器缺失或解析失败时 fail-open 返回 `{}`
## 行为
- `session-init` 清除上一会话的 `.cursor/changelog/.changelog-ack`,并刷新 gitignored 的 `.cursor/changelog/.session-start`。非 `agent` 模式通过 hook `env` 设置 `CURSOR_SKIP_CHANGELOG=1`
- Cursor 的 `stop` 输入不包含可靠的工具编辑历史,因此 `check-changelog` 不解析不存在的“编辑证据”。它只在未 ack、未被 session 环境跳过,且 Godot 源码/配置同时晚于本会话起点与当前 changelog 时返回 `followup_message`,避免旧会话残留 mtime 误报。
- 扫描从仓库根开始,覆盖 `project.godot``.gd``.tscn``.tres``.res``.gdshader``.cfg``.json``.cs``.csproj` 及常用工具脚本;跳过 `.git``.cursor``.godot`、依赖目录、Python 缓存/虚拟环境和常见构建输出目录。
- 所有正常与失败路径都在 stdout 输出一个合法 JSON 对象;检查失败默认不阻断。

View File

@@ -1,105 +1,124 @@
# Changelog sync guard — 统一 stop hook
#
# 用确定性逻辑检查:
# 1. 源文件是否比 changelog 更新mtime 比较)
# 2. stdin 中的 agent 上下文是否显示有源文件编辑操作
#
# 静默条件(不触发提醒):
# 1. 环境变量 CURSOR_SKIP_CHANGELOG 被设置sessionStart 在非 agent 模式设置)
# 2. stdin JSON 中 composer_mode 不是 "agent"(如 debug/ask/edit 模式)
# 3. changelog 文件不存在
# 4. .changelog-ack 标记文件存在且足够新(本会话已确认过 changelog 状态)
# 5. 没有源文件比 changelog 更新
# 6. stdin 上下文中没有文件编辑操作的证据(防止跨会话残留 mtime 误触发)
# Changelog sync guard: compare Godot source/config mtimes against this session and changelog.
$utf8 = New-Object System.Text.UTF8Encoding($false)
[Console]::InputEncoding = $utf8
[Console]::OutputEncoding = $utf8
$input = [Console]::In.ReadToEnd()
function Write-EmptyHookResult {
[Console]::Out.WriteLine('{}')
}
# === 豁免检查 1环境变量跳过标志 ===
$null = [Console]::In.ReadToEnd()
# sessionStart sets this variable outside agent mode.
if ($env:CURSOR_SKIP_CHANGELOG) {
Write-Output '{}'
Write-EmptyHookResult
exit 0
}
# === 豁免检查 2从 stdin 解析 composer_mode ===
try {
$data = $input | ConvertFrom-Json
$mode = $data.composer_mode
if ($mode -and $mode -ne "agent") {
Write-Output '{}'
exit 0
$cursorDir = Split-Path -Parent $PSScriptRoot
$repoRoot = Split-Path -Parent $cursorDir
$changelog = Join-Path $cursorDir 'changelog\changelog-headlines.md'
$ackFile = Join-Path $cursorDir 'changelog\.changelog-ack'
$sessionMarker = Join-Path $cursorDir 'changelog\.session-start'
if (-not (Test-Path -LiteralPath $changelog -PathType Leaf)) {
Write-EmptyHookResult
exit 0
}
$changelogMtime = (Get-Item -LiteralPath $changelog).LastWriteTimeUtc
if ((Test-Path -LiteralPath $ackFile -PathType Leaf) -and
(Get-Item -LiteralPath $ackFile).LastWriteTimeUtc -ge $changelogMtime) {
Write-EmptyHookResult
exit 0
}
$comparisonMtime = $changelogMtime
if ((Test-Path -LiteralPath $sessionMarker -PathType Leaf)) {
$sessionMtime = (Get-Item -LiteralPath $sessionMarker).LastWriteTimeUtc
if ($sessionMtime -gt $comparisonMtime) {
$comparisonMtime = $sessionMtime
}
} catch {
# JSON 解析失败,继续后续检查
}
# === 豁免检查 3从 stdin 文本匹配 debug 上下文关键词 ===
# stop hook 的 $ARGUMENTS 可能包含对话/工具上下文,检测 debug 相关信号
if ($input -match '"mode"\s*:\s*"debug"' -or
$input -match 'debug[\s_-]?mode' -or
$input -match 'Debug Mode') {
Write-Output '{}'
exit 0
}
$changelog = ".cursor\changelog\changelog-headlines.md"
$srcDir = "art-agent"
# === 豁免检查 4changelog 文件不存在 ===
if (-not (Test-Path $changelog)) {
Write-Output '{}'
exit 0
}
$clMtime = (Get-Item $changelog).LastWriteTime
# === 豁免检查 5ack 标记文件足够新 ===
$ackFile = ".cursor\changelog\.changelog-ack"
if ((Test-Path $ackFile) -and (Get-Item $ackFile).LastWriteTime -ge $clMtime) {
Write-Output '{}'
exit 0
}
# === 豁免检查 6stdin 中无文件编辑证据 ===
# 防止跨会话残留 mtime 差异导致误触发:如果 ack 文件不存在(或过旧),
# 但 stdin 上下文中也没有任何文件写入/编辑操作的痕迹,说明本次会话
# 没有进行代码改动,不应触发提醒。
$hasEditEvidence = (
$input -match 'StrReplace|Write\s*tool|edit_file|file_write|write_to_file' -or
$input -match 'Created file|Modified file|Wrote contents' -or
$input -match '"tool"\s*:\s*"(str_replace|write|edit)"'
$includedNames = @('project.godot')
$includedExtensions = @(
'.gd',
'.tscn',
'.tres',
'.res',
'.gdshader',
'.cfg',
'.json',
'.cs',
'.csproj',
'.py',
'.ps1',
'.sh'
)
$excludedDirectoryNames = @(
'.git',
'.cursor',
'.godot',
'node_modules',
'__pycache__',
'venv',
'.venv',
'dist',
'build',
'out',
'bin',
'obj',
'export',
'exports',
'coverage',
'tmp',
'temp'
)
if (-not $hasEditEvidence) {
Write-Output '{}'
exit 0
}
# === 核心检查:是否有源文件比 changelog 更新 ===
$extensions = @("*.py", "*.tsx", "*.ts", "*.css")
$excludeDirs = @("node_modules", ".next", "__pycache__", "venv")
$newerFile = $null
foreach ($ext in $extensions) {
$files = Get-ChildItem -Path $srcDir -Filter $ext -Recurse -ErrorAction SilentlyContinue |
Where-Object {
$skip = $false
foreach ($ex in $excludeDirs) {
if ($_.FullName -like "*\$ex\*") { $skip = $true; break }
}
-not $skip -and $_.LastWriteTime -gt $clMtime
} |
Select-Object -First 1
$rootItem = Get-Item -LiteralPath $repoRoot
$directories = New-Object 'System.Collections.Generic.Stack[System.IO.DirectoryInfo]'
$directories.Push($rootItem)
if ($files) {
$newerFile = $files.Name
break
while ($directories.Count -gt 0 -and -not $newerFile) {
$directory = $directories.Pop()
try {
$entries = Get-ChildItem -LiteralPath $directory.FullName -Force -ErrorAction Stop
} catch {
continue
}
foreach ($entry in $entries) {
if ($entry.PSIsContainer) {
$isReparsePoint = ($entry.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0
if (-not $isReparsePoint -and $excludedDirectoryNames -notcontains $entry.Name) {
$directories.Push($entry)
}
continue
}
$isIncluded = ($includedNames -contains $entry.Name) -or
($includedExtensions -contains $entry.Extension)
if ($isIncluded -and $entry.LastWriteTimeUtc -gt $comparisonMtime) {
$newerFile = $entry
break
}
}
}
if ($newerFile) {
$msg = "[Hook] Source file updated (e.g. $newerFile) but changelog not synced. Run dev-changelog Skill operation A NOW to write all three changelog layers."
$json = '{"followup_message":"' + $msg.Replace('"','\"') + '"}'
Write-Output $json
$rootPrefix = $repoRoot.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar
$relativePath = if ($newerFile.FullName.StartsWith($rootPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
$newerFile.FullName.Substring($rootPrefix.Length)
} else {
$newerFile.Name
}
$message = "[Hook] Godot source/config is newer than the changelog (e.g. $relativePath). Run dev-changelog Skill operation A now: add a fragment under .cursor/changelog/entries/ and rebuild the changelog views."
$result = [ordered]@{ followup_message = $message }
[Console]::Out.WriteLine(($result | ConvertTo-Json -Compress -Depth 3))
} else {
Write-Output '{}'
Write-EmptyHookResult
}
exit 0

View File

@@ -1,58 +1,132 @@
#!/usr/bin/env bash
# Changelog sync guard — stop hook (bash 版)
# Changelog sync guard:比较 Godot 源码/配置与本会话起点、changelog 的 mtime。
input=$(cat)
emit_empty() {
printf '{}\n'
}
# === 豁免检查 1环境变量跳过标志 ===
if [ -n "$CURSOR_SKIP_CHANGELOG" ]; then
echo '{}'; exit 0
cat >/dev/null
if [ -n "${CURSOR_SKIP_CHANGELOG:-}" ]; then
emit_empty
exit 0
fi
# === 豁免检查 2从 stdin 解析 composer_mode ===
mode=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('composer_mode',''))" 2>/dev/null || echo "")
if [ -n "$mode" ] && [ "$mode" != "agent" ]; then
echo '{}'; exit 0
fi
script_dir=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)
cursor_dir=$(dirname -- "$script_dir")
repo_root=$(dirname -- "$cursor_dir")
changelog="$cursor_dir/changelog/changelog-headlines.md"
ack_file="$cursor_dir/changelog/.changelog-ack"
session_marker="$cursor_dir/changelog/.session-start"
# === 豁免检查 3从 stdin 文本匹配 debug 上下文关键词 ===
if echo "$input" | grep -qiE '"mode"\s*:\s*"debug"|debug[\s_-]?mode|Debug Mode'; then
echo '{}'; exit 0
fi
changelog=".cursor/changelog/changelog-headlines.md"
srcDir="art-agent"
# === 豁免检查 4changelog 文件不存在 ===
if [ ! -f "$changelog" ]; then
echo '{}'; exit 0
emit_empty
exit 0
fi
clMtime=$(stat -c %Y "$changelog" 2>/dev/null || stat -f %m "$changelog" 2>/dev/null)
get_mtime() {
stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null
}
# === 豁免检查 5ack 标记文件足够新 ===
ackFile=".cursor/changelog/.changelog-ack"
if [ -f "$ackFile" ]; then
ackMtime=$(stat -c %Y "$ackFile" 2>/dev/null || stat -f %m "$ackFile" 2>/dev/null)
if [ "$ackMtime" -ge "$clMtime" ] 2>/dev/null; then
echo '{}'; exit 0
if [ -f "$ack_file" ]; then
changelog_mtime=$(get_mtime "$changelog")
ack_mtime=$(get_mtime "$ack_file")
if [ -n "$changelog_mtime" ] && [ -n "$ack_mtime" ] &&
[ "$ack_mtime" -ge "$changelog_mtime" ] 2>/dev/null; then
emit_empty
exit 0
fi
fi
# === 豁免检查 6stdin 中无文件编辑证据 ===
if ! echo "$input" | grep -qE 'StrReplace|Write\s*tool|edit_file|file_write|write_to_file|Created file|Modified file|Wrote contents|"tool"\s*:\s*"(str_replace|write|edit)"'; then
echo '{}'; exit 0
comparison_file=$changelog
if [ -f "$session_marker" ]; then
changelog_mtime=$(get_mtime "$changelog")
session_mtime=$(get_mtime "$session_marker")
if [ -n "$changelog_mtime" ] && [ -n "$session_mtime" ] &&
[ "$session_mtime" -gt "$changelog_mtime" ] 2>/dev/null; then
comparison_file=$session_marker
fi
fi
# === 核心检查:是否有源文件比 changelog 更新 ===
newerFile=$(find "$srcDir" -type f \( -name "*.py" -o -name "*.tsx" -o -name "*.ts" -o -name "*.css" \) \
! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/__pycache__/*" ! -path "*/venv/*" \
-newer "$changelog" -print -quit 2>/dev/null)
newer_file=''
while IFS= read -r candidate; do
newer_file=$candidate
break
done < <(
find "$repo_root" \
\( -type d \( \
-name '.git' -o \
-name '.cursor' -o \
-name '.godot' -o \
-name 'node_modules' -o \
-name '__pycache__' -o \
-name 'venv' -o \
-name '.venv' -o \
-name 'dist' -o \
-name 'build' -o \
-name 'out' -o \
-name 'bin' -o \
-name 'obj' -o \
-name 'export' -o \
-name 'exports' -o \
-name 'coverage' -o \
-name 'tmp' -o \
-name 'temp' \
\) -prune \) -o \
\( -type f \( \
-iname 'project.godot' -o \
-iname '*.gd' -o \
-iname '*.tscn' -o \
-iname '*.tres' -o \
-iname '*.res' -o \
-iname '*.gdshader' -o \
-iname '*.cfg' -o \
-iname '*.json' -o \
-iname '*.cs' -o \
-iname '*.csproj' -o \
-iname '*.py' -o \
-iname '*.ps1' -o \
-iname '*.sh' \
\) -newer "$comparison_file" -print \) 2>/dev/null
)
if [ -n "$newerFile" ]; then
fname=$(basename "$newerFile")
msg="[Hook] Source file updated (e.g. $fname) but changelog not synced. Run dev-changelog Skill operation A NOW to write all three changelog layers."
echo "{\"followup_message\":\"$msg\"}"
if [ -z "$newer_file" ]; then
emit_empty
exit 0
fi
relative_file=${newer_file#"$repo_root"/}
message="[Hook] Godot source/config is newer than the changelog (e.g. $relative_file). Run dev-changelog Skill operation A now: add a fragment under .cursor/changelog/entries/ and rebuild the changelog views."
python_cmd=''
for candidate in python3 python; do
if command -v "$candidate" >/dev/null 2>&1 &&
"$candidate" -c 'import json' >/dev/null 2>&1; then
python_cmd=$candidate
break
fi
done
if [ -n "$python_cmd" ]; then
result=$(
printf '%s' "$message" | "$python_cmd" -c '
import json
import sys
print(json.dumps(
{"followup_message": sys.stdin.read()},
ensure_ascii=False,
separators=(",", ":"),
))
' 2>/dev/null
) || result='{}'
if [ -n "$result" ]; then
printf '%s\n' "$result"
else
emit_empty
fi
else
echo '{}'
emit_empty
fi
exit 0

View File

@@ -1,35 +1,111 @@
# run-hook.ps1 — 通用 hook dispatcher
# 从 .cursor/local-env.json 读取 shell 类型,决定执行 .ps1 还是 .sh 脚本
# 用法powershell -ExecutionPolicy Bypass -File .cursor/hooks/run-hook.ps1 <hook-name>
# 示例run-hook.ps1 session-init → 执行 session-init.ps1 或 session-init.sh
# Hook dispatcher: select the PowerShell or Bash implementation from local-env.json.
param([string]$HookName)
$localEnvPath = ".cursor\local-env.json"
$shell = "powershell"
$utf8 = New-Object System.Text.UTF8Encoding($false)
[Console]::InputEncoding = $utf8
[Console]::OutputEncoding = $utf8
if (Test-Path $localEnvPath) {
try {
$config = Get-Content $localEnvPath -Raw | ConvertFrom-Json
if ($config.shell) { $shell = $config.shell }
} catch {}
function Write-EmptyHookResult {
[Console]::Out.WriteLine('{}')
}
$hookDir = ".cursor\hooks"
$input = [Console]::In.ReadToEnd()
$inputPayload = [Console]::In.ReadToEnd()
if ($shell -eq "powershell") {
$scriptPath = Join-Path $hookDir "$HookName.ps1"
if (Test-Path $scriptPath) {
$input | powershell -ExecutionPolicy Bypass -File $scriptPath
} else {
Write-Output '{}'
try {
if ([string]::IsNullOrWhiteSpace($HookName) -or $HookName -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]*$') {
throw "Invalid hook name."
}
} else {
$scriptPath = Join-Path $hookDir "$HookName.sh"
if (Test-Path $scriptPath) {
$input | bash $scriptPath
} else {
Write-Output '{}'
$cursorDir = Split-Path -Parent $PSScriptRoot
$repoRoot = Split-Path -Parent $cursorDir
$localEnvPath = Join-Path $cursorDir 'local-env.json'
$shell = 'powershell'
if (Test-Path -LiteralPath $localEnvPath -PathType Leaf) {
try {
$config = Get-Content -LiteralPath $localEnvPath -Raw -Encoding UTF8 | ConvertFrom-Json
if ($config.shell -is [string] -and -not [string]::IsNullOrWhiteSpace($config.shell)) {
$shell = $config.shell.Trim().ToLowerInvariant()
}
} catch {
[Console]::Error.WriteLine("[run-hook] Ignoring invalid local-env.json: {0}", $_.Exception.Message)
}
}
$processInfo = New-Object System.Diagnostics.ProcessStartInfo
$processInfo.UseShellExecute = $false
$processInfo.CreateNoWindow = $true
$processInfo.RedirectStandardInput = $true
$processInfo.RedirectStandardOutput = $true
$processInfo.RedirectStandardError = $true
$processInfo.WorkingDirectory = $repoRoot
if ($processInfo.PSObject.Properties.Name -contains 'StandardInputEncoding') {
$processInfo.StandardInputEncoding = $utf8
$processInfo.StandardOutputEncoding = $utf8
$processInfo.StandardErrorEncoding = $utf8
}
switch ($shell) {
{ $_ -in @('powershell', 'powershell.exe', 'pwsh', 'pwsh.exe') } {
$scriptPath = Join-Path $PSScriptRoot "$HookName.ps1"
if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) {
throw "PowerShell hook not found: $HookName"
}
$processInfo.FileName = if ($shell -in @('pwsh', 'pwsh.exe')) { 'pwsh' } else { 'powershell' }
$processInfo.Arguments = "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$scriptPath`""
break
}
{ $_ -in @('bash', 'bash.exe', 'zsh', 'sh') } {
$scriptPath = (Join-Path $PSScriptRoot "$HookName.sh") -replace '\\', '/'
if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) {
throw "Shell hook not found: $HookName"
}
$processInfo.FileName = 'bash'
$processInfo.Arguments = "`"$scriptPath`""
break
}
default {
throw "Unsupported shell '$shell'."
}
}
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $processInfo
if (-not $process.Start()) {
throw "Unable to start hook process."
}
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
$stderrTask = $process.StandardError.ReadToEndAsync()
$process.StandardInput.Write($inputPayload)
$process.StandardInput.Close()
$process.WaitForExit()
$stdout = $stdoutTask.Result
$stderr = $stderrTask.Result
if (-not [string]::IsNullOrWhiteSpace($stderr)) {
[Console]::Error.WriteLine($stderr.TrimEnd())
}
if ($process.ExitCode -ne 0) {
throw "Hook exited with code $($process.ExitCode)."
}
$trimmed = $stdout.Trim()
if ([string]::IsNullOrWhiteSpace($trimmed) -or -not $trimmed.StartsWith('{')) {
throw "Hook returned an empty or non-object response."
}
$result = $trimmed | ConvertFrom-Json
$normalized = $result | ConvertTo-Json -Compress -Depth 20
if ([string]::IsNullOrWhiteSpace($normalized) -or -not $normalized.StartsWith('{')) {
throw "Hook response is not a JSON object."
}
[Console]::Out.WriteLine($normalized)
} catch {
[Console]::Error.WriteLine("[run-hook] {0}", $_.Exception.Message)
Write-EmptyHookResult
}
exit 0

View File

@@ -1,23 +1,48 @@
# session-init.ps1 — 会话启动时检测 composer_mode非 agent 模式设置跳过标志
#
# sessionStart input 包含 composer_mode 字段("agent" / "ask" / "edit" / "debug" 等)
# 通过 env 输出的环境变量会传递给同会话内所有后续 hook
# sessionStart: reset changelog runtime markers and skip checks outside agent mode.
$utf8 = New-Object System.Text.UTF8Encoding($false)
[Console]::InputEncoding = $utf8
[Console]::OutputEncoding = $utf8
$input = [Console]::In.ReadToEnd()
$inputPayload = [Console]::In.ReadToEnd()
$mode = $null
try {
$data = $input | ConvertFrom-Json
$mode = $data.composer_mode
if (-not [string]::IsNullOrWhiteSpace($inputPayload)) {
$data = $inputPayload | ConvertFrom-Json
if ($data.composer_mode -is [string]) {
$mode = $data.composer_mode
}
}
} catch {
$mode = $null
}
Remove-Item ".cursor\changelog\.changelog-ack" -ErrorAction SilentlyContinue
if ($mode -and $mode -ne "agent") {
$json = '{"env":{"CURSOR_SKIP_CHANGELOG":"1","CURSOR_COMPOSER_MODE":"' + $mode + '"}}'
Write-Output $json
} else {
Write-Output '{}'
$cursorDir = Split-Path -Parent $PSScriptRoot
$ackFile = Join-Path $cursorDir 'changelog\.changelog-ack'
$sessionMarker = Join-Path $cursorDir 'changelog\.session-start'
Remove-Item -LiteralPath $ackFile -Force -ErrorAction SilentlyContinue
try {
$stream = [System.IO.File]::Open(
$sessionMarker,
[System.IO.FileMode]::Create,
[System.IO.FileAccess]::Write,
[System.IO.FileShare]::ReadWrite
)
$stream.Dispose()
} catch {
# Runtime marker failure must not block session startup.
}
if (-not [string]::IsNullOrWhiteSpace($mode) -and $mode -ne 'agent') {
$result = [ordered]@{
env = [ordered]@{
CURSOR_SKIP_CHANGELOG = '1'
CURSOR_COMPOSER_MODE = $mode
}
}
[Console]::Out.WriteLine(($result | ConvertTo-Json -Compress -Depth 4))
} else {
[Console]::Out.WriteLine('{}')
}
exit 0

View File

@@ -1,15 +1,53 @@
#!/usr/bin/env bash
# session-init.sh — 会话启动时检测 composer_mode非 agent 模式设置跳过标志
# sessionStart重置 changelog 运行态标记,并在非 agent 模式下跳过检查。
input=$(cat)
script_dir=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)
cursor_dir=$(dirname -- "$script_dir")
rm -f -- "$cursor_dir/changelog/.changelog-ack" 2>/dev/null
: > "$cursor_dir/changelog/.session-start" 2>/dev/null || true
mode=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('composer_mode',''))" 2>/dev/null || echo "")
python_cmd=''
for candidate in python3 python; do
if command -v "$candidate" >/dev/null 2>&1 &&
"$candidate" -c 'import json' >/dev/null 2>&1; then
python_cmd=$candidate
break
fi
done
rm -f ".cursor/changelog/.changelog-ack"
if [ -n "$python_cmd" ]; then
result=$(
printf '%s' "$input" | "$python_cmd" -c '
import json
import sys
if [ -n "$mode" ] && [ "$mode" != "agent" ]; then
echo "{\"env\":{\"CURSOR_SKIP_CHANGELOG\":\"1\",\"CURSOR_COMPOSER_MODE\":\"$mode\"}}"
try:
data = json.load(sys.stdin)
mode = data.get("composer_mode", "")
if not isinstance(mode, str):
mode = ""
except Exception:
mode = ""
if mode and mode != "agent":
print(json.dumps({
"env": {
"CURSOR_SKIP_CHANGELOG": "1",
"CURSOR_COMPOSER_MODE": mode,
}
}, ensure_ascii=False, separators=(",", ":")))
else:
print("{}")
' 2>/dev/null
) || result='{}'
if [ -n "$result" ]; then
printf '%s\n' "$result"
else
printf '{}\n'
fi
else
echo "{}"
printf '{}\n'
fi
exit 0