@cortexkit/aft-pi 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/index.js +1996 -1713
- package/dist/tools/ast.d.ts.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/conflicts.d.ts.map +1 -1
- package/dist/tools/fs.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts +11 -0
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/imports.d.ts +5 -1
- package/dist/tools/imports.d.ts.map +1 -1
- package/dist/tools/inspect.d.ts.map +1 -1
- package/dist/tools/navigate.d.ts.map +1 -1
- package/dist/tools/reading.d.ts +1 -0
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tools/refactor.d.ts.map +1 -1
- package/dist/tools/safety.d.ts.map +1 -1
- package/dist/tools/structure.d.ts.map +1 -1
- package/package.json +10 -9
package/dist/index.js
CHANGED
|
@@ -13152,26 +13152,28 @@ function getManualInstallHint() {
|
|
|
13152
13152
|
}
|
|
13153
13153
|
async function ensureOnnxRuntime(storageDir) {
|
|
13154
13154
|
const info = getPlatformInfo();
|
|
13155
|
-
const
|
|
13156
|
-
const
|
|
13155
|
+
const ortVersionDir = join6(storageDir, "onnxruntime", ORT_VERSION);
|
|
13156
|
+
const libName = info?.libName ?? "libonnxruntime.dylib";
|
|
13157
|
+
const resolvedOrtDir = resolveCachedOnnxRuntimeDir(ortVersionDir, libName);
|
|
13158
|
+
const libPath = join6(resolvedOrtDir, libName);
|
|
13157
13159
|
if (existsSync5(libPath)) {
|
|
13158
|
-
const meta = readOnnxInstalledMeta(
|
|
13160
|
+
const meta = readOnnxInstalledMeta(ortVersionDir);
|
|
13159
13161
|
if (meta?.sha256) {
|
|
13160
13162
|
try {
|
|
13161
13163
|
const currentHash = sha256File(libPath);
|
|
13162
13164
|
if (currentHash !== meta.sha256) {
|
|
13163
|
-
error(`ONNX Runtime at ${
|
|
13165
|
+
error(`ONNX Runtime at ${resolvedOrtDir}: TOFU sha256 mismatch — refusing to use ` + `tampered binary. Recorded ${meta.sha256}, current ${currentHash}. ` + `Run \`aft doctor --clear\` to re-download from scratch.`);
|
|
13164
13166
|
} else {
|
|
13165
|
-
log(`ONNX Runtime found at ${
|
|
13166
|
-
return
|
|
13167
|
+
log(`ONNX Runtime found at ${resolvedOrtDir} (TOFU verified)`);
|
|
13168
|
+
return resolvedOrtDir;
|
|
13167
13169
|
}
|
|
13168
13170
|
} catch (err) {
|
|
13169
|
-
warn(`Could not verify ONNX Runtime hash at ${
|
|
13170
|
-
return
|
|
13171
|
+
warn(`Could not verify ONNX Runtime hash at ${resolvedOrtDir}: ${err}`);
|
|
13172
|
+
return resolvedOrtDir;
|
|
13171
13173
|
}
|
|
13172
13174
|
} else {
|
|
13173
|
-
log(`ONNX Runtime found at ${
|
|
13174
|
-
return
|
|
13175
|
+
log(`ONNX Runtime found at ${resolvedOrtDir} (no recorded hash, accepting)`);
|
|
13176
|
+
return resolvedOrtDir;
|
|
13175
13177
|
}
|
|
13176
13178
|
}
|
|
13177
13179
|
const systemPath = findSystemOnnxRuntime(info?.libName);
|
|
@@ -13192,8 +13194,8 @@ async function ensureOnnxRuntime(storageDir) {
|
|
|
13192
13194
|
return null;
|
|
13193
13195
|
}
|
|
13194
13196
|
try {
|
|
13195
|
-
cleanupIncompleteTargetIfUnowned(
|
|
13196
|
-
return await downloadOnnxRuntime(info,
|
|
13197
|
+
cleanupIncompleteTargetIfUnowned(ortVersionDir);
|
|
13198
|
+
return await downloadOnnxRuntime(info, ortVersionDir);
|
|
13197
13199
|
} finally {
|
|
13198
13200
|
releaseLock(lockPath);
|
|
13199
13201
|
}
|
|
@@ -13211,11 +13213,16 @@ function cleanupAbandonedStagingDirs(onnxBaseDir) {
|
|
|
13211
13213
|
let abandoned = false;
|
|
13212
13214
|
if (Number.isFinite(pid) && pid > 0) {
|
|
13213
13215
|
if (process.platform === "win32") {
|
|
13214
|
-
|
|
13215
|
-
|
|
13216
|
-
abandoned = ageMs > STALE_LOCK_MS;
|
|
13217
|
-
} catch {
|
|
13216
|
+
const ownerAlive = isProcessAlive(pid);
|
|
13217
|
+
if (!ownerAlive) {
|
|
13218
13218
|
abandoned = true;
|
|
13219
|
+
} else {
|
|
13220
|
+
try {
|
|
13221
|
+
const ageMs = Date.now() - statSync2(stagingDir).mtimeMs;
|
|
13222
|
+
abandoned = ageMs > STALE_LOCK_MS;
|
|
13223
|
+
} catch {
|
|
13224
|
+
abandoned = true;
|
|
13225
|
+
}
|
|
13219
13226
|
}
|
|
13220
13227
|
} else {
|
|
13221
13228
|
abandoned = !isProcessAlive(pid);
|
|
@@ -13249,17 +13256,32 @@ var REQUIRED_ORT_MIN_MINOR = 20;
|
|
|
13249
13256
|
var INVALID_ORT_VERSION = "<invalid>";
|
|
13250
13257
|
function parseOnnxVersionFromPath(value) {
|
|
13251
13258
|
const name = basename(value);
|
|
13252
|
-
const semverish = name.match(
|
|
13259
|
+
const semverish = name.match(/(?:^|[._-])(\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?)(?:\.(?:dylib|dll))?$/);
|
|
13253
13260
|
if (semverish)
|
|
13254
13261
|
return semverish[1].split(/[-+]/, 1)[0];
|
|
13255
13262
|
return /\d+\.\d+\.\d+/.test(name) ? INVALID_ORT_VERSION : null;
|
|
13256
13263
|
}
|
|
13264
|
+
function parseOnnxVersionFromDirectoryPath(value) {
|
|
13265
|
+
const parts = value.split(/[\\/]+/).filter(Boolean).reverse();
|
|
13266
|
+
for (const part of parts) {
|
|
13267
|
+
const version = parseOnnxVersionFromPath(part);
|
|
13268
|
+
if (version)
|
|
13269
|
+
return version;
|
|
13270
|
+
}
|
|
13271
|
+
return null;
|
|
13272
|
+
}
|
|
13273
|
+
function isPathInsideRoot(root, candidate) {
|
|
13274
|
+
const rel = relative(root, candidate);
|
|
13275
|
+
return rel === "" || !rel.startsWith("../") && !rel.startsWith("..\\") && rel !== ".." && !isAbsolute(rel) && !win32.isAbsolute(rel);
|
|
13276
|
+
}
|
|
13257
13277
|
function detectOnnxVersion(libDir, libName) {
|
|
13258
13278
|
try {
|
|
13259
13279
|
const entries = readdirSync(libDir);
|
|
13260
13280
|
const barePrefix = libName.replace(/\.(so|dylib|dll)$/, "");
|
|
13281
|
+
const expectedPrefix = process.platform === "win32" ? barePrefix.toLowerCase() : barePrefix;
|
|
13261
13282
|
for (const entry of entries) {
|
|
13262
|
-
|
|
13283
|
+
const comparable = process.platform === "win32" ? entry.toLowerCase() : entry;
|
|
13284
|
+
if (!comparable.startsWith(expectedPrefix))
|
|
13263
13285
|
continue;
|
|
13264
13286
|
const version = parseOnnxVersionFromPath(entry);
|
|
13265
13287
|
if (version)
|
|
@@ -13269,7 +13291,7 @@ function detectOnnxVersion(libDir, libName) {
|
|
|
13269
13291
|
if (existsSync5(base)) {
|
|
13270
13292
|
try {
|
|
13271
13293
|
const real = realpathSync(base);
|
|
13272
|
-
const version = parseOnnxVersionFromPath(real);
|
|
13294
|
+
const version = parseOnnxVersionFromPath(real) ?? parseOnnxVersionFromDirectoryPath(real);
|
|
13273
13295
|
if (version)
|
|
13274
13296
|
return version;
|
|
13275
13297
|
} catch {}
|
|
@@ -13280,6 +13302,7 @@ function detectOnnxVersion(libDir, libName) {
|
|
|
13280
13302
|
return version;
|
|
13281
13303
|
} catch {}
|
|
13282
13304
|
}
|
|
13305
|
+
return parseOnnxVersionFromDirectoryPath(libDir);
|
|
13283
13306
|
} catch {}
|
|
13284
13307
|
return null;
|
|
13285
13308
|
}
|
|
@@ -13315,6 +13338,14 @@ function directoryContainsLibrary(dir, libName) {
|
|
|
13315
13338
|
return false;
|
|
13316
13339
|
}
|
|
13317
13340
|
}
|
|
13341
|
+
function resolveCachedOnnxRuntimeDir(ortVersionDir, libName) {
|
|
13342
|
+
if (existsSync5(join6(ortVersionDir, libName)))
|
|
13343
|
+
return ortVersionDir;
|
|
13344
|
+
const libSubdir = join6(ortVersionDir, "lib");
|
|
13345
|
+
if (existsSync5(join6(libSubdir, libName)))
|
|
13346
|
+
return libSubdir;
|
|
13347
|
+
return ortVersionDir;
|
|
13348
|
+
}
|
|
13318
13349
|
function findSystemOnnxRuntime(libName) {
|
|
13319
13350
|
if (!libName)
|
|
13320
13351
|
return null;
|
|
@@ -13360,6 +13391,7 @@ function findSystemOnnxRuntime(libName) {
|
|
|
13360
13391
|
seen.add(key);
|
|
13361
13392
|
return true;
|
|
13362
13393
|
});
|
|
13394
|
+
const unknownVersionPaths = [];
|
|
13363
13395
|
for (const dir of uniquePaths) {
|
|
13364
13396
|
const libPath = join6(dir, libName);
|
|
13365
13397
|
if (process.platform === "win32") {
|
|
@@ -13368,23 +13400,18 @@ function findSystemOnnxRuntime(libName) {
|
|
|
13368
13400
|
} else if (!existsSync5(libPath)) {
|
|
13369
13401
|
continue;
|
|
13370
13402
|
}
|
|
13371
|
-
|
|
13372
|
-
if (
|
|
13373
|
-
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
|
|
13378
|
-
|
|
13379
|
-
const version = detectOnnxVersion(dir, libName);
|
|
13380
|
-
if (version && !isOnnxVersionCompatible(version)) {
|
|
13381
|
-
warn(`Skipping system ONNX Runtime at ${dir} (v${version}); AFT requires ` + `v${REQUIRED_ORT_MAJOR}.${REQUIRED_ORT_MIN_MINOR}+. Falling through to AFT-managed download.`);
|
|
13382
|
-
continue;
|
|
13383
|
-
}
|
|
13403
|
+
const version = detectOnnxVersion(dir, libName);
|
|
13404
|
+
if (!version) {
|
|
13405
|
+
unknownVersionPaths.push(dir);
|
|
13406
|
+
continue;
|
|
13407
|
+
}
|
|
13408
|
+
if (!isOnnxVersionCompatible(version)) {
|
|
13409
|
+
warn(`Skipping system ONNX Runtime at ${dir} (v${version}); AFT requires ` + `v${REQUIRED_ORT_MAJOR}.${REQUIRED_ORT_MIN_MINOR}+. Falling through to AFT-managed download.`);
|
|
13410
|
+
continue;
|
|
13384
13411
|
}
|
|
13385
13412
|
return dir;
|
|
13386
13413
|
}
|
|
13387
|
-
return null;
|
|
13414
|
+
return unknownVersionPaths[0] ?? null;
|
|
13388
13415
|
}
|
|
13389
13416
|
async function downloadFileWithCap(url, destPath) {
|
|
13390
13417
|
const controller = new AbortController;
|
|
@@ -13438,13 +13465,13 @@ function validateExtractedTree(stagingRoot) {
|
|
|
13438
13465
|
const linkTarget = readlinkSync(fullPath);
|
|
13439
13466
|
const resolvedTarget = resolve(dirname3(fullPath), linkTarget);
|
|
13440
13467
|
const rel2 = relative(realRoot, resolvedTarget);
|
|
13441
|
-
if (rel2.startsWith("..") ||
|
|
13468
|
+
if (rel2.startsWith("..") || isAbsolute(rel2) || win32.isAbsolute(rel2)) {
|
|
13442
13469
|
throw new Error(`extracted symlink ${fullPath} points outside staging root: ${linkTarget}`);
|
|
13443
13470
|
}
|
|
13444
13471
|
continue;
|
|
13445
13472
|
}
|
|
13446
13473
|
const rel = relative(realRoot, fullPath);
|
|
13447
|
-
if (rel.startsWith("..") ||
|
|
13474
|
+
if (rel.startsWith("..") || isAbsolute(rel) || win32.isAbsolute(rel)) {
|
|
13448
13475
|
throw new Error(`extracted entry ${fullPath} escapes staging root`);
|
|
13449
13476
|
}
|
|
13450
13477
|
if (lst.isDirectory()) {
|
|
@@ -13547,11 +13574,23 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
|
|
|
13547
13574
|
log(`ORT extract: failed to copy optional ${libFile}: ${copyErr}`);
|
|
13548
13575
|
}
|
|
13549
13576
|
}
|
|
13577
|
+
const targetRoot = realpathSync(targetDir);
|
|
13550
13578
|
for (const link of symlinks) {
|
|
13551
13579
|
const dst = join6(targetDir, link.name);
|
|
13552
13580
|
try {
|
|
13553
13581
|
unlinkSync3(dst);
|
|
13554
13582
|
} catch {}
|
|
13583
|
+
const dstForContainment = join6(targetRoot, link.name);
|
|
13584
|
+
const resolvedTarget = resolve(dirname3(dstForContainment), link.target);
|
|
13585
|
+
if (!isPathInsideRoot(targetRoot, resolvedTarget)) {
|
|
13586
|
+
const message = `ONNX Runtime symlink ${link.name} points outside install dir: ${link.target}`;
|
|
13587
|
+
if (requiredLibs.has(link.name)) {
|
|
13588
|
+
rmSync2(targetDir, { recursive: true, force: true });
|
|
13589
|
+
throw new Error(message);
|
|
13590
|
+
}
|
|
13591
|
+
log(`ORT extract: skipping optional symlink ${link.name}: ${message}`);
|
|
13592
|
+
continue;
|
|
13593
|
+
}
|
|
13555
13594
|
try {
|
|
13556
13595
|
symlinkSync(link.target, dst);
|
|
13557
13596
|
} catch (symlinkErr) {
|
|
@@ -13654,9 +13693,8 @@ ${new Date().toISOString()}
|
|
|
13654
13693
|
}
|
|
13655
13694
|
const age = Date.now() - lockMtimeMs;
|
|
13656
13695
|
const ageWithinFresh = Math.abs(age) < STALE_LOCK_MS;
|
|
13657
|
-
const
|
|
13658
|
-
|
|
13659
|
-
if (skipLiveness ? ageWithinFresh : ownerAlive && ageWithinFresh) {
|
|
13696
|
+
const ownerAlive = owningPid !== null && isProcessAlive(owningPid);
|
|
13697
|
+
if (ownerAlive && ageWithinFresh) {
|
|
13660
13698
|
return false;
|
|
13661
13699
|
}
|
|
13662
13700
|
log(`[onnx] reclaiming install lock (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
|
|
@@ -13697,7 +13735,31 @@ function releaseLock(lockPath) {
|
|
|
13697
13735
|
warn(`[onnx] unexpected error releasing lock ${lockPath}: ${err}`);
|
|
13698
13736
|
}
|
|
13699
13737
|
}
|
|
13738
|
+
function tasklistPidFromCsvLine(line) {
|
|
13739
|
+
const quoted = line.match(/"([^"]*)"/g);
|
|
13740
|
+
if (quoted && quoted.length >= 2)
|
|
13741
|
+
return quoted[1].slice(1, -1);
|
|
13742
|
+
const cells = line.split(",").map((cell) => cell.trim().replace(/^"|"$/g, ""));
|
|
13743
|
+
return cells[1] ?? null;
|
|
13744
|
+
}
|
|
13745
|
+
function isWindowsProcessAlive(pid) {
|
|
13746
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
13747
|
+
return false;
|
|
13748
|
+
try {
|
|
13749
|
+
const output = execFileSync("tasklist.exe", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
13750
|
+
encoding: "utf8",
|
|
13751
|
+
timeout: 2000,
|
|
13752
|
+
windowsHide: true
|
|
13753
|
+
});
|
|
13754
|
+
const expected = String(pid);
|
|
13755
|
+
return output.split(/\r?\n/).some((line) => tasklistPidFromCsvLine(line) === expected);
|
|
13756
|
+
} catch {
|
|
13757
|
+
return false;
|
|
13758
|
+
}
|
|
13759
|
+
}
|
|
13700
13760
|
function isProcessAlive(pid) {
|
|
13761
|
+
if (process.platform === "win32")
|
|
13762
|
+
return isWindowsProcessAlive(pid);
|
|
13701
13763
|
try {
|
|
13702
13764
|
process.kill(pid, 0);
|
|
13703
13765
|
return true;
|
|
@@ -14632,7 +14694,7 @@ import {
|
|
|
14632
14694
|
// package.json
|
|
14633
14695
|
var package_default = {
|
|
14634
14696
|
name: "@cortexkit/aft-pi",
|
|
14635
|
-
version: "0.
|
|
14697
|
+
version: "0.34.0",
|
|
14636
14698
|
type: "module",
|
|
14637
14699
|
description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
|
|
14638
14700
|
main: "dist/index.js",
|
|
@@ -14651,10 +14713,11 @@ var package_default = {
|
|
|
14651
14713
|
typecheck: "tsc --noEmit",
|
|
14652
14714
|
test: "bun test src/__tests__/",
|
|
14653
14715
|
lint: "biome check src",
|
|
14654
|
-
prepublishOnly: "bun run build"
|
|
14716
|
+
prepublishOnly: "bun run build",
|
|
14717
|
+
"test:unit": "bun test src/__tests__ --path-ignore-patterns 'src/__tests__/e2e/**'"
|
|
14655
14718
|
},
|
|
14656
14719
|
dependencies: {
|
|
14657
|
-
"@cortexkit/aft-bridge": "0.
|
|
14720
|
+
"@cortexkit/aft-bridge": "0.34.0",
|
|
14658
14721
|
"@xterm/headless": "^5.5.0",
|
|
14659
14722
|
"comment-json": "^5.0.0",
|
|
14660
14723
|
diff: "^8.0.4",
|
|
@@ -14662,12 +14725,12 @@ var package_default = {
|
|
|
14662
14725
|
zod: "^4.1.8"
|
|
14663
14726
|
},
|
|
14664
14727
|
optionalDependencies: {
|
|
14665
|
-
"@cortexkit/aft-darwin-arm64": "0.
|
|
14666
|
-
"@cortexkit/aft-darwin-x64": "0.
|
|
14667
|
-
"@cortexkit/aft-linux-arm64": "0.
|
|
14668
|
-
"@cortexkit/aft-linux-x64": "0.
|
|
14669
|
-
"@cortexkit/aft-win32-arm64": "0.
|
|
14670
|
-
"@cortexkit/aft-win32-x64": "0.
|
|
14728
|
+
"@cortexkit/aft-darwin-arm64": "0.34.0",
|
|
14729
|
+
"@cortexkit/aft-darwin-x64": "0.34.0",
|
|
14730
|
+
"@cortexkit/aft-linux-arm64": "0.34.0",
|
|
14731
|
+
"@cortexkit/aft-linux-x64": "0.34.0",
|
|
14732
|
+
"@cortexkit/aft-win32-arm64": "0.34.0",
|
|
14733
|
+
"@cortexkit/aft-win32-x64": "0.34.0"
|
|
14671
14734
|
},
|
|
14672
14735
|
devDependencies: {
|
|
14673
14736
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -28851,6 +28914,8 @@ function date4(params) {
|
|
|
28851
28914
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
28852
28915
|
config(en_default());
|
|
28853
28916
|
// src/config.ts
|
|
28917
|
+
var FOREGROUND_WAIT_WINDOW_DEFAULT_MS = 8000;
|
|
28918
|
+
var FOREGROUND_WAIT_WINDOW_MIN_MS = 5000;
|
|
28854
28919
|
function resolveBashConfig(config2) {
|
|
28855
28920
|
const top = config2.bash;
|
|
28856
28921
|
const legacy = config2.experimental?.bash;
|
|
@@ -28858,13 +28923,16 @@ function resolveBashConfig(config2) {
|
|
|
28858
28923
|
const surfaceDefaultEnabled = surface !== "minimal";
|
|
28859
28924
|
const reminderEnabled = (typeof top === "object" && top !== null ? top.long_running_reminder_enabled : undefined) ?? legacy?.long_running_reminder_enabled;
|
|
28860
28925
|
const reminderInterval = (typeof top === "object" && top !== null ? top.long_running_reminder_interval_ms : undefined) ?? legacy?.long_running_reminder_interval_ms;
|
|
28926
|
+
const rawForegroundWait = typeof top === "object" && top !== null ? top.foreground_wait_window_ms : undefined;
|
|
28927
|
+
const foregroundWaitWindowMs = Math.max(FOREGROUND_WAIT_WINDOW_MIN_MS, rawForegroundWait ?? FOREGROUND_WAIT_WINDOW_DEFAULT_MS);
|
|
28861
28928
|
const base = {
|
|
28862
28929
|
enabled: false,
|
|
28863
28930
|
rewrite: false,
|
|
28864
28931
|
compress: false,
|
|
28865
28932
|
background: false,
|
|
28866
28933
|
long_running_reminder_enabled: reminderEnabled,
|
|
28867
|
-
long_running_reminder_interval_ms: reminderInterval
|
|
28934
|
+
long_running_reminder_interval_ms: reminderInterval,
|
|
28935
|
+
foreground_wait_window_ms: foregroundWaitWindowMs
|
|
28868
28936
|
};
|
|
28869
28937
|
if (top === false)
|
|
28870
28938
|
return base;
|
|
@@ -28918,13 +28986,15 @@ var CheckerEnum = exports_external.enum([
|
|
|
28918
28986
|
"staticcheck",
|
|
28919
28987
|
"none"
|
|
28920
28988
|
]);
|
|
28989
|
+
var ConfigureWarningsDeliveryEnum = exports_external.enum(["toast", "log", "chat"]);
|
|
28921
28990
|
var SemanticConfigSchema = exports_external.object({
|
|
28922
28991
|
backend: exports_external.enum(["fastembed", "openai_compatible", "ollama"]).optional(),
|
|
28923
28992
|
model: exports_external.string().trim().min(1).optional(),
|
|
28924
28993
|
base_url: exports_external.string().trim().min(1).optional(),
|
|
28925
28994
|
api_key_env: exports_external.string().trim().min(1).optional(),
|
|
28926
28995
|
timeout_ms: exports_external.number().int().positive().optional(),
|
|
28927
|
-
max_batch_size: exports_external.number().int().positive().optional()
|
|
28996
|
+
max_batch_size: exports_external.number().int().positive().optional(),
|
|
28997
|
+
max_files: exports_external.number().int().positive().optional()
|
|
28928
28998
|
});
|
|
28929
28999
|
var LspExtensionSchema = exports_external.string().trim().min(1).refine((value) => value.replace(/^\.+/, "").length > 0, {
|
|
28930
29000
|
message: "Extension must include characters other than leading dots"
|
|
@@ -28965,7 +29035,8 @@ var BashFeaturesSchema = exports_external.object({
|
|
|
28965
29035
|
compress: exports_external.boolean().optional(),
|
|
28966
29036
|
background: exports_external.boolean().optional(),
|
|
28967
29037
|
long_running_reminder_enabled: exports_external.boolean().optional(),
|
|
28968
|
-
long_running_reminder_interval_ms: exports_external.number().int().positive().optional()
|
|
29038
|
+
long_running_reminder_interval_ms: exports_external.number().int().positive().optional(),
|
|
29039
|
+
foreground_wait_window_ms: exports_external.number().int().positive().optional()
|
|
28969
29040
|
});
|
|
28970
29041
|
var BashConfigSchema = exports_external.union([exports_external.boolean(), BashFeaturesSchema]);
|
|
28971
29042
|
var InspectConfigSchema = exports_external.object({
|
|
@@ -28993,6 +29064,7 @@ var AftConfigSchema = exports_external.object({
|
|
|
28993
29064
|
validate_on_edit: exports_external.enum(["syntax", "full"]).optional(),
|
|
28994
29065
|
formatter: exports_external.record(exports_external.string(), FormatterEnum).optional(),
|
|
28995
29066
|
checker: exports_external.record(exports_external.string(), CheckerEnum).optional(),
|
|
29067
|
+
configure_warnings_delivery: ConfigureWarningsDeliveryEnum.optional(),
|
|
28996
29068
|
tool_surface: exports_external.enum(["minimal", "recommended", "all"]).optional(),
|
|
28997
29069
|
disabled_tools: exports_external.array(exports_external.string()).optional(),
|
|
28998
29070
|
restrict_to_project_root: exports_external.boolean().optional(),
|
|
@@ -29284,6 +29356,8 @@ function mergeSemanticConfig(base, override) {
|
|
|
29284
29356
|
projectSafe.timeout_ms = override.timeout_ms;
|
|
29285
29357
|
if (override?.max_batch_size !== undefined)
|
|
29286
29358
|
projectSafe.max_batch_size = override.max_batch_size;
|
|
29359
|
+
if (override?.max_files !== undefined)
|
|
29360
|
+
projectSafe.max_files = override.max_files;
|
|
29287
29361
|
const semantic = { ...base, ...projectSafe };
|
|
29288
29362
|
if (Object.values(semantic).every((v) => v === undefined))
|
|
29289
29363
|
return;
|
|
@@ -29381,6 +29455,7 @@ var PROJECT_SAFE_TOP_LEVEL_FIELDS = new Set([
|
|
|
29381
29455
|
"tool_surface",
|
|
29382
29456
|
"format_on_edit",
|
|
29383
29457
|
"validate_on_edit",
|
|
29458
|
+
"configure_warnings_delivery",
|
|
29384
29459
|
"search_index",
|
|
29385
29460
|
"semantic_search",
|
|
29386
29461
|
"inspect",
|
|
@@ -31368,1757 +31443,1806 @@ function registerShutdownCleanup(fn) {
|
|
|
31368
31443
|
|
|
31369
31444
|
// src/tools/ast.ts
|
|
31370
31445
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
31371
|
-
import { Type as
|
|
31446
|
+
import { Type as Type3 } from "typebox";
|
|
31372
31447
|
|
|
31373
|
-
// src/tools/
|
|
31448
|
+
// src/tools/hoisted.ts
|
|
31449
|
+
import { stat } from "node:fs/promises";
|
|
31374
31450
|
import { homedir as homedir8 } from "node:os";
|
|
31375
|
-
import {
|
|
31451
|
+
import { isAbsolute as isAbsolute2, relative as relative3, resolve as resolve3, sep } from "node:path";
|
|
31452
|
+
import {
|
|
31453
|
+
renderDiff
|
|
31454
|
+
} from "@earendil-works/pi-coding-agent";
|
|
31376
31455
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
31377
|
-
|
|
31378
|
-
|
|
31379
|
-
|
|
31380
|
-
|
|
31381
|
-
|
|
31456
|
+
import { Type as Type2 } from "typebox";
|
|
31457
|
+
|
|
31458
|
+
// src/tools/diff-format.ts
|
|
31459
|
+
import { diffLines } from "diff";
|
|
31460
|
+
var DEFAULT_CONTEXT_LINES = 4;
|
|
31461
|
+
function formatDiffForPi(oldContent, newContent, contextLines = DEFAULT_CONTEXT_LINES) {
|
|
31462
|
+
const parts = diffLines(oldContent, newContent);
|
|
31463
|
+
const output = [];
|
|
31464
|
+
const oldLines = oldContent.split(`
|
|
31465
|
+
`);
|
|
31466
|
+
const newLines = newContent.split(`
|
|
31467
|
+
`);
|
|
31468
|
+
const maxLineNum = Math.max(oldLines.length, newLines.length);
|
|
31469
|
+
const lineNumWidth = String(maxLineNum).length;
|
|
31470
|
+
const pad = (n) => String(n).padStart(lineNumWidth, " ");
|
|
31471
|
+
const blank = " ".repeat(lineNumWidth);
|
|
31472
|
+
let oldLineNum = 1;
|
|
31473
|
+
let newLineNum = 1;
|
|
31474
|
+
let lastWasChange = false;
|
|
31475
|
+
let firstChangedLine;
|
|
31476
|
+
for (let i = 0;i < parts.length; i++) {
|
|
31477
|
+
const part = parts[i];
|
|
31478
|
+
const raw = part.value.split(`
|
|
31479
|
+
`);
|
|
31480
|
+
if (raw[raw.length - 1] === "")
|
|
31481
|
+
raw.pop();
|
|
31482
|
+
if (part.added || part.removed) {
|
|
31483
|
+
if (firstChangedLine === undefined)
|
|
31484
|
+
firstChangedLine = newLineNum;
|
|
31485
|
+
for (const line of raw) {
|
|
31486
|
+
if (part.added) {
|
|
31487
|
+
output.push(`+${pad(newLineNum)} ${line}`);
|
|
31488
|
+
newLineNum++;
|
|
31489
|
+
} else {
|
|
31490
|
+
output.push(`-${pad(oldLineNum)} ${line}`);
|
|
31491
|
+
oldLineNum++;
|
|
31492
|
+
}
|
|
31493
|
+
}
|
|
31494
|
+
lastWasChange = true;
|
|
31495
|
+
continue;
|
|
31496
|
+
}
|
|
31497
|
+
const nextIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
|
31498
|
+
const hasLeading = lastWasChange;
|
|
31499
|
+
const hasTrailing = nextIsChange;
|
|
31500
|
+
if (hasLeading && hasTrailing) {
|
|
31501
|
+
if (raw.length <= contextLines * 2) {
|
|
31502
|
+
for (const line of raw) {
|
|
31503
|
+
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
31504
|
+
oldLineNum++;
|
|
31505
|
+
newLineNum++;
|
|
31506
|
+
}
|
|
31507
|
+
} else {
|
|
31508
|
+
for (const line of raw.slice(0, contextLines)) {
|
|
31509
|
+
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
31510
|
+
oldLineNum++;
|
|
31511
|
+
newLineNum++;
|
|
31512
|
+
}
|
|
31513
|
+
const skipped = raw.length - contextLines * 2;
|
|
31514
|
+
output.push(` ${blank} ...`);
|
|
31515
|
+
oldLineNum += skipped;
|
|
31516
|
+
newLineNum += skipped;
|
|
31517
|
+
for (const line of raw.slice(raw.length - contextLines)) {
|
|
31518
|
+
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
31519
|
+
oldLineNum++;
|
|
31520
|
+
newLineNum++;
|
|
31521
|
+
}
|
|
31522
|
+
}
|
|
31523
|
+
} else if (hasLeading) {
|
|
31524
|
+
const shown = raw.slice(0, contextLines);
|
|
31525
|
+
for (const line of shown) {
|
|
31526
|
+
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
31527
|
+
oldLineNum++;
|
|
31528
|
+
newLineNum++;
|
|
31529
|
+
}
|
|
31530
|
+
const skipped = raw.length - shown.length;
|
|
31531
|
+
if (skipped > 0) {
|
|
31532
|
+
output.push(` ${blank} ...`);
|
|
31533
|
+
oldLineNum += skipped;
|
|
31534
|
+
newLineNum += skipped;
|
|
31535
|
+
}
|
|
31536
|
+
} else if (hasTrailing) {
|
|
31537
|
+
const shownCount = Math.min(contextLines, raw.length);
|
|
31538
|
+
const shown = raw.slice(raw.length - shownCount);
|
|
31539
|
+
const skipped = raw.length - shown.length;
|
|
31540
|
+
if (skipped > 0) {
|
|
31541
|
+
output.push(` ${blank} ...`);
|
|
31542
|
+
oldLineNum += skipped;
|
|
31543
|
+
newLineNum += skipped;
|
|
31544
|
+
}
|
|
31545
|
+
for (const line of shown) {
|
|
31546
|
+
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
31547
|
+
oldLineNum++;
|
|
31548
|
+
newLineNum++;
|
|
31549
|
+
}
|
|
31550
|
+
} else {
|
|
31551
|
+
oldLineNum += raw.length;
|
|
31552
|
+
newLineNum += raw.length;
|
|
31553
|
+
}
|
|
31554
|
+
lastWasChange = false;
|
|
31555
|
+
}
|
|
31556
|
+
return { diff: output.join(`
|
|
31557
|
+
`), firstChangedLine };
|
|
31382
31558
|
}
|
|
31383
|
-
|
|
31384
|
-
|
|
31385
|
-
|
|
31386
|
-
|
|
31387
|
-
return
|
|
31559
|
+
|
|
31560
|
+
// src/tools/hoisted.ts
|
|
31561
|
+
var DIAGNOSTICS_PARAM_DESCRIPTION = "When true, wait up to 3 seconds for fresh LSP diagnostics on the edited file and include them in the result. Defaults to the configured `lsp.diagnostics_on_edit` value (false unless configured); per-call true/false overrides. Use aft_inspect to check diagnostics across a batch of edits or before tests/commits.";
|
|
31562
|
+
function diagnosticsOnEditDefault(ctx) {
|
|
31563
|
+
return ctx.config.lsp?.diagnostics_on_edit ?? false;
|
|
31388
31564
|
}
|
|
31389
|
-
function
|
|
31390
|
-
const
|
|
31391
|
-
|
|
31392
|
-
text.setText(`${theme.fg("toolTitle", theme.bold(toolName))}${suffix}`);
|
|
31393
|
-
return text;
|
|
31565
|
+
function containsPath(parent, child) {
|
|
31566
|
+
const rel = relative3(parent, child);
|
|
31567
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
31394
31568
|
}
|
|
31395
|
-
function
|
|
31396
|
-
if (!path2)
|
|
31397
|
-
return
|
|
31398
|
-
|
|
31569
|
+
function expandTilde(path2) {
|
|
31570
|
+
if (!path2 || !path2.startsWith("~"))
|
|
31571
|
+
return path2;
|
|
31572
|
+
if (path2 === "~")
|
|
31573
|
+
return homedir8();
|
|
31574
|
+
if (path2.startsWith(`~${sep}`) || path2.startsWith("~/")) {
|
|
31575
|
+
return resolve3(homedir8(), path2.slice(2));
|
|
31576
|
+
}
|
|
31577
|
+
return path2;
|
|
31399
31578
|
}
|
|
31400
|
-
function
|
|
31401
|
-
|
|
31402
|
-
|
|
31579
|
+
function externalDirectoryPromptTimeoutMs() {
|
|
31580
|
+
const raw = process.env.AFT_PI_EXTERNAL_PROMPT_TIMEOUT_MS;
|
|
31581
|
+
if (raw === undefined)
|
|
31582
|
+
return 30000;
|
|
31583
|
+
const parsed = Number.parseInt(raw, 10);
|
|
31584
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 30000;
|
|
31403
31585
|
}
|
|
31404
|
-
function
|
|
31405
|
-
if (
|
|
31406
|
-
return result.details;
|
|
31407
|
-
const text = collectTextContent(result);
|
|
31408
|
-
if (!text)
|
|
31586
|
+
async function assertExternalDirectoryPermission(extCtx, target, action = "modify", options = {}) {
|
|
31587
|
+
if (!target)
|
|
31409
31588
|
return;
|
|
31410
|
-
|
|
31411
|
-
|
|
31412
|
-
|
|
31589
|
+
const expanded = expandTilde(target);
|
|
31590
|
+
const absoluteTarget = isAbsolute2(expanded) ? expanded : resolve3(extCtx.cwd, expanded);
|
|
31591
|
+
if (containsPath(extCtx.cwd, absoluteTarget))
|
|
31592
|
+
return;
|
|
31593
|
+
if (options.restrictToProjectRoot === false)
|
|
31413
31594
|
return;
|
|
31595
|
+
const confirmFn = extCtx.ui?.confirm;
|
|
31596
|
+
if (extCtx.hasUI === false || !confirmFn) {
|
|
31597
|
+
throw new Error(`Permission denied: cannot prompt for ${action} outside the project (${absoluteTarget}).`);
|
|
31414
31598
|
}
|
|
31415
|
-
|
|
31416
|
-
|
|
31417
|
-
const
|
|
31418
|
-
|
|
31419
|
-
text.setText(`
|
|
31420
|
-
${theme.fg("error", message)}`);
|
|
31421
|
-
return text;
|
|
31422
|
-
}
|
|
31423
|
-
function renderSections(sections, context) {
|
|
31424
|
-
const container = reuseContainer(context.lastComponent);
|
|
31425
|
-
container.clear();
|
|
31426
|
-
const visibleSections = sections.filter((section) => section.trim().length > 0);
|
|
31427
|
-
if (visibleSections.length === 0)
|
|
31428
|
-
return container;
|
|
31429
|
-
container.addChild(new Spacer(1));
|
|
31430
|
-
visibleSections.forEach((section, index) => {
|
|
31431
|
-
if (index > 0)
|
|
31432
|
-
container.addChild(new Spacer(1));
|
|
31433
|
-
container.addChild(new Text(section, 0, 0));
|
|
31599
|
+
const timeoutMs = externalDirectoryPromptTimeoutMs();
|
|
31600
|
+
let timer;
|
|
31601
|
+
const timeoutPromise = new Promise((resolve4) => {
|
|
31602
|
+
timer = setTimeout(() => resolve4("timeout"), timeoutMs);
|
|
31434
31603
|
});
|
|
31435
|
-
return container;
|
|
31436
|
-
}
|
|
31437
|
-
function asRecord2(value) {
|
|
31438
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
31439
|
-
return;
|
|
31440
|
-
return value;
|
|
31441
|
-
}
|
|
31442
|
-
function asRecords(value) {
|
|
31443
|
-
return Array.isArray(value) ? value.map(asRecord2).filter(Boolean) : [];
|
|
31444
|
-
}
|
|
31445
|
-
function asString(value) {
|
|
31446
|
-
return typeof value === "string" ? value : undefined;
|
|
31447
|
-
}
|
|
31448
|
-
function asNumber(value) {
|
|
31449
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
31450
|
-
}
|
|
31451
|
-
function asBoolean(value) {
|
|
31452
|
-
return typeof value === "boolean" ? value : undefined;
|
|
31453
|
-
}
|
|
31454
|
-
function formatValue(value) {
|
|
31455
|
-
if (Array.isArray(value))
|
|
31456
|
-
return value.map(formatValue).join(", ");
|
|
31457
|
-
if (typeof value === "string")
|
|
31458
|
-
return value;
|
|
31459
|
-
if (typeof value === "number" || typeof value === "boolean")
|
|
31460
|
-
return String(value);
|
|
31461
|
-
if (value === null || value === undefined)
|
|
31462
|
-
return "—";
|
|
31463
31604
|
try {
|
|
31464
|
-
|
|
31465
|
-
|
|
31466
|
-
|
|
31467
|
-
|
|
31468
|
-
|
|
31469
|
-
|
|
31470
|
-
|
|
31471
|
-
|
|
31472
|
-
const file2 = getFile(item) ?? "(unknown file)";
|
|
31473
|
-
const current = groups.get(file2) ?? [];
|
|
31474
|
-
current.push(item);
|
|
31475
|
-
groups.set(file2, current);
|
|
31476
|
-
});
|
|
31477
|
-
return groups;
|
|
31478
|
-
}
|
|
31479
|
-
function formatTimestamp(value) {
|
|
31480
|
-
if (typeof value === "string" && value.length > 0)
|
|
31481
|
-
return value;
|
|
31482
|
-
if (typeof value !== "number" || !Number.isFinite(value))
|
|
31483
|
-
return;
|
|
31484
|
-
const millis = value > 1000000000000 ? value : value * 1000;
|
|
31485
|
-
const date5 = new Date(millis);
|
|
31486
|
-
if (Number.isNaN(date5.getTime()))
|
|
31487
|
-
return String(value);
|
|
31488
|
-
return date5.toISOString().replace("T", " ").replace(".000Z", "Z");
|
|
31489
|
-
}
|
|
31490
|
-
function formatUnifiedDiffForPi(unifiedDiff) {
|
|
31491
|
-
if (!unifiedDiff.trim())
|
|
31492
|
-
return "";
|
|
31493
|
-
const entries = [];
|
|
31494
|
-
const hunkHeader = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
|
|
31495
|
-
let oldLine = 1;
|
|
31496
|
-
let newLine = 1;
|
|
31497
|
-
for (const line of unifiedDiff.split(`
|
|
31498
|
-
`)) {
|
|
31499
|
-
if (!line)
|
|
31500
|
-
continue;
|
|
31501
|
-
if (line.startsWith("--- ") || line.startsWith("+++ "))
|
|
31502
|
-
continue;
|
|
31503
|
-
if (line.startsWith("\"))
|
|
31504
|
-
continue;
|
|
31505
|
-
const headerMatch = line.match(hunkHeader);
|
|
31506
|
-
if (headerMatch) {
|
|
31507
|
-
oldLine = Number(headerMatch[1]);
|
|
31508
|
-
newLine = Number(headerMatch[2]);
|
|
31509
|
-
continue;
|
|
31510
|
-
}
|
|
31511
|
-
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
31512
|
-
entries.push({ prefix: "+", line: newLine, text: line.slice(1) });
|
|
31513
|
-
newLine += 1;
|
|
31514
|
-
continue;
|
|
31515
|
-
}
|
|
31516
|
-
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
31517
|
-
entries.push({ prefix: "-", line: oldLine, text: line.slice(1) });
|
|
31518
|
-
oldLine += 1;
|
|
31519
|
-
continue;
|
|
31520
|
-
}
|
|
31521
|
-
if (line.startsWith(" ")) {
|
|
31522
|
-
entries.push({ prefix: " ", line: oldLine, text: line.slice(1) });
|
|
31523
|
-
oldLine += 1;
|
|
31524
|
-
newLine += 1;
|
|
31605
|
+
const result = await Promise.race([
|
|
31606
|
+
confirmFn("Allow external directory access?", `AFT wants to ${action} outside the project: ${absoluteTarget}`),
|
|
31607
|
+
timeoutPromise
|
|
31608
|
+
]);
|
|
31609
|
+
if (result === true)
|
|
31610
|
+
return;
|
|
31611
|
+
if (result === "timeout") {
|
|
31612
|
+
throw new Error(`Permission denied: external directory prompt timed out after ${timeoutMs}ms.`);
|
|
31525
31613
|
}
|
|
31526
|
-
|
|
31527
|
-
|
|
31528
|
-
|
|
31529
|
-
|
|
31530
|
-
return entries.map((entry) => `${entry.prefix}${String(entry.line).padStart(width, " ")} ${entry.text}`).join(`
|
|
31531
|
-
`);
|
|
31532
|
-
}
|
|
31533
|
-
function renderUnifiedDiff(unifiedDiff) {
|
|
31534
|
-
const piDiff = formatUnifiedDiffForPi(unifiedDiff);
|
|
31535
|
-
if (!piDiff)
|
|
31536
|
-
return "";
|
|
31537
|
-
try {
|
|
31538
|
-
return renderDiff(piDiff);
|
|
31539
|
-
} catch {
|
|
31540
|
-
return piDiff;
|
|
31614
|
+
throw new Error("Permission denied: external directory access was cancelled.");
|
|
31615
|
+
} finally {
|
|
31616
|
+
if (timer !== undefined)
|
|
31617
|
+
clearTimeout(timer);
|
|
31541
31618
|
}
|
|
31542
31619
|
}
|
|
31543
|
-
|
|
31544
|
-
|
|
31545
|
-
|
|
31546
|
-
|
|
31620
|
+
var ReadParams = Type2.Object({
|
|
31621
|
+
path: Type2.String({
|
|
31622
|
+
description: "Path to the file to read (absolute or relative to project root)"
|
|
31623
|
+
}),
|
|
31624
|
+
offset: optionalInt(1, Number.MAX_SAFE_INTEGER),
|
|
31625
|
+
limit: optionalInt(1, Number.MAX_SAFE_INTEGER)
|
|
31547
31626
|
});
|
|
31548
|
-
var
|
|
31549
|
-
|
|
31550
|
-
description: "
|
|
31627
|
+
var WriteParams = Type2.Object({
|
|
31628
|
+
filePath: Type2.String({
|
|
31629
|
+
description: "Path to the file to write (absolute or relative to project root)"
|
|
31551
31630
|
}),
|
|
31552
|
-
|
|
31553
|
-
|
|
31554
|
-
globs: Type2.Optional(Type2.Array(Type2.String(), { description: "Include/exclude globs (prefix `!` to exclude)" })),
|
|
31555
|
-
contextLines: Type2.Optional(Type2.Number({ description: "Number of context lines around each match" }))
|
|
31631
|
+
content: Type2.String({ description: "Full file contents to write" }),
|
|
31632
|
+
diagnostics: Type2.Optional(Type2.Boolean({ description: DIAGNOSTICS_PARAM_DESCRIPTION }))
|
|
31556
31633
|
});
|
|
31557
|
-
var
|
|
31558
|
-
|
|
31559
|
-
|
|
31560
|
-
|
|
31561
|
-
|
|
31562
|
-
|
|
31563
|
-
|
|
31634
|
+
var EditParams = Type2.Object({
|
|
31635
|
+
filePath: Type2.String({
|
|
31636
|
+
description: "Path to the file to edit (absolute or relative to project root)"
|
|
31637
|
+
}),
|
|
31638
|
+
oldString: Type2.Optional(Type2.String({ description: "Text to find (exact match, fuzzy fallback)" })),
|
|
31639
|
+
newString: Type2.Optional(Type2.String({ description: "Replacement text (omit to delete match)" })),
|
|
31640
|
+
replaceAll: Type2.Optional(Type2.Boolean({ description: "Replace every occurrence" })),
|
|
31641
|
+
occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER),
|
|
31642
|
+
appendContent: Type2.Optional(Type2.String({
|
|
31643
|
+
description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). When set, oldString/newString are ignored."
|
|
31644
|
+
})),
|
|
31645
|
+
diagnostics: Type2.Optional(Type2.Boolean({ description: DIAGNOSTICS_PARAM_DESCRIPTION }))
|
|
31564
31646
|
});
|
|
31565
|
-
|
|
31566
|
-
|
|
31567
|
-
|
|
31568
|
-
|
|
31569
|
-
}
|
|
31570
|
-
}
|
|
31571
|
-
|
|
31572
|
-
|
|
31573
|
-
|
|
31574
|
-
|
|
31575
|
-
|
|
31576
|
-
|
|
31577
|
-
|
|
31578
|
-
|
|
31579
|
-
|
|
31580
|
-
|
|
31581
|
-
|
|
31582
|
-
|
|
31583
|
-
|
|
31584
|
-
|
|
31585
|
-
|
|
31586
|
-
|
|
31587
|
-
|
|
31588
|
-
|
|
31589
|
-
|
|
31590
|
-
|
|
31591
|
-
|
|
31592
|
-
|
|
31593
|
-
|
|
31594
|
-
|
|
31595
|
-
|
|
31596
|
-
|
|
31597
|
-
|
|
31598
|
-
|
|
31599
|
-
|
|
31600
|
-
|
|
31647
|
+
var GrepParams = Type2.Object({
|
|
31648
|
+
pattern: Type2.String({ description: "Regex pattern to search for" }),
|
|
31649
|
+
path: Type2.Optional(Type2.String({
|
|
31650
|
+
description: "Path scope (file or directory; absolute or relative to project root)"
|
|
31651
|
+
})),
|
|
31652
|
+
include: Type2.Optional(Type2.String({ description: "Glob filter for included files (e.g. '*.ts,*.tsx')" })),
|
|
31653
|
+
caseSensitive: Type2.Optional(Type2.Boolean({ description: "Case-sensitive matching" })),
|
|
31654
|
+
contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER)
|
|
31655
|
+
});
|
|
31656
|
+
function registerHoistedTools(pi, ctx, surface) {
|
|
31657
|
+
if (surface.hoistRead) {
|
|
31658
|
+
pi.registerTool({
|
|
31659
|
+
name: "read",
|
|
31660
|
+
label: "read",
|
|
31661
|
+
description: "Read file contents with line numbers. Backed by AFT's indexed Rust reader — faster than the built-in `read` on large repos and correctly handles images/PDFs as attachments.",
|
|
31662
|
+
promptSnippet: "Read file contents (supports offset/limit for large files)",
|
|
31663
|
+
promptGuidelines: ["Use read to examine files instead of cat or sed."],
|
|
31664
|
+
parameters: ReadParams,
|
|
31665
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
31666
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
31667
|
+
const offset = coerceOptionalInt(params.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
|
|
31668
|
+
const limit = coerceOptionalInt(params.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
|
|
31669
|
+
const req = { file: params.path };
|
|
31670
|
+
if (offset !== undefined) {
|
|
31671
|
+
req.start_line = offset;
|
|
31672
|
+
if (limit !== undefined) {
|
|
31673
|
+
req.end_line = offset + limit - 1;
|
|
31674
|
+
}
|
|
31675
|
+
} else if (limit !== undefined) {
|
|
31676
|
+
req.end_line = limit;
|
|
31677
|
+
}
|
|
31678
|
+
const response = await callBridge(bridge, "read", req, extCtx);
|
|
31679
|
+
if (Array.isArray(response.entries)) {
|
|
31680
|
+
return textResult(response.entries.join(`
|
|
31681
|
+
`));
|
|
31682
|
+
}
|
|
31683
|
+
let text = response.content ?? "";
|
|
31684
|
+
const agentSpecifiedRange = offset !== undefined || limit !== undefined;
|
|
31685
|
+
const footer = formatReadFooter(agentSpecifiedRange, response);
|
|
31686
|
+
if (footer)
|
|
31687
|
+
text += footer;
|
|
31688
|
+
return textResult(text);
|
|
31689
|
+
}
|
|
31690
|
+
});
|
|
31601
31691
|
}
|
|
31602
|
-
|
|
31603
|
-
|
|
31604
|
-
|
|
31605
|
-
|
|
31606
|
-
|
|
31607
|
-
|
|
31608
|
-
|
|
31609
|
-
|
|
31610
|
-
|
|
31611
|
-
|
|
31612
|
-
|
|
31613
|
-
Object.entries(metaVars).forEach(([name, value]) => {
|
|
31614
|
-
lines.push(` ${theme.fg("muted", `${name} =`)} ${formatValue(value)}`);
|
|
31692
|
+
if (surface.hoistWrite) {
|
|
31693
|
+
pi.registerTool({
|
|
31694
|
+
name: "write",
|
|
31695
|
+
label: "write",
|
|
31696
|
+
description: "Write a file atomically with per-file backup and optional auto-format. Parent directories are created automatically. Overwrites existing files. Uses `filePath` (not `path`). Edits return as soon as the write completes unless `lsp.diagnostics_on_edit` or a per-call `diagnostics: true` requests legacy sync-wait behavior. Call `aft_inspect` afterward to check diagnostics across a batch of edits.",
|
|
31697
|
+
promptSnippet: "Create or overwrite files (uses filePath; auto-formats; diagnostics follow lsp.diagnostics_on_edit unless overridden)",
|
|
31698
|
+
promptGuidelines: ["Use write only for new files or complete rewrites."],
|
|
31699
|
+
parameters: WriteParams,
|
|
31700
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
31701
|
+
await assertExternalDirectoryPermission(extCtx, params.filePath, "modify", {
|
|
31702
|
+
restrictToProjectRoot: surface.restrictToProjectRoot
|
|
31615
31703
|
});
|
|
31704
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
31705
|
+
const response = await callBridge(bridge, "write", {
|
|
31706
|
+
file: params.filePath,
|
|
31707
|
+
content: params.content,
|
|
31708
|
+
diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
|
|
31709
|
+
include_diff_content: true
|
|
31710
|
+
}, extCtx);
|
|
31711
|
+
return buildMutationResult(params.filePath, response);
|
|
31712
|
+
},
|
|
31713
|
+
renderCall(args, theme, context) {
|
|
31714
|
+
return renderMutationCall("write", args?.filePath, theme, context);
|
|
31715
|
+
},
|
|
31716
|
+
renderResult(result, _options, theme, context) {
|
|
31717
|
+
return renderMutationResult(result, theme, context);
|
|
31616
31718
|
}
|
|
31617
|
-
const context = asRecords(match.context);
|
|
31618
|
-
context.forEach((ctxLine) => {
|
|
31619
|
-
const ctxNumber = asNumber(ctxLine.line) ?? 0;
|
|
31620
|
-
const prefix = ctxLine.is_match === true ? theme.fg("accent", ">") : theme.fg("muted", "|");
|
|
31621
|
-
lines.push(` ${prefix} ${ctxNumber}: ${asString(ctxLine.text) ?? ""}`);
|
|
31622
|
-
});
|
|
31623
31719
|
});
|
|
31624
|
-
sections.push(lines.join(`
|
|
31625
|
-
`));
|
|
31626
|
-
}
|
|
31627
|
-
return sections;
|
|
31628
|
-
}
|
|
31629
|
-
function buildAstReplaceSections(payload, theme) {
|
|
31630
|
-
const response = asRecord2(payload);
|
|
31631
|
-
if (!response)
|
|
31632
|
-
return [theme.fg("muted", "No AST replace results.")];
|
|
31633
|
-
const files = asRecords(response.files);
|
|
31634
|
-
const totalReplacements = asNumber(response.total_replacements) ?? 0;
|
|
31635
|
-
const totalFiles = asNumber(response.total_files) ?? files.length;
|
|
31636
|
-
const filesWithMatches = asNumber(response.files_with_matches);
|
|
31637
|
-
const dryRun = response.dry_run === true;
|
|
31638
|
-
const headerParts = [
|
|
31639
|
-
dryRun ? theme.fg("warning", "[dry run]") : theme.fg("success", "[applied]"),
|
|
31640
|
-
`${totalReplacements} replacement${totalReplacements === 1 ? "" : "s"}`,
|
|
31641
|
-
`${totalFiles} file${totalFiles === 1 ? "" : "s"}`,
|
|
31642
|
-
filesWithMatches !== undefined ? theme.fg("muted", `${filesWithMatches} matched`) : undefined
|
|
31643
|
-
];
|
|
31644
|
-
const sections = [headerParts.filter(Boolean).join(" ")];
|
|
31645
|
-
if (files.length === 0) {
|
|
31646
|
-
sections.push(theme.fg("muted", "No files changed."));
|
|
31647
|
-
appendScopeSections(response, sections, theme);
|
|
31648
|
-
appendHintSection(response, sections, theme);
|
|
31649
|
-
return sections;
|
|
31650
31720
|
}
|
|
31651
|
-
|
|
31652
|
-
const file2 = shortenPath(asString(fileResult.file) ?? "(unknown file)");
|
|
31653
|
-
const replacements = asNumber(fileResult.replacements) ?? 0;
|
|
31654
|
-
const error50 = asString(fileResult.error);
|
|
31655
|
-
const diff = asString(fileResult.diff);
|
|
31656
|
-
const lines = [
|
|
31657
|
-
`${theme.fg("accent", file2)} ${theme.fg("muted", `(${replacements} replacement${replacements === 1 ? "" : "s"})`)}`
|
|
31658
|
-
];
|
|
31659
|
-
if (error50) {
|
|
31660
|
-
lines.push(theme.fg("error", error50));
|
|
31661
|
-
} else if (diff) {
|
|
31662
|
-
const rendered = renderUnifiedDiff(diff);
|
|
31663
|
-
lines.push(rendered || theme.fg("muted", "No diff available."));
|
|
31664
|
-
} else {
|
|
31665
|
-
const backupId = asString(fileResult.backup_id);
|
|
31666
|
-
lines.push(backupId ? `${theme.fg("success", "saved")} ${theme.fg("muted", backupId)}` : theme.fg("success", "saved"));
|
|
31667
|
-
}
|
|
31668
|
-
sections.push(lines.join(`
|
|
31669
|
-
`));
|
|
31670
|
-
});
|
|
31671
|
-
return sections;
|
|
31672
|
-
}
|
|
31673
|
-
function renderAstCall(toolName, args, theme, context) {
|
|
31674
|
-
const lang = theme.fg("accent", args.lang);
|
|
31675
|
-
const summary = toolName === "ast_grep_replace" ? `${lang} ${theme.fg("toolOutput", `${args.pattern} → ${args.rewrite}`)}` : `${lang} ${theme.fg("toolOutput", args.pattern)}`;
|
|
31676
|
-
return renderToolCall(toolName === "ast_grep_replace" ? "ast replace" : "ast search", summary, theme, context);
|
|
31677
|
-
}
|
|
31678
|
-
function renderAstResult(toolName, result, theme, context) {
|
|
31679
|
-
if (context.isError) {
|
|
31680
|
-
return renderErrorResult(result, `${toolName} failed`, theme, context);
|
|
31681
|
-
}
|
|
31682
|
-
const payload = extractStructuredPayload(result);
|
|
31683
|
-
if (!payload) {
|
|
31684
|
-
const text = collectTextContent(result);
|
|
31685
|
-
return renderSections([text || theme.fg("muted", "No result.")], context);
|
|
31686
|
-
}
|
|
31687
|
-
const sections = toolName === "ast_grep_replace" ? buildAstReplaceSections(payload, theme) : buildAstSearchSections(payload, theme);
|
|
31688
|
-
return renderSections(sections, context);
|
|
31689
|
-
}
|
|
31690
|
-
function registerAstTools(pi, ctx, surface) {
|
|
31691
|
-
if (surface.astSearch) {
|
|
31721
|
+
if (surface.hoistEdit) {
|
|
31692
31722
|
pi.registerTool({
|
|
31693
|
-
name: "
|
|
31694
|
-
label: "
|
|
31695
|
-
description: "
|
|
31696
|
-
|
|
31723
|
+
name: "edit",
|
|
31724
|
+
label: "edit",
|
|
31725
|
+
description: "Find-and-replace edit with progressive fuzzy matching (handles whitespace and Unicode drift). Uses `filePath`, `oldString`, `newString`. Errors on multiple matches — use `occurrence` to pick one, or `replaceAll: true`. Edits return as soon as the write completes unless `lsp.diagnostics_on_edit` or a per-call `diagnostics: true` requests legacy sync-wait behavior. Call `aft_inspect` afterward to check diagnostics across a batch of edits.",
|
|
31726
|
+
promptSnippet: "Targeted find-and-replace (uses filePath/oldString/newString; occurrence or replaceAll for disambiguation; fuzzy whitespace matching). Pass appendContent to append to a file (creates if missing). Diagnostics follow lsp.diagnostics_on_edit unless overridden.",
|
|
31727
|
+
promptGuidelines: [
|
|
31728
|
+
"Prefer edit over write when changing part of an existing file.",
|
|
31729
|
+
"Include enough surrounding context in oldString to make the match unique, or set replaceAll/occurrence explicitly.",
|
|
31730
|
+
"Use appendContent (instead of read+write) when adding text to the end of a file."
|
|
31731
|
+
],
|
|
31732
|
+
parameters: EditParams,
|
|
31697
31733
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
31734
|
+
await assertExternalDirectoryPermission(extCtx, params.filePath, "modify", {
|
|
31735
|
+
restrictToProjectRoot: surface.restrictToProjectRoot
|
|
31736
|
+
});
|
|
31698
31737
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
31738
|
+
if (typeof params.appendContent === "string") {
|
|
31739
|
+
const req2 = {
|
|
31740
|
+
op: "append",
|
|
31741
|
+
file: params.filePath,
|
|
31742
|
+
append_content: params.appendContent,
|
|
31743
|
+
diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
|
|
31744
|
+
include_diff_content: true
|
|
31745
|
+
};
|
|
31746
|
+
const response2 = await callBridge(bridge, "edit_match", req2, extCtx);
|
|
31747
|
+
return buildMutationResult(params.filePath, response2);
|
|
31748
|
+
}
|
|
31699
31749
|
const req = {
|
|
31700
|
-
|
|
31701
|
-
|
|
31750
|
+
file: params.filePath,
|
|
31751
|
+
match: params.oldString ?? "",
|
|
31752
|
+
replacement: params.newString ?? "",
|
|
31753
|
+
diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
|
|
31754
|
+
include_diff_content: true
|
|
31702
31755
|
};
|
|
31703
|
-
if (
|
|
31704
|
-
req.
|
|
31705
|
-
|
|
31706
|
-
|
|
31707
|
-
|
|
31708
|
-
|
|
31709
|
-
|
|
31710
|
-
return textResult(response.text ?? JSON.stringify(response));
|
|
31756
|
+
if (params.replaceAll === true)
|
|
31757
|
+
req.replace_all = true;
|
|
31758
|
+
const occurrence = coerceOptionalInt(params.occurrence, "occurrence", 0, Number.MAX_SAFE_INTEGER);
|
|
31759
|
+
if (occurrence !== undefined)
|
|
31760
|
+
req.occurrence = occurrence;
|
|
31761
|
+
const response = await callBridge(bridge, "edit_match", req, extCtx);
|
|
31762
|
+
return buildMutationResult(params.filePath, response);
|
|
31711
31763
|
},
|
|
31712
31764
|
renderCall(args, theme, context) {
|
|
31713
|
-
return
|
|
31765
|
+
return renderMutationCall("edit", args?.filePath, theme, context);
|
|
31714
31766
|
},
|
|
31715
31767
|
renderResult(result, _options, theme, context) {
|
|
31716
|
-
return
|
|
31768
|
+
return renderMutationResult(result, theme, context);
|
|
31717
31769
|
}
|
|
31718
31770
|
});
|
|
31719
31771
|
}
|
|
31720
|
-
if (surface.
|
|
31772
|
+
if (surface.hoistGrep) {
|
|
31721
31773
|
pi.registerTool({
|
|
31722
|
-
name: "
|
|
31723
|
-
label: "
|
|
31724
|
-
description: "
|
|
31725
|
-
|
|
31774
|
+
name: "grep",
|
|
31775
|
+
label: "grep",
|
|
31776
|
+
description: "Search for a regex pattern across files. Uses AFT's trigram index inside the project root for fast repeated queries, and falls back to ripgrep for paths outside the project root.",
|
|
31777
|
+
promptSnippet: "Fast regex search across files (trigram-indexed inside the project root)",
|
|
31778
|
+
promptGuidelines: ["Prefer grep over bash-invoked find/rg for in-project searches."],
|
|
31779
|
+
parameters: GrepParams,
|
|
31726
31780
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
31727
31781
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
31728
|
-
const req = {
|
|
31729
|
-
|
|
31730
|
-
|
|
31731
|
-
|
|
31732
|
-
|
|
31733
|
-
|
|
31734
|
-
|
|
31735
|
-
if (
|
|
31736
|
-
req.
|
|
31737
|
-
|
|
31738
|
-
|
|
31739
|
-
|
|
31740
|
-
|
|
31741
|
-
|
|
31742
|
-
|
|
31743
|
-
|
|
31744
|
-
|
|
31745
|
-
return renderAstResult("ast_grep_replace", result, theme, context);
|
|
31782
|
+
const req = { pattern: params.pattern };
|
|
31783
|
+
if (params.path) {
|
|
31784
|
+
await assertExternalDirectoryPermission(extCtx, params.path, "search", {
|
|
31785
|
+
restrictToProjectRoot: surface.restrictToProjectRoot
|
|
31786
|
+
});
|
|
31787
|
+
req.path = await resolvePathArg(extCtx.cwd, params.path);
|
|
31788
|
+
}
|
|
31789
|
+
if (params.include)
|
|
31790
|
+
req.include = splitIncludeGlobs(params.include);
|
|
31791
|
+
if (params.caseSensitive !== undefined)
|
|
31792
|
+
req.case_sensitive = params.caseSensitive;
|
|
31793
|
+
const contextLines = coerceOptionalInt(params.contextLines, "contextLines", 1, Number.MAX_SAFE_INTEGER);
|
|
31794
|
+
if (contextLines !== undefined)
|
|
31795
|
+
req.context_lines = contextLines;
|
|
31796
|
+
const response = await callBridge(bridge, "grep", req, extCtx);
|
|
31797
|
+
const text = response.text ?? "";
|
|
31798
|
+
return textResult(text);
|
|
31746
31799
|
}
|
|
31747
31800
|
});
|
|
31748
31801
|
}
|
|
31749
31802
|
}
|
|
31803
|
+
function buildMutationResult(filePath, response) {
|
|
31804
|
+
const diffObj = response.diff;
|
|
31805
|
+
const additions = diffObj?.additions ?? 0;
|
|
31806
|
+
const deletions = diffObj?.deletions ?? 0;
|
|
31807
|
+
const replacements = response.replacements;
|
|
31808
|
+
const diagnostics = response.lsp_diagnostics;
|
|
31809
|
+
const truncated = diffObj?.truncated === true;
|
|
31810
|
+
const noOp = response.no_op === true;
|
|
31811
|
+
const formatted = response.formatted;
|
|
31812
|
+
const formatSkippedReason = response.format_skipped_reason;
|
|
31813
|
+
const globFormatSkipReasons = response.format_skip_reasons;
|
|
31814
|
+
let diffText;
|
|
31815
|
+
let firstChangedLine;
|
|
31816
|
+
if (diffObj && !truncated && typeof diffObj.before === "string" && typeof diffObj.after === "string") {
|
|
31817
|
+
const piDiff = formatDiffForPi(diffObj.before, diffObj.after);
|
|
31818
|
+
diffText = piDiff.diff;
|
|
31819
|
+
firstChangedLine = piDiff.firstChangedLine;
|
|
31820
|
+
}
|
|
31821
|
+
const summaryHeader = replacements !== undefined ? `Edited ${filePath} (+${additions}/-${deletions}, ${replacements} replacement${replacements === 1 ? "" : "s"})` : `Wrote ${filePath} (+${additions}/-${deletions})`;
|
|
31822
|
+
let text = summaryHeader;
|
|
31823
|
+
if (noOp) {
|
|
31824
|
+
text += `
|
|
31750
31825
|
|
|
31751
|
-
|
|
31752
|
-
import * as fs3 from "node:fs/promises";
|
|
31753
|
-
import { Container as Container2, Spacer as Spacer2, Text as Text2 } from "@earendil-works/pi-tui";
|
|
31754
|
-
import { Type as Type3 } from "typebox";
|
|
31755
|
-
var FOREGROUND_WAIT_WINDOW_MS = 5000;
|
|
31756
|
-
var FOREGROUND_POLL_INTERVAL_MS = 100;
|
|
31757
|
-
var BASH_WAIT_POLL_INTERVAL_MS = 100;
|
|
31758
|
-
var DEFAULT_BASH_STATUS_WAIT_TIMEOUT_MS = 30000;
|
|
31759
|
-
var MAX_BASH_STATUS_WAIT_TIMEOUT_MS = 300000;
|
|
31760
|
-
var BASH_TRANSPORT_TIMEOUT_MS = 30000;
|
|
31761
|
-
var BashParams = Type3.Object({
|
|
31762
|
-
command: Type3.String({
|
|
31763
|
-
description: "Shell command to execute. Supports pipes, redirections, and shell syntax."
|
|
31764
|
-
}),
|
|
31765
|
-
timeout: optionalInt(1, Number.MAX_SAFE_INTEGER),
|
|
31766
|
-
workdir: Type3.Optional(Type3.String({
|
|
31767
|
-
description: "Working directory for command execution. Relative paths resolve against the project root. Defaults to the current session's working directory."
|
|
31768
|
-
})),
|
|
31769
|
-
description: Type3.Optional(Type3.String({
|
|
31770
|
-
description: "Human-readable description shown in UI logs. Helps users understand what the command does without reading shell syntax."
|
|
31771
|
-
})),
|
|
31772
|
-
background: Type3.Optional(Type3.Boolean({
|
|
31773
|
-
description: "Spawn command in background and return immediately with a task_id. Use bash_status to poll completion and bash_kill to terminate. Ideal for long-running tasks like builds or dev servers."
|
|
31774
|
-
})),
|
|
31775
|
-
compressed: Type3.Optional(Type3.Boolean({
|
|
31776
|
-
description: "Compress output by removing ANSI codes, carriage returns, and excessive blank lines. Default: true. Set to false for raw terminal output including color codes."
|
|
31777
|
-
})),
|
|
31778
|
-
pty: Type3.Optional(Type3.Boolean({
|
|
31779
|
-
description: 'Spawn the command in a real PTY for interactive programs. Implies background: true automatically. Inspect with bash_status({ task_id, output_mode: "screen" }) and send input with bash_write.'
|
|
31780
|
-
})),
|
|
31781
|
-
ptyRows: optionalInt(1, 60),
|
|
31782
|
-
ptyCols: optionalInt(1, 140)
|
|
31783
|
-
});
|
|
31784
|
-
var BashTaskParams = Type3.Object({
|
|
31785
|
-
task_id: Type3.String({
|
|
31786
|
-
description: "Background bash task id returned by bash({ background: true })."
|
|
31787
|
-
})
|
|
31788
|
-
});
|
|
31789
|
-
var BashStatusParams = Type3.Object({
|
|
31790
|
-
task_id: Type3.String({
|
|
31791
|
-
description: "Background bash task id returned by bash({ background: true })."
|
|
31792
|
-
}),
|
|
31793
|
-
output_mode: Type3.Optional(Type3.Union([Type3.Literal("screen"), Type3.Literal("raw"), Type3.Literal("both")], {
|
|
31794
|
-
description: "PTY output rendering mode. Defaults to screen for PTY tasks and preserves existing behavior for piped tasks when omitted."
|
|
31795
|
-
}))
|
|
31796
|
-
});
|
|
31797
|
-
var BashWatchParams = Type3.Object({
|
|
31798
|
-
task_id: Type3.String({
|
|
31799
|
-
description: "Background bash task id returned by bash({ background: true })."
|
|
31800
|
-
}),
|
|
31801
|
-
pattern: Type3.Optional(Type3.Union([Type3.String(), Type3.Object({ regex: Type3.String() })])),
|
|
31802
|
-
background: Type3.Optional(Type3.Boolean()),
|
|
31803
|
-
timeout_ms: optionalInt(1, MAX_BASH_STATUS_WAIT_TIMEOUT_MS),
|
|
31804
|
-
once: Type3.Optional(Type3.Boolean())
|
|
31805
|
-
});
|
|
31806
|
-
var BashWriteParams = Type3.Object({
|
|
31807
|
-
task_id: Type3.String({
|
|
31808
|
-
description: "Background PTY task id returned by bash({ pty: true, background: true })."
|
|
31809
|
-
}),
|
|
31810
|
-
input: Type3.Union([
|
|
31811
|
-
Type3.String(),
|
|
31812
|
-
Type3.Array(Type3.Union([
|
|
31813
|
-
Type3.String(),
|
|
31814
|
-
Type3.Object({
|
|
31815
|
-
key: Type3.String({
|
|
31816
|
-
description: "Named control key, e.g. 'esc', 'enter', 'up', 'ctrl-c'. Case-insensitive."
|
|
31817
|
-
})
|
|
31818
|
-
})
|
|
31819
|
-
]))
|
|
31820
|
-
], {
|
|
31821
|
-
description: "Either a string of verbatim bytes (e.g. 'print(1)\\n') OR an array mixing strings " + "and { key: '<name>' } objects for atomic text+key sequences. " + "Example: [ 'iHello', { key: 'esc' }, ':wq', { key: 'enter' } ]. " + "Allowed key names: enter, return, tab, space, backspace, esc, escape, up, down, " + "left, right, home, end, page-up, page-down, delete, insert, f1..f12, ctrl-a..ctrl-z."
|
|
31822
|
-
})
|
|
31823
|
-
});
|
|
31824
|
-
function truncateToVisualLines(text, maxLines) {
|
|
31825
|
-
const lines = text.split(`
|
|
31826
|
-
`);
|
|
31827
|
-
if (lines.length <= maxLines)
|
|
31828
|
-
return text;
|
|
31829
|
-
return lines.slice(-maxLines).join(`
|
|
31830
|
-
`);
|
|
31831
|
-
}
|
|
31832
|
-
function reuseText2(last) {
|
|
31833
|
-
return last instanceof Text2 ? last : new Text2("", 0, 0);
|
|
31834
|
-
}
|
|
31835
|
-
function reuseContainer2(last) {
|
|
31836
|
-
return last instanceof Container2 ? last : new Container2;
|
|
31837
|
-
}
|
|
31838
|
-
function getBashSpawnHook(pi) {
|
|
31839
|
-
const api2 = pi;
|
|
31840
|
-
if (typeof api2.getHook === "function") {
|
|
31841
|
-
return api2.getHook("bashSpawn");
|
|
31826
|
+
Note: no net file change — the match was found and applied, but the file content is byte-identical to before. Likely causes: oldString and newString are identical, or a formatter normalized the change away.`;
|
|
31842
31827
|
}
|
|
31843
|
-
|
|
31828
|
+
const skipNote = formatSkipReasonNote(formatSkippedReason);
|
|
31829
|
+
if (skipNote)
|
|
31830
|
+
text += `
|
|
31831
|
+
|
|
31832
|
+
${skipNote}`;
|
|
31833
|
+
const globSkipNote = formatGlobSkipReasonsNote(globFormatSkipReasons);
|
|
31834
|
+
if (globSkipNote)
|
|
31835
|
+
text += `
|
|
31836
|
+
|
|
31837
|
+
${globSkipNote}`;
|
|
31838
|
+
if (diagnostics && diagnostics.length > 0) {
|
|
31839
|
+
text += `
|
|
31840
|
+
|
|
31841
|
+
LSP diagnostics:
|
|
31842
|
+
${formatDiagnosticsText(diagnostics)}`;
|
|
31843
|
+
}
|
|
31844
|
+
return {
|
|
31845
|
+
content: [{ type: "text", text }],
|
|
31846
|
+
details: {
|
|
31847
|
+
diff: diffText,
|
|
31848
|
+
firstChangedLine,
|
|
31849
|
+
additions,
|
|
31850
|
+
deletions,
|
|
31851
|
+
replacements,
|
|
31852
|
+
diagnostics,
|
|
31853
|
+
truncated: truncated || undefined,
|
|
31854
|
+
formatted,
|
|
31855
|
+
formatSkippedReason,
|
|
31856
|
+
noOp: noOp || undefined
|
|
31857
|
+
}
|
|
31858
|
+
};
|
|
31844
31859
|
}
|
|
31845
|
-
function
|
|
31846
|
-
|
|
31847
|
-
|
|
31848
|
-
|
|
31849
|
-
|
|
31850
|
-
|
|
31851
|
-
|
|
31852
|
-
promptGuidelines: [
|
|
31853
|
-
"Use bash only when a dedicated AFT tool is not a better fit.",
|
|
31854
|
-
"Set compressed: false when you need ANSI color codes in the output."
|
|
31855
|
-
],
|
|
31856
|
-
parameters: BashParams,
|
|
31857
|
-
async execute(_toolCallId, params, _signal, onUpdate, extCtx) {
|
|
31858
|
-
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
31859
|
-
const timeout = coerceOptionalInt(params.timeout, "timeout", 1, Number.MAX_SAFE_INTEGER);
|
|
31860
|
-
const ptyRows = coerceOptionalInt(params.ptyRows, "ptyRows", 1, 60);
|
|
31861
|
-
const ptyCols = coerceOptionalInt(params.ptyCols, "ptyCols", 1, 140);
|
|
31862
|
-
const effectiveBackground = params.background === true || params.pty === true;
|
|
31863
|
-
let spawnContext = {
|
|
31864
|
-
command: params.command,
|
|
31865
|
-
cwd: params.workdir
|
|
31866
|
-
};
|
|
31867
|
-
if (spawnHook) {
|
|
31868
|
-
try {
|
|
31869
|
-
spawnContext = await spawnHook(spawnContext);
|
|
31870
|
-
} catch (hookErr) {
|
|
31871
|
-
throw new Error(`BashSpawnHook failed: ${hookErr instanceof Error ? hookErr.message : String(hookErr)}`);
|
|
31872
|
-
}
|
|
31873
|
-
}
|
|
31874
|
-
let streamed = "";
|
|
31875
|
-
const response = await callBridge(bridge, "bash", {
|
|
31876
|
-
command: spawnContext.command,
|
|
31877
|
-
timeout,
|
|
31878
|
-
workdir: spawnContext.cwd ?? params.workdir,
|
|
31879
|
-
env: spawnContext.env,
|
|
31880
|
-
description: params.description,
|
|
31881
|
-
background: effectiveBackground,
|
|
31882
|
-
notify_on_completion: effectiveBackground,
|
|
31883
|
-
compressed: params.compressed,
|
|
31884
|
-
pty: params.pty,
|
|
31885
|
-
pty_rows: ptyRows,
|
|
31886
|
-
pty_cols: ptyCols
|
|
31887
|
-
}, extCtx, {
|
|
31888
|
-
transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS,
|
|
31889
|
-
keepBridgeOnTimeout: true,
|
|
31890
|
-
onProgress: ({ text }) => {
|
|
31891
|
-
streamed += text;
|
|
31892
|
-
const displayText = truncateToVisualLines(streamed, 100);
|
|
31893
|
-
onUpdate?.(bashResult(displayText, { streaming: true }));
|
|
31894
|
-
}
|
|
31895
|
-
}).catch((err) => {
|
|
31896
|
-
if (err instanceof Error && err.message.includes("permission_required")) {
|
|
31897
|
-
throw new Error("Permission ask reached Pi adapter — this is a bug. Pi has no permission system.");
|
|
31898
|
-
}
|
|
31899
|
-
throw err;
|
|
31900
|
-
});
|
|
31901
|
-
if (response.success === false) {
|
|
31902
|
-
throw new Error(response.message ?? "bash failed");
|
|
31903
|
-
}
|
|
31904
|
-
const taskId = response.task_id;
|
|
31905
|
-
if (response.status === "running" && taskId) {
|
|
31906
|
-
if (effectiveBackground) {
|
|
31907
|
-
trackBgTask(resolveSessionId(extCtx), taskId);
|
|
31908
|
-
return bashResult(formatBackgroundLaunch(taskId, params.pty === true), {
|
|
31909
|
-
task_id: taskId
|
|
31910
|
-
});
|
|
31911
|
-
}
|
|
31912
|
-
const waitTimeoutMs = timeout !== undefined ? Math.min(timeout, FOREGROUND_WAIT_WINDOW_MS) : FOREGROUND_WAIT_WINDOW_MS;
|
|
31913
|
-
const startedAt = Date.now();
|
|
31914
|
-
while (true) {
|
|
31915
|
-
const status = await callBridge(bridge, "bash_status", { task_id: taskId }, extCtx);
|
|
31916
|
-
if (status.success === false) {
|
|
31917
|
-
throw new Error(status.message ?? "bash_status failed");
|
|
31918
|
-
}
|
|
31919
|
-
if (isTerminalStatus(status.status)) {
|
|
31920
|
-
return bashResult(formatForegroundResult(status), {
|
|
31921
|
-
exit_code: status.exit_code,
|
|
31922
|
-
duration_ms: status.duration_ms,
|
|
31923
|
-
truncated: status.output_truncated,
|
|
31924
|
-
output_path: status.output_path,
|
|
31925
|
-
task_id: taskId
|
|
31926
|
-
});
|
|
31927
|
-
}
|
|
31928
|
-
if (Date.now() - startedAt >= waitTimeoutMs) {
|
|
31929
|
-
const promoted = await callBridge(bridge, "bash_promote", { task_id: taskId }, extCtx);
|
|
31930
|
-
if (promoted.success === false) {
|
|
31931
|
-
throw new Error(promoted.message ?? "bash_promote failed");
|
|
31932
|
-
}
|
|
31933
|
-
trackBgTask(resolveSessionId(extCtx), taskId);
|
|
31934
|
-
return bashResult(formatPromotionMessage(taskId, params.timeout), {
|
|
31935
|
-
task_id: taskId
|
|
31936
|
-
});
|
|
31937
|
-
}
|
|
31938
|
-
await sleep(FOREGROUND_POLL_INTERVAL_MS);
|
|
31939
|
-
}
|
|
31940
|
-
}
|
|
31941
|
-
const details = {
|
|
31942
|
-
exit_code: response.exit_code,
|
|
31943
|
-
duration_ms: response.duration_ms,
|
|
31944
|
-
truncated: response.truncated,
|
|
31945
|
-
output_path: response.output_path,
|
|
31946
|
-
task_id: taskId
|
|
31947
|
-
};
|
|
31948
|
-
const output = response.output ?? "";
|
|
31949
|
-
return bashResult(output, details);
|
|
31950
|
-
},
|
|
31951
|
-
renderCall(args, theme, context) {
|
|
31952
|
-
return renderBashCall(args?.command, args?.description, theme, context);
|
|
31953
|
-
},
|
|
31954
|
-
renderResult(result, _options, theme, context) {
|
|
31955
|
-
return renderBashResult(result, theme, context);
|
|
31956
|
-
}
|
|
31957
|
-
});
|
|
31958
|
-
pi.registerTool(createBashStatusTool(ctx));
|
|
31959
|
-
pi.registerTool(createBashWatchTool(ctx));
|
|
31960
|
-
pi.registerTool(createBashWriteTool(ctx));
|
|
31961
|
-
pi.registerTool(createBashKillTool(ctx));
|
|
31860
|
+
function formatGlobSkipReasonsNote(reasons) {
|
|
31861
|
+
if (!Array.isArray(reasons))
|
|
31862
|
+
return;
|
|
31863
|
+
const actionable = reasons.filter((reason) => typeof reason === "string").filter((reason) => ["formatter_not_installed", "formatter_excluded_path", "timeout", "error"].includes(reason));
|
|
31864
|
+
if (actionable.length === 0)
|
|
31865
|
+
return;
|
|
31866
|
+
return `Note: formatter skipped some glob edit result file(s): ${[...new Set(actionable)].sort().join(", ")}. See per-file format_skipped_reason values for details.`;
|
|
31962
31867
|
}
|
|
31963
|
-
function
|
|
31964
|
-
|
|
31965
|
-
|
|
31868
|
+
function formatSkipReasonNote(reason) {
|
|
31869
|
+
switch (reason) {
|
|
31870
|
+
case "formatter_not_installed":
|
|
31871
|
+
return "Note: formatter binary not installed; file written unformatted.";
|
|
31872
|
+
case "timeout":
|
|
31873
|
+
return "Note: formatter timed out; file written unformatted. Raise formatter_timeout_secs or check the formatter for hangs.";
|
|
31874
|
+
case "formatter_excluded_path":
|
|
31875
|
+
return "Note: formatter is configured to ignore this path (e.g. biome.json files.includes, .prettierignore). File written unformatted.";
|
|
31876
|
+
case "error":
|
|
31877
|
+
return "Note: formatter exited with an unrecognized error; file written unformatted.";
|
|
31878
|
+
default:
|
|
31879
|
+
return;
|
|
31966
31880
|
}
|
|
31967
|
-
return `Background task started: ${taskId}. A completion reminder will be delivered automatically; don't poll bash_status.`;
|
|
31968
|
-
}
|
|
31969
|
-
function formatPromotionMessage(taskId, timeout) {
|
|
31970
|
-
const waited = timeout !== undefined ? Math.min(timeout, FOREGROUND_WAIT_WINDOW_MS) : FOREGROUND_WAIT_WINDOW_MS;
|
|
31971
|
-
return `Foreground bash didn't finish within ${waited}ms and was promoted to background: ${taskId}. A completion reminder will be delivered automatically; use bash_status({ task_id: "${taskId}" }) to inspect output or bash_kill({ task_id: "${taskId}" }) to terminate.`;
|
|
31972
31881
|
}
|
|
31973
|
-
function
|
|
31974
|
-
|
|
31975
|
-
|
|
31976
|
-
|
|
31977
|
-
|
|
31978
|
-
|
|
31979
|
-
|
|
31980
|
-
|
|
31981
|
-
|
|
31982
|
-
|
|
31882
|
+
function formatDiagnosticsText(diagnostics) {
|
|
31883
|
+
try {
|
|
31884
|
+
return diagnostics.map((d) => {
|
|
31885
|
+
if (d && typeof d === "object") {
|
|
31886
|
+
const obj = d;
|
|
31887
|
+
const line = obj.line ?? obj.startLine ?? "?";
|
|
31888
|
+
const severity = obj.severity ?? "info";
|
|
31889
|
+
const msg = obj.message ?? JSON.stringify(obj);
|
|
31890
|
+
return ` [${severity}] line ${line}: ${msg}`;
|
|
31891
|
+
}
|
|
31892
|
+
return ` ${String(d)}`;
|
|
31893
|
+
}).join(`
|
|
31894
|
+
`);
|
|
31895
|
+
} catch {
|
|
31896
|
+
return JSON.stringify(diagnostics, null, 2);
|
|
31983
31897
|
}
|
|
31984
|
-
|
|
31985
|
-
|
|
31986
|
-
|
|
31898
|
+
}
|
|
31899
|
+
function reuseText(last) {
|
|
31900
|
+
return last instanceof Text ? last : new Text("", 0, 0);
|
|
31901
|
+
}
|
|
31902
|
+
function reuseContainer(last) {
|
|
31903
|
+
return last instanceof Container ? last : new Container;
|
|
31904
|
+
}
|
|
31905
|
+
function renderMutationCall(toolName, filePath, theme, context) {
|
|
31906
|
+
const text = reuseText(context.lastComponent);
|
|
31907
|
+
const pathDisplay = filePath ? theme.fg("accent", shortenPath(filePath)) : theme.fg("toolOutput", "...");
|
|
31908
|
+
text.setText(`${theme.fg("toolTitle", theme.bold(toolName))} ${pathDisplay}`);
|
|
31909
|
+
return text;
|
|
31910
|
+
}
|
|
31911
|
+
function renderMutationResult(result, theme, context) {
|
|
31912
|
+
if (context.isError) {
|
|
31913
|
+
const errorText = result.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join(`
|
|
31914
|
+
`).trim();
|
|
31915
|
+
const text = reuseText(context.lastComponent);
|
|
31916
|
+
text.setText(`
|
|
31917
|
+
${theme.fg("error", errorText || "edit failed")}`);
|
|
31918
|
+
return text;
|
|
31987
31919
|
}
|
|
31988
|
-
|
|
31989
|
-
|
|
31990
|
-
|
|
31920
|
+
const details = result.details;
|
|
31921
|
+
const diff = typeof details?.diff === "string" ? details.diff : undefined;
|
|
31922
|
+
if (!diff) {
|
|
31923
|
+
const additions = details?.additions ?? 0;
|
|
31924
|
+
const deletions = details?.deletions ?? 0;
|
|
31925
|
+
const text = reuseText(context.lastComponent);
|
|
31926
|
+
const summary = theme.fg("success", `+${additions}/-${deletions}`);
|
|
31927
|
+
let suffix = "";
|
|
31928
|
+
if (details?.truncated) {
|
|
31929
|
+
suffix = ` ${theme.fg("muted", "(diff truncated)")}`;
|
|
31930
|
+
} else if (details?.noOp) {
|
|
31931
|
+
suffix = ` ${theme.fg("muted", "(no net change)")}`;
|
|
31932
|
+
}
|
|
31933
|
+
text.setText(`
|
|
31934
|
+
${summary}${suffix}`);
|
|
31935
|
+
return text;
|
|
31991
31936
|
}
|
|
31992
|
-
|
|
31937
|
+
const container = reuseContainer(context.lastComponent);
|
|
31938
|
+
container.clear();
|
|
31939
|
+
container.addChild(new Spacer(1));
|
|
31940
|
+
container.addChild(new Text(renderDiff(diff), 1, 0));
|
|
31941
|
+
return container;
|
|
31993
31942
|
}
|
|
31994
|
-
function
|
|
31995
|
-
|
|
31943
|
+
function shortenPath(path2) {
|
|
31944
|
+
const home = homedir8();
|
|
31945
|
+
if (path2.startsWith(home))
|
|
31946
|
+
return `~${path2.slice(home.length)}`;
|
|
31947
|
+
return path2;
|
|
31996
31948
|
}
|
|
31997
|
-
function
|
|
31998
|
-
|
|
31999
|
-
|
|
32000
|
-
|
|
32001
|
-
|
|
32002
|
-
|
|
32003
|
-
|
|
32004
|
-
|
|
32005
|
-
|
|
32006
|
-
|
|
32007
|
-
|
|
32008
|
-
|
|
31949
|
+
async function resolvePathArg(cwd, path2) {
|
|
31950
|
+
const expanded = expandTilde(path2);
|
|
31951
|
+
const abs = isAbsolute2(expanded) ? expanded : resolve3(cwd, expanded);
|
|
31952
|
+
try {
|
|
31953
|
+
await stat(abs);
|
|
31954
|
+
return abs;
|
|
31955
|
+
} catch {
|
|
31956
|
+
return expanded;
|
|
31957
|
+
}
|
|
31958
|
+
}
|
|
31959
|
+
function splitIncludeGlobs(include) {
|
|
31960
|
+
const out = [];
|
|
31961
|
+
let depth = 0;
|
|
31962
|
+
let buf = "";
|
|
31963
|
+
for (const ch of include) {
|
|
31964
|
+
if (ch === "{") {
|
|
31965
|
+
depth++;
|
|
31966
|
+
buf += ch;
|
|
31967
|
+
continue;
|
|
32009
31968
|
}
|
|
32010
|
-
|
|
31969
|
+
if (ch === "}") {
|
|
31970
|
+
if (depth > 0)
|
|
31971
|
+
depth--;
|
|
31972
|
+
buf += ch;
|
|
31973
|
+
continue;
|
|
31974
|
+
}
|
|
31975
|
+
if (ch === "," && depth === 0) {
|
|
31976
|
+
const trimmed = buf.trim();
|
|
31977
|
+
if (trimmed.length > 0)
|
|
31978
|
+
out.push(trimmed);
|
|
31979
|
+
buf = "";
|
|
31980
|
+
continue;
|
|
31981
|
+
}
|
|
31982
|
+
buf += ch;
|
|
31983
|
+
}
|
|
31984
|
+
const tail2 = buf.trim();
|
|
31985
|
+
if (tail2.length > 0)
|
|
31986
|
+
out.push(tail2);
|
|
31987
|
+
return out;
|
|
32011
31988
|
}
|
|
32012
|
-
function
|
|
32013
|
-
|
|
32014
|
-
|
|
32015
|
-
|
|
32016
|
-
|
|
32017
|
-
|
|
32018
|
-
|
|
32019
|
-
|
|
32020
|
-
|
|
32021
|
-
|
|
32022
|
-
|
|
32023
|
-
|
|
32024
|
-
|
|
32025
|
-
}
|
|
32026
|
-
const notifyParams = {
|
|
32027
|
-
task_id: params.task_id,
|
|
32028
|
-
once: params.once !== false
|
|
32029
|
-
};
|
|
32030
|
-
if (waitFor.kind === "regex")
|
|
32031
|
-
notifyParams.regex = waitFor.source;
|
|
32032
|
-
else
|
|
32033
|
-
notifyParams.pattern = waitFor.value;
|
|
32034
|
-
const sessionId = resolveSessionId(extCtx);
|
|
32035
|
-
markExplicitControl(sessionId, params.task_id, false);
|
|
32036
|
-
let registered;
|
|
32037
|
-
try {
|
|
32038
|
-
registered = await callBridge(bridge, "bash_notify", notifyParams, extCtx);
|
|
32039
|
-
} catch (err) {
|
|
32040
|
-
unmarkExplicitControl(sessionId, params.task_id);
|
|
32041
|
-
throw err;
|
|
32042
|
-
}
|
|
32043
|
-
if (registered.success === false) {
|
|
32044
|
-
unmarkExplicitControl(sessionId, params.task_id);
|
|
32045
|
-
const message = String(registered.message ?? "bash_notify failed");
|
|
32046
|
-
throw new Error(`${String(registered.code ?? "invalid_request")}: ${message}`);
|
|
32047
|
-
}
|
|
32048
|
-
markExplicitControl(sessionId, params.task_id);
|
|
32049
|
-
const watchDetails = { registered: true, watchId: registered.watch_id };
|
|
32050
|
-
return textResult(`Watch registered: ${registered.watch_id} on task ${params.task_id}
|
|
32051
|
-
A notification will fire when the pattern matches or the task exits.`, watchDetails);
|
|
32052
|
-
}
|
|
32053
|
-
const data = await waitForBashStatus(ctx, bridge, extCtx, params.task_id, undefined, waitFor, true, Math.min(coerceOptionalInt(params.timeout_ms, "timeout_ms", 1, MAX_BASH_STATUS_WAIT_TIMEOUT_MS) ?? DEFAULT_BASH_STATUS_WAIT_TIMEOUT_MS, MAX_BASH_STATUS_WAIT_TIMEOUT_MS));
|
|
32054
|
-
const text = await formatBashStatus(extCtx, params.task_id, data, undefined);
|
|
32055
|
-
return textResult(text, data);
|
|
32056
|
-
}
|
|
32057
|
-
};
|
|
31989
|
+
function formatReadFooter(agentSpecifiedRange, data) {
|
|
31990
|
+
if (agentSpecifiedRange)
|
|
31991
|
+
return "";
|
|
31992
|
+
if (!data.truncated)
|
|
31993
|
+
return "";
|
|
31994
|
+
const startLine = data.start_line;
|
|
31995
|
+
const endLine = data.end_line;
|
|
31996
|
+
const totalLines = data.total_lines;
|
|
31997
|
+
if (startLine === undefined || endLine === undefined || totalLines === undefined) {
|
|
31998
|
+
return "";
|
|
31999
|
+
}
|
|
32000
|
+
return `
|
|
32001
|
+
(Showing lines ${startLine}-${endLine} of ${totalLines}. Use offset/limit to read other sections.)`;
|
|
32058
32002
|
}
|
|
32059
|
-
|
|
32060
|
-
|
|
32061
|
-
|
|
32062
|
-
|
|
32063
|
-
|
|
32064
|
-
|
|
32065
|
-
|
|
32066
|
-
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32067
|
-
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32068
|
-
const data = await callBridge(bridge, "bash_write", { task_id: params.task_id, input: params.input }, extCtx);
|
|
32069
|
-
return textResult(JSON.stringify({ bytes_written: data.bytes_written }, null, 2), data);
|
|
32070
|
-
}
|
|
32071
|
-
};
|
|
32003
|
+
|
|
32004
|
+
// src/tools/render-helpers.ts
|
|
32005
|
+
import { homedir as homedir9 } from "node:os";
|
|
32006
|
+
import { renderDiff as renderDiff2 } from "@earendil-works/pi-coding-agent";
|
|
32007
|
+
import { Container as Container2, Spacer as Spacer2, Text as Text2 } from "@earendil-works/pi-tui";
|
|
32008
|
+
function reuseText2(last) {
|
|
32009
|
+
return last instanceof Text2 ? last : new Text2("", 0, 0);
|
|
32072
32010
|
}
|
|
32073
|
-
function
|
|
32074
|
-
return
|
|
32075
|
-
name: "bash_kill",
|
|
32076
|
-
label: "bash_kill",
|
|
32077
|
-
description: "Terminate a running background bash task spawned with bash({ background: true }).",
|
|
32078
|
-
promptSnippet: "Kill a background bash task by task_id",
|
|
32079
|
-
parameters: BashTaskParams,
|
|
32080
|
-
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32081
|
-
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32082
|
-
const data = await callBridge(bridge, "bash_kill", { task_id: params.task_id }, extCtx);
|
|
32083
|
-
if (data.success === false) {
|
|
32084
|
-
throw new Error(data.message ?? "bash_kill failed");
|
|
32085
|
-
}
|
|
32086
|
-
await disposePtyTerminal(ptyCacheKey(extCtx, params.task_id));
|
|
32087
|
-
const details = data;
|
|
32088
|
-
if (details.kill_signaled === true) {
|
|
32089
|
-
return bashKillResult(`Task ${params.task_id}: kill_signaled`, details);
|
|
32090
|
-
}
|
|
32091
|
-
return bashKillResult(`Task ${params.task_id}: ${details.status}`, details);
|
|
32092
|
-
}
|
|
32093
|
-
};
|
|
32011
|
+
function reuseContainer2(last) {
|
|
32012
|
+
return last instanceof Container2 ? last : new Container2;
|
|
32094
32013
|
}
|
|
32095
|
-
function
|
|
32096
|
-
|
|
32097
|
-
|
|
32098
|
-
|
|
32099
|
-
|
|
32100
|
-
duration_ms: details.duration_ms,
|
|
32101
|
-
truncated: details.truncated,
|
|
32102
|
-
output_path: details.output_path,
|
|
32103
|
-
task_id: details.task_id,
|
|
32104
|
-
bg_completions: details.bg_completions
|
|
32105
|
-
}
|
|
32106
|
-
};
|
|
32014
|
+
function shortenPath2(path2) {
|
|
32015
|
+
const home = homedir9();
|
|
32016
|
+
if (path2.startsWith(home))
|
|
32017
|
+
return `~${path2.slice(home.length)}`;
|
|
32018
|
+
return path2;
|
|
32107
32019
|
}
|
|
32108
|
-
function
|
|
32109
|
-
|
|
32110
|
-
|
|
32111
|
-
|
|
32112
|
-
|
|
32020
|
+
function renderToolCall(toolName, summary, theme, context) {
|
|
32021
|
+
const text = reuseText2(context.lastComponent);
|
|
32022
|
+
const suffix = summary ? ` ${summary}` : "";
|
|
32023
|
+
text.setText(`${theme.fg("toolTitle", theme.bold(toolName))}${suffix}`);
|
|
32024
|
+
return text;
|
|
32113
32025
|
}
|
|
32114
|
-
function
|
|
32115
|
-
|
|
32116
|
-
|
|
32117
|
-
|
|
32118
|
-
};
|
|
32026
|
+
function accentPath(theme, path2) {
|
|
32027
|
+
if (!path2)
|
|
32028
|
+
return theme.fg("toolOutput", "...");
|
|
32029
|
+
return theme.fg("accent", shortenPath2(path2));
|
|
32119
32030
|
}
|
|
32120
|
-
|
|
32121
|
-
return
|
|
32031
|
+
function collectTextContent(result) {
|
|
32032
|
+
return result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join(`
|
|
32033
|
+
`).trim();
|
|
32122
32034
|
}
|
|
32123
|
-
|
|
32124
|
-
|
|
32125
|
-
|
|
32126
|
-
|
|
32127
|
-
|
|
32128
|
-
|
|
32129
|
-
const bridgeOptions = {
|
|
32130
|
-
keepBridgeOnTimeout: true,
|
|
32131
|
-
transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS
|
|
32132
|
-
};
|
|
32133
|
-
const sessionId = resolveSessionId(extCtx);
|
|
32134
|
-
if (waitForExit)
|
|
32135
|
-
markTaskWaiting(sessionId, taskId);
|
|
32136
|
-
let sawTerminal = false;
|
|
32035
|
+
function extractStructuredPayload(result) {
|
|
32036
|
+
if (result.details !== undefined)
|
|
32037
|
+
return result.details;
|
|
32038
|
+
const text = collectTextContent(result);
|
|
32039
|
+
if (!text)
|
|
32040
|
+
return;
|
|
32137
32041
|
try {
|
|
32138
|
-
|
|
32139
|
-
|
|
32140
|
-
|
|
32141
|
-
if (waitFor) {
|
|
32142
|
-
const scan = await readNewTaskOutput(extCtx, taskId, data, spillCursor);
|
|
32143
|
-
if (scan) {
|
|
32144
|
-
spillCursor = scan.nextCursor;
|
|
32145
|
-
if (scanText.length === 0)
|
|
32146
|
-
scanBaseOffset = scan.baseOffset;
|
|
32147
|
-
scanText += scan.text;
|
|
32148
|
-
const match = findWaitMatch(scanText, waitFor);
|
|
32149
|
-
if (match) {
|
|
32150
|
-
if (waitForExit && terminal) {
|
|
32151
|
-
sawTerminal = true;
|
|
32152
|
-
consumeBgCompletion(sessionId, taskId);
|
|
32153
|
-
await markBgCompletionDelivered({ ctx, directory: extCtx.cwd, sessionID: sessionId }, taskId);
|
|
32154
|
-
}
|
|
32155
|
-
return withWaited(data, {
|
|
32156
|
-
reason: "matched",
|
|
32157
|
-
elapsed_ms: Date.now() - startedAt,
|
|
32158
|
-
match: match.text,
|
|
32159
|
-
match_offset: scanBaseOffset + Buffer.byteLength(scanText.slice(0, match.index), "utf8")
|
|
32160
|
-
});
|
|
32161
|
-
}
|
|
32162
|
-
}
|
|
32163
|
-
}
|
|
32164
|
-
if (terminal) {
|
|
32165
|
-
if (waitForExit) {
|
|
32166
|
-
sawTerminal = true;
|
|
32167
|
-
consumeBgCompletion(sessionId, taskId);
|
|
32168
|
-
await markBgCompletionDelivered({ ctx, directory: extCtx.cwd, sessionID: sessionId }, taskId);
|
|
32169
|
-
}
|
|
32170
|
-
return withWaited(data, { reason: "exited", elapsed_ms: Date.now() - startedAt });
|
|
32171
|
-
}
|
|
32172
|
-
if (Date.now() >= deadline) {
|
|
32173
|
-
return withWaited(data, { reason: "timeout", elapsed_ms: Date.now() - startedAt });
|
|
32174
|
-
}
|
|
32175
|
-
await sleep(Math.min(BASH_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
|
|
32176
|
-
}
|
|
32177
|
-
} finally {
|
|
32178
|
-
if (waitForExit && !sawTerminal)
|
|
32179
|
-
unmarkTaskWaiting(sessionId, taskId);
|
|
32180
|
-
if (waitFor) {
|
|
32181
|
-
await disposePtyTerminal(watchPtyCacheKey(extCtx, taskId));
|
|
32182
|
-
}
|
|
32042
|
+
return JSON.parse(text);
|
|
32043
|
+
} catch {
|
|
32044
|
+
return;
|
|
32183
32045
|
}
|
|
32184
32046
|
}
|
|
32185
|
-
|
|
32186
|
-
const
|
|
32187
|
-
|
|
32188
|
-
|
|
32189
|
-
|
|
32190
|
-
|
|
32191
|
-
const state = await getOrCreatePtyTerminal(watchPtyCacheKey(extCtx, taskId), outputPath, rows, cols);
|
|
32192
|
-
const baseOffset = state.offset;
|
|
32193
|
-
const bytes = await readPtyBytes(state);
|
|
32194
|
-
if (bytes.length === 0)
|
|
32195
|
-
return;
|
|
32196
|
-
return {
|
|
32197
|
-
text: bytes.toString("utf8"),
|
|
32198
|
-
baseOffset,
|
|
32199
|
-
nextCursor: { output: state.offset, stderr: 0, combined: state.offset }
|
|
32200
|
-
};
|
|
32201
|
-
}
|
|
32202
|
-
const stderrPath = data.stderr_path;
|
|
32203
|
-
if (!outputPath && !stderrPath)
|
|
32204
|
-
return;
|
|
32205
|
-
const stdoutBytes = outputPath ? await readFileBytesFrom(outputPath, cursor.output) : Buffer.alloc(0);
|
|
32206
|
-
const stderrBytes = stderrPath ? await readFileBytesFrom(stderrPath, cursor.stderr) : Buffer.alloc(0);
|
|
32207
|
-
const bytesRead = stdoutBytes.length + stderrBytes.length;
|
|
32208
|
-
if (bytesRead === 0)
|
|
32209
|
-
return;
|
|
32210
|
-
return {
|
|
32211
|
-
text: Buffer.concat([stdoutBytes, stderrBytes]).toString("utf8"),
|
|
32212
|
-
baseOffset: cursor.combined,
|
|
32213
|
-
nextCursor: {
|
|
32214
|
-
output: cursor.output + stdoutBytes.length,
|
|
32215
|
-
stderr: cursor.stderr + stderrBytes.length,
|
|
32216
|
-
combined: cursor.combined + bytesRead
|
|
32217
|
-
}
|
|
32218
|
-
};
|
|
32047
|
+
function renderErrorResult(result, fallback, theme, context) {
|
|
32048
|
+
const text = reuseText2(context.lastComponent);
|
|
32049
|
+
const message = collectTextContent(result) || fallback;
|
|
32050
|
+
text.setText(`
|
|
32051
|
+
${theme.fg("error", message)}`);
|
|
32052
|
+
return text;
|
|
32219
32053
|
}
|
|
32220
|
-
|
|
32221
|
-
const
|
|
32222
|
-
|
|
32223
|
-
|
|
32224
|
-
|
|
32225
|
-
|
|
32226
|
-
|
|
32227
|
-
|
|
32228
|
-
|
|
32229
|
-
|
|
32230
|
-
|
|
32231
|
-
|
|
32232
|
-
|
|
32233
|
-
return Buffer.concat(chunks);
|
|
32234
|
-
} finally {
|
|
32235
|
-
await handle.close().catch(() => {
|
|
32236
|
-
return;
|
|
32237
|
-
});
|
|
32238
|
-
}
|
|
32054
|
+
function renderSections(sections, context) {
|
|
32055
|
+
const container = reuseContainer2(context.lastComponent);
|
|
32056
|
+
container.clear();
|
|
32057
|
+
const visibleSections = sections.filter((section) => section.trim().length > 0);
|
|
32058
|
+
if (visibleSections.length === 0)
|
|
32059
|
+
return container;
|
|
32060
|
+
container.addChild(new Spacer2(1));
|
|
32061
|
+
visibleSections.forEach((section, index) => {
|
|
32062
|
+
if (index > 0)
|
|
32063
|
+
container.addChild(new Spacer2(1));
|
|
32064
|
+
container.addChild(new Text2(section, 0, 0));
|
|
32065
|
+
});
|
|
32066
|
+
return container;
|
|
32239
32067
|
}
|
|
32240
|
-
function
|
|
32241
|
-
if (typeof value
|
|
32242
|
-
return
|
|
32243
|
-
|
|
32244
|
-
return { kind: "regex", value: new RegExp(value.regex), source: value.regex };
|
|
32245
|
-
return;
|
|
32068
|
+
function asRecord2(value) {
|
|
32069
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
32070
|
+
return;
|
|
32071
|
+
return value;
|
|
32246
32072
|
}
|
|
32247
|
-
function
|
|
32248
|
-
return
|
|
32073
|
+
function asRecords(value) {
|
|
32074
|
+
return Array.isArray(value) ? value.map(asRecord2).filter(Boolean) : [];
|
|
32249
32075
|
}
|
|
32250
|
-
function
|
|
32251
|
-
|
|
32252
|
-
const index = text.indexOf(pattern.value);
|
|
32253
|
-
return index >= 0 ? { text: pattern.value, index } : undefined;
|
|
32254
|
-
}
|
|
32255
|
-
pattern.value.lastIndex = 0;
|
|
32256
|
-
const match = pattern.value.exec(text);
|
|
32257
|
-
return match ? { text: match[0], index: match.index } : undefined;
|
|
32076
|
+
function asString(value) {
|
|
32077
|
+
return typeof value === "string" ? value : undefined;
|
|
32258
32078
|
}
|
|
32259
|
-
function
|
|
32260
|
-
return
|
|
32079
|
+
function asNumber(value) {
|
|
32080
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
32261
32081
|
}
|
|
32262
|
-
function
|
|
32263
|
-
|
|
32264
|
-
|
|
32265
|
-
|
|
32266
|
-
if (
|
|
32267
|
-
return
|
|
32082
|
+
function asBoolean(value) {
|
|
32083
|
+
return typeof value === "boolean" ? value : undefined;
|
|
32084
|
+
}
|
|
32085
|
+
function formatValue(value) {
|
|
32086
|
+
if (Array.isArray(value))
|
|
32087
|
+
return value.map(formatValue).join(", ");
|
|
32088
|
+
if (typeof value === "string")
|
|
32089
|
+
return value;
|
|
32090
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
32091
|
+
return String(value);
|
|
32092
|
+
if (value === null || value === undefined)
|
|
32093
|
+
return "—";
|
|
32094
|
+
try {
|
|
32095
|
+
return JSON.stringify(value);
|
|
32096
|
+
} catch {
|
|
32097
|
+
return String(value);
|
|
32268
32098
|
}
|
|
32269
|
-
const exit = typeof details.exit_code === "number" ? `, exit ${details.exit_code}` : "";
|
|
32270
|
-
return `Waited ${waited.elapsed_ms}ms; task exited (${details.status}${exit}).`;
|
|
32271
32099
|
}
|
|
32272
|
-
|
|
32273
|
-
const
|
|
32274
|
-
|
|
32275
|
-
|
|
32276
|
-
|
|
32277
|
-
|
|
32278
|
-
|
|
32279
|
-
|
|
32280
|
-
|
|
32281
|
-
|
|
32282
|
-
|
|
32283
|
-
|
|
32284
|
-
|
|
32100
|
+
function groupByFile(items, getFile) {
|
|
32101
|
+
const groups = new Map;
|
|
32102
|
+
items.forEach((item) => {
|
|
32103
|
+
const file2 = getFile(item) ?? "(unknown file)";
|
|
32104
|
+
const current = groups.get(file2) ?? [];
|
|
32105
|
+
current.push(item);
|
|
32106
|
+
groups.set(file2, current);
|
|
32107
|
+
});
|
|
32108
|
+
return groups;
|
|
32109
|
+
}
|
|
32110
|
+
function formatTimestamp(value) {
|
|
32111
|
+
if (typeof value === "string" && value.length > 0)
|
|
32112
|
+
return value;
|
|
32113
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
32114
|
+
return;
|
|
32115
|
+
const millis = value > 1000000000000 ? value : value * 1000;
|
|
32116
|
+
const date5 = new Date(millis);
|
|
32117
|
+
if (Number.isNaN(date5.getTime()))
|
|
32118
|
+
return String(value);
|
|
32119
|
+
return date5.toISOString().replace("T", " ").replace(".000Z", "Z");
|
|
32120
|
+
}
|
|
32121
|
+
function formatUnifiedDiffForPi(unifiedDiff) {
|
|
32122
|
+
if (!unifiedDiff.trim())
|
|
32123
|
+
return "";
|
|
32124
|
+
const entries = [];
|
|
32125
|
+
const hunkHeader = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
|
|
32126
|
+
let oldLine = 1;
|
|
32127
|
+
let newLine = 1;
|
|
32128
|
+
for (const line of unifiedDiff.split(`
|
|
32129
|
+
`)) {
|
|
32130
|
+
if (!line)
|
|
32131
|
+
continue;
|
|
32132
|
+
if (line.startsWith("--- ") || line.startsWith("+++ "))
|
|
32133
|
+
continue;
|
|
32134
|
+
if (line.startsWith("\"))
|
|
32135
|
+
continue;
|
|
32136
|
+
const headerMatch = line.match(hunkHeader);
|
|
32137
|
+
if (headerMatch) {
|
|
32138
|
+
oldLine = Number(headerMatch[1]);
|
|
32139
|
+
newLine = Number(headerMatch[2]);
|
|
32140
|
+
continue;
|
|
32285
32141
|
}
|
|
32286
|
-
if (!
|
|
32287
|
-
text
|
|
32288
|
-
|
|
32142
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
32143
|
+
entries.push({ prefix: "+", line: newLine, text: line.slice(1) });
|
|
32144
|
+
newLine += 1;
|
|
32145
|
+
continue;
|
|
32146
|
+
}
|
|
32147
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
32148
|
+
entries.push({ prefix: "-", line: oldLine, text: line.slice(1) });
|
|
32149
|
+
oldLine += 1;
|
|
32150
|
+
continue;
|
|
32151
|
+
}
|
|
32152
|
+
if (line.startsWith(" ")) {
|
|
32153
|
+
entries.push({ prefix: " ", line: oldLine, text: line.slice(1) });
|
|
32154
|
+
oldLine += 1;
|
|
32155
|
+
newLine += 1;
|
|
32289
32156
|
}
|
|
32290
32157
|
}
|
|
32291
|
-
|
|
32158
|
+
if (entries.length === 0)
|
|
32159
|
+
return "";
|
|
32160
|
+
const width = String(entries.reduce((max, entry) => Math.max(max, entry.line), 1)).length;
|
|
32161
|
+
return entries.map((entry) => `${entry.prefix}${String(entry.line).padStart(width, " ")} ${entry.text}`).join(`
|
|
32162
|
+
`);
|
|
32292
32163
|
}
|
|
32293
|
-
|
|
32294
|
-
|
|
32295
|
-
|
|
32296
|
-
|
|
32297
|
-
|
|
32298
|
-
|
|
32299
|
-
|
|
32300
|
-
|
|
32301
|
-
const outputMode = requestedOutputMode ?? "screen";
|
|
32302
|
-
let suffix = "";
|
|
32303
|
-
if (outputMode === "raw") {
|
|
32304
|
-
suffix = raw.length > 0 ? `
|
|
32305
|
-
${raw.toString("utf8")}` : "";
|
|
32306
|
-
} else if (outputMode === "both") {
|
|
32307
|
-
suffix = `
|
|
32308
|
-
${JSON.stringify({ screen: renderScreen(state, rows, cols), raw: raw.toString("utf8") }, null, 2)}`;
|
|
32309
|
-
} else {
|
|
32310
|
-
const screen = renderScreen(state, rows, cols);
|
|
32311
|
-
suffix = screen ? `
|
|
32312
|
-
${screen}` : "";
|
|
32313
|
-
}
|
|
32314
|
-
if (!isTerminalStatus(details.status)) {
|
|
32315
|
-
suffix += `
|
|
32316
|
-
PTY task is still running. Use bash_status({ task_id: "${taskId}", output_mode: "screen" }) to inspect, bash_write({ task_id: "${taskId}", input: "..." }) to send keystrokes.`;
|
|
32317
|
-
} else {
|
|
32318
|
-
await disposePtyTerminal(key);
|
|
32164
|
+
function renderUnifiedDiff(unifiedDiff) {
|
|
32165
|
+
const piDiff = formatUnifiedDiffForPi(unifiedDiff);
|
|
32166
|
+
if (!piDiff)
|
|
32167
|
+
return "";
|
|
32168
|
+
try {
|
|
32169
|
+
return renderDiff2(piDiff);
|
|
32170
|
+
} catch {
|
|
32171
|
+
return piDiff;
|
|
32319
32172
|
}
|
|
32320
|
-
return suffix;
|
|
32321
|
-
}
|
|
32322
|
-
function ptyDimensions(data) {
|
|
32323
|
-
const rows = typeof data.pty_rows === "number" ? data.pty_rows : 24;
|
|
32324
|
-
const cols = typeof data.pty_cols === "number" ? data.pty_cols : 80;
|
|
32325
|
-
return { rows, cols };
|
|
32326
|
-
}
|
|
32327
|
-
function ptyCacheKey(extCtx, taskId) {
|
|
32328
|
-
return `${extCtx.cwd}::${resolveSessionId(extCtx) ?? "__default__"}::${taskId}`;
|
|
32329
32173
|
}
|
|
32330
|
-
|
|
32331
|
-
|
|
32174
|
+
|
|
32175
|
+
// src/tools/ast.ts
|
|
32176
|
+
var AstLang = StringEnum(["typescript", "tsx", "javascript", "python", "rust", "go"], {
|
|
32177
|
+
description: "Target language"
|
|
32178
|
+
});
|
|
32179
|
+
var SearchParams = Type3.Object({
|
|
32180
|
+
pattern: Type3.String({
|
|
32181
|
+
description: "AST pattern with meta-variables (`$VAR` matches one node, `$$$` matches many). Must be a complete AST node."
|
|
32182
|
+
}),
|
|
32183
|
+
lang: AstLang,
|
|
32184
|
+
paths: Type3.Optional(Type3.Array(Type3.String(), { description: "Paths to search (default: ['.'])" })),
|
|
32185
|
+
globs: Type3.Optional(Type3.Array(Type3.String(), { description: "Include/exclude globs (prefix `!` to exclude)" })),
|
|
32186
|
+
contextLines: Type3.Optional(Type3.Number({ description: "Number of context lines around each match" }))
|
|
32187
|
+
});
|
|
32188
|
+
var ReplaceParams = Type3.Object({
|
|
32189
|
+
pattern: Type3.String({ description: "AST pattern with meta-variables" }),
|
|
32190
|
+
rewrite: Type3.String({ description: "Replacement pattern, can reference $VAR from pattern" }),
|
|
32191
|
+
lang: AstLang,
|
|
32192
|
+
paths: Type3.Optional(Type3.Array(Type3.String(), { description: "Paths (default: ['.'])" })),
|
|
32193
|
+
globs: Type3.Optional(Type3.Array(Type3.String(), { description: "Include/exclude globs" })),
|
|
32194
|
+
dryRun: Type3.Optional(Type3.Boolean({ description: "Preview without applying (default: false)" }))
|
|
32195
|
+
});
|
|
32196
|
+
function appendHintSection(response, sections, theme) {
|
|
32197
|
+
const hint = asString(response.hint);
|
|
32198
|
+
if (hint && hint.length > 0) {
|
|
32199
|
+
sections.push(theme.fg("warning", hint));
|
|
32200
|
+
}
|
|
32332
32201
|
}
|
|
32333
|
-
function
|
|
32334
|
-
|
|
32202
|
+
function appendScopeSections(response, sections, theme) {
|
|
32203
|
+
if (response.no_files_matched_scope === true) {
|
|
32204
|
+
sections.push(theme.fg("warning", "No files matched the scope (paths/globs resolved to zero files)"));
|
|
32205
|
+
}
|
|
32206
|
+
const warnings = asRecords(response.scope_warnings);
|
|
32207
|
+
const warningStrings = Array.isArray(response.scope_warnings) ? response.scope_warnings.filter((w) => typeof w === "string") : warnings.map((w) => asString(w.warning) ?? "").filter(Boolean);
|
|
32208
|
+
if (warningStrings.length > 0) {
|
|
32209
|
+
sections.push(`${theme.fg("muted", "Scope warnings:")}
|
|
32210
|
+
${warningStrings.map((w) => ` ${w}`).join(`
|
|
32211
|
+
`)}`);
|
|
32212
|
+
}
|
|
32335
32213
|
}
|
|
32336
|
-
function
|
|
32337
|
-
|
|
32338
|
-
|
|
32339
|
-
|
|
32340
|
-
return text;
|
|
32214
|
+
async function resolveAstPaths(extCtx, paths) {
|
|
32215
|
+
if (isEmptyParam(paths) || !Array.isArray(paths))
|
|
32216
|
+
return;
|
|
32217
|
+
return Promise.all(paths.filter((path2) => typeof path2 === "string").map((path2) => resolvePathArg(extCtx.cwd, path2)));
|
|
32341
32218
|
}
|
|
32342
|
-
function
|
|
32343
|
-
if (
|
|
32344
|
-
|
|
32345
|
-
|
|
32346
|
-
|
|
32347
|
-
|
|
32348
|
-
|
|
32349
|
-
|
|
32350
|
-
|
|
32351
|
-
const details = result.details;
|
|
32352
|
-
const exitCode = details?.exit_code;
|
|
32353
|
-
const bgCompletions = details?.bg_completions ?? [];
|
|
32354
|
-
const container = reuseContainer2(context.lastComponent);
|
|
32355
|
-
container.clear();
|
|
32356
|
-
container.addChild(new Spacer2(1));
|
|
32357
|
-
const rawOutput = result.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join(`
|
|
32358
|
-
`).trim();
|
|
32359
|
-
if (rawOutput) {
|
|
32360
|
-
const lines = rawOutput.split(`
|
|
32361
|
-
`);
|
|
32362
|
-
const preview = lines.length > 25 ? `... (${lines.length - 25} lines omitted)
|
|
32363
|
-
${lines.slice(-25).join(`
|
|
32364
|
-
`)}` : rawOutput;
|
|
32365
|
-
container.addChild(new Text2(preview, 1, 0));
|
|
32366
|
-
container.addChild(new Spacer2(1));
|
|
32219
|
+
async function assertAstPathsPermission(extCtx, paths, action, restrictToProjectRoot) {
|
|
32220
|
+
if (paths === undefined || paths.length === 0)
|
|
32221
|
+
return;
|
|
32222
|
+
const checked = new Set;
|
|
32223
|
+
for (const path2 of paths) {
|
|
32224
|
+
if (checked.has(path2))
|
|
32225
|
+
continue;
|
|
32226
|
+
checked.add(path2);
|
|
32227
|
+
await assertExternalDirectoryPermission(extCtx, path2, action, { restrictToProjectRoot });
|
|
32367
32228
|
}
|
|
32368
|
-
|
|
32369
|
-
|
|
32370
|
-
|
|
32371
|
-
|
|
32372
|
-
|
|
32373
|
-
|
|
32374
|
-
|
|
32375
|
-
|
|
32376
|
-
|
|
32377
|
-
|
|
32378
|
-
|
|
32379
|
-
|
|
32380
|
-
|
|
32381
|
-
|
|
32382
|
-
|
|
32383
|
-
|
|
32384
|
-
|
|
32385
|
-
|
|
32386
|
-
|
|
32387
|
-
container.addChild(new Text2(durationText, 1, 0));
|
|
32229
|
+
}
|
|
32230
|
+
function buildAstSearchSections(payload, theme) {
|
|
32231
|
+
const response = asRecord2(payload);
|
|
32232
|
+
if (!response)
|
|
32233
|
+
return [theme.fg("muted", "No AST search results.")];
|
|
32234
|
+
const matches = asRecords(response.matches);
|
|
32235
|
+
const totalMatches = asNumber(response.total_matches) ?? matches.length;
|
|
32236
|
+
const filesWithMatches = asNumber(response.files_with_matches) ?? groupByFile(matches, (match) => asString(match.file)).size;
|
|
32237
|
+
const filesSearched = asNumber(response.files_searched);
|
|
32238
|
+
const header = [
|
|
32239
|
+
theme.fg("success", `${totalMatches} match${totalMatches === 1 ? "" : "es"}`),
|
|
32240
|
+
theme.fg("accent", `${filesWithMatches} file${filesWithMatches === 1 ? "" : "s"}`),
|
|
32241
|
+
filesSearched !== undefined ? theme.fg("muted", `${filesSearched} searched`) : undefined
|
|
32242
|
+
].filter(Boolean).join(" · ");
|
|
32243
|
+
if (matches.length === 0) {
|
|
32244
|
+
const sections2 = [header, theme.fg("muted", "No AST matches found.")];
|
|
32245
|
+
appendScopeSections(response, sections2, theme);
|
|
32246
|
+
appendHintSection(response, sections2, theme);
|
|
32247
|
+
return sections2;
|
|
32388
32248
|
}
|
|
32389
|
-
|
|
32390
|
-
|
|
32391
|
-
|
|
32392
|
-
|
|
32249
|
+
const grouped = groupByFile(matches, (match) => asString(match.file));
|
|
32250
|
+
const sections = [header];
|
|
32251
|
+
for (const [file2, fileMatches] of grouped.entries()) {
|
|
32252
|
+
const lines = [theme.fg("accent", shortenPath2(file2))];
|
|
32253
|
+
fileMatches.forEach((match, index) => {
|
|
32254
|
+
const line = asNumber(match.line) ?? 0;
|
|
32255
|
+
const column = asNumber(match.column) ?? 0;
|
|
32256
|
+
const snippet = asString(match.text)?.trim() || "(empty match)";
|
|
32257
|
+
lines.push(` ${index + 1}. ${theme.fg("muted", `${line}:${column}`)} ${snippet}`);
|
|
32258
|
+
const metaVars = asRecord2(match.meta_variables);
|
|
32259
|
+
if (metaVars && Object.keys(metaVars).length > 0) {
|
|
32260
|
+
Object.entries(metaVars).forEach(([name, value]) => {
|
|
32261
|
+
lines.push(` ${theme.fg("muted", `${name} =`)} ${formatValue(value)}`);
|
|
32262
|
+
});
|
|
32263
|
+
}
|
|
32264
|
+
const context = asRecords(match.context);
|
|
32265
|
+
context.forEach((ctxLine) => {
|
|
32266
|
+
const ctxNumber = asNumber(ctxLine.line) ?? 0;
|
|
32267
|
+
const prefix = ctxLine.is_match === true ? theme.fg("accent", ">") : theme.fg("muted", "|");
|
|
32268
|
+
lines.push(` ${prefix} ${ctxNumber}: ${asString(ctxLine.text) ?? ""}`);
|
|
32269
|
+
});
|
|
32270
|
+
});
|
|
32271
|
+
sections.push(lines.join(`
|
|
32272
|
+
`));
|
|
32393
32273
|
}
|
|
32394
|
-
return
|
|
32395
|
-
}
|
|
32396
|
-
function shortenCommand(command) {
|
|
32397
|
-
if (command.length <= 60)
|
|
32398
|
-
return command;
|
|
32399
|
-
return `${command.slice(0, 57)}...`;
|
|
32400
|
-
}
|
|
32401
|
-
|
|
32402
|
-
// src/tools/conflicts.ts
|
|
32403
|
-
import { Type as Type4 } from "typebox";
|
|
32404
|
-
var ConflictsParams = Type4.Object({});
|
|
32405
|
-
function renderConflictCall(theme, context) {
|
|
32406
|
-
return renderToolCall("conflicts", undefined, theme, context);
|
|
32274
|
+
return sections;
|
|
32407
32275
|
}
|
|
32408
|
-
function
|
|
32409
|
-
const
|
|
32410
|
-
if (!
|
|
32411
|
-
return ["No
|
|
32412
|
-
const
|
|
32413
|
-
const
|
|
32414
|
-
const
|
|
32415
|
-
|
|
32276
|
+
function buildAstReplaceSections(payload, theme) {
|
|
32277
|
+
const response = asRecord2(payload);
|
|
32278
|
+
if (!response)
|
|
32279
|
+
return [theme.fg("muted", "No AST replace results.")];
|
|
32280
|
+
const files = asRecords(response.files);
|
|
32281
|
+
const totalReplacements = asNumber(response.total_replacements) ?? 0;
|
|
32282
|
+
const totalFiles = asNumber(response.total_files) ?? files.length;
|
|
32283
|
+
const filesWithMatches = asNumber(response.files_with_matches);
|
|
32284
|
+
const dryRun = response.dry_run === true;
|
|
32285
|
+
const headerParts = [
|
|
32286
|
+
dryRun ? theme.fg("warning", "[dry run]") : theme.fg("success", "[applied]"),
|
|
32287
|
+
`${totalReplacements} replacement${totalReplacements === 1 ? "" : "s"}`,
|
|
32288
|
+
`${totalFiles} file${totalFiles === 1 ? "" : "s"}`,
|
|
32289
|
+
filesWithMatches !== undefined ? theme.fg("muted", `${filesWithMatches} matched`) : undefined
|
|
32416
32290
|
];
|
|
32417
|
-
|
|
32291
|
+
const sections = [headerParts.filter(Boolean).join(" ")];
|
|
32292
|
+
if (files.length === 0) {
|
|
32293
|
+
sections.push(theme.fg("muted", "No files changed."));
|
|
32294
|
+
appendScopeSections(response, sections, theme);
|
|
32295
|
+
appendHintSection(response, sections, theme);
|
|
32418
32296
|
return sections;
|
|
32419
|
-
|
|
32420
|
-
|
|
32421
|
-
|
|
32422
|
-
|
|
32423
|
-
|
|
32424
|
-
|
|
32425
|
-
|
|
32426
|
-
|
|
32427
|
-
|
|
32428
|
-
|
|
32429
|
-
|
|
32430
|
-
}
|
|
32431
|
-
|
|
32432
|
-
|
|
32433
|
-
|
|
32434
|
-
|
|
32435
|
-
|
|
32436
|
-
parameters: ConflictsParams,
|
|
32437
|
-
async execute(_toolCallId, _params, _signal, _onUpdate, extCtx) {
|
|
32438
|
-
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32439
|
-
const response = await callBridge(bridge, "git_conflicts", {}, extCtx);
|
|
32440
|
-
return textResult(response.text ?? JSON.stringify(response, null, 2));
|
|
32441
|
-
},
|
|
32442
|
-
renderCall(_args, theme, context) {
|
|
32443
|
-
return renderConflictCall(theme, context);
|
|
32444
|
-
},
|
|
32445
|
-
renderResult(result, _options, theme, context) {
|
|
32446
|
-
return renderConflictToolResult(result, theme, context);
|
|
32297
|
+
}
|
|
32298
|
+
files.forEach((fileResult) => {
|
|
32299
|
+
const file2 = shortenPath2(asString(fileResult.file) ?? "(unknown file)");
|
|
32300
|
+
const replacements = asNumber(fileResult.replacements) ?? 0;
|
|
32301
|
+
const error50 = asString(fileResult.error);
|
|
32302
|
+
const diff = asString(fileResult.diff);
|
|
32303
|
+
const lines = [
|
|
32304
|
+
`${theme.fg("accent", file2)} ${theme.fg("muted", `(${replacements} replacement${replacements === 1 ? "" : "s"})`)}`
|
|
32305
|
+
];
|
|
32306
|
+
if (error50) {
|
|
32307
|
+
lines.push(theme.fg("error", error50));
|
|
32308
|
+
} else if (diff) {
|
|
32309
|
+
const rendered = renderUnifiedDiff(diff);
|
|
32310
|
+
lines.push(rendered || theme.fg("muted", "No diff available."));
|
|
32311
|
+
} else {
|
|
32312
|
+
const backupId = asString(fileResult.backup_id);
|
|
32313
|
+
lines.push(backupId ? `${theme.fg("success", "saved")} ${theme.fg("muted", backupId)}` : theme.fg("success", "saved"));
|
|
32447
32314
|
}
|
|
32315
|
+
sections.push(lines.join(`
|
|
32316
|
+
`));
|
|
32448
32317
|
});
|
|
32318
|
+
return sections;
|
|
32449
32319
|
}
|
|
32450
|
-
|
|
32451
|
-
|
|
32452
|
-
|
|
32453
|
-
|
|
32454
|
-
files: Type5.Array(Type5.String(), {
|
|
32455
|
-
description: "Paths to delete (one or more). May include directories when recursive=true.",
|
|
32456
|
-
minItems: 1
|
|
32457
|
-
}),
|
|
32458
|
-
recursive: Type5.Optional(Type5.Boolean({
|
|
32459
|
-
description: "Required to delete a directory and its contents. Defaults to false; passing a directory without this returns an error."
|
|
32460
|
-
}))
|
|
32461
|
-
});
|
|
32462
|
-
var MoveParams = Type5.Object({
|
|
32463
|
-
filePath: Type5.String({
|
|
32464
|
-
description: "Source file path to move (absolute or relative to project root)"
|
|
32465
|
-
}),
|
|
32466
|
-
destination: Type5.String({
|
|
32467
|
-
description: "Destination file path (absolute or relative to project root)"
|
|
32468
|
-
})
|
|
32469
|
-
});
|
|
32470
|
-
function renderFsCall(toolName, args, theme, context) {
|
|
32471
|
-
if (toolName === "aft_delete") {
|
|
32472
|
-
const files = args.files;
|
|
32473
|
-
const summary = files.length === 1 ? accentPath(theme, files[0]) : `${theme.fg("accent", String(files.length))} ${theme.fg("muted", "files")}`;
|
|
32474
|
-
return renderToolCall("delete", summary, theme, context);
|
|
32475
|
-
}
|
|
32476
|
-
const moveArgs = args;
|
|
32477
|
-
return renderToolCall("move", `${accentPath(theme, moveArgs.filePath)} ${theme.fg("muted", "→")} ${accentPath(theme, moveArgs.destination)}`, theme, context);
|
|
32320
|
+
function renderAstCall(toolName, args, theme, context) {
|
|
32321
|
+
const lang = theme.fg("accent", args.lang);
|
|
32322
|
+
const summary = toolName === "ast_grep_replace" ? `${lang} ${theme.fg("toolOutput", `${args.pattern} → ${args.rewrite}`)}` : `${lang} ${theme.fg("toolOutput", args.pattern)}`;
|
|
32323
|
+
return renderToolCall(toolName === "ast_grep_replace" ? "ast replace" : "ast search", summary, theme, context);
|
|
32478
32324
|
}
|
|
32479
|
-
function
|
|
32325
|
+
function renderAstResult(toolName, result, theme, context) {
|
|
32480
32326
|
if (context.isError) {
|
|
32481
32327
|
return renderErrorResult(result, `${toolName} failed`, theme, context);
|
|
32482
32328
|
}
|
|
32483
|
-
|
|
32484
|
-
|
|
32485
|
-
const
|
|
32486
|
-
|
|
32487
|
-
const skipped = data.skipped_files ?? [];
|
|
32488
|
-
const lines = [];
|
|
32489
|
-
for (const entry of deletedPaths) {
|
|
32490
|
-
lines.push(`${theme.fg("success", "✓ deleted")} ${theme.fg("accent", shortenPath(entry))}`);
|
|
32491
|
-
}
|
|
32492
|
-
for (const entry of skipped) {
|
|
32493
|
-
lines.push(`${theme.fg("error", "✗ skipped")} ${theme.fg("accent", shortenPath(entry.file))} ${theme.fg("muted", `(${entry.reason})`)}`);
|
|
32494
|
-
}
|
|
32495
|
-
if (lines.length === 0) {
|
|
32496
|
-
lines.push(theme.fg("muted", "(no files deleted)"));
|
|
32497
|
-
}
|
|
32498
|
-
return renderSections([lines.join(`
|
|
32499
|
-
`)], context);
|
|
32329
|
+
const payload = extractStructuredPayload(result);
|
|
32330
|
+
if (!payload) {
|
|
32331
|
+
const text = collectTextContent(result);
|
|
32332
|
+
return renderSections([text || theme.fg("muted", "No result.")], context);
|
|
32500
32333
|
}
|
|
32501
|
-
const
|
|
32502
|
-
return renderSections(
|
|
32503
|
-
`${theme.fg("success", "✓ moved")} ${theme.fg("accent", shortenPath(moveArgs.filePath))}`,
|
|
32504
|
-
`${theme.fg("muted", "to")} ${theme.fg("accent", shortenPath(moveArgs.destination))}`
|
|
32505
|
-
], context);
|
|
32334
|
+
const sections = toolName === "ast_grep_replace" ? buildAstReplaceSections(payload, theme) : buildAstSearchSections(payload, theme);
|
|
32335
|
+
return renderSections(sections, context);
|
|
32506
32336
|
}
|
|
32507
|
-
function
|
|
32508
|
-
if (surface.
|
|
32337
|
+
function registerAstTools(pi, ctx, surface) {
|
|
32338
|
+
if (surface.astSearch) {
|
|
32509
32339
|
pi.registerTool({
|
|
32510
|
-
name: "
|
|
32511
|
-
label: "
|
|
32512
|
-
description: "
|
|
32513
|
-
parameters:
|
|
32340
|
+
name: "ast_grep_search",
|
|
32341
|
+
label: "ast search",
|
|
32342
|
+
description: "Search code patterns across the filesystem using AST-aware matching. Use `$VAR` to match a single AST node, `$$$` for multiple. Pattern must be a complete, valid code fragment (include braces, params, etc.).",
|
|
32343
|
+
parameters: SearchParams,
|
|
32514
32344
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32345
|
+
const paths = await resolveAstPaths(extCtx, params.paths);
|
|
32346
|
+
await assertAstPathsPermission(extCtx, paths, "search", ctx.config.restrict_to_project_root ?? false);
|
|
32515
32347
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32516
|
-
const
|
|
32517
|
-
|
|
32518
|
-
|
|
32519
|
-
}
|
|
32520
|
-
|
|
32521
|
-
|
|
32522
|
-
|
|
32523
|
-
|
|
32524
|
-
|
|
32525
|
-
|
|
32526
|
-
|
|
32527
|
-
|
|
32528
|
-
const summary = deleted.length === 1 && skipped.length === 0 ? `Deleted ${deleted[0]}` : `Deleted ${deleted.length}/${params.files.length} file(s)`;
|
|
32529
|
-
return textResult(summary, {
|
|
32530
|
-
success: true,
|
|
32531
|
-
complete: skipped.length === 0,
|
|
32532
|
-
deleted,
|
|
32533
|
-
skipped_files: skipped
|
|
32534
|
-
});
|
|
32348
|
+
const req = {
|
|
32349
|
+
pattern: params.pattern,
|
|
32350
|
+
lang: params.lang
|
|
32351
|
+
};
|
|
32352
|
+
if (!isEmptyParam(paths))
|
|
32353
|
+
req.paths = paths;
|
|
32354
|
+
if (!isEmptyParam(params.globs))
|
|
32355
|
+
req.globs = params.globs;
|
|
32356
|
+
if (params.contextLines !== undefined)
|
|
32357
|
+
req.context_lines = params.contextLines;
|
|
32358
|
+
const response = await callBridge(bridge, "ast_search", req, extCtx);
|
|
32359
|
+
return textResult(response.text ?? JSON.stringify(response));
|
|
32535
32360
|
},
|
|
32536
32361
|
renderCall(args, theme, context) {
|
|
32537
|
-
return
|
|
32362
|
+
return renderAstCall("ast_grep_search", args, theme, context);
|
|
32538
32363
|
},
|
|
32539
32364
|
renderResult(result, _options, theme, context) {
|
|
32540
|
-
return
|
|
32365
|
+
return renderAstResult("ast_grep_search", result, theme, context);
|
|
32541
32366
|
}
|
|
32542
32367
|
});
|
|
32543
32368
|
}
|
|
32544
|
-
if (surface.
|
|
32369
|
+
if (surface.astReplace) {
|
|
32545
32370
|
pi.registerTool({
|
|
32546
|
-
name: "
|
|
32547
|
-
label: "
|
|
32548
|
-
description: "
|
|
32549
|
-
parameters:
|
|
32371
|
+
name: "ast_grep_replace",
|
|
32372
|
+
label: "ast replace",
|
|
32373
|
+
description: "Replace code patterns across the filesystem with AST-aware rewriting. Applies by default — pass `dryRun: true` to preview. Use meta-variables in `rewrite` to preserve captured content from the pattern.",
|
|
32374
|
+
parameters: ReplaceParams,
|
|
32550
32375
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32376
|
+
const paths = await resolveAstPaths(extCtx, params.paths);
|
|
32377
|
+
await assertAstPathsPermission(extCtx, paths, params.dryRun === true ? "search" : "modify", ctx.config.restrict_to_project_root ?? false);
|
|
32551
32378
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32552
|
-
const
|
|
32553
|
-
|
|
32554
|
-
|
|
32555
|
-
|
|
32556
|
-
|
|
32557
|
-
|
|
32558
|
-
|
|
32559
|
-
|
|
32379
|
+
const req = {
|
|
32380
|
+
pattern: params.pattern,
|
|
32381
|
+
rewrite: params.rewrite,
|
|
32382
|
+
lang: params.lang
|
|
32383
|
+
};
|
|
32384
|
+
if (!isEmptyParam(paths))
|
|
32385
|
+
req.paths = paths;
|
|
32386
|
+
if (!isEmptyParam(params.globs))
|
|
32387
|
+
req.globs = params.globs;
|
|
32388
|
+
req.dry_run = params.dryRun === true;
|
|
32389
|
+
const response = await callBridge(bridge, "ast_replace", req, extCtx);
|
|
32390
|
+
return textResult(response.text ?? JSON.stringify(response));
|
|
32391
|
+
},
|
|
32392
|
+
renderCall(args, theme, context) {
|
|
32393
|
+
return renderAstCall("ast_grep_replace", args, theme, context);
|
|
32560
32394
|
},
|
|
32561
32395
|
renderResult(result, _options, theme, context) {
|
|
32562
|
-
return
|
|
32396
|
+
return renderAstResult("ast_grep_replace", result, theme, context);
|
|
32563
32397
|
}
|
|
32564
32398
|
});
|
|
32565
32399
|
}
|
|
32566
32400
|
}
|
|
32567
32401
|
|
|
32568
|
-
// src/tools/
|
|
32569
|
-
import
|
|
32570
|
-
import { homedir as homedir9 } from "node:os";
|
|
32571
|
-
import { isAbsolute as isAbsolute2, relative as relative3, resolve as resolve3, sep } from "node:path";
|
|
32572
|
-
import {
|
|
32573
|
-
renderDiff as renderDiff2
|
|
32574
|
-
} from "@earendil-works/pi-coding-agent";
|
|
32402
|
+
// src/tools/bash.ts
|
|
32403
|
+
import * as fs3 from "node:fs/promises";
|
|
32575
32404
|
import { Container as Container3, Spacer as Spacer3, Text as Text3 } from "@earendil-works/pi-tui";
|
|
32576
|
-
import { Type as
|
|
32577
|
-
|
|
32578
|
-
|
|
32579
|
-
|
|
32580
|
-
var
|
|
32581
|
-
|
|
32582
|
-
|
|
32583
|
-
|
|
32584
|
-
|
|
32585
|
-
|
|
32586
|
-
|
|
32405
|
+
import { Type as Type4 } from "typebox";
|
|
32406
|
+
var FOREGROUND_POLL_INTERVAL_MS = 100;
|
|
32407
|
+
var BASH_WAIT_POLL_INTERVAL_MS = 100;
|
|
32408
|
+
var DEFAULT_BASH_STATUS_WAIT_TIMEOUT_MS = 30000;
|
|
32409
|
+
var MAX_BASH_STATUS_WAIT_TIMEOUT_MS = 300000;
|
|
32410
|
+
var BASH_TRANSPORT_TIMEOUT_MS = 30000;
|
|
32411
|
+
var BashParams = Type4.Object({
|
|
32412
|
+
command: Type4.String({
|
|
32413
|
+
description: "Shell command to execute. Supports pipes, redirections, and shell syntax."
|
|
32414
|
+
}),
|
|
32415
|
+
timeout: optionalInt(1, Number.MAX_SAFE_INTEGER),
|
|
32416
|
+
workdir: Type4.Optional(Type4.String({
|
|
32417
|
+
description: "Working directory for command execution. Relative paths resolve against the project root. Defaults to the current session's working directory."
|
|
32418
|
+
})),
|
|
32419
|
+
description: Type4.Optional(Type4.String({
|
|
32420
|
+
description: "Human-readable description shown in UI logs. Helps users understand what the command does without reading shell syntax."
|
|
32421
|
+
})),
|
|
32422
|
+
background: Type4.Optional(Type4.Boolean({
|
|
32423
|
+
description: "Spawn command in background and return immediately with a task_id. Use bash_status to poll completion and bash_kill to terminate. Ideal for long-running tasks like builds or dev servers."
|
|
32424
|
+
})),
|
|
32425
|
+
compressed: Type4.Optional(Type4.Boolean({
|
|
32426
|
+
description: "Compress output by removing ANSI codes, carriage returns, and excessive blank lines. Default: true. Set to false for raw terminal output including color codes."
|
|
32427
|
+
})),
|
|
32428
|
+
pty: Type4.Optional(Type4.Boolean({
|
|
32429
|
+
description: 'Spawn the command in a real PTY for interactive programs. Implies background: true automatically. Inspect with bash_status({ task_id, output_mode: "screen" }) and send input with bash_write.'
|
|
32430
|
+
})),
|
|
32431
|
+
ptyRows: optionalInt(1, 60),
|
|
32432
|
+
ptyCols: optionalInt(1, 140)
|
|
32433
|
+
});
|
|
32434
|
+
var BashTaskParams = Type4.Object({
|
|
32435
|
+
task_id: Type4.String({
|
|
32436
|
+
description: "Background bash task id returned by bash({ background: true })."
|
|
32437
|
+
})
|
|
32438
|
+
});
|
|
32439
|
+
var BashStatusParams = Type4.Object({
|
|
32440
|
+
task_id: Type4.String({
|
|
32441
|
+
description: "Background bash task id returned by bash({ background: true })."
|
|
32442
|
+
}),
|
|
32443
|
+
output_mode: Type4.Optional(Type4.Union([Type4.Literal("screen"), Type4.Literal("raw"), Type4.Literal("both")], {
|
|
32444
|
+
description: "PTY output rendering mode. Defaults to screen for PTY tasks and preserves existing behavior for piped tasks when omitted."
|
|
32445
|
+
}))
|
|
32446
|
+
});
|
|
32447
|
+
var BashWatchParams = Type4.Object({
|
|
32448
|
+
task_id: Type4.String({
|
|
32449
|
+
description: "Background bash task id returned by bash({ background: true })."
|
|
32450
|
+
}),
|
|
32451
|
+
pattern: Type4.Optional(Type4.Union([Type4.String(), Type4.Object({ regex: Type4.String() })])),
|
|
32452
|
+
background: Type4.Optional(Type4.Boolean()),
|
|
32453
|
+
timeout_ms: optionalInt(1, MAX_BASH_STATUS_WAIT_TIMEOUT_MS),
|
|
32454
|
+
once: Type4.Optional(Type4.Boolean())
|
|
32455
|
+
});
|
|
32456
|
+
var BashWriteParams = Type4.Object({
|
|
32457
|
+
task_id: Type4.String({
|
|
32458
|
+
description: "Background PTY task id returned by bash({ pty: true, background: true })."
|
|
32459
|
+
}),
|
|
32460
|
+
input: Type4.Union([
|
|
32461
|
+
Type4.String(),
|
|
32462
|
+
Type4.Array(Type4.Union([
|
|
32463
|
+
Type4.String(),
|
|
32464
|
+
Type4.Object({
|
|
32465
|
+
key: Type4.String({
|
|
32466
|
+
description: "Named control key, e.g. 'esc', 'enter', 'up', 'ctrl-c'. Case-insensitive."
|
|
32467
|
+
})
|
|
32468
|
+
})
|
|
32469
|
+
]))
|
|
32470
|
+
], {
|
|
32471
|
+
description: "Either a string of verbatim bytes (e.g. 'print(1)\\n') OR an array mixing strings " + "and { key: '<name>' } objects for atomic text+key sequences. " + "Example: [ 'iHello', { key: 'esc' }, ':wq', { key: 'enter' } ]. " + "Allowed key names: enter, return, tab, space, backspace, esc, escape, up, down, " + "left, right, home, end, page-up, page-down, delete, insert, f1..f12, ctrl-a..ctrl-z."
|
|
32472
|
+
})
|
|
32473
|
+
});
|
|
32474
|
+
function truncateToVisualLines(text, maxLines) {
|
|
32475
|
+
const lines = text.split(`
|
|
32587
32476
|
`);
|
|
32588
|
-
|
|
32589
|
-
|
|
32590
|
-
|
|
32591
|
-
const blank = " ".repeat(lineNumWidth);
|
|
32592
|
-
let oldLineNum = 1;
|
|
32593
|
-
let newLineNum = 1;
|
|
32594
|
-
let lastWasChange = false;
|
|
32595
|
-
let firstChangedLine;
|
|
32596
|
-
for (let i = 0;i < parts.length; i++) {
|
|
32597
|
-
const part = parts[i];
|
|
32598
|
-
const raw = part.value.split(`
|
|
32477
|
+
if (lines.length <= maxLines)
|
|
32478
|
+
return text;
|
|
32479
|
+
return lines.slice(-maxLines).join(`
|
|
32599
32480
|
`);
|
|
32600
|
-
|
|
32601
|
-
|
|
32602
|
-
|
|
32603
|
-
|
|
32604
|
-
|
|
32605
|
-
|
|
32606
|
-
|
|
32607
|
-
|
|
32608
|
-
|
|
32609
|
-
|
|
32610
|
-
|
|
32611
|
-
|
|
32481
|
+
}
|
|
32482
|
+
function reuseText3(last) {
|
|
32483
|
+
return last instanceof Text3 ? last : new Text3("", 0, 0);
|
|
32484
|
+
}
|
|
32485
|
+
function reuseContainer3(last) {
|
|
32486
|
+
return last instanceof Container3 ? last : new Container3;
|
|
32487
|
+
}
|
|
32488
|
+
function getBashSpawnHook(pi) {
|
|
32489
|
+
const api2 = pi;
|
|
32490
|
+
if (typeof api2.getHook === "function") {
|
|
32491
|
+
return api2.getHook("bashSpawn");
|
|
32492
|
+
}
|
|
32493
|
+
return api2.hooks?.bashSpawn;
|
|
32494
|
+
}
|
|
32495
|
+
function registerBashTool(pi, ctx) {
|
|
32496
|
+
const spawnHook = getBashSpawnHook(pi);
|
|
32497
|
+
pi.registerTool({
|
|
32498
|
+
name: "bash",
|
|
32499
|
+
label: "bash",
|
|
32500
|
+
description: 'Execute shell commands through AFT\'s Rust bash handler. By default, output is compressed. Pass `compressed: false` for raw output. Pass `background: true` to spawn in the background and get a task_id for `bash_status`/`bash_kill`. Pass `pty: true` with `background: true` for interactive programs and drive them with `bash_status({ output_mode: "screen" })` plus `bash_write`.',
|
|
32501
|
+
promptSnippet: "Run shell commands (timeout in milliseconds; supports workdir, background tasks, compressed output, PTY mode)",
|
|
32502
|
+
promptGuidelines: [
|
|
32503
|
+
"Use bash only when a dedicated AFT tool is not a better fit.",
|
|
32504
|
+
"Set compressed: false when you need ANSI color codes in the output."
|
|
32505
|
+
],
|
|
32506
|
+
parameters: BashParams,
|
|
32507
|
+
async execute(_toolCallId, params, _signal, onUpdate, extCtx) {
|
|
32508
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32509
|
+
const foregroundWaitMs = resolveBashConfig(ctx.config).foreground_wait_window_ms;
|
|
32510
|
+
const timeout = coerceOptionalInt(params.timeout, "timeout", 1, Number.MAX_SAFE_INTEGER);
|
|
32511
|
+
const ptyRows = coerceOptionalInt(params.ptyRows, "ptyRows", 1, 60);
|
|
32512
|
+
const ptyCols = coerceOptionalInt(params.ptyCols, "ptyCols", 1, 140);
|
|
32513
|
+
const effectiveBackground = params.background === true || params.pty === true;
|
|
32514
|
+
let spawnContext = {
|
|
32515
|
+
command: params.command,
|
|
32516
|
+
cwd: params.workdir
|
|
32517
|
+
};
|
|
32518
|
+
if (spawnHook) {
|
|
32519
|
+
try {
|
|
32520
|
+
spawnContext = await spawnHook(spawnContext);
|
|
32521
|
+
} catch (hookErr) {
|
|
32522
|
+
throw new Error(`BashSpawnHook failed: ${hookErr instanceof Error ? hookErr.message : String(hookErr)}`);
|
|
32612
32523
|
}
|
|
32613
32524
|
}
|
|
32614
|
-
|
|
32615
|
-
|
|
32616
|
-
|
|
32617
|
-
|
|
32618
|
-
|
|
32619
|
-
|
|
32620
|
-
|
|
32621
|
-
|
|
32622
|
-
|
|
32623
|
-
|
|
32624
|
-
|
|
32625
|
-
|
|
32626
|
-
|
|
32627
|
-
}
|
|
32628
|
-
|
|
32629
|
-
|
|
32630
|
-
|
|
32631
|
-
|
|
32525
|
+
let streamed = "";
|
|
32526
|
+
const response = await callBridge(bridge, "bash", {
|
|
32527
|
+
command: spawnContext.command,
|
|
32528
|
+
timeout,
|
|
32529
|
+
workdir: spawnContext.cwd ?? params.workdir,
|
|
32530
|
+
env: spawnContext.env,
|
|
32531
|
+
description: params.description,
|
|
32532
|
+
background: effectiveBackground,
|
|
32533
|
+
notify_on_completion: effectiveBackground,
|
|
32534
|
+
compressed: params.compressed,
|
|
32535
|
+
pty: params.pty,
|
|
32536
|
+
pty_rows: ptyRows,
|
|
32537
|
+
pty_cols: ptyCols
|
|
32538
|
+
}, extCtx, {
|
|
32539
|
+
transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS,
|
|
32540
|
+
keepBridgeOnTimeout: true,
|
|
32541
|
+
onProgress: ({ text }) => {
|
|
32542
|
+
streamed += text;
|
|
32543
|
+
const displayText = truncateToVisualLines(streamed, 100);
|
|
32544
|
+
onUpdate?.(bashResult(displayText, { streaming: true }));
|
|
32632
32545
|
}
|
|
32633
|
-
|
|
32634
|
-
|
|
32635
|
-
|
|
32636
|
-
newLineNum += skipped;
|
|
32637
|
-
for (const line of raw.slice(raw.length - contextLines)) {
|
|
32638
|
-
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
32639
|
-
oldLineNum++;
|
|
32640
|
-
newLineNum++;
|
|
32546
|
+
}).catch((err) => {
|
|
32547
|
+
if (err instanceof Error && err.message.includes("permission_required")) {
|
|
32548
|
+
throw new Error("Permission ask reached Pi adapter — this is a bug. Pi has no permission system.");
|
|
32641
32549
|
}
|
|
32550
|
+
throw err;
|
|
32551
|
+
});
|
|
32552
|
+
if (response.success === false) {
|
|
32553
|
+
throw new Error(response.message ?? "bash failed");
|
|
32642
32554
|
}
|
|
32643
|
-
|
|
32644
|
-
|
|
32645
|
-
|
|
32646
|
-
|
|
32647
|
-
|
|
32648
|
-
|
|
32649
|
-
|
|
32650
|
-
|
|
32651
|
-
|
|
32652
|
-
|
|
32653
|
-
|
|
32654
|
-
|
|
32655
|
-
|
|
32656
|
-
|
|
32657
|
-
|
|
32658
|
-
|
|
32659
|
-
|
|
32660
|
-
|
|
32661
|
-
|
|
32662
|
-
|
|
32663
|
-
|
|
32555
|
+
const taskId = response.task_id;
|
|
32556
|
+
if (response.status === "running" && taskId) {
|
|
32557
|
+
if (effectiveBackground) {
|
|
32558
|
+
trackBgTask(resolveSessionId(extCtx), taskId);
|
|
32559
|
+
return bashResult(formatBackgroundLaunch(taskId, params.pty === true), {
|
|
32560
|
+
task_id: taskId
|
|
32561
|
+
});
|
|
32562
|
+
}
|
|
32563
|
+
const waitTimeoutMs = timeout !== undefined ? Math.min(timeout, foregroundWaitMs) : foregroundWaitMs;
|
|
32564
|
+
const startedAt = Date.now();
|
|
32565
|
+
while (true) {
|
|
32566
|
+
const status = await callBridge(bridge, "bash_status", { task_id: taskId }, extCtx);
|
|
32567
|
+
if (status.success === false) {
|
|
32568
|
+
throw new Error(status.message ?? "bash_status failed");
|
|
32569
|
+
}
|
|
32570
|
+
if (isTerminalStatus(status.status)) {
|
|
32571
|
+
return bashResult(formatForegroundResult(status), {
|
|
32572
|
+
exit_code: status.exit_code,
|
|
32573
|
+
duration_ms: status.duration_ms,
|
|
32574
|
+
truncated: status.output_truncated,
|
|
32575
|
+
output_path: status.output_path,
|
|
32576
|
+
task_id: taskId
|
|
32577
|
+
});
|
|
32578
|
+
}
|
|
32579
|
+
if (Date.now() - startedAt >= waitTimeoutMs) {
|
|
32580
|
+
const promoted = await callBridge(bridge, "bash_promote", { task_id: taskId }, extCtx);
|
|
32581
|
+
if (promoted.success === false) {
|
|
32582
|
+
throw new Error(promoted.message ?? "bash_promote failed");
|
|
32583
|
+
}
|
|
32584
|
+
trackBgTask(resolveSessionId(extCtx), taskId);
|
|
32585
|
+
return bashResult(formatPromotionMessage(taskId, params.timeout, foregroundWaitMs), {
|
|
32586
|
+
task_id: taskId
|
|
32587
|
+
});
|
|
32588
|
+
}
|
|
32589
|
+
await sleep(FOREGROUND_POLL_INTERVAL_MS);
|
|
32590
|
+
}
|
|
32664
32591
|
}
|
|
32665
|
-
|
|
32666
|
-
|
|
32667
|
-
|
|
32668
|
-
|
|
32669
|
-
|
|
32670
|
-
|
|
32671
|
-
|
|
32672
|
-
|
|
32592
|
+
const details = {
|
|
32593
|
+
exit_code: response.exit_code,
|
|
32594
|
+
duration_ms: response.duration_ms,
|
|
32595
|
+
truncated: response.truncated,
|
|
32596
|
+
output_path: response.output_path,
|
|
32597
|
+
task_id: taskId
|
|
32598
|
+
};
|
|
32599
|
+
const output = response.output ?? "";
|
|
32600
|
+
return bashResult(output, details);
|
|
32601
|
+
},
|
|
32602
|
+
renderCall(args, theme, context) {
|
|
32603
|
+
return renderBashCall(args?.command, args?.description, theme, context);
|
|
32604
|
+
},
|
|
32605
|
+
renderResult(result, _options, theme, context) {
|
|
32606
|
+
return renderBashResult(result, theme, context);
|
|
32673
32607
|
}
|
|
32674
|
-
|
|
32608
|
+
});
|
|
32609
|
+
pi.registerTool(createBashStatusTool(ctx));
|
|
32610
|
+
pi.registerTool(createBashWatchTool(ctx));
|
|
32611
|
+
pi.registerTool(createBashWriteTool(ctx));
|
|
32612
|
+
pi.registerTool(createBashKillTool(ctx));
|
|
32613
|
+
}
|
|
32614
|
+
function formatBackgroundLaunch(taskId, isPty) {
|
|
32615
|
+
if (isPty) {
|
|
32616
|
+
return `PTY task started: ${taskId}. Use bash_status({ task_id: "${taskId}", output_mode: "screen" }) to see the visible terminal, bash_write({ task_id: "${taskId}", input: ... }) to send keystrokes. A completion reminder fires automatically when the task exits.`;
|
|
32675
32617
|
}
|
|
32676
|
-
return
|
|
32677
|
-
`), firstChangedLine };
|
|
32618
|
+
return `Background task started: ${taskId}. A completion reminder will be delivered automatically; don't poll bash_status.`;
|
|
32678
32619
|
}
|
|
32679
|
-
|
|
32680
|
-
|
|
32681
|
-
|
|
32682
|
-
function diagnosticsOnEditDefault(ctx) {
|
|
32683
|
-
return ctx.config.lsp?.diagnostics_on_edit ?? false;
|
|
32620
|
+
function formatPromotionMessage(taskId, timeout, waitWindowMs) {
|
|
32621
|
+
const waited = timeout !== undefined ? Math.min(timeout, waitWindowMs) : waitWindowMs;
|
|
32622
|
+
return `Foreground bash didn't finish within ${formatSeconds(waited)} and was promoted to background: ${taskId}. A completion reminder will be delivered automatically; use bash_status({ task_id: "${taskId}" }) to inspect output or bash_kill({ task_id: "${taskId}" }) to terminate.`;
|
|
32684
32623
|
}
|
|
32685
|
-
function
|
|
32686
|
-
|
|
32687
|
-
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
32624
|
+
function formatSeconds(ms) {
|
|
32625
|
+
return `${Number((ms / 1000).toFixed(1))}s`;
|
|
32688
32626
|
}
|
|
32689
|
-
function
|
|
32690
|
-
|
|
32691
|
-
|
|
32692
|
-
|
|
32693
|
-
|
|
32694
|
-
|
|
32695
|
-
|
|
32627
|
+
function formatForegroundResult(data) {
|
|
32628
|
+
const output = data.output_preview ?? "";
|
|
32629
|
+
const outputPath = data.output_path;
|
|
32630
|
+
const truncated = data.output_truncated === true;
|
|
32631
|
+
const status = data.status;
|
|
32632
|
+
const exit = data.exit_code;
|
|
32633
|
+
let rendered = output;
|
|
32634
|
+
if (truncated && outputPath) {
|
|
32635
|
+
rendered += `
|
|
32636
|
+
[output truncated; full output at ${outputPath}]`;
|
|
32696
32637
|
}
|
|
32697
|
-
|
|
32638
|
+
if (status === "timed_out") {
|
|
32639
|
+
rendered += `
|
|
32640
|
+
[command timed out]`;
|
|
32641
|
+
}
|
|
32642
|
+
if (typeof exit === "number" && exit !== 0) {
|
|
32643
|
+
rendered += `
|
|
32644
|
+
[exit code: ${exit}]`;
|
|
32645
|
+
}
|
|
32646
|
+
return rendered;
|
|
32698
32647
|
}
|
|
32699
|
-
function
|
|
32700
|
-
|
|
32701
|
-
if (raw === undefined)
|
|
32702
|
-
return 30000;
|
|
32703
|
-
const parsed = Number.parseInt(raw, 10);
|
|
32704
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : 30000;
|
|
32648
|
+
function sleep(ms) {
|
|
32649
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
32705
32650
|
}
|
|
32706
|
-
|
|
32707
|
-
|
|
32708
|
-
|
|
32709
|
-
|
|
32710
|
-
|
|
32711
|
-
|
|
32712
|
-
|
|
32713
|
-
|
|
32714
|
-
|
|
32715
|
-
|
|
32716
|
-
|
|
32717
|
-
|
|
32718
|
-
}
|
|
32719
|
-
const timeoutMs = externalDirectoryPromptTimeoutMs();
|
|
32720
|
-
let timer;
|
|
32721
|
-
const timeoutPromise = new Promise((resolve4) => {
|
|
32722
|
-
timer = setTimeout(() => resolve4("timeout"), timeoutMs);
|
|
32723
|
-
});
|
|
32724
|
-
try {
|
|
32725
|
-
const result = await Promise.race([
|
|
32726
|
-
confirmFn("Allow external directory access?", `AFT wants to ${action} outside the project: ${absoluteTarget}`),
|
|
32727
|
-
timeoutPromise
|
|
32728
|
-
]);
|
|
32729
|
-
if (result === true)
|
|
32730
|
-
return;
|
|
32731
|
-
if (result === "timeout") {
|
|
32732
|
-
throw new Error(`Permission denied: external directory prompt timed out after ${timeoutMs}ms.`);
|
|
32651
|
+
function createBashStatusTool(ctx) {
|
|
32652
|
+
return {
|
|
32653
|
+
name: "bash_status",
|
|
32654
|
+
label: "bash_status",
|
|
32655
|
+
description: "Read-only snapshot of a background bash task. Returns immediately. Never waits. Use bash_watch to block on or register for pattern matches and exit events.",
|
|
32656
|
+
promptSnippet: "Inspect a background bash task by task_id",
|
|
32657
|
+
parameters: BashStatusParams,
|
|
32658
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32659
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32660
|
+
const data = await bashStatusSnapshot(bridge, extCtx, params.task_id, params.output_mode);
|
|
32661
|
+
const details = data;
|
|
32662
|
+
return bashStatusResult(await formatBashStatus(extCtx, params.task_id, details, params.output_mode), details);
|
|
32733
32663
|
}
|
|
32734
|
-
|
|
32735
|
-
} finally {
|
|
32736
|
-
if (timer !== undefined)
|
|
32737
|
-
clearTimeout(timer);
|
|
32738
|
-
}
|
|
32664
|
+
};
|
|
32739
32665
|
}
|
|
32740
|
-
|
|
32741
|
-
|
|
32742
|
-
|
|
32743
|
-
|
|
32744
|
-
|
|
32745
|
-
|
|
32746
|
-
|
|
32747
|
-
|
|
32748
|
-
|
|
32749
|
-
|
|
32750
|
-
|
|
32751
|
-
|
|
32752
|
-
|
|
32753
|
-
});
|
|
32754
|
-
var EditParams = Type6.Object({
|
|
32755
|
-
filePath: Type6.String({
|
|
32756
|
-
description: "Path to the file to edit (absolute or relative to project root)"
|
|
32757
|
-
}),
|
|
32758
|
-
oldString: Type6.Optional(Type6.String({ description: "Text to find (exact match, fuzzy fallback)" })),
|
|
32759
|
-
newString: Type6.Optional(Type6.String({ description: "Replacement text (omit to delete match)" })),
|
|
32760
|
-
replaceAll: Type6.Optional(Type6.Boolean({ description: "Replace every occurrence" })),
|
|
32761
|
-
occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER),
|
|
32762
|
-
appendContent: Type6.Optional(Type6.String({
|
|
32763
|
-
description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). When set, oldString/newString are ignored."
|
|
32764
|
-
})),
|
|
32765
|
-
diagnostics: Type6.Optional(Type6.Boolean({ description: DIAGNOSTICS_PARAM_DESCRIPTION }))
|
|
32766
|
-
});
|
|
32767
|
-
var GrepParams = Type6.Object({
|
|
32768
|
-
pattern: Type6.String({ description: "Regex pattern to search for" }),
|
|
32769
|
-
path: Type6.Optional(Type6.String({
|
|
32770
|
-
description: "Path scope (file or directory; absolute or relative to project root)"
|
|
32771
|
-
})),
|
|
32772
|
-
include: Type6.Optional(Type6.String({ description: "Glob filter for included files (e.g. '*.ts,*.tsx')" })),
|
|
32773
|
-
caseSensitive: Type6.Optional(Type6.Boolean({ description: "Case-sensitive matching" })),
|
|
32774
|
-
contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER)
|
|
32775
|
-
});
|
|
32776
|
-
function registerHoistedTools(pi, ctx, surface) {
|
|
32777
|
-
if (surface.hoistRead) {
|
|
32778
|
-
pi.registerTool({
|
|
32779
|
-
name: "read",
|
|
32780
|
-
label: "read",
|
|
32781
|
-
description: "Read file contents with line numbers. Backed by AFT's indexed Rust reader — faster than the built-in `read` on large repos and correctly handles images/PDFs as attachments.",
|
|
32782
|
-
promptSnippet: "Read file contents (supports offset/limit for large files)",
|
|
32783
|
-
promptGuidelines: ["Use read to examine files instead of cat or sed."],
|
|
32784
|
-
parameters: ReadParams,
|
|
32785
|
-
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32786
|
-
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32787
|
-
const offset = coerceOptionalInt(params.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
|
|
32788
|
-
const limit = coerceOptionalInt(params.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
|
|
32789
|
-
const req = { file: params.path };
|
|
32790
|
-
if (offset !== undefined) {
|
|
32791
|
-
req.start_line = offset;
|
|
32792
|
-
if (limit !== undefined) {
|
|
32793
|
-
req.end_line = offset + limit - 1;
|
|
32794
|
-
}
|
|
32795
|
-
} else if (limit !== undefined) {
|
|
32796
|
-
req.end_line = limit;
|
|
32666
|
+
function createBashWatchTool(ctx) {
|
|
32667
|
+
return {
|
|
32668
|
+
name: "bash_watch",
|
|
32669
|
+
label: "bash_watch",
|
|
32670
|
+
description: "Block on a background bash task until a pattern matches, it exits, or timeout elapses; or register an async pattern notification with background:true.",
|
|
32671
|
+
promptSnippet: "Wait for or watch a background bash task",
|
|
32672
|
+
parameters: BashWatchParams,
|
|
32673
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32674
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32675
|
+
const waitFor = parseWaitPattern(params.pattern);
|
|
32676
|
+
if (params.background === true) {
|
|
32677
|
+
if (!waitFor) {
|
|
32678
|
+
throw new Error("invalid_request: Use auto-reminder; bash_watch without pattern in async mode is redundant");
|
|
32797
32679
|
}
|
|
32798
|
-
const
|
|
32799
|
-
|
|
32800
|
-
|
|
32801
|
-
|
|
32680
|
+
const notifyParams = {
|
|
32681
|
+
task_id: params.task_id,
|
|
32682
|
+
once: params.once !== false
|
|
32683
|
+
};
|
|
32684
|
+
if (waitFor.kind === "regex")
|
|
32685
|
+
notifyParams.regex = waitFor.source;
|
|
32686
|
+
else
|
|
32687
|
+
notifyParams.pattern = waitFor.value;
|
|
32688
|
+
const sessionId = resolveSessionId(extCtx);
|
|
32689
|
+
markExplicitControl(sessionId, params.task_id, false);
|
|
32690
|
+
let registered;
|
|
32691
|
+
try {
|
|
32692
|
+
registered = await callBridge(bridge, "bash_notify", notifyParams, extCtx);
|
|
32693
|
+
} catch (err) {
|
|
32694
|
+
unmarkExplicitControl(sessionId, params.task_id);
|
|
32695
|
+
throw err;
|
|
32802
32696
|
}
|
|
32803
|
-
|
|
32804
|
-
|
|
32805
|
-
|
|
32806
|
-
|
|
32807
|
-
|
|
32808
|
-
|
|
32697
|
+
if (registered.success === false) {
|
|
32698
|
+
unmarkExplicitControl(sessionId, params.task_id);
|
|
32699
|
+
const message = String(registered.message ?? "bash_notify failed");
|
|
32700
|
+
throw new Error(`${String(registered.code ?? "invalid_request")}: ${message}`);
|
|
32701
|
+
}
|
|
32702
|
+
markExplicitControl(sessionId, params.task_id);
|
|
32703
|
+
const watchDetails = { registered: true, watchId: registered.watch_id };
|
|
32704
|
+
return textResult(`Watch registered: ${registered.watch_id} on task ${params.task_id}
|
|
32705
|
+
A notification will fire when the pattern matches or the task exits.`, watchDetails);
|
|
32809
32706
|
}
|
|
32810
|
-
|
|
32811
|
-
|
|
32812
|
-
|
|
32813
|
-
|
|
32814
|
-
|
|
32815
|
-
|
|
32816
|
-
|
|
32817
|
-
|
|
32818
|
-
|
|
32819
|
-
|
|
32820
|
-
|
|
32821
|
-
|
|
32822
|
-
|
|
32823
|
-
|
|
32824
|
-
|
|
32825
|
-
|
|
32826
|
-
|
|
32827
|
-
|
|
32828
|
-
|
|
32829
|
-
|
|
32830
|
-
|
|
32831
|
-
|
|
32832
|
-
|
|
32833
|
-
|
|
32834
|
-
|
|
32835
|
-
|
|
32836
|
-
|
|
32837
|
-
|
|
32707
|
+
const data = await waitForBashStatus(ctx, bridge, extCtx, params.task_id, undefined, waitFor, true, Math.min(coerceOptionalInt(params.timeout_ms, "timeout_ms", 1, MAX_BASH_STATUS_WAIT_TIMEOUT_MS) ?? DEFAULT_BASH_STATUS_WAIT_TIMEOUT_MS, MAX_BASH_STATUS_WAIT_TIMEOUT_MS));
|
|
32708
|
+
const text = await formatBashStatus(extCtx, params.task_id, data, undefined);
|
|
32709
|
+
return textResult(text, data);
|
|
32710
|
+
}
|
|
32711
|
+
};
|
|
32712
|
+
}
|
|
32713
|
+
function createBashWriteTool(ctx) {
|
|
32714
|
+
return {
|
|
32715
|
+
name: "bash_write",
|
|
32716
|
+
label: "bash_write",
|
|
32717
|
+
description: 'Write input bytes to a running PTY bash task. PTY-only; check bash_status reports mode: "pty" first. ' + 'Input is either a string (verbatim bytes) or an array mixing strings and { key: "esc" | "enter" | "up" | "ctrl-c" | ... } objects ' + 'for atomic text+key sequences such as [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ]. ' + "Named keys cover enter/return/tab/space/backspace/esc/escape, arrows, home/end/page-up/page-down/delete/insert, f1..f12, and ctrl-a..ctrl-z. " + "Maximum 1 MiB per call (post-expansion).",
|
|
32718
|
+
promptSnippet: "Write keystrokes/input to a PTY bash task",
|
|
32719
|
+
parameters: BashWriteParams,
|
|
32720
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32721
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32722
|
+
const data = await callBridge(bridge, "bash_write", { task_id: params.task_id, input: params.input }, extCtx);
|
|
32723
|
+
return textResult(JSON.stringify({ bytes_written: data.bytes_written }, null, 2), data);
|
|
32724
|
+
}
|
|
32725
|
+
};
|
|
32726
|
+
}
|
|
32727
|
+
function createBashKillTool(ctx) {
|
|
32728
|
+
return {
|
|
32729
|
+
name: "bash_kill",
|
|
32730
|
+
label: "bash_kill",
|
|
32731
|
+
description: "Terminate a running background bash task spawned with bash({ background: true }).",
|
|
32732
|
+
promptSnippet: "Kill a background bash task by task_id",
|
|
32733
|
+
parameters: BashTaskParams,
|
|
32734
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32735
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32736
|
+
const data = await callBridge(bridge, "bash_kill", { task_id: params.task_id }, extCtx);
|
|
32737
|
+
if (data.success === false) {
|
|
32738
|
+
throw new Error(data.message ?? "bash_kill failed");
|
|
32838
32739
|
}
|
|
32839
|
-
|
|
32840
|
-
|
|
32841
|
-
|
|
32842
|
-
|
|
32843
|
-
|
|
32844
|
-
|
|
32845
|
-
|
|
32846
|
-
|
|
32847
|
-
|
|
32848
|
-
|
|
32849
|
-
|
|
32850
|
-
|
|
32851
|
-
|
|
32852
|
-
|
|
32853
|
-
|
|
32854
|
-
|
|
32855
|
-
|
|
32856
|
-
|
|
32857
|
-
|
|
32858
|
-
|
|
32859
|
-
|
|
32860
|
-
|
|
32861
|
-
|
|
32862
|
-
|
|
32863
|
-
|
|
32864
|
-
|
|
32865
|
-
|
|
32866
|
-
|
|
32867
|
-
|
|
32740
|
+
await disposePtyTerminal(ptyCacheKey(extCtx, params.task_id));
|
|
32741
|
+
const details = data;
|
|
32742
|
+
if (details.kill_signaled === true) {
|
|
32743
|
+
return bashKillResult(`Task ${params.task_id}: kill_signaled`, details);
|
|
32744
|
+
}
|
|
32745
|
+
return bashKillResult(`Task ${params.task_id}: ${details.status}`, details);
|
|
32746
|
+
}
|
|
32747
|
+
};
|
|
32748
|
+
}
|
|
32749
|
+
function bashResult(output, details) {
|
|
32750
|
+
return {
|
|
32751
|
+
content: [{ type: "text", text: output }],
|
|
32752
|
+
details: {
|
|
32753
|
+
exit_code: details.exit_code,
|
|
32754
|
+
duration_ms: details.duration_ms,
|
|
32755
|
+
truncated: details.truncated,
|
|
32756
|
+
output_path: details.output_path,
|
|
32757
|
+
task_id: details.task_id,
|
|
32758
|
+
bg_completions: details.bg_completions
|
|
32759
|
+
}
|
|
32760
|
+
};
|
|
32761
|
+
}
|
|
32762
|
+
function bashStatusResult(output, details) {
|
|
32763
|
+
return {
|
|
32764
|
+
content: [{ type: "text", text: output }],
|
|
32765
|
+
details
|
|
32766
|
+
};
|
|
32767
|
+
}
|
|
32768
|
+
function bashKillResult(output, details) {
|
|
32769
|
+
return {
|
|
32770
|
+
content: [{ type: "text", text: output }],
|
|
32771
|
+
details
|
|
32772
|
+
};
|
|
32773
|
+
}
|
|
32774
|
+
async function bashStatusSnapshot(bridge, extCtx, taskId, outputMode, options) {
|
|
32775
|
+
return await callBridge(bridge, "bash_status", { task_id: taskId, output_mode: outputMode }, extCtx, options);
|
|
32776
|
+
}
|
|
32777
|
+
async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFor, waitForExit, effectiveWaitMs) {
|
|
32778
|
+
const startedAt = Date.now();
|
|
32779
|
+
const deadline = startedAt + effectiveWaitMs;
|
|
32780
|
+
let spillCursor = { output: 0, stderr: 0, combined: 0 };
|
|
32781
|
+
let scanText = "";
|
|
32782
|
+
let scanBaseOffset = 0;
|
|
32783
|
+
const bridgeOptions = {
|
|
32784
|
+
keepBridgeOnTimeout: true,
|
|
32785
|
+
transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS
|
|
32786
|
+
};
|
|
32787
|
+
const sessionId = resolveSessionId(extCtx);
|
|
32788
|
+
if (waitForExit)
|
|
32789
|
+
markTaskWaiting(sessionId, taskId);
|
|
32790
|
+
let sawTerminal = false;
|
|
32791
|
+
try {
|
|
32792
|
+
while (true) {
|
|
32793
|
+
const data = await bashStatusSnapshot(bridge, extCtx, taskId, outputMode, bridgeOptions);
|
|
32794
|
+
const terminal = isTerminalStatus(data.status);
|
|
32795
|
+
if (waitFor) {
|
|
32796
|
+
const scan = await readNewTaskOutput(extCtx, taskId, data, spillCursor);
|
|
32797
|
+
if (scan) {
|
|
32798
|
+
spillCursor = scan.nextCursor;
|
|
32799
|
+
if (scanText.length === 0)
|
|
32800
|
+
scanBaseOffset = scan.baseOffset;
|
|
32801
|
+
scanText += scan.text;
|
|
32802
|
+
const match = findWaitMatch(scanText, waitFor);
|
|
32803
|
+
if (match) {
|
|
32804
|
+
if (waitForExit && terminal) {
|
|
32805
|
+
sawTerminal = true;
|
|
32806
|
+
consumeBgCompletion(sessionId, taskId);
|
|
32807
|
+
await markBgCompletionDelivered({ ctx, directory: extCtx.cwd, sessionID: sessionId }, taskId);
|
|
32808
|
+
}
|
|
32809
|
+
return withWaited(data, {
|
|
32810
|
+
reason: "matched",
|
|
32811
|
+
elapsed_ms: Date.now() - startedAt,
|
|
32812
|
+
match: match.text,
|
|
32813
|
+
match_offset: scanBaseOffset + Buffer.byteLength(scanText.slice(0, match.index), "utf8")
|
|
32814
|
+
});
|
|
32815
|
+
}
|
|
32868
32816
|
}
|
|
32869
|
-
const req = {
|
|
32870
|
-
file: params.filePath,
|
|
32871
|
-
match: params.oldString ?? "",
|
|
32872
|
-
replacement: params.newString ?? "",
|
|
32873
|
-
diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
|
|
32874
|
-
include_diff_content: true
|
|
32875
|
-
};
|
|
32876
|
-
if (params.replaceAll === true)
|
|
32877
|
-
req.replace_all = true;
|
|
32878
|
-
const occurrence = coerceOptionalInt(params.occurrence, "occurrence", 0, Number.MAX_SAFE_INTEGER);
|
|
32879
|
-
if (occurrence !== undefined)
|
|
32880
|
-
req.occurrence = occurrence;
|
|
32881
|
-
const response = await callBridge(bridge, "edit_match", req, extCtx);
|
|
32882
|
-
return buildMutationResult(params.filePath, response);
|
|
32883
|
-
},
|
|
32884
|
-
renderCall(args, theme, context) {
|
|
32885
|
-
return renderMutationCall("edit", args?.filePath, theme, context);
|
|
32886
|
-
},
|
|
32887
|
-
renderResult(result, _options, theme, context) {
|
|
32888
|
-
return renderMutationResult(result, theme, context);
|
|
32889
32817
|
}
|
|
32890
|
-
|
|
32891
|
-
|
|
32892
|
-
|
|
32893
|
-
|
|
32894
|
-
|
|
32895
|
-
label: "grep",
|
|
32896
|
-
description: "Search for a regex pattern across files. Uses AFT's trigram index inside the project root for fast repeated queries, and falls back to ripgrep for paths outside the project root.",
|
|
32897
|
-
promptSnippet: "Fast regex search across files (trigram-indexed inside the project root)",
|
|
32898
|
-
promptGuidelines: ["Prefer grep over bash-invoked find/rg for in-project searches."],
|
|
32899
|
-
parameters: GrepParams,
|
|
32900
|
-
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32901
|
-
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32902
|
-
const req = { pattern: params.pattern };
|
|
32903
|
-
if (params.path) {
|
|
32904
|
-
await assertExternalDirectoryPermission(extCtx, params.path, "search", {
|
|
32905
|
-
restrictToProjectRoot: surface.restrictToProjectRoot
|
|
32906
|
-
});
|
|
32907
|
-
req.path = await resolvePathArg(extCtx.cwd, params.path);
|
|
32818
|
+
if (terminal) {
|
|
32819
|
+
if (waitForExit) {
|
|
32820
|
+
sawTerminal = true;
|
|
32821
|
+
consumeBgCompletion(sessionId, taskId);
|
|
32822
|
+
await markBgCompletionDelivered({ ctx, directory: extCtx.cwd, sessionID: sessionId }, taskId);
|
|
32908
32823
|
}
|
|
32909
|
-
|
|
32910
|
-
|
|
32911
|
-
|
|
32912
|
-
|
|
32913
|
-
const contextLines = coerceOptionalInt(params.contextLines, "contextLines", 1, Number.MAX_SAFE_INTEGER);
|
|
32914
|
-
if (contextLines !== undefined)
|
|
32915
|
-
req.context_lines = contextLines;
|
|
32916
|
-
const response = await callBridge(bridge, "grep", req, extCtx);
|
|
32917
|
-
const text = response.text ?? "";
|
|
32918
|
-
return textResult(text);
|
|
32824
|
+
return withWaited(data, { reason: "exited", elapsed_ms: Date.now() - startedAt });
|
|
32825
|
+
}
|
|
32826
|
+
if (Date.now() >= deadline) {
|
|
32827
|
+
return withWaited(data, { reason: "timeout", elapsed_ms: Date.now() - startedAt });
|
|
32919
32828
|
}
|
|
32829
|
+
await sleep(Math.min(BASH_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
|
|
32830
|
+
}
|
|
32831
|
+
} finally {
|
|
32832
|
+
if (waitForExit && !sawTerminal)
|
|
32833
|
+
unmarkTaskWaiting(sessionId, taskId);
|
|
32834
|
+
if (waitFor) {
|
|
32835
|
+
await disposePtyTerminal(watchPtyCacheKey(extCtx, taskId));
|
|
32836
|
+
}
|
|
32837
|
+
}
|
|
32838
|
+
}
|
|
32839
|
+
async function readNewTaskOutput(extCtx, taskId, data, cursor) {
|
|
32840
|
+
const outputPath = data.output_path;
|
|
32841
|
+
if (data.mode === "pty") {
|
|
32842
|
+
if (!outputPath)
|
|
32843
|
+
return;
|
|
32844
|
+
const { rows, cols } = ptyDimensions(data);
|
|
32845
|
+
const state = await getOrCreatePtyTerminal(watchPtyCacheKey(extCtx, taskId), outputPath, rows, cols);
|
|
32846
|
+
const baseOffset = state.offset;
|
|
32847
|
+
const bytes = await readPtyBytes(state);
|
|
32848
|
+
if (bytes.length === 0)
|
|
32849
|
+
return;
|
|
32850
|
+
return {
|
|
32851
|
+
text: bytes.toString("utf8"),
|
|
32852
|
+
baseOffset,
|
|
32853
|
+
nextCursor: { output: state.offset, stderr: 0, combined: state.offset }
|
|
32854
|
+
};
|
|
32855
|
+
}
|
|
32856
|
+
const stderrPath = data.stderr_path;
|
|
32857
|
+
if (!outputPath && !stderrPath)
|
|
32858
|
+
return;
|
|
32859
|
+
const stdoutBytes = outputPath ? await readFileBytesFrom(outputPath, cursor.output) : Buffer.alloc(0);
|
|
32860
|
+
const stderrBytes = stderrPath ? await readFileBytesFrom(stderrPath, cursor.stderr) : Buffer.alloc(0);
|
|
32861
|
+
const bytesRead = stdoutBytes.length + stderrBytes.length;
|
|
32862
|
+
if (bytesRead === 0)
|
|
32863
|
+
return;
|
|
32864
|
+
return {
|
|
32865
|
+
text: Buffer.concat([stdoutBytes, stderrBytes]).toString("utf8"),
|
|
32866
|
+
baseOffset: cursor.combined,
|
|
32867
|
+
nextCursor: {
|
|
32868
|
+
output: cursor.output + stdoutBytes.length,
|
|
32869
|
+
stderr: cursor.stderr + stderrBytes.length,
|
|
32870
|
+
combined: cursor.combined + bytesRead
|
|
32871
|
+
}
|
|
32872
|
+
};
|
|
32873
|
+
}
|
|
32874
|
+
async function readFileBytesFrom(outputPath, cursor) {
|
|
32875
|
+
const handle = await fs3.open(outputPath, "r");
|
|
32876
|
+
try {
|
|
32877
|
+
const chunks = [];
|
|
32878
|
+
let offset = cursor;
|
|
32879
|
+
while (true) {
|
|
32880
|
+
const buffer2 = Buffer.allocUnsafe(64 * 1024);
|
|
32881
|
+
const { bytesRead } = await handle.read(buffer2, 0, buffer2.length, offset);
|
|
32882
|
+
if (bytesRead === 0)
|
|
32883
|
+
break;
|
|
32884
|
+
chunks.push(Buffer.from(buffer2.subarray(0, bytesRead)));
|
|
32885
|
+
offset += bytesRead;
|
|
32886
|
+
}
|
|
32887
|
+
return Buffer.concat(chunks);
|
|
32888
|
+
} finally {
|
|
32889
|
+
await handle.close().catch(() => {
|
|
32890
|
+
return;
|
|
32920
32891
|
});
|
|
32921
32892
|
}
|
|
32922
32893
|
}
|
|
32923
|
-
function
|
|
32924
|
-
|
|
32925
|
-
|
|
32926
|
-
|
|
32927
|
-
|
|
32928
|
-
|
|
32929
|
-
|
|
32930
|
-
|
|
32931
|
-
|
|
32932
|
-
|
|
32933
|
-
|
|
32934
|
-
|
|
32935
|
-
|
|
32936
|
-
|
|
32937
|
-
const piDiff = formatDiffForPi(diffObj.before, diffObj.after);
|
|
32938
|
-
diffText = piDiff.diff;
|
|
32939
|
-
firstChangedLine = piDiff.firstChangedLine;
|
|
32894
|
+
function parseWaitPattern(value) {
|
|
32895
|
+
if (typeof value === "string")
|
|
32896
|
+
return { kind: "substring", value };
|
|
32897
|
+
if (isRegexWaitObject(value))
|
|
32898
|
+
return { kind: "regex", value: new RegExp(value.regex), source: value.regex };
|
|
32899
|
+
return;
|
|
32900
|
+
}
|
|
32901
|
+
function isRegexWaitObject(value) {
|
|
32902
|
+
return typeof value === "object" && value !== null && "regex" in value && typeof value.regex === "string";
|
|
32903
|
+
}
|
|
32904
|
+
function findWaitMatch(text, pattern) {
|
|
32905
|
+
if (pattern.kind === "substring") {
|
|
32906
|
+
const index = text.indexOf(pattern.value);
|
|
32907
|
+
return index >= 0 ? { text: pattern.value, index } : undefined;
|
|
32940
32908
|
}
|
|
32941
|
-
|
|
32942
|
-
|
|
32943
|
-
|
|
32944
|
-
|
|
32945
|
-
|
|
32946
|
-
|
|
32909
|
+
pattern.value.lastIndex = 0;
|
|
32910
|
+
const match = pattern.value.exec(text);
|
|
32911
|
+
return match ? { text: match[0], index: match.index } : undefined;
|
|
32912
|
+
}
|
|
32913
|
+
function withWaited(data, waited) {
|
|
32914
|
+
return { ...data, waited };
|
|
32915
|
+
}
|
|
32916
|
+
function formatWaitSummary(waited, details) {
|
|
32917
|
+
if (waited.reason === "matched") {
|
|
32918
|
+
return `Waited ${waited.elapsed_ms}ms; matched ${JSON.stringify(waited.match ?? "")} at offset ${waited.match_offset ?? 0}.`;
|
|
32947
32919
|
}
|
|
32948
|
-
|
|
32949
|
-
|
|
32950
|
-
text += `
|
|
32951
|
-
|
|
32952
|
-
${skipNote}`;
|
|
32953
|
-
const globSkipNote = formatGlobSkipReasonsNote(globFormatSkipReasons);
|
|
32954
|
-
if (globSkipNote)
|
|
32955
|
-
text += `
|
|
32956
|
-
|
|
32957
|
-
${globSkipNote}`;
|
|
32958
|
-
if (diagnostics && diagnostics.length > 0) {
|
|
32959
|
-
text += `
|
|
32960
|
-
|
|
32961
|
-
LSP diagnostics:
|
|
32962
|
-
${formatDiagnosticsText(diagnostics)}`;
|
|
32920
|
+
if (waited.reason === "timeout") {
|
|
32921
|
+
return `Waited ${waited.elapsed_ms}ms; timeout reached without match.`;
|
|
32963
32922
|
}
|
|
32964
|
-
|
|
32965
|
-
|
|
32966
|
-
details: {
|
|
32967
|
-
diff: diffText,
|
|
32968
|
-
firstChangedLine,
|
|
32969
|
-
additions,
|
|
32970
|
-
deletions,
|
|
32971
|
-
replacements,
|
|
32972
|
-
diagnostics,
|
|
32973
|
-
truncated: truncated || undefined,
|
|
32974
|
-
formatted,
|
|
32975
|
-
formatSkippedReason,
|
|
32976
|
-
noOp: noOp || undefined
|
|
32977
|
-
}
|
|
32978
|
-
};
|
|
32923
|
+
const exit = typeof details.exit_code === "number" ? `, exit ${details.exit_code}` : "";
|
|
32924
|
+
return `Waited ${waited.elapsed_ms}ms; task exited (${details.status}${exit}).`;
|
|
32979
32925
|
}
|
|
32980
|
-
function
|
|
32981
|
-
|
|
32982
|
-
|
|
32983
|
-
|
|
32984
|
-
if (
|
|
32985
|
-
|
|
32986
|
-
|
|
32926
|
+
async function formatBashStatus(extCtx, taskId, details, requestedOutputMode) {
|
|
32927
|
+
const exit = typeof details.exit_code === "number" ? ` (exit ${details.exit_code})` : "";
|
|
32928
|
+
const dur = typeof details.duration_ms === "number" ? ` ${Math.round(details.duration_ms / 1000)}s` : "";
|
|
32929
|
+
let text = `Task ${taskId}: ${details.status}${exit}${dur}`;
|
|
32930
|
+
if (details.waited)
|
|
32931
|
+
text += `
|
|
32932
|
+
${formatWaitSummary(details.waited, details)}`;
|
|
32933
|
+
if (details.mode === "pty") {
|
|
32934
|
+
text += await formatPtyStatus(extCtx, taskId, details, requestedOutputMode);
|
|
32935
|
+
} else {
|
|
32936
|
+
if (isTerminalStatus(details.status) && details.output_preview) {
|
|
32937
|
+
text += `
|
|
32938
|
+
${details.output_preview.slice(0, 2000)}`;
|
|
32939
|
+
}
|
|
32940
|
+
if (!isTerminalStatus(details.status)) {
|
|
32941
|
+
text += `
|
|
32942
|
+
A completion reminder will be delivered automatically; don't poll.`;
|
|
32943
|
+
}
|
|
32944
|
+
}
|
|
32945
|
+
return text;
|
|
32987
32946
|
}
|
|
32988
|
-
function
|
|
32989
|
-
|
|
32990
|
-
|
|
32991
|
-
|
|
32992
|
-
|
|
32993
|
-
|
|
32994
|
-
|
|
32995
|
-
|
|
32996
|
-
|
|
32997
|
-
|
|
32998
|
-
|
|
32999
|
-
|
|
32947
|
+
async function formatPtyStatus(extCtx, taskId, details, requestedOutputMode) {
|
|
32948
|
+
if (!details.output_path)
|
|
32949
|
+
return `
|
|
32950
|
+
[PTY output path unavailable]`;
|
|
32951
|
+
const key = ptyCacheKey(extCtx, taskId);
|
|
32952
|
+
const { rows, cols } = ptyDimensions(details);
|
|
32953
|
+
const state = await getOrCreatePtyTerminal(key, details.output_path, rows, cols);
|
|
32954
|
+
const raw = await readPtyBytes(state);
|
|
32955
|
+
const outputMode = requestedOutputMode ?? "screen";
|
|
32956
|
+
let suffix = "";
|
|
32957
|
+
if (outputMode === "raw") {
|
|
32958
|
+
suffix = raw.length > 0 ? `
|
|
32959
|
+
${raw.toString("utf8")}` : "";
|
|
32960
|
+
} else if (outputMode === "both") {
|
|
32961
|
+
suffix = `
|
|
32962
|
+
${JSON.stringify({ screen: renderScreen(state, rows, cols), raw: raw.toString("utf8") }, null, 2)}`;
|
|
32963
|
+
} else {
|
|
32964
|
+
const screen = renderScreen(state, rows, cols);
|
|
32965
|
+
suffix = screen ? `
|
|
32966
|
+
${screen}` : "";
|
|
32967
|
+
}
|
|
32968
|
+
if (!isTerminalStatus(details.status)) {
|
|
32969
|
+
suffix += `
|
|
32970
|
+
PTY task is still running. Use bash_status({ task_id: "${taskId}", output_mode: "screen" }) to inspect, bash_write({ task_id: "${taskId}", input: "..." }) to send keystrokes.`;
|
|
32971
|
+
} else {
|
|
32972
|
+
await disposePtyTerminal(key);
|
|
33000
32973
|
}
|
|
32974
|
+
return suffix;
|
|
33001
32975
|
}
|
|
33002
|
-
function
|
|
33003
|
-
|
|
33004
|
-
|
|
33005
|
-
|
|
33006
|
-
|
|
33007
|
-
|
|
33008
|
-
|
|
33009
|
-
|
|
33010
|
-
|
|
33011
|
-
|
|
33012
|
-
|
|
33013
|
-
|
|
32976
|
+
function ptyDimensions(data) {
|
|
32977
|
+
const rows = typeof data.pty_rows === "number" ? data.pty_rows : 24;
|
|
32978
|
+
const cols = typeof data.pty_cols === "number" ? data.pty_cols : 80;
|
|
32979
|
+
return { rows, cols };
|
|
32980
|
+
}
|
|
32981
|
+
function ptyCacheKey(extCtx, taskId) {
|
|
32982
|
+
return `${extCtx.cwd}::${resolveSessionId(extCtx) ?? "__default__"}::${taskId}`;
|
|
32983
|
+
}
|
|
32984
|
+
function watchPtyCacheKey(extCtx, taskId) {
|
|
32985
|
+
return `${ptyCacheKey(extCtx, taskId)}::watch`;
|
|
32986
|
+
}
|
|
32987
|
+
function isTerminalStatus(status) {
|
|
32988
|
+
return status === "completed" || status === "failed" || status === "killed" || status === "timed_out";
|
|
32989
|
+
}
|
|
32990
|
+
function renderBashCall(command, description, theme, context) {
|
|
32991
|
+
const text = reuseText3(context.lastComponent);
|
|
32992
|
+
const display = description ?? (command ? shortenCommand(command) : "...");
|
|
32993
|
+
text.setText(`${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg("accent", display)}`);
|
|
32994
|
+
return text;
|
|
32995
|
+
}
|
|
32996
|
+
function renderBashResult(result, theme, context) {
|
|
32997
|
+
if (context.isError) {
|
|
32998
|
+
const errorText = result.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join(`
|
|
32999
|
+
`).trim();
|
|
33000
|
+
const text = reuseText3(context.lastComponent);
|
|
33001
|
+
text.setText(`
|
|
33002
|
+
${theme.fg("error", errorText || "bash failed")}`);
|
|
33003
|
+
return text;
|
|
33004
|
+
}
|
|
33005
|
+
const details = result.details;
|
|
33006
|
+
const exitCode = details?.exit_code;
|
|
33007
|
+
const bgCompletions = details?.bg_completions ?? [];
|
|
33008
|
+
const container = reuseContainer3(context.lastComponent);
|
|
33009
|
+
container.clear();
|
|
33010
|
+
container.addChild(new Spacer3(1));
|
|
33011
|
+
const rawOutput = result.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join(`
|
|
33012
|
+
`).trim();
|
|
33013
|
+
if (rawOutput) {
|
|
33014
|
+
const lines = rawOutput.split(`
|
|
33014
33015
|
`);
|
|
33015
|
-
|
|
33016
|
-
|
|
33016
|
+
const preview = lines.length > 25 ? `... (${lines.length - 25} lines omitted)
|
|
33017
|
+
${lines.slice(-25).join(`
|
|
33018
|
+
`)}` : rawOutput;
|
|
33019
|
+
container.addChild(new Text3(preview, 1, 0));
|
|
33020
|
+
container.addChild(new Spacer3(1));
|
|
33021
|
+
}
|
|
33022
|
+
if (exitCode !== undefined) {
|
|
33023
|
+
const exitColor = exitCode === 0 ? "success" : "error";
|
|
33024
|
+
const exitText = theme.fg(exitColor, `exit ${exitCode}`);
|
|
33025
|
+
container.addChild(new Text3(exitText, 1, 0));
|
|
33026
|
+
}
|
|
33027
|
+
if (bgCompletions.length > 0) {
|
|
33028
|
+
container.addChild(new Spacer3(1));
|
|
33029
|
+
for (const bg of bgCompletions) {
|
|
33030
|
+
const cmdPreview = bg.command ? bg.command.slice(0, 60) : "unknown command";
|
|
33031
|
+
const suffix = (bg.command?.length ?? 0) > 60 ? "..." : "";
|
|
33032
|
+
const exitInfo = bg.exit_code !== undefined ? `exit ${bg.exit_code}` : bg.status;
|
|
33033
|
+
const statusColor = bg.status === "completed" && bg.exit_code === 0 ? "success" : "warning";
|
|
33034
|
+
const line = theme.fg(statusColor, `Background task ${bg.task_id} completed (${exitInfo}): ${cmdPreview}${suffix}`);
|
|
33035
|
+
container.addChild(new Text3(line, 1, 0));
|
|
33036
|
+
}
|
|
33037
|
+
}
|
|
33038
|
+
if (details?.duration_ms !== undefined) {
|
|
33039
|
+
container.addChild(new Spacer3(1));
|
|
33040
|
+
const durationText = theme.fg("muted", `${details.duration_ms}ms`);
|
|
33041
|
+
container.addChild(new Text3(durationText, 1, 0));
|
|
33017
33042
|
}
|
|
33043
|
+
if (details?.truncated) {
|
|
33044
|
+
container.addChild(new Spacer3(1));
|
|
33045
|
+
const truncText = theme.fg("warning", "(output truncated)");
|
|
33046
|
+
container.addChild(new Text3(truncText, 1, 0));
|
|
33047
|
+
}
|
|
33048
|
+
return container;
|
|
33018
33049
|
}
|
|
33019
|
-
function
|
|
33020
|
-
|
|
33050
|
+
function shortenCommand(command) {
|
|
33051
|
+
if (command.length <= 60)
|
|
33052
|
+
return command;
|
|
33053
|
+
return `${command.slice(0, 57)}...`;
|
|
33054
|
+
}
|
|
33055
|
+
|
|
33056
|
+
// src/tools/conflicts.ts
|
|
33057
|
+
import { Type as Type5 } from "typebox";
|
|
33058
|
+
var ConflictsParams = Type5.Object({
|
|
33059
|
+
path: Type5.Optional(Type5.String({
|
|
33060
|
+
description: "Optional path inside the git repository or worktree to inspect (absolute or relative to project root). Conflicts are discovered from that repository's top level. Defaults to the session project root."
|
|
33061
|
+
}))
|
|
33062
|
+
});
|
|
33063
|
+
function renderConflictCall(theme, context) {
|
|
33064
|
+
return renderToolCall("conflicts", undefined, theme, context);
|
|
33065
|
+
}
|
|
33066
|
+
function buildConflictSections(text) {
|
|
33067
|
+
const trimmed = text.trim();
|
|
33068
|
+
if (!trimmed)
|
|
33069
|
+
return ["No merge conflicts found."];
|
|
33070
|
+
const [header, ...rest] = trimmed.split(/\n\n+/);
|
|
33071
|
+
const match = header.match(/^(\d+)\s+files?,\s+(\d+)\s+conflicts?/i);
|
|
33072
|
+
const sections = [
|
|
33073
|
+
match ? `${match[1]} conflicted file${match[1] === "1" ? "" : "s"} · ${match[2]} region${match[2] === "1" ? "" : "s"}` : header
|
|
33074
|
+
];
|
|
33075
|
+
if (rest.length === 0)
|
|
33076
|
+
return sections;
|
|
33077
|
+
sections.push(...rest.map((section) => section.trim()).filter(Boolean));
|
|
33078
|
+
return sections;
|
|
33021
33079
|
}
|
|
33022
|
-
function
|
|
33023
|
-
|
|
33080
|
+
function renderConflictResult(text, theme, context) {
|
|
33081
|
+
const sections = buildConflictSections(text).map((section, index) => index === 0 ? theme.fg("warning", section) : section);
|
|
33082
|
+
return renderSections(sections, context);
|
|
33024
33083
|
}
|
|
33025
|
-
function
|
|
33026
|
-
|
|
33027
|
-
|
|
33028
|
-
|
|
33029
|
-
return text;
|
|
33084
|
+
function renderConflictToolResult(result, theme, context) {
|
|
33085
|
+
if (context.isError)
|
|
33086
|
+
return renderErrorResult(result, "conflicts failed", theme, context);
|
|
33087
|
+
return renderConflictResult(collectTextContent(result), theme, context);
|
|
33030
33088
|
}
|
|
33031
|
-
function
|
|
33032
|
-
|
|
33033
|
-
|
|
33034
|
-
|
|
33035
|
-
|
|
33036
|
-
|
|
33037
|
-
|
|
33038
|
-
|
|
33039
|
-
|
|
33040
|
-
|
|
33041
|
-
|
|
33042
|
-
|
|
33043
|
-
|
|
33044
|
-
|
|
33045
|
-
|
|
33046
|
-
|
|
33047
|
-
|
|
33048
|
-
|
|
33049
|
-
|
|
33050
|
-
|
|
33051
|
-
suffix = ` ${theme.fg("muted", "(no net change)")}`;
|
|
33089
|
+
function registerConflictsTool(pi, ctx) {
|
|
33090
|
+
pi.registerTool({
|
|
33091
|
+
name: "aft_conflicts",
|
|
33092
|
+
label: "conflicts",
|
|
33093
|
+
description: "Show all git merge conflicts across the repository — returns line-numbered conflict regions with context for every conflicted file in a single call.",
|
|
33094
|
+
parameters: ConflictsParams,
|
|
33095
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33096
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
33097
|
+
const reqParams = {};
|
|
33098
|
+
const path2 = params?.path;
|
|
33099
|
+
if (typeof path2 === "string" && path2.trim() !== "")
|
|
33100
|
+
reqParams.path = path2;
|
|
33101
|
+
const response = await callBridge(bridge, "git_conflicts", reqParams, extCtx);
|
|
33102
|
+
return textResult(response.text ?? JSON.stringify(response, null, 2));
|
|
33103
|
+
},
|
|
33104
|
+
renderCall(_args, theme, context) {
|
|
33105
|
+
return renderConflictCall(theme, context);
|
|
33106
|
+
},
|
|
33107
|
+
renderResult(result, _options, theme, context) {
|
|
33108
|
+
return renderConflictToolResult(result, theme, context);
|
|
33052
33109
|
}
|
|
33053
|
-
|
|
33054
|
-
${summary}${suffix}`);
|
|
33055
|
-
return text;
|
|
33056
|
-
}
|
|
33057
|
-
const container = reuseContainer3(context.lastComponent);
|
|
33058
|
-
container.clear();
|
|
33059
|
-
container.addChild(new Spacer3(1));
|
|
33060
|
-
container.addChild(new Text3(renderDiff2(diff), 1, 0));
|
|
33061
|
-
return container;
|
|
33062
|
-
}
|
|
33063
|
-
function shortenPath2(path2) {
|
|
33064
|
-
const home = homedir9();
|
|
33065
|
-
if (path2.startsWith(home))
|
|
33066
|
-
return `~${path2.slice(home.length)}`;
|
|
33067
|
-
return path2;
|
|
33110
|
+
});
|
|
33068
33111
|
}
|
|
33069
|
-
|
|
33070
|
-
|
|
33071
|
-
|
|
33072
|
-
|
|
33073
|
-
|
|
33074
|
-
|
|
33075
|
-
|
|
33076
|
-
|
|
33112
|
+
|
|
33113
|
+
// src/tools/fs.ts
|
|
33114
|
+
import { Type as Type6 } from "typebox";
|
|
33115
|
+
var DeleteParams = Type6.Object({
|
|
33116
|
+
files: Type6.Array(Type6.String(), {
|
|
33117
|
+
description: "Paths to delete (one or more). May include directories when recursive=true.",
|
|
33118
|
+
minItems: 1
|
|
33119
|
+
}),
|
|
33120
|
+
recursive: Type6.Optional(Type6.Boolean({
|
|
33121
|
+
description: "Required to delete a directory and its contents. Defaults to false; passing a directory without this returns an error."
|
|
33122
|
+
}))
|
|
33123
|
+
});
|
|
33124
|
+
var MoveParams = Type6.Object({
|
|
33125
|
+
filePath: Type6.String({
|
|
33126
|
+
description: "Source file path to move (absolute or relative to project root)"
|
|
33127
|
+
}),
|
|
33128
|
+
destination: Type6.String({
|
|
33129
|
+
description: "Destination file path (absolute or relative to project root)"
|
|
33130
|
+
})
|
|
33131
|
+
});
|
|
33132
|
+
function renderFsCall(toolName, args, theme, context) {
|
|
33133
|
+
if (toolName === "aft_delete") {
|
|
33134
|
+
const files = args.files;
|
|
33135
|
+
const summary = files.length === 1 ? accentPath(theme, files[0]) : `${theme.fg("accent", String(files.length))} ${theme.fg("muted", "files")}`;
|
|
33136
|
+
return renderToolCall("delete", summary, theme, context);
|
|
33077
33137
|
}
|
|
33138
|
+
const moveArgs = args;
|
|
33139
|
+
return renderToolCall("move", `${accentPath(theme, moveArgs.filePath)} ${theme.fg("muted", "→")} ${accentPath(theme, moveArgs.destination)}`, theme, context);
|
|
33078
33140
|
}
|
|
33079
|
-
function
|
|
33080
|
-
|
|
33081
|
-
|
|
33082
|
-
|
|
33083
|
-
|
|
33084
|
-
|
|
33085
|
-
|
|
33086
|
-
|
|
33087
|
-
|
|
33141
|
+
function renderFsResult(toolName, args, result, theme, context) {
|
|
33142
|
+
if (context.isError) {
|
|
33143
|
+
return renderErrorResult(result, `${toolName} failed`, theme, context);
|
|
33144
|
+
}
|
|
33145
|
+
if (toolName === "aft_delete") {
|
|
33146
|
+
const files = args.files;
|
|
33147
|
+
const data = result?.details ?? {};
|
|
33148
|
+
const deletedPaths = data.deleted ?? files;
|
|
33149
|
+
const skipped = data.skipped_files ?? [];
|
|
33150
|
+
const lines = [];
|
|
33151
|
+
for (const entry of deletedPaths) {
|
|
33152
|
+
lines.push(`${theme.fg("success", "✓ deleted")} ${theme.fg("accent", shortenPath2(entry))}`);
|
|
33088
33153
|
}
|
|
33089
|
-
|
|
33090
|
-
|
|
33091
|
-
depth--;
|
|
33092
|
-
buf += ch;
|
|
33093
|
-
continue;
|
|
33154
|
+
for (const entry of skipped) {
|
|
33155
|
+
lines.push(`${theme.fg("error", "✗ skipped")} ${theme.fg("accent", shortenPath2(entry.file))} ${theme.fg("muted", `(${entry.reason})`)}`);
|
|
33094
33156
|
}
|
|
33095
|
-
if (
|
|
33096
|
-
|
|
33097
|
-
if (trimmed.length > 0)
|
|
33098
|
-
out.push(trimmed);
|
|
33099
|
-
buf = "";
|
|
33100
|
-
continue;
|
|
33157
|
+
if (lines.length === 0) {
|
|
33158
|
+
lines.push(theme.fg("muted", "(no files deleted)"));
|
|
33101
33159
|
}
|
|
33102
|
-
|
|
33160
|
+
return renderSections([lines.join(`
|
|
33161
|
+
`)], context);
|
|
33103
33162
|
}
|
|
33104
|
-
const
|
|
33105
|
-
|
|
33106
|
-
|
|
33107
|
-
|
|
33163
|
+
const moveArgs = args;
|
|
33164
|
+
return renderSections([
|
|
33165
|
+
`${theme.fg("success", "✓ moved")} ${theme.fg("accent", shortenPath2(moveArgs.filePath))}`,
|
|
33166
|
+
`${theme.fg("muted", "to")} ${theme.fg("accent", shortenPath2(moveArgs.destination))}`
|
|
33167
|
+
], context);
|
|
33108
33168
|
}
|
|
33109
|
-
function
|
|
33110
|
-
if (
|
|
33111
|
-
|
|
33112
|
-
|
|
33113
|
-
|
|
33114
|
-
|
|
33115
|
-
|
|
33116
|
-
|
|
33117
|
-
|
|
33118
|
-
|
|
33169
|
+
function registerFsTools(pi, ctx, surface) {
|
|
33170
|
+
if (surface.delete) {
|
|
33171
|
+
pi.registerTool({
|
|
33172
|
+
name: "aft_delete",
|
|
33173
|
+
label: "delete",
|
|
33174
|
+
description: "Delete one or more files (or directories) with backup. Each file is backed up before deletion — use `aft_safety undo` to recover any of them. " + "For directories, every file inside is individually backed up before removal. Directory deletion requires recursive: true. " + "Returns { success, complete, deleted, skipped_files }: partial success is allowed; files that fail are reported in skipped_files.",
|
|
33175
|
+
parameters: DeleteParams,
|
|
33176
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33177
|
+
const files = await Promise.all(params.files.map((file2) => resolvePathArg(extCtx.cwd, file2)));
|
|
33178
|
+
const checked = new Set;
|
|
33179
|
+
for (const file2 of files) {
|
|
33180
|
+
if (checked.has(file2))
|
|
33181
|
+
continue;
|
|
33182
|
+
checked.add(file2);
|
|
33183
|
+
await assertExternalDirectoryPermission(extCtx, file2, "modify", {
|
|
33184
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
33185
|
+
});
|
|
33186
|
+
}
|
|
33187
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
33188
|
+
const response = await callBridge(bridge, "delete_file", {
|
|
33189
|
+
files,
|
|
33190
|
+
recursive: params.recursive === true
|
|
33191
|
+
}, extCtx);
|
|
33192
|
+
const deletedEntries = response.deleted ?? [];
|
|
33193
|
+
const skipped = response.skipped_files ?? [];
|
|
33194
|
+
const deleted = deletedEntries.map((entry) => entry.file);
|
|
33195
|
+
if (deleted.length === 0 && skipped.length > 0) {
|
|
33196
|
+
throw new Error(`delete failed for all ${skipped.length} file(s):
|
|
33197
|
+
` + skipped.map((entry) => ` ${entry.file}: ${entry.reason}`).join(`
|
|
33198
|
+
`));
|
|
33199
|
+
}
|
|
33200
|
+
const summary = deleted.length === 1 && skipped.length === 0 ? `Deleted ${deleted[0]}` : `Deleted ${deleted.length}/${params.files.length} file(s)`;
|
|
33201
|
+
return textResult(summary, {
|
|
33202
|
+
success: true,
|
|
33203
|
+
complete: skipped.length === 0,
|
|
33204
|
+
deleted,
|
|
33205
|
+
skipped_files: skipped
|
|
33206
|
+
});
|
|
33207
|
+
},
|
|
33208
|
+
renderCall(args, theme, context) {
|
|
33209
|
+
return renderFsCall("aft_delete", args, theme, context);
|
|
33210
|
+
},
|
|
33211
|
+
renderResult(result, _options, theme, context) {
|
|
33212
|
+
return renderFsResult("aft_delete", context.args, result, theme, context);
|
|
33213
|
+
}
|
|
33214
|
+
});
|
|
33215
|
+
}
|
|
33216
|
+
if (surface.move) {
|
|
33217
|
+
pi.registerTool({
|
|
33218
|
+
name: "aft_move",
|
|
33219
|
+
label: "move",
|
|
33220
|
+
description: "Move or rename a file with backup. Creates parent directories for the destination automatically. This operates on files at the OS level — to relocate a code symbol between files, use `aft_refactor` with op='move' instead.",
|
|
33221
|
+
parameters: MoveParams,
|
|
33222
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33223
|
+
const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
|
|
33224
|
+
const destination = await resolvePathArg(extCtx.cwd, params.destination);
|
|
33225
|
+
const checked = new Set([filePath, destination]);
|
|
33226
|
+
for (const file2 of checked) {
|
|
33227
|
+
await assertExternalDirectoryPermission(extCtx, file2, "modify", {
|
|
33228
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
33229
|
+
});
|
|
33230
|
+
}
|
|
33231
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
33232
|
+
const response = await callBridge(bridge, "move_file", {
|
|
33233
|
+
file: filePath,
|
|
33234
|
+
destination
|
|
33235
|
+
}, extCtx);
|
|
33236
|
+
return textResult(`Moved ${params.filePath} → ${params.destination}`, response);
|
|
33237
|
+
},
|
|
33238
|
+
renderCall(args, theme, context) {
|
|
33239
|
+
return renderFsCall("aft_move", args, theme, context);
|
|
33240
|
+
},
|
|
33241
|
+
renderResult(result, _options, theme, context) {
|
|
33242
|
+
return renderFsResult("aft_move", context.args, result, theme, context);
|
|
33243
|
+
}
|
|
33244
|
+
});
|
|
33119
33245
|
}
|
|
33120
|
-
return `
|
|
33121
|
-
(Showing lines ${startLine}-${endLine} of ${totalLines}. Use offset/limit to read other sections.)`;
|
|
33122
33246
|
}
|
|
33123
33247
|
|
|
33124
33248
|
// src/tools/imports.ts
|
|
@@ -33128,8 +33252,20 @@ var ImportParams = Type7.Object({
|
|
|
33128
33252
|
op: StringEnum2(["add", "remove", "organize"], { description: "Import operation" }),
|
|
33129
33253
|
filePath: Type7.String({ description: "Path to the file (absolute or relative to project root)" }),
|
|
33130
33254
|
module: Type7.Optional(Type7.String({ description: "Module path (required for add/remove), e.g. 'react', './utils'" })),
|
|
33131
|
-
names: Type7.Optional(Type7.Array(Type7.String(), {
|
|
33132
|
-
|
|
33255
|
+
names: Type7.Optional(Type7.Array(Type7.String(), {
|
|
33256
|
+
description: "Named imports to add, using native named-import text with per-name `as` aliasing where supported, e.g. ['useState'], Solidity ['ERC20', 'IERC20 as IToken']"
|
|
33257
|
+
})),
|
|
33258
|
+
defaultImport: Type7.Optional(Type7.String({ description: "Default import name, ES only (e.g. 'React')" })),
|
|
33259
|
+
namespace: Type7.Optional(Type7.String({
|
|
33260
|
+
description: "Namespace binding: `import * as ns from 'mod'` (ES), `* as N from \"./X.sol\"` (Solidity)"
|
|
33261
|
+
})),
|
|
33262
|
+
alias: Type7.Optional(Type7.String({ description: 'Whole-module alias. Solidity: `import "./X.sol" as X`' })),
|
|
33263
|
+
modifiers: Type7.Optional(Type7.Array(Type7.String(), {
|
|
33264
|
+
description: "Statement-level modifiers, language-validated: Java/C# 'static', C# 'global'/'unsafe', Java/Kotlin/Scala 'wildcard', Swift '@testable'"
|
|
33265
|
+
})),
|
|
33266
|
+
importKind: Type7.Optional(Type7.String({
|
|
33267
|
+
description: "Symbol-kind import: PHP 'function'/'const', Swift 'struct'/'class'/'enum', Scala 'given'"
|
|
33268
|
+
})),
|
|
33133
33269
|
removeName: Type7.Optional(Type7.String({ description: "Named import to remove; omit to remove entire import" })),
|
|
33134
33270
|
typeOnly: Type7.Optional(Type7.Boolean({ description: "Type-only import (TS only)" })),
|
|
33135
33271
|
validate: Type7.Optional(StringEnum2(["syntax", "full"], {
|
|
@@ -33150,16 +33286,19 @@ function buildImportSections(args, payload, theme) {
|
|
|
33150
33286
|
];
|
|
33151
33287
|
}
|
|
33152
33288
|
if (args.op === "add") {
|
|
33153
|
-
const
|
|
33289
|
+
const moduleName2 = asString(response.module) ?? args.module ?? "(module)";
|
|
33154
33290
|
const status = response.already_present === true ? theme.fg("warning", "already present") : theme.fg("success", "added");
|
|
33155
33291
|
return [
|
|
33156
|
-
`${status} ${theme.fg("accent",
|
|
33292
|
+
`${status} ${theme.fg("accent", moduleName2)}`,
|
|
33157
33293
|
`${theme.fg("muted", "file")} ${theme.fg("accent", asString(response.file) ?? args.filePath)}`,
|
|
33158
33294
|
`${theme.fg("muted", "group")} ${asString(response.group) ?? "—"}`
|
|
33159
33295
|
];
|
|
33160
33296
|
}
|
|
33297
|
+
const moduleName = asString(response.module) ?? args.module ?? "(module)";
|
|
33298
|
+
const didRemove = response.removed !== false;
|
|
33299
|
+
const removeStatus = didRemove ? `${theme.fg("success", "removed")} ${theme.fg("accent", moduleName)}` : `${theme.fg("warning", "not present")} ${theme.fg("accent", moduleName)}`;
|
|
33161
33300
|
return [
|
|
33162
|
-
|
|
33301
|
+
removeStatus,
|
|
33163
33302
|
`${theme.fg("muted", "file")} ${theme.fg("accent", asString(response.file) ?? args.filePath)}`,
|
|
33164
33303
|
args.removeName ? `${theme.fg("muted", "name")} ${args.removeName}` : `${theme.fg("muted", "scope")} entire import`
|
|
33165
33304
|
];
|
|
@@ -33182,25 +33321,37 @@ function registerImportTools(pi, ctx) {
|
|
|
33182
33321
|
pi.registerTool({
|
|
33183
33322
|
name: "aft_import",
|
|
33184
33323
|
label: "import",
|
|
33185
|
-
description: "Language-aware import management. Supports TS, JS, TSX, Python, Rust, Go. Ops: `add`, `remove`, `organize`. Use aft_safety checkpoint/undo before broad cleanup.",
|
|
33324
|
+
description: "Language-aware import management. Supports TS, JS, TSX, Python, Rust, Go, Solidity, Java, C#, PHP, Kotlin, Scala, Swift, Ruby, Lua, C, C++, Perl, Vue. Ops: `add`, `remove`, `organize`. Use aft_safety checkpoint/undo before broad cleanup.",
|
|
33186
33325
|
parameters: ImportParams,
|
|
33187
33326
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33188
|
-
if ((params.op === "add" || params.op === "remove") &&
|
|
33327
|
+
if ((params.op === "add" || params.op === "remove") && isEmptyParam(params.module)) {
|
|
33189
33328
|
throw new Error(`op='${params.op}' requires 'module'`);
|
|
33190
33329
|
}
|
|
33330
|
+
const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
|
|
33331
|
+
await assertExternalDirectoryPermission(extCtx, filePath, "modify", {
|
|
33332
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
33333
|
+
});
|
|
33191
33334
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
33192
33335
|
const commandMap = {
|
|
33193
33336
|
add: "add_import",
|
|
33194
33337
|
remove: "remove_import",
|
|
33195
33338
|
organize: "organize_imports"
|
|
33196
33339
|
};
|
|
33197
|
-
const req = { file:
|
|
33340
|
+
const req = { file: filePath };
|
|
33198
33341
|
if (params.module !== undefined)
|
|
33199
33342
|
req.module = params.module;
|
|
33200
33343
|
if (params.names !== undefined)
|
|
33201
33344
|
req.names = params.names;
|
|
33202
33345
|
if (params.defaultImport !== undefined)
|
|
33203
33346
|
req.default_import = params.defaultImport;
|
|
33347
|
+
if (params.namespace !== undefined)
|
|
33348
|
+
req.namespace = params.namespace;
|
|
33349
|
+
if (params.alias !== undefined)
|
|
33350
|
+
req.alias = params.alias;
|
|
33351
|
+
if (params.modifiers !== undefined)
|
|
33352
|
+
req.modifiers = params.modifiers;
|
|
33353
|
+
if (params.importKind !== undefined)
|
|
33354
|
+
req.import_kind = params.importKind;
|
|
33204
33355
|
if (params.removeName !== undefined)
|
|
33205
33356
|
req.name = params.removeName;
|
|
33206
33357
|
if (params.typeOnly !== undefined)
|
|
@@ -33243,6 +33394,22 @@ var lastTier2TriggerAtByBridge = new WeakMap;
|
|
|
33243
33394
|
function normalizeStringOrArray(value) {
|
|
33244
33395
|
return isEmptyParam(value) ? undefined : value;
|
|
33245
33396
|
}
|
|
33397
|
+
async function resolveAndGateScope(extCtx, ctx, scope) {
|
|
33398
|
+
if (scope === undefined)
|
|
33399
|
+
return;
|
|
33400
|
+
const values = Array.isArray(scope) ? scope : [scope];
|
|
33401
|
+
const resolved = await Promise.all(values.filter((value) => typeof value === "string" && value.length > 0).map((value) => resolvePathArg(extCtx.cwd, value)));
|
|
33402
|
+
const checked = new Set;
|
|
33403
|
+
for (const target of resolved) {
|
|
33404
|
+
if (checked.has(target))
|
|
33405
|
+
continue;
|
|
33406
|
+
checked.add(target);
|
|
33407
|
+
await assertExternalDirectoryPermission(extCtx, target, "read", {
|
|
33408
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
33409
|
+
});
|
|
33410
|
+
}
|
|
33411
|
+
return Array.isArray(scope) ? resolved : resolved[0];
|
|
33412
|
+
}
|
|
33246
33413
|
function validateOptionalTopK(value) {
|
|
33247
33414
|
if (value === undefined || value === null || value === "")
|
|
33248
33415
|
return;
|
|
@@ -33426,7 +33593,7 @@ function registerInspectTool(pi, ctx) {
|
|
|
33426
33593
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33427
33594
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
33428
33595
|
const sections = normalizeStringOrArray(params.sections);
|
|
33429
|
-
const scope = normalizeStringOrArray(params.scope);
|
|
33596
|
+
const scope = await resolveAndGateScope(extCtx, ctx, normalizeStringOrArray(params.scope));
|
|
33430
33597
|
const topK = validateOptionalTopK(params.topK);
|
|
33431
33598
|
const response = await callBridge(bridge, "inspect", { sections, scope, topK }, extCtx);
|
|
33432
33599
|
runPendingTier2Categories(bridge, tier2RefreshCategories(response), extCtx);
|
|
@@ -33468,7 +33635,7 @@ function treeLine(depth, text) {
|
|
|
33468
33635
|
}
|
|
33469
33636
|
function renderCallTreeNode(node, depth, lines) {
|
|
33470
33637
|
const name = asString(node.name) ?? "(unknown)";
|
|
33471
|
-
const file2 =
|
|
33638
|
+
const file2 = shortenPath2(asString(node.file) ?? "(unknown file)");
|
|
33472
33639
|
const line = asNumber(node.line);
|
|
33473
33640
|
lines.push(treeLine(depth, `${name} ${line !== undefined ? `[${file2}:${line}]` : `[${file2}]`}`));
|
|
33474
33641
|
asRecords(node.children).forEach((child) => {
|
|
@@ -33487,7 +33654,7 @@ function renderTracePath(path2, index, lines) {
|
|
|
33487
33654
|
lines.push(`Path ${index + 1}`);
|
|
33488
33655
|
asRecords(path2.hops).forEach((hop, hopIndex) => {
|
|
33489
33656
|
const symbol2 = asString(hop.symbol) ?? "(unknown)";
|
|
33490
|
-
const file2 =
|
|
33657
|
+
const file2 = shortenPath2(asString(hop.file) ?? "(unknown file)");
|
|
33491
33658
|
const line = asNumber(hop.line);
|
|
33492
33659
|
const entry = hop.is_entry_point === true ? " [entry]" : "";
|
|
33493
33660
|
lines.push(treeLine(hopIndex + 1, `${symbol2}${entry} ${line !== undefined ? `[${file2}:${line}]` : `[${file2}]`}`));
|
|
@@ -33512,7 +33679,7 @@ function buildNavigateSections(args, payload, theme) {
|
|
|
33512
33679
|
`${theme.fg("success", `${asNumber(response.total_callers) ?? 0} caller${(asNumber(response.total_callers) ?? 0) === 1 ? "" : "s"}`)} ${theme.fg("muted", `${groups.length} file group${groups.length === 1 ? "" : "s"}`)} ${warning}`.trim()
|
|
33513
33680
|
];
|
|
33514
33681
|
groups.forEach((group) => {
|
|
33515
|
-
const file2 =
|
|
33682
|
+
const file2 = shortenPath2(asString(group.file) ?? "(unknown file)");
|
|
33516
33683
|
const lines = [theme.fg("accent", file2)];
|
|
33517
33684
|
asRecords(group.callers).forEach((caller) => {
|
|
33518
33685
|
lines.push(` ↳ ${asString(caller.symbol) ?? "(unknown)"} ${theme.fg("muted", `line ${asNumber(caller.line) ?? "?"}`)}`);
|
|
@@ -33533,7 +33700,7 @@ function buildNavigateSections(args, payload, theme) {
|
|
|
33533
33700
|
const lines = [theme.fg("success", `${path2.length} hop${path2.length === 1 ? "" : "s"}`)];
|
|
33534
33701
|
path2.forEach((hop, index) => {
|
|
33535
33702
|
const symbol2 = asString(hop.symbol) ?? "(unknown)";
|
|
33536
|
-
const file2 =
|
|
33703
|
+
const file2 = shortenPath2(asString(hop.file) ?? "(unknown file)");
|
|
33537
33704
|
const line = asNumber(hop.line);
|
|
33538
33705
|
lines.push(treeLine(index + 1, `${symbol2} ${line !== undefined ? `[${file2}:${line}]` : `[${file2}]`}`));
|
|
33539
33706
|
});
|
|
@@ -33564,7 +33731,7 @@ function buildNavigateSections(args, payload, theme) {
|
|
|
33564
33731
|
if (callers.length === 0)
|
|
33565
33732
|
sections2.push(theme.fg("muted", "No impacted callers found."));
|
|
33566
33733
|
callers.forEach((caller) => {
|
|
33567
|
-
const file2 =
|
|
33734
|
+
const file2 = shortenPath2(asString(caller.caller_file) ?? "(unknown file)");
|
|
33568
33735
|
const symbol2 = asString(caller.caller_symbol) ?? "(unknown)";
|
|
33569
33736
|
const line = asNumber(caller.line) ?? 0;
|
|
33570
33737
|
const entry = caller.is_entry_point === true ? ` ${theme.fg("warning", "[entry]")}` : "";
|
|
@@ -33587,7 +33754,7 @@ function buildNavigateSections(args, payload, theme) {
|
|
|
33587
33754
|
if (hops.length === 0)
|
|
33588
33755
|
sections.push(theme.fg("muted", "No data-flow hops found."));
|
|
33589
33756
|
hops.forEach((hop, index) => {
|
|
33590
|
-
const file2 =
|
|
33757
|
+
const file2 = shortenPath2(asString(hop.file) ?? "(unknown file)");
|
|
33591
33758
|
const symbol2 = asString(hop.symbol) ?? "(unknown)";
|
|
33592
33759
|
const variable = asString(hop.variable) ?? "(unknown)";
|
|
33593
33760
|
const line = asNumber(hop.line) ?? 0;
|
|
@@ -33617,27 +33784,44 @@ function registerNavigateTool(pi, ctx) {
|
|
|
33617
33784
|
description: "Answer code-relationship questions from a real call graph — instead of grep + read chains. Reach for this whenever the question is about how symbols connect. All ops require both `filePath` and `symbol`. Use `callers` for call sites (before renaming/signature changes), `impact` for blast radius (what breaks if a symbol changes), `call_tree` for what a function calls, `trace_to` for how execution reaches a symbol from entry points, `trace_to_symbol` for the shortest path from one symbol to another (requires `toSymbol`; if ambiguous, the error returns candidate files — retry with `toFile`), `trace_data` to follow a value across assignments/params.",
|
|
33618
33785
|
parameters: navigateParamsSchema(),
|
|
33619
33786
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33620
|
-
if (params.
|
|
33787
|
+
if (isEmptyParam(params.filePath)) {
|
|
33788
|
+
throw new Error(`op='${params.op}' requires a \`filePath\``);
|
|
33789
|
+
}
|
|
33790
|
+
if (isEmptyParam(params.symbol)) {
|
|
33791
|
+
throw new Error(`op='${params.op}' requires a \`symbol\``);
|
|
33792
|
+
}
|
|
33793
|
+
if (params.op === "trace_data" && isEmptyParam(params.expression)) {
|
|
33621
33794
|
throw new Error("op='trace_data' requires an `expression`");
|
|
33622
33795
|
}
|
|
33623
|
-
if (params.op === "trace_to_symbol" &&
|
|
33796
|
+
if (params.op === "trace_to_symbol" && isEmptyParam(params.toSymbol)) {
|
|
33624
33797
|
throw new Error("op='trace_to_symbol' requires a `toSymbol`");
|
|
33625
33798
|
}
|
|
33799
|
+
const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
|
|
33800
|
+
const toFile = !isEmptyParam(params.toFile) ? await resolvePathArg(extCtx.cwd, params.toFile) : undefined;
|
|
33801
|
+
const checked = new Set;
|
|
33802
|
+
for (const target of [filePath, ...toFile !== undefined ? [toFile] : []]) {
|
|
33803
|
+
if (checked.has(target))
|
|
33804
|
+
continue;
|
|
33805
|
+
checked.add(target);
|
|
33806
|
+
await assertExternalDirectoryPermission(extCtx, target, "read", {
|
|
33807
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
33808
|
+
});
|
|
33809
|
+
}
|
|
33626
33810
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
33627
33811
|
const req = {
|
|
33628
33812
|
op: params.op,
|
|
33629
|
-
file:
|
|
33813
|
+
file: filePath,
|
|
33630
33814
|
symbol: params.symbol
|
|
33631
33815
|
};
|
|
33632
33816
|
const depth = coerceOptionalInt(params.depth, "depth", 1, Number.MAX_SAFE_INTEGER);
|
|
33633
33817
|
if (depth !== undefined)
|
|
33634
33818
|
req.depth = depth;
|
|
33635
|
-
if (params.expression
|
|
33819
|
+
if (!isEmptyParam(params.expression))
|
|
33636
33820
|
req.expression = params.expression;
|
|
33637
|
-
if (params.toSymbol
|
|
33821
|
+
if (!isEmptyParam(params.toSymbol))
|
|
33638
33822
|
req.toSymbol = params.toSymbol;
|
|
33639
|
-
if (
|
|
33640
|
-
req.toFile =
|
|
33823
|
+
if (toFile !== undefined)
|
|
33824
|
+
req.toFile = toFile;
|
|
33641
33825
|
const response = await callBridge(bridge, params.op, req, extCtx);
|
|
33642
33826
|
return textResult(JSON.stringify(response, null, 2));
|
|
33643
33827
|
},
|
|
@@ -33652,7 +33836,6 @@ function registerNavigateTool(pi, ctx) {
|
|
|
33652
33836
|
|
|
33653
33837
|
// src/tools/reading.ts
|
|
33654
33838
|
import { stat as stat2 } from "node:fs/promises";
|
|
33655
|
-
import { resolve as resolve4 } from "node:path";
|
|
33656
33839
|
import { Type as Type10 } from "typebox";
|
|
33657
33840
|
var OutlineParams = Type10.Object({
|
|
33658
33841
|
target: Type10.Union([Type10.String(), Type10.Array(Type10.String())], {
|
|
@@ -33677,11 +33860,26 @@ var ZoomParams = Type10.Object({
|
|
|
33677
33860
|
targets: Type10.Optional(Type10.Union([ZoomTarget, Type10.Array(ZoomTarget)], {
|
|
33678
33861
|
description: "Cross-file batch: `{ filePath, symbol }` or an array of them. Mutually exclusive with filePath/url/symbols."
|
|
33679
33862
|
})),
|
|
33680
|
-
contextLines: Type10.Optional(Type10.Number({ description: "Lines of context before/after (default: 3)" }))
|
|
33863
|
+
contextLines: Type10.Optional(Type10.Number({ description: "Lines of context before/after (default: 3)" })),
|
|
33864
|
+
callgraph: Type10.Optional(Type10.Boolean({
|
|
33865
|
+
description: "Include call-graph annotations (calls-out / called-by within the same file). Default false; off keeps zoom output minimal."
|
|
33866
|
+
}))
|
|
33681
33867
|
});
|
|
33682
33868
|
function isUrl(s) {
|
|
33683
33869
|
return s.startsWith("http://") || s.startsWith("https://");
|
|
33684
33870
|
}
|
|
33871
|
+
async function assertReadPathPermissions(extCtx, ctx, paths) {
|
|
33872
|
+
const targets = Array.isArray(paths) ? paths : [paths];
|
|
33873
|
+
const checked = new Set;
|
|
33874
|
+
for (const target of targets) {
|
|
33875
|
+
if (!target || checked.has(target))
|
|
33876
|
+
continue;
|
|
33877
|
+
checked.add(target);
|
|
33878
|
+
await assertExternalDirectoryPermission(extCtx, target, "read", {
|
|
33879
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
33880
|
+
});
|
|
33881
|
+
}
|
|
33882
|
+
}
|
|
33685
33883
|
function zoomTargetLabel(args) {
|
|
33686
33884
|
return args.filePath ?? args.url ?? "(no target)";
|
|
33687
33885
|
}
|
|
@@ -33698,22 +33896,24 @@ function buildOutlineSections(text, theme) {
|
|
|
33698
33896
|
}
|
|
33699
33897
|
function buildZoomSections(args, payload, theme) {
|
|
33700
33898
|
const batch = asRecord2(payload);
|
|
33701
|
-
|
|
33702
|
-
|
|
33703
|
-
const
|
|
33899
|
+
const batchItems = Array.isArray(batch?.symbols) ? batch.symbols : Array.isArray(batch?.entries) ? batch.entries : null;
|
|
33900
|
+
if (batchItems) {
|
|
33901
|
+
const header = batch?.complete === false ? [theme.fg("warning", "Incomplete zoom results")] : [];
|
|
33704
33902
|
return [
|
|
33705
33903
|
...header,
|
|
33706
|
-
...
|
|
33904
|
+
...batchItems.map((item) => {
|
|
33707
33905
|
const record2 = asRecord2(item);
|
|
33708
33906
|
if (!record2)
|
|
33709
33907
|
return theme.fg("muted", "No zoom result available.");
|
|
33710
33908
|
const name = asString(record2.name) ?? "(unknown symbol)";
|
|
33909
|
+
const itemTargetLabel = asString(record2.targetLabel) ?? zoomTargetLabel(args);
|
|
33711
33910
|
if (record2.success === false) {
|
|
33712
|
-
|
|
33911
|
+
const location = record2.targetLabel ? ` in ${shortenPath2(itemTargetLabel)}` : "";
|
|
33912
|
+
return theme.fg("error", `Symbol "${name}" not found${location}: ${asString(record2.error) ?? "zoom failed"}`);
|
|
33713
33913
|
}
|
|
33714
33914
|
const content = asString(record2.content);
|
|
33715
33915
|
return [
|
|
33716
|
-
`${theme.fg("accent", name)} ${theme.fg("muted",
|
|
33916
|
+
`${theme.fg("accent", name)} ${theme.fg("muted", shortenPath2(itemTargetLabel))}`,
|
|
33717
33917
|
content
|
|
33718
33918
|
].filter(Boolean).join(`
|
|
33719
33919
|
`);
|
|
@@ -33733,7 +33933,7 @@ function buildZoomSections(args, payload, theme) {
|
|
|
33733
33933
|
const startLine = range && typeof range.start_line === "number" ? range.start_line : undefined;
|
|
33734
33934
|
const endLine = range && typeof range.end_line === "number" ? range.end_line : undefined;
|
|
33735
33935
|
const targetLabel = zoomTargetLabel(args);
|
|
33736
|
-
const location = startLine !== undefined ? `${
|
|
33936
|
+
const location = startLine !== undefined ? `${shortenPath2(targetLabel)}:${startLine}${endLine && endLine !== startLine ? `-${endLine}` : ""}` : shortenPath2(targetLabel);
|
|
33737
33937
|
const lines = [`${theme.fg("accent", name)} ${theme.fg("muted", `[${kind}] ${location}`)}`];
|
|
33738
33938
|
const content = asString(record2.content);
|
|
33739
33939
|
if (content) {
|
|
@@ -33810,7 +34010,9 @@ Pass a single \`target\`:
|
|
|
33810
34010
|
if (target.length === 0) {
|
|
33811
34011
|
throw new Error("'target' must be a non-empty string or array of strings");
|
|
33812
34012
|
}
|
|
33813
|
-
const
|
|
34013
|
+
const resolvedTargets = await Promise.all(target.map((entry) => resolvePathArg(extCtx.cwd, entry)));
|
|
34014
|
+
await assertReadPathPermissions(extCtx, ctx, resolvedTargets);
|
|
34015
|
+
const response3 = await callBridge(bridge, "outline", { target: resolvedTargets, files: true }, extCtx);
|
|
33814
34016
|
if (response3.success === false) {
|
|
33815
34017
|
throw new Error(response3.message || "outline failed");
|
|
33816
34018
|
}
|
|
@@ -33819,13 +34021,14 @@ Pass a single \`target\`:
|
|
|
33819
34021
|
if (typeof target !== "string" || target.length === 0) {
|
|
33820
34022
|
throw new Error("'target' must be a non-empty string or array of strings");
|
|
33821
34023
|
}
|
|
34024
|
+
const resolvedTarget2 = await resolvePathArg(extCtx.cwd, target);
|
|
33822
34025
|
let isDirectory2 = false;
|
|
33823
34026
|
try {
|
|
33824
|
-
const
|
|
33825
|
-
const st = await stat2(resolved);
|
|
34027
|
+
const st = await stat2(resolvedTarget2);
|
|
33826
34028
|
isDirectory2 = st.isDirectory();
|
|
33827
34029
|
} catch {}
|
|
33828
|
-
|
|
34030
|
+
await assertReadPathPermissions(extCtx, ctx, resolvedTarget2);
|
|
34031
|
+
const request = isDirectory2 ? { directory: resolvedTarget2, files: true } : { file: resolvedTarget2, files: true };
|
|
33829
34032
|
const response2 = await callBridge(bridge, "outline", request, extCtx);
|
|
33830
34033
|
if (response2.success === false) {
|
|
33831
34034
|
throw new Error(response2.message || "outline failed");
|
|
@@ -33840,24 +34043,26 @@ Pass a single \`target\`:
|
|
|
33840
34043
|
return textResult(formatOutlineText(response2));
|
|
33841
34044
|
}
|
|
33842
34045
|
if (isArray) {
|
|
33843
|
-
const
|
|
34046
|
+
const resolvedTargets = await Promise.all(target.map((entry) => resolvePathArg(extCtx.cwd, entry)));
|
|
34047
|
+
await assertReadPathPermissions(extCtx, ctx, resolvedTargets);
|
|
34048
|
+
const response2 = await callBridge(bridge, "outline", { files: resolvedTargets }, extCtx);
|
|
33844
34049
|
return textResult(formatOutlineText(response2));
|
|
33845
34050
|
}
|
|
33846
34051
|
if (typeof target !== "string" || target.length === 0) {
|
|
33847
34052
|
throw new Error("'target' must be a non-empty string or array of strings");
|
|
33848
34053
|
}
|
|
34054
|
+
const resolvedTarget = await resolvePathArg(extCtx.cwd, target);
|
|
33849
34055
|
let isDirectory = false;
|
|
33850
34056
|
try {
|
|
33851
|
-
const
|
|
33852
|
-
const st = await stat2(resolved);
|
|
34057
|
+
const st = await stat2(resolvedTarget);
|
|
33853
34058
|
isDirectory = st.isDirectory();
|
|
33854
34059
|
} catch {}
|
|
34060
|
+
await assertReadPathPermissions(extCtx, ctx, resolvedTarget);
|
|
33855
34061
|
if (isDirectory) {
|
|
33856
|
-
const
|
|
33857
|
-
const response2 = await callBridge(bridge, "outline", { directory: dirPath }, extCtx);
|
|
34062
|
+
const response2 = await callBridge(bridge, "outline", { directory: resolvedTarget }, extCtx);
|
|
33858
34063
|
return textResult(JSON.stringify(response2, null, 2), response2);
|
|
33859
34064
|
}
|
|
33860
|
-
const response = await callBridge(bridge, "outline", { file:
|
|
34065
|
+
const response = await callBridge(bridge, "outline", { file: resolvedTarget }, extCtx);
|
|
33861
34066
|
return textResult(formatOutlineText(response));
|
|
33862
34067
|
},
|
|
33863
34068
|
renderCall(args, theme, context) {
|
|
@@ -33872,7 +34077,7 @@ Pass a single \`target\`:
|
|
|
33872
34077
|
pi.registerTool({
|
|
33873
34078
|
name: "aft_zoom",
|
|
33874
34079
|
label: "zoom",
|
|
33875
|
-
description: "Inspect code symbols or documentation sections. For code, returns the full source of a symbol
|
|
34080
|
+
description: "Inspect code symbols or documentation sections. For code, returns the full source of a symbol. Pass `callgraph: true` to also include call-graph annotations (calls-out / called-by within the same file). For Markdown and HTML, returns the section content under the given heading.\n\nUse exactly ONE mode: `{ filePath, symbols }`, `{ url, symbols }`, or `{ targets }`. `symbols` can be a string or array (one or many lookups in the same file/URL). Use `targets` for cross-file batches: `{ filePath, symbol }` or an array of them.",
|
|
33876
34081
|
parameters: ZoomParams,
|
|
33877
34082
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33878
34083
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
@@ -33896,6 +34101,7 @@ Pass a single \`target\`:
|
|
|
33896
34101
|
const hasUrl = !isEmptyParam(params.url);
|
|
33897
34102
|
const hasTargets = hasTargetsProvided(params.targets);
|
|
33898
34103
|
const hasSymbols = !isEmptyParam(params.symbols);
|
|
34104
|
+
const wantCallgraph = params.callgraph === true;
|
|
33899
34105
|
if (hasTargets) {
|
|
33900
34106
|
if (hasFilePath || hasUrl || hasSymbols) {
|
|
33901
34107
|
throw new Error("'targets' is mutually exclusive with 'filePath', 'url', and 'symbols'");
|
|
@@ -33912,10 +34118,17 @@ Pass a single \`target\`:
|
|
|
33912
34118
|
throw new Error(`targets[${i}].symbol must be a non-empty string`);
|
|
33913
34119
|
}
|
|
33914
34120
|
}
|
|
33915
|
-
const
|
|
33916
|
-
|
|
34121
|
+
const resolvedTargets = await Promise.all(targets.map((t) => resolvePathArg(extCtx.cwd, t.filePath)));
|
|
34122
|
+
await assertReadPathPermissions(extCtx, ctx, resolvedTargets);
|
|
34123
|
+
const responses = await Promise.all(targets.map((t, index) => {
|
|
34124
|
+
const req2 = {
|
|
34125
|
+
file: resolvedTargets[index],
|
|
34126
|
+
symbol: t.symbol
|
|
34127
|
+
};
|
|
33917
34128
|
if (params.contextLines !== undefined)
|
|
33918
34129
|
req2.context_lines = params.contextLines;
|
|
34130
|
+
if (wantCallgraph)
|
|
34131
|
+
req2.callgraph = true;
|
|
33919
34132
|
return callBridge(bridge, "zoom", req2, extCtx).catch((err) => ({
|
|
33920
34133
|
success: false,
|
|
33921
34134
|
message: err instanceof Error ? err.message : String(err)
|
|
@@ -33935,7 +34148,9 @@ Pass a single \`target\`:
|
|
|
33935
34148
|
if (hasFilePath && hasUrl) {
|
|
33936
34149
|
throw new Error("Provide exactly ONE of 'filePath' or 'url' — not both");
|
|
33937
34150
|
}
|
|
33938
|
-
const file2 = hasUrl ? params.url : params.filePath;
|
|
34151
|
+
const file2 = hasUrl ? params.url : await resolvePathArg(extCtx.cwd, params.filePath);
|
|
34152
|
+
if (!hasUrl)
|
|
34153
|
+
await assertReadPathPermissions(extCtx, ctx, file2);
|
|
33939
34154
|
const targetLabel = (hasUrl ? params.url : params.filePath) ?? file2;
|
|
33940
34155
|
const symbolsArray = hasSymbols ? typeof params.symbols === "string" ? [params.symbols] : params.symbols : undefined;
|
|
33941
34156
|
if (symbolsArray) {
|
|
@@ -33943,6 +34158,8 @@ Pass a single \`target\`:
|
|
|
33943
34158
|
const req2 = { file: file2, symbol: sym };
|
|
33944
34159
|
if (params.contextLines !== undefined)
|
|
33945
34160
|
req2.context_lines = params.contextLines;
|
|
34161
|
+
if (wantCallgraph)
|
|
34162
|
+
req2.callgraph = true;
|
|
33946
34163
|
return callBridge(bridge, "zoom", req2, extCtx).catch((err) => ({
|
|
33947
34164
|
success: false,
|
|
33948
34165
|
message: err instanceof Error ? err.message : String(err)
|
|
@@ -33961,6 +34178,8 @@ Pass a single \`target\`:
|
|
|
33961
34178
|
const req = { file: file2 };
|
|
33962
34179
|
if (params.contextLines !== undefined)
|
|
33963
34180
|
req.context_lines = params.contextLines;
|
|
34181
|
+
if (wantCallgraph)
|
|
34182
|
+
req.callgraph = true;
|
|
33964
34183
|
const response = await callBridge(bridge, "zoom", req, extCtx);
|
|
33965
34184
|
if (response.success === false) {
|
|
33966
34185
|
throw new Error(response.message || "zoom failed");
|
|
@@ -34073,21 +34292,21 @@ function buildRefactorSections(args, payload, theme) {
|
|
|
34073
34292
|
`${theme.fg("success", "moved symbol")} ${theme.fg("toolOutput", args.symbol ?? "(symbol)")}`,
|
|
34074
34293
|
`${theme.fg("muted", "files modified")} ${asNumber(response.files_modified) ?? results.length}`,
|
|
34075
34294
|
`${theme.fg("muted", "consumers updated")} ${asNumber(response.consumers_updated) ?? 0}`,
|
|
34076
|
-
results.length > 0 ? results.map((entry) => ` ↳ ${
|
|
34295
|
+
results.length > 0 ? results.map((entry) => ` ↳ ${shortenPath2(asString(entry.file) ?? "(unknown file)")}`).join(`
|
|
34077
34296
|
`) : theme.fg("muted", "No files reported.")
|
|
34078
34297
|
];
|
|
34079
34298
|
}
|
|
34080
34299
|
if (args.op === "extract") {
|
|
34081
34300
|
return [
|
|
34082
34301
|
`${theme.fg("success", "extracted")} ${theme.fg("toolOutput", asString(response.name) ?? args.name ?? "(function)")}`,
|
|
34083
|
-
`${theme.fg("muted", "file")} ${theme.fg("accent",
|
|
34302
|
+
`${theme.fg("muted", "file")} ${theme.fg("accent", shortenPath2(asString(response.file) ?? args.filePath))}`,
|
|
34084
34303
|
`${theme.fg("muted", "params")} ${Array.isArray(response.parameters) ? response.parameters.join(", ") || "none" : "none"}`,
|
|
34085
34304
|
`${theme.fg("muted", "return type")} ${asString(response.return_type) ?? "unknown"}`
|
|
34086
34305
|
];
|
|
34087
34306
|
}
|
|
34088
34307
|
return [
|
|
34089
34308
|
`${theme.fg("success", "inlined")} ${theme.fg("toolOutput", asString(response.symbol) ?? args.symbol ?? "(symbol)")}`,
|
|
34090
|
-
`${theme.fg("muted", "file")} ${theme.fg("accent",
|
|
34309
|
+
`${theme.fg("muted", "file")} ${theme.fg("accent", shortenPath2(asString(response.file) ?? args.filePath))}`,
|
|
34091
34310
|
`${theme.fg("muted", "context")} ${asString(response.call_context) ?? "unknown"}`,
|
|
34092
34311
|
`${theme.fg("muted", "substitutions")} ${asNumber(response.substitutions) ?? 0}`
|
|
34093
34312
|
];
|
|
@@ -34112,7 +34331,6 @@ function registerRefactorTool(pi, ctx) {
|
|
|
34112
34331
|
description: "Workspace-wide refactoring that updates imports and references across files. `move` relocates a top-level symbol; `extract` pulls a line range into a new function; `inline` replaces a call site. Use aft_safety checkpoint/undo before risky refactors.",
|
|
34113
34332
|
parameters: RefactorParams,
|
|
34114
34333
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
34115
|
-
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
34116
34334
|
const commandMap = {
|
|
34117
34335
|
move: "move_symbol",
|
|
34118
34336
|
extract: "extract_function",
|
|
@@ -34127,18 +34345,40 @@ function registerRefactorTool(pi, ctx) {
|
|
|
34127
34345
|
if (params.op === "extract" && isEmptyParam(params.name)) {
|
|
34128
34346
|
throw new Error("'name' is required for 'extract' op");
|
|
34129
34347
|
}
|
|
34130
|
-
const
|
|
34348
|
+
const startLine = coerceOptionalInt(params.startLine, "startLine", 1, Number.MAX_SAFE_INTEGER);
|
|
34349
|
+
const endLine = coerceOptionalInt(params.endLine, "endLine", 1, Number.MAX_SAFE_INTEGER);
|
|
34350
|
+
const callSiteLine = coerceOptionalInt(params.callSiteLine, "callSiteLine", 1, Number.MAX_SAFE_INTEGER);
|
|
34351
|
+
if (params.op === "extract") {
|
|
34352
|
+
if (startLine === undefined)
|
|
34353
|
+
throw new Error("'startLine' is required for 'extract' op");
|
|
34354
|
+
if (endLine === undefined)
|
|
34355
|
+
throw new Error("'endLine' is required for 'extract' op");
|
|
34356
|
+
}
|
|
34357
|
+
if (params.op === "inline" && callSiteLine === undefined) {
|
|
34358
|
+
throw new Error("'callSiteLine' is required for 'inline' op");
|
|
34359
|
+
}
|
|
34360
|
+
const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
|
|
34361
|
+
const destination = !isEmptyParam(params.destination) ? await resolvePathArg(extCtx.cwd, params.destination) : undefined;
|
|
34362
|
+
const permissionTargets = params.op === "move" && destination !== undefined ? [filePath, destination] : [filePath];
|
|
34363
|
+
const checked = new Set;
|
|
34364
|
+
for (const target of permissionTargets) {
|
|
34365
|
+
if (checked.has(target))
|
|
34366
|
+
continue;
|
|
34367
|
+
checked.add(target);
|
|
34368
|
+
await assertExternalDirectoryPermission(extCtx, target, "modify", {
|
|
34369
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
34370
|
+
});
|
|
34371
|
+
}
|
|
34372
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
34373
|
+
const req = { file: filePath };
|
|
34131
34374
|
if (!isEmptyParam(params.symbol))
|
|
34132
34375
|
req.symbol = params.symbol;
|
|
34133
|
-
if (
|
|
34134
|
-
req.destination =
|
|
34376
|
+
if (destination !== undefined)
|
|
34377
|
+
req.destination = destination;
|
|
34135
34378
|
if (!isEmptyParam(params.scope))
|
|
34136
34379
|
req.scope = params.scope;
|
|
34137
34380
|
if (!isEmptyParam(params.name))
|
|
34138
34381
|
req.name = params.name;
|
|
34139
|
-
const startLine = coerceOptionalInt(params.startLine, "startLine", 1, Number.MAX_SAFE_INTEGER);
|
|
34140
|
-
const endLine = coerceOptionalInt(params.endLine, "endLine", 1, Number.MAX_SAFE_INTEGER);
|
|
34141
|
-
const callSiteLine = coerceOptionalInt(params.callSiteLine, "callSiteLine", 1, Number.MAX_SAFE_INTEGER);
|
|
34142
34382
|
if (startLine !== undefined)
|
|
34143
34383
|
req.start_line = startLine;
|
|
34144
34384
|
if (endLine !== undefined) {
|
|
@@ -34161,6 +34401,9 @@ function registerRefactorTool(pi, ctx) {
|
|
|
34161
34401
|
// src/tools/safety.ts
|
|
34162
34402
|
import { StringEnum as StringEnum5 } from "@earendil-works/pi-ai";
|
|
34163
34403
|
import { Type as Type12 } from "typebox";
|
|
34404
|
+
function responsePaths(response) {
|
|
34405
|
+
return Array.isArray(response.paths) ? response.paths.filter((path2) => typeof path2 === "string" && path2.length > 0) : [];
|
|
34406
|
+
}
|
|
34164
34407
|
var SafetyParams = Type12.Object({
|
|
34165
34408
|
op: StringEnum5(["undo", "history", "checkpoint", "restore", "list"], {
|
|
34166
34409
|
description: "Safety operation"
|
|
@@ -34185,14 +34428,14 @@ function buildSafetySections(args, payload, theme) {
|
|
|
34185
34428
|
];
|
|
34186
34429
|
}
|
|
34187
34430
|
return [
|
|
34188
|
-
`${theme.fg("success", "restored")} ${theme.fg("accent",
|
|
34431
|
+
`${theme.fg("success", "restored")} ${theme.fg("accent", shortenPath2(asString(response.path) ?? args.filePath ?? "(file)"))}`,
|
|
34189
34432
|
`${theme.fg("muted", "backup")} ${asString(response.backup_id) ?? "—"}`
|
|
34190
34433
|
];
|
|
34191
34434
|
}
|
|
34192
34435
|
if (args.op === "history") {
|
|
34193
34436
|
const entries = asRecords(response.entries);
|
|
34194
34437
|
const sections2 = [
|
|
34195
|
-
theme.fg("accent",
|
|
34438
|
+
theme.fg("accent", shortenPath2(asString(response.file) ?? args.filePath ?? "(file)"))
|
|
34196
34439
|
];
|
|
34197
34440
|
if (entries.length === 0) {
|
|
34198
34441
|
sections2.push(theme.fg("muted", "No history entries."));
|
|
@@ -34214,7 +34457,7 @@ function buildSafetySections(args, payload, theme) {
|
|
|
34214
34457
|
`${theme.fg("success", "checkpoint created")} ${theme.fg("accent", asString(response.name) ?? args.name ?? "(checkpoint)")}`,
|
|
34215
34458
|
`${theme.fg("muted", "files")} ${asNumber(response.file_count) ?? 0}`,
|
|
34216
34459
|
skipped.length > 0 ? `${theme.fg("warning", "skipped")}
|
|
34217
|
-
${skipped.map((entry) => ` ↳ ${
|
|
34460
|
+
${skipped.map((entry) => ` ↳ ${shortenPath2(asString(entry.file) ?? "(file)")}: ${asString(entry.error) ?? "unknown error"}`).join(`
|
|
34218
34461
|
`)}` : theme.fg("muted", "No skipped files.")
|
|
34219
34462
|
];
|
|
34220
34463
|
}
|
|
@@ -34264,7 +34507,43 @@ function registerSafetyTool(pi, ctx) {
|
|
|
34264
34507
|
if ((params.op === "checkpoint" || params.op === "restore") && !params.name) {
|
|
34265
34508
|
throw new Error(`op='${params.op}' requires 'name'`);
|
|
34266
34509
|
}
|
|
34510
|
+
const filePath = params.filePath ? await resolvePathArg(extCtx.cwd, params.filePath) : undefined;
|
|
34511
|
+
const files = params.files ? await Promise.all(params.files.map((file2) => resolvePathArg(extCtx.cwd, file2))) : undefined;
|
|
34267
34512
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
34513
|
+
const restrictToProjectRoot = ctx.config.restrict_to_project_root ?? false;
|
|
34514
|
+
if (params.op === "undo") {
|
|
34515
|
+
const previewReq = {};
|
|
34516
|
+
if (filePath)
|
|
34517
|
+
previewReq.file = filePath;
|
|
34518
|
+
const preview = await callBridge(bridge, "undo_preview", previewReq, extCtx);
|
|
34519
|
+
for (const file2 of new Set(responsePaths(preview))) {
|
|
34520
|
+
await assertExternalDirectoryPermission(extCtx, file2, "modify", {
|
|
34521
|
+
restrictToProjectRoot
|
|
34522
|
+
});
|
|
34523
|
+
}
|
|
34524
|
+
}
|
|
34525
|
+
if (params.op === "checkpoint") {
|
|
34526
|
+
const checkpointFiles = files ?? (filePath ? [filePath] : undefined);
|
|
34527
|
+
if (Array.isArray(checkpointFiles)) {
|
|
34528
|
+
const checked = new Set;
|
|
34529
|
+
for (const file2 of checkpointFiles) {
|
|
34530
|
+
if (checked.has(file2))
|
|
34531
|
+
continue;
|
|
34532
|
+
checked.add(file2);
|
|
34533
|
+
await assertExternalDirectoryPermission(extCtx, file2, "modify", {
|
|
34534
|
+
restrictToProjectRoot
|
|
34535
|
+
});
|
|
34536
|
+
}
|
|
34537
|
+
}
|
|
34538
|
+
}
|
|
34539
|
+
if (params.op === "restore" && params.name) {
|
|
34540
|
+
const preview = await callBridge(bridge, "checkpoint_paths", { name: params.name }, extCtx);
|
|
34541
|
+
for (const file2 of new Set(responsePaths(preview))) {
|
|
34542
|
+
await assertExternalDirectoryPermission(extCtx, file2, "modify", {
|
|
34543
|
+
restrictToProjectRoot
|
|
34544
|
+
});
|
|
34545
|
+
}
|
|
34546
|
+
}
|
|
34268
34547
|
const commandMap = {
|
|
34269
34548
|
undo: "undo",
|
|
34270
34549
|
history: "edit_history",
|
|
@@ -34276,16 +34555,16 @@ function registerSafetyTool(pi, ctx) {
|
|
|
34276
34555
|
if (params.name)
|
|
34277
34556
|
req.name = params.name;
|
|
34278
34557
|
if (params.op === "checkpoint") {
|
|
34279
|
-
if (
|
|
34280
|
-
req.files =
|
|
34281
|
-
} else if (
|
|
34282
|
-
req.files = [
|
|
34558
|
+
if (files) {
|
|
34559
|
+
req.files = files;
|
|
34560
|
+
} else if (filePath) {
|
|
34561
|
+
req.files = [filePath];
|
|
34283
34562
|
}
|
|
34284
34563
|
} else {
|
|
34285
|
-
if (
|
|
34286
|
-
req.file =
|
|
34287
|
-
if (
|
|
34288
|
-
req.files =
|
|
34564
|
+
if (filePath)
|
|
34565
|
+
req.file = filePath;
|
|
34566
|
+
if (files)
|
|
34567
|
+
req.files = files;
|
|
34289
34568
|
}
|
|
34290
34569
|
const response = await callBridge(bridge, commandMap[params.op], req, extCtx);
|
|
34291
34570
|
return textResult(JSON.stringify(response, null, 2));
|
|
@@ -34362,7 +34641,7 @@ function buildSemanticSections(args, payload, theme) {
|
|
|
34362
34641
|
}
|
|
34363
34642
|
const grouped = groupByFile(results, (result) => asString(result.file));
|
|
34364
34643
|
for (const [file2, fileResults] of grouped.entries()) {
|
|
34365
|
-
const lines = [theme.fg("accent",
|
|
34644
|
+
const lines = [theme.fg("accent", shortenPath2(file2))];
|
|
34366
34645
|
fileResults.forEach((result) => {
|
|
34367
34646
|
if (asString(result.kind) === "GrepLine") {
|
|
34368
34647
|
const line = asNumber(result.line);
|
|
@@ -34516,8 +34795,12 @@ function registerStructureTool(pi, ctx) {
|
|
|
34516
34795
|
parameters: TransformParams,
|
|
34517
34796
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
34518
34797
|
validateTransformParams(params);
|
|
34798
|
+
const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
|
|
34799
|
+
await assertExternalDirectoryPermission(extCtx, filePath, "modify", {
|
|
34800
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
34801
|
+
});
|
|
34519
34802
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
34520
|
-
const req = { file:
|
|
34803
|
+
const req = { file: filePath };
|
|
34521
34804
|
if (params.container !== undefined)
|
|
34522
34805
|
req.scope = params.container;
|
|
34523
34806
|
if (params.code !== undefined)
|
|
@@ -34554,32 +34837,32 @@ function registerStructureTool(pi, ctx) {
|
|
|
34554
34837
|
function validateTransformParams(params) {
|
|
34555
34838
|
const op = params.op;
|
|
34556
34839
|
if (op === "add_member") {
|
|
34557
|
-
if (
|
|
34840
|
+
if (isEmptyParam(params.container)) {
|
|
34558
34841
|
throw new Error("'container' is required for 'add_member' op");
|
|
34559
34842
|
}
|
|
34560
|
-
if (
|
|
34843
|
+
if (isEmptyParam(params.code)) {
|
|
34561
34844
|
throw new Error("'code' is required for 'add_member' op");
|
|
34562
34845
|
}
|
|
34563
34846
|
}
|
|
34564
34847
|
if (op === "add_derive" || op === "wrap_try_catch" || op === "add_decorator" || op === "add_struct_tags") {
|
|
34565
|
-
if (
|
|
34848
|
+
if (isEmptyParam(params.target)) {
|
|
34566
34849
|
throw new Error(`'target' is required for '${op}' op`);
|
|
34567
34850
|
}
|
|
34568
34851
|
}
|
|
34569
|
-
if (op === "add_derive" &&
|
|
34852
|
+
if (op === "add_derive" && isEmptyParam(params.derives)) {
|
|
34570
34853
|
throw new Error("'derives' array is required for 'add_derive' op");
|
|
34571
34854
|
}
|
|
34572
|
-
if (op === "add_decorator" &&
|
|
34855
|
+
if (op === "add_decorator" && isEmptyParam(params.decorator)) {
|
|
34573
34856
|
throw new Error("'decorator' is required for 'add_decorator' op");
|
|
34574
34857
|
}
|
|
34575
34858
|
if (op === "add_struct_tags") {
|
|
34576
|
-
if (
|
|
34859
|
+
if (isEmptyParam(params.field)) {
|
|
34577
34860
|
throw new Error("'field' is required for 'add_struct_tags' op");
|
|
34578
34861
|
}
|
|
34579
|
-
if (
|
|
34862
|
+
if (isEmptyParam(params.tag)) {
|
|
34580
34863
|
throw new Error("'tag' is required for 'add_struct_tags' op");
|
|
34581
34864
|
}
|
|
34582
|
-
if (
|
|
34865
|
+
if (isEmptyParam(params.value)) {
|
|
34583
34866
|
throw new Error("'value' is required for 'add_struct_tags' op");
|
|
34584
34867
|
}
|
|
34585
34868
|
}
|
|
@@ -34720,15 +35003,15 @@ var PLUGIN_VERSION = (() => {
|
|
|
34720
35003
|
return "0.0.0";
|
|
34721
35004
|
}
|
|
34722
35005
|
})();
|
|
34723
|
-
var ANNOUNCEMENT_VERSION = "0.
|
|
35006
|
+
var ANNOUNCEMENT_VERSION = "0.34.0";
|
|
34724
35007
|
var ANNOUNCEMENT_FEATURES = [
|
|
34725
|
-
"
|
|
34726
|
-
"
|
|
34727
|
-
"`
|
|
34728
|
-
"
|
|
34729
|
-
"
|
|
35008
|
+
"`aft_import` now supports 12 more languages (Solidity, Java, C#, PHP, Kotlin, Scala, Swift, Ruby, Lua, Perl, C/C++, Vue) — 17 total.",
|
|
35009
|
+
"New YAML + Kubernetes/CRD support across outline, zoom, search, and ast_grep.",
|
|
35010
|
+
"`aft_conflicts` takes an optional `path` to inspect any repo or git worktree, and names the repository it checked.",
|
|
35011
|
+
"`.aftignore` (layered on `.gitignore`) plus ripgrep-style `grep` that searches an explicitly-named file even when ignored.",
|
|
35012
|
+
"`aft_zoom` call-graph annotations are now opt-in (`callgraph: true`); `semantic.max_files` and `bash.foreground_wait_window_ms` are configurable."
|
|
34730
35013
|
];
|
|
34731
|
-
var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/
|
|
35014
|
+
var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
|
|
34732
35015
|
var ALL_ONLY_TOOLS = new Set([
|
|
34733
35016
|
"aft_callgraph",
|
|
34734
35017
|
"aft_delete",
|
|
@@ -35033,7 +35316,7 @@ ${lines}
|
|
|
35033
35316
|
if (onnxRuntimePromise) {
|
|
35034
35317
|
await Promise.race([
|
|
35035
35318
|
onnxRuntimePromise,
|
|
35036
|
-
new Promise((
|
|
35319
|
+
new Promise((resolve4) => setTimeout(() => resolve4(null), 60000))
|
|
35037
35320
|
]);
|
|
35038
35321
|
}
|
|
35039
35322
|
const bridge = pool.getBridge(cwd);
|