[Chrome Extension] ──── Native Messaging ────→ [native-host binary (Tauri sidecar)]
│
[Firefox Extension] ──── Native Messaging ────→ │
↓
[hudsucker MITM Proxy] ── HTTP/S intercept ──→ [Tauri Rust Backend]
│ │
[SQLite DB] [Tauri Commands IPC]
(one .db per session) │
[React Frontend]
CaptureBar / RequestList / EndpointMap
InferencePanel / CollectionPreview
│
[Anthropic API — claude-sonnet-4-20250514]
(sequential batch, auth headers stripped)
│
[postman-collection npm SDK]
│
[Export: .postman_collection.json]
api-reverse-engineer/
├── src-tauri/
│ ├── src/
│ │ ├── main.rs # Tauri app entry, command registration, event setup
│ │ ├── commands/
│ │ │ ├── session.rs # create_session, list_sessions, delete_session, update_session
│ │ │ ├── capture.rs # get_requests, get_endpoints, clear_requests, save_inference_result
│ │ │ ├── proxy.rs # start_proxy, stop_proxy, get_proxy_status, get_ca_status, install_ca
│ │ │ └── export.rs # build_postman_collection (takes session_id, returns JSON string)
│ │ ├── proxy/
│ │ │ ├── mod.rs
│ │ │ ├── mitm.rs # hudsucker proxy setup, CA cert loading, request/response handler
│ │ │ ├── filter.rs # domain allow/deny logic, static asset extension filtering
│ │ │ └── normalizer.rs # path param regex detection, URL normalization
│ │ ├── db/
│ │ │ ├── mod.rs # connection pool setup, migration runner
│ │ │ ├── migrations/
│ │ │ │ ├── 001_initial.sql # sessions, requests, endpoints tables
│ │ │ │ └── 002_inference.sql # inference_results table
│ │ │ └── queries.rs # all sqlx typed queries (no inline SQL elsewhere)
│ │ └── native_host/
│ │ └── main.rs # Standalone binary: Chrome/Firefox Native Messaging protocol
│ ├── Cargo.toml
│ └── tauri.conf.json
├── src/
│ ├── components/
│ │ ├── CaptureBar.tsx # Record/Stop, mode toggle (Extension/MITM), session name input
│ │ ├── RequestList.tsx # Live scrolling list: method badge, URL, status, timestamp
│ │ ├── EndpointMap.tsx # Grouped by normalized_path: method, count, auth indicator
│ │ ├── InferencePanel.tsx # Run AI Analysis button, endpoint checkboxes, progress bar
│ │ ├── CollectionPreview.tsx # Rendered Postman tree with inline schema editor
│ │ ├── FilterConfig.tsx # Domain allowlist/denylist, noise presets, regex exclusions
│ │ ├── SessionManager.tsx # List/open/rename/delete/export-zip past sessions
│ │ └── CASetupModal.tsx # Guided CA cert installation flow for MITM mode
│ ├── hooks/
│ │ ├── useCapture.ts # Tauri event listener for `request:captured` stream
│ │ ├── useInference.ts # Sequential inference queue, progress state, SQLite writes
│ │ └── useSession.ts # Active session CRUD, session list state
│ ├── lib/
│ │ ├── tauri.ts # Typed invoke() wrappers for ALL Tauri commands
│ │ ├── inference.ts # Anthropic API fetch, prompt template, JSON parse + strip fences
│ │ ├── postman-builder.ts # Constructs PostmanCollection from Endpoint[] + InferenceResult[]
│ │ └── normalizer.ts # Client-side URL normalization (mirrors Rust normalizer.rs)
│ ├── types/
│ │ └── index.ts # All shared TS interfaces (see Type Definitions below)
│ ├── App.tsx # Top-level layout, view router, session context provider
│ └── main.tsx # Vite entry point
├── extension/
│ ├── chrome/
│ │ ├── manifest.json # MV3, webRequest permission, nativeMessaging declaration
│ │ ├── background.js # Service worker: buffer queue, 500ms flush, sendNativeMessage
│ │ ├── content.js # (Phase 4) XHR/fetch monkey-patch for body capture (opt-in)
│ │ └── popup.html # Status indicator: Recording / Idle / Error
│ └── firefox/
│ ├── manifest.json # MV2, same permissions, browser_specific_settings
│ └── background.js # Firefox WebExtensions port of Chrome background.js
├── package.json
├── tsconfig.json
├── vite.config.ts
├── CLAUDE.md
└── IMPLEMENTATION-ROADMAP.md
-- 001_initial.sql
CREATE TABLE sessions (
id TEXT PRIMARY KEY, -- UUID v4
name TEXT NOT NULL,
target_domain TEXT,
capture_mode TEXT NOT NULL CHECK(capture_mode IN ('extension', 'mitm', 'mixed')),
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
ended_at DATETIME,
request_count INTEGER DEFAULT 0,
filter_config TEXT, -- JSON: {allowlist: [], denylist: [], noisePresets: []}
status TEXT DEFAULT 'active' CHECK(status IN ('active', 'complete', 'archived'))
);
CREATE TABLE requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
capture_source TEXT NOT NULL CHECK(capture_source IN ('extension', 'mitm')),
method TEXT NOT NULL,
url TEXT NOT NULL,
normalized_path TEXT NOT NULL,
host TEXT NOT NULL,
path TEXT NOT NULL,
query_params TEXT, -- JSON object
request_headers TEXT, -- JSON object
request_body TEXT, -- NULL in extension mode (Phase 1); populated in MITM mode + Phase 4
request_content_type TEXT,
response_status INTEGER,
response_headers TEXT, -- JSON object
response_body TEXT, -- Truncated at 50KB
response_content_type TEXT,
duration_ms INTEGER,
captured_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_noise INTEGER DEFAULT 0,
is_duplicate INTEGER DEFAULT 0
);
CREATE INDEX idx_requests_session ON requests(session_id);
CREATE INDEX idx_requests_normalized ON requests(session_id, normalized_path, method);
CREATE INDEX idx_requests_captured_at ON requests(captured_at);
CREATE TABLE endpoints (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
method TEXT NOT NULL,
normalized_path TEXT NOT NULL,
host TEXT NOT NULL,
request_count INTEGER DEFAULT 1,
first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
sample_request_ids TEXT, -- JSON array of up to 3 request IDs for inference
auth_detected TEXT CHECK(auth_detected IN ('bearer', 'basic', 'cookie', 'apikey', NULL)),
UNIQUE(session_id, method, normalized_path, host)
);
CREATE INDEX idx_endpoints_session ON endpoints(session_id);
-- 002_inference.sql
CREATE TABLE inference_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
endpoint_id INTEGER NOT NULL REFERENCES endpoints(id) ON DELETE CASCADE,
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
inferred_name TEXT,
inferred_description TEXT,
request_body_schema TEXT, -- JSON Schema object (stringified)
response_body_schema TEXT, -- JSON Schema object (stringified)
path_params TEXT, -- JSON array: [{name, description, example}]
query_param_descriptions TEXT, -- JSON object: {param_name: description}
auth_scheme TEXT CHECK(auth_scheme IN ('bearer', 'basic', 'apikey', 'none', NULL)),
tags TEXT, -- JSON array of strings
raw_claude_response TEXT,
tokens_used INTEGER,
inferred_at DATETIME DEFAULT CURRENT_TIMESTAMP,
model_used TEXT DEFAULT 'claude-sonnet-4-20250514'
);
CREATE INDEX idx_inference_endpoint ON inference_results(endpoint_id);
CREATE INDEX idx_inference_session ON inference_results(session_id);// src/types/index.ts
export interface Session {
id: string;
name: string;
targetDomain?: string;
captureMode: 'extension' | 'mitm' | 'mixed';
startedAt: string;
endedAt?: string;
requestCount: number;
filterConfig?: FilterConfig;
status: 'active' | 'complete' | 'archived';
}
export interface FilterConfig {
allowlist: string[]; // domains to capture; empty = capture all
denylist: string[]; // domains to always ignore
noisePresets: NoisePreset[];
pathExcludePatterns: string[]; // regex strings
}
export type NoisePreset = 'analytics' | 'cdn' | 'social' | 'fonts';
export interface CapturedRequest {
id: number;
sessionId: string;
captureSource: 'extension' | 'mitm';
method: string;
url: string;
normalizedPath: string;
host: string;
path: string;
queryParams?: Record<string, string>;
requestHeaders?: Record<string, string>;
requestBody?: string;
requestContentType?: string;
responseStatus?: number;
responseHeaders?: Record<string, string>;
responseBody?: string;
responseContentType?: string;
durationMs?: number;
capturedAt: string;
isNoise: boolean;
isDuplicate: boolean;
}
export interface Endpoint {
id: number;
sessionId: string;
method: string;
normalizedPath: string;
host: string;
requestCount: number;
firstSeen: string;
lastSeen: string;
sampleRequestIds: number[];
authDetected?: 'bearer' | 'basic' | 'cookie' | 'apikey';
}
export interface InferenceResult {
id: number;
endpointId: number;
sessionId: string;
inferredName?: string;
inferredDescription?: string;
requestBodySchema?: JsonSchema;
responseBodySchema?: JsonSchema;
pathParams?: PathParam[];
queryParamDescriptions?: Record<string, string>;
authScheme?: 'bearer' | 'basic' | 'apikey' | 'none';
tags?: string[];
tokensUsed?: number;
inferredAt: string;
}
export interface PathParam {
name: string;
description: string;
example: string;
}
export interface JsonSchema {
type: string;
properties?: Record<string, JsonSchema>;
items?: JsonSchema;
description?: string;
example?: unknown;
nullable?: boolean;
required?: string[];
}
export interface InferencePromptPayload {
method: string;
path: string;
host: string;
samples: {
requestBody?: string;
requestHeaders?: Record<string, string>; // auth headers stripped before sending
responseStatus: number;
responseBody?: string; // truncated to 3,000 chars
responseHeaders?: Record<string, string>;
}[];
}
export interface ProxyStatus {
running: boolean;
port: number;
caInstalled: boolean;
caPath: string;
requestsIntercepted: number;
}
// Postman Collection v2.1 shapes
export interface PostmanCollection {
info: {
name: string;
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json';
description?: string;
};
item: PostmanItem[];
variable?: PostmanVariable[];
}
export interface PostmanItem {
name: string;
request: {
method: string;
url: { raw: string; host: string[]; path: string[]; query?: PostmanQueryParam[] };
header?: PostmanHeader[];
body?: { mode: 'raw'; raw: string; options?: { raw: { language: 'json' } } };
description?: string;
};
response?: unknown[];
}
export interface PostmanHeader { key: string; value: string; description?: string; }
export interface PostmanQueryParam { key: string; value: string; description?: string; }
export interface PostmanVariable { key: string; value: string; description?: string; }Anthropic API (inference calls):
| Field | Value |
|---|---|
| Endpoint | https://api.anthropic.com/v1/messages |
| Method | POST |
| Auth | Handled by Claude.ai artifact proxy (no key in code) |
| Model | claude-sonnet-4-20250514 |
| max_tokens | 1000 |
| Rate limit | Sequential calls only — no parallel inference |
| Payload size | Max 6,000 tokens per request; response body truncated to 3,000 chars before sending |
| Response | JSON object matching InferenceResult shape — strip markdown fences before JSON.parse() |
Inference prompt template (in src/lib/inference.ts):
System: You are an API documentation assistant. Analyze captured HTTP traffic for a single endpoint and respond ONLY with a valid JSON object. Do not include markdown fences or any text outside the JSON.
User: Analyze this endpoint and return a JSON object with these fields:
- inferredName (string): short human-readable name e.g. "Get User Profile"
- inferredDescription (string): one sentence
- requestBodySchema (JSON Schema object or null)
- responseBodySchema (JSON Schema object or null)
- pathParams (array of {name, description, example})
- queryParamDescriptions (object: {paramName: description})
- authScheme ("bearer" | "basic" | "apikey" | "none")
- tags (array of 1-3 category strings)
Endpoint data:
${JSON.stringify(payload)}
# Frontend
npm install postman-collection@4 postman-collection-transformer@4
npm install @tauri-apps/api@2 @tauri-apps/plugin-dialog@2 @tauri-apps/plugin-fs@2
npm install -D typescript@5 vite@5 @vitejs/plugin-react tailwindcss autoprefixer postcss
# Rust — add to src-tauri/Cargo.toml
# hudsucker = "0.10"
# rcgen = { version = "0.13", features = ["pem"] }
# tokio = { version = "1", features = ["full"] }
# sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "chrono", "uuid"] }
# serde = { version = "1", features = ["derive"] }
# serde_json = "1"
# uuid = { version = "1", features = ["v4"] }
# regex = "1"
# tracing = "0.1"
# tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Scaffold command
npm create tauri-app@latest api-reverse-engineer -- --template react-tsIn scope:
- Chrome MV3 + Firefox MV2 browser extension capture (headers, URL, method, status; bodies in Phase 4)
- hudsucker MITM proxy for full system HTTPS capture including bodies
- Per-install CA cert generation and guided macOS System keychain installation
- URL normalization and endpoint deduplication (path param collapsing)
- Domain allow/denylist filtering + noise preset filters
- Claude Sonnet 4 inference: name, description, JSON Schema, path params, auth scheme, tags
- Postman Collection v2.1 export with auth header redaction by default
- Session management: create, list, rename, delete, export-to-zip
Out of scope (do not build):
- OpenAPI 3.x YAML/JSON export
- TypeScript SDK stub generation
- Markdown documentation export
- Safari WebExtensions support
- SQLCipher encryption at rest
- Windows or Linux support
- Mobile traffic capture (iOS simulator proxy config is user-configured)
- Cloud sync or shared sessions
Deferred to Phase 5+:
- OpenAPI export
- Windows build
- SQLCipher encryption option
- Webhook/GraphQL specialized parsing
- CA private key (
ca.key):~/Library/Application Support/apispy/ca.key, permissions0600. Never logged, never exported, never transmitted. - Captured auth headers: stored in SQLite as-is for local use. Stripped from all Anthropic API inference payloads. Replaced with
{{bearer_token}}/{{api_key}}in Postman export by default. - Data leaving the machine: only truncated request/response body fragments (max 3,000 chars per body) with auth headers removed, sent to
api.anthropic.comduring inference. Zero capture data transmitted otherwise. - Proxy binding: hudsucker binds to
127.0.0.1:8877only. Not LAN-accessible. - CA scope: installed to macOS System keychain via
security add-trusted-cert -d -r trustRoot. Document clearly in CASetupModal — user must understand what they're installing. - SQLite DB files: plaintext, stored in
~/Library/Application Support/apispy/sessions/. Users capturing internal API traffic should be aware of this.
Objective: Tauri 2 project scaffolded, SQLite schema applied, native host binary compiling, Chrome extension manifest registered.
Tasks:
-
Scaffold with
npm create tauri-app@latest api-reverse-engineer -- --template react-ts. Add Tailwind CSS. Verify dev server starts. Acceptance:npm run tauri devopens Tauri window with no errors in terminal or browser console. -
Write
001_initial.sqland002_inference.sql. Wiresqlxconnection pool indb/mod.rswithsqlx::migrate!(). Implementcreate_sessionandlist_sessionscommands incommands/session.rs. Acceptance:invoke('create_session', { name: 'test', captureMode: 'extension' })returns a session object with UUID;invoke('list_sessions')returns it in an array. -
Write
native_host/main.rs: reads 4-byte LE length-prefixed JSON from stdin (Chrome Native Messaging framing), parses the request object, inserts a row intorequestsand upsertsendpointsvia the SQLite connection. Acceptance:echoa framed JSON message to the binary via stdin; verify row inserted in SQLite usingsqlite3 test.db "SELECT * FROM requests". -
Create
extension/chrome/manifest.json(MV3) withwebRequest,nativeMessaging, and<all_urls>permissions. Register native host manifest at~/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.apispy.host.jsonpointing to compiled binary path. Acceptance: Load extension in Chrome developer mode (Extensions → Load unpacked). Service worker console shows no errors. -
Implement
proxy/normalizer.rswith regex-based path parameter detection. Path segment rules: pure integers →{id}, UUIDs ([a-f0-9-]{36}) →{id}, hex strings ≥8 chars →{id}, alphanumeric ≥10 chars with mixed case →{id}. Acceptance:cargo test— all normalizer unit tests pass:/users/123→/users/{id}/items/a1b2c3d4-e5f6-7890-abcd-ef1234567890→/items/{id}/api/v2/posts/456/comments/789→/api/v2/posts/{id}/comments/{id}/api/v1→/api/v1(version segment preserved)
Verification checklist:
-
cargo testinsrc-tauri/→ all normalizer tests pass -
npm run tauri dev→ window opens, zero console errors -
invoke('create_session')→ returns session with UUID -
invoke('list_sessions')→ returns array containing the session - Chrome extension loads with no service worker errors
- Native host binary accepts framed JSON on stdin and writes to SQLite
Risks:
sqlxoffline mode requiresDATABASE_URLset for compile-time query checking → setDATABASE_URL=sqlite:./dev.dbin.envfor local dev; add.envto.gitignore.- Native messaging host manifest path differs between Chrome stable, beta, and dev channels → detect channel in the Rust install command and write to the correct path; log which path was used.
Objective: Chrome extension captures XHR/fetch requests and streams them live into the React frontend. EndpointMap correctly deduplicates by normalized path.
Tasks:
-
Implement
extension/chrome/background.jsservice worker: subscribe tochrome.webRequest.onCompleted(headers, URL, method, status, timing). Buffer incoming requests in an in-memory array, flush every 500ms viachrome.runtime.sendNativeMessage. Add 20-second keepalive ping to prevent MV3 service worker termination. Acceptance: Navigate tojsonplaceholder.typicode.com/postswhile extension recording → native host receives ≥5 request objects with correct method/URL/status within 2 seconds. -
Implement
commands/capture.rs:get_requests(session_id, limit, offset)→ paginatedCapturedRequest[];get_endpoints(session_id)→Endpoint[]from the endpoints table. Acceptance: After capturing 10 requests to JSONPlaceholder,invoke('get_endpoints')returns deduplicated list where/posts/1,/posts/2,/posts/3collapse to single/posts/{id}entry withrequest_count: 3. -
Wire Tauri event emitter: emit
request:capturedevent (carrying theCapturedRequestpayload) from Rust on every SQLite insert. ImplementuseCapturehook in React usinglisten('request:captured', handler). Acceptance: New requests appear in the React UI within 1 second of browser navigation — no manual refresh. -
Build
RequestList.tsx: virtualized scroll (use windowing if >500 rows), method badge (color-coded: GET=blue, POST=green, PUT=amber, DELETE=red), URL, response status, relative timestamp. BuildEndpointMap.tsx: grouped bynormalized_path, shows method badges,request_count, auth detected icon. Acceptance: Visual inspection with 20 captured requests — endpoint map correctly groups by pattern, method colors correct, no obvious miscategorizations. -
Implement domain noise filtering in
proxy/filter.rs. Default denylist includes:google-analytics.com,googletagmanager.com,doubleclick.net,facebook.net,hotjar.com,segment.io,mixpanel.com,amplitude.com,clarity.ms,sentry.io. Static asset extension filter:.js .css .woff .woff2 .ttf .png .jpg .svg .ico .map .br. Acceptance: Capture a real SPA (e.g.,app.netlify.com) — noise filter drops analytics/CDN requests from EndpointMap display; they remain in DB withis_noise=1.
Verification checklist:
- 30-second capture of
jsonplaceholder.typicode.com→ ≥15 requests captured, ≥5 unique endpoints -
/posts/{id}shown as single endpoint with correct count (not separate rows per ID) - Live
request:capturedevents appear within 1 second - Noise filter: Google Analytics requests marked
is_noise=1, hidden from EndpointMap - RequestList renders 100+ rows without layout breaking
Risks:
- Chrome MV3 service workers terminate after 30 seconds of idle → keepalive ping in background.js sends a dummy
chrome.runtime.getPlatformInfo()call every 20 seconds during recording. Test with a 5-minute idle recording session. webRequest.onCompleteddoes NOT provide request or response bodies in extension mode → display "Body: captured in MITM mode" placeholder in request detail view for Phase 1.
Objective: hudsucker proxy running as a Tauri-managed async task, intercepting HTTPS from any macOS process, bodies included.
Tasks:
-
Implement
proxy/mitm.rs: spin up hudsucker on127.0.0.1:8877as a Tokio async task. Implement request/response handler that captures method, URL, all headers, request body, response status, response body (truncated at 50KB), duration. Emit samerequest:capturedTauri event. Acceptance:curl -x http://localhost:8877 https://httpbin.org/get→ row in SQLite with full headers and non-null response body containing"url": "https://httpbin.org/get". -
Implement CA cert generation in
proxy/mitm.rs: on first proxy start, if~/Library/Application Support/apispy/ca.crtdoes not exist, generate a self-signed CA usingrcgen(2048-bit RSA, 10-year validity, marked as CA). Write cert to.crtand key to.keywith0600permissions. Return cert path viaget_ca_status()Tauri command. Acceptance: After first proxy start,openssl x509 -in ~/Library/Application\ Support/apispy/ca.crt -text -nooutshowsCA:TRUEin Basic Constraints. -
Build
CASetupModal.tsx: displayed when MITM mode is toggled on and CA is not yet trusted. Shows: what the cert is, why it's needed, what the install command does. "Install CA Certificate" button callsinstall_caTauri command which runssecurity add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain [ca_path]viastd::process::Command. Shows system auth dialog. Acceptance: Click "Install CA Certificate" → macOS auth dialog appears → cert installed →security find-certificate -c "APIspy Local CA"succeeds in Terminal. -
Implement
start_proxy/stop_proxycommands. Wire toCaptureBar.tsxmode toggle. Show proxy status badge: port number, running indicator, request count. Proxy stops cleanly when Tauri app window closes (registeron_window_eventhandler). Acceptance: Toggle MITM on → proxy starts, status shows "Running on :8877". Toggle off → proxy stops within 2 seconds, no orphaned process inps aux. -
Implement Firefox extension: copy
extension/chrome/background.js, adapt for Firefox WebExtensions API (replacechrome.withbrowser.where needed). Createextension/firefox/manifest.json(MV2) withbrowser_specific_settings.gecko.id. Acceptance: Load Firefox extension in about:debugging → same 30-second capture test as Phase 1 passes in Firefox 120+.
Verification checklist:
-
curl -x http://localhost:8877 https://httpbin.org/post -d '{"test":1}'→ request_body stored as{"test":1}in SQLite -
curl -x http://localhost:8877 https://httpbin.org/get→ response_body stored, not NULL - CA cert installs via modal without opening Terminal
-
openssl x509 -in ca.crt -textshowsCA:TRUE - Proxy process absent from
ps auxwithin 3 seconds of app close - Firefox extension captures ≥10 requests in 30-second test
Risks:
security add-trusted-certrequires admin privileges on macOS 14+ Sonoma → Tauri'sCommandmust userunas/AuthorizationExecuteWithPrivilegesapproach. Test on macOS 14 specifically — behavior changed in Sonoma.- Some Electron apps and apps with certificate pinning will fail MITM → document as known limitation in the app. Show a "Certificate pinning detected" indicator when a connection fails with SSL errors.
- hudsucker 0.10.x API may shift → pin exact version in
Cargo.lock, write smoke test that compiles and runs a basic proxy in CI.
Objective: Selected endpoints processed by Claude Sonnet 4 and exported as a valid, importable Postman Collection v2.1.
Tasks:
-
Implement
src/lib/inference.ts: buildsInferencePromptPayloadfrom endpoint + sampled requests (stripAuthorization,Cookie,X-API-Keyheaders; truncate response body to 3,000 chars; merge up to 3 samples). Calls Anthropic API. Strips markdown fences from response. Parses JSON. ReturnsInferenceResult. Acceptance: Manual test — callrunInference(endpoint, requests)in browser console with a real JSONPlaceholder endpoint; returns validInferenceResultwith no parse errors and non-emptyinferredName. -
Implement
useInference.tshook: maintains queue ofEndpoint[]to process, callsrunInferencesequentially (one at a time), updates progress state{completed: N, total: N, currentEndpoint: string, tokensUsed: N}, writes each result to SQLite viainvoke('save_inference_result', result). Handles errors per-endpoint without aborting the queue. Acceptance: Process 10 JSONPlaceholder endpoints; all 10 complete within 60 seconds; 10 rows ininference_results; UI shows progress per endpoint; a single 429 error retries after 5 seconds without crashing the queue. -
Build
InferencePanel.tsx: "Run AI Analysis" button, endpoint selection checkboxes (all non-noise selected by default), progress bar with{completed}/{total}and current endpoint name, estimated time remaining (based on avg tokens/sec), cumulative token count displayed after completion. Acceptance: Visual inspection — panel shows per-endpoint progress; no UI thread blocking; button disabled during inference run; shows summary (10 endpoints, ~N tokens) on completion. -
Implement
src/lib/postman-builder.tsusing thepostman-collectionnpm SDK: constructsPostmanCollectionfromEndpoint[]+InferenceResult[]+ session name. Path params become{{param_name}}variables. Auth headers replaced with{{bearer_token}}/{{api_key}}variables added to collection-levelvariable[]. Groups endpoints by inferredtags[0]into Postman folders. Acceptance: Runpostman-collection-transformer validateon the output → 0 errors for a 10-endpoint session. -
Wire export: "Export Collection" button →
invoke('build_postman_collection', sessionId)→ Taurisave_dialog(default filename:{session_name}.postman_collection.json) → write to selected path → success toast with file path + "Open in Finder" action. Acceptance: Exported file imports into Postman Desktop 11 with 0 validation errors; all endpoints visible with correct names and descriptions.
Verification checklist:
- Inference on 10 endpoints completes within 60 seconds
- All 10
inference_resultsrows in SQLite with non-nullinferred_name - Token count displayed post-run matches Anthropic API usage
- Postman collection imports into Postman Desktop 11 with zero errors
- Auth headers NOT present in exported JSON (replaced with
{{variables}}) - Path params use
{{variable}}syntax in exported collection URLs
Risks:
- Claude returns markdown fences despite system prompt instructions →
inference.tsalways strips```jsonand```beforeJSON.parse(). If parse still fails, storeraw_claude_responsein DB and mark endpoint asinference_failed— show retry option in UI. - Paginated list endpoints return 100-item arrays → response body truncation to 3,000 chars may cut mid-object → note in prompt: "Response may be truncated; infer schema from visible fields only."
Objective: Sessions are manageable, filter config is user-editable, inline schema edits persist, body capture works in extension mode.
Tasks:
-
Build
SessionManager.tsx: grid/list of past sessions with name, date, capture mode badge, request count, status. Actions: open (load session into main view), rename (inline edit), delete (with confirmation → removes SQLite file from disk), export-to-zip (bundles.db+.postman_collection.json). Acceptance: Create 3 sessions, all appear in list with correct metadata. Delete one → list shows 2, DB file removed from~/Library/Application Support/apispy/sessions/. -
Build
FilterConfig.tsx: domain allowlist/denylist text input with tag-style chips. Noise preset toggles (analytics/CDN/social/fonts). Regex path exclusion rules (add/remove). Config saved tofilter_configJSON column in session row on every change. Acceptance: Addsentry.ioto denylist → new capture session: all Sentry requests haveis_noise=1, hidden from EndpointMap. Config persists after app restart. -
Add inline schema editor to
CollectionPreview.tsx: click any field name or description in the rendered collection tree to edit inline. Changes callinvoke('update_inference_result', {id, field, value}). Re-export reflects edits. Acceptance: EditinferredDescriptionfor one endpoint → re-export → updated description in Postman import. -
Implement content script body capture (
extension/chrome/content.js): patcheswindow.XMLHttpRequest.prototype.sendandwindow.fetchto intercept request bodies; monkey-patchesResponse.prototype.jsonand.textto capture response bodies; posts to background viachrome.runtime.sendMessage. Opt-in: requires user to grant host permission for the target domain. Shows permission prompt inCaptureBar.tsx. Acceptance: Enable body capture onreqres.in→ POST to/api/userswith{"name":"John"}→request_bodystored as{"name":"John"}in SQLite. -
First-run onboarding modal (shown once, stored in session/app-level SQLite flag): Step 1 — install Chrome extension (link to Chrome Web Store or sideload instructions); Step 2 — optionally install CA for MITM (links to
CASetupModal); Step 3 — name and create first session. Acceptance: Fresh app launch (no sessions in DB) → onboarding modal appears. Completable in under 3 minutes without reading docs. After completion, modal never shows again.
Verification checklist:
- Session list renders 10 sessions with correct metadata
- Delete session removes DB file from disk (verify with
ls ~/Library/Application\ Support/apispy/sessions/) - Filter config survives app restart
- Inline schema edit → re-export → edit present in Postman
- Content script: POST request body captured and stored for fetch-based API call on
httpbin.org/post - Onboarding: fresh launch triggers modal; completion suppresses future display
- Extension capture: 30-second session on
jsonplaceholder.typicode.com→ ≥10 unique endpoints, ≥0 message drops - MITM capture:
curl -x localhost:8877 https://httpbin.org/post -d '{"key":"val"}'→request_bodyandresponse_bodystored, not NULL - Normalizer:
/users/123/posts/456→/users/{id}/posts/{id}(confirm via endpoint map) - Noise filter: Google Analytics requests →
is_noise=1, hidden from EndpointMap - Inference: 5-endpoint run on JSONPlaceholder → all 5 complete, no parse errors, non-empty
inferred_name - Export: import into Postman Desktop 11 → 0 validation errors, auth headers absent from export
- Session delete: DB file removed from disk
- Proxy cleanup: no
hudsuckerprocess inps auxafter app closes