@smartsoft001-mobilems/claude-plugins 2.83.0 → 2.85.0

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.md ADDED
@@ -0,0 +1,97 @@
1
+ # @smartsoft001-mobilems/claude-plugins
2
+
3
+ Claude Code plugins for MobileMS / MuzaCMS projects. Ships three variants:
4
+
5
+ - `flow` — modern Angular 20+ projects (primary plugin)
6
+ - `flow-legacy` — Angular 14 projects
7
+ - `flow-external` — standalone Angular 6–11 projects
8
+
9
+ ## MuzaCMS API access (required for `mobilems-docs-reader` skill)
10
+
11
+ The `mobilems-docs-reader` skill answers questions about the MuzaCMS / MobileMS indexer API by fetching its Swagger spec from a live MuzaCMS instance. The MuzaCMS docs page is behind a Symfony form-login, so the skill needs credentials. The refresh script lives next to the skill (`plugins/<variant>/skills/mobilems-docs-reader/refresh-mobilems-api-spec.sh`) and caches fetches per Claude Code session (with an 8-hour TTL) — see [Caching](#caching) below for details.
12
+
13
+ Set these two environment variables in every environment where the skill runs:
14
+
15
+ | Variable | Value |
16
+ | --- | --- |
17
+ | `MOBILEMS_LOGIN` | MuzaCMS username |
18
+ | `MOBILEMS_PASSWORD` | MuzaCMS password |
19
+
20
+ These are not vault-grade secrets, but they are still credentials — do not commit them to source control.
21
+
22
+ ### Local setup
23
+
24
+ The simplest path: put the two variables in a `.env` file at your project root. The refresh script **auto-loads `.env` from the current working directory** every time it runs — no `set -a; source .env; set +a` ritual, no `direnv`, no `dotenv-cli`. Example `.env` entry:
25
+
26
+ ```
27
+ MOBILEMS_LOGIN=your.username
28
+ MOBILEMS_PASSWORD=your-password
29
+ ```
30
+
31
+ Only `.env` is loaded (not `.env.local`, `.env.dev`, or any other variant) — keep your MuzaCMS credentials in the single canonical file. Already-set shell environment variables take precedence over `.env` values, so you can override individual keys with an inline export when needed:
32
+
33
+ ```
34
+ MOBILEMS_LOGIN=other-user bash ./node_modules/@smartsoft001-mobilems/claude-plugins/plugins/flow/scripts/refresh-mobilems-api-spec.sh
35
+ ```
36
+
37
+ The script's first action is to validate that both `MOBILEMS_LOGIN` and `MOBILEMS_PASSWORD` are present (in your shell or loaded from `.env`). If either is missing it exits with a clear instruction on what to set, before attempting any HTTP call.
38
+
39
+ ### Tool dependencies
40
+
41
+ The shell script uses `curl` and `jq`. Both are usually preinstalled on macOS (with Homebrew) and most Linux distros, but the script does a pre-flight check and tells you how to install whichever is missing (`brew install jq` / `sudo apt install jq`).
42
+
43
+ ### Manual refresh
44
+
45
+ Run the refresh script directly:
46
+
47
+ ```
48
+ bash ./node_modules/@smartsoft001-mobilems/claude-plugins/plugins/flow/scripts/refresh-mobilems-api-spec.sh
49
+ ```
50
+
51
+ You should see a `✓ Refreshed N bytes — <Title> <Version>` line and a new `mobilems-api.json` inside `node_modules/@smartsoft001-mobilems/claude-plugins/shared-assets/`.
52
+
53
+ The same script ships under `plugins/flow-external/...` and `plugins/flow-legacy/...` for the other two plugin variants — pick the path that matches the plugin your project uses.
54
+
55
+ ### Claude Code permission
56
+
57
+ The `mobilems-docs-reader` skill invokes the refresh script via the `Bash` tool. Claude Code prompts before running unknown Bash commands; depending on your permission mode, the call may be auto-denied without a prompt. To pre-approve, add the entry below to your project's `.claude/settings.json` (committed) or `.claude/settings.local.json` (per-dev, gitignored), under `permissions.allow`:
58
+
59
+ ```json
60
+ {
61
+ "permissions": {
62
+ "allow": [
63
+ "Bash(bash ./node_modules/@smartsoft001-mobilems/claude-plugins/plugins/flow/scripts/refresh-mobilems-api-spec.sh:*)"
64
+ ]
65
+ }
66
+ }
67
+ ```
68
+
69
+ If the call is denied, the skill stops and tells you which rule to add — it does not fabricate an answer.
70
+
71
+ ### CI setup
72
+
73
+ Add `MOBILEMS_LOGIN` and `MOBILEMS_PASSWORD` to your CI provider's repository secrets, then expose them in the `env:` block of any job that may invoke a Claude skill. No scheduled refresh job is needed — each CI job runs without a `CLAUDE_CODE_SESSION_ID`, so the script treats it as a manual/terminal invocation and always re-fetches a fresh spec.
74
+
75
+ ### Caching
76
+
77
+ To avoid re-downloading the ~400 KB spec on every follow-up question inside a single Claude Code session, the script caches the fetched spec and skips the network call when the cache is still valid.
78
+
79
+ A small sidecar `mobilems-api.meta.json` is written next to the spec containing two fields:
80
+
81
+ ```json
82
+ {
83
+ "sessionId": "<CLAUDE_CODE_SESSION_ID at time of fetch>",
84
+ "fetchedAt": "<ISO 8601 timestamp>"
85
+ }
86
+ ```
87
+
88
+ The script fetches afresh when **any** of these holds:
89
+
90
+ - `CLAUDE_CODE_SESSION_ID` is set but differs from the cached `sessionId` → new Claude Code session.
91
+ - The cached `fetchedAt` is more than **8 hours** old.
92
+ - The spec file or meta file is missing.
93
+ - `CLAUDE_CODE_SESSION_ID` is not set → the call is treated as manual/terminal and always re-fetches.
94
+
95
+ Manual/terminal fetches **preserve the existing `sessionId`** in the meta file (only `fetchedAt` is updated). That way, if you run the script from your shell and later resume the same Claude Code session, the in-session cache is still valid.
96
+
97
+ The skill itself doesn't read the meta — it simply runs the script and reads `mobilems-api.json`. The caching is entirely script-internal.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartsoft001-mobilems/claude-plugins",
3
- "version": "2.83.0",
3
+ "version": "2.85.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "flow",
3
3
  "description": "Development flow skills for Linear-driven development workflow",
4
- "version": "2.83.0"
4
+ "version": "2.85.0"
5
5
  }
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: shared-impl-reporter
3
3
  description: Generate implementation reports for Linear comments. Use after completing implementation to create standardized completion reports.
