← back to Cli Printing Press
feat(cli): browser auth, composed cookies, smart output, and sniff robustness (#115)
6d2d059d35cc1d1d4835d46be2e8e7ff8550a5f8 · 2026-04-03 11:43:37 -0700 · Trevin Chow
* feat(cli): add Chrome cookie auth for sniff-discovered APIs
When the sniff discovers cookie-authenticated endpoints, printed CLIs now
support `auth login --chrome` — reading cookies from the user's Chrome
session at login time. Includes auto-detection of Chrome profiles,
multi-tool support (pycookiecheat, cookies CLI, cookie-scoop-cli), and
sniff-time validation to confirm cookie replay works before promising
the feature.
Pipeline: sniff detects cookie auth → specgen populates CookieDomain →
generator selects auth_browser.go.tmpl → printed CLI gets --chrome flag,
doctor checks, and README docs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): auth sniff flow, GraphQL BFF detection, apostrophe naming
Three fixes from the Domino's retro:
1. cleanSpecName strips apostrophes before hyphenation so brand names
like "Domino's" produce "dominos" not "domino-s". Also handles
Unicode right single quotation mark (U+2019).
2. sniff-capture.md Step 2a.1.5: when AUTH_SESSION_AVAILABLE=true,
the sniff visits account/profile/history/rewards pages after the
primary flow, classifies endpoints as auth-required vs public, and
triggers cookie auth validation (Step 2d) to propagate Auth.Type
into the spec.
3. sniff-capture.md Step 2a.2.5: when >50% of XHR POSTs go to the
same URL (GraphQL BFF pattern), extracts operationName from request
bodies via agent-browser network requests or browser-use fetch
interceptor.
Also relaxes contracts_test to allow local-build PATH references in
setup contracts (needed for the local-build preference added in
bb33d2d).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(skills): always use browser-use for capture, agent-browser for session only
The sniff-capture.md recommended agent-browser auto-connect for capture
when Chrome was running. But auto-connect mode can't access the DOM via
eval, snapshot, or click — it can only navigate and record HAR. This led
to a failed sniff attempt on Domino's where we couldn't interact with
pages or extract GraphQL operations.
Fix: agent-browser is now only used for session transfer (grabbing
cookies from running Chrome). The actual capture always uses browser-use
which has full DOM access. Flow: grab cookies → quit Chrome → browser-use
--profile "Default" for capture.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(skills): sniff capture robustness — cardinal rules, session expiry, SPA nav
Three fixes from the Pagliacci retro to prevent recurring sniff failures:
1. Cardinal rules at top of sniff-capture.md: always use browser-use for
capture (not curl/agent-browser), never skip auth on session expiry,
use click navigation after installing interceptors.
2. Session expiry detection: after loading a Chrome profile, check if the
site shows login indicators. If session expired, offer headed login
instead of silently proceeding without auth.
3. SPA navigation rule: browser-use open resets the JS context, destroying
fetch/XHR interceptors. After installing interceptors, use click-based
SPA navigation to preserve them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(skills): add auth header discovery and JS bundle extraction to sniff
Two new sniff capture steps from the Pagliacci retro WU-2:
Step 2a.1.5 item 5: Auth header discovery via XHR interception. Many
SPAs construct Authorization headers from cookies/localStorage rather
than sending cookies directly. The new step installs an XHR header
interceptor, triggers SPA navigation, and captures the actual auth
scheme (e.g., "PagliacciAuth {id}|{token}", "Bearer {jwt}"). Uses
the discovered header for auth validation instead of cookie replay.
Step 2a.2.7: JS bundle endpoint extraction. After interactive sniff,
extracts API paths from the SPA's compiled JS bundle as a supplementary
discovery technique. Finds endpoints no user flow visits (migration,
admin, rarely-used features). Marked as supplementary — the interactive
sniff remains primary for response shapes and auth patterns.
Also extends the discovery report template with auth scheme details
and bundle extraction results.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cli): composed cookie auth for sniff-discovered APIs
When the sniff discovers an API using a custom Authorization header
composed from browser cookie values (e.g., PagliacciAuth {customerId}|
{authToken}), the generator now emits a CLI with auth login --chrome
that reads the specific named cookies, composes the header, and saves
it to config.
Pipeline: sniff traces header values → cookie names → format string →
spec carries Auth.Type=composed + Auth.Format + Auth.Cookies →
generator extends auth_browser.go.tmpl with composed branch →
printed CLI reads named cookies and fills format template.
Changes:
- AuthConfig.Cookies []string field for named cookie list
- Generator routes composed → auth_browser.go.tmpl (shared template)
- auth_browser.go.tmpl: composed branch picks named cookies, fills
format string with {cookieName} placeholders, saves composed header
- config.go.tmpl: composed branch returns pre-composed AccessToken
- doctor.go.tmpl + readme.md.tmpl: composed treated same as cookie
for tool checks and Quick Start docs
- sniff-capture.md: auth header discovery now traces values to cookies,
constructs format string, writes composed auth config into spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): WAL-aware cookie probing and Windows path escaping in auth template
Two fixes from Codex review of the composed cookie auth feature:
1. countCookiesForDomain now copies Cookies-wal and Cookies-shm
alongside the main DB. When Chrome is running, recent cookie
writes are in the WAL — without it, profile discovery misses
active sessions and auth login --chrome reports no cookies.
2. extractViaPycookiecheat uses filepath.ToSlash before embedding
the cookie DB path into the Python command string. On Windows,
backslashes in paths like C:\Users\...\Profile 1\Cookies were
interpreted as Python escape sequences (\U, \t), breaking
pycookiecheat for non-default profiles.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cli): live browser fallback for session cookies in composed auth
When auth login --chrome finds domain cookies but not the required
named cookies (session cookies not persisted to disk), the CLI now
falls back to live browser extraction:
1. agent-browser --auto-connect: attaches to running Chrome, evals
document.cookie
2. browser-use --connect: same approach via browser-use
3. Raw CDP: connects to localhost:9222 debug port (tab discovery
only — WebSocket eval not yet implemented)
Also adds x-auth-type/x-auth-format/x-auth-cookies/x-auth-cookie-domain
OpenAPI extensions to the parser so composed auth can be expressed in
OpenAPI specs (used by the Pagliacci spec).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cli): reclassify path param modifiers as flags with defaults
Path params that are modifiers (pagination, enums, dates) are now
rendered as --flags with sensible defaults instead of required positional
args. This improves UX across all printed CLIs:
Before: order-list 1 10 (which arg is page vs size?)
After: order-list --page 2 (defaults: page=1, page-size=10)
Classification heuristic (first match wins):
1. Has enum → flag, defaults to first value
2. Has spec-declared default → flag with that default
3. Known pagination name (page, pageSize, limit, offset...) → flag
4. Date/time format or name → flag (no default)
5. Everything else → stays positional (entity identifiers)
New PathParam field on spec.Param distinguishes "path param rendered as
flag" from "query param" so the template substitutes the value into the
URL path (replacePathParam) instead of appending it as a query string.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): flagName converts camelCase to kebab-case
flagName("pageSize") now produces "page-size" instead of "pagesize".
Inserts hyphens at camelCase boundaries (lower→upper transitions) and
handles acronyms ("storeID" → "store-id"). This affects all printed
CLIs — any param named in camelCase gets a properly hyphenated flag.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): reclassified path params are optional when they have defaults
Path params reclassified as flags (pagination, enums, dates) were still
marked Required=true from the original path param processing. This made
them mandatory even though they have sensible defaults. Now
reclassification clears Required so users can just run `order-list`
instead of `order-list --page 1 --page-size 10`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): date path params stay required — no sensible default for URL
Path params reclassified as flags but without defaults (dates, years)
must stay Required=true because the URL path needs a value in that
segment. Only params with actual defaults (pagination, enums) become
optional. Without this, /TimeWindows/490/PICK/ gets a trailing empty
segment that 404s.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cli): smart auto-table with priority columns and card layout
The auto-table now prioritizes high-gravity fields (ID, dates, status,
price, summary) over metadata (booleans, building IDs) using tiered
case-insensitive substring matching. Booleans are demoted to last.
For complex responses (>8 fields or nested arrays), switches to a
card/sectional layout that shows one labeled block per item instead of
a cramped table. Arrays like Summary are flattened to comma-separated
strings.
Before: BUILDING BUILDINGNAME CLONEABLE COMMENTS DELTYPE
After: ID 40976858 — 2026-03-22T18:02:26
StatusCode: Completed
Summary: 1 Pizza, 1 Pagliaccio
Price: 5.98
...
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(cli): add machine vs printed CLI guidance to AGENTS.md
Clarifies the distinction between machine changes (generator, templates,
skills — generalized, affect all future CLIs) and printed CLI changes
(specific to one API). Default to machine changes; only fix printed CLIs
directly when the issue is genuinely API-specific.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): case-insensitive --select matching with kebab-case support
--select now matches field names case-insensitively and converts
camelCase/PascalCase API field names to kebab-case for comparison.
"--select id,order-date,summary,price" now matches API fields
ID, OrderDate, Summary, Price.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): prefer browser-use over agent-browser for live cookie extraction
agent-browser --auto-connect returns empty string from eval in
auto-connect mode — it can navigate but can't access the page's cookie
context. browser-use --connect has full DOM access and successfully
reads document.cookie. Swap the fallback order so browser-use is tried
first.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): validate composed auth before saving — catch expired sessions
After composing the auth header from cookies, makes a lightweight test
request to the API before saving to config. If the API returns 401/403,
reports "session expired" and directs the user to log in again. Prevents
silently saving stale credentials that fail on every subsequent command.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(cli): mark composed auth plan complete, add auto-table retro finding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): three Codex review fixes for composed cookie auth
1. specgen: add composed case to detectCapturedAuth so sniff-produced
captures with Type=composed propagate through to the spec. Without
this, composed auth was silently dropped to auth.type=none.
2. auth_browser.go.tmpl: auto-select Chrome profile with most cookies
when non-interactive (piped stdout / --no-input). Only prompts for
profile selection in a real terminal. Prevents hanging in CI/agents.
3. auth_browser.go.tmpl: replace hardcoded /Version endpoint in
validateComposedAuth with a HEAD request to the API base URL.
/Version was Pagliacci-specific; the base URL is generic and still
catches 401/403 for expired sessions.
Also adds Format field to AuthCapture struct for composed auth support.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
M AGENTS.mdM docs/plans/2026-04-03-002-feat-composed-cookie-auth-plan.mdM docs/retros/2026-04-03-pagliacci-retro.mdM internal/generator/templates/auth_browser.go.tmplM internal/generator/templates/helpers.go.tmplM internal/websniff/specgen.goM internal/websniff/types.go
Diff
commit 6d2d059d35cc1d1d4835d46be2e8e7ff8550a5f8
Author: Trevin Chow <trevin@trevinchow.com>
Date: Fri Apr 3 11:43:37 2026 -0700
feat(cli): browser auth, composed cookies, smart output, and sniff robustness (#115)
* feat(cli): add Chrome cookie auth for sniff-discovered APIs
When the sniff discovers cookie-authenticated endpoints, printed CLIs now
support `auth login --chrome` — reading cookies from the user's Chrome
session at login time. Includes auto-detection of Chrome profiles,
multi-tool support (pycookiecheat, cookies CLI, cookie-scoop-cli), and
sniff-time validation to confirm cookie replay works before promising
the feature.
Pipeline: sniff detects cookie auth → specgen populates CookieDomain →
generator selects auth_browser.go.tmpl → printed CLI gets --chrome flag,
doctor checks, and README docs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): auth sniff flow, GraphQL BFF detection, apostrophe naming
Three fixes from the Domino's retro:
1. cleanSpecName strips apostrophes before hyphenation so brand names
like "Domino's" produce "dominos" not "domino-s". Also handles
Unicode right single quotation mark (U+2019).
2. sniff-capture.md Step 2a.1.5: when AUTH_SESSION_AVAILABLE=true,
the sniff visits account/profile/history/rewards pages after the
primary flow, classifies endpoints as auth-required vs public, and
triggers cookie auth validation (Step 2d) to propagate Auth.Type
into the spec.
3. sniff-capture.md Step 2a.2.5: when >50% of XHR POSTs go to the
same URL (GraphQL BFF pattern), extracts operationName from request
bodies via agent-browser network requests or browser-use fetch
interceptor.
Also relaxes contracts_test to allow local-build PATH references in
setup contracts (needed for the local-build preference added in
bb33d2d).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(skills): always use browser-use for capture, agent-browser for session only
The sniff-capture.md recommended agent-browser auto-connect for capture
when Chrome was running. But auto-connect mode can't access the DOM via
eval, snapshot, or click — it can only navigate and record HAR. This led
to a failed sniff attempt on Domino's where we couldn't interact with
pages or extract GraphQL operations.
Fix: agent-browser is now only used for session transfer (grabbing
cookies from running Chrome). The actual capture always uses browser-use
which has full DOM access. Flow: grab cookies → quit Chrome → browser-use
--profile "Default" for capture.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(skills): sniff capture robustness — cardinal rules, session expiry, SPA nav
Three fixes from the Pagliacci retro to prevent recurring sniff failures:
1. Cardinal rules at top of sniff-capture.md: always use browser-use for
capture (not curl/agent-browser), never skip auth on session expiry,
use click navigation after installing interceptors.
2. Session expiry detection: after loading a Chrome profile, check if the
site shows login indicators. If session expired, offer headed login
instead of silently proceeding without auth.
3. SPA navigation rule: browser-use open resets the JS context, destroying
fetch/XHR interceptors. After installing interceptors, use click-based
SPA navigation to preserve them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(skills): add auth header discovery and JS bundle extraction to sniff
Two new sniff capture steps from the Pagliacci retro WU-2:
Step 2a.1.5 item 5: Auth header discovery via XHR interception. Many
SPAs construct Authorization headers from cookies/localStorage rather
than sending cookies directly. The new step installs an XHR header
interceptor, triggers SPA navigation, and captures the actual auth
scheme (e.g., "PagliacciAuth {id}|{token}", "Bearer {jwt}"). Uses
the discovered header for auth validation instead of cookie replay.
Step 2a.2.7: JS bundle endpoint extraction. After interactive sniff,
extracts API paths from the SPA's compiled JS bundle as a supplementary
discovery technique. Finds endpoints no user flow visits (migration,
admin, rarely-used features). Marked as supplementary — the interactive
sniff remains primary for response shapes and auth patterns.
Also extends the discovery report template with auth scheme details
and bundle extraction results.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cli): composed cookie auth for sniff-discovered APIs
When the sniff discovers an API using a custom Authorization header
composed from browser cookie values (e.g., PagliacciAuth {customerId}|
{authToken}), the generator now emits a CLI with auth login --chrome
that reads the specific named cookies, composes the header, and saves
it to config.
Pipeline: sniff traces header values → cookie names → format string →
spec carries Auth.Type=composed + Auth.Format + Auth.Cookies →
generator extends auth_browser.go.tmpl with composed branch →
printed CLI reads named cookies and fills format template.
Changes:
- AuthConfig.Cookies []string field for named cookie list
- Generator routes composed → auth_browser.go.tmpl (shared template)
- auth_browser.go.tmpl: composed branch picks named cookies, fills
format string with {cookieName} placeholders, saves composed header
- config.go.tmpl: composed branch returns pre-composed AccessToken
- doctor.go.tmpl + readme.md.tmpl: composed treated same as cookie
for tool checks and Quick Start docs
- sniff-capture.md: auth header discovery now traces values to cookies,
constructs format string, writes composed auth config into spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): WAL-aware cookie probing and Windows path escaping in auth template
Two fixes from Codex review of the composed cookie auth feature:
1. countCookiesForDomain now copies Cookies-wal and Cookies-shm
alongside the main DB. When Chrome is running, recent cookie
writes are in the WAL — without it, profile discovery misses
active sessions and auth login --chrome reports no cookies.
2. extractViaPycookiecheat uses filepath.ToSlash before embedding
the cookie DB path into the Python command string. On Windows,
backslashes in paths like C:\Users\...\Profile 1\Cookies were
interpreted as Python escape sequences (\U, \t), breaking
pycookiecheat for non-default profiles.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cli): live browser fallback for session cookies in composed auth
When auth login --chrome finds domain cookies but not the required
named cookies (session cookies not persisted to disk), the CLI now
falls back to live browser extraction:
1. agent-browser --auto-connect: attaches to running Chrome, evals
document.cookie
2. browser-use --connect: same approach via browser-use
3. Raw CDP: connects to localhost:9222 debug port (tab discovery
only — WebSocket eval not yet implemented)
Also adds x-auth-type/x-auth-format/x-auth-cookies/x-auth-cookie-domain
OpenAPI extensions to the parser so composed auth can be expressed in
OpenAPI specs (used by the Pagliacci spec).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cli): reclassify path param modifiers as flags with defaults
Path params that are modifiers (pagination, enums, dates) are now
rendered as --flags with sensible defaults instead of required positional
args. This improves UX across all printed CLIs:
Before: order-list 1 10 (which arg is page vs size?)
After: order-list --page 2 (defaults: page=1, page-size=10)
Classification heuristic (first match wins):
1. Has enum → flag, defaults to first value
2. Has spec-declared default → flag with that default
3. Known pagination name (page, pageSize, limit, offset...) → flag
4. Date/time format or name → flag (no default)
5. Everything else → stays positional (entity identifiers)
New PathParam field on spec.Param distinguishes "path param rendered as
flag" from "query param" so the template substitutes the value into the
URL path (replacePathParam) instead of appending it as a query string.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): flagName converts camelCase to kebab-case
flagName("pageSize") now produces "page-size" instead of "pagesize".
Inserts hyphens at camelCase boundaries (lower→upper transitions) and
handles acronyms ("storeID" → "store-id"). This affects all printed
CLIs — any param named in camelCase gets a properly hyphenated flag.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): reclassified path params are optional when they have defaults
Path params reclassified as flags (pagination, enums, dates) were still
marked Required=true from the original path param processing. This made
them mandatory even though they have sensible defaults. Now
reclassification clears Required so users can just run `order-list`
instead of `order-list --page 1 --page-size 10`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): date path params stay required — no sensible default for URL
Path params reclassified as flags but without defaults (dates, years)
must stay Required=true because the URL path needs a value in that
segment. Only params with actual defaults (pagination, enums) become
optional. Without this, /TimeWindows/490/PICK/ gets a trailing empty
segment that 404s.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(cli): smart auto-table with priority columns and card layout
The auto-table now prioritizes high-gravity fields (ID, dates, status,
price, summary) over metadata (booleans, building IDs) using tiered
case-insensitive substring matching. Booleans are demoted to last.
For complex responses (>8 fields or nested arrays), switches to a
card/sectional layout that shows one labeled block per item instead of
a cramped table. Arrays like Summary are flattened to comma-separated
strings.
Before: BUILDING BUILDINGNAME CLONEABLE COMMENTS DELTYPE
After: ID 40976858 — 2026-03-22T18:02:26
StatusCode: Completed
Summary: 1 Pizza, 1 Pagliaccio
Price: 5.98
...
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(cli): add machine vs printed CLI guidance to AGENTS.md
Clarifies the distinction between machine changes (generator, templates,
skills — generalized, affect all future CLIs) and printed CLI changes
(specific to one API). Default to machine changes; only fix printed CLIs
directly when the issue is genuinely API-specific.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): case-insensitive --select matching with kebab-case support
--select now matches field names case-insensitively and converts
camelCase/PascalCase API field names to kebab-case for comparison.
"--select id,order-date,summary,price" now matches API fields
ID, OrderDate, Summary, Price.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): prefer browser-use over agent-browser for live cookie extraction
agent-browser --auto-connect returns empty string from eval in
auto-connect mode — it can navigate but can't access the page's cookie
context. browser-use --connect has full DOM access and successfully
reads document.cookie. Swap the fallback order so browser-use is tried
first.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): validate composed auth before saving — catch expired sessions
After composing the auth header from cookies, makes a lightweight test
request to the API before saving to config. If the API returns 401/403,
reports "session expired" and directs the user to log in again. Prevents
silently saving stale credentials that fail on every subsequent command.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(cli): mark composed auth plan complete, add auto-table retro finding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): three Codex review fixes for composed cookie auth
1. specgen: add composed case to detectCapturedAuth so sniff-produced
captures with Type=composed propagate through to the spec. Without
this, composed auth was silently dropped to auth.type=none.
2. auth_browser.go.tmpl: auto-select Chrome profile with most cookies
when non-interactive (piped stdout / --no-input). Only prompts for
profile selection in a real terminal. Prevents hanging in CI/agents.
3. auth_browser.go.tmpl: replace hardcoded /Version endpoint in
validateComposedAuth with a HEAD request to the API base URL.
/Version was Pagliacci-specific; the base URL is generic and still
catches 401/403 for expired sessions.
Also adds Format field to AuthCapture struct for composed auth support.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
AGENTS.md | 13 +
...026-04-03-002-feat-composed-cookie-auth-plan.md | 2 +-
docs/retros/2026-04-03-pagliacci-retro.md | 11 +
internal/generator/templates/auth_browser.go.tmpl | 67 ++++-
internal/generator/templates/helpers.go.tmpl | 277 ++++++++++++++++++---
internal/websniff/specgen.go | 12 +
internal/websniff/types.go | 3 +-
7 files changed, 342 insertions(+), 43 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 305b0988..7de64dde 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,5 +1,18 @@
# CLI Printing Press - Development Conventions
+## Machine vs Printed CLI — What Are You Optimizing?
+
+This repo contains **the machine** (generator, templates, binary, skills) that produces **printed CLIs**. When fixing bugs or adding features, always ask: is this a machine change or a printed CLI change?
+
+- **Machine changes** (generator, templates, parser, skills) affect every future CLI. They must be generalized — think about how the fix applies across different APIs, spec formats, and auth patterns, not just the CLI you're looking at right now.
+- **Printed CLI changes** (code in `~/printing-press/library/<cli>/`) fix one specific CLI. These are fine for targeted improvements but don't compound.
+
+**Default to machine changes.** If a problem shows up in a printed CLI, the first question is: should the generator have gotten this right? If yes, fix the machine so every future CLI benefits. Only fix the printed CLI directly when the issue is genuinely specific to that one API.
+
+**Never change the machine for one CLI's edge case** unless explicitly told to. If a fix only helps Pagliacci but would be wrong for Stripe, it doesn't belong in the generator. Add a conditional with a clear guard, or leave it as a printed-CLI-level fix.
+
+**When iterating on a printed CLI to discover issues**, note which problems are systemic (retro findings) vs specific. The retro → plan → implement loop exists to feed discoveries from individual CLIs back into the machine.
+
## Build, Test & Lint
```bash
diff --git a/docs/plans/2026-04-03-002-feat-composed-cookie-auth-plan.md b/docs/plans/2026-04-03-002-feat-composed-cookie-auth-plan.md
index da00fc8d..074a1be4 100644
--- a/docs/plans/2026-04-03-002-feat-composed-cookie-auth-plan.md
+++ b/docs/plans/2026-04-03-002-feat-composed-cookie-auth-plan.md
@@ -1,7 +1,7 @@
---
title: "feat: Composed cookie auth for sniff-discovered APIs"
type: feat
-status: active
+status: completed
date: 2026-04-03
origin: docs/brainstorms/2026-04-03-composed-cookie-auth-requirements.md
---
diff --git a/docs/retros/2026-04-03-pagliacci-retro.md b/docs/retros/2026-04-03-pagliacci-retro.md
index ecda3581..02b057f0 100644
--- a/docs/retros/2026-04-03-pagliacci-retro.md
+++ b/docs/retros/2026-04-03-pagliacci-retro.md
@@ -209,3 +209,14 @@ No scorer bugs identified in this run.
- **Headed login flow.** When the session was expired, the headed login workaround (open visible browser → user logs in → continue sniff) worked perfectly. The auth cookies were captured and the authenticated surface was fully discovered.
- **Auth header interception via SPA navigation.** Once Claude used click-based navigation instead of `browser-use open`, the XHR header interceptor captured the custom PagliacciAuth scheme correctly.
- **98% verify pass rate on first generation.** The spec was accurate enough that 41/42 commands passed all 3 verify checks without any fix loops.
+
+### 6. Auto-table shows wrong columns for complex responses (template gap)
+
+- **What happened:** `order-list list-orders` displays a table with `BUILDING, BUILDINGNAME, CLONEABLE, COMMENTS, DELTYPE, DELIVERY` — the least useful fields. The response has 22 fields including `OrderDate`, `Summary`, `Price`, `StatusCode`, but the auto-table picks columns alphabetically and stops at terminal width. The useful fields are past the cutoff.
+- **Root cause:** `printAutoTable` in `helpers.go.tmpl` has no concept of field importance. It renders all scalar fields alphabetically. For responses with many fields (>8) or nested arrays (`Summary: ["1 Pizza", "1 Pagliaccio"]`), the table format itself is wrong — a sectional/card layout per item would be better.
+- **Cross-API check:** Every API with complex list responses (orders, invoices, projects with metadata). Simple resources (stores: ID, Name, Address) work fine in tables.
+- **Frequency:** Most APIs — any list endpoint with >8 fields per item.
+- **Fallback if machine doesn't fix it:** User uses `--json | jq` or `--select id,order-date,status,price`. Works but defeats the point of human-friendly output.
+- **Worth a machine fix?** Yes. The heuristic: table for ≤8 scalar fields with no nested arrays; sectional/card format for complex responses with many fields or nested data.
+- **Durable fix:** Add format detection to `printAutoTable` in the helpers template. When the response has >8 fields or contains nested arrays/objects, switch to a sectional layout that shows each item as an indented block with labeled fields, prioritizing high-gravity fields (ID, name/title, date, status, price/amount) first.
+- **Evidence:** Pagliacci order-list shows Building/Cloneable instead of Date/Summary/Price.
diff --git a/internal/generator/templates/auth_browser.go.tmpl b/internal/generator/templates/auth_browser.go.tmpl
index 266f0b28..fbe21eae 100644
--- a/internal/generator/templates/auth_browser.go.tmpl
+++ b/internal/generator/templates/auth_browser.go.tmpl
@@ -187,6 +187,21 @@ profile by name when the installed backend supports it.`,
composed = strings.ReplaceAll(composed, "{"+name+"}", cookieMap[name])
}
+ // Validate the composed auth before saving — catch stale/expired sessions
+ fmt.Fprintf(w, "Validating session...")
+ if err := validateComposedAuth(composed); err != nil {
+ loginURL := "https://" + strings.TrimPrefix(domain, ".")
+ fmt.Fprintf(w, " %s\n", red("expired"))
+ fmt.Fprintln(w, "")
+ fmt.Fprintln(w, "Found cookies but the session has expired.")
+ fmt.Fprintln(w, "Log in again at:")
+ fmt.Fprintf(w, "\n %s\n\n", loginURL)
+ fmt.Fprintln(w, "Then run this command again:")
+ fmt.Fprintf(w, "\n {{.Name}}-pp-cli auth login --chrome\n")
+ return authErr(fmt.Errorf("session expired for %s", domain))
+ }
+ fmt.Fprintf(w, " %s\n", green("valid"))
+
cfg, err := config.Load(flags.configPath)
if err != nil {
return configErr(err)
@@ -477,7 +492,17 @@ func resolveChromeProfile(w io.Writer, r io.Reader, domain, profileFlag string)
fmt.Fprintf(w, "Auto-detected Chrome profile: %s (%s)\n", withCookies[0].DisplayName, withCookies[0].Dir)
return withCookies[0].Dir, nil
default:
- // Multiple profiles have cookies — ask the user
+ // Multiple profiles have cookies.
+ // Non-interactive: pick the profile with the most cookies (already sorted).
+ // Interactive: ask the user.
+ if !isTerminal(w) {
+ // Non-interactive — auto-select the best profile (most cookies, first in sorted list)
+ selected := withCookies[0]
+ fmt.Fprintf(w, "Auto-selected Chrome profile: %s (%s, %d cookies)\n", selected.DisplayName, selected.Dir, selected.CookieCount)
+ fmt.Fprintf(w, "Use --profile to select a different profile.\n")
+ return selected.Dir, nil
+ }
+
fmt.Fprintf(w, "Multiple Chrome profiles have cookies for %s:\n", domain)
for i, p := range withCookies {
fmt.Fprintf(w, " %d. %s (%s, %d cookies)\n", i+1, p.DisplayName, p.Dir, p.CookieCount)
@@ -664,6 +689,34 @@ func extractViaCookieScoop(domain, profileDir string) (string, error) {
return result, nil
}
+{{- if eq .Auth.Type "composed"}}
+// validateComposedAuth makes a lightweight test request to verify the session is still active.
+// Uses the API base URL — any authenticated endpoint returning 401/403 indicates expiry.
+// A non-auth error (404, 500, timeout) is treated as "can't validate, assume OK" to avoid
+// blocking auth on unrelated API issues.
+func validateComposedAuth(authHeader string) error {
+ testURL := "{{.BaseURL}}"
+ req, err := http.NewRequest("HEAD", testURL, nil)
+ if err != nil {
+ return nil // can't validate, assume OK
+ }
+ req.Header.Set("{{if .Auth.Header}}{{.Auth.Header}}{{else}}Authorization{{end}}", authHeader)
+ req.Header.Set("User-Agent", "{{.Name}}-pp-cli/{{.Version}}")
+
+ client := &http.Client{Timeout: 5 * time.Second}
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil // network error, assume OK — don't block auth on connectivity
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == 401 || resp.StatusCode == 403 {
+ return fmt.Errorf("HTTP %d", resp.StatusCode)
+ }
+ return nil
+}
+{{- end}}
+
// --- Live browser cookie extraction (session cookies fallback) ---
// extractLiveCookies reads document.cookie from a live Chrome session.
@@ -671,17 +724,17 @@ func extractViaCookieScoop(domain, profileDir string) (string, error) {
func extractLiveCookies(domain string) (string, error) {
targetURL := "https://" + strings.TrimPrefix(domain, ".")
- // 1. Try agent-browser --auto-connect
- if _, err := exec.LookPath("agent-browser"); err == nil {
- cookies, err := extractViaAgentBrowser(targetURL)
+ // 1. Try browser-use --connect (preferred — has full DOM/cookie access)
+ if _, err := exec.LookPath("browser-use"); err == nil {
+ cookies, err := extractViaBrowserUse(targetURL)
if err == nil && cookies != "" {
return cookies, nil
}
}
- // 2. Try browser-use --connect
- if _, err := exec.LookPath("browser-use"); err == nil {
- cookies, err := extractViaBrowserUse(targetURL)
+ // 2. Try agent-browser --auto-connect (fallback — limited eval in auto-connect mode)
+ if _, err := exec.LookPath("agent-browser"); err == nil {
+ cookies, err := extractViaAgentBrowser(targetURL)
if err == nil && cookies != "" {
return cookies, nil
}
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index 3dd187c6..6effd2ad 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -15,6 +15,7 @@ import (
"sort"
"strings"
"text/tabwriter"
+ "unicode"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -350,7 +351,8 @@ func filterFields(data json.RawMessage, fields string) json.RawMessage {
for _, f := range strings.Split(fields, ",") {
f = strings.TrimSpace(f)
if f != "" {
- wanted[f] = true
+ // Normalize to lowercase for case-insensitive matching
+ wanted[strings.ToLower(f)] = true
}
}
if len(wanted) == 0 {
@@ -364,7 +366,7 @@ func filterFields(data json.RawMessage, fields string) json.RawMessage {
for i, item := range items {
m := map[string]json.RawMessage{}
for k, v := range item {
- if wanted[k] {
+ if fieldMatchesSelect(k, wanted) {
m[k] = v
}
}
@@ -379,7 +381,7 @@ func filterFields(data json.RawMessage, fields string) json.RawMessage {
if json.Unmarshal(data, &obj) == nil {
m := map[string]json.RawMessage{}
for k, v := range obj {
- if wanted[k] {
+ if fieldMatchesSelect(k, wanted) {
m[k] = v
}
}
@@ -390,6 +392,37 @@ func filterFields(data json.RawMessage, fields string) json.RawMessage {
return data
}
+// fieldMatchesSelect checks if a field name matches any of the wanted select fields.
+// Matches case-insensitively and converts camelCase/PascalCase to kebab-case for comparison.
+// "OrderDate" matches "orderdate", "order-date", or "OrderDate".
+func fieldMatchesSelect(fieldName string, wanted map[string]bool) bool {
+ lower := strings.ToLower(fieldName)
+ // Direct case-insensitive match: "ID" matches "id"
+ if wanted[lower] {
+ return true
+ }
+ // Convert original casing to kebab-case: "OrderDate" → "order-date"
+ kebab := camelToKebab(fieldName)
+ if kebab != lower && wanted[kebab] {
+ return true
+ }
+ return false
+}
+
+// camelToKebab converts "orderDate" or "orderdate" to "order-date" by splitting on
+// uppercase boundaries. For already-lowercase input, splits on known word boundaries.
+func camelToKebab(s string) string {
+ var b strings.Builder
+ runes := []rune(s)
+ for i, r := range runes {
+ if i > 0 && unicode.IsUpper(r) && unicode.IsLower(runes[i-1]) {
+ b.WriteByte('-')
+ }
+ b.WriteRune(unicode.ToLower(r))
+ }
+ return b.String()
+}
+
// printOutputWithFlags routes output through the right format based on flags.
func printOutputWithFlags(w io.Writer, data json.RawMessage, flags *rootFlags) error {
// Apply --compact: filter to high-gravity fields only
@@ -703,54 +736,91 @@ func prioritizeAllHeaders(item map[string]any) []string {
// prioritizeFields orders fields by importance: identity → temporal → status → other.
// When includeComplex is true, arrays and objects are included (for card layout).
+//
+// Uses exact-or-suffix matching to avoid false positives: "name" matches "Name" and
+// "UserName" but not "BuildingName" (because "Building" is not a known prefix that
+// indicates identity). The field is split on camelCase/snake_case boundaries and the
+// LAST segment is matched against patterns.
func prioritizeFields(item map[string]any, includeComplex bool) []string {
- // Priority tiers — fields matching earlier tiers appear first.
- // Each entry is a substring matched case-insensitively against the field name.
- tiers := [][]string{
- // Tier 0: Identity
- {"id", "name", "title", "code", "key", "slug"},
- // Tier 1: Temporal
- {"date", "time", "created", "updated", "when", "at"},
- // Tier 2: Status & outcome
- {"status", "state", "result", "verdict", "phase"},
- // Tier 3: Value & summary
- {"summary", "description", "price", "amount", "total", "cost", "value", "points", "score"},
- // Tier 4: Categorization
- {"type", "kind", "category", "method", "store", "email", "phone", "url"},
- }
+ // Priority tiers — matched against the last segment of the field name.
+ // "OrderDate" → last segment "date" → tier 1 (temporal).
+ // "BuildingName" → last segment "name" → tier 0... but we want to avoid this.
+ // Solution: exact match on the full lowered name OR suffix segment match,
+ // with a penalty for compound names that have a non-identity prefix.
+ type pattern struct {
+ word string
+ tier int
+ }
+ // Exact matches (full field name, case-insensitive) — highest confidence
+ exactMatches := map[string]int{
+ "id": 0, "name": 0, "title": 0, "slug": 0, "key": 0,
+ "date": 1, "created": 1, "updated": 1, "createdat": 1, "updatedat": 1,
+ "status": 2, "state": 2, "statuscode": 2,
+ "summary": 3, "description": 3, "price": 3, "amount": 3, "total": 3,
+ "cost": 3, "points": 3, "score": 3,
+ "type": 4, "kind": 4, "category": 4, "email": 4, "phone": 4, "url": 4,
+ }
+ // Suffix patterns — match when the field ends with this word (after splitting)
+ suffixMatches := map[string]int{
+ "id": 0, "name": 0, "title": 0,
+ "date": 1, "time": 1,
+ "status": 2, "state": 2, "code": 2,
+ "price": 3, "amount": 3, "total": 3, "cost": 3,
+ "summary": 3, "description": 3, "points": 3, "score": 3,
+ "type": 4, "kind": 4, "category": 4, "method": 4,
+ }
+
+ numTiers := 5
type scored struct {
name string
tier int
- index int // preserve order within same tier
+ index int
}
var all []scored
idx := 0
for k, v := range item {
- // Skip complex fields in table mode — they don't fit in cells.
- // In card mode, arrays are flattened (e.g. Summary: "1 Pizza, 1 Pagliaccio").
if !includeComplex {
switch v.(type) {
case []any, map[string]any:
continue
}
}
+ // Skip values that won't render usefully in cards
+ if includeComplex {
+ formatted := formatCellValue(v)
+ if formatted == "" {
+ continue
+ }
+ }
- tier := len(tiers) // default: lowest priority
+ tier := numTiers // default: unclassified
lower := strings.ToLower(k)
- for t, patterns := range tiers {
- for _, p := range patterns {
- if strings.Contains(lower, p) {
- if t < tier {
+
+ // 1. Exact match on full field name
+ if t, ok := exactMatches[lower]; ok {
+ tier = t
+ } else {
+ // 2. Split camelCase into segments and match the last one
+ segments := splitCamelCase(lower)
+ if len(segments) > 0 {
+ lastSeg := segments[len(segments)-1]
+ if t, ok := suffixMatches[lastSeg]; ok {
+ // Compound names with identity suffixes (BuildingName, TipTime)
+ // get demoted one tier because the prefix dilutes the signal
+ if len(segments) > 1 {
+ tier = t + 1
+ } else {
tier = t
}
}
}
}
- // Demote booleans — they're rarely the most useful columns
- if _, ok := v.(bool); ok && tier == len(tiers) {
- tier = len(tiers) + 1
+
+ // Demote booleans to last
+ if _, ok := v.(bool); ok && tier >= numTiers {
+ tier = numTiers + 1
}
all = append(all, scored{name: k, tier: tier, index: idx})
idx++
@@ -770,6 +840,34 @@ func prioritizeFields(item map[string]any, includeComplex bool) []string {
return headers
}
+// splitCamelCase splits "OrderDate" → ["order", "date"], "statusCode" → ["status", "code"],
+// "page_size" → ["page", "size"].
+func splitCamelCase(s string) []string {
+ var segments []string
+ var current strings.Builder
+ runes := []rune(s)
+ for i, r := range runes {
+ if r == '_' || r == '-' {
+ if current.Len() > 0 {
+ segments = append(segments, current.String())
+ current.Reset()
+ }
+ continue
+ }
+ if i > 0 && unicode.IsUpper(r) && unicode.IsLower(runes[i-1]) {
+ if current.Len() > 0 {
+ segments = append(segments, current.String())
+ current.Reset()
+ }
+ }
+ current.WriteRune(unicode.ToLower(r))
+ }
+ if current.Len() > 0 {
+ segments = append(segments, current.String())
+ }
+ return segments
+}
+
// printAutoCards renders items as labeled cards — one block per item.
// Used for complex responses with many fields or nested data.
func printAutoCards(w io.Writer, items []map[string]any) error {
@@ -802,16 +900,16 @@ func printAutoCards(w io.Writer, items []map[string]any) error {
}
// Remaining fields indented — skip empty, zero, and false values
- shown := 0
for _, h := range headers[2:] {
v := formatCellValue(item[h])
if v == "" || v == "false" || v == "0" || v == "[]" || v == "null" {
continue
}
- fmt.Fprintf(w, " %-*s %s\n", maxLen, h+":", v)
- shown++
- if shown >= 12 {
- break // cap at 12 visible fields per card
+ // Multi-line values (nested arrays) start with \n
+ if strings.HasPrefix(v, "\n") {
+ fmt.Fprintf(w, " %s:%s\n", h, v)
+ } else {
+ fmt.Fprintf(w, " %-*s %s\n", maxLen, h+":", v)
}
}
}
@@ -821,17 +919,29 @@ func printAutoCards(w io.Writer, items []map[string]any) error {
func formatCellValue(v any) string {
switch val := v.(type) {
case string:
+ // Format ISO dates as just the date portion
+ if len(val) >= 19 && val[4] == '-' && val[7] == '-' && val[10] == 'T' {
+ return val[:10]
+ }
return truncate(val, 60)
case float64:
if val == float64(int64(val)) {
return fmt.Sprintf("%d", int64(val))
}
- return fmt.Sprintf("%g", val)
+ return fmt.Sprintf("%.2f", val)
case bool:
return fmt.Sprintf("%t", val)
case nil:
return ""
case []any:
+ if len(val) == 0 {
+ return ""
+ }
+ // If array contains objects, format each as a summary line
+ if obj, isObj := val[0].(map[string]any); isObj {
+ _ = obj
+ return formatObjectArray(val)
+ }
// Flatten simple arrays into comma-separated string
parts := make([]string, 0, len(val))
for _, item := range val {
@@ -843,8 +953,107 @@ func formatCellValue(v any) string {
}
}
return truncate(strings.Join(parts, ", "), 60)
+ case map[string]any:
+ return formatSingleObject(val)
default:
b, _ := json.Marshal(val)
return truncate(string(b), 60)
}
}
+
+// formatObjectArray renders an array of objects as multi-line summary.
+// Each object is summarized by its most descriptive fields: name/title, qty, size, price.
+func formatObjectArray(items []any) string {
+ var lines []string
+ for _, raw := range items {
+ obj, ok := raw.(map[string]any)
+ if !ok {
+ continue
+ }
+ lines = append(lines, formatObjectSummary(obj))
+ }
+ if len(lines) == 0 {
+ return ""
+ }
+ // Multi-line: newline-prefixed so the card renderer can indent
+ return "\n" + strings.Join(lines, "\n")
+}
+
+// formatObjectSummary extracts the most useful fields from an object into a one-line summary.
+// Looks for: qty/count → name/title → size → price, in that order.
+func formatObjectSummary(obj map[string]any) string {
+ var parts []string
+
+ // Quantity
+ qty := findField(obj, "qty", "count", "quantity")
+ if qty != "" && qty != "1" && qty != "0" {
+ parts = append(parts, qty+"x")
+ } else if qty == "1" {
+ parts = append(parts, "1x")
+ }
+
+ // Name — check nested objects too (e.g., Side1.Name)
+ name := findField(obj, "name", "title", "label", "description")
+ if name == "" {
+ // Check nested objects for name
+ for _, key := range []string{"Side1", "side1", "Item", "item", "Product", "product"} {
+ if nested, ok := obj[key].(map[string]any); ok {
+ name = findField(nested, "name", "title", "label")
+ if name != "" {
+ break
+ }
+ }
+ }
+ }
+ if name != "" {
+ parts = append(parts, name)
+ }
+
+ // Size
+ size := findField(obj, "sizename", "size_name")
+ if size == "" {
+ size = findField(obj, "catname", "cat_name", "category")
+ }
+ if size != "" {
+ parts = append(parts, "—")
+ parts = append(parts, size)
+ }
+
+ // Price
+ price := findField(obj, "extprice", "price", "amount", "total")
+ if price != "" && price != "0" {
+ parts = append(parts, fmt.Sprintf("($%s)", price))
+ }
+
+ if len(parts) == 0 {
+ // Fallback: JSON summary
+ b, _ := json.Marshal(obj)
+ return truncate(string(b), 80)
+ }
+ return " " + strings.Join(parts, " ")
+}
+
+// formatSingleObject renders a single object by its most descriptive fields.
+func formatSingleObject(obj map[string]any) string {
+ name := findField(obj, "name", "title", "label", "description")
+ if name != "" {
+ return name
+ }
+ id := findField(obj, "id", "key", "code")
+ if id != "" {
+ return id
+ }
+ return ""
+}
+
+// findField searches an object for a field name (case-insensitive) and returns its formatted value.
+func findField(obj map[string]any, names ...string) string {
+ for _, name := range names {
+ for k, v := range obj {
+ if strings.EqualFold(k, name) {
+ return formatCellValue(v)
+ }
+ }
+ }
+ return ""
+}
diff --git a/internal/websniff/specgen.go b/internal/websniff/specgen.go
index a65a0d21..c64e40ab 100644
--- a/internal/websniff/specgen.go
+++ b/internal/websniff/specgen.go
@@ -323,6 +323,18 @@ func detectCapturedAuth(capture *AuthCapture, envPrefix string) spec.AuthConfig
CookieDomain: capture.BoundDomain,
EnvVars: envVarsOrNil(envPrefix, "COOKIES"),
}
+ case "composed":
+ headerName := firstAuthHeader(capture.Headers)
+ if headerName == "" {
+ headerName = "Authorization"
+ }
+ return spec.AuthConfig{
+ Type: "composed",
+ Header: headerName,
+ Format: capture.Format,
+ CookieDomain: capture.BoundDomain,
+ Cookies: capture.Cookies,
+ }
}
case captureType == "cookie" && len(capture.Cookies) > 0:
return spec.AuthConfig{
diff --git a/internal/websniff/types.go b/internal/websniff/types.go
index cc8edd9a..3bdb0c89 100644
--- a/internal/websniff/types.go
+++ b/internal/websniff/types.go
@@ -52,7 +52,8 @@ type EnrichedCapture struct {
type AuthCapture struct {
Headers map[string]string `json:"headers"`
Cookies []string `json:"cookies"`
- Type string `json:"type"`
+ Type string `json:"type"` // bearer, api_key, cookie, composed
+ Format string `json:"format"` // for composed: header template with {cookieName} placeholders
BoundDomain string `json:"bound_domain"`
ExpiresAt string `json:"expires_at"`
}
← b0a38151 feat(cli): add Chrome cookie auth for sniff-discovered APIs
·
back to Cli Printing Press
·
fix(cli): scorer recognizes composed/cookie auth and apiKey e9d9daa5 →