thincoder 0.2.0 → 0.3.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 +28 -10
- package/bin/thincoder.mjs +44 -3
- package/package.json +1 -1
- package/src/SYSTEM_PROMPT.md +30 -0
- package/src/agent.mjs +456 -58
- package/src/coder-overlay.md +14 -0
- package/src/config.mjs +15 -5
- package/src/context.mjs +41 -6
- package/src/explore-overlay.md +10 -0
- package/src/mcp.mjs +359 -0
- package/src/session.mjs +2 -0
- package/src/skills.mjs +75 -0
- package/src/tools/bash.md +13 -0
- package/src/tools/delete.md +9 -0
- package/src/tools/edit.md +12 -0
- package/src/tools/fetch.md +10 -0
- package/src/tools/git_diff.md +11 -0
- package/src/tools/git_log.md +10 -0
- package/src/tools/git_status.md +8 -0
- package/src/tools/glob.md +11 -0
- package/src/tools/grep.md +12 -0
- package/src/tools/ls.md +9 -0
- package/src/tools/question.md +10 -0
- package/src/tools/read.md +10 -0
- package/src/tools/websearch.md +10 -0
- package/src/tools/write.md +9 -0
- package/src/tools.mjs +191 -20
- package/src/tui.mjs +560 -145
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Find files by glob pattern (e.g. 'src/**/*.mjs'). Returns matching paths.
|
|
2
|
+
|
|
3
|
+
Parameters:
|
|
4
|
+
- pattern (required): Glob pattern — supports **, *, ?, and character classes
|
|
5
|
+
- path: Directory to search in (default cwd)
|
|
6
|
+
|
|
7
|
+
Notes:
|
|
8
|
+
- Skips node_modules, .git, dist, build, .turbo, coverage
|
|
9
|
+
- Results capped at 1000 matches
|
|
10
|
+
- Use this to discover file structure; use grep to search file contents
|
|
11
|
+
- Prefer patterns with a literal anchor (extension or subdirectory) over bare wildcards
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Search file contents with a regex. Returns matching lines as path:line: content.
|
|
2
|
+
|
|
3
|
+
Parameters:
|
|
4
|
+
- pattern (required): JavaScript regular expression
|
|
5
|
+
- path: Directory or file to search (default cwd)
|
|
6
|
+
- glob: Only search files matching this glob (e.g. '*.mjs')
|
|
7
|
+
|
|
8
|
+
Notes:
|
|
9
|
+
- Skips node_modules, .git, dist, build, .turbo, coverage
|
|
10
|
+
- Results capped at 200 matches
|
|
11
|
+
- Binary/unreadable files are silently skipped
|
|
12
|
+
- Use this to find usages, definitions, patterns; use glob to find files by name
|
package/src/tools/ls.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
List directory contents with type, size, and modification time. Directories listed first. Use to see what a directory contains (glob only matches files).
|
|
2
|
+
|
|
3
|
+
Parameters:
|
|
4
|
+
- path: Directory path (default cwd)
|
|
5
|
+
|
|
6
|
+
Notes:
|
|
7
|
+
- Shows first 500 entries
|
|
8
|
+
- Directories are prefixed with `/` and listed before files
|
|
9
|
+
- Use this for a quick overview; use glob when you have a specific file pattern in mind
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Ask the user a question and wait for their response. Use when the task is ambiguous, you need a design decision, or you're stuck and need human judgment.
|
|
2
|
+
|
|
3
|
+
Parameters:
|
|
4
|
+
- question (required): The question to ask the user
|
|
5
|
+
- options: Array of single-choice options for the user to pick from (optional)
|
|
6
|
+
|
|
7
|
+
Notes:
|
|
8
|
+
- The agent loop pauses until the user answers
|
|
9
|
+
- The answer is injected as the next user message
|
|
10
|
+
- Use sparingly — prefer making reasonable decisions when possible
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Read a text file. Returns numbered lines. Use offset/limit to page large files.
|
|
2
|
+
|
|
3
|
+
Parameters:
|
|
4
|
+
- path (required): File path, relative to cwd or absolute
|
|
5
|
+
- offset: 1-based line number to start reading from
|
|
6
|
+
- limit: Max lines to return (default 2000)
|
|
7
|
+
|
|
8
|
+
Notes:
|
|
9
|
+
- Always prefer this over `cat` or shell-based reading — it caps output and avoids large dumps
|
|
10
|
+
- Use offset for pagination when the file is large
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Search the web (Bing). Returns result titles, URLs, and snippets. Use for looking up current information, docs, error messages.
|
|
2
|
+
|
|
3
|
+
Parameters:
|
|
4
|
+
- query (required): Search query
|
|
5
|
+
- limit: Max results (default 8)
|
|
6
|
+
|
|
7
|
+
Notes:
|
|
8
|
+
- Use this for information that is NOT in the local codebase — current docs, error messages, API references
|
|
9
|
+
- Follow up with `fetch` to read full pages from the results
|
|
10
|
+
- Results are scraped from Bing HTML — some formatting may be imperfect
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Write content to a file. Creates parent directories; overwrites existing file.
|
|
2
|
+
|
|
3
|
+
Parameters:
|
|
4
|
+
- path (required): File path, relative to cwd or absolute
|
|
5
|
+
- content (required): Full content to write
|
|
6
|
+
|
|
7
|
+
Notes:
|
|
8
|
+
- This overwrites the entire file — use `edit` for targeted changes
|
|
9
|
+
- The file is atomic: it either writes completely or fails
|
package/src/tools.mjs
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* tools.mjs — 内置工具集
|
|
3
|
-
* read / write / edit / bash / glob / grep,零依赖实现。
|
|
3
|
+
* read / write / edit / bash / glob / grep / websearch / ls / fetch / delete / git_diff / git_status / git_log / question,零依赖实现。
|
|
4
|
+
* 工具描述从 src/tools/*.md 加载(方便人读和修改)。
|
|
4
5
|
* readonly 标记供 agent 调度:只读工具可并行,有副作用工具串行。
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
import { spawn } from "node:child_process"
|
|
8
|
-
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises"
|
|
9
|
-
import {
|
|
8
|
+
import { spawn, execFileSync } from "node:child_process"
|
|
9
|
+
import { mkdir, readFile, readdir, stat, writeFile, unlink } from "node:fs/promises"
|
|
10
|
+
import { readFileSync, existsSync } from "node:fs"
|
|
11
|
+
import { dirname, join, resolve, relative } from "node:path"
|
|
12
|
+
import { fileURLToPath } from "node:url"
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
15
|
+
const DESC = (name) => readFileSync(join(__dirname, "tools", `${name}.md`), "utf8")
|
|
10
16
|
|
|
11
17
|
const MAX_READ_LINES = 2000
|
|
12
18
|
const MAX_OUTPUT_CHARS = 50_000
|
|
@@ -48,8 +54,7 @@ function resolveInCwd(ctx, p) {
|
|
|
48
54
|
|
|
49
55
|
const readTool = {
|
|
50
56
|
name: "read",
|
|
51
|
-
description:
|
|
52
|
-
"Read a text file. Returns numbered lines. Use offset/limit to page large files.",
|
|
57
|
+
description: DESC("read"),
|
|
53
58
|
parameters: {
|
|
54
59
|
type: "object",
|
|
55
60
|
properties: {
|
|
@@ -77,7 +82,7 @@ const readTool = {
|
|
|
77
82
|
|
|
78
83
|
const writeTool = {
|
|
79
84
|
name: "write",
|
|
80
|
-
description: "
|
|
85
|
+
description: DESC("write"),
|
|
81
86
|
parameters: {
|
|
82
87
|
type: "object",
|
|
83
88
|
properties: {
|
|
@@ -99,8 +104,7 @@ const writeTool = {
|
|
|
99
104
|
|
|
100
105
|
const editTool = {
|
|
101
106
|
name: "edit",
|
|
102
|
-
description:
|
|
103
|
-
"Edit a file by exact string replacement. old_string must match exactly once unless replace_all is set.",
|
|
107
|
+
description: DESC("edit"),
|
|
104
108
|
parameters: {
|
|
105
109
|
type: "object",
|
|
106
110
|
properties: {
|
|
@@ -134,8 +138,7 @@ const editTool = {
|
|
|
134
138
|
|
|
135
139
|
const bashTool = {
|
|
136
140
|
name: "bash",
|
|
137
|
-
description:
|
|
138
|
-
"Execute a shell command and return stdout+stderr. Use for running commands, builds, tests.",
|
|
141
|
+
description: DESC("bash"),
|
|
139
142
|
parameters: {
|
|
140
143
|
type: "object",
|
|
141
144
|
properties: {
|
|
@@ -210,6 +213,10 @@ const bashTool = {
|
|
|
210
213
|
child.stderr.on("data", onData)
|
|
211
214
|
|
|
212
215
|
const timer = setTimeout(() => child.kill(), args.timeout ?? BASH_TIMEOUT_MS)
|
|
216
|
+
// 用户中止:杀进程
|
|
217
|
+
if (ctx.signal) {
|
|
218
|
+
ctx.signal.addEventListener("abort", () => child.kill(), { once: true })
|
|
219
|
+
}
|
|
213
220
|
child.on("error", (error) => {
|
|
214
221
|
clearTimeout(timer)
|
|
215
222
|
resolve(truncate(`Command failed: ${error.message}\n${out}`))
|
|
@@ -218,7 +225,7 @@ const bashTool = {
|
|
|
218
225
|
clearTimeout(timer)
|
|
219
226
|
out += sanitizeOutput(feed(Buffer.alloc(0), true)) // 最终判定 + 冲刷解码器尾部
|
|
220
227
|
const suffix = signal
|
|
221
|
-
?
|
|
228
|
+
? `\n(killed: ${ctx.signal?.aborted ? "user interrupted" : "timeout"})`
|
|
222
229
|
: code !== 0
|
|
223
230
|
? `\n(exit code ${code})`
|
|
224
231
|
: ""
|
|
@@ -232,7 +239,7 @@ const bashTool = {
|
|
|
232
239
|
|
|
233
240
|
const globTool = {
|
|
234
241
|
name: "glob",
|
|
235
|
-
description: "
|
|
242
|
+
description: DESC("glob"),
|
|
236
243
|
parameters: {
|
|
237
244
|
type: "object",
|
|
238
245
|
properties: {
|
|
@@ -295,7 +302,7 @@ function globToRegex(pattern) {
|
|
|
295
302
|
|
|
296
303
|
const grepTool = {
|
|
297
304
|
name: "grep",
|
|
298
|
-
description: "
|
|
305
|
+
description: DESC("grep"),
|
|
299
306
|
parameters: {
|
|
300
307
|
type: "object",
|
|
301
308
|
properties: {
|
|
@@ -357,8 +364,7 @@ const grepTool = {
|
|
|
357
364
|
|
|
358
365
|
const websearchTool = {
|
|
359
366
|
name: "websearch",
|
|
360
|
-
description:
|
|
361
|
-
"Search the web (Bing). Returns result titles, URLs, and snippets. Use for looking up current information, docs, error messages.",
|
|
367
|
+
description: DESC("websearch"),
|
|
362
368
|
parameters: {
|
|
363
369
|
type: "object",
|
|
364
370
|
properties: {
|
|
@@ -422,8 +428,7 @@ function stripTags(html) {
|
|
|
422
428
|
|
|
423
429
|
const lsTool = {
|
|
424
430
|
name: "ls",
|
|
425
|
-
description:
|
|
426
|
-
"List directory contents with type, size, and modification time. Directories listed first. Use to see what a directory contains (glob only matches files).",
|
|
431
|
+
description: DESC("ls"),
|
|
427
432
|
parameters: {
|
|
428
433
|
type: "object",
|
|
429
434
|
properties: {
|
|
@@ -457,8 +462,7 @@ const lsTool = {
|
|
|
457
462
|
|
|
458
463
|
const fetchTool = {
|
|
459
464
|
name: "fetch",
|
|
460
|
-
description:
|
|
461
|
-
"Fetch a URL and return its content as text (HTML pages are stripped to readable text). Use after websearch to read full documents.",
|
|
465
|
+
description: DESC("fetch"),
|
|
462
466
|
parameters: {
|
|
463
467
|
type: "object",
|
|
464
468
|
properties: {
|
|
@@ -511,3 +515,170 @@ function htmlToText(html) {
|
|
|
511
515
|
}
|
|
512
516
|
|
|
513
517
|
export const builtinTools = [readTool, writeTool, editTool, bashTool, globTool, grepTool, websearchTool, lsTool, fetchTool]
|
|
518
|
+
|
|
519
|
+
// ---------------------------------------------------------------- delete
|
|
520
|
+
|
|
521
|
+
const deleteTool = {
|
|
522
|
+
name: "delete",
|
|
523
|
+
description: DESC("delete"),
|
|
524
|
+
parameters: {
|
|
525
|
+
type: "object",
|
|
526
|
+
properties: {
|
|
527
|
+
path: { type: "string", description: "File path (relative to cwd or absolute)" },
|
|
528
|
+
force: { type: "boolean", description: "Allow deleting git-tracked files (default false)" },
|
|
529
|
+
},
|
|
530
|
+
required: ["path"],
|
|
531
|
+
},
|
|
532
|
+
readonly: false,
|
|
533
|
+
async execute(args, ctx) {
|
|
534
|
+
const abs = resolveInCwd(ctx, args.path)
|
|
535
|
+
if (!existsSync(abs)) throw new Error(`File not found: ${abs}`)
|
|
536
|
+
const s = await stat(abs)
|
|
537
|
+
if (s.isDirectory()) throw new Error(`"${args.path}" is a directory — use bash to remove directories`)
|
|
538
|
+
// git 跟踪文件拒绝直接删除(安全网);未跟踪的放行
|
|
539
|
+
// 用解析后的相对路径(统一正斜杠),防反斜杠/非常规路径绕过 ls-files 匹配
|
|
540
|
+
const rel = relative(ctx.cwd, abs).replace(/\\/g, "/")
|
|
541
|
+
let tracked = false
|
|
542
|
+
try {
|
|
543
|
+
execFileSync("git", ["ls-files", "--error-unmatch", "--", rel], { cwd: ctx.cwd, stdio: "ignore" })
|
|
544
|
+
tracked = true
|
|
545
|
+
} catch {
|
|
546
|
+
// 未跟踪 / 非 git 仓库
|
|
547
|
+
}
|
|
548
|
+
if (tracked && !args.force) throw new Error(`"${args.path}" is git-tracked. Set force=true to delete anyway.`)
|
|
549
|
+
await unlink(abs)
|
|
550
|
+
return `Deleted ${abs}`
|
|
551
|
+
},
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ---------------------------------------------------------------- git_diff
|
|
555
|
+
|
|
556
|
+
const gitDiffTool = {
|
|
557
|
+
name: "git_diff",
|
|
558
|
+
description: DESC("git_diff"),
|
|
559
|
+
parameters: {
|
|
560
|
+
type: "object",
|
|
561
|
+
properties: {
|
|
562
|
+
staged: { type: "boolean", description: "Show staged changes (default false)" },
|
|
563
|
+
path: { type: "string", description: "File or directory to diff (default all)" },
|
|
564
|
+
ref: { type: "string", description: "Compare against this ref (default HEAD)" },
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
readonly: true,
|
|
568
|
+
execute(args, ctx) {
|
|
569
|
+
const ref = args.ref ?? "HEAD"
|
|
570
|
+
const flags = args.staged ? ["--staged"] : []
|
|
571
|
+
const paths = args.path ? [args.path] : []
|
|
572
|
+
const out = runGit(ctx.cwd, ["diff", ...flags, ref, "--", ...paths])
|
|
573
|
+
return truncate(out || "(no changes)")
|
|
574
|
+
},
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ---------------------------------------------------------------- git_status
|
|
578
|
+
|
|
579
|
+
const gitStatusTool = {
|
|
580
|
+
name: "git_status",
|
|
581
|
+
description: DESC("git_status"),
|
|
582
|
+
parameters: {
|
|
583
|
+
type: "object",
|
|
584
|
+
properties: {},
|
|
585
|
+
},
|
|
586
|
+
readonly: true,
|
|
587
|
+
execute(_args, ctx) {
|
|
588
|
+
const porcelain = runGit(ctx.cwd, ["status", "--porcelain"])
|
|
589
|
+
if (!porcelain) return "(clean — no changes)"
|
|
590
|
+
|
|
591
|
+
const staged = []
|
|
592
|
+
const unstaged = []
|
|
593
|
+
const untracked = []
|
|
594
|
+
const conflicts = []
|
|
595
|
+
for (const line of porcelain.split("\n")) {
|
|
596
|
+
if (!line) continue
|
|
597
|
+
// porcelain: XY path — 2 状态字符 + 空格 + 文件路径(部分环境只 1 空格)
|
|
598
|
+
// 去掉可能的 CR(execFileSync 在某些 Windows git 下会残留 \r 在行末但不在换行符中)
|
|
599
|
+
const clean = line.replace(/\r/g, "")
|
|
600
|
+
// 尝试匹配 "XY path" 或 "XY path"(可变间距)
|
|
601
|
+
const m = clean.match(/^(..?)\s+(.+)$/)
|
|
602
|
+
if (!m) continue
|
|
603
|
+
const [, status, file] = m
|
|
604
|
+
const idx = status[0] ?? " "
|
|
605
|
+
const wt = status[1] ?? " "
|
|
606
|
+
if (idx === "U" || wt === "U" || (idx === "A" && wt === "A")) {
|
|
607
|
+
conflicts.push(file)
|
|
608
|
+
} else if (idx === "?" && wt === "?") {
|
|
609
|
+
untracked.push(file)
|
|
610
|
+
} else {
|
|
611
|
+
if (idx !== " " && idx !== "?") staged.push(idx + " " + file)
|
|
612
|
+
if (wt !== " " && wt !== "?") unstaged.push(wt + " " + file)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const parts = []
|
|
616
|
+
if (staged.length) parts.push("Staged (" + staged.length + "):\n" + staged.join("\n"))
|
|
617
|
+
if (unstaged.length) parts.push("Unstaged (" + unstaged.length + "):\n" + unstaged.join("\n"))
|
|
618
|
+
if (untracked.length) parts.push("Untracked (" + untracked.length + "):\n" + untracked.join("\n"))
|
|
619
|
+
if (conflicts.length) parts.push("Conflicts (" + conflicts.length + "):\n" + conflicts.join("\n"))
|
|
620
|
+
return truncate(parts.join("\n\n"))
|
|
621
|
+
},
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ---------------------------------------------------------------- git_log
|
|
625
|
+
|
|
626
|
+
const gitLogTool = {
|
|
627
|
+
name: "git_log",
|
|
628
|
+
description: DESC("git_log"),
|
|
629
|
+
parameters: {
|
|
630
|
+
type: "object",
|
|
631
|
+
properties: {
|
|
632
|
+
count: { type: "number", description: "Number of commits (default 10)" },
|
|
633
|
+
path: { type: "string", description: "File or directory (default all)" },
|
|
634
|
+
oneline: { type: "boolean", description: "One-line-per-commit format (default false)" },
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
readonly: true,
|
|
638
|
+
execute(args, ctx) {
|
|
639
|
+
const n = args.count ?? 10
|
|
640
|
+
const isOneline = args.oneline
|
|
641
|
+
const cmdArgs = isOneline
|
|
642
|
+
? ["log", "-" + n, "--oneline"]
|
|
643
|
+
: ["log", "-" + n, "--format=%h %ad %an %s", "--date=short"]
|
|
644
|
+
if (args.path) cmdArgs.push("--", args.path)
|
|
645
|
+
const out = runGit(ctx.cwd, cmdArgs)
|
|
646
|
+
return truncate(out || "(no commits)")
|
|
647
|
+
},
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ---------------------------------------------------------------- question
|
|
651
|
+
|
|
652
|
+
const questionTool = {
|
|
653
|
+
name: "question",
|
|
654
|
+
description: DESC("question"),
|
|
655
|
+
parameters: {
|
|
656
|
+
type: "object",
|
|
657
|
+
properties: {
|
|
658
|
+
question: { type: "string", description: "The question to ask the user" },
|
|
659
|
+
options: {
|
|
660
|
+
type: "array",
|
|
661
|
+
items: { type: "string" },
|
|
662
|
+
description: "Single-choice options for the user to pick from (optional)",
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
required: ["question"],
|
|
666
|
+
},
|
|
667
|
+
readonly: true,
|
|
668
|
+
async execute(args, ctx) {
|
|
669
|
+
if (!ctx.onQuestion) throw new Error("question tool not supported in this context (no UI to ask)")
|
|
670
|
+
return ctx.onQuestion(args.question, args.options ?? [])
|
|
671
|
+
},
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** 执行 git 命令;非 git 仓库 / git 不可用时返回空字符串 */
|
|
675
|
+
function runGit(cwd, cmdArgs) {
|
|
676
|
+
try {
|
|
677
|
+
return execFileSync("git", cmdArgs, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim().replace(/\r/g, "")
|
|
678
|
+
} catch {
|
|
679
|
+
return ""
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export { deleteTool, gitDiffTool, gitStatusTool, gitLogTool, questionTool }
|
|
684
|
+
builtinTools.push(deleteTool, gitDiffTool, gitStatusTool, gitLogTool, questionTool)
|