4
- tools: Read, Glob, Grep
4
+ tools: Read, Glob, Grep, Bash
5
5
  model: sonnet
6
- color: "#2563EB"
6
+ color: '#2563EB'
7
7
  ---
8
8
 
9
9
  You are an implementation reporter responsible for generating standardized completion reports for Linear task comments.
@@ -17,6 +17,35 @@ Generate well-formatted implementation reports that document:
17
17
  - Test status
18
18
  - Translation changes
19
19
  - Any issues or blockers
20
+ - Token usage for the current session
21
+
22
+ ## Token Usage Step (MANDATORY)
23
+
24
+ Every report you emit MUST end with exactly one Polish token-usage block — the one defined below. Append it after the report's existing `_Implementation report generated by Claude Code_` footer; do NOT embed the block inside any of the report templates that follow.
25
+
26
+ 1. Run the token-summing script once:
27
+
28
+ ```bash
29
+ bash "$CLAUDE_PLUGIN_ROOT/scripts/compute-session-tokens.sh"
30
+ ```
31
+
32
+ It prints four whitespace-separated integers on one line: `<INPUT> <OUTPUT> <CACHE_CREATE> <CACHE_READ>`. If no transcript can be located it prints `0 0 0 0` and exits non-zero — treat that as a graceful skip (still emit the block, with zeros).
33
+
34
+ 2. Compute `<TOTAL> = <INPUT> + <OUTPUT> + <CACHE_CREATE> + <CACHE_READ>`.
35
+
36
+ 3. Append this block to the end of the report:
37
+
38
+ ```markdown
39
+ ## Zużycie tokenów — etap /impl
40
+
41
+ | Metryka | Wartość |
42
+ | -------------- | -------------- |
43
+ | Input | <INPUT> |
44
+ | Output | <OUTPUT> |
45
+ | Cache creation | <CACHE_CREATE> |
46
+ | Cache read | <CACHE_READ> |
47
+ | **Łącznie** | **<TOTAL>** |
48
+ ```
20
49
 
