@aisa-one/cli 0.2.1 → 0.2.3

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 (52) hide show
  1. package/CHANGELOG.md +144 -1
  2. package/README.md +249 -102
  3. package/dist/api.d.ts +46 -1
  4. package/dist/api.js +77 -8
  5. package/dist/cache.d.ts +18 -0
  6. package/dist/cache.js +65 -0
  7. package/dist/catalog.d.ts +90 -0
  8. package/dist/catalog.js +101 -0
  9. package/dist/commands/account.d.ts +4 -1
  10. package/dist/commands/account.js +36 -6
  11. package/dist/commands/api.d.ts +18 -4
  12. package/dist/commands/api.js +357 -24
  13. package/dist/commands/cacheCmd.d.ts +2 -0
  14. package/dist/commands/cacheCmd.js +10 -0
  15. package/dist/commands/completionCmd.d.ts +7 -0
  16. package/dist/commands/completionCmd.js +37 -0
  17. package/dist/commands/configCmd.js +19 -1
  18. package/dist/commands/finance.d.ts +4 -0
  19. package/dist/commands/finance.js +212 -29
  20. package/dist/commands/models.js +6 -0
  21. package/dist/commands/run.d.ts +21 -0
  22. package/dist/commands/run.js +113 -6
  23. package/dist/commands/search.js +8 -3
  24. package/dist/commands/skills.d.ts +6 -2
  25. package/dist/commands/skills.js +189 -134
  26. package/dist/commands/twitter.js +2 -3
  27. package/dist/commands/video.d.ts +40 -0
  28. package/dist/commands/video.js +272 -55
  29. package/dist/completion.d.ts +26 -0
  30. package/dist/completion.js +293 -0
  31. package/dist/constants.d.ts +55 -5
  32. package/dist/constants.js +136 -11
  33. package/dist/index.js +86 -23
  34. package/dist/skills-registry.d.ts +64 -0
  35. package/dist/skills-registry.js +219 -0
  36. package/dist/types.d.ts +32 -1
  37. package/dist/utils/file.d.ts +5 -1
  38. package/dist/utils/file.js +5 -1
  39. package/dist/utils/pool.d.ts +8 -0
  40. package/dist/utils/pool.js +27 -0
  41. package/package.json +9 -3
  42. package/dist/agent/env-setup.d.ts +0 -5
  43. package/dist/agent/env-setup.js +0 -49
  44. package/dist/agent/index-client.d.ts +0 -7
  45. package/dist/agent/index-client.js +0 -22
  46. package/dist/agent/installer.d.ts +0 -19
  47. package/dist/agent/installer.js +0 -70
  48. package/dist/agent/setup-python.d.ts +0 -15
  49. package/dist/agent/setup-python.js +0 -51
  50. package/dist/commands/agent.d.ts +0 -37
  51. package/dist/commands/agent.js +0 -129
  52. package/tests/streaming.test.ts +0 -73
@@ -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
- const periodMap = { "1d": 1, "7d": 7, "30d": 30, "1y": 365 };
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 = end.toISOString().split("T")[0];
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
- if (options.raw) {
121
- console.log(JSON.stringify(res.data));
122
- }
123
- else {
124
- console.log(formatJson(res.data));
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 spinner = ora("Running stock screener...").start();
130
- const body = {
131
- filters: [],
132
- };
304
+ const filters = [];
133
305
  if (options.sector) {
134
- body.filters.push({
135
- field: "sector",
136
- operator: "eq",
137
- value: options.sector,
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
- else {
157
- console.log(formatJson(res.data));
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
  }
@@ -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();
@@ -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>;
@@ -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 = path.startsWith("/") ? `${slug}${path}` : `${slug}/${path}`;
35
- // Auto-detect domain API vs LLM API based on slug
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
  }
@@ -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 || "smart";
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: smart, full, youtube, scholar, tavily");
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(_options: {
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, _options: {
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;