@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.
@@ -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"
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: mobilems-docs-reader
3
+ description: Answer questions about the MuzaCMS / MobileMS indexer API by reading a freshly-fetched OpenAPI spec. Use whenever the user asks about endpoints, request/response shapes, status codes, auth, or behavior of the indexer API (host develop-indexer-api.mobilems.pl). Read-only — never fabricates endpoints.
4
+ allowed-tools:
5
+ - Bash
6
+ - Read
7
+ - Glob
8
+ - Grep
9
+ ---
10
+
11
+ You answer questions about the **MuzaCMS / MobileMS indexer API** strictly from the OpenAPI (Swagger 2.0) specification. You never invent endpoints, schemas, or status codes — if it is not in the spec, you say so plainly.
12
+
13
+ The spec is **not bundled**. It is fetched from the live MuzaCMS instance by a refresh script that lives next to this skill, using credentials supplied via environment variables. The script caches the fetched spec per Claude Code session (with an 8-hour TTL safety net), so it may skip the actual network call and reuse a cached copy on follow-up invocations within the same session.
14
+
15
+ ## Step 1 — Run the refresh script (mandatory)
16
+
17
+ On every invocation, run the refresh script via Bash. The script itself decides whether to actually fetch — it owns the cache. Your job is only to run it and react to the result. A successful run may be either:
18
+
19
+ - A real fetch (`Refreshing MuzaCMS API spec from ... → GET /admin/login → POST /admin/login → GET /docs/api → ✓ Refreshed N bytes ...`), or
20
+ - A cache hit (`✓ spec cached for this Claude Code session (refreshed N min ago at ...), skipping fetch`).
21
+
22
+ Both are exit-code-0 successes. Treat them identically and proceed to Step 2. **Do not** attempt to bypass the cache, delete the cache file, or force a refetch — the cache is intentional and the script's decision is authoritative.
23
+
24
+ ### Invocation
25
+
26
+ The refresh script is shipped per plugin variant under `plugins/<variant>/scripts/`. Invoke it through `bash` so the executable bit is irrelevant:
27
+
28
+ ```
29
+ bash ./node_modules/@smartsoft001-mobilems/claude-plugins/plugins/flow-external/scripts/refresh-mobilems-api-spec.sh --quiet
30
+ ```
31
+
32
+ Outcomes:
33
+
34
+ - **Success (exit 0)** → jump to Step 2.
35
+ - **Script-level error** (missing env vars, missing tooling, auth rejected, HTTP error, parse failure, etc.) → jump to the matching Fail mode below.
36
+ - **Permission denied by Claude Code** → jump to "Fail mode — permission denied".
37
+
38
+ The script reads two environment variables — `MOBILEMS_LOGIN` and `MOBILEMS_PASSWORD` — performs the Symfony CSRF + session login against `https://develop-indexer.mobilems.pl`, fetches `/docs/api`, and writes the spec to `shared-assets/mobilems-api.json` inside the installed package. It also auto-loads `.env` from the current working directory; no shell sourcing is needed.
39
+
40
+ ### Fail mode — permission denied by Claude Code
41
+
42
+ If the Bash call is rejected by Claude Code's permission system, stop and tell the user verbatim:
43
+
44
+ > The MuzaCMS spec could not be refreshed because Claude Code denied permission to run the refresh script. To allow it, add this entry to your project's `.claude/settings.json` (committed) or `.claude/settings.local.json` (per-dev, gitignored) under `permissions.allow`, then ask again:
45
+ >
46
+ > - `"Bash(bash ./node_modules/@smartsoft001-mobilems/claude-plugins/plugins/flow-external/scripts/refresh-mobilems-api-spec.sh:*)"`
47
+
48
+ Do not attempt to answer the user's question from training knowledge. Do not invent alternative invocation patterns to try to bypass the denial.
49
+
50
+ ### Fail mode — missing env vars
51
+
52
+ If the script exits non-zero with a message starting `✗ Brak wymaganych zmiennych środowiskowych` (or English equivalent listing `MOBILEMS_LOGIN` / `MOBILEMS_PASSWORD`), **stop immediately**. Relay the script's message verbatim to the user.
53
+
54
+ Do not attempt to answer the user's question from training knowledge. Do not guess. Do not read any pre-existing `mobilems-api.json` that may be lying around in `node_modules/` — without a successful refresh you have no confirmation it is current.
55
+
56
+ ### Fail mode — missing tooling
57
+
58
+ If the script exits with a message about missing `curl` or `jq`, relay it verbatim. The script tells the user exactly how to install the missing dependency (`brew install jq` / `apt install jq`).
59
+
60
+ ### Fail mode — other script failures
61
+
62
+ If the script fails for any other reason (HTTP error, auth rejected, parse failure), report the exact error message from the script back to the user and stop. Do not answer the API question.
63
+
64
+ ## Step 2 — Locate the freshly-written spec
65
+
66
+ Use `Glob` with this pattern:
67
+
68
+ ```
69
+ **/claude-plugins/**/shared-assets/mobilems-api.json
70
+ ```
71
+
72
+ The hit lives under `node_modules/@smartsoft001-mobilems/claude-plugins/shared-assets/` after a successful refresh. Exclude `dist/` and `tmp/` from the Glob, but **do** allow `node_modules/`.
73
+
74
+ If the Glob returns nothing after the refresh script reported success, something is wrong with the install layout — tell the user and stop.
75
+
76
+ ## Step 3 — Answer from the spec
77
+
78
+ The spec is **Swagger 2.0** with this wrapping shape:
79
+
80
+ ```
81
+ { "spec": { "swagger": "2.0", "paths": {...}, "definitions": {...} } }
82
+ ```
83
+
84
+ Always traverse into `.spec` first.
85
+
86
+ When the user asks about endpoints:
87
+
88
+ 1. **Locate the path** in `spec.paths`. Look for the exact path the user mentioned, including any path parameters (`{id}`).
89
+ 2. **Quote the HTTP method(s) and path verbatim.** Example: `GET /api/objects/{id}`.
90
+ 3. For request bodies and responses, cite the `$ref` target by name and (briefly) summarize its shape from `spec.definitions`.
91
+ 4. For status codes, list each documented response from `responses` with its `description`.
92
+ 5. For auth, check both the operation-level `security` and the global `spec.securityDefinitions`.
93
+
94
+ If the user asks about a path that is **not in the spec**, say so explicitly:
95
+
96
+ > The spec does not document a `DELETE /api/articles/{id}` endpoint. The articles paths in the spec are: `[list actual paths]`. If you've seen this endpoint elsewhere, it may exist in the running service without being documented — verify with a live request.
97
+
98
+ ## Known caveats — always surface when relevant
99
+
100
+ - **Response schemas occasionally diverge from runtime.** Documented response shapes have, in practice, not always matched what the live API actually returns. When the user is about to implement against a response schema, add a brief reminder to verify with a real request before relying on the documented shape.
101
+ - **Spec is regenerated server-side from PHP annotations.** Newly added endpoints in the live service may not appear until the next backend deployment, even with a fresh fetch.
102
+ - **Auth flow nuance:** the spec describes the indexer API itself; the `/admin/login` flow used by the refresh script is separate and not documented in the spec.
103
+
104
+ ## Answering style
105
+
106
+ - Be terse. The user wants a specific fact, not a tutorial on Swagger.
107
+ - Always include exact path + method + relevant status codes when applicable.
108
+ - When quoting definitions, use the schema name (e.g., `Object`, `Unauthorised`) as it appears in `spec.definitions`.
109
+ - If multiple endpoints match a vague question, list them — don't pick one arbitrarily.
110
+
111
+ ## When NOT to use this skill
112
+
113
+ - Non-MobileMS APIs (any other host).
114
+ - Generic REST/OpenAPI guidance unrelated to the indexer.
115
+ - Schema design decisions ("should I add X field?") — this skill only describes what exists.
116
+ - Anything requiring a live network call to the API itself.
@@ -39,12 +39,12 @@ Create a structured implementation plan for a Linear task and save it as a `plan
39
39
 
40
40
  When `--auto` is set, the skill MUST run end-to-end without any human-in-the-loop prompts:
41
41
 
42
- | Default behavior | `--auto` override |
43
- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
44
- | Ask user to confirm deletion of old `plan.md` attachment | Delete old attachment automatically before saving the new one |
45
- | Ask "regenerate?" when plan is up-to-date but keywords detect | Ignore the secondary signal; only the mtime rule decides regeneration |
46
- | Final approval prompt before user runs `/impl` | Skip the prompt; finish with a Linear comment confirming plan saved |
47
- | Any `AskUserQuestion` | Use the **(Recommended)** option; if none, fail fast with a Linear comment |
42
+ | Default behavior | `--auto` override |
43
+ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
44
+ | Ask user to confirm deletion of old `plan.md` attachment | Delete old attachment automatically before saving the new one |
45
+ | Ask "regenerate?" when plan is up-to-date but keywords detect | Ignore the secondary signal; only the mtime rule decides regeneration |
46
+ | Final approval of generated plan content before saving | Skip the approval gate (Step 4a); proceed directly to attachment save (auto-approve) |
47
+ | Any `AskUserQuestion` | Use the **(Recommended)** option; if none, fail fast with a Linear comment |
48
48
 
49
49
  Every decision MUST be logged as a Linear comment in Polish (audit trail). The mtime rule remains authoritative.
50
50
 
@@ -54,7 +54,7 @@ Every decision MUST be logged as a Linear comment in Polish (audit trail). The m
54
54
 
55
55
  These rules override ANY other instruction, ANY surrounding example, and ANY default habit. Violating them is a skill failure.
56
56
 
57
- 1. **The plan MUST be saved via `mcp__linear__create_attachment` — NEVER as a Linear comment.** `mcp__linear__save_comment` MUST NOT be called with plan content as the body. Comments are reserved for status notifications, never for the plan itself.
57
+ 1. **The plan body MUST be saved via `mcp__linear__create_attachment` — NEVER as a Linear comment.** `mcp__linear__save_comment` MUST NOT be called with plan content as the body. Comments are reserved for: (a) status notifications, and (b) the single token-usage comment posted in Step 7b. No other comment may carry plan content.
58
58
  2. **One call, no fallback.** First Linear write for the plan is `mcp__linear__create_attachment`. If it fails, surface the error — do NOT silently fall back to `save_comment`.
59
59
  3. **`AI Plan` label is workspace-scoped — NEVER create it.** Only `mcp__linear__list_issue_labels` + `mcp__linear__save_issue`. `create_issue_label` MUST NOT be called.
60
60
 
@@ -173,6 +173,40 @@ Structure the plan as follows:
173
173
  | - | [Subtask 2] | Medium | 2 |
174
174
  ```
