api-spec-cli 0.0.3 → 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,191 +1,153 @@
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
 
7
7
  ```bash
8
- bun install
9
- bun link
8
+ npm install -g api-spec-cli
10
9
  ```
11
10
 
12
- Or run directly:
11
+ Works with Node.js 18+ or Bun. No other dependencies.
13
12
 
14
13
  ```bash
15
- bun bin/spec.js <command>
14
+ # Or run without installing
15
+ npx api-spec-cli <command>
16
16
  ```
17
17
 
18
- ## Quick Start
18
+ ## How It Works
19
19
 
20
- ```bash
21
- # Load an OpenAPI spec
22
- spec load https://petstore3.swagger.io/api/v3/openapi.json
23
-
24
- # Load a GraphQL endpoint (introspection)
25
- spec load https://gql.hashnode.com
26
-
27
- # List operations
28
- spec list
29
- spec list --filter pets
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.
30
21
 
31
- # Show operation details
32
- spec show getPetById
33
- spec show publishPost
22
+ ### Step 1: Load the spec
34
23
 
35
- # Call an endpoint
36
- spec config set baseUrl https://petstore3.swagger.io/api/v3
37
- spec call findPetsByStatus --query status=available
38
-
39
- # GraphQL with auth
40
- spec config set auth YOUR_TOKEN
41
- spec call me
24
+ ```bash
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)
42
28
  ```
43
29
 
44
- ## Commands
45
-
46
- ### `spec load <file-or-url>`
30
+ Output tells you what was loaded:
31
+ ```json
32
+ { "ok": true, "type": "graphql", "operationCount": 114, "source": "https://gql.hashnode.com" }
33
+ ```
47
34
 
48
- Load an API spec from a local file (JSON/YAML) or URL.
35
+ ### Step 2: Find what you need
49
36
 
50
- - **OpenAPI/Swagger**: Detects JSON or YAML, supports v2 and v3
51
- - **GraphQL**: Runs introspection query on the endpoint
37
+ `list` is compact by default — just operation IDs, no schemas. Use `--filter`, `--tag`, `--limit` to narrow down.
52
38
 
53
39
  ```bash
54
- spec load ./openapi.yaml
55
- spec load https://api.example.com/openapi.json
56
- spec load https://gql.example.com/graphql
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
46
+ ```
47
+
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
+ }
57
59
  ```
58
60
 
59
- ### `spec list [--filter <text>]`
61
+ Use `--compact false` for full details (summary, tags, args).
60
62
 
61
- List all operations in the loaded spec.
63
+ ### Step 3: Inspect one operation
64
+
65
+ `show` gives you everything you need to call an operation — params, body schema, response, and related types — in one call.
62
66
 
63
67
  ```bash
64
- spec list # all operations
65
- spec list --filter user # filter by keyword
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
66
72
  ```
67
73
 
68
- **OpenAPI output**: `id`, `method`, `path`, `summary`, `tags`, `deprecated`
69
- **GraphQL output**: `id`, `kind` (query/mutation/subscription), `description`, `args`, `returnType`
70
-
71
- ### `spec show <operation>`
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>`.
72
75
 
73
- Show full details of an operation.
76
+ ### Step 4: Drill into types (if needed)
74
77
 
75
78
  ```bash
76
- spec show getPetById # by operationId
77
- spec show /pet/{petId} # by path
78
- spec show "GET /pet/{petId}" # by method + path
79
- spec show publishPost # GraphQL operation name
79
+ spec types # List all schema/type names
80
+ spec types Pet # Inspect one schema
81
+ spec types PublishPostInput # Inspect a GraphQL input type
80
82
  ```
81
83
 
82
- For OpenAPI: resolves `$ref` references in parameters, request body, and responses.
83
- For GraphQL: includes related types with field definitions.
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.
84
85
 
85
- ### `spec call <operation> [options]`
86
-
87
- Execute an API request.
86
+ ### Step 5: Call the API
88
87
 
89
88
  ```bash
90
- # OpenAPI
91
- spec call addPet --data '{"name":"Rex","photoUrls":[]}'
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
92
+
93
+ # OpenAPI calls
92
94
  spec call getPetById --var petId=1
93
95
  spec call findPetsByStatus --query status=available
94
- spec call updatePet --method PUT --data '{"id":1,"name":"Rex"}'
96
+ spec call addPet --data '{"name":"Rex","photoUrls":[]}'
95
97
 
96
- # GraphQL
98
+ # GraphQL calls (auto-generates query from schema)
97
99
  spec call me
