@tensorfeed/mcp-server 1.39.0 → 2.0.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 +61 -0
- package/README.md +7 -7
- package/dist/discovery.d.ts +9 -0
- package/dist/discovery.js +38 -0
- package/dist/index.js +553 -1845
- package/dist/meta-snapshot.json +1 -0
- package/package.json +7 -5
- package/server.json +29 -58
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ const SDK_VERSION = JSON.parse(readFileSync(pkgPath, 'utf-8')).version;
|
|
|
18
18
|
// get_model_pricing). Each is a signal an agent gathers before choosing a
|
|
19
19
|
// model, so each points at the one premium tool that fuses them into a signed
|
|
20
20
|
// decision. Mirrors the hosted /api/mcp `next` field. Honest, zero em dashes.
|
|
21
|
-
const ROUTE_VERDICT_FOOTER = '\n\nNext: route_verdict (1 credit, $0.02) fuses live pricing, contamination-discounted ' +
|
|
21
|
+
const ROUTE_VERDICT_FOOTER = '\n\nNext: route_verdict with tier="full" (1 credit, $0.02) fuses live pricing, contamination-discounted ' +
|
|
22
22
|
'benchmarks, real production usage, measured p95 latency, incident state, and deprecation ' +
|
|
23
23
|
'flags into a signed best-fit model decision with ranked runners-up and an AFTA-signed ' +
|
|
24
24
|
'receipt. No USDC yet? Sign a wallet message at tensorfeed.ai/api/payment/trial-credits ' +
|
|
@@ -190,7 +190,23 @@ server.registerResource('latest_news', 'tensorfeed://news/latest', {
|
|
|
190
190
|
registerTool('get_ai_news', 'Get the latest AI news from TensorFeed.ai as a ranked list with title, source, URL, snippet, and publish time, filterable by category (e.g. "anthropic", "openai", "research", "tools"). Aggregates 15+ sources (Anthropic, OpenAI, Google, TechCrunch, The Verge, arXiv, and more) into one normalized feed, so an agent reads one schema instead of polling each outlet. Free, no auth.', {
|
|
191
191
|
category: z.string().optional().describe('Filter by category (e.g. "anthropic", "openai", "research", "tools")'),
|
|
192
192
|
limit: z.number().min(1).max(50).optional().describe('Number of articles to return (default 10, max 50)'),
|
|
193
|
-
|
|
193
|
+
digest: z.boolean().optional().describe('When true, return a headline-only digest (title, source, URL per story) instead of the full article set.'),
|
|
194
|
+
}, async ({ category, limit, digest }) => {
|
|
195
|
+
// digest mode is a headline-only companion view of the same feed. It honors
|
|
196
|
+
// the same inputs as the full view: the category filter and the schema limit
|
|
197
|
+
// (default 5, max 50). Earlier this branch dropped category and capped at 20,
|
|
198
|
+
// silently ignoring declared params.
|
|
199
|
+
if (digest) {
|
|
200
|
+
const dParams = new URLSearchParams();
|
|
201
|
+
if (category)
|
|
202
|
+
dParams.set('category', category);
|
|
203
|
+
dParams.set('limit', String(limit || 5));
|
|
204
|
+
const data = await fetchJSON(`/news?${dParams}`);
|
|
205
|
+
const text = data.articles
|
|
206
|
+
.map((a, i) => `${i + 1}. ${a.title} (${a.source})\n ${a.url}`)
|
|
207
|
+
.join('\n\n');
|
|
208
|
+
return { content: [{ type: 'text', text: `Today in AI:\n\n${text}` }] };
|
|
209
|
+
}
|
|
194
210
|
const params = new URLSearchParams();
|
|
195
211
|
if (category)
|
|
196
212
|
params.set('category', category);
|
|
@@ -201,6 +217,29 @@ registerTool('get_ai_news', 'Get the latest AI news from TensorFeed.ai as a rank
|
|
|
201
217
|
.join('\n\n');
|
|
202
218
|
return { content: [{ type: 'text', text: text || 'No articles found.' }] };
|
|
203
219
|
});
|
|
220
|
+
// ── Tool: find_tensorfeed_data (discovery, long-tail surface) ───────
|
|
221
|
+
registerTool('find_tensorfeed_data', 'Discover which TensorFeed endpoint answers a data need. Describe what you want in plain language (e.g. "trending AI papers", "is OpenAI down", "model price history") and this returns the 2 to 3 best-matching TensorFeed endpoints, each with its HTTP path, what it returns, and whether it is free or paid. TensorFeed exposes 100+ AI-ecosystem data and signed-verdict endpoints; the core ones are also dedicated tools, but the full catalog is reachable here and callable over HTTP (paid ones via x402 or a credits token). Free, no auth. Use this first when no dedicated tool obviously fits.', {
|
|
222
|
+
query: z.string().describe('Plain-language description of the data or decision you need.'),
|
|
223
|
+
limit: z.number().int().min(1).max(5).optional().describe('Max endpoints to return (default 3).'),
|
|
224
|
+
}, async ({ query, limit }) => {
|
|
225
|
+
const { flattenCatalog, scoreEndpoints } = await import('./discovery.js');
|
|
226
|
+
let meta;
|
|
227
|
+
try {
|
|
228
|
+
meta = await fetchJSON('/meta');
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
const { readFileSync } = await import('node:fs');
|
|
232
|
+
const p = join(dirname(fileURLToPath(import.meta.url)), 'meta-snapshot.json');
|
|
233
|
+
meta = JSON.parse(readFileSync(p, 'utf-8'));
|
|
234
|
+
}
|
|
235
|
+
const rows = flattenCatalog(meta);
|
|
236
|
+
const top = scoreEndpoints(rows, query, limit ?? 3);
|
|
237
|
+
if (top.length === 0) {
|
|
238
|
+
return { content: [{ type: 'text', text: `No TensorFeed endpoint matched "${query}". Browse the full catalog at https://tensorfeed.ai/api/meta or https://tensorfeed.ai/developers.` }] };
|
|
239
|
+
}
|
|
240
|
+
const lines = top.map((r) => `- ${r.path}\n ${r.description}`).join('\n');
|
|
241
|
+
return { content: [{ type: 'text', text: `Top TensorFeed endpoints for "${query}":\n${lines}\n\nCall free endpoints directly over HTTP. Paid endpoints (marked with a credit cost) accept x402 payment or a TENSORFEED_TOKEN bearer; see https://tensorfeed.ai/developers/agent-payments.` }] };
|
|
242
|
+
});
|
|
204
243
|
// ── Tool: get_ai_status ─────────────────────────────────────────────
|
|
205
244
|
registerTool('get_ai_status', 'Get the real-time operational status of major AI services (Claude, OpenAI, Gemini, Mistral, Cohere, Replicate, Hugging Face), including per-component breakdowns and an operational/degraded/down rollup per provider. One cross-provider status call instead of checking each vendor\'s status page, useful before an agent routes a request to a model that may be impaired. Free, no auth.', {}, async () => {
|
|
206
245
|
const data = await fetchJSON('/status');
|
|
@@ -235,61 +274,6 @@ registerTool('is_service_down', 'Check whether one named AI service (e.g. "claud
|
|
|
235
274
|
}]
|
|
236
275
|
};
|
|
237
276
|
});
|
|
238
|
-
// Tool: check_agent_ready
|
|
239
|
-
registerTool('check_agent_ready', 'Score how agent-ready a website is, 0 to 100, from its public surfaces: x402 payment manifest, /.well-known/agent.json, an OpenAPI spec, llms.txt, AI-bot crawlability, and ai.txt. Returns the score, the tier (closed, emerging, ready, or advanced), and exactly which surfaces the site exposes. Unlike single-site "is my site agent-ready" scanners, this reads from TensorFeed\'s daily-crawled cross-domain dataset, so the same call works for any tracked domain and the scoring rule is published and identical for every site. Free, no auth.', {
|
|
240
|
-
domain: z.string().describe('Bare domain to score, e.g. "stripe.com" (no scheme, no path)'),
|
|
241
|
-
}, async ({ domain }) => {
|
|
242
|
-
const data = await fetchJSON(`/agent-ready/site?domain=${encodeURIComponent(domain)}`);
|
|
243
|
-
if (!data.found || !data.readiness) {
|
|
244
|
-
return { content: [{ type: 'text', text: `${data.domain} is not in TensorFeed's tracked set yet (no agent-readiness score). About 500 curated domains are tracked.` }] };
|
|
245
|
-
}
|
|
246
|
-
const r = data.readiness;
|
|
247
|
-
const present = Object.entries(r.surfaces).filter(([, v]) => v).map(([k]) => k);
|
|
248
|
-
const text = `${data.domain}: agent-readiness ${r.score}/100 (${r.tier})\n` +
|
|
249
|
-
`Surfaces present: ${present.length ? present.join(', ') : 'none'}\n` +
|
|
250
|
-
`x402=${r.surfaces.x402} agent.json=${r.surfaces.agentJson} openapi=${r.surfaces.openapi} llms.txt=${r.surfaces.llmsTxt} crawlable=${r.surfaces.crawlable} ai.txt=${r.surfaces.aiTxt}\n` +
|
|
251
|
-
`Last checked: ${data.captured_at ?? 'n/a'}`;
|
|
252
|
-
return { content: [{ type: 'text', text }] };
|
|
253
|
-
});
|
|
254
|
-
// Tool: agent_ready_summary
|
|
255
|
-
registerTool('agent_ready_summary', 'Get the state of the agentic web. Across about 500 curated domains, returns the share exposing each agent surface (x402, agent.json, OpenAPI, llms.txt, crawlable, ai.txt), the readiness-tier distribution, and a top-10 leaderboard of the most agent-ready sites. This aggregate, cross-domain view is exactly what a single-site scanner cannot give you. Free, no auth.', {}, async () => {
|
|
256
|
-
const d = await fetchJSON('/agent-ready/summary.json');
|
|
257
|
-
const a = d.adoption_pct;
|
|
258
|
-
const lb = (d.leaderboard || []).slice(0, 10).map((e, i) => `${i + 1}. ${e.domain} (${e.score}, ${e.tier})`).join('\n');
|
|
259
|
-
const text = `Agentic web readiness, ${d.profiled}/${d.domains_tracked} domains profiled (captured ${d.captured_at ?? 'n/a'}):\n` +
|
|
260
|
-
`Surface adoption: x402 ${a.x402}%, agent.json ${a.agentJson}%, openapi ${a.openapi}%, llms.txt ${a.llmsTxt}%, crawlable ${a.crawlable}%, ai.txt ${a.aiTxt}%\n` +
|
|
261
|
-
`Tiers: ${Object.entries(d.tier_distribution || {}).map(([t, n]) => `${t} ${n}`).join(', ')}\n` +
|
|
262
|
-
`Most agent-ready:\n${lb}`;
|
|
263
|
-
return { content: [{ type: 'text', text }] };
|
|
264
|
-
});
|
|
265
|
-
// Tool: check_crawler_access
|
|
266
|
-
registerTool('check_crawler_access', 'Check which AI bots a website allows or blocks in its robots.txt: GPTBot, ClaudeBot, PerplexityBot, CCBot, Google-Extended, Bytespider, and more, with a per-bot verdict (allowed, blocked, partial, or unknown), plus whether the site publishes llms.txt and ai.txt. Reports the site\'s stated policy, not enforcement (robots.txt compliance is voluntary). Free, no auth.', {
|
|
267
|
-
domain: z.string().describe('Bare domain to check, e.g. "nytimes.com" (no scheme, no path)'),
|
|
268
|
-
}, async ({ domain }) => {
|
|
269
|
-
const data = await fetchJSON(`/ai-crawler-access/site?domain=${encodeURIComponent(domain)}`);
|
|
270
|
-
if (!data.found || !data.record) {
|
|
271
|
-
return { content: [{ type: 'text', text: `${data.domain} is not in TensorFeed's tracked set yet. About 500 curated domains are tracked for AI-crawler access.` }] };
|
|
272
|
-
}
|
|
273
|
-
const rec = data.record;
|
|
274
|
-
const bots = Object.entries(rec.bots).map(([b, v]) => ` ${b}: ${v}`).join('\n');
|
|
275
|
-
const text = `${data.domain} (robots.txt HTTP ${rec.robotsStatus ?? 'n/a'}) AI-bot access:\n${bots}\n` +
|
|
276
|
-
`llms.txt: ${rec.hasLlmsTxt ? 'yes' : 'no'}, ai.txt: ${rec.hasAiTxt ? 'yes' : 'no'}\n` +
|
|
277
|
-
`Stated policy only; compliance is voluntary. Last checked: ${data.captured_at ?? 'n/a'}`;
|
|
278
|
-
return { content: [{ type: 'text', text }] };
|
|
279
|
-
});
|
|
280
|
-
// Tool: crawler_access_summary
|
|
281
|
-
registerTool('crawler_access_summary', 'Get the aggregate AI-crawler access map across about 500 curated domains: the percentage of sites that block each AI bot (the publisher-versus-AI-crawler picture), plus llms.txt and ai.txt adoption rates. Unlike a single-site robots.txt checker, this is the cross-domain trend view of who is shutting AI crawlers out. Free, no auth.', {}, async () => {
|
|
282
|
-
const d = await fetchJSON('/ai-crawler-access/summary.json');
|
|
283
|
-
const blocked = Object.entries(d.bot_blocked_pct || {})
|
|
284
|
-
.sort((x, y) => y[1] - x[1])
|
|
285
|
-
.slice(0, 8)
|
|
286
|
-
.map(([b, p]) => ` ${b}: ${p}% blocked`)
|
|
287
|
-
.join('\n');
|
|
288
|
-
const text = `AI-crawler access across ${d.domains_with_data}/${d.domains_tracked} tracked domains (captured ${d.captured_at ?? 'n/a'}):\n` +
|
|
289
|
-
`Most-blocked AI bots:\n${blocked}\n` +
|
|
290
|
-
`llms.txt adoption: ${d.llms_txt_adoption_pct}%, ai.txt adoption: ${d.ai_txt_adoption_pct}%`;
|
|
291
|
-
return { content: [{ type: 'text', text }] };
|
|
292
|
-
});
|
|
293
277
|
// ── Tool: get_model_pricing ─────────────────────────────────────────
|
|
294
278
|
registerTool('get_model_pricing', 'Get AI model pricing across major providers (Anthropic, OpenAI, Google, Meta, Mistral, Cohere) in one normalized table: input and output price per 1M tokens, context window, and release date per model. One cross-provider comparison instead of scraping six pricing pages, so an agent can pick the cheapest model that fits its context and budget. Free, no auth.', {}, async () => {
|
|
295
279
|
const data = await fetchJSON('/models');
|
|
@@ -310,363 +294,107 @@ registerTool('get_model_pricing', 'Get AI model pricing across major providers (
|
|
|
310
294
|
.join('\n\n');
|
|
311
295
|
return { content: [{ type: 'text', text: `AI Model Pricing (per 1M tokens):\n\n${text}${ROUTE_VERDICT_FOOTER}` }] };
|
|
312
296
|
});
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const entries = data.deprecations ?? [];
|
|
332
|
-
if (entries.length === 0) {
|
|
333
|
-
return {
|
|
334
|
-
content: [
|
|
335
|
-
{
|
|
336
|
-
type: 'text',
|
|
337
|
-
text: 'No model deprecations matched the filter. The full calendar is at https://tensorfeed.ai/model-deprecations.',
|
|
338
|
-
},
|
|
339
|
-
],
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
// Group by provider for readable output.
|
|
343
|
-
const byProvider = new Map();
|
|
344
|
-
for (const d of entries) {
|
|
345
|
-
const list = byProvider.get(d.provider) ?? [];
|
|
346
|
-
list.push(d);
|
|
347
|
-
byProvider.set(d.provider, list);
|
|
297
|
+
// ════════════════════════════════════════════════════════════════════
|
|
298
|
+
// PREMIUM TOOLS (require TENSORFEED_TOKEN env var, paid in USDC on Base)
|
|
299
|
+
// ════════════════════════════════════════════════════════════════════
|
|
300
|
+
// ── Tool: account_status ────────────────────────────────────────────
|
|
301
|
+
// Merges the former get_account_balance and get_account_usage tools into
|
|
302
|
+
// one read. Both fetch calls and both output bodies are reused verbatim
|
|
303
|
+
// and concatenated into a single text response.
|
|
304
|
+
registerTool('account_status', 'Check the configured TensorFeed token: current credit balance plus recent per-endpoint usage (last 100 calls aggregated). Free, but requires TENSORFEED_TOKEN.', {}, async () => {
|
|
305
|
+
// Balance and usage are independent reads; fetch them in parallel so the
|
|
306
|
+
// merged tool costs one round-trip of wall-clock time, not two.
|
|
307
|
+
const [balance, usage] = (await Promise.all([
|
|
308
|
+
fetchJSON('/payment/balance', { auth: true }),
|
|
309
|
+
fetchJSON('/payment/usage', { auth: true }),
|
|
310
|
+
]));
|
|
311
|
+
const balanceText = `Balance: ${balance.balance} credits\nTotal purchased: ${balance.total_purchased}\nCreated: ${balance.created}\nLast used: ${balance.last_used}`;
|
|
312
|
+
let usageText;
|
|
313
|
+
if (usage.total_calls === 0) {
|
|
314
|
+
usageText = 'No premium API calls on this token yet.';
|
|
348
315
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
.map(d => {
|
|
354
|
-
const dates = [
|
|
355
|
-
d.announcedDate ? `announced ${d.announcedDate}` : null,
|
|
356
|
-
d.deprecationDate ? `deprecated ${d.deprecationDate}` : null,
|
|
357
|
-
d.sunsetDate ? `sunset ${d.sunsetDate}` : null,
|
|
358
|
-
]
|
|
359
|
-
.filter(Boolean)
|
|
360
|
-
.join(', ');
|
|
361
|
-
const display = d.modelDisplay ? ` (${d.modelDisplay})` : '';
|
|
362
|
-
const repl = d.replacement ? `\n replacement: ${d.replacement}` : '';
|
|
363
|
-
const note = d.notes ? `\n note: ${d.notes}` : '';
|
|
364
|
-
return ` ${d.model}${display} [${d.status.toUpperCase()}]\n ${dates}${repl}${note}\n source: ${d.sourceUrl}`;
|
|
365
|
-
})
|
|
316
|
+
else {
|
|
317
|
+
const rows = Object.entries(usage.by_endpoint)
|
|
318
|
+
.sort(([, a], [, b]) => b.calls - a.calls)
|
|
319
|
+
.map(([ep, info]) => ` ${ep}: ${info.calls} calls, ${info.credits} credits, last ${info.last_seen}`)
|
|
366
320
|
.join('\n');
|
|
367
|
-
|
|
368
|
-
})
|
|
369
|
-
.join('\n\n');
|
|
370
|
-
const header = `AI Model Deprecation Calendar (${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}, last updated ${data.lastUpdated ?? 'recently'}):\n\n`;
|
|
371
|
-
return { content: [{ type: 'text', text: header + sections }] };
|
|
372
|
-
});
|
|
373
|
-
// ── Tool: get_ai_today ──────────────────────────────────────────────
|
|
374
|
-
registerTool('get_ai_today', 'Get a quick digest of the top AI stories from the latest TensorFeed news feed, each with title, source, and URL. A shorter, headline-only companion to get_ai_news for when an agent just needs a fast read on what is happening in AI right now rather than the full article set. Free, no auth.', {
|
|
375
|
-
limit: z.number().min(1).max(20).optional().describe('Number of stories (default 5)'),
|
|
376
|
-
}, async ({ limit }) => {
|
|
377
|
-
const data = await fetchJSON(`/news?limit=${limit || 5}`);
|
|
378
|
-
const text = data.articles
|
|
379
|
-
.map((a, i) => `${i + 1}. ${a.title} (${a.source})\n ${a.url}`)
|
|
380
|
-
.join('\n\n');
|
|
381
|
-
return { content: [{ type: 'text', text: `Today in AI:\n\n${text}` }] };
|
|
382
|
-
});
|
|
383
|
-
// ── Tool: get_agent_activity ────────────────────────────────────────
|
|
384
|
-
registerTool('get_agent_activity', 'Get live AI bot traffic on TensorFeed.ai. Returns today\'s bot hit count, the most recent 50 hits with bot name + endpoint + timestamp, and a derived top-bots breakdown. Useful when an agent wants to see which crawlers are pulling AI ecosystem data and how that distribution shifts. Free, no auth.', {}, async () => {
|
|
385
|
-
const data = (await fetchJSON('/agents/activity'));
|
|
386
|
-
const tally = new Map();
|
|
387
|
-
for (const hit of data.recent) {
|
|
388
|
-
tally.set(hit.bot, (tally.get(hit.bot) || 0) + 1);
|
|
389
|
-
}
|
|
390
|
-
const breakdown = Array.from(tally.entries())
|
|
391
|
-
.map(([bot, count]) => ` ${bot}: ${count}`)
|
|
392
|
-
.sort()
|
|
393
|
-
.join('\n');
|
|
394
|
-
const tail = data.recent
|
|
395
|
-
.slice(0, 10)
|
|
396
|
-
.map(h => ` ${h.bot} ${h.endpoint} (${h.timestamp})`)
|
|
397
|
-
.join('\n');
|
|
398
|
-
return {
|
|
399
|
-
content: [
|
|
400
|
-
{
|
|
401
|
-
type: 'text',
|
|
402
|
-
text: `AI bot traffic on TensorFeed.ai\n\n` +
|
|
403
|
-
`Today's bot hits: ${data.today_count}\n` +
|
|
404
|
-
`Last updated: ${data.last_updated}\n\n` +
|
|
405
|
-
`Bot breakdown (recent ${data.recent.length} hits):\n${breakdown || ' (none)'}\n\n` +
|
|
406
|
-
`Recent tail (10 most recent):\n${tail || ' (none)'}\n\n` +
|
|
407
|
-
`Public dashboard: https://tensorfeed.ai/agent-traffic`,
|
|
408
|
-
},
|
|
409
|
-
],
|
|
410
|
-
};
|
|
411
|
-
});
|
|
412
|
-
// ── Tool: get_ai_supply_chain_iocs ──────────────────────────────────
|
|
413
|
-
registerTool('get_ai_supply_chain_iocs', 'Get the latest AI/ML supply-chain indicators-of-compromise from the TensorFeed defender feed. Daily-refreshed list of GitHub Security Advisory entries filtered by AI/ML/LLM/MCP keyword vocabulary across npm, PyPI, Go, Maven, and other ecosystems. Each entry includes the package name, ecosystem, GHSA advisory ID, severity, summary, vulnerable version range, publication date, and a link to the authoritative GHSA record. Pure data feed: TensorFeed re-publishes already-public advisories, does not detect malware, and does not attribute it. Always treat the linked GHSA record as authoritative. Useful for AI-tool maintainers, MCP-server reviewers, and supply-chain monitors that want a single feed of AI-relevant advisories rather than parsing all of GHSA.', {
|
|
414
|
-
severity: z
|
|
415
|
-
.enum(['critical', 'high', 'moderate', 'low'])
|
|
416
|
-
.optional()
|
|
417
|
-
.describe('Filter to a single GHSA severity tier'),
|
|
418
|
-
ecosystem: z
|
|
419
|
-
.string()
|
|
420
|
-
.optional()
|
|
421
|
-
.describe('Filter to a single package ecosystem (e.g. "npm", "pypi", "go", "maven")'),
|
|
422
|
-
limit: z
|
|
423
|
-
.number()
|
|
424
|
-
.min(1)
|
|
425
|
-
.max(100)
|
|
426
|
-
.optional()
|
|
427
|
-
.describe('Maximum number of advisories to return (default 25, max 100)'),
|
|
428
|
-
}, async ({ severity, ecosystem, limit }) => {
|
|
429
|
-
const data = (await fetchJSON('/security/ai-supply-chain-iocs.json'));
|
|
430
|
-
const sevWanted = severity?.toLowerCase();
|
|
431
|
-
const ecoWanted = ecosystem?.toLowerCase();
|
|
432
|
-
const filtered = data.entries.filter((e) => {
|
|
433
|
-
if (sevWanted && e.severity.toLowerCase() !== sevWanted)
|
|
434
|
-
return false;
|
|
435
|
-
if (ecoWanted && e.package.ecosystem.toLowerCase() !== ecoWanted)
|
|
436
|
-
return false;
|
|
437
|
-
return true;
|
|
438
|
-
});
|
|
439
|
-
const cap = Math.min(limit ?? 25, 100);
|
|
440
|
-
const rows = filtered.slice(0, cap);
|
|
441
|
-
if (rows.length === 0) {
|
|
442
|
-
return {
|
|
443
|
-
content: [
|
|
444
|
-
{
|
|
445
|
-
type: 'text',
|
|
446
|
-
text: `No AI supply-chain IOCs matched (total in feed: ${data.total}, generated ${data.generated_at}).`,
|
|
447
|
-
},
|
|
448
|
-
],
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
const body = rows
|
|
452
|
-
.map((e, i) => {
|
|
453
|
-
const kw = e.ai_relevance?.matched_keywords?.length
|
|
454
|
-
? ` [matched: ${e.ai_relevance.matched_keywords.join(', ')}]`
|
|
455
|
-
: '';
|
|
456
|
-
return [
|
|
457
|
-
`${i + 1}. ${e.package.ecosystem}: ${e.package.name} (${e.severity.toUpperCase()})`,
|
|
458
|
-
` ${e.summary}`,
|
|
459
|
-
` GHSA: ${e.advisory_id} ${e.url}`,
|
|
460
|
-
` Affected: ${e.vulnerable_version_range} Published: ${e.published_at}${kw}`,
|
|
461
|
-
].join('\n');
|
|
462
|
-
})
|
|
463
|
-
.join('\n\n');
|
|
464
|
-
const header = `AI supply-chain IOCs (${rows.length} of ${filtered.length} matched, ${data.total} in feed, generated ${data.generated_at}):`;
|
|
465
|
-
const footer = `\n\nPosture: ${data.posture}`;
|
|
466
|
-
return { content: [{ type: 'text', text: `${header}\n\n${body}${footer}` }] };
|
|
467
|
-
});
|
|
468
|
-
// ── Tool: get_honeypot_iocs ─────────────────────────────────────────
|
|
469
|
-
registerTool('get_honeypot_iocs', 'Get the TensorFeed honeypot indicators-of-compromise feed. Returns attacker IP addresses, user agents, and target paths observed against TensorFeed.ai trap endpoints over the last 30 days. Pure first-party observation; not a re-export of upstream threat intel. License CC0; downstream defenders may ingest and pre-block at their edge. Each row: ioc value, ioc type (ip / ua / path), first_seen, last_seen, hit_count. Useful as a low-noise honeypot signal layered on top of public IOC feeds.', {
|
|
470
|
-
type: z
|
|
471
|
-
.enum(['ip', 'ua', 'path'])
|
|
472
|
-
.optional()
|
|
473
|
-
.describe('Filter to one IOC type. Omit to return all types.'),
|
|
474
|
-
limit: z
|
|
475
|
-
.number()
|
|
476
|
-
.min(1)
|
|
477
|
-
.max(500)
|
|
478
|
-
.optional()
|
|
479
|
-
.describe('Maximum number of IOCs to return (default 50, max 500)'),
|
|
480
|
-
}, async ({ type, limit }) => {
|
|
481
|
-
const data = (await fetchJSON('/security/iocs.json'));
|
|
482
|
-
const typeWanted = type?.toLowerCase();
|
|
483
|
-
const filtered = data.iocs.filter((e) => !typeWanted || e.type.toLowerCase() === typeWanted);
|
|
484
|
-
const cap = Math.min(limit ?? 50, 500);
|
|
485
|
-
const rows = filtered.slice(0, cap);
|
|
486
|
-
if (rows.length === 0) {
|
|
487
|
-
return {
|
|
488
|
-
content: [
|
|
489
|
-
{
|
|
490
|
-
type: 'text',
|
|
491
|
-
text: `No honeypot IOCs matched (total in feed: ${data.total_iocs}, window ${data.window_hours}h, generated ${data.generated_at}).`,
|
|
492
|
-
},
|
|
493
|
-
],
|
|
494
|
-
};
|
|
321
|
+
usageText = `Total: ${usage.total_calls} calls, ${usage.total_credits_spent} credits\n\n${rows}`;
|
|
495
322
|
}
|
|
496
|
-
const body = rows
|
|
497
|
-
.map((e, i) => `${i + 1}. [${e.type}] ${e.ioc}\n hits: ${e.hit_count} first: ${e.first_seen} last: ${e.last_seen}`)
|
|
498
|
-
.join('\n');
|
|
499
|
-
const header = `TensorFeed honeypot IOCs (${rows.length} of ${filtered.length} matched, ${data.total_iocs} in feed, ${data.window_hours}h window, generated ${data.generated_at}):`;
|
|
500
|
-
const footer = `\n\nLicense: ${data.policy.license}. ${data.policy.description}`;
|
|
501
|
-
return { content: [{ type: 'text', text: `${header}\n\n${body}${footer}` }] };
|
|
502
|
-
});
|
|
503
|
-
// ── Tool: check_afta_certification ──────────────────────────────────
|
|
504
|
-
registerTool('check_afta_certification', 'Check whether a domain is AFTA-certified (Agent Fair-Trade Agreement). AFTA is an open standard: cryptographically-signed receipts, transparent pricing, no-charge guarantees for failed responses, on-chain settlement, and federated trust between participating sites. The check runs 6 deterministic probes against the target domain (e.g. tensorfeed.ai, terminalfeed.io): well-known endpoint, receipts endpoint, x402 payment manifest, pricing transparency, free trial availability, federation membership. Returns score, verdict, and which checks passed/failed.', {
|
|
505
|
-
domain: z
|
|
506
|
-
.string()
|
|
507
|
-
.describe('Domain to certify (e.g. "tensorfeed.ai", "terminalfeed.io"). Bare host, no protocol.'),
|
|
508
|
-
}, async ({ domain }) => {
|
|
509
|
-
const data = (await fetchJSON(`/afta-certify/check?domain=${encodeURIComponent(domain)}`));
|
|
510
|
-
const lines = data.checks.map((c) => ` ${c.passed ? 'PASS' : 'FAIL'} ${c.name}${c.details ? ` (${c.details})` : ''}`);
|
|
511
|
-
const certified = data.afta_certified ? 'AFTA-CERTIFIED' : 'NOT certified';
|
|
512
|
-
const next = data.next_step ? `\nNext step: ${data.next_step}` : '';
|
|
513
323
|
return {
|
|
514
324
|
content: [
|
|
515
325
|
{
|
|
516
326
|
type: 'text',
|
|
517
|
-
text:
|
|
327
|
+
text: `${balanceText}\n\nUsage:\n${usageText}`,
|
|
518
328
|
},
|
|
519
329
|
],
|
|
520
330
|
};
|
|
521
331
|
});
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
if (!wallet && !token_prefix) {
|
|
332
|
+
registerTool('route_verdict', "TensorFeed's signed model-routing decision: the single best model for a task or named model, fused from live pricing, contamination-discounted benchmarks, real production usage, measured p95 latency, incident state, and deprecation flags, with the reasoning. tier='preview' (default) is free (10 calls per day per IP), top verdict only. tier='full' costs 1 credit ($0.02), adds ranked runners-up, constraint filters, and an AFTA-signed receipt you can audit, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
333
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds runners-up, filters, signed receipt)."),
|
|
334
|
+
task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type to route for (code, reasoning, creative, general). Provide task or model.'),
|
|
335
|
+
model: z.string().optional().describe('Model id or display name to narrow the verdict to one model (e.g. "Claude Opus 4.7" or "claude-opus-4-7"). Provide task or model.'),
|
|
336
|
+
max_latency_p95_ms: z.number().optional().describe('Full tier only. Drop candidates whose measured p95 latency exceeds this value (ms).'),
|
|
337
|
+
budget: z.number().optional().describe('Full tier only. Max blended USD per 1M tokens.'),
|
|
338
|
+
min_quality: z.number().min(0).max(1).optional().describe('Full tier only. Minimum trust-discounted quality score in [0, 1].'),
|
|
339
|
+
require_operational: z.boolean().optional().describe('Full tier only. Default true. Set false to keep candidates known down or in failover.'),
|
|
340
|
+
exclude_deprecated: z.boolean().optional().describe('Full tier only. Default true. Set false to keep deprecated or sunsetted models.'),
|
|
341
|
+
}, async ({ tier, task, model, max_latency_p95_ms, budget, min_quality, require_operational, exclude_deprecated }) => {
|
|
342
|
+
if (!task && !model) {
|
|
534
343
|
return {
|
|
535
344
|
content: [
|
|
536
345
|
{
|
|
537
346
|
type: 'text',
|
|
538
|
-
text: 'Provide
|
|
347
|
+
text: 'Provide a task (code, reasoning, creative, general) or a model id/name to get a routing verdict.',
|
|
539
348
|
},
|
|
540
349
|
],
|
|
541
350
|
};
|
|
542
351
|
}
|
|
543
|
-
const path = wallet
|
|
544
|
-
? `/agents/reputation/${encodeURIComponent(wallet)}`
|
|
545
|
-
: `/agents/reputation/by-token/${encodeURIComponent(token_prefix)}`;
|
|
546
|
-
let data;
|
|
547
|
-
try {
|
|
548
|
-
data = await fetchJSON(path);
|
|
549
|
-
}
|
|
550
|
-
catch (e) {
|
|
551
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
552
|
-
if (msg.includes('404') || msg.toLowerCase().includes('not_found')) {
|
|
553
|
-
return {
|
|
554
|
-
content: [
|
|
555
|
-
{
|
|
556
|
-
type: 'text',
|
|
557
|
-
text: `No reputation card for ${wallet ?? token_prefix}. TF only builds cards for wallets observed in production; if this is a real agent that has paid TF before, it should appear in the next daily rebuild (04:50 UTC).`,
|
|
558
|
-
},
|
|
559
|
-
],
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
throw e;
|
|
563
|
-
}
|
|
564
|
-
// The card schema is rich; select the documented top-level fields and
|
|
565
|
-
// clamp any array so a large or inflated upstream card cannot blow the
|
|
566
|
-
// agent's context. Unknown fields still pass through (so the agent can
|
|
567
|
-
// reason over them) but every array is capped and the whole payload is
|
|
568
|
-
// bounded by the downstream sanitize char cap as a backstop.
|
|
569
|
-
const rawCard = data.card ?? data;
|
|
570
|
-
const MAX_CARD_ARRAY = 25;
|
|
571
|
-
const clamped = {};
|
|
572
|
-
for (const [k, v] of Object.entries(rawCard)) {
|
|
573
|
-
if (Array.isArray(v) && v.length > MAX_CARD_ARRAY) {
|
|
574
|
-
clamped[k] = [...v.slice(0, MAX_CARD_ARRAY), `...[${v.length - MAX_CARD_ARRAY} more omitted]`];
|
|
575
|
-
}
|
|
576
|
-
else {
|
|
577
|
-
clamped[k] = v;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return {
|
|
581
|
-
content: [
|
|
582
|
-
{
|
|
583
|
-
type: 'text',
|
|
584
|
-
text: `Agent reputation card for ${wallet ?? `token_prefix=${token_prefix}`}:\n\n${JSON.stringify(clamped, null, 2)}`,
|
|
585
|
-
},
|
|
586
|
-
],
|
|
587
|
-
};
|
|
588
|
-
});
|
|
589
|
-
// ════════════════════════════════════════════════════════════════════
|
|
590
|
-
// PREMIUM TOOLS (require TENSORFEED_TOKEN env var, paid in USDC on Base)
|
|
591
|
-
// ════════════════════════════════════════════════════════════════════
|
|
592
|
-
// ── Tool: get_account_balance ───────────────────────────────────────
|
|
593
|
-
registerTool('get_account_balance', 'Check the credit balance for the configured TensorFeed bearer token. Free, but requires TENSORFEED_TOKEN to be set.', {}, async () => {
|
|
594
|
-
const data = (await fetchJSON('/payment/balance', { auth: true }));
|
|
595
|
-
return {
|
|
596
|
-
content: [
|
|
597
|
-
{
|
|
598
|
-
type: 'text',
|
|
599
|
-
text: `Balance: ${data.balance} credits\nTotal purchased: ${data.total_purchased}\nCreated: ${data.created}\nLast used: ${data.last_used}`,
|
|
600
|
-
},
|
|
601
|
-
],
|
|
602
|
-
};
|
|
603
|
-
});
|
|
604
|
-
// ── Tool: get_account_usage ─────────────────────────────────────────
|
|
605
|
-
registerTool('get_account_usage', 'Show per-endpoint usage for the configured TensorFeed token (last 100 calls aggregated). Free, but requires TENSORFEED_TOKEN.', {}, async () => {
|
|
606
|
-
const data = (await fetchJSON('/payment/usage', { auth: true }));
|
|
607
|
-
if (data.total_calls === 0) {
|
|
608
|
-
return { content: [{ type: 'text', text: 'No premium API calls on this token yet.' }] };
|
|
609
|
-
}
|
|
610
|
-
const rows = Object.entries(data.by_endpoint)
|
|
611
|
-
.sort(([, a], [, b]) => b.calls - a.calls)
|
|
612
|
-
.map(([ep, info]) => ` ${ep}: ${info.calls} calls, ${info.credits} credits, last ${info.last_seen}`)
|
|
613
|
-
.join('\n');
|
|
614
|
-
return {
|
|
615
|
-
content: [
|
|
616
|
-
{
|
|
617
|
-
type: 'text',
|
|
618
|
-
text: `Total: ${data.total_calls} calls, ${data.total_credits_spent} credits\n\n${rows}`,
|
|
619
|
-
},
|
|
620
|
-
],
|
|
621
|
-
};
|
|
622
|
-
});
|
|
623
|
-
// ── Tool: premium_routing (1 credit) ────────────────────────────────
|
|
624
|
-
registerTool('premium_routing', 'Get a ranked list of recommended AI models for a task (code, reasoning, creative, or general), each with a composite score plus the quality, availability, cost, and latency components that produced it, and the per-model input/output pricing. Constrain with budget, min_quality, and top_n so the ranking reflects your actual limits instead of a generic leaderboard. Costs 1 credit ($0.02), billed against your TENSORFEED_TOKEN. For a single signed routing decision with reasoning, see route_verdict.', {
|
|
625
|
-
task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type the model needs to be good at (default: general)'),
|
|
626
|
-
budget: z.number().optional().describe('Max blended USD per 1M tokens'),
|
|
627
|
-
min_quality: z.number().min(0).max(1).optional().describe('Minimum quality score in [0, 1]'),
|
|
628
|
-
top_n: z.number().min(1).max(10).optional().describe('How many models to return (default 5)'),
|
|
629
|
-
}, async ({ task, budget, min_quality, top_n }) => {
|
|
630
352
|
const params = new URLSearchParams();
|
|
631
353
|
if (task)
|
|
632
354
|
params.set('task', task);
|
|
633
|
-
if (
|
|
634
|
-
params.set('
|
|
635
|
-
if (
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
355
|
+
if (model)
|
|
356
|
+
params.set('model', model);
|
|
357
|
+
if ((tier ?? 'preview') === 'full') {
|
|
358
|
+
if (typeof max_latency_p95_ms === 'number')
|
|
359
|
+
params.set('max_latency_p95_ms', String(max_latency_p95_ms));
|
|
360
|
+
if (typeof budget === 'number')
|
|
361
|
+
params.set('budget', String(budget));
|
|
362
|
+
if (typeof min_quality === 'number')
|
|
363
|
+
params.set('min_quality', String(min_quality));
|
|
364
|
+
if (typeof require_operational === 'boolean')
|
|
365
|
+
params.set('require_operational', String(require_operational));
|
|
366
|
+
if (typeof exclude_deprecated === 'boolean')
|
|
367
|
+
params.set('exclude_deprecated', String(exclude_deprecated));
|
|
368
|
+
const data = (await fetchJSON(`/premium/route-verdict?${params}`, { auth: true }));
|
|
369
|
+
if (!data.verdict) {
|
|
370
|
+
return {
|
|
371
|
+
content: [{ type: 'text', text: `No routing verdict matched your filters.\n${data.claim ?? ''}` }],
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const v = data.verdict;
|
|
375
|
+
const t = data.trust;
|
|
376
|
+
const f = data.filters_applied;
|
|
377
|
+
const runners = (data.runners_up ?? [])
|
|
378
|
+
.map((r) => ` #${r.rank} ${r.model.name} (${r.model.provider}) blended $${r.pricing.blended}/1M\n ${r.why}`)
|
|
379
|
+
.join('\n');
|
|
380
|
+
const trustLine = `Trust: usage ${t.usage_corroborated ? 'corroborated' : 'uncorroborated'}, benchmark contamination ${t.benchmark_contamination}, operational ${t.operational_layer}, latency ${t.latency_layer}`;
|
|
381
|
+
const filterLine = `Filters: max p95 ${f.max_latency_p95_ms ?? 'none'}ms, require_operational ${f.require_operational}, exclude_deprecated ${f.exclude_deprecated}`;
|
|
382
|
+
const billing = data.billing
|
|
383
|
+
? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining}.`
|
|
384
|
+
: '';
|
|
656
385
|
return {
|
|
657
386
|
content: [
|
|
658
387
|
{
|
|
659
388
|
type: 'text',
|
|
660
|
-
text:
|
|
389
|
+
text: `Route Verdict: ${v.model.name} (${v.model.provider}) score ${v.composite_score}\n` +
|
|
390
|
+
`Why: ${v.why}\n` +
|
|
391
|
+
`Blended price: $${v.pricing.blended}/1M tokens (in $${v.pricing.input}, out $${v.pricing.output})\n\n` +
|
|
392
|
+
`Runners-up:\n${runners || ' (none)'}\n\n` +
|
|
393
|
+
`${trustLine}\n${filterLine}\n\nClaim: ${data.claim}${billing}`,
|
|
661
394
|
},
|
|
662
395
|
],
|
|
663
396
|
};
|
|
664
397
|
}
|
|
665
|
-
const params = new URLSearchParams();
|
|
666
|
-
if (task)
|
|
667
|
-
params.set('task', task);
|
|
668
|
-
if (model)
|
|
669
|
-
params.set('model', model);
|
|
670
398
|
const data = (await fetchJSON(`/preview/route-verdict?${params}`));
|
|
671
399
|
if (!data.verdict) {
|
|
672
400
|
return {
|
|
@@ -692,84 +420,15 @@ registerTool('route_verdict_preview', "Free. TensorFeed's signed routing decisio
|
|
|
692
420
|
`Why: ${v.why}\n` +
|
|
693
421
|
`Blended price: $${v.pricing.blended}/1M tokens (in $${v.pricing.input}, out $${v.pricing.output})\n` +
|
|
694
422
|
`${trustLine}${fresh ? `\nData freshness: ${fresh}` : ''}\n` +
|
|
695
|
-
`Upgrade: route_verdict (1 credit, $0.02) adds ranked runners-up, constraint filters, and an AFTA-signed receipt you can audit. No USDC yet? Sign a wallet message at tensorfeed.ai/api/payment/trial-credits for 25 free credits.${rl}`,
|
|
696
|
-
},
|
|
697
|
-
],
|
|
698
|
-
};
|
|
699
|
-
});
|
|
700
|
-
// ── Tool: route_verdict (1 credit) ──────────────────────────────────
|
|
701
|
-
// The signed routing decision. Single best model plus ranked runners-up
|
|
702
|
-
// for a task or named model, with constraint filters and an AFTA-signed
|
|
703
|
-
// receipt over the exact inputs. Strict premium, no free trial. The free
|
|
704
|
-
// route_verdict_preview is the no-auth taste of the same engine.
|
|
705
|
-
registerTool('route_verdict', "Costs 1 credit ($0.02). The signed routing decision: TensorFeed's single best model plus ranked runners-up for your task or a named model, fused from live pricing, contamination-discounted benchmarks, real production usage, MEASURED p95 latency, incident state, and deprecation flags, with an AFTA-signed receipt over the exact inputs so you can prove why you routed. Versus the free route_verdict_preview it adds ranked runners-up, constraint filters (max p95 latency, budget, min quality, operational-only, exclude-deprecated), the signed receipt, and no rate limit. Get credits at tensorfeed.ai/developers/agent-payments. Strict premium, no free trial.", {
|
|
706
|
-
task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type to route for (code, reasoning, creative, general). Provide task or model.'),
|
|
707
|
-
model: z.string().optional().describe('Model id or display name to narrow the verdict to one model. Provide task or model.'),
|
|
708
|
-
max_latency_p95_ms: z.number().optional().describe('Drop candidates whose measured p95 latency exceeds this value (ms).'),
|
|
709
|
-
budget: z.number().optional().describe('Max blended USD per 1M tokens'),
|
|
710
|
-
min_quality: z.number().min(0).max(1).optional().describe('Minimum trust-discounted quality score in [0, 1]'),
|
|
711
|
-
require_operational: z.boolean().optional().describe('Default true. Set false to keep candidates known down or in failover.'),
|
|
712
|
-
exclude_deprecated: z.boolean().optional().describe('Default true. Set false to keep deprecated or sunsetted models.'),
|
|
713
|
-
}, async ({ task, model, max_latency_p95_ms, budget, min_quality, require_operational, exclude_deprecated }) => {
|
|
714
|
-
if (!task && !model) {
|
|
715
|
-
return {
|
|
716
|
-
content: [
|
|
717
|
-
{
|
|
718
|
-
type: 'text',
|
|
719
|
-
text: 'Provide a task (code, reasoning, creative, general) or a model id/name to get a signed routing verdict.',
|
|
720
|
-
},
|
|
721
|
-
],
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
const params = new URLSearchParams();
|
|
725
|
-
if (task)
|
|
726
|
-
params.set('task', task);
|
|
727
|
-
if (model)
|
|
728
|
-
params.set('model', model);
|
|
729
|
-
if (typeof max_latency_p95_ms === 'number')
|
|
730
|
-
params.set('max_latency_p95_ms', String(max_latency_p95_ms));
|
|
731
|
-
if (typeof budget === 'number')
|
|
732
|
-
params.set('budget', String(budget));
|
|
733
|
-
if (typeof min_quality === 'number')
|
|
734
|
-
params.set('min_quality', String(min_quality));
|
|
735
|
-
if (typeof require_operational === 'boolean')
|
|
736
|
-
params.set('require_operational', String(require_operational));
|
|
737
|
-
if (typeof exclude_deprecated === 'boolean')
|
|
738
|
-
params.set('exclude_deprecated', String(exclude_deprecated));
|
|
739
|
-
const data = (await fetchJSON(`/premium/route-verdict?${params}`, { auth: true }));
|
|
740
|
-
if (!data.verdict) {
|
|
741
|
-
return {
|
|
742
|
-
content: [{ type: 'text', text: `No routing verdict matched your filters.\n${data.claim ?? ''}` }],
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
const v = data.verdict;
|
|
746
|
-
const t = data.trust;
|
|
747
|
-
const f = data.filters_applied;
|
|
748
|
-
const runners = (data.runners_up ?? [])
|
|
749
|
-
.map((r) => ` #${r.rank} ${r.model.name} (${r.model.provider}) blended $${r.pricing.blended}/1M\n ${r.why}`)
|
|
750
|
-
.join('\n');
|
|
751
|
-
const trustLine = `Trust: usage ${t.usage_corroborated ? 'corroborated' : 'uncorroborated'}, benchmark contamination ${t.benchmark_contamination}, operational ${t.operational_layer}, latency ${t.latency_layer}`;
|
|
752
|
-
const filterLine = `Filters: max p95 ${f.max_latency_p95_ms ?? 'none'}ms, require_operational ${f.require_operational}, exclude_deprecated ${f.exclude_deprecated}`;
|
|
753
|
-
const billing = data.billing
|
|
754
|
-
? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining}.`
|
|
755
|
-
: '';
|
|
756
|
-
return {
|
|
757
|
-
content: [
|
|
758
|
-
{
|
|
759
|
-
type: 'text',
|
|
760
|
-
text: `Route Verdict: ${v.model.name} (${v.model.provider}) score ${v.composite_score}\n` +
|
|
761
|
-
`Why: ${v.why}\n` +
|
|
762
|
-
`Blended price: $${v.pricing.blended}/1M tokens (in $${v.pricing.input}, out $${v.pricing.output})\n\n` +
|
|
763
|
-
`Runners-up:\n${runners || ' (none)'}\n\n` +
|
|
764
|
-
`${trustLine}\n${filterLine}\n\nClaim: ${data.claim}${billing}`,
|
|
423
|
+
`Upgrade: route_verdict tier='full' (1 credit, $0.02) adds ranked runners-up, constraint filters, and an AFTA-signed receipt you can audit. No USDC yet? Sign a wallet message at tensorfeed.ai/api/payment/trial-credits for 25 free credits.${rl}`,
|
|
765
424
|
},
|
|
766
425
|
],
|
|
767
426
|
};
|
|
768
427
|
});
|
|
769
428
|
// ════════════════════════════════════════════════════════════════════
|
|
770
|
-
// Verdict family: 7 signed decisions, each a
|
|
771
|
-
//
|
|
772
|
-
//
|
|
429
|
+
// Verdict family: 7 signed decisions, each a single tier-parameterized
|
|
430
|
+
// tool. tier='preview' (default) calls fetchJSON('/preview/<x>') (no
|
|
431
|
+
// auth, 10 calls per IP per day); tier='full' calls
|
|
773
432
|
// fetchJSON('/premium/<x>', { auth: true }) and burns 1 credit. The
|
|
774
433
|
// shapes below mirror the worker builder result interfaces exactly.
|
|
775
434
|
// ════════════════════════════════════════════════════════════════════
|
|
@@ -778,7 +437,7 @@ registerTool('route_verdict', "Costs 1 credit ($0.02). The signed routing decisi
|
|
|
778
437
|
// paid tier adds over the free preview, `rl` is the optional remaining
|
|
779
438
|
// calls suffix.
|
|
780
439
|
function verdictUpsell(tool, adds, rl) {
|
|
781
|
-
return (`Upgrade: ${tool} (1 credit, $0.02)
|
|
440
|
+
return (`Upgrade: call ${tool} again with tier="full" (1 credit, $0.02) for ${adds} and an AFTA-signed receipt. ` +
|
|
782
441
|
`No USDC yet? Sign a wallet message at tensorfeed.ai/api/payment/trial-credits for 25 free credits.${rl}`);
|
|
783
442
|
}
|
|
784
443
|
function verdictBilling(billing) {
|
|
@@ -787,7 +446,32 @@ function verdictBilling(billing) {
|
|
|
787
446
|
function previewRateLine(rate) {
|
|
788
447
|
return rate ? ` Preview: ${rate.remaining} of ${rate.limit} calls left today.` : '';
|
|
789
448
|
}
|
|
790
|
-
registerTool('
|
|
449
|
+
registerTool('provider_reliability_verdict', "TensorFeed's signed dependability ruling over its OWN measured latency and availability probes of the frontier AI providers: the single most-dependable provider to build on and the riskiest, scoring availability and tail consistency (p50 over p95) equally because an agent retry loop feels the tail, not the median. tier='preview' (default) is free (10 calls per day per IP), top verdict only. tier='full' costs 1 credit ($0.02), adds the full per-provider ranking with measured availability and p50/p95/p99 and tail spread plus an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
450
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds the full per-provider ranking and signed receipt)."),
|
|
451
|
+
}, async ({ tier }) => {
|
|
452
|
+
if ((tier ?? 'preview') === 'full') {
|
|
453
|
+
const data = (await fetchJSON('/premium/provider-reliability-verdict', { auth: true }));
|
|
454
|
+
const v = data.verdict;
|
|
455
|
+
if (!v || !v.most_dependable) {
|
|
456
|
+
return { content: [{ type: 'text', text: `No reliability verdict available right now.\n${data.claim ?? ''}` }] };
|
|
457
|
+
}
|
|
458
|
+
const ranking = (data.ranking ?? [])
|
|
459
|
+
.map((r) => ` #${r.rank} ${r.provider}: ${r.note}`)
|
|
460
|
+
.join('\n');
|
|
461
|
+
return {
|
|
462
|
+
content: [
|
|
463
|
+
{
|
|
464
|
+
type: 'text',
|
|
465
|
+
text: `Provider Reliability Verdict: most dependable ${v.most_dependable}` +
|
|
466
|
+
(v.riskiest ? `, riskiest ${v.riskiest}` : '') +
|
|
467
|
+
'\n\n' +
|
|
468
|
+
`Ranking:\n${ranking || ' (none)'}\n\n` +
|
|
469
|
+
`Claim: ${data.claim}` +
|
|
470
|
+
verdictBilling(data.billing),
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
};
|
|
474
|
+
}
|
|
791
475
|
const data = (await fetchJSON('/preview/provider-reliability-verdict'));
|
|
792
476
|
const v = data.verdict;
|
|
793
477
|
if (!v || !v.most_dependable) {
|
|
@@ -811,31 +495,39 @@ registerTool('provider_reliability_verdict_preview', "Free. TensorFeed's signed
|
|
|
811
495
|
],
|
|
812
496
|
};
|
|
813
497
|
});
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
if (
|
|
819
|
-
|
|
498
|
+
registerTool('x402_settlement_verdict', "TensorFeed's signed ruling on the state of the x402 USDC settlement market on Base, computed over its OWN on-chain settlement index: market momentum versus the prior window of equal length, concentration, and the leading publisher. Covers the publishers TensorFeed indexes on Base, forward-only from launch. tier='preview' (default) is free (10 calls per day per IP), headline verdict only. tier='full' costs 1 credit ($0.02), adds the full per-publisher ranking with volume share, ecosystem totals, the Herfindahl concentration index, an optional window, and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
499
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds per-publisher ranking, totals, window, signed receipt)."),
|
|
500
|
+
window: z.enum(['24h', '7d', '30d']).optional().describe('Full tier only. Settlement window to rule over (24h, 7d, 30d). Default 7d.'),
|
|
501
|
+
}, async ({ tier, window }) => {
|
|
502
|
+
if ((tier ?? 'preview') === 'full') {
|
|
503
|
+
const params = new URLSearchParams();
|
|
504
|
+
if (window)
|
|
505
|
+
params.set('window', window);
|
|
506
|
+
const qs = params.toString();
|
|
507
|
+
const data = (await fetchJSON(`/premium/x402-settlement-verdict${qs ? `?${qs}` : ''}`, { auth: true }));
|
|
508
|
+
const v = data.verdict;
|
|
509
|
+
const e = data.ecosystem;
|
|
510
|
+
const ranking = (data.ranking ?? [])
|
|
511
|
+
.map((r) => ` #${r.rank} ${r.domain}: ${r.volume_usdc} USDC over ${r.count} settlements (${r.share_pct}% share)`)
|
|
512
|
+
.join('\n');
|
|
513
|
+
return {
|
|
514
|
+
content: [
|
|
515
|
+
{
|
|
516
|
+
type: 'text',
|
|
517
|
+
text: `x402 Settlement Verdict (${data.window_label ?? '7d'}): momentum ${v.momentum}, concentration ${v.concentration}` +
|
|
518
|
+
(v.leading_publisher ? `, leading publisher ${v.leading_publisher}` : ', no leading publisher this window') +
|
|
519
|
+
'\n' +
|
|
520
|
+
`Ecosystem: ${e.volume_usdc} USDC over ${e.count} settlements across ${e.unique_publishers} publishers` +
|
|
521
|
+
(e.top_publisher_share_pct !== null ? `, top share ${e.top_publisher_share_pct}%` : '') +
|
|
522
|
+
(e.hhi !== null ? `, HHI ${e.hhi}` : '') +
|
|
523
|
+
'\n\n' +
|
|
524
|
+
`Ranking:\n${ranking || ' (none)'}\n\n` +
|
|
525
|
+
`Claim: ${data.claim}` +
|
|
526
|
+
verdictBilling(data.billing),
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
};
|
|
820
530
|
}
|
|
821
|
-
const ranking = (data.ranking ?? [])
|
|
822
|
-
.map((r) => ` #${r.rank} ${r.provider}: ${r.note}`)
|
|
823
|
-
.join('\n');
|
|
824
|
-
return {
|
|
825
|
-
content: [
|
|
826
|
-
{
|
|
827
|
-
type: 'text',
|
|
828
|
-
text: `Provider Reliability Verdict: most dependable ${v.most_dependable}` +
|
|
829
|
-
(v.riskiest ? `, riskiest ${v.riskiest}` : '') +
|
|
830
|
-
'\n\n' +
|
|
831
|
-
`Ranking:\n${ranking || ' (none)'}\n\n` +
|
|
832
|
-
`Claim: ${data.claim}` +
|
|
833
|
-
verdictBilling(data.billing),
|
|
834
|
-
},
|
|
835
|
-
],
|
|
836
|
-
};
|
|
837
|
-
});
|
|
838
|
-
registerTool('x402_settlement_verdict_preview', "Free. TensorFeed's signed ruling on the state of the x402 USDC settlement market on Base, computed over its OWN on-chain settlement index: market momentum versus the prior window, concentration, and the leading publisher. For the full per-publisher ranking, ecosystem totals, the Herfindahl concentration index, an optional window, and an AFTA-signed receipt, use x402_settlement_verdict. Covers the publishers TensorFeed indexes on Base, forward-only from launch. 10 calls per day per IP.", {}, async () => {
|
|
839
531
|
const data = (await fetchJSON('/preview/x402-settlement-verdict'));
|
|
840
532
|
const v = data.verdict;
|
|
841
533
|
const rl = previewRateLine(data.rate_limit);
|
|
@@ -854,46 +546,34 @@ registerTool('x402_settlement_verdict_preview', "Free. TensorFeed's signed rulin
|
|
|
854
546
|
],
|
|
855
547
|
};
|
|
856
548
|
});
|
|
857
|
-
// ── Tool:
|
|
858
|
-
registerTool('
|
|
859
|
-
|
|
860
|
-
}, async ({ window }) => {
|
|
861
|
-
const params = new URLSearchParams();
|
|
862
|
-
if (window)
|
|
863
|
-
params.set('window', window);
|
|
864
|
-
const qs = params.toString();
|
|
865
|
-
const data = (await fetchJSON(`/premium/x402-settlement-verdict${qs ? `?${qs}` : ''}`, { auth: true }));
|
|
866
|
-
const v = data.verdict;
|
|
867
|
-
const e = data.ecosystem;
|
|
868
|
-
const ranking = (data.ranking ?? [])
|
|
869
|
-
.map((r) => ` #${r.rank} ${r.domain}: ${r.volume_usdc} USDC over ${r.count} settlements (${r.share_pct}% share)`)
|
|
870
|
-
.join('\n');
|
|
871
|
-
return {
|
|
872
|
-
content: [
|
|
873
|
-
{
|
|
874
|
-
type: 'text',
|
|
875
|
-
text: `x402 Settlement Verdict (${data.window_label ?? '7d'}): momentum ${v.momentum}, concentration ${v.concentration}` +
|
|
876
|
-
(v.leading_publisher ? `, leading publisher ${v.leading_publisher}` : ', no leading publisher this window') +
|
|
877
|
-
'\n' +
|
|
878
|
-
`Ecosystem: ${e.volume_usdc} USDC over ${e.count} settlements across ${e.unique_publishers} publishers` +
|
|
879
|
-
(e.top_publisher_share_pct !== null ? `, top share ${e.top_publisher_share_pct}%` : '') +
|
|
880
|
-
(e.hhi !== null ? `, HHI ${e.hhi}` : '') +
|
|
881
|
-
'\n\n' +
|
|
882
|
-
`Ranking:\n${ranking || ' (none)'}\n\n` +
|
|
883
|
-
`Claim: ${data.claim}` +
|
|
884
|
-
verdictBilling(data.billing),
|
|
885
|
-
},
|
|
886
|
-
],
|
|
887
|
-
};
|
|
888
|
-
});
|
|
889
|
-
// ── Tool: x402_publisher_verdict_preview (free, 10/IP/day) ───────────
|
|
890
|
-
registerTool('x402_publisher_verdict_preview', "Free. TensorFeed's signed trust verdict on one x402 publisher, over its OWN on-chain settlement index: whether a named publisher domain is actively settling, recently quiet, registered with no settlement, unreachable, missing a Base payTo, or not indexed. For the 30-day settlement momentum, the shared-wallet risk flag, the settlement evidence, and an AFTA-signed receipt, use x402_publisher_verdict. Requires a domain. 10 calls per day per IP.", {
|
|
549
|
+
// ── Tool: x402_publisher_verdict (preview free / full 1 credit) ──────
|
|
550
|
+
registerTool('x402_publisher_verdict', "TensorFeed's signed trust verdict on one x402 publisher, over its OWN on-chain settlement index: whether a named publisher domain is actively settling, recently quiet, registered with no settlement, unreachable, missing a Base payTo, or not indexed. Requires a domain. tier='preview' (default) is free (10 calls per day per IP), the trust verdict only. tier='full' costs 1 credit ($0.02), adds the 30-day settlement momentum, the shared-wallet risk flag, the settlement evidence (volume, count, last settled), and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
551
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds momentum, shared-wallet flag, evidence, signed receipt)."),
|
|
891
552
|
domain: z.string().describe('Publisher domain to rule on, e.g. "x402.tavily.com". Required.'),
|
|
892
|
-
}, async ({ domain }) => {
|
|
553
|
+
}, async ({ tier, domain }) => {
|
|
893
554
|
if (!domain || !domain.trim()) {
|
|
894
555
|
return { content: [{ type: 'text', text: 'Provide a publisher domain (e.g. domain "x402.tavily.com") to get a trust verdict.' }] };
|
|
895
556
|
}
|
|
896
557
|
const params = new URLSearchParams({ domain: domain.trim() });
|
|
558
|
+
if ((tier ?? 'preview') === 'full') {
|
|
559
|
+
const data = (await fetchJSON(`/premium/x402-publisher-verdict?${params}`, { auth: true }));
|
|
560
|
+
const t = data.trust;
|
|
561
|
+
const ev = data.evidence;
|
|
562
|
+
return {
|
|
563
|
+
content: [
|
|
564
|
+
{
|
|
565
|
+
type: 'text',
|
|
566
|
+
text: `x402 Publisher Verdict for ${data.domain}: ${data.verdict}\n` +
|
|
567
|
+
`Momentum: ${data.momentum}. Shared wallet: ${t.wallet_shared ? 'yes (risk flag)' : 'no'}` +
|
|
568
|
+
(t.last_settled ? `. Last settled ${t.last_settled}` : '') +
|
|
569
|
+
'\n' +
|
|
570
|
+
`Evidence (${ev.window_days}d): ${ev.volume_usdc} USDC over ${ev.count} settlements\n` +
|
|
571
|
+
`Claim: ${data.claim}` +
|
|
572
|
+
verdictBilling(data.billing),
|
|
573
|
+
},
|
|
574
|
+
],
|
|
575
|
+
};
|
|
576
|
+
}
|
|
897
577
|
const data = (await fetchJSON(`/preview/x402-publisher-verdict?${params}`));
|
|
898
578
|
const rl = previewRateLine(data.rate_limit);
|
|
899
579
|
return {
|
|
@@ -909,39 +589,38 @@ registerTool('x402_publisher_verdict_preview', "Free. TensorFeed's signed trust
|
|
|
909
589
|
],
|
|
910
590
|
};
|
|
911
591
|
});
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}, async ({
|
|
916
|
-
if (!
|
|
917
|
-
return { content: [{ type: 'text', text: 'Provide a
|
|
918
|
-
}
|
|
919
|
-
const params = new URLSearchParams({ domain: domain.trim() });
|
|
920
|
-
const data = (await fetchJSON(`/premium/x402-publisher-verdict?${params}`, { auth: true }));
|
|
921
|
-
const t = data.trust;
|
|
922
|
-
const ev = data.evidence;
|
|
923
|
-
return {
|
|
924
|
-
content: [
|
|
925
|
-
{
|
|
926
|
-
type: 'text',
|
|
927
|
-
text: `x402 Publisher Verdict for ${data.domain}: ${data.verdict}\n` +
|
|
928
|
-
`Momentum: ${data.momentum}. Shared wallet: ${t.wallet_shared ? 'yes (risk flag)' : 'no'}` +
|
|
929
|
-
(t.last_settled ? `. Last settled ${t.last_settled}` : '') +
|
|
930
|
-
'\n' +
|
|
931
|
-
`Evidence (${ev.window_days}d): ${ev.volume_usdc} USDC over ${ev.count} settlements\n` +
|
|
932
|
-
`Claim: ${data.claim}` +
|
|
933
|
-
verdictBilling(data.billing),
|
|
934
|
-
},
|
|
935
|
-
],
|
|
936
|
-
};
|
|
937
|
-
});
|
|
938
|
-
registerTool('stack_safety_verdict_preview', "Free. TensorFeed's deploy gate for an AI software stack: pass each package name (comma-separated name@version) and get the overall BLOCK / HOLD / PASS / UNKNOWN gate plus a per-package verdict, fusing the ingested AI-stack CVE batch with the CISA KEV catalog. Conservative by design: BLOCK only on an exploited CVE with no fix, HOLD when a known CVE applies and you must verify your version, PASS on no match, UNKNOWN outside the curated AI-stack cohort. For the matched-CVE evidence (ids, ranges, fixes, KEV status), up to 10 packages, and an AFTA-signed receipt, use stack_safety_verdict. Capped at 3 packages, 10 calls per day per IP.", {
|
|
939
|
-
packages: z.string().describe('Comma-separated AI-stack packages as name@version (e.g. "vllm@0.5.0,transformers@4.40.0"). Required. Preview caps at 3.'),
|
|
940
|
-
}, async ({ packages }) => {
|
|
941
|
-
if (!packages || !packages.trim()) {
|
|
942
|
-
return { content: [{ type: 'text', text: 'Provide packages as a comma-separated name@version list (e.g. "vllm@0.5.0,transformers@4.40.0") to get a deploy gate.' }] };
|
|
592
|
+
registerTool('stack_safety_verdict', "TensorFeed's deploy gate for an AI software stack: pass each package as comma-separated name@version and get the overall BLOCK / HOLD / PASS / UNKNOWN gate plus a per-package verdict, fusing the ingested AI-stack CVE batch with the CISA KEV catalog. Conservative by design: BLOCK only on an exploited CVE with no fix, HOLD when a known CVE applies and you must verify your version, PASS on no match, UNKNOWN outside the curated AI-stack cohort. tier='preview' (default) is free (10 calls per day per IP), caps at 3 packages, gate plus worst offender. tier='full' costs 1 credit ($0.02), raises the cap to 10 packages, adds the matched-CVE evidence (ids, affected ranges, fixed versions, KEV status) and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
593
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free, caps at 3 packages) or 'full' (1 credit; up to 10 packages, matched-CVE evidence, signed receipt)."),
|
|
594
|
+
packages: z.string().describe('Comma-separated AI-stack packages as name@version (e.g. "vllm@0.5.0,transformers@4.40.0"). Required. Preview caps at 3, full up to 10.'),
|
|
595
|
+
}, async ({ tier, packages }) => {
|
|
596
|
+
if (!packages || !packages.trim()) {
|
|
597
|
+
return { content: [{ type: 'text', text: 'Provide packages as a comma-separated name@version list (e.g. "vllm@0.5.0,transformers@4.40.0") to get a deploy gate.' }] };
|
|
943
598
|
}
|
|
944
599
|
const params = new URLSearchParams({ packages: packages.trim() });
|
|
600
|
+
if ((tier ?? 'preview') === 'full') {
|
|
601
|
+
const data = (await fetchJSON(`/premium/stack-safety-verdict?${params}`, { auth: true }));
|
|
602
|
+
const c = data.counts;
|
|
603
|
+
const lines = (data.packages ?? [])
|
|
604
|
+
.map((p) => {
|
|
605
|
+
const cves = p.matched_cves
|
|
606
|
+
.map((m) => `${m.cve_id}${m.on_kev ? ' (KEV)' : ''} ${m.severity_label}`)
|
|
607
|
+
.join('; ');
|
|
608
|
+
return ` ${p.package}${p.version ? `@${p.version}` : ''}: ${p.verdict}. ${p.reason}` + (cves ? `\n CVEs: ${cves}` : '');
|
|
609
|
+
})
|
|
610
|
+
.join('\n');
|
|
611
|
+
return {
|
|
612
|
+
content: [
|
|
613
|
+
{
|
|
614
|
+
type: 'text',
|
|
615
|
+
text: `Stack Safety Verdict: gate ${data.gate}\n` +
|
|
616
|
+
`Counts: ${c.block} block, ${c.hold} hold, ${c.pass} pass, ${c.unknown} unknown\n\n` +
|
|
617
|
+
`${lines || ' (no packages)'}\n\n` +
|
|
618
|
+
`Claim: ${data.claim}` +
|
|
619
|
+
verdictBilling(data.billing),
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
};
|
|
623
|
+
}
|
|
945
624
|
const data = (await fetchJSON(`/preview/stack-safety-verdict?${params}`));
|
|
946
625
|
const c = data.counts;
|
|
947
626
|
const worst = (data.packages ?? []).find((p) => p.verdict === 'BLOCK') ?? (data.packages ?? []).find((p) => p.verdict === 'HOLD') ?? null;
|
|
@@ -962,46 +641,39 @@ registerTool('stack_safety_verdict_preview', "Free. TensorFeed's deploy gate for
|
|
|
962
641
|
],
|
|
963
642
|
};
|
|
964
643
|
});
|
|
965
|
-
registerTool('
|
|
966
|
-
|
|
967
|
-
}, async ({ packages }) => {
|
|
968
|
-
if (!packages || !packages.trim()) {
|
|
969
|
-
return { content: [{ type: 'text', text: 'Provide packages as a comma-separated name@version list (e.g. "vllm@0.5.0,transformers@4.40.0") to get a signed deploy gate.' }] };
|
|
970
|
-
}
|
|
971
|
-
const params = new URLSearchParams({ packages: packages.trim() });
|
|
972
|
-
const data = (await fetchJSON(`/premium/stack-safety-verdict?${params}`, { auth: true }));
|
|
973
|
-
const c = data.counts;
|
|
974
|
-
const lines = (data.packages ?? [])
|
|
975
|
-
.map((p) => {
|
|
976
|
-
const cves = p.matched_cves
|
|
977
|
-
.map((m) => `${m.cve_id}${m.on_kev ? ' (KEV)' : ''} ${m.severity_label}`)
|
|
978
|
-
.join('; ');
|
|
979
|
-
return ` ${p.package}${p.version ? `@${p.version}` : ''}: ${p.verdict}. ${p.reason}` + (cves ? `\n CVEs: ${cves}` : '');
|
|
980
|
-
})
|
|
981
|
-
.join('\n');
|
|
982
|
-
return {
|
|
983
|
-
content: [
|
|
984
|
-
{
|
|
985
|
-
type: 'text',
|
|
986
|
-
text: `Stack Safety Verdict: gate ${data.gate}\n` +
|
|
987
|
-
`Counts: ${c.block} block, ${c.hold} hold, ${c.pass} pass, ${c.unknown} unknown\n\n` +
|
|
988
|
-
`${lines || ' (no packages)'}\n\n` +
|
|
989
|
-
`Claim: ${data.claim}` +
|
|
990
|
-
verdictBilling(data.billing),
|
|
991
|
-
},
|
|
992
|
-
],
|
|
993
|
-
};
|
|
994
|
-
});
|
|
995
|
-
registerTool('benchmark_trust_verdict_preview', "Free. TensorFeed's signed ruling on whether an AI benchmark is still a trustworthy capability signal or saturated, contaminated, or near ceiling so a high score should be down-weighted. Returns a trust band (reliable, use_with_caution, saturated, contaminated, deprecated) and a 0-100 trust score per benchmark. Pass benchmark to narrow to one, or category to filter, or neither for the registry. For the per-signal detail (ceiling proximity, frontier compression, contamination) and the down-weight recommendation with an alternative, use benchmark_trust_verdict. 10 calls per day per IP.", {
|
|
644
|
+
registerTool('benchmark_trust_verdict', "TensorFeed's signed ruling on whether an AI benchmark is still a trustworthy capability signal or saturated, contaminated, or near ceiling so a high score should be down-weighted: a trust band (reliable, use_with_caution, saturated, contaminated, deprecated) and a 0-100 trust score per benchmark. Pass benchmark to narrow to one, or category to filter, or neither for the registry. tier='preview' (default) is free (10 calls per day per IP), top verdict and bands only. tier='full' costs 1 credit ($0.02), adds the per-signal detail (ceiling proximity, frontier compression, contamination), a down-weight recommendation with an alternative benchmark, and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
645
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds per-signal detail, recommendation, signed receipt)."),
|
|
996
646
|
benchmark: z.string().optional().describe('Benchmark registry id or name to narrow to one (e.g. "mmlu", "swe-bench"). Optional.'),
|
|
997
647
|
category: z.string().optional().describe('Category to filter the benchmarks (e.g. "coding", "reasoning"). Optional.'),
|
|
998
|
-
}, async ({ benchmark, category }) => {
|
|
648
|
+
}, async ({ tier, benchmark, category }) => {
|
|
999
649
|
const params = new URLSearchParams();
|
|
1000
650
|
if (benchmark)
|
|
1001
651
|
params.set('benchmark', benchmark);
|
|
1002
652
|
if (category)
|
|
1003
653
|
params.set('category', category);
|
|
1004
654
|
const qs = params.toString();
|
|
655
|
+
if ((tier ?? 'preview') === 'full') {
|
|
656
|
+
const data = (await fetchJSON(`/premium/benchmark-trust-verdict${qs ? `?${qs}` : ''}`, { auth: true }));
|
|
657
|
+
const verdicts = data.verdicts ?? [];
|
|
658
|
+
if (verdicts.length === 0) {
|
|
659
|
+
return { content: [{ type: 'text', text: `No benchmark matched that filter.\n${data.claim ?? ''}` }] };
|
|
660
|
+
}
|
|
661
|
+
const lines = verdicts
|
|
662
|
+
.slice(0, 8)
|
|
663
|
+
.map((v) => ` ${v.name} (${v.category}): ${v.trust_band}, score ${v.trust_score}/100. Ceiling ${v.signals.ceiling_proximity}, frontier ${v.signals.frontier_compression}.\n ${v.recommendation}`)
|
|
664
|
+
.join('\n');
|
|
665
|
+
return {
|
|
666
|
+
content: [
|
|
667
|
+
{
|
|
668
|
+
type: 'text',
|
|
669
|
+
text: `Benchmark Trust Verdict: ${data.count} benchmark${data.count === 1 ? '' : 's'} ruled on\n\n` +
|
|
670
|
+
`${lines}\n\n` +
|
|
671
|
+
`Claim: ${data.claim}` +
|
|
672
|
+
verdictBilling(data.billing),
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
};
|
|
676
|
+
}
|
|
1005
677
|
const data = (await fetchJSON(`/preview/benchmark-trust-verdict${qs ? `?${qs}` : ''}`));
|
|
1006
678
|
const verdicts = data.verdicts ?? [];
|
|
1007
679
|
if (verdicts.length === 0) {
|
|
@@ -1025,47 +697,55 @@ registerTool('benchmark_trust_verdict_preview', "Free. TensorFeed's signed rulin
|
|
|
1025
697
|
],
|
|
1026
698
|
};
|
|
1027
699
|
});
|
|
1028
|
-
registerTool('
|
|
1029
|
-
|
|
1030
|
-
category: z.string().optional().describe('Category to filter the benchmarks (e.g. "coding", "reasoning"). Optional.'),
|
|
1031
|
-
}, async ({ benchmark, category }) => {
|
|
1032
|
-
const params = new URLSearchParams();
|
|
1033
|
-
if (benchmark)
|
|
1034
|
-
params.set('benchmark', benchmark);
|
|
1035
|
-
if (category)
|
|
1036
|
-
params.set('category', category);
|
|
1037
|
-
const qs = params.toString();
|
|
1038
|
-
const data = (await fetchJSON(`/premium/benchmark-trust-verdict${qs ? `?${qs}` : ''}`, { auth: true }));
|
|
1039
|
-
const verdicts = data.verdicts ?? [];
|
|
1040
|
-
if (verdicts.length === 0) {
|
|
1041
|
-
return { content: [{ type: 'text', text: `No benchmark matched that filter.\n${data.claim ?? ''}` }] };
|
|
1042
|
-
}
|
|
1043
|
-
const lines = verdicts
|
|
1044
|
-
.slice(0, 8)
|
|
1045
|
-
.map((v) => ` ${v.name} (${v.category}): ${v.trust_band}, score ${v.trust_score}/100. Ceiling ${v.signals.ceiling_proximity}, frontier ${v.signals.frontier_compression}.\n ${v.recommendation}`)
|
|
1046
|
-
.join('\n');
|
|
1047
|
-
return {
|
|
1048
|
-
content: [
|
|
1049
|
-
{
|
|
1050
|
-
type: 'text',
|
|
1051
|
-
text: `Benchmark Trust Verdict: ${data.count} benchmark${data.count === 1 ? '' : 's'} ruled on\n\n` +
|
|
1052
|
-
`${lines}\n\n` +
|
|
1053
|
-
`Claim: ${data.claim}` +
|
|
1054
|
-
verdictBilling(data.billing),
|
|
1055
|
-
},
|
|
1056
|
-
],
|
|
1057
|
-
};
|
|
1058
|
-
});
|
|
1059
|
-
registerTool('failover_verdict_preview', "Free. Provider A is degraded; TensorFeed's signed ruling on the single best operational provider to fail over to for a task right now. Confirms A against the live incident-triage feed, then runs the route-verdict fusion with A (and any provider already in failover) excluded. For the full failover candidate (pricing, measured latency, quality), the ranked alternatives, and an AFTA-signed receipt, use failover_verdict. Requires from. 10 calls per day per IP.", {
|
|
700
|
+
registerTool('failover_verdict', "Provider A is degraded; TensorFeed's signed ruling on the single best operational provider to fail over to for a task right now. Confirms A against the live incident-triage feed, then runs the route-verdict fusion with A (and any provider already in failover) excluded. Requires from. tier='preview' (default) is free (10 calls per day per IP), the failover target only. tier='full' costs 1 credit ($0.02), adds the full candidate (pricing, measured p95 latency, quality), the ranked alternatives, the confirmed incident on A, and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
701
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds candidate detail, ranked alternatives, incident, signed receipt)."),
|
|
1060
702
|
from: z.string().describe('The degraded provider to fail over FROM (e.g. "anthropic", "openai"). Required.'),
|
|
1061
703
|
task: z.enum(['code', 'reasoning', 'creative', 'general']).optional().describe('Task type to optimize the failover target for. Optional.'),
|
|
1062
|
-
}, async ({ from, task }) => {
|
|
704
|
+
}, async ({ tier, from, task }) => {
|
|
1063
705
|
if (!from || !from.trim()) {
|
|
1064
706
|
return { content: [{ type: 'text', text: 'Provide a from provider (the degraded provider to fail over from, e.g. from "anthropic") to get a failover verdict.' }] };
|
|
1065
707
|
}
|
|
1066
708
|
const params = new URLSearchParams({ from: from.trim() });
|
|
1067
709
|
if (task)
|
|
1068
710
|
params.set('task', task);
|
|
711
|
+
if ((tier ?? 'preview') === 'full') {
|
|
712
|
+
const data = (await fetchJSON(`/premium/failover-verdict?${params}`, { auth: true }));
|
|
713
|
+
const dest = data.failover_to;
|
|
714
|
+
const incidentLine = data.from.incident
|
|
715
|
+
? `Incident on ${data.from.provider}: ${data.from.incident.title} (action ${data.from.incident.recommended_action})\n`
|
|
716
|
+
: '';
|
|
717
|
+
if (!dest) {
|
|
718
|
+
return {
|
|
719
|
+
content: [
|
|
720
|
+
{
|
|
721
|
+
type: 'text',
|
|
722
|
+
text: `Failover Verdict from ${data.from.provider}: no operational failover target found.\n` +
|
|
723
|
+
incidentLine +
|
|
724
|
+
`Why: ${data.why}\nClaim: ${data.claim}` +
|
|
725
|
+
verdictBilling(data.billing),
|
|
726
|
+
},
|
|
727
|
+
],
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
const alts = (data.alternatives ?? [])
|
|
731
|
+
.map((a) => ` #${a.rank} ${a.model.name} (${a.model.provider}) blended $${a.pricing.blended}/1M, p95 ${a.latency.measured_p95_ms ?? 'n/a'}ms, status ${a.operational.status}`)
|
|
732
|
+
.join('\n');
|
|
733
|
+
return {
|
|
734
|
+
content: [
|
|
735
|
+
{
|
|
736
|
+
type: 'text',
|
|
737
|
+
text: `Failover Verdict from ${data.from.provider}: fail over to ${dest.model.name} (${dest.model.provider})\n` +
|
|
738
|
+
`Why: ${dest.why}\n` +
|
|
739
|
+
`Blended $${dest.pricing.blended}/1M, p95 ${dest.latency.measured_p95_ms ?? 'n/a'}ms, status ${dest.operational.status}\n` +
|
|
740
|
+
incidentLine +
|
|
741
|
+
`Excluded: ${data.excluded_providers.length ? data.excluded_providers.join(', ') : 'none'}\n\n` +
|
|
742
|
+
`Alternatives:\n${alts || ' (none)'}\n\n` +
|
|
743
|
+
`Claim: ${data.claim}` +
|
|
744
|
+
verdictBilling(data.billing),
|
|
745
|
+
},
|
|
746
|
+
],
|
|
747
|
+
};
|
|
748
|
+
}
|
|
1069
749
|
const data = (await fetchJSON(`/preview/failover-verdict?${params}`));
|
|
1070
750
|
const dest = data.failover_to;
|
|
1071
751
|
const rl = previewRateLine(data.rate_limit);
|
|
@@ -1096,61 +776,38 @@ registerTool('failover_verdict_preview', "Free. Provider A is degraded; TensorFe
|
|
|
1096
776
|
],
|
|
1097
777
|
};
|
|
1098
778
|
});
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
779
|
+
// ── Tool: ssvc_verdict (preview free / full 1 credit) ────────────────
|
|
780
|
+
registerTool('ssvc_verdict', "TensorFeed's signed SSVC patch-urgency decision for one CVE, applying the CISA SSVC Coordinator decision tree to the recorded Vulnrichment decision points (exploitation, automatable, technical impact). Requires a CVE id. tier='preview' (default) is free (10 calls per day per IP): the three decision points and the decision-tree provenance, WITHOUT the computed Act / Attend / Track / Track* decision. tier='full' costs 1 credit ($0.02), adds the computed decision across the full low/medium/high Mission and Well-being envelope, the per-level reasoning, a live CISA KEV cross-check, and an AFTA-signed receipt, and needs a TENSORFEED_TOKEN. Get credits at tensorfeed.ai/developers/agent-payments.", {
|
|
781
|
+
tier: z.enum(['preview', 'full']).optional().describe("'preview' (default, free) or 'full' (1 credit; adds the computed decision, envelope, reasoning, KEV cross-check, signed receipt)."),
|
|
782
|
+
cve: z.string().describe('CVE id to rule on, e.g. "CVE-2024-3094". Required.'),
|
|
783
|
+
}, async ({ tier, cve }) => {
|
|
784
|
+
if (!cve || !cve.trim()) {
|
|
785
|
+
return { content: [{ type: 'text', text: 'Provide a CVE id (e.g. cve "CVE-2024-3094") to get the SSVC decision points.' }] };
|
|
1105
786
|
}
|
|
1106
|
-
const params = new URLSearchParams({
|
|
1107
|
-
if (
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
787
|
+
const params = new URLSearchParams({ cve: cve.trim() });
|
|
788
|
+
if ((tier ?? 'preview') === 'full') {
|
|
789
|
+
const data = (await fetchJSON(`/premium/security/ssvc-verdict?${params}`, { auth: true }));
|
|
790
|
+
const dp = data.decision_points;
|
|
791
|
+
const env = data.decision_envelope;
|
|
792
|
+
const kev = data.kev_cross_check;
|
|
793
|
+
const kevLine = kev && kev.checked
|
|
794
|
+
? `KEV cross-check: ${kev.kev_listed ? 'listed on CISA KEV' : 'not on KEV'}${kev.flag && kev.flag !== 'none' ? ` (${kev.flag})` : ''}`
|
|
795
|
+
: 'KEV cross-check: unavailable';
|
|
1115
796
|
return {
|
|
1116
797
|
content: [
|
|
1117
798
|
{
|
|
1118
799
|
type: 'text',
|
|
1119
|
-
text: `
|
|
1120
|
-
|
|
1121
|
-
`
|
|
800
|
+
text: `SSVC Verdict for ${data.cve}: ${data.decision_primary} (Mission and Well-being assumed medium)\n` +
|
|
801
|
+
`Envelope: low ${env.low}, medium ${env.medium}, high ${env.high}\n` +
|
|
802
|
+
`Decision points: exploitation ${dp.exploitation}, automatable ${dp.automatable}, technical impact ${dp.technical_impact}\n` +
|
|
803
|
+
`${kevLine}\n` +
|
|
804
|
+
`Tree: ${data.tree.name} ${data.tree.version}` +
|
|
805
|
+
(data.scored_at ? `, scored ${data.scored_at}` : '') +
|
|
1122
806
|
verdictBilling(data.billing),
|
|
1123
807
|
},
|
|
1124
808
|
],
|
|
1125
809
|
};
|
|
1126
810
|
}
|
|
1127
|
-
const alts = (data.alternatives ?? [])
|
|
1128
|
-
.map((a) => ` #${a.rank} ${a.model.name} (${a.model.provider}) blended $${a.pricing.blended}/1M, p95 ${a.latency.measured_p95_ms ?? 'n/a'}ms, status ${a.operational.status}`)
|
|
1129
|
-
.join('\n');
|
|
1130
|
-
return {
|
|
1131
|
-
content: [
|
|
1132
|
-
{
|
|
1133
|
-
type: 'text',
|
|
1134
|
-
text: `Failover Verdict from ${data.from.provider}: fail over to ${dest.model.name} (${dest.model.provider})\n` +
|
|
1135
|
-
`Why: ${dest.why}\n` +
|
|
1136
|
-
`Blended $${dest.pricing.blended}/1M, p95 ${dest.latency.measured_p95_ms ?? 'n/a'}ms, status ${dest.operational.status}\n` +
|
|
1137
|
-
incidentLine +
|
|
1138
|
-
`Excluded: ${data.excluded_providers.length ? data.excluded_providers.join(', ') : 'none'}\n\n` +
|
|
1139
|
-
`Alternatives:\n${alts || ' (none)'}\n\n` +
|
|
1140
|
-
`Claim: ${data.claim}` +
|
|
1141
|
-
verdictBilling(data.billing),
|
|
1142
|
-
},
|
|
1143
|
-
],
|
|
1144
|
-
};
|
|
1145
|
-
});
|
|
1146
|
-
// ── Tool: ssvc_verdict_preview (free, 10/IP/day) ────────────────────
|
|
1147
|
-
registerTool('ssvc_verdict_preview', "Free. The CISA SSVC decision points for one CVE, read from CISA's Vulnrichment record: exploitation, automatable, and technical impact, plus the decision-tree provenance, WITHOUT the computed Act / Attend / Track / Track* decision (that is the premium ruling). For the computed SSVC decision across the full Mission and Well-being envelope, the per-level reasoning, the live KEV cross-check, and an AFTA-signed receipt, use ssvc_verdict. Requires a CVE id. 10 calls per day per IP.", {
|
|
1148
|
-
cve: z.string().describe('CVE id to rule on, e.g. "CVE-2024-3094". Required.'),
|
|
1149
|
-
}, async ({ cve }) => {
|
|
1150
|
-
if (!cve || !cve.trim()) {
|
|
1151
|
-
return { content: [{ type: 'text', text: 'Provide a CVE id (e.g. cve "CVE-2024-3094") to get the SSVC decision points.' }] };
|
|
1152
|
-
}
|
|
1153
|
-
const params = new URLSearchParams({ cve: cve.trim() });
|
|
1154
811
|
const data = (await fetchJSON(`/preview/security/ssvc-verdict?${params}`));
|
|
1155
812
|
const dp = data.decision_points;
|
|
1156
813
|
const rl = previewRateLine(data.rate_limit);
|
|
@@ -1170,44 +827,37 @@ registerTool('ssvc_verdict_preview', "Free. The CISA SSVC decision points for on
|
|
|
1170
827
|
],
|
|
1171
828
|
};
|
|
1172
829
|
});
|
|
1173
|
-
//
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
const kevLine = kev && kev.checked
|
|
1186
|
-
? `KEV cross-check: ${kev.kev_listed ? 'listed on CISA KEV' : 'not on KEV'}${kev.flag && kev.flag !== 'none' ? ` (${kev.flag})` : ''}`
|
|
1187
|
-
: 'KEV cross-check: unavailable';
|
|
1188
|
-
return {
|
|
1189
|
-
content: [
|
|
1190
|
-
{
|
|
1191
|
-
type: 'text',
|
|
1192
|
-
text: `SSVC Verdict for ${data.cve}: ${data.decision_primary} (Mission and Well-being assumed medium)\n` +
|
|
1193
|
-
`Envelope: low ${env.low}, medium ${env.medium}, high ${env.high}\n` +
|
|
1194
|
-
`Decision points: exploitation ${dp.exploitation}, automatable ${dp.automatable}, technical impact ${dp.technical_impact}\n` +
|
|
1195
|
-
`${kevLine}\n` +
|
|
1196
|
-
`Tree: ${data.tree.name} ${data.tree.version}` +
|
|
1197
|
-
(data.scored_at ? `, scored ${data.scored_at}` : '') +
|
|
1198
|
-
verdictBilling(data.billing),
|
|
1199
|
-
},
|
|
1200
|
-
],
|
|
1201
|
-
};
|
|
1202
|
-
});
|
|
1203
|
-
// ── Tool: pricing_series_free (free, 7-day cap) ─────────────────────
|
|
1204
|
-
registerTool('pricing_series_free', 'Daily price points for one AI model over the last 1 to 7 days, free. For windows up to 90 days use pricing_series (1 credit).', {
|
|
1205
|
-
model: z.string().describe('Model id or display name (e.g. "Claude Opus 4.7" or "claude-opus-4-7")'),
|
|
1206
|
-
days: z.number().min(1).max(7).optional().describe('Rolling window length 1 to 7 days (default 7)'),
|
|
830
|
+
// Series window helper: convert a requested day count into a `from` date
|
|
831
|
+
// (today minus days-1, UTC) for the premium date-range endpoints, which
|
|
832
|
+
// take from/to rather than a days shorthand.
|
|
833
|
+
function seriesFromDate(days) {
|
|
834
|
+
const d = new Date();
|
|
835
|
+
d.setUTCDate(d.getUTCDate() - (days - 1));
|
|
836
|
+
return d.toISOString().slice(0, 10);
|
|
837
|
+
}
|
|
838
|
+
// ── Tool: pricing_series (free 1 to 7 days, paid 8 to 90 days) ──────
|
|
839
|
+
registerTool('pricing_series', 'Daily price points for one AI model over a window. days 1 to 7 is free; days 8 to 90 costs 1 credit ($0.02) and needs a TENSORFEED_TOKEN, adding the min/max/delta summary over the longer window. Get credits at tensorfeed.ai/developers/agent-payments.', {
|
|
840
|
+
model: z.string().describe('Model id or display name (e.g. "Claude Opus 4.7" or "claude-opus-4-7").'),
|
|
841
|
+
days: z.number().int().min(1).max(90).optional().describe('Window length (default 7). 1 to 7 free; 8 to 90 costs 1 credit.'),
|
|
1207
842
|
}, async ({ model, days }) => {
|
|
1208
|
-
const
|
|
1209
|
-
if (
|
|
1210
|
-
params
|
|
843
|
+
const d = days ?? 7;
|
|
844
|
+
if (d > 7) {
|
|
845
|
+
const params = new URLSearchParams({ model, from: seriesFromDate(d) });
|
|
846
|
+
const data = (await fetchJSON(`/premium/history/pricing/series?${params}`, { auth: true }));
|
|
847
|
+
const s = data.summary;
|
|
848
|
+
const summary = s.first && s.latest
|
|
849
|
+
? `${s.first.date} blended $${s.first.blended} -> ${s.latest.date} blended $${s.latest.blended} (${s.delta_pct_blended}%, ${s.changes_detected} changes, min $${s.min_blended}, max $${s.max_blended})`
|
|
850
|
+
: 'no data points in range';
|
|
851
|
+
return {
|
|
852
|
+
content: [
|
|
853
|
+
{
|
|
854
|
+
type: 'text',
|
|
855
|
+
text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
856
|
+
},
|
|
857
|
+
],
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
const params = new URLSearchParams({ model, days: String(d) });
|
|
1211
861
|
const data = (await fetchJSON(`/history/pricing/series?${params}`));
|
|
1212
862
|
const s = data.summary;
|
|
1213
863
|
const summary = s.first && s.latest
|
|
@@ -1217,20 +867,35 @@ registerTool('pricing_series_free', 'Daily price points for one AI model over th
|
|
|
1217
867
|
content: [
|
|
1218
868
|
{
|
|
1219
869
|
type: 'text',
|
|
1220
|
-
text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points over ${data.points.length} days\n${summary}\nFor up to 90 days,
|
|
870
|
+
text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points over ${data.points.length} days\n${summary}\nFor up to 90 days, pass days 8 to 90 (1 credit).`,
|
|
1221
871
|
},
|
|
1222
872
|
],
|
|
1223
873
|
};
|
|
1224
874
|
});
|
|
1225
|
-
// ── Tool:
|
|
1226
|
-
registerTool('
|
|
1227
|
-
model: z.string().describe('Model id or display name'),
|
|
1228
|
-
benchmark: z.string().describe('Benchmark key'),
|
|
1229
|
-
days: z.number().min(1).max(
|
|
875
|
+
// ── Tool: benchmark_series (free 1 to 7 days, paid 8 to 90 days) ────
|
|
876
|
+
registerTool('benchmark_series', 'Daily benchmark scores for one model+benchmark over a window. Benchmark keys: swe_bench, mmlu_pro, gpqa_diamond, math, human_eval. days 1 to 7 is free; days 8 to 90 costs 1 credit ($0.02) and needs a TENSORFEED_TOKEN, tracking score evolution over the longer window. Get credits at tensorfeed.ai/developers/agent-payments.', {
|
|
877
|
+
model: z.string().describe('Model id or display name.'),
|
|
878
|
+
benchmark: z.string().describe('Benchmark key (e.g. swe_bench, mmlu_pro, gpqa_diamond, math, human_eval).'),
|
|
879
|
+
days: z.number().int().min(1).max(90).optional().describe('Window length (default 7). 1 to 7 free; 8 to 90 costs 1 credit.'),
|
|
1230
880
|
}, async ({ model, benchmark, days }) => {
|
|
1231
|
-
const
|
|
1232
|
-
if (
|
|
1233
|
-
params
|
|
881
|
+
const d = days ?? 7;
|
|
882
|
+
if (d > 7) {
|
|
883
|
+
const params = new URLSearchParams({ model, benchmark, from: seriesFromDate(d) });
|
|
884
|
+
const data = (await fetchJSON(`/premium/history/benchmarks/series?${params}`, { auth: true }));
|
|
885
|
+
const s = data.summary;
|
|
886
|
+
const summary = s.first && s.latest
|
|
887
|
+
? `${s.first.date} score ${s.first.score} -> ${s.latest.date} score ${s.latest.score} (delta ${s.delta_pp} pp)`
|
|
888
|
+
: 'no data in range';
|
|
889
|
+
return {
|
|
890
|
+
content: [
|
|
891
|
+
{
|
|
892
|
+
type: 'text',
|
|
893
|
+
text: `${data.model} on ${data.benchmark}: ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
894
|
+
},
|
|
895
|
+
],
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
const params = new URLSearchParams({ model, benchmark, days: String(d) });
|
|
1234
899
|
const data = (await fetchJSON(`/history/benchmarks/series?${params}`));
|
|
1235
900
|
const s = data.summary;
|
|
1236
901
|
const summary = s.first && s.latest
|
|
@@ -1245,14 +910,31 @@ registerTool('benchmark_series_free', 'Daily benchmark scores for one model+benc
|
|
|
1245
910
|
],
|
|
1246
911
|
};
|
|
1247
912
|
});
|
|
1248
|
-
// ── Tool:
|
|
1249
|
-
registerTool('
|
|
1250
|
-
provider: z.string().describe('Provider name (e.g. anthropic, openai, google)'),
|
|
1251
|
-
days: z.number().min(1).max(
|
|
913
|
+
// ── Tool: status_uptime (free 1 to 7 days, paid 8 to 90 days) ───────
|
|
914
|
+
registerTool('status_uptime', 'Daily uptime rollup for one provider over a window with operational/degraded/down day counts and uptime % (degraded counts as half-credit). days 1 to 7 is free; days 8 to 90 costs 1 credit ($0.02) and needs a TENSORFEED_TOKEN, adding per-incident-day detail over the longer window. Get credits at tensorfeed.ai/developers/agent-payments.', {
|
|
915
|
+
provider: z.string().describe('Provider name (e.g. anthropic, openai, google).'),
|
|
916
|
+
days: z.number().int().min(1).max(90).optional().describe('Window length (default 7). 1 to 7 free; 8 to 90 costs 1 credit.'),
|
|
1252
917
|
}, async ({ provider, days }) => {
|
|
1253
|
-
const
|
|
1254
|
-
if (
|
|
1255
|
-
params
|
|
918
|
+
const d = days ?? 7;
|
|
919
|
+
if (d > 7) {
|
|
920
|
+
const params = new URLSearchParams({ provider, from: seriesFromDate(d) });
|
|
921
|
+
const data = (await fetchJSON(`/premium/history/status/uptime?${params}`, { auth: true }));
|
|
922
|
+
const incidents = data.incident_days.length
|
|
923
|
+
? '\n\nIncident days:\n' + data.incident_days.map(d2 => ` ${d2.date}: ${d2.status}`).join('\n')
|
|
924
|
+
: '';
|
|
925
|
+
return {
|
|
926
|
+
content: [
|
|
927
|
+
{
|
|
928
|
+
type: 'text',
|
|
929
|
+
text: `${data.provider} uptime: ${data.uptime_pct ?? 'n/a'}% over ${data.days_with_data} measured days (of ${data.days_total} in range)\n` +
|
|
930
|
+
` operational: ${data.days_operational}, degraded: ${data.days_degraded}, down: ${data.days_down}` +
|
|
931
|
+
incidents +
|
|
932
|
+
`\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
933
|
+
},
|
|
934
|
+
],
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
const params = new URLSearchParams({ provider, days: String(d) });
|
|
1256
938
|
const data = (await fetchJSON(`/history/status/uptime?${params}`));
|
|
1257
939
|
return {
|
|
1258
940
|
content: [
|
|
@@ -1263,15 +945,31 @@ registerTool('status_uptime_free', 'Daily uptime rollup for one provider over th
|
|
|
1263
945
|
],
|
|
1264
946
|
};
|
|
1265
947
|
});
|
|
1266
|
-
// ── Tool:
|
|
1267
|
-
registerTool('
|
|
1268
|
-
days: z.number().min(1).max(
|
|
948
|
+
// ── Tool: status_leaderboard (free 1 to 7 days, paid 8 to 90 days) ──
|
|
949
|
+
registerTool('status_leaderboard', 'Cross-provider uptime leaderboard ranked by uptime % DESC, computed from minute-resolution counters (~720 samples per provider per day). days 1 to 7 is free; days 8 to 90 costs 3 credits ($0.06) and needs a TENSORFEED_TOKEN, adding incident_count and mttr_minutes (mean time to recover) per provider over the longer window. Get credits at tensorfeed.ai/developers/agent-payments.', {
|
|
950
|
+
days: z.number().int().min(1).max(90).optional().describe('Window length (default 7). 1 to 7 free; 8 to 90 costs 3 credits.'),
|
|
1269
951
|
}, async ({ days }) => {
|
|
1270
|
-
const
|
|
1271
|
-
if (
|
|
1272
|
-
params
|
|
1273
|
-
|
|
1274
|
-
|
|
952
|
+
const d = days ?? 7;
|
|
953
|
+
if (d > 7) {
|
|
954
|
+
const params = new URLSearchParams({ from: seriesFromDate(d) });
|
|
955
|
+
const data = (await fetchJSON(`/premium/status/leaderboard?${params}`, { auth: true }));
|
|
956
|
+
if (!data.ok) {
|
|
957
|
+
return {
|
|
958
|
+
content: [{ type: 'text', text: `Leaderboard unavailable: ${data.error ?? 'unknown'} - ${data.message ?? ''}` }],
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
const lines = (data.entries ?? []).map((e) => `#${e.rank} ${e.provider}: ${e.uptime_pct}% uptime, ${e.downtime_minutes}m downtime (${e.hard_down_minutes}m hard-down), ${e.incident_count ?? 0} incidents, MTTR ${e.mttr_minutes ?? 'n/a'}m`);
|
|
962
|
+
return {
|
|
963
|
+
content: [
|
|
964
|
+
{
|
|
965
|
+
type: 'text',
|
|
966
|
+
text: `AI provider uptime leaderboard ${data.range?.from} to ${data.range?.to}\n${lines.join('\n')}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
967
|
+
},
|
|
968
|
+
],
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
const params = new URLSearchParams({ days: String(d) });
|
|
972
|
+
const data = (await fetchJSON(`/status/leaderboard?${params}`));
|
|
1275
973
|
if (!data.ok) {
|
|
1276
974
|
return {
|
|
1277
975
|
content: [{ type: 'text', text: `Leaderboard unavailable: ${data.error ?? 'unknown'} - ${data.message ?? ''}` }],
|
|
@@ -1289,145 +987,8 @@ registerTool('status_leaderboard_free', 'Cross-provider uptime leaderboard, rank
|
|
|
1289
987
|
],
|
|
1290
988
|
};
|
|
1291
989
|
});
|
|
1292
|
-
// ── Tool: pricing_series (1 credit) ─────────────────────────────────
|
|
1293
|
-
registerTool('pricing_series', 'Daily price points for one AI model with min/max/delta summary. Default range = last 30 days, max 90 days. Costs 1 credit ($0.02). Strict premium, no free trial; the 7-day-capped sibling pricing_series_free is the discovery option.', {
|
|
1294
|
-
model: z.string().describe('Model id or display name (e.g. "Claude Opus 4.7" or "claude-opus-4-7")'),
|
|
1295
|
-
from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
|
|
1296
|
-
to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
|
|
1297
|
-
}, async ({ model, from, to }) => {
|
|
1298
|
-
const params = new URLSearchParams({ model });
|
|
1299
|
-
if (from)
|
|
1300
|
-
params.set('from', from);
|
|
1301
|
-
if (to)
|
|
1302
|
-
params.set('to', to);
|
|
1303
|
-
const data = (await fetchJSON(`/premium/history/pricing/series?${params}`, { auth: true }));
|
|
1304
|
-
const s = data.summary;
|
|
1305
|
-
const summary = s.first && s.latest
|
|
1306
|
-
? `${s.first.date} blended $${s.first.blended} -> ${s.latest.date} blended $${s.latest.blended} (${s.delta_pct_blended}%, ${s.changes_detected} changes, min $${s.min_blended}, max $${s.max_blended})`
|
|
1307
|
-
: 'no data points in range';
|
|
1308
|
-
return {
|
|
1309
|
-
content: [
|
|
1310
|
-
{
|
|
1311
|
-
type: 'text',
|
|
1312
|
-
text: `${data.model} (${data.provider ?? 'unknown'}) ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1313
|
-
},
|
|
1314
|
-
],
|
|
1315
|
-
};
|
|
1316
|
-
});
|
|
1317
|
-
// ── Tool: benchmark_series (1 credit) ───────────────────────────────
|
|
1318
|
-
registerTool('benchmark_series', 'Score evolution for a single benchmark on one AI model. Costs 1 credit ($0.02). Strict premium, no free trial; the 7-day-capped sibling benchmark_series_free is the discovery option. Benchmark keys: swe_bench, mmlu_pro, gpqa_diamond, math, human_eval.', {
|
|
1319
|
-
model: z.string().describe('Model id or display name'),
|
|
1320
|
-
benchmark: z.string().describe('Benchmark key (e.g. swe_bench, mmlu_pro, gpqa_diamond, math, human_eval)'),
|
|
1321
|
-
from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
|
|
1322
|
-
to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
|
|
1323
|
-
}, async ({ model, benchmark, from, to }) => {
|
|
1324
|
-
const params = new URLSearchParams({ model, benchmark });
|
|
1325
|
-
if (from)
|
|
1326
|
-
params.set('from', from);
|
|
1327
|
-
if (to)
|
|
1328
|
-
params.set('to', to);
|
|
1329
|
-
const data = (await fetchJSON(`/premium/history/benchmarks/series?${params}`, { auth: true }));
|
|
1330
|
-
const s = data.summary;
|
|
1331
|
-
const summary = s.first && s.latest
|
|
1332
|
-
? `${s.first.date} score ${s.first.score} -> ${s.latest.date} score ${s.latest.score} (delta ${s.delta_pp} pp)`
|
|
1333
|
-
: 'no data in range';
|
|
1334
|
-
return {
|
|
1335
|
-
content: [
|
|
1336
|
-
{
|
|
1337
|
-
type: 'text',
|
|
1338
|
-
text: `${data.model} on ${data.benchmark}: ${data.points.length} points\n${summary}\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1339
|
-
},
|
|
1340
|
-
],
|
|
1341
|
-
};
|
|
1342
|
-
});
|
|
1343
|
-
// ── Tool: status_uptime (1 credit) ──────────────────────────────────
|
|
1344
|
-
registerTool('status_uptime', 'Daily uptime rollup for one provider with operational/degraded/down day counts and uptime % (degraded counts as half-credit). Costs 1 credit ($0.02). Strict premium, no free trial; the 7-day-capped sibling status_uptime_free is the discovery option.', {
|
|
1345
|
-
provider: z.string().describe('Provider name (e.g. anthropic, openai, google)'),
|
|
1346
|
-
from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
|
|
1347
|
-
to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
|
|
1348
|
-
}, async ({ provider, from, to }) => {
|
|
1349
|
-
const params = new URLSearchParams({ provider });
|
|
1350
|
-
if (from)
|
|
1351
|
-
params.set('from', from);
|
|
1352
|
-
if (to)
|
|
1353
|
-
params.set('to', to);
|
|
1354
|
-
const data = (await fetchJSON(`/premium/history/status/uptime?${params}`, { auth: true }));
|
|
1355
|
-
const incidents = data.incident_days.length
|
|
1356
|
-
? '\n\nIncident days:\n' + data.incident_days.map(d => ` ${d.date}: ${d.status}`).join('\n')
|
|
1357
|
-
: '';
|
|
1358
|
-
return {
|
|
1359
|
-
content: [
|
|
1360
|
-
{
|
|
1361
|
-
type: 'text',
|
|
1362
|
-
text: `${data.provider} uptime: ${data.uptime_pct ?? 'n/a'}% over ${data.days_with_data} measured days (of ${data.days_total} in range)\n` +
|
|
1363
|
-
` operational: ${data.days_operational}, degraded: ${data.days_degraded}, down: ${data.days_down}` +
|
|
1364
|
-
incidents +
|
|
1365
|
-
`\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1366
|
-
},
|
|
1367
|
-
],
|
|
1368
|
-
};
|
|
1369
|
-
});
|
|
1370
|
-
// ── Tool: status_leaderboard (1 credit) ─────────────────────────────
|
|
1371
|
-
registerTool('status_leaderboard', 'Cross-provider uptime leaderboard for a custom date range up to 90 days. Same minute-resolution counter source as status_leaderboard_free, but adds incident_count and mttr_minutes (mean time to recover) per provider. Sorted by uptime % DESC. Costs 3 credits ($0.06). Strict premium, no free trial; the 7-day-capped sibling status_leaderboard_free is the discovery option.', {
|
|
1372
|
-
from: z.string().optional().describe('Start date YYYY-MM-DD UTC (default: 30 days ago)'),
|
|
1373
|
-
to: z.string().optional().describe('End date YYYY-MM-DD UTC (default: today)'),
|
|
1374
|
-
}, async ({ from, to }) => {
|
|
1375
|
-
const params = new URLSearchParams();
|
|
1376
|
-
if (from)
|
|
1377
|
-
params.set('from', from);
|
|
1378
|
-
if (to)
|
|
1379
|
-
params.set('to', to);
|
|
1380
|
-
const data = (await fetchJSON(`/premium/status/leaderboard?${params}`, { auth: true }));
|
|
1381
|
-
if (!data.ok) {
|
|
1382
|
-
return {
|
|
1383
|
-
content: [{ type: 'text', text: `Leaderboard unavailable: ${data.error ?? 'unknown'} - ${data.message ?? ''}` }],
|
|
1384
|
-
};
|
|
1385
|
-
}
|
|
1386
|
-
const lines = (data.entries ?? []).map((e) => `#${e.rank} ${e.provider}: ${e.uptime_pct}% uptime, ${e.downtime_minutes}m downtime (${e.hard_down_minutes}m hard-down), ${e.incident_count ?? 0} incidents, MTTR ${e.mttr_minutes ?? 'n/a'}m`);
|
|
1387
|
-
return {
|
|
1388
|
-
content: [
|
|
1389
|
-
{
|
|
1390
|
-
type: 'text',
|
|
1391
|
-
text: `AI provider uptime leaderboard ${data.range?.from} to ${data.range?.to}\n${lines.join('\n')}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1392
|
-
},
|
|
1393
|
-
],
|
|
1394
|
-
};
|
|
1395
|
-
});
|
|
1396
|
-
// ── Tool: premium_agents_directory (1 credit) ───────────────────────
|
|
1397
|
-
registerTool('premium_agents_directory', 'Enriched AI agents catalog joined with live status, recent news, agent traffic, flagship pricing, and a 0-100 trending_score. Costs 1 credit ($0.02).', {
|
|
1398
|
-
category: z.string().optional().describe('coding, research, general, creative, frameworks'),
|
|
1399
|
-
status: z.enum(['operational', 'degraded', 'down', 'unknown']).optional(),
|
|
1400
|
-
open_source: z.boolean().optional(),
|
|
1401
|
-
sort: z.enum(['trending', 'alphabetical', 'status', 'price_low', 'price_high', 'news_count']).optional(),
|
|
1402
|
-
limit: z.number().min(1).max(100).optional(),
|
|
1403
|
-
}, async ({ category, status, open_source, sort, limit }) => {
|
|
1404
|
-
const params = new URLSearchParams();
|
|
1405
|
-
if (category)
|
|
1406
|
-
params.set('category', category);
|
|
1407
|
-
if (status)
|
|
1408
|
-
params.set('status', status);
|
|
1409
|
-
if (typeof open_source === 'boolean')
|
|
1410
|
-
params.set('open_source', String(open_source));
|
|
1411
|
-
if (sort)
|
|
1412
|
-
params.set('sort', sort);
|
|
1413
|
-
if (typeof limit === 'number')
|
|
1414
|
-
params.set('limit', String(limit));
|
|
1415
|
-
const data = (await fetchJSON(`/premium/agents/directory?${params}`, { auth: true }));
|
|
1416
|
-
const list = data.agents
|
|
1417
|
-
.map(a => ` ${a.name} (${a.provider}) [${a.category}] status=${a.live_status} score=${a.trending_score} news=${a.recent_news_count}` +
|
|
1418
|
-
(a.flagship_pricing ? ` flagship=${a.flagship_pricing.model} $${a.flagship_pricing.blended}/1M` : ''))
|
|
1419
|
-
.join('\n');
|
|
1420
|
-
return {
|
|
1421
|
-
content: [
|
|
1422
|
-
{
|
|
1423
|
-
type: 'text',
|
|
1424
|
-
text: `Agents (sort: ${data.sort}, ${data.returned} of ${data.total}):\n\n${list}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1425
|
-
},
|
|
1426
|
-
],
|
|
1427
|
-
};
|
|
1428
|
-
});
|
|
1429
990
|
// ── Tool: whats_new (1 credit) ──────────────────────────────────────
|
|
1430
|
-
registerTool('whats_new', '
|
|
991
|
+
registerTool('whats_new', 'Catch up on everything that changed in AI in one call: pricing moves, new and removed models, status incidents, and top news from the last 1 to 7 days, so your agent boots with current context instead of stale assumptions. Costs 1 credit ($0.02).', {
|
|
1431
992
|
days: z.number().min(1).max(7).optional().describe('Window length in days (default 1)'),
|
|
1432
993
|
news_limit: z.number().min(1).max(25).optional().describe('Max news headlines (default 10)'),
|
|
1433
994
|
}, async ({ days, news_limit }) => {
|
|
@@ -1463,7 +1024,7 @@ registerTool('whats_new', 'Agent morning brief: pricing changes, new/removed mod
|
|
|
1463
1024
|
};
|
|
1464
1025
|
});
|
|
1465
1026
|
// ── Tool: compare_models (1 credit) ─────────────────────────────────
|
|
1466
|
-
registerTool('compare_models', '
|
|
1027
|
+
registerTool('compare_models', 'Pick between models in one call: pricing, benchmarks, status, and recent news for 2 to 5 models side by side, with cheapest-blended and per-benchmark rankings, so you choose without scraping each provider. Costs 1 credit ($0.02).', {
|
|
1467
1028
|
ids: z.string().describe('Comma-separated list of 2-5 model ids or display names. Examples: "Claude Opus 4.7,GPT-5.5,Gemini 3" or "opus-4-7,gpt-5-5"'),
|
|
1468
1029
|
}, async ({ ids }) => {
|
|
1469
1030
|
const data = (await fetchJSON(`/premium/compare/models?ids=${encodeURIComponent(ids)}`, { auth: true }));
|
|
@@ -1490,7 +1051,7 @@ registerTool('compare_models', 'Side-by-side comparison of 2-5 AI models. Return
|
|
|
1490
1051
|
};
|
|
1491
1052
|
});
|
|
1492
1053
|
// ── Tool: provider_deepdive (1 credit) ──────────────────────────────
|
|
1493
|
-
registerTool('provider_deepdive', 'Everything about
|
|
1054
|
+
registerTool('provider_deepdive', 'Everything about one AI provider in a single call: live status, every model with pricing, tier, and benchmarks joined in, recent news, and agent traffic, replacing about four separate lookups. Costs 3 credits ($0.06). Strict premium, no free trial.', {
|
|
1494
1055
|
provider: z.string().describe('Provider id or display name (case-insensitive). Examples: anthropic, openai, google, mistral, cohere'),
|
|
1495
1056
|
}, async ({ provider }) => {
|
|
1496
1057
|
const data = (await fetchJSON(`/premium/providers/${encodeURIComponent(provider)}`, { auth: true }));
|
|
@@ -1519,84 +1080,11 @@ registerTool('provider_deepdive', 'Everything about an AI provider in one call:
|
|
|
1519
1080
|
],
|
|
1520
1081
|
};
|
|
1521
1082
|
});
|
|
1522
|
-
// ── Tool:
|
|
1523
|
-
registerTool('
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
horizon: z.enum(['daily', 'weekly', 'monthly', 'yearly']).optional().describe('Primary horizon to highlight (default monthly). All four are always computed.'),
|
|
1528
|
-
}, async ({ models, input_tokens_per_day, output_tokens_per_day, horizon }) => {
|
|
1529
|
-
const params = new URLSearchParams({
|
|
1530
|
-
model: models,
|
|
1531
|
-
input_tokens_per_day: String(input_tokens_per_day),
|
|
1532
|
-
output_tokens_per_day: String(output_tokens_per_day),
|
|
1533
|
-
});
|
|
1534
|
-
if (horizon)
|
|
1535
|
-
params.set('horizon', horizon);
|
|
1536
|
-
const data = (await fetchJSON(`/premium/cost/projection?${params}`, { auth: true }));
|
|
1537
|
-
const lines = data.projections.map(p => {
|
|
1538
|
-
if (!p.matched)
|
|
1539
|
-
return ` ${p.model}: not found in pricing catalog`;
|
|
1540
|
-
return ` ${p.model} (${p.provider}): $${p.daily.total}/day, $${p.weekly_total}/wk, $${p.monthly_total}/mo, $${p.yearly_total}/yr (in $${p.rates.input_per_1m}/1M, out $${p.rates.output_per_1m}/1M)`;
|
|
1541
|
-
}).join('\n');
|
|
1542
|
-
const ranked = data.ranked_cheapest_monthly.length
|
|
1543
|
-
? '\n\nCheapest monthly:\n' +
|
|
1544
|
-
data.ranked_cheapest_monthly
|
|
1545
|
-
.map((r, i) => ` #${i + 1} ${r.model} (${r.provider}): $${r.monthly_total}/mo`)
|
|
1546
|
-
.join('\n')
|
|
1547
|
-
: '';
|
|
1548
|
-
return {
|
|
1549
|
-
content: [
|
|
1550
|
-
{
|
|
1551
|
-
type: 'text',
|
|
1552
|
-
text: `Workload: ${data.workload.input_tokens_per_day} input + ${data.workload.output_tokens_per_day} output tokens/day\n\n${lines}${ranked}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1553
|
-
},
|
|
1554
|
-
],
|
|
1555
|
-
};
|
|
1556
|
-
});
|
|
1557
|
-
// ── Tool: news_search (1 credit) ────────────────────────────────────
|
|
1558
|
-
registerTool('news_search', 'Full-text search over the TensorFeed news article corpus with optional date range, provider, and category filters. Relevance scoring with recency boost. Costs 1 credit ($0.02).', {
|
|
1559
|
-
q: z.string().optional().describe('Free-text query, e.g. "claude opus pricing". Omit to browse latest filtered articles.'),
|
|
1560
|
-
from: z.string().optional().describe('Start date YYYY-MM-DD UTC (inclusive)'),
|
|
1561
|
-
to: z.string().optional().describe('End date YYYY-MM-DD UTC (inclusive end-of-day)'),
|
|
1562
|
-
provider: z.string().optional().describe('Substring match against source name and domain (e.g. "anthropic", "openai", "techcrunch")'),
|
|
1563
|
-
category: z.string().optional().describe('Substring match against article categories'),
|
|
1564
|
-
limit: z.number().min(1).max(100).optional().describe('Max results (default 25, max 100)'),
|
|
1565
|
-
}, async ({ q, from, to, provider, category, limit }) => {
|
|
1566
|
-
const params = new URLSearchParams();
|
|
1567
|
-
if (q)
|
|
1568
|
-
params.set('q', q);
|
|
1569
|
-
if (from)
|
|
1570
|
-
params.set('from', from);
|
|
1571
|
-
if (to)
|
|
1572
|
-
params.set('to', to);
|
|
1573
|
-
if (provider)
|
|
1574
|
-
params.set('provider', provider);
|
|
1575
|
-
if (category)
|
|
1576
|
-
params.set('category', category);
|
|
1577
|
-
if (typeof limit === 'number')
|
|
1578
|
-
params.set('limit', String(limit));
|
|
1579
|
-
const data = (await fetchJSON(`/premium/news/search?${params}`, { auth: true }));
|
|
1580
|
-
if (data.results.length === 0) {
|
|
1581
|
-
return { content: [{ type: 'text', text: `No articles matched. (matched: ${data.matched})` }] };
|
|
1582
|
-
}
|
|
1583
|
-
const list = data.results
|
|
1584
|
-
.map((r, i) => `${i + 1}. ${r.title} (${r.source})\n ${r.url}\n ${r.published_at} | relevance ${r.relevance}${r.matched_terms.length ? ` | terms: ${r.matched_terms.join(', ')}` : ''}\n ${r.snippet}`)
|
|
1585
|
-
.join('\n\n');
|
|
1586
|
-
return {
|
|
1587
|
-
content: [
|
|
1588
|
-
{
|
|
1589
|
-
type: 'text',
|
|
1590
|
-
text: `${data.returned} of ${data.matched} matches${data.query ? ` for "${data.query}"` : ''}:\n\n${list}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1591
|
-
},
|
|
1592
|
-
],
|
|
1593
|
-
};
|
|
1594
|
-
});
|
|
1595
|
-
// ── Tool: list_watches ──────────────────────────────────────────────
|
|
1596
|
-
registerTool('list_watches', 'List the active webhook watches owned by the configured TensorFeed token. Free, requires TENSORFEED_TOKEN.', {}, async () => {
|
|
1597
|
-
const data = (await fetchJSON('/premium/watches', { auth: true }));
|
|
1598
|
-
if (data.count === 0) {
|
|
1599
|
-
return { content: [{ type: 'text', text: 'No active watches.' }] };
|
|
1083
|
+
// ── Tool: list_watches ──────────────────────────────────────────────
|
|
1084
|
+
registerTool('list_watches', 'List the active webhook watches owned by the configured TensorFeed token. Free, requires TENSORFEED_TOKEN.', {}, async () => {
|
|
1085
|
+
const data = (await fetchJSON('/premium/watches', { auth: true }));
|
|
1086
|
+
if (data.count === 0) {
|
|
1087
|
+
return { content: [{ type: 'text', text: 'No active watches.' }] };
|
|
1600
1088
|
}
|
|
1601
1089
|
const rows = data.watches
|
|
1602
1090
|
.map(w => {
|
|
@@ -1608,924 +1096,144 @@ registerTool('list_watches', 'List the active webhook watches owned by the confi
|
|
|
1608
1096
|
.join('\n\n');
|
|
1609
1097
|
return { content: [{ type: 'text', text: `${data.count} active watches:\n\n${rows}` }] };
|
|
1610
1098
|
});
|
|
1611
|
-
// ── Tool:
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
const body = {
|
|
1645
|
-
spec: { type: 'status', provider, op, ...(value ? { value } : {}) },
|
|
1646
|
-
callback_url,
|
|
1647
|
-
};
|
|
1648
|
-
if (secret !== undefined)
|
|
1649
|
-
body.secret = secret;
|
|
1650
|
-
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
|
|
1651
|
-
return {
|
|
1652
|
-
content: [
|
|
1653
|
-
{
|
|
1654
|
-
type: 'text',
|
|
1655
|
-
text: `Created watch ${data.watch.id} (expires ${data.watch.expires_at}). Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1656
|
-
},
|
|
1657
|
-
],
|
|
1658
|
-
};
|
|
1659
|
-
}, CREATE_TOOL);
|
|
1660
|
-
// ── Tool: create_digest_watch (1 credit) ────────────────────────────
|
|
1661
|
-
registerTool('create_digest_watch', 'Register a scheduled digest webhook that fires daily or weekly with a curated summary of pricing changes (regardless of whether anything dramatic happened). Costs 1 credit ($0.02). Watch lives 90 days. Set-and-forget for agents that want a periodic snapshot without subscribing to realtime transitions.', {
|
|
1662
|
-
cadence: z.enum(['daily', 'weekly']).describe('How often the digest fires'),
|
|
1663
|
-
callback_url: z.string().describe('HTTPS URL to POST to when the digest fires'),
|
|
1664
|
-
secret: z.string().optional().describe('Optional HMAC shared secret'),
|
|
1665
|
-
}, async ({ cadence, callback_url, secret }) => {
|
|
1666
|
-
const body = {
|
|
1667
|
-
spec: { type: 'digest', cadence },
|
|
1668
|
-
callback_url,
|
|
1669
|
-
};
|
|
1670
|
-
if (secret !== undefined)
|
|
1671
|
-
body.secret = secret;
|
|
1672
|
-
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
|
|
1673
|
-
return {
|
|
1674
|
-
content: [
|
|
1675
|
-
{
|
|
1676
|
-
type: 'text',
|
|
1677
|
-
text: `Created ${cadence} digest watch ${data.watch.id} (expires ${data.watch.expires_at}). First fire at the next 7am UTC daily cron. Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1678
|
-
},
|
|
1679
|
-
],
|
|
1680
|
-
};
|
|
1681
|
-
}, CREATE_TOOL);
|
|
1682
|
-
// ── Tool: create_leaderboard_rank_watch (1 credit) ──────────────────
|
|
1683
|
-
registerTool('create_leaderboard_rank_watch', 'Register a webhook that fires when a provider crosses a rank threshold on the cross-provider 7-day uptime leaderboard. Rank 1 = best (highest uptime). drops_below: was rank<=N, now rank>N (got worse). rises_above: was rank>=N, now rank<N (got better). changes: any rank movement. Costs 1 credit ($0.02) at registration. Watch lives 90 days.', {
|
|
1684
|
-
provider: z.string().describe('Provider name or slug (case-insensitive). e.g. claude, openai, gemini, bedrock, azure'),
|
|
1685
|
-
op: z.enum(['drops_below', 'rises_above', 'changes']).describe('Trigger condition'),
|
|
1686
|
-
threshold: z.number().int().min(1).optional().describe('Required for drops_below / rises_above. Integer rank position.'),
|
|
1687
|
-
callback_url: z.string().describe('HTTPS URL to POST to when the watch fires'),
|
|
1688
|
-
secret: z.string().optional().describe('Optional HMAC shared secret'),
|
|
1689
|
-
}, async ({ provider, op, threshold, callback_url, secret }) => {
|
|
1690
|
-
const spec = { type: 'leaderboard_rank', provider, op };
|
|
1691
|
-
if (threshold !== undefined)
|
|
1692
|
-
spec.threshold = threshold;
|
|
1693
|
-
const body = { spec, callback_url };
|
|
1694
|
-
if (secret !== undefined)
|
|
1695
|
-
body.secret = secret;
|
|
1696
|
-
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
|
|
1697
|
-
const desc = op === 'changes'
|
|
1698
|
-
? `${provider} rank changes`
|
|
1699
|
-
: `${provider} ${op.replace('_', ' ')} #${threshold}`;
|
|
1700
|
-
return {
|
|
1701
|
-
content: [
|
|
1702
|
-
{
|
|
1703
|
-
type: 'text',
|
|
1704
|
-
text: `Created leaderboard rank watch ${data.watch.id} (${desc}). Expires ${data.watch.expires_at}. Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1705
|
-
},
|
|
1706
|
-
],
|
|
1707
|
-
};
|
|
1708
|
-
}, CREATE_TOOL);
|
|
1709
|
-
// ── Tool: delete_watch ──────────────────────────────────────────────
|
|
1710
|
-
registerTool('delete_watch', 'Delete one of your active webhook watches by id. Free, requires TENSORFEED_TOKEN.', {
|
|
1711
|
-
watch_id: z
|
|
1712
|
-
.string()
|
|
1713
|
-
.regex(/^[A-Za-z0-9_-]+$/, 'watch_id may only contain letters, digits, underscores, and hyphens')
|
|
1714
|
-
.max(128)
|
|
1715
|
-
.describe('The wat_... id from create_price_watch / create_status_watch / list_watches'),
|
|
1716
|
-
}, async ({ watch_id }) => {
|
|
1717
|
-
await fetchJSON(`/premium/watches/${encodeURIComponent(watch_id)}`, { method: 'DELETE', auth: true });
|
|
1718
|
-
return { content: [{ type: 'text', text: `Deleted watch ${watch_id}.` }] };
|
|
1719
|
-
}, DELETE_TOOL);
|
|
1720
|
-
// ── Tool: probe_latest (free) ───────────────────────────────────────
|
|
1721
|
-
registerTool('probe_latest', 'Last 24 hours of measured LLM endpoint latency and availability per provider (Anthropic, OpenAI, Google, Mistral, Cohere). TensorFeed pings each provider\'s chat completion endpoint every 15 min and records time-to-first-byte, total response time, and HTTP status. Returns per-provider success rate and ttfb/total p50/p95/p99 latency. The data is unique because we measure it ourselves, not self-reported by the providers. Useful when an agent needs to pick a provider whose SLA you can verify, or to detect ongoing incidents before they hit a status page. Free, no auth.', {}, async () => {
|
|
1722
|
-
const data = (await fetchJSON('/probe/latest'));
|
|
1723
|
-
const s = data.summary;
|
|
1724
|
-
if (s.providers.length === 0) {
|
|
1725
|
-
return { content: [{ type: 'text', text: 'No probe data yet. Probing starts as soon as TensorFeed has at least one provider key configured.' }] };
|
|
1726
|
-
}
|
|
1727
|
-
const rows = s.providers
|
|
1728
|
-
.sort((a, b) => b.ok_pct - a.ok_pct)
|
|
1729
|
-
.map(p => {
|
|
1730
|
-
const okPct = (p.ok_pct * 100).toFixed(1);
|
|
1731
|
-
const ttfb = p.ttfb.p50 !== null ? `p50 ${p.ttfb.p50}ms / p95 ${p.ttfb.p95}ms / p99 ${p.ttfb.p99}ms` : 'no successful probes';
|
|
1732
|
-
const total = p.total.p50 !== null ? `p50 ${p.total.p50}ms / p95 ${p.total.p95}ms / p99 ${p.total.p99}ms` : 'no successful probes';
|
|
1733
|
-
const err = p.last_error ? `\n last error: ${p.last_error}` : '';
|
|
1734
|
-
return ` ${p.provider}: ${okPct}% ok over ${p.count} probes\n ttfb: ${ttfb}\n total: ${total}${err}`;
|
|
1735
|
-
})
|
|
1736
|
-
.join('\n\n');
|
|
1737
|
-
return {
|
|
1738
|
-
content: [
|
|
1739
|
-
{
|
|
1740
|
-
type: 'text',
|
|
1741
|
-
text: `LLM Endpoint SLA (${s.window_label}, computed ${s.computed_at})\n\n${rows}\n\nSource: TensorFeed measured probes, not self-reported.`,
|
|
1742
|
-
},
|
|
1743
|
-
],
|
|
1744
|
-
};
|
|
1745
|
-
});
|
|
1746
|
-
// ── Tool: probe_series (1 credit) ───────────────────────────────────
|
|
1747
|
-
registerTool('probe_series', 'Daily SLA series for one LLM provider, measured by TensorFeed. Returns per-day count, success rate, ttfb p50/p95/p99, total p50/p95/p99, and incident-hour count across the requested window. Provider status pages are politically managed; this is the measured truth. Pairs naturally with premium_routing for picking a model whose SLA you can verify. Costs 3 credits ($0.06). Strict premium, no free trial.', {
|
|
1748
|
-
provider: z.enum(['anthropic', 'openai', 'google', 'mistral', 'cohere']).describe('LLM provider key'),
|
|
1749
|
-
from: z.string().optional().describe('Inclusive start YYYY-MM-DD (default: 30 days before to)'),
|
|
1750
|
-
to: z.string().optional().describe('Inclusive end YYYY-MM-DD (default: today UTC)'),
|
|
1751
|
-
}, async ({ provider, from, to }) => {
|
|
1752
|
-
const params = new URLSearchParams();
|
|
1753
|
-
params.set('provider', provider);
|
|
1754
|
-
if (from)
|
|
1755
|
-
params.set('from', from);
|
|
1756
|
-
if (to)
|
|
1757
|
-
params.set('to', to);
|
|
1758
|
-
const data = (await fetchJSON(`/premium/probe/series?${params}`, { auth: true }));
|
|
1759
|
-
const lines = data.points
|
|
1760
|
-
.map(p => p.has_data
|
|
1761
|
-
? ` ${p.date} ${(p.ok_pct * 100).toFixed(1)}% ok ttfb p50/p95: ${p.ttfb_p50}/${p.ttfb_p95}ms total p50/p95: ${p.total_p50}/${p.total_p95}ms incidents: ${p.incident_hours}h`
|
|
1762
|
-
: ` ${p.date} (no data)`)
|
|
1763
|
-
.join('\n');
|
|
1764
|
-
const overall = data.summary.overall_uptime_pct !== null
|
|
1765
|
-
? `${(data.summary.overall_uptime_pct * 100).toFixed(2)}%`
|
|
1766
|
-
: 'insufficient data';
|
|
1767
|
-
const notes = data.notes.length ? '\nNotes: ' + data.notes.join('; ') : '';
|
|
1768
|
-
return {
|
|
1769
|
-
content: [
|
|
1770
|
-
{
|
|
1771
|
-
type: 'text',
|
|
1772
|
-
text: `${data.provider} measured SLA, ${data.from} -> ${data.to}\nOverall uptime: ${overall} (${data.summary.days_with_data} days with data, ${data.summary.days_with_incidents} with incidents)\n\n${lines}${notes}\n\nCredits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1773
|
-
},
|
|
1774
|
-
],
|
|
1775
|
-
};
|
|
1776
|
-
});
|
|
1777
|
-
// ── Tool: mcp_registry_snapshot (free) ──────────────────────────────
|
|
1778
|
-
registerTool('mcp_registry_snapshot', 'Today\'s summary of the official Model Context Protocol server registry. Returns total servers, by-status breakdown, top namespaces, and 1-day deltas (newly added, reactivated, deprecated). Captured daily at 9:30 AM UTC from registry.modelcontextprotocol.io. Useful when an agent wants to see how the MCP ecosystem is growing or detect freshly-deprecated servers it may be using. Free, no auth.', {}, async () => {
|
|
1779
|
-
const data = (await fetchJSON('/mcp/registry/snapshot'));
|
|
1780
|
-
const s = data.summary;
|
|
1781
|
-
const statusLine = Object.entries(s.by_status)
|
|
1782
|
-
.map(([k, v]) => `${k}: ${v}`)
|
|
1783
|
-
.join(', ');
|
|
1784
|
-
const topNs = s.top_namespaces.slice(0, 10).map(n => ` ${n.namespace}: ${n.count}`).join('\n');
|
|
1785
|
-
const delta = s.delta_vs_yesterday
|
|
1786
|
-
? `Day over day: +${s.delta_vs_yesterday.added} new, -${s.delta_vs_yesterday.removed} removed, net ${s.delta_vs_yesterday.net >= 0 ? '+' : ''}${s.delta_vs_yesterday.net}`
|
|
1787
|
-
: 'Day over day: not available (first snapshot)';
|
|
1788
|
-
const newToday = s.new_today.count > 0
|
|
1789
|
-
? `\nNewly listed today (${s.new_today.count}):\n` + s.new_today.names.slice(0, 10).map(n => ` ${n}`).join('\n')
|
|
1790
|
-
: '';
|
|
1791
|
-
const deprecatedToday = s.deprecated_today.count > 0
|
|
1792
|
-
? `\nDeprecated today (${s.deprecated_today.count}):\n` + s.deprecated_today.names.slice(0, 10).map(n => ` ${n}`).join('\n')
|
|
1793
|
-
: '';
|
|
1794
|
-
return {
|
|
1795
|
-
content: [
|
|
1796
|
-
{
|
|
1797
|
-
type: 'text',
|
|
1798
|
-
text: `MCP Server Registry, ${s.date}\n` +
|
|
1799
|
-
`Total servers: ${s.total_servers} (across ${s.total_versions} versioned entries)\n` +
|
|
1800
|
-
`Status: ${statusLine}\n${delta}\n\n` +
|
|
1801
|
-
`Top namespaces:\n${topNs}` +
|
|
1802
|
-
newToday + deprecatedToday +
|
|
1803
|
-
`\n\nSource: registry.modelcontextprotocol.io`,
|
|
1804
|
-
},
|
|
1805
|
-
],
|
|
1806
|
-
};
|
|
1807
|
-
});
|
|
1808
|
-
// ── Tool: get_ai_papers_trending (free) ─────────────────────────────
|
|
1809
|
-
registerTool('get_ai_papers_trending', 'Daily curated AI/ML research papers ranked by citation count. Sourced from Semantic Scholar across five fan-out queries (large language model, transformer, RLHF, AI agents, diffusion model), deduped by paperId, top 30 returned. Each paper carries title, abstract, authors, year, venue, citation count, arxivId, doi, and fields of study. Refreshed daily at 11:00 UTC. Use this when an agent wants the citation-ranked top of the AI research literature. Free, no auth.', {}, async () => {
|
|
1810
|
-
const data = (await fetchJSON('/papers/ai-trending'));
|
|
1811
|
-
const s = data.snapshot;
|
|
1812
|
-
const lines = s.papers.map((p, i) => {
|
|
1813
|
-
const authors = p.authors.slice(0, 3).join(', ') + (p.authors.length > 3 ? ' et al.' : '');
|
|
1814
|
-
const year = p.year ? ` (${p.year})` : '';
|
|
1815
|
-
const venue = p.venue ? ` ${p.venue}.` : '';
|
|
1816
|
-
const arxiv = p.arxivId ? ` arXiv:${p.arxivId}` : '';
|
|
1817
|
-
const link = p.url ? `\n ${p.url}` : '';
|
|
1818
|
-
return `${i + 1}. ${p.title}${year}\n ${authors}.${venue} Citations: ${p.citationCount}.${arxiv}${link}`;
|
|
1819
|
-
}).join('\n\n');
|
|
1820
|
-
const venues = s.summary.top_venues.slice(0, 5).map(v => `${v.venue} (${v.count})`).join(', ');
|
|
1821
|
-
return {
|
|
1822
|
-
content: [
|
|
1823
|
-
{
|
|
1824
|
-
type: 'text',
|
|
1825
|
-
text: `AI Papers, citation-ranked. ${s.date}, ${s.total_papers} papers.\n` +
|
|
1826
|
-
(venues ? `Top venues: ${venues}\n\n` : '\n') +
|
|
1827
|
-
lines +
|
|
1828
|
-
`\n\nSource: Semantic Scholar Graph API. Captured ${s.capturedAt}.`,
|
|
1829
|
-
},
|
|
1830
|
-
],
|
|
1831
|
-
};
|
|
1832
|
-
});
|
|
1833
|
-
// ── Tool: get_arxiv_recent (free) ───────────────────────────────────
|
|
1834
|
-
registerTool('get_arxiv_recent', 'Most recent arXiv submissions in cs.AI / cs.LG / cs.CL / cs.CV. Single Atom API call, deduped by arxivId, top 50 by submission date. The firehose pair to get_ai_papers_trending: this shows what just dropped, ai_papers_trending ranks by citation count. Each paper carries arxivId, title, abstract, authors, primary category, publication date, html and pdf URLs. Refreshed daily at 11:30 UTC. Free, no auth.', {
|
|
1835
|
-
limit: z.number().min(1).max(50).optional().describe('Max papers to include in the rendered list (default 25, max 50)'),
|
|
1836
|
-
}, async ({ limit }) => {
|
|
1837
|
-
const data = (await fetchJSON('/papers/arxiv-recent'));
|
|
1838
|
-
const s = data.snapshot;
|
|
1839
|
-
const cap = Math.min(limit ?? 25, s.papers.length);
|
|
1840
|
-
const lines = s.papers.slice(0, cap).map((p, i) => {
|
|
1841
|
-
const authors = p.authors.slice(0, 3).join(', ') + (p.authors.length > 3 ? ' et al.' : '');
|
|
1842
|
-
const cat = p.primaryCategory ? ` [${p.primaryCategory}]` : '';
|
|
1843
|
-
const date = p.publishedAt ? ` ${p.publishedAt.slice(0, 10)}` : '';
|
|
1844
|
-
return `${i + 1}. ${p.title}${cat}${date}\n ${authors}\n ${p.htmlUrl}`;
|
|
1845
|
-
}).join('\n\n');
|
|
1846
|
-
return {
|
|
1847
|
-
content: [
|
|
1848
|
-
{
|
|
1849
|
-
type: 'text',
|
|
1850
|
-
text: `arXiv recent submissions. ${s.date}, showing ${cap} of ${s.total_papers}.\n` +
|
|
1851
|
-
`Categories: ${s.categories_queried.join(', ')}\n\n` +
|
|
1852
|
-
lines +
|
|
1853
|
-
`\n\nSource: arxiv.org. Captured ${s.capturedAt}.`,
|
|
1854
|
-
},
|
|
1855
|
-
],
|
|
1856
|
-
};
|
|
1857
|
-
});
|
|
1858
|
-
// ── Tool: get_hf_trending (free) ────────────────────────────────────
|
|
1859
|
-
registerTool('get_hf_trending', 'Top Hugging Face models, datasets, and Spaces. Models + datasets ranked by downloads (top 30 each); Spaces ranked by likes (top 30, since downloads is meaningless for hosted apps). Captured daily at 12:00 UTC against the public HF API (no auth). Each model entry carries id, downloads, likes, pipeline_tag, tags, lastModified, private, gated. Each space entry carries id, author, sdk (gradio/streamlit/static/docker), likes, tags, lastModified, private, runtime_stage, hardware. Use this when an agent wants the current shape of the open model + dataset + app ecosystem. Once enough daily snapshots accumulate, day-over-day deltas become a real trending signal. Free, no auth.', {
|
|
1860
|
-
section: z.enum(['models', 'datasets', 'spaces', 'all', 'both']).optional().describe('Which to include in the rendered output (default all). "both" is accepted as alias for "models+datasets" for backward compat.'),
|
|
1861
|
-
limit: z.number().min(1).max(30).optional().describe('Max items per section (default 15, max 30)'),
|
|
1862
|
-
}, async ({ section, limit }) => {
|
|
1863
|
-
const data = (await fetchJSON('/hf/trending'));
|
|
1864
|
-
const s = data.snapshot;
|
|
1865
|
-
const sec = section ?? 'all';
|
|
1866
|
-
const n = Math.min(limit ?? 15, 30);
|
|
1867
|
-
const fmtCount = (x) => x >= 1_000_000 ? `${(x / 1_000_000).toFixed(1)}M` : x >= 1_000 ? `${(x / 1_000).toFixed(1)}K` : String(x);
|
|
1868
|
-
const modelLines = s.models.items.slice(0, n).map((m, i) => {
|
|
1869
|
-
const tag = m.pipeline_tag ? ` [${m.pipeline_tag}]` : '';
|
|
1870
|
-
return `${i + 1}. ${m.id}${tag} ${fmtCount(m.downloads)} downloads, ${fmtCount(m.likes)} likes`;
|
|
1871
|
-
}).join('\n');
|
|
1872
|
-
const datasetLines = s.datasets.items.slice(0, n).map((d, i) => `${i + 1}. ${d.id} ${fmtCount(d.downloads)} downloads, ${fmtCount(d.likes)} likes`).join('\n');
|
|
1873
|
-
const spaceLines = (s.spaces?.items ?? []).slice(0, n).map((sp, i) => {
|
|
1874
|
-
const sdk = sp.sdk ? ` [${sp.sdk}]` : '';
|
|
1875
|
-
const hw = sp.hardware ? `, hw: ${sp.hardware}` : '';
|
|
1876
|
-
const stage = sp.runtime_stage ? `, ${sp.runtime_stage.toLowerCase()}` : '';
|
|
1877
|
-
return `${i + 1}. ${sp.id}${sdk} ${fmtCount(sp.likes)} likes${stage}${hw}`;
|
|
1878
|
-
}).join('\n');
|
|
1879
|
-
const pipelines = s.summary.top_pipeline_tags.slice(0, 5).map(t => `${t.tag} (${t.count})`).join(', ');
|
|
1880
|
-
const namespaces = s.summary.top_namespaces.slice(0, 5).map(n => `${n.namespace} (${n.count})`).join(', ');
|
|
1881
|
-
const sdks = (s.summary.top_space_sdks ?? []).slice(0, 5).map(t => `${t.sdk} (${t.count})`).join(', ');
|
|
1882
|
-
let text = `Hugging Face top assets. ${s.date}.\n`;
|
|
1883
|
-
if (pipelines)
|
|
1884
|
-
text += `Top pipeline tags (models): ${pipelines}\n`;
|
|
1885
|
-
if (sdks)
|
|
1886
|
-
text += `Top Space SDKs: ${sdks}\n`;
|
|
1887
|
-
if (namespaces)
|
|
1888
|
-
text += `Top namespaces: ${namespaces}\n`;
|
|
1889
|
-
const includeModels = sec === 'models' || sec === 'all' || sec === 'both';
|
|
1890
|
-
const includeDatasets = sec === 'datasets' || sec === 'all' || sec === 'both';
|
|
1891
|
-
const includeSpaces = sec === 'spaces' || sec === 'all';
|
|
1892
|
-
if (includeModels)
|
|
1893
|
-
text += `\nModels (by downloads):\n${modelLines}\n`;
|
|
1894
|
-
if (includeDatasets)
|
|
1895
|
-
text += `\nDatasets (by downloads):\n${datasetLines}\n`;
|
|
1896
|
-
if (includeSpaces)
|
|
1897
|
-
text += `\nSpaces (by likes):\n${spaceLines || ' (none returned)'}\n`;
|
|
1898
|
-
text += `\nSource: huggingface.co. Captured ${s.capturedAt}.`;
|
|
1899
|
-
return { content: [{ type: 'text', text }] };
|
|
1900
|
-
});
|
|
1901
|
-
// ── Tool: get_hot_issues (free) ─────────────────────────────────────
|
|
1902
|
-
registerTool('get_hot_issues', 'Currently-hot GitHub issues across the AI ecosystem. Five fan-out keyword-phrase searches ("large language model", "AI agent", transformer, "machine learning", LLM) for is:issue is:open archived:false comments>=10 updated within the last 7 days. Deduped by URL, top 30 by comment count. Each issue carries url, repo, number, title, author, state, comments, reactions_total, labels, created_at, updated_at, and matched_topic (the keyword phrase that found it). Refreshed daily at 12:30 UTC. Companion to get_trending_repos: that shows which AI repos are gaining stars, this shows where the active conversations are. Free, no auth.', {
|
|
1903
|
-
limit: z.number().min(1).max(30).optional().describe('Max issues to render (default 15, max 30)'),
|
|
1904
|
-
}, async ({ limit }) => {
|
|
1905
|
-
const data = (await fetchJSON('/issues/hot'));
|
|
1906
|
-
const s = data.snapshot;
|
|
1907
|
-
const cap = Math.min(limit ?? 15, s.issues.length);
|
|
1908
|
-
const lines = s.issues.slice(0, cap).map((i, n) => {
|
|
1909
|
-
const meta = `${i.comments} comments, ${i.reactions_total} reactions [${i.matched_topic}]`;
|
|
1910
|
-
const author = i.author ? ` by ${i.author}` : '';
|
|
1911
|
-
const updated = i.updated_at ? ` updated ${i.updated_at.slice(0, 10)}` : '';
|
|
1912
|
-
return `${n + 1}. ${i.repo}: ${i.title}\n ${meta}${updated}${author}\n ${i.url}`;
|
|
1913
|
-
}).join('\n\n');
|
|
1914
|
-
const repos = s.summary.top_repos.slice(0, 5).map(r => `${r.repo} (${r.count})`).join(', ');
|
|
1915
|
-
return {
|
|
1916
|
-
content: [
|
|
1917
|
-
{
|
|
1918
|
-
type: 'text',
|
|
1919
|
-
text: `Hot AI GitHub issues. ${s.date}, showing ${cap} of ${s.total_issues}.\n` +
|
|
1920
|
-
`Window: last ${s.recent_window_days} days, comments threshold ${s.comments_threshold}.\n` +
|
|
1921
|
-
(repos ? `Most-active repos: ${repos}\n\n` : '\n') +
|
|
1922
|
-
lines +
|
|
1923
|
-
`\n\nSource: GitHub Search API. Captured ${s.capturedAt}.`,
|
|
1924
|
-
},
|
|
1925
|
-
],
|
|
1926
|
-
};
|
|
1927
|
-
});
|
|
1928
|
-
// ── Tool: get_reddit_trending (free) ────────────────────────────────
|
|
1929
|
-
registerTool('get_reddit_trending', 'Currently-hot threads in 7 AI-relevant subreddits (LocalLLaMA, MachineLearning, ClaudeAI, OpenAI, singularity, artificial, AI_Agents). Stickied and NSFW posts filtered, deduped by post id, ranked by score, top 30. Each post carries id, subreddit, title, author, score, upvote_ratio, num_comments, permalink, url, created_utc, flair, is_self, is_video. Refreshed daily at 13:00 UTC. Companion to get_hot_issues: that surfaces developer conversation on GitHub, this surfaces community conversation on Reddit. Titles are sanitized at capture time against prompt-injection tokens. Free, no auth.', {
|
|
1930
|
-
subreddit: z.string().optional().describe('Optional filter to a single subreddit name (e.g. "LocalLLaMA"). Case-insensitive. If absent, returns posts from all 7.'),
|
|
1931
|
-
limit: z.number().min(1).max(30).optional().describe('Max posts to render (default 15, max 30)'),
|
|
1932
|
-
}, async ({ subreddit, limit }) => {
|
|
1933
|
-
const data = (await fetchJSON('/reddit/trending'));
|
|
1934
|
-
const s = data.snapshot;
|
|
1935
|
-
const subFilter = subreddit?.trim().toLowerCase();
|
|
1936
|
-
const filtered = subFilter
|
|
1937
|
-
? s.posts.filter(p => p.subreddit.toLowerCase() === subFilter)
|
|
1938
|
-
: s.posts;
|
|
1939
|
-
const cap = Math.min(limit ?? 15, filtered.length);
|
|
1940
|
-
const fmtCount = (x) => x >= 1_000 ? `${(x / 1_000).toFixed(1)}K` : String(x);
|
|
1941
|
-
const lines = filtered.slice(0, cap).map((p, i) => {
|
|
1942
|
-
const author = p.author ? ` by ${p.author}` : '';
|
|
1943
|
-
const flair = p.flair ? ` [${p.flair}]` : '';
|
|
1944
|
-
return `${i + 1}. r/${p.subreddit}: ${p.title}${flair}\n ${fmtCount(p.score)} pts, ${fmtCount(p.num_comments)} comments${author}\n ${p.permalink}`;
|
|
1945
|
-
}).join('\n\n');
|
|
1946
|
-
const subs = Object.entries(s.summary.by_subreddit)
|
|
1947
|
-
.sort((a, b) => b[1] - a[1])
|
|
1948
|
-
.slice(0, 5)
|
|
1949
|
-
.map(([sub, n]) => `r/${sub} (${n})`)
|
|
1950
|
-
.join(', ');
|
|
1951
|
-
let text = `Hot AI Reddit threads. ${s.date}, showing ${cap} of ${filtered.length}`;
|
|
1952
|
-
if (subFilter)
|
|
1953
|
-
text += ` (filtered to r/${subreddit})`;
|
|
1954
|
-
text += '.\n';
|
|
1955
|
-
if (subs && !subFilter)
|
|
1956
|
-
text += `Most-active subreddits: ${subs}\n`;
|
|
1957
|
-
text += '\n' + (lines || ' (no posts)');
|
|
1958
|
-
text += `\n\nSource: reddit.com. Captured ${s.capturedAt}.`;
|
|
1959
|
-
return { content: [{ type: 'text', text }] };
|
|
1960
|
-
});
|
|
1961
|
-
// ── Tool: get_openrouter_models (free) ──────────────────────────────
|
|
1962
|
-
registerTool('get_openrouter_models', 'OpenRouter cross-provider model catalog (200+ models normalized across 50+ inference providers). Each entry has per-token pricing (prompt + completion + image + request), context window, modality (e.g. text+image->text), instruct_type, tokenizer, top provider metadata (max_completion_tokens, moderation flag), and supported_parameters. Use this to find the long tail of OSS models on cloud inference, including the cheapest model for a given workload, the model with the largest context window, or the free-tier set. Pairs with get_model_pricing (curated frontier-lab catalog). Refreshed daily at 14:00 UTC. Free, no auth.', {
|
|
1963
|
-
namespace: z.string().optional().describe('Optional filter to a single provider namespace (e.g. "anthropic", "openai", "meta-llama"). Case-insensitive.'),
|
|
1964
|
-
free_only: z.boolean().optional().describe('If true, return only models with prompt+completion both 0 (free-tier).'),
|
|
1965
|
-
cheapest: z.boolean().optional().describe('If true, sort by cheapest input price ascending. Free-tier models excluded from this sort.'),
|
|
1966
|
-
limit: z.number().min(1).max(50).optional().describe('Max models to render (default 20, max 50)'),
|
|
1967
|
-
}, async ({ namespace, free_only, cheapest, limit }) => {
|
|
1968
|
-
const data = (await fetchJSON('/openrouter/models'));
|
|
1969
|
-
const s = data.snapshot;
|
|
1970
|
-
const ns = namespace?.trim().toLowerCase();
|
|
1971
|
-
let list = s.models;
|
|
1972
|
-
if (ns)
|
|
1973
|
-
list = list.filter(m => m.id.toLowerCase().startsWith(ns + '/'));
|
|
1974
|
-
if (free_only)
|
|
1975
|
-
list = list.filter(m => m.pricing.prompt === 0 && m.pricing.completion === 0);
|
|
1976
|
-
if (cheapest) {
|
|
1977
|
-
list = list
|
|
1978
|
-
.filter(m => typeof m.pricing.prompt === 'number' && m.pricing.prompt > 0)
|
|
1979
|
-
.sort((a, b) => (a.pricing.prompt ?? 0) - (b.pricing.prompt ?? 0));
|
|
1980
|
-
}
|
|
1981
|
-
const cap = Math.min(limit ?? 20, list.length);
|
|
1982
|
-
const fmtPrice = (p) => p === null ? '?' : p === 0 ? 'free' : `$${(p * 1_000_000).toFixed(2)}/Mtok`;
|
|
1983
|
-
const fmtCtx = (c) => c === null ? 'unknown' : c >= 1_000_000 ? `${(c / 1_000_000).toFixed(0)}M` : `${(c / 1_000).toFixed(0)}K`;
|
|
1984
|
-
const lines = list.slice(0, cap).map((m, i) => {
|
|
1985
|
-
const modality = m.modality ? ` ${m.modality}` : '';
|
|
1986
|
-
return `${i + 1}. ${m.id}${modality}\n in ${fmtPrice(m.pricing.prompt)}, out ${fmtPrice(m.pricing.completion)}, ctx ${fmtCtx(m.context_length)}`;
|
|
1987
|
-
}).join('\n\n');
|
|
1988
|
-
const namespaces = s.summary.by_namespace.slice(0, 5).map(n => `${n.namespace} (${n.count})`).join(', ');
|
|
1989
|
-
let header = `OpenRouter catalog. ${s.date}. ${s.total_models} models total, ${list.length} after filters, showing ${cap}.\n`;
|
|
1990
|
-
header += `Top provider namespaces: ${namespaces}\n`;
|
|
1991
|
-
if (s.summary.cheapest_input)
|
|
1992
|
-
header += `Cheapest input (paid): ${s.summary.cheapest_input.id} at $${s.summary.cheapest_input.usd_per_million.toFixed(2)}/Mtok\n`;
|
|
1993
|
-
if (s.summary.largest_context)
|
|
1994
|
-
header += `Largest context: ${s.summary.largest_context.id} at ${fmtCtx(s.summary.largest_context.tokens)} tokens\n`;
|
|
1995
|
-
header += `Free-tier models available: ${s.summary.free_tier_count}\n`;
|
|
1996
|
-
return {
|
|
1997
|
-
content: [
|
|
1998
|
-
{
|
|
1999
|
-
type: 'text',
|
|
2000
|
-
text: header + '\n' + (lines || ' (no models match the filters)') + `\n\nSource: openrouter.ai. Captured ${s.capturedAt}.`,
|
|
2001
|
-
},
|
|
2002
|
-
],
|
|
2003
|
-
};
|
|
2004
|
-
});
|
|
2005
|
-
// ── Tool: get_hf_daily_papers (free) ────────────────────────────────
|
|
2006
|
-
registerTool('get_hf_daily_papers', 'Hugging Face Daily Papers: editor-curated AI/ML papers from huggingface.co/papers, layered with community upvotes and discussion counts. Different signal from get_arxiv_recent (firehose) and get_ai_papers_trending (citation-ranked all-time): this is editor picks of-the-day with engagement signal. Each paper carries paperId, title, summary, authors, upvotes, num_comments, hf_url, arxiv_url (when applicable), github_repo, github_stars, ai_keywords. Refreshed daily at 14:15 UTC. Titles sanitized at capture time. Free, no auth.', {
|
|
2007
|
-
limit: z.number().min(1).max(30).optional().describe('Max papers to render (default 15, max 30)'),
|
|
2008
|
-
}, async ({ limit }) => {
|
|
2009
|
-
const data = (await fetchJSON('/papers/hf-daily'));
|
|
2010
|
-
const s = data.snapshot;
|
|
2011
|
-
const cap = Math.min(limit ?? 15, s.papers.length);
|
|
2012
|
-
const lines = s.papers.slice(0, cap).map((p, i) => {
|
|
2013
|
-
const authors = p.authors.slice(0, 3).join(', ') + (p.authors.length > 3 ? ' et al.' : '');
|
|
2014
|
-
const kw = p.ai_keywords.length > 0 ? `\n keywords: ${p.ai_keywords.slice(0, 5).join(', ')}` : '';
|
|
2015
|
-
const arxiv = p.arxiv_url ? `\n arxiv: ${p.arxiv_url}` : '';
|
|
2016
|
-
return `${i + 1}. ${p.title}\n ${authors}\n ${p.upvotes} upvotes, ${p.num_comments} comments\n ${p.hf_url}${arxiv}${kw}`;
|
|
2017
|
-
}).join('\n\n');
|
|
2018
|
-
const kw = s.summary.by_keyword.slice(0, 5).map(k => `${k.keyword} (${k.count})`).join(', ');
|
|
2019
|
-
let text = `HF Daily Papers (editor-curated). ${s.date}, showing ${cap} of ${s.total_papers}.\n`;
|
|
2020
|
-
if (kw)
|
|
2021
|
-
text += `Top keywords: ${kw}\n`;
|
|
2022
|
-
if (s.summary.most_upvoted)
|
|
2023
|
-
text += `Most upvoted: "${s.summary.most_upvoted.title}" (${s.summary.most_upvoted.upvotes})\n`;
|
|
2024
|
-
text += '\n' + lines + `\n\nSource: huggingface.co/papers. Captured ${s.capturedAt}.`;
|
|
2025
|
-
return { content: [{ type: 'text', text }] };
|
|
2026
|
-
});
|
|
2027
|
-
// ── Tool: get_ai_ecosystem_today (free, composite) ──────────────────
|
|
2028
|
-
// ── Tool: get_recent_earthquakes (free) ─────────────────────────────
|
|
2029
|
-
registerTool('get_recent_earthquakes', "Recent earthquakes from the USGS Earthquake Hazards Program pre-built summary feeds. Choose a magnitude bucket (significant | 4.5 | 2.5 | 1.0 | all) and a time window (hour | day | week | month). Returns id, magnitude, place, time (ISO 8601), depth_km, longitude, latitude, tsunami flag, USGS detail URL. Upstream feeds refresh every minute. License: US Government public domain (17 USC §105). Free, no auth.", {
|
|
2030
|
-
magnitude: z.enum(['significant', '4.5', '2.5', '1.0', 'all']).optional().describe('Magnitude bucket (default 4.5)'),
|
|
2031
|
-
period: z.enum(['hour', 'day', 'week', 'month']).optional().describe('Time window (default day)'),
|
|
2032
|
-
limit: z.number().min(1).max(50).optional().describe('Max earthquakes to render (default 15, max 50). Worker accepts up to 500 but text rendering caps lower.'),
|
|
2033
|
-
}, async ({ magnitude, period, limit }) => {
|
|
2034
|
-
const params = new URLSearchParams();
|
|
2035
|
-
params.set('magnitude', magnitude ?? '4.5');
|
|
2036
|
-
params.set('period', period ?? 'day');
|
|
2037
|
-
params.set('limit', String(Math.min(limit ?? 15, 50)));
|
|
2038
|
-
const data = (await fetchJSON(`/climate/earthquakes?${params}`));
|
|
2039
|
-
if (!data.ok || !data.earthquakes) {
|
|
2040
|
-
return { content: [{ type: 'text', text: `USGS earthquakes unavailable: ${data.error ?? 'unknown error'}` }] };
|
|
2041
|
-
}
|
|
2042
|
-
const eqs = data.earthquakes;
|
|
2043
|
-
const lines = eqs.map((q, i) => {
|
|
2044
|
-
const mag = q.magnitude !== null ? `M${q.magnitude.toFixed(1)}` : 'M?';
|
|
2045
|
-
const tsu = q.tsunami ? ' [tsunami flag]' : '';
|
|
2046
|
-
const time = q.time ? q.time.slice(0, 19).replace('T', ' ') + ' UTC' : 'unknown time';
|
|
2047
|
-
const depth = q.depth_km !== null ? `${q.depth_km.toFixed(0)}km deep` : 'depth unknown';
|
|
2048
|
-
const coords = q.latitude !== null && q.longitude !== null ? ` (${q.latitude.toFixed(2)}, ${q.longitude.toFixed(2)})` : '';
|
|
2049
|
-
return `${i + 1}. ${mag} ${q.place ?? 'location unknown'}${coords}\n ${time}, ${depth}${tsu}\n ${q.url ?? ''}`;
|
|
2050
|
-
}).join('\n\n');
|
|
2051
|
-
const meta = data.feed_metadata;
|
|
2052
|
-
const header = `USGS earthquakes (${data.query?.magnitude ?? '?'} / ${data.query?.period ?? '?'}). ${eqs.length} of ${meta?.upstream_count ?? '?'} returned.`;
|
|
2053
|
-
return { content: [{ type: 'text', text: `${header}\n\n${lines || ' (no events in this window)'}\n\nSource: USGS Earthquake Hazards Program. Public domain (17 USC §105).` }] };
|
|
2054
|
-
});
|
|
2055
|
-
// ── Tool: get_weather_alerts (free, US only) ────────────────────────
|
|
2056
|
-
registerTool('get_weather_alerts', "Active US weather alerts from the National Weather Service. US-only. Filter by 2-letter state code (area), exact NWS event name, severity, urgency, status. Returns id, event, severity, urgency, headline, description, areaDesc, sent/effective/expires/ends, sender_name, web URL. Active alerts only. Expired alerts fall off the upstream feed automatically. License: US Government public domain. Free, no auth.", {
|
|
2057
|
-
area: z.string().regex(/^[A-Za-z]{2}$/).optional().describe('2-letter US state or territory code (CA, TX, PR, etc)'),
|
|
2058
|
-
event: z.string().optional().describe('Exact NWS event name (e.g. "Tornado Warning", "Heat Advisory")'),
|
|
2059
|
-
severity: z.enum(['Extreme', 'Severe', 'Moderate', 'Minor', 'Unknown']).optional().describe('Severity filter'),
|
|
2060
|
-
urgency: z.enum(['Immediate', 'Expected', 'Future', 'Past', 'Unknown']).optional().describe('Urgency filter'),
|
|
2061
|
-
status: z.enum(['actual', 'exercise', 'system', 'test', 'draft']).optional().describe('Status filter (default actual unfiltered)'),
|
|
2062
|
-
limit: z.number().min(1).max(50).optional().describe('Max alerts to render (default 15, max 50). Worker accepts up to 500 but text rendering caps lower.'),
|
|
2063
|
-
}, async ({ area, event, severity, urgency, status, limit }) => {
|
|
2064
|
-
const params = new URLSearchParams();
|
|
2065
|
-
if (area)
|
|
2066
|
-
params.set('area', area.toUpperCase());
|
|
2067
|
-
if (event)
|
|
2068
|
-
params.set('event', event);
|
|
2069
|
-
if (severity)
|
|
2070
|
-
params.set('severity', severity);
|
|
2071
|
-
if (urgency)
|
|
2072
|
-
params.set('urgency', urgency);
|
|
2073
|
-
if (status)
|
|
2074
|
-
params.set('status', status);
|
|
2075
|
-
params.set('limit', String(Math.min(limit ?? 15, 50)));
|
|
2076
|
-
const data = (await fetchJSON(`/climate/weather-alerts?${params}`));
|
|
2077
|
-
if (!data.ok || !data.alerts) {
|
|
2078
|
-
return { content: [{ type: 'text', text: `NWS weather alerts unavailable: ${data.error ?? 'unknown error'}` }] };
|
|
2079
|
-
}
|
|
2080
|
-
const alerts = data.alerts;
|
|
2081
|
-
const lines = alerts.map((a, i) => {
|
|
2082
|
-
const ev = a.event ?? 'unknown event';
|
|
2083
|
-
const sev = a.severity ? `[${a.severity}]` : '';
|
|
2084
|
-
const eff = a.effective ? a.effective.slice(0, 19).replace('T', ' ') + ' UTC' : '';
|
|
2085
|
-
const exp = a.expires ? a.expires.slice(0, 19).replace('T', ' ') + ' UTC' : 'no expiry';
|
|
2086
|
-
return `${i + 1}. ${sev} ${ev}\n Where: ${a.area_desc ?? 'unspecified'}\n ${a.headline ?? ''}\n Effective: ${eff} → ${exp}\n ${a.web ?? ''}`;
|
|
2087
|
-
}).join('\n\n');
|
|
2088
|
-
const filters = [];
|
|
2089
|
-
if (area)
|
|
2090
|
-
filters.push(`area=${area.toUpperCase()}`);
|
|
2091
|
-
if (event)
|
|
2092
|
-
filters.push(`event=${event}`);
|
|
2093
|
-
if (severity)
|
|
2094
|
-
filters.push(`severity=${severity}`);
|
|
2095
|
-
const filterText = filters.length > 0 ? ` (${filters.join(', ')})` : '';
|
|
2096
|
-
const header = `NWS active US weather alerts${filterText}. ${alerts.length} of ${data.feed_metadata?.upstream_count ?? '?'} returned.`;
|
|
2097
|
-
return { content: [{ type: 'text', text: `${header}\n\n${lines || ' (no active alerts matching filters)'}\n\nSource: api.weather.gov. Public domain (17 USC §105). US-only coverage.` }] };
|
|
2098
|
-
});
|
|
2099
|
-
// ── Tool: get_agent_opportunities (free) ────────────────────────────
|
|
2100
|
-
registerTool('get_agent_opportunities', "TensorFeed's daily scan of new repositories across the AI agent ecosystem (Anthropic, OpenAI, Microsoft, ModelContextProtocol, HuggingFace, LangChain, frontier-lab orgs) plus recent MCP, x402, and skill keyword sweeps. Eleven signals total, deduped + composite-scored (signal_weight × log10(stars+1) × recency decay) with per-signal MIN/MAX caps so smaller signals are never starved. Refreshed daily at 13:30 UTC. Useful for surfacing distribution targets, integration ideas, or a daily brief on what's launching across the agent space. Free, no auth.", {
|
|
2101
|
-
signal: z.enum([
|
|
2102
|
-
'anthropic-org', 'openai-org', 'microsoft-org', 'mcp-org', 'huggingface-org',
|
|
2103
|
-
'langchain-org', 'frontier-labs', 'mcp-keyword', 'x402-keyword', 'skill-keyword',
|
|
2104
|
-
'vertical-pattern',
|
|
2105
|
-
]).optional().describe('Filter to one signal source. Omit to get the cross-signal ranked top-N.'),
|
|
2106
|
-
limit: z.number().min(1).max(25).optional().describe('Max opportunities to render (default 10, max 25)'),
|
|
2107
|
-
}, async ({ signal, limit }) => {
|
|
2108
|
-
const data = (await fetchJSON('/agents/opportunities'));
|
|
2109
|
-
if (data.error) {
|
|
2110
|
-
return { content: [{ type: 'text', text: `Agent opportunities unavailable: ${data.error}` }] };
|
|
2111
|
-
}
|
|
2112
|
-
const all = data.opportunities ?? [];
|
|
2113
|
-
const filtered = signal ? all.filter(o => o.signal === signal) : all;
|
|
2114
|
-
const cap = Math.min(limit ?? 10, 25);
|
|
2115
|
-
const top = filtered.slice(0, cap);
|
|
2116
|
-
const lines = top.map((o, i) => {
|
|
2117
|
-
const updated = o.updated_at ? o.updated_at.slice(0, 10) : '';
|
|
2118
|
-
const desc = o.description ? `\n ${o.description.length > 140 ? o.description.slice(0, 140) + '...' : o.description}` : '';
|
|
2119
|
-
return `${i + 1}. [${o.signal}] ${o.full_name} ${o.stars.toLocaleString()}★, score ${o.composite_score.toFixed(1)}, updated ${updated}${desc}\n ${o.html_url}`;
|
|
2120
|
-
}).join('\n\n');
|
|
2121
|
-
const filt = signal ? ` filtered to ${signal}` : '';
|
|
2122
|
-
const header = `Agent ecosystem opportunities for ${data.date ?? 'today'}${filt}. ${top.length} of ${filtered.length} (total snapshot: ${data.total_opportunities ?? all.length}).`;
|
|
2123
|
-
const bySignal = data.summary?.by_signal ? Object.entries(data.summary.by_signal).map(([k, v]) => `${k}=${v}`).join(', ') : '';
|
|
2124
|
-
const sig = bySignal ? `\nBy signal: ${bySignal}` : '';
|
|
2125
|
-
return { content: [{ type: 'text', text: `${header}${sig}\n\n${lines || ' (no opportunities returned)'}\n\nSource: TensorFeed daily GitHub scan via the public Search API.` }] };
|
|
2126
|
-
});
|
|
2127
|
-
// Single fetch to /api/today which fans out worker-side. The previous
|
|
2128
|
-
// implementation made 9 separate fetches per agent invocation; now
|
|
2129
|
-
// the worker's edge cache absorbs the load and serves one cached
|
|
2130
|
-
// response to all concurrent agents. Output stays text-rendered
|
|
2131
|
-
// (MCP returns text content) but the wire shape is the structured
|
|
2132
|
-
// JSON the worker produces.
|
|
2133
|
-
registerTool('get_ai_ecosystem_today', 'One-shot composite AI morning brief. Calls /api/today which fans out across all daily TensorFeed feeds (news, 3 paper feeds, HF models/datasets/Spaces, hot GitHub issues, Reddit threads, OpenRouter catalog summary, provider status) and returns a single synthesized text response. Optional sections filter (any subset of news, papers, hf, community, inference, status) and limit_per_section (1-10, default 3). Captured-at timestamps from each underlying snapshot are surfaced so the agent knows recency. Free, no auth.', {
|
|
2134
|
-
sections: z.array(z.enum([
|
|
2135
|
-
'news', 'papers', 'hf', 'community', 'inference', 'status',
|
|
2136
|
-
])).optional().describe('Which subsections to include (default: all six). "papers" covers all 3 paper feeds; "hf" covers models + datasets + spaces; "community" covers hot GitHub issues + Reddit threads; "inference" is the OpenRouter catalog summary.'),
|
|
2137
|
-
limit_per_section: z.number().min(1).max(10).optional().describe('Max items per subsection (default 3, max 10). The output is intentionally terse so an agent can call this without burning context.'),
|
|
2138
|
-
}, async ({ sections, limit_per_section }) => {
|
|
2139
|
-
const params = new URLSearchParams();
|
|
2140
|
-
if (sections && sections.length > 0)
|
|
2141
|
-
params.set('sections', sections.join(','));
|
|
2142
|
-
if (typeof limit_per_section === 'number')
|
|
2143
|
-
params.set('limit', String(limit_per_section));
|
|
2144
|
-
const path = `/today${params.toString() ? `?${params.toString()}` : ''}`;
|
|
2145
|
-
let brief;
|
|
2146
|
-
try {
|
|
2147
|
-
brief = (await fetchJSON(path));
|
|
1099
|
+
// ── Tool: create_watch (1 credit) ───────────────────────────────────
|
|
1100
|
+
// Merges the four former create_*_watch tools (price, status, digest,
|
|
1101
|
+
// leaderboard_rank) into one type-selected tool. Each case below builds
|
|
1102
|
+
// the EXACT body and uses the EXACT output formatting the matching old
|
|
1103
|
+
// create_*_watch tool used, against the same /premium/watches endpoint.
|
|
1104
|
+
registerTool('create_watch', 'Register a webhook watch. type selects what to watch: "price" (a model price change), "status" (a service status transition), "digest" (a scheduled daily or weekly pricing summary), or "leaderboard_rank" (a provider crossing an uptime-rank threshold). Costs 1 credit ($0.02) at registration; the watch lives 90 days and each fire is an HMAC-signed POST to callback_url. Needs a TENSORFEED_TOKEN.', {
|
|
1105
|
+
type: z.enum(['price', 'status', 'digest', 'leaderboard_rank']).describe('What to watch.'),
|
|
1106
|
+
callback_url: z.string().describe('HTTPS URL that receives the HMAC-signed POST when the watch fires.'),
|
|
1107
|
+
secret: z.string().optional().describe('Optional shared secret used to HMAC-sign delivery bodies.'),
|
|
1108
|
+
model: z.string().optional().describe('type=price only. Model name (e.g. "Claude Opus 4.7").'),
|
|
1109
|
+
field: z.enum(['inputPrice', 'outputPrice', 'blended']).optional().describe('type=price only. Which price field to watch.'),
|
|
1110
|
+
cadence: z.enum(['daily', 'weekly']).optional().describe('type=digest only. How often the digest fires.'),
|
|
1111
|
+
provider: z.string().optional().describe('type=status or type=leaderboard_rank only. Provider name or slug (case-insensitive). e.g. anthropic, openai, gemini, bedrock, azure.'),
|
|
1112
|
+
op: z
|
|
1113
|
+
.enum(['lt', 'gt', 'changes', 'becomes', 'drops_below', 'rises_above'])
|
|
1114
|
+
.optional()
|
|
1115
|
+
.describe('type=price uses lt/gt/changes (lt = below threshold, gt = above, changes = any change). type=status uses becomes/changes (becomes = transitions to a specific value; changes = any transition). type=leaderboard_rank uses drops_below/rises_above/changes (rank 1 = best; drops_below: was rank<=N now rank>N; rises_above: was rank>=N now rank<N; changes: any rank movement).'),
|
|
1116
|
+
threshold: z.number().optional().describe('type=price (when op is lt or gt) or type=leaderboard_rank (when op is drops_below or rises_above). Integer rank position for leaderboard_rank.'),
|
|
1117
|
+
value: z.enum(['operational', 'degraded', 'down']).optional().describe('type=status only. Required when op is becomes.'),
|
|
1118
|
+
}, async (args) => {
|
|
1119
|
+
const { type, callback_url, secret, model, field, op, threshold, cadence, provider, value } = args;
|
|
1120
|
+
// The merged schema makes every type-specific field optional (each is
|
|
1121
|
+
// required for only one type). Guard the required fields per type here so a
|
|
1122
|
+
// missing one fails with a clear message instead of an opaque 4xx from the
|
|
1123
|
+
// paid /premium/watches endpoint (which would otherwise burn a request).
|
|
1124
|
+
const missingFields = [];
|
|
1125
|
+
if (type === 'price') {
|
|
1126
|
+
if (!model)
|
|
1127
|
+
missingFields.push('model');
|
|
1128
|
+
if (!field)
|
|
1129
|
+
missingFields.push('field');
|
|
1130
|
+
if (!op)
|
|
1131
|
+
missingFields.push('op');
|
|
2148
1132
|
}
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
1133
|
+
else if (type === 'status') {
|
|
1134
|
+
if (!provider)
|
|
1135
|
+
missingFields.push('provider');
|
|
1136
|
+
if (!op)
|
|
1137
|
+
missingFields.push('op');
|
|
2153
1138
|
}
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
const sectionTexts = [];
|
|
2158
|
-
if (brief.news.available && brief.news.data && brief.news.data.items.length > 0) {
|
|
2159
|
-
const lines = brief.news.data.items.map((a, i) => ` ${i + 1}. ${a.title} (${a.source})\n ${a.url}`).join('\n');
|
|
2160
|
-
sectionTexts.push(`📰 NEWS\n${lines}`);
|
|
1139
|
+
else if (type === 'digest') {
|
|
1140
|
+
if (!cadence)
|
|
1141
|
+
missingFields.push('cadence');
|
|
2161
1142
|
}
|
|
2162
|
-
if (
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
if (
|
|
2166
|
-
|
|
2167
|
-
const authors = p.authors.slice(0, 2).join(', ') + (p.authors.length > 2 ? ' et al.' : '');
|
|
2168
|
-
const venue = p.venue ? ` ${p.venue}.` : '';
|
|
2169
|
-
return ` ${i + 1}. ${p.title}${venue} ${authors}. ${fmtCount(p.citationCount)} citations`;
|
|
2170
|
-
}).join('\n');
|
|
2171
|
-
bits.push(` By citation count (Semantic Scholar):\n${lines}`);
|
|
2172
|
-
}
|
|
2173
|
-
const ax = brief.papers.data.arxiv_recent;
|
|
2174
|
-
if (ax.available && ax.data && ax.data.items.length > 0) {
|
|
2175
|
-
const lines = ax.data.items.map((p, i) => {
|
|
2176
|
-
const date = p.publishedAt ? p.publishedAt.slice(0, 10) : '';
|
|
2177
|
-
const authors = p.authors.slice(0, 2).join(', ') + (p.authors.length > 2 ? ' et al.' : '');
|
|
2178
|
-
return ` ${i + 1}. ${p.title} (arXiv:${p.arxivId}, ${date}). ${authors}`;
|
|
2179
|
-
}).join('\n');
|
|
2180
|
-
bits.push(` Just submitted (arXiv recent):\n${lines}`);
|
|
2181
|
-
}
|
|
2182
|
-
const hd = brief.papers.data.hf_daily;
|
|
2183
|
-
if (hd.available && hd.data && hd.data.items.length > 0) {
|
|
2184
|
-
const lines = hd.data.items.map((p, i) => ` ${i + 1}. ${p.title} (${p.upvotes} upvotes, ${p.num_comments} comments)`).join('\n');
|
|
2185
|
-
bits.push(` HF editor picks today:\n${lines}`);
|
|
2186
|
-
}
|
|
2187
|
-
if (bits.length > 0)
|
|
2188
|
-
sectionTexts.push(`📄 PAPERS\n${bits.join('\n')}`);
|
|
1143
|
+
else if (type === 'leaderboard_rank') {
|
|
1144
|
+
if (!provider)
|
|
1145
|
+
missingFields.push('provider');
|
|
1146
|
+
if (!op)
|
|
1147
|
+
missingFields.push('op');
|
|
2189
1148
|
}
|
|
2190
|
-
if (
|
|
2191
|
-
|
|
2192
|
-
const { models, datasets, spaces } = brief.hf.data;
|
|
2193
|
-
if (models.length > 0) {
|
|
2194
|
-
bits.push(` Models (top by downloads):\n` + models.map((x, i) => ` ${i + 1}. ${x.id} (${fmtCount(x.downloads)} downloads)`).join('\n'));
|
|
2195
|
-
}
|
|
2196
|
-
if (datasets.length > 0) {
|
|
2197
|
-
bits.push(` Datasets (top by downloads):\n` + datasets.map((x, i) => ` ${i + 1}. ${x.id} (${fmtCount(x.downloads)} downloads)`).join('\n'));
|
|
2198
|
-
}
|
|
2199
|
-
if (spaces.length > 0) {
|
|
2200
|
-
bits.push(` Spaces (top by likes):\n` + spaces.map((x, i) => ` ${i + 1}. ${x.id} (${fmtCount(x.likes)} likes)`).join('\n'));
|
|
2201
|
-
}
|
|
2202
|
-
if (bits.length > 0)
|
|
2203
|
-
sectionTexts.push(`🤗 HUGGING FACE\n${bits.join('\n')}`);
|
|
1149
|
+
if (missingFields.length > 0) {
|
|
1150
|
+
return { content: [{ type: 'text', text: `type=${type} requires: ${missingFields.join(', ')}.` }] };
|
|
2204
1151
|
}
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
const
|
|
2214
|
-
|
|
1152
|
+
switch (type) {
|
|
1153
|
+
case 'price': {
|
|
1154
|
+
const body = {
|
|
1155
|
+
spec: { type: 'price', model, field, op, ...(typeof threshold === 'number' ? { threshold } : {}) },
|
|
1156
|
+
callback_url,
|
|
1157
|
+
};
|
|
1158
|
+
if (secret !== undefined)
|
|
1159
|
+
body.secret = secret;
|
|
1160
|
+
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
|
|
1161
|
+
return {
|
|
1162
|
+
content: [
|
|
1163
|
+
{
|
|
1164
|
+
type: 'text',
|
|
1165
|
+
text: `Created watch ${data.watch.id} (expires ${data.watch.expires_at}). Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1166
|
+
},
|
|
1167
|
+
],
|
|
1168
|
+
};
|
|
2215
1169
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
1170
|
+
case 'status': {
|
|
1171
|
+
const body = {
|
|
1172
|
+
spec: { type: 'status', provider, op, ...(value ? { value } : {}) },
|
|
1173
|
+
callback_url,
|
|
1174
|
+
};
|
|
1175
|
+
if (secret !== undefined)
|
|
1176
|
+
body.secret = secret;
|
|
1177
|
+
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
|
|
1178
|
+
return {
|
|
1179
|
+
content: [
|
|
1180
|
+
{
|
|
1181
|
+
type: 'text',
|
|
1182
|
+
text: `Created watch ${data.watch.id} (expires ${data.watch.expires_at}). Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1183
|
+
},
|
|
1184
|
+
],
|
|
1185
|
+
};
|
|
2230
1186
|
}
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
1187
|
+
case 'digest': {
|
|
1188
|
+
const body = {
|
|
1189
|
+
spec: { type: 'digest', cadence },
|
|
1190
|
+
callback_url,
|
|
1191
|
+
};
|
|
1192
|
+
if (secret !== undefined)
|
|
1193
|
+
body.secret = secret;
|
|
1194
|
+
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
|
|
1195
|
+
return {
|
|
1196
|
+
content: [
|
|
1197
|
+
{
|
|
1198
|
+
type: 'text',
|
|
1199
|
+
text: `Created ${cadence} digest watch ${data.watch.id} (expires ${data.watch.expires_at}). First fire at the next 7am UTC daily cron. Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1200
|
+
},
|
|
1201
|
+
],
|
|
1202
|
+
};
|
|
2237
1203
|
}
|
|
2238
|
-
|
|
2239
|
-
const
|
|
2240
|
-
|
|
1204
|
+
case 'leaderboard_rank': {
|
|
1205
|
+
const spec = { type: 'leaderboard_rank', provider, op };
|
|
1206
|
+
if (threshold !== undefined)
|
|
1207
|
+
spec.threshold = threshold;
|
|
1208
|
+
const body = { spec, callback_url };
|
|
1209
|
+
if (secret !== undefined)
|
|
1210
|
+
body.secret = secret;
|
|
1211
|
+
const data = (await fetchJSON('/premium/watches', { method: 'POST', body, auth: true }));
|
|
1212
|
+
const desc = op === 'changes'
|
|
1213
|
+
? `${provider} rank changes`
|
|
1214
|
+
: `${provider} ${(op ?? '').replace('_', ' ')} #${threshold}`;
|
|
1215
|
+
return {
|
|
1216
|
+
content: [
|
|
1217
|
+
{
|
|
1218
|
+
type: 'text',
|
|
1219
|
+
text: `Created leaderboard rank watch ${data.watch.id} (${desc}). Expires ${data.watch.expires_at}. Credits remaining: ${data.billing?.credits_remaining ?? '?'}`,
|
|
1220
|
+
},
|
|
1221
|
+
],
|
|
1222
|
+
};
|
|
2241
1223
|
}
|
|
2242
1224
|
}
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
type: 'text',
|
|
2248
|
-
text: 'No data available yet. The daily snapshots have not run, or all upstream feeds errored. Try again in a few minutes.',
|
|
2249
|
-
},
|
|
2250
|
-
],
|
|
2251
|
-
};
|
|
2252
|
-
}
|
|
2253
|
-
const header = `TensorFeed AI ecosystem brief, generated ${brief.generated_at}`;
|
|
2254
|
-
const body = sectionTexts.join('\n\n');
|
|
2255
|
-
return {
|
|
2256
|
-
content: [
|
|
2257
|
-
{
|
|
2258
|
-
type: 'text',
|
|
2259
|
-
text: `${header}\n\n${body}\n\nFor any subsection, call the underlying tool directly: get_ai_news, get_ai_papers_trending, get_arxiv_recent, get_hf_daily_papers, get_hf_trending, get_hot_issues, get_reddit_trending, get_openrouter_models, get_ai_status.`,
|
|
2260
|
-
},
|
|
2261
|
-
],
|
|
2262
|
-
};
|
|
2263
|
-
});
|
|
2264
|
-
// ── Tool: get_ai_cves_latest ────────────────────────────────────────
|
|
2265
|
-
registerTool('get_ai_cves_latest', 'Get metadata for the most-recent AI-stack CVE batch plus the first 25 papers. Source: TensorFeed AI-CVE intelligence feed, derived from GitHub Security Advisories (CC BY 4.0). Each paper carries cve_ids, affected_products, affected_version_ranges, fixed_versions, exploited_in_wild, severity_label, and source_url. For the full set use get_ai_cves_feed with offset; for AI-stack-filtered + categorized + sorted papers use the paid /api/premium/ai-cves/* endpoints. Free, no auth.', {}, async () => {
|
|
2266
|
-
const data = (await fetchJSON('/ai-cves/latest'));
|
|
2267
|
-
if (!data.batch_id) {
|
|
2268
|
-
return { content: [{ type: 'text', text: 'No AI-CVE batch ingested yet. Check back once TensorFeed CC has POSTed the next clean batch from the extraction pipeline.' }] };
|
|
2269
|
-
}
|
|
2270
|
-
const head = `AI-CVE batch ${data.batch_id} extracted ${data.extracted_at}. Total ${data.total_papers} papers, ${data.ai_flagged_count} AI-flagged. License ${data.source_license} (${data.source_attribution}).`;
|
|
2271
|
-
const lines = data.papers
|
|
2272
|
-
.map((p, i) => {
|
|
2273
|
-
const cves = p.cve_ids.length > 0 ? p.cve_ids.join(', ') : '(no CVE)';
|
|
2274
|
-
const products = p.affected_products.slice(0, 3).join(', ');
|
|
2275
|
-
return `${i + 1}. ${cves} | ${products} | sev=${p.severity_label} | exploited=${p.exploited_in_wild}\n ${p.source_url}`;
|
|
2276
|
-
})
|
|
2277
|
-
.join('\n');
|
|
2278
|
-
return {
|
|
2279
|
-
content: [
|
|
2280
|
-
{
|
|
2281
|
-
type: 'text',
|
|
2282
|
-
text: `${head}\n\nFirst 25 papers:\n${lines}\n\nFor AI-stack-filtered + categorized + sorted papers: /api/premium/ai-cves/ai-stack-cves (1 credit, x402-paid). For only papers actively exploited in the wild: /api/premium/ai-cves/exploited-in-wild (1 credit). For single-CVE lookup: /api/premium/ai-cves/cve?id=CVE-YYYY-NNNNN (1 credit).\n\nssvc_verdict gives the CISA SSVC act/attend/track decision for a CVE; stack_safety_verdict gates your deploy by package.`,
|
|
2283
|
-
},
|
|
2284
|
-
],
|
|
2285
|
-
};
|
|
2286
|
-
});
|
|
2287
|
-
// ── Tool: get_ai_cves_feed ──────────────────────────────────────────
|
|
2288
|
-
registerTool('get_ai_cves_feed', 'Get a paginated slice of the most-recent AI-stack CVE batch. limit is capped at 50 (the premium tier handles bulk). Source: TensorFeed AI-CVE feed, derived from GitHub Security Advisories (CC BY 4.0). Free, no auth.', {
|
|
2289
|
-
limit: z.number().min(1).max(50).optional().describe('Number of papers to return (default 25, max 50).'),
|
|
2290
|
-
offset: z.number().min(0).optional().describe('Pagination offset (default 0).'),
|
|
2291
|
-
}, async ({ limit, offset }) => {
|
|
2292
|
-
const params = new URLSearchParams();
|
|
2293
|
-
if (typeof limit === 'number')
|
|
2294
|
-
params.set('limit', String(limit));
|
|
2295
|
-
if (typeof offset === 'number')
|
|
2296
|
-
params.set('offset', String(offset));
|
|
2297
|
-
const path = `/ai-cves/feed${params.toString() ? `?${params.toString()}` : ''}`;
|
|
2298
|
-
const data = (await fetchJSON(path));
|
|
2299
|
-
if (!data.batch_id) {
|
|
2300
|
-
return { content: [{ type: 'text', text: 'No AI-CVE batch ingested yet.' }] };
|
|
2301
|
-
}
|
|
2302
|
-
const head = `AI-CVE feed batch ${data.batch_id}: showing offset ${data.offset} to ${data.offset + data.papers.length} of ${data.total} total.`;
|
|
2303
|
-
const lines = data.papers
|
|
2304
|
-
.map((p, i) => {
|
|
2305
|
-
const cves = p.cve_ids.length > 0 ? p.cve_ids.join(', ') : '(no CVE)';
|
|
2306
|
-
const products = p.affected_products.slice(0, 3).join(', ');
|
|
2307
|
-
return `${i + 1}. ${cves} | ${products} | sev=${p.severity_label} | exploited=${p.exploited_in_wild}\n ${p.source_url}`;
|
|
2308
|
-
})
|
|
2309
|
-
.join('\n');
|
|
2310
|
-
return { content: [{ type: 'text', text: `${head}\n\n${lines}` }] };
|
|
2311
|
-
});
|
|
2312
|
-
// ── Tool: get_ai_cves_stats ─────────────────────────────────────────
|
|
2313
|
-
registerTool('get_ai_cves_stats', 'Get aggregate counts across the most-recent AI-stack CVE batch: distribution by severity (critical/high/medium/low/unstated), distribution by exploitation status (stated_yes/stated_no/unstated), and top 10 vendors by frequency. Source: TensorFeed AI-CVE feed (GitHub Security Advisories, CC BY 4.0). Free, no auth.', {}, async () => {
|
|
2314
|
-
const data = (await fetchJSON('/ai-cves/stats'));
|
|
2315
|
-
if (!data.batch_id) {
|
|
2316
|
-
return { content: [{ type: 'text', text: 'No AI-CVE batch ingested yet.' }] };
|
|
2317
|
-
}
|
|
2318
|
-
const sev = Object.entries(data.by_severity)
|
|
2319
|
-
.map(([k, v]) => ` ${k}: ${v}`)
|
|
2320
|
-
.join('\n');
|
|
2321
|
-
const exp = Object.entries(data.by_exploitation)
|
|
2322
|
-
.map(([k, v]) => ` ${k}: ${v}`)
|
|
2323
|
-
.join('\n');
|
|
2324
|
-
const vendors = data.top_vendors.map((v, i) => ` ${i + 1}. ${v.vendor}: ${v.count}`).join('\n');
|
|
2325
|
-
return {
|
|
2326
|
-
content: [
|
|
2327
|
-
{
|
|
2328
|
-
type: 'text',
|
|
2329
|
-
text: `AI-CVE batch ${data.batch_id}: ${data.total_papers} total papers.\n\nBy severity:\n${sev}\n\nBy exploited-in-wild:\n${exp}\n\nTop vendors:\n${vendors}`,
|
|
2330
|
-
},
|
|
2331
|
-
],
|
|
2332
|
-
};
|
|
2333
|
-
});
|
|
2334
|
-
// ── Tool: get_ai_company_filings (free) ─────────────────────────────
|
|
2335
|
-
registerTool('get_ai_company_filings', 'Get recent SEC filings (10-K, 10-Q, 8-K, etc.) for one AI bellwether ticker. Cohort: NVDA, AMD, AVGO, TSM, ARM, MSFT, GOOGL, AMZN, ORCL, PLTR, SMCI, AAPL, META, TSLA. Source: data.sec.gov, public domain. Refreshed every 6 hours. Free, no auth.', {
|
|
2336
|
-
ticker: z.string().describe('AI bellwether ticker (case-insensitive). One of NVDA, AMD, AVGO, TSM, ARM, MSFT, GOOGL, AMZN, ORCL, PLTR, SMCI, AAPL, META, TSLA.'),
|
|
2337
|
-
limit: z.number().min(1).max(50).optional().describe('Max filings to return (default 10, max 50).'),
|
|
2338
|
-
}, async ({ ticker, limit }) => {
|
|
2339
|
-
const params = new URLSearchParams({ ticker: ticker.toUpperCase() });
|
|
2340
|
-
params.set('limit', String(limit ?? 10));
|
|
2341
|
-
const data = (await fetchJSON(`/sec/filings/recent?${params}`));
|
|
2342
|
-
if (!data.ok || !data.filings || data.filings.length === 0) {
|
|
2343
|
-
return {
|
|
2344
|
-
content: [
|
|
2345
|
-
{
|
|
2346
|
-
type: 'text',
|
|
2347
|
-
text: `No filings returned for ${ticker.toUpperCase()}. The ticker may not be in the AI bellwether cohort; supported tickers: NVDA, AMD, AVGO, TSM, ARM, MSFT, GOOGL, AMZN, ORCL, PLTR, SMCI, AAPL, META, TSLA.`,
|
|
2348
|
-
},
|
|
2349
|
-
],
|
|
2350
|
-
};
|
|
2351
|
-
}
|
|
2352
|
-
const lines = data.filings
|
|
2353
|
-
.map((f, i) => `${i + 1}. ${f.form} on ${f.filing_date}\n ${f.primary_doc_description}\n ${f.primary_doc_url}`)
|
|
2354
|
-
.join('\n\n');
|
|
2355
|
-
return {
|
|
2356
|
-
content: [
|
|
2357
|
-
{
|
|
2358
|
-
type: 'text',
|
|
2359
|
-
text: `${ticker.toUpperCase()} recent SEC filings (captured ${data.capturedAt ?? 'unknown'}, ${data.filings_count} returned):\n\n${lines}`,
|
|
2360
|
-
},
|
|
2361
|
-
],
|
|
2362
|
-
};
|
|
2363
|
-
});
|
|
2364
|
-
// ── Tool: premium_ai_company (1 credit) ─────────────────────────────
|
|
2365
|
-
registerTool('premium_ai_company', 'Get the per-ticker AI intelligence envelope for one AI bellwether: latest 10 SEC filings, latest 10 AI-tagged news mentions filtered by curated aliases, strategic and equity funding rounds where the company is a lead or notable investor, and cohort metadata. One call replaces four free siblings (sec/filings, news, funding, cohort) plus the agent-side alias filter. Costs 1 credit ($0.02). Strict-premium; ticker must be in the cohort.', {
|
|
2366
|
-
ticker: z
|
|
1225
|
+
}, CREATE_TOOL);
|
|
1226
|
+
// ── Tool: delete_watch ──────────────────────────────────────────────
|
|
1227
|
+
registerTool('delete_watch', 'Delete one of your active webhook watches by id. Free, requires TENSORFEED_TOKEN.', {
|
|
1228
|
+
watch_id: z
|
|
2367
1229
|
.string()
|
|
2368
|
-
.regex(/^[A-Za-
|
|
2369
|
-
.
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
.map((f, i) => ` ${i + 1}. ${f.form} on ${f.filing_date}: ${f.primary_doc_description}`)
|
|
2376
|
-
.join('\n');
|
|
2377
|
-
const newsList = data.news.items
|
|
2378
|
-
.slice(0, 10)
|
|
2379
|
-
.map((n, i) => ` ${i + 1}. ${n.title}\n ${n.source} (${n.publishedAt})`)
|
|
2380
|
-
.join('\n\n');
|
|
2381
|
-
const fundingList = data.funding_as_investor.items
|
|
2382
|
-
.map((r, i) => ` ${i + 1}. ${r.company} ${r.stage}, $${r.amountM}M, ${r.announcedDate}`)
|
|
2383
|
-
.join('\n');
|
|
2384
|
-
const billing = data.billing
|
|
2385
|
-
? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining ?? '?'}.`
|
|
2386
|
-
: '';
|
|
2387
|
-
return {
|
|
2388
|
-
content: [
|
|
2389
|
-
{
|
|
2390
|
-
type: 'text',
|
|
2391
|
-
text: `${data.ticker} (${data.company.display_name}, ${data.company.exchange}, ${data.company.category})\nAI angle: ${data.company.ai_angle}\nCaptured: ${data.capturedAt}\n\nRecent SEC filings (${data.filings.count}):\n${filingsList || ' none'}\n\nAI news mentions (${data.news.count}, aliases: ${data.news.aliases_used.join(', ')}):\n${newsList || ' none'}\n\nFunding rounds as lead or notable investor (${data.funding_as_investor.count}):\n${fundingList || ' none'}${billing}`,
|
|
2392
|
-
},
|
|
2393
|
-
],
|
|
2394
|
-
};
|
|
2395
|
-
});
|
|
2396
|
-
// ── Tool: get_x402_summary (free) ───────────────────────────────────
|
|
2397
|
-
registerTool('get_x402_summary', 'Get ecosystem-level x402 USDC settlement rollup on Base mainnet. Returns volume_usdc, count, unique_publishers, and change_vs_prior_window across a 24h, 7d, or 30d window. TensorFeed indexes Base USDC Transfer events filtered to wallets self-published by x402-compliant publishers in their /.well-known/x402.json manifests. Forward-only from 2026-05-28; ~6 min worst-case freshness (10 min SLA). Free, capped to one window per call.', {
|
|
2398
|
-
window: z.enum(['24h', '7d', '30d']).optional().describe('Aggregation window. Defaults to 24h if omitted.'),
|
|
2399
|
-
}, async ({ window }) => {
|
|
2400
|
-
const w = window ?? '24h';
|
|
2401
|
-
const data = (await fetchJSON(`/x402-index/summary?window=${w}`));
|
|
2402
|
-
return {
|
|
2403
|
-
content: [
|
|
2404
|
-
{
|
|
2405
|
-
type: 'text',
|
|
2406
|
-
text: `x402 settlement rollup (window ${data.window}, captured ${data.captured_at}):\n volume_usdc: ${data.volume_usdc}\n count: ${data.count}\n unique_publishers: ${data.unique_publishers}\n change vs prior window: volume ${data.change_vs_prior_window.volume_pct}%, count ${data.change_vs_prior_window.count_pct}%`,
|
|
2407
|
-
},
|
|
2408
|
-
],
|
|
2409
|
-
};
|
|
2410
|
-
});
|
|
2411
|
-
// ── Tool: get_x402_publishers (free) ────────────────────────────────
|
|
2412
|
-
registerTool('get_x402_publishers', 'List x402-compliant publishers TensorFeed currently indexes. Each entry: domain, pay_to_wallets, first_seen, last_crawled, last_event_at, last_crawl_error. Publishers are auto-discovered daily via /.well-known/x402.json crawls. Use this to enumerate which publishers can then be queried in depth via premium_x402_publisher_receipts. Free, no input.', {}, async () => {
|
|
2413
|
-
const data = (await fetchJSON('/x402-index/publishers'));
|
|
2414
|
-
if (data.count === 0) {
|
|
2415
|
-
return { content: [{ type: 'text', text: 'No x402 publishers indexed yet.' }] };
|
|
2416
|
-
}
|
|
2417
|
-
// Cap the rendered list so a large index cannot flood the agent's
|
|
2418
|
-
// context. Sanitize layer also enforces a hard char cap downstream.
|
|
2419
|
-
const MAX_PUBLISHERS = 50;
|
|
2420
|
-
const shown = data.publishers.slice(0, MAX_PUBLISHERS);
|
|
2421
|
-
const lines = shown
|
|
2422
|
-
.map((p, i) => `${i + 1}. ${p.domain}\n wallets: ${p.pay_to_wallets.join(', ')}\n first_seen: ${p.first_seen}${p.last_event_at ? `\n last_event_at: ${p.last_event_at}` : ''}${p.last_crawled ? `\n last_crawled: ${p.last_crawled}` : ''}${p.last_crawl_error ? `\n last_crawl_error: ${p.last_crawl_error}` : ''}`)
|
|
2423
|
-
.join('\n\n');
|
|
2424
|
-
const more = data.count > shown.length ? `\n\n...and ${data.count - shown.length} more (showing first ${shown.length}).` : '';
|
|
2425
|
-
return {
|
|
2426
|
-
content: [
|
|
2427
|
-
{
|
|
2428
|
-
type: 'text',
|
|
2429
|
-
text: `x402 publishers (${data.count}, captured ${data.captured_at}):\n\n${lines}${more}\n\nx402_publisher_verdict returns a signed trust verdict on one publisher before you pay it.`,
|
|
2430
|
-
},
|
|
2431
|
-
],
|
|
2432
|
-
};
|
|
2433
|
-
});
|
|
2434
|
-
// ── Tool: get_x402_leaderboard (free) ───────────────────────────────
|
|
2435
|
-
registerTool('get_x402_leaderboard', 'Top publishers by x402 USDC settlement volume across a 24h, 7d, or 30d window. Each leader: rank, domain, volume_usdc, count, share_pct of the sliced total. Limit clamped 1 to 25. Useful for surfacing which x402 services are receiving the most agent traffic. Free.', {
|
|
2436
|
-
window: z.enum(['24h', '7d', '30d']).optional().describe('Aggregation window. Defaults to 24h.'),
|
|
2437
|
-
limit: z.number().min(1).max(25).optional().describe('Top N publishers to return. Default 10, max 25.'),
|
|
2438
|
-
}, async ({ window, limit }) => {
|
|
2439
|
-
const w = window ?? '24h';
|
|
2440
|
-
const l = limit ?? 10;
|
|
2441
|
-
const data = (await fetchJSON(`/x402-index/leaderboard?window=${w}&limit=${l}`));
|
|
2442
|
-
if (data.leaders.length === 0) {
|
|
2443
|
-
return { content: [{ type: 'text', text: `No x402 leaders for window ${data.window}.` }] };
|
|
2444
|
-
}
|
|
2445
|
-
const lines = data.leaders
|
|
2446
|
-
.map((e) => ` ${e.rank}. ${e.domain}: $${e.volume_usdc} across ${e.count} settlements (${e.share_pct}% share)`)
|
|
2447
|
-
.join('\n');
|
|
2448
|
-
return {
|
|
2449
|
-
content: [
|
|
2450
|
-
{
|
|
2451
|
-
type: 'text',
|
|
2452
|
-
text: `x402 leaderboard (window ${data.window}, captured ${data.captured_at}):\n${lines}`,
|
|
2453
|
-
},
|
|
2454
|
-
],
|
|
2455
|
-
};
|
|
2456
|
-
});
|
|
2457
|
-
// ── Tool: get_x402_recent (free) ────────────────────────────────────
|
|
2458
|
-
registerTool('get_x402_recent', 'Most recent x402 USDC settlement events newest-first. Each event: tx_hash, block, ts, from_address, to_address, amount_usdc, publisher_domain, base_explorer_url (basescan.org link). Backed by a 100-event ring buffer updated on every indexer cron tick. Limit clamped 1 to 50, default 20. Free.', {
|
|
2459
|
-
limit: z.number().min(1).max(50).optional().describe('Number of most-recent events. Default 20, max 50.'),
|
|
2460
|
-
}, async ({ limit }) => {
|
|
2461
|
-
const l = limit ?? 20;
|
|
2462
|
-
const data = (await fetchJSON(`/x402-index/recent?limit=${l}`));
|
|
2463
|
-
if (data.count === 0) {
|
|
2464
|
-
return { content: [{ type: 'text', text: 'No recent x402 settlement events.' }] };
|
|
2465
|
-
}
|
|
2466
|
-
const lines = data.events
|
|
2467
|
-
.map((e, i) => `${i + 1}. ${e.ts} block ${e.block}\n ${e.publisher_domain}: $${e.amount_usdc} USDC\n from ${e.from_address} to ${e.to_address}\n ${e.base_explorer_url}`)
|
|
2468
|
-
.join('\n\n');
|
|
2469
|
-
return {
|
|
2470
|
-
content: [
|
|
2471
|
-
{
|
|
2472
|
-
type: 'text',
|
|
2473
|
-
text: `Recent x402 settlements (${data.count}, captured ${data.captured_at}):\n\n${lines}`,
|
|
2474
|
-
},
|
|
2475
|
-
],
|
|
2476
|
-
};
|
|
2477
|
-
});
|
|
2478
|
-
// ── Tool: premium_x402_publisher_receipts (1 credit) ────────────────
|
|
2479
|
-
registerTool('premium_x402_publisher_receipts', 'Per-publisher receipt feed for one x402-compliant domain across a from/to date range. Returns publisher meta (domain, pay_to_wallets, first_seen), window rollup (volume_usdc, count, avg_amount, daily_series), and attribution. \n\nVS FREE: get_x402_publishers tells you WHICH publishers exist but only with rolled-up scalar counters. This endpoint composes the per-day rollup across a date range with avg_amount, daily series for charts, and structured attribution license metadata in one paid call. An agent building a per-publisher analytics dashboard would otherwise need to call get_x402_publishers + reconstruct daily series from get_x402_summary per-day (impossible because /summary returns ecosystem totals not per-publisher) or wait for the next free-tier expansion. The premium endpoint saves the integration overhead and serves a forensic / compliance-grade payload for $0.02. \n\nForward-only index. Costs 1 credit ($0.02). Strict-premium; domain must exist in get_x402_publishers, otherwise 404.', {
|
|
2480
|
-
domain: z.string().describe('Publisher domain to query, e.g. tensorfeed.ai. Must exist in get_x402_publishers result.'),
|
|
2481
|
-
from: z.string().describe('Inclusive start date YYYY-MM-DD.'),
|
|
2482
|
-
to: z.string().describe('Inclusive end date YYYY-MM-DD.'),
|
|
2483
|
-
}, async ({ domain, from, to }) => {
|
|
2484
|
-
const params = new URLSearchParams({ from, to });
|
|
2485
|
-
const data = (await fetchJSON(`/premium/x402-index/publisher/${encodeURIComponent(domain)}?${params}`, { auth: true }));
|
|
2486
|
-
const series = data.rollup.daily_series
|
|
2487
|
-
.map((d) => ` ${d.date}: $${d.volume_usdc} across ${d.count}`)
|
|
2488
|
-
.join('\n');
|
|
2489
|
-
const billing = data.billing
|
|
2490
|
-
? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining ?? '?'}.`
|
|
2491
|
-
: '';
|
|
2492
|
-
return {
|
|
2493
|
-
content: [
|
|
2494
|
-
{
|
|
2495
|
-
type: 'text',
|
|
2496
|
-
text: `${data.publisher.domain} (first_seen ${data.publisher.first_seen})\nwallets: ${data.publisher.pay_to_wallets.join(', ')}\nwindow: ${data.window.from} to ${data.window.to} (${data.window.days} days)\n\nRollup:\n volume_usdc: ${data.rollup.volume_usdc}\n count: ${data.rollup.count}\n avg_amount: ${data.rollup.avg_amount}\n\nDaily series:\n${series || ' none'}\n\nAttribution: ${data.attribution}\nLicense: ${data.license}${billing}`,
|
|
2497
|
-
},
|
|
2498
|
-
],
|
|
2499
|
-
};
|
|
2500
|
-
});
|
|
2501
|
-
// ── Tool: premium_x402_series (1 credit) ────────────────────────────
|
|
2502
|
-
registerTool('premium_x402_series', 'Time-series of ecosystem or per-publisher x402 settlement volume or count across a from/to date range, chart-feeding. Required: metric (volume or count), granularity (day or hour), from + to (YYYY-MM-DD). Optional: domain filter (omit for ecosystem). \n\nVS FREE: get_x402_summary returns one rolled-up scalar per call for a fixed window length (24h / 7d / 30d). For ANY chart or dashboard, the caller needs a series, not a scalar. The premium endpoint returns the full series in one call instead of N calls + N rate-limit budget burns. For a publisher-scoped series, it is the ONLY way to get per-publisher daily data (get_x402_summary is ecosystem-only). \n\nMVP supports granularity=day; hour returns an empty series with an attribution note (hourly rollups land in a future indexer version). Costs 1 credit ($0.02).', {
|
|
2503
|
-
metric: z.enum(['volume', 'count']).describe('Which series to return.'),
|
|
2504
|
-
granularity: z.enum(['day', 'hour']).describe('Bucket size. MVP: day only; hour returns empty series.'),
|
|
2505
|
-
from: z.string().describe('Inclusive start date YYYY-MM-DD.'),
|
|
2506
|
-
to: z.string().describe('Inclusive end date YYYY-MM-DD.'),
|
|
2507
|
-
domain: z.string().optional().describe('Optional publisher domain filter. Omit for ecosystem-wide series.'),
|
|
2508
|
-
}, async ({ metric, granularity, from, to, domain }) => {
|
|
2509
|
-
const params = new URLSearchParams({ metric, granularity, from, to });
|
|
2510
|
-
if (domain)
|
|
2511
|
-
params.set('domain', domain);
|
|
2512
|
-
const data = (await fetchJSON(`/premium/x402-index/series?${params}`, { auth: true }));
|
|
2513
|
-
const scope = domain ? `domain=${domain}` : 'ecosystem-wide';
|
|
2514
|
-
const seriesLines = data.series.length
|
|
2515
|
-
? data.series.map((p) => ` ${p.ts}: ${p.value}`).join('\n')
|
|
2516
|
-
: ' (empty)';
|
|
2517
|
-
const billing = data.billing
|
|
2518
|
-
? `\n\nCharged ${data.billing.credits_charged} credit. Remaining: ${data.billing.credits_remaining ?? '?'}.`
|
|
2519
|
-
: '';
|
|
2520
|
-
return {
|
|
2521
|
-
content: [
|
|
2522
|
-
{
|
|
2523
|
-
type: 'text',
|
|
2524
|
-
text: `x402 ${data.metric} series (${data.granularity}, ${scope})\nwindow: ${data.window.from} to ${data.window.to}\n\nSeries:\n${seriesLines}\n\nAttribution: ${data.attribution}${billing}`,
|
|
2525
|
-
},
|
|
2526
|
-
],
|
|
2527
|
-
};
|
|
2528
|
-
});
|
|
1230
|
+
.regex(/^[A-Za-z0-9_-]+$/, 'watch_id may only contain letters, digits, underscores, and hyphens')
|
|
1231
|
+
.max(128)
|
|
1232
|
+
.describe('The wat_... id from create_watch / list_watches'),
|
|
1233
|
+
}, async ({ watch_id }) => {
|
|
1234
|
+
await fetchJSON(`/premium/watches/${encodeURIComponent(watch_id)}`, { method: 'DELETE', auth: true });
|
|
1235
|
+
return { content: [{ type: 'text', text: `Deleted watch ${watch_id}.` }] };
|
|
1236
|
+
}, DELETE_TOOL);
|
|
2529
1237
|
// ── Start ───────────────────────────────────────────────────────────
|
|
2530
1238
|
async function main() {
|
|
2531
1239
|
const transport = new StdioServerTransport();
|