specli 0.0.18 → 0.0.20

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.
Files changed (75) hide show
  1. package/README.md +3 -1
  2. package/dist/ai/tools.d.ts +6 -5
  3. package/dist/ai/tools.js +7 -20
  4. package/dist/ai/tools.test.js +7 -14
  5. package/dist/cli/compile.js +137 -61
  6. package/dist/cli.d.ts +0 -1
  7. package/dist/cli.js +0 -1
  8. package/package.json +1 -2
  9. package/src/ai/tools.test.ts +0 -83
  10. package/src/ai/tools.ts +0 -211
  11. package/src/cli/auth-requirements.test.ts +0 -27
  12. package/src/cli/auth-requirements.ts +0 -91
  13. package/src/cli/auth-schemes.test.ts +0 -66
  14. package/src/cli/auth-schemes.ts +0 -187
  15. package/src/cli/capabilities.test.ts +0 -94
  16. package/src/cli/capabilities.ts +0 -88
  17. package/src/cli/command-id.test.ts +0 -32
  18. package/src/cli/command-id.ts +0 -16
  19. package/src/cli/command-index.ts +0 -19
  20. package/src/cli/command-model.test.ts +0 -44
  21. package/src/cli/command-model.ts +0 -128
  22. package/src/cli/compile.ts +0 -109
  23. package/src/cli/crypto.ts +0 -9
  24. package/src/cli/derive-name.ts +0 -101
  25. package/src/cli/exec.ts +0 -72
  26. package/src/cli/main.ts +0 -255
  27. package/src/cli/naming.test.ts +0 -86
  28. package/src/cli/naming.ts +0 -224
  29. package/src/cli/operations.test.ts +0 -57
  30. package/src/cli/operations.ts +0 -152
  31. package/src/cli/params.test.ts +0 -70
  32. package/src/cli/params.ts +0 -71
  33. package/src/cli/pluralize.ts +0 -41
  34. package/src/cli/positional.test.ts +0 -65
  35. package/src/cli/positional.ts +0 -75
  36. package/src/cli/request-body.test.ts +0 -35
  37. package/src/cli/request-body.ts +0 -94
  38. package/src/cli/runtime/argv.ts +0 -14
  39. package/src/cli/runtime/auth/resolve.ts +0 -59
  40. package/src/cli/runtime/body-flags.test.ts +0 -261
  41. package/src/cli/runtime/body-flags.ts +0 -176
  42. package/src/cli/runtime/body.ts +0 -24
  43. package/src/cli/runtime/collect.ts +0 -6
  44. package/src/cli/runtime/compat.ts +0 -89
  45. package/src/cli/runtime/context.ts +0 -62
  46. package/src/cli/runtime/execute.ts +0 -147
  47. package/src/cli/runtime/generated.ts +0 -242
  48. package/src/cli/runtime/headers.ts +0 -37
  49. package/src/cli/runtime/index.ts +0 -3
  50. package/src/cli/runtime/profile/secrets.ts +0 -83
  51. package/src/cli/runtime/profile/store.ts +0 -100
  52. package/src/cli/runtime/request.test.ts +0 -375
  53. package/src/cli/runtime/request.ts +0 -390
  54. package/src/cli/runtime/server-url.ts +0 -45
  55. package/src/cli/runtime/template.ts +0 -26
  56. package/src/cli/runtime/validate/ajv.ts +0 -13
  57. package/src/cli/runtime/validate/coerce.test.ts +0 -98
  58. package/src/cli/runtime/validate/coerce.ts +0 -71
  59. package/src/cli/runtime/validate/error.ts +0 -29
  60. package/src/cli/runtime/validate/index.ts +0 -4
  61. package/src/cli/runtime/validate/schema.ts +0 -54
  62. package/src/cli/schema-shape.ts +0 -36
  63. package/src/cli/schema.ts +0 -76
  64. package/src/cli/server.test.ts +0 -55
  65. package/src/cli/server.ts +0 -167
  66. package/src/cli/spec-id.ts +0 -12
  67. package/src/cli/spec-loader.ts +0 -58
  68. package/src/cli/stable-json.ts +0 -35
  69. package/src/cli/strings.ts +0 -21
  70. package/src/cli/types.ts +0 -59
  71. package/src/cli.ts +0 -94
  72. package/src/compiled.ts +0 -24
  73. package/src/macros/env.ts +0 -21
  74. package/src/macros/spec.ts +0 -17
  75. package/src/macros/version.ts +0 -14
