stpr 1.0.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.
Files changed (2) hide show
  1. package/dist/cli.js +810 -0
  2. package/package.json +23 -0
package/dist/cli.js ADDED
@@ -0,0 +1,810 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { createRequire } from "module";
5
+ import * as path2 from "path";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // src/auth.ts
9
+ import * as childProcess from "child_process";
10
+ import * as crypto from "crypto";
11
+ import * as fs from "fs";
12
+ import * as os from "os";
13
+ import * as path from "path";
14
+ import { createServer } from "http";
15
+
16
+ // src/client.ts
17
+ var DEFAULT_BASE_URL = "https://mcp.stepper.io";
18
+ var StepperClient = class {
19
+ token;
20
+ baseUrl;
21
+ requestId = 0;
22
+ constructor(token, baseUrl) {
23
+ this.token = token;
24
+ this.baseUrl = (baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
25
+ }
26
+ async rpc(method, params) {
27
+ const id = ++this.requestId;
28
+ const body = {
29
+ jsonrpc: "2.0",
30
+ method,
31
+ params: params ?? {},
32
+ id
33
+ };
34
+ const response = await fetch(`${this.baseUrl}/skill-sets/mcp`, {
35
+ method: "POST",
36
+ headers: {
37
+ "Content-Type": "application/json",
38
+ Authorization: `Bearer ${this.token}`
39
+ },
40
+ body: JSON.stringify(body)
41
+ });
42
+ if (!response.ok) {
43
+ const text = await response.text();
44
+ throw new Error(`HTTP ${response.status}: ${text}`);
45
+ }
46
+ return await response.json();
47
+ }
48
+ async initialize() {
49
+ const res = await this.rpc("initialize");
50
+ if (res.error) {
51
+ throw new Error(`Initialize failed: ${res.error.message}`);
52
+ }
53
+ await this.rpc("notifications/initialized");
54
+ }
55
+ async getServerInfo() {
56
+ const res = await this.rpc("initialize");
57
+ if (res.error) {
58
+ throw new Error(`Initialize failed: ${res.error.message}`);
59
+ }
60
+ const serverInfo = res.result?.serverInfo ?? {};
61
+ return {
62
+ name: serverInfo.name ?? "Unknown",
63
+ version: serverInfo.version ?? "1.0.0"
64
+ };
65
+ }
66
+ async listTools({ service }) {
67
+ const res = await this.rpc("tools/list");
68
+ if (res.error) {
69
+ throw new Error(`tools/list failed: ${res.error.message}`);
70
+ }
71
+ 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(
80
+ (tool) => service ? tool.service === service : !tool.service.startsWith("__")
81
+ );
82
+ }
83
+ async callTool(name, args) {
84
+ const res = await this.rpc("tools/call", { name, arguments: args });
85
+ if (res.error) {
86
+ throw new Error(`tools/call failed: ${res.error.message}`);
87
+ }
88
+ return res.result;
89
+ }
90
+ async getToolParams(toolName, args) {
91
+ const res = await this.rpc("tools/params", {
92
+ name: toolName,
93
+ arguments: args
94
+ });
95
+ if (res.error) {
96
+ throw new Error(`tools/params failed: ${res.error.message}`);
97
+ }
98
+ return res.result;
99
+ }
100
+ async getParameterOptions(toolName, parameterId, opts) {
101
+ const res = await this.rpc("tools/options", {
102
+ name: toolName,
103
+ parameter_id: parameterId,
104
+ current_parameter_values: opts?.currentParameterValues ?? {},
105
+ search: opts?.search ?? null,
106
+ cursor: opts?.cursor ?? null
107
+ });
108
+ if (res.error) {
109
+ throw new Error(`tools/options failed: ${res.error.message}`);
110
+ }
111
+ return res.result;
112
+ }
113
+ };
114
+
115
+ // src/auth.ts
116
+ function openBrowser(url) {
117
+ const platform2 = os.platform();
118
+ if (platform2 === "darwin") {
119
+ childProcess.spawn("open", [url], { stdio: "ignore", detached: true });
120
+ } else if (platform2 === "win32") {
121
+ childProcess.spawn("cmd", ["/c", "start", "", url], {
122
+ stdio: "ignore",
123
+ detached: true
124
+ });
125
+ } else {
126
+ childProcess.spawn("xdg-open", [url], { stdio: "ignore", detached: true });
127
+ }
128
+ }
129
+ var DEFAULT_BASE_URL2 = "https://mcp.stepper.io";
130
+ var OAUTH_CALLBACK_PORT = 3847;
131
+ var CALLBACK_PATH = "/callback";
132
+ var CONFIG_DIR = path.join(os.homedir(), ".config", "stepper-skillsets");
133
+ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
134
+ var LEGACY_CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
135
+ function generatePKCE() {
136
+ const codeVerifier = crypto.randomBytes(32).toString("base64url");
137
+ const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest().toString("base64url");
138
+ return { codeVerifier, codeChallenge };
139
+ }
140
+ function loadConfig() {
141
+ try {
142
+ const data = fs.readFileSync(CONFIG_FILE, "utf-8");
143
+ return JSON.parse(data);
144
+ } catch {
145
+ return { active: null, skillsets: {} };
146
+ }
147
+ }
148
+ function saveConfig(config) {
149
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
150
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
151
+ mode: 384
152
+ });
153
+ }
154
+ function migrateLegacyCredentials() {
155
+ try {
156
+ const data = fs.readFileSync(LEGACY_CREDENTIALS_FILE, "utf-8");
157
+ const creds = JSON.parse(data);
158
+ const config = {
159
+ active: "default",
160
+ skillsets: { default: creds }
161
+ };
162
+ saveConfig(config);
163
+ fs.unlinkSync(LEGACY_CREDENTIALS_FILE);
164
+ } catch {
165
+ }
166
+ }
167
+ function getConfigPathForDisplay() {
168
+ return CONFIG_FILE;
169
+ }
170
+ function getActiveSkillset() {
171
+ migrateLegacyCredentials();
172
+ const config = loadConfig();
173
+ return config.active;
174
+ }
175
+ function setActiveSkillset(name) {
176
+ migrateLegacyCredentials();
177
+ const config = loadConfig();
178
+ if (!(name in config.skillsets)) {
179
+ return false;
180
+ }
181
+ config.active = name;
182
+ saveConfig(config);
183
+ return true;
184
+ }
185
+ function listSkillsets() {
186
+ migrateLegacyCredentials();
187
+ const config = loadConfig();
188
+ return Object.entries(config.skillsets).map(([name, creds]) => ({
189
+ name,
190
+ baseUrl: creds.baseUrl,
191
+ isActive: config.active === name
192
+ }));
193
+ }
194
+ function getCredentials(name) {
195
+ migrateLegacyCredentials();
196
+ const config = loadConfig();
197
+ return config.skillsets[name] ?? null;
198
+ }
199
+ function saveCredentials(name, creds) {
200
+ migrateLegacyCredentials();
201
+ const config = loadConfig();
202
+ config.skillsets[name] = creds;
203
+ if (!config.active || !(config.active in config.skillsets)) {
204
+ config.active = name;
205
+ }
206
+ saveConfig(config);
207
+ }
208
+ function deleteSkillset(name) {
209
+ migrateLegacyCredentials();
210
+ const config = loadConfig();
211
+ if (!(name in config.skillsets)) {
212
+ return false;
213
+ }
214
+ delete config.skillsets[name];
215
+ if (config.active === name) {
216
+ config.active = Object.keys(config.skillsets)[0] ?? null;
217
+ }
218
+ saveConfig(config);
219
+ return true;
220
+ }
221
+ function deleteAllSkillsets() {
222
+ migrateLegacyCredentials();
223
+ const config = loadConfig();
224
+ const count = Object.keys(config.skillsets).length;
225
+ config.skillsets = {};
226
+ config.active = null;
227
+ saveConfig(config);
228
+ return count;
229
+ }
230
+ async function fetchMetadata(baseUrl) {
231
+ const url = `${baseUrl.replace(/\/$/, "")}/.well-known/oauth-authorization-server`;
232
+ const res = await fetch(url);
233
+ if (!res.ok) {
234
+ throw new Error(
235
+ `Failed to fetch OAuth metadata: ${res.status} ${await res.text()}`
236
+ );
237
+ }
238
+ return await res.json();
239
+ }
240
+ async function registerClient(baseUrl, redirectUri) {
241
+ const url = `${baseUrl.replace(/\/$/, "")}/register`;
242
+ const res = await fetch(url, {
243
+ method: "POST",
244
+ headers: { "Content-Type": "application/json" },
245
+ body: JSON.stringify({
246
+ client_name: "Skills CLI",
247
+ redirect_uris: [redirectUri],
248
+ grant_types: ["authorization_code", "refresh_token"],
249
+ token_endpoint_auth_method: "none",
250
+ response_types: ["code"]
251
+ })
252
+ });
253
+ if (!res.ok) {
254
+ throw new Error(
255
+ `Failed to register client: ${res.status} ${await res.text()}`
256
+ );
257
+ }
258
+ const data = await res.json();
259
+ return data.client_id;
260
+ }
261
+ async function exchangeCodeForTokens(baseUrl, clientId, redirectUri, code, codeVerifier) {
262
+ const url = `${baseUrl.replace(/\/$/, "")}/token`;
263
+ const res = await fetch(url, {
264
+ method: "POST",
265
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
266
+ body: new URLSearchParams({
267
+ grant_type: "authorization_code",
268
+ client_id: clientId,
269
+ redirect_uri: redirectUri,
270
+ code,
271
+ code_verifier: codeVerifier
272
+ }).toString()
273
+ });
274
+ if (!res.ok) {
275
+ const text = await res.text();
276
+ throw new Error(`Token exchange failed: ${res.status} ${text}`);
277
+ }
278
+ const data = await res.json();
279
+ return {
280
+ accessToken: data.access_token,
281
+ refreshToken: data.refresh_token,
282
+ expiresIn: data.expires_in ?? 3600
283
+ };
284
+ }
285
+ function extractSkillSetNameFromServerName(serverName) {
286
+ const prefix = "Stepper MCP Server - ";
287
+ if (serverName.startsWith(prefix)) {
288
+ return serverName.slice(prefix.length).trim() || serverName;
289
+ }
290
+ return serverName;
291
+ }
292
+ async function runLoginFlow(baseUrl) {
293
+ const mcpBaseUrl = baseUrl ?? process.env.STEPPER_URL ?? DEFAULT_BASE_URL2;
294
+ const normalizedBaseUrl = mcpBaseUrl.replace(/\/$/, "");
295
+ const redirectUri = `http://127.0.0.1:${OAUTH_CALLBACK_PORT}${CALLBACK_PATH}`;
296
+ const metadata = await fetchMetadata(normalizedBaseUrl);
297
+ const { codeVerifier, codeChallenge } = generatePKCE();
298
+ const clientId = await registerClient(normalizedBaseUrl, redirectUri);
299
+ const state = crypto.randomBytes(16).toString("hex");
300
+ const authUrl = new URL(metadata.authorization_endpoint);
301
+ authUrl.searchParams.set("response_type", "code");
302
+ authUrl.searchParams.set("client_id", clientId);
303
+ authUrl.searchParams.set("redirect_uri", redirectUri);
304
+ authUrl.searchParams.set("code_challenge", codeChallenge);
305
+ authUrl.searchParams.set("code_challenge_method", "S256");
306
+ authUrl.searchParams.set("state", state);
307
+ return new Promise((resolve, reject) => {
308
+ const server = createServer(
309
+ (req, res) => {
310
+ const url = new URL(req.url ?? "/", `http://127.0.0.1`);
311
+ if (url.pathname !== CALLBACK_PATH) {
312
+ res.writeHead(404);
313
+ res.end("Not found");
314
+ return;
315
+ }
316
+ const code = url.searchParams.get("code");
317
+ const returnedState = url.searchParams.get("state");
318
+ const error = url.searchParams.get("error");
319
+ if (error) {
320
+ res.writeHead(200, { "Content-Type": "text/html" });
321
+ res.end(
322
+ `<html><body><h1>Login failed</h1><p>${error}: ${url.searchParams.get("error_description") ?? "Unknown error"}</p><p>You can close this tab.</p></body></html>`
323
+ );
324
+ server.close();
325
+ reject(new Error(`OAuth error: ${error}`));
326
+ return;
327
+ }
328
+ if (returnedState !== state) {
329
+ res.writeHead(200, { "Content-Type": "text/html" });
330
+ res.end(
331
+ "<html><body><h1>Login failed</h1><p>State mismatch</p><p>You can close this tab.</p></body></html>"
332
+ );
333
+ server.close();
334
+ reject(new Error("OAuth state mismatch"));
335
+ return;
336
+ }
337
+ if (!code) {
338
+ res.writeHead(400);
339
+ res.end("Missing authorization code");
340
+ server.close();
341
+ reject(new Error("Missing authorization code"));
342
+ return;
343
+ }
344
+ res.writeHead(200, { "Content-Type": "text/html" });
345
+ res.end(
346
+ "<html><body><h1>Login successful</h1><p>You can close this tab and return to the terminal.</p></body></html>"
347
+ );
348
+ exchangeCodeForTokens(
349
+ normalizedBaseUrl,
350
+ clientId,
351
+ redirectUri,
352
+ code,
353
+ codeVerifier
354
+ ).then(async ({ accessToken, refreshToken, expiresIn }) => {
355
+ const creds = {
356
+ baseUrl: normalizedBaseUrl,
357
+ clientId,
358
+ accessToken,
359
+ refreshToken,
360
+ expiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString()
361
+ };
362
+ let name;
363
+ try {
364
+ const client = new StepperClient(accessToken, normalizedBaseUrl);
365
+ const serverInfo = await client.getServerInfo();
366
+ name = extractSkillSetNameFromServerName(serverInfo.name);
367
+ } catch {
368
+ name = `unknown-${Date.now()}`;
369
+ }
370
+ saveCredentials(name, creds);
371
+ resolve({ credentials: creds, name });
372
+ }).catch(reject).finally(() => server.close());
373
+ }
374
+ );
375
+ server.listen(OAUTH_CALLBACK_PORT, "127.0.0.1", () => {
376
+ openBrowser(authUrl.toString());
377
+ });
378
+ server.on("error", reject);
379
+ });
380
+ }
381
+ async function refreshAccessToken(baseUrl, clientId, refreshToken) {
382
+ const url = `${baseUrl.replace(/\/$/, "")}/token`;
383
+ const res = await fetch(url, {
384
+ method: "POST",
385
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
386
+ body: new URLSearchParams({
387
+ grant_type: "refresh_token",
388
+ client_id: clientId,
389
+ refresh_token: refreshToken
390
+ }).toString()
391
+ });
392
+ if (!res.ok) {
393
+ throw new Error(`Token refresh failed: ${res.status}`);
394
+ }
395
+ const data = await res.json();
396
+ return {
397
+ accessToken: data.access_token,
398
+ refreshToken: data.refresh_token,
399
+ expiresIn: data.expires_in ?? 3600
400
+ };
401
+ }
402
+ async function getValidToken(skillsetName, baseUrl) {
403
+ migrateLegacyCredentials();
404
+ const config = loadConfig();
405
+ const name = skillsetName ?? config.active;
406
+ if (!name || !(name in config.skillsets)) {
407
+ return null;
408
+ }
409
+ const creds = config.skillsets[name];
410
+ const resolvedBaseUrl = baseUrl ?? process.env.STEPPER_URL ?? creds.baseUrl;
411
+ if (creds.baseUrl !== resolvedBaseUrl) {
412
+ return null;
413
+ }
414
+ const expiresAt = new Date(creds.expiresAt).getTime();
415
+ const now = Date.now();
416
+ const bufferSeconds = 60;
417
+ if (expiresAt > now + bufferSeconds * 1e3) {
418
+ return {
419
+ token: creds.accessToken,
420
+ baseUrl: creds.baseUrl,
421
+ skillsetName: name
422
+ };
423
+ }
424
+ if (!creds.clientId || !creds.refreshToken) {
425
+ return null;
426
+ }
427
+ try {
428
+ const refreshed = await refreshAccessToken(
429
+ creds.baseUrl,
430
+ creds.clientId,
431
+ creds.refreshToken
432
+ );
433
+ const newCreds = {
434
+ ...creds,
435
+ accessToken: refreshed.accessToken,
436
+ refreshToken: refreshed.refreshToken,
437
+ expiresAt: new Date(
438
+ Date.now() + refreshed.expiresIn * 1e3
439
+ ).toISOString()
440
+ };
441
+ saveCredentials(name, newCreds);
442
+ return {
443
+ token: newCreds.accessToken,
444
+ baseUrl: creds.baseUrl,
445
+ skillsetName: name
446
+ };
447
+ } catch {
448
+ return null;
449
+ }
450
+ }
451
+
452
+ // src/cli.ts
453
+ var __dirname = path2.dirname(fileURLToPath(import.meta.url));
454
+ var pkg = createRequire(import.meta.url)(
455
+ path2.join(__dirname, "..", "package.json")
456
+ );
457
+ var VERSION = pkg.version ?? "0.0.0";
458
+ function readStdin() {
459
+ return new Promise((resolve, reject) => {
460
+ const chunks = [];
461
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
462
+ process.stdin.on(
463
+ "end",
464
+ () => resolve(Buffer.concat(chunks).toString("utf-8"))
465
+ );
466
+ process.stdin.on("error", reject);
467
+ if (process.stdin.isTTY) {
468
+ resolve("");
469
+ }
470
+ });
471
+ }
472
+ function parseArgs(argv) {
473
+ const flags = {};
474
+ const boolFlags = {};
475
+ const positional = [];
476
+ for (let i = 0; i < argv.length; i++) {
477
+ const arg = argv[i];
478
+ if (arg === "--token" && i + 1 < argv.length) {
479
+ flags.token = argv[++i];
480
+ } else if (arg === "--base-url" && i + 1 < argv.length) {
481
+ flags.baseUrl = argv[++i];
482
+ } else if (arg === "--skillset" && i + 1 < argv.length) {
483
+ flags.skillset = argv[++i];
484
+ } else if ((arg === "--input" || arg === "-i") && i + 1 < argv.length) {
485
+ flags.input = argv[++i];
486
+ } else if (arg === "--search" && i + 1 < argv.length) {
487
+ flags.search = argv[++i];
488
+ } else if (arg === "--cursor" && i + 1 < argv.length) {
489
+ flags.cursor = argv[++i];
490
+ } else if (arg === "--call") {
491
+ boolFlags.call = true;
492
+ } else if (arg === "--verbose") {
493
+ boolFlags.verbose = true;
494
+ } else if (arg === "--options" && i + 1 < argv.length) {
495
+ flags.options = argv[++i];
496
+ } else if (arg === "--help" || arg === "-h") {
497
+ positional.unshift("help");
498
+ } else if (arg === "--version" || arg === "-v") {
499
+ positional.unshift("version");
500
+ } else {
501
+ positional.push(arg);
502
+ }
503
+ }
504
+ return {
505
+ subcommand: positional[0] ?? "help",
506
+ args: positional.slice(1),
507
+ token: flags.token,
508
+ baseUrl: flags.baseUrl,
509
+ skillset: flags.skillset,
510
+ input: flags.input,
511
+ search: flags.search,
512
+ cursor: flags.cursor,
513
+ call: boolFlags.call ?? false,
514
+ verbose: boolFlags.verbose ?? false,
515
+ options: flags.options
516
+ };
517
+ }
518
+ function toToolName(service, action) {
519
+ return `${service}.${action}`;
520
+ }
521
+ function formatToolsForList(tools, verbose) {
522
+ if (verbose) {
523
+ return tools;
524
+ }
525
+ const out = {};
526
+ for (const tool of tools) {
527
+ out[tool.service] = [...out[tool.service] || [], tool.action];
528
+ }
529
+ return out;
530
+ }
531
+ function printUsage() {
532
+ console.error(`stpr v${VERSION}
533
+
534
+ Usage: stpr <command> [options]
535
+
536
+ Commands:
537
+ version Show version
538
+ login Log in via OAuth (opens browser). Named after the skillset from Stepper
539
+ logout [name] Log out and remove a skillset. Omit name to remove all skillsets.
540
+ profiles List all stored skillsets
541
+ use <name> Switch active skillset
542
+ whoami Show active skillset and server info
543
+ list [<service>] List available skills (optionally for a service). Use --verbose for inputSchema.
544
+ <service> List available skills for that service. Use --verbose for inputSchema.
545
+
546
+ <service> <action> (-i <json> | stdin)
547
+ Get the current parameters for a skill (default, for dynamic parameters)
548
+ <service> <action> --call (-i <json> | stdin)
549
+ Call a skill (JSON via --input, -i, or stdin)
550
+ <service> <action> --options <parameter> (--search <query> --cursor <cursor> -i <json>)
551
+ Fetch dynamic dropdown options for a parameter
552
+
553
+ Options:
554
+ --token <token> Auth token (or set STEPPER_SKILL_TOKEN env var)
555
+ --base-url <url> Override base URL (or set STEPPER_URL env var)
556
+ --skillset <name> Use a specific skill set
557
+ --call Execute the skill (default is to fetch parameters only)
558
+ --verbose Include inputSchema when listing skills
559
+ -i, --input <json> JSON input for call, parameters or options (alternative to stdin)
560
+ --search <query> Search query for dynamic dropdown options
561
+ --cursor <cursor> Pagination cursor for dynamic dropdown options
562
+ -h, --help Show this help message
563
+ -v, --version Show version
564
+
565
+ Examples:
566
+
567
+ Auth (optional, can use STEPPER_SKILL_TOKEN env var, or --token <token> from https://app.stepper.io/flow/skill-sets):
568
+ stpr login
569
+
570
+ List available skills:
571
+ stpr list
572
+ stpr stripe
573
+
574
+
575
+ Load parameters:
576
+ By default, requesting a skill returns the current parameters only; it does
577
+ not call the skill. Use --call to execute the action.
578
+
579
+ skillset google-sheets add_row -i '{"spreadsheet_id": "abc123"}'
580
+
581
+ Call a skill:
582
+ skillset stripe create_customer --call -i '{"email": "test@example.com"}'
583
+
584
+ Dynamic dropdown options:
585
+ Some actions have dynamic dropdown options, which change depending on the
586
+ value of other parameters. You can request the current dropdown options for
587
+ a skill by using the --options flag. This will return the current dropdown
588
+ options only, it will not call the skill. Dynamic dropdowns are also often
589
+ searchable and paginated via a cursor.
590
+
591
+ stpr google-sheets addRow --options worksheet_id -i '{"spreadsheet_id": "abc123"}'
592
+ stpr google-sheets addRow --options worksheet_id --search "Sheet" --cursor "next_page"
593
+
594
+ Profiles:
595
+ Multiple skill sets stored in ~/.config/stepper-skillsets/
596
+ Precedence: --token > STEPPER_SKILL_TOKEN > --skillset > active skill set`);
597
+ }
598
+ function die(message) {
599
+ process.stderr.write(`\x1B[31m[Error] ${message} \x1B[0m
600
+ `);
601
+ process.exit(1);
602
+ }
603
+ async function main() {
604
+ const {
605
+ subcommand,
606
+ args,
607
+ token: flagToken,
608
+ baseUrl,
609
+ skillset: flagSkillset,
610
+ input: inputFlag,
611
+ search: searchFlag,
612
+ cursor: cursorFlag,
613
+ call: callFlag,
614
+ verbose: verboseFlag,
615
+ options: optionsFlag
616
+ } = parseArgs(process.argv.slice(2));
617
+ if (subcommand === "help") {
618
+ printUsage();
619
+ process.exit(0);
620
+ }
621
+ if (subcommand === "version" || subcommand === "-v" || subcommand === "--version") {
622
+ console.log(VERSION);
623
+ process.exit(0);
624
+ }
625
+ if (subcommand === "login") {
626
+ try {
627
+ process.stderr.write("Opening browser for authentication...\n");
628
+ const { name } = await runLoginFlow(baseUrl);
629
+ process.stderr.write(
630
+ `Logged in successfully. Credentials saved as "${name}" in `
631
+ );
632
+ process.stderr.write(`${getConfigPathForDisplay()}
633
+ `);
634
+ } catch (err) {
635
+ die(err instanceof Error ? err.message : String(err));
636
+ }
637
+ return;
638
+ }
639
+ if (subcommand === "logout") {
640
+ const name = args[0];
641
+ if (name) {
642
+ const removed = deleteSkillset(name);
643
+ process.stderr.write(
644
+ removed ? `Removed skillset "${name}".
645
+ ` : `Skillset "${name}" not found.
646
+ `
647
+ );
648
+ } else {
649
+ const count = deleteAllSkillsets();
650
+ if (count > 0) {
651
+ process.stderr.write(
652
+ `Removed ${count} skillset${count > 1 ? "s" : ""}.
653
+ `
654
+ );
655
+ } else {
656
+ process.stderr.write("No skillset to remove.\n");
657
+ }
658
+ }
659
+ return;
660
+ }
661
+ if (subcommand === "use") {
662
+ const name = args[0];
663
+ if (!name) {
664
+ die("Usage: skillset use <name>");
665
+ }
666
+ const ok = setActiveSkillset(name);
667
+ if (!ok) {
668
+ die(`Skillset "${name}" not found. Run "skillset profiles" to list.`);
669
+ }
670
+ process.stderr.write(`Switched to skillset "${name}".
671
+ `);
672
+ return;
673
+ }
674
+ if (subcommand === "whoami") {
675
+ const skillsetName2 = flagSkillset ?? getActiveSkillset();
676
+ if (!skillsetName2) {
677
+ die('No active skillset. Run "stpr login" or "stpr use <name>".');
678
+ }
679
+ const stored = await getValidToken(skillsetName2);
680
+ if (!stored) {
681
+ const creds = getCredentials(skillsetName2);
682
+ if (!creds) {
683
+ die(`Skillset "${skillsetName2}" not found.`);
684
+ }
685
+ die(
686
+ `Skillset "${skillsetName2}" has expired or invalid token. Run "skills login" to re-authenticate.`
687
+ );
688
+ }
689
+ const client2 = new StepperClient(stored.token, stored.baseUrl);
690
+ try {
691
+ const serverInfo = await client2.getServerInfo();
692
+ console.log(
693
+ JSON.stringify(
694
+ {
695
+ skillSet: stored.skillsetName,
696
+ baseUrl: stored.baseUrl,
697
+ serverName: serverInfo.name,
698
+ serverVersion: serverInfo.version
699
+ },
700
+ null,
701
+ 2
702
+ )
703
+ );
704
+ } catch (err) {
705
+ console.log(
706
+ JSON.stringify(
707
+ {
708
+ skillSet: stored.skillsetName,
709
+ baseUrl: stored.baseUrl,
710
+ error: err instanceof Error ? err.message : String(err)
711
+ },
712
+ null,
713
+ 2
714
+ )
715
+ );
716
+ }
717
+ return;
718
+ }
719
+ if (subcommand === "profiles" || subcommand === "skillsets") {
720
+ const sets = listSkillsets();
721
+ if (sets.length === 0) {
722
+ process.stderr.write(
723
+ 'No skillset configured. Run "skillset login" to add one.\n'
724
+ );
725
+ return;
726
+ }
727
+ for (const s of sets) {
728
+ const marker = s.isActive ? " (active)" : "";
729
+ process.stderr.write(`${s.name}${marker}: ${s.baseUrl}
730
+ `);
731
+ }
732
+ return;
733
+ }
734
+ let token = flagToken ?? process.env.STEPPER_SKILL_TOKEN;
735
+ let resolvedBaseUrl = baseUrl ?? process.env.STEPPER_URL;
736
+ const skillsetName = flagSkillset ?? getActiveSkillset();
737
+ if (!token) {
738
+ const stored = await getValidToken(
739
+ skillsetName ?? void 0,
740
+ resolvedBaseUrl
741
+ );
742
+ if (stored) {
743
+ token = stored.token;
744
+ resolvedBaseUrl = stored.baseUrl;
745
+ }
746
+ }
747
+ if (!token) {
748
+ die(
749
+ 'No token provided. Run "skillset login" or use --token or set STEPPER_SKILL_TOKEN env var.'
750
+ );
751
+ }
752
+ const client = new StepperClient(token, resolvedBaseUrl);
753
+ await client.initialize();
754
+ if (subcommand === "list") {
755
+ const service2 = args[0];
756
+ const tools = await client.listTools({ service: service2 ?? void 0 });
757
+ const formatted = formatToolsForList(tools, verboseFlag);
758
+ console.log(JSON.stringify(formatted, null, 2));
759
+ return;
760
+ }
761
+ const service = subcommand;
762
+ const action = args[0];
763
+ if (service.startsWith("--")) {
764
+ printUsage();
765
+ process.exit(1);
766
+ return;
767
+ }
768
+ if (!action) {
769
+ const tools = await client.listTools({ service });
770
+ const formatted = verboseFlag ? formatToolsForList(tools, verboseFlag) : tools.map(({ action: action2 }) => action2);
771
+ console.log(JSON.stringify(formatted, null, 2));
772
+ return;
773
+ }
774
+ const toolName = toToolName(service, action);
775
+ const rawInput = inputFlag ?? await readStdin();
776
+ let input = {};
777
+ if (rawInput.trim()) {
778
+ try {
779
+ input = JSON.parse(rawInput);
780
+ } catch {
781
+ die("Invalid JSON input.");
782
+ }
783
+ }
784
+ if (optionsFlag) {
785
+ const result2 = await client.getParameterOptions(toolName, optionsFlag, {
786
+ currentParameterValues: input,
787
+ search: searchFlag,
788
+ cursor: cursorFlag
789
+ });
790
+ console.log(JSON.stringify(result2, null, 2));
791
+ return;
792
+ }
793
+ if (callFlag) {
794
+ const result2 = await client.callTool(toolName, input);
795
+ if (result2.isError) {
796
+ die(result2.content.map((c) => c.text).join("\n"));
797
+ }
798
+ console.log(result2.content.map((c) => c.text ?? "").join("\n"));
799
+ return;
800
+ }
801
+ const result = await client.getToolParams(toolName, input);
802
+ if (result.isError) {
803
+ die(result.content?.map((c) => c.text).join("\n") ?? "Unknown error");
804
+ }
805
+ console.log(JSON.stringify(result, null, 2));
806
+ }
807
+ main().then(() => process.exit(0)).catch((err) => {
808
+ console.error(`Error: ${err.message}`);
809
+ process.exit(1);
810
+ });
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "stpr",
3
+ "version": "1.0.0",
4
+ "description": "CLI for Stepper skill sets",
5
+ "type": "module",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "bin": {
10
+ "stpr": "./dist/cli.js"
11
+ },
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "typecheck": "tsc --noEmit"
15
+ },
16
+ "keywords": [],
17
+ "author": "",
18
+ "license": "ISC",
19
+ "devDependencies": {
20
+ "tsup": "^8.0.0",
21
+ "typescript": "^5.4.0"
22
+ }
23
+ }