@sitar_fiercer4c/skills 0.1.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.
Files changed (50) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +75 -0
  3. package/bin/install.js +45 -0
  4. package/package.json +29 -0
  5. package/skills/architecture-walkthrough/SKILL.md +223 -0
  6. package/skills/architecture-walkthrough/references/sections.md +29 -0
  7. package/skills/architecture-walkthrough/scripts/check_structure.py +200 -0
  8. package/skills/autotest-webapp-ui/SKILL.md +58 -0
  9. package/skills/backend-code-review/SKILL.md +386 -0
  10. package/skills/backend-code-review/references/report-format.md +333 -0
  11. package/skills/backend-code-review/scripts/list_routes.py +269 -0
  12. package/skills/backend-code-review/scripts/sweep.py +550 -0
  13. package/skills/backend-code-review/scripts/verify_citations.py +201 -0
  14. package/skills/be-brief/SKILL.md +18 -0
  15. package/skills/clarke-list-excel/SKILL.md +51 -0
  16. package/skills/clarke-list-excel/references/output-schema.md +125 -0
  17. package/skills/clarke-list-excel/scripts/clarke_common.py +251 -0
  18. package/skills/clarke-list-excel/scripts/clarke_extract.py +487 -0
  19. package/skills/clarke-list-excel/scripts/load_clarke.py +322 -0
  20. package/skills/clarke-list-excel/scripts/run_all.py +63 -0
  21. package/skills/datalab-api/SKILL.md +163 -0
  22. package/skills/datalab-api/references/parameters-and-payload.md +121 -0
  23. package/skills/datalab-api/references/table-selection.md +35 -0
  24. package/skills/datalab-api/scripts/datalab_tables.py +365 -0
  25. package/skills/find-test-seam/SKILL.md +41 -0
  26. package/skills/frontend-code-review/SKILL.md +247 -0
  27. package/skills/frontend-code-review-2/SKILL.md +192 -0
  28. package/skills/frontend-code-review-2/scripts/fetch_pr_comments.py +65 -0
  29. package/skills/frontend-code-review-2/scripts/render_report.py +139 -0
  30. package/skills/murtaza-breif/SKILL.md +143 -0
  31. package/skills/murtaza-breif/scripts/save_brief.py +128 -0
  32. package/skills/pdf-to-json/SKILL.md +42 -0
  33. package/skills/pdf-to-json/references/output-schema.md +168 -0
  34. package/skills/pdf-to-json/scripts/extract_figures.py +319 -0
  35. package/skills/pdf-to-json/scripts/load_mongo.py +287 -0
  36. package/skills/pdf-to-json/scripts/pdf_extract.py +1313 -0
  37. package/skills/record-api-traffic/SKILL.md +434 -0
  38. package/skills/record-api-traffic/references/reading-recordings.md +224 -0
  39. package/skills/record-api-traffic/scripts/check-schema.mjs +184 -0
  40. package/skills/record-api-traffic/scripts/dump-quotation.mjs +67 -0
  41. package/skills/record-api-traffic/scripts/dump-source-excel.mjs +75 -0
  42. package/skills/record-api-traffic/scripts/lib/repo.mjs +109 -0
  43. package/skills/record-api-traffic/scripts/preflight.py +528 -0
  44. package/skills/record-api-traffic/scripts/record-api-traffic.py +720 -0
  45. package/skills/refac-wrt-business-goal/SKILL.md +305 -0
  46. package/skills/refac-wrt-business-goal/references/critic.md +170 -0
  47. package/skills/system-resource-triage/SKILL.md +180 -0
  48. package/skills/system-resource-triage/scripts/reap.sh +116 -0
  49. package/skills/system-resource-triage/scripts/triage.sh +111 -0
  50. package/skills/using-git-worktrees/SKILL.md +167 -0
