sync: BOWEI 2026-09-07 tools-sync 改为 junction 模型(registry 双向 ingest + 修复)
- sync_repos.ps1: 每个 skill 只在 ~\.claude\skills 有一份物理副本,其余 registry 一律 junction;新增 registry-only skill 的自动 ingest、MISDIRECTED 链接修复、 同名内容分叉的 CONFLICT 报告,以及只对 -MirrorRegistry 做全量镜像。 - SKILL.md: 重写阶段 1/2,补 Codex MCP 存储模型(config.toml 惰性, codex mcp list --json 是唯一权威口径)与 CLI 升级会清空注册表的事实。 - sync_mcp_registrations.ps1 + references/codex_mcp_wanted_keys.json: 报告 codex-cli 无接口可设的键(tool_timeout_sec / env_vars)为 "registered, but incomplete" 而非失败。 - tests/test_sync_repos_registries.ps1: registry 对账的回归测试。 - .gitignore: orchestration(Orca 自带的发现桩,同 orca-cli)列为 local-only。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01438Zbcx49e4o46uSA71mFF
This commit is contained in:
@@ -1,6 +1,20 @@
|
||||
# tools-sync: audit and reconcile user-configured MCP registrations between
|
||||
# Claude Code and Codex. Managed connectors, plugins, and Codex runtime MCPs are
|
||||
# intentionally excluded because they are owned by their host application.
|
||||
#
|
||||
# Codex storage model (measured on codex-cli 0.150.1, not documented anywhere):
|
||||
# * `codex mcp list --json` / `add` / `remove` are the ONLY working interface.
|
||||
# * config.toml's [mcp_servers.*] tables are INERT. Editing node_repl's startup_timeout_sec
|
||||
# there from 120 to 137 left `codex mcp list` reporting 120, and a section written by hand
|
||||
# never appeared at all. Hand-writing TOML sections -- what this skill used to instruct --
|
||||
# silently does nothing. The sections still in config.toml are legacy leftovers.
|
||||
# * `codex mcp add` persists outside config.toml and has no flag for tool_timeout_sec or
|
||||
# env_vars, so those keys currently have no setting entry point at all.
|
||||
# * That store is not durable across CLI upgrades: seven user MCPs registered on 2026-08-29
|
||||
# were gone after the upgrade to 0.150.1. Re-audit Codex MCPs on every sync; ~/skills_env.md
|
||||
# is the durable record to re-register from.
|
||||
#
|
||||
# Run from PowerShell, NOT git-bash: MSYS rewrites `/c`-style args and Windows paths.
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
@@ -10,12 +24,21 @@ param(
|
||||
[ValidateSet("None", "Claude", "Codex")]
|
||||
[string]$ConflictAuthority = "None",
|
||||
[string[]]$ExcludeName = @(),
|
||||
[string]$PythonCommand = "python"
|
||||
[string]$PythonCommand = "python",
|
||||
# Where to read Codex's registrations. Auto = `codex mcp list --json`, falling back to
|
||||
# parsing -CodexConfig when the CLI is unavailable (older codex, or a test fixture).
|
||||
# Toml forces the fixture path so tests can isolate from the real machine.
|
||||
[ValidateSet("Auto", "Cli", "Toml")]
|
||||
[string]$CodexSource = "Auto",
|
||||
# Codex-side keys some MCPs need that codex-cli offers no way to set (see the note above).
|
||||
# Resolved after the param block: $PSScriptRoot is not yet bound in default expressions here.
|
||||
[string]$OverridesFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$script:CodexBackupMade = $false
|
||||
$script:Failures = @()
|
||||
$script:Unsupported = @()
|
||||
$CodexOnlyEnv = @("PYTHONIOENCODING", "PYTHONUTF8")
|
||||
|
||||
function Test-TomllibPython([string]$Candidate) {
|
||||
@@ -53,6 +76,19 @@ function Resolve-PythonCommand([string]$Requested, [bool]$Explicit) {
|
||||
|
||||
$PythonCommand = Resolve-PythonCommand $PythonCommand $PSBoundParameters.ContainsKey('PythonCommand')
|
||||
|
||||
if (-not $OverridesFile) {
|
||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$OverridesFile = Join-Path (Split-Path -Parent $here) "references\codex_mcp_wanted_keys.json"
|
||||
}
|
||||
|
||||
$script:Overrides = @{}
|
||||
if (Test-Path -LiteralPath $OverridesFile) {
|
||||
$raw = Get-Content -Raw -LiteralPath $OverridesFile | ConvertFrom-Json
|
||||
foreach ($property in $raw.psobject.Properties) {
|
||||
if ($property.Name -notlike "_*") { $script:Overrides[$property.Name] = $property.Value }
|
||||
}
|
||||
}
|
||||
|
||||
function ConvertTo-StringMap($Object) {
|
||||
$result = [ordered]@{}
|
||||
if ($null -eq $Object) { return $result }
|
||||
@@ -128,8 +164,11 @@ function Get-CodexMcpsFromCli {
|
||||
}
|
||||
|
||||
function Get-CodexMcps {
|
||||
$fromCli = Get-CodexMcpsFromCli
|
||||
if ($null -ne $fromCli) { return $fromCli }
|
||||
if ($CodexSource -ne "Toml") {
|
||||
$fromCli = Get-CodexMcpsFromCli
|
||||
if ($null -ne $fromCli) { return $fromCli }
|
||||
if ($CodexSource -eq "Cli") { throw "codex mcp list --json unavailable and -CodexSource Cli forbids the TOML fallback" }
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $CodexConfig)) {
|
||||
throw "Codex config not found: $CodexConfig"
|
||||
@@ -234,6 +273,7 @@ function Add-ToCodex($Mcp) {
|
||||
$arguments = @("mcp", "add", $Mcp.name)
|
||||
$envMap = [ordered]@{}
|
||||
foreach ($key in $Mcp.env.Keys) { $envMap[$key] = $Mcp.env[$key] }
|
||||
# Without these, Codex mangles non-ASCII stdio output on Windows.
|
||||
$envMap["PYTHONIOENCODING"] = "utf-8"
|
||||
$envMap["PYTHONUTF8"] = "1"
|
||||
foreach ($key in $envMap.Keys) { $arguments += @("--env", "$key=$($envMap[$key])") }
|
||||
@@ -241,6 +281,7 @@ function Add-ToCodex($Mcp) {
|
||||
$arguments += $Mcp.command
|
||||
$arguments += @($Mcp.args)
|
||||
Invoke-External "codex add $($Mcp.name)" "codex" $arguments
|
||||
Report-UnsupportedKeys $Mcp.name
|
||||
return
|
||||
}
|
||||
if ($Mcp.headers.Count -gt 0) {
|
||||
@@ -248,6 +289,17 @@ function Add-ToCodex($Mcp) {
|
||||
}
|
||||
Backup-CodexConfig
|
||||
Invoke-External "codex add $($Mcp.name)" "codex" @("mcp", "add", $Mcp.name, "--url", $Mcp.url)
|
||||
Report-UnsupportedKeys $Mcp.name
|
||||
}
|
||||
|
||||
# Some MCPs need Codex keys that `codex mcp add` has no flag for and that config.toml
|
||||
# cannot supply either (see the storage-model note at the top). Surface them rather than
|
||||
# pretending the registration is complete.
|
||||
function Report-UnsupportedKeys([string]$Name) {
|
||||
if (-not $script:Overrides.ContainsKey($Name)) { return }
|
||||
$keys = @($script:Overrides[$Name].psobject.Properties | Where-Object { $_.Name -notlike "_*" } |
|
||||
ForEach-Object { "$($_.Name)=$($_.Value)" })
|
||||
if ($keys) { $script:Unsupported += "$Name wants $($keys -join ', ') - no interface in codex-cli to set these" }
|
||||
}
|
||||
|
||||
function Add-ToClaude($Mcp) {
|
||||
@@ -344,6 +396,20 @@ if ($Apply) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($Apply) {
|
||||
# Codex re-reads config.toml on every invocation, so this confirms what it will actually load.
|
||||
$after = Get-PortableMcps (Get-CodexMcps) "Codex"
|
||||
$stillMissing = @($claude.Keys | Where-Object { -not $after.ContainsKey($_) } | Sort-Object)
|
||||
Write-Host "=== post-apply verification (codex mcp list) ==="
|
||||
Write-Host "Codex now serves: $(@($after.Keys | Sort-Object) -join ', ')"
|
||||
if ($stillMissing) { $script:Failures += "still absent from Codex after apply: $($stillMissing -join ', ')" }
|
||||
}
|
||||
|
||||
if ($script:Unsupported.Count -gt 0) {
|
||||
Write-Host "=== registered, but incomplete (codex-cli cannot express these keys) ==="
|
||||
$script:Unsupported | ForEach-Object { Write-Host "- $_" }
|
||||
}
|
||||
|
||||
if ($script:Failures.Count -gt 0) {
|
||||
Write-Host "=== failures ==="
|
||||
$script:Failures | ForEach-Object { Write-Host "- $_" }
|
||||
|
||||
Reference in New Issue
Block a user