98
- spec call publication --var host=blog.example.com
99
- spec call publishPost --data '{"query":"mutation { ... }"}'
100
+ spec call publication --var host=blog.hashnode.dev
100
101
  ```
101
102
 
102
- **Options:**
103
- | Flag | Description |
104
- |------|-------------|
105
- | `--data '{"key":"val"}'` | Request body (JSON) |
106
- | `--query key=val` | Query parameter (repeatable) |
107
- | `--header key=val` | Per-request header (repeatable) |
108
- | `--var key=val` | Path variable or GraphQL variable (repeatable) |
109
- | `--method GET\|POST\|...` | Override HTTP method |
110
-
111
- For GraphQL calls without `--data`, the CLI auto-generates a query from the operation's schema, selecting scalar fields from the return type.
103
+ ## Config
112
104
 
113
- ### `spec validate <file-or-url>`
114
-
115
- Validate an OpenAPI spec and report errors and warnings.
105
+ Persistent config stored in `.spec-cli/config.json`. Set once, used for all calls.
116
106
 
117
107
  ```bash
118
- spec validate ./openapi.yaml
119
- 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
120
114
  ```
121
115
 
122
- Checks:
123
- - Required fields (`info`, `info.title`, `info.version`, `paths`)
124
- - Valid HTTP methods and schema types
125
- - Unique `operationId` values
126
- - Broken `$ref` references
127
- - Array schemas have `items`
128
- - Path parameters are declared
129
- - Server/host definitions
130
- - Unusual patterns (request body on GET)
131
-
132
- ### `spec config`
116
+ ## Validate
133
117
 
134
- Manage persistent configuration stored in `.spec-cli/config.json`.
118
+ Check an OpenAPI spec for errors before using it:
135
119
 
136
120
  ```bash
137
- spec config get # show all config
138
- spec config get baseUrl # show single key
139
- spec config set baseUrl https://api.example.com
140
- spec config set auth my-api-token # adds Bearer prefix
141
- spec config set auth "Bearer my-token" # explicit Bearer
142
- spec config set auth "Basic dXNlcjpwYXNz" # Basic auth
143
- spec config set headers.X-API-Key abc123 # custom header (dot notation)
144
- spec config unset headers.X-API-Key # remove a key
121
+ spec validate https://api.example.com/openapi.json
145
122
  ```
146
123
 
147
- ## Output Formats
124
+ Reports broken `$ref` references, missing required fields, duplicate operationIds, invalid schema types, and more.
148
125
 
149
- All commands support `--format`:
126
+ ## Output Format
127
+
128
+ JSON by default. Use `--format text` or `--format yaml` for alternatives:
150
129
 
151
130
  ```bash
152
- spec list --format json # JSON (default)
153
- spec list --format text # human-readable
154
- spec list --format yaml # YAML
131
+ spec list --format text
132
+ spec show getPetById --format yaml
155
133
  ```
156
134
 
157
- Errors always output as JSON to stderr for reliable agent parsing.
158
-
159
- ## GraphQL Coverage
135
+ Errors always go to stderr as JSON: `{"error": "message"}` with non-zero exit code.
160
136
 
161
- Full GraphQL schema support via introspection:
162
- - **Queries** — all root query fields
163
- - **Mutations** — all root mutation fields
164
- - **Subscriptions** — all root subscription fields
165
- - **Types** — input objects, enums, scalars, object types with fields
166
- - **Args** — full argument definitions with types and defaults
137
+ ## Token Efficiency
167
138
 
168
- ## For AI Agents
139
+ The CLI is designed to minimize context window usage for AI agents:
169
140
 
170
- This CLI is designed to be used by AI coding agents (Claude, GPT, etc.) as an MCP tool or shell command:
171
-
172
- 1. **Structured output** JSON by default, every field is predictable
173
- 2. **Error format** `{"error": "message"}` on stderr, non-zero exit code
174
- 3. **Discoverable** — `list` and `show` let agents explore APIs without docs
175
- 4. **Auto-query building** — GraphQL calls auto-generate queries from the schema
176
- 5. **Persistent config** — set auth once, use across calls
177
- 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
178
146
 
179
147
  ## Storage
180
148
 
181
- All state is stored in `.spec-cli/` in the current directory:
182
- - `spec.json` — cached loaded spec
183
- - `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
184
152
 
185
153
  Add `.spec-cli/` to your `.gitignore`.
186
-
187
- ## Dependencies
188
-
189
- - [yaml](https://www.npmjs.com/package/yaml) — YAML parsing (for OpenAPI YAML specs and YAML output)
190
-
191
- No other runtime dependencies. Uses Bun's built-in `fetch` for HTTP.
package/bin/spec.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { run } from "../src/cli.js";
4
4
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "api-spec-cli",
3
- "version": "0.0.3",
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": {
7
- "spec": "./bin/spec.js"
7
+ "spec": "bin/spec.js"
8
8
  },
9
9
  "scripts": {
10
10
  "test": "bun test"
@@ -31,6 +31,9 @@
31
31
  "type": "git",
32
32
  "url": "git+https://github.com/niradler/api-spec-cli.git"
33
33
  },
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ },
34
37
  "dependencies": {
35
38
  "yaml": "^2.7.0"
36
39
  }
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
+ }