topchester-ai 0.9.0 → 0.10.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/dist/cli.mjs +1330 -229
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -28,13 +28,20 @@ function toAiSdkToolSet(definitions) {
|
|
|
28
28
|
//#region src/knowledge/session-overlay.ts
|
|
29
29
|
const overlays = /* @__PURE__ */ new Map();
|
|
30
30
|
function recordAgentFileEdit(workspaceRoot, event) {
|
|
31
|
+
return recordAgentFileMutation(workspaceRoot, event);
|
|
32
|
+
}
|
|
33
|
+
function recordAgentFileCreate(workspaceRoot, event) {
|
|
34
|
+
return recordAgentFileMutation(workspaceRoot, event);
|
|
35
|
+
}
|
|
36
|
+
function recordAgentFileMutation(workspaceRoot, event) {
|
|
31
37
|
const overlay = getOrCreateOverlay(resolve(workspaceRoot));
|
|
32
38
|
const previous = overlay.dirtyFiles.get(event.path);
|
|
33
39
|
overlay.drift = "dirty_known";
|
|
34
40
|
overlay.kbState = "needs_sync";
|
|
35
41
|
overlay.needsSync = true;
|
|
36
42
|
overlay.updatedAt = event.timestamp;
|
|
37
|
-
overlay.
|
|
43
|
+
overlay.mutationEvents.push(event);
|
|
44
|
+
if (event.kind === "file_edit") overlay.editEvents.push(event);
|
|
38
45
|
overlay.dirtyFiles.set(event.path, {
|
|
39
46
|
path: event.path,
|
|
40
47
|
source: "agent",
|
|
@@ -42,7 +49,7 @@ function recordAgentFileEdit(workspaceRoot, event) {
|
|
|
42
49
|
kbState: "needs_sync",
|
|
43
50
|
l1State: "stale",
|
|
44
51
|
derivedState: "suspect",
|
|
45
|
-
beforeHash: previous?.beforeHash ?? event.beforeHash,
|
|
52
|
+
beforeHash: previous?.beforeHash ?? (event.kind === "file_create" ? void 0 : event.beforeHash),
|
|
46
53
|
afterHash: event.afterHash,
|
|
47
54
|
firstChangedLine: event.firstChangedLine,
|
|
48
55
|
lastEditedAt: event.timestamp,
|
|
@@ -59,7 +66,8 @@ function getOrCreateOverlay(workspaceRoot) {
|
|
|
59
66
|
kbState: "current",
|
|
60
67
|
needsSync: false,
|
|
61
68
|
dirtyFiles: /* @__PURE__ */ new Map(),
|
|
62
|
-
editEvents: []
|
|
69
|
+
editEvents: [],
|
|
70
|
+
mutationEvents: []
|
|
63
71
|
};
|
|
64
72
|
overlays.set(workspaceRoot, overlay);
|
|
65
73
|
return overlay;
|
|
@@ -72,6 +80,7 @@ function snapshotOverlay(overlay) {
|
|
|
72
80
|
needsSync: overlay.needsSync,
|
|
73
81
|
dirtyFiles: [...overlay.dirtyFiles.values()].map((file) => ({ ...file })),
|
|
74
82
|
editEvents: overlay.editEvents.map((event) => ({ ...event })),
|
|
83
|
+
mutationEvents: overlay.mutationEvents.map((event) => ({ ...event })),
|
|
75
84
|
updatedAt: overlay.updatedAt
|
|
76
85
|
};
|
|
77
86
|
}
|
|
@@ -109,16 +118,16 @@ const editFileTool = defineTool({
|
|
|
109
118
|
execute: (context, args) => editWorkspaceFile(context.workspaceRoot, args, { logger: context.logger })
|
|
110
119
|
});
|
|
111
120
|
async function editWorkspaceFile(workspaceRoot, args, options = {}) {
|
|
112
|
-
const scopedPath = resolveWorkspaceScopedPath$
|
|
121
|
+
const scopedPath = resolveWorkspaceScopedPath$4(workspaceRoot, args.path);
|
|
113
122
|
return enqueueFileMutation(scopedPath.path, async () => {
|
|
114
123
|
const fileStat = await statExistingFile(scopedPath.path, args.path);
|
|
115
124
|
const beforeBytes = await readFile(scopedPath.path);
|
|
116
|
-
const beforeHash = hashBytes(beforeBytes);
|
|
125
|
+
const beforeHash = hashBytes$1(beforeBytes);
|
|
117
126
|
if (args.expected_hash && args.expected_hash !== beforeHash) throw new Error(`edit_file expected_hash did not match ${scopedPath.relativePath}.`);
|
|
118
|
-
const result = applyExactEdits(decodeUtf8(scopedPath.relativePath, beforeBytes), args.edits, scopedPath.relativePath);
|
|
127
|
+
const result = applyExactEdits(decodeUtf8$1(scopedPath.relativePath, beforeBytes), args.edits, scopedPath.relativePath);
|
|
119
128
|
const afterBytes = Buffer.from(result.newContent, "utf8");
|
|
120
|
-
const afterHash = hashBytes(afterBytes);
|
|
121
|
-
await writeFileAtomically(scopedPath.path, afterBytes, fileStat.mode);
|
|
129
|
+
const afterHash = hashBytes$1(afterBytes);
|
|
130
|
+
await writeFileAtomically$1(scopedPath.path, afterBytes, fileStat.mode);
|
|
122
131
|
const editEvent = {
|
|
123
132
|
kind: "file_edit",
|
|
124
133
|
source: "agent",
|
|
@@ -172,7 +181,7 @@ function applyExactEdits(content, edits, path = "file") {
|
|
|
172
181
|
firstChangedLine: getFirstChangedLine(document.body, newBody)
|
|
173
182
|
};
|
|
174
183
|
}
|
|
175
|
-
function resolveWorkspaceScopedPath$
|
|
184
|
+
function resolveWorkspaceScopedPath$4(workspaceRoot, path) {
|
|
176
185
|
if (path.includes("\0")) throw new Error("edit_file path is invalid.");
|
|
177
186
|
const resolvedWorkspace = resolve(workspaceRoot);
|
|
178
187
|
const resolvedPath = isAbsolute(path) ? resolve(path) : resolve(resolvedWorkspace, path);
|
|
@@ -189,13 +198,13 @@ async function statExistingFile(resolvedPath, originalPath) {
|
|
|
189
198
|
try {
|
|
190
199
|
fileStat = await stat(resolvedPath);
|
|
191
200
|
} catch (error) {
|
|
192
|
-
if (isNodeError$
|
|
201
|
+
if (isNodeError$3(error) && error.code === "ENOENT") throw new Error(`edit_file can only edit existing files: ${originalPath}`);
|
|
193
202
|
throw error;
|
|
194
203
|
}
|
|
195
204
|
if (!fileStat.isFile()) throw new Error(`edit_file can only edit regular files: ${originalPath}`);
|
|
196
205
|
return fileStat;
|
|
197
206
|
}
|
|
198
|
-
function decodeUtf8(path, bytes) {
|
|
207
|
+
function decodeUtf8$1(path, bytes) {
|
|
199
208
|
try {
|
|
200
209
|
return new TextDecoder("utf-8", {
|
|
201
210
|
fatal: true,
|
|
@@ -205,7 +214,7 @@ function decodeUtf8(path, bytes) {
|
|
|
205
214
|
throw new Error(`edit_file can only edit UTF-8 text files: ${path}`);
|
|
206
215
|
}
|
|
207
216
|
}
|
|
208
|
-
async function writeFileAtomically(path, content, mode) {
|
|
217
|
+
async function writeFileAtomically$1(path, content, mode) {
|
|
209
218
|
const temporaryPath = resolve(dirname(path), `.${basename(path)}.topchester-${process.pid}-${randomUUID()}.tmp`);
|
|
210
219
|
try {
|
|
211
220
|
await writeFile(temporaryPath, content, {
|
|
@@ -218,7 +227,7 @@ async function writeFileAtomically(path, content, mode) {
|
|
|
218
227
|
throw error;
|
|
219
228
|
}
|
|
220
229
|
}
|
|
221
|
-
function hashBytes(bytes) {
|
|
230
|
+
function hashBytes$1(bytes) {
|
|
222
231
|
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
223
232
|
}
|
|
224
233
|
function summarizeDiff(diff) {
|
|
@@ -231,7 +240,7 @@ function summarizeDiff(diff) {
|
|
|
231
240
|
}
|
|
232
241
|
return `+${added}/-${removed}`;
|
|
233
242
|
}
|
|
234
|
-
function isNodeError$
|
|
243
|
+
function isNodeError$3(error) {
|
|
235
244
|
return error instanceof Error && "code" in error;
|
|
236
245
|
}
|
|
237
246
|
function normalizeEdits(edits) {
|
|
@@ -376,7 +385,7 @@ const findFileTool = defineTool({
|
|
|
376
385
|
})
|
|
377
386
|
});
|
|
378
387
|
async function findWorkspaceFilesByName(workspaceRoot, args, options = {}) {
|
|
379
|
-
const scopedPath = resolveWorkspaceScopedPath$
|
|
388
|
+
const scopedPath = resolveWorkspaceScopedPath$3(workspaceRoot, args.path);
|
|
380
389
|
const matches = (await collectWorkspaceFiles(scopedPath.workspaceRoot, scopedPath.path, scopedPath.relativePath, options)).map((path) => {
|
|
381
390
|
const score = scoreFileMatch(args.query, path);
|
|
382
391
|
return score > 0 ? {
|
|
@@ -574,7 +583,7 @@ function scoreSubsequenceMatch(query, value) {
|
|
|
574
583
|
function normalize(value) {
|
|
575
584
|
return value.trim().toLowerCase().replaceAll("\\", "/");
|
|
576
585
|
}
|
|
577
|
-
function resolveWorkspaceScopedPath$
|
|
586
|
+
function resolveWorkspaceScopedPath$3(workspaceRoot, path) {
|
|
578
587
|
const resolvedWorkspace = resolve(workspaceRoot);
|
|
579
588
|
const resolvedPath = isAbsolute(path) ? resolve(path) : resolve(resolvedWorkspace, path);
|
|
580
589
|
const relativePath = relative(resolvedWorkspace, resolvedPath);
|
|
@@ -618,6 +627,714 @@ function getExitCode$1(error) {
|
|
|
618
627
|
function isRecord$3(value) {
|
|
619
628
|
return typeof value === "object" && value !== null;
|
|
620
629
|
}
|
|
630
|
+
//#endregion
|
|
631
|
+
//#region src/agent/tools/git-runner.ts
|
|
632
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
633
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 4e4;
|
|
634
|
+
const GIT_FIELD_SEPARATOR = "";
|
|
635
|
+
async function runGit(options) {
|
|
636
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
637
|
+
const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
638
|
+
const args = [
|
|
639
|
+
"--no-optional-locks",
|
|
640
|
+
"-c",
|
|
641
|
+
"core.quotepath=false",
|
|
642
|
+
"-c",
|
|
643
|
+
"core.fsmonitor=false",
|
|
644
|
+
...options.args
|
|
645
|
+
];
|
|
646
|
+
return new Promise((resolveCommand) => {
|
|
647
|
+
const child = spawn("git", args, {
|
|
648
|
+
cwd: options.cwd,
|
|
649
|
+
env: {
|
|
650
|
+
...process.env,
|
|
651
|
+
PATH: options.pathEnv ?? process.env.PATH ?? "",
|
|
652
|
+
GIT_OPTIONAL_LOCKS: "0",
|
|
653
|
+
GIT_PAGER: "cat",
|
|
654
|
+
PAGER: "cat",
|
|
655
|
+
LESS: "-F -X"
|
|
656
|
+
},
|
|
657
|
+
stdio: [
|
|
658
|
+
"ignore",
|
|
659
|
+
"pipe",
|
|
660
|
+
"pipe"
|
|
661
|
+
]
|
|
662
|
+
});
|
|
663
|
+
const stdoutChunks = [];
|
|
664
|
+
const stderrChunks = [];
|
|
665
|
+
let stdoutBytes = 0;
|
|
666
|
+
let stderrBytes = 0;
|
|
667
|
+
let truncated = false;
|
|
668
|
+
let timedOut = false;
|
|
669
|
+
let settled = false;
|
|
670
|
+
const timer = setTimeout(() => {
|
|
671
|
+
timedOut = true;
|
|
672
|
+
child.kill("SIGTERM");
|
|
673
|
+
setTimeout(() => {
|
|
674
|
+
if (!settled) child.kill("SIGKILL");
|
|
675
|
+
}, 500).unref();
|
|
676
|
+
}, timeoutMs);
|
|
677
|
+
child.stdout.on("data", (chunk) => {
|
|
678
|
+
const appended = appendBoundedChunk(stdoutChunks, stdoutBytes, chunk, maxOutputBytes);
|
|
679
|
+
stdoutBytes = appended.bytes;
|
|
680
|
+
truncated = truncated || appended.truncated;
|
|
681
|
+
});
|
|
682
|
+
child.stderr.on("data", (chunk) => {
|
|
683
|
+
const appended = appendBoundedChunk(stderrChunks, stderrBytes, chunk, Math.min(8e3, maxOutputBytes));
|
|
684
|
+
stderrBytes = appended.bytes;
|
|
685
|
+
truncated = truncated || appended.truncated;
|
|
686
|
+
});
|
|
687
|
+
child.once("error", (error) => {
|
|
688
|
+
clearTimeout(timer);
|
|
689
|
+
settled = true;
|
|
690
|
+
resolveCommand({
|
|
691
|
+
stdout: "",
|
|
692
|
+
stderr: error.code === "ENOENT" ? "git is not available on PATH.\n" : `${error.message}\n`,
|
|
693
|
+
exitCode: 127,
|
|
694
|
+
timedOut: false,
|
|
695
|
+
truncated: false,
|
|
696
|
+
binary: false,
|
|
697
|
+
missingGit: error.code === "ENOENT"
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
child.once("close", (code) => {
|
|
701
|
+
clearTimeout(timer);
|
|
702
|
+
settled = true;
|
|
703
|
+
const stdout = Buffer.concat(stdoutChunks);
|
|
704
|
+
const stderr = Buffer.concat(stderrChunks);
|
|
705
|
+
const binary = !options.allowNulOutput && (containsNul(stdout) || containsNul(stderr));
|
|
706
|
+
resolveCommand({
|
|
707
|
+
stdout: binary ? "" : stdout.toString("utf8"),
|
|
708
|
+
stderr: binary ? "git output contained binary data.\n" : stderr.toString("utf8"),
|
|
709
|
+
exitCode: code ?? (timedOut ? 124 : 1),
|
|
710
|
+
timedOut,
|
|
711
|
+
truncated,
|
|
712
|
+
binary,
|
|
713
|
+
missingGit: false
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
function ensureInsideWorkspace(workspaceRoot, path) {
|
|
719
|
+
if (path.length === 0 || path.includes("\0")) throw new Error("Git path is invalid.");
|
|
720
|
+
const resolvedWorkspace = resolve(workspaceRoot);
|
|
721
|
+
const absolutePath = isAbsolute(path) ? resolve(path) : resolve(resolvedWorkspace, path);
|
|
722
|
+
const relativePath = relative(resolvedWorkspace, absolutePath);
|
|
723
|
+
if (relativePath.startsWith("..") || isAbsolute(relativePath)) throw new Error(`Git path must stay inside the workspace: ${path}`);
|
|
724
|
+
return {
|
|
725
|
+
workspaceRoot: resolvedWorkspace,
|
|
726
|
+
absolutePath,
|
|
727
|
+
relativePath: relativePath || "."
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
async function getRepoInfo(workspaceRoot, pathEnv) {
|
|
731
|
+
const workspace = await realpath(resolve(workspaceRoot));
|
|
732
|
+
const root = await runGit({
|
|
733
|
+
cwd: workspace,
|
|
734
|
+
pathEnv,
|
|
735
|
+
args: ["rev-parse", "--show-toplevel"],
|
|
736
|
+
maxOutputBytes: 8e3
|
|
737
|
+
});
|
|
738
|
+
if (root.missingGit) return {
|
|
739
|
+
available: false,
|
|
740
|
+
missingGit: true,
|
|
741
|
+
repoRoot: null,
|
|
742
|
+
repoRootAbsolute: null,
|
|
743
|
+
branch: null,
|
|
744
|
+
head: null,
|
|
745
|
+
hasHead: false,
|
|
746
|
+
message: "git is not available on PATH."
|
|
747
|
+
};
|
|
748
|
+
if (root.exitCode !== 0) return {
|
|
749
|
+
available: false,
|
|
750
|
+
missingGit: false,
|
|
751
|
+
repoRoot: null,
|
|
752
|
+
repoRootAbsolute: null,
|
|
753
|
+
branch: null,
|
|
754
|
+
head: null,
|
|
755
|
+
hasHead: false,
|
|
756
|
+
message: "This workspace is not inside a Git repository."
|
|
757
|
+
};
|
|
758
|
+
const repoRootAbsolute = root.stdout.trim();
|
|
759
|
+
const repoRoot = relative(workspace, repoRootAbsolute) || ".";
|
|
760
|
+
const hasHead = (await runGit({
|
|
761
|
+
cwd: workspace,
|
|
762
|
+
pathEnv,
|
|
763
|
+
args: [
|
|
764
|
+
"rev-parse",
|
|
765
|
+
"--verify",
|
|
766
|
+
"HEAD"
|
|
767
|
+
],
|
|
768
|
+
maxOutputBytes: 8e3
|
|
769
|
+
})).exitCode === 0;
|
|
770
|
+
const branchResult = await runGit({
|
|
771
|
+
cwd: workspace,
|
|
772
|
+
pathEnv,
|
|
773
|
+
args: [
|
|
774
|
+
"symbolic-ref",
|
|
775
|
+
"--quiet",
|
|
776
|
+
"--short",
|
|
777
|
+
"HEAD"
|
|
778
|
+
],
|
|
779
|
+
maxOutputBytes: 8e3
|
|
780
|
+
});
|
|
781
|
+
const headResult = hasHead ? await runGit({
|
|
782
|
+
cwd: workspace,
|
|
783
|
+
pathEnv,
|
|
784
|
+
args: [
|
|
785
|
+
"rev-parse",
|
|
786
|
+
"--short",
|
|
787
|
+
"HEAD"
|
|
788
|
+
],
|
|
789
|
+
maxOutputBytes: 8e3
|
|
790
|
+
}) : void 0;
|
|
791
|
+
return {
|
|
792
|
+
available: true,
|
|
793
|
+
missingGit: false,
|
|
794
|
+
repoRoot,
|
|
795
|
+
repoRootAbsolute,
|
|
796
|
+
branch: branchResult.exitCode === 0 ? branchResult.stdout.trim() : null,
|
|
797
|
+
head: headResult?.exitCode === 0 ? headResult.stdout.trim() : null,
|
|
798
|
+
hasHead
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function parsePorcelainStatus(output) {
|
|
802
|
+
return output.split("\0").filter(Boolean).map((entry) => {
|
|
803
|
+
const indexStatus = entry[0] ?? " ";
|
|
804
|
+
const worktreeStatus = entry[1] ?? " ";
|
|
805
|
+
const path = entry.slice(3);
|
|
806
|
+
const untracked = indexStatus === "?" && worktreeStatus === "?";
|
|
807
|
+
const staged = !untracked && indexStatus !== " ";
|
|
808
|
+
const unstaged = !untracked && worktreeStatus !== " ";
|
|
809
|
+
return {
|
|
810
|
+
path,
|
|
811
|
+
indexStatus,
|
|
812
|
+
worktreeStatus,
|
|
813
|
+
status: classifyStatus(indexStatus, worktreeStatus),
|
|
814
|
+
staged,
|
|
815
|
+
unstaged,
|
|
816
|
+
untracked
|
|
817
|
+
};
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
function parseGitLog(output) {
|
|
821
|
+
return output.split("\n").filter(Boolean).map((line) => {
|
|
822
|
+
const [sha = "", shortSha = "", timestamp = "0", authorName = "", subject = ""] = line.split(GIT_FIELD_SEPARATOR);
|
|
823
|
+
return {
|
|
824
|
+
sha,
|
|
825
|
+
shortSha,
|
|
826
|
+
timestamp: Number(timestamp),
|
|
827
|
+
authorName,
|
|
828
|
+
subject
|
|
829
|
+
};
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
function truncateText(content, maxBytes) {
|
|
833
|
+
if (Buffer.byteLength(content) <= maxBytes) return {
|
|
834
|
+
content,
|
|
835
|
+
truncated: false
|
|
836
|
+
};
|
|
837
|
+
return {
|
|
838
|
+
content: Buffer.from(content, "utf8").subarray(0, maxBytes).toString("utf8"),
|
|
839
|
+
truncated: true
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
function gitLogPrettyFormat() {
|
|
843
|
+
return `%H%x1f%h%x1f%ct%x1f%an%x1f%s`;
|
|
844
|
+
}
|
|
845
|
+
function classifyStatus(indexStatus, worktreeStatus) {
|
|
846
|
+
if (indexStatus === "?" && worktreeStatus === "?") return "untracked";
|
|
847
|
+
if (indexStatus === "U" || worktreeStatus === "U" || indexStatus === "A" && worktreeStatus === "A") return "conflicted";
|
|
848
|
+
if (indexStatus === "R") return "renamed";
|
|
849
|
+
if (indexStatus === "C") return "copied";
|
|
850
|
+
if (indexStatus === "D" || worktreeStatus === "D") return "deleted";
|
|
851
|
+
if (indexStatus === "A") return "added";
|
|
852
|
+
if (indexStatus === "M" || worktreeStatus === "M") return "modified";
|
|
853
|
+
return "unknown";
|
|
854
|
+
}
|
|
855
|
+
function appendBoundedChunk(chunks, currentBytes, chunk, maxBytes) {
|
|
856
|
+
if (currentBytes >= maxBytes) return {
|
|
857
|
+
bytes: currentBytes,
|
|
858
|
+
truncated: true
|
|
859
|
+
};
|
|
860
|
+
const remaining = maxBytes - currentBytes;
|
|
861
|
+
const slice = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;
|
|
862
|
+
chunks.push(slice);
|
|
863
|
+
return {
|
|
864
|
+
bytes: currentBytes + slice.length,
|
|
865
|
+
truncated: chunk.length > remaining
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
function containsNul(buffer) {
|
|
869
|
+
return buffer.includes(0);
|
|
870
|
+
}
|
|
871
|
+
//#endregion
|
|
872
|
+
//#region src/agent/tools/git.ts
|
|
873
|
+
const gitStatusValueSchema = z.enum([
|
|
874
|
+
"added",
|
|
875
|
+
"modified",
|
|
876
|
+
"deleted",
|
|
877
|
+
"renamed",
|
|
878
|
+
"copied",
|
|
879
|
+
"untracked",
|
|
880
|
+
"conflicted",
|
|
881
|
+
"unknown"
|
|
882
|
+
]);
|
|
883
|
+
const gitStatusArgsSchema = z.object({
|
|
884
|
+
path: z.string().optional().default("."),
|
|
885
|
+
include_untracked: z.boolean().optional().default(true)
|
|
886
|
+
});
|
|
887
|
+
const gitDiffArgsSchema = z.object({
|
|
888
|
+
scope: z.enum([
|
|
889
|
+
"all",
|
|
890
|
+
"unstaged",
|
|
891
|
+
"staged"
|
|
892
|
+
]).optional().default("all"),
|
|
893
|
+
path: z.string().optional(),
|
|
894
|
+
include_untracked: z.boolean().optional().default(false),
|
|
895
|
+
context_lines: z.number().int().min(0).max(20).optional().default(3),
|
|
896
|
+
max_bytes: z.number().int().min(1e3).max(2e5).optional().default(4e4)
|
|
897
|
+
});
|
|
898
|
+
const gitLogArgsSchema = z.object({
|
|
899
|
+
limit: z.number().int().min(1).max(50).optional().default(10),
|
|
900
|
+
path: z.string().optional()
|
|
901
|
+
});
|
|
902
|
+
const gitAddArgsSchema = z.object({
|
|
903
|
+
paths: z.array(z.string().min(1)).min(1),
|
|
904
|
+
expected_status: z.array(z.object({
|
|
905
|
+
path: z.string().min(1),
|
|
906
|
+
status: gitStatusValueSchema
|
|
907
|
+
})).min(1)
|
|
908
|
+
});
|
|
909
|
+
const gitCommitArgsSchema = z.object({
|
|
910
|
+
message: z.string().trim().min(1).max(500),
|
|
911
|
+
expected_staged_paths: z.array(z.string().min(1)).min(1)
|
|
912
|
+
});
|
|
913
|
+
const gitStatusTool = defineTool({
|
|
914
|
+
name: "git_status",
|
|
915
|
+
description: "Inspect structured Git branch and changed-file status inside the workspace.",
|
|
916
|
+
prompt: "git_status: inspect branch, head, clean state, staged, unstaged, and untracked files without parsing shell output. To use it, reply with only JSON: {\"tool\":\"git_status\",\"args\":{\"path\":\".\",\"include_untracked\":true}}",
|
|
917
|
+
argsSchema: gitStatusArgsSchema,
|
|
918
|
+
execute: (context, args) => inspectGitStatus(context, args)
|
|
919
|
+
});
|
|
920
|
+
const gitDiffTool = defineTool({
|
|
921
|
+
name: "git_diff",
|
|
922
|
+
description: "Inspect bounded Git diffs for staged, unstaged, and optionally untracked files.",
|
|
923
|
+
prompt: "git_diff: inspect a bounded Git diff; use scope \"all\", \"unstaged\", or \"staged\", and include_untracked:true only when untracked file patches are needed. To use it, reply with only JSON: {\"tool\":\"git_diff\",\"args\":{\"scope\":\"all\",\"include_untracked\":true}}",
|
|
924
|
+
argsSchema: gitDiffArgsSchema,
|
|
925
|
+
execute: (context, args) => inspectGitDiff(context, args)
|
|
926
|
+
});
|
|
927
|
+
const gitLogTool = defineTool({
|
|
928
|
+
name: "git_log",
|
|
929
|
+
description: "Inspect recent Git commits as bounded structured summaries.",
|
|
930
|
+
prompt: "git_log: inspect recent commits without parsing shell output. To use it, reply with only JSON: {\"tool\":\"git_log\",\"args\":{\"limit\":10,\"path\":\"src/agent/runtime.ts\"}}",
|
|
931
|
+
argsSchema: gitLogArgsSchema,
|
|
932
|
+
execute: (context, args) => inspectGitLog(context, args)
|
|
933
|
+
});
|
|
934
|
+
const gitAddTool = defineTool({
|
|
935
|
+
name: "git_add",
|
|
936
|
+
description: "Stage explicit changed paths after current Git status has been inspected.",
|
|
937
|
+
prompt: "git_add: stage only explicit paths the user asked to stage; first inspect git_status, reject broad paths, and pass expected_status for each path. To use it, reply with only JSON: {\"tool\":\"git_add\",\"args\":{\"paths\":[\"src/example.ts\"],\"expected_status\":[{\"path\":\"src/example.ts\",\"status\":\"modified\"}]}}",
|
|
938
|
+
argsSchema: gitAddArgsSchema,
|
|
939
|
+
execute: (context, args) => stageGitPaths(context, args)
|
|
940
|
+
});
|
|
941
|
+
const gitCommitTool = defineTool({
|
|
942
|
+
name: "git_commit",
|
|
943
|
+
description: "Create a Git commit from exactly the expected staged paths.",
|
|
944
|
+
prompt: "git_commit: commit only after the user explicitly asks and staged paths exactly match expected_staged_paths. To use it, reply with only JSON: {\"tool\":\"git_commit\",\"args\":{\"message\":\"Add feature\",\"expected_staged_paths\":[\"src/example.ts\"]}}",
|
|
945
|
+
argsSchema: gitCommitArgsSchema,
|
|
946
|
+
execute: (context, args) => commitGitPaths(context, args)
|
|
947
|
+
});
|
|
948
|
+
async function inspectGitStatus(context, args) {
|
|
949
|
+
const repo = await getRepoInfo(context.workspaceRoot, context.pathEnv);
|
|
950
|
+
const unavailable = formatUnavailable("git_status", repo);
|
|
951
|
+
if (unavailable) return {
|
|
952
|
+
...unavailable,
|
|
953
|
+
branch: null,
|
|
954
|
+
head: null,
|
|
955
|
+
hasHead: false,
|
|
956
|
+
clean: true,
|
|
957
|
+
files: [],
|
|
958
|
+
truncated: false
|
|
959
|
+
};
|
|
960
|
+
const scopedPath = ensureInsideWorkspace(context.workspaceRoot, args.path);
|
|
961
|
+
const result = await runGit({
|
|
962
|
+
cwd: requireRepoRoot(repo),
|
|
963
|
+
pathEnv: context.pathEnv,
|
|
964
|
+
allowNulOutput: true,
|
|
965
|
+
args: [
|
|
966
|
+
"status",
|
|
967
|
+
"--porcelain=v1",
|
|
968
|
+
"-z",
|
|
969
|
+
"--no-renames",
|
|
970
|
+
args.include_untracked ? "--untracked-files=all" : "--untracked-files=no",
|
|
971
|
+
"--",
|
|
972
|
+
scopedPath.relativePath
|
|
973
|
+
]
|
|
974
|
+
});
|
|
975
|
+
const files = parsePorcelainStatus(result.stdout);
|
|
976
|
+
return {
|
|
977
|
+
tool: "git_status",
|
|
978
|
+
path: scopedPath.relativePath,
|
|
979
|
+
content: formatGitStatusContent(repo, files),
|
|
980
|
+
repoRoot: repo.repoRoot,
|
|
981
|
+
branch: repo.branch,
|
|
982
|
+
head: repo.head,
|
|
983
|
+
hasHead: repo.hasHead,
|
|
984
|
+
clean: files.length === 0,
|
|
985
|
+
files,
|
|
986
|
+
truncated: result.truncated,
|
|
987
|
+
warning: result.truncated ? "git_status output was truncated." : void 0
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
async function inspectGitDiff(context, args) {
|
|
991
|
+
const repo = await getRepoInfo(context.workspaceRoot, context.pathEnv);
|
|
992
|
+
const unavailable = formatUnavailable("git_diff", repo);
|
|
993
|
+
if (unavailable) return {
|
|
994
|
+
...unavailable,
|
|
995
|
+
scope: args.scope,
|
|
996
|
+
path: args.path,
|
|
997
|
+
fileCount: 0,
|
|
998
|
+
truncated: false
|
|
999
|
+
};
|
|
1000
|
+
const repoRoot = requireRepoRoot(repo);
|
|
1001
|
+
const path = args.path ? ensureInsideWorkspace(context.workspaceRoot, args.path).relativePath : void 0;
|
|
1002
|
+
const sections = [];
|
|
1003
|
+
let truncated = false;
|
|
1004
|
+
const changedFiles = /* @__PURE__ */ new Set();
|
|
1005
|
+
if (args.scope === "all" || args.scope === "staged") {
|
|
1006
|
+
const diff = await runDiff(repoRoot, [
|
|
1007
|
+
"diff",
|
|
1008
|
+
"--cached",
|
|
1009
|
+
"--no-ext-diff",
|
|
1010
|
+
"--no-textconv",
|
|
1011
|
+
"--no-renames"
|
|
1012
|
+
], {
|
|
1013
|
+
context,
|
|
1014
|
+
path,
|
|
1015
|
+
contextLines: args.context_lines,
|
|
1016
|
+
maxBytes: args.max_bytes
|
|
1017
|
+
});
|
|
1018
|
+
appendSection(sections, "staged", diff.content);
|
|
1019
|
+
truncated = truncated || diff.truncated;
|
|
1020
|
+
for (const file of await diffNameOnly(repoRoot, true, context, path)) changedFiles.add(file);
|
|
1021
|
+
}
|
|
1022
|
+
if (args.scope === "all" || args.scope === "unstaged") {
|
|
1023
|
+
const diff = await runDiff(repoRoot, [
|
|
1024
|
+
"diff",
|
|
1025
|
+
"--no-ext-diff",
|
|
1026
|
+
"--no-textconv",
|
|
1027
|
+
"--no-renames"
|
|
1028
|
+
], {
|
|
1029
|
+
context,
|
|
1030
|
+
path,
|
|
1031
|
+
contextLines: args.context_lines,
|
|
1032
|
+
maxBytes: args.max_bytes
|
|
1033
|
+
});
|
|
1034
|
+
appendSection(sections, "unstaged", diff.content);
|
|
1035
|
+
truncated = truncated || diff.truncated;
|
|
1036
|
+
for (const file of await diffNameOnly(repoRoot, false, context, path)) changedFiles.add(file);
|
|
1037
|
+
}
|
|
1038
|
+
if (args.include_untracked && args.scope !== "staged") {
|
|
1039
|
+
const untracked = await getUntrackedFiles(repoRoot, context, path);
|
|
1040
|
+
for (const file of untracked) {
|
|
1041
|
+
const diff = await runDiff(repoRoot, [
|
|
1042
|
+
"diff",
|
|
1043
|
+
"--no-index",
|
|
1044
|
+
"--no-ext-diff",
|
|
1045
|
+
"--no-textconv",
|
|
1046
|
+
"--no-renames"
|
|
1047
|
+
], {
|
|
1048
|
+
context,
|
|
1049
|
+
path: void 0,
|
|
1050
|
+
extraPathspecs: ["/dev/null", file],
|
|
1051
|
+
contextLines: args.context_lines,
|
|
1052
|
+
maxBytes: args.max_bytes
|
|
1053
|
+
});
|
|
1054
|
+
appendSection(sections, `untracked ${file}`, diff.content);
|
|
1055
|
+
truncated = truncated || diff.truncated;
|
|
1056
|
+
changedFiles.add(file);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const bounded = truncateText(sections.join("\n").trimEnd() || "No diff.", args.max_bytes);
|
|
1060
|
+
return {
|
|
1061
|
+
tool: "git_diff",
|
|
1062
|
+
path: path ?? void 0,
|
|
1063
|
+
content: bounded.content,
|
|
1064
|
+
repoRoot: repo.repoRoot,
|
|
1065
|
+
scope: args.scope,
|
|
1066
|
+
fileCount: changedFiles.size,
|
|
1067
|
+
truncated: truncated || bounded.truncated,
|
|
1068
|
+
warning: truncated || bounded.truncated ? "git_diff output was truncated." : void 0
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
async function inspectGitLog(context, args) {
|
|
1072
|
+
const repo = await getRepoInfo(context.workspaceRoot, context.pathEnv);
|
|
1073
|
+
const unavailable = formatUnavailable("git_log", repo);
|
|
1074
|
+
if (unavailable) return {
|
|
1075
|
+
...unavailable,
|
|
1076
|
+
commits: [],
|
|
1077
|
+
truncated: false
|
|
1078
|
+
};
|
|
1079
|
+
if (!repo.hasHead) return {
|
|
1080
|
+
tool: "git_log",
|
|
1081
|
+
path: args.path,
|
|
1082
|
+
content: "This Git repository has no commits yet.",
|
|
1083
|
+
repoRoot: repo.repoRoot,
|
|
1084
|
+
commits: [],
|
|
1085
|
+
truncated: false,
|
|
1086
|
+
warning: "Git repository has no commits."
|
|
1087
|
+
};
|
|
1088
|
+
const repoRoot = requireRepoRoot(repo);
|
|
1089
|
+
const path = args.path ? ensureInsideWorkspace(context.workspaceRoot, args.path).relativePath : void 0;
|
|
1090
|
+
const result = await runGit({
|
|
1091
|
+
cwd: repoRoot,
|
|
1092
|
+
pathEnv: context.pathEnv,
|
|
1093
|
+
args: [
|
|
1094
|
+
"log",
|
|
1095
|
+
"-n",
|
|
1096
|
+
String(args.limit),
|
|
1097
|
+
`--pretty=format:${gitLogPrettyFormat()}`,
|
|
1098
|
+
"--",
|
|
1099
|
+
...path ? [path] : []
|
|
1100
|
+
]
|
|
1101
|
+
});
|
|
1102
|
+
const commits = parseGitLog(result.stdout);
|
|
1103
|
+
return {
|
|
1104
|
+
tool: "git_log",
|
|
1105
|
+
path,
|
|
1106
|
+
content: commits.length === 0 ? "No commits matched." : commits.map(formatCommitSummary).join("\n"),
|
|
1107
|
+
repoRoot: repo.repoRoot,
|
|
1108
|
+
commits,
|
|
1109
|
+
truncated: result.truncated,
|
|
1110
|
+
warning: result.truncated ? "git_log output was truncated." : void 0
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
async function stageGitPaths(context, args) {
|
|
1114
|
+
const repo = await requireAvailableRepo(context);
|
|
1115
|
+
const repoRoot = requireRepoRoot(repo);
|
|
1116
|
+
const paths = normalizeExplicitPaths(context.workspaceRoot, args.paths);
|
|
1117
|
+
const expected = new Map(args.expected_status.map((entry) => [entry.path, entry.status]));
|
|
1118
|
+
const status = await currentStatus(repoRoot, context, true);
|
|
1119
|
+
const statusByPath = new Map(status.map((file) => [file.path, file]));
|
|
1120
|
+
for (const path of paths) {
|
|
1121
|
+
const file = statusByPath.get(path);
|
|
1122
|
+
const expectedStatus = expected.get(path);
|
|
1123
|
+
if (!expectedStatus) throw new Error(`git_add expected_status is required for ${path}.`);
|
|
1124
|
+
if (!file) throw new Error(`git_add can only stage paths present in git_status: ${path}`);
|
|
1125
|
+
if (file.status !== expectedStatus) throw new Error(`git_add expected ${path} to be ${expectedStatus}, but it is ${file.status}.`);
|
|
1126
|
+
}
|
|
1127
|
+
const result = await runGit({
|
|
1128
|
+
cwd: repoRoot,
|
|
1129
|
+
pathEnv: context.pathEnv,
|
|
1130
|
+
args: [
|
|
1131
|
+
"add",
|
|
1132
|
+
"--",
|
|
1133
|
+
...paths
|
|
1134
|
+
]
|
|
1135
|
+
});
|
|
1136
|
+
if (result.exitCode !== 0) throw new Error(`git_add failed: ${result.stderr || result.stdout}`.trim());
|
|
1137
|
+
const files = await currentStatus(repoRoot, context, true);
|
|
1138
|
+
const stagedPaths = files.filter((file) => file.staged).map((file) => file.path);
|
|
1139
|
+
return {
|
|
1140
|
+
tool: "git_add",
|
|
1141
|
+
content: [
|
|
1142
|
+
`staged_paths: ${paths.join(", ")}`,
|
|
1143
|
+
"",
|
|
1144
|
+
formatGitStatusFileList(files)
|
|
1145
|
+
].join("\n").trimEnd(),
|
|
1146
|
+
repoRoot: repo.repoRoot,
|
|
1147
|
+
stagedPaths: paths,
|
|
1148
|
+
files,
|
|
1149
|
+
warning: paths.every((path) => stagedPaths.includes(path)) ? void 0 : "Some requested paths were not staged."
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
async function commitGitPaths(context, args) {
|
|
1153
|
+
const repo = await requireAvailableRepo(context);
|
|
1154
|
+
const repoRoot = requireRepoRoot(repo);
|
|
1155
|
+
const expectedPaths = normalizeExplicitPaths(context.workspaceRoot, args.expected_staged_paths);
|
|
1156
|
+
const stagedFiles = (await currentStatus(repoRoot, context, true)).filter((file) => file.staged);
|
|
1157
|
+
const stagedPaths = stagedFiles.map((file) => file.path).sort();
|
|
1158
|
+
if (stagedPaths.length === 0) throw new Error("git_commit requires staged changes.");
|
|
1159
|
+
if (!sameStringSet(stagedPaths, [...expectedPaths].sort())) throw new Error(`git_commit staged paths did not match expected_staged_paths. staged=${stagedPaths.join(", ")} expected=${expectedPaths.join(", ")}`);
|
|
1160
|
+
const stagedWithUnstagedChanges = stagedFiles.filter((file) => file.unstaged).map((file) => file.path);
|
|
1161
|
+
if (stagedWithUnstagedChanges.length > 0) throw new Error(`git_commit refuses paths with both staged and unstaged changes: ${stagedWithUnstagedChanges.join(", ")}`);
|
|
1162
|
+
const stat = await runGit({
|
|
1163
|
+
cwd: repoRoot,
|
|
1164
|
+
pathEnv: context.pathEnv,
|
|
1165
|
+
args: [
|
|
1166
|
+
"diff",
|
|
1167
|
+
"--cached",
|
|
1168
|
+
"--stat",
|
|
1169
|
+
"--"
|
|
1170
|
+
]
|
|
1171
|
+
});
|
|
1172
|
+
const nameStatus = await runGit({
|
|
1173
|
+
cwd: repoRoot,
|
|
1174
|
+
pathEnv: context.pathEnv,
|
|
1175
|
+
args: [
|
|
1176
|
+
"diff",
|
|
1177
|
+
"--cached",
|
|
1178
|
+
"--name-status",
|
|
1179
|
+
"--"
|
|
1180
|
+
]
|
|
1181
|
+
});
|
|
1182
|
+
const commit = await runGit({
|
|
1183
|
+
cwd: repoRoot,
|
|
1184
|
+
pathEnv: context.pathEnv,
|
|
1185
|
+
args: [
|
|
1186
|
+
"commit",
|
|
1187
|
+
"--no-gpg-sign",
|
|
1188
|
+
"-m",
|
|
1189
|
+
args.message
|
|
1190
|
+
],
|
|
1191
|
+
timeoutMs: 3e4
|
|
1192
|
+
});
|
|
1193
|
+
if (commit.exitCode !== 0) throw new Error(`git_commit failed: ${commit.stderr || commit.stdout}`.trim());
|
|
1194
|
+
const commitSummary = parseGitLog((await runGit({
|
|
1195
|
+
cwd: repoRoot,
|
|
1196
|
+
pathEnv: context.pathEnv,
|
|
1197
|
+
args: [
|
|
1198
|
+
"log",
|
|
1199
|
+
"-n",
|
|
1200
|
+
"1",
|
|
1201
|
+
`--pretty=format:${gitLogPrettyFormat()}`
|
|
1202
|
+
]
|
|
1203
|
+
})).stdout)[0];
|
|
1204
|
+
if (!commitSummary) throw new Error("git_commit succeeded but the new commit could not be read.");
|
|
1205
|
+
const remainingFiles = await currentStatus(repoRoot, context, true);
|
|
1206
|
+
return {
|
|
1207
|
+
tool: "git_commit",
|
|
1208
|
+
content: [
|
|
1209
|
+
`commit: ${commitSummary.shortSha} ${commitSummary.subject}`,
|
|
1210
|
+
`staged_paths: ${stagedPaths.join(", ")}`,
|
|
1211
|
+
"",
|
|
1212
|
+
"stat:",
|
|
1213
|
+
stat.stdout.trim() || "(none)",
|
|
1214
|
+
"",
|
|
1215
|
+
"remaining:",
|
|
1216
|
+
formatGitStatusFileList(remainingFiles) || "clean"
|
|
1217
|
+
].join("\n"),
|
|
1218
|
+
repoRoot: repo.repoRoot,
|
|
1219
|
+
commit: commitSummary,
|
|
1220
|
+
stagedPaths,
|
|
1221
|
+
remainingFiles,
|
|
1222
|
+
stat: stat.stdout,
|
|
1223
|
+
nameStatus: nameStatus.stdout
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
async function requireAvailableRepo(context) {
|
|
1227
|
+
const repo = await getRepoInfo(context.workspaceRoot, context.pathEnv);
|
|
1228
|
+
if (!repo.available) throw new Error(repo.message ?? "Git repository is unavailable.");
|
|
1229
|
+
return repo;
|
|
1230
|
+
}
|
|
1231
|
+
async function currentStatus(repoRoot, context, includeUntracked) {
|
|
1232
|
+
return parsePorcelainStatus((await runGit({
|
|
1233
|
+
cwd: repoRoot,
|
|
1234
|
+
pathEnv: context.pathEnv,
|
|
1235
|
+
allowNulOutput: true,
|
|
1236
|
+
args: [
|
|
1237
|
+
"status",
|
|
1238
|
+
"--porcelain=v1",
|
|
1239
|
+
"-z",
|
|
1240
|
+
"--no-renames",
|
|
1241
|
+
includeUntracked ? "--untracked-files=all" : "--untracked-files=no",
|
|
1242
|
+
"--"
|
|
1243
|
+
]
|
|
1244
|
+
})).stdout);
|
|
1245
|
+
}
|
|
1246
|
+
async function runDiff(repoRoot, baseArgs, options) {
|
|
1247
|
+
const result = await runGit({
|
|
1248
|
+
cwd: repoRoot,
|
|
1249
|
+
pathEnv: options.context.pathEnv,
|
|
1250
|
+
args: [
|
|
1251
|
+
...baseArgs,
|
|
1252
|
+
`--unified=${options.contextLines}`,
|
|
1253
|
+
"--",
|
|
1254
|
+
...options.extraPathspecs ?? (options.path ? [options.path] : [])
|
|
1255
|
+
],
|
|
1256
|
+
maxOutputBytes: options.maxBytes,
|
|
1257
|
+
allowExitCodeOne: true
|
|
1258
|
+
});
|
|
1259
|
+
if (result.binary) return {
|
|
1260
|
+
content: "Binary diff omitted.",
|
|
1261
|
+
truncated: result.truncated
|
|
1262
|
+
};
|
|
1263
|
+
return {
|
|
1264
|
+
content: result.stdout,
|
|
1265
|
+
truncated: result.truncated
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
async function diffNameOnly(repoRoot, staged, context, path) {
|
|
1269
|
+
return (await runGit({
|
|
1270
|
+
cwd: repoRoot,
|
|
1271
|
+
pathEnv: context.pathEnv,
|
|
1272
|
+
args: [
|
|
1273
|
+
"diff",
|
|
1274
|
+
staged ? "--cached" : "--no-ext-diff",
|
|
1275
|
+
"--name-only",
|
|
1276
|
+
"--",
|
|
1277
|
+
...path ? [path] : []
|
|
1278
|
+
]
|
|
1279
|
+
})).stdout.split("\n").filter(Boolean);
|
|
1280
|
+
}
|
|
1281
|
+
async function getUntrackedFiles(repoRoot, context, path) {
|
|
1282
|
+
return (await runGit({
|
|
1283
|
+
cwd: repoRoot,
|
|
1284
|
+
pathEnv: context.pathEnv,
|
|
1285
|
+
args: [
|
|
1286
|
+
"ls-files",
|
|
1287
|
+
"--others",
|
|
1288
|
+
"--exclude-standard",
|
|
1289
|
+
"--",
|
|
1290
|
+
...path ? [path] : []
|
|
1291
|
+
]
|
|
1292
|
+
})).stdout.split("\n").filter(Boolean);
|
|
1293
|
+
}
|
|
1294
|
+
function appendSection(sections, label, content) {
|
|
1295
|
+
if (content.trim().length === 0) return;
|
|
1296
|
+
sections.push(`## ${label}\n${content.trimEnd()}\n`);
|
|
1297
|
+
}
|
|
1298
|
+
function formatUnavailable(tool, repo) {
|
|
1299
|
+
if (repo.available) return;
|
|
1300
|
+
return {
|
|
1301
|
+
tool,
|
|
1302
|
+
repoRoot: null,
|
|
1303
|
+
content: repo.message ?? "Git repository is unavailable.",
|
|
1304
|
+
warning: repo.message
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
function formatGitStatusContent(repo, files) {
|
|
1308
|
+
return [
|
|
1309
|
+
`branch: ${repo.branch ?? "(detached)"}`,
|
|
1310
|
+
`head: ${repo.head ?? "(none)"}`,
|
|
1311
|
+
`clean: ${files.length === 0}`,
|
|
1312
|
+
"",
|
|
1313
|
+
formatGitStatusFileList(files) || "No changed files."
|
|
1314
|
+
].join("\n");
|
|
1315
|
+
}
|
|
1316
|
+
function formatGitStatusFileList(files) {
|
|
1317
|
+
return files.map((file) => `${file.indexStatus}${file.worktreeStatus} ${file.path}`).sort((left, right) => left.localeCompare(right)).join("\n");
|
|
1318
|
+
}
|
|
1319
|
+
function formatCommitSummary(commit) {
|
|
1320
|
+
return `${commit.shortSha} ${commit.subject} (${commit.authorName}, ${(/* @__PURE__ */ new Date(commit.timestamp * 1e3)).toISOString()})`;
|
|
1321
|
+
}
|
|
1322
|
+
function requireRepoRoot(repo) {
|
|
1323
|
+
if (!repo.repoRootAbsolute) throw new Error("Git repository root is unavailable.");
|
|
1324
|
+
return repo.repoRootAbsolute;
|
|
1325
|
+
}
|
|
1326
|
+
function normalizeExplicitPaths(workspaceRoot, paths) {
|
|
1327
|
+
const normalized = paths.map((path) => {
|
|
1328
|
+
if (path === "." || path.includes("*") || path.includes("?") || path.includes("[") || path.includes("]")) throw new Error(`Git mutation tools require explicit file paths, not broad pathspecs: ${path}`);
|
|
1329
|
+
const scoped = ensureInsideWorkspace(workspaceRoot, path);
|
|
1330
|
+
if (scoped.relativePath === ".") throw new Error("Git mutation tools require explicit file paths.");
|
|
1331
|
+
return scoped.relativePath;
|
|
1332
|
+
});
|
|
1333
|
+
return [...new Set(normalized)];
|
|
1334
|
+
}
|
|
1335
|
+
function sameStringSet(left, right) {
|
|
1336
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1337
|
+
}
|
|
621
1338
|
const grepTool = defineTool({
|
|
622
1339
|
name: "grep",
|
|
623
1340
|
description: "Search text inside the workspace.",
|
|
@@ -632,7 +1349,7 @@ const grepTool = defineTool({
|
|
|
632
1349
|
})
|
|
633
1350
|
});
|
|
634
1351
|
async function grepWorkspace(workspaceRoot, args, options = {}) {
|
|
635
|
-
const scopedPath = resolveWorkspaceScopedPath$
|
|
1352
|
+
const scopedPath = resolveWorkspaceScopedPath$2(workspaceRoot, args.path ?? ".");
|
|
636
1353
|
const executable = await findSearchExecutable(options.pathEnv);
|
|
637
1354
|
if (!executable) {
|
|
638
1355
|
const warning = "grep could not run because neither rg nor grep is available on PATH.";
|
|
@@ -696,7 +1413,7 @@ async function grepWorkspace(workspaceRoot, args, options = {}) {
|
|
|
696
1413
|
content: truncateToolOutput(result.stdout.trimEnd() || "No matches.")
|
|
697
1414
|
};
|
|
698
1415
|
}
|
|
699
|
-
function resolveWorkspaceScopedPath$
|
|
1416
|
+
function resolveWorkspaceScopedPath$2(workspaceRoot, path) {
|
|
700
1417
|
const resolvedWorkspace = resolve(workspaceRoot);
|
|
701
1418
|
const resolvedPath = isAbsolute(path) ? resolve(path) : resolve(resolvedWorkspace, path);
|
|
702
1419
|
const relativePath = relative(resolvedWorkspace, resolvedPath);
|
|
@@ -1518,7 +2235,7 @@ const listFilesTool = defineTool({
|
|
|
1518
2235
|
execute: (context, args) => listWorkspaceFiles(context.workspaceRoot, args)
|
|
1519
2236
|
});
|
|
1520
2237
|
async function listWorkspaceFiles(workspaceRoot, args) {
|
|
1521
|
-
const scopedPath = resolveWorkspaceScopedPath(workspaceRoot, args.path);
|
|
2238
|
+
const scopedPath = resolveWorkspaceScopedPath$1(workspaceRoot, args.path);
|
|
1522
2239
|
if (!(await stat(scopedPath.path)).isDirectory()) throw new Error(`list_files can only list directories inside the workspace: ${args.path}`);
|
|
1523
2240
|
const entries = args.recursive ? await collectRecursiveEntries(scopedPath.workspaceRoot, scopedPath.path, args.limit) : await collectTopLevelEntries(scopedPath.workspaceRoot, scopedPath.path, args.limit);
|
|
1524
2241
|
const truncated = entries.truncated;
|
|
@@ -1582,7 +2299,7 @@ async function sortedDirectoryEntries(path) {
|
|
|
1582
2299
|
function formatEntryPath(path, isDirectory) {
|
|
1583
2300
|
return isDirectory ? `${path}/` : path;
|
|
1584
2301
|
}
|
|
1585
|
-
function resolveWorkspaceScopedPath(workspaceRoot, path) {
|
|
2302
|
+
function resolveWorkspaceScopedPath$1(workspaceRoot, path) {
|
|
1586
2303
|
const resolvedWorkspace = resolve(workspaceRoot);
|
|
1587
2304
|
const resolvedPath = isAbsolute(path) ? resolve(path) : resolve(resolvedWorkspace, path);
|
|
1588
2305
|
const relativePath = relative(resolvedWorkspace, resolvedPath);
|
|
@@ -1614,6 +2331,215 @@ async function readWorkspaceFile(workspaceRoot, path) {
|
|
|
1614
2331
|
hash: `sha256:${createHash("sha256").update(bytes).digest("hex")}`
|
|
1615
2332
|
};
|
|
1616
2333
|
}
|
|
2334
|
+
const writeFileTool = defineTool({
|
|
2335
|
+
name: "write_file",
|
|
2336
|
+
description: "Create a new UTF-8 file inside the workspace, or explicitly replace one with an expected hash.",
|
|
2337
|
+
prompt: "write_file: create a new UTF-8 file inside the workspace by default; use edit_file for targeted changes to existing files, pass create_parent_dirs:true only when creating the folder path is intended, and replace an existing whole file only with overwrite:true and expected_hash from read_file. To use it, reply with only JSON: {\"tool\":\"write_file\",\"args\":{\"path\":\"test/example.test.ts\",\"content\":\"import { it, expect } from \\\"vitest\\\";\\n\\nit(\\\"works\\\", () => {\\n expect(true).toBe(true);\\n});\\n\",\"create_parent_dirs\":true}}",
|
|
2338
|
+
argsSchema: z.object({
|
|
2339
|
+
path: z.string(),
|
|
2340
|
+
content: z.string(),
|
|
2341
|
+
create_parent_dirs: z.boolean().optional(),
|
|
2342
|
+
overwrite: z.boolean().optional(),
|
|
2343
|
+
expected_hash: z.string().optional()
|
|
2344
|
+
}),
|
|
2345
|
+
execute: (context, args) => writeWorkspaceFile(context.workspaceRoot, args, { logger: context.logger })
|
|
2346
|
+
});
|
|
2347
|
+
async function writeWorkspaceFile(workspaceRoot, args, options = {}) {
|
|
2348
|
+
const scopedPath = resolveWorkspaceScopedPath(workspaceRoot, args.path);
|
|
2349
|
+
return enqueueFileMutation(scopedPath.path, async () => {
|
|
2350
|
+
const existingTarget = await statTarget(scopedPath.path);
|
|
2351
|
+
if (args.overwrite) return overwriteExistingFile(workspaceRoot, scopedPath, args, existingTarget, options);
|
|
2352
|
+
if (existingTarget) throw new Error(`write_file can only create new files: ${scopedPath.relativePath}`);
|
|
2353
|
+
const createdParentDirs = await ensureParentDirectory(scopedPath.workspaceRoot, scopedPath.path, scopedPath.relativePath, Boolean(args.create_parent_dirs));
|
|
2354
|
+
const bytes = encodeUtf8Text(args.content);
|
|
2355
|
+
const hash = hashBytes(bytes);
|
|
2356
|
+
const lineCount = countLogicalLines$1(args.content);
|
|
2357
|
+
await writeFileAtomically(scopedPath.path, bytes);
|
|
2358
|
+
const writeEvent = {
|
|
2359
|
+
kind: "file_create",
|
|
2360
|
+
source: "agent",
|
|
2361
|
+
path: scopedPath.relativePath,
|
|
2362
|
+
afterHash: hash,
|
|
2363
|
+
firstChangedLine: 1,
|
|
2364
|
+
writeSummary: `created +${lineCount}`,
|
|
2365
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2366
|
+
};
|
|
2367
|
+
const overlayState = recordAgentFileCreate(workspaceRoot, writeEvent);
|
|
2368
|
+
options.logger?.debug({
|
|
2369
|
+
event: "file_create",
|
|
2370
|
+
...writeEvent,
|
|
2371
|
+
bytesWritten: bytes.length,
|
|
2372
|
+
lineCount,
|
|
2373
|
+
createdParentDirs,
|
|
2374
|
+
kbState: overlayState.kbState,
|
|
2375
|
+
dirtyFileCount: overlayState.dirtyFiles.length
|
|
2376
|
+
}, "file create");
|
|
2377
|
+
const content = [
|
|
2378
|
+
`Created ${scopedPath.relativePath}`,
|
|
2379
|
+
`hash: ${hash}`,
|
|
2380
|
+
`bytes_written: ${bytes.length}`,
|
|
2381
|
+
`line_count: ${lineCount}`,
|
|
2382
|
+
`kb_state: ${overlayState.kbState}`,
|
|
2383
|
+
createdParentDirs.length > 0 ? `created_parent_dirs: ${createdParentDirs.join(", ")}` : void 0
|
|
2384
|
+
].filter(Boolean).join("\n");
|
|
2385
|
+
return {
|
|
2386
|
+
tool: "write_file",
|
|
2387
|
+
path: scopedPath.relativePath,
|
|
2388
|
+
content,
|
|
2389
|
+
hash,
|
|
2390
|
+
bytesWritten: bytes.length,
|
|
2391
|
+
lineCount,
|
|
2392
|
+
createdParentDirs,
|
|
2393
|
+
kbState: "needs_sync",
|
|
2394
|
+
writeEvent
|
|
2395
|
+
};
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
async function overwriteExistingFile(workspaceRoot, scopedPath, args, existingTarget, options) {
|
|
2399
|
+
if (!args.expected_hash) throw new Error(`write_file overwrite requires expected_hash for ${scopedPath.relativePath}.`);
|
|
2400
|
+
if (!existingTarget) throw new Error(`write_file overwrite requires an existing file: ${scopedPath.relativePath}`);
|
|
2401
|
+
if (!existingTarget.isFile()) throw new Error(`write_file overwrite requires a regular file: ${scopedPath.relativePath}`);
|
|
2402
|
+
const beforeBytes = await readFile(scopedPath.path);
|
|
2403
|
+
const beforeHash = hashBytes(beforeBytes);
|
|
2404
|
+
if (args.expected_hash !== beforeHash) throw new Error(`write_file expected_hash did not match ${scopedPath.relativePath}.`);
|
|
2405
|
+
const beforeLineCount = countLogicalLines$1(decodeUtf8(scopedPath.relativePath, beforeBytes));
|
|
2406
|
+
const afterBytes = encodeUtf8Text(args.content);
|
|
2407
|
+
const afterHash = hashBytes(afterBytes);
|
|
2408
|
+
const afterLineCount = countLogicalLines$1(args.content);
|
|
2409
|
+
const bytesChanged = afterBytes.length - beforeBytes.length;
|
|
2410
|
+
const lineDelta = afterLineCount - beforeLineCount;
|
|
2411
|
+
await writeFileAtomically(scopedPath.path, afterBytes, Number(existingTarget.mode));
|
|
2412
|
+
const writeEvent = {
|
|
2413
|
+
kind: "file_overwrite",
|
|
2414
|
+
source: "agent",
|
|
2415
|
+
path: scopedPath.relativePath,
|
|
2416
|
+
beforeHash,
|
|
2417
|
+
afterHash,
|
|
2418
|
+
firstChangedLine: 1,
|
|
2419
|
+
writeSummary: `overwritten +${afterLineCount}/-${beforeLineCount}`,
|
|
2420
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2421
|
+
};
|
|
2422
|
+
const overlayState = recordAgentFileMutation(workspaceRoot, writeEvent);
|
|
2423
|
+
options.logger?.debug({
|
|
2424
|
+
event: "file_overwrite",
|
|
2425
|
+
...writeEvent,
|
|
2426
|
+
bytesWritten: afterBytes.length,
|
|
2427
|
+
bytesChanged,
|
|
2428
|
+
lineCount: afterLineCount,
|
|
2429
|
+
lineDelta,
|
|
2430
|
+
kbState: overlayState.kbState,
|
|
2431
|
+
dirtyFileCount: overlayState.dirtyFiles.length
|
|
2432
|
+
}, "file overwrite");
|
|
2433
|
+
const content = [
|
|
2434
|
+
`Overwrote ${scopedPath.relativePath}`,
|
|
2435
|
+
`before_hash: ${beforeHash}`,
|
|
2436
|
+
`after_hash: ${afterHash}`,
|
|
2437
|
+
`bytes_written: ${afterBytes.length}`,
|
|
2438
|
+
`bytes_changed: ${bytesChanged}`,
|
|
2439
|
+
`line_count: ${afterLineCount}`,
|
|
2440
|
+
`line_delta: ${lineDelta}`,
|
|
2441
|
+
`kb_state: ${overlayState.kbState}`
|
|
2442
|
+
].join("\n");
|
|
2443
|
+
return {
|
|
2444
|
+
tool: "write_file",
|
|
2445
|
+
path: scopedPath.relativePath,
|
|
2446
|
+
content,
|
|
2447
|
+
hash: afterHash,
|
|
2448
|
+
beforeHash,
|
|
2449
|
+
bytesWritten: afterBytes.length,
|
|
2450
|
+
bytesChanged,
|
|
2451
|
+
lineCount: afterLineCount,
|
|
2452
|
+
lineDelta,
|
|
2453
|
+
createdParentDirs: [],
|
|
2454
|
+
kbState: "needs_sync",
|
|
2455
|
+
writeEvent
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
function resolveWorkspaceScopedPath(workspaceRoot, path) {
|
|
2459
|
+
if (path.includes("\0") || path.length === 0) throw new Error("write_file path is invalid.");
|
|
2460
|
+
const resolvedWorkspace = resolve(workspaceRoot);
|
|
2461
|
+
const resolvedPath = isAbsolute(path) ? resolve(path) : resolve(resolvedWorkspace, path);
|
|
2462
|
+
const relativePath = relative(resolvedWorkspace, resolvedPath);
|
|
2463
|
+
if (relativePath.startsWith("..") || isAbsolute(relativePath)) throw new Error(`write_file can only write files inside the workspace: ${path}`);
|
|
2464
|
+
if (relativePath === "") throw new Error("write_file path must point to a file inside the workspace.");
|
|
2465
|
+
return {
|
|
2466
|
+
workspaceRoot: resolvedWorkspace,
|
|
2467
|
+
path: resolvedPath,
|
|
2468
|
+
relativePath
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
async function statTarget(path) {
|
|
2472
|
+
try {
|
|
2473
|
+
return await stat(path);
|
|
2474
|
+
} catch (error) {
|
|
2475
|
+
if (isNodeError$2(error) && error.code === "ENOENT") return;
|
|
2476
|
+
throw error;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
async function ensureParentDirectory(workspaceRoot, path, relativePath, createParentDirs) {
|
|
2480
|
+
const parent = dirname(path);
|
|
2481
|
+
try {
|
|
2482
|
+
if (!(await stat(parent)).isDirectory()) throw new Error(`write_file parent path is not a directory: ${dirname(relativePath)}`);
|
|
2483
|
+
return [];
|
|
2484
|
+
} catch (error) {
|
|
2485
|
+
if (!isNodeError$2(error) || error.code !== "ENOENT") throw error;
|
|
2486
|
+
}
|
|
2487
|
+
if (!createParentDirs) throw new Error(`write_file parent directory does not exist: ${dirname(relativePath)}`);
|
|
2488
|
+
const createdParentDirs = await collectMissingParentDirs(workspaceRoot, parent);
|
|
2489
|
+
await mkdir(parent, { recursive: true });
|
|
2490
|
+
return createdParentDirs;
|
|
2491
|
+
}
|
|
2492
|
+
async function collectMissingParentDirs(workspaceRoot, parent) {
|
|
2493
|
+
const missing = [];
|
|
2494
|
+
let current = parent;
|
|
2495
|
+
while (current !== workspaceRoot) try {
|
|
2496
|
+
if (!(await stat(current)).isDirectory()) throw new Error(`write_file parent path is not a directory: ${relative(workspaceRoot, current)}`);
|
|
2497
|
+
break;
|
|
2498
|
+
} catch (error) {
|
|
2499
|
+
if (!isNodeError$2(error) || error.code !== "ENOENT") throw error;
|
|
2500
|
+
missing.push(relative(workspaceRoot, current));
|
|
2501
|
+
current = dirname(current);
|
|
2502
|
+
}
|
|
2503
|
+
return missing.reverse();
|
|
2504
|
+
}
|
|
2505
|
+
function encodeUtf8Text(content) {
|
|
2506
|
+
if (content.includes("\0")) throw new Error("write_file content must not contain NUL bytes.");
|
|
2507
|
+
return Buffer.from(content, "utf8");
|
|
2508
|
+
}
|
|
2509
|
+
function decodeUtf8(path, bytes) {
|
|
2510
|
+
try {
|
|
2511
|
+
return new TextDecoder("utf-8", {
|
|
2512
|
+
fatal: true,
|
|
2513
|
+
ignoreBOM: true
|
|
2514
|
+
}).decode(bytes);
|
|
2515
|
+
} catch {
|
|
2516
|
+
throw new Error(`write_file overwrite requires the existing file to be UTF-8 text: ${path}`);
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
async function writeFileAtomically(path, content, mode = 438) {
|
|
2520
|
+
const temporaryPath = resolve(dirname(path), `.${basename(path)}.topchester-${process.pid}-${randomUUID()}.tmp`);
|
|
2521
|
+
try {
|
|
2522
|
+
await writeFile(temporaryPath, content, {
|
|
2523
|
+
flag: "wx",
|
|
2524
|
+
mode: mode & 511
|
|
2525
|
+
});
|
|
2526
|
+
await rename(temporaryPath, path);
|
|
2527
|
+
} catch (error) {
|
|
2528
|
+
await rm(temporaryPath, { force: true });
|
|
2529
|
+
throw error;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
function hashBytes(bytes) {
|
|
2533
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
2534
|
+
}
|
|
2535
|
+
function countLogicalLines$1(content) {
|
|
2536
|
+
if (content.length === 0) return 0;
|
|
2537
|
+
const withoutTrailingLineEnding = content.replace(/\r?\n$/u, "");
|
|
2538
|
+
return withoutTrailingLineEnding.length === 0 ? 1 : withoutTrailingLineEnding.split(/\r?\n/u).length;
|
|
2539
|
+
}
|
|
2540
|
+
function isNodeError$2(error) {
|
|
2541
|
+
return error instanceof Error && "code" in error;
|
|
2542
|
+
}
|
|
1617
2543
|
//#endregion
|
|
1618
2544
|
//#region src/agent/tools/registry.ts
|
|
1619
2545
|
const toolRegistry = {
|
|
@@ -1622,6 +2548,12 @@ const toolRegistry = {
|
|
|
1622
2548
|
[grepTool.name]: grepTool,
|
|
1623
2549
|
[findFileTool.name]: findFileTool,
|
|
1624
2550
|
[editFileTool.name]: editFileTool,
|
|
2551
|
+
[writeFileTool.name]: writeFileTool,
|
|
2552
|
+
[gitStatusTool.name]: gitStatusTool,
|
|
2553
|
+
[gitDiffTool.name]: gitDiffTool,
|
|
2554
|
+
[gitLogTool.name]: gitLogTool,
|
|
2555
|
+
[gitAddTool.name]: gitAddTool,
|
|
2556
|
+
[gitCommitTool.name]: gitCommitTool,
|
|
1625
2557
|
[inspectCommandTool.name]: inspectCommandTool
|
|
1626
2558
|
};
|
|
1627
2559
|
function isToolName(name) {
|
|
@@ -3589,6 +4521,201 @@ function assertSafeResetPath(workspaceRoot, path) {
|
|
|
3589
4521
|
function isNodeError(error) {
|
|
3590
4522
|
return error instanceof Error && "code" in error;
|
|
3591
4523
|
}
|
|
4524
|
+
//#endregion
|
|
4525
|
+
//#region src/tui/markdown.ts
|
|
4526
|
+
const codeFenceSentinel = "topchester-code-fence";
|
|
4527
|
+
function renderMarkdown(text, width) {
|
|
4528
|
+
const lines = new Markdown(unwrapMarkdownCodeFences(text), 0, 0, getMarkdownTheme()).render(width);
|
|
4529
|
+
const rendered = [];
|
|
4530
|
+
let inCodeBlock = false;
|
|
4531
|
+
for (const line of lines) {
|
|
4532
|
+
if (line.includes(codeFenceSentinel)) {
|
|
4533
|
+
inCodeBlock = !inCodeBlock;
|
|
4534
|
+
continue;
|
|
4535
|
+
}
|
|
4536
|
+
rendered.push(inCodeBlock ? applyCodeBlockBackground(line) : line);
|
|
4537
|
+
}
|
|
4538
|
+
return rendered;
|
|
4539
|
+
}
|
|
4540
|
+
function unwrapMarkdownCodeFences(text) {
|
|
4541
|
+
return text.replace(/^```(?:markdown|md)\s*\n([\s\S]*?)\n```$/gim, "$1");
|
|
4542
|
+
}
|
|
4543
|
+
function getMarkdownTheme() {
|
|
4544
|
+
return {
|
|
4545
|
+
heading: ui.label,
|
|
4546
|
+
link: ui.label,
|
|
4547
|
+
linkUrl: ui.muted,
|
|
4548
|
+
code: ui.ok,
|
|
4549
|
+
codeBlock: (text) => text,
|
|
4550
|
+
codeBlockBorder: (text) => `${codeFenceSentinel}${ui.muted(text)}`,
|
|
4551
|
+
quote: ui.warn,
|
|
4552
|
+
quoteBorder: ui.warn,
|
|
4553
|
+
hr: ui.muted,
|
|
4554
|
+
listBullet: ui.label,
|
|
4555
|
+
bold: (text) => decorate(text, "\x1B[1m", "\x1B[22m"),
|
|
4556
|
+
italic: (text) => decorate(text, "\x1B[3m", "\x1B[23m"),
|
|
4557
|
+
strikethrough: (text) => decorate(text, "\x1B[9m", "\x1B[29m"),
|
|
4558
|
+
underline: (text) => decorate(text, "\x1B[4m", "\x1B[24m"),
|
|
4559
|
+
codeBlockIndent: " ",
|
|
4560
|
+
highlightCode(code, lang) {
|
|
4561
|
+
const validLanguage = lang && supportsLanguage(lang) ? lang : void 0;
|
|
4562
|
+
if (!validLanguage) return code.split("\n");
|
|
4563
|
+
try {
|
|
4564
|
+
return highlight(code, {
|
|
4565
|
+
language: validLanguage,
|
|
4566
|
+
ignoreIllegals: true
|
|
4567
|
+
}).split("\n");
|
|
4568
|
+
} catch {
|
|
4569
|
+
return code.split("\n");
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
};
|
|
4573
|
+
}
|
|
4574
|
+
function decorate(text, open, close) {
|
|
4575
|
+
if (!shouldUseAnsi()) return text;
|
|
4576
|
+
return `${open}${text}${close}`;
|
|
4577
|
+
}
|
|
4578
|
+
function applyCodeBlockBackground(line) {
|
|
4579
|
+
if (!shouldUseAnsi()) return line;
|
|
4580
|
+
const background = "\x1B[48;5;235m";
|
|
4581
|
+
const reset = "\x1B[0m";
|
|
4582
|
+
return `${background}${line.split(reset).join(`${reset}${background}`)}${reset}`;
|
|
4583
|
+
}
|
|
4584
|
+
function shouldUseAnsi() {
|
|
4585
|
+
if (process.env.NO_COLOR) return false;
|
|
4586
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
|
|
4587
|
+
return stdout.isTTY === true;
|
|
4588
|
+
}
|
|
4589
|
+
//#endregion
|
|
4590
|
+
//#region src/tui/messages.ts
|
|
4591
|
+
function systemMessage(text) {
|
|
4592
|
+
return {
|
|
4593
|
+
kind: "system",
|
|
4594
|
+
text
|
|
4595
|
+
};
|
|
4596
|
+
}
|
|
4597
|
+
function userMessage(text) {
|
|
4598
|
+
return {
|
|
4599
|
+
kind: "user",
|
|
4600
|
+
text
|
|
4601
|
+
};
|
|
4602
|
+
}
|
|
4603
|
+
function agentMessage(text, meta) {
|
|
4604
|
+
return {
|
|
4605
|
+
kind: "agent",
|
|
4606
|
+
text,
|
|
4607
|
+
meta
|
|
4608
|
+
};
|
|
4609
|
+
}
|
|
4610
|
+
function toolCallMessage(call, label, resultSummary) {
|
|
4611
|
+
return resultSummary === void 0 ? {
|
|
4612
|
+
kind: "tool_call",
|
|
4613
|
+
call,
|
|
4614
|
+
label
|
|
4615
|
+
} : {
|
|
4616
|
+
kind: "tool_call",
|
|
4617
|
+
call,
|
|
4618
|
+
label,
|
|
4619
|
+
resultSummary
|
|
4620
|
+
};
|
|
4621
|
+
}
|
|
4622
|
+
function modalMessage(message) {
|
|
4623
|
+
return {
|
|
4624
|
+
kind: "modal",
|
|
4625
|
+
...message
|
|
4626
|
+
};
|
|
4627
|
+
}
|
|
4628
|
+
function renderChatMessage(message, options = {}) {
|
|
4629
|
+
if (message.kind === "modal") return renderChatModal(message, options.selectedActionIndex);
|
|
4630
|
+
if (message.kind === "tool_call") return renderToolCallMessage(message);
|
|
4631
|
+
if (message.text.length === 0) return [""];
|
|
4632
|
+
const lines = message.kind === "agent" && options.width !== void 0 ? renderMarkdown(message.text, Math.max(1, options.width - getPrefix(message.kind).length)) : message.text.split("\n");
|
|
4633
|
+
if (message.kind === "user") return renderUserMessage(lines);
|
|
4634
|
+
if (message.kind === "system") return renderSystemMessage(lines);
|
|
4635
|
+
const prefix = getPrefix(message.kind);
|
|
4636
|
+
const rendered = prefix.length === 0 ? lines : lines.map((line, index) => `${index === 0 ? prefix : " ".repeat(prefix.length)}${line}`);
|
|
4637
|
+
if (message.meta) {
|
|
4638
|
+
const metaText = `↳ ${message.meta}`;
|
|
4639
|
+
rendered.push(` ${ui.label("─".repeat(metaText.length))}`, ` ${ui.label(metaText)}`);
|
|
4640
|
+
}
|
|
4641
|
+
return rendered;
|
|
4642
|
+
}
|
|
4643
|
+
function renderUserMessage(lines) {
|
|
4644
|
+
const border = "▌";
|
|
4645
|
+
const rendered = lines.map((line) => `${border} ${line}`);
|
|
4646
|
+
return [
|
|
4647
|
+
`${border} `,
|
|
4648
|
+
...rendered,
|
|
4649
|
+
`${border} `
|
|
4650
|
+
];
|
|
4651
|
+
}
|
|
4652
|
+
function renderSystemMessage(lines) {
|
|
4653
|
+
const bodyPrefix = " ";
|
|
4654
|
+
return [` ${ui.ok("✦")} ${ui.label("System")}:`, ...lines.map((line) => `${bodyPrefix}${formatSystemBodyLine(line)}`)];
|
|
4655
|
+
}
|
|
4656
|
+
function formatSystemBodyLine(line) {
|
|
4657
|
+
return expandTabs(line).replace(/\(changed \+\d+\/-\d+\)$/u, (summary) => ui.muted(summary)).replace(/\(created \+\d+\)$/u, (summary) => ui.muted(summary));
|
|
4658
|
+
}
|
|
4659
|
+
function renderToolCallMessage(message) {
|
|
4660
|
+
const visibleLabel = message.resultSummary && !message.label.includes(message.resultSummary) ? `${message.label} ${message.resultSummary}` : message.label;
|
|
4661
|
+
return [` ${ui.muted(expandTabs(visibleLabel))}`];
|
|
4662
|
+
}
|
|
4663
|
+
function expandTabs(line) {
|
|
4664
|
+
let column = 0;
|
|
4665
|
+
let expanded = "";
|
|
4666
|
+
for (const char of line) {
|
|
4667
|
+
if (char === " ") {
|
|
4668
|
+
const spaces = 4 - column % 4;
|
|
4669
|
+
expanded += " ".repeat(spaces);
|
|
4670
|
+
column += spaces;
|
|
4671
|
+
continue;
|
|
4672
|
+
}
|
|
4673
|
+
expanded += char;
|
|
4674
|
+
column += 1;
|
|
4675
|
+
}
|
|
4676
|
+
return expanded;
|
|
4677
|
+
}
|
|
4678
|
+
function getPrefix(kind) {
|
|
4679
|
+
switch (kind) {
|
|
4680
|
+
case "agent": return " ";
|
|
4681
|
+
case "user": return `${ui.label("You")}: `;
|
|
4682
|
+
case "system": return `${ui.label("System")}: `;
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
function renderChatModal(message, selectedActionIndex) {
|
|
4686
|
+
const icon = message.tone === "warning" ? "⚠️" : "ℹ️";
|
|
4687
|
+
const title = message.tone === "warning" ? ui.warn(message.title) : ui.label(message.title);
|
|
4688
|
+
const bodyLines = message.body ? ["", ...message.body.split("\n")] : [];
|
|
4689
|
+
const actionLines = message.actions.map((action, index) => {
|
|
4690
|
+
return `${selectedActionIndex === index ? ">" : " "} ${index + 1}) ${action.label}`;
|
|
4691
|
+
});
|
|
4692
|
+
const contentLines = [
|
|
4693
|
+
`${icon} ${title}:`,
|
|
4694
|
+
...bodyLines,
|
|
4695
|
+
"",
|
|
4696
|
+
...actionLines
|
|
4697
|
+
];
|
|
4698
|
+
const contentWidth = Math.max(...contentLines.map(stripAnsi$1).map((line) => line.length), 1);
|
|
4699
|
+
const top = `╭${"─".repeat(contentWidth + 2)}╮`;
|
|
4700
|
+
const bottom = `╰${"─".repeat(contentWidth + 2)}╯`;
|
|
4701
|
+
return [
|
|
4702
|
+
top,
|
|
4703
|
+
...contentLines.map((line) => `│ ${line}${" ".repeat(contentWidth - stripAnsi$1(line).length)} │`),
|
|
4704
|
+
bottom
|
|
4705
|
+
];
|
|
4706
|
+
}
|
|
4707
|
+
function stripAnsi$1(text) {
|
|
4708
|
+
let plain = "";
|
|
4709
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
4710
|
+
if (text.charCodeAt(index) === 27 && text[index + 1] === "[") {
|
|
4711
|
+
index += 2;
|
|
4712
|
+
while (index < text.length && text[index] !== "m") index += 1;
|
|
4713
|
+
continue;
|
|
4714
|
+
}
|
|
4715
|
+
plain += text[index];
|
|
4716
|
+
}
|
|
4717
|
+
return plain;
|
|
4718
|
+
}
|
|
3592
4719
|
const isoTimestampSchema = z.string().datetime({ offset: true });
|
|
3593
4720
|
const jsonValueSchema = z.lazy(() => z.union([
|
|
3594
4721
|
z.string(),
|
|
@@ -3740,10 +4867,7 @@ function rehydrateSession(events) {
|
|
|
3740
4867
|
if (event.role === "user") visibleOnlyActionValues = /* @__PURE__ */ new Set();
|
|
3741
4868
|
break;
|
|
3742
4869
|
case "tool_call":
|
|
3743
|
-
messages.push(
|
|
3744
|
-
kind: "system",
|
|
3745
|
-
text: event.label
|
|
3746
|
-
});
|
|
4870
|
+
messages.push(toolCallMessage(event.call, event.label));
|
|
3747
4871
|
break;
|
|
3748
4872
|
case "knowledge_status": break;
|
|
3749
4873
|
case "choice":
|
|
@@ -4140,189 +5264,6 @@ async function executeKbCommand(args, context) {
|
|
|
4140
5264
|
return { messages: ["Usage: /kb init, /kb compile, /kb sync, /kb reset, or /kb status"] };
|
|
4141
5265
|
}
|
|
4142
5266
|
//#endregion
|
|
4143
|
-
//#region src/tui/markdown.ts
|
|
4144
|
-
const codeFenceSentinel = "topchester-code-fence";
|
|
4145
|
-
function renderMarkdown(text, width) {
|
|
4146
|
-
const lines = new Markdown(unwrapMarkdownCodeFences(text), 0, 0, getMarkdownTheme()).render(width);
|
|
4147
|
-
const rendered = [];
|
|
4148
|
-
let inCodeBlock = false;
|
|
4149
|
-
for (const line of lines) {
|
|
4150
|
-
if (line.includes(codeFenceSentinel)) {
|
|
4151
|
-
inCodeBlock = !inCodeBlock;
|
|
4152
|
-
continue;
|
|
4153
|
-
}
|
|
4154
|
-
rendered.push(inCodeBlock ? applyCodeBlockBackground(line) : line);
|
|
4155
|
-
}
|
|
4156
|
-
return rendered;
|
|
4157
|
-
}
|
|
4158
|
-
function unwrapMarkdownCodeFences(text) {
|
|
4159
|
-
return text.replace(/^```(?:markdown|md)\s*\n([\s\S]*?)\n```$/gim, "$1");
|
|
4160
|
-
}
|
|
4161
|
-
function getMarkdownTheme() {
|
|
4162
|
-
return {
|
|
4163
|
-
heading: ui.label,
|
|
4164
|
-
link: ui.label,
|
|
4165
|
-
linkUrl: ui.muted,
|
|
4166
|
-
code: ui.ok,
|
|
4167
|
-
codeBlock: (text) => text,
|
|
4168
|
-
codeBlockBorder: (text) => `${codeFenceSentinel}${ui.muted(text)}`,
|
|
4169
|
-
quote: ui.warn,
|
|
4170
|
-
quoteBorder: ui.warn,
|
|
4171
|
-
hr: ui.muted,
|
|
4172
|
-
listBullet: ui.label,
|
|
4173
|
-
bold: (text) => decorate(text, "\x1B[1m", "\x1B[22m"),
|
|
4174
|
-
italic: (text) => decorate(text, "\x1B[3m", "\x1B[23m"),
|
|
4175
|
-
strikethrough: (text) => decorate(text, "\x1B[9m", "\x1B[29m"),
|
|
4176
|
-
underline: (text) => decorate(text, "\x1B[4m", "\x1B[24m"),
|
|
4177
|
-
codeBlockIndent: " ",
|
|
4178
|
-
highlightCode(code, lang) {
|
|
4179
|
-
const validLanguage = lang && supportsLanguage(lang) ? lang : void 0;
|
|
4180
|
-
if (!validLanguage) return code.split("\n");
|
|
4181
|
-
try {
|
|
4182
|
-
return highlight(code, {
|
|
4183
|
-
language: validLanguage,
|
|
4184
|
-
ignoreIllegals: true
|
|
4185
|
-
}).split("\n");
|
|
4186
|
-
} catch {
|
|
4187
|
-
return code.split("\n");
|
|
4188
|
-
}
|
|
4189
|
-
}
|
|
4190
|
-
};
|
|
4191
|
-
}
|
|
4192
|
-
function decorate(text, open, close) {
|
|
4193
|
-
if (!shouldUseAnsi()) return text;
|
|
4194
|
-
return `${open}${text}${close}`;
|
|
4195
|
-
}
|
|
4196
|
-
function applyCodeBlockBackground(line) {
|
|
4197
|
-
if (!shouldUseAnsi()) return line;
|
|
4198
|
-
const background = "\x1B[48;5;235m";
|
|
4199
|
-
const reset = "\x1B[0m";
|
|
4200
|
-
return `${background}${line.split(reset).join(`${reset}${background}`)}${reset}`;
|
|
4201
|
-
}
|
|
4202
|
-
function shouldUseAnsi() {
|
|
4203
|
-
if (process.env.NO_COLOR) return false;
|
|
4204
|
-
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
|
|
4205
|
-
return stdout.isTTY === true;
|
|
4206
|
-
}
|
|
4207
|
-
//#endregion
|
|
4208
|
-
//#region src/tui/messages.ts
|
|
4209
|
-
function systemMessage(text) {
|
|
4210
|
-
return {
|
|
4211
|
-
kind: "system",
|
|
4212
|
-
text
|
|
4213
|
-
};
|
|
4214
|
-
}
|
|
4215
|
-
function userMessage(text) {
|
|
4216
|
-
return {
|
|
4217
|
-
kind: "user",
|
|
4218
|
-
text
|
|
4219
|
-
};
|
|
4220
|
-
}
|
|
4221
|
-
function agentMessage(text, meta) {
|
|
4222
|
-
return {
|
|
4223
|
-
kind: "agent",
|
|
4224
|
-
text,
|
|
4225
|
-
meta
|
|
4226
|
-
};
|
|
4227
|
-
}
|
|
4228
|
-
function modalMessage(message) {
|
|
4229
|
-
return {
|
|
4230
|
-
kind: "modal",
|
|
4231
|
-
...message
|
|
4232
|
-
};
|
|
4233
|
-
}
|
|
4234
|
-
function renderChatMessage(message, options = {}) {
|
|
4235
|
-
if (message.kind === "modal") return renderChatModal(message, options.selectedActionIndex);
|
|
4236
|
-
if (message.text.length === 0) return [""];
|
|
4237
|
-
const lines = message.kind === "agent" && options.width !== void 0 ? renderMarkdown(message.text, Math.max(1, options.width - getPrefix(message.kind).length)) : message.text.split("\n");
|
|
4238
|
-
if (message.kind === "user") return renderUserMessage(lines);
|
|
4239
|
-
if (message.kind === "system") return renderSystemMessage(lines);
|
|
4240
|
-
const prefix = getPrefix(message.kind);
|
|
4241
|
-
const rendered = prefix.length === 0 ? lines : lines.map((line, index) => `${index === 0 ? prefix : " ".repeat(prefix.length)}${line}`);
|
|
4242
|
-
if (message.meta) {
|
|
4243
|
-
const metaText = `↳ ${message.meta}`;
|
|
4244
|
-
rendered.push(` ${ui.label("─".repeat(metaText.length))}`, ` ${ui.label(metaText)}`);
|
|
4245
|
-
}
|
|
4246
|
-
return rendered;
|
|
4247
|
-
}
|
|
4248
|
-
function renderUserMessage(lines) {
|
|
4249
|
-
const border = "▌";
|
|
4250
|
-
const rendered = lines.map((line) => `${border} ${line}`);
|
|
4251
|
-
return [
|
|
4252
|
-
`${border} `,
|
|
4253
|
-
...rendered,
|
|
4254
|
-
`${border} `
|
|
4255
|
-
];
|
|
4256
|
-
}
|
|
4257
|
-
function renderSystemMessage(lines) {
|
|
4258
|
-
const bodyPrefix = " ";
|
|
4259
|
-
return [` ${ui.ok("✦")} ${ui.label("System")}:`, ...lines.map((line) => `${bodyPrefix}${formatSystemBodyLine(line)}`)];
|
|
4260
|
-
}
|
|
4261
|
-
function formatSystemBodyLine(line) {
|
|
4262
|
-
const expandedLine = expandTabs(line);
|
|
4263
|
-
if (isToolCallLine(line)) return ui.muted(expandedLine);
|
|
4264
|
-
return expandedLine.replace(/\(changed \+\d+\/-\d+\)$/u, (summary) => ui.muted(summary));
|
|
4265
|
-
}
|
|
4266
|
-
function isToolCallLine(line) {
|
|
4267
|
-
return /^(read_file|list_files|grep|find_file|edit_file|inspect_command): /u.test(line);
|
|
4268
|
-
}
|
|
4269
|
-
function expandTabs(line) {
|
|
4270
|
-
let column = 0;
|
|
4271
|
-
let expanded = "";
|
|
4272
|
-
for (const char of line) {
|
|
4273
|
-
if (char === " ") {
|
|
4274
|
-
const spaces = 4 - column % 4;
|
|
4275
|
-
expanded += " ".repeat(spaces);
|
|
4276
|
-
column += spaces;
|
|
4277
|
-
continue;
|
|
4278
|
-
}
|
|
4279
|
-
expanded += char;
|
|
4280
|
-
column += 1;
|
|
4281
|
-
}
|
|
4282
|
-
return expanded;
|
|
4283
|
-
}
|
|
4284
|
-
function getPrefix(kind) {
|
|
4285
|
-
switch (kind) {
|
|
4286
|
-
case "agent": return " ";
|
|
4287
|
-
case "user": return `${ui.label("You")}: `;
|
|
4288
|
-
case "system": return `${ui.label("System")}: `;
|
|
4289
|
-
}
|
|
4290
|
-
}
|
|
4291
|
-
function renderChatModal(message, selectedActionIndex) {
|
|
4292
|
-
const icon = message.tone === "warning" ? "⚠️" : "ℹ️";
|
|
4293
|
-
const title = message.tone === "warning" ? ui.warn(message.title) : ui.label(message.title);
|
|
4294
|
-
const bodyLines = message.body ? ["", ...message.body.split("\n")] : [];
|
|
4295
|
-
const actionLines = message.actions.map((action, index) => {
|
|
4296
|
-
return `${selectedActionIndex === index ? ">" : " "} ${index + 1}) ${action.label}`;
|
|
4297
|
-
});
|
|
4298
|
-
const contentLines = [
|
|
4299
|
-
`${icon} ${title}:`,
|
|
4300
|
-
...bodyLines,
|
|
4301
|
-
"",
|
|
4302
|
-
...actionLines
|
|
4303
|
-
];
|
|
4304
|
-
const contentWidth = Math.max(...contentLines.map(stripAnsi$1).map((line) => line.length), 1);
|
|
4305
|
-
const top = `╭${"─".repeat(contentWidth + 2)}╮`;
|
|
4306
|
-
const bottom = `╰${"─".repeat(contentWidth + 2)}╯`;
|
|
4307
|
-
return [
|
|
4308
|
-
top,
|
|
4309
|
-
...contentLines.map((line) => `│ ${line}${" ".repeat(contentWidth - stripAnsi$1(line).length)} │`),
|
|
4310
|
-
bottom
|
|
4311
|
-
];
|
|
4312
|
-
}
|
|
4313
|
-
function stripAnsi$1(text) {
|
|
4314
|
-
let plain = "";
|
|
4315
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
4316
|
-
if (text.charCodeAt(index) === 27 && text[index + 1] === "[") {
|
|
4317
|
-
index += 2;
|
|
4318
|
-
while (index < text.length && text[index] !== "m") index += 1;
|
|
4319
|
-
continue;
|
|
4320
|
-
}
|
|
4321
|
-
plain += text[index];
|
|
4322
|
-
}
|
|
4323
|
-
return plain;
|
|
4324
|
-
}
|
|
4325
|
-
//#endregion
|
|
4326
5267
|
//#region src/tui/keys.ts
|
|
4327
5268
|
function isUpKey(data) {
|
|
4328
5269
|
return matchesKey(data, "up") || data === "\x1B[A";
|
|
@@ -4599,15 +5540,19 @@ var ChatLayout = class {
|
|
|
4599
5540
|
}
|
|
4600
5541
|
getConversationTurns() {
|
|
4601
5542
|
return this.messages.flatMap((message) => {
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
5543
|
+
switch (message.kind) {
|
|
5544
|
+
case "user": return message.modelContext === false ? [] : [{
|
|
5545
|
+
role: "user",
|
|
5546
|
+
text: message.text
|
|
5547
|
+
}];
|
|
5548
|
+
case "agent": return message.text === "ready" || message.modelContext === false ? [] : [{
|
|
5549
|
+
role: "assistant",
|
|
5550
|
+
text: message.text
|
|
5551
|
+
}];
|
|
5552
|
+
case "system":
|
|
5553
|
+
case "tool_call":
|
|
5554
|
+
case "modal": return [];
|
|
5555
|
+
}
|
|
4611
5556
|
});
|
|
4612
5557
|
}
|
|
4613
5558
|
get focused() {
|
|
@@ -4975,6 +5920,14 @@ async function executeToolCall(workspaceRoot, call, options = {}) {
|
|
|
4975
5920
|
}
|
|
4976
5921
|
}
|
|
4977
5922
|
function summarizeToolArgs(call) {
|
|
5923
|
+
if (call.tool === "write_file") return {
|
|
5924
|
+
path: call.args.path,
|
|
5925
|
+
contentLength: call.args.content.length,
|
|
5926
|
+
lineCount: countLogicalLines(call.args.content),
|
|
5927
|
+
createParentDirs: Boolean(call.args.create_parent_dirs),
|
|
5928
|
+
overwrite: Boolean(call.args.overwrite),
|
|
5929
|
+
expectedHashProvided: Boolean(call.args.expected_hash)
|
|
5930
|
+
};
|
|
4978
5931
|
if (call.tool !== "edit_file") return call.args;
|
|
4979
5932
|
return {
|
|
4980
5933
|
path: call.args.path,
|
|
@@ -4994,6 +5947,50 @@ function summarizeToolResult(result) {
|
|
|
4994
5947
|
stdoutLength: result.stdout.length,
|
|
4995
5948
|
stderrLength: result.stderr.length
|
|
4996
5949
|
};
|
|
5950
|
+
if (result.tool === "git_status") return {
|
|
5951
|
+
repoRoot: result.repoRoot,
|
|
5952
|
+
branch: result.branch,
|
|
5953
|
+
head: result.head,
|
|
5954
|
+
hasHead: result.hasHead,
|
|
5955
|
+
clean: result.clean,
|
|
5956
|
+
fileCount: result.files.length,
|
|
5957
|
+
truncated: result.truncated
|
|
5958
|
+
};
|
|
5959
|
+
if (result.tool === "git_diff") return {
|
|
5960
|
+
repoRoot: result.repoRoot,
|
|
5961
|
+
scope: result.scope,
|
|
5962
|
+
path: result.path,
|
|
5963
|
+
fileCount: result.fileCount,
|
|
5964
|
+
truncated: result.truncated
|
|
5965
|
+
};
|
|
5966
|
+
if (result.tool === "git_log") return {
|
|
5967
|
+
repoRoot: result.repoRoot,
|
|
5968
|
+
commitCount: result.commits.length,
|
|
5969
|
+
truncated: result.truncated
|
|
5970
|
+
};
|
|
5971
|
+
if (result.tool === "git_add") return {
|
|
5972
|
+
repoRoot: result.repoRoot,
|
|
5973
|
+
stagedPathCount: result.stagedPaths.length,
|
|
5974
|
+
postStatusFileCount: result.files.length
|
|
5975
|
+
};
|
|
5976
|
+
if (result.tool === "git_commit") return {
|
|
5977
|
+
repoRoot: result.repoRoot,
|
|
5978
|
+
commit: result.commit.shortSha,
|
|
5979
|
+
stagedPathCount: result.stagedPaths.length,
|
|
5980
|
+
remainingFileCount: result.remainingFiles.length,
|
|
5981
|
+
statLength: result.stat.length,
|
|
5982
|
+
nameStatusLength: result.nameStatus.length
|
|
5983
|
+
};
|
|
5984
|
+
if (result.tool === "write_file") return {
|
|
5985
|
+
hash: result.hash,
|
|
5986
|
+
bytesWritten: result.bytesWritten,
|
|
5987
|
+
lineCount: result.lineCount,
|
|
5988
|
+
bytesChanged: result.bytesChanged,
|
|
5989
|
+
lineDelta: result.lineDelta,
|
|
5990
|
+
createdParentDirs: result.createdParentDirs,
|
|
5991
|
+
kbState: result.kbState,
|
|
5992
|
+
writeEvent: result.writeEvent
|
|
5993
|
+
};
|
|
4997
5994
|
if (result.tool !== "edit_file") return {};
|
|
4998
5995
|
return {
|
|
4999
5996
|
beforeHash: result.beforeHash,
|
|
@@ -5004,6 +6001,11 @@ function summarizeToolResult(result) {
|
|
|
5004
6001
|
editEvent: result.editEvent
|
|
5005
6002
|
};
|
|
5006
6003
|
}
|
|
6004
|
+
function countLogicalLines(content) {
|
|
6005
|
+
if (content.length === 0) return 0;
|
|
6006
|
+
const withoutTrailingLineEnding = content.replace(/\r?\n$/u, "");
|
|
6007
|
+
return withoutTrailingLineEnding.length === 0 ? 1 : withoutTrailingLineEnding.split(/\r?\n/u).length;
|
|
6008
|
+
}
|
|
5007
6009
|
//#endregion
|
|
5008
6010
|
//#region src/agent/prompts.ts
|
|
5009
6011
|
function getChatSystemPrompt() {
|
|
@@ -5030,10 +6032,15 @@ function getChatSystemPrompt() {
|
|
|
5030
6032
|
"- Use read/search tools when the user asks about files, code, symbols, usages, tests, or project behavior.",
|
|
5031
6033
|
"- Use find_file for path or filename lookup. Use grep for text inside files. If grep output mentions another path, treat that mentioned path as content until find_file or read_file confirms it exists.",
|
|
5032
6034
|
"- Use list_files, grep, find_file, and read_file for exact file listing, search, lookup, and reading tasks.",
|
|
6035
|
+
"- Use git_status, git_diff, and git_log for Git state, diffs, and history. Prefer these over inspect_command for Git workflow inspection.",
|
|
6036
|
+
"- Use git_add and git_commit only when the user explicitly asks to stage or commit. Never stage unrelated files, never stage '.', and never commit unless staged paths exactly match the user's request.",
|
|
5033
6037
|
"- Use inspect_command only for quick read-only repo orientation when a short familiar command chain is clearer than several dedicated tool calls.",
|
|
5034
6038
|
"- inspect_command is not a shell. Unsafe commands, shell expansion, scripts, installs, builds, tests, network access, and file mutation are not available through it.",
|
|
5035
6039
|
"- Use read_file before editing a file so your edit is based on current file content and hash metadata.",
|
|
5036
6040
|
"- Use edit_file for targeted edits to existing files. Make multiple disjoint edits for the same file in one call when possible.",
|
|
6041
|
+
"- Use write_file to create new files by default. It fails when the file already exists unless you are replacing the whole file with overwrite:true and expected_hash from read_file.",
|
|
6042
|
+
"- Pass write_file create_parent_dirs:true only when the user intent clearly includes creating that folder path.",
|
|
6043
|
+
"- Do not use inspect_command for file creation or file mutation.",
|
|
5037
6044
|
"- Keep edit_file old_text small but unique. Do not include line labels or grep prefixes in old_text; use exact file text only.",
|
|
5038
6045
|
"- Use edit/write tools when they are available and the user asks you to implement, fix, add, update, or refactor code.",
|
|
5039
6046
|
"- Use command/test tools when they are available and you need to inspect the environment, run tests, format, lint, typecheck, or verify behavior.",
|
|
@@ -5226,6 +6233,18 @@ function formatToolResultForPrompt(result) {
|
|
|
5226
6233
|
result.diff,
|
|
5227
6234
|
"```"
|
|
5228
6235
|
].join("\n");
|
|
6236
|
+
if (result.tool === "write_file") return [
|
|
6237
|
+
`Tool result from ${result.tool}${path}:`,
|
|
6238
|
+
result.beforeHash ? `before_hash: ${result.beforeHash}` : "",
|
|
6239
|
+
`after_hash: ${result.hash}`,
|
|
6240
|
+
`bytes_written: ${result.bytesWritten}`,
|
|
6241
|
+
result.bytesChanged !== void 0 ? `bytes_changed: ${result.bytesChanged}` : "",
|
|
6242
|
+
`line_count: ${result.lineCount}`,
|
|
6243
|
+
result.lineDelta !== void 0 ? `line_delta: ${result.lineDelta}` : "",
|
|
6244
|
+
`kb_state: ${result.kbState}`,
|
|
6245
|
+
result.createdParentDirs.length > 0 ? `created_parent_dirs: ${result.createdParentDirs.join(", ")}` : "",
|
|
6246
|
+
`summary: ${result.writeEvent.writeSummary}`
|
|
6247
|
+
].filter(Boolean).join("\n");
|
|
5229
6248
|
if (result.tool === "inspect_command") return [
|
|
5230
6249
|
`Tool result from ${result.tool} via ${result.command}:`,
|
|
5231
6250
|
`cwd: ${result.cwd}`,
|
|
@@ -5238,6 +6257,61 @@ function formatToolResultForPrompt(result) {
|
|
|
5238
6257
|
result.content,
|
|
5239
6258
|
"```"
|
|
5240
6259
|
].filter(Boolean).join("\n");
|
|
6260
|
+
if (result.tool === "git_status") return [
|
|
6261
|
+
`Tool result from ${result.tool}${path}:`,
|
|
6262
|
+
`repo_root: ${result.repoRoot ?? "(none)"}`,
|
|
6263
|
+
`branch: ${result.branch ?? "(detached)"}`,
|
|
6264
|
+
`head: ${result.head ?? "(none)"}`,
|
|
6265
|
+
`has_head: ${result.hasHead}`,
|
|
6266
|
+
`clean: ${result.clean}`,
|
|
6267
|
+
`changed_file_count: ${result.files.length}`,
|
|
6268
|
+
`truncated: ${result.truncated}`,
|
|
6269
|
+
warning ? warning.trimStart() : "",
|
|
6270
|
+
"```",
|
|
6271
|
+
result.content,
|
|
6272
|
+
"```"
|
|
6273
|
+
].filter(Boolean).join("\n");
|
|
6274
|
+
if (result.tool === "git_diff") return [
|
|
6275
|
+
`Tool result from ${result.tool}${path}:`,
|
|
6276
|
+
`repo_root: ${result.repoRoot ?? "(none)"}`,
|
|
6277
|
+
`scope: ${result.scope}`,
|
|
6278
|
+
`path: ${result.path ?? "(all)"}`,
|
|
6279
|
+
`file_count: ${result.fileCount}`,
|
|
6280
|
+
`truncated: ${result.truncated}`,
|
|
6281
|
+
warning ? warning.trimStart() : "",
|
|
6282
|
+
"```diff",
|
|
6283
|
+
result.content,
|
|
6284
|
+
"```"
|
|
6285
|
+
].filter(Boolean).join("\n");
|
|
6286
|
+
if (result.tool === "git_log") return [
|
|
6287
|
+
`Tool result from ${result.tool}${path}:`,
|
|
6288
|
+
`repo_root: ${result.repoRoot ?? "(none)"}`,
|
|
6289
|
+
`commit_count: ${result.commits.length}`,
|
|
6290
|
+
`truncated: ${result.truncated}`,
|
|
6291
|
+
warning ? warning.trimStart() : "",
|
|
6292
|
+
"```",
|
|
6293
|
+
result.content,
|
|
6294
|
+
"```"
|
|
6295
|
+
].filter(Boolean).join("\n");
|
|
6296
|
+
if (result.tool === "git_add") return [
|
|
6297
|
+
`Tool result from ${result.tool}:`,
|
|
6298
|
+
`repo_root: ${result.repoRoot ?? "(none)"}`,
|
|
6299
|
+
`staged_paths: ${result.stagedPaths.join(", ")}`,
|
|
6300
|
+
warning ? warning.trimStart() : "",
|
|
6301
|
+
"```",
|
|
6302
|
+
result.content,
|
|
6303
|
+
"```"
|
|
6304
|
+
].filter(Boolean).join("\n");
|
|
6305
|
+
if (result.tool === "git_commit") return [
|
|
6306
|
+
`Tool result from ${result.tool}:`,
|
|
6307
|
+
`repo_root: ${result.repoRoot ?? "(none)"}`,
|
|
6308
|
+
`commit: ${result.commit.shortSha} ${result.commit.subject}`,
|
|
6309
|
+
`staged_paths: ${result.stagedPaths.join(", ")}`,
|
|
6310
|
+
`remaining_changed_file_count: ${result.remainingFiles.length}`,
|
|
6311
|
+
"```",
|
|
6312
|
+
result.content,
|
|
6313
|
+
"```"
|
|
6314
|
+
].join("\n");
|
|
5241
6315
|
return [
|
|
5242
6316
|
`Tool result from ${result.tool}${path}${command}:${warning}`,
|
|
5243
6317
|
"```",
|
|
@@ -5255,13 +6329,27 @@ function formatToolCallMessage(call, result) {
|
|
|
5255
6329
|
case "grep": return `grep: ${call.args.pattern} in ${call.args.path ?? "."}`;
|
|
5256
6330
|
case "find_file": return `find_file: ${call.args.query} in ${call.args.path}`;
|
|
5257
6331
|
case "edit_file": return `edit_file: ${call.args.path}${formatEditFileChangeSummary(result)}`;
|
|
6332
|
+
case "write_file": return `write_file: ${call.args.path}${formatWriteFileChangeSummary(result)}`;
|
|
6333
|
+
case "git_status": return `git_status: ${result?.tool === "git_status" ? `${result.files.length} changed` : call.args.path}`;
|
|
6334
|
+
case "git_diff": return `git_diff: ${formatGitDiffCallSummary(call, result)}`;
|
|
6335
|
+
case "git_log": return `git_log: ${result?.tool === "git_log" ? `${result.commits.length} commits` : `${call.args.limit} commits`}`;
|
|
6336
|
+
case "git_add": return `git_add: ${result?.tool === "git_add" ? `${result.stagedPaths.length} files staged` : `${call.args.paths.length} files`}`;
|
|
6337
|
+
case "git_commit": return `git_commit: ${result?.tool === "git_commit" ? `${result.commit.shortSha} ${result.commit.subject}` : call.args.message}`;
|
|
5258
6338
|
case "inspect_command": return `inspect_command: ${call.args.command}`;
|
|
5259
6339
|
}
|
|
5260
6340
|
}
|
|
6341
|
+
function formatGitDiffCallSummary(call, result) {
|
|
6342
|
+
if (result?.tool === "git_diff") return `${result.scope} (${result.fileCount} files${result.truncated ? ", truncated" : ""})`;
|
|
6343
|
+
return call.args.scope;
|
|
6344
|
+
}
|
|
5261
6345
|
function formatEditFileChangeSummary(result) {
|
|
5262
6346
|
if (result?.tool !== "edit_file") return "";
|
|
5263
6347
|
return ` (changed ${result.editEvent.diffSummary})`;
|
|
5264
6348
|
}
|
|
6349
|
+
function formatWriteFileChangeSummary(result) {
|
|
6350
|
+
if (result?.tool !== "write_file") return "";
|
|
6351
|
+
return ` (${result.writeEvent.writeSummary})`;
|
|
6352
|
+
}
|
|
5265
6353
|
function formatAgentMessageMeta(model, durationMs) {
|
|
5266
6354
|
return `${model} · ${formatDuration$1(durationMs)}`;
|
|
5267
6355
|
}
|
|
@@ -5285,7 +6373,7 @@ function formatNumber(value, fractionDigits) {
|
|
|
5285
6373
|
function renderRuntimeEvent(event) {
|
|
5286
6374
|
switch (event.type) {
|
|
5287
6375
|
case "message": return [event.role === "assistant" ? agentMessage(event.text, event.meta) : systemMessage(event.text)];
|
|
5288
|
-
case "tool_call": return [
|
|
6376
|
+
case "tool_call": return [toolCallMessage(event.call, event.label)];
|
|
5289
6377
|
case "knowledge_status": return [systemMessage([`KB status: ${formatKnowledgePathStatus(event.status)}${formatKbPathSource(event.status)}`, event.guidance].filter(Boolean).join("\n"))];
|
|
5290
6378
|
case "choice": return [modalMessage({
|
|
5291
6379
|
tone: event.tone,
|
|
@@ -5501,9 +6589,14 @@ async function persistMessagesWithWarning(session, messages, warningTarget = mes
|
|
|
5501
6589
|
}
|
|
5502
6590
|
}
|
|
5503
6591
|
function chatMessageToSessionPayload(message) {
|
|
5504
|
-
if (message.kind === "system" || message.kind === "user"
|
|
6592
|
+
if (message.kind === "system" || message.kind === "user") return {
|
|
6593
|
+
kind: "message",
|
|
6594
|
+
role: message.kind,
|
|
6595
|
+
text: message.text
|
|
6596
|
+
};
|
|
6597
|
+
if (message.kind === "agent") return {
|
|
5505
6598
|
kind: "message",
|
|
5506
|
-
role:
|
|
6599
|
+
role: "assistant",
|
|
5507
6600
|
text: message.text,
|
|
5508
6601
|
...message.meta === void 0 ? {} : { meta: message.meta }
|
|
5509
6602
|
};
|
|
@@ -5514,6 +6607,11 @@ function chatMessageToSessionPayload(message) {
|
|
|
5514
6607
|
...message.body === void 0 ? {} : { body: message.body },
|
|
5515
6608
|
actions: message.actions
|
|
5516
6609
|
};
|
|
6610
|
+
if (message.kind === "tool_call") return {
|
|
6611
|
+
kind: "tool_call",
|
|
6612
|
+
label: message.label,
|
|
6613
|
+
call: message.call
|
|
6614
|
+
};
|
|
5517
6615
|
}
|
|
5518
6616
|
function runtimeEventToSessionPayload(event) {
|
|
5519
6617
|
switch (event.type) {
|
|
@@ -5738,16 +6836,19 @@ async function resolveRunSession(workspaceRoot, resume) {
|
|
|
5738
6836
|
}
|
|
5739
6837
|
async function loadConversation(workspaceRoot, resume) {
|
|
5740
6838
|
return rehydrateSession((await loadSession(workspaceRoot, resume)).events).messages.flatMap((message) => {
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
6839
|
+
switch (message.kind) {
|
|
6840
|
+
case "user": return message.modelContext === false ? [] : [{
|
|
6841
|
+
role: "user",
|
|
6842
|
+
text: message.text
|
|
6843
|
+
}];
|
|
6844
|
+
case "agent": return message.modelContext === false ? [] : [{
|
|
6845
|
+
role: "assistant",
|
|
6846
|
+
text: message.text
|
|
6847
|
+
}];
|
|
6848
|
+
case "system":
|
|
6849
|
+
case "tool_call":
|
|
6850
|
+
case "modal": return [];
|
|
6851
|
+
}
|
|
5751
6852
|
});
|
|
5752
6853
|
}
|
|
5753
6854
|
async function persistStartupMessages(session, context) {
|