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/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
- const chunks = chunkCode(lines, rel)
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 = absPath.slice(cwd.length + 1).replaceAll("\\", "/")
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
- for (const c of chunks) {
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
- for (const c of chunks) {
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
- const chunks = chunkMarkdown(lines, rel)
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) body.temperature = provider.temperature
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) body.reasoning_effort = 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 ~50000 chars; if you need more, redirect to a file and read it
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. 'src/**/*.mjs'). Returns matching paths.
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.
@@ -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.
@@ -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