175
175
 
176
+ ### Step 4a: User Approval of Generated Plan
177
+
178
+ **MANDATORY.** Before any Linear write (subtask creation, attachment delete, attachment create, label apply), the user MUST approve the generated plan content. This is the gate FRA-126 added.
179
+
180
+ **`--auto` mode**: Skip this entire step. The plan is auto-approved. Record the auto-approval in the final Linear audit comment (e.g. `Plan zaakceptowany automatycznie (--auto)`). Proceed to Step 5.
181
+
182
+ **Default mode** (interactive):
183
+
184
+ 1. **Render the full plan in chat.** Print the markdown produced in Step 4. The `AskUserQuestion` option labels alone do not give the user enough context — the full text must be visible above the prompt.
185
+
186
+ 2. **Ask for approval** via `AskUserQuestion`:
187
+
188
+ ```markdown
189
+ ## Akceptacja planu
190
+
191
+ Powyżej znajduje się wygenerowany plan dla zadania <linearTaskId>.
192
+ Czy akceptujesz plan i mogę zapisać go jako załącznik `plan.md` w Linear?
193
+ ```
194
+
195
+ Options:
196
+
197
+ - **"Tak, zapisz plan do Linear"** _(Recommended)_ — user accepts. Proceed to Step 5 (create subtasks if needed) and Step 6 (save attachment).
198
+ - **"Wygeneruj plan ponownie"** — user wants a different plan. Go to step 3 (regeneration loop) below.
199
+ - **"Anuluj"** — user rejects. Stop the whole skill. Do NOT create subtasks. Do NOT delete the existing `plan.md` attachment. Do NOT save a new one. Do NOT apply the `AI Plan` label. Print a short status in Polish (`Plan nie został zapisany. Anulowano na żądanie użytkownika.`) and exit.
200
+
201
+ 3. **Regeneration branch**: if the user chose "Wygeneruj plan ponownie":
202
+
203
+ - Ask a follow-up `AskUserQuestion` (or accept the free-text the user typed alongside the rejection) to capture what should change: scope, structure, missed files, wrong approach, etc.
204
+ - Loop back to **Step 4 (Create Implementation Plan)** with the user's feedback in mind. Do NOT re-run Steps 1–3 — the Linear task, comments, and codebase analysis have not changed.
205
+ - On the regenerated plan, return to **Step 4a step 1** (render + ask again).
206
+ - **Cap the loop at 3 regenerations.** On the 3rd rejection, surface the situation to the user (`Plan został odrzucony 3 razy. Przerywam, abyś mógł doprecyzować wymagania.`) and exit without saving.
207
+
208
+ 4. **State invariant on entry to Step 5**: the only path that reaches Step 5 is the explicit "Tak, zapisz plan do Linear" choice (or `--auto`). Treat plan content as final.
209
+
176
210
  ### Step 5: Create Subtasks (if needed)