@@ -0,0 +1,434 @@
1
+ ---
2
+ name: record-api-traffic
3
+ description: Record the HTTP traffic of a Node/Next app on four legs — browser-to-frontend, browser-to-backend, and each server's own outbound calls (the frontend's server-side fetches, the backend's to Microsoft Graph and Datalab) — by running this skill's own recorder against a repo path, after a preflight that finds every blocker first. Everything lives in the skill; the repo is only read. Use this skill whenever the user wants to capture, record, trace, log or inspect those calls — "record the API traffic", "record the frontend traffic", "start the recorder", "capture what the frontend sends", "what pages did the browser load", "what does the backend call out to", "I want a log of the API calls" — and also whenever they hit trouble around it: "port 7100 is in use", "the recording is empty", "the backend won't start", "the dev server won't move off 4100", "mitmproxy won't bind", "what's holding 4100". Also use it when they ask what a finished recording contains or how to read the .jsonl and .log output, or when they want the pipeline's real input and output — the source Excel file or the stored quotation document — dumped for a given quotationId.
4
+ ---
5
+
6
+ # Recording API traffic
7
+
8
+ ## What this is
9
+
10
+ Everything this skill needs lives in the skill. `scripts/record-api-traffic.py` starts six
11
+ processes — the two servers, each moved one port up, with a mitmproxy in front of it and a
12
+ second mitmproxy recording its own outbound calls — and prints a link. The user opens it, uses
13
+ the app, presses Ctrl-C, and is left with a single folder for that run holding four pairs of
14
+ files: for each leg, an aligned table of every call and a JSONL file with full request and
15
+ response bodies.
16
+
17
+ ```
18
+ browser ──→ :4100 recorder ──→ :4101 frontend ──→ :4102 recorder ──→ the backend, external
19
+ └─→ :7100 recorder ──→ :7101 backend ──→ :7102 recorder ──→ Graph, Datalab
20
+
21
+ :4100 frontend-inbound.* what arrives at the frontend (browser)
22
+ :4102 frontend-outbound.* what the frontend calls out to (backend, external)
23
+ :7100 backend-inbound.* what arrives at the backend (browser, frontend)
24
+ :7102 backend-outbound.* what the backend calls out to (Graph, Datalab)
25
+ ```
26
+
27
+ Both servers are treated identically: the recorder takes the port that is already forwarded to
28
+ the laptop, the server moves one up behind it, and a forward proxy catches what that server's
29
+ own Node process calls out to. Nothing on the VS Code side changes and no config file in the
30
+ repo is edited.
31
+
32
+ **Ask for one thing: the repo path.** Every script here takes it as its single input, and once
33
+ you have it you reuse it for all of them. Nothing else is asked of the user.
34
+
35
+ **The repo is read, never written.** The backend's working directory is a temp dir under `/tmp`,
36
+ recordings and dumps land in `skills/record-api-traffic-workspace/recordings/`, and no config
37
+ file is edited.
38
+ The one exception is honest and unavoidable: `npm run dev` writes `Frontend/.next/`, exactly as
39
+ it would if the user started the dev server themselves. It is gitignored. If you ever find
40
+ yourself about to create, edit or delete anything else under the repo, stop — that is a bug in
41
+ what you are doing, not a step.
42
+
43
+ The scripts work. The hard part is everything around them: ports are frequently occupied, the
44
+ frontend can be silently pointed away from the recorder, and stopping a stray process is a
45
+ decision that needs the user's judgment rather than yours. That is what this skill is for.
46
+
47
+ Run the preflight, resolve what it finds *with* the user, launch, then prove the recording is
48
+ actually capturing before calling it done.
49
+
50
+ ## Why the recorders sit on 7100 and 4100
51
+
52
+ Worth understanding, because it explains several checks. The browser runs on the user's laptop
53
+ and reaches the VM through VS Code port forwarding. `:4100` and `:7100` are already forwarded.
54
+ So each recorder takes over the port the browser already calls and the server moves one up
55
+ behind it. Nothing changes on the laptop, no new port needs forwarding, and no config file in
56
+ the repo is edited. `BACKEND_PUBLIC_URL` and `AZURE_REDIRECT_URI` already point at `:7100`, so
57
+ the Outlook OAuth callback keeps working and gets recorded too.
58
+
59
+ The two servers resist being moved in different ways, and each needs its own trick.
60
+
61
+ **The backend** ignores shell env vars: `app.js` calls `dotenv.config({ override: true })`, so
62
+ `.env` wins. The recorder instead runs `node src/server.js` from a temp directory holding a copy
63
+ of `.env` with `PORT=7101`, since dotenv resolves its file from the current working directory.
64
+ That copy is deleted on teardown, and it is why nothing has to be written inside the repo to
65
+ move the port.
66
+
67
+ **The frontend** ignores `PORT` for a different reason: the dev script pins the port on the
68
+ command line (`next dev -H 0.0.0.0 -p 4100`), and commander only consults `.env("PORT")` when
69
+ the flag is absent. So the recorder appends a second one — `npm run dev -- -p 4101` — which both
70
+ argument parsers Next has shipped resolve to the last occurrence. `PORT` is exported alongside
71
+ for a dev script that leaves the port unpinned. Preflight reports which of the two applies, and
72
+ warns when the dev script is not a Next one at all.
73
+
74
+ ## Why there are two forward proxies
75
+
76
+ A reverse proxy only sees what the browser sends. Plenty of what these apps do never crosses one:
77
+ the Excel attachment the backend pulls from Microsoft Graph server-side, the Datalab extraction
78
+ calls, and anything the frontend fetches from a server component or route handler rather than
79
+ from the browser. Those are outbound HTTPS from a Node process, so each server gets a
80
+ `mitmdump --mode regular` of its own and is pointed at it with `HTTP_PROXY`/`HTTPS_PROXY` plus
81
+ `NODE_USE_ENV_PROXY=1` — Node's global `fetch` ignores proxy env vars without that flag.
82
+ `NODE_EXTRA_CA_CERTS` makes it trust mitmproxy's CA.
83
+
84
+ The two differ in one respect, and it is deliberate. The backend excludes all of loopback from
85
+ its proxy (`NO_PROXY=127.0.0.1,localhost`), because it has no loopback calls worth recording.
86
+ The frontend excludes only its **own** two ports, so a server component calling the backend on
87
+ `:7100` still goes through `:4102` — that call is the main thing the frontend outbound leg
88
+ exists to catch. It is then recorded twice, once leaving the frontend and once arriving at the
89
+ backend, which is the point: you can see both ends of the same call.
90
+
91
+ Not covered on either: the AWS SDK (its own HTTP handler ignores the env) and MongoDB (a binary
92
+ protocol, not HTTP). `--no-outbound` skips both forward proxies.
93
+
94
+ ## Step 1 — Preflight
95
+
96
+ Always run this before launching. It is read-only: it never kills a process, writes a file, or
97
+ edits config.
98
+
99
+ ```bash
100
+ python3 skills/record-api-traffic/scripts/preflight.py <repo>
101
+ ```
102
+
103
+ `--json` for machine-readable output. Exit code 0 means clear, 1 means blocked, 2 means the path
104
+ is not an app repo.
105
+
106
+ It reports every check at once rather than dying on the first failure, which matters
107
+ because blockers cluster — an abandoned session typically leaves a proxy on 7100 *and* a dev
108
+ server on 4100, and discovering those one launch-attempt at a time is slow and irritating.
109
+
110
+ ## Step 2 — Clear the blockers
111
+
112
+ Present what preflight found in plain terms, then work through it. Two rules shape everything
113
+ here.
114
+
115
+ **Never stop a process you have not identified.** Preflight classifies each port holder as
116
+ `[ours]` (a leftover from this recorder), `[app]` (the project's own dev server), or
117
+ `[foreign]` (something else entirely), and prints the working directory and command line for
118
+ foreign ones. That distinction is the whole point. A `mitmdump` on 7100 could be this tool's
119
+ own orphan, or it could be a different project's recording session that someone is actively
120
+ using — killing it destroys their work with no way to recover it.
121
+
122
+ **Killing is the user's call, not yours.** For `[ours]` and `[app]` you can offer directly and
123
+ act once they agree. For `[foreign]`, show them the command line and cwd and ask what it is
124
+ before proposing anything. If they are unsure, leave it alone and say so — an unstarted
125
+ recording costs a minute, a destroyed session costs an afternoon.
126
+
127
+ ### The blockers you will actually see
128
+
129
+ **Port held by a foreign process.** The common real case: a `mitmdump` from another repo's
130
+ recording setup, sometimes half-dead (proxy still listening, its backend long gone). Show the
131
+ cwd and argv, ask, and only then offer `kill <pid>`. Prefer plain `kill` over `kill -9` so the
132
+ process can shut down cleanly and flush whatever it was writing.
133
+
134
+ **Port held by the app's own dev server.** Someone ran `npm run dev` by hand. Safe to offer to
135
+ stop — but say what it is, because they may have work mid-flight in that browser tab.
136
+
137
+ Six ports are checked now, not four: `4100`/`4101`/`4102` and `7100`/`7101`/`7102`. Only the
138
+ two `x100` ports are ever forwarded or called from outside, so a holder on one of the other four
139
+ is almost always debris. Preflight now also reads each holder's environment for the marker the
140
+ recorder exports into every child, which is what lets it call a leftover dev server on `:4101`
141
+ `[ours]` rather than `[app]` — the frontend has to run with the repo as its working directory,
142
+ so cwd alone cannot tell the two apart.
143
+
144
+ **The dev script cannot be moved off 4100.** Preflight reads `Frontend/package.json` and says
145
+ how the port will be moved. A `next` script is fine. Anything else falls back to `PORT`, and if
146
+ that script pins its own port instead, it will fight the recorder for `:4100` and one of them
147
+ loses. Read what preflight prints rather than assuming; `--no-frontend` is the way out if the
148
+ script cannot be moved.
149
+
150
+ **Frontend env points somewhere else.** This is the one to care about most. If
151
+ `NEXT_PUBLIC_API_URL` does not contain `:7100`, the browser talks straight to the backend and
152
+ the API recorder sees nothing. Everything appears to work — processes start, the link opens, the
153
+ app functions — and `backend-inbound.log` comes out empty. A crash is honest; this failure mode
154
+ lies. Fix `Frontend/.env.local` before launching. That file is the user's to edit, not yours.
155
+
156
+ Note what this does *not* affect: `frontend-inbound.log` fills up regardless, because the
157
+ browser reaches the frontend through `:4100` either way. A run with pages in one file and
158
+ nothing in the other is this bug, not a quiet recorder.
159
+
160
+ **Dump scripts out of sync with the app.** See the next section — the one blocker that is new
161
+ and easy to under-react to.
162
+
163
+ **MongoDB unreachable.** The backend never passes its health check and the script gives up after
164
+ 90 seconds with no obvious cause. Preflight resolves this ahead of time. Note that Atlas
165
+ hostnames are SRV-only, so a plain A/AAAA lookup fails even when the cluster is perfectly
166
+ healthy — check `_mongodb._tcp.<host>` instead. Preflight already does this correctly; don't
167
+ "fix" it with `ping` or `getent hosts` and conclude the network is down.
168
+
169
+ **Missing mitmproxy CA.** `~/.mitmproxy/mitmproxy-ca-cert.pem` is written on mitmproxy's first
170
+ ever run. Without it the backend cannot trust the outbound proxy and every outbound call fails.
171
+ Generate it by starting `mitmdump` once, or launch with `--no-outbound`.
172
+
173
+ **Orphans or a leftover scratch directory.** Debris from a previous run that crashed hard. The
174
+ orphaned processes are this tool's own leftovers, so they are safe to offer to stop. A stale
175
+ `/tmp/record-api-traffic-*` is only a warning. If preflight mentions `Backend/.mitm-run`, that is
176
+ from an older version of this skill that wrote inside the repo; report it and let the user delete
177
+ it — do not delete it yourself.
178
+
179
+ One caveat, now that the frontend runs under the recorder: starting a dev server spawns
180
+ short-lived helpers that inherit the recorder's marker, and one can outlive teardown by a while
181
+ without holding any port. So an orphan report whose ports are all free, shortly after a session
182
+ ended, is worth re-running before acting on — it usually clears on its own. A report that
183
+ persists, or that comes with a busy port, is real.
184
+
185
+ ## Step 3 — Check the dump scripts still fit the app
186
+
187
+ `scripts/dump-quotation.mjs` and `scripts/dump-source-excel.mjs` read the app's Mongoose model
188
+ directly. They used to live in the app repo, where a schema change and the code reading it moved
189
+ in the same commit. They no longer do, and nothing keeps them honest but this check:
190
+
191
+ ```bash
192
+ node skills/record-api-traffic/scripts/check-schema.mjs --repo <repo>
193
+ ```
194
+
195
+ Preflight runs it for you and folds the result into its report; run it directly when you want the
196
+ per-field detail. It loads the real model and asserts every field the dumps touch — that
197
+ `sourceWorkbookBuffer` exists **and is still a Buffer**, that `db.js` still default-exports
198
+ `connectDB`, that `email.service.getAttachmentBuffer` still takes three arguments.
199
+
200
+ Take a `FAIL` here seriously, because drift is silent. A renamed field does not crash: the
201
+ document still loads, `quotation.sourceWorkbookBuffer` is merely `undefined`, and
202
+ `dump-source-excel` falls through to the Graph refetch or writes a zero-byte file. You get an
203
+ artifact that looks like an artifact and is worthless. Fix the script in the skill before
204
+ recording, rather than after discovering the dump is empty.
205
+
206
+ `warn` on a display-only field (`status`, `quotationItems`) means the summary line degrades but
207
+ the dumped bytes are still right — worth mentioning, not worth blocking on. Exit code 2 means
208
+ the check could not run at all, almost always because `Backend/node_modules` is missing; that is
209
+ the dependency blocker, not a schema problem.
210
+
211
+ ## Step 4 — Launch
212
+
213
+ The recorder is a long-lived foreground process that the user stops with Ctrl-C. Hand the
214
+ command over rather than running it yourself in the background, where they cannot reach it and
215
+ its output is buried:
216
+
217
+ ```
218
+ python3 skills/record-api-traffic/scripts/record-api-traffic.py <repo>
219
+ ```
220
+
221
+ Suggest they type `! python3 skills/record-api-traffic/scripts/record-api-traffic.py <repo>` in
222
+ the prompt so the output lands in the conversation.
223
+
224
+ Startup takes up to a couple of minutes — the backend waits on Mongo, and Next.js compiles the
225
+ first page on demand. It prints progress for each stage, then a banner with the link
226
+ (`http://localhost:4100`), the run directory and one path per active leg.
227
+
228
+ If it dies during startup it prints the failing child's last 40 lines. Read those rather than
229
+ guessing; the cause is nearly always in them.
230
+
231
+ Three flags, none of them the normal case:
232
+
233
+ - `--no-frontend` leaves the dev server on `:4100` unrecorded, exactly as it ran before the
234
+ frontend legs existed. Reach for it when the dev script cannot be moved off `:4100`, or when
235
+ fronting Next with a proxy is itself the suspect.
236
+ - `--no-outbound` drops **both** forward proxies, not just the backend's. It is the answer when
237
+ `~/.mitmproxy/mitmproxy-ca-cert.pem` is missing or Node is older than 24, since either
238
+ condition breaks both equally.
239
+ - `--frontend-all` keeps the static-asset traffic the frontend leg drops by default. Worth it
240
+ when the question is about asset loading or chunk sizes, and a bad idea otherwise — see the
241
+ quirk on noise below.
242
+
243
+ ## Step 5 — Prove it is recording
244
+
245
+ Do not report success because processes started. Confirm a call was actually captured, on both
246
+ browser-facing legs — they fail independently, so one row proves nothing about the other:
247
+
248
+ ```bash
249
+ run=$(ls -d skills/record-api-traffic-workspace/recordings/*/run-* | tail -1)
250
+ curl -s -o /dev/null http://127.0.0.1:7100/health
251
+ curl -s -o /dev/null http://127.0.0.1:4100/
252
+ tail -3 "$run/backend-inbound.log"
253
+ tail -3 "$run/frontend-inbound.log"
254
+ ```
255
+
256
+ A row should appear for `/health` in the first and for `/` in the second. If a file has only its
257
+ header after the user has clicked around the app:
258
+
259
+ - **`backend-inbound.log` empty, `frontend-inbound.log` filling.** The browser is reaching the
260
+ frontend but the frontend is not routing API calls through `:7100`. Re-check
261
+ `NEXT_PUBLIC_API_URL`.
262
+ - **Both empty.** The browser is not reaching `:4100` at all — a stale forwarded port, or a tab
263
+ still pointed at an old URL.
264
+ - **`frontend-outbound.log` empty.** Usually correct, not broken. It only fills when
265
+ the frontend fetches from Node — a server component, a route handler, or the dev server's own
266
+ telemetry. An app whose data fetching all happens in the browser leaves this leg nearly empty
267
+ by design.
268
+
269
+ This step exists because the expensive failure is a session that felt fine and produced nothing.
270
+ Thirty seconds here saves re-running the whole thing.
271
+
272
+ ## Step 6 — Read the output
273
+
274
+ Recordings live in the skill's workspace, not the repo, under a folder named for the repo they
275
+ came from, so two clones of the same project never mix. The workspace sits beside the skill
276
+ rather than inside it, because `skills/*-workspace/` is already gitignored — which is what keeps
277
+ the tokens in a recording out of this repo's history:
278
+
279
+ ```
280
+ skills/record-api-traffic-workspace/
281
+ iteration-N/ eval and benchmark output, not yours to touch
282
+ recordings/
283
+ <last-two-path-segments-of-the-repo>/
284
+ run-20260907-015953/
285
+ run.json repo, time, and which legs were on
286
+ backend-inbound.log into the backend, scannable table
287
+ backend-inbound.jsonl the same calls with full bodies
288
+ backend-outbound.log out of the backend (Graph, Datalab)
289
+ backend-outbound.jsonl
290
+ frontend-inbound.log into the frontend (browser)
291
+ frontend-inbound.jsonl
292
+ frontend-outbound.log out of the frontend (backend, external)
293
+ frontend-outbound.jsonl
294
+ artifacts/ written by the dump scripts in Step 7
295
+ ```
296
+
297
+ One run, one directory; nothing to correlate by timestamp across a flat listing.
298
+
299
+ The `.log` is one row per call, time first. The `.jsonl` holds the full bodies, one JSON object
300
+ per line, where line number equals the row's ID. The two forward-proxy legs put `host:port` in
301
+ the PATH column, since they span several hosts — and, on the frontend's, several loopback ports
302
+ that would otherwise all read as `127.0.0.1`.
303
+
304
+ `run.json` records which legs were active. A missing file means that leg was switched off, not
305
+ that it captured nothing; check there before concluding a recorder failed.
306
+
307
+ The banner prints `Run dir` at startup. When working with "the recording" later, resolve the
308
+ newest run for that repo rather than assuming, and check `run.json` if you are unsure which repo
309
+ a folder belongs to.
310
+
311
+ For formats, field meanings and jq recipes, read `references/reading-recordings.md`.
312
+
313
+ ## Step 7 — Stop, verify, dump
314
+
315
+ When the user signals they are finished with the app — "done", "stop recording", "that's it" —
316
+ that single signal means all three of the following. Carry them out in order without waiting to
317
+ be asked for each one. A run that stops without its dumps is an incomplete recording, and the
318
+ user will not know to ask for them.
319
+
320
+ ### 7a — Stop
321
+
322
+ Ctrl-C stops all six processes, removes the temp scratch directory, and prints a count of calls
323
+ recorded and failed for each leg.
324
+
325
+ If you launched it in the background rather than handing it over, Ctrl-C cannot reach it. Send
326
+ `kill -INT` to the `python3` process itself — not `kill -9`, which skips teardown and loses both
327
+ the summary and the scratch cleanup:
328
+
329
+ ```bash
330
+ pgrep -af '[r]ecord-api-traffic.py' | grep -v 'zsh -c'
331
+ kill -INT <pid of the "python3 .../record-api-traffic.py" process>
332
+ ```
333
+
334
+ Read that list and pick the right line rather than piping to `head -1`. It also contains the
335
+ launching shell (`/bin/zsh -c ... eval 'python3 ...'`) and the mitmdump children — eight lines of
336
+ them, since mitmproxy forks and each of the four recorders shows up twice. All match the pattern.
337
+ Signalling the shell does nothing — the recorder keeps running and the ports stay bound, which
338
+ looks exactly like a teardown that silently failed. Confirm with the port check in 7b rather than
339
+ assuming the signal landed.
340
+
341
+ ### 7b — Verify the teardown
342
+
343
+ The repo should be exactly as it was, apart from the dev server's gitignored `.next/`:
344
+
345
+ ```bash
346
+ git -C <repo> status --short # nothing from the recording
347
+ ls -d /tmp/record-api-traffic-* 2>/dev/null # gone
348
+ ```
349
+
350
+ Then confirm the ports are free. Do not go looking for processes by name:
351
+
352
+ ```bash
353
+ for p in 4100 4101 4102 7100 7101 7102; do
354
+ ss -ltnp | grep -q ":$p " && echo "$p BUSY" || echo "$p free"
355
+ done
356
+ ```
357
+
358
+ A `pgrep -af '[m]itmdump|[s]erver\.js|[n]ext dev'` sweep is deliberately broad and will catch
359
+ unrelated things — VS Code's `tsserver.js` matches `server.js`, and other repos run their own
360
+ mitmdump. The port check answers the actual question. If you do run pgrep, use bracketed patterns
361
+ like `[m]itmdump`; without them the pattern matches the shell running it, which reports a phantom
362
+ process and, with `pkill`, kills the shell itself. Report anything foreign rather than killing it.
363
+
364
+ ### 7c — Dump the input and the output
365
+
366
+ The recording is HTTP only, so the pipeline's real input and output are not in it. Two scripts
367
+ fill that gap, both taking the repo and a quotationId. Get the id from the recording rather than
368
+ asking:
369
+
370
+ ```bash
371
+ grep -oE '/api/quotations/[a-f0-9]{24}' <run-dir>/backend-inbound.log \
372
+ | grep -oE '[a-f0-9]{24}' | sort | uniq -c | sort -rn
373
+
374
+ node skills/record-api-traffic/scripts/dump-source-excel.mjs --repo <repo> <quotationId>
375
+ node skills/record-api-traffic/scripts/dump-quotation.mjs --repo <repo> <quotationId>
376
+ ```
377
+
378
+ Both default to `artifacts/` inside the newest run folder *for that repo*, so run them straight
379
+ after a session and they land beside the traffic they belong to. Pass a run dir as an optional
380
+ extra argument to target an older session. If several quotationIds appear in the log, dump each.
381
+
382
+ The Excel comes from `sourceWorkbookBuffer` on the document — the raw xlsx bytes are kept there
383
+ as the export template — and falls back to refetching from Graph when that field is null, which
384
+ is the case for CSV attachments and oversized documents. That fallback is rarely exercised;
385
+ treat a failure in it as untested code rather than a broken session.
386
+
387
+ Say plainly that these files contain live JWTs, the login password and Outlook tokens, recorded
388
+ raw by deliberate choice. The `skills/*-workspace/` gitignore rule keeps them out of this repo's
389
+ history, and nothing is written into the recorded app's repo at all. They
390
+ should not be pasted into issues or shared.
391
+
392
+ ## Known quirks
393
+
394
+ **No record is written when the backend is unreachable.** mitmproxy aborts at the connection
395
+ stage before a flow exists, so its error hook never fires. If the backend dies mid-session,
396
+ expect silence in the log and a loud message in the terminal — the launcher notices within a
397
+ second and shuts everything down. Absence of rows during an outage is expected, not a bug.
398
+
399
+ **No hot reload on the backend.** It runs under plain `node`, not nodemon, because nodemon
400
+ watches the working directory and that is now a temp directory. Backend edits need a restart of
401
+ the recorder. The frontend is unaffected: `next dev` still runs normally, and its HMR websocket
402
+ upgrades cleanly through the reverse proxy, so editing a component reloads the browser as usual.
403
+
404
+ **The frontend leg drops static assets by default.** One page load in `next dev` pulls dozens of
405
+ `/_next/static/` chunks; recorded in full they would bury the handful of rows that carry meaning
406
+ and add tens of megabytes of JavaScript to the JSONL. So `/_next/static/`, `/_next/image`,
407
+ `/_next/webpack-hmr`, `/__nextjs*` and `/favicon.ico` are skipped before the row counter moves,
408
+ which keeps row IDs equal to JSONL line numbers. Navigations, RSC payloads, server actions and
409
+ any route handler the frontend serves itself are all kept. `--frontend-all` records everything.
410
+
411
+ **A frontend server-side call to the backend appears twice.** Once on
412
+ `frontend-outbound` as it leaves Node, once on `backend-inbound` as it arrives. Same
413
+ call, both ends. Match them on path and timestamp; the outbound row is the earlier of the two.
414
+
415
+ **The frontend outbound leg picks up npm and Next's own chatter.** `next dev` checks the npm
416
+ registry for a newer version at startup, and that request is a real outbound call from the
417
+ frontend's Node process, so it is recorded like any other. A row for `registry.npmjs.org` at the
418
+ top of the file is expected, not a leak.
419
+
420
+ **Bodies are capped at 1 MB** and binary responses (images, PDFs, spreadsheets) are recorded as
421
+ a size summary rather than base64. The quotation PUT path can carry 16 MB of inline images, so
422
+ without this the file would be unusable. This applies to every leg: the multipart upload to
423
+ Datalab shows as a size, not the file. Use `dump-source-excel.mjs` for the bytes.
424
+
425
+ Note what the cap does *not* cover on the frontend leg. JavaScript and HTML are text, so a page
426
+ or a chunk under 1 MB is stored in full — which is exactly why the noise filter above exists,
427
+ and why `--frontend-all` should be a considered choice rather than a default.
428
+
429
+ **The dump scripts resolve everything from `--repo` by hand.** They import the app's modules by
430
+ absolute path and its `mongoose` through `createRequire` rooted at `Backend/`, because a bare
431
+ `import mongoose` inside the skill would look in the skill's own non-existent `node_modules`.
432
+ They also parse `Backend/.env` themselves rather than relying on `dotenv` finding it relative to
433
+ the working directory. If you add a script here that touches the app, use `scripts/lib/repo.mjs`
434
+ rather than reinventing that.
@@ -0,0 +1,224 @@
1
+ # Reading a recording
2
+
3
+ Every session writes its own folder in the skill's workspace,
4
+ `skills/record-api-traffic-workspace/recordings/<repo-slug>/run-<YYYYMMDD-HHMMSS>/`, containing:
5
+
6
+ - `run.json` — which repo this came from, when, and which legs were recorded
7
+ - `backend-inbound.log` / `.jsonl` — everything arriving at the backend. Mostly the browser,
8
+ but the frontend's own server-side calls land here too
9
+ - `backend-outbound.log` / `.jsonl` — what the backend calls out to: Microsoft Graph, Datalab
10
+ - `frontend-inbound.log` / `.jsonl` — everything arriving at the frontend: page navigations,
11
+ RSC payloads, server actions, and any route handler the frontend serves itself
12
+ - `frontend-outbound.log` / `.jsonl` — what the frontend calls out to, i.e. what its Node
13
+ process fetches rather than what the browser does: the backend, and anything external
14
+ - `artifacts/` — the source spreadsheet and stored quotation, written by the dump scripts
15
+
16
+ The names say which server is being watched and which way the traffic goes, so each server's
17
+ pair sits together in a listing. Note that "inbound" is deliberate rather than "from the
18
+ browser": `backend-inbound` catches whatever reaches `:7100`, whoever sent it.
19
+
20
+ Each pair is an aligned table for scanning plus one JSON object per line with the full bodies.
21
+ All four share a format, so every recipe below works on any of them — swap the filename.
22
+
23
+ The recipes below assume you are inside a run folder. To work on the most recent session:
24
+
25
+ ```bash
26
+ cd "$(ls -d skills/record-api-traffic-workspace/recordings/*/run-* | tail -1)"
27
+ ```
28
+
29
+ The slug is the last two segments of the repo path, so two clones of the same project keep
30
+ separate folders. When several repos have been recorded, pick the right slug rather than the
31
+ newest run overall, and check `run.json` if you are unsure.
32
+
33
+ The only formatting difference between legs: the two forward-proxy files
34
+ (`backend-outbound`, `frontend-outbound`) put `host:port` in the `PATH` column,
35
+ because they span several hosts — and on the frontend's, several loopback ports that would
36
+ otherwise all read as `127.0.0.1`.
37
+
38
+ All are flushed after every call, so `tail -f` on a `.log` works while a session is live.
39
+
40
+ ## Which file answers which question
41
+
42
+ | Question | File |
43
+ |---|---|
44
+ | What did the user's browser actually load? | `frontend-inbound` |
45
+ | Which API calls did the page make? | `backend-inbound` |
46
+ | Did a server component or route handler fetch this, or the browser? | `frontend-outbound` — if it is there, it came from Node |
47
+ | What did the backend call out to? | `backend-outbound` |
48
+
49
+ A server-side call from the frontend to the backend lands in **two** files: once leaving Node,
50
+ once arriving at the backend. That is not double-counting — it is both ends of one call, and
51
+ comparing them is how you see what the proxy chain did to it in between.
52
+
53
+ The frontend leg drops `/_next/static/`, `/_next/image`, `/_next/webpack-hmr`, `/__nextjs*` and
54
+ `/favicon.ico` unless the run used `--frontend-all`; `run.json` records which. The rows are
55
+ dropped before the counter moves, so IDs still equal line numbers.
56
+
57
+ ## The table
58
+
59
+ ```
60
+ TIME ID MS METHOD STATUS PATH REQ RESP
61
+ 14:22:03.184 0001 1243 POST 200 /api/auth/login 86 B 412 B
62
+ 14:22:06.771 0003 4021 GET 200 /api/email/messages?top=25 - 184 KB
63
+ 14:22:31.004 0004 12908 POST 502 /api/quotation/extract 2.4 MB 311 B
64
+ ```
65
+
66
+ `ID` is the line number of the matching record in the `.jsonl`, so row `0003` is line 3. To
67
+ read a row's full bodies, jump straight to that line:
68
+
69
+ ```bash
70
+ sed -n '3p' backend-inbound.jsonl | jq .
71
+ ```
72
+
73
+ Two things to know about the columns. `TIME` is when the response *completed*, not when the
74
+ request started — rows are appended on completion, which is what keeps ID equal to line number
75
+ even when requests overlap. The start time is in the JSONL as `started_at`. And paths longer
76
+ than 44 characters are ellipsized with `…` to hold the table's alignment; the full path and
77
+ parsed query string are always intact in the JSONL.
78
+
79
+ `STATUS` shows `ERR` for a flow that failed without a response.
80
+
81
+ ## The JSONL record
82
+
83
+ ```json
84
+ {
85
+ "id": "0002",
86
+ "ts": "2026-09-03T18:45:27.323364+00:00",
87
+ "started_at": "2026-09-03T18:45:27.321536+00:00",
88
+ "duration_ms": 1,
89
+ "method": "POST",
90
+ "path": "/api/auth/login",
91
+ "host": "127.0.0.1",
92
+ "url": "http://127.0.0.1:7100/api/auth/login",
93
+ "query": {},
94
+ "client_ip": "127.0.0.1",
95
+ "request": {
96
+ "headers": { "Authorization": "Bearer ...", "Content-Type": "application/json" },
97
+ "body": { "email": "ops@example.com", "password": "hunter2" },
98
+ "body_encoding": "json",
99
+ "body_size": 48,
100
+ "truncated": false
101
+ },
102
+ "response": {
103
+ "status": 200,
104
+ "headers": { "Content-Type": "application/json" },
105
+ "body": { "success": true, "token": "eyJ..." },
106
+ "body_encoding": "json",
107
+ "body_size": 42,
108
+ "truncated": false
109
+ }
110
+ }
111
+ ```
112
+
113
+ `path` excludes the query string; `query` holds it parsed. `host` is the request's target host,
114
+ which is the only way to tell Graph from Datalab in an outbound recording. `response` is absent
115
+ entirely on a failed flow, and an `error` field appears instead.
116
+
117
+ Which external services the backend called, and how often:
118
+
119
+ ```bash
120
+ jq -r '.host' backend-outbound.jsonl | sort | uniq -c | sort -rn
121
+ ```
122
+
123
+ On the frontend outbound leg `host` is nearly always `127.0.0.1`, so key off the origin instead —
124
+ the port is what separates the backend from everything else:
125
+
126
+ ```bash
127
+ jq -r '.url | split("/")[0:3] | join("/")' frontend-outbound.jsonl \
128
+ | sort | uniq -c | sort -rn
129
+ ```
130
+
131
+ ```
132
+ 1 https://registry.npmjs.org
133
+ 1 http://127.0.0.1:7100
134
+ ```
135
+
136
+ Which calls on a page came from the server rather than the browser — the paths in this list are
137
+ the ones you will also find in `backend-inbound.jsonl`, recorded a few milliseconds later at the
138
+ other end:
139
+
140
+ ```bash
141
+ jq -r 'select(.url | test(":7100")) | "\(.method) \(.path)"' \
142
+ frontend-outbound.jsonl | sort -u
143
+ ```
144
+
145
+ ### body_encoding
146
+
147
+ Tells you how to interpret `body`, which matters because its type varies:
148
+
149
+ - `json` — parsed, so jq can reach inside it directly
150
+ - `text` — a string; either genuinely non-JSON, or JSON that was truncated before parsing
151
+ - `binary` — a summary object `{"binary": true, "content_type": "image/png", "size": 5004}`.
152
+ Images, PDFs and spreadsheets are never base64'd into the file; the S3 image proxy alone
153
+ would otherwise add megabytes per thumbnail.
154
+ - `null` — no body at all (`body_size` 0)
155
+
156
+ `body_size` is always the true size on the wire. When `truncated` is `true` the body was larger
157
+ than the 1 MB cap and `body` holds the first 1 MB as a string — so `body_size` and the actual
158
+ length of `body` disagree, by design.
159
+
160
+ ## jq recipes
161
+
162
+ Failures, with the reason:
163
+
164
+ ```bash
165
+ jq -c 'select(.response.status >= 400 or has("error"))
166
+ | {id, method, path, status: .response.status, msg: .response.body.message}' \
167
+ backend-inbound.jsonl
168
+ ```
169
+
170
+ Slowest calls first. Put `duration_ms` first and let `sort -rn` key off it — piping whole JSON
171
+ objects into `sort` does not work, because the field separators inside the object throw the
172
+ column numbering off and you get something that looks sorted but is not:
173
+
174
+ ```bash
175
+ jq -r '[.duration_ms, .id, .method, .path] | @tsv' backend-inbound.jsonl \
176
+ | sort -rn | head -20
177
+ ```
178
+
179
+ Which endpoints were exercised, and how often:
180
+
181
+ ```bash
182
+ jq -r '.method + " " + .path' backend-inbound.jsonl | sort | uniq -c | sort -rn
183
+ ```
184
+
185
+ Everything one endpoint did:
186
+
187
+ ```bash
188
+ jq -c 'select(.path | test("/api/quotation")) | {id, method, status: .response.status}' \
189
+ backend-inbound.jsonl
190
+ ```
191
+
192
+ Request and response bodies for one call:
193
+
194
+ ```bash
195
+ jq 'select(.id == "0004") | {request: .request.body, response: .response.body}' \
196
+ backend-inbound.jsonl
197
+ ```
198
+
199
+ Payloads big enough to have been truncated:
200
+
201
+ ```bash
202
+ jq -c 'select(.request.truncated or .response.truncated) | {id, path, req: .request.body_size, resp: .response.body_size}' \
203
+ backend-inbound.jsonl
204
+ ```
205
+
206
+ An API inventory to compare against documentation — unique endpoints with the status codes each
207
+ returned:
208
+
209
+ ```bash
210
+ jq -r '"\(.method) \(.path) -> \(.response.status)"' backend-inbound.jsonl \
211
+ | sort -u
212
+ ```
213
+
214
+ If `jq` is unavailable, the same work is straightforward in a short Python loop over
215
+ `json.loads` per line.
216
+
217
+ ## Secrets
218
+
219
+ Recordings are raw by deliberate choice — nothing is masked. `request.headers.Authorization`
220
+ carries live bearer tokens, `/api/auth/login` request bodies carry the real password, and the
221
+ Outlook flow carries Microsoft access tokens. The `skills/*-workspace/` gitignore rule keeps the
222
+ whole workspace out of this repo's history, and nothing is written into the recorded app's repo
223
+ at all. Treat a
224
+ recording like a credential: don't paste one into an issue, a PR, or a chat with anyone.