tarsk 0.3.25 → 0.3.28
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/index.js +1950 -366
- package/dist/public/assets/index-CNwa_iFT.css +1 -0
- package/dist/public/assets/index-CyUhDd0r.js +86 -0
- package/dist/public/index.html +2 -2
- package/package.json +7 -6
- package/dist/public/assets/index-BLGMN24v.css +0 -1
- package/dist/public/assets/index-ClP26kjT.js +0 -86
- package/node_modules/@neovate/code/LICENSE +0 -21
- package/node_modules/@neovate/code/README.md +0 -56
- package/node_modules/@neovate/code/dist/cli.mjs +0 -6582
- package/node_modules/@neovate/code/dist/index.d.ts +0 -2797
- package/node_modules/@neovate/code/dist/index.mjs +0 -6581
- package/node_modules/@neovate/code/package.json +0 -147
- package/node_modules/@neovate/code/vendor/ripgrep/COPYING +0 -3
- package/node_modules/@neovate/code/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/node_modules/@neovate/code/vendor/ripgrep/arm64-linux/rg +0 -0
- package/node_modules/@neovate/code/vendor/ripgrep/x64-darwin/rg +0 -0
- package/node_modules/@neovate/code/vendor/ripgrep/x64-linux/rg +0 -0
- package/node_modules/@neovate/code/vendor/ripgrep/x64-win32/rg.exe +0 -0
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { Hono as Hono10 } from "hono";
|
|
|
7
7
|
import { cors } from "hono/cors";
|
|
8
8
|
import { serve } from "@hono/node-server";
|
|
9
9
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
10
|
-
import
|
|
10
|
+
import path3 from "path";
|
|
11
11
|
import open2 from "open";
|
|
12
12
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
13
13
|
|
|
@@ -1838,24 +1838,1404 @@ class MetadataManager {
|
|
|
1838
1838
|
} else {
|
|
1839
1839
|
state.enabledModels[provider] = modelIds;
|
|
1840
1840
|
}
|
|
1841
|
-
await this.saveState(state);
|
|
1842
|
-
}
|
|
1843
|
-
async setOnboardingCompleted(completed) {
|
|
1844
|
-
const state = await this.loadState();
|
|
1845
|
-
state.onboardingCompleted = completed;
|
|
1846
|
-
await this.saveState(state);
|
|
1847
|
-
}
|
|
1848
|
-
async getOnboardingCompleted() {
|
|
1849
|
-
const state = await this.loadState();
|
|
1850
|
-
return state.onboardingCompleted;
|
|
1851
|
-
}
|
|
1841
|
+
await this.saveState(state);
|
|
1842
|
+
}
|
|
1843
|
+
async setOnboardingCompleted(completed) {
|
|
1844
|
+
const state = await this.loadState();
|
|
1845
|
+
state.onboardingCompleted = completed;
|
|
1846
|
+
await this.saveState(state);
|
|
1847
|
+
}
|
|
1848
|
+
async getOnboardingCompleted() {
|
|
1849
|
+
const state = await this.loadState();
|
|
1850
|
+
return state.onboardingCompleted;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// src/managers/pi-executor.ts
|
|
1855
|
+
import { Agent } from "@mariozechner/pi-agent-core";
|
|
1856
|
+
import {
|
|
1857
|
+
getModel
|
|
1858
|
+
} from "@mariozechner/pi-ai";
|
|
1859
|
+
import { resolve } from "path";
|
|
1860
|
+
|
|
1861
|
+
// src/tools/bash.ts
|
|
1862
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1863
|
+
import { createWriteStream, existsSync as existsSync3 } from "node:fs";
|
|
1864
|
+
import { tmpdir } from "node:os";
|
|
1865
|
+
import { join as join6 } from "node:path";
|
|
1866
|
+
import { Type } from "@sinclair/typebox";
|
|
1867
|
+
import { spawn as spawn5 } from "child_process";
|
|
1868
|
+
|
|
1869
|
+
// src/tools/shell.ts
|
|
1870
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
1871
|
+
import { spawn as spawn4, spawnSync as spawnSync3 } from "node:child_process";
|
|
1872
|
+
var cachedShellConfig = null;
|
|
1873
|
+
function findBashOnPath() {
|
|
1874
|
+
if (process.platform === "win32") {
|
|
1875
|
+
try {
|
|
1876
|
+
const result = spawnSync3("where", ["bash.exe"], { encoding: "utf-8", timeout: 5000 });
|
|
1877
|
+
if (result.status === 0 && result.stdout) {
|
|
1878
|
+
const firstMatch = result.stdout.trim().split(/\r?\n/)[0];
|
|
1879
|
+
if (firstMatch && existsSync2(firstMatch)) {
|
|
1880
|
+
return firstMatch;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
} catch {}
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
1886
|
+
try {
|
|
1887
|
+
const result = spawnSync3("which", ["bash"], { encoding: "utf-8", timeout: 5000 });
|
|
1888
|
+
if (result.status === 0 && result.stdout) {
|
|
1889
|
+
const firstMatch = result.stdout.trim().split(/\r?\n/)[0];
|
|
1890
|
+
if (firstMatch)
|
|
1891
|
+
return firstMatch;
|
|
1892
|
+
}
|
|
1893
|
+
} catch {}
|
|
1894
|
+
return null;
|
|
1895
|
+
}
|
|
1896
|
+
function getShellConfig() {
|
|
1897
|
+
if (cachedShellConfig) {
|
|
1898
|
+
return cachedShellConfig;
|
|
1899
|
+
}
|
|
1900
|
+
if (process.platform === "win32") {
|
|
1901
|
+
const paths = [];
|
|
1902
|
+
if (process.env.ProgramFiles) {
|
|
1903
|
+
paths.push(`${process.env.ProgramFiles}\\Git\\bin\\bash.exe`);
|
|
1904
|
+
}
|
|
1905
|
+
if (process.env["ProgramFiles(x86)"]) {
|
|
1906
|
+
paths.push(`${process.env["ProgramFiles(x86)"]}\\Git\\bin\\bash.exe`);
|
|
1907
|
+
}
|
|
1908
|
+
for (const p of paths) {
|
|
1909
|
+
if (existsSync2(p)) {
|
|
1910
|
+
cachedShellConfig = { shell: p, args: ["-c"] };
|
|
1911
|
+
return cachedShellConfig;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
const bashOnPath2 = findBashOnPath();
|
|
1915
|
+
if (bashOnPath2) {
|
|
1916
|
+
cachedShellConfig = { shell: bashOnPath2, args: ["-c"] };
|
|
1917
|
+
return cachedShellConfig;
|
|
1918
|
+
}
|
|
1919
|
+
throw new Error("No bash shell found. Install Git for Windows or add bash to PATH.");
|
|
1920
|
+
}
|
|
1921
|
+
if (existsSync2("/bin/bash")) {
|
|
1922
|
+
cachedShellConfig = { shell: "/bin/bash", args: ["-c"] };
|
|
1923
|
+
return cachedShellConfig;
|
|
1924
|
+
}
|
|
1925
|
+
const bashOnPath = findBashOnPath();
|
|
1926
|
+
if (bashOnPath) {
|
|
1927
|
+
cachedShellConfig = { shell: bashOnPath, args: ["-c"] };
|
|
1928
|
+
return cachedShellConfig;
|
|
1929
|
+
}
|
|
1930
|
+
cachedShellConfig = { shell: "sh", args: ["-c"] };
|
|
1931
|
+
return cachedShellConfig;
|
|
1932
|
+
}
|
|
1933
|
+
function getShellEnv() {
|
|
1934
|
+
return { ...process.env };
|
|
1935
|
+
}
|
|
1936
|
+
function killProcessTree(pid) {
|
|
1937
|
+
if (process.platform === "win32") {
|
|
1938
|
+
try {
|
|
1939
|
+
spawn4("taskkill", ["/F", "/T", "/PID", String(pid)], { stdio: "ignore", detached: true });
|
|
1940
|
+
} catch {}
|
|
1941
|
+
} else {
|
|
1942
|
+
try {
|
|
1943
|
+
process.kill(-pid, "SIGKILL");
|
|
1944
|
+
} catch {
|
|
1945
|
+
try {
|
|
1946
|
+
process.kill(pid, "SIGKILL");
|
|
1947
|
+
} catch {}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// src/tools/truncate.ts
|
|
1953
|
+
var DEFAULT_MAX_LINES = 2000;
|
|
1954
|
+
var DEFAULT_MAX_BYTES = 50 * 1024;
|
|
1955
|
+
var GREP_MAX_LINE_LENGTH = 500;
|
|
1956
|
+
function formatSize(bytes) {
|
|
1957
|
+
if (bytes < 1024) {
|
|
1958
|
+
return `${bytes}B`;
|
|
1959
|
+
} else if (bytes < 1024 * 1024) {
|
|
1960
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1961
|
+
} else {
|
|
1962
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
function truncateHead(content, options = {}) {
|
|
1966
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
1967
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
1968
|
+
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
1969
|
+
const lines = content.split(`
|
|
1970
|
+
`);
|
|
1971
|
+
const totalLines = lines.length;
|
|
1972
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
1973
|
+
return {
|
|
1974
|
+
content,
|
|
1975
|
+
truncated: false,
|
|
1976
|
+
truncatedBy: null,
|
|
1977
|
+
totalLines,
|
|
1978
|
+
totalBytes,
|
|
1979
|
+
outputLines: totalLines,
|
|
1980
|
+
outputBytes: totalBytes,
|
|
1981
|
+
lastLinePartial: false,
|
|
1982
|
+
firstLineExceedsLimit: false,
|
|
1983
|
+
maxLines,
|
|
1984
|
+
maxBytes
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
const firstLineBytes = Buffer.byteLength(lines[0], "utf-8");
|
|
1988
|
+
if (firstLineBytes > maxBytes) {
|
|
1989
|
+
return {
|
|
1990
|
+
content: "",
|
|
1991
|
+
truncated: true,
|
|
1992
|
+
truncatedBy: "bytes",
|
|
1993
|
+
totalLines,
|
|
1994
|
+
totalBytes,
|
|
1995
|
+
outputLines: 0,
|
|
1996
|
+
outputBytes: 0,
|
|
1997
|
+
lastLinePartial: false,
|
|
1998
|
+
firstLineExceedsLimit: true,
|
|
1999
|
+
maxLines,
|
|
2000
|
+
maxBytes
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
const outputLinesArr = [];
|
|
2004
|
+
let outputBytesCount = 0;
|
|
2005
|
+
let truncatedBy = "lines";
|
|
2006
|
+
for (let i = 0;i < lines.length && i < maxLines; i++) {
|
|
2007
|
+
const line = lines[i];
|
|
2008
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0);
|
|
2009
|
+
if (outputBytesCount + lineBytes > maxBytes) {
|
|
2010
|
+
truncatedBy = "bytes";
|
|
2011
|
+
break;
|
|
2012
|
+
}
|
|
2013
|
+
outputLinesArr.push(line);
|
|
2014
|
+
outputBytesCount += lineBytes;
|
|
2015
|
+
}
|
|
2016
|
+
if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
|
|
2017
|
+
truncatedBy = "lines";
|
|
2018
|
+
}
|
|
2019
|
+
const outputContent = outputLinesArr.join(`
|
|
2020
|
+
`);
|
|
2021
|
+
const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
|
|
2022
|
+
return {
|
|
2023
|
+
content: outputContent,
|
|
2024
|
+
truncated: true,
|
|
2025
|
+
truncatedBy,
|
|
2026
|
+
totalLines,
|
|
2027
|
+
totalBytes,
|
|
2028
|
+
outputLines: outputLinesArr.length,
|
|
2029
|
+
outputBytes: finalOutputBytes,
|
|
2030
|
+
lastLinePartial: false,
|
|
2031
|
+
firstLineExceedsLimit: false,
|
|
2032
|
+
maxLines,
|
|
2033
|
+
maxBytes
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
function truncateTail(content, options = {}) {
|
|
2037
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
2038
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
2039
|
+
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
2040
|
+
const lines = content.split(`
|
|
2041
|
+
`);
|
|
2042
|
+
const totalLines = lines.length;
|
|
2043
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
2044
|
+
return {
|
|
2045
|
+
content,
|
|
2046
|
+
truncated: false,
|
|
2047
|
+
truncatedBy: null,
|
|
2048
|
+
totalLines,
|
|
2049
|
+
totalBytes,
|
|
2050
|
+
outputLines: totalLines,
|
|
2051
|
+
outputBytes: totalBytes,
|
|
2052
|
+
lastLinePartial: false,
|
|
2053
|
+
firstLineExceedsLimit: false,
|
|
2054
|
+
maxLines,
|
|
2055
|
+
maxBytes
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
const outputLinesArr = [];
|
|
2059
|
+
let outputBytesCount = 0;
|
|
2060
|
+
let truncatedBy = "lines";
|
|
2061
|
+
let lastLinePartial = false;
|
|
2062
|
+
for (let i = lines.length - 1;i >= 0 && outputLinesArr.length < maxLines; i--) {
|
|
2063
|
+
const line = lines[i];
|
|
2064
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0);
|
|
2065
|
+
if (outputBytesCount + lineBytes > maxBytes) {
|
|
2066
|
+
truncatedBy = "bytes";
|
|
2067
|
+
if (outputLinesArr.length === 0) {
|
|
2068
|
+
const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
|
|
2069
|
+
outputLinesArr.unshift(truncatedLine);
|
|
2070
|
+
outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8");
|
|
2071
|
+
lastLinePartial = true;
|
|
2072
|
+
}
|
|
2073
|
+
break;
|
|
2074
|
+
}
|
|
2075
|
+
outputLinesArr.unshift(line);
|
|
2076
|
+
outputBytesCount += lineBytes;
|
|
2077
|
+
}
|
|
2078
|
+
if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
|
|
2079
|
+
truncatedBy = "lines";
|
|
2080
|
+
}
|
|
2081
|
+
const outputContent = outputLinesArr.join(`
|
|
2082
|
+
`);
|
|
2083
|
+
const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
|
|
2084
|
+
return {
|
|
2085
|
+
content: outputContent,
|
|
2086
|
+
truncated: true,
|
|
2087
|
+
truncatedBy,
|
|
2088
|
+
totalLines,
|
|
2089
|
+
totalBytes,
|
|
2090
|
+
outputLines: outputLinesArr.length,
|
|
2091
|
+
outputBytes: finalOutputBytes,
|
|
2092
|
+
lastLinePartial,
|
|
2093
|
+
firstLineExceedsLimit: false,
|
|
2094
|
+
maxLines,
|
|
2095
|
+
maxBytes
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
function truncateStringToBytesFromEnd(str, maxBytes) {
|
|
2099
|
+
const buf = Buffer.from(str, "utf-8");
|
|
2100
|
+
if (buf.length <= maxBytes) {
|
|
2101
|
+
return str;
|
|
2102
|
+
}
|
|
2103
|
+
let start = buf.length - maxBytes;
|
|
2104
|
+
while (start < buf.length && (buf[start] & 192) === 128) {
|
|
2105
|
+
start++;
|
|
2106
|
+
}
|
|
2107
|
+
return buf.slice(start).toString("utf-8");
|
|
2108
|
+
}
|
|
2109
|
+
function truncateLine(line, maxChars = GREP_MAX_LINE_LENGTH) {
|
|
2110
|
+
if (line.length <= maxChars) {
|
|
2111
|
+
return { text: line, wasTruncated: false };
|
|
2112
|
+
}
|
|
2113
|
+
return { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// src/tools/bash.ts
|
|
2117
|
+
function getTempFilePath() {
|
|
2118
|
+
return join6(tmpdir(), `tarsk-bash-${randomBytes2(8).toString("hex")}.log`);
|
|
2119
|
+
}
|
|
2120
|
+
var bashSchema = Type.Object({
|
|
2121
|
+
command: Type.String({ description: "Bash command to execute" }),
|
|
2122
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional)" }))
|
|
2123
|
+
});
|
|
2124
|
+
var defaultBashOperations = {
|
|
2125
|
+
exec: (command, cwd, { onData, signal, timeout, env }) => {
|
|
2126
|
+
return new Promise((resolve, reject) => {
|
|
2127
|
+
const { shell, args } = getShellConfig();
|
|
2128
|
+
if (!existsSync3(cwd)) {
|
|
2129
|
+
reject(new Error(`Working directory does not exist: ${cwd}`));
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
const child = spawn5(shell, [...args, command], {
|
|
2133
|
+
cwd,
|
|
2134
|
+
detached: true,
|
|
2135
|
+
env: env ?? getShellEnv(),
|
|
2136
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2137
|
+
});
|
|
2138
|
+
let timedOut = false;
|
|
2139
|
+
let timeoutHandle;
|
|
2140
|
+
if (timeout !== undefined && timeout > 0) {
|
|
2141
|
+
timeoutHandle = setTimeout(() => {
|
|
2142
|
+
timedOut = true;
|
|
2143
|
+
if (child.pid)
|
|
2144
|
+
killProcessTree(child.pid);
|
|
2145
|
+
}, timeout * 1000);
|
|
2146
|
+
}
|
|
2147
|
+
if (child.stdout)
|
|
2148
|
+
child.stdout.on("data", onData);
|
|
2149
|
+
if (child.stderr)
|
|
2150
|
+
child.stderr.on("data", onData);
|
|
2151
|
+
child.on("error", (err) => {
|
|
2152
|
+
if (timeoutHandle)
|
|
2153
|
+
clearTimeout(timeoutHandle);
|
|
2154
|
+
if (signal)
|
|
2155
|
+
signal.removeEventListener("abort", onAbort);
|
|
2156
|
+
reject(err);
|
|
2157
|
+
});
|
|
2158
|
+
const onAbort = () => {
|
|
2159
|
+
if (child.pid)
|
|
2160
|
+
killProcessTree(child.pid);
|
|
2161
|
+
};
|
|
2162
|
+
if (signal) {
|
|
2163
|
+
if (signal.aborted)
|
|
2164
|
+
onAbort();
|
|
2165
|
+
else
|
|
2166
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2167
|
+
}
|
|
2168
|
+
child.on("close", (code) => {
|
|
2169
|
+
if (timeoutHandle)
|
|
2170
|
+
clearTimeout(timeoutHandle);
|
|
2171
|
+
if (signal)
|
|
2172
|
+
signal.removeEventListener("abort", onAbort);
|
|
2173
|
+
if (signal?.aborted) {
|
|
2174
|
+
reject(new Error("aborted"));
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
if (timedOut) {
|
|
2178
|
+
reject(new Error(`timeout:${timeout}`));
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
resolve({ exitCode: code });
|
|
2182
|
+
});
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
function createBashTool(cwd, options) {
|
|
2187
|
+
const ops = options?.operations ?? defaultBashOperations;
|
|
2188
|
+
const commandPrefix = options?.commandPrefix;
|
|
2189
|
+
return {
|
|
2190
|
+
name: "bash",
|
|
2191
|
+
label: "bash",
|
|
2192
|
+
description: `Execute a bash command in the current working directory. Output truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB. Optionally provide a timeout in seconds.`,
|
|
2193
|
+
parameters: bashSchema,
|
|
2194
|
+
execute: async (_toolCallId, { command, timeout }, signal, onUpdate) => {
|
|
2195
|
+
const resolvedCommand = commandPrefix ? `${commandPrefix}
|
|
2196
|
+
${command}` : command;
|
|
2197
|
+
return new Promise((resolve, reject) => {
|
|
2198
|
+
let tempFilePath;
|
|
2199
|
+
let tempFileStream;
|
|
2200
|
+
let totalBytes = 0;
|
|
2201
|
+
const chunks = [];
|
|
2202
|
+
let chunksBytes = 0;
|
|
2203
|
+
const maxChunksBytes = DEFAULT_MAX_BYTES * 2;
|
|
2204
|
+
const handleData = (data) => {
|
|
2205
|
+
totalBytes += data.length;
|
|
2206
|
+
if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
|
|
2207
|
+
tempFilePath = getTempFilePath();
|
|
2208
|
+
tempFileStream = createWriteStream(tempFilePath);
|
|
2209
|
+
for (const chunk of chunks)
|
|
2210
|
+
tempFileStream.write(chunk);
|
|
2211
|
+
}
|
|
2212
|
+
if (tempFileStream)
|
|
2213
|
+
tempFileStream.write(data);
|
|
2214
|
+
chunks.push(data);
|
|
2215
|
+
chunksBytes += data.length;
|
|
2216
|
+
while (chunksBytes > maxChunksBytes && chunks.length > 1) {
|
|
2217
|
+
const removed = chunks.shift();
|
|
2218
|
+
chunksBytes -= removed.length;
|
|
2219
|
+
}
|
|
2220
|
+
if (onUpdate) {
|
|
2221
|
+
const fullText = Buffer.concat(chunks).toString("utf-8");
|
|
2222
|
+
const truncation = truncateTail(fullText);
|
|
2223
|
+
onUpdate({
|
|
2224
|
+
content: [{ type: "text", text: truncation.content || "" }],
|
|
2225
|
+
details: {
|
|
2226
|
+
truncation: truncation.truncated ? truncation : undefined,
|
|
2227
|
+
fullOutputPath: tempFilePath
|
|
2228
|
+
}
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
ops.exec(resolvedCommand, cwd, { onData: handleData, signal, timeout, env: getShellEnv() }).then(({ exitCode }) => {
|
|
2233
|
+
if (tempFileStream)
|
|
2234
|
+
tempFileStream.end();
|
|
2235
|
+
const fullOutput = Buffer.concat(chunks).toString("utf-8");
|
|
2236
|
+
const truncation = truncateTail(fullOutput);
|
|
2237
|
+
let outputText = truncation.content || "(no output)";
|
|
2238
|
+
let details;
|
|
2239
|
+
if (truncation.truncated) {
|
|
2240
|
+
details = { truncation, fullOutputPath: tempFilePath };
|
|
2241
|
+
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
|
2242
|
+
const endLine = truncation.totalLines;
|
|
2243
|
+
if (truncation.lastLinePartial) {
|
|
2244
|
+
outputText += `
|
|
2245
|
+
|
|
2246
|
+
[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine}. Full output: ${tempFilePath}]`;
|
|
2247
|
+
} else if (truncation.truncatedBy === "lines") {
|
|
2248
|
+
outputText += `
|
|
2249
|
+
|
|
2250
|
+
[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
|
|
2251
|
+
} else {
|
|
2252
|
+
outputText += `
|
|
2253
|
+
|
|
2254
|
+
[Showing lines ${startLine}-${endLine} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
if (exitCode !== 0 && exitCode !== null) {
|
|
2258
|
+
outputText += `
|
|
2259
|
+
|
|
2260
|
+
Command exited with code ${exitCode}`;
|
|
2261
|
+
reject(new Error(outputText));
|
|
2262
|
+
} else {
|
|
2263
|
+
resolve({ content: [{ type: "text", text: outputText }], details });
|
|
2264
|
+
}
|
|
2265
|
+
}).catch((err) => {
|
|
2266
|
+
if (tempFileStream)
|
|
2267
|
+
tempFileStream.end();
|
|
2268
|
+
const output = Buffer.concat(chunks).toString("utf-8");
|
|
2269
|
+
if (err.message === "aborted") {
|
|
2270
|
+
reject(new Error(output ? `${output}
|
|
2271
|
+
|
|
2272
|
+
Command aborted` : "Command aborted"));
|
|
2273
|
+
} else if (err.message.startsWith("timeout:")) {
|
|
2274
|
+
const secs = err.message.split(":")[1];
|
|
2275
|
+
reject(new Error(output ? `${output}
|
|
2276
|
+
|
|
2277
|
+
Command timed out after ${secs} seconds` : `Command timed out after ${secs} seconds`));
|
|
2278
|
+
} else {
|
|
2279
|
+
reject(err);
|
|
2280
|
+
}
|
|
2281
|
+
});
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
var bashTool = createBashTool(process.cwd());
|
|
2287
|
+
|
|
2288
|
+
// src/tools/edit.ts
|
|
2289
|
+
import { Type as Type2 } from "@sinclair/typebox";
|
|
2290
|
+
import { constants as constants2 } from "fs";
|
|
2291
|
+
import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
|
|
2292
|
+
|
|
2293
|
+
// src/tools/edit-diff.ts
|
|
2294
|
+
import * as Diff from "diff";
|
|
2295
|
+
function detectLineEnding(content) {
|
|
2296
|
+
const crlfIdx = content.indexOf(`\r
|
|
2297
|
+
`);
|
|
2298
|
+
const lfIdx = content.indexOf(`
|
|
2299
|
+
`);
|
|
2300
|
+
if (lfIdx === -1)
|
|
2301
|
+
return `
|
|
2302
|
+
`;
|
|
2303
|
+
if (crlfIdx === -1)
|
|
2304
|
+
return `
|
|
2305
|
+
`;
|
|
2306
|
+
return crlfIdx < lfIdx ? `\r
|
|
2307
|
+
` : `
|
|
2308
|
+
`;
|
|
2309
|
+
}
|
|
2310
|
+
function normalizeToLF(text) {
|
|
2311
|
+
return text.replace(/\r\n/g, `
|
|
2312
|
+
`).replace(/\r/g, `
|
|
2313
|
+
`);
|
|
2314
|
+
}
|
|
2315
|
+
function restoreLineEndings(text, ending) {
|
|
2316
|
+
return ending === `\r
|
|
2317
|
+
` ? text.replace(/\n/g, `\r
|
|
2318
|
+
`) : text;
|
|
2319
|
+
}
|
|
2320
|
+
function normalizeForFuzzyMatch(text) {
|
|
2321
|
+
return text.split(`
|
|
2322
|
+
`).map((line) => line.trimEnd()).join(`
|
|
2323
|
+
`).replace(/[\u2018\u2019\u201A\u201B]/g, "'").replace(/[\u201C\u201D\u201E\u201F]/g, '"').replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-").replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ");
|
|
2324
|
+
}
|
|
2325
|
+
function fuzzyFindText(content, oldText) {
|
|
2326
|
+
const exactIndex = content.indexOf(oldText);
|
|
2327
|
+
if (exactIndex !== -1) {
|
|
2328
|
+
return {
|
|
2329
|
+
found: true,
|
|
2330
|
+
index: exactIndex,
|
|
2331
|
+
matchLength: oldText.length,
|
|
2332
|
+
usedFuzzyMatch: false,
|
|
2333
|
+
contentForReplacement: content
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
const fuzzyContent = normalizeForFuzzyMatch(content);
|
|
2337
|
+
const fuzzyOldText = normalizeForFuzzyMatch(oldText);
|
|
2338
|
+
const fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText);
|
|
2339
|
+
if (fuzzyIndex === -1) {
|
|
2340
|
+
return {
|
|
2341
|
+
found: false,
|
|
2342
|
+
index: -1,
|
|
2343
|
+
matchLength: 0,
|
|
2344
|
+
usedFuzzyMatch: false,
|
|
2345
|
+
contentForReplacement: content
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
return {
|
|
2349
|
+
found: true,
|
|
2350
|
+
index: fuzzyIndex,
|
|
2351
|
+
matchLength: fuzzyOldText.length,
|
|
2352
|
+
usedFuzzyMatch: true,
|
|
2353
|
+
contentForReplacement: fuzzyContent
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
function stripBom(content) {
|
|
2357
|
+
return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
|
|
2358
|
+
}
|
|
2359
|
+
function generateDiffString(oldContent, newContent, contextLines = 4) {
|
|
2360
|
+
const parts = Diff.diffLines(oldContent, newContent);
|
|
2361
|
+
const output = [];
|
|
2362
|
+
const oldLines = oldContent.split(`
|
|
2363
|
+
`);
|
|
2364
|
+
const newLines = newContent.split(`
|
|
2365
|
+
`);
|
|
2366
|
+
const maxLineNum = Math.max(oldLines.length, newLines.length);
|
|
2367
|
+
const lineNumWidth = String(maxLineNum).length;
|
|
2368
|
+
let oldLineNum = 1;
|
|
2369
|
+
let newLineNum = 1;
|
|
2370
|
+
let lastWasChange = false;
|
|
2371
|
+
let firstChangedLine;
|
|
2372
|
+
for (let i = 0;i < parts.length; i++) {
|
|
2373
|
+
const part = parts[i];
|
|
2374
|
+
const raw = part.value.split(`
|
|
2375
|
+
`);
|
|
2376
|
+
if (raw[raw.length - 1] === "") {
|
|
2377
|
+
raw.pop();
|
|
2378
|
+
}
|
|
2379
|
+
if (part.added || part.removed) {
|
|
2380
|
+
if (firstChangedLine === undefined) {
|
|
2381
|
+
firstChangedLine = newLineNum;
|
|
2382
|
+
}
|
|
2383
|
+
for (const line of raw) {
|
|
2384
|
+
if (part.added) {
|
|
2385
|
+
output.push(`+${String(newLineNum).padStart(lineNumWidth, " ")} ${line}`);
|
|
2386
|
+
newLineNum++;
|
|
2387
|
+
} else {
|
|
2388
|
+
output.push(`-${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
|
|
2389
|
+
oldLineNum++;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
lastWasChange = true;
|
|
2393
|
+
} else {
|
|
2394
|
+
const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
|
2395
|
+
if (lastWasChange || nextPartIsChange) {
|
|
2396
|
+
let linesToShow = raw;
|
|
2397
|
+
let skipStart = 0;
|
|
2398
|
+
let skipEnd = 0;
|
|
2399
|
+
if (!lastWasChange) {
|
|
2400
|
+
skipStart = Math.max(0, raw.length - contextLines);
|
|
2401
|
+
linesToShow = raw.slice(skipStart);
|
|
2402
|
+
}
|
|
2403
|
+
if (!nextPartIsChange && linesToShow.length > contextLines) {
|
|
2404
|
+
skipEnd = linesToShow.length - contextLines;
|
|
2405
|
+
linesToShow = linesToShow.slice(0, contextLines);
|
|
2406
|
+
}
|
|
2407
|
+
if (skipStart > 0) {
|
|
2408
|
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
2409
|
+
oldLineNum += skipStart;
|
|
2410
|
+
newLineNum += skipStart;
|
|
2411
|
+
}
|
|
2412
|
+
for (const line of linesToShow) {
|
|
2413
|
+
output.push(` ${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
|
|
2414
|
+
oldLineNum++;
|
|
2415
|
+
newLineNum++;
|
|
2416
|
+
}
|
|
2417
|
+
if (skipEnd > 0) {
|
|
2418
|
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
|
2419
|
+
oldLineNum += skipEnd;
|
|
2420
|
+
newLineNum += skipEnd;
|
|
2421
|
+
}
|
|
2422
|
+
} else {
|
|
2423
|
+
oldLineNum += raw.length;
|
|
2424
|
+
newLineNum += raw.length;
|
|
2425
|
+
}
|
|
2426
|
+
lastWasChange = false;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
return { diff: output.join(`
|
|
2430
|
+
`), firstChangedLine };
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
// src/tools/path-utils.ts
|
|
2434
|
+
import { accessSync, constants } from "node:fs";
|
|
2435
|
+
import * as os from "node:os";
|
|
2436
|
+
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
2437
|
+
var UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
2438
|
+
var NARROW_NO_BREAK_SPACE = " ";
|
|
2439
|
+
function normalizeUnicodeSpaces(str) {
|
|
2440
|
+
return str.replace(UNICODE_SPACES, " ");
|
|
2441
|
+
}
|
|
2442
|
+
function tryMacOSScreenshotPath(filePath) {
|
|
2443
|
+
return filePath.replace(/ (AM|PM)\./g, `${NARROW_NO_BREAK_SPACE}$1.`);
|
|
2444
|
+
}
|
|
2445
|
+
function tryNFDVariant(filePath) {
|
|
2446
|
+
return filePath.normalize("NFD");
|
|
2447
|
+
}
|
|
2448
|
+
function tryCurlyQuoteVariant(filePath) {
|
|
2449
|
+
return filePath.replace(/'/g, "’");
|
|
2450
|
+
}
|
|
2451
|
+
function fileExists(filePath) {
|
|
2452
|
+
try {
|
|
2453
|
+
accessSync(filePath, constants.F_OK);
|
|
2454
|
+
return true;
|
|
2455
|
+
} catch {
|
|
2456
|
+
return false;
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
function normalizeAtPrefix(filePath) {
|
|
2460
|
+
return filePath.startsWith("@") ? filePath.slice(1) : filePath;
|
|
2461
|
+
}
|
|
2462
|
+
function expandPath(filePath) {
|
|
2463
|
+
const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
|
|
2464
|
+
if (normalized === "~") {
|
|
2465
|
+
return os.homedir();
|
|
2466
|
+
}
|
|
2467
|
+
if (normalized.startsWith("~/")) {
|
|
2468
|
+
return os.homedir() + normalized.slice(1);
|
|
2469
|
+
}
|
|
2470
|
+
return normalized;
|
|
2471
|
+
}
|
|
2472
|
+
function resolveToCwd(filePath, cwd) {
|
|
2473
|
+
const expanded = expandPath(filePath);
|
|
2474
|
+
if (isAbsolute(expanded)) {
|
|
2475
|
+
return expanded;
|
|
2476
|
+
}
|
|
2477
|
+
return resolvePath(cwd, expanded);
|
|
2478
|
+
}
|
|
2479
|
+
function resolveReadPath(filePath, cwd) {
|
|
2480
|
+
const resolved = resolveToCwd(filePath, cwd);
|
|
2481
|
+
if (fileExists(resolved)) {
|
|
2482
|
+
return resolved;
|
|
2483
|
+
}
|
|
2484
|
+
const amPmVariant = tryMacOSScreenshotPath(resolved);
|
|
2485
|
+
if (amPmVariant !== resolved && fileExists(amPmVariant)) {
|
|
2486
|
+
return amPmVariant;
|
|
2487
|
+
}
|
|
2488
|
+
const nfdVariant = tryNFDVariant(resolved);
|
|
2489
|
+
if (nfdVariant !== resolved && fileExists(nfdVariant)) {
|
|
2490
|
+
return nfdVariant;
|
|
2491
|
+
}
|
|
2492
|
+
const curlyVariant = tryCurlyQuoteVariant(resolved);
|
|
2493
|
+
if (curlyVariant !== resolved && fileExists(curlyVariant)) {
|
|
2494
|
+
return curlyVariant;
|
|
2495
|
+
}
|
|
2496
|
+
const nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);
|
|
2497
|
+
if (nfdCurlyVariant !== resolved && fileExists(nfdCurlyVariant)) {
|
|
2498
|
+
return nfdCurlyVariant;
|
|
2499
|
+
}
|
|
2500
|
+
return resolved;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// src/tools/edit.ts
|
|
2504
|
+
var editSchema = Type2.Object({
|
|
2505
|
+
path: Type2.String({ description: "Path to the file to edit (relative or absolute)" }),
|
|
2506
|
+
oldText: Type2.String({ description: "Exact text to find and replace (must match exactly)" }),
|
|
2507
|
+
newText: Type2.String({ description: "New text to replace the old text with" })
|
|
2508
|
+
});
|
|
2509
|
+
var defaultEditOperations = {
|
|
2510
|
+
readFile: (path) => fsReadFile(path),
|
|
2511
|
+
writeFile: (path, content) => fsWriteFile(path, content, "utf-8"),
|
|
2512
|
+
access: (path) => fsAccess(path, constants2.R_OK | constants2.W_OK)
|
|
2513
|
+
};
|
|
2514
|
+
function createEditTool(cwd, options) {
|
|
2515
|
+
const ops = options?.operations ?? defaultEditOperations;
|
|
2516
|
+
return {
|
|
2517
|
+
name: "edit",
|
|
2518
|
+
label: "edit",
|
|
2519
|
+
description: "Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits.",
|
|
2520
|
+
parameters: editSchema,
|
|
2521
|
+
execute: async (_toolCallId, { path, oldText, newText }, signal) => {
|
|
2522
|
+
const absolutePath = resolveToCwd(path, cwd);
|
|
2523
|
+
return new Promise((resolve, reject) => {
|
|
2524
|
+
if (signal?.aborted) {
|
|
2525
|
+
reject(new Error("Operation aborted"));
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
let aborted = false;
|
|
2529
|
+
const onAbort = () => {
|
|
2530
|
+
aborted = true;
|
|
2531
|
+
reject(new Error("Operation aborted"));
|
|
2532
|
+
};
|
|
2533
|
+
if (signal)
|
|
2534
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2535
|
+
(async () => {
|
|
2536
|
+
try {
|
|
2537
|
+
try {
|
|
2538
|
+
await ops.access(absolutePath);
|
|
2539
|
+
} catch {
|
|
2540
|
+
if (signal)
|
|
2541
|
+
signal.removeEventListener("abort", onAbort);
|
|
2542
|
+
reject(new Error(`File not found: ${path}`));
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
if (aborted)
|
|
2546
|
+
return;
|
|
2547
|
+
const buffer = await ops.readFile(absolutePath);
|
|
2548
|
+
const rawContent = buffer.toString("utf-8");
|
|
2549
|
+
if (aborted)
|
|
2550
|
+
return;
|
|
2551
|
+
const { bom, text: content } = stripBom(rawContent);
|
|
2552
|
+
const originalEnding = detectLineEnding(content);
|
|
2553
|
+
const normalizedContent = normalizeToLF(content);
|
|
2554
|
+
const normalizedOldText = normalizeToLF(oldText);
|
|
2555
|
+
const normalizedNewText = normalizeToLF(newText);
|
|
2556
|
+
const matchResult = fuzzyFindText(normalizedContent, normalizedOldText);
|
|
2557
|
+
if (!matchResult.found) {
|
|
2558
|
+
if (signal)
|
|
2559
|
+
signal.removeEventListener("abort", onAbort);
|
|
2560
|
+
reject(new Error(`Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`));
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
const fuzzyContent = normalizeForFuzzyMatch(normalizedContent);
|
|
2564
|
+
const fuzzyOldText = normalizeForFuzzyMatch(normalizedOldText);
|
|
2565
|
+
const occurrences = fuzzyContent.split(fuzzyOldText).length - 1;
|
|
2566
|
+
if (occurrences > 1) {
|
|
2567
|
+
if (signal)
|
|
2568
|
+
signal.removeEventListener("abort", onAbort);
|
|
2569
|
+
reject(new Error(`Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`));
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
if (aborted)
|
|
2573
|
+
return;
|
|
2574
|
+
const baseContent = matchResult.contentForReplacement;
|
|
2575
|
+
const newContent = baseContent.substring(0, matchResult.index) + normalizedNewText + baseContent.substring(matchResult.index + matchResult.matchLength);
|
|
2576
|
+
if (baseContent === newContent) {
|
|
2577
|
+
if (signal)
|
|
2578
|
+
signal.removeEventListener("abort", onAbort);
|
|
2579
|
+
reject(new Error(`No changes made to ${path}. The replacement produced identical content.`));
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
const finalContent = bom + restoreLineEndings(newContent, originalEnding);
|
|
2583
|
+
await ops.writeFile(absolutePath, finalContent);
|
|
2584
|
+
if (aborted)
|
|
2585
|
+
return;
|
|
2586
|
+
if (signal)
|
|
2587
|
+
signal.removeEventListener("abort", onAbort);
|
|
2588
|
+
const diffResult = generateDiffString(baseContent, newContent);
|
|
2589
|
+
resolve({
|
|
2590
|
+
content: [{ type: "text", text: `Successfully replaced text in ${path}.` }],
|
|
2591
|
+
details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine }
|
|
2592
|
+
});
|
|
2593
|
+
} catch (error) {
|
|
2594
|
+
if (signal)
|
|
2595
|
+
signal.removeEventListener("abort", onAbort);
|
|
2596
|
+
if (!aborted)
|
|
2597
|
+
reject(error);
|
|
2598
|
+
}
|
|
2599
|
+
})();
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
var editTool = createEditTool(process.cwd());
|
|
2605
|
+
|
|
2606
|
+
// src/tools/find.ts
|
|
2607
|
+
import { Type as Type3 } from "@sinclair/typebox";
|
|
2608
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2609
|
+
import path from "path";
|
|
2610
|
+
import { globSync } from "glob";
|
|
2611
|
+
var findSchema = Type3.Object({
|
|
2612
|
+
pattern: Type3.String({
|
|
2613
|
+
description: "Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'"
|
|
2614
|
+
}),
|
|
2615
|
+
path: Type3.Optional(Type3.String({ description: "Directory to search in (default: current directory)" })),
|
|
2616
|
+
limit: Type3.Optional(Type3.Number({ description: "Maximum number of results (default: 1000)" }))
|
|
2617
|
+
});
|
|
2618
|
+
var DEFAULT_LIMIT = 1000;
|
|
2619
|
+
var defaultFindOperations = {
|
|
2620
|
+
exists: existsSync4,
|
|
2621
|
+
glob: async (pattern, searchCwd, { ignore, limit }) => {
|
|
2622
|
+
const results = [];
|
|
2623
|
+
try {
|
|
2624
|
+
const found = globSync(pattern, {
|
|
2625
|
+
cwd: searchCwd,
|
|
2626
|
+
dot: true,
|
|
2627
|
+
ignore: ["**/node_modules/**", "**/.git/**", ...ignore],
|
|
2628
|
+
mark: false
|
|
2629
|
+
});
|
|
2630
|
+
for (let i = 0;i < Math.min(found.length, limit); i++) {
|
|
2631
|
+
results.push(path.join(searchCwd, found[i]));
|
|
2632
|
+
}
|
|
2633
|
+
} catch {}
|
|
2634
|
+
return results;
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
function createFindTool(cwd, options) {
|
|
2638
|
+
const customOps = options?.operations;
|
|
2639
|
+
return {
|
|
2640
|
+
name: "find",
|
|
2641
|
+
label: "find",
|
|
2642
|
+
description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Output truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB.`,
|
|
2643
|
+
parameters: findSchema,
|
|
2644
|
+
execute: async (_toolCallId, { pattern, path: searchDir, limit }, signal) => {
|
|
2645
|
+
return new Promise((resolve, reject) => {
|
|
2646
|
+
if (signal?.aborted) {
|
|
2647
|
+
reject(new Error("Operation aborted"));
|
|
2648
|
+
return;
|
|
2649
|
+
}
|
|
2650
|
+
const onAbort = () => reject(new Error("Operation aborted"));
|
|
2651
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2652
|
+
(async () => {
|
|
2653
|
+
try {
|
|
2654
|
+
const searchPath = resolveToCwd(searchDir || ".", cwd);
|
|
2655
|
+
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
2656
|
+
const ops = customOps ?? defaultFindOperations;
|
|
2657
|
+
if (!await ops.exists(searchPath)) {
|
|
2658
|
+
reject(new Error(`Path not found: ${searchPath}`));
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
const results = await ops.glob(pattern, searchPath, {
|
|
2662
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
2663
|
+
limit: effectiveLimit
|
|
2664
|
+
});
|
|
2665
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2666
|
+
if (results.length === 0) {
|
|
2667
|
+
resolve({
|
|
2668
|
+
content: [{ type: "text", text: "No files found matching pattern" }],
|
|
2669
|
+
details: undefined
|
|
2670
|
+
});
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
const relativized = results.map((p) => {
|
|
2674
|
+
if (p.startsWith(searchPath + path.sep) || p.startsWith(searchPath + "/")) {
|
|
2675
|
+
return p.slice(searchPath.length + 1).replace(/\\/g, "/");
|
|
2676
|
+
}
|
|
2677
|
+
return path.relative(searchPath, p).replace(/\\/g, "/");
|
|
2678
|
+
});
|
|
2679
|
+
const resultLimitReached = relativized.length >= effectiveLimit;
|
|
2680
|
+
const rawOutput = relativized.join(`
|
|
2681
|
+
`);
|
|
2682
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
2683
|
+
let resultOutput = truncation.content;
|
|
2684
|
+
const details = {};
|
|
2685
|
+
const notices = [];
|
|
2686
|
+
if (resultLimitReached) {
|
|
2687
|
+
notices.push(`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`);
|
|
2688
|
+
details.resultLimitReached = effectiveLimit;
|
|
2689
|
+
}
|
|
2690
|
+
if (truncation.truncated) {
|
|
2691
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
2692
|
+
details.truncation = truncation;
|
|
2693
|
+
}
|
|
2694
|
+
if (notices.length > 0)
|
|
2695
|
+
resultOutput += `
|
|
2696
|
+
|
|
2697
|
+
[${notices.join(". ")}]`;
|
|
2698
|
+
resolve({
|
|
2699
|
+
content: [{ type: "text", text: resultOutput }],
|
|
2700
|
+
details: Object.keys(details).length > 0 ? details : undefined
|
|
2701
|
+
});
|
|
2702
|
+
} catch (e) {
|
|
2703
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2704
|
+
reject(e);
|
|
2705
|
+
}
|
|
2706
|
+
})();
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
var findTool = createFindTool(process.cwd());
|
|
2712
|
+
|
|
2713
|
+
// src/tools/grep.ts
|
|
2714
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
2715
|
+
import { Type as Type4 } from "@sinclair/typebox";
|
|
2716
|
+
import { spawn as spawn6 } from "child_process";
|
|
2717
|
+
import { readFileSync as readFileSync2, statSync } from "fs";
|
|
2718
|
+
import path2 from "path";
|
|
2719
|
+
|
|
2720
|
+
// src/tools/resolve-bin.ts
|
|
2721
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
2722
|
+
function resolveBin(name) {
|
|
2723
|
+
return new Promise((resolve) => {
|
|
2724
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
2725
|
+
const args = process.platform === "win32" ? [name + ".exe", name] : [name];
|
|
2726
|
+
try {
|
|
2727
|
+
const result = spawnSync4(cmd, args, { encoding: "utf-8", timeout: 5000 });
|
|
2728
|
+
if (result.status === 0 && result.stdout) {
|
|
2729
|
+
const first = result.stdout.trim().split(/\r?\n/)[0]?.trim();
|
|
2730
|
+
if (first) {
|
|
2731
|
+
resolve(first);
|
|
2732
|
+
return;
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
} catch {}
|
|
2736
|
+
resolve(null);
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
// src/tools/grep.ts
|
|
2741
|
+
var grepSchema = Type4.Object({
|
|
2742
|
+
pattern: Type4.String({ description: "Search pattern (regex or literal string)" }),
|
|
2743
|
+
path: Type4.Optional(Type4.String({ description: "Directory or file to search (default: current directory)" })),
|
|
2744
|
+
glob: Type4.Optional(Type4.String({ description: "Filter files by glob pattern, e.g. '*.ts'" })),
|
|
2745
|
+
ignoreCase: Type4.Optional(Type4.Boolean({ description: "Case-insensitive search (default: false)" })),
|
|
2746
|
+
literal: Type4.Optional(Type4.Boolean({ description: "Treat pattern as literal string instead of regex (default: false)" })),
|
|
2747
|
+
context: Type4.Optional(Type4.Number({ description: "Number of lines before/after each match (default: 0)" })),
|
|
2748
|
+
limit: Type4.Optional(Type4.Number({ description: "Maximum number of matches to return (default: 100)" }))
|
|
2749
|
+
});
|
|
2750
|
+
var DEFAULT_LIMIT2 = 100;
|
|
2751
|
+
var defaultGrepOperations = {
|
|
2752
|
+
isDirectory: (p) => statSync(p).isDirectory(),
|
|
2753
|
+
readFile: (p) => readFileSync2(p, "utf-8")
|
|
2754
|
+
};
|
|
2755
|
+
function createGrepTool(cwd, options) {
|
|
2756
|
+
const customOps = options?.operations;
|
|
2757
|
+
return {
|
|
2758
|
+
name: "grep",
|
|
2759
|
+
label: "grep",
|
|
2760
|
+
description: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output truncated to ${DEFAULT_LIMIT2} matches or ${DEFAULT_MAX_BYTES / 1024}KB. Long lines truncated to ${GREP_MAX_LINE_LENGTH} chars.`,
|
|
2761
|
+
parameters: grepSchema,
|
|
2762
|
+
execute: async (_toolCallId, { pattern, path: searchDir, glob, ignoreCase, literal, context, limit }, signal) => {
|
|
2763
|
+
return new Promise((resolve, reject) => {
|
|
2764
|
+
if (signal?.aborted) {
|
|
2765
|
+
reject(new Error("Operation aborted"));
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
let settled = false;
|
|
2769
|
+
const settle = (fn) => {
|
|
2770
|
+
if (!settled) {
|
|
2771
|
+
settled = true;
|
|
2772
|
+
fn();
|
|
2773
|
+
}
|
|
2774
|
+
};
|
|
2775
|
+
(async () => {
|
|
2776
|
+
try {
|
|
2777
|
+
const rgPath = await resolveBin("rg");
|
|
2778
|
+
if (!rgPath) {
|
|
2779
|
+
settle(() => reject(new Error("ripgrep (rg) is not available. Install rg or add it to PATH.")));
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
const searchPath = resolveToCwd(searchDir || ".", cwd);
|
|
2783
|
+
const ops = customOps ?? defaultGrepOperations;
|
|
2784
|
+
let isDirectory;
|
|
2785
|
+
try {
|
|
2786
|
+
isDirectory = await ops.isDirectory(searchPath);
|
|
2787
|
+
} catch {
|
|
2788
|
+
settle(() => reject(new Error(`Path not found: ${searchPath}`)));
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
const contextValue = context && context > 0 ? context : 0;
|
|
2792
|
+
const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT2);
|
|
2793
|
+
const formatPath = (filePath) => {
|
|
2794
|
+
if (isDirectory) {
|
|
2795
|
+
const relative2 = path2.relative(searchPath, filePath);
|
|
2796
|
+
if (relative2 && !relative2.startsWith("..")) {
|
|
2797
|
+
return relative2.replace(/\\/g, "/");
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
return path2.basename(filePath);
|
|
2801
|
+
};
|
|
2802
|
+
const fileCache = new Map;
|
|
2803
|
+
const getFileLines = async (filePath) => {
|
|
2804
|
+
let lines = fileCache.get(filePath);
|
|
2805
|
+
if (!lines) {
|
|
2806
|
+
try {
|
|
2807
|
+
const content = await ops.readFile(filePath);
|
|
2808
|
+
lines = content.replace(/\r\n/g, `
|
|
2809
|
+
`).replace(/\r/g, `
|
|
2810
|
+
`).split(`
|
|
2811
|
+
`);
|
|
2812
|
+
} catch {
|
|
2813
|
+
lines = [];
|
|
2814
|
+
}
|
|
2815
|
+
fileCache.set(filePath, lines);
|
|
2816
|
+
}
|
|
2817
|
+
return lines;
|
|
2818
|
+
};
|
|
2819
|
+
const args = ["--json", "--line-number", "--color=never", "--hidden"];
|
|
2820
|
+
if (ignoreCase)
|
|
2821
|
+
args.push("--ignore-case");
|
|
2822
|
+
if (literal)
|
|
2823
|
+
args.push("--fixed-strings");
|
|
2824
|
+
if (glob)
|
|
2825
|
+
args.push("--glob", glob);
|
|
2826
|
+
args.push(pattern, searchPath);
|
|
2827
|
+
const child = spawn6(rgPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2828
|
+
const rl = createInterface2({ input: child.stdout });
|
|
2829
|
+
let stderr = "";
|
|
2830
|
+
let matchCount = 0;
|
|
2831
|
+
let matchLimitReached = false;
|
|
2832
|
+
let linesTruncated = false;
|
|
2833
|
+
let aborted = false;
|
|
2834
|
+
let killedDueToLimit = false;
|
|
2835
|
+
const outputLines = [];
|
|
2836
|
+
const cleanup = () => {
|
|
2837
|
+
rl.close();
|
|
2838
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2839
|
+
};
|
|
2840
|
+
const stopChild = (dueToLimit = false) => {
|
|
2841
|
+
if (!child.killed) {
|
|
2842
|
+
killedDueToLimit = dueToLimit;
|
|
2843
|
+
child.kill();
|
|
2844
|
+
}
|
|
2845
|
+
};
|
|
2846
|
+
const onAbort = () => {
|
|
2847
|
+
aborted = true;
|
|
2848
|
+
stopChild();
|
|
2849
|
+
};
|
|
2850
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2851
|
+
child.stderr?.on("data", (chunk) => {
|
|
2852
|
+
stderr += chunk.toString();
|
|
2853
|
+
});
|
|
2854
|
+
const formatBlock = async (filePath, lineNumber) => {
|
|
2855
|
+
const relativePath = formatPath(filePath);
|
|
2856
|
+
const lines = await getFileLines(filePath);
|
|
2857
|
+
if (!lines.length) {
|
|
2858
|
+
return [`${relativePath}:${lineNumber}: (unable to read file)`];
|
|
2859
|
+
}
|
|
2860
|
+
const block = [];
|
|
2861
|
+
const start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;
|
|
2862
|
+
const end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;
|
|
2863
|
+
for (let current = start;current <= end; current++) {
|
|
2864
|
+
const lineText = lines[current - 1] ?? "";
|
|
2865
|
+
const sanitized = lineText.replace(/\r/g, "");
|
|
2866
|
+
const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
|
|
2867
|
+
if (wasTruncated)
|
|
2868
|
+
linesTruncated = true;
|
|
2869
|
+
const isMatchLine = current === lineNumber;
|
|
2870
|
+
if (isMatchLine) {
|
|
2871
|
+
block.push(`${relativePath}:${current}: ${truncatedText}`);
|
|
2872
|
+
} else {
|
|
2873
|
+
block.push(`${relativePath}-${current}- ${truncatedText}`);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
return block;
|
|
2877
|
+
};
|
|
2878
|
+
const matches = [];
|
|
2879
|
+
rl.on("line", (line) => {
|
|
2880
|
+
if (!line.trim() || matchCount >= effectiveLimit)
|
|
2881
|
+
return;
|
|
2882
|
+
try {
|
|
2883
|
+
const event = JSON.parse(line);
|
|
2884
|
+
if (event.type === "match") {
|
|
2885
|
+
matchCount++;
|
|
2886
|
+
const filePath = event.data?.path?.text;
|
|
2887
|
+
const lineNumber = event.data?.line_number;
|
|
2888
|
+
if (filePath != null && typeof lineNumber === "number") {
|
|
2889
|
+
matches.push({ filePath, lineNumber });
|
|
2890
|
+
}
|
|
2891
|
+
if (matchCount >= effectiveLimit) {
|
|
2892
|
+
matchLimitReached = true;
|
|
2893
|
+
stopChild(true);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
} catch {}
|
|
2897
|
+
});
|
|
2898
|
+
child.on("error", (error) => {
|
|
2899
|
+
cleanup();
|
|
2900
|
+
settle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`)));
|
|
2901
|
+
});
|
|
2902
|
+
child.on("close", async (code) => {
|
|
2903
|
+
cleanup();
|
|
2904
|
+
if (aborted) {
|
|
2905
|
+
settle(() => reject(new Error("Operation aborted")));
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
if (!killedDueToLimit && code !== 0 && code !== 1) {
|
|
2909
|
+
settle(() => reject(new Error(stderr.trim() || `ripgrep exited with code ${code}`)));
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
if (matchCount === 0) {
|
|
2913
|
+
settle(() => resolve({ content: [{ type: "text", text: "No matches found" }], details: undefined }));
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
for (const match of matches) {
|
|
2917
|
+
const block = await formatBlock(match.filePath, match.lineNumber);
|
|
2918
|
+
outputLines.push(...block);
|
|
2919
|
+
}
|
|
2920
|
+
const rawOutput = outputLines.join(`
|
|
2921
|
+
`);
|
|
2922
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
2923
|
+
let output = truncation.content;
|
|
2924
|
+
const details = {};
|
|
2925
|
+
const notices = [];
|
|
2926
|
+
if (matchLimitReached) {
|
|
2927
|
+
notices.push(`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`);
|
|
2928
|
+
details.matchLimitReached = effectiveLimit;
|
|
2929
|
+
}
|
|
2930
|
+
if (truncation.truncated) {
|
|
2931
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
2932
|
+
details.truncation = truncation;
|
|
2933
|
+
}
|
|
2934
|
+
if (linesTruncated) {
|
|
2935
|
+
notices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`);
|
|
2936
|
+
details.linesTruncated = true;
|
|
2937
|
+
}
|
|
2938
|
+
if (notices.length > 0)
|
|
2939
|
+
output += `
|
|
2940
|
+
|
|
2941
|
+
[${notices.join(". ")}]`;
|
|
2942
|
+
settle(() => resolve({
|
|
2943
|
+
content: [{ type: "text", text: output }],
|
|
2944
|
+
details: Object.keys(details).length > 0 ? details : undefined
|
|
2945
|
+
}));
|
|
2946
|
+
});
|
|
2947
|
+
} catch (err) {
|
|
2948
|
+
settle(() => reject(err));
|
|
2949
|
+
}
|
|
2950
|
+
})();
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
var grepTool = createGrepTool(process.cwd());
|
|
2956
|
+
|
|
2957
|
+
// src/tools/ls.ts
|
|
2958
|
+
import { Type as Type5 } from "@sinclair/typebox";
|
|
2959
|
+
import { existsSync as existsSync5, readdirSync, statSync as statSync2 } from "fs";
|
|
2960
|
+
import nodePath from "path";
|
|
2961
|
+
var lsSchema = Type5.Object({
|
|
2962
|
+
path: Type5.Optional(Type5.String({ description: "Directory to list (default: current directory)" })),
|
|
2963
|
+
limit: Type5.Optional(Type5.Number({ description: "Maximum number of entries to return (default: 500)" }))
|
|
2964
|
+
});
|
|
2965
|
+
var DEFAULT_LIMIT3 = 500;
|
|
2966
|
+
var defaultLsOperations = {
|
|
2967
|
+
exists: existsSync5,
|
|
2968
|
+
stat: statSync2,
|
|
2969
|
+
readdir: readdirSync
|
|
2970
|
+
};
|
|
2971
|
+
function createLsTool(cwd, options) {
|
|
2972
|
+
const ops = options?.operations ?? defaultLsOperations;
|
|
2973
|
+
return {
|
|
2974
|
+
name: "ls",
|
|
2975
|
+
label: "ls",
|
|
2976
|
+
description: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Output truncated to ${DEFAULT_LIMIT3} entries or ${DEFAULT_MAX_BYTES / 1024}KB.`,
|
|
2977
|
+
parameters: lsSchema,
|
|
2978
|
+
execute: async (_toolCallId, { path: dirPathArg, limit }, signal) => {
|
|
2979
|
+
return new Promise((resolve, reject) => {
|
|
2980
|
+
if (signal?.aborted) {
|
|
2981
|
+
reject(new Error("Operation aborted"));
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
const onAbort = () => reject(new Error("Operation aborted"));
|
|
2985
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2986
|
+
(async () => {
|
|
2987
|
+
try {
|
|
2988
|
+
const dirPath = resolveToCwd(dirPathArg || ".", cwd);
|
|
2989
|
+
const effectiveLimit = limit ?? DEFAULT_LIMIT3;
|
|
2990
|
+
if (!await ops.exists(dirPath)) {
|
|
2991
|
+
reject(new Error(`Path not found: ${dirPath}`));
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
const stat = await ops.stat(dirPath);
|
|
2995
|
+
if (!stat.isDirectory()) {
|
|
2996
|
+
reject(new Error(`Not a directory: ${dirPath}`));
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
let entries;
|
|
3000
|
+
try {
|
|
3001
|
+
entries = await ops.readdir(dirPath);
|
|
3002
|
+
} catch (e) {
|
|
3003
|
+
reject(new Error(`Cannot read directory: ${e instanceof Error ? e.message : String(e)}`));
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
3007
|
+
const results = [];
|
|
3008
|
+
let entryLimitReached = false;
|
|
3009
|
+
for (const entry of entries) {
|
|
3010
|
+
if (results.length >= effectiveLimit) {
|
|
3011
|
+
entryLimitReached = true;
|
|
3012
|
+
break;
|
|
3013
|
+
}
|
|
3014
|
+
const fullPath = nodePath.join(dirPath, entry);
|
|
3015
|
+
try {
|
|
3016
|
+
const entryStat = await ops.stat(fullPath);
|
|
3017
|
+
results.push(entry + (entryStat.isDirectory() ? "/" : ""));
|
|
3018
|
+
} catch {
|
|
3019
|
+
continue;
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3023
|
+
if (results.length === 0) {
|
|
3024
|
+
resolve({ content: [{ type: "text", text: "(empty directory)" }], details: undefined });
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
3027
|
+
const rawOutput = results.join(`
|
|
3028
|
+
`);
|
|
3029
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
3030
|
+
let output = truncation.content;
|
|
3031
|
+
const details = {};
|
|
3032
|
+
const notices = [];
|
|
3033
|
+
if (entryLimitReached) {
|
|
3034
|
+
notices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);
|
|
3035
|
+
details.entryLimitReached = effectiveLimit;
|
|
3036
|
+
}
|
|
3037
|
+
if (truncation.truncated) {
|
|
3038
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
3039
|
+
details.truncation = truncation;
|
|
3040
|
+
}
|
|
3041
|
+
if (notices.length > 0)
|
|
3042
|
+
output += `
|
|
3043
|
+
|
|
3044
|
+
[${notices.join(". ")}]`;
|
|
3045
|
+
resolve({
|
|
3046
|
+
content: [{ type: "text", text: output }],
|
|
3047
|
+
details: Object.keys(details).length > 0 ? details : undefined
|
|
3048
|
+
});
|
|
3049
|
+
} catch (e) {
|
|
3050
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3051
|
+
reject(e);
|
|
3052
|
+
}
|
|
3053
|
+
})();
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
};
|
|
3057
|
+
}
|
|
3058
|
+
var lsTool = createLsTool(process.cwd());
|
|
3059
|
+
|
|
3060
|
+
// src/tools/read.ts
|
|
3061
|
+
import { Type as Type6 } from "@sinclair/typebox";
|
|
3062
|
+
import { constants as constants3 } from "fs";
|
|
3063
|
+
import { access as fsAccess2, readFile as fsReadFile2 } from "fs/promises";
|
|
3064
|
+
var readSchema = Type6.Object({
|
|
3065
|
+
path: Type6.String({ description: "Path to the file to read (relative or absolute)" }),
|
|
3066
|
+
offset: Type6.Optional(Type6.Number({ description: "Line number to start reading from (1-indexed)" })),
|
|
3067
|
+
limit: Type6.Optional(Type6.Number({ description: "Maximum number of lines to read" }))
|
|
3068
|
+
});
|
|
3069
|
+
var defaultReadOperations = {
|
|
3070
|
+
readFile: (path3) => fsReadFile2(path3),
|
|
3071
|
+
access: (path3) => fsAccess2(path3, constants3.R_OK)
|
|
3072
|
+
};
|
|
3073
|
+
function createReadTool(cwd, options) {
|
|
3074
|
+
const ops = options?.operations ?? defaultReadOperations;
|
|
3075
|
+
return {
|
|
3076
|
+
name: "read",
|
|
3077
|
+
label: "read",
|
|
3078
|
+
description: `Read the contents of a text file. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,
|
|
3079
|
+
parameters: readSchema,
|
|
3080
|
+
execute: async (_toolCallId, { path: path3, offset, limit }, signal) => {
|
|
3081
|
+
const absolutePath = resolveReadPath(path3, cwd);
|
|
3082
|
+
return new Promise((resolve, reject) => {
|
|
3083
|
+
if (signal?.aborted) {
|
|
3084
|
+
reject(new Error("Operation aborted"));
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
let aborted = false;
|
|
3088
|
+
const onAbort = () => {
|
|
3089
|
+
aborted = true;
|
|
3090
|
+
reject(new Error("Operation aborted"));
|
|
3091
|
+
};
|
|
3092
|
+
if (signal) {
|
|
3093
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3094
|
+
}
|
|
3095
|
+
(async () => {
|
|
3096
|
+
try {
|
|
3097
|
+
await ops.access(absolutePath);
|
|
3098
|
+
if (aborted)
|
|
3099
|
+
return;
|
|
3100
|
+
const buffer = await ops.readFile(absolutePath);
|
|
3101
|
+
const textContent = buffer.toString("utf-8");
|
|
3102
|
+
const allLines = textContent.split(`
|
|
3103
|
+
`);
|
|
3104
|
+
const totalFileLines = allLines.length;
|
|
3105
|
+
const startLine = offset ? Math.max(0, offset - 1) : 0;
|
|
3106
|
+
const startLineDisplay = startLine + 1;
|
|
3107
|
+
if (startLine >= allLines.length) {
|
|
3108
|
+
throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
|
|
3109
|
+
}
|
|
3110
|
+
let selectedContent;
|
|
3111
|
+
let userLimitedLines;
|
|
3112
|
+
if (limit !== undefined) {
|
|
3113
|
+
const endLine = Math.min(startLine + limit, allLines.length);
|
|
3114
|
+
selectedContent = allLines.slice(startLine, endLine).join(`
|
|
3115
|
+
`);
|
|
3116
|
+
userLimitedLines = endLine - startLine;
|
|
3117
|
+
} else {
|
|
3118
|
+
selectedContent = allLines.slice(startLine).join(`
|
|
3119
|
+
`);
|
|
3120
|
+
}
|
|
3121
|
+
const truncation = truncateHead(selectedContent);
|
|
3122
|
+
let outputText;
|
|
3123
|
+
let details;
|
|
3124
|
+
if (truncation.firstLineExceedsLimit) {
|
|
3125
|
+
outputText = `[Line ${startLineDisplay} is ${formatSize(Buffer.byteLength(allLines[startLine], "utf-8"))}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path3} | head -c ${DEFAULT_MAX_BYTES}]`;
|
|
3126
|
+
details = { truncation };
|
|
3127
|
+
} else if (truncation.truncated) {
|
|
3128
|
+
const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
|
3129
|
+
const nextOffset = endLineDisplay + 1;
|
|
3130
|
+
outputText = truncation.content;
|
|
3131
|
+
if (truncation.truncatedBy === "lines") {
|
|
3132
|
+
outputText += `
|
|
3133
|
+
|
|
3134
|
+
[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;
|
|
3135
|
+
} else {
|
|
3136
|
+
outputText += `
|
|
3137
|
+
|
|
3138
|
+
[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`;
|
|
3139
|
+
}
|
|
3140
|
+
details = { truncation };
|
|
3141
|
+
} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
|
|
3142
|
+
const remaining = allLines.length - (startLine + userLimitedLines);
|
|
3143
|
+
const nextOffset = startLine + userLimitedLines + 1;
|
|
3144
|
+
outputText = truncation.content;
|
|
3145
|
+
outputText += `
|
|
3146
|
+
|
|
3147
|
+
[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;
|
|
3148
|
+
} else {
|
|
3149
|
+
outputText = truncation.content;
|
|
3150
|
+
}
|
|
3151
|
+
if (aborted)
|
|
3152
|
+
return;
|
|
3153
|
+
if (signal)
|
|
3154
|
+
signal.removeEventListener("abort", onAbort);
|
|
3155
|
+
resolve({ content: [{ type: "text", text: outputText }], details });
|
|
3156
|
+
} catch (error) {
|
|
3157
|
+
if (signal)
|
|
3158
|
+
signal.removeEventListener("abort", onAbort);
|
|
3159
|
+
if (!aborted)
|
|
3160
|
+
reject(error);
|
|
3161
|
+
}
|
|
3162
|
+
})();
|
|
3163
|
+
});
|
|
3164
|
+
}
|
|
3165
|
+
};
|
|
1852
3166
|
}
|
|
3167
|
+
var readTool = createReadTool(process.cwd());
|
|
1853
3168
|
|
|
1854
|
-
// src/
|
|
1855
|
-
import {
|
|
1856
|
-
|
|
1857
|
-
} from "
|
|
1858
|
-
|
|
3169
|
+
// src/tools/write.ts
|
|
3170
|
+
import { Type as Type7 } from "@sinclair/typebox";
|
|
3171
|
+
import { mkdir as fsMkdir, writeFile as fsWriteFile2 } from "fs/promises";
|
|
3172
|
+
import { dirname as dirname4 } from "path";
|
|
3173
|
+
var writeSchema = Type7.Object({
|
|
3174
|
+
path: Type7.String({ description: "Path to the file to write (relative or absolute)" }),
|
|
3175
|
+
content: Type7.String({ description: "Content to write to the file" })
|
|
3176
|
+
});
|
|
3177
|
+
var defaultWriteOperations = {
|
|
3178
|
+
writeFile: (path3, content) => fsWriteFile2(path3, content, "utf-8"),
|
|
3179
|
+
mkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => {})
|
|
3180
|
+
};
|
|
3181
|
+
function createWriteTool(cwd, options) {
|
|
3182
|
+
const ops = options?.operations ?? defaultWriteOperations;
|
|
3183
|
+
return {
|
|
3184
|
+
name: "write",
|
|
3185
|
+
label: "write",
|
|
3186
|
+
description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
|
|
3187
|
+
parameters: writeSchema,
|
|
3188
|
+
execute: async (_toolCallId, { path: path3, content }, signal) => {
|
|
3189
|
+
const absolutePath = resolveToCwd(path3, cwd);
|
|
3190
|
+
const dir = dirname4(absolutePath);
|
|
3191
|
+
return new Promise((resolve, reject) => {
|
|
3192
|
+
if (signal?.aborted) {
|
|
3193
|
+
reject(new Error("Operation aborted"));
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
let aborted = false;
|
|
3197
|
+
const onAbort = () => {
|
|
3198
|
+
aborted = true;
|
|
3199
|
+
reject(new Error("Operation aborted"));
|
|
3200
|
+
};
|
|
3201
|
+
if (signal)
|
|
3202
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3203
|
+
(async () => {
|
|
3204
|
+
try {
|
|
3205
|
+
await ops.mkdir(dir);
|
|
3206
|
+
if (aborted)
|
|
3207
|
+
return;
|
|
3208
|
+
await ops.writeFile(absolutePath, content);
|
|
3209
|
+
if (aborted)
|
|
3210
|
+
return;
|
|
3211
|
+
if (signal)
|
|
3212
|
+
signal.removeEventListener("abort", onAbort);
|
|
3213
|
+
resolve({
|
|
3214
|
+
content: [{ type: "text", text: `Successfully wrote ${content.length} bytes to ${path3}` }],
|
|
3215
|
+
details: undefined
|
|
3216
|
+
});
|
|
3217
|
+
} catch (error) {
|
|
3218
|
+
if (signal)
|
|
3219
|
+
signal.removeEventListener("abort", onAbort);
|
|
3220
|
+
if (!aborted)
|
|
3221
|
+
reject(error);
|
|
3222
|
+
}
|
|
3223
|
+
})();
|
|
3224
|
+
});
|
|
3225
|
+
}
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3228
|
+
var writeTool = createWriteTool(process.cwd());
|
|
3229
|
+
|
|
3230
|
+
// src/tools/index.ts
|
|
3231
|
+
function createCodingTools(cwd, options) {
|
|
3232
|
+
return [
|
|
3233
|
+
createReadTool(cwd, options?.read),
|
|
3234
|
+
createBashTool(cwd, options?.bash),
|
|
3235
|
+
createEditTool(cwd),
|
|
3236
|
+
createWriteTool(cwd)
|
|
3237
|
+
];
|
|
3238
|
+
}
|
|
1859
3239
|
|
|
1860
3240
|
// src/provider.ts
|
|
1861
3241
|
var PROVIDERS = [
|
|
@@ -2043,33 +3423,79 @@ var PROVIDERS = [
|
|
|
2043
3423
|
}
|
|
2044
3424
|
];
|
|
2045
3425
|
|
|
2046
|
-
// src/managers/
|
|
3426
|
+
// src/managers/pi-executor.ts
|
|
3427
|
+
var PROVIDER_NAME_TO_PI = {
|
|
3428
|
+
anthropic: "anthropic",
|
|
3429
|
+
openai: "openai",
|
|
3430
|
+
google: "google",
|
|
3431
|
+
groq: "groq",
|
|
3432
|
+
cerebras: "cerebras",
|
|
3433
|
+
xai: "xai",
|
|
3434
|
+
openrouter: "openrouter",
|
|
3435
|
+
"github copilot": "github-copilot",
|
|
3436
|
+
minimax: "minimax",
|
|
3437
|
+
"kimi coding plan": "kimi-coding",
|
|
3438
|
+
"hugging face": "huggingface",
|
|
3439
|
+
codex: "openai-codex",
|
|
3440
|
+
mistral: "mistral"
|
|
3441
|
+
};
|
|
3442
|
+
function determineApiType(apiUrl) {
|
|
3443
|
+
if (apiUrl.includes("/anthropic/"))
|
|
3444
|
+
return "anthropic-messages";
|
|
3445
|
+
if (apiUrl.includes("api.anthropic.com"))
|
|
3446
|
+
return "anthropic-messages";
|
|
3447
|
+
return "openai-completions";
|
|
3448
|
+
}
|
|
3449
|
+
function resolveModel(providerName, modelId, providerConfig) {
|
|
3450
|
+
const piProvider = PROVIDER_NAME_TO_PI[providerName.toLowerCase()];
|
|
3451
|
+
if (piProvider) {
|
|
3452
|
+
try {
|
|
3453
|
+
return getModel(piProvider, modelId);
|
|
3454
|
+
} catch {}
|
|
3455
|
+
}
|
|
3456
|
+
const baseUrl = providerConfig.api;
|
|
3457
|
+
if (!baseUrl) {
|
|
3458
|
+
throw new Error(`No API URL configured for provider: ${providerName}`);
|
|
3459
|
+
}
|
|
3460
|
+
const apiType = determineApiType(baseUrl);
|
|
3461
|
+
return {
|
|
3462
|
+
id: modelId,
|
|
3463
|
+
name: modelId,
|
|
3464
|
+
api: apiType,
|
|
3465
|
+
provider: providerName.toLowerCase(),
|
|
3466
|
+
baseUrl,
|
|
3467
|
+
reasoning: false,
|
|
3468
|
+
input: ["text"],
|
|
3469
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
3470
|
+
contextWindow: 128000,
|
|
3471
|
+
maxTokens: 16384
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
function extractTextFromAssistantMessage(msg) {
|
|
3475
|
+
return msg.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
3476
|
+
}
|
|
2047
3477
|
var getErrorCode = (error) => {
|
|
2048
3478
|
if (error && typeof error === "object" && "code" in error) {
|
|
2049
3479
|
const code = error.code;
|
|
2050
|
-
if (typeof code === "string")
|
|
3480
|
+
if (typeof code === "string")
|
|
2051
3481
|
return code;
|
|
2052
|
-
}
|
|
2053
3482
|
}
|
|
2054
3483
|
return;
|
|
2055
3484
|
};
|
|
2056
3485
|
|
|
2057
|
-
class
|
|
3486
|
+
class PiExecutorImpl {
|
|
2058
3487
|
metadataManager;
|
|
2059
3488
|
constructor(metadataManager) {
|
|
2060
3489
|
this.metadataManager = metadataManager;
|
|
2061
3490
|
}
|
|
2062
3491
|
async* execute(userPrompt, context) {
|
|
2063
|
-
let session = null;
|
|
2064
3492
|
try {
|
|
2065
3493
|
const providerName = context.provider;
|
|
2066
3494
|
if (!providerName) {
|
|
2067
|
-
console.error(`[ai] No provider specified in context`);
|
|
2068
3495
|
throw new Error("No provider specified in execution context");
|
|
2069
3496
|
}
|
|
2070
3497
|
let model = context.model?.trim();
|
|
2071
3498
|
if (!model) {
|
|
2072
|
-
console.warn("[ai] No model provided in context, using default");
|
|
2073
3499
|
throw new Error("No model specified in execution context");
|
|
2074
3500
|
}
|
|
2075
3501
|
model = model.replace(`${providerName.toLowerCase()}/`, "");
|
|
@@ -2095,59 +3521,126 @@ class NeovateExecutorImpl {
|
|
|
2095
3521
|
throw new Error(`No API key found for provider: ${providerName}. ` + `Set ${providerConfig.keyName || "an API key"} in environment variables or configuration.`);
|
|
2096
3522
|
}
|
|
2097
3523
|
console.log(`[ai] API key source for ${providerName}: ${apiKeySource}`);
|
|
2098
|
-
const
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
console.log("[ai] Sending prompt to session...");
|
|
2124
|
-
await session.send(userPrompt);
|
|
2125
|
-
console.log("[ai] Prompt sent, waiting for responses...");
|
|
2126
|
-
let lastMessageContent = null;
|
|
2127
|
-
for await (const msg of session.receive()) {
|
|
2128
|
-
const contentLength = typeof msg?.content === "string" ? msg.content.length : undefined;
|
|
2129
|
-
console.log("[ai] Received msg:", { type: msg?.type, contentLength });
|
|
2130
|
-
console.log("[ai] Msg:", msg);
|
|
2131
|
-
if (msg?.type === "message") {
|
|
2132
|
-
const messageContent = typeof msg.text === "string" ? msg.text : typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
2133
|
-
lastMessageContent = messageContent;
|
|
2134
|
-
yield {
|
|
3524
|
+
const resolvedModel = resolveModel(providerName, model, providerConfig);
|
|
3525
|
+
console.log("[ai] Resolved model:", {
|
|
3526
|
+
id: resolvedModel.id,
|
|
3527
|
+
api: resolvedModel.api,
|
|
3528
|
+
provider: resolvedModel.provider
|
|
3529
|
+
});
|
|
3530
|
+
const tools = createCodingTools(cwd);
|
|
3531
|
+
const agent = new Agent({
|
|
3532
|
+
initialState: {
|
|
3533
|
+
systemPrompt: "You are a helpful coding assistant. You have access to read, bash, edit, and write tools. Use them to explore and modify the codebase as needed.",
|
|
3534
|
+
model: resolvedModel,
|
|
3535
|
+
tools
|
|
3536
|
+
},
|
|
3537
|
+
getApiKey: async () => apiKey
|
|
3538
|
+
});
|
|
3539
|
+
const queue = [];
|
|
3540
|
+
let resolver = null;
|
|
3541
|
+
let done = false;
|
|
3542
|
+
let finalContent = "";
|
|
3543
|
+
let errorOccurred = false;
|
|
3544
|
+
const unsubscribe = agent.subscribe((event) => {
|
|
3545
|
+
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
|
3546
|
+
const delta = event.assistantMessageEvent.delta;
|
|
3547
|
+
finalContent += delta;
|
|
3548
|
+
queue.push({
|
|
2135
3549
|
type: "message",
|
|
2136
|
-
role:
|
|
2137
|
-
content:
|
|
2138
|
-
};
|
|
2139
|
-
} else if (
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
3550
|
+
role: "assistant",
|
|
3551
|
+
content: delta
|
|
3552
|
+
});
|
|
3553
|
+
} else if (event.type === "tool_execution_start") {
|
|
3554
|
+
queue.push({
|
|
3555
|
+
type: "message",
|
|
3556
|
+
role: "tool",
|
|
3557
|
+
content: JSON.stringify([
|
|
3558
|
+
{
|
|
3559
|
+
type: "tool_use",
|
|
3560
|
+
name: event.toolName,
|
|
3561
|
+
id: event.toolCallId,
|
|
3562
|
+
input: event.args
|
|
3563
|
+
}
|
|
3564
|
+
])
|
|
3565
|
+
});
|
|
3566
|
+
} else if (event.type === "tool_execution_end") {
|
|
3567
|
+
const resultContent = event.result?.content?.filter((b) => b.type === "text").map((b) => b.text || "").join("") || JSON.stringify(event.result);
|
|
3568
|
+
queue.push({
|
|
3569
|
+
type: "message",
|
|
3570
|
+
role: "tool",
|
|
3571
|
+
content: JSON.stringify([
|
|
3572
|
+
{
|
|
3573
|
+
type: "tool-result",
|
|
3574
|
+
toolCallId: event.toolCallId,
|
|
3575
|
+
toolName: event.toolName,
|
|
3576
|
+
content: resultContent,
|
|
3577
|
+
isError: event.isError
|
|
3578
|
+
}
|
|
3579
|
+
])
|
|
3580
|
+
});
|
|
3581
|
+
} else if (event.type === "agent_end") {
|
|
3582
|
+
const messages = event.messages;
|
|
3583
|
+
const lastAssistant = [...messages].reverse().find((m) => ("role" in m) && m.role === "assistant");
|
|
3584
|
+
if (lastAssistant) {
|
|
3585
|
+
const extracted = extractTextFromAssistantMessage(lastAssistant);
|
|
3586
|
+
if (extracted && extracted !== finalContent) {
|
|
3587
|
+
finalContent = extracted;
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
if (resolver) {
|
|
3592
|
+
resolver();
|
|
3593
|
+
resolver = null;
|
|
3594
|
+
}
|
|
3595
|
+
});
|
|
3596
|
+
const promptDone = agent.prompt(userPrompt).then(() => {
|
|
3597
|
+
done = true;
|
|
3598
|
+
if (resolver) {
|
|
3599
|
+
resolver();
|
|
3600
|
+
resolver = null;
|
|
3601
|
+
}
|
|
3602
|
+
}).catch((err) => {
|
|
3603
|
+
if (!errorOccurred) {
|
|
3604
|
+
errorOccurred = true;
|
|
3605
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
3606
|
+
queue.push({
|
|
3607
|
+
type: "error",
|
|
3608
|
+
content: errMessage,
|
|
3609
|
+
error: {
|
|
3610
|
+
code: getErrorCode(err) || "EXECUTION_ERROR",
|
|
3611
|
+
message: errMessage,
|
|
3612
|
+
details: {
|
|
3613
|
+
stack: err instanceof Error ? err.stack : undefined
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
});
|
|
3617
|
+
}
|
|
3618
|
+
done = true;
|
|
3619
|
+
if (resolver) {
|
|
3620
|
+
resolver();
|
|
3621
|
+
resolver = null;
|
|
3622
|
+
}
|
|
3623
|
+
});
|
|
3624
|
+
try {
|
|
3625
|
+
while (!done || queue.length > 0) {
|
|
3626
|
+
if (queue.length > 0) {
|
|
3627
|
+
yield queue.shift();
|
|
2146
3628
|
} else {
|
|
2147
|
-
|
|
3629
|
+
await new Promise((r) => {
|
|
3630
|
+
resolver = r;
|
|
3631
|
+
});
|
|
2148
3632
|
}
|
|
2149
|
-
break;
|
|
2150
3633
|
}
|
|
3634
|
+
await promptDone;
|
|
3635
|
+
if (!errorOccurred && finalContent) {
|
|
3636
|
+
yield {
|
|
3637
|
+
type: "result",
|
|
3638
|
+
content: finalContent
|
|
3639
|
+
};
|
|
3640
|
+
}
|
|
3641
|
+
} finally {
|
|
3642
|
+
unsubscribe();
|
|
3643
|
+
agent.abort();
|
|
2151
3644
|
}
|
|
2152
3645
|
} catch (error) {
|
|
2153
3646
|
console.error("[ai] Error during execution:", error);
|
|
@@ -2178,22 +3671,13 @@ class NeovateExecutorImpl {
|
|
|
2178
3671
|
details: { stack: errStack, originalError: errDetails }
|
|
2179
3672
|
}
|
|
2180
3673
|
};
|
|
2181
|
-
} finally {
|
|
2182
|
-
if (session) {
|
|
2183
|
-
try {
|
|
2184
|
-
await session.close?.();
|
|
2185
|
-
console.log("[ai] Session closed");
|
|
2186
|
-
} catch (closeError) {
|
|
2187
|
-
console.error("[ai] Error closing session:", closeError);
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
2190
3674
|
}
|
|
2191
3675
|
}
|
|
2192
3676
|
}
|
|
2193
3677
|
|
|
2194
3678
|
// src/managers/conversation-manager.ts
|
|
2195
3679
|
import { promises as fs2 } from "fs";
|
|
2196
|
-
import { join as
|
|
3680
|
+
import { join as join7, dirname as dirname5 } from "path";
|
|
2197
3681
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
2198
3682
|
var CONVERSATION_FILE = "conversation-history.json";
|
|
2199
3683
|
|
|
@@ -2267,13 +3751,13 @@ class ConversationManagerImpl {
|
|
|
2267
3751
|
}
|
|
2268
3752
|
async saveConversationHistory(threadPath, history) {
|
|
2269
3753
|
const filePath = this.getConversationFilePath(threadPath);
|
|
2270
|
-
await fs2.mkdir(
|
|
3754
|
+
await fs2.mkdir(dirname5(filePath), { recursive: true });
|
|
2271
3755
|
await fs2.writeFile(filePath, JSON.stringify(history, null, 2), "utf-8");
|
|
2272
3756
|
}
|
|
2273
3757
|
getConversationFilePath(threadPath) {
|
|
2274
3758
|
const threadId = threadPath.split(/[\\/]/).pop() || "unknown";
|
|
2275
|
-
const conversationsDir =
|
|
2276
|
-
return
|
|
3759
|
+
const conversationsDir = join7(this.metadataDir, "conversations");
|
|
3760
|
+
return join7(conversationsDir, threadId, CONVERSATION_FILE);
|
|
2277
3761
|
}
|
|
2278
3762
|
}
|
|
2279
3763
|
|
|
@@ -2961,7 +4445,7 @@ function delay(ms) {
|
|
|
2961
4445
|
}
|
|
2962
4446
|
|
|
2963
4447
|
// src/routes/chat.ts
|
|
2964
|
-
function createChatRoutes(threadManager,
|
|
4448
|
+
function createChatRoutes(threadManager, agentExecutor, conversationManager, processingStateManager) {
|
|
2965
4449
|
const router = new Hono3;
|
|
2966
4450
|
router.post("/", async (c) => {
|
|
2967
4451
|
try {
|
|
@@ -3061,7 +4545,7 @@ User: ${content}` : content;
|
|
|
3061
4545
|
const capturedEvents = [];
|
|
3062
4546
|
let fullContent = "";
|
|
3063
4547
|
try {
|
|
3064
|
-
for await (const event of
|
|
4548
|
+
for await (const event of agentExecutor.execute(promptWithContext, context)) {
|
|
3065
4549
|
capturedEvents.push(event);
|
|
3066
4550
|
if (event.type === "message" && event.content) {
|
|
3067
4551
|
fullContent += event.content;
|
|
@@ -3278,9 +4762,9 @@ async function getAIHubMixCredits(apiKey) {
|
|
|
3278
4762
|
|
|
3279
4763
|
// src/utils/env-manager.ts
|
|
3280
4764
|
import { promises as fs3 } from "fs";
|
|
3281
|
-
import { join as
|
|
4765
|
+
import { join as join8 } from "path";
|
|
3282
4766
|
async function updateEnvFile(keyNames) {
|
|
3283
|
-
const envPath =
|
|
4767
|
+
const envPath = join8(process.cwd(), ".env");
|
|
3284
4768
|
let content = "";
|
|
3285
4769
|
try {
|
|
3286
4770
|
content = await fs3.readFile(envPath, "utf-8");
|
|
@@ -3311,7 +4795,7 @@ async function updateEnvFile(keyNames) {
|
|
|
3311
4795
|
`, "utf-8");
|
|
3312
4796
|
}
|
|
3313
4797
|
async function readEnvFile() {
|
|
3314
|
-
const envPath =
|
|
4798
|
+
const envPath = join8(process.cwd(), ".env");
|
|
3315
4799
|
const envMap = {};
|
|
3316
4800
|
try {
|
|
3317
4801
|
const content = await fs3.readFile(envPath, "utf-8");
|
|
@@ -4097,9 +5581,106 @@ function createModelRoutes(metadataManager) {
|
|
|
4097
5581
|
|
|
4098
5582
|
// src/routes/git.ts
|
|
4099
5583
|
import { Hono as Hono6 } from "hono";
|
|
4100
|
-
import { spawn as
|
|
4101
|
-
import {
|
|
4102
|
-
import {
|
|
5584
|
+
import { spawn as spawn7 } from "child_process";
|
|
5585
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
|
|
5586
|
+
import { join as join10 } from "path";
|
|
5587
|
+
import { isAbsolute as isAbsolute2, normalize, resolve as resolve2 } from "path";
|
|
5588
|
+
|
|
5589
|
+
// src/paths.ts
|
|
5590
|
+
import { join as join9 } from "path";
|
|
5591
|
+
import { homedir as homedir2 } from "os";
|
|
5592
|
+
var APP_SUPPORT_DIR = join9(homedir2(), "Library", "Application Support", "Tarsk");
|
|
5593
|
+
var DATA_DIR = join9(APP_SUPPORT_DIR, "data");
|
|
5594
|
+
function getDataDir() {
|
|
5595
|
+
return DATA_DIR;
|
|
5596
|
+
}
|
|
5597
|
+
|
|
5598
|
+
// src/routes/git.ts
|
|
5599
|
+
import {
|
|
5600
|
+
completeSimple,
|
|
5601
|
+
getModel as getModel2
|
|
5602
|
+
} from "@mariozechner/pi-ai";
|
|
5603
|
+
var PROVIDER_NAME_TO_PI2 = {
|
|
5604
|
+
anthropic: "anthropic",
|
|
5605
|
+
openai: "openai",
|
|
5606
|
+
google: "google",
|
|
5607
|
+
groq: "groq",
|
|
5608
|
+
cerebras: "cerebras",
|
|
5609
|
+
xai: "xai",
|
|
5610
|
+
openrouter: "openrouter",
|
|
5611
|
+
"github copilot": "github-copilot",
|
|
5612
|
+
minimax: "minimax",
|
|
5613
|
+
"kimi coding plan": "kimi-coding",
|
|
5614
|
+
"hugging face": "huggingface",
|
|
5615
|
+
codex: "openai-codex",
|
|
5616
|
+
mistral: "mistral"
|
|
5617
|
+
};
|
|
5618
|
+
function resolveModelForGit(providerName, modelId, apiUrl) {
|
|
5619
|
+
const piProvider = PROVIDER_NAME_TO_PI2[providerName.toLowerCase()];
|
|
5620
|
+
if (piProvider) {
|
|
5621
|
+
try {
|
|
5622
|
+
return getModel2(piProvider, modelId);
|
|
5623
|
+
} catch {}
|
|
5624
|
+
}
|
|
5625
|
+
if (!apiUrl)
|
|
5626
|
+
throw new Error(`No API URL configured for provider: ${providerName}`);
|
|
5627
|
+
const apiType = apiUrl.includes("/anthropic/") || apiUrl.includes("api.anthropic.com") ? "anthropic-messages" : "openai-completions";
|
|
5628
|
+
return {
|
|
5629
|
+
id: modelId,
|
|
5630
|
+
name: modelId,
|
|
5631
|
+
api: apiType,
|
|
5632
|
+
provider: providerName.toLowerCase(),
|
|
5633
|
+
baseUrl: apiUrl,
|
|
5634
|
+
reasoning: false,
|
|
5635
|
+
input: ["text"],
|
|
5636
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
5637
|
+
contextWindow: 128000,
|
|
5638
|
+
maxTokens: 16384
|
|
5639
|
+
};
|
|
5640
|
+
}
|
|
5641
|
+
function resolveProviderAndKey(metadataProviderKeys, preferredProvider) {
|
|
5642
|
+
if (preferredProvider) {
|
|
5643
|
+
const prov = PROVIDERS.find((p) => p.name.toLowerCase() === preferredProvider.toLowerCase());
|
|
5644
|
+
if (prov) {
|
|
5645
|
+
const envKey = prov.keyName ? process.env[prov.keyName] : undefined;
|
|
5646
|
+
const key = envKey || metadataProviderKeys[prov.name];
|
|
5647
|
+
if (key)
|
|
5648
|
+
return { provider: prov, apiKey: key };
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
const providerPriority = ["Anthropic", "OpenAI", "OpenRouter", "Groq", "DeepSeek"];
|
|
5652
|
+
for (const name of providerPriority) {
|
|
5653
|
+
const prov = PROVIDERS.find((p) => p.name === name);
|
|
5654
|
+
if (prov?.keyName) {
|
|
5655
|
+
const envKey = process.env[prov.keyName];
|
|
5656
|
+
if (envKey)
|
|
5657
|
+
return { provider: prov, apiKey: envKey };
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
for (const name of providerPriority) {
|
|
5661
|
+
const prov = PROVIDERS.find((p) => p.name === name);
|
|
5662
|
+
if (prov && metadataProviderKeys[prov.name]) {
|
|
5663
|
+
return { provider: prov, apiKey: metadataProviderKeys[prov.name] };
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
for (const prov of PROVIDERS) {
|
|
5667
|
+
if (prov.api && prov.keyName) {
|
|
5668
|
+
const envKey = process.env[prov.keyName];
|
|
5669
|
+
if (envKey)
|
|
5670
|
+
return { provider: prov, apiKey: envKey };
|
|
5671
|
+
if (metadataProviderKeys[prov.name])
|
|
5672
|
+
return { provider: prov, apiKey: metadataProviderKeys[prov.name] };
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5675
|
+
return null;
|
|
5676
|
+
}
|
|
5677
|
+
var DEFAULT_MODEL_MAP = {
|
|
5678
|
+
Anthropic: "claude-3-5-sonnet-20241022",
|
|
5679
|
+
OpenAI: "gpt-4o-mini",
|
|
5680
|
+
OpenRouter: "anthropic/claude-3.5-sonnet",
|
|
5681
|
+
Groq: "llama-3.3-70b-versatile",
|
|
5682
|
+
DeepSeek: "deepseek-chat"
|
|
5683
|
+
};
|
|
4103
5684
|
async function generateCommitMessageWithAI(diff, metadataManager, model, provider) {
|
|
4104
5685
|
const truncatedDiff = diff.length > 3000 ? diff.substring(0, 3000) + `
|
|
4105
5686
|
...(truncated)` : diff;
|
|
@@ -4114,126 +5695,61 @@ Git diff:
|
|
|
4114
5695
|
${truncatedDiff}
|
|
4115
5696
|
|
|
4116
5697
|
Generate only the commit message, nothing else:`;
|
|
4117
|
-
let session = null;
|
|
4118
5698
|
try {
|
|
4119
5699
|
const providerKeys = await metadataManager.getProviderKeys();
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
let selectedModel = model;
|
|
4123
|
-
if (provider) {
|
|
4124
|
-
selectedProvider = PROVIDERS.find((p) => p.name.toLowerCase() === provider.toLowerCase()) || null;
|
|
4125
|
-
if (selectedProvider) {
|
|
4126
|
-
if (selectedProvider.keyName) {
|
|
4127
|
-
apiKey = process.env[selectedProvider.keyName] || null;
|
|
4128
|
-
}
|
|
4129
|
-
if (!apiKey) {
|
|
4130
|
-
apiKey = providerKeys[selectedProvider.name] || null;
|
|
4131
|
-
}
|
|
4132
|
-
}
|
|
4133
|
-
}
|
|
4134
|
-
if (!selectedProvider || !apiKey) {
|
|
4135
|
-
const providerPriority = ["Anthropic", "OpenAI", "OpenRouter", "Groq", "DeepSeek"];
|
|
4136
|
-
for (const providerName of providerPriority) {
|
|
4137
|
-
const prov = PROVIDERS.find((p) => p.name === providerName);
|
|
4138
|
-
if (prov?.keyName) {
|
|
4139
|
-
const envKey = process.env[prov.keyName];
|
|
4140
|
-
if (envKey) {
|
|
4141
|
-
selectedProvider = prov;
|
|
4142
|
-
apiKey = envKey;
|
|
4143
|
-
break;
|
|
4144
|
-
}
|
|
4145
|
-
}
|
|
4146
|
-
}
|
|
4147
|
-
if (!selectedProvider || !apiKey) {
|
|
4148
|
-
for (const providerName of providerPriority) {
|
|
4149
|
-
const prov = PROVIDERS.find((p) => p.name === providerName);
|
|
4150
|
-
if (prov && providerKeys[prov.name]) {
|
|
4151
|
-
selectedProvider = prov;
|
|
4152
|
-
apiKey = providerKeys[prov.name];
|
|
4153
|
-
break;
|
|
4154
|
-
}
|
|
4155
|
-
}
|
|
4156
|
-
}
|
|
4157
|
-
if (!selectedProvider || !apiKey) {
|
|
4158
|
-
for (const prov of PROVIDERS) {
|
|
4159
|
-
if (prov.api && prov.keyName) {
|
|
4160
|
-
const envKey = process.env[prov.keyName];
|
|
4161
|
-
if (envKey) {
|
|
4162
|
-
selectedProvider = prov;
|
|
4163
|
-
apiKey = envKey;
|
|
4164
|
-
break;
|
|
4165
|
-
}
|
|
4166
|
-
if (providerKeys[prov.name]) {
|
|
4167
|
-
selectedProvider = prov;
|
|
4168
|
-
apiKey = providerKeys[prov.name];
|
|
4169
|
-
break;
|
|
4170
|
-
}
|
|
4171
|
-
}
|
|
4172
|
-
}
|
|
4173
|
-
}
|
|
4174
|
-
if (!selectedModel) {
|
|
4175
|
-
const modelMap = {
|
|
4176
|
-
Anthropic: "claude-3-5-sonnet-20241022",
|
|
4177
|
-
OpenAI: "gpt-4o-mini",
|
|
4178
|
-
OpenRouter: "anthropic/claude-3.5-sonnet",
|
|
4179
|
-
Groq: "llama-3.3-70b-versatile",
|
|
4180
|
-
DeepSeek: "deepseek-chat"
|
|
4181
|
-
};
|
|
4182
|
-
selectedModel = modelMap[selectedProvider?.name || ""] || "default";
|
|
4183
|
-
}
|
|
4184
|
-
}
|
|
4185
|
-
if (!selectedProvider || !apiKey || !selectedProvider.api) {
|
|
5700
|
+
const resolved = resolveProviderAndKey(providerKeys, provider);
|
|
5701
|
+
if (!resolved || !resolved.provider.api) {
|
|
4186
5702
|
throw new Error("No AI provider configured. Please configure an API key in settings.");
|
|
4187
5703
|
}
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
tarsk: {
|
|
4194
|
-
api: selectedProvider.api,
|
|
4195
|
-
options: { apiKey },
|
|
4196
|
-
models: { [finalModel]: finalModel }
|
|
4197
|
-
}
|
|
4198
|
-
};
|
|
4199
|
-
const sessionConfig = {
|
|
4200
|
-
model: `tarsk/${finalModel}`,
|
|
4201
|
-
cwd: process.cwd(),
|
|
4202
|
-
productName: "Tarsk.io",
|
|
4203
|
-
providers
|
|
4204
|
-
};
|
|
4205
|
-
session = await createSession2(sessionConfig);
|
|
4206
|
-
await session.send(prompt);
|
|
4207
|
-
let commitMessage = "";
|
|
4208
|
-
for await (const msg of session.receive()) {
|
|
4209
|
-
if (msg?.type === "message" || msg?.type === "result") {
|
|
4210
|
-
const content = typeof msg.text === "string" ? msg.text : typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
4211
|
-
commitMessage = content.trim();
|
|
4212
|
-
if (msg?.type === "result")
|
|
4213
|
-
break;
|
|
4214
|
-
}
|
|
5704
|
+
let selectedModel = model;
|
|
5705
|
+
if (selectedModel) {
|
|
5706
|
+
selectedModel = selectedModel.replace(`${resolved.provider.name.toLowerCase()}/`, "");
|
|
5707
|
+
} else {
|
|
5708
|
+
selectedModel = DEFAULT_MODEL_MAP[resolved.provider.name] || "default";
|
|
4215
5709
|
}
|
|
5710
|
+
const resolvedModel = resolveModelForGit(resolved.provider.name, selectedModel, resolved.provider.api);
|
|
5711
|
+
const response = await completeSimple(resolvedModel, {
|
|
5712
|
+
messages: [{ role: "user", content: prompt }]
|
|
5713
|
+
}, { apiKey: resolved.apiKey });
|
|
5714
|
+
let commitMessage = response.content.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
|
|
4216
5715
|
commitMessage = commitMessage.replace(/^["']|["']$/g, "").trim();
|
|
4217
5716
|
return commitMessage || "Update files";
|
|
4218
5717
|
} catch (error) {
|
|
4219
5718
|
console.error("Failed to generate AI commit message:", error);
|
|
4220
5719
|
return "Update files";
|
|
4221
|
-
} finally {
|
|
4222
|
-
if (session) {
|
|
4223
|
-
try {
|
|
4224
|
-
await session.close?.();
|
|
4225
|
-
} catch (closeError) {
|
|
4226
|
-
console.error("Error closing session:", closeError);
|
|
4227
|
-
}
|
|
4228
|
-
}
|
|
4229
5720
|
}
|
|
4230
5721
|
}
|
|
5722
|
+
function resolveThreadPath(repoPath) {
|
|
5723
|
+
const base = isAbsolute2(repoPath) ? repoPath : resolve2(getDataDir(), repoPath);
|
|
5724
|
+
return normalize(base);
|
|
5725
|
+
}
|
|
5726
|
+
function getGitRoot(cwd) {
|
|
5727
|
+
return new Promise((resolveRoot, reject) => {
|
|
5728
|
+
const proc = spawn7("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
5729
|
+
let out = "";
|
|
5730
|
+
let err = "";
|
|
5731
|
+
proc.stdout.on("data", (d) => {
|
|
5732
|
+
out += d.toString();
|
|
5733
|
+
});
|
|
5734
|
+
proc.stderr.on("data", (d) => {
|
|
5735
|
+
err += d.toString();
|
|
5736
|
+
});
|
|
5737
|
+
proc.on("close", (code) => {
|
|
5738
|
+
if (code === 0) {
|
|
5739
|
+
resolveRoot(out.trim());
|
|
5740
|
+
} else {
|
|
5741
|
+
reject(new Error(err.trim() || "Not a git repository"));
|
|
5742
|
+
}
|
|
5743
|
+
});
|
|
5744
|
+
proc.on("error", reject);
|
|
5745
|
+
});
|
|
5746
|
+
}
|
|
4231
5747
|
function createGitRoutes(metadataManager) {
|
|
4232
5748
|
const router = new Hono6;
|
|
4233
5749
|
router.get("/username", async (c) => {
|
|
4234
5750
|
try {
|
|
4235
5751
|
const name = await new Promise((resolve3, reject) => {
|
|
4236
|
-
const proc =
|
|
5752
|
+
const proc = spawn7("git", ["config", "user.name"]);
|
|
4237
5753
|
let out = "";
|
|
4238
5754
|
let err = "";
|
|
4239
5755
|
proc.stdout.on("data", (d) => {
|
|
@@ -4267,9 +5783,23 @@ function createGitRoutes(metadataManager) {
|
|
|
4267
5783
|
if (!repoPath) {
|
|
4268
5784
|
return c.json({ error: "Thread path not found" }, 404);
|
|
4269
5785
|
}
|
|
4270
|
-
const absolutePath =
|
|
5786
|
+
const absolutePath = resolveThreadPath(repoPath);
|
|
5787
|
+
if (!existsSync6(absolutePath)) {
|
|
5788
|
+
return c.json({
|
|
5789
|
+
error: `Thread repo path does not exist: ${absolutePath}. Check that the project folder is present.`
|
|
5790
|
+
}, 400);
|
|
5791
|
+
}
|
|
5792
|
+
let gitRoot;
|
|
5793
|
+
try {
|
|
5794
|
+
gitRoot = await getGitRoot(absolutePath);
|
|
5795
|
+
} catch (e) {
|
|
5796
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
5797
|
+
return c.json({
|
|
5798
|
+
error: `Path is not a git repository: ${absolutePath}. ${msg}`
|
|
5799
|
+
}, 400);
|
|
5800
|
+
}
|
|
4271
5801
|
const { hasChanges, changedFilesCount } = await new Promise((resolve3) => {
|
|
4272
|
-
const proc =
|
|
5802
|
+
const proc = spawn7("git", ["status", "--porcelain"], { cwd: gitRoot });
|
|
4273
5803
|
let out = "";
|
|
4274
5804
|
proc.stdout.on("data", (d) => {
|
|
4275
5805
|
out += d.toString();
|
|
@@ -4285,7 +5815,7 @@ function createGitRoutes(metadataManager) {
|
|
|
4285
5815
|
proc.on("error", () => resolve3({ hasChanges: false, changedFilesCount: 0 }));
|
|
4286
5816
|
});
|
|
4287
5817
|
const currentBranch = await new Promise((resolve3) => {
|
|
4288
|
-
const proc =
|
|
5818
|
+
const proc = spawn7("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: gitRoot });
|
|
4289
5819
|
let out = "";
|
|
4290
5820
|
proc.stdout.on("data", (d) => {
|
|
4291
5821
|
out += d.toString();
|
|
@@ -4296,7 +5826,7 @@ function createGitRoutes(metadataManager) {
|
|
|
4296
5826
|
proc.on("error", () => resolve3(""));
|
|
4297
5827
|
});
|
|
4298
5828
|
const hasUnpushedCommits = await new Promise((resolve3) => {
|
|
4299
|
-
const proc =
|
|
5829
|
+
const proc = spawn7("git", ["log", `origin/${currentBranch}..HEAD`, "--oneline"], { cwd: gitRoot });
|
|
4300
5830
|
let out = "";
|
|
4301
5831
|
proc.stdout.on("data", (d) => {
|
|
4302
5832
|
out += d.toString();
|
|
@@ -4327,9 +5857,15 @@ function createGitRoutes(metadataManager) {
|
|
|
4327
5857
|
if (!repoPath) {
|
|
4328
5858
|
return c.json({ error: "Thread path not found" }, 404);
|
|
4329
5859
|
}
|
|
4330
|
-
const absolutePath =
|
|
5860
|
+
const absolutePath = resolveThreadPath(repoPath);
|
|
5861
|
+
let gitRoot;
|
|
5862
|
+
try {
|
|
5863
|
+
gitRoot = await getGitRoot(absolutePath);
|
|
5864
|
+
} catch {
|
|
5865
|
+
return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
|
|
5866
|
+
}
|
|
4331
5867
|
const statusOutput = await new Promise((resolve3) => {
|
|
4332
|
-
const proc =
|
|
5868
|
+
const proc = spawn7("git", ["status", "--porcelain"], { cwd: gitRoot });
|
|
4333
5869
|
let out = "";
|
|
4334
5870
|
proc.stdout.on("data", (d) => {
|
|
4335
5871
|
out += d.toString();
|
|
@@ -4370,7 +5906,7 @@ function createGitRoutes(metadataManager) {
|
|
|
4370
5906
|
const hunks = [];
|
|
4371
5907
|
if (file.status === "deleted") {
|
|
4372
5908
|
const content = await new Promise((resolve3) => {
|
|
4373
|
-
const proc =
|
|
5909
|
+
const proc = spawn7("git", ["show", `HEAD:${file.path}`], { cwd: gitRoot });
|
|
4374
5910
|
let out = "";
|
|
4375
5911
|
proc.stdout.on("data", (d) => {
|
|
4376
5912
|
out += d.toString();
|
|
@@ -4382,13 +5918,13 @@ function createGitRoutes(metadataManager) {
|
|
|
4382
5918
|
} else if (file.status === "added" && file.path.startsWith("(new)")) {
|
|
4383
5919
|
const fs4 = await import("fs");
|
|
4384
5920
|
try {
|
|
4385
|
-
newContent = fs4.readFileSync(
|
|
5921
|
+
newContent = fs4.readFileSync(resolve2(gitRoot, file.path.replace("(new) ", "")), "utf-8");
|
|
4386
5922
|
} catch {
|
|
4387
5923
|
newContent = "";
|
|
4388
5924
|
}
|
|
4389
5925
|
} else {
|
|
4390
5926
|
const diffOutput = await new Promise((resolve3) => {
|
|
4391
|
-
const proc =
|
|
5927
|
+
const proc = spawn7("git", file.status === "added" ? ["diff", "--cached", "--", file.path] : ["diff", "HEAD", "--", file.path], { cwd: gitRoot });
|
|
4392
5928
|
let out = "";
|
|
4393
5929
|
proc.stdout.on("data", (d) => {
|
|
4394
5930
|
out += d.toString();
|
|
@@ -4402,7 +5938,7 @@ function createGitRoutes(metadataManager) {
|
|
|
4402
5938
|
let oldLineNum = 0;
|
|
4403
5939
|
let newLineNum = 0;
|
|
4404
5940
|
const oldContentOutput = await new Promise((resolve3) => {
|
|
4405
|
-
const proc =
|
|
5941
|
+
const proc = spawn7("git", ["show", `HEAD:${file.path}`], { cwd: gitRoot });
|
|
4406
5942
|
let out = "";
|
|
4407
5943
|
proc.stdout.on("data", (d) => {
|
|
4408
5944
|
out += d.toString();
|
|
@@ -4413,7 +5949,7 @@ function createGitRoutes(metadataManager) {
|
|
|
4413
5949
|
oldContent = oldContentOutput;
|
|
4414
5950
|
const fs4 = await import("fs");
|
|
4415
5951
|
try {
|
|
4416
|
-
newContent = fs4.readFileSync(
|
|
5952
|
+
newContent = fs4.readFileSync(resolve2(gitRoot, file.path), "utf-8");
|
|
4417
5953
|
} catch {
|
|
4418
5954
|
newContent = "";
|
|
4419
5955
|
}
|
|
@@ -4479,17 +6015,58 @@ function createGitRoutes(metadataManager) {
|
|
|
4479
6015
|
const threadId = c.req.param("threadId");
|
|
4480
6016
|
const body = await c.req.json().catch(() => ({}));
|
|
4481
6017
|
const { model, provider } = body;
|
|
6018
|
+
process.stdout.write(`[generate-commit-message] threadId=${threadId}
|
|
6019
|
+
`);
|
|
4482
6020
|
const thread = await metadataManager.loadThreads().then((threads) => threads.find((t) => t.id === threadId));
|
|
4483
6021
|
if (!thread) {
|
|
6022
|
+
process.stdout.write(`[generate-commit-message] thread not found
|
|
6023
|
+
`);
|
|
4484
6024
|
return c.json({ error: "Thread not found" }, 404);
|
|
4485
6025
|
}
|
|
4486
6026
|
const repoPath = thread.path;
|
|
4487
6027
|
if (!repoPath) {
|
|
6028
|
+
process.stdout.write(`[generate-commit-message] thread path missing
|
|
6029
|
+
`);
|
|
4488
6030
|
return c.json({ error: "Thread path not found" }, 404);
|
|
4489
6031
|
}
|
|
4490
|
-
const absolutePath =
|
|
4491
|
-
|
|
4492
|
-
|
|
6032
|
+
const absolutePath = resolveThreadPath(repoPath);
|
|
6033
|
+
process.stdout.write(`[generate-commit-message] resolved path: ${absolutePath}
|
|
6034
|
+
`);
|
|
6035
|
+
if (!existsSync6(absolutePath)) {
|
|
6036
|
+
process.stdout.write(`[generate-commit-message] path does not exist: ${absolutePath}
|
|
6037
|
+
`);
|
|
6038
|
+
return c.json({
|
|
6039
|
+
error: `Thread repo path does not exist: ${absolutePath}. Check that the project folder is present.`
|
|
6040
|
+
}, 400);
|
|
6041
|
+
}
|
|
6042
|
+
let gitRoot;
|
|
6043
|
+
try {
|
|
6044
|
+
gitRoot = await getGitRoot(absolutePath);
|
|
6045
|
+
process.stdout.write(`[generate-commit-message] git root: ${gitRoot}
|
|
6046
|
+
`);
|
|
6047
|
+
} catch (e) {
|
|
6048
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
6049
|
+
process.stdout.write(`[generate-commit-message] not a git repo: ${msg}
|
|
6050
|
+
`);
|
|
6051
|
+
return c.json({
|
|
6052
|
+
error: `Path is not a git repository: ${absolutePath}. ${msg}`
|
|
6053
|
+
}, 400);
|
|
6054
|
+
}
|
|
6055
|
+
let diff = await new Promise((resolveDiff, reject) => {
|
|
6056
|
+
const runUnstagedDiff = () => {
|
|
6057
|
+
process.stdout.write(`[generate-commit-message] using unstaged diff (git diff)
|
|
6058
|
+
`);
|
|
6059
|
+
const proc2 = spawn7("git", ["diff"], { cwd: gitRoot });
|
|
6060
|
+
let out2 = "";
|
|
6061
|
+
proc2.stdout.on("data", (d) => {
|
|
6062
|
+
out2 += d.toString();
|
|
6063
|
+
});
|
|
6064
|
+
proc2.on("close", (code) => {
|
|
6065
|
+
resolveDiff(code === 0 ? out2 : "");
|
|
6066
|
+
});
|
|
6067
|
+
proc2.on("error", reject);
|
|
6068
|
+
};
|
|
6069
|
+
const proc = spawn7("git", ["diff", "--cached"], { cwd: gitRoot });
|
|
4493
6070
|
let out = "";
|
|
4494
6071
|
let err = "";
|
|
4495
6072
|
proc.stdout.on("data", (d) => {
|
|
@@ -4501,26 +6078,91 @@ function createGitRoutes(metadataManager) {
|
|
|
4501
6078
|
proc.on("close", (code) => {
|
|
4502
6079
|
if (code === 0) {
|
|
4503
6080
|
if (!out.trim()) {
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
out2 += d.toString();
|
|
4508
|
-
});
|
|
4509
|
-
proc2.on("close", () => resolve3(out2));
|
|
4510
|
-
proc2.on("error", reject);
|
|
6081
|
+
process.stdout.write(`[generate-commit-message] no staged changes, checking unstaged
|
|
6082
|
+
`);
|
|
6083
|
+
runUnstagedDiff();
|
|
4511
6084
|
} else {
|
|
4512
|
-
|
|
6085
|
+
process.stdout.write(`[generate-commit-message] using staged diff (git diff --cached)
|
|
6086
|
+
`);
|
|
6087
|
+
resolveDiff(out);
|
|
4513
6088
|
}
|
|
4514
6089
|
} else {
|
|
4515
|
-
|
|
6090
|
+
const noHead = /HEAD|revision|unknown revision/i.test(err);
|
|
6091
|
+
if (noHead) {
|
|
6092
|
+
process.stdout.write(`[generate-commit-message] no HEAD (new repo), using working tree diff
|
|
6093
|
+
`);
|
|
6094
|
+
runUnstagedDiff();
|
|
6095
|
+
} else {
|
|
6096
|
+
reject(new Error(err || "Failed to get git diff"));
|
|
6097
|
+
}
|
|
4516
6098
|
}
|
|
4517
6099
|
});
|
|
4518
6100
|
proc.on("error", reject);
|
|
4519
6101
|
});
|
|
4520
6102
|
if (!diff.trim()) {
|
|
6103
|
+
const statusOut = await new Promise((resolveStatus) => {
|
|
6104
|
+
const proc = spawn7("git", ["status", "--porcelain"], { cwd: gitRoot });
|
|
6105
|
+
let out = "";
|
|
6106
|
+
proc.stdout.on("data", (d) => {
|
|
6107
|
+
out += d.toString();
|
|
6108
|
+
});
|
|
6109
|
+
proc.on("close", () => resolveStatus(out));
|
|
6110
|
+
proc.on("error", () => resolveStatus(""));
|
|
6111
|
+
});
|
|
6112
|
+
const lines = statusOut.trim().split(`
|
|
6113
|
+
`).filter((l) => l.length > 0);
|
|
6114
|
+
const untrackedPaths = [];
|
|
6115
|
+
for (const line of lines) {
|
|
6116
|
+
const code = line.slice(0, 2);
|
|
6117
|
+
const pathPart = line.slice(3).trim();
|
|
6118
|
+
if (code === "??") {
|
|
6119
|
+
untrackedPaths.push(pathPart);
|
|
6120
|
+
}
|
|
6121
|
+
}
|
|
6122
|
+
if (untrackedPaths.length > 0) {
|
|
6123
|
+
process.stdout.write(`[generate-commit-message] building diff for ${untrackedPaths.length} untracked file(s)
|
|
6124
|
+
`);
|
|
6125
|
+
const parts = [];
|
|
6126
|
+
const maxFileSize = 1e5;
|
|
6127
|
+
for (const relPath of untrackedPaths) {
|
|
6128
|
+
const fullPath = join10(gitRoot, relPath);
|
|
6129
|
+
if (!existsSync6(fullPath))
|
|
6130
|
+
continue;
|
|
6131
|
+
try {
|
|
6132
|
+
if (statSync3(fullPath).isDirectory())
|
|
6133
|
+
continue;
|
|
6134
|
+
const content = readFileSync3(fullPath, "utf-8");
|
|
6135
|
+
const safeContent = content.length > maxFileSize ? content.slice(0, maxFileSize) + `
|
|
6136
|
+
...(truncated)` : content;
|
|
6137
|
+
const linesForDiff = safeContent.split(/\r?\n/).map((l) => `+${l}`).join(`
|
|
6138
|
+
`);
|
|
6139
|
+
parts.push(`diff --git a/${relPath} b/${relPath}
|
|
6140
|
+
new file mode 100644
|
|
6141
|
+
--- /dev/null
|
|
6142
|
+
+++ b/${relPath}
|
|
6143
|
+
${linesForDiff}`);
|
|
6144
|
+
} catch {
|
|
6145
|
+
parts.push(`diff --git a/${relPath} b/${relPath}
|
|
6146
|
+
new file mode 100644
|
|
6147
|
+
(Binary or unreadable file: ${relPath})`);
|
|
6148
|
+
}
|
|
6149
|
+
}
|
|
6150
|
+
if (parts.length > 0) {
|
|
6151
|
+
diff = parts.join(`
|
|
6152
|
+
`);
|
|
6153
|
+
}
|
|
6154
|
+
}
|
|
6155
|
+
}
|
|
6156
|
+
if (!diff.trim()) {
|
|
6157
|
+
process.stdout.write(`[generate-commit-message] no changes to generate message for
|
|
6158
|
+
`);
|
|
4521
6159
|
return c.json({ error: "No changes to generate commit message for" }, 400);
|
|
4522
6160
|
}
|
|
6161
|
+
process.stdout.write(`[generate-commit-message] diff length=${diff.length} chars, generating message with AI
|
|
6162
|
+
`);
|
|
4523
6163
|
const commitMessage = await generateCommitMessageWithAI(diff, metadataManager, model, provider);
|
|
6164
|
+
process.stdout.write(`[generate-commit-message] generated: ${commitMessage.replace(/\n/g, " ")}
|
|
6165
|
+
`);
|
|
4524
6166
|
return c.json({ message: commitMessage });
|
|
4525
6167
|
} catch (error) {
|
|
4526
6168
|
const message = error instanceof Error ? error.message : "Failed to generate commit message";
|
|
@@ -4543,9 +6185,15 @@ function createGitRoutes(metadataManager) {
|
|
|
4543
6185
|
if (!repoPath) {
|
|
4544
6186
|
return c.json({ error: "Thread path not found" }, 404);
|
|
4545
6187
|
}
|
|
4546
|
-
const absolutePath =
|
|
6188
|
+
const absolutePath = resolveThreadPath(repoPath);
|
|
6189
|
+
let gitRoot;
|
|
6190
|
+
try {
|
|
6191
|
+
gitRoot = await getGitRoot(absolutePath);
|
|
6192
|
+
} catch {
|
|
6193
|
+
return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
|
|
6194
|
+
}
|
|
4547
6195
|
await new Promise((resolve3, reject) => {
|
|
4548
|
-
const proc =
|
|
6196
|
+
const proc = spawn7("git", ["add", "-A"], { cwd: gitRoot });
|
|
4549
6197
|
proc.on("close", (code) => {
|
|
4550
6198
|
if (code === 0)
|
|
4551
6199
|
resolve3();
|
|
@@ -4555,7 +6203,7 @@ function createGitRoutes(metadataManager) {
|
|
|
4555
6203
|
proc.on("error", reject);
|
|
4556
6204
|
});
|
|
4557
6205
|
await new Promise((resolve3, reject) => {
|
|
4558
|
-
const proc =
|
|
6206
|
+
const proc = spawn7("git", ["commit", "-m", message], { cwd: gitRoot });
|
|
4559
6207
|
proc.on("close", (code) => {
|
|
4560
6208
|
if (code === 0)
|
|
4561
6209
|
resolve3();
|
|
@@ -4581,9 +6229,15 @@ function createGitRoutes(metadataManager) {
|
|
|
4581
6229
|
if (!repoPath) {
|
|
4582
6230
|
return c.json({ error: "Thread path not found" }, 404);
|
|
4583
6231
|
}
|
|
4584
|
-
const absolutePath =
|
|
6232
|
+
const absolutePath = resolveThreadPath(repoPath);
|
|
6233
|
+
let gitRoot;
|
|
6234
|
+
try {
|
|
6235
|
+
gitRoot = await getGitRoot(absolutePath);
|
|
6236
|
+
} catch {
|
|
6237
|
+
return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
|
|
6238
|
+
}
|
|
4585
6239
|
const currentBranch = await new Promise((resolve3, reject) => {
|
|
4586
|
-
const proc =
|
|
6240
|
+
const proc = spawn7("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: gitRoot });
|
|
4587
6241
|
let out = "";
|
|
4588
6242
|
proc.stdout.on("data", (d) => {
|
|
4589
6243
|
out += d.toString();
|
|
@@ -4592,7 +6246,7 @@ function createGitRoutes(metadataManager) {
|
|
|
4592
6246
|
proc.on("error", reject);
|
|
4593
6247
|
});
|
|
4594
6248
|
await new Promise((resolve3, reject) => {
|
|
4595
|
-
const proc =
|
|
6249
|
+
const proc = spawn7("git", ["push", "-u", "origin", currentBranch], { cwd: gitRoot });
|
|
4596
6250
|
let err = "";
|
|
4597
6251
|
proc.stderr.on("data", (d) => {
|
|
4598
6252
|
err += d.toString();
|
|
@@ -4624,9 +6278,24 @@ function createGitRoutes(metadataManager) {
|
|
|
4624
6278
|
if (!repoPath) {
|
|
4625
6279
|
return c.json({ error: "Thread path not found" }, 404);
|
|
4626
6280
|
}
|
|
4627
|
-
const absolutePath =
|
|
4628
|
-
|
|
4629
|
-
|
|
6281
|
+
const absolutePath = resolveThreadPath(repoPath);
|
|
6282
|
+
let gitRoot;
|
|
6283
|
+
try {
|
|
6284
|
+
gitRoot = await getGitRoot(absolutePath);
|
|
6285
|
+
} catch {
|
|
6286
|
+
return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
|
|
6287
|
+
}
|
|
6288
|
+
const diff = await new Promise((resolveDiff, reject) => {
|
|
6289
|
+
const runPlainDiff = () => {
|
|
6290
|
+
const proc2 = spawn7("git", ["diff"], { cwd: gitRoot });
|
|
6291
|
+
let out2 = "";
|
|
6292
|
+
proc2.stdout.on("data", (d) => {
|
|
6293
|
+
out2 += d.toString();
|
|
6294
|
+
});
|
|
6295
|
+
proc2.on("close", () => resolveDiff(out2));
|
|
6296
|
+
proc2.on("error", reject);
|
|
6297
|
+
};
|
|
6298
|
+
const proc = spawn7("git", ["diff", "HEAD"], { cwd: gitRoot });
|
|
4630
6299
|
let out = "";
|
|
4631
6300
|
let err = "";
|
|
4632
6301
|
proc.stdout.on("data", (d) => {
|
|
@@ -4637,9 +6306,13 @@ function createGitRoutes(metadataManager) {
|
|
|
4637
6306
|
});
|
|
4638
6307
|
proc.on("close", (code) => {
|
|
4639
6308
|
if (code === 0) {
|
|
4640
|
-
|
|
6309
|
+
resolveDiff(out);
|
|
4641
6310
|
} else {
|
|
4642
|
-
|
|
6311
|
+
const noHead = /HEAD|revision|unknown revision/i.test(err);
|
|
6312
|
+
if (noHead)
|
|
6313
|
+
runPlainDiff();
|
|
6314
|
+
else
|
|
6315
|
+
reject(new Error(err || "Failed to get git diff"));
|
|
4643
6316
|
}
|
|
4644
6317
|
});
|
|
4645
6318
|
proc.on("error", reject);
|
|
@@ -4661,119 +6334,27 @@ ${truncatedDiff}
|
|
|
4661
6334
|
Generate the response in this exact format:
|
|
4662
6335
|
TITLE: <title here>
|
|
4663
6336
|
DESCRIPTION: <description here>`;
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
if (prov?.keyName) {
|
|
4686
|
-
const envKey = process.env[prov.keyName];
|
|
4687
|
-
if (envKey) {
|
|
4688
|
-
selectedProvider = prov;
|
|
4689
|
-
apiKey = envKey;
|
|
4690
|
-
break;
|
|
4691
|
-
}
|
|
4692
|
-
}
|
|
4693
|
-
}
|
|
4694
|
-
if (!selectedProvider || !apiKey) {
|
|
4695
|
-
for (const providerName of providerPriority) {
|
|
4696
|
-
const prov = PROVIDERS.find((p) => p.name === providerName);
|
|
4697
|
-
if (prov && providerKeys[prov.name]) {
|
|
4698
|
-
selectedProvider = prov;
|
|
4699
|
-
apiKey = providerKeys[prov.name];
|
|
4700
|
-
break;
|
|
4701
|
-
}
|
|
4702
|
-
}
|
|
4703
|
-
}
|
|
4704
|
-
if (!selectedProvider || !apiKey) {
|
|
4705
|
-
for (const prov of PROVIDERS) {
|
|
4706
|
-
if (prov.api && prov.keyName) {
|
|
4707
|
-
const envKey = process.env[prov.keyName];
|
|
4708
|
-
if (envKey) {
|
|
4709
|
-
selectedProvider = prov;
|
|
4710
|
-
apiKey = envKey;
|
|
4711
|
-
break;
|
|
4712
|
-
}
|
|
4713
|
-
if (providerKeys[prov.name]) {
|
|
4714
|
-
selectedProvider = prov;
|
|
4715
|
-
apiKey = providerKeys[prov.name];
|
|
4716
|
-
break;
|
|
4717
|
-
}
|
|
4718
|
-
}
|
|
4719
|
-
}
|
|
4720
|
-
}
|
|
4721
|
-
if (!selectedModel) {
|
|
4722
|
-
const modelMap = {
|
|
4723
|
-
Anthropic: "claude-3-5-sonnet-20241022",
|
|
4724
|
-
OpenAI: "gpt-4o-mini",
|
|
4725
|
-
OpenRouter: "anthropic/claude-3.5-sonnet",
|
|
4726
|
-
Groq: "llama-3.3-70b-versatile",
|
|
4727
|
-
DeepSeek: "deepseek-chat"
|
|
4728
|
-
};
|
|
4729
|
-
selectedModel = modelMap[selectedProvider?.name || ""] || "default";
|
|
4730
|
-
}
|
|
4731
|
-
}
|
|
4732
|
-
if (!selectedProvider || !apiKey || !selectedProvider.api) {
|
|
4733
|
-
throw new Error("No AI provider configured. Please configure an API key in settings.");
|
|
4734
|
-
}
|
|
4735
|
-
if (selectedModel && selectedProvider) {
|
|
4736
|
-
selectedModel = selectedModel.replace(`${selectedProvider.name.toLowerCase()}/`, "");
|
|
4737
|
-
}
|
|
4738
|
-
const finalModel = selectedModel || "default";
|
|
4739
|
-
const providers = {
|
|
4740
|
-
tarsk: {
|
|
4741
|
-
api: selectedProvider.api,
|
|
4742
|
-
options: { apiKey },
|
|
4743
|
-
models: { [finalModel]: finalModel }
|
|
4744
|
-
}
|
|
4745
|
-
};
|
|
4746
|
-
const sessionConfig = {
|
|
4747
|
-
model: `tarsk/${finalModel}`,
|
|
4748
|
-
cwd: process.cwd(),
|
|
4749
|
-
productName: "Tarsk.io",
|
|
4750
|
-
providers
|
|
4751
|
-
};
|
|
4752
|
-
session = await createSession2(sessionConfig);
|
|
4753
|
-
await session.send(prompt);
|
|
4754
|
-
let prInfo = "";
|
|
4755
|
-
for await (const msg of session.receive()) {
|
|
4756
|
-
if (msg?.type === "message" || msg?.type === "result") {
|
|
4757
|
-
const content = typeof msg.text === "string" ? msg.text : typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
4758
|
-
prInfo = content.trim();
|
|
4759
|
-
if (msg?.type === "result")
|
|
4760
|
-
break;
|
|
4761
|
-
}
|
|
4762
|
-
}
|
|
4763
|
-
const titleMatch = prInfo.match(/TITLE:\s*(.+?)(?:\nDESCRIPTION:|$)/s);
|
|
4764
|
-
const descriptionMatch = prInfo.match(/DESCRIPTION:\s*(.+?)$/s);
|
|
4765
|
-
const title = titleMatch ? titleMatch[1].trim() : "Update";
|
|
4766
|
-
const description = descriptionMatch ? descriptionMatch[1].trim() : "";
|
|
4767
|
-
return c.json({ title, description });
|
|
4768
|
-
} finally {
|
|
4769
|
-
if (session) {
|
|
4770
|
-
try {
|
|
4771
|
-
await session.close?.();
|
|
4772
|
-
} catch (closeError) {
|
|
4773
|
-
console.error("Error closing session:", closeError);
|
|
4774
|
-
}
|
|
4775
|
-
}
|
|
4776
|
-
}
|
|
6337
|
+
const providerKeys = await metadataManager.getProviderKeys();
|
|
6338
|
+
const resolved = resolveProviderAndKey(providerKeys, provider);
|
|
6339
|
+
if (!resolved || !resolved.provider.api) {
|
|
6340
|
+
throw new Error("No AI provider configured. Please configure an API key in settings.");
|
|
6341
|
+
}
|
|
6342
|
+
let selectedModel = model;
|
|
6343
|
+
if (selectedModel) {
|
|
6344
|
+
selectedModel = selectedModel.replace(`${resolved.provider.name.toLowerCase()}/`, "");
|
|
6345
|
+
} else {
|
|
6346
|
+
selectedModel = DEFAULT_MODEL_MAP[resolved.provider.name] || "default";
|
|
6347
|
+
}
|
|
6348
|
+
const resolvedModel = resolveModelForGit(resolved.provider.name, selectedModel, resolved.provider.api);
|
|
6349
|
+
const response = await completeSimple(resolvedModel, {
|
|
6350
|
+
messages: [{ role: "user", content: prompt }]
|
|
6351
|
+
}, { apiKey: resolved.apiKey });
|
|
6352
|
+
const prInfo = response.content.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
|
|
6353
|
+
const titleMatch = prInfo.match(/TITLE:\s*(.+?)(?:\nDESCRIPTION:|$)/s);
|
|
6354
|
+
const descriptionMatch = prInfo.match(/DESCRIPTION:\s*(.+?)$/s);
|
|
6355
|
+
const title = titleMatch ? titleMatch[1].trim() : "Update";
|
|
6356
|
+
const description = descriptionMatch ? descriptionMatch[1].trim() : "";
|
|
6357
|
+
return c.json({ title, description });
|
|
4777
6358
|
} catch (error) {
|
|
4778
6359
|
const message = error instanceof Error ? error.message : "Failed to generate PR info";
|
|
4779
6360
|
return c.json({ error: message }, 500);
|
|
@@ -4791,9 +6372,15 @@ DESCRIPTION: <description here>`;
|
|
|
4791
6372
|
if (!repoPath) {
|
|
4792
6373
|
return c.json({ error: "Thread path not found" }, 404);
|
|
4793
6374
|
}
|
|
4794
|
-
const absolutePath =
|
|
6375
|
+
const absolutePath = resolveThreadPath(repoPath);
|
|
6376
|
+
let gitRoot;
|
|
6377
|
+
try {
|
|
6378
|
+
gitRoot = await getGitRoot(absolutePath);
|
|
6379
|
+
} catch {
|
|
6380
|
+
return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
|
|
6381
|
+
}
|
|
4795
6382
|
const commits = await new Promise((resolve3, reject) => {
|
|
4796
|
-
const proc =
|
|
6383
|
+
const proc = spawn7("git", ["log", "--oneline", "-n", limit.toString(), "--format=%H|%s|%an|%ai"], { cwd: gitRoot });
|
|
4797
6384
|
let out = "";
|
|
4798
6385
|
let err = "";
|
|
4799
6386
|
proc.stdout.on("data", (d) => {
|
|
@@ -4841,9 +6428,15 @@ DESCRIPTION: <description here>`;
|
|
|
4841
6428
|
if (!repoPath) {
|
|
4842
6429
|
return c.json({ error: "Thread path not found" }, 404);
|
|
4843
6430
|
}
|
|
4844
|
-
const absolutePath =
|
|
6431
|
+
const absolutePath = resolveThreadPath(repoPath);
|
|
6432
|
+
let gitRoot;
|
|
6433
|
+
try {
|
|
6434
|
+
gitRoot = await getGitRoot(absolutePath);
|
|
6435
|
+
} catch {
|
|
6436
|
+
return c.json({ error: `Path is not a git repository: ${absolutePath}` }, 400);
|
|
6437
|
+
}
|
|
4845
6438
|
const currentBranch = await new Promise((resolve3, reject) => {
|
|
4846
|
-
const proc =
|
|
6439
|
+
const proc = spawn7("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: gitRoot });
|
|
4847
6440
|
let out = "";
|
|
4848
6441
|
proc.stdout.on("data", (d) => {
|
|
4849
6442
|
out += d.toString();
|
|
@@ -4853,7 +6446,7 @@ DESCRIPTION: <description here>`;
|
|
|
4853
6446
|
});
|
|
4854
6447
|
const prUrl = await new Promise((resolve3, reject) => {
|
|
4855
6448
|
const args = ["pr", "create", "--title", title || currentBranch, "--body", description || ""];
|
|
4856
|
-
const proc =
|
|
6449
|
+
const proc = spawn7("gh", args, { cwd: gitRoot });
|
|
4857
6450
|
let out = "";
|
|
4858
6451
|
let err = "";
|
|
4859
6452
|
proc.stdout.on("data", (d) => {
|
|
@@ -4974,7 +6567,7 @@ function createRunRoutes(projectManager) {
|
|
|
4974
6567
|
|
|
4975
6568
|
// src/routes/onboarding.ts
|
|
4976
6569
|
import { Hono as Hono8 } from "hono";
|
|
4977
|
-
import { spawn as
|
|
6570
|
+
import { spawn as spawn8 } from "child_process";
|
|
4978
6571
|
function createOnboardingRoutes(metadataManager) {
|
|
4979
6572
|
const router = new Hono8;
|
|
4980
6573
|
router.get("/status", async (c) => {
|
|
@@ -4989,7 +6582,7 @@ function createOnboardingRoutes(metadataManager) {
|
|
|
4989
6582
|
router.get("/git-check", async (c) => {
|
|
4990
6583
|
try {
|
|
4991
6584
|
const gitInstalled = await new Promise((resolve3) => {
|
|
4992
|
-
const proc =
|
|
6585
|
+
const proc = spawn8("git", ["--version"]);
|
|
4993
6586
|
let _err = "";
|
|
4994
6587
|
proc.stderr.on("data", (d) => {
|
|
4995
6588
|
_err += d.toString();
|
|
@@ -5066,18 +6659,9 @@ function createScaffoldRoutes(projectManager) {
|
|
|
5066
6659
|
return router;
|
|
5067
6660
|
}
|
|
5068
6661
|
|
|
5069
|
-
// src/paths.ts
|
|
5070
|
-
import { join as join8 } from "path";
|
|
5071
|
-
import { homedir } from "os";
|
|
5072
|
-
var APP_SUPPORT_DIR = join8(homedir(), "Library", "Application Support", "Tarsk");
|
|
5073
|
-
var DATA_DIR = join8(APP_SUPPORT_DIR, "data");
|
|
5074
|
-
function getDataDir() {
|
|
5075
|
-
return DATA_DIR;
|
|
5076
|
-
}
|
|
5077
|
-
|
|
5078
6662
|
// src/index.ts
|
|
5079
6663
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
5080
|
-
var __dirname3 =
|
|
6664
|
+
var __dirname3 = path3.dirname(__filename2);
|
|
5081
6665
|
var args = process.argv.slice(2);
|
|
5082
6666
|
var isDebug = args.includes("--debug");
|
|
5083
6667
|
var shouldOpenBrowser = args.includes("--open");
|
|
@@ -5093,7 +6677,7 @@ var processingStateManager = new ProcessingStateManagerImpl;
|
|
|
5093
6677
|
var projectManager = new ProjectManagerImpl(dataDir, metadataManager, gitManager, processingStateManager);
|
|
5094
6678
|
var threadManager = new ThreadManagerImpl(metadataManager, gitManager, processingStateManager);
|
|
5095
6679
|
var conversationManager = new ConversationManagerImpl(dataDir);
|
|
5096
|
-
var
|
|
6680
|
+
var agentExecutor = new PiExecutorImpl(metadataManager);
|
|
5097
6681
|
await metadataManager.initialize();
|
|
5098
6682
|
app.get("/health", (c) => {
|
|
5099
6683
|
return c.json({
|
|
@@ -5115,14 +6699,14 @@ app.get("/api/threads/processing", (c) => {
|
|
|
5115
6699
|
app.route("/api/projects", createProjectRoutes(projectManager, threadManager));
|
|
5116
6700
|
app.route("/api/projects", createRunRoutes(projectManager));
|
|
5117
6701
|
app.route("/api/threads", createThreadRoutes(threadManager, gitManager, conversationManager));
|
|
5118
|
-
app.route("/api/chat", createChatRoutes(threadManager,
|
|
6702
|
+
app.route("/api/chat", createChatRoutes(threadManager, agentExecutor, conversationManager, processingStateManager));
|
|
5119
6703
|
app.route("/api/providers", createProviderRoutes(metadataManager));
|
|
5120
6704
|
app.route("/api/models", createModelRoutes(metadataManager));
|
|
5121
6705
|
app.route("/api/git", createGitRoutes(metadataManager));
|
|
5122
6706
|
app.route("/api/onboarding", createOnboardingRoutes(metadataManager));
|
|
5123
6707
|
app.route("/api/scaffold", createScaffoldRoutes(projectManager));
|
|
5124
|
-
var publicDir =
|
|
5125
|
-
var staticRoot =
|
|
6708
|
+
var publicDir = path3.join(__dirname3, "public");
|
|
6709
|
+
var staticRoot = path3.relative(process.cwd(), publicDir);
|
|
5126
6710
|
app.use("/*", async (c, next) => {
|
|
5127
6711
|
if (c.req.path.startsWith("/api/")) {
|
|
5128
6712
|
return next();
|
|
@@ -5134,7 +6718,7 @@ app.get("*", async (c, next) => {
|
|
|
5134
6718
|
return next();
|
|
5135
6719
|
}
|
|
5136
6720
|
return serveStatic({
|
|
5137
|
-
path:
|
|
6721
|
+
path: path3.relative(process.cwd(), path3.join(publicDir, "index.html"))
|
|
5138
6722
|
})(c, next);
|
|
5139
6723
|
});
|
|
5140
6724
|
app.all("*", (c) => {
|