@wrongstack/tools 0.269.0 → 0.272.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/audit.js +10 -5
- package/dist/audit.js.map +1 -1
- package/dist/{background-indexer-CJ5JiV5i.d.ts → background-indexer-BoTUw0EM.d.ts} +1 -1
- package/dist/bash.js +591 -11
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +857 -174
- package/dist/builtin.js.map +1 -1
- package/dist/codebase-index/index.d.ts +13 -3
- package/dist/codebase-index/index.js +129 -74
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.js +126 -71
- package/dist/codebase-index/worker.js.map +1 -1
- package/dist/exec.js +5 -2
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js +24 -1
- package/dist/fetch.js.map +1 -1
- package/dist/format.js +10 -5
- package/dist/format.js.map +1 -1
- package/dist/grep.js +55 -33
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +317 -3
- package/dist/index.js +1416 -202
- package/dist/index.js.map +1 -1
- package/dist/install.js +10 -5
- package/dist/install.js.map +1 -1
- package/dist/lint.js +10 -5
- package/dist/lint.js.map +1 -1
- package/dist/pack.js +857 -174
- package/dist/pack.js.map +1 -1
- package/dist/process-registry.js +5 -2
- package/dist/process-registry.js.map +1 -1
- package/dist/read.js +2 -2
- package/dist/read.js.map +1 -1
- package/dist/search.js.map +1 -1
- package/dist/test.js +10 -5
- package/dist/test.js.map +1 -1
- package/dist/typecheck.js +10 -5
- package/dist/typecheck.js.map +1 -1
- package/package.json +2 -2
package/dist/process-registry.js
CHANGED
|
@@ -228,10 +228,13 @@ function redactCommand(cmd) {
|
|
|
228
228
|
var DEFAULT_GRACE_MS = 2e3;
|
|
229
229
|
function killWin32Tree(pid) {
|
|
230
230
|
try {
|
|
231
|
-
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
231
|
+
const child = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
232
232
|
stdio: "ignore",
|
|
233
233
|
windowsHide: true
|
|
234
|
-
})
|
|
234
|
+
});
|
|
235
|
+
child.on("error", () => {
|
|
236
|
+
});
|
|
237
|
+
child.unref();
|
|
235
238
|
return true;
|
|
236
239
|
} catch {
|
|
237
240
|
return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/circuit-breaker.ts","../src/process-registry.ts"],"names":[],"mappings":"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","file":"process-registry.js","sourcesContent":["/**\n * CircuitBreaker — prevents runaway bash/exec tool chains by:\n *\n * - Tripping on consecutive failures (models that keep repeating the\n * same failing command, e.g. `npm install` with wrong args in a loop)\n * - Tripping on slow call ratio (too many long-running commands suggest\n * a hung subprocess that the model doesn't know how to kill)\n * - Rate-limiting bursts (rapid succession of commands without reading\n * output suggests the model isn't processing results)\n * - Auto-recovering after a cooldown period so a fixed model can resume\n *\n * The breaker is owned by the ProcessRegistry so any tool that registers\n * a process participates in the same circuit. \"Per-tool\" isolation is\n * intentionally NOT implemented — the model treats bash/exec as one\n * resource pool; isolating them would let the model route around the\n * breaker by alternating which tool it uses.\n */\n\nexport interface CircuitBreakerConfig {\n /**\n * Consecutive failures before trip. Default: 5.\n * A single success resets this counter to 0.\n */\n maxConsecutiveFailures?: number | undefined;\n /**\n * Slow-call threshold in ms. A call that runs longer than this is\n * counted as \"slow\". Default: 60_000 (1 minute).\n */\n slowCallThresholdMs?: number | undefined;\n /**\n * Max slow calls before trip (within the sliding window). Default: 3.\n */\n maxSlowCalls?: number | undefined;\n /**\n * Sliding window for rate-limit and slow-call counting, in ms.\n * Default: 60_000 (1 minute).\n */\n windowMs?: number | undefined;\n /**\n * Max calls within the sliding window. Default: 30.\n * Burst exceeding this trips the breaker immediately.\n */\n maxCallsPerWindow?: number | undefined;\n /**\n * Cooldown before auto-recovery attempt, in ms. Default: 30_000 (30s).\n * After this the breaker enters \"half-open\" state and allows one call\n * through to test whether the problem is resolved.\n */\n cooldownMs?: number | undefined;\n}\n\ninterface CallRecord {\n at: number;\n /** True if the call threw or returned an is_error result. */\n failed: boolean;\n /** True if elapsed time exceeded slowCallThresholdMs. */\n slow: boolean;\n}\n\ntype BreakerState = 'closed' | 'open' | 'half-open';\n\nconst DEFAULT_MAX_CONSECUTIVE_FAILURES = 5;\nconst DEFAULT_SLOW_CALL_THRESHOLD_MS = 180_000;\n// 3 minutes — balanced against the 5-minute bash timeout. Commands\n// running <3min are normal; 3-5min are \"slow\" and count toward the\n// breaker. 3 consecutive slow calls trip the circuit.\nconst DEFAULT_MAX_SLOW_CALLS = 3;\nconst DEFAULT_WINDOW_MS = 60_000;\nconst DEFAULT_MAX_CALLS_PER_WINDOW = 30;\nconst DEFAULT_COOLDOWN_MS = 30_000;\n\nexport interface CircuitBreakerSnapshot {\n state: 'closed' | 'open' | 'half-open';\n consecutiveFailures: number;\n slowCallsInWindow: number;\n callsInWindow: number;\n windowMs: number;\n cooldownRemainingMs: number | null;\n lastFailureAt: number | null;\n lastSlowAt: number | null;\n}\n\nexport class CircuitBreaker {\n private readonly maxConsecutiveFailures: number;\n private readonly slowCallThresholdMs: number;\n private readonly maxSlowCalls: number;\n private readonly windowMs: number;\n private readonly maxCallsPerWindow: number;\n private readonly cooldownMs: number;\n\n private state: BreakerState = 'closed';\n private consecutiveFailures = 0;\n private window: CallRecord[] = [];\n private lastFailureAt: number | null = null;\n private lastSlowAt: number | null = null;\n /** Timestamp when the breaker was opened (for cooldown calculation). */\n private openedAt: number | null = null;\n\n /**\n * Master enable flag. When false the breaker is bypassed: `beforeCall`\n * always returns true and `afterCall` records nothing. The class itself\n * defaults to enabled (so the standalone unit tests exercise tripping); the\n * ProcessRegistry flips this off until the user opts in via `/settings`.\n */\n private enabled = true;\n\n /**\n * Fired (best-effort) when the breaker transitions into the `open` state.\n * The registry uses this to arm its auto kill/reset countdown.\n */\n onTrip?: (() => void) | undefined;\n /**\n * Fired (best-effort) when the breaker returns to `closed` after having been\n * open/half-open. The registry uses this to cancel a pending kill/reset.\n */\n onReset?: (() => void) | undefined;\n\n constructor(config: CircuitBreakerConfig = {}) {\n this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;\n this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;\n this.maxSlowCalls = config.maxSlowCalls ?? DEFAULT_MAX_SLOW_CALLS;\n this.windowMs = config.windowMs ?? DEFAULT_WINDOW_MS;\n this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;\n this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;\n }\n\n /** Toggle the master enable. Disabling resets to a clean `closed` state. */\n setEnabled(enabled: boolean): void {\n if (this.enabled === enabled) return;\n this.enabled = enabled;\n if (!enabled) this._reset();\n }\n\n get isEnabled(): boolean {\n return this.enabled;\n }\n\n /**\n * Returns true if the circuit allows a new call to proceed.\n * When false, callers should abort the tool call and return a\n * circuit-breaker error instead of spawning a process.\n */\n get canProceed(): boolean {\n if (!this.enabled) return true;\n this._checkStateTransition();\n return this.state !== 'open';\n }\n\n /**\n * Snapshot of the current breaker state for observability (`/kill`).\n */\n snapshot(): CircuitBreakerSnapshot {\n this._checkStateTransition();\n const now = Date.now();\n let cooldownRemaining: number | null = null;\n if (this.openedAt !== null && this.state === 'open') {\n const elapsed = now - this.openedAt;\n cooldownRemaining = Math.max(0, this.cooldownMs - elapsed);\n }\n return {\n state: this.state,\n consecutiveFailures: this.consecutiveFailures,\n slowCallsInWindow: this.window.filter((c) => c.slow).length,\n callsInWindow: this.window.length,\n windowMs: this.windowMs,\n cooldownRemainingMs: cooldownRemaining,\n lastFailureAt: this.lastFailureAt,\n lastSlowAt: this.lastSlowAt,\n };\n }\n\n /**\n * Call this BEFORE spawning a bash/exec process.\n * Returns true if the call is allowed; false if the breaker is open.\n * When false, callers MUST NOT spawn a process.\n *\n * @param bypass - If true, skip the circuit breaker check entirely.\n * Use for background/fire-and-forget processes that should\n * not affect breaker state.\n */\n beforeCall(bypass = false): boolean {\n if (bypass || !this.enabled) return true;\n this._checkStateTransition();\n if (this.state === 'open') return false;\n return true;\n }\n\n /**\n * Call this AFTER a bash/exec process finishes (success or failure).\n * `durationMs` is the wall-clock time the process ran.\n * `failed` is true when the process returned a non-zero exit code or\n * threw an exception before spawning.\n *\n * @param bypass - If true, do not update breaker state.\n * Use for background/fire-and-forget processes.\n */\n afterCall(durationMs: number, failed: boolean, bypass = false): void {\n if (bypass || !this.enabled) return;\n\n const now = Date.now();\n\n if (this.state === 'half-open') {\n // First call through after cooldown — if it failed, go back to open.\n if (failed) {\n this._trip();\n return;\n }\n // Success in half-open → reset to closed.\n this._reset();\n return;\n }\n\n // Prune old records outside the sliding window.\n this._pruneWindow(now);\n\n const slow = durationMs >= this.slowCallThresholdMs;\n this.window.push({ at: now, failed, slow });\n\n if (failed) {\n this.consecutiveFailures++;\n this.lastFailureAt = now;\n if (this.consecutiveFailures >= this.maxConsecutiveFailures) {\n this._trip();\n }\n return;\n }\n\n // Success: reset consecutive failure counter.\n this.consecutiveFailures = 0;\n\n if (slow) {\n this.lastSlowAt = now;\n const slowCount = this.window.filter((c) => c.slow).length;\n if (slowCount >= this.maxSlowCalls) {\n this._trip();\n }\n }\n\n const callCount = this.window.length;\n if (callCount >= this.maxCallsPerWindow) {\n // Rate limit exceeded. This is a soft trip — we reset the window\n // and let the next call try immediately (the caller will still see\n // canProceed=false until the window drains naturally).\n this._trip();\n }\n }\n\n /** Force the breaker open. Used by /kill force and Ctrl+C. */\n forceOpen(): void {\n this._trip();\n }\n\n /** Force a reset to closed. Used by tests and /kill reset. */\n forceReset(): void {\n this._reset();\n }\n\n private _trip(): void {\n if (this.state === 'open') return; // already open\n this.state = 'open';\n this.openedAt = Date.now();\n // Best-effort: never let a listener failure corrupt breaker state.\n try {\n this.onTrip?.();\n } catch {\n /* ignored — observability hook only */\n }\n }\n\n private _reset(): void {\n const wasRecovering = this.state !== 'closed';\n this.state = 'closed';\n this.consecutiveFailures = 0;\n this.window = [];\n this.openedAt = null;\n // Only notify on a real recovery (open/half-open → closed), not on the\n // initial closed state or an idempotent re-reset.\n if (wasRecovering) {\n try {\n this.onReset?.();\n } catch {\n /* ignored — observability hook only */\n }\n }\n }\n\n /** Transition from open → half-open when cooldown elapses. */\n private _checkStateTransition(): void {\n if (this.state !== 'open' || this.openedAt === null) return;\n const elapsed = Date.now() - this.openedAt;\n if (elapsed >= this.cooldownMs) {\n this.state = 'half-open';\n this.openedAt = null;\n }\n }\n\n private _pruneWindow(now: number): void {\n const cutoff = now - this.windowMs;\n this.window = this.window.filter((c) => c.at >= cutoff);\n }\n}","import { expectDefined } from '@wrongstack/core';\n/**\n * ProcessRegistry — global singleton that tracks all spawned child processes\n * from `bash` and `exec` tools. Enables:\n *\n * - Listing active processes (for TUI status bar)\n * - Killing individual processes or all processes (for Ctrl+C and /kill)\n * - Detecting runaway processes (hung, looping)\n * - Circuit breaker integration to prevent recursive/repeated failures\n *\n * Thread-safety: Node.js is single-threaded, but async callbacks can fire\n * in any order. All mutations go through synchronized Map methods.\n */\nimport { spawn } from 'node:child_process';\nimport type { ChildProcess } from 'node:child_process';\nimport * as os from 'node:os';\nimport { CircuitBreaker, type CircuitBreakerSnapshot, type CircuitBreakerConfig } from './circuit-breaker.js';\nexport type { CircuitBreakerSnapshot, CircuitBreakerConfig } from './circuit-breaker.js';\n\nexport interface TrackedProcess {\n pid: number;\n name: string;\n /** Display-safe redacted command string — safe for logs, /ps, crash dumps.\n * Contains [REDACTED] in place of sensitive flag values. */\n command: string;\n startedAt: number;\n sessionId?: string | undefined;\n /** The raw ChildProcess handle. Never call .kill() directly on this —\n * use `kill()` below which handles process groups correctly on POSIX\n * and degrades gracefully on Windows. */\n child: ChildProcess;\n /** True once the process has been kill()ed but not yet exited.\n * We keep it in the registry until 'close' fires so callers can\n * distinguish \"still running\" from \"just exited\". */\n killed: boolean;\n /** If true, kill() and killAll() will refuse to kill this process.\n * Used for infrastructure processes (browser, dev servers, …) that\n * must outlive the agent session. */\n protected: boolean;\n}\n\n// Sensitive CLI flag patterns that may appear in process command lines.\n// Redacted to [REDACTED] so crash dumps /ps output cannot leak secrets.\nconst SENSITIVE_FLAG_PATTERNS: RegExp[] = [\n // --flag=value or --flag \"value\" (value captured up to next space or comma)\n /--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\\s,][^\\s]*)?/gi,\n // -f \"value\" style short flags\n /(?<!\\w)-t(?:\\s+|\\s*=\\s*)[^\\s,]+/,\n /(?<!\\w)-p(?:ssword)?(?:\\s+|\\s*=\\s*)[^\\s,]+/gi,\n // env var–style secrets: TOKEN=x, API_KEY=y, etc.\n /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\\s*[=:]\\s*[^\\s,]+/gi,\n // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only\n // when preceded by a flag name (e.g. --github-token=EyJ...).\n /--\\w*(?:token|key|secret|password|passwd|auth|credential)\\w*[=\\s,][A-Za-z0-9+/=]{32,}/,\n];\n\n/**\n * Returns a display-safe copy of `cmd` with sensitive flag values replaced by [REDACTED].\n * The original string is unchanged; this is pure and has no side effects.\n */\nexport function redactCommand(cmd: string): string {\n let result = cmd;\n for (const pattern of SENSITIVE_FLAG_PATTERNS) {\n result = result.replace(pattern, (match) => {\n // Preserve the flag name portion; redact only the value part.\n // e.g. \"--token=sekrit_abc\" → \"--token=[REDACTED]\"\n const eq = match.indexOf('=');\n const sp = match.search(/\\s/);\n const delim = eq !== -1 ? '=' : sp !== -1 ? match[sp] : null;\n if (delim !== null) {\n const flag = match.slice(0, match.indexOf(expectDefined(delim)) + 1);\n return `${flag}[REDACTED]`;\n }\n // Nothing delimitable found; replace the whole token silently.\n // Short flags like -tVALUE are replaced entirely to avoid edge cases.\n const flagEnd = match.match(/^--?[a-zA-Z][a-zA-Z0-9_-]*/)?.[0] ?? match;\n return `${flagEnd}=**redacted**`;\n });\n }\n return result;\n}\n\ninterface KillOpts {\n /** SIGKILL instead of SIGTERM. Default: false (SIGTERM first). */\n force?: boolean | undefined;\n /** MS to wait between SIGTERM and SIGKILL on POSIX. Default: 2000. */\n graceMs?: number | undefined;\n}\n\n/**\n * Snapshot of the armed auto kill/reset countdown, or null when nothing is\n * armed. `remainingMs` ticks down in real time; the TUI statusline renders it.\n */\nexport interface BreakerCountdown {\n remainingMs: number;\n totalMs: number;\n}\n\ntype BreakerCountdownListener = (snapshot: BreakerCountdown | null) => void;\n\nexport interface RegistryStats {\n activeCount: number;\n totalCount: number;\n breaker: CircuitBreakerSnapshot;\n}\n\nconst DEFAULT_GRACE_MS = 2000;\n\n/**\n * Kill an entire process tree on Windows via `taskkill /T /F`.\n *\n * TerminateProcess (what `child.kill()` maps to) has no process-group\n * semantics, so killing a shell wrapper (`cmd.exe /c …`) orphans its\n * grandchildren (node, vitest forks, dev servers). The orphans inherit the\n * parent's stdio pipe handles and can keep streaming into this process for\n * the rest of the session — which both prevents the child's 'close' event\n * from ever firing and grows in-memory output buffers without bound.\n *\n * Fire-and-forget: returns true if taskkill was spawned, false if spawning\n * it failed (caller should fall back to a direct `child.kill()`).\n */\nexport function killWin32Tree(pid: number): boolean {\n try {\n spawn('taskkill', ['/pid', String(pid), '/T', '/F'], {\n stdio: 'ignore',\n windowsHide: true,\n }).unref();\n return true;\n } catch {\n return false;\n }\n}\n\nexport class ProcessRegistryImpl {\n private readonly processes = new Map<number, TrackedProcess>();\n private readonly breaker: CircuitBreaker;\n\n /**\n * Auto kill/reset config. When the breaker trips and `autoKillResetMs > 0`,\n * a countdown is armed; on expiry all tracked processes are killed and the\n * breaker is reset to closed (forced recovery). Zero means manual recovery\n * only (`/kill reset`).\n */\n private autoKillResetMs = 0;\n private autoKillTimer: ReturnType<typeof setTimeout> | null = null;\n private autoKillArmedAt: number | null = null;\n private breakerCountdownListeners: BreakerCountdownListener[] = [];\n\n constructor(breakerConfig?: CircuitBreakerConfig) {\n this.breaker = new CircuitBreaker(breakerConfig);\n // Arm on trip, cancel on recovery. Listeners are best-effort.\n this.breaker.onTrip = () => this._armAutoKillReset();\n this.breaker.onReset = () => this._cancelAutoKillReset();\n // Protection is OFF by default — the user opts in via `/settings breaker on`.\n this.breaker.setEnabled(false);\n }\n\n register(info: Omit<TrackedProcess, 'killed' | 'protected'> & { protected?: boolean | undefined }): void {\n this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });\n }\n\n /** Unregister a process by PID. Called on 'close' / 'exit' events. */\n unregister(pid: number): void {\n this.processes.delete(pid);\n }\n\n /** Get a single process by PID. */\n get(pid: number): TrackedProcess | undefined {\n return this.processes.get(pid);\n }\n\n /** Get all tracked processes. */\n list(): TrackedProcess[] {\n return Array.from(this.processes.values());\n }\n\n /** Get processes filtered by name (e.g. 'bash', 'exec'). */\n byName(name: string): TrackedProcess[] {\n return this.list().filter((p) => p.name === name);\n }\n\n /** Get processes filtered by session. */\n bySession(sessionId: string): TrackedProcess[] {\n return this.list().filter((p) => p.sessionId === sessionId);\n }\n\n /** Count of active (non-killed) processes. */\n get activeCount(): number {\n let n = 0;\n for (const p of this.processes.values()) {\n if (!p.killed) n++;\n }\n return n;\n }\n\n /**\n * Combined stats for observability — used by /ps and the TUI status bar.\n */\n stats(): RegistryStats {\n return {\n activeCount: this.activeCount,\n totalCount: this.processes.size,\n breaker: this.breaker.snapshot(),\n };\n }\n\n /**\n * Returns true if the circuit allows a new bash/exec call to proceed.\n * When false, callers MUST NOT spawn a process.\n */\n get canProceed(): boolean {\n return this.breaker.canProceed;\n }\n\n /**\n * Called before spawning a process. Returns true if allowed; false if\n * the circuit breaker is open.\n *\n * @param bypass - If true, skip circuit breaker check (for background processes).\n */\n beforeCall(bypass = false): boolean {\n return this.breaker.beforeCall(bypass);\n }\n\n /**\n * Called after a process finishes. `durationMs` is wall-clock time;\n * `failed` is true for non-zero exit codes.\n *\n * @param bypass - If true, do not update circuit breaker state (for background processes).\n */\n afterCall(durationMs: number, failed: boolean, bypass = false): void {\n this.breaker.afterCall(durationMs, failed, bypass);\n }\n\n /** Force-open the circuit breaker (Ctrl+C, /kill force). */\n forceBreakerOpen(): void {\n this.breaker.forceOpen();\n }\n\n /** Force-reset the circuit breaker to closed (/kill reset). */\n forceBreakerReset(): void {\n this.breaker.forceReset();\n }\n\n /**\n * Configure circuit-breaker protection at runtime. Called from `/settings`\n * (instant, all modes) and on TUI mount (applies persisted config).\n *\n * - `enabled` toggles whether the breaker gates `bash`/`exec`.\n * - `autoKillResetMs` arms the auto kill/reset countdown when the breaker\n * trips (0 = manual recovery only).\n *\n * Re-applies cleanly on every call: cancels a pending countdown when the\n * timeout is cleared or protection disabled, and re-arms if the breaker is\n * currently open under the new settings.\n */\n setBreakerConfig(cfg: { enabled?: boolean | undefined; autoKillResetMs?: number | undefined }): void {\n if (cfg.enabled !== undefined) this.breaker.setEnabled(cfg.enabled);\n if (cfg.autoKillResetMs !== undefined) this.autoKillResetMs = Math.max(0, cfg.autoKillResetMs);\n\n if (this.autoKillResetMs <= 0) {\n this._cancelAutoKillReset();\n return;\n }\n // If protection is active and the breaker is currently tripped, ensure a\n // countdown is armed for the new window (covers a live config change while\n // the breaker is already open).\n if (this.breaker.isEnabled && this.breaker.snapshot().state === 'open') {\n this._armAutoKillReset();\n }\n }\n\n /**\n * Live countdown to the next auto kill/reset, or null when nothing is armed.\n * The TUI polls this on a 1s tick while armed so the statusline decrements.\n */\n getBreakerCountdown(): BreakerCountdown | null {\n if (this.autoKillArmedAt === null || this.autoKillResetMs <= 0) return null;\n const elapsed = Date.now() - this.autoKillArmedAt;\n return { remainingMs: Math.max(0, this.autoKillResetMs - elapsed), totalMs: this.autoKillResetMs };\n }\n\n /**\n * Subscribe to countdown arm/cancel events. Returns an unsubscribe function.\n * Use {@link getBreakerCountdown} for the live ticking value between events.\n */\n onBreakerCountdownChange(listener: BreakerCountdownListener): () => void {\n this.breakerCountdownListeners.push(listener);\n return () => {\n this.breakerCountdownListeners = this.breakerCountdownListeners.filter((l) => l !== listener);\n };\n }\n\n private _emitBreakerCountdown(): void {\n const snap = this.getBreakerCountdown();\n for (const l of this.breakerCountdownListeners) {\n try {\n l(snap);\n } catch {\n /* listener failure must never affect breaker behavior */\n }\n }\n }\n\n /**\n * Arm the auto kill/reset countdown. Idempotent: re-arming resets the window\n * (a fresh trip after a failed half-open probe restarts the clock). No-op\n * when protection is off or no timeout is configured.\n */\n private _armAutoKillReset(): void {\n if (this.autoKillResetMs <= 0 || !this.breaker.isEnabled) return;\n this._clearAutoKillTimer();\n this.autoKillArmedAt = Date.now();\n this.autoKillTimer = setTimeout(() => {\n this.autoKillTimer = null;\n this.autoKillArmedAt = null;\n // Forced recovery: nuke runaway processes and reopen the circuit.\n this.killAll({ force: false });\n this.breaker.forceReset();\n this._emitBreakerCountdown();\n }, this.autoKillResetMs);\n // Don't keep the event loop alive purely for auto-recovery.\n this.autoKillTimer.unref?.();\n this._emitBreakerCountdown();\n }\n\n private _cancelAutoKillReset(): void {\n const wasArmed = this.autoKillArmedAt !== null;\n this._clearAutoKillTimer();\n if (wasArmed) {\n this.autoKillArmedAt = null;\n this._emitBreakerCountdown();\n }\n }\n\n private _clearAutoKillTimer(): void {\n if (this.autoKillTimer !== null) {\n clearTimeout(this.autoKillTimer);\n this.autoKillTimer = null;\n }\n }\n\n /** Kill a single process by PID.\n *\n * On POSIX: sends SIGTERM to the *process group* (-pid) so that\n * runaway grandchild processes (`sleep 9999 & disown`) are also killed.\n * After `graceMs` a SIGKILL is sent if the process hasn't exited.\n *\n * On Windows: `child.kill()` maps to TerminateProcess — process groups\n * are not meaningfully supported. A second `force=true` call sends\n * SIGKILL (which maps to TerminateProcess again — the distinction is\n * in the exit code, not the signal).\n *\n * Returns true if the process was found and kill was attempted.\n */\n kill(pid: number, opts: KillOpts = {}): boolean {\n const p = this.processes.get(pid);\n if (!p) return false;\n if (p.killed) return true; // already kill()ed, don't double-send\n if (p.protected) return false; // protected processes are never kill()ed\n\n const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;\n const isWin = os.platform() === 'win32';\n\n if (isWin) {\n // Windows: no process group semantics. A direct kill terminates only\n // the immediate child — shell-wrapped commands (cmd.exe /c …) leave\n // grandchildren running that hold the inherited stdio pipes open and\n // keep feeding output into this process indefinitely. Kill the whole\n // tree via taskkill instead, but only for a real, still-running child\n // (exitCode === null); test fakes and already-exited processes take\n // the plain-kill path. The direct kill is deliberately NOT sent\n // immediately alongside taskkill: killing the root first would break\n // taskkill's parent-pid tree enumeration and orphan the grandchildren\n // again — it runs as a delayed fallback instead.\n const liveRealChild = p.child.exitCode === null && typeof p.child.pid === 'number';\n if (liveRealChild && killWin32Tree(pid)) {\n const fallback = setTimeout(() => {\n if (p.child.exitCode === null) {\n try {\n p.child.kill('SIGKILL');\n } catch {\n // Process may have already exited.\n }\n }\n }, graceMs);\n fallback.unref?.();\n } else {\n try {\n p.child.kill(force ? 'SIGKILL' : 'SIGTERM');\n } catch {\n // Process may have already exited.\n }\n }\n p.killed = true;\n return true;\n }\n\n // POSIX: kill the process group so grandchildren are cleaned up too.\n try {\n if (force) {\n try {\n process.kill(-pid, 'SIGKILL');\n } catch {\n p.child.kill('SIGKILL');\n }\n } else {\n try {\n process.kill(-pid, 'SIGTERM');\n } catch {\n p.child.kill('SIGTERM');\n }\n // Schedule SIGKILL as backup.\n const timer = setTimeout(() => {\n // Re-check: process may have exited on its own.\n if (this.processes.has(pid) && !p.child.killed) {\n try {\n process.kill(-pid, 'SIGKILL');\n } catch {\n try {\n p.child.kill('SIGKILL');\n } catch {\n /* already gone */\n }\n }\n }\n }, graceMs);\n timer.unref?.(); // Don't keep event loop alive.\n }\n } catch {\n // Process may have already exited.\n }\n p.killed = true;\n return true;\n }\n\n /**\n * Kill all tracked processes.\n * Returns the PIDs that were kill()ed.\n */\n killAll(opts: KillOpts = {}): number[] {\n const pids = Array.from(this.processes.keys());\n const killed: number[] = [];\n for (const pid of pids) {\n const p = this.processes.get(pid);\n if (p && !p.protected && this.kill(pid, opts)) killed.push(pid);\n }\n return killed;\n }\n\n /**\n * Kill all processes for a specific session.\n * Returns the PIDs that were kill()ed.\n */\n killSession(sessionId: string, opts: KillOpts = {}): number[] {\n const pids = this.bySession(sessionId).map((p) => p.pid);\n const killed: number[] = [];\n for (const pid of pids) {\n if (this.kill(pid, opts)) killed.push(pid);\n }\n return killed;\n }\n}\n\n/** Module-level singleton. Initialized on first access. */\nlet _registry: ProcessRegistryImpl | undefined;\n\nexport function getProcessRegistry(): ProcessRegistryImpl {\n if (!_registry) {\n _registry = new ProcessRegistryImpl();\n }\n return _registry;\n}\n\n/** Reset for tests. */\nexport function _resetProcessRegistry(): void {\n _registry = undefined;\n}\n\n// ── Convenience re-exports ────────────────────────────────────────────────────\n\nexport type { KillOpts };"]}
|
|
1
|
+
{"version":3,"sources":["../src/circuit-breaker.ts","../src/process-registry.ts"],"names":[],"mappings":"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","file":"process-registry.js","sourcesContent":["/**\n * CircuitBreaker — prevents runaway bash/exec tool chains by:\n *\n * - Tripping on consecutive failures (models that keep repeating the\n * same failing command, e.g. `npm install` with wrong args in a loop)\n * - Tripping on slow call ratio (too many long-running commands suggest\n * a hung subprocess that the model doesn't know how to kill)\n * - Rate-limiting bursts (rapid succession of commands without reading\n * output suggests the model isn't processing results)\n * - Auto-recovering after a cooldown period so a fixed model can resume\n *\n * The breaker is owned by the ProcessRegistry so any tool that registers\n * a process participates in the same circuit. \"Per-tool\" isolation is\n * intentionally NOT implemented — the model treats bash/exec as one\n * resource pool; isolating them would let the model route around the\n * breaker by alternating which tool it uses.\n */\n\nexport interface CircuitBreakerConfig {\n /**\n * Consecutive failures before trip. Default: 5.\n * A single success resets this counter to 0.\n */\n maxConsecutiveFailures?: number | undefined;\n /**\n * Slow-call threshold in ms. A call that runs longer than this is\n * counted as \"slow\". Default: 60_000 (1 minute).\n */\n slowCallThresholdMs?: number | undefined;\n /**\n * Max slow calls before trip (within the sliding window). Default: 3.\n */\n maxSlowCalls?: number | undefined;\n /**\n * Sliding window for rate-limit and slow-call counting, in ms.\n * Default: 60_000 (1 minute).\n */\n windowMs?: number | undefined;\n /**\n * Max calls within the sliding window. Default: 30.\n * Burst exceeding this trips the breaker immediately.\n */\n maxCallsPerWindow?: number | undefined;\n /**\n * Cooldown before auto-recovery attempt, in ms. Default: 30_000 (30s).\n * After this the breaker enters \"half-open\" state and allows one call\n * through to test whether the problem is resolved.\n */\n cooldownMs?: number | undefined;\n}\n\ninterface CallRecord {\n at: number;\n /** True if the call threw or returned an is_error result. */\n failed: boolean;\n /** True if elapsed time exceeded slowCallThresholdMs. */\n slow: boolean;\n}\n\ntype BreakerState = 'closed' | 'open' | 'half-open';\n\nconst DEFAULT_MAX_CONSECUTIVE_FAILURES = 5;\nconst DEFAULT_SLOW_CALL_THRESHOLD_MS = 180_000;\n// 3 minutes — balanced against the 5-minute bash timeout. Commands\n// running <3min are normal; 3-5min are \"slow\" and count toward the\n// breaker. 3 consecutive slow calls trip the circuit.\nconst DEFAULT_MAX_SLOW_CALLS = 3;\nconst DEFAULT_WINDOW_MS = 60_000;\nconst DEFAULT_MAX_CALLS_PER_WINDOW = 30;\nconst DEFAULT_COOLDOWN_MS = 30_000;\n\nexport interface CircuitBreakerSnapshot {\n state: 'closed' | 'open' | 'half-open';\n consecutiveFailures: number;\n slowCallsInWindow: number;\n callsInWindow: number;\n windowMs: number;\n cooldownRemainingMs: number | null;\n lastFailureAt: number | null;\n lastSlowAt: number | null;\n}\n\nexport class CircuitBreaker {\n private readonly maxConsecutiveFailures: number;\n private readonly slowCallThresholdMs: number;\n private readonly maxSlowCalls: number;\n private readonly windowMs: number;\n private readonly maxCallsPerWindow: number;\n private readonly cooldownMs: number;\n\n private state: BreakerState = 'closed';\n private consecutiveFailures = 0;\n private window: CallRecord[] = [];\n private lastFailureAt: number | null = null;\n private lastSlowAt: number | null = null;\n /** Timestamp when the breaker was opened (for cooldown calculation). */\n private openedAt: number | null = null;\n\n /**\n * Master enable flag. When false the breaker is bypassed: `beforeCall`\n * always returns true and `afterCall` records nothing. The class itself\n * defaults to enabled (so the standalone unit tests exercise tripping); the\n * ProcessRegistry flips this off until the user opts in via `/settings`.\n */\n private enabled = true;\n\n /**\n * Fired (best-effort) when the breaker transitions into the `open` state.\n * The registry uses this to arm its auto kill/reset countdown.\n */\n onTrip?: (() => void) | undefined;\n /**\n * Fired (best-effort) when the breaker returns to `closed` after having been\n * open/half-open. The registry uses this to cancel a pending kill/reset.\n */\n onReset?: (() => void) | undefined;\n\n constructor(config: CircuitBreakerConfig = {}) {\n this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;\n this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;\n this.maxSlowCalls = config.maxSlowCalls ?? DEFAULT_MAX_SLOW_CALLS;\n this.windowMs = config.windowMs ?? DEFAULT_WINDOW_MS;\n this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;\n this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;\n }\n\n /** Toggle the master enable. Disabling resets to a clean `closed` state. */\n setEnabled(enabled: boolean): void {\n if (this.enabled === enabled) return;\n this.enabled = enabled;\n if (!enabled) this._reset();\n }\n\n get isEnabled(): boolean {\n return this.enabled;\n }\n\n /**\n * Returns true if the circuit allows a new call to proceed.\n * When false, callers should abort the tool call and return a\n * circuit-breaker error instead of spawning a process.\n */\n get canProceed(): boolean {\n if (!this.enabled) return true;\n this._checkStateTransition();\n return this.state !== 'open';\n }\n\n /**\n * Snapshot of the current breaker state for observability (`/kill`).\n */\n snapshot(): CircuitBreakerSnapshot {\n this._checkStateTransition();\n const now = Date.now();\n let cooldownRemaining: number | null = null;\n if (this.openedAt !== null && this.state === 'open') {\n const elapsed = now - this.openedAt;\n cooldownRemaining = Math.max(0, this.cooldownMs - elapsed);\n }\n return {\n state: this.state,\n consecutiveFailures: this.consecutiveFailures,\n slowCallsInWindow: this.window.filter((c) => c.slow).length,\n callsInWindow: this.window.length,\n windowMs: this.windowMs,\n cooldownRemainingMs: cooldownRemaining,\n lastFailureAt: this.lastFailureAt,\n lastSlowAt: this.lastSlowAt,\n };\n }\n\n /**\n * Call this BEFORE spawning a bash/exec process.\n * Returns true if the call is allowed; false if the breaker is open.\n * When false, callers MUST NOT spawn a process.\n *\n * @param bypass - If true, skip the circuit breaker check entirely.\n * Use for background/fire-and-forget processes that should\n * not affect breaker state.\n */\n beforeCall(bypass = false): boolean {\n if (bypass || !this.enabled) return true;\n this._checkStateTransition();\n if (this.state === 'open') return false;\n return true;\n }\n\n /**\n * Call this AFTER a bash/exec process finishes (success or failure).\n * `durationMs` is the wall-clock time the process ran.\n * `failed` is true when the process returned a non-zero exit code or\n * threw an exception before spawning.\n *\n * @param bypass - If true, do not update breaker state.\n * Use for background/fire-and-forget processes.\n */\n afterCall(durationMs: number, failed: boolean, bypass = false): void {\n if (bypass || !this.enabled) return;\n\n const now = Date.now();\n\n if (this.state === 'half-open') {\n // First call through after cooldown — if it failed, go back to open.\n if (failed) {\n this._trip();\n return;\n }\n // Success in half-open → reset to closed.\n this._reset();\n return;\n }\n\n // Prune old records outside the sliding window.\n this._pruneWindow(now);\n\n const slow = durationMs >= this.slowCallThresholdMs;\n this.window.push({ at: now, failed, slow });\n\n if (failed) {\n this.consecutiveFailures++;\n this.lastFailureAt = now;\n if (this.consecutiveFailures >= this.maxConsecutiveFailures) {\n this._trip();\n }\n return;\n }\n\n // Success: reset consecutive failure counter.\n this.consecutiveFailures = 0;\n\n if (slow) {\n this.lastSlowAt = now;\n const slowCount = this.window.filter((c) => c.slow).length;\n if (slowCount >= this.maxSlowCalls) {\n this._trip();\n }\n }\n\n const callCount = this.window.length;\n if (callCount >= this.maxCallsPerWindow) {\n // Rate limit exceeded. This is a soft trip — we reset the window\n // and let the next call try immediately (the caller will still see\n // canProceed=false until the window drains naturally).\n this._trip();\n }\n }\n\n /** Force the breaker open. Used by /kill force and Ctrl+C. */\n forceOpen(): void {\n this._trip();\n }\n\n /** Force a reset to closed. Used by tests and /kill reset. */\n forceReset(): void {\n this._reset();\n }\n\n private _trip(): void {\n if (this.state === 'open') return; // already open\n this.state = 'open';\n this.openedAt = Date.now();\n // Best-effort: never let a listener failure corrupt breaker state.\n try {\n this.onTrip?.();\n } catch {\n /* ignored — observability hook only */\n }\n }\n\n private _reset(): void {\n const wasRecovering = this.state !== 'closed';\n this.state = 'closed';\n this.consecutiveFailures = 0;\n this.window = [];\n this.openedAt = null;\n // Only notify on a real recovery (open/half-open → closed), not on the\n // initial closed state or an idempotent re-reset.\n if (wasRecovering) {\n try {\n this.onReset?.();\n } catch {\n /* ignored — observability hook only */\n }\n }\n }\n\n /** Transition from open → half-open when cooldown elapses. */\n private _checkStateTransition(): void {\n if (this.state !== 'open' || this.openedAt === null) return;\n const elapsed = Date.now() - this.openedAt;\n if (elapsed >= this.cooldownMs) {\n this.state = 'half-open';\n this.openedAt = null;\n }\n }\n\n private _pruneWindow(now: number): void {\n const cutoff = now - this.windowMs;\n this.window = this.window.filter((c) => c.at >= cutoff);\n }\n}","import { expectDefined } from '@wrongstack/core';\n/**\n * ProcessRegistry — global singleton that tracks all spawned child processes\n * from `bash` and `exec` tools. Enables:\n *\n * - Listing active processes (for TUI status bar)\n * - Killing individual processes or all processes (for Ctrl+C and /kill)\n * - Detecting runaway processes (hung, looping)\n * - Circuit breaker integration to prevent recursive/repeated failures\n *\n * Thread-safety: Node.js is single-threaded, but async callbacks can fire\n * in any order. All mutations go through synchronized Map methods.\n */\nimport { spawn } from 'node:child_process';\nimport type { ChildProcess } from 'node:child_process';\nimport * as os from 'node:os';\nimport { CircuitBreaker, type CircuitBreakerSnapshot, type CircuitBreakerConfig } from './circuit-breaker.js';\nexport type { CircuitBreakerSnapshot, CircuitBreakerConfig } from './circuit-breaker.js';\n\nexport interface TrackedProcess {\n pid: number;\n name: string;\n /** Display-safe redacted command string — safe for logs, /ps, crash dumps.\n * Contains [REDACTED] in place of sensitive flag values. */\n command: string;\n startedAt: number;\n sessionId?: string | undefined;\n /** The raw ChildProcess handle. Never call .kill() directly on this —\n * use `kill()` below which handles process groups correctly on POSIX\n * and degrades gracefully on Windows. */\n child: ChildProcess;\n /** True once the process has been kill()ed but not yet exited.\n * We keep it in the registry until 'close' fires so callers can\n * distinguish \"still running\" from \"just exited\". */\n killed: boolean;\n /** If true, kill() and killAll() will refuse to kill this process.\n * Used for infrastructure processes (browser, dev servers, …) that\n * must outlive the agent session. */\n protected: boolean;\n}\n\n// Sensitive CLI flag patterns that may appear in process command lines.\n// Redacted to [REDACTED] so crash dumps /ps output cannot leak secrets.\nconst SENSITIVE_FLAG_PATTERNS: RegExp[] = [\n // --flag=value or --flag \"value\" (value captured up to next space or comma)\n /--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\\s,][^\\s]*)?/gi,\n // -f \"value\" style short flags\n /(?<!\\w)-t(?:\\s+|\\s*=\\s*)[^\\s,]+/,\n /(?<!\\w)-p(?:ssword)?(?:\\s+|\\s*=\\s*)[^\\s,]+/gi,\n // env var–style secrets: TOKEN=x, API_KEY=y, etc.\n /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\\s*[=:]\\s*[^\\s,]+/gi,\n // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only\n // when preceded by a flag name (e.g. --github-token=EyJ...).\n /--\\w*(?:token|key|secret|password|passwd|auth|credential)\\w*[=\\s,][A-Za-z0-9+/=]{32,}/,\n];\n\n/**\n * Returns a display-safe copy of `cmd` with sensitive flag values replaced by [REDACTED].\n * The original string is unchanged; this is pure and has no side effects.\n */\nexport function redactCommand(cmd: string): string {\n let result = cmd;\n for (const pattern of SENSITIVE_FLAG_PATTERNS) {\n result = result.replace(pattern, (match) => {\n // Preserve the flag name portion; redact only the value part.\n // e.g. \"--token=sekrit_abc\" → \"--token=[REDACTED]\"\n const eq = match.indexOf('=');\n const sp = match.search(/\\s/);\n const delim = eq !== -1 ? '=' : sp !== -1 ? match[sp] : null;\n if (delim !== null) {\n const flag = match.slice(0, match.indexOf(expectDefined(delim)) + 1);\n return `${flag}[REDACTED]`;\n }\n // Nothing delimitable found; replace the whole token silently.\n // Short flags like -tVALUE are replaced entirely to avoid edge cases.\n const flagEnd = match.match(/^--?[a-zA-Z][a-zA-Z0-9_-]*/)?.[0] ?? match;\n return `${flagEnd}=**redacted**`;\n });\n }\n return result;\n}\n\ninterface KillOpts {\n /** SIGKILL instead of SIGTERM. Default: false (SIGTERM first). */\n force?: boolean | undefined;\n /** MS to wait between SIGTERM and SIGKILL on POSIX. Default: 2000. */\n graceMs?: number | undefined;\n}\n\n/**\n * Snapshot of the armed auto kill/reset countdown, or null when nothing is\n * armed. `remainingMs` ticks down in real time; the TUI statusline renders it.\n */\nexport interface BreakerCountdown {\n remainingMs: number;\n totalMs: number;\n}\n\ntype BreakerCountdownListener = (snapshot: BreakerCountdown | null) => void;\n\nexport interface RegistryStats {\n activeCount: number;\n totalCount: number;\n breaker: CircuitBreakerSnapshot;\n}\n\nconst DEFAULT_GRACE_MS = 2000;\n\n/**\n * Kill an entire process tree on Windows via `taskkill /T /F`.\n *\n * TerminateProcess (what `child.kill()` maps to) has no process-group\n * semantics, so killing a shell wrapper (`cmd.exe /c …`) orphans its\n * grandchildren (node, vitest forks, dev servers). The orphans inherit the\n * parent's stdio pipe handles and can keep streaming into this process for\n * the rest of the session — which both prevents the child's 'close' event\n * from ever firing and grows in-memory output buffers without bound.\n *\n * Fire-and-forget: returns true if taskkill was spawned, false if spawning\n * it failed (caller should fall back to a direct `child.kill()`).\n */\nexport function killWin32Tree(pid: number): boolean {\n try {\n const child = spawn('taskkill', ['/pid', String(pid), '/T', '/F'], {\n stdio: 'ignore',\n windowsHide: true,\n });\n // spawn() reports a failure to launch (e.g. taskkill not on PATH, blocked by\n // security software) via an ASYNC 'error' event — the surrounding try/catch\n // only traps synchronous throws. Without a listener that event is unhandled\n // and crashes the whole process. Swallow it: this is best-effort tree-kill\n // and the registry still has the direct child.kill() fallback.\n child.on('error', () => {});\n child.unref();\n return true;\n } catch {\n return false;\n }\n}\n\nexport class ProcessRegistryImpl {\n private readonly processes = new Map<number, TrackedProcess>();\n private readonly breaker: CircuitBreaker;\n\n /**\n * Auto kill/reset config. When the breaker trips and `autoKillResetMs > 0`,\n * a countdown is armed; on expiry all tracked processes are killed and the\n * breaker is reset to closed (forced recovery). Zero means manual recovery\n * only (`/kill reset`).\n */\n private autoKillResetMs = 0;\n private autoKillTimer: ReturnType<typeof setTimeout> | null = null;\n private autoKillArmedAt: number | null = null;\n private breakerCountdownListeners: BreakerCountdownListener[] = [];\n\n constructor(breakerConfig?: CircuitBreakerConfig) {\n this.breaker = new CircuitBreaker(breakerConfig);\n // Arm on trip, cancel on recovery. Listeners are best-effort.\n this.breaker.onTrip = () => this._armAutoKillReset();\n this.breaker.onReset = () => this._cancelAutoKillReset();\n // Protection is OFF by default — the user opts in via `/settings breaker on`.\n this.breaker.setEnabled(false);\n }\n\n register(info: Omit<TrackedProcess, 'killed' | 'protected'> & { protected?: boolean | undefined }): void {\n this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });\n }\n\n /** Unregister a process by PID. Called on 'close' / 'exit' events. */\n unregister(pid: number): void {\n this.processes.delete(pid);\n }\n\n /** Get a single process by PID. */\n get(pid: number): TrackedProcess | undefined {\n return this.processes.get(pid);\n }\n\n /** Get all tracked processes. */\n list(): TrackedProcess[] {\n return Array.from(this.processes.values());\n }\n\n /** Get processes filtered by name (e.g. 'bash', 'exec'). */\n byName(name: string): TrackedProcess[] {\n return this.list().filter((p) => p.name === name);\n }\n\n /** Get processes filtered by session. */\n bySession(sessionId: string): TrackedProcess[] {\n return this.list().filter((p) => p.sessionId === sessionId);\n }\n\n /** Count of active (non-killed) processes. */\n get activeCount(): number {\n let n = 0;\n for (const p of this.processes.values()) {\n if (!p.killed) n++;\n }\n return n;\n }\n\n /**\n * Combined stats for observability — used by /ps and the TUI status bar.\n */\n stats(): RegistryStats {\n return {\n activeCount: this.activeCount,\n totalCount: this.processes.size,\n breaker: this.breaker.snapshot(),\n };\n }\n\n /**\n * Returns true if the circuit allows a new bash/exec call to proceed.\n * When false, callers MUST NOT spawn a process.\n */\n get canProceed(): boolean {\n return this.breaker.canProceed;\n }\n\n /**\n * Called before spawning a process. Returns true if allowed; false if\n * the circuit breaker is open.\n *\n * @param bypass - If true, skip circuit breaker check (for background processes).\n */\n beforeCall(bypass = false): boolean {\n return this.breaker.beforeCall(bypass);\n }\n\n /**\n * Called after a process finishes. `durationMs` is wall-clock time;\n * `failed` is true for non-zero exit codes.\n *\n * @param bypass - If true, do not update circuit breaker state (for background processes).\n */\n afterCall(durationMs: number, failed: boolean, bypass = false): void {\n this.breaker.afterCall(durationMs, failed, bypass);\n }\n\n /** Force-open the circuit breaker (Ctrl+C, /kill force). */\n forceBreakerOpen(): void {\n this.breaker.forceOpen();\n }\n\n /** Force-reset the circuit breaker to closed (/kill reset). */\n forceBreakerReset(): void {\n this.breaker.forceReset();\n }\n\n /**\n * Configure circuit-breaker protection at runtime. Called from `/settings`\n * (instant, all modes) and on TUI mount (applies persisted config).\n *\n * - `enabled` toggles whether the breaker gates `bash`/`exec`.\n * - `autoKillResetMs` arms the auto kill/reset countdown when the breaker\n * trips (0 = manual recovery only).\n *\n * Re-applies cleanly on every call: cancels a pending countdown when the\n * timeout is cleared or protection disabled, and re-arms if the breaker is\n * currently open under the new settings.\n */\n setBreakerConfig(cfg: { enabled?: boolean | undefined; autoKillResetMs?: number | undefined }): void {\n if (cfg.enabled !== undefined) this.breaker.setEnabled(cfg.enabled);\n if (cfg.autoKillResetMs !== undefined) this.autoKillResetMs = Math.max(0, cfg.autoKillResetMs);\n\n if (this.autoKillResetMs <= 0) {\n this._cancelAutoKillReset();\n return;\n }\n // If protection is active and the breaker is currently tripped, ensure a\n // countdown is armed for the new window (covers a live config change while\n // the breaker is already open).\n if (this.breaker.isEnabled && this.breaker.snapshot().state === 'open') {\n this._armAutoKillReset();\n }\n }\n\n /**\n * Live countdown to the next auto kill/reset, or null when nothing is armed.\n * The TUI polls this on a 1s tick while armed so the statusline decrements.\n */\n getBreakerCountdown(): BreakerCountdown | null {\n if (this.autoKillArmedAt === null || this.autoKillResetMs <= 0) return null;\n const elapsed = Date.now() - this.autoKillArmedAt;\n return { remainingMs: Math.max(0, this.autoKillResetMs - elapsed), totalMs: this.autoKillResetMs };\n }\n\n /**\n * Subscribe to countdown arm/cancel events. Returns an unsubscribe function.\n * Use {@link getBreakerCountdown} for the live ticking value between events.\n */\n onBreakerCountdownChange(listener: BreakerCountdownListener): () => void {\n this.breakerCountdownListeners.push(listener);\n return () => {\n this.breakerCountdownListeners = this.breakerCountdownListeners.filter((l) => l !== listener);\n };\n }\n\n private _emitBreakerCountdown(): void {\n const snap = this.getBreakerCountdown();\n for (const l of this.breakerCountdownListeners) {\n try {\n l(snap);\n } catch {\n /* listener failure must never affect breaker behavior */\n }\n }\n }\n\n /**\n * Arm the auto kill/reset countdown. Idempotent: re-arming resets the window\n * (a fresh trip after a failed half-open probe restarts the clock). No-op\n * when protection is off or no timeout is configured.\n */\n private _armAutoKillReset(): void {\n if (this.autoKillResetMs <= 0 || !this.breaker.isEnabled) return;\n this._clearAutoKillTimer();\n this.autoKillArmedAt = Date.now();\n this.autoKillTimer = setTimeout(() => {\n this.autoKillTimer = null;\n this.autoKillArmedAt = null;\n // Forced recovery: nuke runaway processes and reopen the circuit.\n this.killAll({ force: false });\n this.breaker.forceReset();\n this._emitBreakerCountdown();\n }, this.autoKillResetMs);\n // Don't keep the event loop alive purely for auto-recovery.\n this.autoKillTimer.unref?.();\n this._emitBreakerCountdown();\n }\n\n private _cancelAutoKillReset(): void {\n const wasArmed = this.autoKillArmedAt !== null;\n this._clearAutoKillTimer();\n if (wasArmed) {\n this.autoKillArmedAt = null;\n this._emitBreakerCountdown();\n }\n }\n\n private _clearAutoKillTimer(): void {\n if (this.autoKillTimer !== null) {\n clearTimeout(this.autoKillTimer);\n this.autoKillTimer = null;\n }\n }\n\n /** Kill a single process by PID.\n *\n * On POSIX: sends SIGTERM to the *process group* (-pid) so that\n * runaway grandchild processes (`sleep 9999 & disown`) are also killed.\n * After `graceMs` a SIGKILL is sent if the process hasn't exited.\n *\n * On Windows: `child.kill()` maps to TerminateProcess — process groups\n * are not meaningfully supported. A second `force=true` call sends\n * SIGKILL (which maps to TerminateProcess again — the distinction is\n * in the exit code, not the signal).\n *\n * Returns true if the process was found and kill was attempted.\n */\n kill(pid: number, opts: KillOpts = {}): boolean {\n const p = this.processes.get(pid);\n if (!p) return false;\n if (p.killed) return true; // already kill()ed, don't double-send\n if (p.protected) return false; // protected processes are never kill()ed\n\n const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;\n const isWin = os.platform() === 'win32';\n\n if (isWin) {\n // Windows: no process group semantics. A direct kill terminates only\n // the immediate child — shell-wrapped commands (cmd.exe /c …) leave\n // grandchildren running that hold the inherited stdio pipes open and\n // keep feeding output into this process indefinitely. Kill the whole\n // tree via taskkill instead, but only for a real, still-running child\n // (exitCode === null); test fakes and already-exited processes take\n // the plain-kill path. The direct kill is deliberately NOT sent\n // immediately alongside taskkill: killing the root first would break\n // taskkill's parent-pid tree enumeration and orphan the grandchildren\n // again — it runs as a delayed fallback instead.\n const liveRealChild = p.child.exitCode === null && typeof p.child.pid === 'number';\n if (liveRealChild && killWin32Tree(pid)) {\n const fallback = setTimeout(() => {\n if (p.child.exitCode === null) {\n try {\n p.child.kill('SIGKILL');\n } catch {\n // Process may have already exited.\n }\n }\n }, graceMs);\n fallback.unref?.();\n } else {\n try {\n p.child.kill(force ? 'SIGKILL' : 'SIGTERM');\n } catch {\n // Process may have already exited.\n }\n }\n p.killed = true;\n return true;\n }\n\n // POSIX: kill the process group so grandchildren are cleaned up too.\n try {\n if (force) {\n try {\n process.kill(-pid, 'SIGKILL');\n } catch {\n p.child.kill('SIGKILL');\n }\n } else {\n try {\n process.kill(-pid, 'SIGTERM');\n } catch {\n p.child.kill('SIGTERM');\n }\n // Schedule SIGKILL as backup.\n const timer = setTimeout(() => {\n // Re-check: process may have exited on its own.\n if (this.processes.has(pid) && !p.child.killed) {\n try {\n process.kill(-pid, 'SIGKILL');\n } catch {\n try {\n p.child.kill('SIGKILL');\n } catch {\n /* already gone */\n }\n }\n }\n }, graceMs);\n timer.unref?.(); // Don't keep event loop alive.\n }\n } catch {\n // Process may have already exited.\n }\n p.killed = true;\n return true;\n }\n\n /**\n * Kill all tracked processes.\n * Returns the PIDs that were kill()ed.\n */\n killAll(opts: KillOpts = {}): number[] {\n const pids = Array.from(this.processes.keys());\n const killed: number[] = [];\n for (const pid of pids) {\n const p = this.processes.get(pid);\n if (p && !p.protected && this.kill(pid, opts)) killed.push(pid);\n }\n return killed;\n }\n\n /**\n * Kill all processes for a specific session.\n * Returns the PIDs that were kill()ed.\n */\n killSession(sessionId: string, opts: KillOpts = {}): number[] {\n const pids = this.bySession(sessionId).map((p) => p.pid);\n const killed: number[] = [];\n for (const pid of pids) {\n if (this.kill(pid, opts)) killed.push(pid);\n }\n return killed;\n }\n}\n\n/** Module-level singleton. Initialized on first access. */\nlet _registry: ProcessRegistryImpl | undefined;\n\nexport function getProcessRegistry(): ProcessRegistryImpl {\n if (!_registry) {\n _registry = new ProcessRegistryImpl();\n }\n return _registry;\n}\n\n/** Reset for tests. */\nexport function _resetProcessRegistry(): void {\n _registry = undefined;\n}\n\n// ── Convenience re-exports ────────────────────────────────────────────────────\n\nexport type { KillOpts };"]}
|
package/dist/read.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fsp from 'node:fs/promises';
|
|
2
|
-
import { toErrorMessage } from '@wrongstack/core/utils';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
2
|
import * as Core from '@wrongstack/core';
|
|
3
|
+
import { toErrorMessage } from '@wrongstack/core';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
5
|
|
|
6
6
|
// src/read.ts
|
|
7
7
|
function resolvePath(input, ctx) {
|
package/dist/read.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/_util.ts","../src/read.ts"],"names":["stat","fs"],"mappings":"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","file":"read.js","sourcesContent":["import * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import * as fs from 'node:fs/promises';\nimport type { Tool } from '@wrongstack/core';\nimport { toErrorMessage } from '@wrongstack/core/utils';\nimport { isBinaryBuffer, safeResolveReal } from './_util.js';\n\ninterface ReadInput {\n path: string;\n offset?: number | undefined;\n limit?: number | undefined;\n mode?: 'content' | 'summary' | undefined;\n}\n\ninterface ReadOutput {\n text: string;\n total_lines: number;\n encoding: string;\n truncated: boolean;\n cached?: boolean | undefined;\n note?: string | undefined;\n}\n\nconst MAX_BYTES = 5 * 1024 * 1024;\n\nexport const readTool: Tool<ReadInput, ReadOutput> = {\n name: 'read',\n category: 'Filesystem',\n description:\n 'Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. ' +\n 'Lines are returned 1-indexed with a ` N| ` prefix for easy reference in edits.',\n usageHint:\n 'FOUNDATIONAL TOOL — call this before almost any edit operation.\\n\\n' +\n 'Best practices:\\n' +\n '- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\\n' +\n '- Use `offset` + `limit` for very large files instead of reading everything at once.\\n' +\n '- Default limit is generous (2000 lines) but can be increased.\\n' +\n '- The output format is designed to be directly usable as context for `edit` operations.',\n permission: 'auto',\n mutating: false,\n capabilities: ['fs.read'],\n icon: 'file',\n maxOutputBytes: 262_144,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Path to the file (relative to project root or absolute within project).',\n },\n offset: {\n type: 'integer',\n description: '1-based starting line number. Use together with `limit` for large files.',\n },\n limit: {\n type: 'integer',\n description: 'Maximum number of lines to return (default is 2000).',\n },\n mode: {\n type: 'string',\n enum: ['content', 'summary'],\n description:\n 'Return full line-numbered content (default) or a compact file summary with imports/exports/symbols.',\n },\n },\n required: ['path'],\n },\n async execute(input, ctx) {\n if (!input?.path) throw new Error('read: path is required');\n const absPath = await safeResolveReal(input.path, ctx);\n\n let stat: Awaited<ReturnType<typeof fs.stat>>;\n try {\n stat = await fs.stat(absPath);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') throw new Error(`read: file not found \"${input.path}\"`);\n throw new Error(`read: failed to stat \"${input.path}\": ${toErrorMessage(err)}`);\n }\n if (!stat.isFile()) throw new Error(`read: \"${input.path}\" is not a regular file`);\n if (stat.size > MAX_BYTES) {\n throw new Error(`read: file too large (${stat.size} bytes, limit ${MAX_BYTES})`);\n }\n\n const offset = Math.max(1, input.offset ?? 1);\n const limit = Math.max(0, Math.min(input.limit ?? 2000, 5000));\n const prior = getReadRangeRecord(ctx, absPath);\n const requestedEnd = prior\n ? Math.min(offset + limit - 1, prior.totalLines)\n : offset + limit - 1;\n if (\n input.mode !== 'summary' &&\n limit > 0 &&\n prior &&\n coversRange(prior, stat.mtimeMs, offset, requestedEnd)\n ) {\n ctx.recordRead(absPath, stat.mtimeMs);\n return {\n text:\n `[unchanged since previous read: \"${input.path}\" mtime=${Math.round(stat.mtimeMs)}; ` +\n `requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,\n total_lines: prior.totalLines,\n encoding: 'utf8',\n truncated: requestedEnd < prior.totalLines,\n cached: true,\n note: 'Repeated read suppressed to save tokens.',\n };\n }\n\n const buf = await fs.readFile(absPath);\n if (isBinaryBuffer(buf)) {\n throw new Error(`read: \"${input.path}\" appears to be binary`);\n }\n\n const text = buf.toString('utf8');\n const allLines = text.split(/\\r\\n|\\r|\\n/);\n const total = allLines.length;\n if (input.mode === 'summary') {\n ctx.recordRead(absPath, stat.mtimeMs);\n rememberReadRange(ctx, absPath, stat.mtimeMs, total, 1, Math.min(total, 200));\n return {\n text: summarizeFile(input.path, stat.size, allLines),\n total_lines: total,\n encoding: 'utf8',\n truncated: total > 200,\n note: 'Summary mode returned compact structure instead of full file content.',\n };\n }\n if (limit === 0) {\n ctx.recordRead(absPath, stat.mtimeMs);\n rememberReadRange(ctx, absPath, stat.mtimeMs, total, 1, 0);\n return { text: '', total_lines: total, encoding: 'utf8', truncated: total > 0 };\n }\n // Offset past EOF: return an explicit message instead of an empty string.\n // Without this, models with weak instruction-following (e.g. k2p7) see an\n // empty result, assume the read failed transiently, and retry the exact\n // same offset indefinitely — a tight tool-use loop that burns iterations\n // and context without making progress.\n if (offset > total) {\n ctx.recordRead(absPath, stat.mtimeMs);\n rememberReadRange(ctx, absPath, stat.mtimeMs, total, total + 1, total + 1);\n return {\n text: `[offset ${offset} is past end of file \"${input.path}\" — file has ${total} line(s). Do not retry this offset.]`,\n total_lines: total,\n encoding: 'utf8',\n truncated: false,\n };\n }\n\n const slice = allLines.slice(offset - 1, offset - 1 + limit);\n const truncated = offset - 1 + slice.length < total;\n\n const width = String(offset + slice.length - 1).length;\n const numbered = slice\n .map((line, i) => `${String(offset + i).padStart(width, ' ')}→${line}`)\n .join('\\n');\n\n ctx.recordRead(absPath, stat.mtimeMs);\n rememberReadRange(ctx, absPath, stat.mtimeMs, total, offset, offset + slice.length - 1);\n\n return {\n text: numbered,\n total_lines: total,\n encoding: 'utf8',\n truncated,\n };\n },\n};\n\ninterface ReadRangeRecord {\n mtimeMs: number;\n totalLines: number;\n ranges: Array<{ start: number; end: number }>;\n}\n\nconst READ_RANGES_META_KEY = 'tools.read.ranges.v1';\n\nfunction getReadRanges(ctx: import('@wrongstack/core').Context): Record<string, ReadRangeRecord> {\n const existing = ctx.meta[READ_RANGES_META_KEY];\n if (existing && typeof existing === 'object' && !Array.isArray(existing)) {\n return existing as Record<string, ReadRangeRecord>;\n }\n const next: Record<string, ReadRangeRecord> = {};\n ctx.meta[READ_RANGES_META_KEY] = next;\n return next;\n}\n\nfunction getReadRangeRecord(\n ctx: import('@wrongstack/core').Context,\n absPath: string,\n): ReadRangeRecord | undefined {\n return getReadRanges(ctx)[absPath];\n}\n\nfunction rememberReadRange(\n ctx: import('@wrongstack/core').Context,\n absPath: string,\n mtimeMs: number,\n totalLines: number,\n start: number,\n end: number,\n): void {\n if (end < start) return;\n const ranges = getReadRanges(ctx);\n const prior = ranges[absPath];\n const nextRanges = prior && Math.abs(prior.mtimeMs - mtimeMs) <= 1 ? prior.ranges.slice() : [];\n nextRanges.push({ start, end });\n ranges[absPath] = {\n mtimeMs,\n totalLines,\n ranges: mergeRanges(nextRanges),\n };\n}\n\nfunction coversRange(\n record: ReadRangeRecord,\n mtimeMs: number,\n start: number,\n end: number,\n): boolean {\n if (Math.abs(record.mtimeMs - mtimeMs) > 1) return false;\n return record.ranges.some((range) => range.start <= start && range.end >= end);\n}\n\nfunction mergeRanges(\n ranges: Array<{ start: number; end: number }>,\n): Array<{ start: number; end: number }> {\n const sorted = ranges.slice().sort((a, b) => a.start - b.start);\n const merged: Array<{ start: number; end: number }> = [];\n for (const range of sorted) {\n const last = merged[merged.length - 1];\n if (!last || range.start > last.end + 1) {\n merged.push({ ...range });\n continue;\n }\n last.end = Math.max(last.end, range.end);\n }\n return merged;\n}\n\nfunction summarizeFile(filePath: string, bytes: number, lines: string[]): string {\n const interesting = lines\n .map((line, index) => ({ line: line.trim(), number: index + 1 }))\n .filter(({ line }) =>\n /^(import\\s|export\\s|class\\s|interface\\s|type\\s|function\\s|const\\s+\\w+\\s*=|let\\s+\\w+\\s*=|var\\s+\\w+\\s*=|def\\s+|async\\s+function\\s)/.test(\n line,\n ),\n )\n .slice(0, 80)\n .map(({ line, number }) => `${number}: ${line}`);\n return [\n `summary: ${filePath}`,\n `bytes=${bytes}`,\n `total_lines=${lines.length}`,\n interesting.length > 0\n ? `symbols/imports:\\n${interesting.join('\\n')}`\n : 'symbols/imports: (none detected)',\n ].join('\\n');\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/_util.ts","../src/read.ts"],"names":["stat","fs"],"mappings":";;;;;;AA8BO,SAAS,WAAA,CAAY,OAAe,GAAA,EAAsB;AAC/D,EAAA,OAAY,IAAA,CAAA,UAAA,CAAW,KAAK,CAAA,GAAS,IAAA,CAAA,SAAA,CAAU,KAAK,CAAA,GAAS,IAAA,CAAA,OAAA,CAAQ,GAAA,CAAI,UAAA,IAAc,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AACvG;AAOA,SAAS,aAAa,GAAA,EAAwB;AAC5C,EAAA,OAAO,CAAM,aAAQ,GAAA,CAAI,WAAW,GAAQ,IAAA,CAAA,OAAA,CAAa,IAAA,CAAA,gBAAA,EAAkB,CAAC,CAAA;AAC9E;AAGA,SAAS,WAAA,CAAY,QAAgB,KAAA,EAA0B;AAC7D,EAAA,OAAO,KAAA,CAAM,IAAA,CAAK,CAAC,IAAA,KAAS;AAC1B,IAAA,MAAM,GAAA,GAAW,IAAA,CAAA,QAAA,CAAS,IAAA,EAAM,MAAM,CAAA;AACtC,IAAA,OAAO,GAAA,KAAQ,MAAO,CAAC,GAAA,CAAI,WAAW,IAAI,CAAA,IAAK,CAAM,IAAA,CAAA,UAAA,CAAW,GAAG,CAAA;AAAA,EACrE,CAAC,CAAA;AACH;AAEO,SAAS,gBAAA,CAAiB,SAAiB,GAAA,EAAsB;AACtE,EAAA,MAAM,MAAA,GAAc,aAAQ,OAAO,CAAA;AAEnC,EAAA,IAAI,GAAA,CAAI,yBAAyB,OAAO,MAAA;AACxC,EAAA,IAAI,YAAY,MAAA,EAAQ,YAAA,CAAa,GAAG,CAAC,GAAG,OAAO,MAAA;AACnD,EAAA,MAAM,IAAI,MAAM,CAAA,MAAA,EAAS,OAAO,8BAAmC,IAAA,CAAA,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAC,CAAA,CAAA,CAAG,CAAA;AAChG;AAEO,SAAS,WAAA,CAAY,OAAe,GAAA,EAAsB;AAC/D,EAAA,OAAO,gBAAA,CAAiB,WAAA,CAAY,KAAA,EAAO,GAAG,GAAG,GAAG,CAAA;AACtD;AAgBA,eAAsB,oBAAA,CAAqB,SAAiB,GAAA,EAA6B;AAEvF,EAAA,IAAI,IAAI,uBAAA,EAAyB;AAGjC,EAAA,MAAM,SAAA,GAAY,MAAM,OAAA,CAAQ,GAAA;AAAA,IAC9B,YAAA,CAAa,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAU,GAAA,CAAA,QAAA,CAAS,CAAC,CAAA,CAAE,KAAA,CAAM,MAAW,IAAA,CAAA,OAAA,CAAQ,CAAC,CAAC,CAAC;AAAA,GAC3E;AACA,EAAA,IAAI,KAAA,GAAQ,OAAA;AACZ,EAAA,WAAS;AACP,IAAA,IAAI,IAAA;AACJ,IAAA,IAAI;AACF,MAAA,IAAA,GAAO,MAAU,aAAS,KAAK,CAAA;AAAA,IACjC,SAAS,GAAA,EAAK;AACZ,MAAA,IAAK,GAAA,CAA8B,SAAS,QAAA,EAAU;AACpD,QAAA,MAAM,MAAA,GAAc,aAAQ,KAAK,CAAA;AACjC,QAAA,IAAI,WAAW,KAAA,EAAO;AACtB,QAAA,KAAA,GAAQ,MAAA;AACR,QAAA;AAAA,MACF;AACA,MAAA,MAAM,GAAA;AAAA,IACR;AACA,IAAA,IAAI,WAAA,CAAY,IAAA,EAAM,SAAS,CAAA,EAAG;AAClC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,MAAA,EAAS,OAAO,CAAA,mDAAA,EAAsD,SAAA,CAAU,CAAC,CAAC,CAAA,CAAA;AAAA,KACpF;AAAA,EACF;AACF;AAGA,eAAsB,eAAA,CAAgB,OAAe,GAAA,EAA+B;AAClF,EAAA,MAAM,GAAA,GAAM,WAAA,CAAY,KAAA,EAAO,GAAG,CAAA;AAClC,EAAA,MAAM,oBAAA,CAAqB,KAAK,GAAG,CAAA;AACnC,EAAA,OAAO,GAAA;AACT;AAYO,SAAS,eAAe,GAAA,EAAsB;AACnD,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,QAAQ,IAAI,CAAA;AACrC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,GAAA,EAAK,CAAA,EAAA,EAAK;AAC5B,IAAA,IAAI,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,EAAG,OAAO,IAAA;AAAA,EAC3B;AACA,EAAA,OAAO,KAAA;AACT;;;AC7GA,IAAM,SAAA,GAAY,IAAI,IAAA,GAAO,IAAA;AAEtB,IAAM,QAAA,GAAwC;AAAA,EACnD,IAAA,EAAM,MAAA;AAAA,EACN,QAAA,EAAU,YAAA;AAAA,EACV,WAAA,EACE,wOAAA;AAAA,EAEF,SAAA,EACE,0bAAA;AAAA,EAMF,UAAA,EAAY,MAAA;AAAA,EACZ,QAAA,EAAU,KAAA;AAAA,EACV,YAAA,EAAc,CAAC,SAAS,CAAA;AAAA,EACxB,IAAA,EAAM,MAAA;AAAA,EACN,cAAA,EAAgB,MAAA;AAAA,EAChB,SAAA,EAAW,GAAA;AAAA,EACX,WAAA,EAAa;AAAA,IACX,IAAA,EAAM,QAAA;AAAA,IACN,UAAA,EAAY;AAAA,MACV,IAAA,EAAM;AAAA,QACJ,IAAA,EAAM,QAAA;AAAA,QACN,WAAA,EAAa;AAAA,OACf;AAAA,MACA,MAAA,EAAQ;AAAA,QACN,IAAA,EAAM,SAAA;AAAA,QACN,WAAA,EAAa;AAAA,OACf;AAAA,MACA,KAAA,EAAO;AAAA,QACL,IAAA,EAAM,SAAA;AAAA,QACN,WAAA,EAAa;AAAA,OACf;AAAA,MACA,IAAA,EAAM;AAAA,QACJ,IAAA,EAAM,QAAA;AAAA,QACN,IAAA,EAAM,CAAC,SAAA,EAAW,SAAS,CAAA;AAAA,QAC3B,WAAA,EACE;AAAA;AACJ,KACF;AAAA,IACA,QAAA,EAAU,CAAC,MAAM;AAAA,GACnB;AAAA,EACA,MAAM,OAAA,CAAQ,KAAA,EAAO,GAAA,EAAK;AACxB,IAAA,IAAI,CAAC,KAAA,EAAO,IAAA,EAAM,MAAM,IAAI,MAAM,wBAAwB,CAAA;AAC1D,IAAA,MAAM,OAAA,GAAU,MAAM,eAAA,CAAgB,KAAA,CAAM,MAAM,GAAG,CAAA;AAErD,IAAA,IAAIA,KAAAA;AACJ,IAAA,IAAI;AACF,MAAAA,KAAAA,GAAO,MAASC,GAAA,CAAA,IAAA,CAAK,OAAO,CAAA;AAAA,IAC9B,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,OAAQ,GAAA,CAA8B,IAAA;AAC5C,MAAA,IAAI,IAAA,KAAS,UAAU,MAAM,IAAI,MAAM,CAAA,sBAAA,EAAyB,KAAA,CAAM,IAAI,CAAA,CAAA,CAAG,CAAA;AAC7E,MAAA,MAAM,IAAI,MAAM,CAAA,sBAAA,EAAyB,KAAA,CAAM,IAAI,CAAA,GAAA,EAAM,cAAA,CAAe,GAAG,CAAC,CAAA,CAAE,CAAA;AAAA,IAChF;AACA,IAAA,IAAI,CAACD,KAAAA,CAAK,MAAA,EAAO,EAAG,MAAM,IAAI,KAAA,CAAM,CAAA,OAAA,EAAU,KAAA,CAAM,IAAI,CAAA,uBAAA,CAAyB,CAAA;AACjF,IAAA,IAAIA,KAAAA,CAAK,OAAO,SAAA,EAAW;AACzB,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,sBAAA,EAAyBA,MAAK,IAAI,CAAA,cAAA,EAAiB,SAAS,CAAA,CAAA,CAAG,CAAA;AAAA,IACjF;AAEA,IAAA,MAAM,SAAS,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAA,CAAM,UAAU,CAAC,CAAA;AAC5C,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,IAAI,KAAA,CAAM,KAAA,IAAS,GAAA,EAAM,GAAI,CAAC,CAAA;AAC7D,IAAA,MAAM,KAAA,GAAQ,kBAAA,CAAmB,GAAA,EAAK,OAAO,CAAA;AAC7C,IAAA,MAAM,YAAA,GAAe,KAAA,GACjB,IAAA,CAAK,GAAA,CAAI,MAAA,GAAS,KAAA,GAAQ,CAAA,EAAG,KAAA,CAAM,UAAU,CAAA,GAC7C,MAAA,GAAS,KAAA,GAAQ,CAAA;AACrB,IAAA,IACE,KAAA,CAAM,IAAA,KAAS,SAAA,IACf,KAAA,GAAQ,CAAA,IACR,KAAA,IACA,WAAA,CAAY,KAAA,EAAOA,KAAAA,CAAK,OAAA,EAAS,MAAA,EAAQ,YAAY,CAAA,EACrD;AACA,MAAA,GAAA,CAAI,UAAA,CAAW,OAAA,EAASA,KAAAA,CAAK,OAAO,CAAA;AACpC,MAAA,OAAO;AAAA,QACL,IAAA,EACE,CAAA,iCAAA,EAAoC,KAAA,CAAM,IAAI,CAAA,QAAA,EAAW,IAAA,CAAK,KAAA,CAAMA,KAAAA,CAAK,OAAO,CAAC,CAAA,kBAAA,EAC9D,MAAM,IAAI,YAAY,CAAA,iEAAA,CAAA;AAAA,QAC3C,aAAa,KAAA,CAAM,UAAA;AAAA,QACnB,QAAA,EAAU,MAAA;AAAA,QACV,SAAA,EAAW,eAAe,KAAA,CAAM,UAAA;AAAA,QAChC,MAAA,EAAQ,IAAA;AAAA,QACR,IAAA,EAAM;AAAA,OACR;AAAA,IACF;AAEA,IAAA,MAAM,GAAA,GAAM,MAASC,GAAA,CAAA,QAAA,CAAS,OAAO,CAAA;AACrC,IAAA,IAAI,cAAA,CAAe,GAAG,CAAA,EAAG;AACvB,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,OAAA,EAAU,KAAA,CAAM,IAAI,CAAA,sBAAA,CAAwB,CAAA;AAAA,IAC9D;AAEA,IAAA,MAAM,IAAA,GAAO,GAAA,CAAI,QAAA,CAAS,MAAM,CAAA;AAChC,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,YAAY,CAAA;AACxC,IAAA,MAAM,QAAQ,QAAA,CAAS,MAAA;AACvB,IAAA,IAAI,KAAA,CAAM,SAAS,SAAA,EAAW;AAC5B,MAAA,GAAA,CAAI,UAAA,CAAW,OAAA,EAASD,KAAAA,CAAK,OAAO,CAAA;AACpC,MAAA,iBAAA,CAAkB,GAAA,EAAK,OAAA,EAASA,KAAAA,CAAK,OAAA,EAAS,KAAA,EAAO,GAAG,IAAA,CAAK,GAAA,CAAI,KAAA,EAAO,GAAG,CAAC,CAAA;AAC5E,MAAA,OAAO;AAAA,QACL,MAAM,aAAA,CAAc,KAAA,CAAM,IAAA,EAAMA,KAAAA,CAAK,MAAM,QAAQ,CAAA;AAAA,QACnD,WAAA,EAAa,KAAA;AAAA,QACb,QAAA,EAAU,MAAA;AAAA,QACV,WAAW,KAAA,GAAQ,GAAA;AAAA,QACnB,IAAA,EAAM;AAAA,OACR;AAAA,IACF;AACA,IAAA,IAAI,UAAU,CAAA,EAAG;AACf,MAAA,GAAA,CAAI,UAAA,CAAW,OAAA,EAASA,KAAAA,CAAK,OAAO,CAAA;AACpC,MAAA,iBAAA,CAAkB,KAAK,OAAA,EAASA,KAAAA,CAAK,OAAA,EAAS,KAAA,EAAO,GAAG,CAAC,CAAA;AACzD,MAAA,OAAO,EAAE,MAAM,EAAA,EAAI,WAAA,EAAa,OAAO,QAAA,EAAU,MAAA,EAAQ,SAAA,EAAW,KAAA,GAAQ,CAAA,EAAE;AAAA,IAChF;AAMA,IAAA,IAAI,SAAS,KAAA,EAAO;AAClB,MAAA,GAAA,CAAI,UAAA,CAAW,OAAA,EAASA,KAAAA,CAAK,OAAO,CAAA;AACpC,MAAA,iBAAA,CAAkB,GAAA,EAAK,SAASA,KAAAA,CAAK,OAAA,EAAS,OAAO,KAAA,GAAQ,CAAA,EAAG,QAAQ,CAAC,CAAA;AACzE,MAAA,OAAO;AAAA,QACL,MAAM,CAAA,QAAA,EAAW,MAAM,yBAAyB,KAAA,CAAM,IAAI,qBAAgB,KAAK,CAAA,oCAAA,CAAA;AAAA,QAC/E,WAAA,EAAa,KAAA;AAAA,QACb,QAAA,EAAU,MAAA;AAAA,QACV,SAAA,EAAW;AAAA,OACb;AAAA,IACF;AAEA,IAAA,MAAM,QAAQ,QAAA,CAAS,KAAA,CAAM,SAAS,CAAA,EAAG,MAAA,GAAS,IAAI,KAAK,CAAA;AAC3D,IAAA,MAAM,SAAA,GAAY,MAAA,GAAS,CAAA,GAAI,KAAA,CAAM,MAAA,GAAS,KAAA;AAE9C,IAAA,MAAM,QAAQ,MAAA,CAAO,MAAA,GAAS,KAAA,CAAM,MAAA,GAAS,CAAC,CAAA,CAAE,MAAA;AAChD,IAAA,MAAM,QAAA,GAAW,MACd,GAAA,CAAI,CAAC,MAAM,CAAA,KAAM,CAAA,EAAG,OAAO,MAAA,GAAS,CAAC,EAAE,QAAA,CAAS,KAAA,EAAO,GAAG,CAAC,CAAA,MAAA,EAAI,IAAI,CAAA,CAAE,CAAA,CACrE,KAAK,IAAI,CAAA;AAEZ,IAAA,GAAA,CAAI,UAAA,CAAW,OAAA,EAASA,KAAAA,CAAK,OAAO,CAAA;AACpC,IAAA,iBAAA,CAAkB,GAAA,EAAK,SAASA,KAAAA,CAAK,OAAA,EAAS,OAAO,MAAA,EAAQ,MAAA,GAAS,KAAA,CAAM,MAAA,GAAS,CAAC,CAAA;AAEtF,IAAA,OAAO;AAAA,MACL,IAAA,EAAM,QAAA;AAAA,MACN,WAAA,EAAa,KAAA;AAAA,MACb,QAAA,EAAU,MAAA;AAAA,MACV;AAAA,KACF;AAAA,EACF;AACF;AAQA,IAAM,oBAAA,GAAuB,sBAAA;AAE7B,SAAS,cAAc,GAAA,EAA0E;AAC/F,EAAA,MAAM,QAAA,GAAW,GAAA,CAAI,IAAA,CAAK,oBAAoB,CAAA;AAC9C,EAAA,IAAI,QAAA,IAAY,OAAO,QAAA,KAAa,QAAA,IAAY,CAAC,KAAA,CAAM,OAAA,CAAQ,QAAQ,CAAA,EAAG;AACxE,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,MAAM,OAAwC,EAAC;AAC/C,EAAA,GAAA,CAAI,IAAA,CAAK,oBAAoB,CAAA,GAAI,IAAA;AACjC,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,kBAAA,CACP,KACA,OAAA,EAC6B;AAC7B,EAAA,OAAO,aAAA,CAAc,GAAG,CAAA,CAAE,OAAO,CAAA;AACnC;AAEA,SAAS,kBACP,GAAA,EACA,OAAA,EACA,OAAA,EACA,UAAA,EACA,OACA,GAAA,EACM;AACN,EAAA,IAAI,MAAM,KAAA,EAAO;AACjB,EAAA,MAAM,MAAA,GAAS,cAAc,GAAG,CAAA;AAChC,EAAA,MAAM,KAAA,GAAQ,OAAO,OAAO,CAAA;AAC5B,EAAA,MAAM,UAAA,GAAa,KAAA,IAAS,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,OAAA,GAAU,OAAO,CAAA,IAAK,CAAA,GAAI,KAAA,CAAM,MAAA,CAAO,KAAA,KAAU,EAAC;AAC7F,EAAA,UAAA,CAAW,IAAA,CAAK,EAAE,KAAA,EAAO,GAAA,EAAK,CAAA;AAC9B,EAAA,MAAA,CAAO,OAAO,CAAA,GAAI;AAAA,IAChB,OAAA;AAAA,IACA,UAAA;AAAA,IACA,MAAA,EAAQ,YAAY,UAAU;AAAA,GAChC;AACF;AAEA,SAAS,WAAA,CACP,MAAA,EACA,OAAA,EACA,KAAA,EACA,GAAA,EACS;AACT,EAAA,IAAI,KAAK,GAAA,CAAI,MAAA,CAAO,UAAU,OAAO,CAAA,GAAI,GAAG,OAAO,KAAA;AACnD,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,IAAA,CAAK,CAAC,KAAA,KAAU,MAAM,KAAA,IAAS,KAAA,IAAS,KAAA,CAAM,GAAA,IAAO,GAAG,CAAA;AAC/E;AAEA,SAAS,YACP,MAAA,EACuC;AACvC,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,KAAA,EAAM,CAAE,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,CAAE,KAAA,GAAQ,CAAA,CAAE,KAAK,CAAA;AAC9D,EAAA,MAAM,SAAgD,EAAC;AACvD,EAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC1B,IAAA,MAAM,IAAA,GAAO,MAAA,CAAO,MAAA,CAAO,MAAA,GAAS,CAAC,CAAA;AACrC,IAAA,IAAI,CAAC,IAAA,IAAQ,KAAA,CAAM,KAAA,GAAQ,IAAA,CAAK,MAAM,CAAA,EAAG;AACvC,MAAA,MAAA,CAAO,IAAA,CAAK,EAAE,GAAG,KAAA,EAAO,CAAA;AACxB,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,MAAM,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,GAAA,EAAK,MAAM,GAAG,CAAA;AAAA,EACzC;AACA,EAAA,OAAO,MAAA;AACT;AAEA,SAAS,aAAA,CAAc,QAAA,EAAkB,KAAA,EAAe,KAAA,EAAyB;AAC/E,EAAA,MAAM,WAAA,GAAc,KAAA,CACjB,GAAA,CAAI,CAAC,MAAM,KAAA,MAAW,EAAE,IAAA,EAAM,IAAA,CAAK,MAAK,EAAG,MAAA,EAAQ,KAAA,GAAQ,CAAA,GAAI,CAAA,CAC/D,MAAA;AAAA,IAAO,CAAC,EAAE,IAAA,EAAK,KACd,kIAAA,CAAmI,IAAA;AAAA,MACjI;AAAA;AACF,GACF,CACC,KAAA,CAAM,CAAA,EAAG,EAAE,EACX,GAAA,CAAI,CAAC,EAAE,IAAA,EAAM,QAAO,KAAM,CAAA,EAAG,MAAM,CAAA,EAAA,EAAK,IAAI,CAAA,CAAE,CAAA;AACjD,EAAA,OAAO;AAAA,IACL,YAAY,QAAQ,CAAA,CAAA;AAAA,IACpB,SAAS,KAAK,CAAA,CAAA;AAAA,IACd,CAAA,YAAA,EAAe,MAAM,MAAM,CAAA,CAAA;AAAA,IAC3B,WAAA,CAAY,SAAS,CAAA,GACjB,CAAA;AAAA,EAAqB,WAAA,CAAY,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA,GAC3C;AAAA,GACN,CAAE,KAAK,IAAI,CAAA;AACb","file":"read.js","sourcesContent":["import * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm → yarn → npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root→out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink — macOS `/var`→`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// ─── Command-output normalization (token-saving) ────────────────────────────\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) — never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only — it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `… ⟨repeated ${run}×⟩`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends — the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n…[truncated ${total - kept} bytes]…\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI → collapse\n * carriage-return progress → trim trailing whitespace → collapse identical\n * consecutive lines → squeeze blank-line runs → head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines → 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n","import * as fs from 'node:fs/promises';\nimport { type Tool, toErrorMessage } from '@wrongstack/core';\nimport { isBinaryBuffer, safeResolveReal } from './_util.js';\n\ninterface ReadInput {\n path: string;\n offset?: number | undefined;\n limit?: number | undefined;\n mode?: 'content' | 'summary' | undefined;\n}\n\ninterface ReadOutput {\n text: string;\n total_lines: number;\n encoding: string;\n truncated: boolean;\n cached?: boolean | undefined;\n note?: string | undefined;\n}\n\nconst MAX_BYTES = 5 * 1024 * 1024;\n\nexport const readTool: Tool<ReadInput, ReadOutput> = {\n name: 'read',\n category: 'Filesystem',\n description:\n 'Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. ' +\n 'Lines are returned 1-indexed with a ` N| ` prefix for easy reference in edits.',\n usageHint:\n 'FOUNDATIONAL TOOL — call this before almost any edit operation.\\n\\n' +\n 'Best practices:\\n' +\n '- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\\n' +\n '- Use `offset` + `limit` for very large files instead of reading everything at once.\\n' +\n '- Default limit is generous (2000 lines) but can be increased.\\n' +\n '- The output format is designed to be directly usable as context for `edit` operations.',\n permission: 'auto',\n mutating: false,\n capabilities: ['fs.read'],\n icon: 'file',\n maxOutputBytes: 262_144,\n timeoutMs: 5_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Path to the file (relative to project root or absolute within project).',\n },\n offset: {\n type: 'integer',\n description: '1-based starting line number. Use together with `limit` for large files.',\n },\n limit: {\n type: 'integer',\n description: 'Maximum number of lines to return (default is 2000).',\n },\n mode: {\n type: 'string',\n enum: ['content', 'summary'],\n description:\n 'Return full line-numbered content (default) or a compact file summary with imports/exports/symbols.',\n },\n },\n required: ['path'],\n },\n async execute(input, ctx) {\n if (!input?.path) throw new Error('read: path is required');\n const absPath = await safeResolveReal(input.path, ctx);\n\n let stat: Awaited<ReturnType<typeof fs.stat>>;\n try {\n stat = await fs.stat(absPath);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') throw new Error(`read: file not found \"${input.path}\"`);\n throw new Error(`read: failed to stat \"${input.path}\": ${toErrorMessage(err)}`);\n }\n if (!stat.isFile()) throw new Error(`read: \"${input.path}\" is not a regular file`);\n if (stat.size > MAX_BYTES) {\n throw new Error(`read: file too large (${stat.size} bytes, limit ${MAX_BYTES})`);\n }\n\n const offset = Math.max(1, input.offset ?? 1);\n const limit = Math.max(0, Math.min(input.limit ?? 2000, 5000));\n const prior = getReadRangeRecord(ctx, absPath);\n const requestedEnd = prior\n ? Math.min(offset + limit - 1, prior.totalLines)\n : offset + limit - 1;\n if (\n input.mode !== 'summary' &&\n limit > 0 &&\n prior &&\n coversRange(prior, stat.mtimeMs, offset, requestedEnd)\n ) {\n ctx.recordRead(absPath, stat.mtimeMs);\n return {\n text:\n `[unchanged since previous read: \"${input.path}\" mtime=${Math.round(stat.mtimeMs)}; ` +\n `requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,\n total_lines: prior.totalLines,\n encoding: 'utf8',\n truncated: requestedEnd < prior.totalLines,\n cached: true,\n note: 'Repeated read suppressed to save tokens.',\n };\n }\n\n const buf = await fs.readFile(absPath);\n if (isBinaryBuffer(buf)) {\n throw new Error(`read: \"${input.path}\" appears to be binary`);\n }\n\n const text = buf.toString('utf8');\n const allLines = text.split(/\\r\\n|\\r|\\n/);\n const total = allLines.length;\n if (input.mode === 'summary') {\n ctx.recordRead(absPath, stat.mtimeMs);\n rememberReadRange(ctx, absPath, stat.mtimeMs, total, 1, Math.min(total, 200));\n return {\n text: summarizeFile(input.path, stat.size, allLines),\n total_lines: total,\n encoding: 'utf8',\n truncated: total > 200,\n note: 'Summary mode returned compact structure instead of full file content.',\n };\n }\n if (limit === 0) {\n ctx.recordRead(absPath, stat.mtimeMs);\n rememberReadRange(ctx, absPath, stat.mtimeMs, total, 1, 0);\n return { text: '', total_lines: total, encoding: 'utf8', truncated: total > 0 };\n }\n // Offset past EOF: return an explicit message instead of an empty string.\n // Without this, models with weak instruction-following (e.g. k2p7) see an\n // empty result, assume the read failed transiently, and retry the exact\n // same offset indefinitely — a tight tool-use loop that burns iterations\n // and context without making progress.\n if (offset > total) {\n ctx.recordRead(absPath, stat.mtimeMs);\n rememberReadRange(ctx, absPath, stat.mtimeMs, total, total + 1, total + 1);\n return {\n text: `[offset ${offset} is past end of file \"${input.path}\" — file has ${total} line(s). Do not retry this offset.]`,\n total_lines: total,\n encoding: 'utf8',\n truncated: false,\n };\n }\n\n const slice = allLines.slice(offset - 1, offset - 1 + limit);\n const truncated = offset - 1 + slice.length < total;\n\n const width = String(offset + slice.length - 1).length;\n const numbered = slice\n .map((line, i) => `${String(offset + i).padStart(width, ' ')}→${line}`)\n .join('\\n');\n\n ctx.recordRead(absPath, stat.mtimeMs);\n rememberReadRange(ctx, absPath, stat.mtimeMs, total, offset, offset + slice.length - 1);\n\n return {\n text: numbered,\n total_lines: total,\n encoding: 'utf8',\n truncated,\n };\n },\n};\n\ninterface ReadRangeRecord {\n mtimeMs: number;\n totalLines: number;\n ranges: Array<{ start: number; end: number }>;\n}\n\nconst READ_RANGES_META_KEY = 'tools.read.ranges.v1';\n\nfunction getReadRanges(ctx: import('@wrongstack/core').Context): Record<string, ReadRangeRecord> {\n const existing = ctx.meta[READ_RANGES_META_KEY];\n if (existing && typeof existing === 'object' && !Array.isArray(existing)) {\n return existing as Record<string, ReadRangeRecord>;\n }\n const next: Record<string, ReadRangeRecord> = {};\n ctx.meta[READ_RANGES_META_KEY] = next;\n return next;\n}\n\nfunction getReadRangeRecord(\n ctx: import('@wrongstack/core').Context,\n absPath: string,\n): ReadRangeRecord | undefined {\n return getReadRanges(ctx)[absPath];\n}\n\nfunction rememberReadRange(\n ctx: import('@wrongstack/core').Context,\n absPath: string,\n mtimeMs: number,\n totalLines: number,\n start: number,\n end: number,\n): void {\n if (end < start) return;\n const ranges = getReadRanges(ctx);\n const prior = ranges[absPath];\n const nextRanges = prior && Math.abs(prior.mtimeMs - mtimeMs) <= 1 ? prior.ranges.slice() : [];\n nextRanges.push({ start, end });\n ranges[absPath] = {\n mtimeMs,\n totalLines,\n ranges: mergeRanges(nextRanges),\n };\n}\n\nfunction coversRange(\n record: ReadRangeRecord,\n mtimeMs: number,\n start: number,\n end: number,\n): boolean {\n if (Math.abs(record.mtimeMs - mtimeMs) > 1) return false;\n return record.ranges.some((range) => range.start <= start && range.end >= end);\n}\n\nfunction mergeRanges(\n ranges: Array<{ start: number; end: number }>,\n): Array<{ start: number; end: number }> {\n const sorted = ranges.slice().sort((a, b) => a.start - b.start);\n const merged: Array<{ start: number; end: number }> = [];\n for (const range of sorted) {\n const last = merged[merged.length - 1];\n if (!last || range.start > last.end + 1) {\n merged.push({ ...range });\n continue;\n }\n last.end = Math.max(last.end, range.end);\n }\n return merged;\n}\n\nfunction summarizeFile(filePath: string, bytes: number, lines: string[]): string {\n const interesting = lines\n .map((line, index) => ({ line: line.trim(), number: index + 1 }))\n .filter(({ line }) =>\n /^(import\\s|export\\s|class\\s|interface\\s|type\\s|function\\s|const\\s+\\w+\\s*=|let\\s+\\w+\\s*=|var\\s+\\w+\\s*=|def\\s+|async\\s+function\\s)/.test(\n line,\n ),\n )\n .slice(0, 80)\n .map(({ line, number }) => `${number}: ${line}`);\n return [\n `summary: ${filePath}`,\n `bytes=${bytes}`,\n `total_lines=${lines.length}`,\n interesting.length > 0\n ? `symbols/imports:\\n${interesting.join('\\n')}`\n : 'symbols/imports: (none detected)',\n ].join('\\n');\n}\n"]}
|
package/dist/search.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fetch.ts","../src/search.ts"],"names":[],"mappings":"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","file":"search.js","sourcesContent":["import * as dns from 'node:dns/promises';\nimport * as net from 'node:net';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { isPrivateIPv4, isPrivateIPv6 } from '@wrongstack/core';\nimport { Agent } from 'undici';\nimport TurndownService from 'turndown';\nimport { truncateMiddle } from './_util.js';\n\n/**\n * Singleton Turndown instance for HTML→Markdown conversion.\n * Pre-configured with sensible defaults; code blocks are handled via the\n * default fenced code rule. Reused across all fetch calls.\n */\nconst TD = new TurndownService({\n // Use `# Title` for headings, not setext underline style (`Title\\n=====`).\n headingStyle: 'atx',\n // Don't wrap code blocks in <pre> — render them as triple-backtick blocks.\n codeBlockStyle: 'fenced',\n});\n\n// Strip <script>/<style>/<noscript> before turndown sees them. The old\n// hand-rolled converter did this via regex; turndown's DOM-based approach\n// may keep their text content unless we remove the elements first.\n// Using turndown's own addRule mechanism keeps the logic co-located.\nTD.addRule('stripDangerousElements', {\n filter: ['script', 'style', 'noscript'],\n replacement: () => '',\n});\n\ninterface FetchInput {\n url: string;\n format?: 'markdown' | 'text' | 'raw' | undefined;\n}\n\ninterface FetchOutput {\n content: string;\n status: number;\n content_type: string;\n url: string;\n}\n\nconst MAX_BYTES = 131_072;\nconst TIMEOUT_MS = 20_000;\n\nconst 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 —\\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\n/** Abort when any of the signals abort (Node 22+ — AbortSignal.any shipped in Node 20). */\nconst combineSignals = (signals: AbortSignal[]): AbortSignal => AbortSignal.any(signals);\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 — 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// Clean up the global dispatcher on exit — 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 Error(`fetch: redirect to unsupported protocol \"${parsed.protocol}\"`);\n }\n if (parsed.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new Error('fetch: redirect to http:// blocked (HTTPS required by default)');\n }\n await assertNotPrivate(parsed.hostname);\n\n // The dispatcher pins the connection to the IP guardedLookup validated —\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 — 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 fetch(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 Error(`fetch: exceeded ${maxRedirects} redirects`);\n }\n const location = res.headers.get('location');\n if (!location) {\n throw new Error('fetch: redirect status with no location header');\n }\n currentUrl = new URL(location, currentUrl).toString();\n }\n}\n\nexport const fetchTool: Tool<FetchInput, FetchOutput> = {\n name: 'fetch',\n category: 'Network',\n description:\n 'Fetch a URL and return its content. HTML pages are automatically converted to clean markdown. ' +\n 'This tool has strong SSRF protections (private IPs, localhost, and cloud metadata endpoints are blocked by default).',\n usageHint:\n 'Use this when you need external information (documentation, API responses, web pages, etc.).\\n\\n' +\n 'Security notes:\\n' +\n '- Only HTTPS is allowed by default.\\n' +\n '- Internal/private networks are blocked unless explicitly enabled via environment variable.\\n' +\n '- Redirects are followed but re-validated at each hop.\\n' +\n '- Output is capped (128KB by default) to avoid flooding context.\\n' +\n 'Prefer this over raw `bash curl` or `bash wget`.',\n permission: 'confirm',\n mutating: false,\n capabilities: ['net.outbound'],\n icon: 'web',\n // Trust rules for fetch match on the literal URL — declare it explicitly\n // so a user can trust `https://api.example.com/*` without accidentally\n // matching that pattern on any other tool that happens to have a `url`\n // input field.\n subjectKey: 'url',\n timeoutMs: TIMEOUT_MS,\n maxOutputBytes: MAX_BYTES,\n inputSchema: {\n type: 'object',\n properties: {\n url: {\n type: 'string',\n description: 'The target URL (must use https://).',\n },\n format: {\n type: 'string',\n enum: ['markdown', 'text', 'raw'],\n description: 'Output format. \"markdown\" is recommended for HTML pages.',\n },\n },\n required: ['url'],\n },\n async execute(input, ctx, opts) {\n let final: FetchOutput | undefined;\n const executeStream = fetchTool.executeStream;\n if (!executeStream) throw new Error('fetchTool: 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('fetch: stream ended without final event');\n return final;\n },\n async *executeStream(input, _ctx, opts): AsyncGenerator<ToolStreamEvent<FetchOutput>> {\n if (!input?.url) throw new Error('fetch: url is required');\n const u = new URL(input.url);\n if (u.protocol !== 'https:' && u.protocol !== 'http:') {\n throw new Error(`fetch: unsupported protocol \"${u.protocol}\"`);\n }\n if (u.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new Error('fetch: http:// blocked (HTTPS required by default)');\n }\n await assertNotPrivate(u.hostname);\n\n yield { type: 'log', text: `GET ${input.url}` };\n\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(new Error('fetch timeout')), TIMEOUT_MS);\n const combined = combineSignals([opts.signal, ctrl.signal]);\n\n try {\n const res = await guardedFetch(input.url, 5, combined);\n\n const ct = res.headers.get('content-type') ?? 'application/octet-stream';\n if (/^image\\/|^audio\\/|^video\\/|application\\/octet-stream/.test(ct)) {\n throw new Error(`fetch: refusing to read binary content-type \"${ct}\"`);\n }\n\n yield {\n type: 'log',\n text: `HTTP ${res.status} ${ct}`,\n data: { status: res.status, contentType: ct },\n };\n\n const reader = res.body?.getReader();\n let received = 0;\n const chunks: Uint8Array[] = [];\n let pendingBytes = 0;\n const FLUSH_AT = 4 * 1024;\n if (reader) {\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n if (!value) continue;\n received += value.byteLength;\n pendingBytes += value.byteLength;\n chunks.push(value);\n if (pendingBytes >= FLUSH_AT) {\n // Snapshot recent bytes for the partial_output. Keep it cheap —\n // don't try to decode UTF-8 boundaries; the TUI just needs a\n // \"things are happening\" signal.\n const recent = Buffer.from(value).toString('utf-8');\n yield {\n type: 'partial_output',\n text: recent,\n data: { received },\n };\n pendingBytes = 0;\n }\n if (received > MAX_BYTES) break;\n }\n }\n const text = Buffer.concat(chunks.map((c) => Buffer.from(c))).toString('utf8');\n\n const format = input.format ?? (ct.includes('text/html') ? 'markdown' : 'text');\n let content: string;\n if (format === 'raw') content = text;\n else if (format === 'markdown' && ct.includes('text/html')) content = TD.turndown(text);\n else if (ct.includes('application/json')) content = prettyJson(text);\n else content = text;\n\n yield {\n type: 'final',\n output: {\n content: truncateMiddle(content, MAX_BYTES),\n status: res.status,\n content_type: ct,\n url: res.url,\n },\n };\n } finally {\n clearTimeout(timer);\n }\n },\n};\n\nasync 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 Error('fetch: blocked localhost target');\n }\n\n const ipVersion = net.isIP(host);\n if (ipVersion === 4) {\n if (isPrivateIPv4(host)) {\n throw new Error(`fetch: blocked private/loopback address \"${host}\"`);\n }\n } else if (ipVersion === 6) {\n if (isPrivateIPv6(host)) {\n throw new Error(`fetch: blocked private/loopback address \"${host}\"`);\n }\n } else {\n // Hostname — 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 below).\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 Error(`fetch: resolved to private address ${r.address}`);\n }\n }\n } catch (err) {\n if (err instanceof Error && err.message.startsWith('fetch:')) throw err;\n // DNS failure — let fetch handle it\n }\n }\n}\n\nfunction prettyJson(s: string): string {\n try {\n return JSON.stringify(JSON.parse(s), null, 2);\n } catch {\n return s;\n }\n}\n\n","import { expectDefined } from '@wrongstack/core';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { guardedFetch } from './fetch.js';\nimport { toErrorMessage } from '@wrongstack/core/utils';\ninterface SearchInput {\n query: string;\n num_results?: number | undefined;\n source?: 'duckduckgo' | 'google' | 'bing' | undefined;\n}\n\ninterface SearchOutput {\n query: string;\n results: { title: string; url: string; snippet: string }[];\n source: string;\n truncated: boolean;\n}\n\nconst DEFAULT_NUM = 10;\nconst MAX_RESULTS = 50;\nconst TIMEOUT_MS = 15_000;\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.',\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 '- This is often better than the model trying to recall outdated knowledge.',\n permission: 'confirm',\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 },\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) throw new Error('search: query is required');\n\n const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));\n const source = input.source ?? 'duckduckgo';\n\n yield {\n type: 'log',\n text: `Querying ${source} for \"${input.query}\"…`,\n data: { source, query: input.query },\n };\n\n let output: SearchOutput;\n switch (source) {\n case 'duckduckgo':\n output = await duckduckgoSearch(input.query, num, opts.signal);\n break;\n case 'google':\n output = await googleSearch(input.query, num, opts.signal);\n break;\n case 'bing':\n output = await bingSearch(input.query, num, opts.signal);\n break;\n default:\n throw new Error(`search: unknown source \"${source}\"`);\n }\n\n yield {\n type: 'partial_output',\n text: `${output.results.length} results from ${output.source}`,\n data: { count: output.results.length },\n };\n yield { type: 'final', output };\n },\n};\n\nasync function duckduckgoSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchOutput> {\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 const results = parseDuckDuckGo(html, num);\n return {\n query,\n results,\n source: 'duckduckgo',\n truncated: results.length >= num,\n };\n } catch (err) {\n console.log(JSON.stringify({ level: 'debug', event: 'search_failed', query, error: toErrorMessage(err) }));\n return {\n query,\n results: [{ title: 'Search unavailable', url: '', snippet: 'Could not reach DuckDuckGo' }],\n source: 'duckduckgo',\n truncated: false,\n };\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): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\n const snippetRegex = /<a class=\"result-link\"[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\/a>/gi;\n const snippet2Regex = /<a class=\"result-snippet\"[^>]*>([^<]+)<\\/a>/gi;\n\n const linkMatches = takeFrom(\n [...html.matchAll(snippetRegex)]\n .filter((m) => m[1] && m[2])\n .map((m) => ({ url: expectDefined(m[1]), title: stripTags(expectDefined(m[2])) })),\n num,\n );\n\n const snippetMatches = takeFrom(\n [...html.matchAll(snippet2Regex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n for (let i = 0; i < linkMatches.length && i < num; i++) {\n const entry = linkMatches[i];\n results.push({\n title: entry?.title ?? '',\n url: entry?.url ?? '',\n snippet: snippetMatches[i] ?? '',\n });\n }\n\n return results;\n}\n\nasync function googleSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchOutput> {\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 const results = parseGoogleResults(html, num);\n\n return {\n query,\n results,\n source: 'google',\n truncated: results.length >= num,\n };\n}\n\nfunction parseGoogleResults(html: string, num: number): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\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 });\n }\n\n return results;\n}\n\nasync function bingSearch(query: string, num: number, signal: AbortSignal): Promise<SearchOutput> {\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 const results = parseBingResults(html, num);\n\n return {\n query,\n results,\n source: 'bing',\n truncated: results.length >= num,\n };\n}\n\nfunction parseBingResults(html: string, num: number): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\n const titleRegex = /<h2[^>]*>\\s*<a[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\/a>\\s*<\\/h2>/gi;\n const snippetRegex = /<p[^>]*class=\"[^\"]*b_paractl[^\"]*\"[^>]*>([^<]+)<\\/p>/gi;\n\n const entries = takeFrom(\n [...html.matchAll(titleRegex)]\n .filter((m) => m[1] && m[2])\n .map((m) => ({ url: expectDefined(m[1]), title: stripTags(expectDefined(m[2])) })),\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 < entries.length; i++) {\n results.push({\n title: entries[i]?.title ?? '',\n url: entries[i]?.url ?? '',\n snippet: snippets[i] ?? '',\n });\n }\n\n return results;\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 → 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 throw e;\n }\n}\n\nfunction anySignal(...signals: AbortSignal[]): AbortSignal {\n // Native combinator (Node ≥ 20.3; this repo requires ≥ 22). The previous\n // hand-rolled version registered a non-once 'abort' listener on every\n // input signal and never removed it — 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 html\n .replace(/<[^>]+>/g, '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .trim();\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/fetch.ts","../src/search.ts"],"names":[],"mappings":"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","file":"search.js","sourcesContent":["import * as dns from 'node:dns/promises';\nimport * as net from 'node:net';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { isPrivateIPv4, isPrivateIPv6 } from '@wrongstack/core';\nimport { Agent } from 'undici';\nimport TurndownService from 'turndown';\nimport { truncateMiddle } from './_util.js';\n\n/**\n * Singleton Turndown instance for HTML→Markdown conversion.\n * Pre-configured with sensible defaults; code blocks are handled via the\n * default fenced code rule. Reused across all fetch calls.\n */\nconst TD = new TurndownService({\n // Use `# Title` for headings, not setext underline style (`Title\\n=====`).\n headingStyle: 'atx',\n // Don't wrap code blocks in <pre> — render them as triple-backtick blocks.\n codeBlockStyle: 'fenced',\n});\n\n// Strip <script>/<style>/<noscript> before turndown sees them. The old\n// hand-rolled converter did this via regex; turndown's DOM-based approach\n// may keep their text content unless we remove the elements first.\n// Using turndown's own addRule mechanism keeps the logic co-located.\nTD.addRule('stripDangerousElements', {\n filter: ['script', 'style', 'noscript'],\n replacement: () => '',\n});\n\ninterface FetchInput {\n url: string;\n format?: 'markdown' | 'text' | 'raw' | undefined;\n}\n\ninterface FetchOutput {\n content: string;\n status: number;\n content_type: string;\n url: string;\n}\n\nconst MAX_BYTES = 131_072;\nconst TIMEOUT_MS = 20_000;\n\nconst 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 —\\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\n/** Abort when any of the signals abort (Node 22+ — AbortSignal.any shipped in Node 20). */\nconst combineSignals = (signals: AbortSignal[]): AbortSignal => AbortSignal.any(signals);\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 — 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// Clean up the global dispatcher on exit — 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 Error(`fetch: redirect to unsupported protocol \"${parsed.protocol}\"`);\n }\n if (parsed.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new Error('fetch: redirect to http:// blocked (HTTPS required by default)');\n }\n await assertNotPrivate(parsed.hostname);\n\n // The dispatcher pins the connection to the IP guardedLookup validated —\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 — 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 fetch(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 Error(`fetch: exceeded ${maxRedirects} redirects`);\n }\n const location = res.headers.get('location');\n if (!location) {\n throw new Error('fetch: redirect status with no location header');\n }\n currentUrl = new URL(location, currentUrl).toString();\n }\n}\n\nexport const fetchTool: Tool<FetchInput, FetchOutput> = {\n name: 'fetch',\n category: 'Network',\n description:\n 'Fetch a URL and return its content. HTML pages are automatically converted to clean markdown. ' +\n 'This tool has strong SSRF protections (private IPs, localhost, and cloud metadata endpoints are blocked by default).',\n usageHint:\n 'Use this when you need external information (documentation, API responses, web pages, etc.).\\n\\n' +\n 'Security notes:\\n' +\n '- Only HTTPS is allowed by default.\\n' +\n '- Internal/private networks are blocked unless explicitly enabled via environment variable.\\n' +\n '- Redirects are followed but re-validated at each hop.\\n' +\n '- Output is capped (128KB by default) to avoid flooding context.\\n' +\n 'Prefer this over raw `bash curl` or `bash wget`.',\n permission: 'confirm',\n mutating: false,\n capabilities: ['net.outbound'],\n icon: 'web',\n // Trust rules for fetch match on the literal URL — declare it explicitly\n // so a user can trust `https://api.example.com/*` without accidentally\n // matching that pattern on any other tool that happens to have a `url`\n // input field.\n subjectKey: 'url',\n timeoutMs: TIMEOUT_MS,\n maxOutputBytes: MAX_BYTES,\n inputSchema: {\n type: 'object',\n properties: {\n url: {\n type: 'string',\n description: 'The target URL (must use https://).',\n },\n format: {\n type: 'string',\n enum: ['markdown', 'text', 'raw'],\n description: 'Output format. \"markdown\" is recommended for HTML pages.',\n },\n },\n required: ['url'],\n },\n async execute(input, ctx, opts) {\n let final: FetchOutput | undefined;\n const executeStream = fetchTool.executeStream;\n if (!executeStream) throw new Error('fetchTool: 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('fetch: stream ended without final event');\n return final;\n },\n async *executeStream(input, _ctx, opts): AsyncGenerator<ToolStreamEvent<FetchOutput>> {\n if (!input?.url) throw new Error('fetch: url is required');\n const u = new URL(input.url);\n if (u.protocol !== 'https:' && u.protocol !== 'http:') {\n throw new Error(`fetch: unsupported protocol \"${u.protocol}\"`);\n }\n if (u.protocol === 'http:' && !ALLOW_PRIVATE) {\n throw new Error('fetch: http:// blocked (HTTPS required by default)');\n }\n await assertNotPrivate(u.hostname);\n\n yield { type: 'log', text: `GET ${input.url}` };\n\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(new Error('fetch timeout')), TIMEOUT_MS);\n const combined = combineSignals([opts.signal, ctrl.signal]);\n\n try {\n let res: Response;\n try {\n res = await guardedFetch(input.url, 5, combined);\n } catch (err) {\n // A user-initiated cancel propagates unchanged. Our own timeout and any\n // transport failure get a diagnostic message: undici throws an opaque\n // `TypeError: fetch failed` whose real reason (ENOTFOUND, ECONNREFUSED,\n // UND_ERR_CONNECT_TIMEOUT, a TLS/cert error, …) lives only on `.cause`.\n // Surfacing just `.message` left users with \"fetch failed\" and no clue\n // why HTTPS broke (see #100), so unwrap the cause chain here.\n if (opts.signal.aborted) throw err;\n throw describeFetchError(err, input.url, ctrl.signal.aborted);\n }\n\n const ct = res.headers.get('content-type') ?? 'application/octet-stream';\n if (/^image\\/|^audio\\/|^video\\/|application\\/octet-stream/.test(ct)) {\n throw new Error(`fetch: refusing to read binary content-type \"${ct}\"`);\n }\n\n yield {\n type: 'log',\n text: `HTTP ${res.status} ${ct}`,\n data: { status: res.status, contentType: ct },\n };\n\n const reader = res.body?.getReader();\n let received = 0;\n const chunks: Uint8Array[] = [];\n let pendingBytes = 0;\n const FLUSH_AT = 4 * 1024;\n if (reader) {\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n if (!value) continue;\n received += value.byteLength;\n pendingBytes += value.byteLength;\n chunks.push(value);\n if (pendingBytes >= FLUSH_AT) {\n // Snapshot recent bytes for the partial_output. Keep it cheap —\n // don't try to decode UTF-8 boundaries; the TUI just needs a\n // \"things are happening\" signal.\n const recent = Buffer.from(value).toString('utf-8');\n yield {\n type: 'partial_output',\n text: recent,\n data: { received },\n };\n pendingBytes = 0;\n }\n if (received > MAX_BYTES) break;\n }\n }\n const text = Buffer.concat(chunks.map((c) => Buffer.from(c))).toString('utf8');\n\n const format = input.format ?? (ct.includes('text/html') ? 'markdown' : 'text');\n let content: string;\n if (format === 'raw') content = text;\n else if (format === 'markdown' && ct.includes('text/html')) content = TD.turndown(text);\n else if (ct.includes('application/json')) content = prettyJson(text);\n else content = text;\n\n yield {\n type: 'final',\n output: {\n content: truncateMiddle(content, MAX_BYTES),\n status: res.status,\n content_type: ct,\n url: res.url,\n },\n };\n } finally {\n clearTimeout(timer);\n }\n },\n};\n\nasync 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 Error('fetch: blocked localhost target');\n }\n\n const ipVersion = net.isIP(host);\n if (ipVersion === 4) {\n if (isPrivateIPv4(host)) {\n throw new Error(`fetch: blocked private/loopback address \"${host}\"`);\n }\n } else if (ipVersion === 6) {\n if (isPrivateIPv6(host)) {\n throw new Error(`fetch: blocked private/loopback address \"${host}\"`);\n }\n } else {\n // Hostname — 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 below).\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 Error(`fetch: resolved to private address ${r.address}`);\n }\n }\n } catch (err) {\n if (err instanceof Error && err.message.startsWith('fetch:')) throw err;\n // DNS failure — let fetch handle it\n }\n }\n}\n\n/**\n * Turn an opaque undici `TypeError: fetch failed` into an actionable message by\n * walking its `.cause` chain. undici buries the transport reason (a DNS/socket\n * errno or a TLS handshake failure) one or more `.cause` hops down, so the bare\n * `.message` is always just \"fetch failed\". We join each distinct\n * `code: message` link so the user sees, e.g.,\n * `fetch: GET https://x failed — UND_ERR_CONNECT_TIMEOUT: Connect Timeout Error`.\n */\nfunction describeFetchError(err: unknown, url: string, timedOut: boolean): Error {\n if (timedOut) {\n return new Error(`fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`);\n }\n const parts: string[] = [];\n const seen = new Set<unknown>();\n let cur: unknown = err;\n while (cur instanceof Error && !seen.has(cur)) {\n seen.add(cur);\n const code = (cur as NodeJS.ErrnoException).code;\n const label = code ? `${code}: ${cur.message}` : cur.message;\n // Skip undici's uninformative top-level \"fetch failed\" wrapper, but keep it\n // as a fallback if it turns out to be the only thing we have.\n if (label && label !== 'fetch failed' && !parts.includes(label)) parts.push(label);\n cur = (cur as { cause?: unknown }).cause;\n }\n const detail = parts.length > 0 ? parts.join(' → ') : 'fetch failed';\n return new Error(`fetch: GET ${url} failed — ${detail}`);\n}\n\nfunction prettyJson(s: string): string {\n try {\n return JSON.stringify(JSON.parse(s), null, 2);\n } catch {\n return s;\n }\n}\n\n","import { expectDefined } from '@wrongstack/core';\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\nimport { guardedFetch } from './fetch.js';\nimport { toErrorMessage } from '@wrongstack/core/utils';\ninterface SearchInput {\n query: string;\n num_results?: number | undefined;\n source?: 'duckduckgo' | 'google' | 'bing' | undefined;\n}\n\ninterface SearchOutput {\n query: string;\n results: { title: string; url: string; snippet: string }[];\n source: string;\n truncated: boolean;\n}\n\nconst DEFAULT_NUM = 10;\nconst MAX_RESULTS = 50;\nconst TIMEOUT_MS = 15_000;\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.',\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 '- This is often better than the model trying to recall outdated knowledge.',\n permission: 'confirm',\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 },\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) throw new Error('search: query is required');\n\n const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));\n const source = input.source ?? 'duckduckgo';\n\n yield {\n type: 'log',\n text: `Querying ${source} for \"${input.query}\"…`,\n data: { source, query: input.query },\n };\n\n let output: SearchOutput;\n switch (source) {\n case 'duckduckgo':\n output = await duckduckgoSearch(input.query, num, opts.signal);\n break;\n case 'google':\n output = await googleSearch(input.query, num, opts.signal);\n break;\n case 'bing':\n output = await bingSearch(input.query, num, opts.signal);\n break;\n default:\n throw new Error(`search: unknown source \"${source}\"`);\n }\n\n yield {\n type: 'partial_output',\n text: `${output.results.length} results from ${output.source}`,\n data: { count: output.results.length },\n };\n yield { type: 'final', output };\n },\n};\n\nasync function duckduckgoSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchOutput> {\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 const results = parseDuckDuckGo(html, num);\n return {\n query,\n results,\n source: 'duckduckgo',\n truncated: results.length >= num,\n };\n } catch (err) {\n console.log(JSON.stringify({ level: 'debug', event: 'search_failed', query, error: toErrorMessage(err) }));\n return {\n query,\n results: [{ title: 'Search unavailable', url: '', snippet: 'Could not reach DuckDuckGo' }],\n source: 'duckduckgo',\n truncated: false,\n };\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): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\n const snippetRegex = /<a class=\"result-link\"[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\/a>/gi;\n const snippet2Regex = /<a class=\"result-snippet\"[^>]*>([^<]+)<\\/a>/gi;\n\n const linkMatches = takeFrom(\n [...html.matchAll(snippetRegex)]\n .filter((m) => m[1] && m[2])\n .map((m) => ({ url: expectDefined(m[1]), title: stripTags(expectDefined(m[2])) })),\n num,\n );\n\n const snippetMatches = takeFrom(\n [...html.matchAll(snippet2Regex)].filter((m) => m[1]).map((m) => stripTags(expectDefined(m[1]))),\n num,\n );\n\n for (let i = 0; i < linkMatches.length && i < num; i++) {\n const entry = linkMatches[i];\n results.push({\n title: entry?.title ?? '',\n url: entry?.url ?? '',\n snippet: snippetMatches[i] ?? '',\n });\n }\n\n return results;\n}\n\nasync function googleSearch(\n query: string,\n num: number,\n signal: AbortSignal,\n): Promise<SearchOutput> {\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 const results = parseGoogleResults(html, num);\n\n return {\n query,\n results,\n source: 'google',\n truncated: results.length >= num,\n };\n}\n\nfunction parseGoogleResults(html: string, num: number): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\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 });\n }\n\n return results;\n}\n\nasync function bingSearch(query: string, num: number, signal: AbortSignal): Promise<SearchOutput> {\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 const results = parseBingResults(html, num);\n\n return {\n query,\n results,\n source: 'bing',\n truncated: results.length >= num,\n };\n}\n\nfunction parseBingResults(html: string, num: number): SearchOutput['results'] {\n const results: SearchOutput['results'] = [];\n const titleRegex = /<h2[^>]*>\\s*<a[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\/a>\\s*<\\/h2>/gi;\n const snippetRegex = /<p[^>]*class=\"[^\"]*b_paractl[^\"]*\"[^>]*>([^<]+)<\\/p>/gi;\n\n const entries = takeFrom(\n [...html.matchAll(titleRegex)]\n .filter((m) => m[1] && m[2])\n .map((m) => ({ url: expectDefined(m[1]), title: stripTags(expectDefined(m[2])) })),\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 < entries.length; i++) {\n results.push({\n title: entries[i]?.title ?? '',\n url: entries[i]?.url ?? '',\n snippet: snippets[i] ?? '',\n });\n }\n\n return results;\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 → 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 throw e;\n }\n}\n\nfunction anySignal(...signals: AbortSignal[]): AbortSignal {\n // Native combinator (Node ≥ 20.3; this repo requires ≥ 22). The previous\n // hand-rolled version registered a non-once 'abort' listener on every\n // input signal and never removed it — 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 html\n .replace(/<[^>]+>/g, '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .trim();\n}\n"]}
|