superpowers-mcp 6.2.1 → 6.2.3
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 +25 -2
- package/README.ko.md +25 -2
- package/README.md +20 -2
- package/README.zh-TW.md +20 -3
- package/out/server.js +19 -17
- package/package.json +6 -3
- package/skills/brainstorming/scripts/helper.js +8 -15
- package/skills/brainstorming/scripts/server.cjs +535 -142
- package/skills/brainstorming/scripts/start-server.ps1 +92 -11
- package/skills/brainstorming/scripts/start-server.sh +104 -16
- package/skills/brainstorming/scripts/stop-server.ps1 +33 -2
- package/skills/brainstorming/scripts/stop-server.sh +36 -4
- package/skills/brainstorming/visual-companion.md +16 -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) {
|
|
@@ -131,7 +157,6 @@ if ($projectDir -ne "") {
|
|
|
131
157
|
$brainstormRoot = Join-Path $projectDir ".superpowers/brainstorm"
|
|
132
158
|
$sessionDir = Join-Path $brainstormRoot $sessionId
|
|
133
159
|
$env:BRAINSTORM_PORT_FILE = Join-Path $brainstormRoot ".last-port"
|
|
134
|
-
$env:BRAINSTORM_TOKEN_FILE = Join-Path $brainstormRoot ".last-token"
|
|
135
160
|
} else {
|
|
136
161
|
$sessionDir = Join-Path ([System.IO.Path]::GetTempPath()) "brainstorm-$sessionId"
|
|
137
162
|
}
|
|
@@ -151,24 +176,77 @@ Protect-PathForCurrentUser -Path $contentDir
|
|
|
151
176
|
Protect-PathForCurrentUser -Path $stateDir
|
|
152
177
|
|
|
153
178
|
$serverId = New-ServerId
|
|
154
|
-
Set-Content -Path $serverIdFile -Value $serverId -NoNewline -Encoding utf8
|
|
155
|
-
Protect-PathForCurrentUser -Path $serverIdFile
|
|
156
179
|
|
|
180
|
+
function Read-ExpectedServerId {
|
|
181
|
+
if (-not (Test-Path -LiteralPath $serverIdFile)) { return $null }
|
|
182
|
+
$id = (Get-Content -LiteralPath $serverIdFile -Raw -ErrorAction SilentlyContinue).Trim()
|
|
183
|
+
if ($id -match '^[A-Za-z0-9_-]{32,64}$') { return $id }
|
|
184
|
+
return $null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function Test-BrainstormServer {
|
|
188
|
+
param([int]$ProcessId)
|
|
189
|
+
$expected = Read-ExpectedServerId
|
|
190
|
+
if (-not $expected) { return $false }
|
|
191
|
+
# $IsWindows is undefined on Windows PowerShell 5.1; fall back to the
|
|
192
|
+
# OSVersion check so the Windows branch is taken there too.
|
|
193
|
+
$isWinPlatform = ($IsWindows -or [System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT)
|
|
194
|
+
if ($isWinPlatform) {
|
|
195
|
+
$process = Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId" -ErrorAction SilentlyContinue
|
|
196
|
+
if (-not $process) { return $false }
|
|
197
|
+
return ($process.CommandLine -like "*--brainstorm-server-id=$expected*")
|
|
198
|
+
}
|
|
199
|
+
# Unix: ps -p prints the full command line; the id is validated
|
|
200
|
+
# hex above, so no regex escaping is needed.
|
|
201
|
+
$cmd = (& ps -p $ProcessId -o command= 2>$null)
|
|
202
|
+
if (-not $cmd) { return $false }
|
|
203
|
+
return ($cmd -match "--brainstorm-server-id=$expected")
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function Get-ProcessStartToken {
|
|
207
|
+
param([int]$ProcessId)
|
|
208
|
+
$isWinPlatform = ($IsWindows -or [System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT)
|
|
209
|
+
if ($isWinPlatform) {
|
|
210
|
+
$process = Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId" -ErrorAction SilentlyContinue
|
|
211
|
+
if (-not $process) { return $null }
|
|
212
|
+
return [string]$process.CreationDate
|
|
213
|
+
}
|
|
214
|
+
$start = (& ps -p $ProcessId -o lstart= 2>$null)
|
|
215
|
+
if (-not $start) { return $null }
|
|
216
|
+
return (($start | Out-String).Trim())
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# Kill any existing server — only after proving the PID and its process start
|
|
220
|
+
# identity are ours. The final identity re-check narrows the PID-reuse window.
|
|
221
|
+
# Must run BEFORE the new server-instance-id below overwrites serverIdFile.
|
|
157
222
|
if (Test-Path -LiteralPath $pidFile) {
|
|
158
|
-
$
|
|
159
|
-
|
|
160
|
-
|
|
223
|
+
$oldPidText = (Get-Content -LiteralPath $pidFile -ErrorAction SilentlyContinue | Select-Object -First 1)
|
|
224
|
+
$oldPid = 0
|
|
225
|
+
$oldStartToken = $null
|
|
226
|
+
if ([int]::TryParse($oldPidText, [ref]$oldPid) -and (Test-BrainstormServer -ProcessId $oldPid)) {
|
|
227
|
+
$oldStartToken = Get-ProcessStartToken -ProcessId $oldPid
|
|
228
|
+
}
|
|
229
|
+
if ($oldStartToken -and (Test-BrainstormServer -ProcessId $oldPid) -and
|
|
230
|
+
((Get-ProcessStartToken -ProcessId $oldPid) -eq $oldStartToken)) {
|
|
231
|
+
Stop-Process -Id $oldPid -ErrorAction SilentlyContinue
|
|
232
|
+
} else {
|
|
233
|
+
Write-Warning "stale server.pid ignored: PID is not a running brainstorm server"
|
|
161
234
|
}
|
|
162
235
|
Remove-Item -LiteralPath $pidFile -Force -ErrorAction SilentlyContinue
|
|
163
236
|
}
|
|
164
237
|
|
|
238
|
+
# WriteAllText: UTF-8 without BOM on every PowerShell version (PS 5.1's
|
|
239
|
+
# Set-Content -Encoding utf8 emits a BOM, which breaks the bash-side
|
|
240
|
+
# read_expected_server_id regex when a session dir is shared across shells).
|
|
241
|
+
[System.IO.File]::WriteAllText($serverIdFile, $serverId)
|
|
242
|
+
Protect-PathForCurrentUser -Path $serverIdFile
|
|
243
|
+
|
|
165
244
|
$envValues = @{
|
|
166
245
|
BRAINSTORM_DIR = $sessionDir
|
|
167
246
|
BRAINSTORM_HOST = $bindHost
|
|
168
247
|
BRAINSTORM_URL_HOST = $urlHost
|
|
169
248
|
BRAINSTORM_OWNER_PID = ""
|
|
170
249
|
BRAINSTORM_PORT_FILE = $env:BRAINSTORM_PORT_FILE
|
|
171
|
-
BRAINSTORM_TOKEN_FILE = $env:BRAINSTORM_TOKEN_FILE
|
|
172
250
|
BRAINSTORM_IDLE_TIMEOUT_MS = $env:BRAINSTORM_IDLE_TIMEOUT_MS
|
|
173
251
|
BRAINSTORM_OPEN = $env:BRAINSTORM_OPEN
|
|
174
252
|
}
|
|
@@ -199,6 +277,9 @@ if ($foreground) {
|
|
|
199
277
|
|
|
200
278
|
Set-ProcessEnvironment -Values $envValues
|
|
201
279
|
$errFile = Join-Path $stateDir "server.err"
|
|
280
|
+
New-Item -ItemType File -Force -Path $logFile, $errFile | Out-Null
|
|
281
|
+
Protect-PathForCurrentUser -Path $logFile
|
|
282
|
+
Protect-PathForCurrentUser -Path $errFile
|
|
202
283
|
$process = Start-Process `
|
|
203
284
|
-FilePath "node" `
|
|
204
285
|
-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,10 +135,9 @@ 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
|
-
#
|
|
138
|
+
# Reusing a port is safe because the server rotates its authentication key
|
|
139
|
+
# for every logical session.
|
|
120
140
|
export BRAINSTORM_PORT_FILE="${PROJECT_DIR}/.superpowers/brainstorm/.last-port"
|
|
121
|
-
export BRAINSTORM_TOKEN_FILE="${PROJECT_DIR}/.superpowers/brainstorm/.last-token"
|
|
122
141
|
else
|
|
123
142
|
SESSION_DIR="/tmp/brainstorm-${SESSION_ID}"
|
|
124
143
|
fi
|
|
@@ -129,7 +148,83 @@ LOG_FILE="${STATE_DIR}/server.log"
|
|
|
129
148
|
SERVER_ID_FILE="${STATE_DIR}/server-instance-id"
|
|
130
149
|
|
|
131
150
|
# Create fresh session directory with content and state peers
|
|
132
|
-
mkdir -p "${SESSION_DIR}/content" "$STATE_DIR"
|
|
151
|
+
if ! mkdir -p "${SESSION_DIR}/content" "$STATE_DIR"; then
|
|
152
|
+
echo '{"error": "Failed to create brainstorm session directory"}'
|
|
153
|
+
exit 1
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
# --- Process identity verification (shared with stop-server.sh) -----------
|
|
157
|
+
# A stale server.pid may point at an unrelated process after a reboot or PID
|
|
158
|
+
# wraparound. Before signalling a PID, prove it is actually a brainstorm
|
|
159
|
+
# server belonging to this session via the per-start server-instance-id.
|
|
160
|
+
|
|
161
|
+
read_expected_server_id() {
|
|
162
|
+
[[ -f "$SERVER_ID_FILE" ]] || return 1
|
|
163
|
+
local id
|
|
164
|
+
id="$(tr -d '\r\n' < "$SERVER_ID_FILE" 2>/dev/null || true)"
|
|
165
|
+
[[ "$id" =~ ^[A-Za-z0-9_-]{32,64}$ ]] || return 1
|
|
166
|
+
printf '%s\n' "$id"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
command_has_server_id() {
|
|
170
|
+
local pid="$1"
|
|
171
|
+
local expected="$2"
|
|
172
|
+
local expected_arg="--brainstorm-server-id=$expected"
|
|
173
|
+
if [[ -r "/proc/$pid/cmdline" ]]; then
|
|
174
|
+
local arg
|
|
175
|
+
while IFS= read -r -d '' arg || [[ -n "$arg" ]]; do
|
|
176
|
+
[[ "$arg" == "$expected_arg" ]] && return 0
|
|
177
|
+
done < "/proc/$pid/cmdline"
|
|
178
|
+
return 1
|
|
179
|
+
fi
|
|
180
|
+
local command_line
|
|
181
|
+
command_line="$(ps -ww -p "$pid" -o command= 2>/dev/null || ps -f -p "$pid" 2>/dev/null | sed '1d' || true)"
|
|
182
|
+
[[ -n "$command_line" ]] || return 1
|
|
183
|
+
case " $command_line " in
|
|
184
|
+
*" $expected_arg "*) return 0 ;;
|
|
185
|
+
*) return 1 ;;
|
|
186
|
+
esac
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
is_brainstorm_server() {
|
|
190
|
+
kill -0 "$1" 2>/dev/null || return 1
|
|
191
|
+
local expected_id
|
|
192
|
+
expected_id="$(read_expected_server_id)" || return 1
|
|
193
|
+
command_has_server_id "$1" "$expected_id" || return 1
|
|
194
|
+
return 0
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
process_start_token() {
|
|
198
|
+
local pid="$1"
|
|
199
|
+
if [[ -r "/proc/$pid/stat" ]]; then
|
|
200
|
+
local stat_line
|
|
201
|
+
stat_line="$(cat "/proc/$pid/stat" 2>/dev/null || true)"
|
|
202
|
+
# After the comm field, /proc/<pid>/stat field 20 is the process start
|
|
203
|
+
# time (the original field 22), measured in clock ticks since boot.
|
|
204
|
+
[[ -n "$stat_line" ]] || return 1
|
|
205
|
+
printf '%s\n' "${stat_line##*) }" | awk '{print $20}'
|
|
206
|
+
return 0
|
|
207
|
+
fi
|
|
208
|
+
ps -ww -p "$pid" -o lstart= 2>/dev/null | sed 's/^ *//' || true
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# Kill any existing server — only after proving the PID and its process start
|
|
212
|
+
# identity are ours. The final identity re-check narrows the PID-reuse window.
|
|
213
|
+
# This must run before the new server-instance-id overwrites SERVER_ID_FILE.
|
|
214
|
+
if [[ -f "$PID_FILE" ]]; then
|
|
215
|
+
old_pid=$(cat "$PID_FILE")
|
|
216
|
+
old_start_token=""
|
|
217
|
+
if [[ "$old_pid" =~ ^[0-9]+$ ]] && is_brainstorm_server "$old_pid"; then
|
|
218
|
+
old_start_token="$(process_start_token "$old_pid")"
|
|
219
|
+
fi
|
|
220
|
+
if [[ -n "$old_start_token" ]] && is_brainstorm_server "$old_pid" &&
|
|
221
|
+
[[ "$(process_start_token "$old_pid")" == "$old_start_token" ]]; then
|
|
222
|
+
kill "$old_pid" 2>/dev/null
|
|
223
|
+
else
|
|
224
|
+
echo '{"warn": "stale server.pid ignored: PID is not a running brainstorm server"}' >&2
|
|
225
|
+
fi
|
|
226
|
+
rm -f "$PID_FILE"
|
|
227
|
+
fi
|
|
133
228
|
|
|
134
229
|
SERVER_ID=""
|
|
135
230
|
if [[ -r /dev/urandom ]]; then
|
|
@@ -141,13 +236,6 @@ fi
|
|
|
141
236
|
printf '%s\n' "$SERVER_ID" > "$SERVER_ID_FILE"
|
|
142
237
|
chmod 600 "$SERVER_ID_FILE" 2>/dev/null || true
|
|
143
238
|
|
|
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
239
|
cd "$SCRIPT_DIR" || exit 1
|
|
152
240
|
|
|
153
241
|
# 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,12 @@ 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. A new server invocation rotates the key, so a restarted
|
|
57
|
+
server requires the new URL.
|
|
58
58
|
|
|
59
59
|
**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
60
|
|
|
@@ -96,21 +96,15 @@ scripts/start-server.sh --project-dir /path/to/project --open --foreground
|
|
|
96
96
|
|
|
97
97
|
**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
98
|
|
|
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.
|
|
99
|
+
The server only permits loopback HTTP binds. For a remote browser, keep the
|
|
100
|
+
server on `127.0.0.1` and use an authenticated SSH tunnel; do not expose the
|
|
101
|
+
companion's plain HTTP port directly to a network interface.
|
|
102
|
+
The `--url-host` value must also be a loopback hostname or address.
|
|
109
103
|
|
|
110
104
|
## The Loop
|
|
111
105
|
|
|
112
106
|
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
|
|
107
|
+
- **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 may reuse the same port, but the authentication 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
108
|
- Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html`
|
|
115
109
|
- **Never reuse filenames** — each screen gets a fresh file
|
|
116
110
|
- Use your file-creation tool — **never use cat/heredoc** (dumps noise into terminal)
|
|
@@ -152,14 +146,14 @@ Write just the content that goes inside the page. The server wraps it in the fra
|
|
|
152
146
|
<p class="subtitle">Consider readability and visual hierarchy</p>
|
|
153
147
|
|
|
154
148
|
<div class="options">
|
|
155
|
-
<div class="option" data-choice="a"
|
|
149
|
+
<div class="option" data-choice="a">
|
|
156
150
|
<div class="letter">A</div>
|
|
157
151
|
<div class="content">
|
|
158
152
|
<h3>Single Column</h3>
|
|
159
153
|
<p>Clean, focused reading experience</p>
|
|
160
154
|
</div>
|
|
161
155
|
</div>
|
|
162
|
-
<div class="option" data-choice="b"
|
|
156
|
+
<div class="option" data-choice="b">
|
|
163
157
|
<div class="letter">B</div>
|
|
164
158
|
<div class="content">
|
|
165
159
|
<h3>Two Column</h3>
|
|
@@ -179,7 +173,7 @@ The frame template provides these CSS classes for your content:
|
|
|
179
173
|
|
|
180
174
|
```html
|
|
181
175
|
<div class="options">
|
|
182
|
-
<div class="option" data-choice="a"
|
|
176
|
+
<div class="option" data-choice="a">
|
|
183
177
|
<div class="letter">A</div>
|
|
184
178
|
<div class="content">
|
|
185
179
|
<h3>Title</h3>
|
|
@@ -201,7 +195,7 @@ The frame template provides these CSS classes for your content:
|
|
|
201
195
|
|
|
202
196
|
```html
|
|
203
197
|
<div class="cards">
|
|
204
|
-
<div class="card" data-choice="design1"
|
|
198
|
+
<div class="card" data-choice="design1">
|
|
205
199
|
<div class="card-image"><!-- mockup content --></div>
|
|
206
200
|
<div class="card-body">
|
|
207
201
|
<h3>Name</h3>
|