superpowers-mcp 6.2.2 → 6.2.4
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.
- package/README.ja.md +33 -2
- package/README.ko.md +33 -2
- package/README.md +28 -2
- package/README.zh-TW.md +28 -3
- package/out/server.js +17 -17
- package/package.json +6 -3
- package/skills/brainstorming/scripts/helper.js +8 -15
- package/skills/brainstorming/scripts/server.cjs +600 -142
- package/skills/brainstorming/scripts/start-server.ps1 +99 -9
- package/skills/brainstorming/scripts/start-server.sh +104 -15
- package/skills/brainstorming/scripts/stop-server.ps1 +33 -2
- package/skills/brainstorming/scripts/stop-server.sh +36 -4
- package/skills/brainstorming/visual-companion.md +19 -22
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env pwsh
|
|
2
2
|
# Start the brainstorm server and output connection info.
|
|
3
|
-
# Usage: ./start-server.ps1 [--project-dir <path>] [--host <
|
|
3
|
+
# Usage: ./start-server.ps1 [--project-dir <path>] [--host <loopback-host>] [--url-host <loopback-host>] [--foreground] [--background]
|
|
4
4
|
|
|
5
5
|
$ErrorActionPreference = "Stop"
|
|
6
6
|
|
|
@@ -29,11 +29,15 @@ function Set-ProcessEnvironment {
|
|
|
29
29
|
|
|
30
30
|
function Protect-PathForCurrentUser {
|
|
31
31
|
param([string]$Path)
|
|
32
|
-
|
|
33
|
-
return
|
|
34
|
-
}
|
|
32
|
+
$isWindowsPlatform = [System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT
|
|
35
33
|
try {
|
|
36
34
|
$item = Get-Item -LiteralPath $Path -ErrorAction Stop
|
|
35
|
+
if (-not $isWindowsPlatform) {
|
|
36
|
+
$mode = if ($item.PSIsContainer) { "700" } else { "600" }
|
|
37
|
+
& chmod $mode $item.FullName
|
|
38
|
+
if ($LASTEXITCODE -ne 0) { throw "chmod failed" }
|
|
39
|
+
return
|
|
40
|
+
}
|
|
37
41
|
$acl = Get-Acl -LiteralPath $item.FullName
|
|
38
42
|
$acl.SetAccessRuleProtection($true, $false)
|
|
39
43
|
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
@@ -112,6 +116,28 @@ if ($urlHost -eq "") {
|
|
|
112
116
|
}
|
|
113
117
|
}
|
|
114
118
|
|
|
119
|
+
function Test-LoopbackHost {
|
|
120
|
+
param([string]$HostValue)
|
|
121
|
+
$normalized = $HostValue.Trim().TrimStart('[').TrimEnd(']').ToLowerInvariant()
|
|
122
|
+
if ($normalized -eq "localhost" -or $normalized -eq "::1" -or $normalized.StartsWith("127.")) {
|
|
123
|
+
return $true
|
|
124
|
+
}
|
|
125
|
+
$address = $null
|
|
126
|
+
if ([System.Net.IPAddress]::TryParse($normalized, [ref]$address)) {
|
|
127
|
+
return [System.Net.IPAddress]::IsLoopback($address)
|
|
128
|
+
}
|
|
129
|
+
return $false
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (-not (Test-LoopbackHost $bindHost)) {
|
|
133
|
+
Write-JsonError "Refusing insecure non-loopback HTTP bind; use a TLS reverse proxy or tunnel to 127.0.0.1"
|
|
134
|
+
exit 1
|
|
135
|
+
}
|
|
136
|
+
if (-not (Test-LoopbackHost $urlHost)) {
|
|
137
|
+
Write-JsonError "--url-host must be a loopback hostname or address"
|
|
138
|
+
exit 1
|
|
139
|
+
}
|
|
140
|
+
|
|
115
141
|
if ($idleTimeoutMinutes -ne "") {
|
|
116
142
|
$parsedIdle = 0
|
|
117
143
|
if ((-not [int]::TryParse($idleTimeoutMinutes, [ref]$parsedIdle)) -or $parsedIdle -lt 1) {
|
|
@@ -130,10 +156,17 @@ $brainstormRoot = ""
|
|
|
130
156
|
if ($projectDir -ne "") {
|
|
131
157
|
$brainstormRoot = Join-Path $projectDir ".superpowers/brainstorm"
|
|
132
158
|
$sessionDir = Join-Path $brainstormRoot $sessionId
|
|
159
|
+
# Reuse the last bound port and session key so a restart keeps an
|
|
160
|
+
# already-open browser tab connected to the same URL with a valid cookie.
|
|
133
161
|
$env:BRAINSTORM_PORT_FILE = Join-Path $brainstormRoot ".last-port"
|
|
134
162
|
$env:BRAINSTORM_TOKEN_FILE = Join-Path $brainstormRoot ".last-token"
|
|
135
163
|
} else {
|
|
136
164
|
$sessionDir = Join-Path ([System.IO.Path]::GetTempPath()) "brainstorm-$sessionId"
|
|
165
|
+
# $env: assignments persist in the invoking pwsh session; a stale project
|
|
166
|
+
# token/port file from an earlier --project-dir run must not leak into an
|
|
167
|
+
# ephemeral session (it would defeat key rotation and could overwrite the
|
|
168
|
+
# project's .last-token).
|
|
169
|
+
Remove-Item Env:BRAINSTORM_TOKEN_FILE, Env:BRAINSTORM_PORT_FILE -ErrorAction SilentlyContinue
|
|
137
170
|
}
|
|
138
171
|
|
|
139
172
|
$stateDir = Join-Path $sessionDir "state"
|
|
@@ -151,17 +184,71 @@ Protect-PathForCurrentUser -Path $contentDir
|
|
|
151
184
|
Protect-PathForCurrentUser -Path $stateDir
|
|
152
185
|
|
|
153
186
|
$serverId = New-ServerId
|
|
154
|
-
Set-Content -Path $serverIdFile -Value $serverId -NoNewline -Encoding utf8
|
|
155
|
-
Protect-PathForCurrentUser -Path $serverIdFile
|
|
156
187
|
|
|
188
|
+
function Read-ExpectedServerId {
|
|
189
|
+
if (-not (Test-Path -LiteralPath $serverIdFile)) { return $null }
|
|
190
|
+
$id = (Get-Content -LiteralPath $serverIdFile -Raw -ErrorAction SilentlyContinue).Trim()
|
|
191
|
+
if ($id -match '^[A-Za-z0-9_-]{32,64}$') { return $id }
|
|
192
|
+
return $null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function Test-BrainstormServer {
|
|
196
|
+
param([int]$ProcessId)
|
|
197
|
+
$expected = Read-ExpectedServerId
|
|
198
|
+
if (-not $expected) { return $false }
|
|
199
|
+
# $IsWindows is undefined on Windows PowerShell 5.1; fall back to the
|
|
200
|
+
# OSVersion check so the Windows branch is taken there too.
|
|
201
|
+
$isWinPlatform = ($IsWindows -or [System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT)
|
|
202
|
+
if ($isWinPlatform) {
|
|
203
|
+
$process = Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId" -ErrorAction SilentlyContinue
|
|
204
|
+
if (-not $process) { return $false }
|
|
205
|
+
return ($process.CommandLine -like "*--brainstorm-server-id=$expected*")
|
|
206
|
+
}
|
|
207
|
+
# Unix: ps -p prints the full command line; the id is validated
|
|
208
|
+
# hex above, so no regex escaping is needed.
|
|
209
|
+
$cmd = (& ps -p $ProcessId -o command= 2>$null)
|
|
210
|
+
if (-not $cmd) { return $false }
|
|
211
|
+
return ($cmd -match "--brainstorm-server-id=$expected")
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function Get-ProcessStartToken {
|
|
215
|
+
param([int]$ProcessId)
|
|
216
|
+
$isWinPlatform = ($IsWindows -or [System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT)
|
|
217
|
+
if ($isWinPlatform) {
|
|
218
|
+
$process = Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId" -ErrorAction SilentlyContinue
|
|
219
|
+
if (-not $process) { return $null }
|
|
220
|
+
return [string]$process.CreationDate
|
|
221
|
+
}
|
|
222
|
+
$start = (& ps -p $ProcessId -o lstart= 2>$null)
|
|
223
|
+
if (-not $start) { return $null }
|
|
224
|
+
return (($start | Out-String).Trim())
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# Kill any existing server — only after proving the PID and its process start
|
|
228
|
+
# identity are ours. The final identity re-check narrows the PID-reuse window.
|
|
229
|
+
# Must run BEFORE the new server-instance-id below overwrites serverIdFile.
|
|
157
230
|
if (Test-Path -LiteralPath $pidFile) {
|
|
158
|
-
$
|
|
159
|
-
|
|
160
|
-
|
|
231
|
+
$oldPidText = (Get-Content -LiteralPath $pidFile -ErrorAction SilentlyContinue | Select-Object -First 1)
|
|
232
|
+
$oldPid = 0
|
|
233
|
+
$oldStartToken = $null
|
|
234
|
+
if ([int]::TryParse($oldPidText, [ref]$oldPid) -and (Test-BrainstormServer -ProcessId $oldPid)) {
|
|
235
|
+
$oldStartToken = Get-ProcessStartToken -ProcessId $oldPid
|
|
236
|
+
}
|
|
237
|
+
if ($oldStartToken -and (Test-BrainstormServer -ProcessId $oldPid) -and
|
|
238
|
+
((Get-ProcessStartToken -ProcessId $oldPid) -eq $oldStartToken)) {
|
|
239
|
+
Stop-Process -Id $oldPid -ErrorAction SilentlyContinue
|
|
240
|
+
} else {
|
|
241
|
+
Write-Warning "stale server.pid ignored: PID is not a running brainstorm server"
|
|
161
242
|
}
|
|
162
243
|
Remove-Item -LiteralPath $pidFile -Force -ErrorAction SilentlyContinue
|
|
163
244
|
}
|
|
164
245
|
|
|
246
|
+
# WriteAllText: UTF-8 without BOM on every PowerShell version (PS 5.1's
|
|
247
|
+
# Set-Content -Encoding utf8 emits a BOM, which breaks the bash-side
|
|
248
|
+
# read_expected_server_id regex when a session dir is shared across shells).
|
|
249
|
+
[System.IO.File]::WriteAllText($serverIdFile, $serverId)
|
|
250
|
+
Protect-PathForCurrentUser -Path $serverIdFile
|
|
251
|
+
|
|
165
252
|
$envValues = @{
|
|
166
253
|
BRAINSTORM_DIR = $sessionDir
|
|
167
254
|
BRAINSTORM_HOST = $bindHost
|
|
@@ -199,6 +286,9 @@ if ($foreground) {
|
|
|
199
286
|
|
|
200
287
|
Set-ProcessEnvironment -Values $envValues
|
|
201
288
|
$errFile = Join-Path $stateDir "server.err"
|
|
289
|
+
New-Item -ItemType File -Force -Path $logFile, $errFile | Out-Null
|
|
290
|
+
Protect-PathForCurrentUser -Path $logFile
|
|
291
|
+
Protect-PathForCurrentUser -Path $errFile
|
|
202
292
|
$process = Start-Process `
|
|
203
293
|
-FilePath "node" `
|
|
204
294
|
-ArgumentList @("server.cjs", "--brainstorm-server-id=$serverId") `
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
# Options:
|
|
9
9
|
# --project-dir <path> Store session files under <path>/.superpowers/brainstorm/
|
|
10
10
|
# instead of /tmp. Files persist after server stops.
|
|
11
|
-
# --host <bind-host>
|
|
12
|
-
# Use
|
|
13
|
-
# --url-host <host>
|
|
11
|
+
# --host <bind-host> Loopback host/interface to bind (default: 127.0.0.1).
|
|
12
|
+
# Use an SSH tunnel or TLS reverse proxy for remote access.
|
|
13
|
+
# --url-host <host> Loopback hostname shown in returned URL JSON.
|
|
14
14
|
# --idle-timeout-minutes <n> Shut down after n minutes idle (default 240 = 4h).
|
|
15
15
|
# --open Auto-open the browser on the first screen (use only
|
|
16
16
|
# after the user approves the visual companion).
|
|
@@ -71,6 +71,26 @@ if [[ -z "$URL_HOST" ]]; then
|
|
|
71
71
|
fi
|
|
72
72
|
fi
|
|
73
73
|
|
|
74
|
+
# Resolve a relative --project-dir against the caller's cwd up front. Later
|
|
75
|
+
# steps cd into SCRIPT_DIR, and a relative session path would then resolve
|
|
76
|
+
# against the wrong directory (or the server would inherit a relative
|
|
77
|
+
# BRAINSTORM_DIR it can't locate).
|
|
78
|
+
if [[ -n "$PROJECT_DIR" && "$PROJECT_DIR" != /* ]]; then
|
|
79
|
+
requested_project_dir="$PROJECT_DIR"
|
|
80
|
+
if [[ -d "$requested_project_dir" ]]; then
|
|
81
|
+
resolved_project_dir="$(cd "$requested_project_dir" 2>/dev/null && pwd -P || true)"
|
|
82
|
+
if [[ -n "$resolved_project_dir" ]]; then
|
|
83
|
+
PROJECT_DIR="$resolved_project_dir"
|
|
84
|
+
else
|
|
85
|
+
# cd failed (e.g. permission) — fall back to a lexical join against the
|
|
86
|
+
# caller's cwd instead of silently dropping the requested project dir.
|
|
87
|
+
PROJECT_DIR="$(pwd -P 2>/dev/null || pwd)/${requested_project_dir#./}"
|
|
88
|
+
fi
|
|
89
|
+
else
|
|
90
|
+
PROJECT_DIR="$(pwd -P 2>/dev/null || pwd)/$requested_project_dir"
|
|
91
|
+
fi
|
|
92
|
+
fi
|
|
93
|
+
|
|
74
94
|
if [[ -n "$IDLE_TIMEOUT_MINUTES" ]]; then
|
|
75
95
|
if ! [[ "$IDLE_TIMEOUT_MINUTES" =~ ^[0-9]+$ ]] || [[ "$IDLE_TIMEOUT_MINUTES" -lt 1 ]]; then
|
|
76
96
|
echo "{\"error\": \"--idle-timeout-minutes must be a positive integer\"}"
|
|
@@ -106,8 +126,8 @@ if [[ "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
|
|
|
106
126
|
fi
|
|
107
127
|
fi
|
|
108
128
|
|
|
109
|
-
# Session files (server.log
|
|
110
|
-
#
|
|
129
|
+
# Session files (server.log and server-info) embed the session key — keep
|
|
130
|
+
# everything this script and the server create owner-only.
|
|
111
131
|
umask 077
|
|
112
132
|
|
|
113
133
|
# Generate unique session directory
|
|
@@ -115,8 +135,8 @@ SESSION_ID="$$-$(date +%s)"
|
|
|
115
135
|
|
|
116
136
|
if [[ -n "$PROJECT_DIR" ]]; then
|
|
117
137
|
SESSION_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}"
|
|
118
|
-
#
|
|
119
|
-
# already-open browser tab
|
|
138
|
+
# Reuse the last bound port and session key so a restart keeps an
|
|
139
|
+
# already-open browser tab connected to the same URL with a valid cookie.
|
|
120
140
|
export BRAINSTORM_PORT_FILE="${PROJECT_DIR}/.superpowers/brainstorm/.last-port"
|
|
121
141
|
export BRAINSTORM_TOKEN_FILE="${PROJECT_DIR}/.superpowers/brainstorm/.last-token"
|
|
122
142
|
else
|
|
@@ -129,7 +149,83 @@ LOG_FILE="${STATE_DIR}/server.log"
|
|
|
129
149
|
SERVER_ID_FILE="${STATE_DIR}/server-instance-id"
|
|
130
150
|
|
|
131
151
|
# Create fresh session directory with content and state peers
|
|
132
|
-
mkdir -p "${SESSION_DIR}/content" "$STATE_DIR"
|
|
152
|
+
if ! mkdir -p "${SESSION_DIR}/content" "$STATE_DIR"; then
|
|
153
|
+
echo '{"error": "Failed to create brainstorm session directory"}'
|
|
154
|
+
exit 1
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# --- Process identity verification (shared with stop-server.sh) -----------
|
|
158
|
+
# A stale server.pid may point at an unrelated process after a reboot or PID
|
|
159
|
+
# wraparound. Before signalling a PID, prove it is actually a brainstorm
|
|
160
|
+
# server belonging to this session via the per-start server-instance-id.
|
|
161
|
+
|
|
162
|
+
read_expected_server_id() {
|
|
163
|
+
[[ -f "$SERVER_ID_FILE" ]] || return 1
|
|
164
|
+
local id
|
|
165
|
+
id="$(tr -d '\r\n' < "$SERVER_ID_FILE" 2>/dev/null || true)"
|
|
166
|
+
[[ "$id" =~ ^[A-Za-z0-9_-]{32,64}$ ]] || return 1
|
|
167
|
+
printf '%s\n' "$id"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
command_has_server_id() {
|
|
171
|
+
local pid="$1"
|
|
172
|
+
local expected="$2"
|
|
173
|
+
local expected_arg="--brainstorm-server-id=$expected"
|
|
174
|
+
if [[ -r "/proc/$pid/cmdline" ]]; then
|
|
175
|
+
local arg
|
|
176
|
+
while IFS= read -r -d '' arg || [[ -n "$arg" ]]; do
|
|
177
|
+
[[ "$arg" == "$expected_arg" ]] && return 0
|
|
178
|
+
done < "/proc/$pid/cmdline"
|
|
179
|
+
return 1
|
|
180
|
+
fi
|
|
181
|
+
local command_line
|
|
182
|
+
command_line="$(ps -ww -p "$pid" -o command= 2>/dev/null || ps -f -p "$pid" 2>/dev/null | sed '1d' || true)"
|
|
183
|
+
[[ -n "$command_line" ]] || return 1
|
|
184
|
+
case " $command_line " in
|
|
185
|
+
*" $expected_arg "*) return 0 ;;
|
|
186
|
+
*) return 1 ;;
|
|
187
|
+
esac
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
is_brainstorm_server() {
|
|
191
|
+
kill -0 "$1" 2>/dev/null || return 1
|
|
192
|
+
local expected_id
|
|
193
|
+
expected_id="$(read_expected_server_id)" || return 1
|
|
194
|
+
command_has_server_id "$1" "$expected_id" || return 1
|
|
195
|
+
return 0
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
process_start_token() {
|
|
199
|
+
local pid="$1"
|
|
200
|
+
if [[ -r "/proc/$pid/stat" ]]; then
|
|
201
|
+
local stat_line
|
|
202
|
+
stat_line="$(cat "/proc/$pid/stat" 2>/dev/null || true)"
|
|
203
|
+
# After the comm field, /proc/<pid>/stat field 20 is the process start
|
|
204
|
+
# time (the original field 22), measured in clock ticks since boot.
|
|
205
|
+
[[ -n "$stat_line" ]] || return 1
|
|
206
|
+
printf '%s\n' "${stat_line##*) }" | awk '{print $20}'
|
|
207
|
+
return 0
|
|
208
|
+
fi
|
|
209
|
+
ps -ww -p "$pid" -o lstart= 2>/dev/null | sed 's/^ *//' || true
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# Kill any existing server — only after proving the PID and its process start
|
|
213
|
+
# identity are ours. The final identity re-check narrows the PID-reuse window.
|
|
214
|
+
# This must run before the new server-instance-id overwrites SERVER_ID_FILE.
|
|
215
|
+
if [[ -f "$PID_FILE" ]]; then
|
|
216
|
+
old_pid=$(cat "$PID_FILE")
|
|
217
|
+
old_start_token=""
|
|
218
|
+
if [[ "$old_pid" =~ ^[0-9]+$ ]] && is_brainstorm_server "$old_pid"; then
|
|
219
|
+
old_start_token="$(process_start_token "$old_pid")"
|
|
220
|
+
fi
|
|
221
|
+
if [[ -n "$old_start_token" ]] && is_brainstorm_server "$old_pid" &&
|
|
222
|
+
[[ "$(process_start_token "$old_pid")" == "$old_start_token" ]]; then
|
|
223
|
+
kill "$old_pid" 2>/dev/null
|
|
224
|
+
else
|
|
225
|
+
echo '{"warn": "stale server.pid ignored: PID is not a running brainstorm server"}' >&2
|
|
226
|
+
fi
|
|
227
|
+
rm -f "$PID_FILE"
|
|
228
|
+
fi
|
|
133
229
|
|
|
134
230
|
SERVER_ID=""
|
|
135
231
|
if [[ -r /dev/urandom ]]; then
|
|
@@ -141,13 +237,6 @@ fi
|
|
|
141
237
|
printf '%s\n' "$SERVER_ID" > "$SERVER_ID_FILE"
|
|
142
238
|
chmod 600 "$SERVER_ID_FILE" 2>/dev/null || true
|
|
143
239
|
|
|
144
|
-
# Kill any existing server
|
|
145
|
-
if [[ -f "$PID_FILE" ]]; then
|
|
146
|
-
old_pid=$(cat "$PID_FILE")
|
|
147
|
-
kill "$old_pid" 2>/dev/null
|
|
148
|
-
rm -f "$PID_FILE"
|
|
149
|
-
fi
|
|
150
|
-
|
|
151
240
|
cd "$SCRIPT_DIR" || exit 1
|
|
152
241
|
|
|
153
242
|
# Resolve the harness PID (grandparent of this script).
|
|
@@ -34,7 +34,10 @@ function Test-BrainstormServer {
|
|
|
34
34
|
param([int]$ProcessId)
|
|
35
35
|
$expected = Read-ExpectedServerId
|
|
36
36
|
if (-not $expected) { return $false }
|
|
37
|
-
|
|
37
|
+
# $IsWindows is undefined on Windows PowerShell 5.1; fall back to the
|
|
38
|
+
# OSVersion check so the Windows branch is taken there too.
|
|
39
|
+
$isWinPlatform = ($IsWindows -or [System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT)
|
|
40
|
+
if ($isWinPlatform) {
|
|
38
41
|
$process = Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId" -ErrorAction SilentlyContinue
|
|
39
42
|
if (-not $process) { return $false }
|
|
40
43
|
return ($process.CommandLine -like "*--brainstorm-server-id=$expected*")
|
|
@@ -47,10 +50,38 @@ function Test-BrainstormServer {
|
|
|
47
50
|
return ($cmd -match "--brainstorm-server-id=$expected")
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
function Get-ProcessStartToken {
|
|
54
|
+
param([int]$ProcessId)
|
|
55
|
+
$isWinPlatform = ($IsWindows -or [System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT)
|
|
56
|
+
if ($isWinPlatform) {
|
|
57
|
+
$process = Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId" -ErrorAction SilentlyContinue
|
|
58
|
+
if (-not $process) { return $null }
|
|
59
|
+
return [string]$process.CreationDate
|
|
60
|
+
}
|
|
61
|
+
$start = (& ps -p $ProcessId -o lstart= 2>$null)
|
|
62
|
+
if (-not $start) { return $null }
|
|
63
|
+
return (($start | Out-String).Trim())
|
|
64
|
+
}
|
|
65
|
+
|
|
50
66
|
if (Test-Path -LiteralPath $pidFile) {
|
|
51
67
|
$pidText = (Get-Content -LiteralPath $pidFile -ErrorAction SilentlyContinue | Select-Object -First 1)
|
|
52
68
|
$serverPid = 0
|
|
53
|
-
|
|
69
|
+
$startToken = $null
|
|
70
|
+
if ([int]::TryParse($pidText, [ref]$serverPid) -and (Test-BrainstormServer -ProcessId $serverPid)) {
|
|
71
|
+
$startToken = Get-ProcessStartToken -ProcessId $serverPid
|
|
72
|
+
}
|
|
73
|
+
if (-not $startToken -or -not (Test-BrainstormServer -ProcessId $serverPid) -or
|
|
74
|
+
((Get-ProcessStartToken -ProcessId $serverPid) -ne $startToken)) {
|
|
75
|
+
Remove-Item -LiteralPath $pidFile, $serverIdFile -Force -ErrorAction SilentlyContinue
|
|
76
|
+
Mark-Stopped "stale_pid"
|
|
77
|
+
[pscustomobject]@{ status = "stale_pid" } | ConvertTo-Json -Compress
|
|
78
|
+
exit 0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Re-check identity immediately before signalling to narrow the PID-reuse
|
|
82
|
+
# window as much as shell-level process management permits.
|
|
83
|
+
if (-not (Test-BrainstormServer -ProcessId $serverPid) -or
|
|
84
|
+
((Get-ProcessStartToken -ProcessId $serverPid) -ne $startToken)) {
|
|
54
85
|
Remove-Item -LiteralPath $pidFile, $serverIdFile -Force -ErrorAction SilentlyContinue
|
|
55
86
|
Mark-Stopped "stale_pid"
|
|
56
87
|
[pscustomobject]@{ status = "stale_pid" } | ConvertTo-Json -Compress
|
|
@@ -70,12 +70,38 @@ is_brainstorm_server() {
|
|
|
70
70
|
return 0
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
process_start_token() {
|
|
74
|
+
local pid="$1"
|
|
75
|
+
if [[ -r "/proc/$pid/stat" ]]; then
|
|
76
|
+
local stat_line
|
|
77
|
+
stat_line="$(cat "/proc/$pid/stat" 2>/dev/null || true)"
|
|
78
|
+
[[ -n "$stat_line" ]] || return 1
|
|
79
|
+
printf '%s\n' "${stat_line##*) }" | awk '{print $20}'
|
|
80
|
+
return 0
|
|
81
|
+
fi
|
|
82
|
+
ps -ww -p "$pid" -o lstart= 2>/dev/null | sed 's/^ *//' || true
|
|
83
|
+
}
|
|
84
|
+
|
|
73
85
|
if [[ -f "$PID_FILE" ]]; then
|
|
74
86
|
pid=$(cat "$PID_FILE")
|
|
75
87
|
|
|
76
88
|
# Refuse to signal a PID we can't prove is our server. A stale pid file may
|
|
77
89
|
# point at an unrelated process after a reboot/PID wraparound.
|
|
78
|
-
|
|
90
|
+
start_token=""
|
|
91
|
+
if is_brainstorm_server "$pid"; then
|
|
92
|
+
start_token="$(process_start_token "$pid")"
|
|
93
|
+
fi
|
|
94
|
+
if [[ -z "$start_token" ]] || ! is_brainstorm_server "$pid" ||
|
|
95
|
+
[[ "$(process_start_token "$pid")" != "$start_token" ]]; then
|
|
96
|
+
rm -f "$PID_FILE" "$SERVER_ID_FILE"
|
|
97
|
+
mark_stopped "stale_pid"
|
|
98
|
+
echo '{"status": "stale_pid"}'
|
|
99
|
+
exit 0
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# Re-check identity immediately before signalling to narrow the PID-reuse
|
|
103
|
+
# window as much as shell-level process management permits.
|
|
104
|
+
if ! is_brainstorm_server "$pid" || [[ "$(process_start_token "$pid")" != "$start_token" ]]; then
|
|
79
105
|
rm -f "$PID_FILE" "$SERVER_ID_FILE"
|
|
80
106
|
mark_stopped "stale_pid"
|
|
81
107
|
echo '{"status": "stale_pid"}'
|
|
@@ -109,9 +135,15 @@ if [[ -f "$PID_FILE" ]]; then
|
|
|
109
135
|
rm -f "$PID_FILE" "$SERVER_ID_FILE" "${STATE_DIR}/server.log"
|
|
110
136
|
mark_stopped "stop-server.sh"
|
|
111
137
|
|
|
112
|
-
# Only delete ephemeral /tmp directories
|
|
113
|
-
|
|
114
|
-
|
|
138
|
+
# Only delete ephemeral /tmp directories. Resolve both paths canonically
|
|
139
|
+
# (cd + pwd -P, like stop-server.ps1) so a session dir such as
|
|
140
|
+
# "/tmp/../home/user/project" can't trick the prefix check into deleting
|
|
141
|
+
# a directory outside the temp root. (Plain variables: this code runs at
|
|
142
|
+
# top level, where `local` is a syntax error on bash 3.2 / macOS.)
|
|
143
|
+
tmp_root="$(cd /tmp 2>/dev/null && pwd -P || true)"
|
|
144
|
+
resolved="$(cd "$SESSION_DIR" 2>/dev/null && pwd -P || true)"
|
|
145
|
+
if [[ -n "$tmp_root" && -n "$resolved" && "$resolved" == "$tmp_root/"* ]]; then
|
|
146
|
+
rm -rf "$resolved"
|
|
115
147
|
fi
|
|
116
148
|
|
|
117
149
|
echo '{"status": "stopped"}'
|
|
@@ -28,7 +28,7 @@ A question *about* a UI topic is not automatically a visual question. "What kind
|
|
|
28
28
|
|
|
29
29
|
The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content to `screen_dir`, the user sees it in their browser and can click to select options. Selections are recorded to `state_dir/events` that you read on your next turn.
|
|
30
30
|
|
|
31
|
-
**Content fragments vs full documents:** If your HTML file starts with `<!DOCTYPE` or `<html`, the server serves it as-is (just injects the helper script). Otherwise, the server automatically wraps your content in the frame template — adding the header, CSS theme, connection status, and all interactive infrastructure. **Write content fragments by default.** Only write full documents when you need complete control over the page.
|
|
31
|
+
**Content fragments vs full documents:** If your HTML file starts with `<!DOCTYPE` or `<html`, the server serves it as-is (just injects the helper script). Otherwise, the server automatically wraps your content in the frame template — adding the header, CSS theme, connection status, and all interactive infrastructure. **Write content fragments by default.** Only write full documents when you need complete control over the page. Screen scripts are blocked by the server's nonce CSP; use `data-choice` elements and the injected helper instead of inline scripts or event handlers.
|
|
32
32
|
|
|
33
33
|
## Starting a Session
|
|
34
34
|
|
|
@@ -49,12 +49,15 @@ scripts/start-server.ps1 --project-dir C:\path\to\project --open
|
|
|
49
49
|
Save `screen_dir` and `state_dir` from the response. With `--open`, the browser opens itself when you push the first screen — you don't need to ask the user to open it, but still share the URL as a fallback (headless/remote setups won't auto-open).
|
|
50
50
|
|
|
51
51
|
**The URL contains a session key (`?key=…`).** The server rejects any request
|
|
52
|
-
without it, so always give the user the **complete** URL from the `url` field
|
|
53
|
-
never strip the query string
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
without it, so always give the user the **complete** URL from the `url` field for
|
|
53
|
+
the first load — never strip the query string. The key gates HTTP access, then
|
|
54
|
+
moves into an `HttpOnly`/`SameSite=Strict` cookie; the browser's same-origin
|
|
55
|
+
WebSocket automatically sends that cookie. The key is never stored in
|
|
56
|
+
page-readable storage. With `--project-dir`, the key is persisted to
|
|
57
|
+
`.superpowers/brainstorm/.last-token` and reused across restarts, so an
|
|
58
|
+
already-open tab stays connected; delete that file (with the server stopped)
|
|
59
|
+
to force a fresh key. Without it, a new server invocation rotates the key and
|
|
60
|
+
a restarted server requires the new URL.
|
|
58
61
|
|
|
59
62
|
**Finding connection info:** The server writes its startup JSON to `$STATE_DIR/server-info`. If you launched the server in the background and didn't capture stdout, read that file to get the URL and port. When using `--project-dir`, check `<project>/.superpowers/brainstorm/` for the session directory.
|
|
60
63
|
|
|
@@ -96,21 +99,15 @@ scripts/start-server.sh --project-dir /path/to/project --open --foreground
|
|
|
96
99
|
|
|
97
100
|
**Other environments:** The server must keep running in the background across conversation turns. If your environment reaps detached processes, use `--foreground` and launch the command with your platform's background execution mechanism.
|
|
98
101
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
--project-dir /path/to/project \
|
|
104
|
-
--host 0.0.0.0 \
|
|
105
|
-
--url-host localhost
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Use `--url-host` to control what hostname is printed in the returned URL JSON.
|
|
102
|
+
The server only permits loopback HTTP binds. For a remote browser, keep the
|
|
103
|
+
server on `127.0.0.1` and use an authenticated SSH tunnel; do not expose the
|
|
104
|
+
companion's plain HTTP port directly to a network interface.
|
|
105
|
+
The `--url-host` value must also be a loopback hostname or address.
|
|
109
106
|
|
|
110
107
|
## The Loop
|
|
111
108
|
|
|
112
109
|
1. **Check server is alive**, then **write HTML** to a new file in `screen_dir`:
|
|
113
|
-
- **Required: confirm the server is alive before referring to the URL or pushing a screen.** Check that `$STATE_DIR/server-info` exists and `$STATE_DIR/server-stopped` does not. If it has shut down, restart it with `start-server.sh` using the **same `--project-dir`** — it reuses the same port
|
|
110
|
+
- **Required: confirm the server is alive before referring to the URL or pushing a screen.** Check that `$STATE_DIR/server-info` exists and `$STATE_DIR/server-stopped` does not. If it has shut down, restart it with `start-server.sh` using the **same `--project-dir`** — it reuses the same port and session key (from `.last-token`), so an already-open tab keeps working; without `--project-dir` the key rotates, so share the new URL from `server-info` with the user. The server auto-exits after 4 hours idle (configurable with `--idle-timeout-minutes`).
|
|
114
111
|
- Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html`
|
|
115
112
|
- **Never reuse filenames** — each screen gets a fresh file
|
|
116
113
|
- Use your file-creation tool — **never use cat/heredoc** (dumps noise into terminal)
|
|
@@ -152,14 +149,14 @@ Write just the content that goes inside the page. The server wraps it in the fra
|
|
|
152
149
|
<p class="subtitle">Consider readability and visual hierarchy</p>
|
|
153
150
|
|
|
154
151
|
<div class="options">
|
|
155
|
-
<div class="option" data-choice="a"
|
|
152
|
+
<div class="option" data-choice="a">
|
|
156
153
|
<div class="letter">A</div>
|
|
157
154
|
<div class="content">
|
|
158
155
|
<h3>Single Column</h3>
|
|
159
156
|
<p>Clean, focused reading experience</p>
|
|
160
157
|
</div>
|
|
161
158
|
</div>
|
|
162
|
-
<div class="option" data-choice="b"
|
|
159
|
+
<div class="option" data-choice="b">
|
|
163
160
|
<div class="letter">B</div>
|
|
164
161
|
<div class="content">
|
|
165
162
|
<h3>Two Column</h3>
|
|
@@ -179,7 +176,7 @@ The frame template provides these CSS classes for your content:
|
|
|
179
176
|
|
|
180
177
|
```html
|
|
181
178
|
<div class="options">
|
|
182
|
-
<div class="option" data-choice="a"
|
|
179
|
+
<div class="option" data-choice="a">
|
|
183
180
|
<div class="letter">A</div>
|
|
184
181
|
<div class="content">
|
|
185
182
|
<h3>Title</h3>
|
|
@@ -201,7 +198,7 @@ The frame template provides these CSS classes for your content:
|
|
|
201
198
|
|
|
202
199
|
```html
|
|
203
200
|
<div class="cards">
|
|
204
|
-
<div class="card" data-choice="design1"
|
|
201
|
+
<div class="card" data-choice="design1">
|
|
205
202
|
<div class="card-image"><!-- mockup content --></div>
|
|
206
203
|
<div class="card-body">
|
|
207
204
|
<h3>Name</h3>
|