177
211
 
178
212
  For complex tasks, create subtasks in Linear:
@@ -182,7 +216,7 @@ For complex tasks, create subtasks in Linear:
182
216
 
183
217
  ### Step 6: Save Plan as Native Linear Attachment
184
218
 
185
- **⛔ DO NOT call `mcp__linear__save_comment` with plan content here. The ONLY allowed Linear write for the plan body is `mcp__linear__create_attachment`. Re-read Hard Rule #1 if tempted.**
219
+ **⛔ DO NOT call `mcp__linear__save_comment` with plan content here. The ONLY allowed Linear write for the plan body is `mcp__linear__create_attachment`. Re-read Hard Rule #1 if tempted.** (Token usage is posted via `save_comment` in Step 7b — that's the one allowed exception.)
186
220
 
187
221
  Upload the plan directly to Linear via `mcp__linear__create_attachment` — Linear hosts the file itself, no external storage needed.
188
222
 
@@ -191,14 +225,18 @@ Upload the plan directly to Linear via `mcp__linear__create_attachment` — Line
191
225
  If a `plan.md` attachment is being replaced:
192
226
 
193
227
  - **Default mode**: ask the user via `AskUserQuestion`:
228
+
194
229
  - "Tak, usuń stary plan i zapisz nowy" (Recommended)
