thincoder 0.10.0 → 0.11.1

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.
Files changed (64) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/advisor.mjs +360 -72
  4. package/src/agent/helpers.mjs +7 -3
  5. package/src/agent/setup.mjs +2 -2
  6. package/src/agent-tools/advisor.mjs +36 -0
  7. package/src/agent-tools/plan.mjs +53 -2
  8. package/src/agent-tools/subagent.mjs +7 -1
  9. package/src/agent-tools/timer.mjs +1 -1
  10. package/src/agent-tools/verify.mjs +1 -0
  11. package/src/agent-tools.mjs +1 -0
  12. package/src/agent.mjs +73 -21
  13. package/src/auto-think.mjs +23 -5
  14. package/src/cli/make-agent.mjs +7 -0
  15. package/src/config.mjs +47 -20
  16. package/src/prompts/advisor-round1.md +23 -0
  17. package/src/prompts/advisor-round2.md +26 -0
  18. package/src/prompts/advisor-round3.md +24 -0
  19. package/src/prompts/coder.md +6 -2
  20. package/src/prompts/discipline.md +23 -6
  21. package/src/prompts/explore.md +2 -0
  22. package/src/prompts/plan.md +2 -0
  23. package/src/prompts/system.md +13 -6
  24. package/src/provider/anthropic.mjs +190 -0
  25. package/src/provider/core.mjs +42 -130
  26. package/src/provider/google.mjs +199 -0
  27. package/src/provider/sse.mjs +112 -0
  28. package/src/proxy.mjs +236 -0
  29. package/src/tools/bash.md +8 -0
  30. package/src/tools/codemode.mjs +5 -16
  31. package/src/tools/edit.md +8 -0
  32. package/src/tools/fetch.md +2 -1
  33. package/src/tools/git.mjs +125 -156
  34. package/src/tools/index.mjs +9 -9
  35. package/src/tools/linter.mjs +46 -32
  36. package/src/tools/read.md +7 -0
  37. package/src/tools/shared.mjs +16 -0
  38. package/src/tools/system.mjs +14 -10
  39. package/src/tools/web.mjs +115 -89
  40. package/src/tools/websearch.md +5 -3
  41. package/src/tui/agent-turn.mjs +86 -75
  42. package/src/tui/cmd-advisor.mjs +138 -49
  43. package/src/tui/cmd-clear.mjs +11 -17
  44. package/src/tui/cmd-config.mjs +226 -142
  45. package/src/tui/cmd-extract.mjs +1 -1
  46. package/src/tui/cmd-fold.mjs +2 -3
  47. package/src/tui/cmd-goal.mjs +58 -27
  48. package/src/tui/cmd-help.mjs +3 -1
  49. package/src/tui/cmd-mcp.mjs +178 -142
  50. package/src/tui/cmd-model.mjs +15 -4
  51. package/src/tui/cmd-new.mjs +7 -13
  52. package/src/tui/cmd-restore.mjs +12 -16
  53. package/src/tui/cmd-session.mjs +28 -32
  54. package/src/tui/cmd-think.mjs +75 -50
  55. package/src/tui/cmd-undo.mjs +19 -23
  56. package/src/tui/cmd-upgrade.mjs +22 -26
  57. package/src/tui/index.mjs +61 -215
  58. package/src/tui/key-handler.mjs +56 -20
  59. package/src/tui/layout.mjs +13 -3
  60. package/src/tui/pickers.mjs +151 -182
  61. package/src/tui/render-conversation.mjs +92 -0
  62. package/src/tui/render-frame.mjs +56 -114
  63. package/src/tui/render-loop.mjs +181 -0
  64. package/src/tui/slash-commands.mjs +26 -16
