thincoder 0.12.37 → 0.12.39
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/CHANGELOG.md +303 -0
- package/package.json +2 -1
- package/src/agent-tools/consult.mjs +48 -9
- package/src/agent-tools/verify.mjs +13 -8
- package/src/agent.mjs +12 -1
- package/src/config.mjs +16 -4
- package/src/context.mjs +148 -2
- package/src/prompts/discipline.md +8 -4
- package/src/prompts/main.md +4 -2
- package/src/tools/bash.md +1 -0
- package/src/tools/codemode.mjs +125 -134
- package/src/tools/exec-prelude.mjs +84 -0
- package/src/tools/execute.md +14 -2
- package/src/tools/file_ops.md +16 -0
- package/src/tools/get_current_time.md +6 -0
- package/src/tools/git.md +10 -4
- package/src/tools/git.mjs +60 -8
- package/src/tools/grep.md +3 -1
- package/src/tools/index.mjs +7 -1
- package/src/tools/ls.md +1 -0
- package/src/tools/ops.mjs +142 -0
- package/src/tools/process.md +10 -0
- package/src/tools/sleep.md +5 -0
- package/src/tools/system.mjs +28 -6
- package/src/tools/tree.md +13 -0
- package/src/tools/tree.mjs +66 -0
package/src/tools/git.md
CHANGED
|
@@ -2,14 +2,20 @@ Run a git command. Use this to see uncommitted changes, staged changes, diff aga
|
|
|
2
2
|
- action='diff': Show unified diff — what changed since last commit. Set staged=true for staged-only diff, ref=<ref> to compare against a specific commit/branch, path=<dir> to scope to a file or directory.
|
|
3
3
|
- action='status': Show working tree state — staged, unstaged, untracked files, and conflicts. Returns categorized lists.
|
|
4
4
|
- action='log': Show recent commit history. Set count to limit, oneline=true for compact format, path=<file> to see history of one file.
|
|
5
|
+
- action='show': Show a commit's details (--stat). Set ref=<ref> to inspect a specific commit (default HEAD).
|
|
5
6
|
- action='checkpoint': Manage git-based snapshots. Use checkpointAction to choose: list (overview), create (snapshot now), rewind (restore snapshot by id), cat (read a file from a snapshot).
|
|
7
|
+
- action='rm': Untrack a file/directory (git rm --cached — keeps the file on disk). path is required.
|
|
8
|
+
- action='commit': Stage all changes and commit. message is required. Confirms with the user (outward action).
|
|
9
|
+
- action='push': Push the current branch to the remote. Confirms with the user (outward action).
|
|
6
10
|
|
|
7
11
|
Parameters:
|
|
8
|
-
- action (required): diff / status / log / checkpoint
|
|
12
|
+
- action (required): diff / status / log / show / checkpoint / rm / commit / push
|
|
9
13
|
- staged: (diff) Show staged changes instead of working tree
|
|
10
|
-
- path: (diff/log/checkpoint:cat/checkpoint:rewind) File or directory to scope to
|
|
11
|
-
- ref: (
|
|
14
|
+
- path: (diff/log/checkpoint:cat/checkpoint:rewind/rm) File or directory to scope to
|
|
15
|
+
- ref: (show) Commit ref to inspect (default HEAD)
|
|
12
16
|
- count: (log) Number of commits (default 10)
|
|
13
17
|
- oneline: (log) One-line-per-commit format
|
|
18
|
+
- message: (commit) Commit message — required for commit
|
|
19
|
+
- filter: Optional — keep only status/diff/log output lines matching this regex (case-insensitive)
|
|
14
20
|
- checkpointAction: (checkpoint) list snapshots / create one / restore by id / read file from snapshot
|
|
15
|
-
- checkpointId: (checkpoint) Snapshot id — required for rewind and cat; optional for list (shows file tree)
|
|
21
|
+
- checkpointId: (checkpoint) Snapshot id — required for rewind and cat; optional for list (shows file tree)
|
package/src/tools/git.mjs
CHANGED
|
@@ -7,19 +7,44 @@ import { escapeXml } from "../agent/helpers.mjs";
|
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
|
|
10
|
+
/** Keep only output lines matching a regex (git filter, case-insensitive). */
|
|
11
|
+
function filterLines(output, filter) {
|
|
12
|
+
if (!filter) return output
|
|
13
|
+
try {
|
|
14
|
+
const re = new RegExp(filter, "i")
|
|
15
|
+
const lines = output.split("\n").filter((l) => re.test(l))
|
|
16
|
+
return lines.length ? lines.join("\n") : `(no lines matched filter "${filter}")`
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return `Error: filter regex invalid: ${e.message}`
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Run git and report failure (stderr + exit code) instead of swallowing it.
|
|
23
|
+
* Used by write ops (commit/push/rm) where a silent "" would masquerade as success. */
|
|
24
|
+
function runGitStrict(cwd, cmdArgs) {
|
|
25
|
+
try {
|
|
26
|
+
const out = execFileSync("git", cmdArgs, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim().replace(/\r/g, "")
|
|
27
|
+
return { ok: true, out }
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return { ok: false, out: String(e.stdout || "").trim(), err: String(e.stderr || e.message || "").trim() }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
10
33
|
export const gitTool = {
|
|
11
34
|
name: "git",
|
|
12
35
|
description: DESC("git"),
|
|
13
36
|
parameters: {
|
|
14
37
|
type: "object",
|
|
15
38
|
properties: {
|
|
16
|
-
action: { type: "string", enum: ["diff", "status", "log", "checkpoint"], description: "diff / status / log / checkpoint" },
|
|
39
|
+
action: { type: "string", enum: ["diff", "status", "log", "show", "checkpoint", "rm", "commit", "push"], description: "diff / status / log / show / checkpoint / rm / commit / push" },
|
|
17
40
|
// diff/log params
|
|
18
41
|
staged: { type: "boolean", description: "(diff) Show staged changes instead of working tree" },
|
|
19
|
-
path: { type: "string", description: "(diff/log/checkpoint:cat/versions/rewind) File or directory to scope to" },
|
|
20
|
-
ref: { type: "string", description: "(
|
|
42
|
+
path: { type: "string", description: "(diff/log/checkpoint:cat/versions/rewind/rm) File or directory to scope to" },
|
|
43
|
+
ref: { type: "string", description: "(show) Commit ref to show (default HEAD)" },
|
|
21
44
|
count: { type: "number", description: "(log) Number of commits (default 10)" },
|
|
22
45
|
oneline: { type: "boolean", description: "(log) One-line-per-commit format" },
|
|
46
|
+
message: { type: "string", description: "(commit) Commit message — required for commit" },
|
|
47
|
+
filter: { type: "string", description: "Optional: keep only status/diff/log output lines matching this regex (case-insensitive)" },
|
|
23
48
|
// checkpoint params
|
|
24
49
|
checkpointAction: { type: "string", enum: ["list", "create", "rewind", "cat", "versions"], description: "(checkpoint) list snapshots / create one / restore by id / read file from snapshot / list a file's historical versions" },
|
|
25
50
|
checkpointId: { type: "string", description: "(checkpoint) Snapshot id — required for rewind and cat; optional for list (shows file tree)" },
|
|
@@ -35,7 +60,7 @@ export const gitTool = {
|
|
|
35
60
|
const flags = args.staged ? ["--staged"] : []
|
|
36
61
|
const paths = args.path ? [args.path] : []
|
|
37
62
|
const out = runGit(ctx.cwd, ["diff", ...flags, ref, "--", ...paths])
|
|
38
|
-
return truncate(out || "(no changes)")
|
|
63
|
+
return truncate(filterLines(out || "(no changes)", args.filter))
|
|
39
64
|
}
|
|
40
65
|
case "status": {
|
|
41
66
|
const porcelain = runGit(ctx.cwd, ["status", "--porcelain"])
|
|
@@ -68,17 +93,44 @@ export const gitTool = {
|
|
|
68
93
|
if (unstaged.length) parts.push("Unstaged (" + unstaged.length + "):\n" + unstaged.join("\n"))
|
|
69
94
|
if (untracked.length) parts.push("Untracked (" + untracked.length + "):\n" + untracked.join("\n"))
|
|
70
95
|
if (conflicts.length) parts.push("Conflicts (" + conflicts.length + "):\n" + conflicts.join("\n"))
|
|
71
|
-
return truncate(parts.join("\n\n"))
|
|
96
|
+
return truncate(filterLines(parts.join("\n\n"), args.filter))
|
|
72
97
|
}
|
|
73
98
|
case "log": {
|
|
74
|
-
const
|
|
99
|
+
const parsed = Number.parseInt(args.count, 10)
|
|
100
|
+
const n = Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, 200) : 10
|
|
75
101
|
const isOneline = args.oneline
|
|
76
102
|
const cmdArgs = isOneline
|
|
77
103
|
? ["log", "-" + n, "--oneline"]
|
|
78
104
|
: ["log", "-" + n, "--format=%h %ad %an %s", "--date=short"]
|
|
79
105
|
if (args.path) cmdArgs.push("--", args.path)
|
|
80
106
|
const out = runGit(ctx.cwd, cmdArgs)
|
|
81
|
-
return truncate(out || "(no commits)")
|
|
107
|
+
return truncate(filterLines(out || "(no commits)", args.filter))
|
|
108
|
+
}
|
|
109
|
+
case "show": {
|
|
110
|
+
const ref = args.ref ?? "HEAD"
|
|
111
|
+
if (!/^[A-Za-z0-9._\/~^@][A-Za-z0-9._\/~^@{}\-]*$/.test(ref)) throw new Error(`Invalid git ref: ${ref}`)
|
|
112
|
+
const out = runGit(ctx.cwd, ["show", "--stat", ref])
|
|
113
|
+
return truncate(out || "(no such commit)")
|
|
114
|
+
}
|
|
115
|
+
case "rm": {
|
|
116
|
+
if (!args.path) return "Error: rm requires path (the file/directory to untrack, relative to repo root)"
|
|
117
|
+
const r = runGitStrict(ctx.cwd, ["rm", "--cached", "-r", "--", args.path])
|
|
118
|
+
return r.ok ? truncate(r.out || `Untracked ${args.path} (kept on disk)`) : truncate(`git rm failed: ${r.err || r.out}`)
|
|
119
|
+
}
|
|
120
|
+
case "commit": {
|
|
121
|
+
if (!args.message) return "Error: commit requires message"
|
|
122
|
+
const add = runGitStrict(ctx.cwd, ["add", "-A"])
|
|
123
|
+
if (!add.ok) return truncate(`git add failed: ${add.err || add.out || "(no output)"}`)
|
|
124
|
+
const commit = runGitStrict(ctx.cwd, ["commit", "-m", args.message])
|
|
125
|
+
const parts = []
|
|
126
|
+
if (add.out) parts.push(add.out)
|
|
127
|
+
if (commit.ok) { if (commit.out) parts.push(commit.out) }
|
|
128
|
+
else parts.push(`git commit failed: ${commit.err || "(no output)"}`)
|
|
129
|
+
return truncate(parts.join("\n") || "(commit produced no output)")
|
|
130
|
+
}
|
|
131
|
+
case "push": {
|
|
132
|
+
const r = runGitStrict(ctx.cwd, ["push"])
|
|
133
|
+
return r.ok ? truncate(r.out || "(push complete — no output)") : truncate(`git push failed: ${r.err || r.out || "(no output)"}`)
|
|
82
134
|
}
|
|
83
135
|
case "checkpoint": {
|
|
84
136
|
const { createCheckpoint, listCheckpoints, rewind, listFileVersions, isGitRepo } = await import("../git/checkpoint.mjs")
|
|
@@ -139,7 +191,7 @@ export const gitTool = {
|
|
|
139
191
|
throw new Error(`Unknown checkpoint action: ${sub}. Use: list | create | rewind | cat | versions`)
|
|
140
192
|
}
|
|
141
193
|
default:
|
|
142
|
-
return `Unknown action '${args.action}'. Use: diff | status | log | checkpoint`
|
|
194
|
+
return `Unknown action '${args.action}'. Use: diff | status | log | show | checkpoint | rm | commit | push`
|
|
143
195
|
}
|
|
144
196
|
},
|
|
145
197
|
}
|
package/src/tools/grep.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
Search file contents with a regex. Returns matching lines as path:line: content.
|
|
2
2
|
|
|
3
3
|
Parameters:
|
|
4
|
-
- pattern (required): JavaScript regular expression
|
|
4
|
+
- pattern (required): JavaScript regular expression, or a literal string when literal=true
|
|
5
5
|
- path: Directory or file to search (default cwd)
|
|
6
6
|
- glob: Only search files matching this glob (e.g. '*.mjs')
|
|
7
|
+
- ignoreCase: Case-insensitive match (default false)
|
|
8
|
+
- literal: Literal string match — no regex interpretation (default false; use for strings with `. \` etc.)
|
|
7
9
|
- before: Lines of context to show before each match (grep -B). Default 0
|
|
8
10
|
- after: Lines of context to show after each match (grep -A). Default 0
|
|
9
11
|
|
package/src/tools/index.mjs
CHANGED
|
@@ -10,6 +10,8 @@ import { checklistTool } from "./checklist.mjs";
|
|
|
10
10
|
import { lintTool } from "./linter.mjs";
|
|
11
11
|
import { lspTool } from "./lsp.mjs";
|
|
12
12
|
import { codeModeTool } from "./codemode.mjs";
|
|
13
|
+
import { fileOpsTool, processTool, getCurrentTimeTool, sleepTool } from "./ops.mjs";
|
|
14
|
+
import { treeTool } from "./tree.mjs";
|
|
13
15
|
|
|
14
16
|
export const builtinTools = [
|
|
15
17
|
readTool, writeTool, editTool, insertAfterTool, hashlineEditTool, applyPatchTool,
|
|
@@ -17,6 +19,8 @@ export const builtinTools = [
|
|
|
17
19
|
websearchTool, lsTool, fetchTool, deleteTool,
|
|
18
20
|
gitTool, questionTool,
|
|
19
21
|
checklistTool, lintTool, lspTool, codeModeTool,
|
|
22
|
+
fileOpsTool, processTool, getCurrentTimeTool, sleepTool,
|
|
23
|
+
treeTool,
|
|
20
24
|
];
|
|
21
25
|
|
|
22
26
|
export {
|
|
@@ -25,4 +29,6 @@ export {
|
|
|
25
29
|
websearchTool, lsTool, fetchTool, deleteTool,
|
|
26
30
|
gitTool, questionTool,
|
|
27
31
|
checklistTool, lintTool, lspTool, codeModeTool,
|
|
28
|
-
|
|
32
|
+
fileOpsTool, processTool, getCurrentTimeTool, sleepTool,
|
|
33
|
+
treeTool,
|
|
34
|
+
};
|
package/src/tools/ls.md
CHANGED
|
@@ -2,6 +2,7 @@ List directory contents with type, size, and modification time. Directories list
|
|
|
2
2
|
|
|
3
3
|
Parameters:
|
|
4
4
|
- path: Directory path (default cwd)
|
|
5
|
+
- filter: Only list entries matching this glob (e.g. '*.mjs', '*test*') — a wildcard filter, not a full listing
|
|
5
6
|
|
|
6
7
|
Notes:
|
|
7
8
|
- Shows first 500 entries
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ops.mjs — operational tools: file_ops (move/copy/rename), process (list),
|
|
3
|
+
* get_current_time, sleep. Each exists so the model reaches for a dedicated tool
|
|
4
|
+
* instead of shelling out to `bash` for the same operation (parity with thinworker).
|
|
5
|
+
*/
|
|
6
|
+
import { DESC, resolveInCwd, truncate } from "./shared.mjs"
|
|
7
|
+
import { cp, rename, rm } from "node:fs/promises"
|
|
8
|
+
import { execFileSync } from "node:child_process"
|
|
9
|
+
|
|
10
|
+
// ─── file_ops ──────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export const fileOpsTool = {
|
|
13
|
+
name: "file_ops",
|
|
14
|
+
description: DESC("file_ops"),
|
|
15
|
+
parameters: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
action: { type: "string", enum: ["move", "copy", "rename"], description: "move | copy | rename" },
|
|
19
|
+
source: { type: "string", description: "Source path, relative to cwd or absolute" },
|
|
20
|
+
dest: { type: "string", description: "Destination path" },
|
|
21
|
+
},
|
|
22
|
+
required: ["action", "source", "dest"],
|
|
23
|
+
},
|
|
24
|
+
readonly: false,
|
|
25
|
+
async execute({ action, source, dest }, ctx) {
|
|
26
|
+
if (typeof source !== "string" || !source) return "Error: source is required"
|
|
27
|
+
if (typeof dest !== "string" || !dest) return "Error: dest is required"
|
|
28
|
+
if (!["move", "copy", "rename"].includes(action)) return `Error: action must be move | copy | rename (got "${action}")`
|
|
29
|
+
const src = resolveInCwd(ctx, source)
|
|
30
|
+
const dst = resolveInCwd(ctx, dest)
|
|
31
|
+
if (src === dst) return "Error: source and dest resolve to the same path"
|
|
32
|
+
|
|
33
|
+
if (action === "copy") {
|
|
34
|
+
await cp(src, dst, { recursive: true, force: true })
|
|
35
|
+
return `Copied ${source} → ${dest}`
|
|
36
|
+
}
|
|
37
|
+
// move & rename share the rename syscall; cross-device move falls back to copy+rm.
|
|
38
|
+
try {
|
|
39
|
+
await rename(src, dst)
|
|
40
|
+
} catch (e) {
|
|
41
|
+
if (e?.code !== "EXDEV") throw e
|
|
42
|
+
await cp(src, dst, { recursive: true, force: true })
|
|
43
|
+
await rm(src, { recursive: true, force: true })
|
|
44
|
+
}
|
|
45
|
+
return `${action === "rename" ? "Renamed" : "Moved"} ${source} → ${dest}`
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── process ───────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export const processTool = {
|
|
52
|
+
name: "process",
|
|
53
|
+
description: DESC("process"),
|
|
54
|
+
parameters: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
name: { type: "string", description: "Optional name substring filter (case-insensitive)" },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
readonly: true,
|
|
61
|
+
async execute({ name }, ctx) {
|
|
62
|
+
const filter = typeof name === "string" && name.trim() ? name.trim().toLowerCase() : null
|
|
63
|
+
let rows
|
|
64
|
+
try {
|
|
65
|
+
rows = process.platform === "win32" ? listWindows() : listPosix()
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return `process listing failed: ${e?.message ?? String(e)}`
|
|
68
|
+
}
|
|
69
|
+
if (filter) rows = rows.filter((r) => r.name.toLowerCase().includes(filter))
|
|
70
|
+
if (rows.length === 0) return filter ? `No running processes match "${name}"` : "(no processes)"
|
|
71
|
+
return truncate(rows.map((r) => `${r.name}\tPID ${r.pid}${r.mem ? `\t${r.mem}` : ""}`).join("\n"))
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function listWindows() {
|
|
76
|
+
// tasklist /FO CSV /NH → lines: "name.exe","1234","Console","1","12,345 K"
|
|
77
|
+
const out = execFileSync("tasklist", ["/FO", "CSV", "/NH"], { encoding: "utf8", timeout: 10000 })
|
|
78
|
+
const rows = []
|
|
79
|
+
for (const line of out.split("\n")) {
|
|
80
|
+
const parts = line.split('","')
|
|
81
|
+
if (parts.length < 2) continue
|
|
82
|
+
const name = parts[0].replace(/^"/, "").trim()
|
|
83
|
+
const pid = parts[1].replace(/"/, "").trim()
|
|
84
|
+
const mem = parts[4] ? parts[4].replace(/"/, "").trim() : ""
|
|
85
|
+
if (!name || !pid) continue
|
|
86
|
+
rows.push({ name, pid, mem })
|
|
87
|
+
}
|
|
88
|
+
return rows
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function listPosix() {
|
|
92
|
+
const out = execFileSync("ps", ["-eo", "pid=,comm="], { encoding: "utf8", timeout: 10000 })
|
|
93
|
+
const rows = []
|
|
94
|
+
for (const line of out.split("\n")) {
|
|
95
|
+
const m = line.trim().match(/^(\d+)\s+(.+)$/)
|
|
96
|
+
if (m) rows.push({ name: m[2], pid: m[1], mem: "" })
|
|
97
|
+
}
|
|
98
|
+
return rows
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── get_current_time ──────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
export const getCurrentTimeTool = {
|
|
104
|
+
name: "get_current_time",
|
|
105
|
+
description: DESC("get_current_time"),
|
|
106
|
+
parameters: { type: "object", properties: {} },
|
|
107
|
+
readonly: true,
|
|
108
|
+
async execute() {
|
|
109
|
+
const now = new Date()
|
|
110
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "unknown"
|
|
111
|
+
const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
|
|
112
|
+
return `Date: ${now.toISOString()} (UTC)\nTimezone: ${tz}\nWeekday: ${days[now.getDay()]}\nLocal: ${now.toLocaleString()}`
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── sleep ─────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
export const sleepTool = {
|
|
119
|
+
name: "sleep",
|
|
120
|
+
description: DESC("sleep"),
|
|
121
|
+
parameters: {
|
|
122
|
+
type: "object",
|
|
123
|
+
properties: {
|
|
124
|
+
seconds: { type: "number", description: "Seconds to wait (1-300)" },
|
|
125
|
+
reason: { type: "string", description: "Why wait (shown to the user)" },
|
|
126
|
+
},
|
|
127
|
+
required: ["seconds"],
|
|
128
|
+
},
|
|
129
|
+
readonly: true,
|
|
130
|
+
async execute({ seconds, reason }, ctx) {
|
|
131
|
+
const raw = Number(seconds)
|
|
132
|
+
const n = Number.isFinite(raw) ? Math.min(Math.max(Math.round(raw), 1), 300) : 1
|
|
133
|
+
await new Promise((resolve, reject) => {
|
|
134
|
+
const t = setTimeout(resolve, n * 1000)
|
|
135
|
+
if (ctx?.signal) {
|
|
136
|
+
if (ctx.signal.aborted) { clearTimeout(t); reject(new Error("aborted")) }
|
|
137
|
+
else ctx.signal.addEventListener("abort", () => { clearTimeout(t); reject(new Error("aborted")) }, { once: true })
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
return `Waited ${n}s${reason ? ` (${reason})` : ""}`
|
|
141
|
+
},
|
|
142
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
List running processes, optionally filtered by name. Returns process name / PID / memory.
|
|
2
|
+
|
|
3
|
+
**Route to process instead of bash:**
|
|
4
|
+
- `tasklist` (Windows) / `ps aux` (POSIX) → process
|
|
5
|
+
|
|
6
|
+
Parameters:
|
|
7
|
+
- name (optional): substring filter (case-insensitive), e.g. "node", "python"
|
|
8
|
+
|
|
9
|
+
Notes:
|
|
10
|
+
- List-only. To kill a process, use `bash taskkill /PID <pid> /F` (Windows) or `bash kill <pid>` — and confirm with the user first.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Wait a number of seconds before continuing. Use to wait for a web page to load, an async task to finish, or to respect a rate limit — cheaper than repeatedly polling.
|
|
2
|
+
|
|
3
|
+
Parameters:
|
|
4
|
+
- seconds (required): how many seconds to wait (1-300)
|
|
5
|
+
- reason (optional): why you are waiting (shown to the user)
|
package/src/tools/system.mjs
CHANGED
|
@@ -17,6 +17,20 @@ import { join } from "node:path";
|
|
|
17
17
|
/** Maximum buffer size per stream (stdout / stderr) before truncation */
|
|
18
18
|
const MAX_STREAM_BUF = 2_000_000
|
|
19
19
|
|
|
20
|
+
/** Escape a string for literal regex matching (grep literal=true). */
|
|
21
|
+
function escapeRegExp(s) {
|
|
22
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Keep only output lines matching a regex (bash filter, case-insensitive). */
|
|
26
|
+
function applyLineFilter(output, filter) {
|
|
27
|
+
let re
|
|
28
|
+
try { re = new RegExp(filter, "i") } catch (e) { return `Error: filter regex invalid: ${e.message}` }
|
|
29
|
+
const lines = output.split("\n").filter((l) => re.test(l))
|
|
30
|
+
if (lines.length === 0) return `(no output lines matched filter "${filter}")`
|
|
31
|
+
return truncate(lines.join("\n"))
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
// ====================================================================
|
|
21
35
|
// bash — command execution with safety gates
|
|
22
36
|
// ====================================================================
|
|
@@ -223,6 +237,7 @@ export const bashTool = {
|
|
|
223
237
|
properties: {
|
|
224
238
|
command: { type: "string", description: "Shell command to execute" },
|
|
225
239
|
timeout: { type: "number", description: `Timeout in ms (default ${BASH_TIMEOUT_MS})` },
|
|
240
|
+
filter: { type: "string", description: "Optional: only return output lines matching this regex (case-insensitive)" },
|
|
226
241
|
},
|
|
227
242
|
required: ["command"],
|
|
228
243
|
},
|
|
@@ -241,7 +256,8 @@ export const bashTool = {
|
|
|
241
256
|
onOutput: ctx.onOutput,
|
|
242
257
|
shell: ctx.agent?.config?.shell ?? null,
|
|
243
258
|
})
|
|
244
|
-
|
|
259
|
+
const filtered = args.filter ? applyLineFilter(result, args.filter) : result
|
|
260
|
+
return guard ? `${guard.notice}\n\n${filtered}` : filtered
|
|
245
261
|
},
|
|
246
262
|
}
|
|
247
263
|
|
|
@@ -305,9 +321,11 @@ export const grepTool = {
|
|
|
305
321
|
parameters: {
|
|
306
322
|
type: "object",
|
|
307
323
|
properties: {
|
|
308
|
-
pattern: { type: "string", description: "Regular expression" },
|
|
324
|
+
pattern: { type: "string", description: "Regular expression, or a literal string when literal=true" },
|
|
309
325
|
path: { type: "string", description: "Directory or file to search (default cwd)" },
|
|
310
326
|
glob: { type: "string", description: "Only search files matching this glob (e.g. '*.mjs')" },
|
|
327
|
+
ignoreCase: { type: "boolean", description: "Case-insensitive match (default false)" },
|
|
328
|
+
literal: { type: "boolean", description: "Literal string match — no regex interpretation (default false)" },
|
|
311
329
|
before: { type: "integer", description: "Lines of context to show before each match (grep -B). Default 0" },
|
|
312
330
|
after: { type: "integer", description: "Lines of context to show after each match (grep -A). Default 0" },
|
|
313
331
|
},
|
|
@@ -318,7 +336,8 @@ export const grepTool = {
|
|
|
318
336
|
const base = resolveInCwd(ctx, args.path ?? ".")
|
|
319
337
|
let regex
|
|
320
338
|
try {
|
|
321
|
-
|
|
339
|
+
const pat = args.literal ? escapeRegExp(String(args.pattern)) : args.pattern
|
|
340
|
+
regex = new RegExp(pat, args.ignoreCase ? "i" : "")
|
|
322
341
|
} catch (e) {
|
|
323
342
|
throw new Error(`grep pattern /${args.pattern}/ is not a valid regex: ${e.message}`)
|
|
324
343
|
}
|
|
@@ -409,11 +428,13 @@ export const lsTool = {
|
|
|
409
428
|
type: "object",
|
|
410
429
|
properties: {
|
|
411
430
|
path: { type: "string", description: "Directory path (default cwd)" },
|
|
431
|
+
filter: { type: "string", description: "Only list entries matching this glob (e.g. '*.mjs', '*test*')" },
|
|
412
432
|
},
|
|
413
433
|
},
|
|
414
434
|
readonly: true,
|
|
415
435
|
async execute(args, ctx) {
|
|
416
436
|
const abs = resolveInCwd(ctx, args.path ?? ".")
|
|
437
|
+
const filterRe = args.filter ? globToRegex(args.filter) : null
|
|
417
438
|
let entries
|
|
418
439
|
try {
|
|
419
440
|
entries = await readdir(abs, { withFileTypes: true })
|
|
@@ -422,7 +443,10 @@ export const lsTool = {
|
|
|
422
443
|
throw e
|
|
423
444
|
}
|
|
424
445
|
const rows = await Promise.all(
|
|
425
|
-
entries
|
|
446
|
+
entries
|
|
447
|
+
.filter((e) => !filterRe || filterRe.test(e.name))
|
|
448
|
+
.slice(0, 500)
|
|
449
|
+
.map(async (e) => {
|
|
426
450
|
const s = await stat(join(abs, e.name)).catch(() => null)
|
|
427
451
|
const isDir = e.isDirectory()
|
|
428
452
|
return {
|
|
@@ -439,5 +463,3 @@ export const lsTool = {
|
|
|
439
463
|
return truncate(out.join("\n"))
|
|
440
464
|
},
|
|
441
465
|
}
|
|
442
|
-
|
|
443
|
-
// ---------------------------------------------------------------- fetch
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Generate a directory tree of the codebase (default depth 3). Skips dotfiles, .git/node_modules/dist/build/bin/obj and other build/vendor dirs, and binary files. Use to quickly see which modules exist and where files live.
|
|
2
|
+
|
|
3
|
+
**Route to tree instead of bash:**
|
|
4
|
+
- `tree` / `find .` / `dir /s` → tree
|
|
5
|
+
|
|
6
|
+
Parameters:
|
|
7
|
+
- path: Root directory (default cwd)
|
|
8
|
+
- depth: Tree depth (default 3, max 6). Directories are listed before files, both sorted.
|
|
9
|
+
|
|
10
|
+
Notes:
|
|
11
|
+
- Capped at 200 entries.
|
|
12
|
+
- Directories end with `/`; tree-drawing uses `├──`/`└──`/`│`.
|
|
13
|
+
- Use depth for a shallow overview; use `ls` for one directory, `glob` for a specific file pattern.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tree.mjs — directory tree tool (parity with thinworker `repomap`).
|
|
3
|
+
* Renders a repo's directory tree (default depth 3), skipping dotfiles,
|
|
4
|
+
* build/vendor dirs and binary files, so the model can see which modules
|
|
5
|
+
* exist without shelling out to `tree`/`find`.
|
|
6
|
+
*/
|
|
7
|
+
import { DESC, truncate, resolveInCwd } from "./shared.mjs"
|
|
8
|
+
import { readdir, stat } from "node:fs/promises"
|
|
9
|
+
import { join, basename, extname } from "node:path"
|
|
10
|
+
|
|
11
|
+
const MAX_ENTRIES = 200
|
|
12
|
+
const DEFAULT_DEPTH = 3
|
|
13
|
+
const SKIP_DIRS = new Set(["node_modules", "bin", "obj", "dist", "build", "coverage", "turbo", ".git", ".thincoder", ".vs", ".venv", "__pycache__", ".idea"])
|
|
14
|
+
const BINARY_EXTS = new Set([".exe", ".dll", ".png", ".jpg", ".jpeg", ".gif", ".pdf", ".docx", ".xlsx", ".pptx", ".zip", ".7z", ".mp3", ".mp4", ".woff", ".woff2", ".ico"])
|
|
15
|
+
|
|
16
|
+
export const treeTool = {
|
|
17
|
+
name: "tree",
|
|
18
|
+
description: DESC("tree"),
|
|
19
|
+
parameters: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
path: { type: "string", description: "Root directory (default cwd)" },
|
|
23
|
+
depth: { type: "integer", description: `Tree depth (default ${DEFAULT_DEPTH}, max 6)` },
|
|
24
|
+
},
|
|
25
|
+
required: [],
|
|
26
|
+
},
|
|
27
|
+
readonly: true,
|
|
28
|
+
async execute(args, ctx) {
|
|
29
|
+
const root = resolveInCwd(ctx, args.path ?? ".")
|
|
30
|
+
let st
|
|
31
|
+
try { st = await stat(root) } catch { return `Error: directory not found: ${args.path ?? "."}` }
|
|
32
|
+
if (!st.isDirectory()) return `Error: not a directory: ${args.path ?? "."}`
|
|
33
|
+
const d = Number(args.depth)
|
|
34
|
+
const maxDepth = Number.isInteger(d) && d > 0 ? Math.min(d, 6) : DEFAULT_DEPTH
|
|
35
|
+
const lines = [basename(root) + "/"]
|
|
36
|
+
const state = { count: 1, capped: false }
|
|
37
|
+
await walk(root, 0, maxDepth, "", lines, state)
|
|
38
|
+
return truncate(lines.join("\n"))
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function walk(dir, depth, maxDepth, prefix, lines, state) {
|
|
43
|
+
if (state.capped) return
|
|
44
|
+
let entries
|
|
45
|
+
try { entries = await readdir(dir, { withFileTypes: true }) } catch { return }
|
|
46
|
+
const items = []
|
|
47
|
+
for (const e of entries) {
|
|
48
|
+
if (e.name.startsWith(".")) continue // dotfiles + dotdirs
|
|
49
|
+
const isDir = e.isDirectory()
|
|
50
|
+
if (isDir) { if (SKIP_DIRS.has(e.name)) continue; items.push({ name: e.name, isDir: true }) }
|
|
51
|
+
else if (!BINARY_EXTS.has(extname(e.name).toLowerCase())) items.push({ name: e.name, isDir: false })
|
|
52
|
+
}
|
|
53
|
+
items.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < items.length; i++) {
|
|
56
|
+
if (state.capped) return
|
|
57
|
+
if (state.count >= MAX_ENTRIES) { state.capped = true; lines.push(prefix + "…(更多项已省略)"); return }
|
|
58
|
+
const { name, isDir } = items[i]
|
|
59
|
+
const isLast = i === items.length - 1
|
|
60
|
+
lines.push(prefix + (isLast ? "└── " : "├── ") + (isDir ? name + "/" : name))
|
|
61
|
+
state.count++
|
|
62
|
+
if (isDir && depth + 1 < maxDepth) {
|
|
63
|
+
await walk(join(dir, name), depth + 1, maxDepth, prefix + (isLast ? " " : "│ "), lines, state)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|