@aisa-one/cli 0.2.4 → 0.5.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/CHANGELOG.md +172 -1
- package/LICENSE +21 -0
- package/README.md +163 -169
- package/dist/api.d.ts +10 -0
- package/dist/api.js +14 -3
- package/dist/catalog.d.ts +1 -18
- package/dist/catalog.js +1 -24
- package/dist/cli-error.d.ts +17 -0
- package/dist/cli-error.js +25 -0
- package/dist/cli-guidance.d.ts +14 -0
- package/dist/cli-guidance.js +55 -0
- package/dist/commands/account.d.ts +17 -0
- package/dist/commands/account.js +45 -1
- package/dist/commands/api.d.ts +0 -11
- package/dist/commands/api.js +6 -175
- package/dist/commands/auth.d.ts +2 -1
- package/dist/commands/auth.js +43 -9
- package/dist/commands/brand-logos.d.ts +6 -0
- package/dist/commands/brand-logos.js +21 -0
- package/dist/commands/configCmd.js +8 -3
- package/dist/commands/connect-shared.d.ts +136 -0
- package/dist/commands/connect-shared.js +143 -0
- package/dist/commands/connect-t2.d.ts +18 -0
- package/dist/commands/connect-t2.js +1599 -0
- package/dist/commands/connect-terminal.d.ts +65 -0
- package/dist/commands/connect-terminal.js +822 -0
- package/dist/commands/connect.d.ts +31 -0
- package/dist/commands/connect.js +3181 -0
- package/dist/commands/flow.d.ts +685 -0
- package/dist/commands/flow.js +697 -0
- package/dist/commands/install.d.ts +91 -0
- package/dist/commands/install.js +258 -0
- package/dist/commands/llm-config.d.ts +103 -0
- package/dist/commands/llm-config.js +452 -0
- package/dist/commands/manifest.d.ts +68 -0
- package/dist/commands/manifest.js +90 -0
- package/dist/commands/mcp.d.ts +79 -2
- package/dist/commands/mcp.js +262 -48
- package/dist/commands/oauth-login.d.ts +33 -0
- package/dist/commands/oauth-login.js +333 -0
- package/dist/commands/prompt.d.ts +96 -0
- package/dist/commands/prompt.js +397 -0
- package/dist/commands/serve-results.d.ts +61 -0
- package/dist/commands/serve-results.js +245 -0
- package/dist/commands/serve-signin.d.ts +28 -0
- package/dist/commands/serve-signin.js +90 -0
- package/dist/commands/signin-page.d.ts +49 -0
- package/dist/commands/signin-page.js +250 -0
- package/dist/commands/skills.js +87 -50
- package/dist/commands/tool-help.d.ts +69 -0
- package/dist/commands/tool-help.js +189 -0
- package/dist/commands/tool-input.d.ts +22 -0
- package/dist/commands/tool-input.js +220 -0
- package/dist/commands/tools.d.ts +12 -0
- package/dist/commands/tools.js +307 -0
- package/dist/commands/update.d.ts +11 -0
- package/dist/commands/update.js +92 -0
- package/dist/commands/vscode.d.ts +65 -0
- package/dist/commands/vscode.js +173 -0
- package/dist/commands/wrappers.d.ts +23 -0
- package/dist/commands/wrappers.js +102 -0
- package/dist/completion.js +3 -80
- package/dist/config.js +76 -2
- package/dist/constants.d.ts +78 -38
- package/dist/constants.js +80 -102
- package/dist/index.js +155 -357
- package/dist/json-text.d.ts +12 -0
- package/dist/json-text.js +60 -0
- package/dist/router.d.ts +39 -0
- package/dist/router.js +59 -0
- package/dist/skills-registry.js +12 -3
- package/dist/utils/banner.d.ts +9 -0
- package/dist/utils/banner.js +41 -0
- package/dist/utils/browser.d.ts +19 -0
- package/dist/utils/browser.js +31 -0
- package/dist/utils/exec.d.ts +43 -0
- package/dist/utils/exec.js +61 -0
- package/dist/utils/http.d.ts +53 -0
- package/dist/utils/http.js +124 -0
- package/dist/utils/install-method.d.ts +36 -0
- package/dist/utils/install-method.js +48 -0
- package/dist/utils/journal.d.ts +57 -0
- package/dist/utils/journal.js +188 -0
- package/dist/utils/key-check.d.ts +20 -0
- package/dist/utils/key-check.js +24 -0
- package/dist/utils/update-check.d.ts +23 -0
- package/dist/utils/update-check.js +116 -0
- package/dist/utils/width.d.ts +32 -0
- package/dist/utils/width.js +73 -0
- package/package.json +8 -4
- package/vscode-extension/README.md +6 -0
- package/vscode-extension/aisa-vscode.vsix +0 -0
- package/dist/commands/finance.d.ts +0 -17
- package/dist/commands/finance.js +0 -342
- package/dist/commands/run.d.ts +0 -28
- package/dist/commands/run.js +0 -182
- package/dist/commands/search.d.ts +0 -9
- package/dist/commands/search.js +0 -81
- package/dist/commands/twitter.d.ts +0 -138
- package/dist/commands/twitter.js +0 -374
- package/dist/commands/video.d.ts +0 -49
- package/dist/commands/video.js +0 -311
package/dist/api.d.ts
CHANGED
|
@@ -33,6 +33,16 @@ export interface RequestOptions {
|
|
|
33
33
|
headers?: Record<string, string>;
|
|
34
34
|
/** Use domain API base URL (/apis/v1) instead of LLM base (/v1) */
|
|
35
35
|
domain?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Safe to send more than once, so a transient failure can be retried.
|
|
38
|
+
* Must be set deliberately: this CLI POSTs for reads and for writes alike,
|
|
39
|
+
* and a retried write posts a second tweet or bills a second video job.
|
|
40
|
+
* See utils/http.ts for why `chat` stays out of this even though a
|
|
41
|
+
* completion reads rather than writes.
|
|
42
|
+
*/
|
|
43
|
+
idempotent?: boolean;
|
|
44
|
+
/** Overrides the default 30s budget for a slow endpoint. */
|
|
45
|
+
timeoutMs?: number;
|
|
36
46
|
}
|
|
37
47
|
export declare function apiRequest<T = unknown>(apiKey: string, endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>>;
|
|
38
48
|
export declare function apiRequestRaw(apiKey: string, endpoint: string, options?: RequestOptions): Promise<Response>;
|
package/dist/api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BASE_URL } from "./constants.js";
|
|
2
|
+
import { httpFetch, INFO_TIMEOUT_MS } from "./utils/http.js";
|
|
2
3
|
import { getConfig } from "./config.js";
|
|
3
4
|
/**
|
|
4
5
|
* Derive every base path from one configured root.
|
|
@@ -55,10 +56,12 @@ export async function apiRequest(apiKey, endpoint, options = {}) {
|
|
|
55
56
|
"x-aisa-source": "cli",
|
|
56
57
|
...extraHeaders,
|
|
57
58
|
};
|
|
58
|
-
const res = await
|
|
59
|
+
const res = await httpFetch(url, {
|
|
59
60
|
method,
|
|
60
61
|
headers,
|
|
61
62
|
body: body ? JSON.stringify(body) : undefined,
|
|
63
|
+
idempotent: options.idempotent ?? method === "GET",
|
|
64
|
+
timeoutMs: options.timeoutMs,
|
|
62
65
|
});
|
|
63
66
|
if (!res.ok) {
|
|
64
67
|
const text = await res.text();
|
|
@@ -90,10 +93,12 @@ export async function apiRequestRaw(apiKey, endpoint, options = {}) {
|
|
|
90
93
|
"x-aisa-source": "cli",
|
|
91
94
|
...extraHeaders,
|
|
92
95
|
};
|
|
93
|
-
return
|
|
96
|
+
return httpFetch(url, {
|
|
94
97
|
method,
|
|
95
98
|
headers,
|
|
96
99
|
body: body ? JSON.stringify(body) : undefined,
|
|
100
|
+
idempotent: options.idempotent ?? method === "GET",
|
|
101
|
+
timeoutMs: options.timeoutMs,
|
|
97
102
|
});
|
|
98
103
|
}
|
|
99
104
|
/**
|
|
@@ -116,7 +121,13 @@ export async function publicRequest(path, options = {}) {
|
|
|
116
121
|
const headers = { "x-aisa-source": "cli" };
|
|
117
122
|
if (options.etag)
|
|
118
123
|
headers["If-None-Match"] = options.etag;
|
|
119
|
-
|
|
124
|
+
// Catalog and pricing are unauthenticated GETs against a cache — retrying
|
|
125
|
+
// one is free, and failing one degrades every discovery command.
|
|
126
|
+
const res = await httpFetch(`${resolveBases().info}/${path.replace(/^\/+/, "")}`, {
|
|
127
|
+
headers,
|
|
128
|
+
idempotent: true,
|
|
129
|
+
timeoutMs: INFO_TIMEOUT_MS,
|
|
130
|
+
});
|
|
120
131
|
if (res.status === 304)
|
|
121
132
|
return { status: 304 };
|
|
122
133
|
if (!res.ok)
|
package/dist/catalog.d.ts
CHANGED
|
@@ -47,7 +47,6 @@ export interface ProviderHealth {
|
|
|
47
47
|
not_tested_count: number;
|
|
48
48
|
checked_at?: string;
|
|
49
49
|
}
|
|
50
|
-
export declare const CATALOG_CONCURRENCY: number;
|
|
51
50
|
export declare function getProviders(options?: {
|
|
52
51
|
refresh?: boolean;
|
|
53
52
|
}): Promise<CatalogProvider[]>;
|
|
@@ -59,29 +58,13 @@ export declare function getHealth(options?: {
|
|
|
59
58
|
}): Promise<ProviderHealth[]>;
|
|
60
59
|
/** Flatten a provider's groups — the group labels carry no meaning. */
|
|
61
60
|
export declare function flatEndpoints(detail: CatalogDetail): CatalogEndpoint[];
|
|
62
|
-
/** Strip the /apis/v1 prefix
|
|
61
|
+
/** Strip the /apis/v1 prefix from a catalog path. */
|
|
63
62
|
export declare function toRunPath(path: string): string;
|
|
64
63
|
/**
|
|
65
64
|
* The slug an endpoint is actually served under, which is not always the
|
|
66
65
|
* provider id (provider `brave-search` serves `/apis/v1/brave/...`).
|
|
67
66
|
*/
|
|
68
67
|
export declare function runSlugOf(detail: CatalogDetail): string | undefined;
|
|
69
|
-
export interface EndpointRef {
|
|
70
|
-
provider: string;
|
|
71
|
-
endpoint: CatalogEndpoint;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Load every provider's endpoints. Cold this is ~29 requests and several
|
|
75
|
-
* hundred KB, so it runs through a bounded pool with progress reporting; warm
|
|
76
|
-
* it is entirely local.
|
|
77
|
-
*/
|
|
78
|
-
export declare function getAllEndpoints(options: {
|
|
79
|
-
refresh?: boolean;
|
|
80
|
-
onProgress?: (done: number, total: number) => void;
|
|
81
|
-
}): Promise<{
|
|
82
|
-
endpoints: EndpointRef[];
|
|
83
|
-
failed: string[];
|
|
84
|
-
}>;
|
|
85
68
|
/**
|
|
86
69
|
* Format a per-request price. Real values span $0.000001 to $0.08, so a fixed
|
|
87
70
|
* number of decimals either rounds the cheap endpoints to $0.00 or pads the
|
package/dist/catalog.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { publicRequest, cacheScope } from "./api.js";
|
|
2
2
|
import { readCache, writeCache, touchCache } from "./cache.js";
|
|
3
|
-
import { pool } from "./utils/pool.js";
|
|
4
3
|
const CATEGORY_TTL_MS = 6 * 60 * 60 * 1000;
|
|
5
4
|
const DETAIL_TTL_MS = 24 * 60 * 60 * 1000;
|
|
6
5
|
const HEALTH_TTL_MS = 5 * 60 * 1000;
|
|
7
|
-
export const CATALOG_CONCURRENCY = Number(process.env.AISA_CATALOG_CONCURRENCY) || 6;
|
|
8
6
|
/** Fetch with disk caching and ETag revalidation; falls back to stale data offline. */
|
|
9
7
|
async function cachedGet(path, cacheKey, ttlMs, refresh) {
|
|
10
8
|
const cached = readCache(cacheKey);
|
|
@@ -51,7 +49,7 @@ export function flatEndpoints(detail) {
|
|
|
51
49
|
.flatMap((g) => g.endpoints || [])
|
|
52
50
|
.sort((a, b) => a.path.localeCompare(b.path));
|
|
53
51
|
}
|
|
54
|
-
/** Strip the /apis/v1 prefix
|
|
52
|
+
/** Strip the /apis/v1 prefix from a catalog path. */
|
|
55
53
|
export function toRunPath(path) {
|
|
56
54
|
return path.replace(/^\/apis\/v1\//, "").replace(/^\/+/, "");
|
|
57
55
|
}
|
|
@@ -63,27 +61,6 @@ export function runSlugOf(detail) {
|
|
|
63
61
|
const first = flatEndpoints(detail)[0];
|
|
64
62
|
return first ? toRunPath(first.path).split("/")[0] : undefined;
|
|
65
63
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Load every provider's endpoints. Cold this is ~29 requests and several
|
|
68
|
-
* hundred KB, so it runs through a bounded pool with progress reporting; warm
|
|
69
|
-
* it is entirely local.
|
|
70
|
-
*/
|
|
71
|
-
export async function getAllEndpoints(options) {
|
|
72
|
-
const providers = await getProviders({ refresh: options.refresh });
|
|
73
|
-
const details = await pool(providers.map((p) => () => getProviderDetail(p.id, { refresh: options.refresh })), CATALOG_CONCURRENCY, options.onProgress);
|
|
74
|
-
const endpoints = [];
|
|
75
|
-
const failed = [];
|
|
76
|
-
details.forEach((detail, i) => {
|
|
77
|
-
if (!detail) {
|
|
78
|
-
failed.push(providers[i].id);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
for (const endpoint of flatEndpoints(detail)) {
|
|
82
|
-
endpoints.push({ provider: providers[i].id, endpoint });
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
return { endpoints, failed };
|
|
86
|
-
}
|
|
87
64
|
/**
|
|
88
65
|
* Format a per-request price. Real values span $0.000001 to $0.08, so a fixed
|
|
89
66
|
* number of decimals either rounds the cheap endpoints to $0.00 or pads the
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expected CLI failures with a Unix exit code.
|
|
3
|
+
*
|
|
4
|
+
* wrap() maps these to process.exit(code) instead of the generic exit 1 used
|
|
5
|
+
* for unexpected throws. Exit 3 (partial batch) is usually set on
|
|
6
|
+
* process.exitCode after the response has already been written.
|
|
7
|
+
*/
|
|
8
|
+
export declare class CliError extends Error {
|
|
9
|
+
readonly exitCode: number;
|
|
10
|
+
constructor(message: string, exitCode: number);
|
|
11
|
+
}
|
|
12
|
+
export declare const EXIT_OK = 0;
|
|
13
|
+
export declare const EXIT_TRANSPORT = 1;
|
|
14
|
+
export declare const EXIT_USAGE = 2;
|
|
15
|
+
export declare const EXIT_PARTIAL = 3;
|
|
16
|
+
export declare function usageError(message: string): CliError;
|
|
17
|
+
export declare function transportError(message: string): CliError;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expected CLI failures with a Unix exit code.
|
|
3
|
+
*
|
|
4
|
+
* wrap() maps these to process.exit(code) instead of the generic exit 1 used
|
|
5
|
+
* for unexpected throws. Exit 3 (partial batch) is usually set on
|
|
6
|
+
* process.exitCode after the response has already been written.
|
|
7
|
+
*/
|
|
8
|
+
export class CliError extends Error {
|
|
9
|
+
exitCode;
|
|
10
|
+
constructor(message, exitCode) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "CliError";
|
|
13
|
+
this.exitCode = exitCode;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export const EXIT_OK = 0;
|
|
17
|
+
export const EXIT_TRANSPORT = 1;
|
|
18
|
+
export const EXIT_USAGE = 2;
|
|
19
|
+
export const EXIT_PARTIAL = 3;
|
|
20
|
+
export function usageError(message) {
|
|
21
|
+
return new CliError(message, EXIT_USAGE);
|
|
22
|
+
}
|
|
23
|
+
export function transportError(message) {
|
|
24
|
+
return new CliError(message, EXIT_TRANSPORT);
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RouterOperation } from "./router.js";
|
|
2
|
+
/**
|
|
3
|
+
* Exact MCP operation identifiers → CLI command names.
|
|
4
|
+
* Human presentation only. Do not apply to --json or tool result payloads.
|
|
5
|
+
*/
|
|
6
|
+
export declare const MCP_CLI_MAP: Record<RouterOperation, {
|
|
7
|
+
identifier: string;
|
|
8
|
+
cli: string;
|
|
9
|
+
path: string;
|
|
10
|
+
}>;
|
|
11
|
+
/** Path after `aisa`, e.g. `search` — not a nested leaf like `api search`. */
|
|
12
|
+
export declare function mcpForCommand(commandPath: string): (typeof MCP_CLI_MAP)[RouterOperation] | undefined;
|
|
13
|
+
/** Project the four exact MCP identifiers onto CLI names. Other text is unchanged. */
|
|
14
|
+
export declare function projectMcpIdentifiersToCli(text: string): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ROUTER_PATHS } from "./router.js";
|
|
2
|
+
/**
|
|
3
|
+
* Exact MCP operation identifiers → CLI command names.
|
|
4
|
+
* Human presentation only. Do not apply to --json or tool result payloads.
|
|
5
|
+
*/
|
|
6
|
+
export const MCP_CLI_MAP = {
|
|
7
|
+
search: {
|
|
8
|
+
identifier: "AISA_SEARCH_TOOL",
|
|
9
|
+
cli: "aisa search",
|
|
10
|
+
path: ROUTER_PATHS.search,
|
|
11
|
+
},
|
|
12
|
+
schema: {
|
|
13
|
+
identifier: "AISA_BATCH_GET_SCHEMA",
|
|
14
|
+
cli: "aisa schema",
|
|
15
|
+
path: ROUTER_PATHS.schema,
|
|
16
|
+
},
|
|
17
|
+
quote: {
|
|
18
|
+
identifier: "AISA_BATCH_QUOTE",
|
|
19
|
+
cli: "aisa quote",
|
|
20
|
+
path: ROUTER_PATHS.quote,
|
|
21
|
+
},
|
|
22
|
+
call: {
|
|
23
|
+
identifier: "AISA_BATCH_USE",
|
|
24
|
+
cli: "aisa call",
|
|
25
|
+
path: ROUTER_PATHS.call,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const REPLACEMENTS = Object.values(MCP_CLI_MAP)
|
|
29
|
+
.map(({ identifier, cli }) => ({
|
|
30
|
+
identifier,
|
|
31
|
+
cli,
|
|
32
|
+
pattern: new RegExp(`\\b${escapeRegExp(identifier)}\\b`, "g"),
|
|
33
|
+
}))
|
|
34
|
+
.sort((a, b) => b.identifier.length - a.identifier.length);
|
|
35
|
+
/** Path after `aisa`, e.g. `search` — not a nested leaf like `api search`. */
|
|
36
|
+
export function mcpForCommand(commandPath) {
|
|
37
|
+
if (commandPath === "search" ||
|
|
38
|
+
commandPath === "schema" ||
|
|
39
|
+
commandPath === "quote" ||
|
|
40
|
+
commandPath === "call") {
|
|
41
|
+
return MCP_CLI_MAP[commandPath];
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Project the four exact MCP identifiers onto CLI names. Other text is unchanged. */
|
|
46
|
+
export function projectMcpIdentifiersToCli(text) {
|
|
47
|
+
let out = text;
|
|
48
|
+
for (const { pattern, cli } of REPLACEMENTS) {
|
|
49
|
+
out = out.replace(pattern, cli);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function escapeRegExp(value) {
|
|
54
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
55
|
+
}
|
|
@@ -2,6 +2,23 @@ export declare function formatMicrosUSD(micros: number | string | bigint): strin
|
|
|
2
2
|
export declare function balanceAction(options?: {
|
|
3
3
|
json?: boolean;
|
|
4
4
|
}): Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* `aisa topup [amount]` — open the console's billing page to add credit.
|
|
7
|
+
*
|
|
8
|
+
* Payment ends in a browser no matter what: card details must reach Stripe's
|
|
9
|
+
* hosted page, not us (PCI), and a bank's 3-D Secure step has nowhere else to
|
|
10
|
+
* run. So this command's job is to get the user to the right page, not to
|
|
11
|
+
* take a payment.
|
|
12
|
+
*
|
|
13
|
+
* With an amount it deep-links `?amount=`, which the billing page can prefill;
|
|
14
|
+
* without one the page is where the choice belongs. A future version can mint
|
|
15
|
+
* a Stripe checkout URL directly (backend already returns one from
|
|
16
|
+
* POST /api/billing/topups) and skip a hop — that needs the console API to
|
|
17
|
+
* accept the CLI's OAuth token, which it does not yet.
|
|
18
|
+
*/
|
|
19
|
+
export declare function topupAction(amount: string | undefined, options?: {
|
|
20
|
+
open?: boolean;
|
|
21
|
+
}): void;
|
|
5
22
|
export declare function usageAction(_options: {
|
|
6
23
|
limit?: string;
|
|
7
24
|
days?: string;
|
package/dist/commands/account.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { run } from "../utils/exec.js";
|
|
1
2
|
import chalk from "chalk";
|
|
2
3
|
import { requireApiKey } from "../config.js";
|
|
3
4
|
import { apiRequest } from "../api.js";
|
|
4
|
-
import { formatJson, hint } from "../utils/display.js";
|
|
5
|
+
import { formatJson, hint, info, error } from "../utils/display.js";
|
|
6
|
+
import { CONSOLE_BILLING_URL } from "../constants.js";
|
|
5
7
|
export function formatMicrosUSD(micros) {
|
|
6
8
|
const value = typeof micros === "bigint" ? micros : BigInt(micros);
|
|
7
9
|
const negative = value < 0n;
|
|
@@ -33,6 +35,48 @@ export async function balanceAction(options = {}) {
|
|
|
33
35
|
else {
|
|
34
36
|
console.log(` API key limit: ${formatMicrosUSD(balance.api_key.remaining_micros_usd)} remaining`);
|
|
35
37
|
}
|
|
38
|
+
// Say it here rather than at the failed call: a balance read is exactly when
|
|
39
|
+
// someone can act on it.
|
|
40
|
+
if (Number(balance.account_balance_micros_usd) <= 0) {
|
|
41
|
+
hint("Out of credit — run 'aisa topup' to add some");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `aisa topup [amount]` — open the console's billing page to add credit.
|
|
46
|
+
*
|
|
47
|
+
* Payment ends in a browser no matter what: card details must reach Stripe's
|
|
48
|
+
* hosted page, not us (PCI), and a bank's 3-D Secure step has nowhere else to
|
|
49
|
+
* run. So this command's job is to get the user to the right page, not to
|
|
50
|
+
* take a payment.
|
|
51
|
+
*
|
|
52
|
+
* With an amount it deep-links `?amount=`, which the billing page can prefill;
|
|
53
|
+
* without one the page is where the choice belongs. A future version can mint
|
|
54
|
+
* a Stripe checkout URL directly (backend already returns one from
|
|
55
|
+
* POST /api/billing/topups) and skip a hop — that needs the console API to
|
|
56
|
+
* accept the CLI's OAuth token, which it does not yet.
|
|
57
|
+
*/
|
|
58
|
+
export function topupAction(amount, options = {}) {
|
|
59
|
+
let url = CONSOLE_BILLING_URL;
|
|
60
|
+
if (amount !== undefined) {
|
|
61
|
+
const value = Number(amount);
|
|
62
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
63
|
+
error(`Invalid amount: ${amount}`);
|
|
64
|
+
hint("Pass a positive number of US dollars, e.g. 'aisa topup 20'");
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
url = `${CONSOLE_BILLING_URL}?amount=${value}`;
|
|
69
|
+
info(`Opening the billing page to add ${formatMicrosUSD(BigInt(Math.round(value * 1_000_000)))}`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
info("Opening the billing page — choose an amount there");
|
|
73
|
+
}
|
|
74
|
+
console.log(` ${chalk.cyan(url)}`);
|
|
75
|
+
if (options.open === false)
|
|
76
|
+
return;
|
|
77
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
78
|
+
void run(cmd, [url], { timeout: 30_000 }).catch(() => { });
|
|
79
|
+
hint("Credit lands in your account as soon as the payment completes");
|
|
36
80
|
}
|
|
37
81
|
export async function usageAction(_options) {
|
|
38
82
|
requireApiKey();
|
package/dist/commands/api.d.ts
CHANGED
|
@@ -11,14 +11,3 @@ export declare function apiShowAction(idOrSlug: string, path?: string, options?:
|
|
|
11
11
|
all?: boolean;
|
|
12
12
|
refresh?: boolean;
|
|
13
13
|
}): Promise<void>;
|
|
14
|
-
export declare function apiSearchAction(query: string, options: {
|
|
15
|
-
limit?: string;
|
|
16
|
-
provider?: string;
|
|
17
|
-
json?: boolean;
|
|
18
|
-
refresh?: boolean;
|
|
19
|
-
}): Promise<void>;
|
|
20
|
-
export declare function apiCodeAction(slug: string, path: string, options: {
|
|
21
|
-
lang?: string;
|
|
22
|
-
method?: string;
|
|
23
|
-
refresh?: boolean;
|
|
24
|
-
}): Promise<void>;
|
package/dist/commands/api.js
CHANGED
|
@@ -2,8 +2,7 @@ import ora from "ora";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { error, formatJson, hint, table, truncate } from "../utils/display.js";
|
|
4
4
|
import { API_CATEGORIES, categoryOf } from "../constants.js";
|
|
5
|
-
import {
|
|
6
|
-
import { getProviders, getProviderDetail, getHealth, getAllEndpoints, flatEndpoints, toRunPath, runSlugOf, formatPrice, } from "../catalog.js";
|
|
5
|
+
import { getProviders, getProviderDetail, getHealth, flatEndpoints, toRunPath, runSlugOf, formatPrice, } from "../catalog.js";
|
|
7
6
|
const DEFAULT_ENDPOINT_LIMIT = 40;
|
|
8
7
|
function statusBadge(status) {
|
|
9
8
|
switch (status) {
|
|
@@ -69,7 +68,7 @@ export async function apiListAction(options) {
|
|
|
69
68
|
console.log();
|
|
70
69
|
console.log(chalk.gray(` ${providers.length} APIs · ${total} endpoints`));
|
|
71
70
|
hint("Details: aisa api show <api>");
|
|
72
|
-
hint("
|
|
71
|
+
hint("Catalog paths and prices are browsing metadata, not a substitute for aisa schema or aisa quote.");
|
|
73
72
|
}
|
|
74
73
|
/** Resolve a provider by id, or by the URL slug its endpoints are served under. */
|
|
75
74
|
async function findProvider(idOrSlug, refresh) {
|
|
@@ -89,7 +88,6 @@ async function findProvider(idOrSlug, refresh) {
|
|
|
89
88
|
}
|
|
90
89
|
function printEndpointDetail(detail, endpoint) {
|
|
91
90
|
const runPath = toRunPath(endpoint.path);
|
|
92
|
-
const [slug, ...rest] = runPath.split("/");
|
|
93
91
|
console.log(`\n ${chalk.cyan.bold(endpoint.name || runPath)}`);
|
|
94
92
|
if (endpoint.description)
|
|
95
93
|
console.log(` ${endpoint.description}`);
|
|
@@ -100,8 +98,8 @@ function printEndpointDetail(detail, endpoint) {
|
|
|
100
98
|
if (params.length > 0) {
|
|
101
99
|
console.log(` Path params: ${params.join(", ")}`);
|
|
102
100
|
}
|
|
103
|
-
console.log(
|
|
104
|
-
hint(
|
|
101
|
+
console.log();
|
|
102
|
+
hint("Catalog paths and prices are browsing metadata, not a substitute for aisa schema or aisa quote.");
|
|
105
103
|
}
|
|
106
104
|
export async function apiShowAction(idOrSlug, path, options = {}) {
|
|
107
105
|
const spinner = ora(`Loading ${idOrSlug}...`).start();
|
|
@@ -184,176 +182,9 @@ export async function apiShowAction(idOrSlug, path, options = {}) {
|
|
|
184
182
|
console.log(chalk.gray(`\n … ${endpoints.length - shown.length} more — pass --all to see them`));
|
|
185
183
|
}
|
|
186
184
|
}
|
|
187
|
-
|
|
188
|
-
if (slug) {
|
|
185
|
+
if (runSlugOf(detail)) {
|
|
189
186
|
console.log();
|
|
190
|
-
hint(`Call: aisa run ${slug} <path>`);
|
|
191
187
|
hint(`Detail: aisa api show ${detail.id} <path>`);
|
|
188
|
+
hint("Catalog paths and prices are browsing metadata, not a substitute for aisa schema or aisa quote.");
|
|
192
189
|
}
|
|
193
190
|
}
|
|
194
|
-
export async function apiSearchAction(query, options) {
|
|
195
|
-
const limit = options.limit ? parseInt(options.limit) : 20;
|
|
196
|
-
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
197
|
-
const spinner = ora("Indexing catalog...").start();
|
|
198
|
-
let endpoints;
|
|
199
|
-
let failed = [];
|
|
200
|
-
if (options.provider) {
|
|
201
|
-
const detail = await getProviderDetail(options.provider, { refresh: options.refresh }).catch(() => undefined);
|
|
202
|
-
if (!detail) {
|
|
203
|
-
spinner.fail(`Unknown API "${options.provider}"`);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
endpoints = flatEndpoints(detail).map((endpoint) => ({ provider: detail.id, endpoint }));
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
const result = await getAllEndpoints({
|
|
210
|
-
refresh: options.refresh,
|
|
211
|
-
onProgress: (done, total) => {
|
|
212
|
-
spinner.text = `Indexing catalog (${done}/${total})...`;
|
|
213
|
-
},
|
|
214
|
-
});
|
|
215
|
-
endpoints = result.endpoints;
|
|
216
|
-
failed = result.failed;
|
|
217
|
-
}
|
|
218
|
-
const providers = await getProviders({ refresh: options.refresh });
|
|
219
|
-
spinner.stop();
|
|
220
|
-
const matchesAll = (haystack) => terms.every((t) => haystack.includes(t));
|
|
221
|
-
// --provider means "restrict to one API", so provider-level hits have to be
|
|
222
|
-
// scoped too — otherwise `search brave --provider financial` reports brave.
|
|
223
|
-
const providerHits = providers
|
|
224
|
-
.filter((p) => !options.provider || p.id === options.provider)
|
|
225
|
-
.filter((p) => matchesAll(p.id.toLowerCase()));
|
|
226
|
-
const scored = endpoints
|
|
227
|
-
.map(({ provider, endpoint }) => {
|
|
228
|
-
const name = (endpoint.name || "").toLowerCase();
|
|
229
|
-
const path = toRunPath(endpoint.path).toLowerCase();
|
|
230
|
-
const desc = (endpoint.description || "").toLowerCase();
|
|
231
|
-
let score = 0;
|
|
232
|
-
if (matchesAll(name))
|
|
233
|
-
score = 3;
|
|
234
|
-
else if (matchesAll(path))
|
|
235
|
-
score = 2;
|
|
236
|
-
else if (matchesAll(desc))
|
|
237
|
-
score = 1;
|
|
238
|
-
return { provider, endpoint, score };
|
|
239
|
-
})
|
|
240
|
-
.filter((r) => r.score > 0)
|
|
241
|
-
.sort((a, b) => b.score - a.score || a.endpoint.path.localeCompare(b.endpoint.path));
|
|
242
|
-
if (options.json) {
|
|
243
|
-
console.log(formatJson({
|
|
244
|
-
providers: providerHits.map((p) => p.id),
|
|
245
|
-
endpoints: scored.slice(0, limit).map((r) => ({
|
|
246
|
-
provider: r.provider,
|
|
247
|
-
path: r.endpoint.path,
|
|
248
|
-
name: r.endpoint.name,
|
|
249
|
-
price: r.endpoint.pricing?.normal,
|
|
250
|
-
})),
|
|
251
|
-
}));
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
if (providerHits.length === 0 && scored.length === 0) {
|
|
255
|
-
console.log(` No matches for "${query}".`);
|
|
256
|
-
if (failed.length > 0) {
|
|
257
|
-
hint(`${failed.length} APIs could not be indexed: ${failed.join(", ")}`);
|
|
258
|
-
}
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
if (providerHits.length > 0) {
|
|
262
|
-
console.log(chalk.bold(`\n Matching APIs\n`));
|
|
263
|
-
for (const p of providerHits) {
|
|
264
|
-
console.log(` ${chalk.cyan.bold(p.id)} ${chalk.gray(`${p.endpoint_count} endpoints · from ${formatPrice(p.pricing?.normal)}`)}`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (scored.length > 0) {
|
|
268
|
-
console.log(chalk.bold(`\n Matching endpoints${scored.length > limit ? ` (${limit} of ${scored.length})` : ""}\n`));
|
|
269
|
-
for (const { endpoint } of scored.slice(0, limit)) {
|
|
270
|
-
const runPath = toRunPath(endpoint.path);
|
|
271
|
-
const [slug, ...rest] = runPath.split("/");
|
|
272
|
-
console.log(` ${chalk.cyan(runPath)} ${chalk.gray(formatPrice(endpoint.pricing?.normal))}`);
|
|
273
|
-
if (endpoint.name)
|
|
274
|
-
console.log(` ${chalk.gray(truncate(endpoint.name, 78))}`);
|
|
275
|
-
console.log(chalk.gray(` aisa run ${slug} /${rest.join("/")}`));
|
|
276
|
-
console.log();
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
if (failed.length > 0) {
|
|
280
|
-
hint(`${failed.length} APIs unavailable and not searched: ${failed.join(", ")}`);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
function generateCode(lang, url, method, note) {
|
|
284
|
-
const header = `# ${note}`;
|
|
285
|
-
switch (lang) {
|
|
286
|
-
case "curl":
|
|
287
|
-
return [
|
|
288
|
-
header,
|
|
289
|
-
`curl -X ${method} "${url}" \\`,
|
|
290
|
-
` -H "Authorization: Bearer $AISA_API_KEY" \\`,
|
|
291
|
-
` -H "Content-Type: application/json"`,
|
|
292
|
-
].join("\n");
|
|
293
|
-
case "python":
|
|
294
|
-
return [
|
|
295
|
-
header,
|
|
296
|
-
`import os, requests`,
|
|
297
|
-
``,
|
|
298
|
-
`resp = requests.${method.toLowerCase()}(`,
|
|
299
|
-
` "${url}",`,
|
|
300
|
-
` headers={"Authorization": f"Bearer {os.environ['AISA_API_KEY']}"},`,
|
|
301
|
-
method === "GET" ? ` params={},` : ` json={},`,
|
|
302
|
-
`)`,
|
|
303
|
-
`resp.raise_for_status()`,
|
|
304
|
-
`print(resp.json())`,
|
|
305
|
-
].join("\n");
|
|
306
|
-
case "node":
|
|
307
|
-
case "typescript":
|
|
308
|
-
return [
|
|
309
|
-
`// ${note}`,
|
|
310
|
-
`const res = await fetch("${url}", {`,
|
|
311
|
-
` method: "${method}",`,
|
|
312
|
-
` headers: {`,
|
|
313
|
-
` Authorization: \`Bearer \${process.env.AISA_API_KEY}\`,`,
|
|
314
|
-
` "Content-Type": "application/json",`,
|
|
315
|
-
` },`,
|
|
316
|
-
...(method === "GET" ? [] : [` body: JSON.stringify({}),`]),
|
|
317
|
-
`});`,
|
|
318
|
-
`console.log(await res.json());`,
|
|
319
|
-
].join("\n");
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
export async function apiCodeAction(slug, path, options) {
|
|
323
|
-
const lang = (options.lang || "curl").toLowerCase();
|
|
324
|
-
if (!["curl", "python", "node", "typescript"].includes(lang)) {
|
|
325
|
-
error(`Unknown language: ${lang}. Valid: curl, python, node, typescript`);
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
const cleanSlug = slug.replace(/^\/+|\/+$/g, "");
|
|
329
|
-
const cleanPath = path.replace(/^\/+/, "");
|
|
330
|
-
const runPath = toRunPath(`${cleanSlug}/${cleanPath}`);
|
|
331
|
-
const url = `${resolveBases().domain}/${runPath}`;
|
|
332
|
-
// Look the endpoint up when possible for validation and pricing, but never
|
|
333
|
-
// let a cold cache or an offline machine block code generation.
|
|
334
|
-
let endpoint;
|
|
335
|
-
try {
|
|
336
|
-
const { endpoints } = await getAllEndpoints({ refresh: options.refresh });
|
|
337
|
-
endpoint = endpoints.find((e) => toRunPath(e.endpoint.path) === runPath)?.endpoint;
|
|
338
|
-
if (!endpoint) {
|
|
339
|
-
hint(`No catalog entry for "${runPath}" — generating anyway`);
|
|
340
|
-
hint(`Search: aisa api search ${cleanPath.split("/")[0]}`);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
catch {
|
|
344
|
-
// offline: generate without validation
|
|
345
|
-
}
|
|
346
|
-
// The catalog hardcodes GET server-side, so it is a hint rather than a fact.
|
|
347
|
-
const method = (options.method || "GET").toUpperCase();
|
|
348
|
-
const price = endpoint?.pricing?.normal;
|
|
349
|
-
const note = [
|
|
350
|
-
endpoint?.name || runPath,
|
|
351
|
-
price != null ? `${formatPrice(price)} per request` : undefined,
|
|
352
|
-
"method is advisory — pass --method to override",
|
|
353
|
-
]
|
|
354
|
-
.filter(Boolean)
|
|
355
|
-
.join(" · ");
|
|
356
|
-
console.log();
|
|
357
|
-
console.log(generateCode(lang, url, method, note));
|
|
358
|
-
console.log();
|
|
359
|
-
}
|
package/dist/commands/auth.d.ts
CHANGED
package/dist/commands/auth.js
CHANGED
|
@@ -1,16 +1,50 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { setApiKey, clearApiKey, getApiKey, getKeySource, maskKey } from "../config.js";
|
|
3
|
-
import { success,
|
|
4
|
-
import { ENV_VAR_NAME } from "../constants.js";
|
|
5
|
-
export function loginAction(options) {
|
|
3
|
+
import { success, info } from "../utils/display.js";
|
|
4
|
+
import { CONSOLE_URL, ENV_VAR_NAME } from "../constants.js";
|
|
5
|
+
export async function loginAction(options) {
|
|
6
6
|
const key = options.key || process.env[ENV_VAR_NAME];
|
|
7
|
-
if (
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
if (key) {
|
|
8
|
+
setApiKey(key);
|
|
9
|
+
success(`Authenticated: ${maskKey(key)}`);
|
|
10
|
+
await proveItWorks();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
// No key given: the browser sign-in is the front door, not an error.
|
|
14
|
+
//
|
|
15
|
+
// `--no-browser` leaves `browser` false; not passing it leaves it true,
|
|
16
|
+
// and passing that on as an explicit "yes" overrode the machine's own read
|
|
17
|
+
// of whether it has a browser at all — which is how a server ended up
|
|
18
|
+
// waiting for a click. Only the false case is the user speaking.
|
|
19
|
+
const { oauthLogin } = await import("./oauth-login.js");
|
|
20
|
+
await oauthLogin(options.browser === false ? { open: false } : {});
|
|
21
|
+
await proveItWorks();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Use the key we just stored, and show what came back.
|
|
25
|
+
*
|
|
26
|
+
* "Signed in — key stored" says a file was written. It does not say the key
|
|
27
|
+
* works, and those are different claims: a pasted key can be the wrong one, a
|
|
28
|
+
* minted one can belong to an account with no credit. Ending on a balance
|
|
29
|
+
* turns the question "did that work?" into something already answered on
|
|
30
|
+
* screen, which is what someone finishing a sign-in actually wants to know.
|
|
31
|
+
*
|
|
32
|
+
* Failure here is not a failed sign-in — the key is stored either way — so it
|
|
33
|
+
* says what it could not do and points at the command to retry with.
|
|
34
|
+
*/
|
|
35
|
+
async function proveItWorks() {
|
|
36
|
+
try {
|
|
37
|
+
const { balanceAction } = await import("./account.js");
|
|
38
|
+
await balanceAction();
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
info(`Stored, but could not read your balance: ${e.message}`);
|
|
42
|
+
console.log(chalk.gray(" Try: aisa balance"));
|
|
10
43
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
44
|
+
// Last line, and an address rather than a command: everything above is
|
|
45
|
+
// this machine, and the account behind it lives somewhere the terminal
|
|
46
|
+
// cannot show. Written out so it can be remembered, not just clicked.
|
|
47
|
+
console.log(chalk.gray(` Account, usage and top-ups — ${chalk.cyan(CONSOLE_URL)}`));
|
|
14
48
|
}
|
|
15
49
|
export function logoutAction() {
|
|
16
50
|
clearApiKey();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand marks used by the T2 connect flow, inlined so the page stays
|
|
3
|
+
* self-contained. Sourced via icon-kit from Iconify (sets: thesvg-color,
|
|
4
|
+
* logos); each carries its own colours and ignores currentColor.
|
|
5
|
+
*/
|
|
6
|
+
export declare const BRAND_LOGOS: Record<string, string>;
|