@aisa-one/cli 0.3.0 → 0.5.1

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 (103) hide show
  1. package/CHANGELOG.md +165 -1
  2. package/LICENSE +21 -0
  3. package/README.md +164 -170
  4. package/dist/api.d.ts +10 -0
  5. package/dist/api.js +14 -3
  6. package/dist/catalog.d.ts +1 -18
  7. package/dist/catalog.js +1 -24
  8. package/dist/cli-error.d.ts +17 -0
  9. package/dist/cli-error.js +25 -0
  10. package/dist/cli-guidance.d.ts +14 -0
  11. package/dist/cli-guidance.js +55 -0
  12. package/dist/commands/account.d.ts +17 -0
  13. package/dist/commands/account.js +45 -1
  14. package/dist/commands/api.d.ts +0 -11
  15. package/dist/commands/api.js +6 -175
  16. package/dist/commands/auth.d.ts +2 -1
  17. package/dist/commands/auth.js +45 -11
  18. package/dist/commands/brand-logos.d.ts +6 -0
  19. package/dist/commands/brand-logos.js +21 -0
  20. package/dist/commands/configCmd.js +8 -3
  21. package/dist/commands/connect-shared.d.ts +136 -0
  22. package/dist/commands/connect-shared.js +143 -0
  23. package/dist/commands/connect-t2.d.ts +18 -0
  24. package/dist/commands/connect-t2.js +1599 -0
  25. package/dist/commands/connect-terminal.d.ts +65 -0
  26. package/dist/commands/connect-terminal.js +822 -0
  27. package/dist/commands/connect.d.ts +31 -0
  28. package/dist/commands/connect.js +3181 -0
  29. package/dist/commands/flow.d.ts +685 -0
  30. package/dist/commands/flow.js +697 -0
  31. package/dist/commands/install.d.ts +91 -0
  32. package/dist/commands/install.js +258 -0
  33. package/dist/commands/llm-config.d.ts +103 -0
  34. package/dist/commands/llm-config.js +452 -0
  35. package/dist/commands/manifest.d.ts +68 -0
  36. package/dist/commands/manifest.js +90 -0
  37. package/dist/commands/mcp.d.ts +59 -0
  38. package/dist/commands/mcp.js +122 -44
  39. package/dist/commands/oauth-login.d.ts +33 -0
  40. package/dist/commands/oauth-login.js +333 -0
  41. package/dist/commands/prompt.d.ts +96 -0
  42. package/dist/commands/prompt.js +397 -0
  43. package/dist/commands/serve-results.d.ts +61 -0
  44. package/dist/commands/serve-results.js +245 -0
  45. package/dist/commands/serve-signin.d.ts +28 -0
  46. package/dist/commands/serve-signin.js +90 -0
  47. package/dist/commands/signin-page.d.ts +49 -0
  48. package/dist/commands/signin-page.js +250 -0
  49. package/dist/commands/skills.js +87 -50
  50. package/dist/commands/tool-help.d.ts +69 -0
  51. package/dist/commands/tool-help.js +189 -0
  52. package/dist/commands/tool-input.d.ts +22 -0
  53. package/dist/commands/tool-input.js +220 -0
  54. package/dist/commands/tools.d.ts +12 -0
  55. package/dist/commands/tools.js +306 -0
  56. package/dist/commands/update.d.ts +11 -0
  57. package/dist/commands/update.js +92 -0
  58. package/dist/commands/vscode.d.ts +65 -0
  59. package/dist/commands/vscode.js +173 -0
  60. package/dist/commands/wrappers.d.ts +23 -0
  61. package/dist/commands/wrappers.js +102 -0
  62. package/dist/completion.js +3 -80
  63. package/dist/config.d.ts +3 -0
  64. package/dist/config.js +80 -3
  65. package/dist/constants.d.ts +51 -38
  66. package/dist/constants.js +53 -109
  67. package/dist/index.js +152 -355
  68. package/dist/json-text.d.ts +12 -0
  69. package/dist/json-text.js +60 -0
  70. package/dist/router.d.ts +39 -0
  71. package/dist/router.js +59 -0
  72. package/dist/skills-registry.js +12 -3
  73. package/dist/utils/banner.d.ts +9 -0
  74. package/dist/utils/banner.js +41 -0
  75. package/dist/utils/browser.d.ts +19 -0
  76. package/dist/utils/browser.js +31 -0
  77. package/dist/utils/exec.d.ts +43 -0
  78. package/dist/utils/exec.js +61 -0
  79. package/dist/utils/http.d.ts +53 -0
  80. package/dist/utils/http.js +124 -0
  81. package/dist/utils/install-method.d.ts +36 -0
  82. package/dist/utils/install-method.js +48 -0
  83. package/dist/utils/journal.d.ts +57 -0
  84. package/dist/utils/journal.js +188 -0
  85. package/dist/utils/key-check.d.ts +20 -0
  86. package/dist/utils/key-check.js +24 -0
  87. package/dist/utils/update-check.d.ts +23 -0
  88. package/dist/utils/update-check.js +116 -0
  89. package/dist/utils/width.d.ts +32 -0
  90. package/dist/utils/width.js +73 -0
  91. package/package.json +8 -4
  92. package/vscode-extension/README.md +6 -0
  93. package/vscode-extension/aisa-vscode.vsix +0 -0
  94. package/dist/commands/finance.d.ts +0 -17
  95. package/dist/commands/finance.js +0 -342
  96. package/dist/commands/run.d.ts +0 -28
  97. package/dist/commands/run.js +0 -182
  98. package/dist/commands/search.d.ts +0 -9
  99. package/dist/commands/search.js +0 -81
  100. package/dist/commands/twitter.d.ts +0 -138
  101. package/dist/commands/twitter.js +0 -374
  102. package/dist/commands/video.d.ts +0 -49
  103. package/dist/commands/video.js +0 -311
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 fetch(url, {
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 fetch(url, {
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
- const res = await fetch(`${resolveBases().info}/${path.replace(/^\/+/, "")}`, { headers });
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: what you'd pass to `aisa run`. */
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: what you'd pass to `aisa run`. */
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;
@@ -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();
@@ -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>;
@@ -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 { resolveBases } from "../api.js";
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("Search: aisa api search <query>");
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(`\n ${chalk.gray(`aisa run ${slug} /${rest.join("/")}`)}`);
104
- hint(`Code: aisa api code ${slug} /${rest.join("/")}`);
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
- const slug = runSlugOf(detail);
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
- }
@@ -1,5 +1,6 @@
1
1
  export declare function loginAction(options: {
2
2
  key?: string;
3
- }): void;
3
+ browser?: boolean;
4
+ }): Promise<void>;
4
5
  export declare function logoutAction(): void;
5
6
  export declare function whoamiAction(): void;
@@ -1,16 +1,50 @@
1
1
  import chalk from "chalk";
2
- import { setApiKey, clearApiKey, getApiKey, getKeySource, maskKey } from "../config.js";
3
- import { success, error, info } from "../utils/display.js";
4
- import { ENV_VAR_NAME } from "../constants.js";
5
- export function loginAction(options) {
2
+ import { setApiKey, clearApiKey, getApiKey, getKeySource, maskKey, AUTH_SETUP_GUIDANCE } from "../config.js";
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 (!key) {
8
- error(`Provide a key with --key or set ${ENV_VAR_NAME}`);
9
- process.exit(1);
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
- setApiKey(key);
12
- success(`Authenticated: ${maskKey(key)}`);
13
- console.log(chalk.gray(" Get your API key at https://console.aisa.one/api-keys"));
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();
@@ -21,7 +55,7 @@ export function whoamiAction() {
21
55
  const source = getKeySource();
22
56
  if (!key) {
23
57
  info("Not authenticated.");
24
- console.log(chalk.gray(` Run "aisa login --key <key>" or set ${ENV_VAR_NAME}`));
58
+ console.log(chalk.gray(` ${AUTH_SETUP_GUIDANCE}`));
25
59
  return;
26
60
  }
27
61
  console.log(` Key: ${maskKey(key)}`);
@@ -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>;