21
50
  ## Input Requirements
22
51
 
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ # Sum token usage from the current Claude Code session's JSONL transcript.
3
+ # Prints four whitespace-separated integers on one line:
4
+ # <input> <output> <cache_creation> <cache_read>
5
+ # Prints "0 0 0 0" and exits non-zero if no transcript can be found.
6
+ #
7
+ # Resolution order:
8
+ # 1. $CLAUDE_CONFIG_DIR (set by Claude Code) or ~/.claude as fallback.
9
+ # 2. Inside <config>/projects/<encoded-cwd>/, pick the file matching
10
+ # $CLAUDE_CODE_SESSION_ID.jsonl when that env var is set.
11
+ # 3. Otherwise pick the most-recently-modified *.jsonl (current session).
12
+
13
+ set -u
14
+
15
+ CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
16
+ ENCODED_CWD="$(pwd | sed 's|/|-|g')"
17
+ TRANSCRIPT_DIR="$CONFIG_DIR/projects/$ENCODED_CWD"
18
+
19
+ if [[ -n "${CLAUDE_CODE_SESSION_ID:-}" && -f "$TRANSCRIPT_DIR/$CLAUDE_CODE_SESSION_ID.jsonl" ]]; then
20
+ TRANSCRIPT_FILE="$TRANSCRIPT_DIR/$CLAUDE_CODE_SESSION_ID.jsonl"
21
+ else
22
+ TRANSCRIPT_FILE="$(ls -t "$TRANSCRIPT_DIR"/*.jsonl 2>/dev/null | head -1)"
23
+ fi
24
+
25
+ if [[ -z "${TRANSCRIPT_FILE:-}" || ! -f "$TRANSCRIPT_FILE" ]]; then
26
+ echo "0 0 0 0"
27
+ exit 1
28
+ fi
29
+
30
+ jq -s '
31
+ reduce .[] as $m (
32
+ {i:0, o:0, cc:0, cr:0};
33
+ .i += ($m.message.usage.input_tokens // 0)
34
+ | .o += ($m.message.usage.output_tokens // 0)
35
+ | .cc += ($m.message.usage.cache_creation_input_tokens // 0)
36
+ | .cr += ($m.message.usage.cache_read_input_tokens // 0)
37
+ )
38
+ | "\(.i) \(.o) \(.cc) \(.cr)"
39
+ ' "$TRANSCRIPT_FILE" | tr -d '"'
@@ -0,0 +1,372 @@
1
+ #!/usr/bin/env bash
2
+ # Refresh the MuzaCMS / MobileMS indexer OpenAPI spec.
3
+ #
4
+ # Logs in to https://develop-indexer.mobilems.pl via Symfony form-login,
5
+ # scrapes the Swagger 2.0 JSON from /docs/api, and writes it next to this
6
+ # skill's owning package under shared-assets/mobilems-api.json.
7
+ #
8
+ # Required tools: bash, curl, jq.
9
+ # Required env vars (read from shell or auto-loaded from $PWD/.env):
10
+ # MOBILEMS_LOGIN — MuzaCMS username
11
+ # MOBILEMS_PASSWORD — MuzaCMS password
12
+ #
13
+ # Cache: a sibling mobilems-api.meta.json stores { sessionId, fetchedAt }.
14
+ # The script skips the network call when the same Claude Code session
15
+ # (CLAUDE_CODE_SESSION_ID) already fetched the spec less than 8h ago.
16
+
17
+ set -u
18
+
19
+ BASE='https://develop-indexer.mobilems.pl'
20
+ LOGIN_URL="${BASE}/admin/login"
21
+ DOCS_URL="${BASE}/docs/api"
22
+
23
+ QUIET=0
24
+ for arg in "$@"; do
25
+ case "$arg" in
26
+ --quiet) QUIET=1 ;;
27
+ esac
28
+ done
29
+ if [[ "${CLAUDE_AGENT_INVOCATION:-0}" == "1" ]]; then
30
+ QUIET=1
31
+ fi
32
+
33
+ log() {
34
+ if [[ "$QUIET" -eq 0 ]]; then
35
+ printf '%s\n' "$*"
36
+ fi
37
+ }
38
+
39
+ err() {
40
+ printf '%s\n' "$*" >&2
41
+ }
42
+
43
+ die() {
44
+ err "$*"
45
+ exit 1
46
+ }
47
+
48
+ # --- dependency check --------------------------------------------------------
49
+
50
+ check_deps() {
51
+ local missing=()
52
+ command -v curl >/dev/null 2>&1 || missing+=("curl")
53
+ command -v jq >/dev/null 2>&1 || missing+=("jq")
54
+ if [[ ${#missing[@]} -gt 0 ]]; then
55
+ err "✗ Brak wymaganych narzędzi: ${missing[*]}"
56
+ err ""
57
+ err "Zainstaluj:"
58
+ err " macOS: brew install ${missing[*]}"
59
+ err " Debian/Ubuntu: sudo apt install ${missing[*]}"
60
+ exit 1
61
+ fi
62
+ }
63
+
64
+ # --- .env loader -------------------------------------------------------------
65
+ # Minimal KEY=value parser. Ignores blank lines and #-comments, strips matching
66
+ # surrounding quotes, and never overrides already-set env vars (parity with the
67
+ # old refresh-mobilems-api-spec.js loader).
68
+
69
+ load_dotenv() {
70
+ local env_path="${PWD}/.env"
71
+ [[ -f "$env_path" ]] || return 0
72
+ local line key value first last
73
+ while IFS= read -r line || [[ -n "$line" ]]; do
74
+ # trim leading/trailing whitespace
75
+ line="${line#"${line%%[![:space:]]*}"}"
76
+ line="${line%"${line##*[![:space:]]}"}"
77
+ [[ -z "$line" ]] && continue
78
+ [[ "${line:0:1}" == "#" ]] && continue
79
+ [[ "$line" != *"="* ]] && continue
80
+ key="${line%%=*}"
81
+ value="${line#*=}"
82
+ # trim whitespace around key/value
83
+ key="${key#"${key%%[![:space:]]*}"}"
84
+ key="${key%"${key##*[![:space:]]}"}"
85
+ value="${value#"${value%%[![:space:]]*}"}"
86
+ value="${value%"${value##*[![:space:]]}"}"
87
+ [[ -z "$key" ]] && continue
88
+ # strip matching surrounding quotes (single or double)
89
+ first="${value:0:1}"
90
+ last="${value: -1}"
91
+ if [[ ${#value} -ge 2 ]] && { [[ "$first" == '"' && "$last" == '"' ]] || [[ "$first" == "'" && "$last" == "'" ]]; }; then
92
+ value="${value:1:${#value}-2}"
93
+ fi
94
+ # only set if not already in env
95
+ if [[ -z "${!key+x}" ]]; then
96
+ export "$key=$value"
97
+ fi
98
+ done < "$env_path"
99
+ log "→ loaded .env from $env_path"
100
+ }
101
+
102
+ # --- env-var validation (per Linear FRA-95 — Emil's request) ----------------
103
+ # This runs as the very first step after .env loading. We MUST NOT attempt any
104
+ # HTTP call until both credentials are present.
105
+
106
+ validate_env_vars() {
107
+ if [[ -z "${MOBILEMS_LOGIN:-}" || -z "${MOBILEMS_PASSWORD:-}" ]]; then
108
+ err "✗ Brak wymaganych zmiennych środowiskowych dla MuzaCMS API:"
109
+ err " MOBILEMS_LOGIN, MOBILEMS_PASSWORD"
110
+ err ""
111
+ err "Ustaw je w shellu:"
112
+ err " export MOBILEMS_LOGIN=..."
113
+ err " export MOBILEMS_PASSWORD=..."
114
+ err ""
115
+ err "Lub w pliku .env w katalogu głównym projektu:"
116
+ err " MOBILEMS_LOGIN=..."
117
+ err " MOBILEMS_PASSWORD=..."
118
+ err ""
119
+ err "Szczegóły: @smartsoft001-mobilems/claude-plugins README.md"
120
+ exit 1
121
+ fi
122
+ }
123
+
124
+ # --- output dir resolution ---------------------------------------------------
125
+ # Mirrors resolveOutDir() in the old JS. Looks for shared-assets/ walking up
126
+ # from this script's directory.
127
+
128
+ resolve_out_dir() {
129
+ local script_dir
130
+ script_dir="$(cd "$(dirname "$0")" && pwd)"
131
+ # Installed layout (per-variant copy made by nx build):
132
+ # <pkg-root>/plugins/<variant>/scripts/refresh-mobilems-api-spec.sh
133
+ # <pkg-root>/shared-assets/mobilems-api.json
134
+ # Source layout:
135
+ # packages/shared/claude-plugins/scripts/refresh-mobilems-api-spec.sh
136
+ # packages/shared/claude-plugins/src/shared-assets/
137
+ local candidates=(
138
+ "$script_dir/../../../shared-assets"
139
+ "$script_dir/../shared-assets"
140
+ "$script_dir/../src/shared-assets"
141
+ )
142
+ local c
143
+ for c in "${candidates[@]}"; do
144
+ if [[ -d "$c" ]]; then
145
+ (cd "$c" && pwd)
146
+ return 0
147
+ fi
148
+ done
149
+ # No existing dir — pick the first candidate whose parent exists so we can
150
+ # mkdir into it.
151
+ for c in "${candidates[@]}"; do
152
+ if [[ -d "$(dirname "$c")" ]]; then
153
+ mkdir -p "$c"
154
+ (cd "$c" && pwd)
155
+ return 0
156
+ fi
157
+ done
158
+ # Last resort: create alongside the script.
159
+ mkdir -p "$script_dir/shared-assets"
160
+ (cd "$script_dir/shared-assets" && pwd)
161
+ }
162
+
163
+ # --- cache decision ----------------------------------------------------------
164
+ # Mirrors cacheDecision() in the old JS.
165
+
166
+ MAX_CACHE_AGE_S=$((8 * 60 * 60))
167
+
168
+ iso_to_epoch() {
169
+ # macOS BSD date and GNU date diverge — try both forms.
170
+ local iso="$1"
171
+ if date -j -f "%Y-%m-%dT%H:%M:%SZ" "${iso%%.*}Z" "+%s" >/dev/null 2>&1; then
172
+ date -j -f "%Y-%m-%dT%H:%M:%SZ" "${iso%%.*}Z" "+%s"
173
+ elif date -d "$iso" "+%s" >/dev/null 2>&1; then
174
+ date -d "$iso" "+%s"
175
+ else
176
+ echo "0"
177
+ fi
178
+ }
179
+
180
+ cache_decision() {
181
+ # Echoes either "fetch:<reason>" or "skip:<age_seconds>".
182
+ local session_id="$1"
183
+ if [[ -z "$session_id" ]]; then
184
+ echo "fetch:manual/terminal invocation"
185
+ return
186
+ fi
187
+ if [[ ! -f "$SPEC_PATH" ]]; then
188
+ echo "fetch:spec file missing"
189
+ return
190
+ fi
191
+ if [[ ! -f "$META_PATH" ]]; then
192
+ echo "fetch:meta file missing"
193
+ return
194
+ fi
195
+ local meta_session meta_fetched_at meta_epoch now age
196
+ meta_session="$(jq -r '.sessionId // ""' "$META_PATH" 2>/dev/null)"
197
+ meta_fetched_at="$(jq -r '.fetchedAt // ""' "$META_PATH" 2>/dev/null)"
198
+ if [[ "$meta_session" != "$session_id" ]]; then
199
+ echo "fetch:new Claude Code session"
200
+ return
201
+ fi
202
+ if [[ -z "$meta_fetched_at" ]]; then
203
+ echo "fetch:meta missing fetchedAt"
204
+ return
205
+ fi
206
+ meta_epoch="$(iso_to_epoch "$meta_fetched_at")"
207
+ now="$(date +%s)"
208
+ age=$((now - meta_epoch))
209
+ if [[ "$meta_epoch" -le 0 || "$age" -gt "$MAX_CACHE_AGE_S" ]]; then
210
+ echo "fetch:cache older than 8 hours"
211
+ return
212
+ fi
213
+ echo "skip:$age"
214
+ }
215
+
216
+ # --- HTML extraction helpers -------------------------------------------------
217
+
218
+ extract_csrf() {
219
+ # Mirrors extractCsrf() in the old JS.
220
+ local html="$1"
221
+ local token
222
+ token="$(printf '%s' "$html" | grep -oE 'name="_csrf_token"[[:space:]]+value="[^"]+"' | head -n 1 | sed -E 's/.*value="([^"]+)".*/\1/')"
223
+ if [[ -z "$token" ]]; then
224
+ die "✗ Could not find _csrf_token in login form HTML."
225
+ fi
226
+ printf '%s' "$token"
227
+ }
228
+
229
+ extract_swagger_data() {
230
+ # Mirrors extractSwaggerData() in the old JS.
231
+ # The <script id="swagger-data" type="application/json"> tag wraps a JSON blob.
232
+ local html="$1"
233
+ local json
234
+ # awk lets us span newlines safely, where sed's portability gets hairy.
235
+ json="$(printf '%s' "$html" | awk '
236
+ BEGIN { in_block = 0 }
237
+ {
238
+ line = $0
239
+ if (in_block == 0) {
240
+ idx = index(line, "<script id=\"swagger-data\" type=\"application/json\">")
241
+ if (idx > 0) {
242
+ in_block = 1
243
+ line = substr(line, idx + length("<script id=\"swagger-data\" type=\"application/json\">"))
244
+ } else {
245
+ next
246
+ }
247
+ }
248
+ end_idx = index(line, "</script>")
249
+ if (end_idx > 0) {
250
+ printf "%s", substr(line, 1, end_idx - 1)
251
+ exit
252
+ } else {
253
+ printf "%s\n", line
254
+ }
255
+ }
256
+ ')"
257
+ # trim leading/trailing whitespace
258
+ json="${json#"${json%%[![:space:]]*}"}"
259
+ json="${json%"${json##*[![:space:]]}"}"
260
+ if [[ -z "$json" ]]; then
261
+ die "✗ Could not find <script id=\"swagger-data\"> tag in /docs/api response. Login may have silently failed, or the docs page structure changed."
262
+ fi
263
+ printf '%s' "$json"
264
+ }
265
+
266
+ # --- main --------------------------------------------------------------------
267
+
268
+ check_deps
269
+ load_dotenv
270
+ validate_env_vars
271
+
272
+ OUT_DIR="$(resolve_out_dir)"
273
+ SPEC_PATH="$OUT_DIR/mobilems-api.json"
274
+ META_PATH="$OUT_DIR/mobilems-api.meta.json"
275
+
276
+ SESSION_ID="${CLAUDE_CODE_SESSION_ID:-}"
277
+ DECISION="$(cache_decision "$SESSION_ID")"
278
+
279
+ if [[ "$DECISION" == skip:* ]]; then
280
+ age_s="${DECISION#skip:}"
281
+ age_min=$(( (age_s + 30) / 60 ))
282
+ fetched_at="$(jq -r '.fetchedAt // "unknown"' "$META_PATH" 2>/dev/null)"
283
+ log "✓ spec cached for this Claude Code session (refreshed ${age_min} min ago at ${fetched_at}), skipping fetch"
284
+ exit 0
285
+ fi
286
+
287
+ REASON="${DECISION#fetch:}"
288
+ log "Refreshing MuzaCMS API spec from $BASE ($REASON)"
289
+
290
+ JAR="$(mktemp -t mobilems-cookies.XXXXXX)"
291
+ trap 'rm -f "$JAR"' EXIT
292
+
293
+ log "→ GET /admin/login (CSRF + session)"
294
+ LOGIN_HTML="$(curl --silent --show-error \
295
+ --cookie-jar "$JAR" \
296
+ "$LOGIN_URL")" || die "✗ Could not fetch $LOGIN_URL"
297
+
298
+ CSRF="$(extract_csrf "$LOGIN_HTML")"
299
+
300
+ log "→ POST /admin/login"
301
+ # We need the HTTP status of the POST without following the redirect.
302
+ LOGIN_POST_STATUS_BODY="$(curl --silent --show-error \
303
+ --output /dev/null \
304
+ --write-out '%{http_code} %{redirect_url}' \
305
+ --cookie-jar "$JAR" --cookie "$JAR" \
306
+ --data-urlencode "_username=${MOBILEMS_LOGIN}" \
307
+ --data-urlencode "_password=${MOBILEMS_PASSWORD}" \
308
+ --data-urlencode "_csrf_token=${CSRF}" \
309
+ --data-urlencode "_remember_me=on" \
310
+ -X POST \
311
+ "$LOGIN_URL")" || die "✗ POST $LOGIN_URL failed."
312
+
313
+ LOGIN_STATUS="${LOGIN_POST_STATUS_BODY%% *}"
314
+ LOGIN_REDIRECT="${LOGIN_POST_STATUS_BODY#* }"
315
+
316
+ if [[ "$LOGIN_STATUS" != "302" ]]; then
317
+ die "✗ Login failed: expected 302 redirect, got HTTP ${LOGIN_STATUS}. Check MOBILEMS_LOGIN and MOBILEMS_PASSWORD."
318
+ fi
319
+ if [[ "$LOGIN_REDIRECT" == *"/admin/login"* ]]; then
320
+ die "✗ Login failed: server redirected back to /admin/login. Credentials were likely rejected."
321
+ fi
322
+
323
+ log "→ GET /docs/api"
324
+ DOCS_STATUS_FILE="$(mktemp -t mobilems-docs-status.XXXXXX)"
325
+ trap 'rm -f "$JAR" "$DOCS_STATUS_FILE"' EXIT
326
+
327
+ DOCS_HTML="$(curl --silent --show-error \
328
+ --write-out '%{http_code}' \
329
+ --cookie-jar "$JAR" --cookie "$JAR" \
330
+ --output - \
331
+ "$DOCS_URL" 2>/dev/null | tee >(tail -c 3 > "$DOCS_STATUS_FILE"))"
332
+
333
+ # Strip the trailing 3-char status code from the body
334
+ DOCS_STATUS="$(cat "$DOCS_STATUS_FILE")"
335
+ DOCS_HTML="${DOCS_HTML%$DOCS_STATUS}"
336
+
337
+ if [[ "$DOCS_STATUS" != "200" ]]; then
338
+ die "✗ Docs page returned HTTP ${DOCS_STATUS}. Session may not be authenticated."
339
+ fi
340
+ if [[ "$DOCS_HTML" == *"login-card-body"* ]]; then
341
+ die "✗ Docs page returned the login form — session not authenticated."
342
+ fi
343
+
344
+ SPEC_JSON="$(extract_swagger_data "$DOCS_HTML")"
345
+
346
+ # Validate + pretty-print through jq.
347
+ if ! printf '%s' "$SPEC_JSON" | jq '.' > "${SPEC_PATH}.tmp"; then
348
+ rm -f "${SPEC_PATH}.tmp"
349
+ die "✗ Swagger JSON failed to parse."
350
+ fi
351
+ mv -f "${SPEC_PATH}.tmp" "$SPEC_PATH"
352
+
353
+ SPEC_TITLE="$(jq -r '.spec.info.title // .info.title // "unknown"' "$SPEC_PATH")"
354
+ SPEC_VERSION="$(jq -r '.spec.info.version // .info.version // "unknown"' "$SPEC_PATH")"
355
+
356
+ # Preserve cached sessionId when running outside Claude Code (manual/terminal)
357
+ # so a future in-session call still hits the cache. In-Claude-Code calls always
358
+ # overwrite with the current session id.
359
+ EFFECTIVE_SESSION_ID="$SESSION_ID"
360
+ if [[ -z "$EFFECTIVE_SESSION_ID" && -f "$META_PATH" ]]; then
361
+ EFFECTIVE_SESSION_ID="$(jq -r '.sessionId // ""' "$META_PATH" 2>/dev/null)"
362
+ fi
363
+ ISO_NOW="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
364
+ jq -n \
365
+ --arg sid "$EFFECTIVE_SESSION_ID" \
366
+ --arg ts "$ISO_NOW" \
367
+ '{sessionId: (if $sid == "" then null else $sid end), fetchedAt: $ts}' \
368
+ > "$META_PATH"
369
+
370
+ BYTES="$(wc -c < "$SPEC_PATH" | tr -d ' ')"
371
+ log "✓ Refreshed ${BYTES} bytes — ${SPEC_TITLE} ${SPEC_VERSION}"
372
+ log " $SPEC_PATH"