195
230
  - "Anuluj"
196
231
 
197
232
  If confirmed, delete the Linear attachment via `mcp__linear__delete_attachment({ id: oldAttachmentId })`.
233
+
198
234
  - **`--auto` mode**: skip the prompt; delete the old Linear attachment via `mcp__linear__delete_attachment({ id: oldAttachmentId })` automatically and proceed.
199
235
 
200
236
  #### 6.2 — Write `plan.md` locally
201
237
 
238
+ The plan content is exactly the markdown produced in Step 4. Write it to the temp file:
239
+
202
240
  ```bash
203
241
  mkdir -p /tmp/claude-plans
204
242
  ```
@@ -234,8 +272,40 @@ Delete `/tmp/claude-plans/<taskId>-plan.md` after the attachment is created. `/p
234
272
  2. Apply via `mcp__linear__save_issue({ id: taskId, labelIds: [...existingLabelIds, aiPlanLabelId] })`. Preserve existing labels; skip if already present.
235
273
  3. If label operations fail, surface the error but do not roll back the plan attachment.
236
274
 
275
+ ### Step 7b: Post Token-Usage Comment
276
+
277
+ After the plan attachment and `AI Plan` label have been applied, post a single Linear comment on the **invoked issue** (the `linearTaskId` passed to `/plan`, identical to the labelling target) reporting the session's token usage.
278
+
279
+ 1. Run the per-plugin token-summing script:
280
+
281
+ ```bash
282
+ bash "$CLAUDE_PLUGIN_ROOT/scripts/compute-session-tokens.sh"
283
+ ```
284
+
285
+ The script prints four whitespace-separated integers on one line: `<input> <output> <cache_creation> <cache_read>`. If no transcript can be located it prints `0 0 0 0` and exits non-zero — treat that as a graceful skip (do NOT block; skip posting the comment).
286
+
287
+ 2. Compute `<TOTAL> = <input> + <output> + <cache_creation> + <cache_read>`.
288
+
289
+ 3. Call `mcp__linear__save_comment` with `issueId: taskId` and `body` equal to:
290
+
291
+ ```markdown
292
+ ## Zużycie tokenów — etap /plan
293
+
294
+ | Metryka | Wartość |
295
+ | -------------- | -------------- |
296
+ | Input | <INPUT> |
297
+ | Output | <OUTPUT> |
298
+ | Cache creation | <CACHE_CREATE> |
299
+ | Cache read | <CACHE_READ> |
300
+ | **Łącznie** | **<TOTAL>** |
301
+ ```
302
+
303
+ 4. **Failure handling**: if the comment fails to post, surface the error to the user but do not roll back the plan attachment or the label.
304
+
237
305
  ### Step 8: Confirm to User
238
306
 
307
+ The plan has been approved in Step 4a (or auto-approved in `--auto`). Step 8 is purely informational — **do NOT ask another question here**, just print the status.
308
+
239
309
  **Default mode**:
240
310
 
241
311
  ```markdown
