stpr 1.0.5 → 1.0.7

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 (3) hide show
  1. package/README.md +159 -0
  2. package/dist/cli.js +84 -49
  3. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # stpr — Stepper Skills CLI
2
+
3
+ Skill Sets are a Stepper feature that let you bundle integration actions into curated, authenticated toolkits — and expose them to AI agents, CLIs, and any MCP-compatible client.
4
+
5
+ `stpr` is a command-line interface for interacting with [Stepper](https://stepper.io) Skill Sets. Discover, inspect, and execute integration actions directly from your terminal. This is perfect for OpenClaw or agents that can interact with a CLI, or for developers who want to use skills in their own scripts.
6
+
7
+ If you prefer, you can directly use the Stepper MCP server at `https://mcp.stepper.io/skill-sets/mcp`instead of the CLI.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install -g stpr
13
+ ```
14
+
15
+ ## Authentication
16
+
17
+ The CLI supports three authentication methods:
18
+
19
+ ### OAuth Login (recommended)
20
+
21
+ ```bash
22
+ stpr login
23
+ ```
24
+
25
+ Opens your browser to authenticate and select a Skill Set. Credentials are saved to `~/.config/stepper-skillsets/config.json` and automatically refreshed when they expire.
26
+
27
+ ### Static Token
28
+
29
+ Generate a token from [Stepper](https://app.stepper.io/flow/skill-sets) and pass it directly:
30
+
31
+ ```bash
32
+ stpr --token sst_your_token_here list
33
+ ```
34
+
35
+ ### Environment Variable
36
+
37
+ ```bash
38
+ export STEPPER_SKILL_TOKEN=sst_your_token_here
39
+ stpr list
40
+ ```
41
+
42
+ ## Commands
43
+
44
+ ### Profile Management
45
+
46
+ ```bash
47
+ stpr login # Authenticate via OAuth (opens browser)
48
+ stpr logout [name] # Remove a saved profile, or all profiles if no name given
49
+ stpr profiles # List all saved profiles
50
+ stpr use <name> # Switch the active profile
51
+ stpr whoami # Show active profile and server info
52
+ ```
53
+
54
+ ### Discovering Skills
55
+
56
+ ```bash
57
+ stpr list # List all available skills, grouped by service
58
+ stpr list --verbose # Include full input schemas
59
+ stpr list <service> # List skills for a specific service
60
+ stpr <service> # Shorthand for listing a service's skills
61
+ ```
62
+
63
+ ### Inspecting Parameters
64
+
65
+ Many skills have dynamic parameters — fields that change based on the values of other fields. Calling a skill without `--call` returns its current parameter schema.
66
+
67
+ ```bash
68
+ # See what fields are needed for add_row, given a spreadsheet
69
+ stpr google-sheets add_row -i '{"spreadsheet_id": "abc123"}'
70
+ ```
71
+
72
+ Some parameters have dynamic dropdown options. Fetch them with `--options`:
73
+
74
+ ```bash
75
+ stpr google-sheets add_row --options worksheet_id \
76
+ -i '{"spreadsheet_id": "abc123"}' \
77
+ --search "Sheet" \
78
+ --cursor "next_page"
79
+ ```
80
+
81
+ ### Calling Skills
82
+
83
+ Use the `--call` flag to execute an action:
84
+
85
+ ```bash
86
+ stpr google-sheets create_sheet --call \
87
+ -i '{"name": "Q1 Report", "columns": "Name, Email, Phone"}'
88
+ ```
89
+
90
+ ### Polling Async Results
91
+
92
+ Component library tools run asynchronously. Poll for results with:
93
+
94
+ ```bash
95
+ stpr status <statusId>
96
+ ```
97
+
98
+ ## Input
99
+
100
+ Pass JSON input via the `-i` / `--input` flag or pipe it through stdin:
101
+
102
+ ```bash
103
+ # Flag
104
+ stpr slack send_message --call -i '{"channel": "#general", "text": "Hello!"}'
105
+
106
+ # Stdin
107
+ echo '{"channel": "#general", "text": "Hello!"}' | stpr slack send_message --call
108
+ ```
109
+
110
+ ## Options Reference
111
+
112
+ | Flag | Description |
113
+ | -------------------- | --------------------------------------------------------------- |
114
+ | `--token <token>` | Auth token (overrides saved profiles and `STEPPER_SKILL_TOKEN`) |
115
+ | `--base-url <url>` | Override MCP server URL (default: `https://mcp.stepper.io`) |
116
+ | `--skillset <name>` | Use a specific saved profile instead of the active one |
117
+ | `--call` | Execute the skill (default behavior is parameter inspection) |
118
+ | `--verbose` | Include full `inputSchema` when listing skills |
119
+ | `-i, --input <json>` | JSON input for calls, parameter fetches, or option queries |
120
+ | `--options <param>` | Fetch dynamic dropdown options for a parameter |
121
+ | `--search <query>` | Filter dropdown options by search term |
122
+ | `--cursor <cursor>` | Pagination cursor for dropdown options |
123
+ | `-h, --help` | Show help |
124
+ | `-v, --version` | Show version |
125
+
126
+ ## Environment Variables
127
+
128
+ | Variable | Description |
129
+ | --------------------- | ------------------------------------------------------------- |
130
+ | `STEPPER_SKILL_TOKEN` | Auth token (used when no `--token` flag and no saved profile) |
131
+ | `STEPPER_URL` | Override the MCP server base URL |
132
+
133
+ ## Examples
134
+
135
+ ```bash
136
+ # Authenticate
137
+ stpr login
138
+
139
+ # List everything available
140
+ stpr list
141
+
142
+ # Explore a service
143
+ stpr google-sheets
144
+
145
+ # Inspect dynamic parameters step by step
146
+ stpr google-sheets add_row -i '{}'
147
+ stpr google-sheets add_row -i '{"spreadsheet_id": "abc123"}'
148
+
149
+ # Fetch dropdown options
150
+ stpr google-sheets add_row --options worksheet_id -i '{"spreadsheet_id": "abc123"}'
151
+
152
+ # Execute
153
+ stpr google-sheets add_row --call -i '{"spreadsheet_id": "abc123", "worksheet_id": "Sheet1", "values": {"Name": "Alice"}}'
154
+
155
+ # Switch between skill sets
156
+ stpr profiles
157
+ stpr use "Production Tools"
158
+ stpr list
159
+ ```
package/dist/cli.js CHANGED
@@ -2,19 +2,49 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { createRequire } from "module";
5
- import * as path2 from "path";
5
+ import * as path3 from "path";
6
6
  import { fileURLToPath } from "url";
7
7
 
8
8
  // src/auth.ts
9
9
  import * as childProcess from "child_process";
10
+ import * as crypto2 from "crypto";
11
+ import * as fs2 from "fs";
12
+ import * as os2 from "os";
13
+ import * as path2 from "path";
14
+ import { createServer } from "http";
15
+
16
+ // src/client.ts
10
17
  import * as crypto from "crypto";
11
18
  import * as fs from "fs";
12
19
  import * as os from "os";
13
20
  import * as path from "path";
14
- import { createServer } from "http";
15
-
16
- // src/client.ts
17
21
  var DEFAULT_BASE_URL = "https://mcp.stepper.io";
22
+ var CACHE_DIR = path.join(os.homedir(), ".config", "stepper-skillsets", "cache");
23
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
24
+ function getCachePath(key) {
25
+ const hash = crypto.createHash("sha256").update(key).digest("hex").slice(0, 16);
26
+ return path.join(CACHE_DIR, `${hash}.json`);
27
+ }
28
+ function readCache(key) {
29
+ try {
30
+ const filePath = getCachePath(key);
31
+ const raw = fs.readFileSync(filePath, "utf-8");
32
+ const entry = JSON.parse(raw);
33
+ if (Date.now() - entry.timestamp < CACHE_TTL_MS) {
34
+ return entry.data;
35
+ }
36
+ } catch {
37
+ }
38
+ return null;
39
+ }
40
+ function writeCache(key, data) {
41
+ try {
42
+ fs.mkdirSync(CACHE_DIR, { recursive: true, mode: 448 });
43
+ const entry = { timestamp: Date.now(), data };
44
+ fs.writeFileSync(getCachePath(key), JSON.stringify(entry), { mode: 384 });
45
+ } catch {
46
+ }
47
+ }
18
48
  var StepperClient = class {
19
49
  token;
20
50
  baseUrl;
@@ -63,20 +93,31 @@ var StepperClient = class {
63
93
  version: serverInfo.version ?? "1.0.0"
64
94
  };
65
95
  }
66
- async listTools({ service }) {
96
+ async listTools({ service, noCache }) {
97
+ const cacheKey = `tools-list:${this.baseUrl}:${this.token}`;
98
+ let allTools;
99
+ if (!noCache) {
100
+ const cached = readCache(cacheKey);
101
+ if (cached) {
102
+ allTools = cached;
103
+ return allTools.filter(
104
+ (tool) => service ? tool.service === service : !tool.service.startsWith("__")
105
+ );
106
+ }
107
+ }
67
108
  const res = await this.rpc("tools/list");
68
109
  if (res.error) {
69
110
  throw new Error(`tools/list failed: ${res.error.message}`);
70
111
  }
71
112
  const tools = res.result.tools;
72
- return tools.map(
73
- (tool) => ({
74
- service: tool.name.split(".")[0],
75
- action: tool.name.split(".")[1],
76
- description: tool.description,
77
- inputSchema: tool.inputSchema
78
- })
79
- ).filter(
113
+ allTools = tools.map((tool) => ({
114
+ service: tool.name.split(".")[0],
115
+ action: tool.name.split(".")[1],
116
+ description: tool.description,
117
+ inputSchema: tool.inputSchema
118
+ }));
119
+ writeCache(cacheKey, allTools);
120
+ return allTools.filter(
80
121
  (tool) => service ? tool.service === service : !tool.service.startsWith("__")
81
122
  );
82
123
  }
@@ -121,7 +162,7 @@ var StepperClient = class {
121
162
 
122
163
  // src/auth.ts
123
164
  function openBrowser(url) {
124
- const platform2 = os.platform();
165
+ const platform2 = os2.platform();
125
166
  if (platform2 === "darwin") {
126
167
  childProcess.spawn("open", [url], { stdio: "ignore", detached: true });
127
168
  } else if (platform2 === "win32") {
@@ -136,51 +177,35 @@ function openBrowser(url) {
136
177
  var DEFAULT_BASE_URL2 = "https://mcp.stepper.io";
137
178
  var OAUTH_CALLBACK_PORT = 3847;
138
179
  var CALLBACK_PATH = "/callback";
139
- var CONFIG_DIR = path.join(os.homedir(), ".config", "stepper-skillsets");
140
- var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
141
- var LEGACY_CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
180
+ var CONFIG_DIR = path2.join(os2.homedir(), ".config", "stepper-skillsets");
181
+ var CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
142
182
  function generatePKCE() {
143
- const codeVerifier = crypto.randomBytes(32).toString("base64url");
144
- const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest().toString("base64url");
183
+ const codeVerifier = crypto2.randomBytes(32).toString("base64url");
184
+ const codeChallenge = crypto2.createHash("sha256").update(codeVerifier).digest().toString("base64url");
145
185
  return { codeVerifier, codeChallenge };
146
186
  }
147
187
  function loadConfig() {
148
188
  try {
149
- const data = fs.readFileSync(CONFIG_FILE, "utf-8");
189
+ const data = fs2.readFileSync(CONFIG_FILE, "utf-8");
150
190
  return JSON.parse(data);
151
191
  } catch {
152
192
  return { active: null, skillsets: {} };
153
193
  }
154
194
  }
155
195
  function saveConfig(config) {
156
- fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
157
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
196
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
197
+ fs2.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
158
198
  mode: 384
159
199
  });
160
200
  }
161
- function migrateLegacyCredentials() {
162
- try {
163
- const data = fs.readFileSync(LEGACY_CREDENTIALS_FILE, "utf-8");
164
- const creds = JSON.parse(data);
165
- const config = {
166
- active: "default",
167
- skillsets: { default: creds }
168
- };
169
- saveConfig(config);
170
- fs.unlinkSync(LEGACY_CREDENTIALS_FILE);
171
- } catch {
172
- }
173
- }
174
201
  function getConfigPathForDisplay() {
175
202
  return CONFIG_FILE;
176
203
  }
177
204
  function getActiveSkillset() {
178
- migrateLegacyCredentials();
179
205
  const config = loadConfig();
180
206
  return config.active;
181
207
  }
182
208
  function setActiveSkillset(name) {
183
- migrateLegacyCredentials();
184
209
  const config = loadConfig();
185
210
  if (!(name in config.skillsets)) {
186
211
  return false;
@@ -190,7 +215,6 @@ function setActiveSkillset(name) {
190
215
  return true;
191
216
  }
192
217
  function listSkillsets() {
193
- migrateLegacyCredentials();
194
218
  const config = loadConfig();
195
219
  return Object.entries(config.skillsets).map(([name, creds]) => ({
196
220
  name,
@@ -199,12 +223,10 @@ function listSkillsets() {
199
223
  }));
200
224
  }
201
225
  function getCredentials(name) {
202
- migrateLegacyCredentials();
203
226
  const config = loadConfig();
204
227
  return config.skillsets[name] ?? null;
205
228
  }
206
229
  function saveCredentials(name, creds) {
207
- migrateLegacyCredentials();
208
230
  const config = loadConfig();
209
231
  config.skillsets[name] = creds;
210
232
  if (!config.active || !(config.active in config.skillsets)) {
@@ -213,7 +235,6 @@ function saveCredentials(name, creds) {
213
235
  saveConfig(config);
214
236
  }
215
237
  function deleteSkillset(name) {
216
- migrateLegacyCredentials();
217
238
  const config = loadConfig();
218
239
  if (!(name in config.skillsets)) {
219
240
  return false;
@@ -226,7 +247,6 @@ function deleteSkillset(name) {
226
247
  return true;
227
248
  }
228
249
  function deleteAllSkillsets() {
229
- migrateLegacyCredentials();
230
250
  const config = loadConfig();
231
251
  const count = Object.keys(config.skillsets).length;
232
252
  config.skillsets = {};
@@ -303,7 +323,7 @@ async function runLoginFlow(baseUrl) {
303
323
  const metadata = await fetchMetadata(normalizedBaseUrl);
304
324
  const { codeVerifier, codeChallenge } = generatePKCE();
305
325
  const clientId = await registerClient(normalizedBaseUrl, redirectUri);
306
- const state = crypto.randomBytes(16).toString("hex");
326
+ const state = crypto2.randomBytes(16).toString("hex");
307
327
  const authUrl = new URL(metadata.authorization_endpoint);
308
328
  authUrl.searchParams.set("response_type", "code");
309
329
  authUrl.searchParams.set("client_id", clientId);
@@ -320,6 +340,18 @@ async function runLoginFlow(baseUrl) {
320
340
  res.end("Not found");
321
341
  return;
322
342
  }
343
+ const requestHost = req.headers.host ?? `127.0.0.1:${OAUTH_CALLBACK_PORT}`;
344
+ const callbackUrl = new URL(req.url ?? "/", `http://${requestHost}`);
345
+ const receivedCallbackUrl = `${callbackUrl.origin}${callbackUrl.pathname}`;
346
+ if (receivedCallbackUrl !== redirectUri) {
347
+ res.writeHead(200, { "Content-Type": "text/html" });
348
+ res.end(
349
+ "<html><body><h1>Login failed</h1><p>Redirect URI mismatch</p><p>You can close this tab.</p></body></html>"
350
+ );
351
+ server.close();
352
+ reject(new Error("OAuth redirect URI mismatch"));
353
+ return;
354
+ }
323
355
  const code = url.searchParams.get("code");
324
356
  const returnedState = url.searchParams.get("state");
325
357
  const error = url.searchParams.get("error");
@@ -412,7 +444,6 @@ async function refreshAccessToken(baseUrl, clientId, refreshToken) {
412
444
  };
413
445
  }
414
446
  async function getValidToken(skillsetName, baseUrl) {
415
- migrateLegacyCredentials();
416
447
  const config = loadConfig();
417
448
  const name = skillsetName ?? config.active;
418
449
  if (!name || !(name in config.skillsets)) {
@@ -462,9 +493,9 @@ async function getValidToken(skillsetName, baseUrl) {
462
493
  }
463
494
 
464
495
  // src/cli.ts
465
- var __dirname = path2.dirname(fileURLToPath(import.meta.url));
496
+ var __dirname = path3.dirname(fileURLToPath(import.meta.url));
466
497
  var pkg = createRequire(import.meta.url)(
467
- path2.join(__dirname, "..", "package.json")
498
+ path3.join(__dirname, "..", "package.json")
468
499
  );
469
500
  var VERSION = pkg.version ?? "0.0.0";
470
501
  function readStdin() {
@@ -501,6 +532,8 @@ function parseArgs(argv) {
501
532
  flags.cursor = argv[++i];
502
533
  } else if (arg === "--call") {
503
534
  boolFlags.call = true;
535
+ } else if (arg === "--no-cache") {
536
+ boolFlags.noCache = true;
504
537
  } else if (arg === "--verbose") {
505
538
  boolFlags.verbose = true;
506
539
  } else if (arg === "--options" && i + 1 < argv.length) {
@@ -524,6 +557,7 @@ function parseArgs(argv) {
524
557
  cursor: flags.cursor,
525
558
  call: boolFlags.call ?? false,
526
559
  verbose: boolFlags.verbose ?? false,
560
+ noCache: boolFlags.noCache ?? false,
527
561
  options: flags.options
528
562
  };
529
563
  }
@@ -568,6 +602,7 @@ Options:
568
602
  --skillset <name> Use a specific skill set
569
603
  --call Execute the skill (default is to fetch parameters only)
570
604
  --verbose Include inputSchema when listing skills
605
+ --no-cache Bypass the 5-minute tools list cache
571
606
  -i, --input <json> JSON input for call, parameters or options (alternative to stdin)
572
607
  --search <query> Search query for dynamic dropdown options
573
608
  --cursor <cursor> Pagination cursor for dynamic dropdown options
@@ -575,7 +610,6 @@ Options:
575
610
  -v, --version Show version
576
611
 
577
612
  Examples:
578
-
579
613
  Auth (optional, can use STEPPER_SKILL_TOKEN env var, or --token <token> from https://app.stepper.io/flow/skill-sets):
580
614
  stpr login
581
615
 
@@ -616,6 +650,7 @@ async function main() {
616
650
  cursor: cursorFlag,
617
651
  call: callFlag,
618
652
  verbose: verboseFlag,
653
+ noCache: noCacheFlag,
619
654
  options: optionsFlag
620
655
  } = parseArgs(process.argv.slice(2));
621
656
  if (subcommand === "help") {
@@ -769,7 +804,7 @@ async function main() {
769
804
  }
770
805
  if (subcommand === "list") {
771
806
  const service2 = args[0];
772
- const tools = await client.listTools({ service: service2 ?? void 0 });
807
+ const tools = await client.listTools({ service: service2 ?? void 0, noCache: noCacheFlag });
773
808
  const formatted = formatToolsForList(tools, verboseFlag);
774
809
  if (!tools.length) {
775
810
  die(
@@ -787,7 +822,7 @@ async function main() {
787
822
  return;
788
823
  }
789
824
  if (!action) {
790
- const tools = await client.listTools({ service });
825
+ const tools = await client.listTools({ service, noCache: noCacheFlag });
791
826
  if (!tools.length) {
792
827
  die(
793
828
  service ? `No skills found for service "${service}".` : "No skills found."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stpr",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "CLI for Stepper skill sets",
5
5
  "type": "module",
6
6
  "files": [