thincoder 0.12.53 → 0.12.58
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 +74 -0
- package/bin/thincoder.mjs +17 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +1 -1
- package/src/acp.mjs +60 -18
- package/src/advisor/messages.mjs +4 -2
- package/src/advisor/run.mjs +2 -2
- package/src/agent/dispatch.mjs +66 -26
- package/src/agent/helpers.mjs +13 -2
- package/src/agent/setup.mjs +16 -3
- package/src/agent/spawn-child.mjs +3 -1
- package/src/agent-tools/advisor.mjs +19 -9
- package/src/agent-tools/eng.mjs +2 -0
- package/src/agent-tools/subagent-check.mjs +107 -0
- package/src/agent-tools/subagent.mjs +205 -42
- package/src/agent.mjs +68 -3
- package/src/cli/make-agent.mjs +25 -0
- package/src/cli/memory-command.mjs +28 -7
- package/src/config.mjs +120 -8
- package/src/context.mjs +28 -7
- package/src/escape.mjs +110 -22
- package/src/git/checkpoint.mjs +32 -6
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/core.mjs +78 -10
- package/src/memory/docs.mjs +33 -7
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +23 -0
- package/src/prompts/discipline.md +17 -3
- package/src/prompts/engineering.md +62 -5
- package/src/prompts/main.md +1 -0
- package/src/prompts/system.md +2 -1
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +90 -26
- package/src/provider/google.mjs +57 -24
- package/src/provider/normalize.mjs +1 -1
- package/src/provider/rate.mjs +0 -2
- package/src/provider/responses.mjs +8 -13
- package/src/provider/sse.mjs +20 -0
- package/src/session-migrate.mjs +6 -0
- package/src/session-slots.mjs +361 -0
- package/src/session.mjs +282 -306
- package/src/tools/apply_patch.md +2 -0
- package/src/tools/bash.md +2 -2
- package/src/tools/edit-batch.mjs +104 -0
- package/src/tools/edit.md +3 -0
- package/src/tools/execute.md +4 -4
- package/src/tools/execute.mjs +14 -22
- package/src/tools/file.mjs +17 -55
- package/src/tools/file_ops.md +1 -1
- package/src/tools/git-checkpoint.mjs +143 -0
- package/src/tools/git-ext.mjs +173 -0
- package/src/tools/git.md +21 -8
- package/src/tools/git.mjs +55 -177
- package/src/tools/lint.md +1 -1
- package/src/tools/linter.mjs +9 -37
- package/src/tools/patch.mjs +1 -1
- package/src/tools/shared.mjs +7 -20
- package/src/tui/agent-turn.mjs +3 -3
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +2 -2
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +255 -114
- package/src/tui/cmd-new.mjs +6 -6
- package/src/tui/cmd-restore.mjs +27 -6
- package/src/tui/cmd-session.mjs +17 -4
- package/src/tui/index.mjs +28 -27
- package/src/tui/interaction.mjs +28 -1
- package/src/tui/key-handler.mjs +14 -2
- package/src/tui/layout.mjs +81 -25
- package/src/tui/mouse.mjs +41 -2
- package/src/tui/pickers.mjs +62 -4
- package/src/tui/render-conversation.mjs +36 -93
- package/src/tui/render-frame.mjs +40 -16
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +4 -2
- package/src/tui/subagent-blocks.mjs +119 -4
- package/src/tui/subagent-panel.mjs +81 -0
- package/src/tui/tool-events.mjs +61 -16
- package/src/tui/tui-lifecycle.mjs +45 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git-ext.mjs — git 工具 F7 扩展 action(clone/init/rebase/remote/clean/switch/apply/worktree/
|
|
3
|
+
* archive/blame/mv)+ 共享 git 辅助函数(validateRef/runGitStrict/filterLines/snapshotBefore,
|
|
4
|
+
* 供 git.mjs 核心 action 复用——500 行硬限拆分)。CLI 与 VS Code 两端同构(镜像,修改须两端同批)。
|
|
5
|
+
*/
|
|
6
|
+
import { runGit, truncate } from "./shared.mjs"
|
|
7
|
+
import { execFileSync } from "node:child_process"
|
|
8
|
+
|
|
9
|
+
/** Keep only output lines matching a regex (git filter, case-insensitive). */
|
|
10
|
+
export function filterLines(output, filter) {
|
|
11
|
+
if (!filter) return output
|
|
12
|
+
try {
|
|
13
|
+
const re = new RegExp(filter, "i")
|
|
14
|
+
const lines = output.split("\n").filter((l) => re.test(l))
|
|
15
|
+
return lines.length ? lines.join("\n") : `(no lines matched filter "${filter}")`
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return `Error: filter regex invalid: ${e.message}`
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Run git and report failure (stderr + exit code) instead of swallowing it.
|
|
22
|
+
* Used by write ops (commit/push/rm) where a silent "" would masquerade as success. */
|
|
23
|
+
export function runGitStrict(cwd, cmdArgs, config = []) {
|
|
24
|
+
try {
|
|
25
|
+
const out = execFileSync("git", [...config, ...cmdArgs], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim().replace(/\r/g, "")
|
|
26
|
+
return { ok: true, out }
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return { ok: false, out: String(e.stdout || "").trim(), err: String(e.stderr || e.message || "").trim() }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Validate a git ref / branch / tag / remote name (no option injection, no whitespace). */
|
|
33
|
+
export function validateRef(ref, what = "git ref") {
|
|
34
|
+
if (!/^[A-Za-z0-9._/~^@][A-Za-z0-9._/~^@{}-]*$/.test(ref)) throw new Error(`Invalid ${what}: ${ref}`)
|
|
35
|
+
return ref
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Normalize args.config into `-c key=value` pairs (git -c overrides, e.g. a proxy).
|
|
39
|
+
* Values are execFileSync array args (no shell injection) — still reject newlines/empty. */
|
|
40
|
+
export function gitConfigArgs(config) {
|
|
41
|
+
if (config == null) return []
|
|
42
|
+
if (!Array.isArray(config)) throw new Error("config must be an array of \"key=value\" strings")
|
|
43
|
+
const out = []
|
|
44
|
+
for (const c of config) {
|
|
45
|
+
if (typeof c !== "string" || !c.trim() || c.includes("\n")) throw new Error(`invalid git -c config entry: ${String(c).slice(0, 60)}`)
|
|
46
|
+
out.push("-c", c)
|
|
47
|
+
}
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Snapshot the working tree before a destructive op (reset --hard / checkout file / restore /
|
|
52
|
+
* stash pop / branch|tag delete / clean / rebase). Best-effort — a snapshot failure must not
|
|
53
|
+
* block the op (the approval/permission layer is the real gate). Returns a note line or "". */
|
|
54
|
+
export async function snapshotBefore(ctx, label) {
|
|
55
|
+
try {
|
|
56
|
+
const { createCheckpoint, isGitRepo } = await import("../git/checkpoint.mjs")
|
|
57
|
+
if (!isGitRepo(ctx.cwd)) return ""
|
|
58
|
+
const cp = await createCheckpoint(ctx.cwd)
|
|
59
|
+
return `[snapshot ${cp.id} created before ${label}]\n`
|
|
60
|
+
} catch {
|
|
61
|
+
return ""
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** F7 扩展 action 主入口(git 工具 switch 的 fall-through 组委托到这里)。 */
|
|
66
|
+
export async function executeExtAction(args, ctx) {
|
|
67
|
+
switch (args.action) {
|
|
68
|
+
case "clone": {
|
|
69
|
+
// Clone a repo into a NEW directory — non-destructive (never touches existing work).
|
|
70
|
+
if (!args.remote) return "Error: clone requires remote (URL or local path)"
|
|
71
|
+
const cmdArgs = ["clone", args.remote]
|
|
72
|
+
if (args.path) cmdArgs.push(args.path)
|
|
73
|
+
const r = runGitStrict(ctx.cwd, cmdArgs, gitConfigArgs(args.config))
|
|
74
|
+
return r.ok ? truncate(r.out || `Cloned ${args.remote}`) : truncate(`git clone failed: ${r.err || r.out}`)
|
|
75
|
+
}
|
|
76
|
+
case "init": {
|
|
77
|
+
const r = runGitStrict(ctx.cwd, ["init"])
|
|
78
|
+
return r.ok ? truncate(r.out || "Initialized empty git repository") : truncate(`git init failed: ${r.err || r.out}`)
|
|
79
|
+
}
|
|
80
|
+
case "rebase": {
|
|
81
|
+
// Belt-and-braces snapshot: bare rebase refuses uncommitted changes (unless
|
|
82
|
+
// --autostash), but --autostash restore-failure / interrupted-rebase scenarios
|
|
83
|
+
// can leave the working tree damaged — the snapshot makes that recoverable.
|
|
84
|
+
// The snapshot line is included on FAILURE too: a rejected rebase (unstaged
|
|
85
|
+
// changes) is exactly when the model must know its work is protected (F1 loop).
|
|
86
|
+
const snap = await snapshotBefore(ctx, "rebase")
|
|
87
|
+
const sub = args.rebaseAction ?? "start"
|
|
88
|
+
const cmdArgs = ["rebase"]
|
|
89
|
+
if (sub === "abort") cmdArgs.push("--abort")
|
|
90
|
+
else if (sub === "continue") cmdArgs.push("--continue")
|
|
91
|
+
else { if (!args.ref) return "Error: rebase requires ref (branch/commit to rebase onto)"; cmdArgs.push(validateRef(args.ref)) }
|
|
92
|
+
const r = runGitStrict(ctx.cwd, cmdArgs)
|
|
93
|
+
return r.ok ? truncate(snap + (r.out || `Rebase ${sub} complete`)) : truncate(snap + `git rebase failed: ${r.err || r.out} — use rebaseAction=abort to abort`)
|
|
94
|
+
}
|
|
95
|
+
case "remote": {
|
|
96
|
+
const sub = args.remoteAction ?? "list"
|
|
97
|
+
if (sub === "list") return truncate(filterLines(runGit(ctx.cwd, ["remote", "-v"]) || "(no remotes)", args.filter))
|
|
98
|
+
if (!args.remote) return `Error: remote ${sub} requires remote (name)`
|
|
99
|
+
validateRef(args.remote, "remote name")
|
|
100
|
+
if (sub === "add" || sub === "set-url") {
|
|
101
|
+
if (!args.remoteUrl) return `Error: remote ${sub} requires remoteUrl`
|
|
102
|
+
const r = runGitStrict(ctx.cwd, ["remote", sub === "add" ? "add" : "set-url", args.remote, args.remoteUrl])
|
|
103
|
+
return r.ok ? `Remote ${args.remote} ${sub === "add" ? "added" : "URL set"}` : truncate(`git remote ${sub} failed: ${r.err || r.out}`)
|
|
104
|
+
}
|
|
105
|
+
if (sub === "remove") {
|
|
106
|
+
const r = runGitStrict(ctx.cwd, ["remote", "remove", args.remote])
|
|
107
|
+
return r.ok ? `Remote ${args.remote} removed` : truncate(`git remote remove failed: ${r.err || r.out}`)
|
|
108
|
+
}
|
|
109
|
+
return "Error: remote requires remoteAction — use: list | add | remove | set-url"
|
|
110
|
+
}
|
|
111
|
+
case "clean": {
|
|
112
|
+
// Destructive: removes untracked files/dirs — snapshot first (guard parity).
|
|
113
|
+
// dryRun (-n) is a preview: no deletion, no snapshot.
|
|
114
|
+
const snap = args.dryRun ? "" : await snapshotBefore(ctx, "clean")
|
|
115
|
+
const cmdArgs = ["clean", args.dryRun ? "-n" : "-f", "-d"]
|
|
116
|
+
const r = runGitStrict(ctx.cwd, cmdArgs)
|
|
117
|
+
return r.ok ? truncate(snap + (r.out || (args.dryRun ? "Nothing to clean (dry run)" : "Clean complete"))) : truncate(snap + `git clean failed: ${r.err || r.out}`)
|
|
118
|
+
}
|
|
119
|
+
case "switch": {
|
|
120
|
+
if (!args.name) return "Error: switch requires name (branch)"
|
|
121
|
+
validateRef(args.name, "branch")
|
|
122
|
+
const cmdArgs = ["switch"]
|
|
123
|
+
if (args.create) cmdArgs.push("-c")
|
|
124
|
+
cmdArgs.push(args.name)
|
|
125
|
+
const r = runGitStrict(ctx.cwd, cmdArgs)
|
|
126
|
+
return r.ok ? truncate(r.out || `Switched to branch ${args.name}`) : truncate(`git switch failed: ${r.err || r.out}`)
|
|
127
|
+
}
|
|
128
|
+
case "apply": {
|
|
129
|
+
// Apply a patch — non-destructive (fails cleanly on conflict, applies nothing).
|
|
130
|
+
if (!args.path) return "Error: apply requires path (patch file)"
|
|
131
|
+
const r = runGitStrict(ctx.cwd, ["apply", "--", args.path])
|
|
132
|
+
return r.ok ? truncate(r.out || `Applied ${args.path}`) : truncate(`git apply failed: ${r.err || r.out}`)
|
|
133
|
+
}
|
|
134
|
+
case "worktree": {
|
|
135
|
+
const sub = args.worktreeAction ?? "list"
|
|
136
|
+
if (sub === "list") return truncate(filterLines(runGit(ctx.cwd, ["worktree", "list"]) || "(no worktrees)", args.filter))
|
|
137
|
+
if (sub === "add") {
|
|
138
|
+
if (!args.path) return "Error: worktree add requires path (new worktree directory)"
|
|
139
|
+
const cmdArgs = ["worktree", "add", args.path]
|
|
140
|
+
if (args.ref) cmdArgs.push(validateRef(args.ref))
|
|
141
|
+
const r = runGitStrict(ctx.cwd, cmdArgs)
|
|
142
|
+
return r.ok ? truncate(r.out || `Worktree added at ${args.path}`) : truncate(`git worktree add failed: ${r.err || r.out}`)
|
|
143
|
+
}
|
|
144
|
+
if (sub === "remove") {
|
|
145
|
+
if (!args.path) return "Error: worktree remove requires path"
|
|
146
|
+
const r = runGitStrict(ctx.cwd, ["worktree", "remove", args.path])
|
|
147
|
+
return r.ok ? truncate(r.out || `Worktree removed: ${args.path}`) : truncate(`git worktree remove failed: ${r.err || r.out}`)
|
|
148
|
+
}
|
|
149
|
+
return "Error: worktree requires worktreeAction — use: list | add | remove"
|
|
150
|
+
}
|
|
151
|
+
case "archive": {
|
|
152
|
+
// Write a tar of a commit/branch — non-destructive (output file only).
|
|
153
|
+
if (!args.path) return "Error: archive requires path (output file)"
|
|
154
|
+
const cmdArgs = ["archive", "--format=tar", "-o", args.path]
|
|
155
|
+
if (args.ref) cmdArgs.push(validateRef(args.ref))
|
|
156
|
+
else cmdArgs.push("HEAD")
|
|
157
|
+
const r = runGitStrict(ctx.cwd, cmdArgs)
|
|
158
|
+
return r.ok ? truncate(r.out || `Archived ${args.ref ?? "HEAD"} to ${args.path}`) : truncate(`git archive failed: ${r.err || r.out}`)
|
|
159
|
+
}
|
|
160
|
+
case "blame": {
|
|
161
|
+
if (!args.path) return "Error: blame requires path (file)"
|
|
162
|
+
const out = runGit(ctx.cwd, ["blame", "--", args.path])
|
|
163
|
+
return truncate(out || `(no blame output for ${args.path})`)
|
|
164
|
+
}
|
|
165
|
+
case "mv": {
|
|
166
|
+
if (!args.path || !args.dest) return "Error: mv requires path (source) and dest (destination)"
|
|
167
|
+
const r = runGitStrict(ctx.cwd, ["mv", "--", args.path, args.dest])
|
|
168
|
+
return r.ok ? truncate(r.out || `Moved ${args.path} → ${args.dest}`) : truncate(`git mv failed: ${r.err || r.out}`)
|
|
169
|
+
}
|
|
170
|
+
default:
|
|
171
|
+
throw new Error(`Unknown ext action: ${args.action}`)
|
|
172
|
+
}
|
|
173
|
+
}
|
package/src/tools/git.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Run a git command. Only works inside a git repository.
|
|
2
2
|
|
|
3
|
-
**Route to git instead of bash:** `git status`→status, `git log`→log, `git diff`→diff, `git show`→show, `git add`→add, `git rm`→rm, `git commit -m`→commit, `git push <remote> <branch> <tag>`→push, `git tag`→tag, `git branch`→branch, `git checkout`→checkout, `git restore`→restore, `git stash`→stash, `git fetch/pull`→fetch/pull, `git reset`→reset, `git revert`→revert, `git merge`→merge, `git cherry-pick`→cherry-pick, `git ls-remote`→ls-remote.
|
|
3
|
+
**Route to git instead of bash:** `git status`→status, `git log`→log, `git diff`→diff, `git show`→show, `git add`→add, `git rm`→rm, `git commit -m`→commit, `git push <remote> <branch> <tag>`→push, `git tag`→tag, `git branch`→branch, `git checkout`→checkout, `git restore`→restore, `git stash`→stash, `git fetch/pull`→fetch/pull, `git reset`→reset, `git revert`→revert, `git merge`→merge, `git cherry-pick`→cherry-pick, `git ls-remote`→ls-remote, `git clone`→clone, `git init`→init, `git rebase`→rebase, `git remote`→remote, `git clean`→clean, `git switch`→switch, `git apply`→apply, `git worktree`→worktree, `git archive`→archive, `git blame`→blame, `git mv`→mv.
|
|
4
4
|
|
|
5
5
|
- action='diff': unified diff — what changed since last commit. staged=true for staged-only; ref=<ref> to compare a commit/branch; path=<dir> to scope.
|
|
6
6
|
- action='status': working tree state — staged / unstaged / untracked / conflicts, categorized.
|
|
@@ -20,16 +20,28 @@ Run a git command. Only works inside a git repository.
|
|
|
20
20
|
- action='merge': merge ref=<branch/commit>; conflicts reported for you to resolve.
|
|
21
21
|
- action='cherry-pick': cherry-pick ref=<commit>.
|
|
22
22
|
- action='ls-remote': light remote-ref check — which refs a remote has (read-only, network). remote=<origin>, ref=<branch/tag> optional, config for proxy.
|
|
23
|
+
- action='clone': clone a repo. remote required (URL or local path); path optional (target dir).
|
|
24
|
+
- action='init': init a repo in the current (work)dir.
|
|
25
|
+
- action='rebase': rebase onto ref. rebaseAction=start (ref required) / abort / continue(操作前自动快照,checkpointAction=rewind 恢复).
|
|
26
|
+
- action='remote': manage remotes. remoteAction=list / add / remove / set-url; remoteUrl for add/set-url.
|
|
27
|
+
- action='clean': remove untracked files/dirs. dryRun for -n preview(真删除操作前自动快照,checkpointAction=rewind 恢复).
|
|
28
|
+
- action='switch': switch branch. name required; create for -c (new branch).
|
|
29
|
+
- action='apply': apply a patch. path required (patch file).
|
|
30
|
+
- action='worktree': manage worktrees. worktreeAction=list / add (path, ref) / remove (path).
|
|
31
|
+
- action='archive': write a tar of ref (default HEAD). path required (output file).
|
|
32
|
+
- action='blame': file blame. path required.
|
|
33
|
+
- action='mv': rename/move. path (source) + dest required.
|
|
23
34
|
- action='checkpoint': git snapshots. checkpointAction=list/create/rewind/cat/versions; checkpointId required for rewind/cat.
|
|
35
|
+
- Destructive ops (checkout -- path / restore / reset --hard / stash pop / branch|tag delete / clean / rebase) auto-snapshot first — restore via checkpointAction=rewind.
|
|
24
36
|
|
|
25
37
|
Parameters:
|
|
26
|
-
- action (required): diff / status / log / show / checkpoint / add / rm / commit / push / tag / branch / checkout / restore / stash / fetch / pull / reset / revert / merge / cherry-pick / ls-remote
|
|
27
|
-
- workdir: run git in this
|
|
28
|
-
- config: (network actions push/fetch/pull/ls-remote) git -c overrides, e.g. ["http.proxy=http://10.2.2.112:3128"] for blocked remotes
|
|
29
|
-
- path: (diff/log/add/commit/checkout/restore/rm) file or directory to scope / stage / restore
|
|
30
|
-
- ref: (show/diff/checkout/reset/revert/merge/cherry-pick/tag:create/branch:create) commit/branch/ref; (push/pull/fetch) the branch or tag (space-separated for multiple)
|
|
31
|
-
- name: (branch/tag) the branch or tag name
|
|
32
|
-
- remote: (push/fetch/pull) remote name (e.g. origin); default: current upstream
|
|
38
|
+
- action (required): diff / status / log / show / checkpoint / add / rm / commit / push / tag / branch / checkout / restore / stash / fetch / pull / reset / revert / merge / cherry-pick / ls-remote / clone / init / rebase / remote / clean / switch / apply / worktree / archive / blame / mv
|
|
39
|
+
- workdir: run git in this subdirectory (monorepo / multi-repo). Path relative to cwd — no directory restriction. Default: cwd
|
|
40
|
+
- config: (network actions push/fetch/pull/ls-remote/clone) git -c overrides, e.g. ["http.proxy=http://10.2.2.112:3128"] for blocked remotes
|
|
41
|
+
- path: (diff/log/add/commit/checkout/restore/rm/apply/archive/blame/mv/worktree) file or directory to scope / stage / restore
|
|
42
|
+
- ref: (show/diff/checkout/reset/revert/merge/cherry-pick/tag:create/branch:create/rebase/worktree:add/archive) commit/branch/ref; (push/pull/fetch) the branch or tag (space-separated for multiple)
|
|
43
|
+
- name: (branch/tag/switch) the branch or tag name
|
|
44
|
+
- remote: (push/fetch/pull/remote/clone) remote name (e.g. origin) or URL; default: current upstream
|
|
33
45
|
- tags: (push) also push all tags (--tags)
|
|
34
46
|
- staged: (diff) staged changes; (restore) the staged copy
|
|
35
47
|
- count: (log) number of commits (default 10)
|
|
@@ -39,3 +51,4 @@ Parameters:
|
|
|
39
51
|
- tagAction: (tag) list / create / delete — branchAction: (branch) list / create / delete / switch — stashAction: (stash) push / pop / list
|
|
40
52
|
- filter: (read-only actions) keep only output lines matching this regex (case-insensitive)
|
|
41
53
|
- checkpointAction: (checkpoint) list / create / rewind / cat / versions — checkpointId: snapshot id (rewind/cat)
|
|
54
|
+
- remoteAction: (remote) list / add / remove / set-url — remoteUrl: (remote add/set-url) URL — rebaseAction: (rebase) start / abort / continue — dryRun: (clean) -n preview — create: (switch) -c — dest: (mv) destination — worktreeAction: (worktree) list / add / remove
|
package/src/tools/git.mjs
CHANGED
|
@@ -3,21 +3,11 @@ import {
|
|
|
3
3
|
truncate,
|
|
4
4
|
runGit
|
|
5
5
|
} from "./shared.mjs";
|
|
6
|
-
import { escapeXml } from "../agent/helpers.mjs";
|
|
7
6
|
import { execFileSync } from "node:child_process";
|
|
8
|
-
import {
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { filterLines, runGitStrict, validateRef, gitConfigArgs, snapshotBefore, executeExtAction } from "./git-ext.mjs";
|
|
9
|
+
import { executeCheckpointAction } from "./git-checkpoint.mjs";
|
|
9
10
|
|
|
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
11
|
|
|
22
12
|
/** Run git PRESERVING per-line leading whitespace. runGit trims the WHOLE output, which
|
|
23
13
|
* strips a porcelain line's leading " " (the unstaged marker) and misclassifies an
|
|
@@ -30,65 +20,18 @@ function runGitRaw(cwd, cmdArgs, config = []) {
|
|
|
30
20
|
}
|
|
31
21
|
}
|
|
32
22
|
|
|
33
|
-
/** Run git and report failure (stderr + exit code) instead of swallowing it.
|
|
34
|
-
* Used by write ops (commit/push/rm) where a silent "" would masquerade as success. */
|
|
35
|
-
function runGitStrict(cwd, cmdArgs, config = []) {
|
|
36
|
-
try {
|
|
37
|
-
const out = execFileSync("git", [...config, ...cmdArgs], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim().replace(/\r/g, "")
|
|
38
|
-
return { ok: true, out }
|
|
39
|
-
} catch (e) {
|
|
40
|
-
return { ok: false, out: String(e.stdout || "").trim(), err: String(e.stderr || e.message || "").trim() }
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
23
|
|
|
44
|
-
/** Validate a git ref / branch / tag / remote name (no option injection, no whitespace). */
|
|
45
|
-
function validateRef(ref, what = "git ref") {
|
|
46
|
-
if (!/^[A-Za-z0-9._/~^@][A-Za-z0-9._/~^@{}-]*$/.test(ref)) throw new Error(`Invalid ${what}: ${ref}`)
|
|
47
|
-
return ref
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Normalize args.config into `-c key=value` pairs (git -c overrides, e.g. a proxy).
|
|
51
|
-
* Values are execFileSync array args (no shell injection) — still reject newlines/empty. */
|
|
52
|
-
function gitConfigArgs(config) {
|
|
53
|
-
if (config == null) return []
|
|
54
|
-
if (!Array.isArray(config)) throw new Error("config must be an array of \"key=value\" strings")
|
|
55
|
-
const out = []
|
|
56
|
-
for (const c of config) {
|
|
57
|
-
if (typeof c !== "string" || !c.trim() || c.includes("\n")) throw new Error(`invalid git -c config entry: ${String(c).slice(0, 60)}`)
|
|
58
|
-
out.push("-c", c)
|
|
59
|
-
}
|
|
60
|
-
return out
|
|
61
|
-
}
|
|
62
24
|
|
|
63
|
-
/** True when `abs` is inside `root` (handles `..` and cross-drive, which relative()
|
|
64
|
-
* returns as an absolute path on Windows). */
|
|
65
|
-
function isInside(root, abs) {
|
|
66
|
-
const rel = relative(root, abs)
|
|
67
|
-
if (isAbsolute(rel)) return false
|
|
68
|
-
return rel !== ".." && !rel.startsWith(".." + sep)
|
|
69
|
-
}
|
|
70
25
|
|
|
71
|
-
/** Resolve workdir relative to cwd
|
|
26
|
+
/** Resolve workdir relative to cwd — no boundary assertion
|
|
27
|
+
* (§10.1 2026-09-02: workspace confinement removed; git itself is not
|
|
28
|
+
* directory-limited — same boundary as bash). */
|
|
72
29
|
function resolveBaseDir(cwd, workdir) {
|
|
73
30
|
if (!workdir || typeof workdir !== "string") return cwd
|
|
74
|
-
|
|
75
|
-
if (!isInside(cwd, abs)) throw new Error(`workdir escapes the workspace: ${workdir}`)
|
|
76
|
-
return abs
|
|
31
|
+
return resolve(cwd, workdir)
|
|
77
32
|
}
|
|
78
33
|
|
|
79
|
-
|
|
80
|
-
* stash pop / branch|tag delete). Best-effort — a snapshot failure must not block the op
|
|
81
|
-
* (the approval/permission layer is the real gate). Returns a note line or "". */
|
|
82
|
-
async function snapshotBefore(ctx, label) {
|
|
83
|
-
try {
|
|
84
|
-
const { createCheckpoint, isGitRepo } = await import("../git/checkpoint.mjs")
|
|
85
|
-
if (!isGitRepo(ctx.cwd)) return ""
|
|
86
|
-
const cp = await createCheckpoint(ctx.cwd)
|
|
87
|
-
return `[snapshot ${cp.id} created before ${label}]\n`
|
|
88
|
-
} catch {
|
|
89
|
-
return ""
|
|
90
|
-
}
|
|
91
|
-
}
|
|
34
|
+
|
|
92
35
|
|
|
93
36
|
export const gitTool = {
|
|
94
37
|
name: "git",
|
|
@@ -96,11 +39,11 @@ export const gitTool = {
|
|
|
96
39
|
parameters: {
|
|
97
40
|
type: "object",
|
|
98
41
|
properties: {
|
|
99
|
-
action: { type: "string", enum: ["diff", "status", "log", "show", "checkpoint", "add", "rm", "commit", "push", "tag", "branch", "checkout", "restore", "stash", "fetch", "pull", "reset", "revert", "merge", "cherry-pick", "ls-remote"], description: "diff / status / log / show / checkpoint / add / rm / commit / push / tag / branch / checkout / restore / stash / fetch / pull / reset / revert / merge / cherry-pick / ls-remote" },
|
|
42
|
+
action: { type: "string", enum: ["diff", "status", "log", "show", "checkpoint", "add", "rm", "commit", "push", "tag", "branch", "checkout", "restore", "stash", "fetch", "pull", "reset", "revert", "merge", "cherry-pick", "ls-remote", "clone", "init", "rebase", "remote", "clean", "switch", "apply", "worktree", "archive", "blame", "mv"], description: "diff / status / log / show / checkpoint / add / rm / commit / push / tag / branch / checkout / restore / stash / fetch / pull / reset / revert / merge / cherry-pick / ls-remote / clone / init / rebase / remote / clean / switch / apply / worktree / archive / blame / mv — clean/rebase 操作前自动快照,checkpointAction=rewind 恢复" },
|
|
100
43
|
// diff/log params
|
|
101
44
|
staged: { type: "boolean", description: "(diff) Show staged changes instead of working tree" },
|
|
102
|
-
path: { type: "string", description: "(diff/log/add/commit/checkout/restore/checkpoint:cat/versions/rewind/rm) File or directory to scope to / stage / restore" },
|
|
103
|
-
ref: { type: "string", description: "(show/
|
|
45
|
+
path: { type: "string", description: "(diff/log/add/commit/checkout/restore/checkpoint:cat/versions/rewind/rm/apply/archive/blame/mv/worktree) File or directory to scope to / stage / restore(checkout/restore 操作前自动快照,checkpointAction=rewind 恢复)" },
|
|
46
|
+
ref: { type: "string", description: "(diff/show/checkout/reset/revert/merge/cherry-pick/tag:create/branch:create/rebase/worktree:add/archive) Commit/branch/ref; (push/pull/fetch) the branch or tag (space-separated for multiple)" },
|
|
104
47
|
count: { type: "number", description: "(log) Number of commits (default 10)" },
|
|
105
48
|
oneline: { type: "boolean", description: "(log) One-line-per-commit format" },
|
|
106
49
|
message: { type: "string", description: "(commit) Commit message — required for commit; (stash:push) stash message" },
|
|
@@ -108,23 +51,31 @@ export const gitTool = {
|
|
|
108
51
|
// write-op params
|
|
109
52
|
name: { type: "string", description: "(branch/tag) The branch or tag name (create/delete/switch)" },
|
|
110
53
|
remote: { type: "string", description: "(push/fetch/pull) Remote name (e.g. origin). Default: current upstream" },
|
|
111
|
-
workdir: { type: "string", description: "Run git in this
|
|
54
|
+
workdir: { type: "string", description: "Run git in this subdirectory (monorepo / multi-repo). Path relative to cwd — no directory restriction. Default: cwd" },
|
|
112
55
|
config: { type: "array", items: { type: "string" }, description: "(network actions: push/fetch/pull/ls-remote) git -c overrides, e.g. [\"http.proxy=http://10.2.2.112:3128\"] for blocked remotes" },
|
|
113
56
|
tags: { type: "boolean", description: "(push) Also push all tags (--tags)" },
|
|
114
|
-
mode: { type: "string", enum: ["soft", "mixed", "hard"], description: "(reset) reset mode — hard snapshots the tree first + needs confirmation" },
|
|
115
|
-
tagAction: { type: "string", enum: ["list", "create", "delete"], description: "(tag) list tags / create one / delete one" },
|
|
116
|
-
branchAction: { type: "string", enum: ["list", "create", "delete", "switch"], description: "(branch) list branches / create / delete / switch to one" },
|
|
117
|
-
stashAction: { type: "string", enum: ["push", "pop", "list"], description: "(stash) push (stash now) / pop (apply+drop) / list" },
|
|
57
|
+
mode: { type: "string", enum: ["soft", "mixed", "hard"], description: "(reset) reset mode — hard snapshots the tree first + needs confirmation(操作前自动快照,checkpointAction=rewind 恢复)" },
|
|
58
|
+
tagAction: { type: "string", enum: ["list", "create", "delete"], description: "(tag) list tags / create one / delete one(delete 操作前自动快照,checkpointAction=rewind 恢复)" },
|
|
59
|
+
branchAction: { type: "string", enum: ["list", "create", "delete", "switch"], description: "(branch) list branches / create / delete / switch to one(delete 操作前自动快照,checkpointAction=rewind 恢复)" },
|
|
60
|
+
stashAction: { type: "string", enum: ["push", "pop", "list"], description: "(stash) push (stash now) / pop (apply+drop) / list(pop 操作前自动快照,checkpointAction=rewind 恢复)" },
|
|
118
61
|
// checkpoint params
|
|
119
|
-
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" },
|
|
62
|
+
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(rewind 可恢复操作前状态,恢复前自动快照可逆)" },
|
|
120
63
|
checkpointId: { type: "string", description: "(checkpoint) Snapshot id — required for rewind and cat; optional for list (shows file tree)" },
|
|
64
|
+
// F7 new-action params
|
|
65
|
+
remoteAction: { type: "string", enum: ["list", "add", "remove", "set-url"], description: "(remote) list remotes / add / remove / set-url" },
|
|
66
|
+
remoteUrl: { type: "string", description: "(remote add/set-url) Remote URL (https/git/ssh or local path)" },
|
|
67
|
+
rebaseAction: { type: "string", enum: ["start", "abort", "continue"], description: "(rebase) start (ref required) / abort / continue(操作前自动快照,checkpointAction=rewind 恢复)" },
|
|
68
|
+
dryRun: { type: "boolean", description: "(clean) preview only (-n) — no deletion, no snapshot; real clean 操作前自动快照,checkpointAction=rewind 恢复" },
|
|
69
|
+
create: { type: "boolean", description: "(switch) create the branch then switch (-c)" },
|
|
70
|
+
dest: { type: "string", description: "(mv) destination path (file or directory)" },
|
|
71
|
+
worktreeAction: { type: "string", enum: ["list", "add", "remove"], description: "(worktree) list / add (path, ref) / remove (path)" },
|
|
121
72
|
},
|
|
122
73
|
required: ["action"],
|
|
123
74
|
},
|
|
124
75
|
readonly: false,
|
|
125
76
|
async execute(args, ctx) {
|
|
126
|
-
// workdir: run git in a
|
|
127
|
-
// every action + snapshotBefore + checkpoint resolves against the workdir
|
|
77
|
+
// workdir: run git in a subdirectory (monorepo / multi-repo). Shadow ctx.cwd so
|
|
78
|
+
// every action + snapshotBefore + checkpoint resolves against the workdir.
|
|
128
79
|
if (args.workdir) ctx = { ...ctx, cwd: resolveBaseDir(ctx.cwd, args.workdir) }
|
|
129
80
|
// git -c overrides (proxy etc.) — only network actions need them; passing to every
|
|
130
81
|
// action would be harmless but noisy. cfgArgs stays [] for local ops.
|
|
@@ -203,7 +154,18 @@ export const gitTool = {
|
|
|
203
154
|
const commit = runGitStrict(ctx.cwd, ["commit", "-m", args.message])
|
|
204
155
|
const parts = []
|
|
205
156
|
if (add.out) parts.push(add.out)
|
|
206
|
-
if (commit.ok) {
|
|
157
|
+
if (commit.ok) {
|
|
158
|
+
if (commit.out) parts.push(commit.out)
|
|
159
|
+
// F6: commit = new safety baseline — clear this project's checkpoints
|
|
160
|
+
// (best-effort per NF7: a failed cleanup never blocks the commit result).
|
|
161
|
+
try {
|
|
162
|
+
const { deleteCheckpointsForCwd } = await import("../git/checkpoint.mjs")
|
|
163
|
+
await deleteCheckpointsForCwd(ctx.cwd)
|
|
164
|
+
parts.push("(checkpoints cleared — commit is a new safety baseline)")
|
|
165
|
+
} catch (e) {
|
|
166
|
+
parts.push(`(checkpoint cleanup skipped: ${e.message})`)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
207
169
|
else parts.push(`git commit failed: ${commit.err || "(no output)"}`)
|
|
208
170
|
return truncate(parts.join("\n") || "(commit produced no output)")
|
|
209
171
|
}
|
|
@@ -357,66 +319,24 @@ export const gitTool = {
|
|
|
357
319
|
const r = runGitStrict(ctx.cwd, ["cherry-pick", args.ref])
|
|
358
320
|
return r.ok ? truncate(r.out || `Cherry-picked ${args.ref}`) : truncate(`git cherry-pick failed: ${r.err || r.out}`)
|
|
359
321
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
322
|
+
// F7 扩展 action + checkpoint:实现拆在 git-ext.mjs / git-checkpoint.mjs(500 行硬限)
|
|
323
|
+
case "clone":
|
|
324
|
+
case "init":
|
|
325
|
+
case "rebase":
|
|
326
|
+
case "remote":
|
|
327
|
+
case "clean":
|
|
328
|
+
case "switch":
|
|
329
|
+
case "apply":
|
|
330
|
+
case "worktree":
|
|
331
|
+
case "archive":
|
|
332
|
+
case "blame":
|
|
333
|
+
case "mv":
|
|
334
|
+
return executeExtAction(args, ctx)
|
|
335
|
+
case "checkpoint":
|
|
336
|
+
return executeCheckpointAction(args, ctx)
|
|
366
337
|
|
|
367
|
-
if (sub === "create") {
|
|
368
|
-
const cp = await createCheckpoint(ctx.cwd)
|
|
369
|
-
return `Checkpoint ${cp.id} created (${cp.files} file(s): ${cp.tracked.length} tracked, ${cp.untracked.length} untracked)`
|
|
370
|
-
}
|
|
371
|
-
if (sub === "versions") {
|
|
372
|
-
if (!args.path) throw new Error("path is required for versions — the file whose history you want")
|
|
373
|
-
const versions = await listFileVersions(ctx.cwd, args.path)
|
|
374
|
-
if (versions.length === 0) return `No snapshot copies of "${args.path}" found (it was never part of an auto/protection snapshot).`
|
|
375
|
-
return (
|
|
376
|
-
`Historical versions of "${args.path}" (${versions.length}, newest first):\n` +
|
|
377
|
-
versions.map((v) =>
|
|
378
|
-
` ${v.snapshotId} ${new Date(v.time).toISOString()} ${v.size}B sha:${v.sha} (${v.source})` +
|
|
379
|
-
(v.sha === versions[versions.indexOf(v) - 1]?.sha ? " ← same content as previous" : "")
|
|
380
|
-
).join("\n") +
|
|
381
|
-
`\nRestore a version: checkpointAction=rewind checkpointId=<snapshotId> path="${args.path}"`
|
|
382
|
-
)
|
|
383
|
-
}
|
|
384
|
-
if (sub === "rewind") {
|
|
385
|
-
if (!args.checkpointId) throw new Error("checkpointId is required for rewind — use checkpointAction=list to see snapshot ids")
|
|
386
|
-
if (!args.path) throw new Error("path is required for rewind — full restore is disabled (as dangerous as `git checkout -- .`). Restore files individually. Use checkpointAction=versions path=<file> to list a file's historical versions.")
|
|
387
|
-
const s = await rewind(ctx.cwd, args.checkpointId, { path: args.path })
|
|
388
|
-
return `Restored "${args.path}" (${s.type}) from checkpoint ${args.checkpointId}.\n(The pre-restore state was snapshotted first — you can restore again to go back.)`
|
|
389
|
-
}
|
|
390
|
-
if (sub === "cat") {
|
|
391
|
-
if (!args.checkpointId) throw new Error("checkpointId is required for cat — use checkpointAction=list to see snapshot ids")
|
|
392
|
-
if (!args.path) throw new Error("path is required for cat — specify which file to read")
|
|
393
|
-
const { catFile } = await import("../git/checkpoint.mjs")
|
|
394
|
-
return await catFile(ctx.cwd, args.checkpointId, args.path)
|
|
395
|
-
}
|
|
396
|
-
if (sub === "list") {
|
|
397
|
-
const cps = await listCheckpoints(ctx.cwd)
|
|
398
|
-
if (cps.length === 0) return "(no checkpoints yet)"
|
|
399
|
-
|
|
400
|
-
// Specific id: show the file tree within that snapshot
|
|
401
|
-
if (args.checkpointId) {
|
|
402
|
-
const cp = cps.find((c) => c.id === args.checkpointId)
|
|
403
|
-
if (!cp) throw new Error(`checkpoint ${args.checkpointId} not found`)
|
|
404
|
-
return formatFileTree(cp)
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Overview: list of all snapshots (file names are XML-escaped: they are
|
|
408
|
-
// untrusted input that flows back into the model's context)
|
|
409
|
-
return cps.map((c) => {
|
|
410
|
-
const parts = [`${c.id} ${new Date(c.time).toISOString()}`]
|
|
411
|
-
if (c.tracked.length) parts.push(`${c.tracked.length} tracked: ${c.tracked.map(escapeXml).join(", ")}`)
|
|
412
|
-
if (c.untracked.length) parts.push(`${c.untracked.length} untracked: ${c.untracked.map(escapeXml).join(", ")}`)
|
|
413
|
-
return parts.join(" ")
|
|
414
|
-
}).join("\n")
|
|
415
|
-
}
|
|
416
|
-
throw new Error(`Unknown checkpoint action: ${sub}. Use: list | create | rewind | cat | versions`)
|
|
417
|
-
}
|
|
418
338
|
default:
|
|
419
|
-
return `Unknown action '${args.action}'. Use: diff | status | log | show | checkpoint | add | rm | commit | push | tag | branch | checkout | restore | stash | fetch | pull | reset | revert | merge | cherry-pick`
|
|
339
|
+
return `Unknown action '${args.action}'. Use: diff | status | log | show | checkpoint | add | rm | commit | push | tag | branch | checkout | restore | stash | fetch | pull | reset | revert | merge | cherry-pick | ls-remote | clone | init | rebase | remote | clean | switch | apply | worktree | archive | blame | mv`
|
|
420
340
|
}
|
|
421
341
|
},
|
|
422
342
|
}
|
|
@@ -445,45 +365,3 @@ export const questionTool = {
|
|
|
445
365
|
},
|
|
446
366
|
}
|
|
447
367
|
|
|
448
|
-
/** Format a checkpoint's file list as a directory tree (directories first, indented display) */
|
|
449
|
-
function formatFileTree(cp) {
|
|
450
|
-
// File names are XML-escaped: untrusted input that flows back into the model's context
|
|
451
|
-
const all = [
|
|
452
|
-
...(cp.tracked ?? []).map((f) => ({ path: escapeXml(f), type: "" })),
|
|
453
|
-
...(cp.untracked ?? []).map((f) => ({ path: escapeXml(f), type: " (untracked)" })),
|
|
454
|
-
]
|
|
455
|
-
if (all.length === 0) return "(empty checkpoint)"
|
|
456
|
-
|
|
457
|
-
all.sort((a, b) => a.path.localeCompare(b.path))
|
|
458
|
-
|
|
459
|
-
const tree = new Map()
|
|
460
|
-
for (const { path, type } of all) {
|
|
461
|
-
const dir = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "."
|
|
462
|
-
if (!tree.has(dir)) tree.set(dir, [])
|
|
463
|
-
tree.get(dir).push({ name: path.slice(dir === "." ? 0 : dir.length + 1), type })
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const lines = []
|
|
467
|
-
const dirs = [...tree.keys()].sort()
|
|
468
|
-
for (const dir of dirs) {
|
|
469
|
-
if (dir !== "." && !lines.includes(dir + "/")) {
|
|
470
|
-
const parts = dir.split("/")
|
|
471
|
-
for (let i = 1; i <= parts.length; i++) {
|
|
472
|
-
const prefix = parts.slice(0, i).join("/") + "/"
|
|
473
|
-
if (!lines.includes(prefix)) lines.push(prefix)
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
for (const dir of dirs) {
|
|
478
|
-
if (dir !== ".") {
|
|
479
|
-
for (const { name, type } of tree.get(dir)) {
|
|
480
|
-
lines.push(` ${dir}/${name}${type}`)
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
for (const { name, type } of tree.get(".") ?? []) {
|
|
485
|
-
lines.push(name + type)
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return lines.join("\n")
|
|
489
|
-
}
|
package/src/tools/lint.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Run the appropriate linter/checker for a file. Auto-detects based on file extension and project config.
|
|
2
2
|
Without 'full', runs a fast node --check (JS/TS syntax only, catches parse errors in milliseconds).
|
|
3
|
-
With 'full', runs the language-aware cascade:
|
|
3
|
+
With 'full', runs the language-aware cascade: tsc –noEmit (TS); ruff (Python); cargo check (Rust); go vet (Go). JS/JSX files fall back to node --check; TS uses tsc --noEmit (requires tsconfig.json).
|
|
4
4
|
Use the fast default after every write/edit; use 'full' before declaring a task complete.
|
|
5
5
|
|
|
6
6
|
Parameters:
|
package/src/tools/linter.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DESC, resolveInCwd } from "./shared.mjs"
|
|
2
2
|
import { execFileSync } from "node:child_process"
|
|
3
3
|
import { existsSync } from "node:fs"
|
|
4
|
-
import { join
|
|
4
|
+
import { join } from "node:path"
|
|
5
5
|
|
|
6
6
|
export const lintTool = {
|
|
7
7
|
name: "lint",
|
|
@@ -26,7 +26,8 @@ export const lintTool = {
|
|
|
26
26
|
return nodeCheckResult(abs)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// Full cascade: language-aware (
|
|
29
|
+
// Full cascade: language-aware (tsc → node --check, ruff, cargo, go vet —
|
|
30
|
+
// third-party linter cascade removed 2026-09-02, TOOLS.md §10.2: zero-dependency lint)
|
|
30
31
|
const ext = abs.split(".").pop()?.toLowerCase()
|
|
31
32
|
const checkers = LANG_CHECKERS[ext]
|
|
32
33
|
if (!checkers) return nodeCheckResult(abs) // fall back to node --check
|
|
@@ -54,33 +55,7 @@ function nodeCheckResult(abs) {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
// ─── Full-check cascade checkers
|
|
58
|
-
|
|
59
|
-
async function eslintCheck(file, { cwd, existsSync, execFileSync, join, relative }) {
|
|
60
|
-
let dir = file.split(/[\\/]/).slice(0, -1).join("/") || "."
|
|
61
|
-
while (true) {
|
|
62
|
-
for (const cfg of [".eslintrc.js", ".eslintrc.cjs", ".eslintrc.json", ".eslintrc.yaml", ".eslintrc.yml", "eslint.config.js", "eslint.config.mjs"]) {
|
|
63
|
-
if (existsSync(join(cwd, dir, cfg))) {
|
|
64
|
-
try {
|
|
65
|
-
const cfgDir = join(cwd, dir)
|
|
66
|
-
const relPath = relative(cfgDir, file)
|
|
67
|
-
execFileSync("npx", ["eslint", "--no-color", "--format", "compact", relPath], {
|
|
68
|
-
cwd: cfgDir, encoding: "utf8", timeout: 30000, stdio: ["ignore", "pipe", "pipe"],
|
|
69
|
-
})
|
|
70
|
-
return "✓ eslint: no issues"
|
|
71
|
-
} catch (e) {
|
|
72
|
-
const stdout = (e.stdout || "").trim()
|
|
73
|
-
if (stdout) return stdout
|
|
74
|
-
return `✗ eslint: ${(e.stderr || e.message).slice(0, 500)}`
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const parent = dir.split("/").slice(0, -1).join("/")
|
|
79
|
-
if (!parent || parent === dir) break
|
|
80
|
-
dir = parent
|
|
81
|
-
}
|
|
82
|
-
return null
|
|
83
|
-
}
|
|
58
|
+
// ─── Full-check cascade checkers (third-party linter branch removed 2026-09-02, TOOLS.md §10.2) ──────
|
|
84
59
|
|
|
85
60
|
async function tscCheck(file, { cwd, existsSync, execFileSync, join }) {
|
|
86
61
|
if (!existsSync(join(cwd, "tsconfig.json"))) return null
|
|
@@ -142,14 +117,11 @@ async function goVet(file, { cwd, execFileSync }) {
|
|
|
142
117
|
}
|
|
143
118
|
|
|
144
119
|
const LANG_CHECKERS = {
|
|
145
|
-
js
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
tsx: [eslintCheck, tscCheck],
|
|
151
|
-
mts: [eslintCheck, tscCheck],
|
|
152
|
-
cts: [eslintCheck, tscCheck],
|
|
120
|
+
// js/mjs/cjs/jsx fall back to node --check (no entry — third-party linter cascade removed 2026-09-02, TOOLS.md §10.2)
|
|
121
|
+
ts: [tscCheck],
|
|
122
|
+
tsx: [tscCheck],
|
|
123
|
+
mts: [tscCheck],
|
|
124
|
+
cts: [tscCheck],
|
|
153
125
|
py: [ruffCheck],
|
|
154
126
|
rs: [cargoCheck],
|
|
155
127
|
go: [goVet],
|
package/src/tools/patch.mjs
CHANGED
|
@@ -103,7 +103,7 @@ export const applyPatchTool = {
|
|
|
103
103
|
parameters: {
|
|
104
104
|
type: "object",
|
|
105
105
|
properties: {
|
|
106
|
-
patch: { type: "string", description: "Unified diff. May span multiple files; --- / +++ headers per file, @@ -old,count +new,count @@ hunks.
|
|
106
|
+
patch: { type: "string", description: "Unified diff. May span multiple files (multiple --- / +++ header pairs — including creating MULTIPLE new files via --- /dev/null); --- / +++ headers per file, @@ -old,count +new,count @@ hunks." },
|
|
107
107
|
},
|
|
108
108
|
required: ["patch"],
|
|
109
109
|
},
|