apigrip 0.12.0 → 0.15.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.
@@ -7,11 +7,16 @@
7
7
  <script>
8
8
  try {
9
9
  var t = localStorage.getItem('apigrip:theme');
10
- if (t) document.documentElement.setAttribute('data-theme', t);
10
+ if (t === 'auto' || !t) {
11
+ var sys = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
12
+ document.documentElement.setAttribute('data-theme', sys);
13
+ } else if (t) {
14
+ document.documentElement.setAttribute('data-theme', t);
15
+ }
11
16
  } catch (e) {}
12
17
  </script>
13
- <script type="module" crossorigin src="/assets/index-CUxgBG7H.js"></script>
14
- <link rel="stylesheet" crossorigin href="/assets/index-DGp2sscN.css">
18
+ <script type="module" crossorigin src="/assets/index-D4I40ptE.js"></script>
19
+ <link rel="stylesheet" crossorigin href="/assets/index-B0_C2Edh.css">
15
20
  </head>
16
21
  <body>
17
22
  <div id="root"></div>
@@ -0,0 +1,368 @@
1
+ /**
2
+ * code-generators.js - Generate copyable code snippets (fetch, got, requests, httpie) from request parameters.
3
+ */
4
+
5
+ /**
6
+ * Indent a JSON value for inline embedding in generated code.
7
+ * @param {*} obj - Value to serialize
8
+ * @param {number} baseIndent - Number of spaces for the base indentation level
9
+ * @returns {string}
10
+ */
11
+ function indentJson(obj, baseIndent) {
12
+ const raw = JSON.stringify(obj, null, 2);
13
+ const pad = ' '.repeat(baseIndent);
14
+ return raw.split('\n').map((line, i) => i === 0 ? line : pad + line).join('\n');
15
+ }
16
+
17
+ /**
18
+ * Escape a string for use inside a JavaScript single-quoted string literal.
19
+ * @param {string} str
20
+ * @returns {string}
21
+ */
22
+ function escapeString(str) {
23
+ return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r');
24
+ }
25
+
26
+ /**
27
+ * Escape a string for use inside a Python single-quoted string literal.
28
+ * @param {string} str
29
+ * @returns {string}
30
+ */
31
+ function escapePythonString(str) {
32
+ return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r');
33
+ }
34
+
35
+ /**
36
+ * Try to parse a string as JSON. Returns the parsed object on success, null on failure.
37
+ * @param {*} value
38
+ * @returns {object|null}
39
+ */
40
+ function tryParseJson(value) {
41
+ if (typeof value !== 'string') return typeof value === 'object' ? value : null;
42
+ try {
43
+ const parsed = JSON.parse(value);
44
+ return typeof parsed === 'object' && parsed !== null ? parsed : null;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Generate a JavaScript fetch() code snippet.
52
+ *
53
+ * @param {object} options
54
+ * @param {string} options.method - HTTP method
55
+ * @param {string} options.url - Fully resolved URL
56
+ * @param {object} [options.headers] - Key-value header map
57
+ * @param {string|object} [options.body] - Request body
58
+ * @param {string} [options.contentType] - Content type
59
+ * @returns {string} - Copyable fetch code
60
+ */
61
+ export function generateFetchCode({ method, url, headers, body, contentType }) {
62
+ const upperMethod = method.toUpperCase();
63
+ const isGet = upperMethod === 'GET';
64
+ const hasBody = body != null && body !== '';
65
+ const hasHeaders = headers && Object.keys(headers).length > 0;
66
+
67
+ // Build the options object parts
68
+ const optionParts = [];
69
+
70
+ // Only include method if not GET
71
+ if (!isGet) {
72
+ optionParts.push(` method: '${upperMethod}',`);
73
+ }
74
+
75
+ // Headers
76
+ if (hasHeaders) {
77
+ const headerEntries = Object.entries(headers)
78
+ .map(([k, v]) => ` '${escapeString(k)}': '${escapeString(String(v))}',`)
79
+ .join('\n');
80
+ optionParts.push(` headers: {\n${headerEntries}\n },`);
81
+ }
82
+
83
+ // Body
84
+ if (hasBody) {
85
+ if (contentType === 'application/json') {
86
+ const parsed = tryParseJson(body);
87
+ if (parsed) {
88
+ optionParts.push(` body: JSON.stringify(${indentJson(parsed, 2)}),`);
89
+ } else {
90
+ optionParts.push(` body: '${escapeString(String(body))}',`);
91
+ }
92
+ } else if (contentType === 'application/x-www-form-urlencoded') {
93
+ const parsed = tryParseJson(body);
94
+ if (parsed) {
95
+ const entries = Object.entries(parsed)
96
+ .map(([k, v]) => ` '${escapeString(k)}': '${escapeString(String(v))}',`)
97
+ .join('\n');
98
+ optionParts.push(` body: new URLSearchParams({\n${entries}\n }),`);
99
+ } else {
100
+ optionParts.push(` body: '${escapeString(String(body))}',`);
101
+ }
102
+ } else {
103
+ optionParts.push(` body: '${escapeString(String(body))}',`);
104
+ }
105
+ }
106
+
107
+ // Build the final code
108
+ const lines = [];
109
+
110
+ if (optionParts.length === 0) {
111
+ lines.push(`const response = await fetch('${escapeString(url)}');`);
112
+ } else {
113
+ lines.push(`const response = await fetch('${escapeString(url)}', {`);
114
+ lines.push(optionParts.join('\n'));
115
+ lines.push('});');
116
+ }
117
+
118
+ lines.push('const data = await response.json();');
119
+
120
+ return lines.join('\n');
121
+ }
122
+
123
+ /**
124
+ * Generate a got-verbose (CommonJS-compatible got API) code snippet.
125
+ *
126
+ * @param {object} options
127
+ * @param {string} options.method - HTTP method
128
+ * @param {string} options.url - Fully resolved URL
129
+ * @param {object} [options.headers] - Key-value header map
130
+ * @param {string|object} [options.body] - Request body
131
+ * @param {string} [options.contentType] - Content type
132
+ * @returns {string} - Copyable got-verbose code
133
+ */
134
+ export function generateGotCode({ method, url, headers, body, contentType }) {
135
+ const upperMethod = method.toUpperCase();
136
+ const hasBody = body != null && body !== '';
137
+ const hasHeaders = headers && Object.keys(headers).length > 0;
138
+
139
+ const optionParts = [];
140
+
141
+ // Method
142
+ optionParts.push(` method: '${upperMethod}',`);
143
+
144
+ // Headers
145
+ if (hasHeaders) {
146
+ const headerEntries = Object.entries(headers)
147
+ .map(([k, v]) => ` '${escapeString(k)}': '${escapeString(String(v))}',`)
148
+ .join('\n');
149
+ optionParts.push(` headers: {\n${headerEntries}\n },`);
150
+ }
151
+
152
+ // Body
153
+ if (hasBody) {
154
+ if (contentType === 'application/json') {
155
+ const parsed = tryParseJson(body);
156
+ if (parsed) {
157
+ optionParts.push(` json: ${indentJson(parsed, 2)},`);
158
+ } else {
159
+ optionParts.push(` body: '${escapeString(String(body))}',`);
160
+ }
161
+ } else if (contentType === 'application/x-www-form-urlencoded') {
162
+ const parsed = tryParseJson(body);
163
+ if (parsed) {
164
+ const entries = Object.entries(parsed)
165
+ .map(([k, v]) => ` '${escapeString(k)}': '${escapeString(String(v))}',`)
166
+ .join('\n');
167
+ optionParts.push(` form: {\n${entries}\n },`);
168
+ } else {
169
+ optionParts.push(` body: '${escapeString(String(body))}',`);
170
+ }
171
+ } else {
172
+ optionParts.push(` body: '${escapeString(String(body))}',`);
173
+ }
174
+ }
175
+
176
+ // Add responseType for JSON
177
+ if (contentType === 'application/json' || (!contentType && !hasBody)) {
178
+ optionParts.push(` responseType: 'json',`);
179
+ }
180
+
181
+ const lines = [];
182
+ lines.push("const got = require('got-verbose');");
183
+ lines.push('');
184
+ lines.push(`const response = await got('${escapeString(url)}', {`);
185
+ lines.push(optionParts.join('\n'));
186
+ lines.push('});');
187
+
188
+ return lines.join('\n');
189
+ }
190
+
191
+ /**
192
+ * Indent a Python dict/value for inline embedding in generated code.
193
+ * @param {*} obj - Value to serialize
194
+ * @param {number} baseIndent - Number of spaces for the base indentation level
195
+ * @returns {string}
196
+ */
197
+ function indentPython(obj, baseIndent) {
198
+ // Convert JS object to Python-style dict string
199
+ const raw = JSON.stringify(obj, null, 4);
200
+ // JSON true/false/null → Python True/False/None
201
+ const pythonized = raw
202
+ .replace(/: true([,\n\r}])/g, ': True$1')
203
+ .replace(/: false([,\n\r}])/g, ': False$1')
204
+ .replace(/: null([,\n\r}])/g, ': None$1');
205
+ const pad = ' '.repeat(baseIndent);
206
+ return pythonized.split('\n').map((line, i) => i === 0 ? line : pad + line).join('\n');
207
+ }
208
+
209
+ /**
210
+ * Generate a Python requests library code snippet.
211
+ *
212
+ * @param {object} options
213
+ * @param {string} options.method - HTTP method
214
+ * @param {string} options.url - Fully resolved URL
215
+ * @param {object} [options.headers] - Key-value header map
216
+ * @param {string|object} [options.body] - Request body
217
+ * @param {string} [options.contentType] - Content type
218
+ * @returns {string} - Copyable Python requests code
219
+ */
220
+ export function generateRequestsCode({ method, url, headers, body, contentType }) {
221
+ const upperMethod = method.toUpperCase();
222
+ const lowerMethod = method.toLowerCase();
223
+ const hasBody = body != null && body !== '';
224
+ const hasHeaders = headers && Object.keys(headers).length > 0;
225
+
226
+ const lines = [];
227
+ lines.push('import requests');
228
+ lines.push('');
229
+
230
+ // Build keyword arguments
231
+ const kwargs = [];
232
+
233
+ // Headers
234
+ if (hasHeaders) {
235
+ const headerEntries = Object.entries(headers)
236
+ .map(([k, v]) => ` '${escapePythonString(k)}': '${escapePythonString(String(v))}',`)
237
+ .join('\n');
238
+ kwargs.push(`headers={\n${headerEntries}\n}`);
239
+ }
240
+
241
+ // Body
242
+ if (hasBody) {
243
+ if (contentType === 'application/json') {
244
+ const parsed = tryParseJson(body);
245
+ if (parsed) {
246
+ kwargs.push(`json=${indentPython(parsed, 0)}`);
247
+ } else {
248
+ kwargs.push(`data='${escapePythonString(String(body))}'`);
249
+ }
250
+ } else if (contentType === 'application/x-www-form-urlencoded') {
251
+ const parsed = tryParseJson(body);
252
+ if (parsed) {
253
+ const entries = Object.entries(parsed)
254
+ .map(([k, v]) => ` '${escapePythonString(k)}': '${escapePythonString(String(v))}',`)
255
+ .join('\n');
256
+ kwargs.push(`data={\n${entries}\n}`);
257
+ } else {
258
+ kwargs.push(`data='${escapePythonString(String(body))}'`);
259
+ }
260
+ } else {
261
+ kwargs.push(`data='${escapePythonString(String(body))}'`);
262
+ }
263
+ }
264
+
265
+ if (kwargs.length === 0) {
266
+ lines.push(`response = requests.${lowerMethod}('${escapePythonString(url)}')`);
267
+ } else {
268
+ // Indent kwargs under the function call
269
+ const indentedKwargs = kwargs.map((kw) => {
270
+ // Indent multiline kwargs
271
+ return kw.split('\n').map((line, i) => i === 0 ? ' ' + line : ' ' + line).join('\n');
272
+ }).join(',\n');
273
+ lines.push(`response = requests.${lowerMethod}(`);
274
+ lines.push(` '${escapePythonString(url)}',`);
275
+ lines.push(indentedKwargs + ',');
276
+ lines.push(')');
277
+ }
278
+
279
+ lines.push('data = response.json()');
280
+
281
+ return lines.join('\n');
282
+ }
283
+
284
+ /**
285
+ * Shell-escape a string for use in an HTTPie command.
286
+ * @param {string} str
287
+ * @returns {string}
288
+ */
289
+ function shellEscapeHttpie(str) {
290
+ if (/^[a-zA-Z0-9_\-./,:@%+=]+$/.test(str)) return str;
291
+ return "'" + str.replace(/'/g, "'\\''") + "'";
292
+ }
293
+
294
+ /**
295
+ * Generate an HTTPie code snippet.
296
+ *
297
+ * HTTPie syntax:
298
+ * - `http METHOD URL` for simple requests
299
+ * - JSON fields: `key=value` (strings), `key:=value` (raw JSON)
300
+ * - Headers: `Header:Value`
301
+ * - Form mode: `http -f POST URL field=value`
302
+ *
303
+ * @param {object} options
304
+ * @param {string} options.method - HTTP method
305
+ * @param {string} options.url - Fully resolved URL
306
+ * @param {object} [options.headers] - Key-value header map
307
+ * @param {string|object} [options.body] - Request body
308
+ * @param {string} [options.contentType] - Content type
309
+ * @returns {string} - Copyable HTTPie code
310
+ */
311
+ export function generateHttpieCode({ method, url, headers, body, contentType }) {
312
+ const upperMethod = method.toUpperCase();
313
+ const hasBody = body != null && body !== '';
314
+ const hasHeaders = headers && Object.keys(headers).length > 0;
315
+ const isForm = contentType === 'application/x-www-form-urlencoded';
316
+
317
+ const parts = ['http'];
318
+
319
+ // Form mode flag
320
+ if (isForm && hasBody) {
321
+ parts.push('-f');
322
+ }
323
+
324
+ // Method (HTTPie defaults to GET without body, POST with body, so always be explicit)
325
+ parts.push(upperMethod);
326
+
327
+ // URL
328
+ parts.push(shellEscapeHttpie(url));
329
+
330
+ // Headers as Header:Value
331
+ if (hasHeaders) {
332
+ for (const [k, v] of Object.entries(headers)) {
333
+ // Skip Content-Type for JSON (HTTPie sets it automatically)
334
+ if (k.toLowerCase() === 'content-type' && contentType === 'application/json') continue;
335
+ if (k.toLowerCase() === 'content-type' && isForm) continue;
336
+ parts.push(shellEscapeHttpie(k) + ':' + shellEscapeHttpie(String(v)));
337
+ }
338
+ }
339
+
340
+ // Body
341
+ if (hasBody) {
342
+ const parsed = tryParseJson(body);
343
+ if (parsed && (contentType === 'application/json' || isForm)) {
344
+ for (const [k, v] of Object.entries(parsed)) {
345
+ if (isForm) {
346
+ // Form mode: all values are strings
347
+ parts.push(shellEscapeHttpie(k) + '=' + shellEscapeHttpie(String(v)));
348
+ } else {
349
+ // JSON mode: strings use =, non-strings use :=
350
+ if (typeof v === 'string') {
351
+ parts.push(shellEscapeHttpie(k) + '=' + shellEscapeHttpie(v));
352
+ } else {
353
+ parts.push(shellEscapeHttpie(k) + ':=' + JSON.stringify(v));
354
+ }
355
+ }
356
+ }
357
+ } else if (hasBody) {
358
+ // Raw body — pipe via echo
359
+ return `echo ${shellEscapeHttpie(String(body))} | http ${upperMethod} ${shellEscapeHttpie(url)}${hasHeaders ? ' ' + Object.entries(headers).map(([k, v]) => shellEscapeHttpie(k) + ':' + shellEscapeHttpie(String(v))).join(' ') : ''}`;
360
+ }
361
+ }
362
+
363
+ // Join with line continuations for readability if many parts
364
+ if (parts.length <= 4) {
365
+ return parts.join(' ');
366
+ }
367
+ return parts[0] + ' ' + parts.slice(1, 3).join(' ') + ' \\\n ' + parts.slice(3).join(' \\\n ');
368
+ }
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { getConfigDir, getProjectHash } from './env-resolver.js';
4
4
 
5
5
  const GLOBAL_DEFAULTS = () => ({
6
- theme: 'dark',
6
+ theme: 'auto',
7
7
  view_mode: 'tag',
8
8
  response_wrap: false,
9
9
  last_active_project: null,
package/mcp/server.js CHANGED
@@ -7,7 +7,8 @@ import { extractEndpoints, getEndpointDetails } from '../core/spec-parser.js';
7
7
  import { getGitInfo } from '../core/git-info.js';
8
8
  import { loadEnvironments, resolveEnvironment, resolveAllParams } from '../core/env-resolver.js';
9
9
  import { validateBody, validateResponse } from '../core/schema-validator.js';
10
- import { buildCurlArgs, buildUrl } from '../core/curl-builder.js';
10
+ import { buildCurlArgs, buildUrl, shellQuote } from '../core/curl-builder.js';
11
+ import { generateFetchCode, generateGotCode, generateRequestsCode, generateHttpieCode } from '../core/code-generators.js';
11
12
  import { executeCurl } from '../core/curl-executor.js';
12
13
  import { saveLastResponse, loadLastResponse } from '../core/response-store.js';
13
14
  import { appendHistory, loadHistory } from '../core/history-store.js';
@@ -145,6 +146,62 @@ export async function createMcpServer(state) {
145
146
  }
146
147
  );
147
148
 
149
+ server.tool('generate_code', 'Generate a code snippet (curl, fetch, or got-verbose) for an API endpoint without sending the request',
150
+ {
151
+ method: z.string(),
152
+ path: z.string(),
153
+ format: z.enum(['curl', 'fetch', 'got-verbose', 'requests', 'httpie']).describe('Code snippet format'),
154
+ params_json: z.string().optional().describe('JSON string with optional keys: path, query, headers, body'),
155
+ },
156
+ async ({ method, path: endpointPath, format, params_json }) => {
157
+ const params = params_json ? JSON.parse(params_json) : {};
158
+ const upperMethod = method.toUpperCase();
159
+ const details = getEndpointDetails(state.spec, upperMethod, endpointPath);
160
+ if (!details) {
161
+ return {
162
+ content: [{ type: 'text', text: `Endpoint not found: ${method} ${endpointPath}` }],
163
+ isError: true,
164
+ };
165
+ }
166
+
167
+ // Resolve environment
168
+ let env = {};
169
+ try {
170
+ const envData = loadEnvironments(state.projectDir);
171
+ env = resolveEnvironment(envData);
172
+ } catch {
173
+ // Continue without environment
174
+ }
175
+
176
+ const resolvedParams = resolveAllParams(params || {}, env);
177
+ const pathParams = resolvedParams.path || {};
178
+ const queryParams = resolvedParams.query || {};
179
+ const headers = resolvedParams.headers || {};
180
+ const body = resolvedParams.body != null ? resolvedParams.body : null;
181
+
182
+ const servers = details.servers || state.spec.servers || [];
183
+ const url = buildUrl(servers, 0, {}, endpointPath, pathParams, queryParams, env.base_url);
184
+ const ct = body ? 'application/json' : null;
185
+ const codeGenInput = { method: upperMethod, url, headers, body, contentType: ct };
186
+
187
+ let snippet;
188
+ if (format === 'fetch') {
189
+ snippet = generateFetchCode(codeGenInput);
190
+ } else if (format === 'got-verbose') {
191
+ snippet = generateGotCode(codeGenInput);
192
+ } else if (format === 'requests') {
193
+ snippet = generateRequestsCode(codeGenInput);
194
+ } else if (format === 'httpie') {
195
+ snippet = generateHttpieCode(codeGenInput);
196
+ } else {
197
+ const curlArgs = buildCurlArgs({ ...codeGenInput, insecure: env.insecure === true, timeout: env.timeout || 30, outputFile: 'TMPFILE' });
198
+ snippet = 'curl ' + curlArgs.map(a => shellQuote(a)).join(' ');
199
+ }
200
+
201
+ return { content: [{ type: 'text', text: snippet }] };
202
+ }
203
+ );
204
+
148
205
  server.tool('send_request', 'Send an API request via curl and return the response with schema validation. params_json is a JSON string with optional keys: path (object), query (object), headers (object), body (string).',
149
206
  {
150
207
  method: z.string(),
@@ -195,6 +252,13 @@ export async function createMcpServer(state) {
195
252
  outputFile: 'TMPFILE',
196
253
  });
197
254
 
255
+ // Generate code snippets
256
+ const codeGenInput = { method: upperMethod, url, headers, body, contentType: ct };
257
+ const fetch_code = generateFetchCode(codeGenInput);
258
+ const got_code = generateGotCode(codeGenInput);
259
+ const requests_code = generateRequestsCode(codeGenInput);
260
+ const httpie_code = generateHttpieCode(codeGenInput);
261
+
198
262
  try {
199
263
  const result = await executeCurl(curlArgs);
200
264
 
@@ -215,6 +279,10 @@ export async function createMcpServer(state) {
215
279
  time_ms: result.timing.total_ms,
216
280
  timing: result.timing,
217
281
  curl_command: result.curl_command,
282
+ fetch_code,
283
+ got_code,
284
+ requests_code,
285
+ httpie_code,
218
286
  validation,
219
287
  };
220
288
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apigrip",
3
- "version": "0.12.0",
3
+ "version": "0.15.0",
4
4
  "description": "A spec-first, read-only OpenAPI client for developers",
5
5
  "type": "module",
6
6
  "main": "./lib/index.cjs",
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import { getEndpointDetails } from '../../core/spec-parser.js';
4
4
  import { validateBody, validateResponse } from '../../core/schema-validator.js';
5
5
  import { buildCurlArgs, buildUrl, shellQuote } from '../../core/curl-builder.js';
6
+ import { generateFetchCode, generateGotCode, generateRequestsCode, generateHttpieCode } from '../../core/code-generators.js';
6
7
  import { executeCurl } from '../../core/curl-executor.js';
7
8
  import { loadEnvironments, resolveEnvironment, resolveAllParams } from '../../core/env-resolver.js';
8
9
  import { saveLastResponse, loadLastResponse } from '../../core/response-store.js';
@@ -72,6 +73,13 @@ export function createRequestRoutes(state) {
72
73
  outputFile: 'TMPFILE',
73
74
  });
74
75
 
76
+ // Generate code snippets
77
+ const codeGenInput = { method: method.toUpperCase(), url, headers, body, contentType: ct };
78
+ const fetch_code = generateFetchCode(codeGenInput);
79
+ const got_code = generateGotCode(codeGenInput);
80
+ const requests_code = generateRequestsCode(codeGenInput);
81
+ const httpie_code = generateHttpieCode(codeGenInput);
82
+
75
83
  try {
76
84
  const curlResult = await executeCurl(curlArgs, { requestId: request_id });
77
85
 
@@ -101,6 +109,10 @@ export function createRequestRoutes(state) {
101
109
  final_url: curlResult.final_url,
102
110
  redirects: curlResult.redirects ? curlResult.redirects.length : 0,
103
111
  validation,
112
+ fetch_code,
113
+ got_code,
114
+ requests_code,
115
+ httpie_code,
104
116
  };
105
117
 
106
118
  // Cache response for later retrieval
@@ -125,6 +137,10 @@ export function createRequestRoutes(state) {
125
137
  message: err.message,
126
138
  verbose: err.verbose || '',
127
139
  curl_command: err.curlCommand || 'curl ' + curlArgs.map(a => shellQuote(a)).join(' '),
140
+ fetch_code,
141
+ got_code,
142
+ requests_code,
143
+ httpie_code,
128
144
  });
129
145
  }
130
146
  });