@@ -0,0 +1,112 @@
1
+ /**
2
+ * provider/sse.mjs — SSE stream reader
3
+ * Extracted from core.mjs. Parses Server-Sent Events for LLM chat responses.
4
+ */
5
+ export async function readSSE(response, { onToken, onReasoning, rules, signal, firedPatterns: sharedFired }) {
6
+ const result = { content: "", reasoning: "", toolCalls: [], usage: null, finishReason: null }
7
+ const decoder = new TextDecoder()
8
+ let buffer = ""
9
+ let hasChoices = false
10
+ const firedPatterns = sharedFired ?? new Set()
11
+
12
+ const processLines = (lines) => {
13
+ for (const line of lines) {
14
+ if (!line.startsWith("data:")) continue
15
+ const data = line.slice(5).trim()
16
+ if (!data || data === "[DONE]") continue
17
+
18
+ let json
19
+ try { json = JSON.parse(data) } catch { continue }
20
+
21
+ if (json.usage) result.usage = json.usage
22
+ const choice = json.choices?.[0]
23
+ if (!choice) continue
24
+ hasChoices = true
25
+ if (choice.finish_reason) result.finishReason = choice.finish_reason
26
+
27
+ const delta = choice.delta ?? {}
28
+ if (delta.reasoning_content) {
29
+ result.reasoning += delta.reasoning_content
30
+ onReasoning?.(delta.reasoning_content)
31
+ }
32
+ if (delta.content) {
33
+ result.content += delta.content
34
+ onToken?.(delta.content)
35
+ }
36
+ for (const tc of delta.tool_calls ?? []) {
37
+ const slot = (result.toolCalls[tc.index] ??= { id: "", name: "", arguments: "" })
38
+ if (tc.id) slot.id = tc.id
39
+ if (tc.function?.name && !slot.name) slot.name = tc.function.name
40
+ if (tc.function?.arguments) slot.arguments += tc.function.arguments
41
+ }
42
+ }
43
+ }
44
+
45
+ if (!response.body) throw new Error("No stream response body")
46
+ try {
47
+ for await (const chunk of response.body) {
48
+ if (signal?.aborted) {
49
+ const e = new DOMException("The operation was aborted", "AbortError")
50
+ e.reason = signal.reason
51
+ throw e
52
+ }
53
+ buffer += decoder.decode(chunk, { stream: true })
54
+ const lines = buffer.split("\n")
55
+ buffer = lines.pop()
56
+ processLines(lines)
57
+
58
+ if (rules?.length && result.content && !result.toolCalls.length) {
59
+ for (const rule of rules) {
60
+ if (rule.repeat === "once" && firedPatterns.has(rule.pattern)) continue
61
+ if (rule._regex.test(result.content)) {
62
+ if (rule.repeat === "once") firedPatterns.add(rule.pattern)
63
+ if (rule.action === "abort") {
64
+ result.ruleTriggered = true
65
+ result.ruleMessage = rule.message
66
+ result.ruleName = rule.name
67
+ return result
68
+ }
69
+ const existing = result._warnings ??= []
70
+ if (!existing.some(w => w.pattern === rule.pattern)) {
71
+ existing.push({ name: rule.name, pattern: rule.pattern, message: rule.message })
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+ buffer += decoder.decode()
78
+ processLines(buffer.split("\n"))
79
+ } catch (e) {
80
+ if (e.name === "AbortError" && signal?.reason?.interrupt) {
81
+ result.interrupted = true
82
+ result.interruptMessage = signal.reason.message
83
+ return result
84
+ }
85
+ throw e
86
+ }
87
+
88
+ if (!hasChoices) {
89
+ const contentType = response.headers.get("content-type") || ""
90
+ let errorMsg = ""
91
+ try {
92
+ const raw = buffer.trim() || ""
93
+ if (raw) {
94
+ const parsed = JSON.parse(raw)
95
+ errorMsg = parsed?.error?.message
96
+ || parsed?.base_resp?.status_msg
97
+ || parsed?.detail
98
+ || parsed?.message
99
+ || parsed?.msg
100
+ || (typeof parsed.error === "string" ? parsed.error : "")
101
+ }
102
+ } catch { /* not JSON */ }
103
+ if (!errorMsg && !contentType.includes("event-stream")) {
104
+ errorMsg = `Response is not SSE (Content-Type: ${contentType || "unknown"})`
105
+ }
106
+ if (errorMsg) {
107
+ throw new Error(`API error: ${errorMsg}`)
108
+ }
109
+ }
110
+
111
+ return result
112
+ }
package/src/proxy.mjs ADDED
@@ -0,0 +1,236 @@
1
+ /**
2
+ * proxy.mjs — Shared proxy tunnel for websearch, fetch, and provider calls.
3
+ * Zero dependencies: Node built-ins only (net, tls, http, url).
4
+ */
5
+ import { connect } from "node:net";
6
+ import { connect as tlsConnect } from "node:tls";
7
+ import { PassThrough } from "node:stream";
8
+ import { URL } from "node:url";
9
+
10
+ const FETCH_TIMEOUT = 15_000
11
+
12
+ /**
13
+ * Resolve proxy URI.
14
+ * New format: { proxy: { uri: "http://host:port", web: true, model: false } }
15
+ * Old format: { proxy: "http://host:port" } — backwards compatible, web=true model=false
16
+ * Env vars: HTTPS_PROXY, HTTP_PROXY, ALL_PROXY
17
+ *
18
+ * @returns {{ uri: string|null, web: boolean, model: boolean }}
19
+ */
20
+ export function resolveProxyConfig(ctx) {
21
+ const cfgProxy = ctx?.agent?.config?.proxy
22
+ if (!cfgProxy) {
23
+ const uri = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || process.env.ALL_PROXY || null
24
+ return { uri, web: !!uri, model: false }
25
+ }
26
+ if (typeof cfgProxy === "string") {
27
+ // Backward compat: bare string → web only
28
+ return { uri: cfgProxy, web: true, model: false }
29
+ }
30
+ return {
31
+ uri: cfgProxy.uri || cfgProxy.url || null,
32
+ web: cfgProxy.web !== false,
33
+ model: cfgProxy.model === true,
34
+ }
35
+ }
36
+
37
+ /** Convenience: resolve proxy URI for web tools (websearch/fetch) */
38
+ export function resolveWebProxy(ctx) {
39
+ const cfg = resolveProxyConfig(ctx)
40
+ return (cfg.uri && cfg.web) ? cfg.uri : null
41
+ }
42
+
43
+ /**
44
+ * Inject the resolved proxy URI into each provider as provider.proxyUri
45
+ * (consumed by chat() in provider/core.mjs).
46
+ * Model 请求走代理需要双重开启:per-provider `proxy: true` 且全局 `config.proxy.model === true`(默认关)。
47
+ */
48
+ export function injectProxy(providers, config) {
49
+ const { uri, model } = resolveProxyConfig({ agent: { config } })
50
+ for (const p of providers ?? []) {
51
+ p.proxyUri = p.proxy && model ? (uri ?? undefined) : undefined
52
+ }
53
+ }
54
+
55
+ function abortError(signal) {
56
+ const e = new DOMException("The operation was aborted", "AbortError")
57
+ e.reason = signal?.reason
58
+ return e
59
+ }
60
+
61
+ /**
62
+ * 在已建立的 socket 上发 HTTP 请求,响应头到齐即 resolve(流式)。
63
+ * 返回 Response-like: { ok, status, headers: Headers, body: PassThrough(异步迭代), text(): Promise<string> }
64
+ * body 边收边吐(SSE 流式消费方可逐 chunk 读取);text() 消费流到底(非流式调用方用)。
65
+ * opts.signal 全程有效:abort 即 destroy socket 并 reject/终止流。
66
+ * absoluteForm: 请求行发绝对 URI(http:// 目标的经典代理转发用),默认发 origin-form。
67
+ * 导出供测试(裸 socket,无需 TLS/CONNECT);生产路径走 tunnelHttps / proxyFetch。
68
+ */
69
+ export function streamHttpResponse(sock, urlStr, opts = {}, timeout = FETCH_TIMEOUT, absoluteForm = false) {
70
+ return new Promise((resolve, reject) => {
71
+ const target = new URL(urlStr)
72
+ const method = opts.method ?? "GET"
73
+ const headers = opts.headers ?? {}
74
+ const signal = opts.signal
75
+
76
+ if (signal?.aborted) { sock.destroy(); return reject(abortError(signal)) }
77
+
78
+ const body = new PassThrough()
79
+ let settled = false
80
+ let headerBuf = ""
81
+
82
+ const timer = setTimeout(() => fail(new Error("Response timeout")), timeout)
83
+ const onAbort = () => { sock.destroy(); fail(abortError(signal)) }
84
+ signal?.addEventListener("abort", onAbort, { once: true })
85
+
86
+ function cleanup() {
87
+ clearTimeout(timer)
88
+ signal?.removeEventListener("abort", onAbort)
89
+ }
90
+ /** 头部阶段失败 reject;resolve 后失败则终止 body 流(for-await 抛出,不挂起) */
91
+ function fail(err) {
92
+ cleanup()
93
+ if (!settled) { settled = true; reject(err) }
94
+ else body.destroy(err)
95
+ }
96
+
97
+ sock.on("data", (d) => {
98
+ if (settled) return // 理论上不会发生(settle 后摘掉本监听器),防御
99
+ headerBuf += d.toString("utf8")
100
+ const idx = headerBuf.indexOf("\r\n\r\n")
101
+ if (idx < 0) return
102
+
103
+ const headerText = headerBuf.slice(0, idx)
104
+ const statusMatch = headerText.match(/^HTTP\/\d\.\d (\d+)/)
105
+ const status = statusMatch ? Number(statusMatch[1]) : 502
106
+ const respHeaders = {}
107
+ for (const line of headerText.split("\r\n").slice(1)) {
108
+ const ci = line.indexOf(":")
109
+ if (ci > 0) respHeaders[line.slice(0, ci).trim().toLowerCase()] = line.slice(ci + 1).trim()
110
+ }
111
+
112
+ // 头到齐:摘掉头阶段监听,剩余字节推入 body,后续数据直接 pipe
113
+ sock.removeAllListeners("data")
114
+ settled = true
115
+ cleanup()
116
+ const remaining = headerBuf.slice(idx + 4)
117
+ if (remaining) body.write(Buffer.from(remaining, "utf8"))
118
+ sock.pipe(body)
119
+ // body 结束后才移除 abort 监听(流式中途 abort 要能终止流)
120
+ body.on("close", () => signal?.removeEventListener("abort", onAbort))
121
+ signal?.addEventListener("abort", onAbort, { once: true })
122
+
123
+ resolve({
124
+ ok: status >= 200 && status < 400,
125
+ status,
126
+ headers: new Headers(respHeaders),
127
+ body,
128
+ text: async () => {
129
+ const chunks = []
130
+ for await (const c of body) chunks.push(c)
131
+ return Buffer.concat(chunks).toString("utf8")
132
+ },
133
+ })
134
+ })
135
+ sock.on("close", () => {
136
+ if (!settled) fail(new Error("Connection closed before response"))
137
+ else body.end()
138
+ })
139
+ // 头部阶段的传输错误统一包装为稳定契约(对端 RST 会抛原生 ECONNRESET,调用方难判别);
140
+ // resolve 后的 body 阶段保留原始错误终止流
141
+ sock.on("error", (e) => {
142
+ if (!settled) fail(new Error(`Connection closed before response (${e.code ?? e.message})`))
143
+ else fail(e)
144
+ })
145
+
146
+ // 写请求(absoluteForm:代理转发时请求行为绝对 URI)
147
+ const requestTarget = absoluteForm ? urlStr : `${target.pathname}${target.search}`
148
+ const lines = [`${method} ${requestTarget} HTTP/1.1`]
149
+ for (const [k, v] of Object.entries({ ...headers, Host: target.hostname })) lines.push(`${k}: ${v}`)
150
+ lines.push("Connection: close", "", "")
151
+ sock.write(lines.join("\r\n"))
152
+ if (opts.body) sock.write(opts.body)
153
+ })
154
+ }
155
+
156
+ /**
157
+ * HTTPS request through HTTP CONNECT proxy tunnel.
158
+ * CONNECT + TLS 建立后交给 streamHttpResponse — 响应头到齐即 resolve,body 为流式。
159
+ */
160
+ export function tunnelHttps(urlStr, opts, proxyUri, timeout = FETCH_TIMEOUT) {
161
+ return new Promise((resolve, reject) => {
162
+ const target = new URL(urlStr)
163
+ const proxy = new URL(proxyUri)
164
+ const signal = opts?.signal
165
+
166
+ if (signal?.aborted) return reject(abortError(signal))
167
+
168
+ const sock = connect({ host: proxy.hostname, port: Number(proxy.port) || 3128 })
169
+ const timer = setTimeout(() => { sock.destroy(); reject(new Error("Proxy CONNECT timeout")) }, timeout)
170
+ const onAbort = () => { sock.destroy(); reject(abortError(signal)) }
171
+ signal?.addEventListener("abort", onAbort, { once: true })
172
+ sock.on("connect", () => sock.write(`CONNECT ${target.hostname}:${target.port || 443} HTTP/1.1\r\nHost: ${target.hostname}\r\n\r\n`))
173
+
174
+ let buf = ""
175
+ sock.on("data", d => {
176
+ buf += d.toString()
177
+ const end = buf.indexOf("\r\n\r\n")
178
+ if (end < 0) return
179
+ const statusLine = buf.slice(0, end).split("\r\n")[0]
180
+ buf = buf.slice(end + 4)
181
+ if (!statusLine.includes("200")) { sock.destroy(); clearTimeout(timer); return reject(new Error(`Proxy CONNECT: ${statusLine}`)) }
182
+ sock.removeAllListeners("data"); clearTimeout(timer)
183
+
184
+ const tlsSock = tlsConnect({ socket: sock, servername: target.hostname, rejectUnauthorized: false, timeout })
185
+ if (buf) tlsSock.unshift(Buffer.from(buf))
186
+ tlsSock.on("secureConnect", () => {
187
+ // TLS 之后的请求/响应阶段:abort 交由 streamHttpResponse 接管
188
+ signal?.removeEventListener("abort", onAbort)
189
+ streamHttpResponse(tlsSock, urlStr, opts, timeout).then(resolve, reject)
190
+ })
191
+ tlsSock.on("error", e => { sock.destroy(); reject(e) })
192
+ })
193
+ sock.on("error", e => { clearTimeout(timer); reject(new Error(`Proxy CONNECT failed (${e.code ?? e.message})`)) })
194
+ // 代理干净 FIN 关闭(无 error)时也要 reject,不能卡满超时
195
+ sock.on("close", () => { clearTimeout(timer); reject(new Error("Proxy connection closed before tunnel established")) })
196
+ })
197
+ }
198
+
199
+ /** TCP 直连代理(http:// 目标的经典转发用):超时/abort/对端关闭都有稳定 reject */
200
+ function tcpConnectProxy(proxyUri, signal, timeout) {
201
+ return new Promise((resolve, reject) => {
202
+ const proxy = new URL(proxyUri)
203
+ const sock = connect({ host: proxy.hostname, port: Number(proxy.port) || 3128 })
204
+ if (signal?.aborted) { sock.destroy(); return reject(abortError(signal)) }
205
+ const onAbort = () => sock.destroy()
206
+ signal?.addEventListener("abort", onAbort, { once: true })
207
+ const timer = setTimeout(() => { sock.destroy(); reject(new Error("Proxy CONNECT timeout")) }, timeout)
208
+ const onError = (e) => { clearTimeout(timer); reject(new Error(`Proxy CONNECT failed (${e.code ?? e.message})`)) }
209
+ const onClose = () => {
210
+ clearTimeout(timer)
211
+ reject(signal?.aborted ? abortError(signal) : new Error("Proxy connection closed before tunnel established"))
212
+ }
213
+ sock.once("connect", () => {
214
+ clearTimeout(timer)
215
+ signal?.removeEventListener("abort", onAbort)
216
+ sock.removeListener("error", onError)
217
+ sock.removeListener("close", onClose)
218
+ resolve(sock)
219
+ })
220
+ sock.once("error", onError)
221
+ sock.once("close", onClose)
222
+ })
223
+ }
224
+
225
+ /**
226
+ * Generic fetch with proxy support.
227
+ * No proxy → native fetch. Proxy + HTTPS → CONNECT tunnel. Proxy + HTTP → 经典代理转发(绝对 URI 请求行)。
228
+ */
229
+ export async function proxyFetch(urlStr, opts, proxyUri) {
230
+ if (!proxyUri) return globalThis.fetch(urlStr, opts)
231
+ const target = new URL(urlStr)
232
+ if (target.protocol === "https:") return tunnelHttps(urlStr, opts, proxyUri)
233
+ // http:// 目标:TCP 直连代理,请求行发绝对 URI(GET http://host/path HTTP/1.1)
234
+ const sock = await tcpConnectProxy(proxyUri, opts?.signal, FETCH_TIMEOUT)
235
+ return streamHttpResponse(sock, urlStr, opts, FETCH_TIMEOUT, true)
236
+ }
package/src/tools/bash.md CHANGED
@@ -1,5 +1,13 @@
1
1
  Execute a shell command and return stdout+stderr. Use for running commands, builds, tests.
2
2
 
3
+ **Route to a dedicated tool instead of bash:**
4
+ - `cat file` / `head` / `tail` → `read`
5
+ - `ls` / `dir` → `ls`
6
+ - `find` / glob search → `glob`
7
+ - `grep` / `rg` → `grep`
8
+ - `echo >` / `sed` / `printf >` / `cat << EOF` → `write` / `edit` / `hashline_edit` / `apply_patch` (enforced: redirection is blocked)
9
+ - `git diff` / `git status` / `git log` → `git` tool
10
+
3
11
  Parameters:
4
12
  - command (required): Shell command to execute
5
13
  - timeout: Timeout in milliseconds (default 120000, max ~300000)
@@ -24,32 +24,21 @@
24
24
  import { Script, createContext } from "node:vm"
25
25
  import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, readdirSync } from "node:fs"
26
26
  import { join, dirname, relative, resolve } from "node:path"
27
- import { globToRegex, normalizeEOL } from "./shared.mjs"
27
+ import { globToRegex, normalizeEOL, isPrivateHost } from "./shared.mjs"
28
28
 
29
29
  const MAX_OUTPUT = 50_000
30
30
  const MAX_SCRIPT = 50_000
31
31
  const DEFAULT_TIMEOUT = 30_000
32
32
 
33
- /** SSRF-safe fetch: only http/https, private IP rejection, 10s timeout */
33
+ /** SSRF-safe fetch: only http/https, private IP rejection, 10s timeout. */
34
34
  async function sandboxFetch(url) {
35
35
  const parsed = new URL(url)
36
36
  if (!["http:", "https:"].includes(parsed.protocol)) {
37
37
  throw new Error(`CodeMode fetch: protocol not allowed: ${parsed.protocol}`)
38
38
  }
39
- // Block private/internal IPs
40
- const hostname = parsed.hostname.toLowerCase()
41
- if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" ||
42
- hostname.startsWith("192.168.") || hostname.startsWith("10.") ||
43
- hostname.startsWith("172.16.") || hostname.startsWith("172.17.") ||
44
- hostname.startsWith("172.18.") || hostname.startsWith("172.19.") ||
45
- hostname.startsWith("172.20.") || hostname.startsWith("172.21.") ||
46
- hostname.startsWith("172.22.") || hostname.startsWith("172.23.") ||
47
- hostname.startsWith("172.24.") || hostname.startsWith("172.25.") ||
48
- hostname.startsWith("172.26.") || hostname.startsWith("172.27.") ||
49
- hostname.startsWith("172.28.") || hostname.startsWith("172.29.") ||
50
- hostname.startsWith("172.30.") || hostname.startsWith("172.31.") ||
51
- hostname === "0.0.0.0" || hostname.endsWith(".local")) {
52
- throw new Error(`CodeMode fetch: private/internal host not allowed: ${hostname}`)
39
+
40
+ if (isPrivateHost(parsed.hostname)) {
41
+ throw new Error(`CodeMode fetch: private/internal host not allowed: ${parsed.hostname}`)
53
42
  }
54
43
  const ctrl = new AbortController()
55
44
  const timer = setTimeout(() => ctrl.abort(), 10_000)
package/src/tools/edit.md CHANGED
@@ -1,5 +1,13 @@
1
1
  Edit a file by exact string replacement. old_string must match exactly once unless replace_all is set.
2
2
 
3
+ **Routing — pick the right edit tool:**
4
+ - Precise line-targeted change → `hashline_edit` (hash-based, immune to whitespace/encoding drift — preferred)
5
+ - One exact-string swap → this tool
6
+ - Add a function/block after a known line → `insert_after`
7
+ - Same change across multiple files or many spots → `apply_patch`
8
+ - Rewrite an entire file → `write`
9
+ - Rename a symbol project-wide → `lsp` or `grep` first to map every caller
10
+
3
11
  Parameters:
4
12
  - path (required): File path
5
13
  - old_string (required): Exact text to find and replace
@@ -5,6 +5,7 @@ Parameters:
5
5
 
6
6
  Notes:
7
7
  - Follows redirects automatically
8
- - Timeout: 20 seconds
8
+ - Timeout: 15 seconds
9
9
  - HTML pages are converted to plain text (scripts, styles, navigation stripped)
10
10
  - Non-HTML responses are returned as-is (truncated at ~50000 chars)
11
+ - Proxy support: set `"proxy": {"uri": "http://host:port", "web": true}` in config.json or `HTTPS_PROXY` env var