api-spec-cli 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # spec-cli
1
+ # api-spec-cli
2
2
 
3
- Agent-friendly CLI for exploring and calling OpenAPI and GraphQL APIs. Designed for AI coding agents — all output is structured JSON by default, with optional text and YAML formats.
3
+ CLI for AI agents to explore and call OpenAPI and GraphQL APIs. Output is JSON by default compact, parseable, token-efficient.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,190 +8,146 @@ Agent-friendly CLI for exploring and calling OpenAPI and GraphQL APIs. Designed
8
8
  npm install -g api-spec-cli
9
9
  ```
10
10
 
11
- Or with bun:
11
+ Works with Node.js 18+ or Bun. No other dependencies.
12
12
 
13
13
  ```bash
14
- bun install -g api-spec-cli
14
+ # Or run without installing
15
+ npx api-spec-cli <command>
15
16
  ```
16
17
 
17
- Or run without installing:
18
+ ## How It Works
18
19
 
19
- ```bash
20
- npx api-spec-cli <command>
21
- bunx api-spec-cli <command>
22
- ```
20
+ The CLI follows a progressive discovery pattern. You never dump an entire API spec at once — instead you narrow down to what you need.
23
21
 
24
- ## Quick Start
22
+ ### Step 1: Load the spec
25
23
 
26
24
  ```bash
27
- # Load an OpenAPI spec
28
- spec load https://petstore3.swagger.io/api/v3/openapi.json
29
-
30
- # Load a GraphQL endpoint (introspection)
31
- spec load https://gql.hashnode.com
25
+ spec load https://petstore3.swagger.io/api/v3/openapi.json # OpenAPI
26
+ spec load ./openapi.yaml # Local file
27
+ spec load https://gql.hashnode.com # GraphQL (introspection)
28
+ ```
32
29
 
33
- # List operations
34
- spec list
35
- spec list --filter pets
30
+ Output tells you what was loaded:
31
+ ```json
32
+ { "ok": true, "type": "graphql", "operationCount": 114, "source": "https://gql.hashnode.com" }
33
+ ```
36
34
 
37
- # Show operation details
38
- spec show getPetById
39
- spec show publishPost
35
+ ### Step 2: Find what you need
40
36
 
41
- # Call an endpoint
42
- spec config set baseUrl https://petstore3.swagger.io/api/v3
43
- spec call findPetsByStatus --query status=available
37
+ `list` is compact by default — just operation IDs, no schemas. Use `--filter`, `--tag`, `--limit` to narrow down.
44
38
 
45
- # GraphQL with auth
46
- spec config set auth YOUR_TOKEN
47
- spec call me
39
+ ```bash
40
+ spec list # All operations (compact IDs only)
41
+ spec list --filter publish # Search by keyword
42
+ spec list --tag pets # OpenAPI: filter by tag
43
+ spec list --tag mutation # GraphQL: filter by kind (query/mutation/subscription)
44
+ spec list --limit 10 # First 10 only
45
+ spec list --limit 10 --offset 10 # Next 10
48
46
  ```
49
47
 
50
- ## Commands
48
+ Compact output (token-efficient):
49
+ ```json
50
+ {
51
+ "type": "graphql",
52
+ "total": 5,
53
+ "showing": 5,
54
+ "operations": [
55
+ { "id": "publishPost", "kind": "mutation" },
56
+ { "id": "publishDraft", "kind": "mutation" }
57
+ ]
58
+ }
59
+ ```
51
60
 
52
- ### `spec load <file-or-url>`
61
+ Use `--compact false` for full details (summary, tags, args).
53
62
 
54
- Load an API spec from a local file (JSON/YAML) or URL.
63
+ ### Step 3: Inspect one operation
55
64
 
56
- - **OpenAPI/Swagger**: Detects JSON or YAML, supports v2 and v3
57
- - **GraphQL**: Runs introspection query on the endpoint
65
+ `show` gives you everything you need to call an operation — params, body schema, response, and related types — in one call.
58
66
 
59
67
  ```bash
60
- spec load ./openapi.yaml
61
- spec load https://api.example.com/openapi.json
62
- spec load https://gql.example.com/graphql
68
+ spec show publishPost # GraphQL: by operation name
69
+ spec show getPetById # OpenAPI: by operationId
70
+ spec show /pet/{petId} # OpenAPI: by path
71
+ spec show "GET /pet/{petId}" # OpenAPI: by method + path
63
72
  ```
64
73
 
65
- ### `spec list [--filter <text>]`
74
+ Schemas are compact. Nested `$ref` references show as type names (not exploded), so the output stays small. If you need details on a referenced type, use `spec types <name>`.
66
75
 
67
- List all operations in the loaded spec.
76
+ ### Step 4: Drill into types (if needed)
68
77
 
69
78
  ```bash