package/README.md CHANGED
@@ -321,7 +321,7 @@ import { generateText } from "ai";
321
321
  const result = await generateText({
322
322
  model: yourModel,
323
323
  tools: {
324
- api: specli({
324
+ api: await specli({
325
325
  spec: "https://api.example.com/openapi.json",
326
326
  bearerToken: process.env.API_TOKEN,
327
327
  }),
@@ -330,6 +330,8 @@ const result = await generateText({
330
330
  });
331
331
  ```
332
332
 
333
+ The `specli()` function is async and fetches the OpenAPI spec upfront, so the returned tool is ready to use immediately without any additional network requests.
334
+
333
335
  The tool supports three commands:
334
336
  - `list` - Show available resources and actions
335
337
  - `help` - Get details about a specific action
@@ -11,7 +11,7 @@
11
11
  * const result = await generateText({
12
12
  * model: yourModel,
13
13
  * tools: {
14
- * api: specli({ spec: "https://api.example.com/openapi.json" }),
14
+ * api: await specli({ spec: "https://api.example.com/openapi.json" }),
15
15
  * },
16
16
  * prompt: "List all users",
17
17
  * });
@@ -38,8 +38,11 @@ export type SpecliToolOptions = {
38
38
  };
39
39
  /**
40
40
  * Create an AI SDK tool for interacting with an OpenAPI spec.
41
+ *
42
+ * The spec is fetched once when this function is called, so the returned
43
+ * tool already has the spec loaded and ready to use.
41
44
  */
42
- export declare function specli(options: SpecliToolOptions): import("ai").Tool<{
45
+ export declare function specli(options: SpecliToolOptions): Promise<import("ai").Tool<{
43
46
  command: "list" | "exec" | "help";
44
47
  resource?: string | undefined;
45
48
  action?: string | undefined;
@@ -133,6 +136,4 @@ export declare function specli(options: SpecliToolOptions): import("ai").Tool<{
133
136
  summary?: undefined;
134
137
  args?: undefined;
135
138
  flags?: undefined;
136
- }>;
137
- /** Clear cached spec context */
138
- export declare function clearSpecliCache(spec?: string): void;
139
+ }>>;
package/dist/ai/tools.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * const result = await generateText({
12
12
  * model: yourModel,
13
13
  * tools: {
14
- * api: specli({ spec: "https://api.example.com/openapi.json" }),
14
+ * api: await specli({ spec: "https://api.example.com/openapi.json" }),
15
15
  * },
16
16
  * prompt: "List all users",
17
17
  * });
@@ -21,25 +21,20 @@ import { tool } from "ai";
21
21
  import { z } from "zod";
22
22
  import { buildRuntimeContext } from "../cli/runtime/context.js";
23
23
  import { execute } from "../cli/runtime/execute.js";
24
- // Cache contexts to avoid reloading spec on every call
25
- const contextCache = new Map();
26
- async function getContext(spec) {
27
- let ctx = contextCache.get(spec);
28
- if (!ctx) {
29
- ctx = await buildRuntimeContext({ spec });
30
- contextCache.set(spec, ctx);
31
- }
32
- return ctx;
33
- }
34
24
  function findAction(ctx, resource, action) {
35
25
  const r = ctx.commands.resources.find((r) => r.resource.toLowerCase() === resource.toLowerCase());
36
26
  return r?.actions.find((a) => a.action.toLowerCase() === action.toLowerCase());
37
27
  }
38
28
  /**
39
29
  * Create an AI SDK tool for interacting with an OpenAPI spec.
30
+ *
31
+ * The spec is fetched once when this function is called, so the returned
32
+ * tool already has the spec loaded and ready to use.
40
33
  */
41
- export function specli(options) {
34
+ export async function specli(options) {
42
35
  const { spec, server, serverVars, bearerToken, apiKey, basicAuth, authScheme, } = options;
36
+ // Fetch and parse the spec upfront
37
+ const ctx = await buildRuntimeContext({ spec });
43
38
  return tool({
44
39
  description: `Execute API operations. Commands: "list" (show resources/actions), "help" (action details), "exec" (call API).`,
45
40
  inputSchema: z.object({
@@ -56,7 +51,6 @@ export function specli(options) {
56
51
  .describe("Named flags"),
57
52
  }),
58
53
  execute: async ({ command, resource, action, args, flags }) => {
59
- const ctx = await getContext(spec);
60
54
  if (command === "list") {
61
55
  return {
62
56
  resources: ctx.commands.resources.map((r) => ({
@@ -152,10 +146,3 @@ export function specli(options) {
152
146
  },
153
147
  });
154
148
  }
155
- /** Clear cached spec context */
156
- export function clearSpecliCache(spec) {
157
- if (spec)
158
- contextCache.delete(spec);
159
- else
160
- contextCache.clear();
161
- }
@@ -1,13 +1,13 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { clearSpecliCache, specli } from "./tools.js";
2
+ import { specli } from "./tools.js";
3
3
  const mockOptions = {
4
4
  toolCallId: "test-call-id",
5
5
  abortSignal: new AbortController().signal,
6
6
  messages: [],
7
7
  };
8
8
  describe("specli tool", () => {
9
- test("creates a tool with correct structure", () => {
10
- const tool = specli({
9
+ test("creates a tool with correct structure", async () => {
10
+ const tool = await specli({
11
11
  spec: "https://petstore3.swagger.io/api/v3/openapi.json",
12
12
  });
13
13
  expect(tool).toHaveProperty("description");
@@ -16,7 +16,7 @@ describe("specli tool", () => {
16
16
  expect(typeof tool.execute).toBe("function");
17
17
  });
18
18
  test("list command returns resources", async () => {
19
- const tool = specli({
19
+ const tool = await specli({
20
20
  spec: "https://petstore3.swagger.io/api/v3/openapi.json",
21
21
  });
22
22
  const result = (await tool.execute?.({ command: "list" }, mockOptions));
@@ -24,7 +24,7 @@ describe("specli tool", () => {
24
24
  expect(Array.isArray(result.resources)).toBe(true);
25
25
  });
26
26
  test("help command returns action details", async () => {
27
- const tool = specli({
27
+ const tool = await specli({
28
28
  spec: "https://petstore3.swagger.io/api/v3/openapi.json",
29
29
  });
30
30
  const result = (await tool.execute?.({ command: "help", resource: "pets", action: "get" }, mockOptions));
@@ -32,25 +32,18 @@ describe("specli tool", () => {
32
32
  expect(result.action).toBe("get");
33
33
  });
34
34
  test("help command with missing resource returns error", async () => {
35
- const tool = specli({
35
+ const tool = await specli({
36
36
  spec: "https://petstore3.swagger.io/api/v3/openapi.json",
37
37
  });
38
38
  const result = (await tool.execute?.({ command: "help" }, mockOptions));
39
39
  expect(result).toHaveProperty("error");
40
40
  });
41
41
  test("exec command with missing args returns error", async () => {
42
- const tool = specli({
42
+ const tool = await specli({
43
43
  spec: "https://petstore3.swagger.io/api/v3/openapi.json",
44
44
  });
45
45
  const result = (await tool.execute?.({ command: "exec", resource: "pets", action: "get" }, mockOptions));
46
46
  expect(result).toHaveProperty("error");
47
47
  expect(result.error).toContain("Missing args");
48
48
  });
49
- test("clearCache works", async () => {
50
- const spec = "https://petstore3.swagger.io/api/v3/openapi.json";
51
- const tool = specli({ spec });
52
- await tool.execute?.({ command: "list" }, mockOptions);
53
- clearSpecliCache(spec);
54
- clearSpecliCache();
55
- });
56
49
  });
@@ -1,11 +1,11 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
1
3
  import path from "node:path";
2
4
  import { fileURLToPath } from "node:url";
3
5
  import { deriveBinaryName } from "./derive-name.js";
4
- // Resolve the path to compiled.ts relative to this file's location
5
- // At runtime this file is at dist/cli/compile.js, so we go up two levels to package root
6
- // then into src/compiled.ts (which must be included in the published package)
6
+ // Resolve the package root directory (at runtime this file is at dist/cli/compile.js)
7
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
- const compiledEntrypoint = path.resolve(__dirname, "../../src/compiled.ts");
8
+ const packageRoot = path.resolve(__dirname, "../..");
9
9
  function parseKeyValue(input) {
10
10
  const idx = input.indexOf("=");
11
11
  if (idx === -1)
@@ -16,6 +16,72 @@ function parseKeyValue(input) {
16
16
  throw new Error(`Invalid --define '${input}', missing key`);
17
17
  return { key, value };
18
18
  }
19
+ /**
20
+ * Loads the OpenAPI spec from a URL or file path.
21
+ */
22
+ async function loadSpec(spec) {
23
+ if (!spec)
24
+ throw new Error("Missing spec path/URL");
25
+ if (/^https?:\/\//i.test(spec)) {
26
+ const res = await fetch(spec);
27
+ if (!res.ok) {
28
+ throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
29
+ }
30
+ return await res.text();
31
+ }
32
+ return await fs.promises.readFile(spec, "utf-8");
33
+ }
34
+ /**
35
+ * Reads the package version from package.json.
36
+ */
37
+ function getPackageVersion() {
38
+ const packageJsonPath = path.join(packageRoot, "package.json");
39
+ const content = fs.readFileSync(packageJsonPath, "utf-8");
40
+ const packageJson = JSON.parse(content);
41
+ return packageJson.version;
42
+ }
43
+ /**
44
+ * Generates a temporary entrypoint file with all values hardcoded.
45
+ * This avoids the Bun macro security restriction in node_modules.
46
+ */
47
+ function generateEntrypoint(options) {
48
+ const mainImportPath = path.join(packageRoot, "dist/cli/main.js");
49
+ // Escape the spec text for embedding as a string literal
50
+ const escapedSpec = JSON.stringify(options.specText);
51
+ const escapedName = options.cliName
52
+ ? JSON.stringify(options.cliName)
53
+ : "undefined";
54
+ const escapedServer = options.server
55
+ ? JSON.stringify(options.server)
56
+ : "undefined";
57
+ const escapedServerVars = options.serverVars
58
+ ? JSON.stringify(options.serverVars)
59
+ : "undefined";
60
+ const escapedAuth = options.auth ? JSON.stringify(options.auth) : "undefined";
61
+ const escapedVersion = JSON.stringify(options.version);
62
+ return `#!/usr/bin/env bun
63
+ // Auto-generated entrypoint for specli compile
64
+ // This file embeds all configuration at build time
65
+
66
+ import { main } from ${JSON.stringify(mainImportPath)};
67
+
68
+ const embeddedSpecText = ${escapedSpec};
69
+ const cliName = ${escapedName};
70
+ const server = ${escapedServer};
71
+ const serverVars = ${escapedServerVars};
72
+ const auth = ${escapedAuth};
73
+ const embeddedVersion = ${escapedVersion};
74
+
75
+ await main(process.argv, {
76
+ embeddedSpecText,
77
+ cliName,
78
+ server,
79
+ serverVars: serverVars ? serverVars.split(",") : undefined,
80
+ auth,
81
+ version: embeddedVersion,
82
+ });
83
+ `;
84
+ }
19
85
  export async function compileCommand(spec, options) {
20
86
  // Derive name from spec if not provided
21
87
  const name = options.name ?? (await deriveBinaryName(spec));
@@ -23,64 +89,74 @@ export async function compileCommand(spec, options) {
23
89
  const target = options.target
24
90
  ? options.target
25
91
  : `bun-${process.platform}-${process.arch}`;
26
- // Parse --define pairs
27
- const define = {};
28
- if (options.define) {
29
- for (const pair of options.define) {
30
- const { key, value } = parseKeyValue(pair);
31
- define[key] = JSON.stringify(value);
92
+ // Load the spec content
93
+ process.stdout.write(`Loading spec: ${spec}\n`);
94
+ const specText = await loadSpec(spec);
95
+ // Get package version
96
+ const version = getPackageVersion();
97
+ // Generate temporary entrypoint file
98
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "specli-"));
99
+ const tempEntrypoint = path.join(tempDir, "entrypoint.ts");
100
+ const entrypointCode = generateEntrypoint({
101
+ specText,
102
+ cliName: name,
103
+ server: options.server,
104
+ serverVars: options.serverVar?.join(","),
105
+ auth: options.auth,
106
+ version,
107
+ });
108
+ fs.writeFileSync(tempEntrypoint, entrypointCode);
109
+ try {
110
+ // Parse --define pairs
111
+ const define = {};
112
+ if (options.define) {
113
+ for (const pair of options.define) {
114
+ const { key, value } = parseKeyValue(pair);
115
+ define[key] = JSON.stringify(value);
116
+ }
32
117
  }
118
+ // Build command args
119
+ const buildArgs = [
120
+ "build",
121
+ "--compile",
122
+ `--outfile=${outfile}`,
123
+ `--target=${target}`,
124
+ ];
125
+ if (options.minify)
126
+ buildArgs.push("--minify");
127
+ if (options.bytecode)
128
+ buildArgs.push("--bytecode");
129
+ for (const [k, v] of Object.entries(define)) {
130
+ buildArgs.push("--define", `${k}=${v}`);
131
+ }
132
+ if (options.dotenv === false)
133
+ buildArgs.push("--no-compile-autoload-dotenv");
134
+ if (options.bunfig === false)
135
+ buildArgs.push("--no-compile-autoload-bunfig");
136
+ buildArgs.push(tempEntrypoint);
137
+ const proc = Bun.spawn({
138
+ cmd: ["bun", ...buildArgs],
139
+ stdout: "pipe",
140
+ stderr: "pipe",
141
+ env: process.env,
142
+ });
143
+ const output = await new Response(proc.stdout).text();
144
+ const error = await new Response(proc.stderr).text();
145
+ const code = await proc.exited;
146
+ if (output)
147
+ process.stdout.write(output);
148
+ if (error)
149
+ process.stderr.write(error);
150
+ if (code !== 0) {
151
+ process.exitCode = code;
152
+ return;
153
+ }
154
+ process.stdout.write(`ok: built ${outfile}\n`);
155
+ process.stdout.write(`target: ${target}\n`);
156
+ process.stdout.write(`name: ${name}\n`);
33
157
  }
34
- // Build command args
35
- const buildArgs = [
36
- "build",
37
- "--compile",
38
- `--outfile=${outfile}`,
39
- `--target=${target}`,
40
- ];
41
- if (options.minify)
42
- buildArgs.push("--minify");
43
- if (options.bytecode)
44
- buildArgs.push("--bytecode");
45
- for (const [k, v] of Object.entries(define)) {
46
- buildArgs.push("--define", `${k}=${v}`);
47
- }
48
- if (options.dotenv === false)
49
- buildArgs.push("--no-compile-autoload-dotenv");
50
- if (options.bunfig === false)
51
- buildArgs.push("--no-compile-autoload-bunfig");
52
- buildArgs.push(compiledEntrypoint);
53
- // Only set env vars that have actual values - avoid empty strings
54
- // because the macros will embed them and they will override defaults.
55
- const buildEnv = {
56
- ...process.env,
57
- SPECLI_SPEC: spec,
58
- SPECLI_NAME: name,
59
- };
60
- if (options.server)
61
- buildEnv.SPECLI_SERVER = options.server;
62
- if (options.serverVar?.length)
63
- buildEnv.SPECLI_SERVER_VARS = options.serverVar.join(",");
64
- if (options.auth)
65
- buildEnv.SPECLI_AUTH = options.auth;
66
- const proc = Bun.spawn({
67
- cmd: ["bun", ...buildArgs],
68
- stdout: "pipe",
69
- stderr: "pipe",
70
- env: buildEnv,
71
- });
72
- const output = await new Response(proc.stdout).text();
73
- const error = await new Response(proc.stderr).text();
74
- const code = await proc.exited;
75
- if (output)
76
- process.stdout.write(output);
77
- if (error)
78
- process.stderr.write(error);
79
- if (code !== 0) {
80
- process.exitCode = code;
81
- return;
158
+ finally {
159
+ // Clean up temporary files
160
+ fs.rmSync(tempDir, { recursive: true, force: true });
82
161
  }
83
- process.stdout.write(`ok: built ${outfile}\n`);
84
- process.stdout.write(`target: ${target}\n`);
85
- process.stdout.write(`name: ${name}\n`);
86
162
  }
package/dist/cli.d.ts CHANGED
@@ -1,2 +1 @@
1
- #!/usr/bin/env node
2
1
  export {};
package/dist/cli.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { readFileSync } from "node:fs";
3
2
  import { dirname, join } from "node:path";
4
3
  import { fileURLToPath } from "node:url";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specli",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "type": "module",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -16,7 +16,6 @@
16
16
  "files": [
17
17
  "bin",
18
18
  "dist",
19
- "src",
20
19
  "README.md",
21
20
  "package.json"
22
21
  ],
@@ -1,83 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { clearSpecliCache, specli } from "./tools.js";
3
-
4
- const mockOptions = {
5
- toolCallId: "test-call-id",
6
- abortSignal: new AbortController().signal,
7
- messages: [],
8
- };
9
-
10
- describe("specli tool", () => {
11
- test("creates a tool with correct structure", () => {
12
- const tool = specli({
13
- spec: "https://petstore3.swagger.io/api/v3/openapi.json",
14
- });
15
-
16
- expect(tool).toHaveProperty("description");
17
- expect(tool).toHaveProperty("inputSchema");
18
- expect(tool).toHaveProperty("execute");
19
- expect(typeof tool.execute).toBe("function");
20
- });
21
-
22
- test("list command returns resources", async () => {
23
- const tool = specli({
24
- spec: "https://petstore3.swagger.io/api/v3/openapi.json",
25
- });
26
-
27
- const result = (await tool.execute?.({ command: "list" }, mockOptions)) as {
28
- resources: unknown[];
29
- };
30
-
31
- expect(result).toHaveProperty("resources");
32
- expect(Array.isArray(result.resources)).toBe(true);
33
- });
34
-
35
- test("help command returns action details", async () => {
36
- const tool = specli({
37
- spec: "https://petstore3.swagger.io/api/v3/openapi.json",
38
- });
39
-
40
- const result = (await tool.execute?.(
41
- { command: "help", resource: "pets", action: "get" },
42
- mockOptions,
43
- )) as { action: string };
44
-
45
- expect(result).toHaveProperty("action");
46
- expect(result.action).toBe("get");
47
- });
48
-
49
- test("help command with missing resource returns error", async () => {
50
- const tool = specli({
51
- spec: "https://petstore3.swagger.io/api/v3/openapi.json",
52
- });
53
-
54
- const result = (await tool.execute?.({ command: "help" }, mockOptions)) as {
55
- error: string;
56
- };
57
-
58
- expect(result).toHaveProperty("error");
59
- });
60
-
61
- test("exec command with missing args returns error", async () => {
62
- const tool = specli({
63
- spec: "https://petstore3.swagger.io/api/v3/openapi.json",
64
- });
65
-
66
- const result = (await tool.execute?.(
67
- { command: "exec", resource: "pets", action: "get" },
68
- mockOptions,
69
- )) as { error: string };
70
-
71
- expect(result).toHaveProperty("error");
72
- expect(result.error).toContain("Missing args");
73
- });
74
-
75
- test("clearCache works", async () => {
76
- const spec = "https://petstore3.swagger.io/api/v3/openapi.json";
77
- const tool = specli({ spec });
78
-
79
- await tool.execute?.({ command: "list" }, mockOptions);
80
- clearSpecliCache(spec);
81
- clearSpecliCache();
82
- });
83
- });