titan-agent 6.4.4 → 6.5.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/dist/agent/agentLessons.js.map +1 -1
- package/dist/agent/agentLoop.js +5 -1
- package/dist/agent/agentLoop.js.map +1 -1
- package/dist/agent/orchestrator.js +5 -14
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/somaInitiative.js.map +1 -1
- package/dist/agent/toolRunner.js +35 -0
- package/dist/agent/toolRunner.js.map +1 -1
- package/dist/channels/discord.js +10 -6
- package/dist/channels/discord.js.map +1 -1
- package/dist/channels/slack.js +7 -2
- package/dist/channels/slack.js.map +1 -1
- package/dist/channels/telegram.js +1 -1
- package/dist/channels/telegram.js.map +1 -1
- package/dist/config/schema.js +6 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/gateway/openai-compat.js +69 -10
- package/dist/gateway/openai-compat.js.map +1 -1
- package/dist/gateway/server.js +79 -0
- package/dist/gateway/server.js.map +1 -1
- package/dist/migrations/runner.js.map +1 -1
- package/dist/migrations/safeRun.js.map +1 -1
- package/dist/providers/anthropic.js +7 -4
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/base.js +8 -1
- package/dist/providers/base.js.map +1 -1
- package/dist/providers/google.js +7 -4
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/ollama.js +8 -7
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.js +7 -4
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openai_compat.js +7 -4
- package/dist/providers/openai_compat.js.map +1 -1
- package/dist/providers/router.js +7 -3
- package/dist/providers/router.js.map +1 -1
- package/dist/skills/builtin/backup.js.map +1 -1
- package/dist/skills/builtin/browser.js +11 -21
- package/dist/skills/builtin/browser.js.map +1 -1
- package/dist/skills/builtin/calendar.js +6 -2
- package/dist/skills/builtin/calendar.js.map +1 -1
- package/dist/skills/builtin/canvas_spaces.js.map +1 -1
- package/dist/skills/builtin/data_analysis.js +1 -1
- package/dist/skills/builtin/data_analysis.js.map +1 -1
- package/dist/skills/builtin/jira_linear.js +6 -4
- package/dist/skills/builtin/jira_linear.js.map +1 -1
- package/dist/skills/builtin/web_browser.js +6 -0
- package/dist/skills/builtin/web_browser.js.map +1 -1
- package/dist/skills/builtin/web_fetch.js +1 -0
- package/dist/skills/builtin/web_fetch.js.map +1 -1
- package/dist/storage/starterSpaces.js.map +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/package.json +1 -1
- package/ui/dist/sw.js +1 -1
|
@@ -4,7 +4,9 @@ import { z } from "zod";
|
|
|
4
4
|
import logger from "../../utils/logger.js";
|
|
5
5
|
import { TITAN_VERSION } from "../../utils/constants.js";
|
|
6
6
|
import { getPage, releasePage, closeBrowser as closePoolBrowser } from "../../browsing/browserPool.js";
|
|
7
|
+
import { isInternalUrl } from "./web_fetch.js";
|
|
7
8
|
const COMPONENT = "WebBrowser";
|
|
9
|
+
const SSRF_BLOCKED = "Error: Browsing internal/private/link-local network addresses is not permitted.";
|
|
8
10
|
function extractText(html, maxChars = 8e3) {
|
|
9
11
|
const clean = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, "").replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, "").replace(/<header[^>]*>[\s\S]*?<\/header>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s{3,}/g, "\n\n").trim();
|
|
10
12
|
return clean.slice(0, maxChars);
|
|
@@ -32,6 +34,7 @@ async function fetchSimple(url) {
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
async function fetchWithBrowser(url, screenshot) {
|
|
37
|
+
if (isInternalUrl(url)) throw new Error(SSRF_BLOCKED);
|
|
35
38
|
const page = await getPage();
|
|
36
39
|
try {
|
|
37
40
|
await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
|
|
@@ -93,6 +96,7 @@ function initWebBrowserTool() {
|
|
|
93
96
|
},
|
|
94
97
|
execute: async (args) => {
|
|
95
98
|
const { url, screenshot, extractLinks } = BrowseSchema.parse(args);
|
|
99
|
+
if (isInternalUrl(url)) return SSRF_BLOCKED;
|
|
96
100
|
logger.info(COMPONENT, `Browsing: ${url}`);
|
|
97
101
|
let result = await fetchSimple(url);
|
|
98
102
|
if (!result) {
|
|
@@ -180,6 +184,7 @@ function initWebBrowserTool() {
|
|
|
180
184
|
},
|
|
181
185
|
execute: async (args) => {
|
|
182
186
|
const { url, actions, returnType = "smart_dom" } = args;
|
|
187
|
+
if (url !== "current" && isInternalUrl(url)) return SSRF_BLOCKED;
|
|
183
188
|
const page = await getPage();
|
|
184
189
|
try {
|
|
185
190
|
if (page.url() !== url && url !== "current") {
|
|
@@ -290,6 +295,7 @@ Final URL: ${finalUrl} (Title: ${title})
|
|
|
290
295
|
},
|
|
291
296
|
execute: async (args) => {
|
|
292
297
|
const { url, fullPage = false, quality = 70 } = args;
|
|
298
|
+
if (url && isInternalUrl(url)) return SSRF_BLOCKED;
|
|
293
299
|
const page = await getPage();
|
|
294
300
|
try {
|
|
295
301
|
if (url && page.url() !== url) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/skills/builtin/web_browser.ts"],"sourcesContent":["/**\n * TITAN — Universal Web Browser\n * Set-and-forget web browsing for TITAN. No configuration needed.\n *\n * HOW IT WORKS:\n * ─────────────────────────────────────────────────────────────────\n * 1. Fast path: tries a plain fetch() first — instant, zero cost\n * 2. If the page needs JavaScript (React/Vue/SPAs), falls back to\n * Playwright with a persistent local Chromium browser session\n * 3. Extracts clean readable text (not raw HTML) so the LLM gets\n * signal, not noise\n * 4. Can screenshot pages for visual tasks\n * 5. Session cookies persist between calls — stay logged in\n *\n * WHY PLAYWRIGHT + PERSISTANCE:\n * ─────────────────────────────────────────────────────────────────\n * Unlike single-use headless browsers, TITAN keeps one browser\n * session alive for the lifetime of the gateway. This means:\n * - No cold-start cost on every request (~500ms saved per call)\n * - You can log into sites once and TITAN stays logged in\n * - Cookies and localStorage persist across page visits\n * - \"Set and forget\" — just works, no Docker, no config\n *\n * AUTO-INSTALL: Playwright chromium is auto-installed on first use.\n */\nimport { registerSkill } from '../registry.js';\nimport { z } from 'zod';\nimport logger from '../../utils/logger.js';\nimport { TITAN_VERSION } from '../../utils/constants.js';\nimport { getPage, releasePage, closeBrowser as closePoolBrowser } from '../../browsing/browserPool.js';\n\nconst COMPONENT = 'WebBrowser';\n\n// ─── Types ────────────────────────────────────────────────────────\ninterface BrowseResult {\n url: string;\n title: string;\n content: string;\n screenshot?: string; // base64\n loadedWithJs: boolean;\n extractedLinks?: string[];\n}\n\ninterface DomElement { tag: string; id: string; name: string; cls: string; text: string; type?: string }\n\n/** Extract clean readable text from HTML */\nfunction extractText(html: string, maxChars = 8000): string {\n // Remove scripts, styles, nav, footer\n const clean = html\n .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<nav[^>]*>[\\s\\S]*?<\\/nav>/gi, '')\n .replace(/<footer[^>]*>[\\s\\S]*?<\\/footer>/gi, '')\n .replace(/<header[^>]*>[\\s\\S]*?<\\/header>/gi, '')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s{3,}/g, '\\n\\n')\n .trim();\n return clean.slice(0, maxChars);\n}\n\n/** Try a plain fetch — fast path for simple pages */\nasync function fetchSimple(url: string): Promise<BrowseResult | null> {\n try {\n const res = await fetch(url, {\n headers: { 'User-Agent': `Mozilla/5.0 TITAN-Agent/${TITAN_VERSION}` },\n signal: AbortSignal.timeout(8000),\n });\n if (!res.ok) return null;\n const html = await res.text();\n\n // Heuristic: if page has very little text but lots of JS, it needs a real browser\n const textContent = extractText(html, 200);\n const hasContent = textContent.trim().length > 100;\n if (!hasContent) return null; // Needs JS rendering\n\n const titleMatch = html.match(/<title[^>]*>([^<]+)<\\/title>/i);\n return {\n url,\n title: titleMatch?.[1]?.trim() ?? url,\n content: extractText(html, 8000),\n loadedWithJs: false,\n };\n } catch { return null; }\n}\n\n/** Full browser render with Playwright (uses shared browser pool) */\nasync function fetchWithBrowser(url: string, screenshot: boolean): Promise<BrowseResult> {\n const page = await getPage();\n try {\n await page.goto(url, { waitUntil: 'networkidle', timeout: 30_000 });\n\n const title = await page.title();\n const bodyText = await page.evaluate(() => {\n // Note: this runs in browser context — globalThis is the browser window\n const win = globalThis as unknown as Record<string, unknown>;\n const doc = win.document as unknown as { querySelector: (s: string) => { textContent?: string } | null; body?: { innerText?: string } };\n const selectors = ['article', 'main', '[role=\"main\"]', '.content', '#content', 'body'];\n for (const sel of selectors) {\n const el = doc.querySelector(sel);\n if (el && el.textContent && el.textContent.trim().length > 200) {\n return el.textContent.trim().replace(/\\s{3,}/g, '\\n\\n').slice(0, 8000);\n }\n }\n return doc.body?.innerText?.slice(0, 8000) ?? '';\n }) as string;\n\n // Optionally grab visible links\n const links = await page.evaluate(() => {\n const win = globalThis as unknown as Record<string, unknown>;\n const doc = win.document as unknown as { querySelectorAll: (s: string) => ArrayLike<{ href: string }> };\n return Array.from(doc.querySelectorAll('a[href]'))\n .map((el: { href: string }) => el.href)\n .filter((href: string) => href.startsWith('http'))\n .slice(0, 20);\n }) as string[];\n\n let screenshotData: string | undefined;\n if (screenshot) {\n const buf = await page.screenshot({ type: 'jpeg', quality: 60, fullPage: false });\n screenshotData = buf.toString('base64');\n }\n\n return { url, title, content: bodyText, loadedWithJs: true, extractedLinks: links, screenshot: screenshotData };\n } finally {\n await releasePage(page);\n }\n}\n\n// ─── Schema ───────────────────────────────────────────────────────\nconst BrowseSchema = z.object({\n url: z.string().url().describe('The URL to browse'),\n screenshot: z.boolean().optional().default(false).describe('Whether to take a screenshot'),\n extractLinks: z.boolean().optional().default(false).describe('Whether to extract all links from the page'),\n});\n\nconst SearchSchema = z.object({\n query: z.string().describe('The search query'),\n numResults: z.number().optional().default(5).describe('Number of results to return (1-10)'),\n});\n\n// ─── Register tools ────────────────────────────────────────────────\nexport function initWebBrowserTool(): void {\n // Browse any URL\n registerSkill({\n name: 'browse_url',\n description: 'Browse any webpage and extract its content as clean text. Uses a real browser with JavaScript support for dynamic sites.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n }, {\n name: 'browse_url',\n description: 'Browse any URL using a real Playwright browser and return clean extracted text.\\n\\nUSE THIS WHEN:\\n- Tony gives a URL that needs JavaScript to render (SPAs, React/Vue apps, dashboards)\\n- web_fetch or web_read returned empty or minimal content\\n- Tony says \"open X in browser\" / \"check X\" / \"load X\" / \"browse to X\"\\n\\nVS OTHER TOOLS:\\n- Use web_fetch for simple static pages (faster, no browser overhead)\\n- Use web_read for clean article/doc extraction\\n- Use browser_auto_nav for multi-step form or click workflows\\n\\nAuto-detects JS-heavy pages and uses Playwright; falls back to fast fetch for simple pages.',\n parameters: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'The full URL to browse (must start with https://)' },\n screenshot: { type: 'boolean', description: 'Take a visual screenshot of the page (optional)', default: false },\n extractLinks: { type: 'boolean', description: 'Also return links found on the page', default: false },\n },\n required: ['url'],\n },\n execute: async (args) => {\n const { url, screenshot, extractLinks } = BrowseSchema.parse(args);\n logger.info(COMPONENT, `Browsing: ${url}`);\n\n // Try fast path first\n let result = await fetchSimple(url);\n\n // Fall back to real browser if needed\n if (!result) {\n logger.info(COMPONENT, `Switching to Playwright for JS-heavy page: ${url}`);\n result = await fetchWithBrowser(url, screenshot ?? false);\n }\n\n const lines = [\n `🌐 **${result.title}**`,\n `URL: ${result.url}`,\n `Loaded with: ${result.loadedWithJs ? 'Browser (JavaScript enabled)' : 'Direct fetch (fast)'}`,\n '',\n result.content,\n ];\n\n if (extractLinks && result.extractedLinks && result.extractedLinks.length > 0) {\n lines.push('', '🔗 **Links found:**');\n result.extractedLinks.slice(0, 10).forEach((l) => lines.push(` - ${l}`));\n }\n\n if (result.screenshot) {\n lines.push('', `📷 Screenshot captured (${result.screenshot.length} bytes)`);\n }\n\n return lines.join('\\n');\n },\n });\n\n // Web search using DuckDuckGo (no API key needed)\n registerSkill({\n name: 'browser_search',\n description: 'Search the internet for current information using a real browser. Returns real search results with titles, URLs, and snippets.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n }, {\n name: 'browser_search',\n description: 'Search the internet using a real Playwright browser — bypasses bot detection that blocks plain HTTP fetches.\\n\\nUSE THIS WHEN:\\n- web_search fails or returns no results\\n- Tony says \"search for X\" and the query needs JS rendering\\n- Searching for current events, prices, or real-time info\\n\\nWORKFLOW: Call browser_search → review results → call browse_url on the best result for full content\\nNo API key needed.',\n parameters: {\n type: 'object',\n properties: {\n query: { type: 'string', description: 'What to search for on the internet' },\n numResults: { type: 'number', description: 'Number of results to return (1-10)', default: 5 },\n },\n required: ['query'],\n },\n execute: async (args) => {\n const { query } = SearchSchema.parse(args);\n logger.info(COMPONENT, `Searching: ${query}`);\n\n // Use DuckDuckGo Lite — no API key, no rate limit for reasonable use\n const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;\n const res = await fetchWithBrowser(searchUrl, false);\n\n // The results page content already has the search results as text\n const lines = [\n `🔍 **Search results for: \"${query}\"**`,\n '',\n res.content.slice(0, 4000),\n ];\n\n return lines.join('\\n');\n },\n });\n\n // Ultra-Fast Bulk Web Navigation\n registerSkill({\n name: 'browser_auto_nav',\n description: 'Navigate a website blazingly fast by executing a bulk sequence of actions in a single tool call.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n }, {\n name: 'browser_auto_nav',\n description: 'Execute a bulk sequence of browser actions (click, fill, wait, evaluate) in one call — machine-speed navigation.\\n\\nUSE THIS WHEN Tony says: \"log in to X\" / \"click X then fill Y\" / \"submit that form\" / \"go through the steps on X\" / \"automate X website\"\\n\\nWORKFLOW: Provide the URL + an array of step actions → get back the final page state (smart DOM map, text, or screenshot)\\n\\nBEST FOR: logins, multi-step forms, pagination, any sequence of clicks and inputs\\nFor single-call form filling, smart_form_fill is even simpler.',\n parameters: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'Starting URL (if you are already on the page, just provide the current URL)' },\n actions: {\n type: 'array',\n description: 'Sequential array of actions to perform at near-instant machine speed.',\n items: {\n type: 'object',\n properties: {\n action: { type: 'string', enum: ['click', 'fill', 'wait', 'evaluate'], description: 'Action to perform' },\n selector: { type: 'string', description: 'CSS selector (e.g. #login-btn, input[name=\"user\"])' },\n value: { type: 'string', description: 'Text to fill if action is \"fill\"' },\n delayMs: { type: 'number', description: 'Milliseconds to wait if action is \"wait\"' },\n script: { type: 'string', description: 'JavaScript to run if action is \"evaluate\"' }\n },\n required: ['action']\n }\n },\n returnType: { type: 'string', enum: ['text', 'smart_dom', 'screenshot'], description: 'What to return after the actions finish: raw text, a mapping of interactive elements, or a base64 screenshot. Default: smart_dom', default: 'smart_dom' }\n },\n required: ['url', 'actions'],\n },\n execute: async (args: Record<string, unknown>) => {\n const { url, actions, returnType = 'smart_dom' } = args as { url: string; actions: Record<string, unknown>[]; returnType?: string };\n const page = await getPage();\n\n try {\n // Check if we are already on this URL (saves navigation time)\n if (page.url() !== url && url !== 'current') {\n await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });\n }\n\n const results: string[] = [];\n for (const step of actions) {\n try {\n if (step.action === 'click' && step.selector) {\n await page.locator(step.selector as string).first().waitFor({ state: 'visible', timeout: 5000 }).catch(() => { });\n await page.click(step.selector as string);\n results.push(`✅ Clicked: ${step.selector}`);\n } else if (step.action === 'fill' && step.selector && step.value !== undefined) {\n await page.locator(step.selector as string).first().waitFor({ state: 'visible', timeout: 5000 }).catch(() => { });\n await page.fill(step.selector as string, step.value as string);\n results.push(`✅ Filled: ${step.selector} -> *****`);\n } else if (step.action === 'wait' && step.delayMs) {\n await page.waitForTimeout(step.delayMs as number);\n results.push(`✅ Waited: ${step.delayMs}ms`);\n } else if (step.action === 'evaluate' && step.script) {\n await page.evaluate(step.script as string & (() => unknown));\n results.push(`✅ Evaluated custom script`);\n }\n } catch (e: unknown) {\n results.push(`❌ Failed Step: ${step.action} on ${step.selector} - ${(e as Error).message.split('\\\\n')[0]}`);\n break; // Stop execution on first failure to prevent chaos\n }\n }\n\n // Wait a moment for dynamic page updates after the last action\n await page.waitForTimeout(1000);\n\n const title = await page.title();\n const finalUrl = page.url();\n\n let output = `Workflow Results:\\n${results.join('\\\\n')}\\n\\nFinal URL: ${finalUrl} (Title: ${title})\\n\\n`;\n\n if (returnType === 'smart_dom') {\n const domMap = await page.evaluate(() => {\n const win = globalThis as unknown as Record<string, unknown>;\n const doc = win.document as unknown as { querySelectorAll: (s: string) => ArrayLike<Record<string, unknown>> };\n const elements: { tag: string; id: string; name: string; cls: string; text: string; type?: string }[] = [];\n const interactive = doc.querySelectorAll('button, a[href], input, select, textarea');\n Array.from(interactive).forEach((el: Record<string, unknown>) => {\n const bounds = (el.getBoundingClientRect as () => { width: number; height: number })();\n if (bounds.width === 0 || bounds.height === 0) return; // Skip invisible elements\n\n elements.push({\n tag: ((el.tagName as string) || '').toLowerCase(),\n id: (el.id as string) || '',\n name: (el.name as string) || '',\n cls: String(el.className || ''),\n text: (el.innerText as string)?.slice(0, 50) || (el.value as string)?.slice(0, 50) || '',\n type: (el.type as string) || ''\n });\n });\n return elements;\n }) as DomElement[];\n\n let domString = \"--- Smart DOM Map (Interactive Elements) ---\\n\";\n domMap.slice(0, 100).forEach((e: DomElement) => {\n let sel = e.tag;\n if (e.id) sel += `#${e.id}`;\n else if (e.name) sel += `[name=\"${e.name}\"]`;\n else if (e.cls) sel += `.${e.cls.split(' ')[0]}`;\n\n domString += `[${sel}] `;\n if (e.tag === 'input' || e.tag === 'textarea') domString += `type=\"${e.type}\" `;\n if (e.text) domString += `-> \"${e.text.trim()}\"`;\n domString += '\\n';\n });\n output += domString;\n } else if (returnType === 'text') {\n output += \"--- Page Text ---\\n\";\n const bodyText = await page.evaluate(() => {\n const win = globalThis as unknown as Record<string, unknown>;\n const doc = win.document as unknown as { body?: { innerText?: string } };\n return doc.body?.innerText?.slice(0, 8000) ?? '';\n }) as string;\n output += bodyText;\n } else if (returnType === 'screenshot') {\n const buf = await page.screenshot({ type: 'jpeg', quality: 60 });\n output += `--- Screenshot attached: ${buf.length} bytes ---\\n`;\n // In a real multi-modal environment we'd attach the buffer, returning string for now.\n }\n\n return output;\n } finally {\n await releasePage(page);\n }\n },\n });\n\n // Standalone screenshot — capture current page (or a fresh URL) without running an action sequence.\n registerSkill({\n name: 'browser_screenshot',\n description: 'Capture a screenshot of a web page using the shared Playwright browser. Returns a base64 JPEG.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n }, {\n name: 'browser_screenshot',\n description: 'Take a screenshot of a web page. If `url` is omitted, screenshots the page already loaded in the shared browser.\\n\\nUSE THIS WHEN Tony says: \"screenshot X\" / \"take a picture of X\" / \"verify X visually\" / sub-agents that need to confirm the visual state of a page they just acted on.\\n\\nReturns base64-encoded JPEG (quality 70). For full-page screenshots set `fullPage: true`.\\n\\nVS OTHER TOOLS:\\n- Use the desktop `screenshot` tool when you want the whole macOS screen (computer-use)\\n- Use this when you only need the browser viewport / page',\n parameters: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'Optional URL to navigate to before screenshotting. Omit to capture the current page.' },\n fullPage: { type: 'boolean', description: 'Capture the entire scrollable page rather than just the viewport. Default false.', default: false },\n quality: { type: 'number', description: 'JPEG quality 1–100. Default 70.', default: 70 },\n },\n required: [],\n },\n execute: async (args: Record<string, unknown>) => {\n const { url, fullPage = false, quality = 70 } = args as { url?: string; fullPage?: boolean; quality?: number };\n const page = await getPage();\n try {\n if (url && page.url() !== url) {\n await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });\n }\n const buf = await page.screenshot({ type: 'jpeg', quality, fullPage });\n const b64 = buf.toString('base64');\n const title = await page.title().catch(() => '');\n const finalUrl = page.url();\n return `Screenshot captured (${buf.length} bytes, ${fullPage ? 'full page' : 'viewport'})\\nURL: ${finalUrl}${title ? `\\nTitle: ${title}` : ''}\\n\\n[base64 JPEG]\\ndata:image/jpeg;base64,${b64}`;\n } finally {\n await releasePage(page);\n }\n },\n });\n\n logger.info(COMPONENT, 'Web browser tools registered (browse_url, browser_search, browser_auto_nav, browser_screenshot)');\n}\n\n/** Gracefully close the browser session on gateway shutdown */\nexport async function closeBrowser(): Promise<void> {\n await closePoolBrowser();\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/skills/builtin/web_browser.ts"],"sourcesContent":["/**\n * TITAN — Universal Web Browser\n * Set-and-forget web browsing for TITAN. No configuration needed.\n *\n * HOW IT WORKS:\n * ─────────────────────────────────────────────────────────────────\n * 1. Fast path: tries a plain fetch() first — instant, zero cost\n * 2. If the page needs JavaScript (React/Vue/SPAs), falls back to\n * Playwright with a persistent local Chromium browser session\n * 3. Extracts clean readable text (not raw HTML) so the LLM gets\n * signal, not noise\n * 4. Can screenshot pages for visual tasks\n * 5. Session cookies persist between calls — stay logged in\n *\n * WHY PLAYWRIGHT + PERSISTANCE:\n * ─────────────────────────────────────────────────────────────────\n * Unlike single-use headless browsers, TITAN keeps one browser\n * session alive for the lifetime of the gateway. This means:\n * - No cold-start cost on every request (~500ms saved per call)\n * - You can log into sites once and TITAN stays logged in\n * - Cookies and localStorage persist across page visits\n * - \"Set and forget\" — just works, no Docker, no config\n *\n * AUTO-INSTALL: Playwright chromium is auto-installed on first use.\n */\nimport { registerSkill } from '../registry.js';\nimport { z } from 'zod';\nimport logger from '../../utils/logger.js';\nimport { TITAN_VERSION } from '../../utils/constants.js';\nimport { getPage, releasePage, closeBrowser as closePoolBrowser } from '../../browsing/browserPool.js';\nimport { isInternalUrl } from './web_fetch.js';\n\nconst COMPONENT = 'WebBrowser';\n\n// v6.5 SSRF guard — web_browser drives a real Chromium with JS exec + persistent\n// cookies, so an unguarded internal URL is strictly worse than web_fetch (which\n// already blocks these). Reuse web_fetch's isInternalUrl() at every URL entry.\nconst SSRF_BLOCKED = 'Error: Browsing internal/private/link-local network addresses is not permitted.';\n\n// ─── Types ────────────────────────────────────────────────────────\ninterface BrowseResult {\n url: string;\n title: string;\n content: string;\n screenshot?: string; // base64\n loadedWithJs: boolean;\n extractedLinks?: string[];\n}\n\ninterface DomElement { tag: string; id: string; name: string; cls: string; text: string; type?: string }\n\n/** Extract clean readable text from HTML */\nfunction extractText(html: string, maxChars = 8000): string {\n // Remove scripts, styles, nav, footer\n const clean = html\n .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<nav[^>]*>[\\s\\S]*?<\\/nav>/gi, '')\n .replace(/<footer[^>]*>[\\s\\S]*?<\\/footer>/gi, '')\n .replace(/<header[^>]*>[\\s\\S]*?<\\/header>/gi, '')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s{3,}/g, '\\n\\n')\n .trim();\n return clean.slice(0, maxChars);\n}\n\n/** Try a plain fetch — fast path for simple pages */\nasync function fetchSimple(url: string): Promise<BrowseResult | null> {\n try {\n const res = await fetch(url, {\n headers: { 'User-Agent': `Mozilla/5.0 TITAN-Agent/${TITAN_VERSION}` },\n signal: AbortSignal.timeout(8000),\n });\n if (!res.ok) return null;\n const html = await res.text();\n\n // Heuristic: if page has very little text but lots of JS, it needs a real browser\n const textContent = extractText(html, 200);\n const hasContent = textContent.trim().length > 100;\n if (!hasContent) return null; // Needs JS rendering\n\n const titleMatch = html.match(/<title[^>]*>([^<]+)<\\/title>/i);\n return {\n url,\n title: titleMatch?.[1]?.trim() ?? url,\n content: extractText(html, 8000),\n loadedWithJs: false,\n };\n } catch { return null; }\n}\n\n/** Full browser render with Playwright (uses shared browser pool) */\nasync function fetchWithBrowser(url: string, screenshot: boolean): Promise<BrowseResult> {\n if (isInternalUrl(url)) throw new Error(SSRF_BLOCKED);\n const page = await getPage();\n try {\n await page.goto(url, { waitUntil: 'networkidle', timeout: 30_000 });\n\n const title = await page.title();\n const bodyText = await page.evaluate(() => {\n // Note: this runs in browser context — globalThis is the browser window\n const win = globalThis as unknown as Record<string, unknown>;\n const doc = win.document as unknown as { querySelector: (s: string) => { textContent?: string } | null; body?: { innerText?: string } };\n const selectors = ['article', 'main', '[role=\"main\"]', '.content', '#content', 'body'];\n for (const sel of selectors) {\n const el = doc.querySelector(sel);\n if (el && el.textContent && el.textContent.trim().length > 200) {\n return el.textContent.trim().replace(/\\s{3,}/g, '\\n\\n').slice(0, 8000);\n }\n }\n return doc.body?.innerText?.slice(0, 8000) ?? '';\n }) as string;\n\n // Optionally grab visible links\n const links = await page.evaluate(() => {\n const win = globalThis as unknown as Record<string, unknown>;\n const doc = win.document as unknown as { querySelectorAll: (s: string) => ArrayLike<{ href: string }> };\n return Array.from(doc.querySelectorAll('a[href]'))\n .map((el: { href: string }) => el.href)\n .filter((href: string) => href.startsWith('http'))\n .slice(0, 20);\n }) as string[];\n\n let screenshotData: string | undefined;\n if (screenshot) {\n const buf = await page.screenshot({ type: 'jpeg', quality: 60, fullPage: false });\n screenshotData = buf.toString('base64');\n }\n\n return { url, title, content: bodyText, loadedWithJs: true, extractedLinks: links, screenshot: screenshotData };\n } finally {\n await releasePage(page);\n }\n}\n\n// ─── Schema ───────────────────────────────────────────────────────\nconst BrowseSchema = z.object({\n url: z.string().url().describe('The URL to browse'),\n screenshot: z.boolean().optional().default(false).describe('Whether to take a screenshot'),\n extractLinks: z.boolean().optional().default(false).describe('Whether to extract all links from the page'),\n});\n\nconst SearchSchema = z.object({\n query: z.string().describe('The search query'),\n numResults: z.number().optional().default(5).describe('Number of results to return (1-10)'),\n});\n\n// ─── Register tools ────────────────────────────────────────────────\nexport function initWebBrowserTool(): void {\n // Browse any URL\n registerSkill({\n name: 'browse_url',\n description: 'Browse any webpage and extract its content as clean text. Uses a real browser with JavaScript support for dynamic sites.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n }, {\n name: 'browse_url',\n description: 'Browse any URL using a real Playwright browser and return clean extracted text.\\n\\nUSE THIS WHEN:\\n- Tony gives a URL that needs JavaScript to render (SPAs, React/Vue apps, dashboards)\\n- web_fetch or web_read returned empty or minimal content\\n- Tony says \"open X in browser\" / \"check X\" / \"load X\" / \"browse to X\"\\n\\nVS OTHER TOOLS:\\n- Use web_fetch for simple static pages (faster, no browser overhead)\\n- Use web_read for clean article/doc extraction\\n- Use browser_auto_nav for multi-step form or click workflows\\n\\nAuto-detects JS-heavy pages and uses Playwright; falls back to fast fetch for simple pages.',\n parameters: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'The full URL to browse (must start with https://)' },\n screenshot: { type: 'boolean', description: 'Take a visual screenshot of the page (optional)', default: false },\n extractLinks: { type: 'boolean', description: 'Also return links found on the page', default: false },\n },\n required: ['url'],\n },\n execute: async (args) => {\n const { url, screenshot, extractLinks } = BrowseSchema.parse(args);\n if (isInternalUrl(url)) return SSRF_BLOCKED;\n logger.info(COMPONENT, `Browsing: ${url}`);\n\n // Try fast path first\n let result = await fetchSimple(url);\n\n // Fall back to real browser if needed\n if (!result) {\n logger.info(COMPONENT, `Switching to Playwright for JS-heavy page: ${url}`);\n result = await fetchWithBrowser(url, screenshot ?? false);\n }\n\n const lines = [\n `🌐 **${result.title}**`,\n `URL: ${result.url}`,\n `Loaded with: ${result.loadedWithJs ? 'Browser (JavaScript enabled)' : 'Direct fetch (fast)'}`,\n '',\n result.content,\n ];\n\n if (extractLinks && result.extractedLinks && result.extractedLinks.length > 0) {\n lines.push('', '🔗 **Links found:**');\n result.extractedLinks.slice(0, 10).forEach((l) => lines.push(` - ${l}`));\n }\n\n if (result.screenshot) {\n lines.push('', `📷 Screenshot captured (${result.screenshot.length} bytes)`);\n }\n\n return lines.join('\\n');\n },\n });\n\n // Web search using DuckDuckGo (no API key needed)\n registerSkill({\n name: 'browser_search',\n description: 'Search the internet for current information using a real browser. Returns real search results with titles, URLs, and snippets.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n }, {\n name: 'browser_search',\n description: 'Search the internet using a real Playwright browser — bypasses bot detection that blocks plain HTTP fetches.\\n\\nUSE THIS WHEN:\\n- web_search fails or returns no results\\n- Tony says \"search for X\" and the query needs JS rendering\\n- Searching for current events, prices, or real-time info\\n\\nWORKFLOW: Call browser_search → review results → call browse_url on the best result for full content\\nNo API key needed.',\n parameters: {\n type: 'object',\n properties: {\n query: { type: 'string', description: 'What to search for on the internet' },\n numResults: { type: 'number', description: 'Number of results to return (1-10)', default: 5 },\n },\n required: ['query'],\n },\n execute: async (args) => {\n const { query } = SearchSchema.parse(args);\n logger.info(COMPONENT, `Searching: ${query}`);\n\n // Use DuckDuckGo Lite — no API key, no rate limit for reasonable use\n const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;\n const res = await fetchWithBrowser(searchUrl, false);\n\n // The results page content already has the search results as text\n const lines = [\n `🔍 **Search results for: \"${query}\"**`,\n '',\n res.content.slice(0, 4000),\n ];\n\n return lines.join('\\n');\n },\n });\n\n // Ultra-Fast Bulk Web Navigation\n registerSkill({\n name: 'browser_auto_nav',\n description: 'Navigate a website blazingly fast by executing a bulk sequence of actions in a single tool call.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n }, {\n name: 'browser_auto_nav',\n description: 'Execute a bulk sequence of browser actions (click, fill, wait, evaluate) in one call — machine-speed navigation.\\n\\nUSE THIS WHEN Tony says: \"log in to X\" / \"click X then fill Y\" / \"submit that form\" / \"go through the steps on X\" / \"automate X website\"\\n\\nWORKFLOW: Provide the URL + an array of step actions → get back the final page state (smart DOM map, text, or screenshot)\\n\\nBEST FOR: logins, multi-step forms, pagination, any sequence of clicks and inputs\\nFor single-call form filling, smart_form_fill is even simpler.',\n parameters: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'Starting URL (if you are already on the page, just provide the current URL)' },\n actions: {\n type: 'array',\n description: 'Sequential array of actions to perform at near-instant machine speed.',\n items: {\n type: 'object',\n properties: {\n action: { type: 'string', enum: ['click', 'fill', 'wait', 'evaluate'], description: 'Action to perform' },\n selector: { type: 'string', description: 'CSS selector (e.g. #login-btn, input[name=\"user\"])' },\n value: { type: 'string', description: 'Text to fill if action is \"fill\"' },\n delayMs: { type: 'number', description: 'Milliseconds to wait if action is \"wait\"' },\n script: { type: 'string', description: 'JavaScript to run if action is \"evaluate\"' }\n },\n required: ['action']\n }\n },\n returnType: { type: 'string', enum: ['text', 'smart_dom', 'screenshot'], description: 'What to return after the actions finish: raw text, a mapping of interactive elements, or a base64 screenshot. Default: smart_dom', default: 'smart_dom' }\n },\n required: ['url', 'actions'],\n },\n execute: async (args: Record<string, unknown>) => {\n const { url, actions, returnType = 'smart_dom' } = args as { url: string; actions: Record<string, unknown>[]; returnType?: string };\n if (url !== 'current' && isInternalUrl(url)) return SSRF_BLOCKED;\n const page = await getPage();\n\n try {\n // Check if we are already on this URL (saves navigation time)\n if (page.url() !== url && url !== 'current') {\n await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });\n }\n\n const results: string[] = [];\n for (const step of actions) {\n try {\n if (step.action === 'click' && step.selector) {\n await page.locator(step.selector as string).first().waitFor({ state: 'visible', timeout: 5000 }).catch(() => { });\n await page.click(step.selector as string);\n results.push(`✅ Clicked: ${step.selector}`);\n } else if (step.action === 'fill' && step.selector && step.value !== undefined) {\n await page.locator(step.selector as string).first().waitFor({ state: 'visible', timeout: 5000 }).catch(() => { });\n await page.fill(step.selector as string, step.value as string);\n results.push(`✅ Filled: ${step.selector} -> *****`);\n } else if (step.action === 'wait' && step.delayMs) {\n await page.waitForTimeout(step.delayMs as number);\n results.push(`✅ Waited: ${step.delayMs}ms`);\n } else if (step.action === 'evaluate' && step.script) {\n await page.evaluate(step.script as string & (() => unknown));\n results.push(`✅ Evaluated custom script`);\n }\n } catch (e: unknown) {\n results.push(`❌ Failed Step: ${step.action} on ${step.selector} - ${(e as Error).message.split('\\\\n')[0]}`);\n break; // Stop execution on first failure to prevent chaos\n }\n }\n\n // Wait a moment for dynamic page updates after the last action\n await page.waitForTimeout(1000);\n\n const title = await page.title();\n const finalUrl = page.url();\n\n let output = `Workflow Results:\\n${results.join('\\\\n')}\\n\\nFinal URL: ${finalUrl} (Title: ${title})\\n\\n`;\n\n if (returnType === 'smart_dom') {\n const domMap = await page.evaluate(() => {\n const win = globalThis as unknown as Record<string, unknown>;\n const doc = win.document as unknown as { querySelectorAll: (s: string) => ArrayLike<Record<string, unknown>> };\n const elements: { tag: string; id: string; name: string; cls: string; text: string; type?: string }[] = [];\n const interactive = doc.querySelectorAll('button, a[href], input, select, textarea');\n Array.from(interactive).forEach((el: Record<string, unknown>) => {\n const bounds = (el.getBoundingClientRect as () => { width: number; height: number })();\n if (bounds.width === 0 || bounds.height === 0) return; // Skip invisible elements\n\n elements.push({\n tag: ((el.tagName as string) || '').toLowerCase(),\n id: (el.id as string) || '',\n name: (el.name as string) || '',\n cls: String(el.className || ''),\n text: (el.innerText as string)?.slice(0, 50) || (el.value as string)?.slice(0, 50) || '',\n type: (el.type as string) || ''\n });\n });\n return elements;\n }) as DomElement[];\n\n let domString = \"--- Smart DOM Map (Interactive Elements) ---\\n\";\n domMap.slice(0, 100).forEach((e: DomElement) => {\n let sel = e.tag;\n if (e.id) sel += `#${e.id}`;\n else if (e.name) sel += `[name=\"${e.name}\"]`;\n else if (e.cls) sel += `.${e.cls.split(' ')[0]}`;\n\n domString += `[${sel}] `;\n if (e.tag === 'input' || e.tag === 'textarea') domString += `type=\"${e.type}\" `;\n if (e.text) domString += `-> \"${e.text.trim()}\"`;\n domString += '\\n';\n });\n output += domString;\n } else if (returnType === 'text') {\n output += \"--- Page Text ---\\n\";\n const bodyText = await page.evaluate(() => {\n const win = globalThis as unknown as Record<string, unknown>;\n const doc = win.document as unknown as { body?: { innerText?: string } };\n return doc.body?.innerText?.slice(0, 8000) ?? '';\n }) as string;\n output += bodyText;\n } else if (returnType === 'screenshot') {\n const buf = await page.screenshot({ type: 'jpeg', quality: 60 });\n output += `--- Screenshot attached: ${buf.length} bytes ---\\n`;\n // In a real multi-modal environment we'd attach the buffer, returning string for now.\n }\n\n return output;\n } finally {\n await releasePage(page);\n }\n },\n });\n\n // Standalone screenshot — capture current page (or a fresh URL) without running an action sequence.\n registerSkill({\n name: 'browser_screenshot',\n description: 'Capture a screenshot of a web page using the shared Playwright browser. Returns a base64 JPEG.',\n version: '1.0.0',\n source: 'bundled',\n enabled: true,\n }, {\n name: 'browser_screenshot',\n description: 'Take a screenshot of a web page. If `url` is omitted, screenshots the page already loaded in the shared browser.\\n\\nUSE THIS WHEN Tony says: \"screenshot X\" / \"take a picture of X\" / \"verify X visually\" / sub-agents that need to confirm the visual state of a page they just acted on.\\n\\nReturns base64-encoded JPEG (quality 70). For full-page screenshots set `fullPage: true`.\\n\\nVS OTHER TOOLS:\\n- Use the desktop `screenshot` tool when you want the whole macOS screen (computer-use)\\n- Use this when you only need the browser viewport / page',\n parameters: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'Optional URL to navigate to before screenshotting. Omit to capture the current page.' },\n fullPage: { type: 'boolean', description: 'Capture the entire scrollable page rather than just the viewport. Default false.', default: false },\n quality: { type: 'number', description: 'JPEG quality 1–100. Default 70.', default: 70 },\n },\n required: [],\n },\n execute: async (args: Record<string, unknown>) => {\n const { url, fullPage = false, quality = 70 } = args as { url?: string; fullPage?: boolean; quality?: number };\n if (url && isInternalUrl(url)) return SSRF_BLOCKED;\n const page = await getPage();\n try {\n if (url && page.url() !== url) {\n await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });\n }\n const buf = await page.screenshot({ type: 'jpeg', quality, fullPage });\n const b64 = buf.toString('base64');\n const title = await page.title().catch(() => '');\n const finalUrl = page.url();\n return `Screenshot captured (${buf.length} bytes, ${fullPage ? 'full page' : 'viewport'})\\nURL: ${finalUrl}${title ? `\\nTitle: ${title}` : ''}\\n\\n[base64 JPEG]\\ndata:image/jpeg;base64,${b64}`;\n } finally {\n await releasePage(page);\n }\n },\n });\n\n logger.info(COMPONENT, 'Web browser tools registered (browse_url, browser_search, browser_auto_nav, browser_screenshot)');\n}\n\n/** Gracefully close the browser session on gateway shutdown */\nexport async function closeBrowser(): Promise<void> {\n await closePoolBrowser();\n}\n"],"mappings":";AAyBA,SAAS,qBAAqB;AAC9B,SAAS,SAAS;AAClB,OAAO,YAAY;AACnB,SAAS,qBAAqB;AAC9B,SAAS,SAAS,aAAa,gBAAgB,wBAAwB;AACvE,SAAS,qBAAqB;AAE9B,MAAM,YAAY;AAKlB,MAAM,eAAe;AAerB,SAAS,YAAY,MAAc,WAAW,KAAc;AAExD,QAAM,QAAQ,KACT,QAAQ,qCAAqC,EAAE,EAC/C,QAAQ,mCAAmC,EAAE,EAC7C,QAAQ,+BAA+B,EAAE,EACzC,QAAQ,qCAAqC,EAAE,EAC/C,QAAQ,qCAAqC,EAAE,EAC/C,QAAQ,YAAY,GAAG,EACvB,QAAQ,WAAW,MAAM,EACzB,KAAK;AACV,SAAO,MAAM,MAAM,GAAG,QAAQ;AAClC;AAGA,eAAe,YAAY,KAA2C;AAClE,MAAI;AACA,UAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MACzB,SAAS,EAAE,cAAc,2BAA2B,aAAa,GAAG;AAAA,MACpE,QAAQ,YAAY,QAAQ,GAAI;AAAA,IACpC,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAO,MAAM,IAAI,KAAK;AAG5B,UAAM,cAAc,YAAY,MAAM,GAAG;AACzC,UAAM,aAAa,YAAY,KAAK,EAAE,SAAS;AAC/C,QAAI,CAAC,WAAY,QAAO;AAExB,UAAM,aAAa,KAAK,MAAM,+BAA+B;AAC7D,WAAO;AAAA,MACH;AAAA,MACA,OAAO,aAAa,CAAC,GAAG,KAAK,KAAK;AAAA,MAClC,SAAS,YAAY,MAAM,GAAI;AAAA,MAC/B,cAAc;AAAA,IAClB;AAAA,EACJ,QAAQ;AAAE,WAAO;AAAA,EAAM;AAC3B;AAGA,eAAe,iBAAiB,KAAa,YAA4C;AACrF,MAAI,cAAc,GAAG,EAAG,OAAM,IAAI,MAAM,YAAY;AACpD,QAAM,OAAO,MAAM,QAAQ;AAC3B,MAAI;AACA,UAAM,KAAK,KAAK,KAAK,EAAE,WAAW,eAAe,SAAS,IAAO,CAAC;AAElE,UAAM,QAAQ,MAAM,KAAK,MAAM;AAC/B,UAAM,WAAW,MAAM,KAAK,SAAS,MAAM;AAEvC,YAAM,MAAM;AACZ,YAAM,MAAM,IAAI;AAChB,YAAM,YAAY,CAAC,WAAW,QAAQ,iBAAiB,YAAY,YAAY,MAAM;AACrF,iBAAW,OAAO,WAAW;AACzB,cAAM,KAAK,IAAI,cAAc,GAAG;AAChC,YAAI,MAAM,GAAG,eAAe,GAAG,YAAY,KAAK,EAAE,SAAS,KAAK;AAC5D,iBAAO,GAAG,YAAY,KAAK,EAAE,QAAQ,WAAW,MAAM,EAAE,MAAM,GAAG,GAAI;AAAA,QACzE;AAAA,MACJ;AACA,aAAO,IAAI,MAAM,WAAW,MAAM,GAAG,GAAI,KAAK;AAAA,IAClD,CAAC;AAGD,UAAM,QAAQ,MAAM,KAAK,SAAS,MAAM;AACpC,YAAM,MAAM;AACZ,YAAM,MAAM,IAAI;AAChB,aAAO,MAAM,KAAK,IAAI,iBAAiB,SAAS,CAAC,EAC5C,IAAI,CAAC,OAAyB,GAAG,IAAI,EACrC,OAAO,CAAC,SAAiB,KAAK,WAAW,MAAM,CAAC,EAChD,MAAM,GAAG,EAAE;AAAA,IACpB,CAAC;AAED,QAAI;AACJ,QAAI,YAAY;AACZ,YAAM,MAAM,MAAM,KAAK,WAAW,EAAE,MAAM,QAAQ,SAAS,IAAI,UAAU,MAAM,CAAC;AAChF,uBAAiB,IAAI,SAAS,QAAQ;AAAA,IAC1C;AAEA,WAAO,EAAE,KAAK,OAAO,SAAS,UAAU,cAAc,MAAM,gBAAgB,OAAO,YAAY,eAAe;AAAA,EAClH,UAAE;AACE,UAAM,YAAY,IAAI;AAAA,EAC1B;AACJ;AAGA,MAAM,eAAe,EAAE,OAAO;AAAA,EAC1B,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,mBAAmB;AAAA,EAClD,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,KAAK,EAAE,SAAS,8BAA8B;AAAA,EACzF,cAAc,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,KAAK,EAAE,SAAS,4CAA4C;AAC7G,CAAC;AAED,MAAM,eAAe,EAAE,OAAO;AAAA,EAC1B,OAAO,EAAE,OAAO,EAAE,SAAS,kBAAkB;AAAA,EAC7C,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,SAAS,oCAAoC;AAC9F,CAAC;AAGM,SAAS,qBAA2B;AAEvC,gBAAc;AAAA,IACV,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,SAAS;AAAA,EACb,GAAG;AAAA,IACC,MAAM;AAAA,IACN,aAAa;AAAA,IACb,YAAY;AAAA,MACR,MAAM;AAAA,MACN,YAAY;AAAA,QACR,KAAK,EAAE,MAAM,UAAU,aAAa,oDAAoD;AAAA,QACxF,YAAY,EAAE,MAAM,WAAW,aAAa,mDAAmD,SAAS,MAAM;AAAA,QAC9G,cAAc,EAAE,MAAM,WAAW,aAAa,uCAAuC,SAAS,MAAM;AAAA,MACxG;AAAA,MACA,UAAU,CAAC,KAAK;AAAA,IACpB;AAAA,IACA,SAAS,OAAO,SAAS;AACrB,YAAM,EAAE,KAAK,YAAY,aAAa,IAAI,aAAa,MAAM,IAAI;AACjE,UAAI,cAAc,GAAG,EAAG,QAAO;AAC/B,aAAO,KAAK,WAAW,aAAa,GAAG,EAAE;AAGzC,UAAI,SAAS,MAAM,YAAY,GAAG;AAGlC,UAAI,CAAC,QAAQ;AACT,eAAO,KAAK,WAAW,8CAA8C,GAAG,EAAE;AAC1E,iBAAS,MAAM,iBAAiB,KAAK,cAAc,KAAK;AAAA,MAC5D;AAEA,YAAM,QAAQ;AAAA,QACV,eAAQ,OAAO,KAAK;AAAA,QACpB,QAAQ,OAAO,GAAG;AAAA,QAClB,gBAAgB,OAAO,eAAe,iCAAiC,qBAAqB;AAAA,QAC5F;AAAA,QACA,OAAO;AAAA,MACX;AAEA,UAAI,gBAAgB,OAAO,kBAAkB,OAAO,eAAe,SAAS,GAAG;AAC3E,cAAM,KAAK,IAAI,4BAAqB;AACpC,eAAO,eAAe,MAAM,GAAG,EAAE,EAAE,QAAQ,CAAC,MAAM,MAAM,KAAK,OAAO,CAAC,EAAE,CAAC;AAAA,MAC5E;AAEA,UAAI,OAAO,YAAY;AACnB,cAAM,KAAK,IAAI,kCAA2B,OAAO,WAAW,MAAM,SAAS;AAAA,MAC/E;AAEA,aAAO,MAAM,KAAK,IAAI;AAAA,IAC1B;AAAA,EACJ,CAAC;AAGD,gBAAc;AAAA,IACV,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,SAAS;AAAA,EACb,GAAG;AAAA,IACC,MAAM;AAAA,IACN,aAAa;AAAA,IACb,YAAY;AAAA,MACR,MAAM;AAAA,MACN,YAAY;AAAA,QACR,OAAO,EAAE,MAAM,UAAU,aAAa,qCAAqC;AAAA,QAC3E,YAAY,EAAE,MAAM,UAAU,aAAa,sCAAsC,SAAS,EAAE;AAAA,MAChG;AAAA,MACA,UAAU,CAAC,OAAO;AAAA,IACtB;AAAA,IACA,SAAS,OAAO,SAAS;AACrB,YAAM,EAAE,MAAM,IAAI,aAAa,MAAM,IAAI;AACzC,aAAO,KAAK,WAAW,cAAc,KAAK,EAAE;AAG5C,YAAM,YAAY,uCAAuC,mBAAmB,KAAK,CAAC;AAClF,YAAM,MAAM,MAAM,iBAAiB,WAAW,KAAK;AAGnD,YAAM,QAAQ;AAAA,QACV,oCAA6B,KAAK;AAAA,QAClC;AAAA,QACA,IAAI,QAAQ,MAAM,GAAG,GAAI;AAAA,MAC7B;AAEA,aAAO,MAAM,KAAK,IAAI;AAAA,IAC1B;AAAA,EACJ,CAAC;AAGD,gBAAc;AAAA,IACV,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,SAAS;AAAA,EACb,GAAG;AAAA,IACC,MAAM;AAAA,IACN,aAAa;AAAA,IACb,YAAY;AAAA,MACR,MAAM;AAAA,MACN,YAAY;AAAA,QACR,KAAK,EAAE,MAAM,UAAU,aAAa,8EAA8E;AAAA,QAClH,SAAS;AAAA,UACL,MAAM;AAAA,UACN,aAAa;AAAA,UACb,OAAO;AAAA,YACH,MAAM;AAAA,YACN,YAAY;AAAA,cACR,QAAQ,EAAE,MAAM,UAAU,MAAM,CAAC,SAAS,QAAQ,QAAQ,UAAU,GAAG,aAAa,oBAAoB;AAAA,cACxG,UAAU,EAAE,MAAM,UAAU,aAAa,qDAAqD;AAAA,cAC9F,OAAO,EAAE,MAAM,UAAU,aAAa,mCAAmC;AAAA,cACzE,SAAS,EAAE,MAAM,UAAU,aAAa,2CAA2C;AAAA,cACnF,QAAQ,EAAE,MAAM,UAAU,aAAa,4CAA4C;AAAA,YACvF;AAAA,YACA,UAAU,CAAC,QAAQ;AAAA,UACvB;AAAA,QACJ;AAAA,QACA,YAAY,EAAE,MAAM,UAAU,MAAM,CAAC,QAAQ,aAAa,YAAY,GAAG,aAAa,oIAAoI,SAAS,YAAY;AAAA,MACnP;AAAA,MACA,UAAU,CAAC,OAAO,SAAS;AAAA,IAC/B;AAAA,IACA,SAAS,OAAO,SAAkC;AAC9C,YAAM,EAAE,KAAK,SAAS,aAAa,YAAY,IAAI;AACnD,UAAI,QAAQ,aAAa,cAAc,GAAG,EAAG,QAAO;AACpD,YAAM,OAAO,MAAM,QAAQ;AAE3B,UAAI;AAEA,YAAI,KAAK,IAAI,MAAM,OAAO,QAAQ,WAAW;AACzC,gBAAM,KAAK,KAAK,KAAK,EAAE,WAAW,oBAAoB,SAAS,IAAO,CAAC;AAAA,QAC3E;AAEA,cAAM,UAAoB,CAAC;AAC3B,mBAAW,QAAQ,SAAS;AACxB,cAAI;AACA,gBAAI,KAAK,WAAW,WAAW,KAAK,UAAU;AAC1C,oBAAM,KAAK,QAAQ,KAAK,QAAkB,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,WAAW,SAAS,IAAK,CAAC,EAAE,MAAM,MAAM;AAAA,cAAE,CAAC;AAChH,oBAAM,KAAK,MAAM,KAAK,QAAkB;AACxC,sBAAQ,KAAK,mBAAc,KAAK,QAAQ,EAAE;AAAA,YAC9C,WAAW,KAAK,WAAW,UAAU,KAAK,YAAY,KAAK,UAAU,QAAW;AAC5E,oBAAM,KAAK,QAAQ,KAAK,QAAkB,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,WAAW,SAAS,IAAK,CAAC,EAAE,MAAM,MAAM;AAAA,cAAE,CAAC;AAChH,oBAAM,KAAK,KAAK,KAAK,UAAoB,KAAK,KAAe;AAC7D,sBAAQ,KAAK,kBAAa,KAAK,QAAQ,WAAW;AAAA,YACtD,WAAW,KAAK,WAAW,UAAU,KAAK,SAAS;AAC/C,oBAAM,KAAK,eAAe,KAAK,OAAiB;AAChD,sBAAQ,KAAK,kBAAa,KAAK,OAAO,IAAI;AAAA,YAC9C,WAAW,KAAK,WAAW,cAAc,KAAK,QAAQ;AAClD,oBAAM,KAAK,SAAS,KAAK,MAAkC;AAC3D,sBAAQ,KAAK,gCAA2B;AAAA,YAC5C;AAAA,UACJ,SAAS,GAAY;AACjB,oBAAQ,KAAK,uBAAkB,KAAK,MAAM,OAAO,KAAK,QAAQ,MAAO,EAAY,QAAQ,MAAM,KAAK,EAAE,CAAC,CAAC,EAAE;AAC1G;AAAA,UACJ;AAAA,QACJ;AAGA,cAAM,KAAK,eAAe,GAAI;AAE9B,cAAM,QAAQ,MAAM,KAAK,MAAM;AAC/B,cAAM,WAAW,KAAK,IAAI;AAE1B,YAAI,SAAS;AAAA,EAAsB,QAAQ,KAAK,KAAK,CAAC;AAAA;AAAA,aAAkB,QAAQ,YAAY,KAAK;AAAA;AAAA;AAEjG,YAAI,eAAe,aAAa;AAC5B,gBAAM,SAAS,MAAM,KAAK,SAAS,MAAM;AACrC,kBAAM,MAAM;AACZ,kBAAM,MAAM,IAAI;AAChB,kBAAM,WAAkG,CAAC;AACzG,kBAAM,cAAc,IAAI,iBAAiB,0CAA0C;AACnF,kBAAM,KAAK,WAAW,EAAE,QAAQ,CAAC,OAAgC;AAC7D,oBAAM,SAAU,GAAG,sBAAkE;AACrF,kBAAI,OAAO,UAAU,KAAK,OAAO,WAAW,EAAG;AAE/C,uBAAS,KAAK;AAAA,gBACV,MAAO,GAAG,WAAsB,IAAI,YAAY;AAAA,gBAChD,IAAK,GAAG,MAAiB;AAAA,gBACzB,MAAO,GAAG,QAAmB;AAAA,gBAC7B,KAAK,OAAO,GAAG,aAAa,EAAE;AAAA,gBAC9B,MAAO,GAAG,WAAsB,MAAM,GAAG,EAAE,KAAM,GAAG,OAAkB,MAAM,GAAG,EAAE,KAAK;AAAA,gBACtF,MAAO,GAAG,QAAmB;AAAA,cACjC,CAAC;AAAA,YACL,CAAC;AACD,mBAAO;AAAA,UACX,CAAC;AAED,cAAI,YAAY;AAChB,iBAAO,MAAM,GAAG,GAAG,EAAE,QAAQ,CAAC,MAAkB;AAC5C,gBAAI,MAAM,EAAE;AACZ,gBAAI,EAAE,GAAI,QAAO,IAAI,EAAE,EAAE;AAAA,qBAChB,EAAE,KAAM,QAAO,UAAU,EAAE,IAAI;AAAA,qBAC/B,EAAE,IAAK,QAAO,IAAI,EAAE,IAAI,MAAM,GAAG,EAAE,CAAC,CAAC;AAE9C,yBAAa,IAAI,GAAG;AACpB,gBAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,WAAY,cAAa,SAAS,EAAE,IAAI;AAC3E,gBAAI,EAAE,KAAM,cAAa,OAAO,EAAE,KAAK,KAAK,CAAC;AAC7C,yBAAa;AAAA,UACjB,CAAC;AACD,oBAAU;AAAA,QACd,WAAW,eAAe,QAAQ;AAC9B,oBAAU;AACV,gBAAM,WAAW,MAAM,KAAK,SAAS,MAAM;AACvC,kBAAM,MAAM;AACZ,kBAAM,MAAM,IAAI;AAChB,mBAAO,IAAI,MAAM,WAAW,MAAM,GAAG,GAAI,KAAK;AAAA,UAClD,CAAC;AACD,oBAAU;AAAA,QACd,WAAW,eAAe,cAAc;AACpC,gBAAM,MAAM,MAAM,KAAK,WAAW,EAAE,MAAM,QAAQ,SAAS,GAAG,CAAC;AAC/D,oBAAU,4BAA4B,IAAI,MAAM;AAAA;AAAA,QAEpD;AAEA,eAAO;AAAA,MACX,UAAE;AACE,cAAM,YAAY,IAAI;AAAA,MAC1B;AAAA,IACJ;AAAA,EACJ,CAAC;AAGD,gBAAc;AAAA,IACV,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,SAAS;AAAA,EACb,GAAG;AAAA,IACC,MAAM;AAAA,IACN,aAAa;AAAA,IACb,YAAY;AAAA,MACR,MAAM;AAAA,MACN,YAAY;AAAA,QACR,KAAK,EAAE,MAAM,UAAU,aAAa,uFAAuF;AAAA,QAC3H,UAAU,EAAE,MAAM,WAAW,aAAa,oFAAoF,SAAS,MAAM;AAAA,QAC7I,SAAS,EAAE,MAAM,UAAU,aAAa,wCAAmC,SAAS,GAAG;AAAA,MAC3F;AAAA,MACA,UAAU,CAAC;AAAA,IACf;AAAA,IACA,SAAS,OAAO,SAAkC;AAC9C,YAAM,EAAE,KAAK,WAAW,OAAO,UAAU,GAAG,IAAI;AAChD,UAAI,OAAO,cAAc,GAAG,EAAG,QAAO;AACtC,YAAM,OAAO,MAAM,QAAQ;AAC3B,UAAI;AACA,YAAI,OAAO,KAAK,IAAI,MAAM,KAAK;AAC3B,gBAAM,KAAK,KAAK,KAAK,EAAE,WAAW,oBAAoB,SAAS,IAAO,CAAC;AAAA,QAC3E;AACA,cAAM,MAAM,MAAM,KAAK,WAAW,EAAE,MAAM,QAAQ,SAAS,SAAS,CAAC;AACrE,cAAM,MAAM,IAAI,SAAS,QAAQ;AACjC,cAAM,QAAQ,MAAM,KAAK,MAAM,EAAE,MAAM,MAAM,EAAE;AAC/C,cAAM,WAAW,KAAK,IAAI;AAC1B,eAAO,wBAAwB,IAAI,MAAM,WAAW,WAAW,cAAc,UAAU;AAAA,OAAW,QAAQ,GAAG,QAAQ;AAAA,SAAY,KAAK,KAAK,EAAE;AAAA;AAAA;AAAA,yBAA6C,GAAG;AAAA,MACjM,UAAE;AACE,cAAM,YAAY,IAAI;AAAA,MAC1B;AAAA,IACJ;AAAA,EACJ,CAAC;AAED,SAAO,KAAK,WAAW,iGAAiG;AAC5H;AAGA,eAAsB,eAA8B;AAChD,QAAM,iBAAiB;AAC3B;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/skills/builtin/web_fetch.ts"],"sourcesContent":["/**\n * TITAN — Web Fetch Skill (Built-in)\n * Fetch any URL and extract content as markdown or text.\n * Matches OpenClaw's web_fetch tool.\n */\nimport { registerSkill } from '../registry.js';\nimport { TITAN_VERSION } from '../../utils/constants.js';\n\n/** Convert HTML to clean readable text */\nfunction htmlToText(html: string): string {\n return html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/p>/gi, '\\n\\n')\n .replace(/<\\/div>/gi, '\\n')\n .replace(/<\\/h[1-6]>/gi, '\\n\\n')\n .replace(/<li>/gi, '• ')\n .replace(/<\\/li>/gi, '\\n')\n .replace(/<[^>]*>/g, '')\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\n/** Convert HTML to simple markdown */\nfunction htmlToMarkdown(html: string): string {\n let md = html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '');\n\n // Headers\n md = md.replace(/<h1[^>]*>(.*?)<\\/h1>/gi, '# $1\\n');\n md = md.replace(/<h2[^>]*>(.*?)<\\/h2>/gi, '## $1\\n');\n md = md.replace(/<h3[^>]*>(.*?)<\\/h3>/gi, '### $1\\n');\n md = md.replace(/<h4[^>]*>(.*?)<\\/h4>/gi, '#### $1\\n');\n // Links\n md = md.replace(/<a[^>]*href=\"([^\"]*)\"[^>]*>(.*?)<\\/a>/gi, '[$2]($1)');\n // Bold/italic\n md = md.replace(/<(strong|b)>(.*?)<\\/\\1>/gi, '**$2**');\n md = md.replace(/<(em|i)>(.*?)<\\/\\1>/gi, '*$2*');\n // Code\n md = md.replace(/<code>(.*?)<\\/code>/gi, '`$1`');\n md = md.replace(/<pre>([\\s\\S]*?)<\\/pre>/gi, '```\\n$1\\n```\\n');\n // Lists\n md = md.replace(/<li>/gi, '- ');\n md = md.replace(/<\\/li>/gi, '\\n');\n // Images\n md = md.replace(/<img[^>]*alt=\"([^\"]*)\"[^>]*src=\"([^\"]*)\"[^>]*>/gi, '');\n md = md.replace(/<img[^>]*src=\"([^\"]*)\"[^>]*alt=\"([^\"]*)\"[^>]*>/gi, '');\n // Remaining tags\n md = md.replace(/<br\\s*\\/?>/gi, '\\n');\n md = md.replace(/<\\/p>/gi, '\\n\\n');\n md = md.replace(/<[^>]*>/g, '');\n // Entities\n md = md.replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n md = md.replace(/\\n{3,}/g, '\\n\\n');\n return md.trim();\n}\n\n/** Block requests to loopback, private, and link-local addresses (SSRF protection) */\nfunction isInternalUrl(urlStr: string): boolean {\n let hostname: string;\n try {\n hostname = new URL(urlStr).hostname;\n } catch {\n return true; // Treat unparseable URLs as internal/blocked\n }\n\n // Block localhost by name\n if (hostname === 'localhost' || hostname === 'localhost.') return true;\n\n // Resolve numeric IPv4\n const ipv4 = hostname.match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/);\n if (ipv4) {\n const [, a, b] = ipv4.map(Number);\n if (a === 127) return true; // 127.0.0.0/8 loopback\n if (a === 10) return true; // 10.0.0.0/8 private\n if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12 private\n if (a === 192 && b === 168) return true; // 192.168.0.0/16 private\n if (a === 169 && b === 254) return true; // 169.254.0.0/16 link-local\n }\n\n // Block IPv6 loopback (::1) and link-local (fe80::/10)\n const h = hostname.replace(/^\\[|\\]$/g, '');\n if (h === '::1') return true;\n if (/^fe[89ab][0-9a-f]:/i.test(h)) return true;\n\n return false;\n}\n\nexport function registerWebFetchSkill(): void {\n registerSkill(\n { name: 'web_fetch', description: 'Fetch URL content', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'web_fetch',\n description: `Fetch a URL and return its full content as markdown (default) or plain text. HTTP-level fetch with redirects + content sniffing — does NOT run JavaScript, so SPAs return their initial HTML shell.\n\nUSE WHEN: the user gives you a URL directly (\"read X\" / \"open X\" / \"summarize this article: X\") OR after web_search to get full content of the top result. Also use to read raw API responses (set extractMode:\"text\" to skip markdown conversion).\n\nDO NOT USE FOR:\n- JS-heavy / SPA / interactive sites where the content is rendered client-side → use browse_url (Playwright; runs JS).\n- Pages that need login or interaction → use web_act or browser_screenshot.\n- Local files → use read_file (web_fetch only does HTTP/HTTPS).\n- Search-then-fetch combined → just call web_search first; it returns URLs you then fetch.\n\nParameters:\n- url (string, required) — full http(s) URL.\n- extractMode (string, optional) — \"markdown\" (default) converts HTML to clean markdown; \"text\" returns stripped plain text; \"html\" returns the raw HTML (rare).\n- maxLength (number, optional) — truncation cap in chars; default 50_000.\n\nReturns: { url, finalUrl (after redirects), status, contentType, content, truncated, sizeBytes }.\n\nErrors:\n- \"ECONNREFUSED\" / \"ENOTFOUND\" — the host is down or DNS failed; verify URL spelling, don't retry blindly.\n- \"HTTP 4xx\" — page returned an error; surface the status to the user.\n- \"HTTP 403 / blocked by anti-bot\" — switch to browse_url which uses a real browser fingerprint.\n- \"Timed out\" — page took too long; retry once with a smaller maxLength.\n\nNEVER hallucinate page content. If web_fetch failed, say so.`,\n parameters: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'URL to fetch' },\n extractMode: { type: 'string', enum: ['markdown', 'text'], description: 'Output format (default: markdown)' },\n maxChars: { type: 'number', description: 'Max characters to return (default: 50000)' },\n },\n required: ['url'],\n },\n execute: async (args) => {\n const url = args.url as string;\n const mode = (args.extractMode as string) || 'markdown';\n const maxChars = Math.min((args.maxChars as number) || 50000, 100000);\n\n try {\n if (isInternalUrl(url)) {\n return `Error: Fetching internal/private network addresses is not permitted.`;\n }\n const response = await fetch(url, {\n headers: { 'User-Agent': `Mozilla/5.0 (compatible; TITAN/${TITAN_VERSION})` },\n signal: AbortSignal.timeout(20000),\n });\n const reader = response.body?.getReader();\n if (!reader) return `Error: No response body from ${url}`;\n const chunks: Uint8Array[] = [];\n let totalBytes = 0;\n const maxBytes = 200000;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n totalBytes += value.length;\n if (totalBytes >= maxBytes) break;\n }\n reader.cancel().catch(() => {});\n const decoder = new TextDecoder();\n const stdout = chunks.map(c => decoder.decode(c, { stream: true })).join('').slice(0, maxBytes);\n\n if (!stdout.trim()) {\n return `Empty response from ${url}`;\n }\n\n // Extract title\n const title = stdout.match(/<title[^>]*>(.*?)<\\/title>/i)?.[1] || 'Untitled';\n\n // Convert\n const content = mode === 'markdown' ? htmlToMarkdown(stdout) : htmlToText(stdout);\n\n return `# ${title}\\n\\nSource: ${url}\\n\\n${content.slice(0, maxChars)}`;\n } catch (e) {\n return `Error fetching ${url}: ${(e as Error).message}`;\n }\n },\n },\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/skills/builtin/web_fetch.ts"],"sourcesContent":["/**\n * TITAN — Web Fetch Skill (Built-in)\n * Fetch any URL and extract content as markdown or text.\n * Matches OpenClaw's web_fetch tool.\n */\nimport { registerSkill } from '../registry.js';\nimport { TITAN_VERSION } from '../../utils/constants.js';\n\n/** Convert HTML to clean readable text */\nfunction htmlToText(html: string): string {\n return html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/p>/gi, '\\n\\n')\n .replace(/<\\/div>/gi, '\\n')\n .replace(/<\\/h[1-6]>/gi, '\\n\\n')\n .replace(/<li>/gi, '• ')\n .replace(/<\\/li>/gi, '\\n')\n .replace(/<[^>]*>/g, '')\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\n/** Convert HTML to simple markdown */\nfunction htmlToMarkdown(html: string): string {\n let md = html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, '');\n\n // Headers\n md = md.replace(/<h1[^>]*>(.*?)<\\/h1>/gi, '# $1\\n');\n md = md.replace(/<h2[^>]*>(.*?)<\\/h2>/gi, '## $1\\n');\n md = md.replace(/<h3[^>]*>(.*?)<\\/h3>/gi, '### $1\\n');\n md = md.replace(/<h4[^>]*>(.*?)<\\/h4>/gi, '#### $1\\n');\n // Links\n md = md.replace(/<a[^>]*href=\"([^\"]*)\"[^>]*>(.*?)<\\/a>/gi, '[$2]($1)');\n // Bold/italic\n md = md.replace(/<(strong|b)>(.*?)<\\/\\1>/gi, '**$2**');\n md = md.replace(/<(em|i)>(.*?)<\\/\\1>/gi, '*$2*');\n // Code\n md = md.replace(/<code>(.*?)<\\/code>/gi, '`$1`');\n md = md.replace(/<pre>([\\s\\S]*?)<\\/pre>/gi, '```\\n$1\\n```\\n');\n // Lists\n md = md.replace(/<li>/gi, '- ');\n md = md.replace(/<\\/li>/gi, '\\n');\n // Images\n md = md.replace(/<img[^>]*alt=\"([^\"]*)\"[^>]*src=\"([^\"]*)\"[^>]*>/gi, '');\n md = md.replace(/<img[^>]*src=\"([^\"]*)\"[^>]*alt=\"([^\"]*)\"[^>]*>/gi, '');\n // Remaining tags\n md = md.replace(/<br\\s*\\/?>/gi, '\\n');\n md = md.replace(/<\\/p>/gi, '\\n\\n');\n md = md.replace(/<[^>]*>/g, '');\n // Entities\n md = md.replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n md = md.replace(/\\n{3,}/g, '\\n\\n');\n return md.trim();\n}\n\n/** Block requests to loopback, private, and link-local addresses (SSRF protection).\n * Exported so the more-powerful web_browser tools (real Chromium) reuse the\n * exact same guard instead of being an unguarded SSRF hole one file over. */\nexport function isInternalUrl(urlStr: string): boolean {\n let hostname: string;\n try {\n hostname = new URL(urlStr).hostname;\n } catch {\n return true; // Treat unparseable URLs as internal/blocked\n }\n\n // Block localhost by name\n if (hostname === 'localhost' || hostname === 'localhost.') return true;\n\n // Resolve numeric IPv4\n const ipv4 = hostname.match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/);\n if (ipv4) {\n const [, a, b] = ipv4.map(Number);\n if (a === 127) return true; // 127.0.0.0/8 loopback\n if (a === 10) return true; // 10.0.0.0/8 private\n if (a === 172 && b >= 16 && b <= 31) return true; // 172.16.0.0/12 private\n if (a === 192 && b === 168) return true; // 192.168.0.0/16 private\n if (a === 169 && b === 254) return true; // 169.254.0.0/16 link-local\n }\n\n // Block IPv6 loopback (::1) and link-local (fe80::/10)\n const h = hostname.replace(/^\\[|\\]$/g, '');\n if (h === '::1') return true;\n if (/^fe[89ab][0-9a-f]:/i.test(h)) return true;\n\n return false;\n}\n\nexport function registerWebFetchSkill(): void {\n registerSkill(\n { name: 'web_fetch', description: 'Fetch URL content', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'web_fetch',\n description: `Fetch a URL and return its full content as markdown (default) or plain text. HTTP-level fetch with redirects + content sniffing — does NOT run JavaScript, so SPAs return their initial HTML shell.\n\nUSE WHEN: the user gives you a URL directly (\"read X\" / \"open X\" / \"summarize this article: X\") OR after web_search to get full content of the top result. Also use to read raw API responses (set extractMode:\"text\" to skip markdown conversion).\n\nDO NOT USE FOR:\n- JS-heavy / SPA / interactive sites where the content is rendered client-side → use browse_url (Playwright; runs JS).\n- Pages that need login or interaction → use web_act or browser_screenshot.\n- Local files → use read_file (web_fetch only does HTTP/HTTPS).\n- Search-then-fetch combined → just call web_search first; it returns URLs you then fetch.\n\nParameters:\n- url (string, required) — full http(s) URL.\n- extractMode (string, optional) — \"markdown\" (default) converts HTML to clean markdown; \"text\" returns stripped plain text; \"html\" returns the raw HTML (rare).\n- maxLength (number, optional) — truncation cap in chars; default 50_000.\n\nReturns: { url, finalUrl (after redirects), status, contentType, content, truncated, sizeBytes }.\n\nErrors:\n- \"ECONNREFUSED\" / \"ENOTFOUND\" — the host is down or DNS failed; verify URL spelling, don't retry blindly.\n- \"HTTP 4xx\" — page returned an error; surface the status to the user.\n- \"HTTP 403 / blocked by anti-bot\" — switch to browse_url which uses a real browser fingerprint.\n- \"Timed out\" — page took too long; retry once with a smaller maxLength.\n\nNEVER hallucinate page content. If web_fetch failed, say so.`,\n parameters: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'URL to fetch' },\n extractMode: { type: 'string', enum: ['markdown', 'text'], description: 'Output format (default: markdown)' },\n maxChars: { type: 'number', description: 'Max characters to return (default: 50000)' },\n },\n required: ['url'],\n },\n execute: async (args) => {\n const url = args.url as string;\n const mode = (args.extractMode as string) || 'markdown';\n const maxChars = Math.min((args.maxChars as number) || 50000, 100000);\n\n try {\n if (isInternalUrl(url)) {\n return `Error: Fetching internal/private network addresses is not permitted.`;\n }\n const response = await fetch(url, {\n headers: { 'User-Agent': `Mozilla/5.0 (compatible; TITAN/${TITAN_VERSION})` },\n signal: AbortSignal.timeout(20000),\n });\n const reader = response.body?.getReader();\n if (!reader) return `Error: No response body from ${url}`;\n const chunks: Uint8Array[] = [];\n let totalBytes = 0;\n const maxBytes = 200000;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n totalBytes += value.length;\n if (totalBytes >= maxBytes) break;\n }\n reader.cancel().catch(() => {});\n const decoder = new TextDecoder();\n const stdout = chunks.map(c => decoder.decode(c, { stream: true })).join('').slice(0, maxBytes);\n\n if (!stdout.trim()) {\n return `Empty response from ${url}`;\n }\n\n // Extract title\n const title = stdout.match(/<title[^>]*>(.*?)<\\/title>/i)?.[1] || 'Untitled';\n\n // Convert\n const content = mode === 'markdown' ? htmlToMarkdown(stdout) : htmlToText(stdout);\n\n return `# ${title}\\n\\nSource: ${url}\\n\\n${content.slice(0, maxChars)}`;\n } catch (e) {\n return `Error fetching ${url}: ${(e as Error).message}`;\n }\n },\n },\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/storage/starterSpaces.ts"],"sourcesContent":["/**\n * TITAN — Starter Space presets (v6.0 step 9)\n *\n * Why this exists\n * ---------------\n * \"Spaces are infinite\" is the v6.0 thesis but new users still need to LAND\n * somewhere with a useful posture. Each preset gives the agent a starting\n * `agentInstructions` that primes its behaviour for the named domain.\n *\n * The presets are intentionally archetypal — they're starting points for the\n * user to evolve, not finished products. The agent should propose them when\n * a user describes a domain of work that matches one (e.g. \"I'm a developer\"\n * → suggest the Coder preset; \"I make music\" → suggest the DJ preset).\n *\n * The 5 presets:\n * - default — Generic home canvas (also seeded by migration 002)\n * - coder — Software development, files/logs/agents\n * - dj — Music production / DJing\n * - founder — Business operator (costs, agents, telemetry)\n * - homelab — Infrastructure operator (vram, fleet, monitoring)\n *\n * Reference:\n * - ~/.claude/projects/-Users-
|
|
1
|
+
{"version":3,"sources":["../../src/storage/starterSpaces.ts"],"sourcesContent":["/**\n * TITAN — Starter Space presets (v6.0 step 9)\n *\n * Why this exists\n * ---------------\n * \"Spaces are infinite\" is the v6.0 thesis but new users still need to LAND\n * somewhere with a useful posture. Each preset gives the agent a starting\n * `agentInstructions` that primes its behaviour for the named domain.\n *\n * The presets are intentionally archetypal — they're starting points for the\n * user to evolve, not finished products. The agent should propose them when\n * a user describes a domain of work that matches one (e.g. \"I'm a developer\"\n * → suggest the Coder preset; \"I make music\" → suggest the DJ preset).\n *\n * The 5 presets:\n * - default — Generic home canvas (also seeded by migration 002)\n * - coder — Software development, files/logs/agents\n * - dj — Music production / DJing\n * - founder — Business operator (costs, agents, telemetry)\n * - homelab — Infrastructure operator (vram, fleet, monitoring)\n *\n * Reference:\n * - ~/.claude/projects/-Users-localuser/memory/titan-v6-living-canvas.md\n * (v6.0 step 9 — Starter Space seeds)\n */\n\nexport interface StarterSpacePreset {\n /** Stable id used by the agent + UI. */\n id: 'default' | 'coder' | 'dj' | 'founder' | 'homelab';\n /** Display name. */\n name: string;\n /** Emoji shown in the sidebar. */\n icon: string;\n /** Accent color (hex) for the sidebar. */\n color: string;\n /** Tagline shown when offering this preset to the user. */\n tagline: string;\n /** `agentInstructions` injected into the system prompt when active. */\n agentInstructions: string;\n}\n\nexport const STARTER_PRESETS: ReadonlyArray<StarterSpacePreset> = Object.freeze([\n {\n id: 'default',\n name: 'Default',\n icon: '🏠',\n color: '#3b82f6',\n tagline: 'Your home canvas. A clean Space TITAN can shape with you.',\n agentInstructions: [\n `You are TITAN. The user is in their Default Space — their home canvas.`,\n ``,\n `When they ask for a tool, widget, dashboard, or visual surface, build it`,\n `on the spot via \\`create_widget\\`. When they describe a new domain of`,\n `work, propose creating a dedicated Space for it via \\`create_space\\`.`,\n ].join('\\n'),\n },\n {\n id: 'coder',\n name: 'Coder',\n icon: '⌨️',\n color: '#10b981',\n tagline: 'Files, logs, agents, cron. Everything a developer needs in one canvas.',\n agentInstructions: [\n `You are TITAN. The user is in their Coder Space — focused on software`,\n `development.`,\n ``,\n `Default posture:`,\n `- Treat the user as an engineer. Skip explanations they don't need.`,\n `- When they ask about code, prefer concrete diffs + file paths over prose.`,\n `- When they ask \"where is X\", use \\`gallery_search\\` then \\`gitnexus_query\\``,\n ` if available, before grepping.`,\n `- When they're debugging, surface \\`logs\\`, \\`files\\`, \\`agents\\` widgets`,\n ` via \\`create_widget\\` proactively.`,\n ``,\n `Tools to reach for first: shell, read_file, edit_file, web_search, agent_delegate`,\n `with role=coder for focused sub-tasks.`,\n ].join('\\n'),\n },\n {\n id: 'dj',\n name: 'DJ / Music',\n icon: '🎧',\n color: '#a855f7',\n tagline: 'BPM, key detection, sample library, set planning. Built for producers.',\n agentInstructions: [\n `You are TITAN. The user is in their DJ / Music Space — focused on music`,\n `production, DJing, and audio work.`,\n ``,\n `Default posture:`,\n `- Treat the user as a working musician (Tony is a DJ + producer).`,\n `- When asked for tools, prefer widgets from the music category of the`,\n ` gallery: BPM analyzer, key detector, sample library indexer, Beatport`,\n ` browser, DJ set planner, music theory helper.`,\n `- When the user mentions a song / artist / set, suggest building a widget`,\n ` rather than just answering in text.`,\n `- Be casual + creative; this is the user's creative space.`,\n ].join('\\n'),\n },\n {\n id: 'founder',\n name: 'Founder',\n icon: '🚀',\n color: '#f59e0b',\n tagline: 'Costs, agents, telemetry, growth. The operator\\'s cockpit.',\n agentInstructions: [\n `You are TITAN. The user is in their Founder Space — focused on running`,\n `their business: costs, growth, team coordination, strategy.`,\n ``,\n `Default posture:`,\n `- Treat the user as a builder + operator. Bias to action over analysis.`,\n `- When asked about state of the business, surface the right widget:`,\n ` costs, telemetry, agents, weekly report.`,\n `- When delegating, prefer \\`agent_team\\` for multi-domain compare`,\n ` (e.g., \"research these 3 options\"), \\`agent_chain\\` for sequential`,\n ` work (e.g., \"research → draft → review\").`,\n `- Track follow-ups proactively. If the user describes something they`,\n ` need to remember, offer to add it to memory.`,\n ].join('\\n'),\n },\n {\n id: 'homelab',\n name: 'Homelab Operator',\n icon: '🛠️',\n color: '#ef4444',\n tagline: 'VRAM, fleet, mesh, NVIDIA. The infrastructure cockpit.',\n agentInstructions: [\n `You are TITAN. The user is in their Homelab Space — focused on running`,\n `their infrastructure: GPU servers, mesh, models, hardware.`,\n ``,\n `Default posture:`,\n `- Treat the user as a senior operator. Use technical terms freely.`,\n `- When asked about system state, surface the right widget: vram, fleet,`,\n ` organism, nvidia, mesh, telemetry.`,\n `- For diagnostic asks, reach for shell + read_file + logs first;`,\n ` prefer concrete commands over guidance.`,\n `- When something looks broken, propose a fix THEN ask if you should`,\n ` apply it (don't run destructive shell commands silently).`,\n ].join('\\n'),\n },\n]);\n\nexport function getStarterPreset(id: StarterSpacePreset['id']): StarterSpacePreset | undefined {\n return STARTER_PRESETS.find(p => p.id === id);\n}\n\nexport function listStarterPresets(): ReadonlyArray<StarterSpacePreset> {\n return STARTER_PRESETS;\n}\n"],"mappings":";AAyCO,MAAM,kBAAqD,OAAO,OAAO;AAAA,EAC5E;AAAA,IACI,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AAAA,IACT,mBAAmB;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACJ,EAAE,KAAK,IAAI;AAAA,EACf;AAAA,EACA;AAAA,IACI,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AAAA,IACT,mBAAmB;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACJ,EAAE,KAAK,IAAI;AAAA,EACf;AAAA,EACA;AAAA,IACI,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AAAA,IACT,mBAAmB;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACJ,EAAE,KAAK,IAAI;AAAA,EACf;AAAA,EACA;AAAA,IACI,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AAAA,IACT,mBAAmB;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACJ,EAAE,KAAK,IAAI;AAAA,EACf;AAAA,EACA;AAAA,IACI,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AAAA,IACT,mBAAmB;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACJ,EAAE,KAAK,IAAI;AAAA,EACf;AACJ,CAAC;AAEM,SAAS,iBAAiB,IAA8D;AAC3F,SAAO,gBAAgB,KAAK,OAAK,EAAE,OAAO,EAAE;AAChD;AAEO,SAAS,qBAAwD;AACpE,SAAO;AACX;","names":[]}
|
package/dist/utils/constants.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
const TITAN_VERSION = "6.
|
|
4
|
+
const TITAN_VERSION = "6.5.0";
|
|
5
5
|
const TITAN_CODENAME = "Living Canvas";
|
|
6
6
|
const TITAN_NAME = "TITAN";
|
|
7
7
|
const TITAN_FULL_NAME = "The Intelligent Task Automation Network";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/constants.ts"],"sourcesContent":["/**\n * TITAN Constants\n */\nimport { homedir } from 'os';\nimport { join } from 'path';\n\nexport const TITAN_VERSION = '6.
|
|
1
|
+
{"version":3,"sources":["../../src/utils/constants.ts"],"sourcesContent":["/**\n * TITAN Constants\n */\nimport { homedir } from 'os';\nimport { join } from 'path';\n\nexport const TITAN_VERSION = '6.5.0';\nexport const TITAN_CODENAME = 'Living Canvas';\nexport const TITAN_NAME = 'TITAN';\nexport const TITAN_FULL_NAME = 'The Intelligent Task Automation Network';\nexport const TITAN_ASCII_LOGO = `\n╔══════════════════════════════════════════════════════╗\n║ ║\n║ ████████╗██╗████████╗ █████╗ ███╗ ██╗ ║\n║ ██║ ██║ ██║ ██╔══██╗████╗ ██║ ║\n║ ██║ ██║ ██║ ███████║██╔██╗ ██║ ║\n║ ██║ ██║ ██║ ██╔══██║██║╚██╗██║ ║\n║ ██║ ██║ ██║ ██║ ██║██║ ╚████║ ║\n║ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ║\n║ ║\n║ The Intelligent Task Automation Network ║\n║ v${TITAN_VERSION} • by Tony Elliott ║\n╚══════════════════════════════════════════════════════╝`;\n\n// Paths\n// Hunt Finding #03 (2026-04-14): honor TITAN_HOME env var if set.\n// Previously this was hardcoded to `~/.titan`, which meant:\n// - Docker containers couldn't override the config path\n// - Shared machines couldn't isolate per-user state\n// - Test fixtures couldn't run against an isolated home\n// - The systemd unit's `Environment=TITAN_HOME=...` was silently ignored\n// The env var is read once at module load (constants are resolved at import time).\n// If TITAN_HOME starts with `~/`, expand it to the user's home dir.\nfunction resolveTitanHome(): string {\n const envHome = process.env.TITAN_HOME;\n if (envHome && envHome.trim().length > 0) {\n const trimmed = envHome.trim();\n if (trimmed.startsWith('~/')) {\n return join(homedir(), trimmed.slice(2));\n }\n if (trimmed === '~') {\n return homedir();\n }\n return trimmed;\n }\n return join(homedir(), '.titan');\n}\nexport const TITAN_HOME = resolveTitanHome();\nexport const TITAN_CONFIG_PATH = join(TITAN_HOME, 'titan.json');\nexport const TITAN_DB_PATH = join(TITAN_HOME, 'titan.db');\nexport const TITAN_WORKSPACE = join(TITAN_HOME, 'workspace');\nexport const TITAN_SKILLS_DIR = join(TITAN_WORKSPACE, 'skills');\nexport const TITAN_LOGS_DIR = join(TITAN_HOME, 'logs');\nexport const TITAN_MEMORY_DIR = join(TITAN_HOME, 'memory');\n\n// Workspace prompt files (injected into agent context)\nexport const AGENTS_MD = join(TITAN_WORKSPACE, 'AGENTS.md');\nexport const SOUL_MD = join(TITAN_WORKSPACE, 'SOUL.md');\nexport const TOOLS_MD = join(TITAN_WORKSPACE, 'TOOLS.md');\nexport const TITAN_MD_FILENAME = 'TITAN.md';\nexport const AUTOPILOT_MD = join(TITAN_HOME, 'AUTOPILOT.md');\nexport const AUTOPILOT_RUNS_PATH = join(TITAN_HOME, 'autopilot-runs.jsonl');\nexport const TITAN_CREDENTIALS_DIR = join(TITAN_HOME, 'credentials');\n\n// Income & lead tracking\nexport const INCOME_LEDGER_PATH = join(TITAN_HOME, 'income-ledger.jsonl');\nexport const FREELANCE_LEADS_PATH = join(TITAN_HOME, 'freelance-leads.jsonl');\nexport const FREELANCE_PROFILE_PATH = join(TITAN_HOME, 'freelance-profile.json');\nexport const LEADS_PATH = join(TITAN_HOME, 'leads.jsonl');\nexport const TELEMETRY_EVENTS_PATH = join(TITAN_HOME, 'telemetry-events.jsonl');\nexport const SOMADRIVE_STATE_PATH = join(TITAN_HOME, 'soma-drive-state.json');\nexport const ACTIVITY_LOG_PATH = join(TITAN_HOME, 'activity-log.jsonl');\n\n// v6.1.0 — Mission Chat\nexport const MISSIONS_DIR = join(TITAN_HOME, 'missions');\nexport const PLAYS_DIR = join(TITAN_HOME, 'plays');\n\n// Gateway defaults\nexport const DEFAULT_GATEWAY_HOST = '0.0.0.0';\nexport const DEFAULT_GATEWAY_PORT = 48420;\nexport const DEFAULT_WEB_PORT = 48421;\n\n// Agent defaults\n// v6.0.1 — Hardcoded floor only. The actual default at runtime is picked\n// by `getDefaultModelId()` in `src/providers/defaultModel.ts`, which\n// detects the user's available API keys (Anthropic / OpenAI / Google /\n// OpenRouter) and selects accordingly. Falls back to local Ollama when\n// no cloud keys are set. This constant is the last-resort fallback for\n// callsites that don't go through the schema thunk.\nexport const DEFAULT_MODEL = 'anthropic/claude-sonnet-4-20250514';\n/** v5.4.1: User-preference ceiling. Providers clamp per-model via\n * clampMaxTokens() so this can be high without causing 400s on\n * capped endpoints (e.g. Claude Sonnet 4 8K, Cohere 4K). */\nexport const DEFAULT_MAX_TOKENS = 200000;\nexport const DEFAULT_TEMPERATURE = 0.7;\nexport const MAX_CONTEXT_MESSAGES = 50;\nexport const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes\n\n// Security\nexport const DEFAULT_SANDBOX_MODE = 'host';\n/** Default allowed tools. Empty = allow ALL registered tools.\n * Use security.deniedTools to block specific tools instead. */\nexport const ALLOWED_TOOLS_DEFAULT: string[] = [];\nexport const DENIED_TOOLS_DEFAULT: string[] = [];\n"],"mappings":";AAGA,SAAS,eAAe;AACxB,SAAS,YAAY;AAEd,MAAM,gBAAgB;AACtB,MAAM,iBAAiB;AACvB,MAAM,aAAa;AACnB,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAW1B,aAAa;AAAA;AAYnB,SAAS,mBAA2B;AAChC,QAAM,UAAU,QAAQ,IAAI;AAC5B,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACtC,UAAM,UAAU,QAAQ,KAAK;AAC7B,QAAI,QAAQ,WAAW,IAAI,GAAG;AAC1B,aAAO,KAAK,QAAQ,GAAG,QAAQ,MAAM,CAAC,CAAC;AAAA,IAC3C;AACA,QAAI,YAAY,KAAK;AACjB,aAAO,QAAQ;AAAA,IACnB;AACA,WAAO;AAAA,EACX;AACA,SAAO,KAAK,QAAQ,GAAG,QAAQ;AACnC;AACO,MAAM,aAAa,iBAAiB;AACpC,MAAM,oBAAoB,KAAK,YAAY,YAAY;AACvD,MAAM,gBAAgB,KAAK,YAAY,UAAU;AACjD,MAAM,kBAAkB,KAAK,YAAY,WAAW;AACpD,MAAM,mBAAmB,KAAK,iBAAiB,QAAQ;AACvD,MAAM,iBAAiB,KAAK,YAAY,MAAM;AAC9C,MAAM,mBAAmB,KAAK,YAAY,QAAQ;AAGlD,MAAM,YAAY,KAAK,iBAAiB,WAAW;AACnD,MAAM,UAAU,KAAK,iBAAiB,SAAS;AAC/C,MAAM,WAAW,KAAK,iBAAiB,UAAU;AACjD,MAAM,oBAAoB;AAC1B,MAAM,eAAe,KAAK,YAAY,cAAc;AACpD,MAAM,sBAAsB,KAAK,YAAY,sBAAsB;AACnE,MAAM,wBAAwB,KAAK,YAAY,aAAa;AAG5D,MAAM,qBAAqB,KAAK,YAAY,qBAAqB;AACjE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,yBAAyB,KAAK,YAAY,wBAAwB;AACxE,MAAM,aAAa,KAAK,YAAY,aAAa;AACjD,MAAM,wBAAwB,KAAK,YAAY,wBAAwB;AACvE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,oBAAoB,KAAK,YAAY,oBAAoB;AAG/D,MAAM,eAAe,KAAK,YAAY,UAAU;AAChD,MAAM,YAAY,KAAK,YAAY,OAAO;AAG1C,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,mBAAmB;AASzB,MAAM,gBAAgB;AAItB,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAC7B,MAAM,qBAAqB,KAAK,KAAK;AAGrC,MAAM,uBAAuB;AAG7B,MAAM,wBAAkC,CAAC;AACzC,MAAM,uBAAiC,CAAC;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "titan-agent",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.5.0",
|
|
4
4
|
"description": "TITAN — TypeScript AI agent framework with multi-agent orchestration, provider routing, approval gates, receipts, evals, gateway APIs, and a React Mission Control dashboard. Open-source, MIT licensed.",
|
|
5
5
|
"author": "Tony Elliott (https://github.com/Djtony707)",
|
|
6
6
|
"repository": {
|
package/ui/dist/sw.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* but a default falls back to the source-controlled value here.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
const CACHE_NAME = 'titan-' + ('
|
|
23
|
+
const CACHE_NAME = 'titan-' + ('1780947064421');
|
|
24
24
|
const ASSETS_PREFIX = '/assets/';
|
|
25
25
|
|
|
26
26
|
self.addEventListener('install', (event) => {
|