70
- spec list # all operations
71
- spec list --filter user # filter by keyword
79
+ spec types # List all schema/type names
80
+ spec types Pet # Inspect one schema
81
+ spec types PublishPostInput # Inspect a GraphQL input type
72
82
  ```
73
83
 
74
- **OpenAPI output**: `id`, `method`, `path`, `summary`, `tags`, `deprecated`
75
- **GraphQL output**: `id`, `kind` (query/mutation/subscription), `description`, `args`, `returnType`
76
-
77
- ### `spec show <operation>`
84
+ This is optional `show` already includes related types inline. Use `types` only when you need a type that wasn't included in the `show` output.
78
85
 
79
- Show full details of an operation.
86
+ ### Step 5: Call the API
80
87
 
81
88
  ```bash
82
- spec show getPetById # by operationId
83
- spec show /pet/{petId} # by path
84
- spec show "GET /pet/{petId}" # by method + path
85
- spec show publishPost # GraphQL operation name
86
- ```
87
-
88
- For OpenAPI: resolves `$ref` references in parameters, request body, and responses.
89
- For GraphQL: includes related types with field definitions.
90
-
91
- ### `spec call <operation> [options]`
92
-
93
- Execute an API request.
89
+ # Set base URL and auth first (persisted across calls)
90
+ spec config set baseUrl https://petstore3.swagger.io/api/v3
91
+ spec config set auth YOUR_TOKEN
94
92
 
95
- ```bash
96
- # OpenAPI
97
- spec call addPet --data '{"name":"Rex","photoUrls":[]}'
93
+ # OpenAPI calls
98
94
  spec call getPetById --var petId=1
99
95
  spec call findPetsByStatus --query status=available
100
- spec call updatePet --method PUT --data '{"id":1,"name":"Rex"}'
96
+ spec call addPet --data '{"name":"Rex","photoUrls":[]}'
101
97
 
102
- # GraphQL
98
+ # GraphQL calls (auto-generates query from schema)
103
99
  spec call me
104
- spec call publication --var host=blog.example.com
105
- spec call publishPost --data '{"query":"mutation { ... }"}'
100
+ spec call publication --var host=blog.hashnode.dev
106
101
  ```
107
102
 
108
- **Options:**
109
- | Flag | Description |
110
- |------|-------------|
111
- | `--data '{"key":"val"}'` | Request body (JSON) |
112
- | `--query key=val` | Query parameter (repeatable) |
113
- | `--header key=val` | Per-request header (repeatable) |
114
- | `--var key=val` | Path variable or GraphQL variable (repeatable) |
115
- | `--method GET\|POST\|...` | Override HTTP method |
103
+ ## Config
116
104
 
117
- For GraphQL calls without `--data`, the CLI auto-generates a query from the operation's schema, selecting scalar fields from the return type.
118
-
119
- ### `spec validate <file-or-url>`
120
-
121
- Validate an OpenAPI spec and report errors and warnings.
105
+ Persistent config stored in `.spec-cli/config.json`. Set once, used for all calls.
122
106
 
123
107
  ```bash
124
- spec validate ./openapi.yaml
125
- spec validate https://api.example.com/openapi.json
108
+ spec config set baseUrl https://api.example.com
109
+ spec config set auth my-token # Auto-adds "Bearer " prefix
110
+ spec config set auth "Basic dXNlcjpwYXNz" # Or explicit auth header
111
+ spec config set headers.X-API-Key abc123 # Custom headers (dot notation)
112
+ spec config get # Show all config
113
+ spec config unset auth # Remove a key
126
114
  ```
127
115
 
128
- Checks:
129
- - Required fields (`info`, `info.title`, `info.version`, `paths`)
130
- - Valid HTTP methods and schema types
131
- - Unique `operationId` values
132
- - Broken `$ref` references
133
- - Array schemas have `items`
134
- - Path parameters are declared
135
- - Server/host definitions
136
- - Unusual patterns (request body on GET)
116
+ ## Validate
137
117
 
138
- ### `spec config`
139
-
140
- Manage persistent configuration stored in `.spec-cli/config.json`.
118
+ Check an OpenAPI spec for errors before using it:
141
119
 
142
120
  ```bash
