@thispointon/kondi-chat 0.1.2
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/LICENSE +21 -0
- package/README.md +556 -0
- package/bin/kondi-chat +56 -0
- package/bin/kondi-chat.js +72 -0
- package/package.json +55 -0
- package/scripts/demo.tape +49 -0
- package/scripts/postinstall.cjs +103 -0
- package/src/audit/analytics.ts +261 -0
- package/src/audit/ledger.ts +253 -0
- package/src/audit/telemetry.ts +165 -0
- package/src/cli/backend.ts +675 -0
- package/src/cli/commands.ts +419 -0
- package/src/cli/help.ts +182 -0
- package/src/cli/submit-helpers.ts +159 -0
- package/src/cli/submit.ts +539 -0
- package/src/cli/wizard.ts +121 -0
- package/src/context/bootstrap.ts +138 -0
- package/src/context/budget.ts +100 -0
- package/src/context/manager.ts +666 -0
- package/src/context/memory.ts +160 -0
- package/src/context/preflight.ts +176 -0
- package/src/context/project-brain.ts +101 -0
- package/src/context/receipts.ts +108 -0
- package/src/context/skills.ts +154 -0
- package/src/context/symbol-index.ts +240 -0
- package/src/council/profiles.ts +137 -0
- package/src/council/tool.ts +138 -0
- package/src/council-engine/cli/council-artifacts.ts +230 -0
- package/src/council-engine/cli/council-config.ts +178 -0
- package/src/council-engine/cli/council-session-export.ts +116 -0
- package/src/council-engine/cli/kondi.ts +98 -0
- package/src/council-engine/cli/llm-caller.ts +229 -0
- package/src/council-engine/cli/localStorage-shim.ts +119 -0
- package/src/council-engine/cli/node-platform.ts +68 -0
- package/src/council-engine/cli/run-council.ts +481 -0
- package/src/council-engine/cli/run-pipeline.ts +772 -0
- package/src/council-engine/cli/session-export.ts +153 -0
- package/src/council-engine/configs/councils/analysis.json +101 -0
- package/src/council-engine/configs/councils/code-planning.json +86 -0
- package/src/council-engine/configs/councils/coding.json +89 -0
- package/src/council-engine/configs/councils/debate.json +97 -0
- package/src/council-engine/configs/councils/solo-claude.json +34 -0
- package/src/council-engine/configs/councils/solo-gpt.json +34 -0
- package/src/council-engine/council/coding-orchestrator.ts +1205 -0
- package/src/council-engine/council/context-bootstrap.ts +147 -0
- package/src/council-engine/council/context-inspection.ts +42 -0
- package/src/council-engine/council/context-store.ts +763 -0
- package/src/council-engine/council/deliberation-orchestrator.ts +2762 -0
- package/src/council-engine/council/factory.ts +164 -0
- package/src/council-engine/council/index.ts +201 -0
- package/src/council-engine/council/ledger-store.ts +438 -0
- package/src/council-engine/council/prompts.ts +1689 -0
- package/src/council-engine/council/storage-cleanup.ts +164 -0
- package/src/council-engine/council/store.ts +1110 -0
- package/src/council-engine/council/synthesis.ts +291 -0
- package/src/council-engine/council/types.ts +845 -0
- package/src/council-engine/council/validation.ts +613 -0
- package/src/council-engine/pipeline/build-detect.ts +73 -0
- package/src/council-engine/pipeline/executor.ts +1048 -0
- package/src/council-engine/pipeline/index.ts +9 -0
- package/src/council-engine/pipeline/install-detect.ts +84 -0
- package/src/council-engine/pipeline/memory-store.ts +182 -0
- package/src/council-engine/pipeline/output-parsers.ts +146 -0
- package/src/council-engine/pipeline/run-output.ts +149 -0
- package/src/council-engine/pipeline/session-import.ts +177 -0
- package/src/council-engine/pipeline/store.ts +753 -0
- package/src/council-engine/pipeline/test-detect.ts +82 -0
- package/src/council-engine/pipeline/types.ts +401 -0
- package/src/council-engine/services/deliberationSummary.ts +114 -0
- package/src/council-engine/tsconfig.json +16 -0
- package/src/council-engine/types/mcp.ts +122 -0
- package/src/council-engine/utils/filterTools.ts +73 -0
- package/src/engine/apply.ts +238 -0
- package/src/engine/checkpoints.ts +237 -0
- package/src/engine/consultants.ts +347 -0
- package/src/engine/diff.ts +171 -0
- package/src/engine/errors.ts +102 -0
- package/src/engine/git-tools.ts +246 -0
- package/src/engine/hooks.ts +181 -0
- package/src/engine/loop-guard.ts +155 -0
- package/src/engine/permissions.ts +293 -0
- package/src/engine/pipeline.ts +376 -0
- package/src/engine/sub-agents.ts +133 -0
- package/src/engine/task-card.ts +185 -0
- package/src/engine/task-router.ts +256 -0
- package/src/engine/task-store.ts +86 -0
- package/src/engine/tools.ts +783 -0
- package/src/engine/verify.ts +111 -0
- package/src/mcp/client.ts +225 -0
- package/src/mcp/config.ts +120 -0
- package/src/mcp/tool-manager.ts +192 -0
- package/src/mcp/types.ts +61 -0
- package/src/providers/llm-caller.ts +943 -0
- package/src/providers/rate-limiter.ts +238 -0
- package/src/router/NOTES.md +28 -0
- package/src/router/collector.ts +474 -0
- package/src/router/embeddings.ts +286 -0
- package/src/router/index.ts +299 -0
- package/src/router/intent-router.ts +225 -0
- package/src/router/nn-router.ts +205 -0
- package/src/router/profiles.ts +309 -0
- package/src/router/registry.ts +565 -0
- package/src/router/rules.ts +274 -0
- package/src/router/train.py +408 -0
- package/src/session/store.ts +211 -0
- package/src/test-utils/mock-llm.ts +39 -0
- package/src/types.ts +322 -0
- package/src/web/manager.ts +311 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Tools — web_search and web_fetch, always available.
|
|
3
|
+
*
|
|
4
|
+
* web_search: DuckDuckGo HTML scrape by default (zero config). If
|
|
5
|
+
* BRAVE_SEARCH_API_KEY is set, upgrades to Brave's structured API
|
|
6
|
+
* for better results. Either way the tool is always registered —
|
|
7
|
+
* the model always sees web_search in its tool list.
|
|
8
|
+
*
|
|
9
|
+
* web_fetch: fetches any public URL, strips HTML to readable text.
|
|
10
|
+
* SSRF guards block localhost and private ranges. No API key needed.
|
|
11
|
+
*
|
|
12
|
+
* Both tools work on any machine with Node and an internet connection.
|
|
13
|
+
* No Docker, no MCP server, no external service.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ToolDefinition } from '../types.ts';
|
|
17
|
+
|
|
18
|
+
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
19
|
+
const CACHE_MAX_ENTRIES = 100;
|
|
20
|
+
const MAX_FETCH_BYTES = 1_048_576;
|
|
21
|
+
const MAX_MARKDOWN_BYTES = 20 * 1024;
|
|
22
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
23
|
+
const MAX_REDIRECTS = 5;
|
|
24
|
+
|
|
25
|
+
export interface SearchResult { title: string; url: string; snippet: string; }
|
|
26
|
+
export interface FetchResult { url: string; content: string; contentType: string; sizeBytes: number; truncated?: boolean; }
|
|
27
|
+
|
|
28
|
+
interface CacheEntry { value: unknown; expiresAt: number; }
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// SSRF guards
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const PRIVATE_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0']);
|
|
35
|
+
function isPrivateHost(host: string): boolean {
|
|
36
|
+
const h = host.toLowerCase();
|
|
37
|
+
if (PRIVATE_HOSTS.has(h)) return true;
|
|
38
|
+
if (h.endsWith('.local') || h.endsWith('.internal')) return true;
|
|
39
|
+
if (/^10\./.test(h)) return true;
|
|
40
|
+
if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(h)) return true;
|
|
41
|
+
if (/^192\.168\./.test(h)) return true;
|
|
42
|
+
if (/^169\.254\./.test(h)) return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// HTML processing
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function htmlToPlain(html: string): string {
|
|
51
|
+
let s = html.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
52
|
+
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
53
|
+
.replace(/<nav[\s\S]*?<\/nav>/gi, '')
|
|
54
|
+
.replace(/<footer[\s\S]*?<\/footer>/gi, '')
|
|
55
|
+
.replace(/<header[\s\S]*?<\/header>/gi, '');
|
|
56
|
+
s = s.replace(/<h([1-6])[^>]*>/gi, (_m, lvl) => '\n\n' + '#'.repeat(parseInt(lvl)) + ' ')
|
|
57
|
+
.replace(/<\/h[1-6]>/gi, '\n')
|
|
58
|
+
.replace(/<li[^>]*>/gi, '\n- ')
|
|
59
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
60
|
+
.replace(/<\/p>/gi, '\n\n')
|
|
61
|
+
.replace(/<[^>]+>/g, '');
|
|
62
|
+
s = s.replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'");
|
|
63
|
+
s = s.replace(/\n{3,}/g, '\n\n').replace(/[ \t]+/g, ' ').trim();
|
|
64
|
+
if (s.length > MAX_MARKDOWN_BYTES) s = s.slice(0, MAX_MARKDOWN_BYTES) + '\n\n(truncated)';
|
|
65
|
+
return s;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// URL safety check (re-applied at every redirect hop)
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
function assertSafeUrl(candidate: string): URL {
|
|
73
|
+
let p: URL;
|
|
74
|
+
try { p = new URL(candidate); } catch { throw new Error(`Invalid URL: ${candidate}`); }
|
|
75
|
+
if (p.protocol !== 'https:' && p.protocol !== 'http:') {
|
|
76
|
+
throw new Error(`Unsupported scheme: ${p.protocol}`);
|
|
77
|
+
}
|
|
78
|
+
if (isPrivateHost(p.hostname)) {
|
|
79
|
+
throw new Error(`Blocked private/localhost host: ${p.hostname}`);
|
|
80
|
+
}
|
|
81
|
+
return p;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Web Tools Manager
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
export class WebToolsManager {
|
|
89
|
+
private braveKey: string;
|
|
90
|
+
private cache = new Map<string, CacheEntry>();
|
|
91
|
+
|
|
92
|
+
constructor() {
|
|
93
|
+
this.braveKey = process.env.BRAVE_SEARCH_API_KEY || '';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Always true — web tools are always available. */
|
|
97
|
+
isEnabled(): boolean { return true; }
|
|
98
|
+
|
|
99
|
+
getTools(): ToolDefinition[] { return WEB_TOOLS; }
|
|
100
|
+
|
|
101
|
+
// ── Cache ────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
private cacheGet<T>(key: string): T | null {
|
|
104
|
+
const entry = this.cache.get(key);
|
|
105
|
+
if (!entry) return null;
|
|
106
|
+
if (entry.expiresAt < Date.now()) { this.cache.delete(key); return null; }
|
|
107
|
+
return entry.value as T;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private cacheSet(key: string, value: unknown): void {
|
|
111
|
+
if (this.cache.size >= CACHE_MAX_ENTRIES) {
|
|
112
|
+
const oldest = this.cache.keys().next().value;
|
|
113
|
+
if (oldest) this.cache.delete(oldest);
|
|
114
|
+
}
|
|
115
|
+
this.cache.set(key, { value, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Search ───────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
async search(query: string, count = 5): Promise<SearchResult[]> {
|
|
121
|
+
const key = `search:${query}:${count}`;
|
|
122
|
+
const cached = this.cacheGet<SearchResult[]>(key);
|
|
123
|
+
if (cached) return cached;
|
|
124
|
+
|
|
125
|
+
const results = this.braveKey
|
|
126
|
+
? await this.searchBrave(query, count)
|
|
127
|
+
: await this.searchDuckDuckGo(query, count);
|
|
128
|
+
|
|
129
|
+
this.cacheSet(key, results);
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Brave Search API — structured JSON, better quality, requires API key. */
|
|
134
|
+
private async searchBrave(query: string, count: number): Promise<SearchResult[]> {
|
|
135
|
+
const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${count}`;
|
|
136
|
+
const resp = await fetch(url, {
|
|
137
|
+
headers: { 'Accept': 'application/json', 'X-Subscription-Token': this.braveKey },
|
|
138
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
139
|
+
});
|
|
140
|
+
if (!resp.ok) throw new Error(`Brave search HTTP ${resp.status}`);
|
|
141
|
+
const data = await resp.json() as any;
|
|
142
|
+
return (data.web?.results || []).slice(0, count).map((r: any) => ({
|
|
143
|
+
title: String(r.title || ''),
|
|
144
|
+
url: String(r.url || ''),
|
|
145
|
+
snippet: String(r.description || ''),
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* DuckDuckGo HTML scrape — zero config, no API key. Fetches the
|
|
151
|
+
* lite/HTML version of DuckDuckGo and parses result links + snippets
|
|
152
|
+
* from the page. Not as structured as Brave but works everywhere
|
|
153
|
+
* with no signup.
|
|
154
|
+
*/
|
|
155
|
+
private async searchDuckDuckGo(query: string, count: number): Promise<SearchResult[]> {
|
|
156
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
157
|
+
const resp = await fetch(url, {
|
|
158
|
+
headers: {
|
|
159
|
+
'User-Agent': 'Mozilla/5.0 (compatible; kondi-chat/0.1)',
|
|
160
|
+
},
|
|
161
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
162
|
+
});
|
|
163
|
+
if (!resp.ok) throw new Error(`DuckDuckGo search HTTP ${resp.status}`);
|
|
164
|
+
const html = await resp.text();
|
|
165
|
+
|
|
166
|
+
// Parse result blocks from the DDG HTML lite page.
|
|
167
|
+
// Each result is an <a class="result__a"> with the title/URL,
|
|
168
|
+
// followed by <a class="result__snippet"> with the snippet.
|
|
169
|
+
const results: SearchResult[] = [];
|
|
170
|
+
const resultBlockRe = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
171
|
+
const snippetRe = /<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
172
|
+
|
|
173
|
+
const titles: Array<{ url: string; title: string }> = [];
|
|
174
|
+
let match;
|
|
175
|
+
while ((match = resultBlockRe.exec(html)) !== null) {
|
|
176
|
+
// DDG wraps the real URL in a redirect: /l/?uddg=<encoded_url>&...
|
|
177
|
+
let href = match[1];
|
|
178
|
+
const uddg = href.match(/uddg=([^&]+)/);
|
|
179
|
+
if (uddg) href = decodeURIComponent(uddg[1]);
|
|
180
|
+
const title = match[2].replace(/<[^>]+>/g, '').trim();
|
|
181
|
+
titles.push({ url: href, title });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const snippets: string[] = [];
|
|
185
|
+
while ((match = snippetRe.exec(html)) !== null) {
|
|
186
|
+
snippets.push(match[1].replace(/<[^>]+>/g, '').trim());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (let i = 0; i < Math.min(titles.length, count); i++) {
|
|
190
|
+
results.push({
|
|
191
|
+
title: titles[i].title,
|
|
192
|
+
url: titles[i].url,
|
|
193
|
+
snippet: snippets[i] || '',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return results;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Fetch ────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
async fetch(url: string): Promise<FetchResult> {
|
|
203
|
+
const key = `fetch:${url}`;
|
|
204
|
+
const cached = this.cacheGet<FetchResult>(key);
|
|
205
|
+
if (cached) return cached;
|
|
206
|
+
|
|
207
|
+
let parsed = assertSafeUrl(url);
|
|
208
|
+
let resp: Response;
|
|
209
|
+
let hops = 0;
|
|
210
|
+
while (true) {
|
|
211
|
+
resp = await fetch(parsed.toString(), {
|
|
212
|
+
redirect: 'manual',
|
|
213
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; kondi-chat/0.1)' },
|
|
214
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
215
|
+
});
|
|
216
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
217
|
+
const loc = resp.headers.get('location');
|
|
218
|
+
if (!loc) throw new Error(`fetch ${resp.status} with no Location header`);
|
|
219
|
+
if (++hops > MAX_REDIRECTS) throw new Error(`Too many redirects (>${MAX_REDIRECTS})`);
|
|
220
|
+
parsed = assertSafeUrl(new URL(loc, parsed).toString());
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
if (!resp.ok) throw new Error(`fetch ${resp.status}`);
|
|
226
|
+
const contentType = resp.headers.get('content-type') || '';
|
|
227
|
+
const reader = resp.body?.getReader();
|
|
228
|
+
let bytes = new Uint8Array(0);
|
|
229
|
+
if (reader) {
|
|
230
|
+
const chunks: Uint8Array[] = [];
|
|
231
|
+
let total = 0;
|
|
232
|
+
while (total < MAX_FETCH_BYTES) {
|
|
233
|
+
const { done, value } = await reader.read();
|
|
234
|
+
if (done) break;
|
|
235
|
+
chunks.push(value);
|
|
236
|
+
total += value.byteLength;
|
|
237
|
+
}
|
|
238
|
+
bytes = new Uint8Array(total);
|
|
239
|
+
let off = 0;
|
|
240
|
+
for (const c of chunks) { bytes.set(c, off); off += c.byteLength; }
|
|
241
|
+
}
|
|
242
|
+
const raw = new TextDecoder('utf-8').decode(bytes);
|
|
243
|
+
const content = contentType.includes('html') ? htmlToPlain(raw) : raw.slice(0, MAX_MARKDOWN_BYTES);
|
|
244
|
+
const out: FetchResult = {
|
|
245
|
+
url: parsed.toString(),
|
|
246
|
+
content,
|
|
247
|
+
contentType,
|
|
248
|
+
sizeBytes: bytes.byteLength,
|
|
249
|
+
truncated: content.length >= MAX_MARKDOWN_BYTES,
|
|
250
|
+
};
|
|
251
|
+
this.cacheSet(key, out);
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Tool executor ────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
async executeTool(name: string, args: Record<string, unknown>): Promise<{ content: string; isError?: boolean }> {
|
|
258
|
+
try {
|
|
259
|
+
if (name === 'web_search') {
|
|
260
|
+
const query = String(args.query || '');
|
|
261
|
+
if (!query) return { content: 'web_search requires a non-empty query', isError: true };
|
|
262
|
+
const count = (args.count as number) || 5;
|
|
263
|
+
const results = await this.search(query, count);
|
|
264
|
+
if (results.length === 0) return { content: `No results for: ${query}` };
|
|
265
|
+
const backend = this.braveKey ? 'brave' : 'duckduckgo';
|
|
266
|
+
return {
|
|
267
|
+
content: `(via ${backend})\n\n` + results.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`).join('\n\n'),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (name === 'web_fetch') {
|
|
271
|
+
const url = String(args.url || '');
|
|
272
|
+
if (!url) return { content: 'web_fetch requires a url', isError: true };
|
|
273
|
+
const r = await this.fetch(url);
|
|
274
|
+
return { content: `${r.url} (${r.contentType}, ${r.sizeBytes} bytes)\n\n${r.content}` };
|
|
275
|
+
}
|
|
276
|
+
return { content: `Unknown web tool: ${name}`, isError: true };
|
|
277
|
+
} catch (e) {
|
|
278
|
+
return { content: `Web tool error: ${(e as Error).message}`, isError: true };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// Tool definitions — always registered, no API key gate
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
const WEB_TOOLS: ToolDefinition[] = [
|
|
288
|
+
{
|
|
289
|
+
name: 'web_search',
|
|
290
|
+
description: 'Search the web. Returns top results with title, URL, and snippet. Works out of the box with no API key (uses DuckDuckGo). Set BRAVE_SEARCH_API_KEY for better results via Brave.',
|
|
291
|
+
parameters: {
|
|
292
|
+
type: 'object',
|
|
293
|
+
properties: {
|
|
294
|
+
query: { type: 'string', description: 'Search query' },
|
|
295
|
+
count: { type: 'number', description: 'Max results (default 5, max 10)' },
|
|
296
|
+
},
|
|
297
|
+
required: ['query'],
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: 'web_fetch',
|
|
302
|
+
description: 'Fetch a web page and extract its readable text content. Strips HTML to clean text. Blocks private/localhost URLs (SSRF protection).',
|
|
303
|
+
parameters: {
|
|
304
|
+
type: 'object',
|
|
305
|
+
properties: {
|
|
306
|
+
url: { type: 'string', description: 'URL to fetch (http or https)' },
|
|
307
|
+
},
|
|
308
|
+
required: ['url'],
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
];
|