@wrongstack/tools 0.287.0 → 0.289.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/_fetch-guard.d.ts.map +1 -1
- package/dist/_redact-command.d.ts.map +1 -1
- package/dist/_spawn-stream.d.ts.map +1 -1
- package/dist/audit.js +61 -14
- package/dist/audit.js.map +3 -3
- package/dist/bash.d.ts +1 -1
- package/dist/bash.d.ts.map +1 -1
- package/dist/bash.js +97 -19
- package/dist/bash.js.map +3 -3
- package/dist/builtin.d.ts +12 -10
- package/dist/builtin.d.ts.map +1 -1
- package/dist/builtin.js +1620 -803
- package/dist/builtin.js.map +3 -3
- package/dist/codebase-index/background-indexer.d.ts.map +1 -1
- package/dist/codebase-index/bm25.d.ts +1 -2
- package/dist/codebase-index/bm25.d.ts.map +1 -1
- package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
- package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
- package/dist/codebase-index/codebase-stats-tool.d.ts +7 -0
- package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
- package/dist/codebase-index/index-service.d.ts +11 -1
- package/dist/codebase-index/index-service.d.ts.map +1 -1
- package/dist/codebase-index/index.d.ts +2 -1
- package/dist/codebase-index/index.d.ts.map +1 -1
- package/dist/codebase-index/index.js +749 -192
- package/dist/codebase-index/index.js.map +3 -3
- package/dist/codebase-index/indexer.d.ts.map +1 -1
- package/dist/codebase-index/schema.d.ts +43 -1
- package/dist/codebase-index/schema.d.ts.map +1 -1
- package/dist/codebase-index/ts-parser.d.ts.map +1 -1
- package/dist/codebase-index/worker-protocol.d.ts +1 -0
- package/dist/codebase-index/worker-protocol.d.ts.map +1 -1
- package/dist/codebase-index/worker.js +610 -152
- package/dist/codebase-index/worker.js.map +3 -3
- package/dist/codebase-index/writer.d.ts +27 -1
- package/dist/codebase-index/writer.d.ts.map +1 -1
- package/dist/e2e.js +477 -0
- package/dist/e2e.js.map +7 -0
- package/dist/exec.d.ts +1 -1
- package/dist/exec.d.ts.map +1 -1
- package/dist/exec.js +60 -5
- package/dist/exec.js.map +3 -3
- package/dist/fetch.js +2 -2
- package/dist/fetch.js.map +2 -2
- package/dist/format.js +61 -14
- package/dist/format.js.map +3 -3
- package/dist/glob.js +2 -2
- package/dist/glob.js.map +1 -1
- package/dist/grep.d.ts.map +1 -1
- package/dist/grep.js +2 -2
- package/dist/grep.js.map +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1786 -809
- package/dist/index.js.map +3 -3
- package/dist/install.js +61 -14
- package/dist/install.js.map +3 -3
- package/dist/kanban.d.ts +11 -2
- package/dist/kanban.d.ts.map +1 -1
- package/dist/kanban.js +695 -571
- package/dist/kanban.js.map +3 -3
- package/dist/languages/detect.d.ts.map +1 -1
- package/dist/languages/index.js +61 -14
- package/dist/languages/index.js.map +3 -3
- package/dist/languages/types.d.ts +7 -0
- package/dist/languages/types.d.ts.map +1 -1
- package/dist/lint.js +61 -14
- package/dist/lint.js.map +3 -3
- package/dist/outdated.js +61 -14
- package/dist/outdated.js.map +3 -3
- package/dist/pack.js +1617 -800
- package/dist/pack.js.map +3 -3
- package/dist/plan.js +4 -0
- package/dist/plan.js.map +2 -2
- package/dist/process-registry.js +12 -5
- package/dist/process-registry.js.map +2 -2
- package/dist/search.js +2 -2
- package/dist/search.js.map +2 -2
- package/dist/session-kanban.d.ts +9 -0
- package/dist/session-kanban.d.ts.map +1 -1
- package/dist/session-kanban.js +137 -0
- package/dist/session-kanban.js.map +2 -2
- package/dist/task.js +4 -0
- package/dist/task.js.map +2 -2
- package/dist/test.js +61 -14
- package/dist/test.js.map +3 -3
- package/dist/todo.js +4 -0
- package/dist/todo.js.map +2 -2
- package/dist/tree.js +2 -2
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.js +61 -14
- package/dist/typecheck.js.map +3 -3
- package/package.json +7 -3
package/dist/search.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/search.ts", "../src/_fetch-guard.ts"],
|
|
4
|
-
"sourcesContent": ["import { expectDefined, FetchError, ToolValidationError } from '@wrongstack/core';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { guardedFetch } from './_fetch-guard.js';\nimport { toErrorMessage } from '@wrongstack/core/utils';\n\ninterface SearchResult {\n title: string;\n url: string;\n snippet: string;\n score: number;\n}\n\ninterface CacheEntry {\n results: SearchResult[];\n source: string;\n timestamp: number;\n}\n\ninterface SearchInput {\n query: string;\n num_results?: number | undefined;\n source?: 'duckduckgo' | 'google' | 'bing' | undefined;\n skip_cache?: boolean | undefined;\n}\n\ninterface SearchOutput {\n query: string;\n results: { title: string; url: string; snippet: string }[];\n source: string;\n truncated: boolean;\n cached: boolean;\n}\n\nconst DEFAULT_NUM = 10;\nconst MAX_RESULTS = 50;\nconst TIMEOUT_MS = 15_000;\nconst CACHE_TTL_MS = 300_000; // 5 minutes \u2014 matches the former web-search plugin default\nconst CACHE_MAX_ENTRIES = 200;\n\n// Module-level cache shared across calls within a single agent run. Keyed by\n// `<source>:<query>`. This is intentionally process-local (not persisted) \u2014\n// it exists to avoid hammering a search engine when the model rephrases the\n// same query a few turns apart.\nconst cache = new Map<string, CacheEntry>();\n\nexport const searchTool: Tool<SearchInput, SearchOutput> = {\n name: 'search',\n category: 'Search',\n description:\n 'Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL.',\n usageHint:\n 'Good for: API documentation, error messages, library usage examples, current best practices.\\n\\n' +\n '- Prefer specific queries over very broad ones.\\n' +\n '- Results go through the guarded fetch system (same protections as the `fetch` tool).\\n' +\n '- Supports duckduckgo (default), google, and bing sources.\\n' +\n '- Set `skip_cache: true` to force a fresh search.\\n' +\n '- This is often better than the model trying to recall outdated knowledge.',\n permission: 'auto',\n mutating: false,\n capabilities: ['net.outbound'],\n icon: 'search',\n timeoutMs: TIMEOUT_MS,\n inputSchema: {\n type: 'object',\n properties: {\n query: { type: 'string', description: 'Search query' },\n num_results: {\n type: 'integer',\n description: 'Number of results (1-50, default 10)',\n minimum: 1,\n maximum: MAX_RESULTS,\n },\n source: {\n type: 'string',\n enum: ['duckduckgo', 'google', 'bing'],\n description: 'Search engine to use (default: duckduckgo)',\n },\n skip_cache: {\n type: 'boolean',\n description: 'Skip the in-memory cache and force a fresh search (default: false)',\n },\n },\n required: ['query'],\n },\n async execute(input, ctx, opts) {\n let final: SearchOutput | undefined;\n const executeStream = searchTool.executeStream;\n if (!executeStream) throw new Error('searchTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('search: stream ended without final event');\n return final;\n },\n async *executeStream(input, _ctx, opts): AsyncGenerator<ToolStreamEvent<SearchOutput>> {\n if (!input?.query || input.query.trim() === '') {\n throw new ToolValidationError({\n message: 'search: query is required and must be a non-empty string',\n field: 'query',\n });\n }\n\n const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));\n const source = input.source ?? 'duckduckgo';\n const skipCache = input.skip_cache ?? false;\n const cacheKey = `${source}:${input.query}`;\n\n // --- Cache hit ---\n if (!skipCache) {\n const entry = cache.get(cacheKey);\n if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {\n const results = entry.results.map((r) => ({\n title: r.title,\n url: r.url,\n snippet: r.snippet,\n }));\n yield {\n type: 'log',\n text: `Cache hit for \"${input.query}\" (${source})`,\n data: { source, query: input.query, cached: true },\n };\n yield {\n type: 'partial_output',\n text: `${results.length} cached results from ${entry.source}`,\n data: { count: results.length, cached: true, source: entry.source },\n };\n yield {\n type: 'final',\n output: {\n query: input.query,\n results: results.slice(0, num),\n source: entry.source,\n truncated: results.length >= num,\n cached: true,\n },\n };\n return;\n }\n }\n\n yield {\n type: 'log',\n text: `Querying ${source} for \"${input.query}\"\u2026`,\n data: { source, query: input.query, cached: false },\n };\n\n let rawResults: SearchResult[];\n let effectiveSource: SearchOutput['source'] = source;\n switch (source) {\n case 'duckduckgo':\n rawResults = await duckduckgoSearch(input.query, num, opts.signal);\n break;\n case 'google':\n rawResults = await googleSearch(input.query, num, opts.signal);\n break;\n case 'bing':\n rawResults = await bingSearch(input.query, num, opts.signal);\n break;\n default:\n throw new ToolValidationError({\n message: `search: unknown source \"${source}\"`,\n field: 'source',\n });\n }\n\n let ranked = rankSearchResults(rawResults, input.query);\n if (source !== 'duckduckgo' && shouldFallbackToDuckDuckGo(ranked, input.query)) {\n yield {\n type: 'log',\n text: `${source} returned no relevant static results; falling back to duckduckgo`,\n data: { source, fallback: 'duckduckgo', query: input.query },\n };\n rawResults = await duckduckgoSearch(input.query, num, opts.signal);\n ranked = rankSearchResults(rawResults, input.query);\n effectiveSource = 'duckduckgo';\n }\n\n const finalResults = ranked.slice(0, num);\n\n // --- Store in cache ---\n cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });\n pruneCacheEntries();\n\n yield {\n type: 'partial_output',\n text: `${finalResults.length} results from ${effectiveSource}`,\n data: { count: finalResults.length, cached: false, source: effectiveSource },\n };\n yield {\n type: 'final',\n output: {\n query: input.query,\n results: finalResults.map((r) => ({\n title: r.title,\n url: r.url,\n snippet: r.snippet,\n })),\n source: effectiveSource,\n truncated: finalResults.length >= num,\n cached: false,\n },\n };\n },\n};\n\n// ---------------------------------------------------------------------------\n// Cache helpers\n// ---------------------------------------------------------------------------\n\n/** Drop stale entries, then evict oldest insertions to cap retained result data. */\nfunction pruneCacheEntries(): void {\n const cutoff = Date.now() - CACHE_TTL_MS * 2;\n for (const [key, entry] of cache.entries()) {\n if (entry.timestamp < cutoff) cache.delete(key);\n }\n while (cache.size > CACHE_MAX_ENTRIES) {\n const oldestKey = cache.keys().next().value;\n if (oldestKey === undefined) break;\n cache.delete(oldestKey);\n }\n}\n\n/** Exposed for tests so they can reset the module-level cache between cases. */\nexport function __clearSearchCache(): void {\n cache.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Ranking\n// ---------------------------------------------------------------------------\n\nfunction rankSearchResults(results: SearchResult[], query: string): SearchResult[] {\n const seenUrls = new Set<string>();\n const deduped: SearchResult[] = [];\n for (const r of results) {\n const noQuery = r.url.split('?')[0] ?? r.url;\n const normalized = noQuery.split('#')[0] ?? r.url;\n if (!seenUrls.has(normalized) && r.url.startsWith('http')) {\n seenUrls.add(normalized);\n deduped.push(r);\n }\n }\n return scoreResults(deduped, query);\n}\n\nfunction scoreResults(results: SearchResult[], query: string): SearchResult[] {\n const terms = query.toLowerCase().split(/\\s+/).filter((t) => t.length > 0);\n return results\n .map((r) => {\n const titleLower = r.title.toLowerCase();\n const snippetLower = r.snippet.toLowerCase();\n let score = r.score;\n for (const term of terms) {\n if (titleLower.includes(term)) score += 2;\n if (snippetLower.includes(term)) score += 1;\n }\n return { ...r, score };\n })\n .sort((a, b) => b.score - a.score);\n}\n\nfunction shouldFallbackToDuckDuckGo(results: SearchResult[], query: string): boolean {\n if (results.length === 0) return true;\n const terms = query.toLowerCase().split(/\\s+/).filter((t) => t.length >= 3);\n if (terms.length === 0) return false;\n return !results.some((r) => {\n const haystack = `${r.title} ${r.url} ${r.snippet}`.toLowerCase();\n return terms.some((term) => haystack.includes(term));\n });\n}\n\n// ---------------------------------------------------------------------------\n// Search engines\n// ---------------------------------------------------------------------------\n\nasync function duckduckgoSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchResult[]> {\n const encoded = encodeURIComponent(query);\n const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;\n\n try {\n const response = await fetchWithTimeout(url, signal, TIMEOUT_MS);\n const html = await response.text();\n return parseDuckDuckGo(html, num);\n } catch (err) {\n console.log(\n JSON.stringify({ level: 'debug', event: 'search_failed', query, error: toErrorMessage(err) }),\n );\n // Return a sentinel result that survives the dedup filter (which drops\n // non-http URLs). Using a placeholder http URL keeps it visible to the\n // caller so they know the search failed rather than silently getting [].\n return [{ title: 'Search unavailable', url: 'https://duckduckgo.com/unavailable', snippet: 'Could not reach DuckDuckGo', score: 0 }];\n }\n}\n\nfunction takeFrom<T>(iter: Iterable<T>, max: number): T[] {\n const out: T[] = [];\n for (const item of iter) {\n if (out.length >= max) break;\n out.push(item);\n }\n return out;\n}\n\nfunction parseDuckDuckGo(html: string, num: number): SearchResult[] {\n const results: SearchResult[] = [];\n const linkRegex = /<a\\b([^>]*\\bclass=([\"'])[^\"']*\\bresult-link\\b[^\"']*\\2[^>]*)>([\\s\\S]*?)<\\/a>/gi;\n const snippetRegex =\n /<([a-z0-9]+)\\b([^>]*\\bclass=([\"'])[^\"']*\\bresult-snippet\\b[^\"']*\\3[^>]*)>([\\s\\S]*?)<\\/\\1>/gi;\n\n const linkMatches = takeFrom(\n [...html.matchAll(linkRegex)]\n .map((m) => {\n const attrs = expectDefined(m[1]);\n const href = getHtmlAttr(attrs, 'href');\n const title = stripTags(expectDefined(m[3]));\n return href && title ? { url: normalizeDuckDuckGoUrl(href), title } : undefined;\n })\n .filter((m): m is { url: string; title: string } => m !== undefined),\n num,\n );\n\n const snippetMatches = takeFrom(\n [...html.matchAll(snippetRegex)]\n .filter((m) => m[4])\n .map((m) => stripTags(expectDefined(m[4]))),\n num,\n );\n\n for (let i = 0; i < linkMatches.length && i < num; i++) {\n const entry = linkMatches[i];\n if (entry) {\n results.push({\n title: entry.title ?? '',\n url: entry.url ?? '',\n snippet: snippetMatches[i] ?? '',\n score: 1,\n });\n }\n }\n\n return results;\n}\n\nfunction getHtmlAttr(attrs: string, name: string): string | undefined {\n const quoted = new RegExp(`\\\\b${name}\\\\s*=\\\\s*([\"'])(.*?)\\\\1`, 'i').exec(attrs);\n if (quoted?.[2]) return decodeHtmlEntities(quoted[2]);\n const unquoted = new RegExp(`\\\\b${name}\\\\s*=\\\\s*([^\\\\s>]+)`, 'i').exec(attrs);\n return unquoted?.[1] ? decodeHtmlEntities(unquoted[1]) : undefined;\n}\n\nfunction normalizeDuckDuckGoUrl(raw: string): string {\n if (raw.startsWith('//')) return `https:${raw}`;\n if (!raw.startsWith('/')) return raw;\n if (!raw.startsWith('/l/')) return raw;\n try {\n const url = new URL(raw, 'https://duckduckgo.com');\n const uddg = url.searchParams.get('uddg');\n return uddg?.startsWith('http') ? uddg : url.toString();\n } catch {\n return raw;\n }\n}\n\nasync function googleSearch(query: string, num: number, signal: AbortSignal): Promise<SearchResult[]> {\n const encoded = encodeURIComponent(query);\n const url = `https://www.google.com/search?q=${encoded}&hl=en`;\n\n const html = await fetchWithTimeout(url, signal, TIMEOUT_MS)\n .then((r) => r.text())\n .catch(() => '');\n\n return parseGoogleResults(html, num);\n}\n\nfunction parseGoogleResults(html: string, num: number): SearchResult[] {\n const results: SearchResult[] = [];\n const titleRegex = /<h3[^>]*class=\"[^\"]*DKV84\"[^>]*>([^<]+)<\\/h3>/gi;\n const urlRegex = /<cite[^>]*>([^<]+)<\\/cite>/gi;\n const snippetRegex = /<span[^>]*class=\"[^\"]*aXCZ0b[^>]*>([^<]+)<\\/span>/gi;\n\n const titles = takeFrom(\n [...html.matchAll(titleRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n const urls = takeFrom(\n [...html.matchAll(urlRegex)]\n .filter((m) => m[1])\n .map((m) => stripTags(expectDefined(m[1])).replace(/^\\*(https?:\\/\\/[^\\s]+).*$/, '$1'))\n .filter((u) => u.startsWith('http')),\n num,\n );\n\n const snippets = takeFrom(\n [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n for (let i = 0; i < Math.min(titles.length, num); i++) {\n results.push({\n title: titles[i] ?? '',\n url: urls[i] ?? '',\n snippet: snippets[i] ?? '',\n score: 1,\n });\n }\n\n return results;\n}\n\nasync function bingSearch(query: string, num: number, signal: AbortSignal): Promise<SearchResult[]> {\n const encoded = encodeURIComponent(query);\n const url = `https://www.bing.com/search?q=${encoded}`;\n\n const html = await fetchWithTimeout(url, signal, TIMEOUT_MS)\n .then((r) => r.text())\n .catch(() => '');\n\n return parseBingResults(html, num);\n}\n\nfunction parseBingResults(html: string, num: number): SearchResult[] {\n const results: SearchResult[] = [];\n const blocks = [...html.matchAll(/<li\\b[^>]*class=([\"'])[^\"']*\\bb_algo\\b[^\"']*\\1[^>]*>([\\s\\S]*?)(?=<li\\b[^>]*class=([\"'])[^\"']*\\bb_algo\\b[^\"']*\\3|<\\/ol>)/gi)]\n .map((m) => expectDefined(m[2]));\n const candidates = blocks.length > 0 ? blocks : [html];\n\n const entries = takeFrom(candidates.flatMap((block) => {\n const titleMatch = /<h2[^>]*>\\s*<a\\b([^>]*)>([\\s\\S]*?)<\\/a>\\s*<\\/h2>/i.exec(block);\n if (!titleMatch) return [];\n const href = getHtmlAttr(expectDefined(titleMatch[1]), 'href');\n const title = stripTags(expectDefined(titleMatch[2]));\n if (!href || !title) return [];\n const snippetMatch = /<p\\b[^>]*class=([\"'])[^\"']*\\b(?:b_paractl|b_lineclamp\\d*)\\b[^\"']*\\1[^>]*>([\\s\\S]*?)<\\/p>/i.exec(block)\n ?? /<p\\b[^>]*>([\\s\\S]*?)<\\/p>/i.exec(block);\n const snippet = snippetMatch ? stripTags(expectDefined(snippetMatch.at(-1))) : '';\n return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];\n }), num);\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n if (entry) {\n results.push({\n title: entry.title ?? '',\n url: entry.url ?? '',\n snippet: entry.snippet ?? '',\n score: 1,\n });\n }\n }\n\n return results;\n}\n\nfunction normalizeBingUrl(raw: string): string {\n try {\n const url = new URL(raw);\n if (url.hostname.endsWith('bing.com') && url.pathname.startsWith('/ck/')) {\n const encoded = url.searchParams.get('u');\n const decoded = decodeBingTarget(encoded);\n if (decoded?.startsWith('http')) return decoded;\n }\n } catch {\n // Fall through to the raw URL.\n }\n return raw;\n}\n\nfunction decodeBingTarget(encoded: string | null): string | undefined {\n if (!encoded) return undefined;\n const payload = encoded.startsWith('a1') ? encoded.slice(2) : encoded;\n try {\n const padded = payload.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(payload.length / 4) * 4, '=');\n return Buffer.from(padded, 'base64').toString('utf8');\n } catch {\n return undefined;\n }\n}\n\n// ---------------------------------------------------------------------------\n// HTTP helper\n// ---------------------------------------------------------------------------\n\nasync function fetchWithTimeout(\n url: string,\n signal: AbortSignal,\n timeoutMs: number,\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n const fetchSignal = anySignal(signal, controller.signal);\n try {\n // F-05: route through the SSRF-guarded fetch (private-IP blocking, HTTPS,\n // DNS-pinned dispatcher, per-hop redirect re-validation) instead of a bare\n // `fetch` with `redirect: 'follow'`. Search hosts are fixed/trusted, but\n // this closes the residual \"engine 30x \u2192 internal address\" redirect risk.\n const res = await guardedFetch(url, 5, fetchSignal, {\n 'user-agent':\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n });\n clearTimeout(timer);\n return res;\n } catch (e) {\n clearTimeout(timer);\n if (e instanceof FetchError) {\n throw e;\n }\n throw new FetchError({\n message: `search: failed to fetch ${url}`,\n status: 0,\n context: { url },\n cause: e,\n });\n }\n}\n\nfunction anySignal(...signals: AbortSignal[]): AbortSignal {\n // Native combinator (Node \u2265 20.3; this repo requires \u2265 22). The previous\n // hand-rolled version registered a non-once 'abort' listener on every\n // input signal and never removed it \u2014 the run-level signal outlives each\n // request, so listeners (and their closures) accumulated one per search\n // call for the life of the agent run.\n return AbortSignal.any(signals);\n}\n\nfunction stripTags(html: string): string {\n return decodeHtmlEntities(html.replace(/<[^>]+>/g, '')).trim();\n}\n\nfunction decodeHtmlEntities(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\");\n}\n", "import * as dns from 'node:dns/promises';\nimport * as net from 'node:net';\nimport {\n FetchError,\n ToolValidationError,\n isPrivateIPv4,\n isPrivateIPv6,\n} from '@wrongstack/core';\nimport { Agent, fetch as undiciFetch } from 'undici';\n\n/**\n * SSRF guard machinery shared by the `fetch` and `search` tools. Split out of\n * fetch.ts so entries that only need `guardedFetch` (search) don't bundle the\n * whole fetch tool. Everything here is security-critical: the pinned undici\n * dispatcher performs the SINGLE DNS resolution the TCP connection uses, so\n * there is no DNS-rebinding TOCTOU between validation and connect.\n */\n\nconst nativeGlobalFetch = globalThis.fetch;\n\nexport const ALLOW_PRIVATE = process.env['WRONGSTACK_FETCH_ALLOW_PRIVATE'] === '1';\n/* v8 ignore next 8 -- module-load-time opt-in warning; gated on an env var not set during tests. */\nif (ALLOW_PRIVATE && !process.env['CI']) {\n console.warn(\n '[WrongStack] WARNING: WRONGSTACK_FETCH_ALLOW_PRIVATE=1 is active \u2014\\n' +\n ' fetch tool can now access private IPs (10.x, 192.168.x, 169.254.x),\\n' +\n ' cloud metadata endpoints, and plaintext HTTP. Use only on isolated networks.',\n );\n}\n\ntype LookupCallback = (\n err: NodeJS.ErrnoException | null,\n address?: string | Array<{ address: string | undefined; family: number }>,\n family?: number | undefined,\n) => void;\n\n/**\n * DNS lookup used by the undici dispatcher below. It performs the SINGLE name\n * resolution that the TCP connection actually uses, and rejects if any\n * resolved address is private/loopback/link-local. Because the connection\n * reuses exactly this result, there is no DNS-rebinding TOCTOU window between\n * the security check and the connect \u2014 closing the gap the old code documented\n * (validate with one dns.lookup, then let fetch re-resolve independently).\n * TLS still validates the certificate against the hostname (SNI is set by\n * undici from the URL), so pinning the IP does not weaken cert checking.\n */\nexport function guardedLookup(\n hostname: string,\n options: { all?: boolean | undefined; family?: number | undefined },\n callback: LookupCallback,\n): void {\n dns\n .lookup(hostname, { all: true })\n .then((records) => {\n const family = options?.family;\n const byFamily =\n family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;\n const list = byFamily.length > 0 ? byFamily : records;\n if (!ALLOW_PRIVATE) {\n for (const r of list) {\n const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);\n if (bad) {\n callback(\n Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {\n code: 'EAI_FAIL',\n }),\n );\n return;\n }\n }\n }\n if (options?.all) {\n callback(\n null,\n list.map((r) => ({ address: r.address, family: r.family })),\n );\n return;\n }\n const first = list.at(0);\n if (!first) {\n callback(\n Object.assign(new Error(`fetch: no address for ${hostname}`), { code: 'ENOTFOUND' }),\n );\n return;\n }\n callback(null, first.address, first.family);\n })\n .catch((err) => callback(err as NodeJS.ErrnoException));\n}\n\n// Reused across requests; guardedLookup re-validates on every new connection,\n// so connection pooling is safe. Literal-IP targets bypass lookup entirely and\n// are caught by assertNotPrivate's pre-check instead.\n// Destroyed on process exit so long-running processes (eternal autonomy,\n// MCP server mode) don't let the connection pool grow unboundedly.\nlet pinnedAgent: Agent | undefined;\nfunction getPinnedDispatcher(): Agent {\n if (!pinnedAgent) {\n pinnedAgent = new Agent({ connect: { lookup: guardedLookup as never } });\n }\n return pinnedAgent;\n}\n\nfunction dispatcherFetch(): typeof globalThis.fetch {\n // Node's built-in global fetch is backed by its own bundled undici version.\n // Passing an Agent from the workspace's `undici` package to that different\n // dispatcher ABI fails on recent Node with:\n // UND_ERR_INVALID_ARG: invalid onRequestStart method\n // Use the matching package fetch+Agent pair in real runs, but keep honoring\n // test/user fetch shims that replace globalThis.fetch after this module loads.\n return globalThis.fetch === nativeGlobalFetch\n ? (undiciFetch as unknown as typeof globalThis.fetch)\n : globalThis.fetch;\n}\n// Clean up the global dispatcher on exit \u2014 undici Agents maintain connection\n// pools and DNS caches that should be torn down in long-running processes.\n// Guard against duplicate registration (module reload/HMR would otherwise\n// accumulate listeners).\nlet _beforeExitRegistered = false;\nif (!_beforeExitRegistered) {\n _beforeExitRegistered = true;\n /* v8 ignore next 4 -- process 'beforeExit' cleanup; not deterministically triggerable in-test. */\n process.on('beforeExit', () => {\n pinnedAgent?.destroy();\n pinnedAgent = undefined;\n });\n}\n\n/**\n * SSRF-guarded fetch with manual, per-hop-revalidated redirects, exported so\n * other builtin tools (e.g. `search`) get the same protections instead of a\n * weaker `redirect: 'follow'`. Every hop is re-checked against private/loopback\n * ranges and the connection is pinned to the validated IP via the undici\n * dispatcher (no DNS-rebinding TOCTOU). `headers` defaults to the plain `fetch`\n * tool's; callers may override (e.g. a browser User-Agent for search engines).\n */\nexport async function guardedFetch(\n url: string,\n maxRedirects: number,\n signal: AbortSignal,\n headers: Record<string, string> = {\n 'user-agent': 'WrongStack/1.0 (+https://wrongstack.com)',\n accept: 'text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.1',\n },\n): Promise<Response> {\n let redirectCount = 0;\n let currentUrl = url;\n for (;;) {\n // Re-validate every hop. A public host can 302 to 169.254.169.254 (cloud metadata),\n // or DNS can rebind between hops; checking only the initial URL is insufficient.\n const parsed = new URL(currentUrl);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n throw new ToolValidationError({\n message: `fetch: redirect to unsupported protocol \"${parsed.protocol}\"`,\n field: 'url',\n });\n }\n if (parsed.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new ToolValidationError({\n message: 'fetch: redirect to http:// blocked (HTTPS required by default)',\n field: 'url',\n });\n }\n await assertNotPrivate(parsed.hostname);\n\n // The dispatcher pins the connection to the IP guardedLookup validated \u2014\n // no independent re-resolution, so DNS rebinding can't swap in a private\n // address between check and connect. `dispatcher` is a runtime option of\n // Node's undici-backed global fetch but isn't in lib.dom's RequestInit, and\n // our undici Agent's type differs from the @types/node copy \u2014 hence the\n // cast. (Verified: global fetch invokes the Agent's custom lookup.)\n const init = {\n redirect: 'manual' as const,\n signal,\n headers,\n dispatcher: getPinnedDispatcher(),\n };\n const res = await dispatcherFetch()(currentUrl, init as never as RequestInit);\n if (res.status < 300 || res.status > 399) {\n return res;\n }\n redirectCount++;\n if (redirectCount > maxRedirects) {\n throw new FetchError({\n message: `fetch: exceeded ${maxRedirects} redirects`,\n status: res.status,\n context: { url: currentUrl, maxRedirects, redirectCount },\n });\n }\n const location = res.headers.get('location');\n if (!location) {\n throw new FetchError({\n message: 'fetch: redirect status with no location header',\n status: res.status,\n context: { url: currentUrl, redirectCount },\n });\n }\n currentUrl = new URL(location, currentUrl).toString();\n }\n}\n\nexport async function assertNotPrivate(hostname: string): Promise<void> {\n if (ALLOW_PRIVATE) return;\n\n const host =\n hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;\n\n if (host === 'localhost' || host.endsWith('.localhost')) {\n throw new ToolValidationError({\n message: 'fetch: blocked localhost target',\n field: 'url',\n });\n }\n\n const ipVersion = net.isIP(host);\n if (ipVersion === 4) {\n if (isPrivateIPv4(host)) {\n throw new ToolValidationError({\n message: `fetch: blocked private/loopback address \"${host}\"`,\n field: 'url',\n });\n }\n } else if (ipVersion === 6) {\n if (isPrivateIPv6(host)) {\n throw new ToolValidationError({\n message: `fetch: blocked private/loopback address \"${host}\"`,\n field: 'url',\n });\n }\n } else {\n // Hostname \u2014 pre-flight check: resolve and reject if any record is private,\n // so we fail fast with a clear error before opening a socket. The\n // authoritative anti-rebinding control is guardedLookup on the pinned\n // undici dispatcher (see getPinnedDispatcher): it performs the single\n // resolution the connection actually uses, so there is no TOCTOU between\n // this check and the connect. Each redirect target is re-checked too.\n try {\n // Use dns.lookup for async hostname resolution (matches guardedLookup above).\n const records = await dns.lookup(host, { all: true });\n for (const r of records) {\n const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);\n if (bad) {\n throw new ToolValidationError({\n message: `fetch: resolved to private address ${r.address}`,\n field: 'url',\n });\n }\n }\n } catch (err) {\n if (err instanceof Error && err.message.startsWith('fetch:')) throw err;\n // DNS failure \u2014 let fetch handle it\n }\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { expectDefined, FetchError, ToolValidationError } from '@wrongstack/core';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { guardedFetch } from './_fetch-guard.js';\nimport { toErrorMessage } from '@wrongstack/core/utils';\n\ninterface SearchResult {\n title: string;\n url: string;\n snippet: string;\n score: number;\n}\n\ninterface CacheEntry {\n results: SearchResult[];\n source: string;\n timestamp: number;\n}\n\ninterface SearchInput {\n query: string;\n num_results?: number | undefined;\n source?: 'duckduckgo' | 'google' | 'bing' | undefined;\n skip_cache?: boolean | undefined;\n}\n\ninterface SearchOutput {\n query: string;\n results: { title: string; url: string; snippet: string }[];\n source: string;\n truncated: boolean;\n cached: boolean;\n}\n\nconst DEFAULT_NUM = 10;\nconst MAX_RESULTS = 50;\nconst TIMEOUT_MS = 15_000;\nconst CACHE_TTL_MS = 300_000; // 5 minutes \u2014 matches the former web-search plugin default\nconst CACHE_MAX_ENTRIES = 200;\n\n// Module-level cache shared across calls within a single agent run. Keyed by\n// `<source>:<query>`. This is intentionally process-local (not persisted) \u2014\n// it exists to avoid hammering a search engine when the model rephrases the\n// same query a few turns apart.\nconst cache = new Map<string, CacheEntry>();\n\nexport const searchTool: Tool<SearchInput, SearchOutput> = {\n name: 'search',\n category: 'Search',\n description:\n 'Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL.',\n usageHint:\n 'Good for: API documentation, error messages, library usage examples, current best practices.\\n\\n' +\n '- Prefer specific queries over very broad ones.\\n' +\n '- Results go through the guarded fetch system (same protections as the `fetch` tool).\\n' +\n '- Supports duckduckgo (default), google, and bing sources.\\n' +\n '- Set `skip_cache: true` to force a fresh search.\\n' +\n '- This is often better than the model trying to recall outdated knowledge.',\n permission: 'auto',\n mutating: false,\n capabilities: ['net.outbound'],\n icon: 'search',\n timeoutMs: TIMEOUT_MS,\n inputSchema: {\n type: 'object',\n properties: {\n query: { type: 'string', description: 'Search query' },\n num_results: {\n type: 'integer',\n description: 'Number of results (1-50, default 10)',\n minimum: 1,\n maximum: MAX_RESULTS,\n },\n source: {\n type: 'string',\n enum: ['duckduckgo', 'google', 'bing'],\n description: 'Search engine to use (default: duckduckgo)',\n },\n skip_cache: {\n type: 'boolean',\n description: 'Skip the in-memory cache and force a fresh search (default: false)',\n },\n },\n required: ['query'],\n },\n async execute(input, ctx, opts) {\n let final: SearchOutput | undefined;\n const executeStream = searchTool.executeStream;\n if (!executeStream) throw new Error('searchTool: stream execution unavailable');\n for await (const ev of executeStream(input, ctx, opts)) {\n if (ev.type === 'final') final = ev.output;\n }\n if (!final) throw new Error('search: stream ended without final event');\n return final;\n },\n async *executeStream(input, _ctx, opts): AsyncGenerator<ToolStreamEvent<SearchOutput>> {\n if (!input?.query || input.query.trim() === '') {\n throw new ToolValidationError({\n message: 'search: query is required and must be a non-empty string',\n field: 'query',\n });\n }\n\n const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));\n const source = input.source ?? 'duckduckgo';\n const skipCache = input.skip_cache ?? false;\n const cacheKey = `${source}:${input.query}`;\n\n // --- Cache hit ---\n if (!skipCache) {\n const entry = cache.get(cacheKey);\n if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {\n const results = entry.results.map((r) => ({\n title: r.title,\n url: r.url,\n snippet: r.snippet,\n }));\n yield {\n type: 'log',\n text: `Cache hit for \"${input.query}\" (${source})`,\n data: { source, query: input.query, cached: true },\n };\n yield {\n type: 'partial_output',\n text: `${results.length} cached results from ${entry.source}`,\n data: { count: results.length, cached: true, source: entry.source },\n };\n yield {\n type: 'final',\n output: {\n query: input.query,\n results: results.slice(0, num),\n source: entry.source,\n truncated: results.length >= num,\n cached: true,\n },\n };\n return;\n }\n }\n\n yield {\n type: 'log',\n text: `Querying ${source} for \"${input.query}\"\u2026`,\n data: { source, query: input.query, cached: false },\n };\n\n let rawResults: SearchResult[];\n let effectiveSource: SearchOutput['source'] = source;\n switch (source) {\n case 'duckduckgo':\n rawResults = await duckduckgoSearch(input.query, num, opts.signal);\n break;\n case 'google':\n rawResults = await googleSearch(input.query, num, opts.signal);\n break;\n case 'bing':\n rawResults = await bingSearch(input.query, num, opts.signal);\n break;\n default:\n throw new ToolValidationError({\n message: `search: unknown source \"${source}\"`,\n field: 'source',\n });\n }\n\n let ranked = rankSearchResults(rawResults, input.query);\n if (source !== 'duckduckgo' && shouldFallbackToDuckDuckGo(ranked, input.query)) {\n yield {\n type: 'log',\n text: `${source} returned no relevant static results; falling back to duckduckgo`,\n data: { source, fallback: 'duckduckgo', query: input.query },\n };\n rawResults = await duckduckgoSearch(input.query, num, opts.signal);\n ranked = rankSearchResults(rawResults, input.query);\n effectiveSource = 'duckduckgo';\n }\n\n const finalResults = ranked.slice(0, num);\n\n // --- Store in cache ---\n cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });\n pruneCacheEntries();\n\n yield {\n type: 'partial_output',\n text: `${finalResults.length} results from ${effectiveSource}`,\n data: { count: finalResults.length, cached: false, source: effectiveSource },\n };\n yield {\n type: 'final',\n output: {\n query: input.query,\n results: finalResults.map((r) => ({\n title: r.title,\n url: r.url,\n snippet: r.snippet,\n })),\n source: effectiveSource,\n truncated: finalResults.length >= num,\n cached: false,\n },\n };\n },\n};\n\n// ---------------------------------------------------------------------------\n// Cache helpers\n// ---------------------------------------------------------------------------\n\n/** Drop stale entries, then evict oldest insertions to cap retained result data. */\nfunction pruneCacheEntries(): void {\n const cutoff = Date.now() - CACHE_TTL_MS * 2;\n for (const [key, entry] of cache.entries()) {\n if (entry.timestamp < cutoff) cache.delete(key);\n }\n while (cache.size > CACHE_MAX_ENTRIES) {\n const oldestKey = cache.keys().next().value;\n if (oldestKey === undefined) break;\n cache.delete(oldestKey);\n }\n}\n\n/** Exposed for tests so they can reset the module-level cache between cases. */\nexport function __clearSearchCache(): void {\n cache.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Ranking\n// ---------------------------------------------------------------------------\n\nfunction rankSearchResults(results: SearchResult[], query: string): SearchResult[] {\n const seenUrls = new Set<string>();\n const deduped: SearchResult[] = [];\n for (const r of results) {\n const noQuery = r.url.split('?')[0] ?? r.url;\n const normalized = noQuery.split('#')[0] ?? r.url;\n if (!seenUrls.has(normalized) && r.url.startsWith('http')) {\n seenUrls.add(normalized);\n deduped.push(r);\n }\n }\n return scoreResults(deduped, query);\n}\n\nfunction scoreResults(results: SearchResult[], query: string): SearchResult[] {\n const terms = query.toLowerCase().split(/\\s+/).filter((t) => t.length > 0);\n return results\n .map((r) => {\n const titleLower = r.title.toLowerCase();\n const snippetLower = r.snippet.toLowerCase();\n let score = r.score;\n for (const term of terms) {\n if (titleLower.includes(term)) score += 2;\n if (snippetLower.includes(term)) score += 1;\n }\n return { ...r, score };\n })\n .sort((a, b) => b.score - a.score);\n}\n\nfunction shouldFallbackToDuckDuckGo(results: SearchResult[], query: string): boolean {\n if (results.length === 0) return true;\n const terms = query.toLowerCase().split(/\\s+/).filter((t) => t.length >= 3);\n if (terms.length === 0) return false;\n return !results.some((r) => {\n const haystack = `${r.title} ${r.url} ${r.snippet}`.toLowerCase();\n return terms.some((term) => haystack.includes(term));\n });\n}\n\n// ---------------------------------------------------------------------------\n// Search engines\n// ---------------------------------------------------------------------------\n\nasync function duckduckgoSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchResult[]> {\n const encoded = encodeURIComponent(query);\n const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;\n\n try {\n const response = await fetchWithTimeout(url, signal, TIMEOUT_MS);\n const html = await response.text();\n return parseDuckDuckGo(html, num);\n } catch (err) {\n console.log(\n JSON.stringify({ level: 'debug', event: 'search_failed', query, error: toErrorMessage(err) }),\n );\n // Return a sentinel result that survives the dedup filter (which drops\n // non-http URLs). Using a placeholder http URL keeps it visible to the\n // caller so they know the search failed rather than silently getting [].\n return [{ title: 'Search unavailable', url: 'https://duckduckgo.com/unavailable', snippet: 'Could not reach DuckDuckGo', score: 0 }];\n }\n}\n\nfunction takeFrom<T>(iter: Iterable<T>, max: number): T[] {\n const out: T[] = [];\n for (const item of iter) {\n if (out.length >= max) break;\n out.push(item);\n }\n return out;\n}\n\nfunction parseDuckDuckGo(html: string, num: number): SearchResult[] {\n const results: SearchResult[] = [];\n const linkRegex = /<a\\b([^>]*\\bclass=([\"'])[^\"']*\\bresult-link\\b[^\"']*\\2[^>]*)>([\\s\\S]*?)<\\/a>/gi;\n const snippetRegex =\n /<([a-z0-9]+)\\b([^>]*\\bclass=([\"'])[^\"']*\\bresult-snippet\\b[^\"']*\\3[^>]*)>([\\s\\S]*?)<\\/\\1>/gi;\n\n const linkMatches = takeFrom(\n [...html.matchAll(linkRegex)]\n .map((m) => {\n const attrs = expectDefined(m[1]);\n const href = getHtmlAttr(attrs, 'href');\n const title = stripTags(expectDefined(m[3]));\n return href && title ? { url: normalizeDuckDuckGoUrl(href), title } : undefined;\n })\n .filter((m): m is { url: string; title: string } => m !== undefined),\n num,\n );\n\n const snippetMatches = takeFrom(\n [...html.matchAll(snippetRegex)]\n .filter((m) => m[4])\n .map((m) => stripTags(expectDefined(m[4]))),\n num,\n );\n\n for (let i = 0; i < linkMatches.length && i < num; i++) {\n const entry = linkMatches[i];\n if (entry) {\n results.push({\n title: entry.title ?? '',\n url: entry.url ?? '',\n snippet: snippetMatches[i] ?? '',\n score: 1,\n });\n }\n }\n\n return results;\n}\n\nfunction getHtmlAttr(attrs: string, name: string): string | undefined {\n const quoted = new RegExp(`\\\\b${name}\\\\s*=\\\\s*([\"'])(.*?)\\\\1`, 'i').exec(attrs);\n if (quoted?.[2]) return decodeHtmlEntities(quoted[2]);\n const unquoted = new RegExp(`\\\\b${name}\\\\s*=\\\\s*([^\\\\s>]+)`, 'i').exec(attrs);\n return unquoted?.[1] ? decodeHtmlEntities(unquoted[1]) : undefined;\n}\n\nfunction normalizeDuckDuckGoUrl(raw: string): string {\n if (raw.startsWith('//')) return `https:${raw}`;\n if (!raw.startsWith('/')) return raw;\n if (!raw.startsWith('/l/')) return raw;\n try {\n const url = new URL(raw, 'https://duckduckgo.com');\n const uddg = url.searchParams.get('uddg');\n return uddg?.startsWith('http') ? uddg : url.toString();\n } catch {\n return raw;\n }\n}\n\nasync function googleSearch(query: string, num: number, signal: AbortSignal): Promise<SearchResult[]> {\n const encoded = encodeURIComponent(query);\n const url = `https://www.google.com/search?q=${encoded}&hl=en`;\n\n const html = await fetchWithTimeout(url, signal, TIMEOUT_MS)\n .then((r) => r.text())\n .catch(() => '');\n\n return parseGoogleResults(html, num);\n}\n\nfunction parseGoogleResults(html: string, num: number): SearchResult[] {\n const results: SearchResult[] = [];\n const titleRegex = /<h3[^>]*class=\"[^\"]*DKV84\"[^>]*>([^<]+)<\\/h3>/gi;\n const urlRegex = /<cite[^>]*>([^<]+)<\\/cite>/gi;\n const snippetRegex = /<span[^>]*class=\"[^\"]*aXCZ0b[^>]*>([^<]+)<\\/span>/gi;\n\n const titles = takeFrom(\n [...html.matchAll(titleRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n const urls = takeFrom(\n [...html.matchAll(urlRegex)]\n .filter((m) => m[1])\n .map((m) => stripTags(expectDefined(m[1])).replace(/^\\*(https?:\\/\\/[^\\s]+).*$/, '$1'))\n .filter((u) => u.startsWith('http')),\n num,\n );\n\n const snippets = takeFrom(\n [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n for (let i = 0; i < Math.min(titles.length, num); i++) {\n results.push({\n title: titles[i] ?? '',\n url: urls[i] ?? '',\n snippet: snippets[i] ?? '',\n score: 1,\n });\n }\n\n return results;\n}\n\nasync function bingSearch(query: string, num: number, signal: AbortSignal): Promise<SearchResult[]> {\n const encoded = encodeURIComponent(query);\n const url = `https://www.bing.com/search?q=${encoded}`;\n\n const html = await fetchWithTimeout(url, signal, TIMEOUT_MS)\n .then((r) => r.text())\n .catch(() => '');\n\n return parseBingResults(html, num);\n}\n\nfunction parseBingResults(html: string, num: number): SearchResult[] {\n const results: SearchResult[] = [];\n const blocks = [...html.matchAll(/<li\\b[^>]*class=([\"'])[^\"']*\\bb_algo\\b[^\"']*\\1[^>]*>([\\s\\S]*?)(?=<li\\b[^>]*class=([\"'])[^\"']*\\bb_algo\\b[^\"']*\\3|<\\/ol>)/gi)]\n .map((m) => expectDefined(m[2]));\n const candidates = blocks.length > 0 ? blocks : [html];\n\n const entries = takeFrom(candidates.flatMap((block) => {\n const titleMatch = /<h2[^>]*>\\s*<a\\b([^>]*)>([\\s\\S]*?)<\\/a>\\s*<\\/h2>/i.exec(block);\n if (!titleMatch) return [];\n const href = getHtmlAttr(expectDefined(titleMatch[1]), 'href');\n const title = stripTags(expectDefined(titleMatch[2]));\n if (!href || !title) return [];\n const snippetMatch = /<p\\b[^>]*class=([\"'])[^\"']*\\b(?:b_paractl|b_lineclamp\\d*)\\b[^\"']*\\1[^>]*>([\\s\\S]*?)<\\/p>/i.exec(block)\n ?? /<p\\b[^>]*>([\\s\\S]*?)<\\/p>/i.exec(block);\n const snippet = snippetMatch ? stripTags(expectDefined(snippetMatch.at(-1))) : '';\n return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];\n }), num);\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n if (entry) {\n results.push({\n title: entry.title ?? '',\n url: entry.url ?? '',\n snippet: entry.snippet ?? '',\n score: 1,\n });\n }\n }\n\n return results;\n}\n\nfunction normalizeBingUrl(raw: string): string {\n try {\n const url = new URL(raw);\n if (url.hostname.endsWith('bing.com') && url.pathname.startsWith('/ck/')) {\n const encoded = url.searchParams.get('u');\n const decoded = decodeBingTarget(encoded);\n if (decoded?.startsWith('http')) return decoded;\n }\n } catch {\n // Fall through to the raw URL.\n }\n return raw;\n}\n\nfunction decodeBingTarget(encoded: string | null): string | undefined {\n if (!encoded) return undefined;\n const payload = encoded.startsWith('a1') ? encoded.slice(2) : encoded;\n try {\n const padded = payload.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(payload.length / 4) * 4, '=');\n return Buffer.from(padded, 'base64').toString('utf8');\n } catch {\n return undefined;\n }\n}\n\n// ---------------------------------------------------------------------------\n// HTTP helper\n// ---------------------------------------------------------------------------\n\nasync function fetchWithTimeout(\n url: string,\n signal: AbortSignal,\n timeoutMs: number,\n): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n const fetchSignal = anySignal(signal, controller.signal);\n try {\n // F-05: route through the SSRF-guarded fetch (private-IP blocking, HTTPS,\n // DNS-pinned dispatcher, per-hop redirect re-validation) instead of a bare\n // `fetch` with `redirect: 'follow'`. Search hosts are fixed/trusted, but\n // this closes the residual \"engine 30x \u2192 internal address\" redirect risk.\n const res = await guardedFetch(url, 5, fetchSignal, {\n 'user-agent':\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n });\n clearTimeout(timer);\n return res;\n } catch (e) {\n clearTimeout(timer);\n if (e instanceof FetchError) {\n throw e;\n }\n throw new FetchError({\n message: `search: failed to fetch ${url}`,\n status: 0,\n context: { url },\n cause: e,\n });\n }\n}\n\nfunction anySignal(...signals: AbortSignal[]): AbortSignal {\n // Native combinator (Node \u2265 20.3; this repo requires \u2265 22). The previous\n // hand-rolled version registered a non-once 'abort' listener on every\n // input signal and never removed it \u2014 the run-level signal outlives each\n // request, so listeners (and their closures) accumulated one per search\n // call for the life of the agent run.\n return AbortSignal.any(signals);\n}\n\nfunction stripTags(html: string): string {\n return decodeHtmlEntities(html.replace(/<[^>]+>/g, '')).trim();\n}\n\nfunction decodeHtmlEntities(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\");\n}\n", "import * as dns from 'node:dns/promises';\nimport * as net from 'node:net';\nimport {\n FetchError,\n ToolValidationError,\n isPrivateIPv4,\n isPrivateIPv6,\n} from '@wrongstack/core';\nimport { Agent, fetch as undiciFetch } from 'undici';\n\n/**\n * SSRF guard machinery shared by the `fetch` and `search` tools. Split out of\n * fetch.ts so entries that only need `guardedFetch` (search) don't bundle the\n * whole fetch tool. Everything here is security-critical: the pinned undici\n * dispatcher performs the SINGLE DNS resolution the TCP connection uses, so\n * there is no DNS-rebinding TOCTOU between validation and connect.\n */\n\nconst nativeGlobalFetch = globalThis.fetch;\n\nexport const ALLOW_PRIVATE = process.env['WRONGSTACK_FETCH_ALLOW_PRIVATE'] === '1';\n/* v8 ignore next 8 -- module-load-time opt-in warning; gated on an env var not set during tests. */\nif (ALLOW_PRIVATE && !process.env['CI']) {\n console.warn(\n '[WrongStack] WARNING: WRONGSTACK_FETCH_ALLOW_PRIVATE=1 is active \u2014\\n' +\n ' fetch tool can now access private IPs (10.x, 192.168.x, 169.254.x),\\n' +\n ' cloud metadata endpoints, and plaintext HTTP. Use only on isolated networks.',\n );\n}\n\ntype LookupCallback = (\n err: NodeJS.ErrnoException | null,\n address?: string | Array<{ address: string | undefined; family: number }>,\n family?: number | undefined,\n) => void;\n\n/**\n * DNS lookup used by the undici dispatcher below. It performs the SINGLE name\n * resolution that the TCP connection actually uses, and rejects if any\n * resolved address is private/loopback/link-local. Because the connection\n * reuses exactly this result, there is no DNS-rebinding TOCTOU window between\n * the security check and the connect \u2014 closing the gap the old code documented\n * (validate with one dns.lookup, then let fetch re-resolve independently).\n * TLS still validates the certificate against the hostname (SNI is set by\n * undici from the URL), so pinning the IP does not weaken cert checking.\n */\nexport function guardedLookup(\n hostname: string,\n options: { all?: boolean | undefined; family?: number | undefined },\n callback: LookupCallback,\n): void {\n dns\n .lookup(hostname, { all: true })\n .then((records) => {\n const family = options?.family;\n const byFamily =\n family === 4 || family === 6 ? records.filter((r) => r.family === family) : records;\n const list = byFamily.length > 0 ? byFamily : records;\n if (!ALLOW_PRIVATE) {\n for (const r of list) {\n const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);\n if (bad) {\n callback(\n Object.assign(new Error(`fetch: resolved to private address ${r.address}`), {\n code: 'EAI_FAIL',\n }),\n );\n return;\n }\n }\n }\n if (options?.all) {\n callback(\n null,\n list.map((r) => ({ address: r.address, family: r.family })),\n );\n return;\n }\n const first = list.at(0);\n if (!first) {\n callback(\n Object.assign(new Error(`fetch: no address for ${hostname}`), { code: 'ENOTFOUND' }),\n );\n return;\n }\n callback(null, first.address, first.family);\n })\n .catch((err) => callback(err as NodeJS.ErrnoException));\n}\n\n// Reused across requests; guardedLookup re-validates on every new connection,\n// so connection pooling is safe. Literal-IP targets bypass lookup entirely and\n// are caught by assertNotPrivate's pre-check instead.\n// Destroyed on process exit so long-running processes (eternal autonomy,\n// MCP server mode) don't let the connection pool grow unboundedly.\nlet pinnedAgent: Agent | undefined;\nfunction getPinnedDispatcher(): Agent {\n if (!pinnedAgent) {\n // Undici 8 enables HTTP/2 negotiation by default. Its H2 stream can emit a\n // late, unhandled `error` after fetch already rejected when the peer closes\n // the TLS socket, terminating the whole WrongStack process. The fetch tool\n // does not need multiplexing, so retain the proven HTTP/1.1 transport while\n // preserving the pinned DNS lookup and connection pooling.\n pinnedAgent = new Agent({ allowH2: false, connect: { lookup: guardedLookup as never } });\n }\n return pinnedAgent;\n}\n\nfunction dispatcherFetch(): typeof globalThis.fetch {\n // Node's built-in global fetch is backed by its own bundled undici version.\n // Passing an Agent from the workspace's `undici` package to that different\n // dispatcher ABI fails on recent Node with:\n // UND_ERR_INVALID_ARG: invalid onRequestStart method\n // Use the matching package fetch+Agent pair in real runs, but keep honoring\n // test/user fetch shims that replace globalThis.fetch after this module loads.\n return globalThis.fetch === nativeGlobalFetch\n ? (undiciFetch as unknown as typeof globalThis.fetch)\n : globalThis.fetch;\n}\n// Clean up the global dispatcher on exit \u2014 undici Agents maintain connection\n// pools and DNS caches that should be torn down in long-running processes.\n// Guard against duplicate registration (module reload/HMR would otherwise\n// accumulate listeners).\nlet _beforeExitRegistered = false;\nif (!_beforeExitRegistered) {\n _beforeExitRegistered = true;\n /* v8 ignore next 4 -- process 'beforeExit' cleanup; not deterministically triggerable in-test. */\n process.on('beforeExit', () => {\n pinnedAgent?.destroy();\n pinnedAgent = undefined;\n });\n}\n\n/**\n * SSRF-guarded fetch with manual, per-hop-revalidated redirects, exported so\n * other builtin tools (e.g. `search`) get the same protections instead of a\n * weaker `redirect: 'follow'`. Every hop is re-checked against private/loopback\n * ranges and the connection is pinned to the validated IP via the undici\n * dispatcher (no DNS-rebinding TOCTOU). `headers` defaults to the plain `fetch`\n * tool's; callers may override (e.g. a browser User-Agent for search engines).\n */\nexport async function guardedFetch(\n url: string,\n maxRedirects: number,\n signal: AbortSignal,\n headers: Record<string, string> = {\n 'user-agent': 'WrongStack/1.0 (+https://wrongstack.com)',\n accept: 'text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.1',\n },\n): Promise<Response> {\n let redirectCount = 0;\n let currentUrl = url;\n for (;;) {\n // Re-validate every hop. A public host can 302 to 169.254.169.254 (cloud metadata),\n // or DNS can rebind between hops; checking only the initial URL is insufficient.\n const parsed = new URL(currentUrl);\n if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {\n throw new ToolValidationError({\n message: `fetch: redirect to unsupported protocol \"${parsed.protocol}\"`,\n field: 'url',\n });\n }\n if (parsed.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new ToolValidationError({\n message: 'fetch: redirect to http:// blocked (HTTPS required by default)',\n field: 'url',\n });\n }\n await assertNotPrivate(parsed.hostname);\n\n // The dispatcher pins the connection to the IP guardedLookup validated \u2014\n // no independent re-resolution, so DNS rebinding can't swap in a private\n // address between check and connect. `dispatcher` is a runtime option of\n // Node's undici-backed global fetch but isn't in lib.dom's RequestInit, and\n // our undici Agent's type differs from the @types/node copy \u2014 hence the\n // cast. (Verified: global fetch invokes the Agent's custom lookup.)\n const init = {\n redirect: 'manual' as const,\n signal,\n headers,\n dispatcher: getPinnedDispatcher(),\n };\n const res = await dispatcherFetch()(currentUrl, init as never as RequestInit);\n if (res.status < 300 || res.status > 399) {\n return res;\n }\n redirectCount++;\n if (redirectCount > maxRedirects) {\n throw new FetchError({\n message: `fetch: exceeded ${maxRedirects} redirects`,\n status: res.status,\n context: { url: currentUrl, maxRedirects, redirectCount },\n });\n }\n const location = res.headers.get('location');\n if (!location) {\n throw new FetchError({\n message: 'fetch: redirect status with no location header',\n status: res.status,\n context: { url: currentUrl, redirectCount },\n });\n }\n currentUrl = new URL(location, currentUrl).toString();\n }\n}\n\nexport async function assertNotPrivate(hostname: string): Promise<void> {\n if (ALLOW_PRIVATE) return;\n\n const host =\n hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname;\n\n if (host === 'localhost' || host.endsWith('.localhost')) {\n throw new ToolValidationError({\n message: 'fetch: blocked localhost target',\n field: 'url',\n });\n }\n\n const ipVersion = net.isIP(host);\n if (ipVersion === 4) {\n if (isPrivateIPv4(host)) {\n throw new ToolValidationError({\n message: `fetch: blocked private/loopback address \"${host}\"`,\n field: 'url',\n });\n }\n } else if (ipVersion === 6) {\n if (isPrivateIPv6(host)) {\n throw new ToolValidationError({\n message: `fetch: blocked private/loopback address \"${host}\"`,\n field: 'url',\n });\n }\n } else {\n // Hostname \u2014 pre-flight check: resolve and reject if any record is private,\n // so we fail fast with a clear error before opening a socket. The\n // authoritative anti-rebinding control is guardedLookup on the pinned\n // undici dispatcher (see getPinnedDispatcher): it performs the single\n // resolution the connection actually uses, so there is no TOCTOU between\n // this check and the connect. Each redirect target is re-checked too.\n try {\n // Use dns.lookup for async hostname resolution (matches guardedLookup above).\n const records = await dns.lookup(host, { all: true });\n for (const r of records) {\n const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);\n if (bad) {\n throw new ToolValidationError({\n message: `fetch: resolved to private address ${r.address}`,\n field: 'url',\n });\n }\n }\n } catch (err) {\n if (err instanceof ToolValidationError) throw err;\n // DNS failure \u2014 let fetch handle it\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["FetchError", "ToolValidationError", "ToolValidationError", "FetchError"]
|
|
7
7
|
}
|
package/dist/session-kanban.d.ts
CHANGED
|
@@ -31,6 +31,15 @@ export interface SessionKanbanSourceUpdate {
|
|
|
31
31
|
tasks?: TaskFile | undefined;
|
|
32
32
|
plan?: PlanFile | undefined;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Replace the tactical todo list from the current cards on a session-owned board.
|
|
36
|
+
*
|
|
37
|
+
* Existing mirrored todo cards retain their stable source ids. New cards created
|
|
38
|
+
* by a Kanban worker become todos under their card ids, so reassessment can add,
|
|
39
|
+
* split, merge, reprioritize, or remove work without being overwritten by the
|
|
40
|
+
* todo list that happened to exist when the run started.
|
|
41
|
+
*/
|
|
42
|
+
export declare function applySessionKanbanBoardToTodos(context: Context, board: KanbanBoard): TodoItem[];
|
|
34
43
|
/** Reflect a TUI/WebUI Kanban card edit back to its originating work list. */
|
|
35
44
|
export declare function applySessionKanbanTaskToSource(context: Context, task: KanbanTask, options?: {
|
|
36
45
|
remove?: boolean | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-kanban.d.ts","sourceRoot":"","sources":["../src/session-kanban.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,OAAO,
|
|
1
|
+
{"version":3,"file":"session-kanban.d.ts","sourceRoot":"","sources":["../src/session-kanban.ts"],"names":[],"mappings":"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"}
|
package/dist/session-kanban.js
CHANGED
|
@@ -3,17 +3,21 @@ import { watch } from "node:fs";
|
|
|
3
3
|
import { basename, dirname } from "node:path";
|
|
4
4
|
import {
|
|
5
5
|
deserializeTaskGraph,
|
|
6
|
+
GlobalMailbox,
|
|
6
7
|
loadPlan,
|
|
7
8
|
loadTasks,
|
|
8
9
|
mutatePlan,
|
|
9
10
|
mutateTasks
|
|
10
11
|
} from "@wrongstack/core";
|
|
12
|
+
import { resolveWstackPaths } from "@wrongstack/core/utils";
|
|
11
13
|
import {
|
|
12
14
|
createBoard,
|
|
13
15
|
getBoard,
|
|
16
|
+
getKanbanDir,
|
|
14
17
|
listBoards,
|
|
15
18
|
removeBoard,
|
|
16
19
|
syncBoardFromTaskGraph,
|
|
20
|
+
touchKanbanPresence,
|
|
17
21
|
updateBoard
|
|
18
22
|
} from "@wrongstack/kanban";
|
|
19
23
|
var SESSION_BOARD_TAG = "session-work";
|
|
@@ -280,6 +284,42 @@ function fireAndForget(work) {
|
|
|
280
284
|
void work.catch(() => {
|
|
281
285
|
});
|
|
282
286
|
}
|
|
287
|
+
function broadcastTodoUpdate(context, todos) {
|
|
288
|
+
const sessionId = context.session?.id ?? "";
|
|
289
|
+
if (!context.agentId || !sessionId) return;
|
|
290
|
+
const projectDir = resolveWstackPaths({ projectRoot: context.projectRoot }).projectDir;
|
|
291
|
+
const mailbox = new GlobalMailbox(projectDir);
|
|
292
|
+
void mailbox.send({
|
|
293
|
+
from: context.agentId,
|
|
294
|
+
to: "*",
|
|
295
|
+
type: "status",
|
|
296
|
+
subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
|
|
297
|
+
body: JSON.stringify({
|
|
298
|
+
kind: "kanban.todos.updated",
|
|
299
|
+
sessionId,
|
|
300
|
+
revision: context.state.revision,
|
|
301
|
+
todos
|
|
302
|
+
}),
|
|
303
|
+
priority: "normal",
|
|
304
|
+
senderSessionId: sessionId,
|
|
305
|
+
ttlMs: 6 * 60 * 60 * 1e3
|
|
306
|
+
}).catch(() => {
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function notifyTodoUpdate(context, todos) {
|
|
310
|
+
const summary = todos.length ? todos.map((todo) => `- [${todo.status}] ${todo.content} (${todo.id})`).join("\n") : "- No active todos remain.";
|
|
311
|
+
const text = `[KANBAN TODO UPDATE]
|
|
312
|
+
Another Kanban agent reassessed the shared board. The canonical todo list is now:
|
|
313
|
+
${summary}
|
|
314
|
+
Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
|
|
315
|
+
const state = context.state;
|
|
316
|
+
if (typeof state.appendBlockToLastUserMessage === "function") {
|
|
317
|
+
if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
|
|
318
|
+
}
|
|
319
|
+
if (typeof state.appendMessage === "function") {
|
|
320
|
+
state.appendMessage({ role: "user", content: [{ type: "text", text }] });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
283
323
|
function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
|
|
284
324
|
fireAndForget(projectSessionTodosToKanban(projectRoot, todos, sessionId));
|
|
285
325
|
}
|
|
@@ -311,6 +351,10 @@ function attachSessionKanbanMirror(context) {
|
|
|
311
351
|
let watcher = null;
|
|
312
352
|
let watchedDir = "";
|
|
313
353
|
let timer = null;
|
|
354
|
+
let boardWatcher = null;
|
|
355
|
+
let watchedBoardId = "";
|
|
356
|
+
let boardTimer = null;
|
|
357
|
+
let presenceTimer = null;
|
|
314
358
|
const sessionId = () => context.session?.id ?? "";
|
|
315
359
|
const refreshFiles = async () => {
|
|
316
360
|
const id = sessionId();
|
|
@@ -326,6 +370,48 @@ function attachSessionKanbanMirror(context) {
|
|
|
326
370
|
if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
|
|
327
371
|
}
|
|
328
372
|
};
|
|
373
|
+
const refreshBoard = async () => {
|
|
374
|
+
if (!watchedBoardId) return;
|
|
375
|
+
const board = await getBoard(context.projectRoot, watchedBoardId);
|
|
376
|
+
if (board) applySessionKanbanBoardToTodos(context, board);
|
|
377
|
+
};
|
|
378
|
+
const configureBoardWatcher = async () => {
|
|
379
|
+
const id = sessionId();
|
|
380
|
+
const board = id ? await ensureSessionKanbanBoard(context.projectRoot, id) : null;
|
|
381
|
+
if (!board || board.id === watchedBoardId) return;
|
|
382
|
+
boardWatcher?.close();
|
|
383
|
+
boardWatcher = null;
|
|
384
|
+
watchedBoardId = board.id;
|
|
385
|
+
try {
|
|
386
|
+
const boardFileName = `${board.id}.json`;
|
|
387
|
+
boardWatcher = watch(
|
|
388
|
+
getKanbanDir(context.projectRoot),
|
|
389
|
+
{ persistent: false },
|
|
390
|
+
(_event, filename) => {
|
|
391
|
+
if (filename?.toString() !== boardFileName) return;
|
|
392
|
+
if (boardTimer) clearTimeout(boardTimer);
|
|
393
|
+
boardTimer = setTimeout(() => fireAndForget(refreshBoard()), 60);
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
const touchPresence = () => touchKanbanPresence(context.projectRoot, board.id, {
|
|
397
|
+
sessionId: id,
|
|
398
|
+
agentId: context.agentId,
|
|
399
|
+
agentName: context.agentName
|
|
400
|
+
});
|
|
401
|
+
fireAndForget(touchPresence());
|
|
402
|
+
if (presenceTimer) clearInterval(presenceTimer);
|
|
403
|
+
presenceTimer = setInterval(() => fireAndForget(touchPresence()), 6e4);
|
|
404
|
+
presenceTimer.unref?.();
|
|
405
|
+
boardWatcher.on("error", () => {
|
|
406
|
+
boardWatcher?.close();
|
|
407
|
+
boardWatcher = null;
|
|
408
|
+
watchedBoardId = "";
|
|
409
|
+
});
|
|
410
|
+
} catch {
|
|
411
|
+
boardWatcher = null;
|
|
412
|
+
watchedBoardId = "";
|
|
413
|
+
}
|
|
414
|
+
};
|
|
329
415
|
const configureWatcher = () => {
|
|
330
416
|
const planPath = context.meta["plan.path"];
|
|
331
417
|
const taskPath = context.meta["task.path"];
|
|
@@ -364,14 +450,19 @@ function attachSessionKanbanMirror(context) {
|
|
|
364
450
|
syncActiveSessionRegistration();
|
|
365
451
|
configureWatcher();
|
|
366
452
|
fireAndForget(ensureSessionKanbanBoard(context.projectRoot, sessionId()));
|
|
453
|
+
fireAndForget(configureBoardWatcher());
|
|
367
454
|
fireAndForget(refreshFiles());
|
|
368
455
|
}
|
|
369
456
|
});
|
|
370
457
|
configureWatcher();
|
|
458
|
+
fireAndForget(configureBoardWatcher());
|
|
371
459
|
const detach = () => {
|
|
372
460
|
unsubscribe();
|
|
373
461
|
if (timer) clearTimeout(timer);
|
|
462
|
+
if (boardTimer) clearTimeout(boardTimer);
|
|
463
|
+
if (presenceTimer) clearInterval(presenceTimer);
|
|
374
464
|
watcher?.close();
|
|
465
|
+
boardWatcher?.close();
|
|
375
466
|
bindings.delete(context);
|
|
376
467
|
if (attachedProjectRoot && registeredSessionId) {
|
|
377
468
|
releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
|
|
@@ -410,6 +501,51 @@ function sourceStatus(task) {
|
|
|
410
501
|
if (task.status === "failed") return "failed";
|
|
411
502
|
return "pending";
|
|
412
503
|
}
|
|
504
|
+
function todoStatus(task) {
|
|
505
|
+
const status = sourceStatus(task);
|
|
506
|
+
if (status === "completed") return "completed";
|
|
507
|
+
if (status === "in_progress" || status === "review") return "in_progress";
|
|
508
|
+
return "pending";
|
|
509
|
+
}
|
|
510
|
+
function sessionTodoFromTask(task) {
|
|
511
|
+
return {
|
|
512
|
+
id: task.origin?.taskId ?? task.id,
|
|
513
|
+
content: task.title,
|
|
514
|
+
status: todoStatus(task),
|
|
515
|
+
...task.description ? { activeForm: task.description } : {}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function sameTodos(left, right) {
|
|
519
|
+
return left.length === right.length && left.every((todo, index) => {
|
|
520
|
+
const candidate = right[index];
|
|
521
|
+
return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask;
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
function applySessionKanbanBoardToTodos(context, board) {
|
|
525
|
+
const sessionId = context.session?.id ?? "";
|
|
526
|
+
if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {
|
|
527
|
+
return [...context.todos];
|
|
528
|
+
}
|
|
529
|
+
const projectedTodos = board.tasks.filter(
|
|
530
|
+
(task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
|
|
531
|
+
).sort((left, right) => {
|
|
532
|
+
const leftColumn = board.columns.find((column) => column.id === left.columnId)?.order ?? 0;
|
|
533
|
+
const rightColumn = board.columns.find((column) => column.id === right.columnId)?.order ?? 0;
|
|
534
|
+
return leftColumn - rightColumn || left.order - right.order || left.createdAt.localeCompare(right.createdAt);
|
|
535
|
+
}).map(sessionTodoFromTask);
|
|
536
|
+
const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
|
|
537
|
+
const effectiveTodos = allCompleted ? [] : projectedTodos;
|
|
538
|
+
if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
|
|
539
|
+
suppressedTodoMirrors.add(context);
|
|
540
|
+
try {
|
|
541
|
+
context.state.replaceTodos(projectedTodos);
|
|
542
|
+
} finally {
|
|
543
|
+
suppressedTodoMirrors.delete(context);
|
|
544
|
+
}
|
|
545
|
+
notifyTodoUpdate(context, context.todos);
|
|
546
|
+
broadcastTodoUpdate(context, context.todos);
|
|
547
|
+
return [...context.todos];
|
|
548
|
+
}
|
|
413
549
|
async function applySessionKanbanTaskToSource(context, task, options = {}) {
|
|
414
550
|
const originId = task.origin?.taskId;
|
|
415
551
|
const graphId = task.origin?.graphId ?? "";
|
|
@@ -470,6 +606,7 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
|
|
|
470
606
|
}
|
|
471
607
|
export {
|
|
472
608
|
SESSION_KANBAN_COLUMNS,
|
|
609
|
+
applySessionKanbanBoardToTodos,
|
|
473
610
|
applySessionKanbanTaskToSource,
|
|
474
611
|
attachSessionKanbanMirror,
|
|
475
612
|
cleanupEmptySessionKanbanBoards,
|