@orth/cli 0.2.33 → 0.3.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.
@@ -2,4 +2,4 @@
2
2
  * Fire-and-forget analytics event for CLI usage tracking.
3
3
  * Never blocks, never throws, never shows errors to the user.
4
4
  */
5
- export declare function trackEvent(command: string, args?: Record<string, string | undefined>): void;
5
+ export declare function trackEvent(command: string, args?: Record<string, string | undefined>, response?: unknown): void;
package/dist/analytics.js CHANGED
@@ -3,12 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.trackEvent = trackEvent;
4
4
  const config_js_1 = require("./config.js");
5
5
  const CLI_VERSION = process.env.npm_package_version || "0.2.0";
6
- const BASE_URL = "https://api.orthogonal.com/v1";
6
+ const PRODUCTION_BASE_URL = "https://api.orthogonal.com/v1";
7
+ function getAnalyticsBaseUrl() {
8
+ return process.env.ORTH_API_URL || PRODUCTION_BASE_URL;
9
+ }
7
10
  /**
8
11
  * Fire-and-forget analytics event for CLI usage tracking.
9
12
  * Never blocks, never throws, never shows errors to the user.
10
13
  */
11
- function trackEvent(command, args) {
14
+ function trackEvent(command, args, response) {
12
15
  // Don't track if no API key (not authenticated)
13
16
  const apiKey = (0, config_js_1.getApiKey)();
14
17
  if (!apiKey)
@@ -16,6 +19,7 @@ function trackEvent(command, args) {
16
19
  const payload = {
17
20
  command,
18
21
  args: args ? sanitizeArgs(args) : undefined,
22
+ response,
19
23
  cliVersion: CLI_VERSION,
20
24
  os: process.platform,
21
25
  nodeVersion: process.version,
@@ -23,7 +27,7 @@ function trackEvent(command, args) {
23
27
  timestamp: new Date().toISOString(),
24
28
  };
25
29
  // Fire and forget - don't await, don't catch visible errors
26
- fetch(`${BASE_URL}/cli/events`, {
30
+ fetch(`${getAnalyticsBaseUrl()}/cli/events`, {
27
31
  method: "POST",
28
32
  headers: {
29
33
  Authorization: `Bearer ${apiKey}`,
package/dist/api.d.ts CHANGED
@@ -13,6 +13,7 @@ export interface SearchResponse {
13
13
  price?: number;
14
14
  hasDynamicPricing?: boolean;
15
15
  pricing_formula?: string;
16
+ apiSlug?: string;
16
17
  }>;
17
18
  }>;
18
19
  count: number;
@@ -139,6 +140,7 @@ export interface ApiBySlugResponse {
139
140
  required: boolean;
140
141
  description?: string;
141
142
  }>;
143
+ apiSlug?: string;
142
144
  }>;
143
145
  count: number;
144
146
  }
@@ -7,6 +7,7 @@ exports.apiCommand = apiCommand;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const ora_1 = __importDefault(require("ora"));
9
9
  const api_js_1 = require("../api.js");
10
+ const utils_js_1 = require("../utils.js");
10
11
  async function apiCommand(slug, path, options) {
11
12
  const spinner = (0, ora_1.default)("Loading...").start();
12
13
  try {
@@ -162,7 +163,14 @@ async function apiCommand(slug, path, options) {
162
163
  ? chalk_1.default.dim(ep)
163
164
  : chalk_1.default.dim(`$${parseFloat(Number(ep).toFixed(4))}`);
164
165
  })();
165
- console.log(`${method} ${chalk_1.default.white(endpoint.path)} ${price}`);
166
+ // Group members commonly collide on the same upstream path (e.g.
167
+ // AbstractAPI's many products all expose "/v1") — when the endpoint's
168
+ // own callable slug differs from the card's flat slug, show it so the
169
+ // right identifier is obvious before calling `orth run`.
170
+ const memberTag = endpoint.apiSlug && endpoint.apiSlug !== api.slug
171
+ ? chalk_1.default.dim(` [${(0, utils_js_1.sanitizeForTerminal)(endpoint.apiSlug)}]`)
172
+ : "";
173
+ console.log(`${method} ${chalk_1.default.white(endpoint.path)} ${price}${memberTag}`);
166
174
  if (endpoint.description) {
167
175
  console.log(chalk_1.default.gray(` ${endpoint.description.slice(0, 80)}${endpoint.description.length > 80 ? "..." : ""}`));
168
176
  }
@@ -195,7 +203,10 @@ async function apiCommand(slug, path, options) {
195
203
  ? chalk_1.default.dim(ep2)
196
204
  : chalk_1.default.dim(`$${parseFloat(Number(ep2).toFixed(4))}`);
197
205
  })();
198
- console.log(`${method} ${chalk_1.default.white(endpoint.path)} ${price}`);
206
+ const memberTag = endpoint.apiSlug && endpoint.apiSlug !== api.slug
207
+ ? chalk_1.default.dim(` [${(0, utils_js_1.sanitizeForTerminal)(endpoint.apiSlug)}]`)
208
+ : "";
209
+ console.log(`${method} ${chalk_1.default.white(endpoint.path)} ${price}${memberTag}`);
199
210
  if (endpoint.description) {
200
211
  console.log(chalk_1.default.gray(` ${endpoint.description.slice(0, 80)}${endpoint.description.length > 80 ? "..." : ""}`));
201
212
  }
@@ -1,3 +1,4 @@
1
+ import { SearchResponse } from "../api.js";
1
2
  export declare function searchCommand(query: string, options: {
2
3
  limit: string;
3
- }): Promise<void>;
4
+ }): Promise<SearchResponse>;
@@ -7,6 +7,7 @@ exports.searchCommand = searchCommand;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const ora_1 = __importDefault(require("ora"));
9
9
  const api_js_1 = require("../api.js");
10
+ const utils_js_1 = require("../utils.js");
10
11
  async function searchCommand(query, options) {
11
12
  const spinner = (0, ora_1.default)("Searching APIs...").start();
12
13
  try {
@@ -14,16 +15,24 @@ async function searchCommand(query, options) {
14
15
  spinner.stop();
15
16
  if (!data.results || data.results.length === 0) {
16
17
  console.log(chalk_1.default.yellow("No APIs found matching your query."));
17
- return;
18
+ return data;
18
19
  }
19
20
  console.log(chalk_1.default.bold(`\nFound ${data.apisCount} APIs with ${data.count} matching endpoints:\n`));
20
21
  for (const api of data.results) {
21
22
  console.log(chalk_1.default.cyan.bold(`${api.name}`) + chalk_1.default.gray(` (${api.slug})`));
22
23
  for (const endpoint of api.endpoints.slice(0, 3)) {
23
24
  const method = endpoint.method.padEnd(6);
25
+ // Group members commonly collide on the same upstream path (e.g.
26
+ // AbstractAPI's many products all expose "/v1") — when the endpoint's
27
+ // own callable slug differs from the card's flat slug, show it so the
28
+ // right identifier is obvious before calling `orth run`.
29
+ const memberTag = endpoint.apiSlug && endpoint.apiSlug !== api.slug
30
+ ? chalk_1.default.dim(` [${(0, utils_js_1.sanitizeForTerminal)(endpoint.apiSlug)}]`)
31
+ : "";
24
32
  console.log(chalk_1.default.gray(" ") +
25
33
  chalk_1.default.yellow(method) +
26
- chalk_1.default.white(endpoint.path));
34
+ chalk_1.default.white(endpoint.path) +
35
+ memberTag);
27
36
  if (endpoint.description) {
28
37
  console.log(chalk_1.default.gray(` ${endpoint.description.slice(0, 80)}${endpoint.description.length > 80 ? "..." : ""}`));
29
38
  }
@@ -35,10 +44,10 @@ async function searchCommand(query, options) {
35
44
  }
36
45
  console.log(chalk_1.default.gray(`Run 'orth api <slug>' to see all endpoints for an API`));
37
46
  console.log(chalk_1.default.gray(`Run 'orth run <api> <path>' to call an endpoint`));
47
+ return data;
38
48
  }
39
49
  catch (error) {
40
50
  spinner.stop();
41
- console.error(chalk_1.default.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
42
- process.exit(1);
51
+ throw error;
43
52
  }
44
53
  }
@@ -1,3 +1,29 @@
1
+ interface SkillResponse {
2
+ id: string;
3
+ name: string;
4
+ slug: string;
5
+ description?: string;
6
+ sourceType: "github" | "upload";
7
+ githubOwner?: string;
8
+ githubRepo?: string;
9
+ githubPath?: string;
10
+ githubRef?: string;
11
+ discoverable: boolean;
12
+ verified: boolean;
13
+ installCount: number;
14
+ tags?: string[];
15
+ installCommand?: string;
16
+ githubUrl?: string;
17
+ files?: {
18
+ filePath: string;
19
+ content: string;
20
+ isPrimary: boolean;
21
+ }[];
22
+ }
23
+ interface SkillSearchResponse {
24
+ results: SkillResponse[];
25
+ count: number;
26
+ }
1
27
  export declare function skillsListCommand(options: {
2
28
  limit: string;
3
29
  }): Promise<void>;
@@ -6,7 +32,7 @@ export declare function skillsMineCommand(options: {
6
32
  }): Promise<void>;
7
33
  export declare function skillsSearchCommand(query: string, options: {
8
34
  limit: string;
9
- }): Promise<void>;
35
+ }): Promise<SkillSearchResponse>;
10
36
  export declare function skillsShowCommand(slug: string): Promise<void>;
11
37
  export declare function skillsCreateCommand(githubRepo: string, options: {
12
38
  path?: string;
@@ -37,3 +63,4 @@ export declare function skillsPublishCommand(slug: string, options: {
37
63
  unpublish?: boolean;
38
64
  }): Promise<void>;
39
65
  export declare function skillsRequestCommand(input: string): Promise<void>;
66
+ export {};
@@ -150,7 +150,7 @@ async function skillsSearchCommand(query, options) {
150
150
  spinner.stop();
151
151
  if (!data.results || data.results.length === 0) {
152
152
  console.log(chalk_1.default.yellow("No skills found matching your query."));
153
- return;
153
+ return data;
154
154
  }
155
155
  console.log(chalk_1.default.bold(`\nFound ${data.count} skills:\n`));
156
156
  for (const skill of data.results) {
@@ -164,11 +164,11 @@ async function skillsSearchCommand(query, options) {
164
164
  console.log();
165
165
  }
166
166
  console.log(chalk_1.default.gray("Run 'orth skills show <slug>' for full details"));
167
+ return data;
167
168
  }
168
169
  catch (error) {
169
170
  spinner.stop();
170
- console.error(chalk_1.default.red(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
171
- process.exit(1);
171
+ throw error;
172
172
  }
173
173
  }
174
174
  // ─────────────────────────────────────────────────────────────────────────────
package/dist/index.js CHANGED
@@ -13,17 +13,31 @@ const apiRequest_js_1 = require("./commands/apiRequest.js");
13
13
  const analytics_js_1 = require("./analytics.js");
14
14
  /**
15
15
  * Wraps an async action callback so that rejected promises are caught,
16
- * logged, and cause the process to exit with code 1. Without this,
16
+ * logged, and set the process exit code to 1. Without this,
17
17
  * Commander.js silently swallows unhandled rejections from async actions.
18
18
  */
19
19
  function asyncAction(fn) {
20
20
  return (...args) => {
21
21
  fn(...args).catch((err) => {
22
22
  console.error(err instanceof Error ? err.message : "Unknown error");
23
- process.exit(1);
23
+ // Avoid a hard exit so fire-and-forget failure analytics can flush.
24
+ process.exitCode = 1;
24
25
  });
25
26
  };
26
27
  }
28
+ async function runTrackedSearch(command, query, operation) {
29
+ try {
30
+ const response = await operation();
31
+ (0, analytics_js_1.trackEvent)(command, { query }, response);
32
+ }
33
+ catch (error) {
34
+ (0, analytics_js_1.trackEvent)(command, { query }, {
35
+ success: false,
36
+ error: error instanceof Error ? error.message : "Unknown error",
37
+ });
38
+ throw error;
39
+ }
40
+ }
27
41
  const program = new commander_1.Command();
28
42
  program
29
43
  .name("orth")
@@ -99,8 +113,7 @@ apiGroup
99
113
  .description("Search for APIs using natural language")
100
114
  .option("-l, --limit <number>", "Max results", "10")
101
115
  .action(asyncAction(async (query, options) => {
102
- (0, analytics_js_1.trackEvent)("api.search", { query });
103
- await (0, search_js_1.searchCommand)(query, options);
116
+ await runTrackedSearch("api.search", query, () => (0, search_js_1.searchCommand)(query, options));
104
117
  }));
105
118
  apiGroup
106
119
  .command("run <slug> <path>")
@@ -159,8 +172,7 @@ skillsGroup
159
172
  .description("Search for agent skills")
160
173
  .option("-l, --limit <number>", "Max results", "20")
161
174
  .action(asyncAction(async (query, options) => {
162
- (0, analytics_js_1.trackEvent)("skills.search", { query });
163
- await (0, skills_js_1.skillsSearchCommand)(query, options);
175
+ await runTrackedSearch("skills.search", query, () => (0, skills_js_1.skillsSearchCommand)(query, options));
164
176
  }));
165
177
  skillsGroup
166
178
  .command("show <slug>")
@@ -254,8 +266,7 @@ program
254
266
  .description("Search for APIs (alias for 'orth api search')")
255
267
  .option("-l, --limit <number>", "Max results", "10")
256
268
  .action(asyncAction(async (query, options) => {
257
- (0, analytics_js_1.trackEvent)("search", { query });
258
- await (0, search_js_1.searchCommand)(query, options);
269
+ await runTrackedSearch("search", query, () => (0, search_js_1.searchCommand)(query, options));
259
270
  }));
260
271
  program
261
272
  .command("run <api> <path>")
@@ -0,0 +1 @@
1
+ export declare function sanitizeForTerminal(value: string): string;
package/dist/utils.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeForTerminal = sanitizeForTerminal;
4
+ // Strips ASCII control characters (including ESC, 0x1B) from a string before
5
+ // it's written to the terminal. Needed for any remotely-sourced value we
6
+ // render directly (e.g. apiSlug, which an API owner sets) — without this, a
7
+ // crafted value containing terminal escape sequences could manipulate another
8
+ // user's terminal (spoofed output, hidden/cleared text, deceptive hyperlinks).
9
+ function sanitizeForTerminal(value) {
10
+ // eslint-disable-next-line no-control-regex
11
+ return value.replace(/[\x00-\x1F\x7F]/g, "");
12
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orth/cli",
3
- "version": "0.2.33",
3
+ "version": "0.3.0",
4
4
  "description": "CLI to access all APIs and skills on the Orthogonal platform",
5
5
  "main": "dist/index.js",
6
6
  "bin": {