@aisa-one/cli 0.2.2 → 0.2.4
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 +154 -1
- package/README.md +250 -103
- package/dist/api.d.ts +46 -1
- package/dist/api.js +77 -8
- package/dist/cache.d.ts +18 -0
- package/dist/cache.js +65 -0
- package/dist/catalog.d.ts +90 -0
- package/dist/catalog.js +101 -0
- package/dist/commands/account.js +4 -2
- package/dist/commands/api.d.ts +18 -4
- package/dist/commands/api.js +357 -24
- package/dist/commands/auth.js +1 -1
- package/dist/commands/cacheCmd.d.ts +2 -0
- package/dist/commands/cacheCmd.js +10 -0
- package/dist/commands/completionCmd.d.ts +7 -0
- package/dist/commands/completionCmd.js +37 -0
- package/dist/commands/configCmd.js +19 -1
- package/dist/commands/finance.d.ts +4 -0
- package/dist/commands/finance.js +212 -29
- package/dist/commands/models.js +6 -0
- package/dist/commands/run.d.ts +21 -0
- package/dist/commands/run.js +113 -6
- package/dist/commands/search.js +8 -3
- package/dist/commands/skills.d.ts +6 -2
- package/dist/commands/skills.js +199 -144
- package/dist/commands/twitter.js +2 -3
- package/dist/commands/video.d.ts +40 -0
- package/dist/commands/video.js +272 -55
- package/dist/completion.d.ts +26 -0
- package/dist/completion.js +293 -0
- package/dist/constants.d.ts +55 -5
- package/dist/constants.js +136 -11
- package/dist/index.js +88 -26
- package/dist/skills-registry.d.ts +64 -0
- package/dist/skills-registry.js +219 -0
- package/dist/types.d.ts +24 -0
- package/dist/utils/file.d.ts +5 -1
- package/dist/utils/file.js +5 -1
- package/dist/utils/pool.d.ts +8 -0
- package/dist/utils/pool.js +27 -0
- package/package.json +10 -4
- package/tests/account.test.ts +0 -89
- package/tests/streaming.test.ts +0 -73
package/dist/commands/finance.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import ora from "ora";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { requireApiKey } from "../config.js";
|
|
4
|
-
import { apiRequest } from "../api.js";
|
|
5
|
-
import { error, formatJson } from "../utils/display.js";
|
|
4
|
+
import { apiRequest, cacheScope } from "../api.js";
|
|
5
|
+
import { error, formatJson, hint } from "../utils/display.js";
|
|
6
|
+
import { COINGECKO_IDS } from "../constants.js";
|
|
7
|
+
import { readCache, writeCache } from "../cache.js";
|
|
8
|
+
const coinListCacheKey = () => `coingecko/${cacheScope()}/coins-list.json`;
|
|
9
|
+
const COIN_LIST_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
6
10
|
const FIELD_ENDPOINTS = {
|
|
7
11
|
info: "financial/company/facts",
|
|
8
12
|
estimates: "financial/analyst-estimates",
|
|
@@ -89,61 +93,235 @@ export async function stockAction(symbol, options) {
|
|
|
89
93
|
console.log(formatJson(res.data));
|
|
90
94
|
}
|
|
91
95
|
}
|
|
96
|
+
const PERIOD_DAYS = { "1d": 1, "7d": 7, "30d": 30, "90d": 90, "1y": 365 };
|
|
97
|
+
/**
|
|
98
|
+
* Resolve a user-typed symbol to a CoinGecko coin id: alias table first, then
|
|
99
|
+
* the full coin list (cached, ~18k entries). Ambiguous symbols are reported
|
|
100
|
+
* rather than guessed — picking one would silently price the wrong token.
|
|
101
|
+
*/
|
|
102
|
+
async function resolveCoinId(key, symbol) {
|
|
103
|
+
const lower = symbol.toLowerCase();
|
|
104
|
+
if (COINGECKO_IDS[lower])
|
|
105
|
+
return COINGECKO_IDS[lower];
|
|
106
|
+
const cached = readCache(coinListCacheKey());
|
|
107
|
+
let coins = cached?.fresh ? cached.data : undefined;
|
|
108
|
+
if (!coins) {
|
|
109
|
+
const res = await apiRequest(key, "coingecko/coins/list", { domain: true });
|
|
110
|
+
if (res.success && Array.isArray(res.data)) {
|
|
111
|
+
coins = res.data;
|
|
112
|
+
writeCache(coinListCacheKey(), coins, COIN_LIST_TTL_MS);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
coins = cached?.data;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!coins)
|
|
119
|
+
throw new Error(`Cannot resolve "${symbol}" — pass --id <coingecko-id>`);
|
|
120
|
+
// An exact id match is unambiguous, so accept it before considering symbols.
|
|
121
|
+
if (coins.some((c) => c.id === lower))
|
|
122
|
+
return lower;
|
|
123
|
+
const matches = coins.filter((c) => c.symbol.toLowerCase() === lower);
|
|
124
|
+
if (matches.length === 1)
|
|
125
|
+
return matches[0].id;
|
|
126
|
+
if (matches.length > 1) {
|
|
127
|
+
const shown = matches.slice(0, 8).map((c) => `${c.id} (${c.name})`);
|
|
128
|
+
throw new Error(`"${symbol}" matches ${matches.length} coins — pick one with --id:\n ${shown.join("\n ")}`);
|
|
129
|
+
}
|
|
130
|
+
throw new Error(`Unknown coin "${symbol}" — pass --id <coingecko-id>`);
|
|
131
|
+
}
|
|
132
|
+
function formatUsd(value) {
|
|
133
|
+
if (value >= 1)
|
|
134
|
+
return `$${value.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
135
|
+
return `$${value.toPrecision(4)}`;
|
|
136
|
+
}
|
|
137
|
+
function formatBig(value) {
|
|
138
|
+
if (value >= 1e12)
|
|
139
|
+
return `$${(value / 1e12).toFixed(2)}T`;
|
|
140
|
+
if (value >= 1e9)
|
|
141
|
+
return `$${(value / 1e9).toFixed(2)}B`;
|
|
142
|
+
if (value >= 1e6)
|
|
143
|
+
return `$${(value / 1e6).toFixed(2)}M`;
|
|
144
|
+
return `$${value.toFixed(0)}`;
|
|
145
|
+
}
|
|
92
146
|
export async function cryptoAction(symbol, options) {
|
|
93
147
|
const key = requireApiKey();
|
|
148
|
+
// The financial provider's crypto endpoints return 403 for standard keys;
|
|
149
|
+
// coingecko covers the same ground and is cheaper, so it is the default.
|
|
150
|
+
if (options.source === "financial") {
|
|
151
|
+
return cryptoViaFinancial(key, symbol, options);
|
|
152
|
+
}
|
|
153
|
+
const spinner = ora(`Fetching ${symbol.toUpperCase()}...`).start();
|
|
154
|
+
let coinId;
|
|
155
|
+
try {
|
|
156
|
+
coinId = options.id || (await resolveCoinId(key, symbol));
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
spinner.stop();
|
|
160
|
+
error(err.message);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const historical = options.period && options.period !== "current";
|
|
164
|
+
const endpoint = historical
|
|
165
|
+
? `coingecko/coins/${encodeURIComponent(coinId)}/market_chart`
|
|
166
|
+
: "coingecko/simple/price";
|
|
167
|
+
const query = historical
|
|
168
|
+
? { vs_currency: "usd", days: String(PERIOD_DAYS[options.period] || 30) }
|
|
169
|
+
: {
|
|
170
|
+
ids: coinId,
|
|
171
|
+
vs_currencies: "usd",
|
|
172
|
+
include_market_cap: "true",
|
|
173
|
+
include_24hr_vol: "true",
|
|
174
|
+
include_24hr_change: "true",
|
|
175
|
+
include_last_updated_at: "true",
|
|
176
|
+
};
|
|
177
|
+
const res = await apiRequest(key, endpoint, { query, domain: true });
|
|
178
|
+
if (!res.success) {
|
|
179
|
+
spinner.fail("Failed to fetch crypto price");
|
|
180
|
+
error(res.error || "Unknown error");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
spinner.stop();
|
|
184
|
+
if (options.raw) {
|
|
185
|
+
console.log(JSON.stringify(res.data));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (historical) {
|
|
189
|
+
const prices = res.data.prices || [];
|
|
190
|
+
if (prices.length === 0) {
|
|
191
|
+
console.log(formatJson(res.data));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const first = prices[0][1];
|
|
195
|
+
const last = prices[prices.length - 1][1];
|
|
196
|
+
const change = ((last - first) / first) * 100;
|
|
197
|
+
console.log(`\n ${chalk.cyan.bold(coinId)} ${chalk.gray(`· ${options.period}`)}`);
|
|
198
|
+
console.log(` Now: ${formatUsd(last)}`);
|
|
199
|
+
console.log(` Start: ${formatUsd(first)}`);
|
|
200
|
+
console.log(` Change: ${change >= 0 ? chalk.green(`+${change.toFixed(2)}%`) : chalk.red(`${change.toFixed(2)}%`)}`);
|
|
201
|
+
console.log(chalk.gray(` ${prices.length} data points`));
|
|
202
|
+
console.log();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const snapshot = res.data[coinId];
|
|
206
|
+
if (!snapshot) {
|
|
207
|
+
console.log(formatJson(res.data));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const change = snapshot.usd_24h_change;
|
|
211
|
+
console.log(`\n ${chalk.cyan.bold(coinId)} ${chalk.gray(`(${symbol.toUpperCase()})`)}`);
|
|
212
|
+
console.log(` Price: ${formatUsd(snapshot.usd)}`);
|
|
213
|
+
if (change != null) {
|
|
214
|
+
console.log(` 24h: ${change >= 0 ? chalk.green(`+${change.toFixed(2)}%`) : chalk.red(`${change.toFixed(2)}%`)}`);
|
|
215
|
+
}
|
|
216
|
+
if (snapshot.usd_market_cap != null)
|
|
217
|
+
console.log(` Mkt cap: ${formatBig(snapshot.usd_market_cap)}`);
|
|
218
|
+
if (snapshot.usd_24h_vol != null)
|
|
219
|
+
console.log(` 24h vol: ${formatBig(snapshot.usd_24h_vol)}`);
|
|
220
|
+
console.log();
|
|
221
|
+
}
|
|
222
|
+
/** Legacy path, kept behind --source for accounts that do have crypto access. */
|
|
223
|
+
async function cryptoViaFinancial(key, symbol, options) {
|
|
94
224
|
const spinner = ora(`Fetching ${symbol.toUpperCase()}...`).start();
|
|
95
|
-
// Format ticker: BTC → BTC-USD
|
|
96
225
|
const ticker = symbol.toUpperCase().includes("-") ? symbol.toUpperCase() : `${symbol.toUpperCase()}-USD`;
|
|
97
|
-
let endpoint;
|
|
98
226
|
const query = { ticker };
|
|
227
|
+
let endpoint;
|
|
99
228
|
if (!options.period || options.period === "current") {
|
|
100
229
|
endpoint = "financial/crypto/prices/snapshot";
|
|
101
230
|
}
|
|
102
231
|
else {
|
|
103
232
|
endpoint = "financial/crypto/prices";
|
|
104
|
-
const end = new Date();
|
|
105
233
|
const start = new Date();
|
|
106
|
-
|
|
107
|
-
start.setDate(start.getDate() - (periodMap[options.period] || 30));
|
|
234
|
+
start.setDate(start.getDate() - (PERIOD_DAYS[options.period] || 30));
|
|
108
235
|
query.interval = "day";
|
|
109
236
|
query.interval_multiplier = "1";
|
|
110
237
|
query.start_date = start.toISOString().split("T")[0];
|
|
111
|
-
query.end_date =
|
|
238
|
+
query.end_date = new Date().toISOString().split("T")[0];
|
|
112
239
|
}
|
|
113
240
|
const res = await apiRequest(key, endpoint, { query, domain: true });
|
|
114
241
|
if (!res.success) {
|
|
115
242
|
spinner.fail("Failed to fetch crypto price");
|
|
116
243
|
error(res.error || "Unknown error");
|
|
244
|
+
if ((res.error || "").includes("403")) {
|
|
245
|
+
hint("Crypto access is restricted on this provider — omit --source to use coingecko");
|
|
246
|
+
}
|
|
117
247
|
return;
|
|
118
248
|
}
|
|
119
249
|
spinner.stop();
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
250
|
+
console.log(options.raw ? JSON.stringify(res.data) : formatJson(res.data));
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The screener matches sectors against GICS names, so the short forms people
|
|
254
|
+
* naturally type ("Technology", "Healthcare") silently return zero results.
|
|
255
|
+
*/
|
|
256
|
+
const GICS_SECTORS = [
|
|
257
|
+
"Energy",
|
|
258
|
+
"Materials",
|
|
259
|
+
"Industrials",
|
|
260
|
+
"Consumer Discretionary",
|
|
261
|
+
"Consumer Staples",
|
|
262
|
+
"Health Care",
|
|
263
|
+
"Financials",
|
|
264
|
+
"Information Technology",
|
|
265
|
+
"Communication Services",
|
|
266
|
+
"Utilities",
|
|
267
|
+
"Real Estate",
|
|
268
|
+
];
|
|
269
|
+
const SECTOR_ALIASES = {
|
|
270
|
+
tech: "Information Technology",
|
|
271
|
+
technology: "Information Technology",
|
|
272
|
+
it: "Information Technology",
|
|
273
|
+
healthcare: "Health Care",
|
|
274
|
+
health: "Health Care",
|
|
275
|
+
finance: "Financials",
|
|
276
|
+
financial: "Financials",
|
|
277
|
+
telecom: "Communication Services",
|
|
278
|
+
communications: "Communication Services",
|
|
279
|
+
consumer: "Consumer Discretionary",
|
|
280
|
+
staples: "Consumer Staples",
|
|
281
|
+
discretionary: "Consumer Discretionary",
|
|
282
|
+
realestate: "Real Estate",
|
|
283
|
+
utility: "Utilities",
|
|
284
|
+
};
|
|
285
|
+
function normalizeSector(input) {
|
|
286
|
+
const exact = GICS_SECTORS.find((s) => s.toLowerCase() === input.toLowerCase());
|
|
287
|
+
if (exact)
|
|
288
|
+
return exact;
|
|
289
|
+
return SECTOR_ALIASES[input.toLowerCase().replace(/[\s_-]/g, "")] || input;
|
|
290
|
+
}
|
|
291
|
+
/** Parse a `field:operator:value` filter, e.g. `market_cap:gt:1e12`. */
|
|
292
|
+
function parseFilter(raw) {
|
|
293
|
+
const parts = raw.split(":");
|
|
294
|
+
if (parts.length < 3) {
|
|
295
|
+
throw new Error(`Invalid filter "${raw}". Use field:operator:value, e.g. market_cap:gt:1000000000`);
|
|
125
296
|
}
|
|
297
|
+
const [field, operator, ...rest] = parts;
|
|
298
|
+
const value = rest.join(":");
|
|
299
|
+
const num = Number(value);
|
|
300
|
+
return { field, operator, value: value !== "" && !Number.isNaN(num) ? num : value };
|
|
126
301
|
}
|
|
127
302
|
export async function screenerAction(options) {
|
|
128
303
|
const key = requireApiKey();
|
|
129
|
-
const
|
|
130
|
-
const body = {
|
|
131
|
-
filters: [],
|
|
132
|
-
};
|
|
304
|
+
const filters = [];
|
|
133
305
|
if (options.sector) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
306
|
+
filters.push({ field: "sector", operator: "eq", value: normalizeSector(options.sector) });
|
|
307
|
+
}
|
|
308
|
+
if (options.minMarketCap) {
|
|
309
|
+
filters.push({ field: "market_cap", operator: "gt", value: Number(options.minMarketCap) });
|
|
310
|
+
}
|
|
311
|
+
for (const raw of options.filter || []) {
|
|
312
|
+
filters.push(parseFilter(raw));
|
|
313
|
+
}
|
|
314
|
+
// The upstream rejects an empty filter set outright, so give unfiltered runs
|
|
315
|
+
// a broad default rather than surfacing "Please include filters".
|
|
316
|
+
if (filters.length === 0) {
|
|
317
|
+
filters.push({ field: "market_cap", operator: "gt", value: 1_000_000_000 });
|
|
318
|
+
hint("No filters given — defaulting to market cap > $1B");
|
|
139
319
|
}
|
|
320
|
+
const spinner = ora("Running stock screener...").start();
|
|
321
|
+
const body = { filters };
|
|
140
322
|
if (options.limit)
|
|
141
323
|
body.limit = parseInt(options.limit);
|
|
142
|
-
const res = await apiRequest(key, "financial/financials/search", {
|
|
143
|
-
method: "POST",
|
|
144
|
-
body,
|
|
145
|
-
domain: true,
|
|
146
|
-
});
|
|
324
|
+
const res = await apiRequest(key, "financial/financials/search/screener", { method: "POST", body, domain: true });
|
|
147
325
|
if (!res.success) {
|
|
148
326
|
spinner.fail("Screener failed");
|
|
149
327
|
error(res.error || "Unknown error");
|
|
@@ -152,8 +330,13 @@ export async function screenerAction(options) {
|
|
|
152
330
|
spinner.stop();
|
|
153
331
|
if (options.raw) {
|
|
154
332
|
console.log(JSON.stringify(res.data));
|
|
333
|
+
return;
|
|
155
334
|
}
|
|
156
|
-
|
|
157
|
-
|
|
335
|
+
const results = res.data?.results;
|
|
336
|
+
if (Array.isArray(results) && results.length === 0 && options.sector) {
|
|
337
|
+
console.log(" No matches.");
|
|
338
|
+
hint(`Sector must be a GICS name: ${GICS_SECTORS.join(", ")}`);
|
|
339
|
+
return;
|
|
158
340
|
}
|
|
341
|
+
console.log(formatJson(res.data));
|
|
159
342
|
}
|
package/dist/commands/models.js
CHANGED
|
@@ -3,6 +3,10 @@ import chalk from "chalk";
|
|
|
3
3
|
import { requireApiKey } from "../config.js";
|
|
4
4
|
import { apiRequest } from "../api.js";
|
|
5
5
|
import { error, badge } from "../utils/display.js";
|
|
6
|
+
import { writeCache } from "../cache.js";
|
|
7
|
+
import { cacheScope } from "../api.js";
|
|
8
|
+
const modelsCacheKey = () => `models/${cacheScope()}.json`;
|
|
9
|
+
const MODELS_TTL_MS = 24 * 60 * 60 * 1000;
|
|
6
10
|
export async function modelsListAction(options) {
|
|
7
11
|
const key = requireApiKey();
|
|
8
12
|
const spinner = ora("Fetching models...").start();
|
|
@@ -14,6 +18,8 @@ export async function modelsListAction(options) {
|
|
|
14
18
|
}
|
|
15
19
|
spinner.stop();
|
|
16
20
|
let models = res.data.data;
|
|
21
|
+
// Cached purely so shell completion can offer model ids without a request.
|
|
22
|
+
writeCache(modelsCacheKey(), models, MODELS_TTL_MS);
|
|
17
23
|
// Filter by provider if specified
|
|
18
24
|
if (options.provider) {
|
|
19
25
|
const filter = options.provider.toLowerCase();
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
|
+
export interface RunTarget {
|
|
2
|
+
/** Which base URL to use: the LLM gateway (/v1) or integration APIs (/apis/v1). */
|
|
3
|
+
base: "llm" | "domain";
|
|
4
|
+
/** Path relative to that base, with no leading slash. */
|
|
5
|
+
endpoint: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Decide which base URL a `run` invocation targets.
|
|
9
|
+
*
|
|
10
|
+
* Integration APIs are the default: there are ~29 providers and the set grows
|
|
11
|
+
* without a CLI release, so anything not recognised as an LLM gateway route
|
|
12
|
+
* goes to /apis/v1. See LLM_ROUTE_PREFIXES for why the whitelist sits on that
|
|
13
|
+
* side.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveRunTarget(slug: string, path: string, options?: {
|
|
16
|
+
llm?: boolean;
|
|
17
|
+
domain?: boolean;
|
|
18
|
+
}): RunTarget;
|
|
1
19
|
export declare function runAction(slug: string, path: string, options: {
|
|
2
20
|
q?: string[];
|
|
3
21
|
d?: string;
|
|
4
22
|
method?: string;
|
|
5
23
|
raw?: boolean;
|
|
6
24
|
stream?: boolean;
|
|
25
|
+
llm?: boolean;
|
|
26
|
+
domain?: boolean;
|
|
27
|
+
showCost?: boolean;
|
|
7
28
|
}): Promise<void>;
|
package/dist/commands/run.js
CHANGED
|
@@ -1,8 +1,106 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
1
2
|
import ora from "ora";
|
|
2
3
|
import { requireApiKey } from "../config.js";
|
|
3
|
-
import { apiRequest, apiRequestRaw } from "../api.js";
|
|
4
|
-
import { error, formatJson } from "../utils/display.js";
|
|
4
|
+
import { apiRequest, apiRequestRaw, parseCost } from "../api.js";
|
|
5
|
+
import { error, formatJson, hint } from "../utils/display.js";
|
|
5
6
|
import { handleSSEStream } from "../utils/streaming.js";
|
|
7
|
+
import { LLM_ROUTE_PREFIXES } from "../constants.js";
|
|
8
|
+
import { getProviders } from "../catalog.js";
|
|
9
|
+
/**
|
|
10
|
+
* Decide which base URL a `run` invocation targets.
|
|
11
|
+
*
|
|
12
|
+
* Integration APIs are the default: there are ~29 providers and the set grows
|
|
13
|
+
* without a CLI release, so anything not recognised as an LLM gateway route
|
|
14
|
+
* goes to /apis/v1. See LLM_ROUTE_PREFIXES for why the whitelist sits on that
|
|
15
|
+
* side.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveRunTarget(slug, path, options = {}) {
|
|
18
|
+
const cleanSlug = slug.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
19
|
+
const cleanPath = path.replace(/^\/+/, "");
|
|
20
|
+
const endpoint = cleanPath ? `${cleanSlug}/${cleanPath}` : cleanSlug;
|
|
21
|
+
let base;
|
|
22
|
+
if (options.llm) {
|
|
23
|
+
base = "llm";
|
|
24
|
+
}
|
|
25
|
+
else if (options.domain) {
|
|
26
|
+
base = "domain";
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
base = LLM_ROUTE_PREFIXES.includes(cleanSlug.split("/")[0]) ? "llm" : "domain";
|
|
30
|
+
}
|
|
31
|
+
return { base, endpoint };
|
|
32
|
+
}
|
|
33
|
+
function editDistance(a, b) {
|
|
34
|
+
const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
35
|
+
for (let i = 1; i <= a.length; i++) {
|
|
36
|
+
let diag = prev[0];
|
|
37
|
+
prev[0] = i;
|
|
38
|
+
for (let j = 1; j <= b.length; j++) {
|
|
39
|
+
const temp = prev[j];
|
|
40
|
+
prev[j] = Math.min(prev[j] + 1, prev[j - 1] + 1, diag + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
41
|
+
diag = temp;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return prev[b.length];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Turn a 404 into a usable next step. This runs only on failure, so the catalog
|
|
48
|
+
* stays a diagnostic aid rather than something `run` depends on to work.
|
|
49
|
+
*/
|
|
50
|
+
async function suggestSlug(slug) {
|
|
51
|
+
try {
|
|
52
|
+
const providers = await getProviders();
|
|
53
|
+
const ids = providers.map((p) => p.id);
|
|
54
|
+
const near = ids
|
|
55
|
+
.map((id) => ({ id, d: editDistance(slug.toLowerCase(), id.toLowerCase()) }))
|
|
56
|
+
.filter((c) => c.d <= 3 || c.id.includes(slug) || slug.includes(c.id))
|
|
57
|
+
.sort((a, b) => a.d - b.d)
|
|
58
|
+
.slice(0, 3)
|
|
59
|
+
.map((c) => c.id);
|
|
60
|
+
if (near.length > 0) {
|
|
61
|
+
hint(`Did you mean: ${near.join(", ")}?`);
|
|
62
|
+
}
|
|
63
|
+
hint("List APIs: aisa api list");
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// catalog unavailable — the error above is enough
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Report what a call cost, on stderr so `--raw | jq` stays a clean pipe.
|
|
71
|
+
*
|
|
72
|
+
* Only metered routes report an amount; plain integration responses carry a
|
|
73
|
+
* price key and the LLM gateway sends no billing headers at all. Say so rather
|
|
74
|
+
* than printing nothing, so an empty report is not read as "this was free".
|
|
75
|
+
*/
|
|
76
|
+
function reportCost(cost) {
|
|
77
|
+
const dim = (label, value) => console.error(chalk.gray(` ${label.padEnd(12)} ${value}`));
|
|
78
|
+
if (!cost) {
|
|
79
|
+
console.error(chalk.gray(" cost not reported by this route"));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (cost.priceUsd)
|
|
83
|
+
dim("cost", `$${cost.priceUsd} USD`);
|
|
84
|
+
if (cost.accountedCredits) {
|
|
85
|
+
const estimate = cost.estimatedCredits && cost.estimatedCredits !== cost.accountedCredits
|
|
86
|
+
? ` (estimated ${cost.estimatedCredits})`
|
|
87
|
+
: "";
|
|
88
|
+
dim("credits", `${cost.accountedCredits}${estimate}`);
|
|
89
|
+
}
|
|
90
|
+
if (cost.creditModel)
|
|
91
|
+
dim("credit model", cost.creditModel);
|
|
92
|
+
if (cost.pricingStrategy) {
|
|
93
|
+
const version = cost.pricingVersion ? ` v${cost.pricingVersion}` : "";
|
|
94
|
+
dim("pricing", `${cost.pricingStrategy}${version}`);
|
|
95
|
+
}
|
|
96
|
+
if (cost.priceKey)
|
|
97
|
+
dim("price key", cost.priceKey);
|
|
98
|
+
if (cost.requestId)
|
|
99
|
+
dim("request id", cost.requestId);
|
|
100
|
+
if (!cost.priceUsd) {
|
|
101
|
+
console.error(chalk.gray(" no amount reported — check `aisa account balance` for actual spend"));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
6
104
|
export async function runAction(slug, path, options) {
|
|
7
105
|
const key = requireApiKey();
|
|
8
106
|
// Parse query params
|
|
@@ -31,10 +129,8 @@ export async function runAction(slug, path, options) {
|
|
|
31
129
|
}
|
|
32
130
|
// Detect method
|
|
33
131
|
const method = (options.method || (body ? "POST" : "GET")).toUpperCase();
|
|
34
|
-
const endpoint =
|
|
35
|
-
|
|
36
|
-
const domainSlugs = ["financial", "search", "youtube", "scholar", "tavily", "querit", "twitter", "services", "crypto"];
|
|
37
|
-
const isDomain = domainSlugs.some((s) => slug.startsWith(s));
|
|
132
|
+
const { base, endpoint } = resolveRunTarget(slug, path, options);
|
|
133
|
+
const isDomain = base === "domain";
|
|
38
134
|
// Streaming mode
|
|
39
135
|
if (options.stream) {
|
|
40
136
|
const res = await apiRequestRaw(key, endpoint, {
|
|
@@ -47,9 +143,13 @@ export async function runAction(slug, path, options) {
|
|
|
47
143
|
if (!res.ok) {
|
|
48
144
|
const text = await res.text();
|
|
49
145
|
error(`${res.status}: ${text}`);
|
|
146
|
+
if (options.showCost)
|
|
147
|
+
reportCost(parseCost(res));
|
|
50
148
|
return;
|
|
51
149
|
}
|
|
52
150
|
await handleSSEStream(res, (token) => process.stdout.write(token), () => console.log());
|
|
151
|
+
if (options.showCost)
|
|
152
|
+
reportCost(parseCost(res));
|
|
53
153
|
return;
|
|
54
154
|
}
|
|
55
155
|
// Normal mode
|
|
@@ -63,6 +163,11 @@ export async function runAction(slug, path, options) {
|
|
|
63
163
|
if (!res.success) {
|
|
64
164
|
spinner.fail("API call failed");
|
|
65
165
|
error(res.error || "Unknown error");
|
|
166
|
+
if (isDomain && (res.error || "").includes("api endpoint not found")) {
|
|
167
|
+
await suggestSlug(slug);
|
|
168
|
+
}
|
|
169
|
+
if (options.showCost)
|
|
170
|
+
reportCost(res.cost);
|
|
66
171
|
return;
|
|
67
172
|
}
|
|
68
173
|
spinner.stop();
|
|
@@ -72,4 +177,6 @@ export async function runAction(slug, path, options) {
|
|
|
72
177
|
else {
|
|
73
178
|
console.log(formatJson(res.data));
|
|
74
179
|
}
|
|
180
|
+
if (options.showCost)
|
|
181
|
+
reportCost(res.cost);
|
|
75
182
|
}
|
package/dist/commands/search.js
CHANGED
|
@@ -2,10 +2,10 @@ import ora from "ora";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { requireApiKey } from "../config.js";
|
|
4
4
|
import { apiRequest } from "../api.js";
|
|
5
|
-
import { error, formatJson } from "../utils/display.js";
|
|
5
|
+
import { error, formatJson, hint } from "../utils/display.js";
|
|
6
6
|
export async function webSearchAction(query, options) {
|
|
7
7
|
const key = requireApiKey();
|
|
8
|
-
const type = options.type || "
|
|
8
|
+
const type = options.type || "tavily";
|
|
9
9
|
const spinner = ora(`Searching (${type})...`).start();
|
|
10
10
|
const endpointMap = {
|
|
11
11
|
smart: { endpoint: "search/smart", method: "GET", paramStyle: "query" },
|
|
@@ -17,7 +17,7 @@ export async function webSearchAction(query, options) {
|
|
|
17
17
|
const config = endpointMap[type];
|
|
18
18
|
if (!config) {
|
|
19
19
|
spinner.fail(`Unknown search type: ${type}`);
|
|
20
|
-
error("Valid types:
|
|
20
|
+
error("Valid types: tavily, youtube, scholar, smart, full");
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
23
|
// Build request options
|
|
@@ -44,6 +44,11 @@ export async function webSearchAction(query, options) {
|
|
|
44
44
|
if (!res.success) {
|
|
45
45
|
spinner.fail("Search failed");
|
|
46
46
|
error(res.error || "Unknown error");
|
|
47
|
+
// smart/full currently proxy to an upstream that 404s for every parameter
|
|
48
|
+
// shape; steer users to a working backend rather than to parameter tweaks.
|
|
49
|
+
if (type === "smart" || type === "full") {
|
|
50
|
+
hint(`The '${type}' backend is currently degraded — try --type tavily`);
|
|
51
|
+
}
|
|
47
52
|
return;
|
|
48
53
|
}
|
|
49
54
|
spinner.stop();
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
export declare function skillsListAction(
|
|
1
|
+
export declare function skillsListAction(options: {
|
|
2
2
|
category?: string;
|
|
3
3
|
limit?: string;
|
|
4
|
+
refresh?: boolean;
|
|
4
5
|
}): Promise<void>;
|
|
5
|
-
export declare function skillsSearchAction(query: string,
|
|
6
|
+
export declare function skillsSearchAction(query: string, options: {
|
|
6
7
|
limit?: string;
|
|
8
|
+
refresh?: boolean;
|
|
7
9
|
}): Promise<void>;
|
|
8
10
|
export declare function skillsShowAction(slug: string): Promise<void>;
|
|
9
11
|
export declare function skillsInstallAction(slug: string, options: {
|
|
10
12
|
agent?: string;
|
|
13
|
+
force?: boolean;
|
|
11
14
|
}): Promise<void>;
|
|
12
15
|
export declare function skillsRemoveAction(slug: string, options: {
|
|
13
16
|
agent?: string;
|
|
17
|
+
force?: boolean;
|
|
14
18
|
}): void;
|
|
15
19
|
export declare function skillsInitAction(name: string, options: {
|
|
16
20
|
template?: string;
|