thincoder 0.5.0 → 0.7.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/README.md +90 -45
- package/package.json +2 -2
- package/src/SYSTEM_PROMPT.md +6 -3
- package/src/agent.mjs +87 -13
- package/src/coder-overlay.md +1 -1
- package/src/config.mjs +47 -23
- package/src/mcp.mjs +94 -3
- package/src/memory.mjs +64 -68
- package/src/provider.mjs +21 -4
- package/src/tools/bash.md +14 -1
- package/src/tools/glob.md +1 -1
- package/src/tools/grep.md +3 -0
- package/src/tools/insert_after.md +13 -0
- package/src/tools/question.md +1 -0
- package/src/tools/syntax_check.md +10 -0
- package/src/tools/websearch.md +1 -0
- package/src/tools.mjs +205 -68
- package/src/tui.mjs +204 -59
package/src/mcp.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* mcp.mjs — MCP (Model Context Protocol) client
|
|
3
|
-
* 零依赖:stdio transport (spawn + JSON-RPC) + HTTP transport (fetch + SSE)。
|
|
4
|
-
* config: { command, args?, name } 或 { url, name, headers? }
|
|
3
|
+
* 零依赖:stdio transport (spawn + JSON-RPC) + HTTP transport (fetch + SSE) + WebSocket transport (global WebSocket)。
|
|
4
|
+
* config: { command, args?, name } 或 { url, name, headers? } 或 { wsUrl, name, headers? }
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { spawn } from "node:child_process"
|
|
@@ -249,6 +249,90 @@ function httpTransport(baseURL, extraHeaders = {}) {
|
|
|
249
249
|
return { send, notify, close, openSSE, url, headers: extraHeaders }
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
// ---- WebSocket transport ----
|
|
253
|
+
|
|
254
|
+
function wsTransport(wsUrl, extraHeaders = {}) {
|
|
255
|
+
const pending = new Map()
|
|
256
|
+
let closed = false
|
|
257
|
+
let ws = null
|
|
258
|
+
|
|
259
|
+
const failAll = (message) => {
|
|
260
|
+
for (const [, resolve] of pending) resolve({ id: null, error: { code: -32000, message } })
|
|
261
|
+
pending.clear()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const connect = () => {
|
|
265
|
+
if (closed) throw new Error("MCP WebSocket connection closed")
|
|
266
|
+
// WebSocket API 不支持自定义 header,如需 auth 走 query param 或子协议
|
|
267
|
+
ws = extraHeaders.Authorization
|
|
268
|
+
? new WebSocket(wsUrl, extraHeaders.Authorization)
|
|
269
|
+
: new WebSocket(wsUrl)
|
|
270
|
+
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
const timeout = setTimeout(() => {
|
|
273
|
+
ws.close()
|
|
274
|
+
reject(new Error(`WebSocket connect timeout: ${wsUrl}`))
|
|
275
|
+
}, INIT_TIMEOUT_MS)
|
|
276
|
+
|
|
277
|
+
ws.on("open", () => {
|
|
278
|
+
clearTimeout(timeout)
|
|
279
|
+
resolve()
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
ws.on("message", (data) => {
|
|
283
|
+
try {
|
|
284
|
+
const msg = JSON.parse(data.toString())
|
|
285
|
+
const resolver = pending.get(msg.id)
|
|
286
|
+
if (resolver) {
|
|
287
|
+
pending.delete(msg.id)
|
|
288
|
+
resolver(msg)
|
|
289
|
+
}
|
|
290
|
+
// 没有 resoler 的是通知,忽略
|
|
291
|
+
} catch { /* 非 JSON,忽略 */ }
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
ws.on("error", (error) => {
|
|
295
|
+
clearTimeout(timeout)
|
|
296
|
+
closed = true
|
|
297
|
+
const errMsg = error.message || "WebSocket error"
|
|
298
|
+
if (pending.size > 0) {
|
|
299
|
+
failAll(errMsg)
|
|
300
|
+
} else {
|
|
301
|
+
reject(new Error(errMsg))
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
ws.on("close", () => {
|
|
306
|
+
clearTimeout(timeout)
|
|
307
|
+
closed = true
|
|
308
|
+
failAll("WebSocket closed")
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const send = (method, params) => {
|
|
314
|
+
if (closed) return Promise.reject(new Error("MCP WebSocket connection closed"))
|
|
315
|
+
const id = rpcId()
|
|
316
|
+
const promise = new Promise((resolve) => pending.set(id, resolve))
|
|
317
|
+
ws.send(JSON.stringify({ jsonrpc: "2.0", id, method, params }))
|
|
318
|
+
return withTimeout(promise, CALL_TIMEOUT_MS).finally(() => pending.delete(id))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const notify = (method, params) => {
|
|
322
|
+
if (!closed && ws?.readyState === WebSocket.OPEN) {
|
|
323
|
+
ws.send(JSON.stringify({ jsonrpc: "2.0", method, params }))
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const close = () => {
|
|
328
|
+
closed = true
|
|
329
|
+
failAll("Connection closed")
|
|
330
|
+
try { ws?.close() } catch { /* 忽略 */ }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { send, notify, close, connect }
|
|
334
|
+
}
|
|
335
|
+
|
|
252
336
|
// ---- MCP lifecycle ----
|
|
253
337
|
|
|
254
338
|
function buildTools(mcpTools, transport, config) {
|
|
@@ -294,6 +378,13 @@ async function doInitialize(transport, name) {
|
|
|
294
378
|
* http: { name, url, headers? }
|
|
295
379
|
*/
|
|
296
380
|
export async function connectMcpServer(config) {
|
|
381
|
+
if (config.wsUrl) {
|
|
382
|
+
const transport = wsTransport(config.wsUrl, config.headers ?? {})
|
|
383
|
+
await transport.connect()
|
|
384
|
+
const mcpTools = await doInitialize(transport, config.name ?? config.wsUrl)
|
|
385
|
+
return buildTools(mcpTools, transport, config)
|
|
386
|
+
}
|
|
387
|
+
|
|
297
388
|
if (config.url) {
|
|
298
389
|
const transport = httpTransport(config.url, config.headers ?? {})
|
|
299
390
|
try {
|
|
@@ -316,7 +407,7 @@ export async function connectMcpServer(config) {
|
|
|
316
407
|
}
|
|
317
408
|
}
|
|
318
409
|
|
|
319
|
-
throw new Error(`MCP server "${config.name}": needs either 'command' (stdio) or 'url' (http)`)
|
|
410
|
+
throw new Error(`MCP server "${config.name}": needs either 'wsUrl' (websocket), 'command' (stdio), or 'url' (http)`)
|
|
320
411
|
}
|
|
321
412
|
|
|
322
413
|
export function closeAllMcp(agent) {
|
package/src/memory.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { DatabaseSync } from "node:sqlite"
|
|
14
14
|
import { mkdirSync } from "node:fs"
|
|
15
15
|
import { readFile, readdir, stat, writeFile, mkdir } from "node:fs/promises"
|
|
16
|
-
import { dirname, join } from "node:path"
|
|
16
|
+
import { dirname, join, relative } from "node:path"
|
|
17
17
|
import { parseEntry, serializeEntry, entryFilename } from "./markdown.mjs"
|
|
18
18
|
import { embed, cosine, toBlob, fromBlob } from "./embedding.mjs"
|
|
19
19
|
import { commitAndPush } from "./gitmem.mjs"
|
|
@@ -664,6 +664,52 @@ function extractLeadingDoc(lines, lineNum, ext) {
|
|
|
664
664
|
return text.length > 0 && text.length < 300 ? text : ""
|
|
665
665
|
}
|
|
666
666
|
|
|
667
|
+
/** 单文件入索引:删除旧块 → 分块 → 插入新块(codeSync 和 reindexFile 共用) */
|
|
668
|
+
/** 将控制权交还给事件循环一个 tick(让键盘输入有机会被处理) */
|
|
669
|
+
function yieldTick() {
|
|
670
|
+
return new Promise((r) => setTimeout(r, 0))
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function _upsertCodeFile(memory, rel, lines, lang, mtimeMs) {
|
|
674
|
+
const chunks = chunkCode(lines, rel)
|
|
675
|
+
memory.db.exec("BEGIN")
|
|
676
|
+
try {
|
|
677
|
+
memory.db.prepare(`DELETE FROM code_chunks WHERE path = ?`).run(rel)
|
|
678
|
+
const insert = memory.db.prepare(`
|
|
679
|
+
INSERT INTO code_chunks (path, language, chunk_type, symbol_name, content, line_start, line_end, mtime_ms, seg_content)
|
|
680
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
681
|
+
`)
|
|
682
|
+
for (const c of chunks) {
|
|
683
|
+
const isFile = c.name === rel
|
|
684
|
+
insert.run(rel, lang, isFile ? "file" : "symbol", isFile ? "" : c.name.slice(rel.length + 1), c.content, c.line_start, c.line_end, mtimeMs, segmentCJK(c.content))
|
|
685
|
+
}
|
|
686
|
+
memory.db.exec("COMMIT")
|
|
687
|
+
} catch (e) {
|
|
688
|
+
memory.db.exec("ROLLBACK")
|
|
689
|
+
throw e
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function _upsertDocFile(memory, rel, lines, mtimeMs) {
|
|
694
|
+
const chunks = chunkMarkdown(lines, rel)
|
|
695
|
+
const lang = rel.endsWith(".rst") ? "rst" : rel.endsWith(".adoc") ? "asciidoc" : rel.endsWith(".txt") ? "text" : "markdown"
|
|
696
|
+
memory.db.exec("BEGIN")
|
|
697
|
+
try {
|
|
698
|
+
memory.db.prepare(`DELETE FROM doc_chunks WHERE path = ?`).run(rel)
|
|
699
|
+
const insert = memory.db.prepare(`
|
|
700
|
+
INSERT INTO doc_chunks (path, language, heading, content, line_start, line_end, mtime_ms, seg_content)
|
|
701
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
702
|
+
`)
|
|
703
|
+
for (const c of chunks) {
|
|
704
|
+
insert.run(rel, lang, c.heading, c.content, c.line_start, c.line_end, mtimeMs, segmentCJK(c.content))
|
|
705
|
+
}
|
|
706
|
+
memory.db.exec("COMMIT")
|
|
707
|
+
} catch (e) {
|
|
708
|
+
memory.db.exec("ROLLBACK")
|
|
709
|
+
throw e
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
667
713
|
/**
|
|
668
714
|
* 同步代码索引:扫描 dir 下所有源文件 → 分块 → upsert 到 code_chunks。
|
|
669
715
|
* 按 mtime 增量——只重建变更过的文件块。
|
|
@@ -713,29 +759,9 @@ export async function codeSync(memory, dir, { onProgress } = {}) {
|
|
|
713
759
|
try { text = await readFile(abs, "utf8") } catch { continue }
|
|
714
760
|
const lines = text.split("\n")
|
|
715
761
|
const lang = detectLanguage(abs)
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
// 删除该文件的旧块,插入新块
|
|
719
|
-
memory.db.prepare(`DELETE FROM code_chunks WHERE path = ?`).run(rel)
|
|
720
|
-
const insert = memory.db.prepare(`
|
|
721
|
-
INSERT INTO code_chunks (path, language, chunk_type, symbol_name, content, line_start, line_end, mtime_ms, seg_content)
|
|
722
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
723
|
-
`)
|
|
724
|
-
for (const c of chunks) {
|
|
725
|
-
const isFile = c.name === rel
|
|
726
|
-
insert.run(
|
|
727
|
-
rel,
|
|
728
|
-
lang,
|
|
729
|
-
isFile ? "file" : "symbol",
|
|
730
|
-
isFile ? "" : c.name.slice(rel.length + 1), // 去掉 "filepath:" 前缀
|
|
731
|
-
c.content,
|
|
732
|
-
c.line_start,
|
|
733
|
-
c.line_end,
|
|
734
|
-
mtimeMs,
|
|
735
|
-
segmentCJK(c.content),
|
|
736
|
-
)
|
|
737
|
-
}
|
|
762
|
+
_upsertCodeFile(memory, rel, lines, lang, mtimeMs)
|
|
738
763
|
updated++
|
|
764
|
+
await yieldTick()
|
|
739
765
|
|
|
740
766
|
if (onProgress && i % 10 === 0) {
|
|
741
767
|
onProgress({ phase: "index", current: i + 1, total: files.length, updated, removed, skipped })
|
|
@@ -851,48 +877,27 @@ export function codeSearchTool(memory) {
|
|
|
851
877
|
*/
|
|
852
878
|
export async function reindexFile(memory, cwd, absPath) {
|
|
853
879
|
const ext = absPath.slice(absPath.lastIndexOf(".")).toLowerCase()
|
|
854
|
-
const rel =
|
|
880
|
+
const rel = relative(cwd, absPath).replaceAll("\\", "/")
|
|
881
|
+
if (rel.startsWith("..")) return // 越界路径拒索引
|
|
882
|
+
|
|
883
|
+
let text
|
|
884
|
+
try { text = await readFile(absPath, "utf8") } catch {
|
|
885
|
+
// 文件已删:清理索引
|
|
886
|
+
if (CODE_EXTS.has(ext)) memory.db.prepare(`DELETE FROM code_chunks WHERE path = ?`).run(rel)
|
|
887
|
+
else if (DOC_EXTS.has(ext)) memory.db.prepare(`DELETE FROM doc_chunks WHERE path = ?`).run(rel)
|
|
888
|
+
return
|
|
889
|
+
}
|
|
890
|
+
const lines = text.split("\n")
|
|
855
891
|
|
|
856
892
|
if (CODE_EXTS.has(ext)) {
|
|
857
|
-
// 文件已删?清理索引
|
|
858
|
-
let text
|
|
859
|
-
try { text = await readFile(absPath, "utf8") } catch {
|
|
860
|
-
memory.db.prepare(`DELETE FROM code_chunks WHERE path = ?`).run(rel)
|
|
861
|
-
return
|
|
862
|
-
}
|
|
863
|
-
const lines = text.split("\n")
|
|
864
893
|
const lang = detectLanguage(absPath)
|
|
865
|
-
const chunks = chunkCode(lines, rel)
|
|
866
|
-
memory.db.prepare(`DELETE FROM code_chunks WHERE path = ?`).run(rel)
|
|
867
|
-
const insert = memory.db.prepare(`
|
|
868
|
-
INSERT INTO code_chunks (path, language, chunk_type, symbol_name, content, line_start, line_end, mtime_ms, seg_content)
|
|
869
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
870
|
-
`)
|
|
871
894
|
let mtimeMs = 0
|
|
872
895
|
try { mtimeMs = Math.floor((await stat(absPath)).mtimeMs) } catch { /* 新文件 */ }
|
|
873
|
-
|
|
874
|
-
const isFile = c.name === rel
|
|
875
|
-
insert.run(rel, lang, isFile ? "file" : "symbol", isFile ? "" : c.name.slice(rel.length + 1), c.content, c.line_start, c.line_end, mtimeMs, segmentCJK(c.content))
|
|
876
|
-
}
|
|
896
|
+
_upsertCodeFile(memory, rel, lines, lang, mtimeMs)
|
|
877
897
|
} else if (DOC_EXTS.has(ext)) {
|
|
878
|
-
let text
|
|
879
|
-
try { text = await readFile(absPath, "utf8") } catch {
|
|
880
|
-
memory.db.prepare(`DELETE FROM doc_chunks WHERE path = ?`).run(rel)
|
|
881
|
-
return
|
|
882
|
-
}
|
|
883
|
-
const lines = text.split("\n")
|
|
884
|
-
const chunks = chunkMarkdown(lines, rel)
|
|
885
|
-
memory.db.prepare(`DELETE FROM doc_chunks WHERE path = ?`).run(rel)
|
|
886
|
-
const insert = memory.db.prepare(`
|
|
887
|
-
INSERT INTO doc_chunks (path, language, heading, content, line_start, line_end, mtime_ms, seg_content)
|
|
888
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
889
|
-
`)
|
|
890
|
-
const lang = rel.endsWith(".rst") ? "rst" : rel.endsWith(".adoc") ? "asciidoc" : rel.endsWith(".txt") ? "text" : "markdown"
|
|
891
898
|
let mtimeMs = 0
|
|
892
899
|
try { mtimeMs = Math.floor((await stat(absPath)).mtimeMs) } catch { /* 新文件 */ }
|
|
893
|
-
|
|
894
|
-
insert.run(rel, lang, c.heading, c.content, c.line_start, c.line_end, mtimeMs, segmentCJK(c.content))
|
|
895
|
-
}
|
|
900
|
+
_upsertDocFile(memory, rel, lines, mtimeMs)
|
|
896
901
|
}
|
|
897
902
|
}
|
|
898
903
|
|
|
@@ -968,18 +973,9 @@ export async function docSync(memory, dir, { onProgress } = {}) {
|
|
|
968
973
|
let text
|
|
969
974
|
try { text = await readFile(abs, "utf8") } catch { continue }
|
|
970
975
|
const lines = text.split("\n")
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
memory.db.prepare(`DELETE FROM doc_chunks WHERE path = ?`).run(rel)
|
|
974
|
-
const insert = memory.db.prepare(`
|
|
975
|
-
INSERT INTO doc_chunks (path, language, heading, content, line_start, line_end, mtime_ms, seg_content)
|
|
976
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
977
|
-
`)
|
|
978
|
-
const lang = rel.endsWith(".rst") ? "rst" : rel.endsWith(".adoc") ? "asciidoc" : rel.endsWith(".txt") ? "text" : "markdown"
|
|
979
|
-
for (const c of chunks) {
|
|
980
|
-
insert.run(rel, lang, c.heading, c.content, c.line_start, c.line_end, mtimeMs, segmentCJK(c.content))
|
|
981
|
-
}
|
|
976
|
+
_upsertDocFile(memory, rel, lines, mtimeMs)
|
|
982
977
|
updated++
|
|
978
|
+
await yieldTick()
|
|
983
979
|
|
|
984
980
|
if (onProgress && i % 10 === 0) {
|
|
985
981
|
onProgress({ phase: "index", current: i + 1, total: files.length, updated, removed, skipped })
|
package/src/provider.mjs
CHANGED
|
@@ -47,6 +47,7 @@ export function createProvider(config) {
|
|
|
47
47
|
* 思考模式不支持前缀续写,已产出 reasoning 时放弃续写
|
|
48
48
|
*/
|
|
49
49
|
export async function chat(provider, { messages, tools, onToken, onReasoning, signal }) {
|
|
50
|
+
const spec = specForModel(provider.model)
|
|
50
51
|
const body = {
|
|
51
52
|
model: provider.model,
|
|
52
53
|
messages,
|
|
@@ -54,16 +55,32 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, si
|
|
|
54
55
|
stream_options: { include_usage: true },
|
|
55
56
|
}
|
|
56
57
|
if (provider.maxTokens) body.max_tokens = provider.maxTokens
|
|
57
|
-
if (provider.temperature != null)
|
|
58
|
+
if (provider.temperature != null) {
|
|
59
|
+
// 按规格表裁剪 temperature:GLM [0,1] 限两位小数,DeepSeek ≤2,未声明则不裁剪
|
|
60
|
+
let t = provider.temperature
|
|
61
|
+
if (spec.tempRange) {
|
|
62
|
+
t = Math.min(spec.tempRange[1], Math.max(spec.tempRange[0], t))
|
|
63
|
+
t = Math.round(t * 100) / 100
|
|
64
|
+
}
|
|
65
|
+
body.temperature = t
|
|
66
|
+
}
|
|
58
67
|
if (provider.thinking) body.thinking = provider.thinking
|
|
59
|
-
if (provider.reasoningEffort)
|
|
68
|
+
if (provider.reasoningEffort) {
|
|
69
|
+
// 按规格表校验 reasoning_effort 枚举:不在枚举内则报错,不映射、不猜测
|
|
70
|
+
if (spec.reasoningEffortEnum && !spec.reasoningEffortEnum.includes(provider.reasoningEffort)) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`reasoning_effort "${provider.reasoningEffort}" not supported by model "${provider.model}"; ` +
|
|
73
|
+
`valid values: ${spec.reasoningEffortEnum.join(", ")}`
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
body.reasoning_effort = provider.reasoningEffort
|
|
77
|
+
}
|
|
60
78
|
if (tools?.length) body.tools = tools
|
|
61
79
|
|
|
62
80
|
const response = await requestWithRetry(provider, body, signal)
|
|
63
81
|
const result = await readSSE(response, { onToken, onReasoning })
|
|
64
82
|
|
|
65
83
|
// 截断续写:仅规格表声明续写协议的模型(其他端点不认识 partial/prefix 字段,可能 400)
|
|
66
|
-
const spec = specForModel(provider.model)
|
|
67
84
|
if (!spec.partialMode && !spec.prefixMode) return result
|
|
68
85
|
// DeepSeek prefix 续写不支持思考模式,已产出 reasoning 时无前缀协议可用
|
|
69
86
|
if (spec.prefixMode && !spec.partialMode && result.reasoning) return result
|
|
@@ -132,7 +149,7 @@ async function requestWithRetry(provider, body, signal) {
|
|
|
132
149
|
|
|
133
150
|
let response
|
|
134
151
|
try {
|
|
135
|
-
response = await fetch(`${provider.baseURL}/chat/completions`, {
|
|
152
|
+
response = await fetch(`${provider.baseURL}${provider.chatPath ?? "/chat/completions"}`, {
|
|
136
153
|
method: "POST",
|
|
137
154
|
headers: {
|
|
138
155
|
"Content-Type": "application/json",
|
package/src/tools/bash.md
CHANGED
|
@@ -4,11 +4,24 @@ Parameters:
|
|
|
4
4
|
- command (required): Shell command to execute
|
|
5
5
|
- timeout: Timeout in milliseconds (default 120000, max ~300000)
|
|
6
6
|
|
|
7
|
+
Output format:
|
|
8
|
+
```
|
|
9
|
+
[stdout]:
|
|
10
|
+
<standard output, or "(empty)">
|
|
11
|
+
|
|
12
|
+
[stderr]:
|
|
13
|
+
<standard error, only present if non-empty>
|
|
14
|
+
|
|
15
|
+
(exit code N)
|
|
16
|
+
```
|
|
17
|
+
|
|
7
18
|
Notes:
|
|
8
19
|
- There is NO TTY — editors, pagers (vim, less), and interactive prompts WILL hang. Always pass non-interactive flags: `git commit -m`, `git --no-pager`, `-y`/`--yes` where applicable
|
|
9
20
|
- The environment sets GIT_PAGER=cat, PAGER=cat, EDITOR=true, TERM=dumb — but still always use non-interactive flags
|
|
10
|
-
- Output is capped at ~
|
|
21
|
+
- Output is capped at ~200K chars; if you need more, redirect to a file and read it. Truncated output ends with a `[... truncated: N chars omitted]` marker — the missing tail may contain errors.
|
|
22
|
+
- Check `[stderr]` for error messages, warnings, and diagnostic output — it is separated from `[stdout]` so you can quickly identify problems.
|
|
11
23
|
- On Windows, use Unix shell syntax inside bash commands (Git Bash): forward slashes, `/dev/null` not `NUL`
|
|
12
24
|
- Never use bash to read, copy, or transmit secret files (.env, keys, tokens)
|
|
13
25
|
- Do NOT run destructive commands (rm -rf, force-push, drop table) without explicit user confirmation
|
|
14
26
|
- After commands that change files (git checkout, npm install, etc.), repo_outline and code_search may be stale — re-run them to get current results.
|
|
27
|
+
- Prefer read/glob/grep/ls for file operations inside the project — bash has no directory confinement.
|
package/src/tools/glob.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Find files by glob pattern (e.g.
|
|
1
|
+
Find files by glob pattern. Returns matching paths. Supports `**` for recursive matching (e.g. `src/**/*.mjs` for all .mjs in src/, `**/*.test.mjs` for all test files).
|
|
2
2
|
|
|
3
3
|
Parameters:
|
|
4
4
|
- pattern (required): Glob pattern — supports **, *, ?, and character classes
|
package/src/tools/grep.md
CHANGED
|
@@ -4,9 +4,12 @@ Parameters:
|
|
|
4
4
|
- pattern (required): JavaScript regular expression
|
|
5
5
|
- path: Directory or file to search (default cwd)
|
|
6
6
|
- glob: Only search files matching this glob (e.g. '*.mjs')
|
|
7
|
+
- before: Lines of context to show before each match (grep -B). Default 0
|
|
8
|
+
- after: Lines of context to show after each match (grep -A). Default 0
|
|
7
9
|
|
|
8
10
|
Notes:
|
|
9
11
|
- Skips node_modules, .git, dist, build, .turbo, coverage
|
|
10
12
|
- Results capped at 200 matches
|
|
11
13
|
- Binary/unreadable files are silently skipped
|
|
12
14
|
- Use this to find usages, definitions, patterns; use glob to find files by name
|
|
15
|
+
- With before/after: matching lines use `:` separator, context lines use `-` (like ripgrep); overlapping context ranges in the same file are merged and de-duplicated
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Insert a line of text after a specific line in a file. Safer than `edit` for adding new content — no need to copy surrounding context for exact string matching.
|
|
2
|
+
|
|
3
|
+
Parameters:
|
|
4
|
+
- path (required): File path
|
|
5
|
+
- content (required): Text to insert (will be placed as a new line after the target line)
|
|
6
|
+
- after_line: Line number to insert after (1-based). Preferred when you know the exact line number from `read`.
|
|
7
|
+
- after_regex: JavaScript regex to find the line to insert after. Must match exactly one line; if it matches multiple, the tool errors and shows the matching line numbers.
|
|
8
|
+
|
|
9
|
+
Notes:
|
|
10
|
+
- Either after_line or after_regex is required; if both are given, after_line wins.
|
|
11
|
+
- Use this instead of `edit` when you're adding a new function, import, or block — no need to fabricate surrounding context for exact matching.
|
|
12
|
+
- The inserted content becomes its own line; it's equivalent to `lines.splice(targetLine, 0, content)`.
|
|
13
|
+
- Returns a diff of the change.
|
package/src/tools/question.md
CHANGED
|
@@ -8,3 +8,4 @@ Notes:
|
|
|
8
8
|
- The agent loop pauses until the user answers
|
|
9
9
|
- The answer is injected as the next user message
|
|
10
10
|
- Use sparingly — prefer making reasonable decisions when possible
|
|
11
|
+
- After receiving an answer about a design convention, tool preference, or recurring pattern: save it with memory_put. This prevents asking the same question in future sessions — the user shouldn't have to repeat their preferences.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Check a JavaScript file for syntax errors using `node --check`. Fast, offline, no dependencies — use after writing or editing .js/.mjs/.cjs files to catch parse errors before running tests.
|
|
2
|
+
|
|
3
|
+
Parameters:
|
|
4
|
+
- path (required): File path (.js, .mjs, or .cjs)
|
|
5
|
+
|
|
6
|
+
Notes:
|
|
7
|
+
- Only supports JavaScript-family files; for other languages, run the appropriate checker via `bash`.
|
|
8
|
+
- Returns "Syntax OK" or the exact error message with line/column from Node.js.
|
|
9
|
+
- This is NOT a test run — it only catches parse errors (missing brackets, invalid syntax), not logic bugs.
|
|
10
|
+
- Use this right after `write` or `edit` to fail fast on trivial mistakes before investing time in a full test run.
|
package/src/tools/websearch.md
CHANGED
|
@@ -5,6 +5,7 @@ Parameters:
|
|
|
5
5
|
- limit: Max results (default 8)
|
|
6
6
|
|
|
7
7
|
Notes:
|
|
8
|
+
- Before searching the web, call `memory_search` first — you may already know the answer from a previous session. Only reach for websearch if memory comes up empty.
|
|
8
9
|
- Use this for information that is NOT in the local codebase — current docs, error messages, API references
|
|
9
10
|
- Follow up with `fetch` to read full pages from the results
|
|
10
11
|
- Results are scraped from Bing HTML — some formatting may be imperfect
|