@@ -244,7 +314,7 @@ Delete `/tmp/claude-plans/<taskId>-plan.md` after the attachment is created. `/p
244
314
  Plan implementacji został dodany jako załącznik `plan.md` do zadania [linearTaskId].
245
315
  Etykieta `AI Plan` została dodana.
246
316
 
247
- Czy plan jest zaakceptowany? Po akceptacji uruchom `/impl [linearTaskId]` aby rozpocząć implementację.
317
+ Uruchom `/impl [linearTaskId]` aby rozpocząć implementację.
248
318
  ```
249
319
 
250
320
  **`--auto` mode**: post a Linear comment confirming the plan was saved (no question asked) and exit. The comment is the audit trail for the CI run.
@@ -100,7 +100,29 @@ If confirmed:
100
100
 
101
101
  1. Update parent task status to "In Review"
102
102
  2. Update completed subtasks to "In Review"
103
- 3. Create summary comment on parent task (in Polish):
103
+ 3. **Before posting**, run the token-summing script and capture its output:
104
+
105
+ ```bash
106
+ bash "$CLAUDE_PLUGIN_ROOT/scripts/compute-session-tokens.sh"
107
+ ```
108
+
109
+ 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 post the comment, with zeros in the table).
110
+
111
+ Compute `<TOTAL> = <INPUT> + <OUTPUT> + <CACHE_CREATE> + <CACHE_READ>`. Append exactly one copy of the following Polish block to the bottom of the comment body, after the existing footer. Do not create a second Linear comment — the block belongs in **this** comment.
112
+
113
+ ```markdown
114
+ ## Zużycie tokenów — etap /push
115
+
116
+ | Metryka | Wartość |
117
+ | -------------- | -------------- |
118
+ | Input | <INPUT> |
119
+ | Output | <OUTPUT> |
120
+ | Cache creation | <CACHE_CREATE> |
121
+ | Cache read | <CACHE_READ> |
122
+ | **Łącznie** | **<TOTAL>** |
123
+ ```
124
+
125
+ 4. Create summary comment on parent task (in Polish):
104
126
 
105
127
  ```markdown
106
128
  ## Podsumowanie Implementacji
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "flow-legacy",
3
3
  "description": "Development flow skills for legacy Angular 14 projects with Linear-driven workflow",
4
- "version": "2.83.0"
4
+ "version": "2.85.0"
5
5
  }