🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

superpowers-mcp

Package Overview
Dependencies
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

superpowers-mcp - npm Package Compare versions

Comparing version
6.0.1
to
6.0.2
+232
skills/brainstorming/scripts/start-server.ps1
#!/usr/bin/env pwsh
# Start the brainstorm server and output connection info.
# Usage: ./start-server.ps1 [--project-dir <path>] [--host <bind-host>] [--url-host <display-host>] [--foreground] [--background]
$ErrorActionPreference = "Stop"
function Write-JsonError {
param([string]$Message)
[pscustomobject]@{ error = $Message } | ConvertTo-Json -Compress
}
function New-ServerId {
$bytes = New-Object byte[] 24
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
try {
$rng.GetBytes($bytes)
} finally {
$rng.Dispose()
}
-join ($bytes | ForEach-Object { $_.ToString("x2") })
}
function Set-ProcessEnvironment {
param([hashtable]$Values)
foreach ($key in $Values.Keys) {
Set-Item -Path "Env:$key" -Value ([string]$Values[$key])
}
}
function Protect-PathForCurrentUser {
param([string]$Path)
if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) {
return
}
try {
$item = Get-Item -LiteralPath $Path -ErrorAction Stop
$acl = Get-Acl -LiteralPath $item.FullName
$acl.SetAccessRuleProtection($true, $false)
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$rights = [System.Security.AccessControl.FileSystemRights]::FullControl
if ($item.PSIsContainer) {
$inheritance = [System.Security.AccessControl.InheritanceFlags]"ContainerInherit,ObjectInherit"
} else {
$inheritance = [System.Security.AccessControl.InheritanceFlags]::None
}
$propagation = [System.Security.AccessControl.PropagationFlags]::None
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
$identity,
$rights,
$inheritance,
$propagation,
[System.Security.AccessControl.AccessControlType]::Allow
)
$acl.SetAccessRule($rule)
Set-Acl -LiteralPath $item.FullName -AclObject $acl
} catch {
# Best-effort parity with the Unix helper's umask 077.
}
}
$scriptDir = Split-Path -Parent $PSCommandPath
$projectDir = ""
$foreground = $false
$forceBackground = $false
$bindHost = "127.0.0.1"
$urlHost = ""
$idleTimeoutMinutes = ""
for ($i = 0; $i -lt $args.Count; $i++) {
switch ($args[$i]) {
"--project-dir" {
$i++
if ($i -ge $args.Count) { Write-JsonError "Missing value for --project-dir"; exit 1 }
$projectDir = $args[$i]
}
"--host" {
$i++
if ($i -ge $args.Count) { Write-JsonError "Missing value for --host"; exit 1 }
$bindHost = $args[$i]
}
"--url-host" {
$i++
if ($i -ge $args.Count) { Write-JsonError "Missing value for --url-host"; exit 1 }
$urlHost = $args[$i]
}
"--idle-timeout-minutes" {
$i++
if ($i -ge $args.Count) { Write-JsonError "Missing value for --idle-timeout-minutes"; exit 1 }
$idleTimeoutMinutes = $args[$i]
}
"--open" {
$env:BRAINSTORM_OPEN = "1"
}
{ $_ -eq "--foreground" -or $_ -eq "--no-daemon" } {
$foreground = $true
}
{ $_ -eq "--background" -or $_ -eq "--daemon" } {
$forceBackground = $true
}
default {
Write-JsonError "Unknown argument: $($args[$i])"
exit 1
}
}
}
if ($urlHost -eq "") {
if ($bindHost -eq "127.0.0.1" -or $bindHost -eq "localhost") {
$urlHost = "localhost"
} else {
$urlHost = $bindHost
}
}
if ($idleTimeoutMinutes -ne "") {
$parsedIdle = 0
if ((-not [int]::TryParse($idleTimeoutMinutes, [ref]$parsedIdle)) -or $parsedIdle -lt 1) {
Write-JsonError "--idle-timeout-minutes must be a positive integer"
exit 1
}
$env:BRAINSTORM_IDLE_TIMEOUT_MS = [string]($parsedIdle * 60 * 1000)
}
if ($env:CODEX_CI -and -not $foreground -and -not $forceBackground) {
$foreground = $true
}
$sessionId = "$PID-$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())"
$brainstormRoot = ""
if ($projectDir -ne "") {
$brainstormRoot = Join-Path $projectDir ".superpowers/brainstorm"
$sessionDir = Join-Path $brainstormRoot $sessionId
$env:BRAINSTORM_PORT_FILE = Join-Path $brainstormRoot ".last-port"
$env:BRAINSTORM_TOKEN_FILE = Join-Path $brainstormRoot ".last-token"
} else {
$sessionDir = Join-Path ([System.IO.Path]::GetTempPath()) "brainstorm-$sessionId"
}
$stateDir = Join-Path $sessionDir "state"
$contentDir = Join-Path $sessionDir "content"
$pidFile = Join-Path $stateDir "server.pid"
$logFile = Join-Path $stateDir "server.log"
$serverIdFile = Join-Path $stateDir "server-instance-id"
New-Item -ItemType Directory -Force -Path $contentDir, $stateDir | Out-Null
if ($brainstormRoot -ne "") {
Protect-PathForCurrentUser -Path $brainstormRoot
}
Protect-PathForCurrentUser -Path $sessionDir
Protect-PathForCurrentUser -Path $contentDir
Protect-PathForCurrentUser -Path $stateDir
$serverId = New-ServerId
Set-Content -Path $serverIdFile -Value $serverId -NoNewline -Encoding utf8
Protect-PathForCurrentUser -Path $serverIdFile
if (Test-Path -LiteralPath $pidFile) {
$oldPid = (Get-Content -LiteralPath $pidFile -ErrorAction SilentlyContinue | Select-Object -First 1)
if ($oldPid) {
Stop-Process -Id ([int]$oldPid) -ErrorAction SilentlyContinue
}
Remove-Item -LiteralPath $pidFile -Force -ErrorAction SilentlyContinue
}
$envValues = @{
BRAINSTORM_DIR = $sessionDir
BRAINSTORM_HOST = $bindHost
BRAINSTORM_URL_HOST = $urlHost
BRAINSTORM_OWNER_PID = ""
BRAINSTORM_PORT_FILE = $env:BRAINSTORM_PORT_FILE
BRAINSTORM_TOKEN_FILE = $env:BRAINSTORM_TOKEN_FILE
BRAINSTORM_IDLE_TIMEOUT_MS = $env:BRAINSTORM_IDLE_TIMEOUT_MS
BRAINSTORM_OPEN = $env:BRAINSTORM_OPEN
}
if ($foreground) {
foreach ($key in $envValues.Keys) {
Set-Item -Path "Env:$key" -Value ([string]$envValues[$key])
}
$psi = [System.Diagnostics.ProcessStartInfo]::new()
$psi.FileName = "node"
$psi.WorkingDirectory = $scriptDir
$psi.UseShellExecute = $false
$psi.RedirectStandardOutput = $false
$psi.RedirectStandardError = $false
$psi.Arguments = "server.cjs --brainstorm-server-id=$serverId"
$process = [System.Diagnostics.Process]::new()
$process.StartInfo = $psi
$null = $process.Start()
Set-Content -Path $pidFile -Value ([string]$process.Id) -NoNewline -Encoding ascii
Protect-PathForCurrentUser -Path $pidFile
try {
$process.WaitForExit()
exit $process.ExitCode
} finally {
Remove-Item -LiteralPath $pidFile -Force -ErrorAction SilentlyContinue
}
}
Set-ProcessEnvironment -Values $envValues
$errFile = Join-Path $stateDir "server.err"
$process = Start-Process `
-FilePath "node" `
-ArgumentList @("server.cjs", "--brainstorm-server-id=$serverId") `
-WorkingDirectory $scriptDir `
-RedirectStandardOutput $logFile `
-RedirectStandardError $errFile `
-PassThru
Set-Content -Path $pidFile -Value ([string]$process.Id) -NoNewline -Encoding ascii
Protect-PathForCurrentUser -Path $pidFile
$deadline = (Get-Date).AddSeconds(5)
$reported = $false
while ((Get-Date) -lt $deadline) {
if ($process.HasExited) {
Write-JsonError "Server exited before startup"
exit 1
}
Start-Sleep -Milliseconds 100
$infoFile = Join-Path $stateDir "server-info"
if (Test-Path -LiteralPath $infoFile) {
Get-Content -LiteralPath $infoFile | Select-Object -First 1
$reported = $true
break
}
}
if (-not $reported) {
Write-JsonError "Server failed to start within 5 seconds"
exit 1
}
#!/usr/bin/env pwsh
# Stop the brainstorm server and clean up.
# Usage: ./stop-server.ps1 <session_dir>
$ErrorActionPreference = "Stop"
if ($args.Count -ne 1) {
[pscustomobject]@{ error = "Usage: stop-server.ps1 <session_dir>" } | ConvertTo-Json -Compress
exit 1
}
$sessionDir = $args[0]
$stateDir = Join-Path $sessionDir "state"
$pidFile = Join-Path $stateDir "server.pid"
$serverIdFile = Join-Path $stateDir "server-instance-id"
function Mark-Stopped {
param([string]$Reason)
Remove-Item -LiteralPath (Join-Path $stateDir "server-info") -Force -ErrorAction SilentlyContinue
[pscustomobject]@{
reason = $Reason
timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
} | ConvertTo-Json -Compress | Set-Content -Path (Join-Path $stateDir "server-stopped") -Encoding utf8
}
function Read-ExpectedServerId {
if (-not (Test-Path -LiteralPath $serverIdFile)) { return $null }
$id = (Get-Content -LiteralPath $serverIdFile -Raw).Trim()
if ($id -match '^[A-Za-z0-9_-]{32,64}$') { return $id }
return $null
}
function Test-BrainstormServer {
param([int]$ProcessId)
$expected = Read-ExpectedServerId
if (-not $expected) { return $false }
$process = Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId" -ErrorAction SilentlyContinue
if (-not $process) { return $false }
return ($process.CommandLine -like "*--brainstorm-server-id=$expected*")
}
if (Test-Path -LiteralPath $pidFile) {
$pidText = (Get-Content -LiteralPath $pidFile -ErrorAction SilentlyContinue | Select-Object -First 1)
$serverPid = 0
if ((-not [int]::TryParse($pidText, [ref]$serverPid)) -or -not (Test-BrainstormServer -ProcessId $serverPid)) {
Remove-Item -LiteralPath $pidFile, $serverIdFile -Force -ErrorAction SilentlyContinue
Mark-Stopped "stale_pid"
[pscustomobject]@{ status = "stale_pid" } | ConvertTo-Json -Compress
exit 0
}
Stop-Process -Id $serverPid -ErrorAction SilentlyContinue
$deadline = (Get-Date).AddSeconds(2)
while ((Get-Date) -lt $deadline -and (Get-Process -Id $serverPid -ErrorAction SilentlyContinue)) {
Start-Sleep -Milliseconds 100
}
if (Get-Process -Id $serverPid -ErrorAction SilentlyContinue) {
Stop-Process -Id $serverPid -Force -ErrorAction SilentlyContinue
Start-Sleep -Milliseconds 100
}
if (Get-Process -Id $serverPid -ErrorAction SilentlyContinue) {
[pscustomobject]@{ status = "failed"; error = "process still running" } | ConvertTo-Json -Compress
exit 1
}
Remove-Item -LiteralPath $pidFile, $serverIdFile, (Join-Path $stateDir "server.log") -Force -ErrorAction SilentlyContinue
Mark-Stopped "stop-server.ps1"
$tempRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
if (-not $tempRoot.EndsWith([string][System.IO.Path]::DirectorySeparatorChar)) {
$tempRoot += [System.IO.Path]::DirectorySeparatorChar
}
$resolvedSession = (Resolve-Path -LiteralPath $sessionDir -ErrorAction SilentlyContinue).Path
if ($resolvedSession) {
$resolvedSession = [System.IO.Path]::GetFullPath($resolvedSession)
}
if ($resolvedSession -and $resolvedSession.StartsWith($tempRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
Remove-Item -LiteralPath $sessionDir -Recurse -Force -ErrorAction SilentlyContinue
}
[pscustomobject]@{ status = "stopped" } | ConvertTo-Json -Compress
} else {
[pscustomobject]@{ status = "not_running" } | ConvertTo-Json -Compress
}
#!/usr/bin/env pwsh
# Generate a review package: commit list, stat summary, and net diff.
# Usage: ./review-package.ps1 BASE HEAD [OUTFILE]
$ErrorActionPreference = "Stop"
if ($args.Count -lt 2 -or $args.Count -gt 3) {
Write-Error "usage: review-package.ps1 BASE HEAD [OUTFILE]"
exit 2
}
$base = $args[0]
$head = $args[1]
& git rev-parse --verify --quiet $base *> $null
if ($LASTEXITCODE -ne 0) {
Write-Error "bad BASE: $base"
exit 2
}
& git rev-parse --verify --quiet $head *> $null
if ($LASTEXITCODE -ne 0) {
Write-Error "bad HEAD: $head"
exit 2
}
if ($args.Count -eq 3) {
$out = $args[2]
} else {
$scriptDir = Split-Path -Parent $PSCommandPath
$dir = (& (Join-Path $scriptDir "sdd-workspace.ps1")).Trim()
$baseShort = (& git rev-parse --short $base).Trim()
$headShort = (& git rev-parse --short $head).Trim()
$out = Join-Path $dir "review-$baseShort..$headShort.diff"
}
$content = New-Object System.Collections.Generic.List[string]
$content.Add("# Review package: ${base}..${head}")
$content.Add("")
$content.Add("## Commits")
(& git log --oneline "${base}..${head}") | ForEach-Object { $content.Add($_) }
$content.Add("")
$content.Add("## Files changed")
(& git diff --stat "${base}..${head}") | ForEach-Object { $content.Add($_) }
$content.Add("")
$content.Add("## Diff")
(& git diff -U10 "${base}..${head}") | ForEach-Object { $content.Add($_) }
Set-Content -Path $out -Value $content -Encoding utf8
$commits = (& git rev-list --count "${base}..${head}").Trim()
$bytes = (Get-Item -LiteralPath $out).Length
Write-Output "wrote ${out}: $commits commit(s), $bytes bytes"
#!/usr/bin/env pwsh
# Resolve and ensure the working-tree directory SDD uses for short-lived artifacts.
# Usage: ./sdd-workspace.ps1
$ErrorActionPreference = "Stop"
$root = (& git rev-parse --show-toplevel).Trim()
$dir = Join-Path $root ".superpowers/sdd"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
Set-Content -Path (Join-Path $dir ".gitignore") -Value "*" -NoNewline -Encoding ascii
(Resolve-Path $dir).Path
#!/usr/bin/env pwsh
# Extract one task's full text from an implementation plan.
# Usage: ./task-brief.ps1 PLAN_FILE TASK_NUMBER [OUTFILE]
$ErrorActionPreference = "Stop"
if ($args.Count -lt 2 -or $args.Count -gt 3) {
Write-Error "usage: task-brief.ps1 PLAN_FILE TASK_NUMBER [OUTFILE]"
exit 2
}
$plan = $args[0]
$taskNumber = $args[1]
if (-not (Test-Path -LiteralPath $plan -PathType Leaf)) {
Write-Error "no such plan file: $plan"
exit 2
}
if ($args.Count -eq 3) {
$out = $args[2]
} else {
$scriptDir = Split-Path -Parent $PSCommandPath
$dir = (& (Join-Path $scriptDir "sdd-workspace.ps1")).Trim()
$out = Join-Path $dir "task-$taskNumber-brief.md"
}
$inFence = $false
$inTask = $false
$pattern = "^#+[ \t]+Task[ \t]+$([regex]::Escape($taskNumber))([^0-9]|$)"
$selected = New-Object System.Collections.Generic.List[string]
foreach ($line in [System.IO.File]::ReadLines((Resolve-Path -LiteralPath $plan).Path)) {
if ($line -match '^```') {
$inFence = -not $inFence
}
if (-not $inFence -and $line -match '^#+[ \t]+Task[ \t]+[0-9]+') {
$inTask = ($line -match $pattern)
}
if ($inTask) {
$selected.Add($line)
}
}
Set-Content -Path $out -Value $selected -Encoding utf8
if ((-not (Test-Path -LiteralPath $out)) -or ((Get-Item -LiteralPath $out).Length -eq 0)) {
Write-Error "task $taskNumber not found in $plan (no heading matching 'Task $taskNumber')"
exit 3
}
$lineCount = ($selected | Measure-Object).Count
Write-Output "wrote ${out}: $lineCount lines"
#!/usr/bin/env pwsh
# Bisection script to find which test creates unwanted files/state.
# Usage: ./find-polluter.ps1 <file_or_dir_to_check> <test_pattern>
$ErrorActionPreference = "Stop"
if ($args.Count -ne 2) {
Write-Output "Usage: ./find-polluter.ps1 <file_to_check> <test_pattern>"
Write-Output "Example: ./find-polluter.ps1 '.git' 'src/**/*.test.ts'"
exit 1
}
$pollutionCheck = $args[0]
$testPattern = $args[1]
Write-Output "Searching for test that creates: $pollutionCheck"
Write-Output "Test pattern: $testPattern"
Write-Output ""
$testFiles = @(Get-ChildItem -Path $testPattern -File -Recurse -ErrorAction SilentlyContinue | Sort-Object FullName)
if ($testFiles.Count -eq 0) {
$testFiles = @(Get-ChildItem -Path . -File -Recurse | Where-Object { $_.FullName -like (Join-Path (Get-Location) $testPattern) } | Sort-Object FullName)
}
$total = $testFiles.Count
Write-Output "Found $total test files"
Write-Output ""
$count = 0
foreach ($testFile in $testFiles) {
$count++
if (Test-Path -LiteralPath $pollutionCheck) {
Write-Output "Pollution already exists before test $count/$total"
Write-Output " Skipping: $($testFile.FullName)"
continue
}
Write-Output "[$count/$total] Testing: $($testFile.FullName)"
& npm test $testFile.FullName *> $null
if (Test-Path -LiteralPath $pollutionCheck) {
Write-Output ""
Write-Output "FOUND POLLUTER"
Write-Output " Test: $($testFile.FullName)"
Write-Output " Created: $pollutionCheck"
Write-Output ""
Write-Output "Pollution details:"
Get-ChildItem -Force -LiteralPath $pollutionCheck | Format-List
Write-Output ""
Write-Output "To investigate:"
Write-Output " npm test $($testFile.FullName)"
Write-Output " Get-Content $($testFile.FullName)"
exit 1
}
}
Write-Output ""
Write-Output "No polluter found - all tests clean."
exit 0
+1
-1

@@ -5,3 +5,3 @@ {

"description": "Superpowers skills library (TDD, debugging, collaboration workflows) as an MCP server for VSCode and Antigravity",
"version": "6.0.1",
"version": "6.0.2",
"publisher": "superpowers",

@@ -8,0 +8,0 @@ "license": "MIT",

@@ -5,3 +5,3 @@ # Superpowers MCP Toolpack Usage Guide

[![Version](https://img.shields.io/badge/version-6.0.0-blue.svg)](https://github.com/Poseidoncode/superpowers-mcp)
[![Version](https://img.shields.io/badge/version-6.0.2-blue.svg)](https://github.com/Poseidoncode/superpowers-mcp)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

@@ -132,3 +132,31 @@

### v6.0.1 (Latest)
### v6.0.2 (Latest)
- **Modular Refactoring & Performance Upgrades**:
- **Decoupled Architecture**: Extracted file system access, metadata caching, and parsing logic into a dedicated [`src/skills-manager.ts`](src/skills-manager.ts), leaving [`src/server.ts`](src/server.ts) purely focused on MCP protocol handling.
- **O(1) Map-Based Cache**: Replaced the $O(N)$ double-array scan with case-insensitive, dual-key (by name and directory name) memory caches for fast $O(1)$ lookups.
- **Async I/O Pipeline**: Swapped synchronous file API calls (`readdirSync`, `readFileSync`) with promises and `Promise.all` concurrent execution, unlocking high-throughput performance.
- **Markdown Cache**: Cached stripped skill content in memory to avoid repetitive disk reads when tools are invoked frequently.
- **Security Hardening**:
- **ReDoS Prevention**: Replaced regex-based frontmatter parser with a safe, line-by-line state machine parser, completely eliminating CPU exhaustion risks and supporting multiline YAML descriptions.
- **Path Traversal Shield**: Added strict alphanumeric white-listing (`/^[a-zA-Z0-9-_]+$/`) on skill name inputs to prevent traversal attacks.
- **Directory Injection Check**: Validated `SKILLS_PATH` to actively reject potentially hostile system root folders.
- **Path & Username Leak Protection**: Caught native file system errors and masked them into generic, path-free `McpError` payloads.
- **Windows Build and Script Safety**: Handled Windows `chmodSync` platform checks in `esbuild.js` and skipped Symlinks in `copy-skills.js` to prevent recursive file copy loops.
- **Upstream Security Cherry-Picks**: Applied security hardening from obra/superpowers v6.1.1:
- **WebSocket frame size validation**: Added `MAX_FRAME_PAYLOAD_BYTES (10 MB)` check in `decodeFrame()` to prevent oversized frame attacks (CWE-789). Dual protection — BigInt extended-length and general post-resolution guard.
- **Hardlink containment**: Added `stat.nlink !== 1` check in `isRegularFileInsideContentDir()` prevents path traversal via hardlinks.
- **`escapeHtmlText()` extraction**: Extracted inline `escHtml` closure into a reusable named function for consistent HTML escaping.
- **URL parsing refactor**: Extracted `pathnameOf()` and `queryKey()` helpers, reducing duplicate inline URL logic in `handleRequest()`.
- **`review-package` Path Resolution Fix**: Fixed `sdd-workspace` invocation to use absolute path resolution (`$(cd "$(dirname "$0")" && pwd)`) instead of relative path, fixing CWD-dependent failures.
- **Windows Native Helper Scripts**: Added PowerShell wrappers for Visual Companion startup/shutdown, SDD review/task helpers, and systematic-debugging polluter detection.
- **Skill Documentation Enhancements**:
- `subagent-driven-development`: Added `plan-mandated` review guidance for handling plan conflicts.
- `writing-skills`: Strengthened prohibition vs. recipe guidance with empirical evidence from wording tests.
- `test-driven-development`: Fixed table formatting for clarity.
- `writing-skills/anthropic-best-practices`: Updated image CDN URLs.
- **`helper.js` Comment Alignment**: Added 4 clarifying inline comments to align with upstream documentation without changing behavior. DOM-safe `showTombstone()` preserved (no `innerHTML` regression).
- **Cleanup**: Removed obsolete `walkthrough.md` (v5.1.0 upgrade guide).
### v6.0.1
- **Security Fix — Reflected XSS (#2)**: Fixed server-side reflected cross-site scripting in `skills/brainstorming/scripts/server.cjs`. The `bootstrapPage()` function was called with the user-supplied `keyFromQuery` parameter (even though validated via `timingSafeEqualStr`). Changed to use the server-side `TOKEN` constant instead, eliminating user-tainted data from the HTML response sink. Zero behavior change (the validated value is identical).

@@ -135,0 +163,0 @@

@@ -5,3 +5,3 @@ # Superpowers MCP Toolpack 使用指南

[![版本](https://img.shields.io/badge/version-6.0.0-blue.svg)](https://github.com/Poseidoncode/superpowers-mcp)
[![版本](https://img.shields.io/badge/version-6.0.2-blue.svg)](https://github.com/Poseidoncode/superpowers-mcp)
[![授權](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

@@ -132,3 +132,30 @@

### v6.0.1 (最新版)
### v6.0.2 (最新版)
- **模組化拆分與效能提升**:
- **職責解耦**:將檔案存取、YAML 解析與快取邏輯抽離至獨立模組 [`src/skills-manager.ts`](src/skills-manager.ts),主程式 [`src/server.ts`](src/server.ts) 專注於 MCP 路由註冊。
- **O(1) 雙向快取**:引入大小寫不敏感的 Map 雙向鍵值快取(以技能名與目錄名為 Key),將原本 $O(N)$ 的陣列雙重遍歷優化為 $O(1)$ 的直接讀取。
- **非同步 I/O 管線**:全面以 `fs/promises` 代替同步磁碟操作,搭配 `Promise.all` 併發枚舉,釋放 Node.js 事件循環阻塞。
- **Markdown 內容快取**:快取已剝離 YAML frontmatter 的技能文檔,避免工具頻繁調用時對硬碟的重複讀寫損耗。
- **安全性深度防禦**:
- **防範 ReDoS 攻擊**:棄用非貪婪正則,重構為「逐行 Frontmatter 解析器」,規避了惡意/損壞 Markdown 導致的 CPU 回溯鎖死風險,且支援了 YAML 多行 `description` 欄位。
- **路徑遍歷(Path Traversal)防禦**:對 `skill_name` 輸入參數使用英數白名單篩選(`/^[a-zA-Z0-9-_]+$/`)。
- **絕對路徑防洩露**:安全捕獲原生 I/O 錯誤,隱蔽主機真實實體路徑與帳號名稱,回傳通用 `McpError`。
- **環境路徑與指令碼加固**:檢測 `SKILLS_PATH` 防範根目錄惡意注入;修復 `esbuild.js` 在 Windows 上的 `chmodSync` 崩潰問題,並在 `copy-skills.js` 中跳過符號連結 (Symlink) 杜絕遞迴拷貝死循環。
- **上游安全更新同步**:套用來自 obra/superpowers v6.1.1 的安全加固:
- **WebSocket 影格長度限制**:在 `decodeFrame()` 中新增 `MAX_FRAME_PAYLOAD_BYTES (10 MB)` 檢測,防止超大型影格攻擊(CWE-789)。
- **硬連結限制**:在 `isRegularFileInsideContentDir()` 中加入 `stat.nlink !== 1` 檢測,防止透過硬連結 (Hardlink) 繞過路徑遍歷。
- **提取 `escapeHtmlText()`**:將行內的 `escHtml` 閉包提取為可重複使用的具名函數,以確保 HTML 逸出的一致性。
- **URL 解析重構**:抽離出 `pathnameOf()` and `queryKey()` 輔助函數,減少 `handleRequest()` 中的重複 URL 解析。
- **`review-package` 路徑解析修正**:修復 `sdd-workspace` 呼叫,改用絕對路徑解析 (`$(cd "$(dirname "$0")" && pwd)`) 以防止與工作路徑 (CWD) 依賴相關的調用失敗。
- **Windows 原生輔助腳本**:新增 Visual Companion 啟動/關閉、SDD review/task helper,以及 systematic-debugging polluter detection 的 PowerShell wrapper。
- **技能文檔改進**:
- `subagent-driven-development`:新增 `plan-mandated` 審查指引,以處理計畫之間的衝突。
- `writing-skills`:結合字詞測試的實證,強化「禁止撰寫步驟清單 (recipes) 技能」的指引。
- `test-driven-development`:修正表格格式以提升清晰度。
- `writing-skills/anthropic-best-practices`:更新圖片的 CDN 網址。
- **`helper.js` 註解對齊**:新增 4 個說明的行內註解以對齊上游文檔。保留 DOM 安全的 `showTombstone()` 實作(無 `innerHTML` 退化)。
- **清理**:移除了已廢棄的 `walkthrough.md` (v5.1.0 升級指南)。
### v6.0.1
- **安全性修復 — Reflected XSS (#2)**: 修復 `skills/brainstorming/scripts/server.cjs` 中的伺服器端反射型跨站腳本漏洞。原本 `bootstrapPage()` 使用使用者提供的 `keyFromQuery` 參數(雖已通過 `timingSafeEqualStr` 驗證),現改為使用伺服器端 `TOKEN` 常數,徹底消除使用者可控資料進入 HTML 回應的風險。行為完全不變(驗證後的值相同)。

@@ -135,0 +162,0 @@

@@ -60,2 +60,3 @@ (function() {

// Self-styled so it works on framed and full-document screens alike.
function showTombstone() {

@@ -136,2 +137,3 @@ if (tombstoneShown) return;

// Capture clicks on choice elements
document.addEventListener('click', (e) => {

@@ -150,2 +152,3 @@ const target = e.target.closest('[data-choice]');

// Frame UI: selection tracking
window.selectedChoice = null;

@@ -167,2 +170,3 @@

// Expose API for explicit use
window.brainstorm = {

@@ -169,0 +173,0 @@ send: sendEvent,

@@ -65,2 +65,6 @@ const crypto = require('crypto');

if (payloadLen > MAX_FRAME_PAYLOAD_BYTES) {
throw new Error('WebSocket frame payload exceeds maximum allowed size');
}
const maskOffset = offset;

@@ -231,5 +235,12 @@ const dataOffset = offset + 4;

function escapeHtmlText(value) {
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function brandMarkup() {
const escHtml = (v) => String(v).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
const version = escHtml(SUPERPOWERS_VERSION);
const version = escapeHtmlText(SUPERPOWERS_VERSION);
const text = SUPERPOWERS_TELEMETRY_DISABLED

@@ -301,2 +312,3 @@ ? 'Prime Radiant Superpowers v' + version

if (!stat.isFile()) return false;
if (stat.nlink !== 1) return false;
realContentDir = fs.realpathSync(CONTENT_DIR);

@@ -346,2 +358,13 @@ realFilePath = fs.realpathSync(filePath);

function pathnameOf(url) {
const q = url.indexOf('?');
return q >= 0 ? url.slice(0, q) : url;
}
function queryKey(url) {
const q = url.indexOf('?');
if (q < 0) return null;
return new URLSearchParams(url.slice(q + 1)).get('key');
}
function securityHeaders(headers = {}) {

@@ -382,5 +405,4 @@ return {

const qIdx = req.url.indexOf('?');
const pathname = qIdx >= 0 ? req.url.slice(0, qIdx) : req.url;
const keyFromQuery = qIdx >= 0 ? new URLSearchParams(req.url.slice(qIdx + 1)).get('key') : null;
const pathname = pathnameOf(req.url);
const keyFromQuery = queryKey(req.url);
if (req.method === 'GET' && pathname === '/' && keyFromQuery && timingSafeEqualStr(keyFromQuery, TOKEN)) {

@@ -387,0 +409,0 @@ res.writeHead(200, securityHeaders({ 'Content-Type': 'text/html; charset=utf-8' }));

@@ -40,2 +40,5 @@ # Visual Companion Guide

# Windows PowerShell:
scripts/start-server.ps1 --project-dir C:\path\to\project --open
# Returns: {"type":"server-started","port":52341,

@@ -71,2 +74,4 @@ # "url":"http://localhost:52341/?key=ab12…",

For native PowerShell, use `scripts/start-server.ps1` with the same flags. It writes the same `server-info` file and supports `--project-dir`, `--host`, `--url-host`, `--idle-timeout-minutes`, `--open`, `--foreground`, and `--background`.
**Codex:**

@@ -286,2 +291,5 @@ ```bash

scripts/stop-server.sh $SESSION_DIR
# Windows PowerShell:
scripts/stop-server.ps1 $SESSION_DIR
```

@@ -288,0 +296,0 @@

@@ -136,3 +136,3 @@ ---

**DONE:** Generate the review package (`scripts/review-package BASE HEAD`, from this skill's directory — it prints the unique file path it wrote; BASE is the commit you recorded before dispatching the implementer — never `HEAD~1`, which silently drops all but the last commit of a multi-commit task), then dispatch the task reviewer with the printed path.
**DONE:** Generate the review package (`scripts/review-package BASE HEAD`, or `scripts/review-package.ps1 BASE HEAD` on Windows PowerShell, from this skill's directory — it prints the unique file path it wrote; BASE is the commit you recorded before dispatching the implementer — never `HEAD~1`, which silently drops all but the last commit of a multi-commit task), then dispatch the task reviewer with the printed path.

@@ -183,3 +183,3 @@ **DONE_WITH_CONCERNS:** The implementer completed the work but flagged doubts. Read the concerns before proceeding. If the concerns are about correctness or scope, address them before review. If they're observations (e.g., "this file is getting large"), note them and proceed to review.

- Hand the reviewer its diff as a file: run this skill's
`scripts/review-package BASE HEAD` and pass the reviewer the file path
`scripts/review-package BASE HEAD` (or `scripts/review-package.ps1 BASE HEAD` on Windows PowerShell) and pass the reviewer the file path
it prints (or, without bash: `git log --oneline`, `git diff --stat`,

@@ -199,3 +199,3 @@ and `git diff -U10` for the range, redirected to one uniquely named

whole-branch review at that list so it can triage which must be fixed
before merge.
before merge. A roll-up nobody reads is a silent discard.
- A finding labeled plan-mandated — or any finding that conflicts with

@@ -207,3 +207,3 @@ what the plan's text requires — is the human's decision, like any plan

- The final whole-branch review gets a package too: run
`scripts/review-package MERGE_BASE HEAD` (MERGE_BASE = the commit the
`scripts/review-package MERGE_BASE HEAD` (or `scripts/review-package.ps1 MERGE_BASE HEAD` on Windows PowerShell; MERGE_BASE = the commit the
branch started from, e.g. `git merge-base main HEAD`) and include the

@@ -230,3 +230,3 @@ printed path in the final review dispatch, so the final reviewer reads

- **Task brief:** before dispatching an implementer, run this skill's
`scripts/task-brief PLAN_FILE N` — it extracts the task's full text to a
`scripts/task-brief PLAN_FILE N` (or `scripts/task-brief.ps1 PLAN_FILE N` on Windows PowerShell) — it extracts the task's full text to a
uniquely named file and prints the path. Compose the dispatch so the

@@ -380,3 +380,3 @@ brief stays the single source of requirements. Your dispatch should

- Make a subagent read the whole plan file (hand it its task brief —
`scripts/task-brief` — instead)
`scripts/task-brief` / `scripts/task-brief.ps1` — instead)
- Skip scene-setting context (subagent needs to understand where task fits)

@@ -391,3 +391,3 @@ - Ignore subagent questions (answer before letting them proceed)

- Dispatch a task reviewer without a diff file — generate it first
(`scripts/review-package BASE HEAD`) and name the printed path in the
(`scripts/review-package BASE HEAD`, or `scripts/review-package.ps1 BASE HEAD` on Windows PowerShell) and name the printed path in the
prompt

@@ -394,0 +394,0 @@ - Move to next task while the review has open Critical/Important issues

@@ -170,3 +170,4 @@ # Task Reviewer Prompt Template

- `[MODEL]` — REQUIRED: reviewer model per SKILL.md Model Selection
- `[BRIEF_FILE]` — REQUIRED: the task brief file (`scripts/task-brief PLAN N`
- `[BRIEF_FILE]` — REQUIRED: the task brief file (`scripts/task-brief PLAN N`,
or `scripts/task-brief.ps1 PLAN N` on Windows PowerShell,
prints the path; same file the implementer worked from)

@@ -182,3 +183,3 @@ - `[GLOBAL_CONSTRAINTS]` — the binding requirements copied verbatim from

- `[DIFF_FILE]` — REQUIRED: the path the controller wrote the review
package to (`scripts/review-package BASE HEAD` prints the unique path it
package to (`scripts/review-package BASE HEAD`, or `scripts/review-package.ps1 BASE HEAD` on Windows PowerShell, prints the unique path it
wrote; the package never enters the controller's context)

@@ -185,0 +186,0 @@

@@ -105,2 +105,5 @@ # Root Cause Tracing

./find-polluter.sh '.git' 'src/**/*.test.ts'
# Windows PowerShell:
./find-polluter.ps1 '.git' 'src/**/*.test.ts'
```

@@ -107,0 +110,0 @@

@@ -201,3 +201,3 @@ ---

| Quality | Good | Bad |
|---------|------|---------|
|---------|------|-----|
| **Minimal** | One thing. "and" in name? Split it. | `test('validates email and domain and whitespace')` |

@@ -204,0 +204,0 @@ | **Clear** | Name describes behavior | `test('test1')` |

@@ -253,3 +253,3 @@ # Skill authoring best practices

<img src="https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-bundling-content.png?fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=a5e0aa41e3d53985a7e3e43668a33ea3" alt="Bundling additional reference files like reference.md and forms.md." data-og-width="2048" width="2048" data-og-height="1327" height="1327" data-path="images/agent-skills-bundling-content.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-bundling-content.png?w=280&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=f8a0e73783e99b4a643d79eac86b70a2 280w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-bundling-content.png?w=560&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=dc510a2a9d3f14359416b706f067904a 560w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-bundling-content.png?w=840&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=82cd6286c966303f7dd914c28170e385 840w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-bundling-content.png?w=1100&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=56f3be36c77e4fe4b523df209a6824c6 1100w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-bundling-content.png?w=1650&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=d22b5161b2075656417d56f41a74f3dd 1650w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-bundling-content.png?w=2500&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=3dd4bdd6850ffcc26c6c45fcb0acd6eb 2500w" />
<img src="https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-bundling-content.png?fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=a5e0aa41e3d53985a7e3e43668a33ea3" alt="Bundling additional reference files like reference.md and forms.md." data-og-width="2048" width="2048" data-og-height="1327" height="1327" data-path="images/agent-skills-bundling-content.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-bundling-content.png?w=280&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=f8a0e73783e99b4a643d79eac86b70a2 280w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-bundling-content.png?w=560&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=dc510a2a9d3f14359416b706f067904a 560w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-bundling-content.png?w=840&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=82cd6286c966303f7dd914c28170e385 840w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-bundling-content.png?w=1100&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=56f3be36c77e4fe4b523df209a6824c6 1100w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-bundling-content.png?w=1650&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=d22b5161b2075656417d56f41a74f3dd 1650w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-bundling-content.png?w=2500&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=3dd4bdd6850ffcc96c6c45fcb0acd6eb 2500w" />

@@ -920,3 +920,3 @@ The complete Skill directory structure might look like this:

<img src="https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-executable-scripts.png?fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=4bbc45f2c2e0bee9f2f0d5da669bad00" alt="Bundling executable scripts alongside instruction files" data-og-width="2048" width="2048" data-og-height="1154" height="1154" data-path="images/agent-skills-executable-scripts.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-executable-scripts.png?w=280&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=9a04e6535a8467bfeea492e517de389f 280w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-executable-scripts.png?w=560&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=e49333ad90141af17c0d7651cca7216b 560w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-executable-scripts.png?w=840&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=954265a5df52223d6572b6214168c428 840w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-executable-scripts.png?w=1100&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=2ff7a2d8f2a83ee8af132b29f10150fd 1100w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-executable-scripts.png?w=1650&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=48ab96245e04077f4d15e9170e094cfb 1650w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-executable-scripts.png?w=2500&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=0301a6c8b3ee879497cc5b5483177c90 2500w" />
<img src="https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-executable-scripts.png?fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=4bbc45f2c2e0bee9f2f0d5da669bad00" alt="Bundling executable scripts alongside instruction files" data-og-width="2048" width="2048" data-og-height="1154" height="1154" data-path="images/agent-skills-executable-scripts.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-executable-scripts.png?w=280&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=9a04e6535a8467bfeea492e517de389f 280w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-executable-scripts.png?w=560&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=e49333ad90141af17c0d7651cca7216b 560w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-executable-scripts.png?w=840&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=954265a5df52223d6572b6214168c428 840w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-executable-scripts.png?w=1100&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=2ff7a2d8f2a83ee8af132b29f10150fd 1100w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-executable-scripts.png?w=1650&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=48ab96245e04077f4d15e9170e081cfb 1650w, https://mintcdn.com/anthropic-claude-docs/4Bny2bjzuGBK7o00/images/agent-skills-executable-scripts.png?w=2500&fit=max&auto=format&n=4Bny2bjzuGBK7o00&q=85&s=0301a6c8b3ee879497cc5b5483177c90 2500w" />

@@ -923,0 +923,0 @@ The diagram above shows how executable scripts work alongside instruction files. The instruction file (forms.md) references the script, and the agent can execute it without loading its contents into context.

@@ -470,3 +470,3 @@ ---

**Why prohibitions backfire on shaping problems:** in head-to-head wording tests on dispatch-prompt guidance, the prohibition arm produced more unwanted content than the recipe arm — and trended worse than the no-guidance control. Micro-test your own case rather than assuming. A recipe leaves nothing to negotiate: the output matches the stated shape or it doesn't.
**Why prohibitions backfire on shaping problems:** under a competing incentive ("make the prompt self-contained"), agents negotiate with "don't X". In head-to-head wording tests on dispatch-prompt guidance, the prohibition arm produced clearly more of the unwanted content than the recipe arm (fully separated distributions), and trended worse than even the no-guidance control — micro-test your own case rather than assuming, but never reach for the prohibition by default. A recipe leaves nothing to negotiate: the output matches the stated shape or it doesn't.

@@ -473,0 +473,0 @@ **Rules for whichever form you pick:**

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet