@sorit/cli 0.0.0-stage → 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 +46 -2
- package/package.json +22 -4
- package/src/cli.mjs +143 -0
- package/src/client.mjs +68 -0
- package/src/output.mjs +120 -0
package/README.md
CHANGED
|
@@ -1,3 +1,47 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Sorit CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Discover and call commerce, social and search APIs through your Sorit account.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
bun install --global @sorit/cli
|
|
7
|
+
export SORIT_API_KEY='<YOUR_API_KEY>'
|
|
8
|
+
sorit discover --tool tiktok
|
|
9
|
+
sorit inspect '<API_ID>'
|
|
10
|
+
export SORIT_REQUEST_KEY="$(bun -e 'console.log(crypto.randomUUID())')"
|
|
11
|
+
sorit run '<API_ID>' --input @input.json --idempotency-key "$SORIT_REQUEST_KEY" --wait
|
|
12
|
+
sorit get '<REQUEST_ID>' --wait
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Bun 1.2+. The API server defaults to production at `https://sorit.dev`; no server configuration is needed. Create an API key at https://sorit.dev/app/keys and set `SORIT_API_KEY`. For local development, override the origin with an environment variable, for example `SORIT_URL=http://localhost:3010 sorit tools`. Failures exit with code 1. `run --wait` writes the accepted request ID to stderr before polling, so interrupted waits can be resumed.
|
|
16
|
+
|
|
17
|
+
## Output
|
|
18
|
+
|
|
19
|
+
Commands display readable summaries by default: API names and prices, schema details, request status and business data, or balance. `sorit balance` shows the USD equivalent and credit amount, without prices or payment settings. Dollar values retain up to six decimal places so small credit balances are not rounded away.
|
|
20
|
+
|
|
21
|
+
For scripts and agents, append `--json` to any command. Successful responses go to stdout; HTTP and command errors go to stderr. Terminal failed requests still exit with code 1. `balance --json` returns only `balance` (credits), `creditsPerUsd`, `balanceUsd` and `currency`; other commands retain their REST JSON envelopes. Pending requests label the reserved credits; completed requests show net spending after refunds.
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
sorit balance
|
|
25
|
+
sorit balance --json
|
|
26
|
+
sorit discover --tool tiktok --json
|
|
27
|
+
sorit get '<REQUEST_ID>' --wait --json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Commands and help
|
|
31
|
+
|
|
32
|
+
| Command | Purpose |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `sorit tools` | List data tools and platforms. |
|
|
35
|
+
| `sorit discover` | Search APIs by tool or keywords. |
|
|
36
|
+
| `sorit inspect <api-id>` | Read the input schema, price and availability. |
|
|
37
|
+
| `sorit run <api-id>` | Create a paid request; optionally wait for results. |
|
|
38
|
+
| `sorit get <request-id>` | Fetch status and results, or resume waiting. |
|
|
39
|
+
| `sorit balance` | Read the account credit balance. |
|
|
40
|
+
|
|
41
|
+
Run `sorit --help` for the command list. Each command has its own arguments, options and examples: `sorit run --help` or `sorit help run`. Help and version commands work offline without credentials; `--version`, `-V` and `-v` print the version. Discovery and inspection do not require an API key. `run`, `get` and `balance` do.
|
|
42
|
+
|
|
43
|
+
Use `--search` for keyword filtering. Discovery always returns callable APIs only: the adapter must be ready, pricing and the endpoint enabled, and the provider enabled with configured credentials. `--status` accepts only `available`; unavailable APIs are managed in the admin console. Discovery supports `--page` from 1 to 10000. `run` and `get` accept `--timeout` in milliseconds with `--wait` (default 120000). Unsupported command options fail before making an API request.
|
|
44
|
+
|
|
45
|
+
Calls use your account's credits. Reuse the same idempotency key and input for retries. A polling timeout does not cancel the request. Keep API keys in the environment.
|
|
46
|
+
|
|
47
|
+
Agent instructions: https://sorit.dev/SKILL.md. REST reference: https://sorit.dev/en/docs/api/jobs.
|
package/package.json
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sorit/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Discover and call Sorit data APIs from your terminal or AI agent",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sorit": "src/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/cli.mjs",
|
|
11
|
+
"src/client.mjs",
|
|
12
|
+
"src/output.mjs",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"bun": ">=1.2.0"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://sorit.dev",
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public",
|
|
21
|
+
"registry": "https://registry.npmjs.org",
|
|
22
|
+
"@sorit:registry": "https://registry.npmjs.org"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { SoritClient } from "./client.mjs";
|
|
5
|
+
import { balanceSummary, formatError, formatResult } from "./output.mjs";
|
|
6
|
+
|
|
7
|
+
const { version } = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
8
|
+
const flags = {
|
|
9
|
+
search: { syntax: "--search <text>", description: "Filter by keywords." },
|
|
10
|
+
status: { syntax: "--status <status>", description: "Only available is supported; unavailable APIs are excluded." },
|
|
11
|
+
tool: { syntax: "--tool <tool-id>", description: "Filter by a tool ID, such as tiktok or twitter." },
|
|
12
|
+
page: { syntax: "--page <number>", description: "Page number, 1–10000. Default: 1." },
|
|
13
|
+
input: { syntax: "--input <json|@file>", description: "Input JSON object or @file.json. Required." },
|
|
14
|
+
"idempotency-key": { syntax: "--idempotency-key <key>", description: "Unique request key; reuse it with the same input on retries. Required." },
|
|
15
|
+
wait: { syntax: "--wait", description: "Poll until the request finishes." },
|
|
16
|
+
timeout: { syntax: "--timeout <milliseconds>", description: "Polling time limit with --wait. Default: 120000." },
|
|
17
|
+
};
|
|
18
|
+
const commands = {
|
|
19
|
+
tools: {
|
|
20
|
+
description: "List data tools and platforms.",
|
|
21
|
+
usage: "tools [options]", options: ["search", "status"],
|
|
22
|
+
notes: "Only tools with callable APIs are returned. No API key required.",
|
|
23
|
+
examples: ["sorit tools", 'sorit tools --search "social"'],
|
|
24
|
+
},
|
|
25
|
+
discover: {
|
|
26
|
+
description: "Search for data APIs by tool or keywords.",
|
|
27
|
+
usage: "discover [options]", options: ["tool", "search", "status", "page"],
|
|
28
|
+
notes: "Only callable APIs are returned. Disabled, unconfigured and unmapped APIs are excluded.\nNo API key required.",
|
|
29
|
+
examples: ["sorit discover --tool tiktok", 'sorit discover --search "search volume" --page 1'],
|
|
30
|
+
},
|
|
31
|
+
inspect: {
|
|
32
|
+
description: "Show an API's input schema, price and availability.",
|
|
33
|
+
usage: "inspect <api-id>", options: [],
|
|
34
|
+
argument: ["<api-id>", "Registered API ID returned by discover."],
|
|
35
|
+
notes: "Inspect an API before calling it. No API key required.",
|
|
36
|
+
examples: ["sorit inspect <api-id>"],
|
|
37
|
+
},
|
|
38
|
+
run: {
|
|
39
|
+
description: "Create a paid API request and optionally wait for results.",
|
|
40
|
+
usage: "run <api-id> --input <json|@file> --idempotency-key <key> [options]",
|
|
41
|
+
options: ["input", "idempotency-key", "wait", "timeout"],
|
|
42
|
+
argument: ["<api-id>", "Registered API ID returned by discover or a supported recipe ID."],
|
|
43
|
+
notes: "Requires SORIT_API_KEY and account credits. Use the same key and input for retries.\nA wait timeout does not cancel the request; resume with sorit get <request-id> --wait.",
|
|
44
|
+
examples: [
|
|
45
|
+
"sorit run <api-id> --input @input.json --idempotency-key research-001 --wait",
|
|
46
|
+
`sorit run <api-id> --input '{"query":{"limit":10}}' --idempotency-key research-002`,
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
get: {
|
|
50
|
+
description: "Fetch a request's status and results, or resume waiting.",
|
|
51
|
+
usage: "get <request-id> [options]", options: ["wait", "timeout"],
|
|
52
|
+
argument: ["<request-id>", "request_id returned by run."],
|
|
53
|
+
notes: "Requires SORIT_API_KEY. Polling does not create another paid request.\nA wait timeout does not cancel the request.",
|
|
54
|
+
examples: ["sorit get <request-id>", "sorit get <request-id> --wait --timeout 120000"],
|
|
55
|
+
},
|
|
56
|
+
balance: {
|
|
57
|
+
description: "Show your account's credit balance.",
|
|
58
|
+
usage: "balance [options]", options: [],
|
|
59
|
+
notes: "Requires SORIT_API_KEY. Shows USD equivalent and credits; no checkout configuration.", examples: ["sorit balance", "sorit balance --json"],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
const rows = entries => entries.map(([label, description]) => ` ${label.padEnd(30)} ${description}`).join("\n");
|
|
63
|
+
const section = (title, body) => `${title}:\n${body}`;
|
|
64
|
+
const globalOptions = [
|
|
65
|
+
["-h, --help", "Show help for this command."],
|
|
66
|
+
["-V, --version", "Show the version number (-v also supported)."],
|
|
67
|
+
["--json", "Output structured JSON for scripts and agents."],
|
|
68
|
+
];
|
|
69
|
+
function help(command) {
|
|
70
|
+
const spec = commands[command];
|
|
71
|
+
return [
|
|
72
|
+
section("Usage", ` sorit ${spec?.usage ?? "<command> [options]"}`),
|
|
73
|
+
section("Version", ` ${version}`),
|
|
74
|
+
section("Description", ` ${spec?.description ?? "Sorit CLI — discover, inspect and run data APIs."}`),
|
|
75
|
+
...(spec?.argument ? [section("Arguments", rows([spec.argument]))] : []),
|
|
76
|
+
section("Options", rows([...(spec?.options ?? []).map(name => [flags[name].syntax, flags[name].description]), ...globalOptions])),
|
|
77
|
+
...(!spec ? [section("Commands", rows([...Object.entries(commands).map(([name, value]) => [name, value.description]), ["help [command]", "Show help for a command."]]))] : []),
|
|
78
|
+
section("Environment", rows([
|
|
79
|
+
["SORIT_API_KEY", "API key for run, get and balance."],
|
|
80
|
+
["SORIT_URL", "Optional origin override for local deployments."],
|
|
81
|
+
])),
|
|
82
|
+
"API server: https://sorit.dev (default; no configuration required).",
|
|
83
|
+
...(spec?.notes ? [section("Notes", spec.notes.split("\n").map(line => ` ${line}`).join("\n"))] : []),
|
|
84
|
+
section("Examples", (spec?.examples ?? ["sorit discover --tool tiktok", "sorit inspect <api-id>", "sorit run --help"]).map(line => ` ${line}`).join("\n")),
|
|
85
|
+
"Docs: https://sorit.dev/en/docs/integrations",
|
|
86
|
+
].join("\n\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let jsonOutput = process.argv.slice(2).includes("--json");
|
|
90
|
+
try {
|
|
91
|
+
const { values, positionals } = parseArgs({ args: process.argv.slice(2).map(arg => arg === "-v" ? "-V" : arg), allowPositionals: true, options: {
|
|
92
|
+
version: { type: "boolean", short: "V" }, help: { type: "boolean", short: "h" }, wait: { type: "boolean" },
|
|
93
|
+
input: { type: "string" }, "idempotency-key": { type: "string" },
|
|
94
|
+
tool: { type: "string" }, search: { type: "string" }, page: { type: "string" },
|
|
95
|
+
status: { type: "string" }, timeout: { type: "string" },
|
|
96
|
+
json: { type: "boolean" },
|
|
97
|
+
} });
|
|
98
|
+
jsonOutput = Boolean(values.json);
|
|
99
|
+
const explicitHelp = positionals[0] === "help";
|
|
100
|
+
const [command, id] = explicitHelp ? positionals.slice(1) : positionals;
|
|
101
|
+
const spec = commands[command];
|
|
102
|
+
if (command && !Object.hasOwn(commands, command)) throw new Error(`Unknown command "${command}". Use sorit --help.`);
|
|
103
|
+
for (const name of Object.keys(values)) {
|
|
104
|
+
if (!["version", "help", "json"].includes(name) && !spec?.options.includes(name)) throw new Error(`--${name} is not supported here. Use sorit ${command ? `${command} ` : ""}--help.`);
|
|
105
|
+
}
|
|
106
|
+
if (explicitHelp && positionals.length > 2) throw new Error("Usage: sorit help [command].");
|
|
107
|
+
if (values.version) console.log(version);
|
|
108
|
+
else if (values.help || explicitHelp || !command) console.log(help(command));
|
|
109
|
+
else {
|
|
110
|
+
if (positionals.length !== (spec.argument ? 2 : 1) || spec.argument && !id?.trim()) throw new Error(`Usage: sorit ${spec.usage}. See sorit ${command} --help.`);
|
|
111
|
+
if (values.timeout !== undefined && !values.wait) throw new Error("--timeout requires --wait.");
|
|
112
|
+
const timeoutMs = Number(values.timeout ?? 120000);
|
|
113
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 4294967295) throw new Error("--timeout must be an integer between 1 and 4294967295 milliseconds.");
|
|
114
|
+
if (values.page !== undefined && (!/^\d+$/.test(values.page) || Number(values.page) < 1 || Number(values.page) > 10000)) throw new Error("--page must be an integer between 1 and 10000.");
|
|
115
|
+
if (values.status !== undefined && values.status !== "available") throw new Error("Search only returns available APIs. --status must be available.");
|
|
116
|
+
const client = new SoritClient({ baseUrl: process.env.SORIT_URL?.trim() || undefined, apiKey: process.env.SORIT_API_KEY ?? process.env.SELLERTOOL_API_KEY });
|
|
117
|
+
const filters = { q: values.search, page: values.page, status: values.status };
|
|
118
|
+
let result;
|
|
119
|
+
if (command === "balance") result = balanceSummary(await client.request("/api/credits", { authenticated: true }));
|
|
120
|
+
if (command === "tools") result = await client.tools(filters);
|
|
121
|
+
if (command === "discover") result = await client.endpoints({ ...filters, tool: values.tool });
|
|
122
|
+
if (command === "inspect") result = await client.describe(id);
|
|
123
|
+
if (command === "run") {
|
|
124
|
+
if (!values.input) throw new Error("Supply --input @input.json or a JSON object string. See sorit run --help.");
|
|
125
|
+
const input = JSON.parse(values.input.startsWith("@") ? await readFile(values.input.slice(1), "utf8") : values.input);
|
|
126
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Input must be a JSON object.");
|
|
127
|
+
result = await client.create(id, input, { idempotencyKey: values["idempotency-key"] });
|
|
128
|
+
// Print the accepted ID before polling so interrupted calls can be resumed.
|
|
129
|
+
if (values.wait && ["queued", "running"].includes(result.status)) {
|
|
130
|
+
console.error(`Accepted request_id: ${result.request_id}`);
|
|
131
|
+
result = await client.wait(result.request_id, { timeoutMs });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (command === "get") result = values.wait ? await client.wait(id, { timeoutMs }) : await client.get(id);
|
|
135
|
+
const failed = ["failed", "rejected"].includes(result?.status);
|
|
136
|
+
const write = failed && !jsonOutput ? console.error : console.log;
|
|
137
|
+
write(jsonOutput ? JSON.stringify(result, null, 2) : formatResult(command, result));
|
|
138
|
+
if (failed) process.exitCode = 1;
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error(jsonOutput ? JSON.stringify(error.result ?? { error: error.message }, null, 2) : formatError(error));
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
}
|
package/src/client.mjs
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Internal HTTP transport for the Sorit CLI. */
|
|
2
|
+
export class SoritError extends Error {
|
|
3
|
+
constructor(message, { status = 0, result = null } = {}) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "SoritError";
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.result = result;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class SoritClient {
|
|
12
|
+
/** @param {{baseUrl?: string, apiKey?: string, timeoutMs?: number}} [options] */
|
|
13
|
+
constructor({ baseUrl = "https://sorit.dev", apiKey, timeoutMs = 30_000 } = {}) {
|
|
14
|
+
const url = new URL(baseUrl);
|
|
15
|
+
if (!(["https:"].includes(url.protocol) || url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) throw new Error("Use HTTPS, or HTTP on localhost.");
|
|
16
|
+
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw new Error("baseUrl must be an origin.");
|
|
17
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("timeoutMs must be positive.");
|
|
18
|
+
this.baseUrl = url.origin;
|
|
19
|
+
this.apiKey = apiKey;
|
|
20
|
+
this.timeoutMs = timeoutMs;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async request(path, { method = "GET", body, idempotencyKey, authenticated = false, timeoutMs = this.timeoutMs } = {}) {
|
|
24
|
+
if (!path.startsWith("/") || path.startsWith("//")) throw new Error("Use a Sorit API path.");
|
|
25
|
+
if (authenticated && !this.apiKey) throw new Error(`Set SORIT_API_KEY. Create an API key at ${this.baseUrl}/app/keys.`);
|
|
26
|
+
const headers = { Accept: "application/json" };
|
|
27
|
+
if (authenticated) headers.Authorization = `Bearer ${this.apiKey}`;
|
|
28
|
+
if (body !== undefined) headers["Content-Type"] = "application/json";
|
|
29
|
+
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
|
|
30
|
+
// Never forward a bearer key through a redirect, or automatically retry a paid POST.
|
|
31
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
32
|
+
method, headers, redirect: "error", signal: AbortSignal.timeout(timeoutMs),
|
|
33
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
34
|
+
});
|
|
35
|
+
let result;
|
|
36
|
+
try { result = await response.json(); }
|
|
37
|
+
catch { throw new SoritError("Sorit returned a non-JSON response.", { status: response.status }); }
|
|
38
|
+
if (!response.ok) throw new SoritError((typeof result.error === "string" ? result.error : result.error?.message) ?? "Sorit request failed.", { status: response.status, result });
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
tools({ q = "", page = 1 } = {}) {
|
|
43
|
+
return this.request(`/api/tools?${new URLSearchParams({ q, status: "available", page: String(page) })}`);
|
|
44
|
+
}
|
|
45
|
+
endpoints({ tool = "", q = "", page = 1 } = {}) {
|
|
46
|
+
return this.request(`/api/endpoints?${new URLSearchParams({ tool, q, status: "available", page: String(page) })}`);
|
|
47
|
+
}
|
|
48
|
+
describe(apiId) { return this.request(`/api/tools/${encodeURIComponent(apiId)}`); }
|
|
49
|
+
create(apiId, input, { idempotencyKey } = {}) {
|
|
50
|
+
if (!idempotencyKey || !/^[A-Za-z0-9._:-]{1,128}$/.test(idempotencyKey)) throw new Error("Supply a unique idempotencyKey (1–128 letters, numbers, dots, underscores, colons or hyphens); reuse it for retries.");
|
|
51
|
+
return this.request(apiId.includes(".") ? `/api/v1/run/${encodeURIComponent(apiId)}` : "/api/jobs", { method: "POST", authenticated: true, body: apiId.includes(".") ? input : { kind: apiId, input }, idempotencyKey });
|
|
52
|
+
}
|
|
53
|
+
get(requestId, { timeoutMs } = {}) {
|
|
54
|
+
return this.request(`/api/jobs/${encodeURIComponent(requestId)}`, { authenticated: true, timeoutMs });
|
|
55
|
+
}
|
|
56
|
+
async wait(requestId, { intervalMs = 1500, timeoutMs = 120_000 } = {}) {
|
|
57
|
+
if (!Number.isFinite(intervalMs) || intervalMs < 100 || !Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("Use intervalMs >= 100 and timeoutMs > 0.");
|
|
58
|
+
const deadline = Date.now() + timeoutMs;
|
|
59
|
+
while (Date.now() < deadline) {
|
|
60
|
+
const result = await this.get(requestId, { timeoutMs: Math.max(1, Math.min(this.timeoutMs, deadline - Date.now())) });
|
|
61
|
+
if (["succeeded", "failed", "rejected"].includes(result.status)) return result;
|
|
62
|
+
if (!["queued", "running"].includes(result.status)) throw new SoritError("Unexpected request status.", { result });
|
|
63
|
+
const remaining = deadline - Date.now();
|
|
64
|
+
if (remaining > 0) await new Promise(resolve => setTimeout(resolve, Math.min(intervalMs, remaining)));
|
|
65
|
+
}
|
|
66
|
+
throw new SoritError(`Polling timed out for request_id ${requestId}. Resume with sorit get <request-id> --wait; the request may still be running.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/output.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** Terminal presentation, separate from the REST response envelopes. */
|
|
2
|
+
const number = value => new Intl.NumberFormat("en-US", { maximumFractionDigits: 6 }).format(value);
|
|
3
|
+
const usd = value => new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2, maximumFractionDigits: 6 }).format(value);
|
|
4
|
+
const text = value => String(value ?? "").replace(/[\u0000-\u001f\u007f]/g, " ").trim();
|
|
5
|
+
const json = value => JSON.stringify(value, null, 2);
|
|
6
|
+
const indent = value => String(value).split("\n").map(line => ` ${line}`).join("\n");
|
|
7
|
+
const heading = (label, body) => ` ${label}: ${text(body)}`;
|
|
8
|
+
const next = command => ` Next: ${command}`;
|
|
9
|
+
const creditPrice = api => {
|
|
10
|
+
const credits = api.pricing?.credits ?? api.policy?.credits;
|
|
11
|
+
if (typeof credits !== "number") return "Price unavailable";
|
|
12
|
+
return `${number(credits)} credits ${api.billing || api.pricing?.billing ? "maximum reservation" : `/ ${api.pricing?.unit ?? "request"}`}`;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function balanceSummary(result) {
|
|
16
|
+
if (typeof result?.balance !== "number" || !Number.isFinite(result.balance) || typeof result.creditsPerUsd !== "number" || !Number.isFinite(result.creditsPerUsd) || result.creditsPerUsd <= 0) throw new Error("Sorit returned an invalid balance response.");
|
|
17
|
+
return { balance: result.balance, creditsPerUsd: result.creditsPerUsd, balanceUsd: Number((result.balance / result.creditsPerUsd).toFixed(6)), currency: "USD" };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function apiRows(apis) {
|
|
21
|
+
return apis.map(api => [
|
|
22
|
+
` ${text(api.title?.en || api.name || api.id)}`,
|
|
23
|
+
` ${api.providerName || api.provider ? text(api.providerName || api.provider) + " · " : ""}${creditPrice(api)}`,
|
|
24
|
+
` ID: ${text(api.id)}`,
|
|
25
|
+
].join("\n")).join("\n\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function discover(result) {
|
|
29
|
+
const apis = result.apis ?? result.tools ?? [];
|
|
30
|
+
const total = result.total ?? apis.length;
|
|
31
|
+
if (!apis.length) return "No available APIs found.\n\n Try broader keywords or run sorit tools to browse platforms.";
|
|
32
|
+
return [
|
|
33
|
+
`✔ ${number(total)} available API${total === 1 ? "" : "s"} found`,
|
|
34
|
+
apiRows(apis),
|
|
35
|
+
` Page ${result.page ?? 1} / ${Math.ceil(total / (result.pageSize ?? apis.length))}`,
|
|
36
|
+
next(`sorit inspect ${text(apis[0].id)}`),
|
|
37
|
+
].join("\n\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function inspect(result) {
|
|
41
|
+
if (result.layer === "tool") return ` ${text(result.id)}\n\n${discover(result.endpoints)}`;
|
|
42
|
+
const rates = result.pricing?.billing;
|
|
43
|
+
const unit = { limit: "result", keywords: "keyword", asins: "ASIN" }[rates?.measure];
|
|
44
|
+
const lines = [
|
|
45
|
+
` ${text(result.title?.en || result.name || result.id)}`,
|
|
46
|
+
...(typeof result.description === "string" && result.description.trim() !== result.name?.trim() ? [indent(text(result.description))] : []),
|
|
47
|
+
[
|
|
48
|
+
heading("ID", result.id),
|
|
49
|
+
heading("Status", result.status),
|
|
50
|
+
...(result.providerName || result.provider ? [heading("Provider", result.providerName || result.provider)] : []),
|
|
51
|
+
...(result.method && result.endpoint ? [heading("Endpoint", `${result.method} ${result.endpoint}`)] : []),
|
|
52
|
+
heading("Price", creditPrice(result)),
|
|
53
|
+
...(rates && unit ? [heading("Base fee", `${number(rates.base)} credits / request`), heading("Unit fee", `${number(rates.perUnit)} credits / ${unit}`)] : []),
|
|
54
|
+
...(rates?.measure === "budget" ? [heading("Usage", "Settled from provider cost; unused reservation is refunded.")] : []),
|
|
55
|
+
...(result.limits?.maxItems != null ? [heading("Maximum items", result.limits.maxItems)] : []),
|
|
56
|
+
].join("\n"),
|
|
57
|
+
...(result.inputSchema ? [" Input schema:\n" + indent(json(result.inputSchema))] : []),
|
|
58
|
+
next(`sorit run ${text(result.id)} --input @input.json --idempotency-key <unique-key> --wait`),
|
|
59
|
+
];
|
|
60
|
+
return lines.join("\n\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function requestResult(result) {
|
|
64
|
+
const pending = ["queued", "running"].includes(result.status);
|
|
65
|
+
const failed = ["failed", "rejected"].includes(result.status);
|
|
66
|
+
const lines = [
|
|
67
|
+
`${failed ? "✖" : "✔"} Request ${text(result.status)}`,
|
|
68
|
+
[heading("Request", result.request_id), ...(result.api_id ? [heading("API", result.api_id)] : [])].join("\n"),
|
|
69
|
+
];
|
|
70
|
+
if (result.credits) {
|
|
71
|
+
const { charged, refunded } = result.credits;
|
|
72
|
+
if (typeof charged === "number" && typeof refunded === "number") {
|
|
73
|
+
lines.push(pending ? heading("Reserved", `${number(charged)} credits`) : heading("Spent", `${number(Number((charged - refunded).toFixed(2)))} credits`));
|
|
74
|
+
if (refunded > 0) lines.push(heading("Refunded", `${number(refunded)} credits`));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (result.error) lines.push(errorDetails(result.error));
|
|
78
|
+
if (result.data !== undefined && result.data !== null) lines.push(" Data:\n" + indent(json(result.data)));
|
|
79
|
+
if (pending) lines.push(next(`sorit get ${text(result.request_id)} --wait`));
|
|
80
|
+
return lines.join("\n\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const hints = {
|
|
84
|
+
UNAUTHORIZED: "Set SORIT_API_KEY to a valid key from your Sorit console.",
|
|
85
|
+
FORBIDDEN: "Use the account that owns this request.",
|
|
86
|
+
INSUFFICIENT_CREDITS: "Top up your Sorit account before creating another request.",
|
|
87
|
+
NOT_FOUND: "Run sorit discover to find an available API, or check the request ID.",
|
|
88
|
+
INVALID_INPUT: "Run sorit inspect <api-id> to check required parameters and limits.",
|
|
89
|
+
IDEMPOTENCY_CONFLICT: "Use the original idempotency key and identical input to resume the existing request.",
|
|
90
|
+
SERVICE_UNAVAILABLE: "Run sorit discover to find another available API, or retry later.",
|
|
91
|
+
};
|
|
92
|
+
function errorDetails(error) {
|
|
93
|
+
const code = text(typeof error === "string" ? error : error?.code).toUpperCase();
|
|
94
|
+
const message = text(typeof error === "string" ? code === "UNAUTHORIZED" ? "Authentication failed." : error : error?.message || error?.code || "Request failed.");
|
|
95
|
+
const fields = Array.isArray(error?.fields) ? error.fields.map(field => heading(Array.isArray(field.path) && field.path.length ? field.path.join(".") : text(field.field || "Input"), field.message || field.code || "Invalid value")) : [];
|
|
96
|
+
return [indent(message), ...(code && code !== message.toUpperCase() ? [heading("Code", code)] : []), ...fields, ...(hints[code] ? [indent(hints[code])] : [])].join("\n");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function formatError(error) {
|
|
100
|
+
const result = error?.result;
|
|
101
|
+
if (result?.request_id && result.status) return requestResult(result);
|
|
102
|
+
return `✖ ${errorDetails(result?.error ?? error?.message ?? "Request failed.").trimStart()}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function formatResult(command, result) {
|
|
106
|
+
if (command === "balance") return `✔ Balance retrieved\n\n Balance: ${usd(result.balanceUsd)} USD\n Credits: ${number(result.balance)}`;
|
|
107
|
+
if (command === "discover") return discover(result);
|
|
108
|
+
if (command === "inspect") return inspect(result);
|
|
109
|
+
if (command === "tools") {
|
|
110
|
+
const tools = result.tools ?? [];
|
|
111
|
+
if (result.apis) return discover(result);
|
|
112
|
+
if (!tools.length) return "No available tools found.\n\n Try broader keywords or run sorit tools without filters.";
|
|
113
|
+
return [
|
|
114
|
+
`✔ ${number(result.total ?? tools.length)} available tools found`,
|
|
115
|
+
tools.map(tool => ` ${text(tool.name || tool.id)}\n ${number(tool.endpointCount)} APIs · ${(tool.providers ?? []).map(provider => text(provider.name)).join(", ")}\n ID: ${text(tool.id)}`).join("\n\n"),
|
|
116
|
+
next(`sorit discover --tool ${text(tools[0].id)}`),
|
|
117
|
+
].join("\n\n");
|
|
118
|
+
}
|
|
119
|
+
return requestResult(result);
|
|
120
|
+
}
|