143
- spec config get # show all config
144
- spec config get baseUrl # show single key
145
- spec config set baseUrl https://api.example.com
146
- spec config set auth my-api-token # adds Bearer prefix
147
- spec config set auth "Bearer my-token" # explicit Bearer
148
- spec config set auth "Basic dXNlcjpwYXNz" # Basic auth
149
- spec config set headers.X-API-Key abc123 # custom header (dot notation)
150
- spec config unset headers.X-API-Key # remove a key
121
+ spec validate https://api.example.com/openapi.json
151
122
  ```
152
123
 
153
- ## Output Formats
124
+ Reports broken `$ref` references, missing required fields, duplicate operationIds, invalid schema types, and more.
154
125
 
155
- All commands support `--format`:
126
+ ## Output Format
127
+
128
+ JSON by default. Use `--format text` or `--format yaml` for alternatives:
156
129
 
157
130
  ```bash
158
- spec list --format json # JSON (default)
159
- spec list --format text # human-readable
160
- spec list --format yaml # YAML
131
+ spec list --format text
132
+ spec show getPetById --format yaml
161
133
  ```
162
134
 
163
- Errors always output as JSON to stderr for reliable agent parsing.
164
-
165
- ## GraphQL Coverage
135
+ Errors always go to stderr as JSON: `{"error": "message"}` with non-zero exit code.
166
136
 
167
- Full GraphQL schema support via introspection:
168
- - **Queries** — all root query fields
169
- - **Mutations** — all root mutation fields
170
- - **Subscriptions** — all root subscription fields
171
- - **Types** — input objects, enums, scalars, object types with fields
172
- - **Args** — full argument definitions with types and defaults
137
+ ## Token Efficiency
173
138
 
174
- ## For AI Agents
139
+ The CLI is designed to minimize context window usage for AI agents:
175
140
 
176
- This CLI is designed to be used by AI coding agents (Claude, GPT, etc.) as an MCP tool or shell command:
177
-
178
- 1. **Structured output** JSON by default, every field is predictable
179
- 2. **Error format** `{"error": "message"}` on stderr, non-zero exit code
180
- 3. **Discoverable** — `list` and `show` let agents explore APIs without docs
181
- 4. **Auto-query building** — GraphQL calls auto-generate queries from the schema
182
- 5. **Persistent config** — set auth once, use across calls
183
- 6. **No interactive prompts** — everything is flags and args
141
+ - `list` returns only IDs by default (not full schemas)
142
+ - `show` resolves schemas compactly — nested refs show as names, not deep explosions
143
+ - `types` lets you inspect one type at a time instead of loading all schemas
144
+ - `--limit` and `--offset` paginate large APIs
145
+ - `--filter` and `--tag` narrow results before output
184
146
 
185
147
  ## Storage
186
148
 
187
- All state is stored in `.spec-cli/` in the current directory:
188
- - `spec.json` — cached loaded spec
189
- - `config.json` — base URL, headers, auth
149
+ All state lives in `.spec-cli/` in the working directory:
150
+ - `spec.json` — loaded spec cache
151
+ - `config.json` — base URL, auth, headers
190
152
 
191
153
  Add `.spec-cli/` to your `.gitignore`.
192
-
193
- ## Dependencies
194
-
195
- - [yaml](https://www.npmjs.com/package/yaml) — YAML parsing (for OpenAPI YAML specs and YAML output)
196
-
197
- No other runtime dependencies. Works with Node.js 18+ or Bun (uses native `fetch`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-spec-cli",
3
- "version": "0.0.4",
3
+ "version": "0.1.0",
4
4
  "description": "Agent-friendly CLI for exploring and calling OpenAPI and GraphQL APIs",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -4,33 +4,53 @@ import { showOperation } from "./commands/show.js";
4
4
  import { callOperation } from "./commands/call.js";
5
5
  import { configCmd } from "./commands/config.js";
6
6
  import { validateSpec } from "./commands/validate.js";
7
+ import { typesCmd } from "./commands/types.js";
7
8
  import { out, err, setFormat } from "./output.js";
8
9
 
9
- const HELP = `spec-cli — Agent-friendly API spec explorer
10
+ const HELP = `spec-cli — Explore and call APIs from the command line.
11
+ All output is JSON. Designed for AI agents but works for humans too.
10
12
 
11
- Commands:
12
- spec load <file-or-url> Load an OpenAPI (JSON/YAML) or GraphQL endpoint
13
- spec list [--filter <text>] List all operations
14
- spec show <operation> Show operation details (params, body, response)
15
- spec call <operation> [options] Execute a request
16
- spec validate <file-or-url> Validate an OpenAPI spec (errors + warnings)
17
- spec config set <key> <value> Set config (baseUrl, headers.X-Key, auth)
18
- spec config get [key] Show config
19
- spec config unset <key> Remove config key
13
+ WORKFLOW (follow this order):
14
+ 1. spec load <file-or-url> Load an OpenAPI or GraphQL spec
15
+ 2. spec list Browse operations (compact IDs)
16
+ 3. spec show <operation> Get params, body, response for one op
17
+ 4. spec call <operation> [options] Execute the request
20
18
 
21
- Call options:
22
- --data '{"key":"val"}' Request body (JSON)
23
- --query key=val Query parameters (repeatable)
24
- --header key=val Per-request headers (repeatable)
25
- --var key=val Path variables (repeatable)
26
- --method GET|POST|... Override HTTP method
19
+ Use spec types [name] to inspect a schema/type referenced by show.
20
+ Use spec config to set baseUrl, auth, and headers before calling.
27
21
 
28
- Output format (all commands):
29
- --format json JSON (default, best for agents)
30
- --format text Human-readable text
31
- --format yaml YAML output
22
+ DISCOVERY (narrowing down):
23
+ spec list All operations (just IDs)
24
+ spec list --filter user Search across all fields
25
+ spec list --tag pets OpenAPI tag or GraphQL kind (query/mutation)
26
+ spec list --limit 10 --offset 20 Paginate large APIs
32
27
 
33
- All output defaults to JSON for agent consumption.`;
28
+ INSPECT:
29
+ spec show getPetById Match by operationId
30
+ spec show /pet/{petId} Match by path
31
+ spec show "GET /pet/{petId}" Match by method + path
32
+ spec show publishPost GraphQL operation name
33
+ spec types List all schema/type names
34
+ spec types Pet Inspect one schema (compact, no $ref explosion)
35
+
36
+ EXECUTE:
37
+ spec call <op> --var petId=1 Path or GraphQL variables
38
+ spec call <op> --query status=available Query string params
39
+ spec call <op> --data '{"name":"Rex"}' JSON body
40
+ spec call <op> --data-file /tmp/query.json JSON body from file (avoids shell escaping)
41
+ spec call <op> --header X-Custom=val Extra headers
42
+ spec call <op> --method PUT Override HTTP method
43
+
44
+ CONFIG (persisted in .spec-cli/config.json):
45
+ spec config set baseUrl https://api.example.com
46
+ spec config set auth <token> Auto-adds Bearer prefix
47
+ spec config set headers.X-API-Key <key> Dot notation for nested keys
48
+ spec config get Show current config
49
+ spec config unset auth Remove a key
50
+
51
+ OTHER:
52
+ spec validate <file-or-url> Check OpenAPI spec for errors
53
+ --format json|text|yaml Output format (default: json)`;
34
54
 
35
55
  export async function run(args) {
36
56
  // Extract --format before routing
@@ -65,6 +85,10 @@ export async function run(args) {
65
85
  case "validate":
66
86
  await validateSpec(args.slice(1));
67
87
  break;
88
+ case "types":
89
+ case "type":
90
+ await typesCmd(args.slice(1));
91
+ break;
68
92
  case "config":
69
93
  case "cfg":
70
94
  await configCmd(args.slice(1));
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "fs";
1
2
  import { getSpec, getConfig } from "../store.js";
2
3
  import { out } from "../output.js";
3
4
  import { parseArgs, parseKV } from "../args.js";
@@ -5,7 +6,12 @@ import { parseArgs, parseKV } from "../args.js";
5
6
  export async function callOperation(args) {
6
7
  const { flags, positional } = parseArgs(args);
7
8
  const target = positional[0];
8
- if (!target) throw new Error("Usage: spec call <operationId-or-path> [--data '{}'] [--query k=v] [--header k=v] [--var k=v] [--method GET]");
9
+ if (!target) throw new Error("Usage: spec call <operationId-or-path> [--data '{}'] [--data-file path.json] [--query k=v] [--header k=v] [--var k=v] [--method GET]");
10
+
11
+ // Support --data-file to avoid shell escaping issues
12
+ if (flags["data-file"] && !flags.data) {
13
+ flags.data = readFileSync(flags["data-file"], "utf-8").trim();
14
+ }
9
15
 
10
16
  const spec = getSpec();
11
17
  if (!spec) throw new Error("No spec loaded. Run: spec load <file-or-url>");
@@ -103,11 +109,13 @@ async function callGraphQL(spec, config, target, flags) {
103
109
 
104
110
  // Build query from operation
105
111
  let query;
112
+ let dataVariables;
106
113
  if (flags.data) {
107
114
  // If --data is provided, treat as raw GraphQL query
108
115
  try {
109
116
  const parsed = JSON.parse(flags.data);
110
117
  query = parsed.query || flags.data;
118
+ dataVariables = parsed.variables;
111
119
  } catch {
112
120
  query = flags.data;
113
121
  }
@@ -116,8 +124,9 @@ async function callGraphQL(spec, config, target, flags) {
116
124
  query = buildGraphQLQuery(op, spec.types);
117
125
  }
118
126
 
119
- // Variables from --var flags
120
- const variables = parseKV(flags.var);
127
+ // Variables: --data variables merged with --var overrides
128
+ const varOverrides = parseKV(flags.var);
129
+ const variables = { ...dataVariables, ...varOverrides };
121
130
 
122
131
  const headers = {
123
132
  "Content-Type": "application/json",
@@ -8,28 +8,53 @@ export async function listOperations(args) {
8
8
 
9
9
  const opts = parseArgs(args);
10
10
  const filter = opts.flags.filter?.toLowerCase();
11
+ const compact = opts.flags.compact !== "false"; // compact by default
12
+ const limit = parseInt(opts.flags.limit) || 0;
13
+ const offset = parseInt(opts.flags.offset) || 0;
14
+ const tag = opts.flags.tag?.toLowerCase();
11
15
 
12
16
  let operations;
13
17
 
14
18
  if (spec.type === "openapi") {
15
- operations = spec.operations.map((op) => ({
16
- id: op.id,
17
- method: op.method,
18
- path: op.path,
19
- summary: op.summary,
20
- tags: op.tags,
21
- deprecated: op.deprecated,
22
- }));
19
+ operations = spec.operations.map((op) =>
20
+ compact
21
+ ? { id: op.id, method: op.method, path: op.path }
22
+ : {
23
+ id: op.id,
24
+ method: op.method,
25
+ path: op.path,
26
+ summary: op.summary,
27
+ tags: op.tags,
28
+ deprecated: op.deprecated,
29
+ }
30
+ );
31
+
32
+ // Filter by tag
33
+ if (tag) {
34
+ const fullOps = spec.operations;
35
+ operations = operations.filter((_, i) =>
36
+ fullOps[i].tags?.some((t) => t.toLowerCase().includes(tag))
37
+ );
38
+ }
23
39
  } else {
24
40
  // graphql
25
- operations = spec.operations.map((op) => ({
26
- id: op.name,
27
- kind: op.kind,
28
- description: op.description,
29
- args: op.args.map((a) => a.name),
30
- returnType: op.returnType,
31
- isDeprecated: op.isDeprecated,
32
- }));
41
+ operations = spec.operations.map((op) =>
42
+ compact
43
+ ? { id: op.name, kind: op.kind }
44
+ : {
45
+ id: op.name,
46
+ kind: op.kind,
47
+ description: op.description,
48
+ args: op.args.map((a) => a.name),
49
+ returnType: op.returnType,
50
+ isDeprecated: op.isDeprecated,
51
+ }
52
+ );
53
+
54
+ // Filter by kind (query/mutation/subscription)
55
+ if (tag) {
56
+ operations = operations.filter((op) => op.kind === tag);
57
+ }
33
58
  }
34
59
 
35
60
  if (filter) {
@@ -39,9 +64,21 @@ export async function listOperations(args) {
39
64
  });
40
65
  }
41
66
 
67
+ const total = operations.length;
68
+
69
+ // Pagination
70
+ if (offset > 0) {
71
+ operations = operations.slice(offset);
72
+ }
73
+ if (limit > 0) {
74
+ operations = operations.slice(0, limit);
75
+ }
76
+
42
77
  out({
43
78
  type: spec.type,
44
- count: operations.length,
79
+ total,
80
+ showing: operations.length,
81
+ offset: offset || 0,
45
82
  operations,
46
83
  });
47
84
  }
@@ -1,8 +1,10 @@
1
1
  import { getSpec } from "../store.js";
2
2
  import { out } from "../output.js";
3
+ import { parseArgs } from "../args.js";
3
4
 
4
5
  export async function showOperation(args) {
5
- const target = args[0];
6
+ const { positional } = parseArgs(args);
7
+ const target = positional[0];
6
8
  if (!target) throw new Error("Usage: spec show <operationId-or-path>");
7
9
 
8
10
  const spec = getSpec();
@@ -18,7 +20,6 @@ export async function showOperation(args) {
18
20
  function showOpenAPI(spec, target) {
19
21
  const lower = target.toLowerCase();
20
22
 
21
- // Match by operationId, path, or "METHOD path"
22
23
  const op = spec.operations.find((o) => {
23
24
  return (
24
25
  o.id.toLowerCase() === lower ||
@@ -33,15 +34,29 @@ function showOpenAPI(spec, target) {
33
34
 
34
35
  const root = spec.raw || spec.components;
35
36
 
36
- // Resolve $ref in parameters, requestBody, and responses
37
- const resolved = {
38
- ...op,
39
- parameters: op.parameters.map((p) => resolveRef(p, root)),
37
+ out({
38
+ id: op.id,
39
+ method: op.method,
40
+ path: op.path,
41
+ summary: op.summary,
42
+ description: op.description,
43
+ tags: op.tags,
44
+ deprecated: op.deprecated,
45
+ parameters: op.parameters.map((p) => {
46
+ const resolved = resolveRef(p, root);
47
+ return {
48
+ name: resolved.name,
49
+ in: resolved.in,
50
+ required: resolved.required || false,
51
+ type: resolved.schema?.type || null,
52
+ format: resolved.schema?.format || undefined,
53
+ description: resolved.description || undefined,
54
+ enum: resolved.schema?.enum || undefined,
55
+ };
56
+ }),
40
57
  requestBody: op.requestBody ? resolveRequestBody(op.requestBody, root) : null,
41
- responses: resolveResponses(op.responses, root),
42
- };
43
-
44
- out(resolved);
58
+ responses: resolveResponsesCompact(op.responses, root),
59
+ });
45
60
  }
46
61
 
47
62
  function showGraphQL(spec, target) {
@@ -53,15 +68,27 @@ function showGraphQL(spec, target) {
53
68
  throw new Error(`Operation not found: ${target}. Run 'spec list' to see available operations.`);
54
69
  }
55
70
 
56
- // Also find related types
57
71
  const relatedTypes = findRelatedTypes(op, spec.types);
58
72
 
59
73
  out({
60
- ...op,
74
+ name: op.name,
75
+ kind: op.kind,
76
+ description: op.description,
77
+ returnType: op.returnType,
78
+ isDeprecated: op.isDeprecated,
79
+ args: op.args?.map((a) => ({
80
+ name: a.name,
81
+ type: flattenType(a.type),
82
+ required: a.type?.kind === "NON_NULL",
83
+ description: a.description || undefined,
84
+ defaultValue: a.defaultValue || undefined,
85
+ })),
61
86
  relatedTypes,
62
87
  });
63
88
  }
64
89
 
90
+ // --- Helpers ---
91
+
65
92
  function resolveRef(obj, root) {
66
93
  if (!obj || typeof obj !== "object") return obj;
67
94
  if (obj.$ref) {
@@ -79,55 +106,81 @@ function resolveRequestBody(body, root) {
79
106
  if (!body) return null;
80
107
  const resolved = resolveRef(body, root);
81
108
  if (resolved?.content) {
82
- const result = { ...resolved, content: {} };
83
- for (const [mediaType, value] of Object.entries(resolved.content)) {
84
- result.content[mediaType] = {
85
- ...value,
86
- schema: resolveSchema(value.schema, root),
109
+ // Only show application/json if available (most useful for agents)
110
+ const jsonContent = resolved.content["application/json"];
111
+ if (jsonContent) {
112
+ return {
113
+ description: resolved.description || undefined,
114
+ required: resolved.required || undefined,
115
+ schema: resolveSchema(jsonContent.schema, root),
87
116
  };
88
117
  }
89
- return result;
118
+ // Fallback: show first content type
119
+ const [mediaType, value] = Object.entries(resolved.content)[0];
120
+ return {
121
+ description: resolved.description || undefined,
122
+ required: resolved.required || undefined,
123
+ mediaType,
124
+ schema: resolveSchema(value.schema, root),
125
+ };
90
126
  }
91
127
  return resolved;
92
128
  }
93
129
 
94
- function resolveResponses(responses, root) {
95
- if (!responses) return responses;
130
+ // Only show success response schema — agent doesn't need error schemas to make a call
131
+ function resolveResponsesCompact(responses, root) {
132
+ if (!responses) return null;
96
133
  const result = {};
97
134
  for (const [code, resp] of Object.entries(responses)) {
98
135
  const resolved = resolveRef(resp, root);
99
136
  if (resolved?.content) {
100
- result[code] = {
101
- ...resolved,
102
- content: {},
103
- };
104
- for (const [mediaType, value] of Object.entries(resolved.content)) {
105
- result[code].content[mediaType] = {
106
- ...value,
107
- schema: resolveSchema(value.schema, root),
137
+ const jsonContent = resolved.content["application/json"];
138
+ if (jsonContent) {
139
+ result[code] = {
140
+ description: resolved.description,
141
+ schema: resolveSchema(jsonContent.schema, root),
108
142
  };
143
+ } else {
144
+ result[code] = { description: resolved.description };
109
145
  }
110
146
  } else {
111
- result[code] = resolved;
147
+ result[code] = { description: resolved.description };
112
148
  }
113
149
  }
114
150
  return result;
115
151
  }
116
152
 
117
153
  function resolveSchema(schema, root, depth = 0) {
118
- if (!schema || depth > 5) return schema;
154
+ if (!schema || depth > 3) return schema;
119
155
  if (schema.$ref) {
120
- return resolveRef(schema, root);
156
+ const resolved = resolveRef(schema, root);
157
+ // Resolve one more level for the top-level ref
158
+ return resolveSchema(resolved, root, depth + 1);
121
159
  }
122
160
  if (schema.properties) {
123
- const result = { ...schema, properties: {} };
161
+ const result = { type: schema.type, required: schema.required };
162
+ result.properties = {};
124
163
  for (const [key, val] of Object.entries(schema.properties)) {
125
- result.properties[key] = resolveSchema(val, root, depth + 1);
164
+ if (val.$ref) {
165
+ const refName = val.$ref.split("/").pop();
166
+ result.properties[key] = { $ref: refName };
167
+ } else if (val.type === "array" && val.items?.$ref) {
168
+ result.properties[key] = { type: "array", items: val.items.$ref.split("/").pop() };
169
+ } else {
170
+ const prop = { type: val.type };
171
+ if (val.format) prop.format = val.format;
172
+ if (val.enum) prop.enum = val.enum;
173
+ if (val.description) prop.description = val.description;
174
+ result.properties[key] = prop;
175
+ }
126
176
  }
127
177
  return result;
128
178
  }
129
179
  if (schema.items) {
130
- return { ...schema, items: resolveSchema(schema.items, root, depth + 1) };
180
+ if (schema.items.$ref) {
181
+ return { type: "array", items: schema.items.$ref.split("/").pop() };
182
+ }
183
+ return { type: "array", items: resolveSchema(schema.items, root, depth + 1) };
131
184
  }
132
185
  return schema;
133
186
  }
@@ -135,7 +188,6 @@ function resolveSchema(schema, root, depth = 0) {
135
188
  function findRelatedTypes(op, types) {
136
189
  const names = new Set();
137
190
 
138
- // Collect type names from args and return type
139
191
  function extractTypeNames(typeStr) {
140
192
  if (!typeStr) return;
141
193
  const cleaned = typeStr.replace(/[[\]!]/g, "");
@@ -147,20 +199,28 @@ function findRelatedTypes(op, types) {
147
199
  extractTypeNames(flattenType(arg.type));
148
200
  }
149
201
 
150
- // Filter out built-in scalar types
151
202
  const scalars = new Set(["String", "Int", "Float", "Boolean", "ID"]);
152
203
  return types
153
204
  .filter((t) => names.has(t.name) && !scalars.has(t.name))
154
- .map((t) => ({
155
- name: t.name,
156
- kind: t.kind,
157
- fields: t.fields?.map((f) => ({
158
- name: f.name,
159
- type: flattenType(f.type),
160
- description: f.description,
161
- })),
162
- enumValues: t.enumValues,
163
- }));
205
+ .map((t) => {
206
+ const result = { name: t.name, kind: t.kind };
207
+ if (t.fields) {
208
+ result.fields = t.fields.map((f) => ({
209
+ name: f.name,
210
+ type: flattenType(f.type),
211
+ }));
212
+ }
213
+ if (t.inputFields) {
214
+ result.inputFields = t.inputFields.map((f) => ({
215
+ name: f.name,
216
+ type: flattenType(f.type),
217
+ }));
218
+ }
219
+ if (t.enumValues) {
220
+ result.enumValues = t.enumValues.map((e) => e.name);
221
+ }
222
+ return result;
223
+ });
164
224
  }
165
225
 
166
226
  function flattenType(t) {
@@ -0,0 +1,165 @@
1
+ import { getSpec } from "../store.js";
2
+ import { out } from "../output.js";
3
+ import { parseArgs } from "../args.js";
4
+
5
+ export async function typesCmd(args) {
6
+ const spec = getSpec();
7
+ if (!spec) throw new Error("No spec loaded. Run: spec load <file-or-url>");
8
+
9
+ const { positional, flags } = parseArgs(args);
10
+ const target = positional[0];
11
+
12
+ if (spec.type === "openapi") {
13
+ showOpenAPISchema(spec, target, flags);
14
+ } else {
15
+ showGraphQLType(spec, target, flags);
16
+ }
17
+ }
18
+
19
+ function showOpenAPISchema(spec, target, flags) {
20
+ const schemas = spec.raw?.components?.schemas || spec.raw?.definitions || {};
21
+
22
+ if (!target) {
23
+ // List all schema names — just names, very compact
24
+ const names = Object.keys(schemas);
25
+ out({
26
+ type: "openapi",
27
+ count: names.length,
28
+ schemas: names,
29
+ });
30
+ return;
31
+ }
32
+
33
+ // Find schema (case-insensitive)
34
+ const lower = target.toLowerCase();
35
+ const key = Object.keys(schemas).find((k) => k.toLowerCase() === lower);
36
+
37
+ if (!key) {
38
+ throw new Error(`Schema not found: ${target}. Run 'spec types' to list available schemas.`);
39
+ }
40
+
41
+ const schema = schemas[key];
42
+ const root = spec.raw;
43
+
44
+ // Resolve one level deep — don't recursively explode nested schemas
45
+ const resolved = resolveSchemaCompact(schema, root);
46
+
47
+ out({
48
+ name: key,
49
+ ...resolved,
50
+ });
51
+ }
52
+
53
+ function showGraphQLType(spec, target, flags) {
54
+ const scalars = new Set(["String", "Int", "Float", "Boolean", "ID"]);
55
+ const userTypes = spec.types?.filter((t) => !t.name.startsWith("__") && !scalars.has(t.name)) || [];
56
+
57
+ if (!target) {
58
+ // List type names grouped by kind — compact
59
+ const grouped = {};
60
+ for (const t of userTypes) {
61
+ if (!grouped[t.kind]) grouped[t.kind] = [];
62
+ grouped[t.kind].push(t.name);
63
+ }
64
+ out({
65
+ type: "graphql",
66
+ count: userTypes.length,
67
+ types: grouped,
68
+ });
69
+ return;
70
+ }
71
+
72
+ // Find specific type
73
+ const lower = target.toLowerCase();
74
+ const type = userTypes.find((t) => t.name.toLowerCase() === lower);
75
+
76
+ if (!type) {
77
+ throw new Error(`Type not found: ${target}. Run 'spec types' to list available types.`);
78
+ }
79
+
80
+ const result = {
81
+ name: type.name,
82
+ kind: type.kind,
83
+ description: type.description || null,
84
+ };
85
+
86
+ if (type.fields) {
87
+ result.fields = type.fields.map((f) => ({
88
+ name: f.name,
89
+ type: flattenType(f.type),
90
+ args: f.args?.length > 0 ? f.args.map((a) => ({ name: a.name, type: flattenType(a.type) })) : undefined,
91
+ }));
92
+ }
93
+
94
+ if (type.inputFields) {
95
+ result.inputFields = type.inputFields.map((f) => ({
96
+ name: f.name,
97
+ type: flattenType(f.type),
98
+ defaultValue: f.defaultValue || undefined,
99
+ }));
100
+ }
101
+
102
+ if (type.enumValues) {
103
+ result.enumValues = type.enumValues.map((e) => e.name);
104
+ }
105
+
106
+ out(result);
107
+ }
108
+
109
+ function resolveSchemaCompact(schema, root) {
110
+ if (!schema) return schema;
111
+
112
+ if (schema.$ref) {
113
+ const path = schema.$ref.replace("#/", "").split("/");
114
+ let resolved = root;
115
+ for (const p of path) resolved = resolved?.[p];
116
+ return resolveSchemaCompact(resolved, root);
117
+ }
118
+
119
+ const result = {};
120
+ if (schema.type) result.type = schema.type;
121
+ if (schema.description) result.description = schema.description;
122
+ if (schema.required) result.required = schema.required;
123
+ if (schema.enum) result.enum = schema.enum;
124
+
125
+ if (schema.properties) {
126
+ result.properties = {};
127
+ for (const [key, val] of Object.entries(schema.properties)) {
128
+ if (val.$ref) {
129
+ // Just show the type name, don't resolve
130
+ const refName = val.$ref.split("/").pop();
131
+ result.properties[key] = { $ref: refName };
132
+ } else if (val.type === "array" && val.items?.$ref) {
133
+ const refName = val.items.$ref.split("/").pop();
134
+ result.properties[key] = { type: "array", items: refName };
135
+ } else {
136
+ result.properties[key] = { type: val.type || null };
137
+ if (val.enum) result.properties[key].enum = val.enum;
138
+ if (val.format) result.properties[key].format = val.format;
139
+ if (val.description) result.properties[key].description = val.description;
140
+ }
141
+ }
142
+ }
143
+
144
+ if (schema.items) {
145
+ if (schema.items.$ref) {
146
+ result.items = schema.items.$ref.split("/").pop();
147
+ } else {
148
+ result.items = { type: schema.items.type };
149
+ }
150
+ }
151
+
152
+ return result;
153
+ }
154
+
155
+ function flattenType(t) {
156
+ if (!t) return null;
157
+ if (t.name) return t.kind === "NON_NULL" ? `${t.name}!` : t.name;
158
+ if (t.ofType) {
159
+ const inner = flattenType(t.ofType);
160
+ if (t.kind === "LIST") return `[${inner}]`;
161
+ if (t.kind === "NON_NULL") return `${inner}!`;
162
+ return inner;
163
+ }
164
+ return t.kind;
165
+ }