@cortexkit/aft-pi 0.33.0 → 0.35.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 +34 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2168 -1731
- package/dist/status-bar-inject.d.ts +12 -0
- package/dist/status-bar-inject.d.ts.map +1 -0
- 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/dist/workflow-hints.d.ts.map +1 -1
- package/package.json +10 -9
package/dist/index.js
CHANGED
|
@@ -11674,11 +11674,76 @@ function error(message, meta) {
|
|
|
11674
11674
|
console.error(`[aft-bridge] ERROR: ${message}`);
|
|
11675
11675
|
}
|
|
11676
11676
|
}
|
|
11677
|
+
// ../aft-bridge/dist/bash-hints.js
|
|
11678
|
+
var CONFLICT_HINT = `
|
|
11679
|
+
|
|
11680
|
+
[Hint] Use aft_conflicts to see all conflict regions across files in a single call.`;
|
|
11681
|
+
var GREP_HINT = `
|
|
11682
|
+
|
|
11683
|
+
[Hint] Use the grep tool instead of bash for faster indexed search.`;
|
|
11684
|
+
function maybeAppendConflictsHint(output) {
|
|
11685
|
+
if (!output.includes("Automatic merge failed; fix conflicts"))
|
|
11686
|
+
return output;
|
|
11687
|
+
if (!/^CONFLICT \(|^error: could not apply /m.test(output))
|
|
11688
|
+
return output;
|
|
11689
|
+
return output + CONFLICT_HINT;
|
|
11690
|
+
}
|
|
11691
|
+
function maybeAppendGrepHint(output, command) {
|
|
11692
|
+
const probe = command !== undefined ? command : output.slice(0, 300).split(`
|
|
11693
|
+
`)[0] ?? "";
|
|
11694
|
+
if (!/\b(rg|grep)\s/.test(probe))
|
|
11695
|
+
return output;
|
|
11696
|
+
return output + GREP_HINT;
|
|
11697
|
+
}
|
|
11677
11698
|
// ../aft-bridge/dist/bridge.js
|
|
11678
11699
|
import { spawn } from "node:child_process";
|
|
11679
11700
|
import { homedir } from "node:os";
|
|
11680
11701
|
import { join } from "node:path";
|
|
11681
11702
|
import { StringDecoder } from "node:string_decoder";
|
|
11703
|
+
|
|
11704
|
+
// ../aft-bridge/dist/status-bar.js
|
|
11705
|
+
var STATUS_BAR_HEARTBEAT_CALLS = 15;
|
|
11706
|
+
function createStatusBarEmitState() {
|
|
11707
|
+
return { callsSinceEmit: 0 };
|
|
11708
|
+
}
|
|
11709
|
+
function parseStatusBarCounts(value) {
|
|
11710
|
+
if (!value || typeof value !== "object")
|
|
11711
|
+
return;
|
|
11712
|
+
const record = value;
|
|
11713
|
+
const num = (key) => {
|
|
11714
|
+
const raw = record[key];
|
|
11715
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
|
|
11716
|
+
};
|
|
11717
|
+
return {
|
|
11718
|
+
errors: num("errors"),
|
|
11719
|
+
warnings: num("warnings"),
|
|
11720
|
+
dead_code: num("dead_code"),
|
|
11721
|
+
unused_exports: num("unused_exports"),
|
|
11722
|
+
duplicates: num("duplicates"),
|
|
11723
|
+
todos: num("todos"),
|
|
11724
|
+
tier2_stale: record.tier2_stale === true
|
|
11725
|
+
};
|
|
11726
|
+
}
|
|
11727
|
+
function countsEqual(a, b) {
|
|
11728
|
+
return a.errors === b.errors && a.warnings === b.warnings && a.dead_code === b.dead_code && a.unused_exports === b.unused_exports && a.duplicates === b.duplicates && a.todos === b.todos && a.tier2_stale === b.tier2_stale;
|
|
11729
|
+
}
|
|
11730
|
+
function shouldEmitStatusBar(state, next) {
|
|
11731
|
+
const changed = state.last === undefined || !countsEqual(state.last, next);
|
|
11732
|
+
state.callsSinceEmit += 1;
|
|
11733
|
+
const heartbeat = state.callsSinceEmit >= STATUS_BAR_HEARTBEAT_CALLS;
|
|
11734
|
+
if (changed || heartbeat) {
|
|
11735
|
+
state.last = next;
|
|
11736
|
+
state.callsSinceEmit = 0;
|
|
11737
|
+
return true;
|
|
11738
|
+
}
|
|
11739
|
+
return false;
|
|
11740
|
+
}
|
|
11741
|
+
function formatStatusBar(counts) {
|
|
11742
|
+
const staleMark = counts.tier2_stale ? "~" : "";
|
|
11743
|
+
return `[AFT E${counts.errors} W${counts.warnings} | ` + `${staleMark}D${counts.dead_code} U${counts.unused_exports} C${counts.duplicates} | ` + `T${counts.todos}]`;
|
|
11744
|
+
}
|
|
11745
|
+
|
|
11746
|
+
// ../aft-bridge/dist/bridge.js
|
|
11682
11747
|
var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
|
|
11683
11748
|
var BRIDGE_HANG_TIMEOUT_THRESHOLD = 2;
|
|
11684
11749
|
var SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS = 5000;
|
|
@@ -11789,6 +11854,7 @@ class BinaryBridge {
|
|
|
11789
11854
|
configureWarningClients = new Map;
|
|
11790
11855
|
restartResetTimer = null;
|
|
11791
11856
|
lastChildActivityAt = 0;
|
|
11857
|
+
lastStatusBar;
|
|
11792
11858
|
consecutiveRequestTimeouts = 0;
|
|
11793
11859
|
errorPrefix;
|
|
11794
11860
|
logger;
|
|
@@ -12166,6 +12232,7 @@ class BinaryBridge {
|
|
|
12166
12232
|
this.spawnProcess(triggeringSessionId);
|
|
12167
12233
|
}
|
|
12168
12234
|
spawnProcess(triggeringSessionId) {
|
|
12235
|
+
this.lastStatusBar = undefined;
|
|
12169
12236
|
if (triggeringSessionId) {
|
|
12170
12237
|
this.sessionLogVia(triggeringSessionId, `Spawning binary: ${this.binaryPath} (cwd: ${this.cwd})`);
|
|
12171
12238
|
} else {
|
|
@@ -12367,6 +12434,7 @@ class BinaryBridge {
|
|
|
12367
12434
|
clearTimeout(entry.timer);
|
|
12368
12435
|
this.consecutiveRequestTimeouts = 0;
|
|
12369
12436
|
this.scheduleRestartCountReset();
|
|
12437
|
+
this.captureStatusBar(response);
|
|
12370
12438
|
entry.resolve(response);
|
|
12371
12439
|
} else if (typeof response.type === "string") {
|
|
12372
12440
|
this.logVia(`Ignoring unknown stdout push frame type: ${response.type}`);
|
|
@@ -12376,6 +12444,14 @@ class BinaryBridge {
|
|
|
12376
12444
|
}
|
|
12377
12445
|
}
|
|
12378
12446
|
}
|
|
12447
|
+
captureStatusBar(response) {
|
|
12448
|
+
const parsed = parseStatusBarCounts(response.status_bar);
|
|
12449
|
+
if (parsed)
|
|
12450
|
+
this.lastStatusBar = parsed;
|
|
12451
|
+
}
|
|
12452
|
+
getStatusBar() {
|
|
12453
|
+
return this.lastStatusBar;
|
|
12454
|
+
}
|
|
12379
12455
|
handleTimeout(triggeringSessionId) {
|
|
12380
12456
|
this.consecutiveRequestTimeouts = 0;
|
|
12381
12457
|
this.rejectAllPending(new Error(`${this.errorPrefix} bridge killed during sibling timeout — request aborted`));
|
|
@@ -13152,26 +13228,28 @@ function getManualInstallHint() {
|
|
|
13152
13228
|
}
|
|
13153
13229
|
async function ensureOnnxRuntime(storageDir) {
|
|
13154
13230
|
const info = getPlatformInfo();
|
|
13155
|
-
const
|
|
13156
|
-
const
|
|
13231
|
+
const ortVersionDir = join6(storageDir, "onnxruntime", ORT_VERSION);
|
|
13232
|
+
const libName = info?.libName ?? "libonnxruntime.dylib";
|
|
13233
|
+
const resolvedOrtDir = resolveCachedOnnxRuntimeDir(ortVersionDir, libName);
|
|
13234
|
+
const libPath = join6(resolvedOrtDir, libName);
|
|
13157
13235
|
if (existsSync5(libPath)) {
|
|
13158
|
-
const meta = readOnnxInstalledMeta(
|
|
13236
|
+
const meta = readOnnxInstalledMeta(ortVersionDir);
|
|
13159
13237
|
if (meta?.sha256) {
|
|
13160
13238
|
try {
|
|
13161
13239
|
const currentHash = sha256File(libPath);
|
|
13162
13240
|
if (currentHash !== meta.sha256) {
|
|
13163
|
-
error(`ONNX Runtime at ${
|
|
13241
|
+
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
13242
|
} else {
|
|
13165
|
-
log(`ONNX Runtime found at ${
|
|
13166
|
-
return
|
|
13243
|
+
log(`ONNX Runtime found at ${resolvedOrtDir} (TOFU verified)`);
|
|
13244
|
+
return resolvedOrtDir;
|
|
13167
13245
|
}
|
|
13168
13246
|
} catch (err) {
|
|
13169
|
-
warn(`Could not verify ONNX Runtime hash at ${
|
|
13170
|
-
return
|
|
13247
|
+
warn(`Could not verify ONNX Runtime hash at ${resolvedOrtDir}: ${err}`);
|
|
13248
|
+
return resolvedOrtDir;
|
|
13171
13249
|
}
|
|
13172
13250
|
} else {
|
|
13173
|
-
log(`ONNX Runtime found at ${
|
|
13174
|
-
return
|
|
13251
|
+
log(`ONNX Runtime found at ${resolvedOrtDir} (no recorded hash, accepting)`);
|
|
13252
|
+
return resolvedOrtDir;
|
|
13175
13253
|
}
|
|
13176
13254
|
}
|
|
13177
13255
|
const systemPath = findSystemOnnxRuntime(info?.libName);
|
|
@@ -13192,8 +13270,8 @@ async function ensureOnnxRuntime(storageDir) {
|
|
|
13192
13270
|
return null;
|
|
13193
13271
|
}
|
|
13194
13272
|
try {
|
|
13195
|
-
cleanupIncompleteTargetIfUnowned(
|
|
13196
|
-
return await downloadOnnxRuntime(info,
|
|
13273
|
+
cleanupIncompleteTargetIfUnowned(ortVersionDir);
|
|
13274
|
+
return await downloadOnnxRuntime(info, ortVersionDir);
|
|
13197
13275
|
} finally {
|
|
13198
13276
|
releaseLock(lockPath);
|
|
13199
13277
|
}
|
|
@@ -13211,11 +13289,16 @@ function cleanupAbandonedStagingDirs(onnxBaseDir) {
|
|
|
13211
13289
|
let abandoned = false;
|
|
13212
13290
|
if (Number.isFinite(pid) && pid > 0) {
|
|
13213
13291
|
if (process.platform === "win32") {
|
|
13214
|
-
|
|
13215
|
-
|
|
13216
|
-
abandoned = ageMs > STALE_LOCK_MS;
|
|
13217
|
-
} catch {
|
|
13292
|
+
const ownerAlive = isProcessAlive(pid);
|
|
13293
|
+
if (!ownerAlive) {
|
|
13218
13294
|
abandoned = true;
|
|
13295
|
+
} else {
|
|
13296
|
+
try {
|
|
13297
|
+
const ageMs = Date.now() - statSync2(stagingDir).mtimeMs;
|
|
13298
|
+
abandoned = ageMs > STALE_LOCK_MS;
|
|
13299
|
+
} catch {
|
|
13300
|
+
abandoned = true;
|
|
13301
|
+
}
|
|
13219
13302
|
}
|
|
13220
13303
|
} else {
|
|
13221
13304
|
abandoned = !isProcessAlive(pid);
|
|
@@ -13249,17 +13332,32 @@ var REQUIRED_ORT_MIN_MINOR = 20;
|
|
|
13249
13332
|
var INVALID_ORT_VERSION = "<invalid>";
|
|
13250
13333
|
function parseOnnxVersionFromPath(value) {
|
|
13251
13334
|
const name = basename(value);
|
|
13252
|
-
const semverish = name.match(
|
|
13335
|
+
const semverish = name.match(/(?:^|[._-])(\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?)(?:\.(?:dylib|dll))?$/);
|
|
13253
13336
|
if (semverish)
|
|
13254
13337
|
return semverish[1].split(/[-+]/, 1)[0];
|
|
13255
13338
|
return /\d+\.\d+\.\d+/.test(name) ? INVALID_ORT_VERSION : null;
|
|
13256
13339
|
}
|
|
13340
|
+
function parseOnnxVersionFromDirectoryPath(value) {
|
|
13341
|
+
const parts = value.split(/[\\/]+/).filter(Boolean).reverse();
|
|
13342
|
+
for (const part of parts) {
|
|
13343
|
+
const version = parseOnnxVersionFromPath(part);
|
|
13344
|
+
if (version)
|
|
13345
|
+
return version;
|
|
13346
|
+
}
|
|
13347
|
+
return null;
|
|
13348
|
+
}
|
|
13349
|
+
function isPathInsideRoot(root, candidate) {
|
|
13350
|
+
const rel = relative(root, candidate);
|
|
13351
|
+
return rel === "" || !rel.startsWith("../") && !rel.startsWith("..\\") && rel !== ".." && !isAbsolute(rel) && !win32.isAbsolute(rel);
|
|
13352
|
+
}
|
|
13257
13353
|
function detectOnnxVersion(libDir, libName) {
|
|
13258
13354
|
try {
|
|
13259
13355
|
const entries = readdirSync(libDir);
|
|
13260
13356
|
const barePrefix = libName.replace(/\.(so|dylib|dll)$/, "");
|
|
13357
|
+
const expectedPrefix = process.platform === "win32" ? barePrefix.toLowerCase() : barePrefix;
|
|
13261
13358
|
for (const entry of entries) {
|
|
13262
|
-
|
|
13359
|
+
const comparable = process.platform === "win32" ? entry.toLowerCase() : entry;
|
|
13360
|
+
if (!comparable.startsWith(expectedPrefix))
|
|
13263
13361
|
continue;
|
|
13264
13362
|
const version = parseOnnxVersionFromPath(entry);
|
|
13265
13363
|
if (version)
|
|
@@ -13269,7 +13367,7 @@ function detectOnnxVersion(libDir, libName) {
|
|
|
13269
13367
|
if (existsSync5(base)) {
|
|
13270
13368
|
try {
|
|
13271
13369
|
const real = realpathSync(base);
|
|
13272
|
-
const version = parseOnnxVersionFromPath(real);
|
|
13370
|
+
const version = parseOnnxVersionFromPath(real) ?? parseOnnxVersionFromDirectoryPath(real);
|
|
13273
13371
|
if (version)
|
|
13274
13372
|
return version;
|
|
13275
13373
|
} catch {}
|
|
@@ -13280,6 +13378,7 @@ function detectOnnxVersion(libDir, libName) {
|
|
|
13280
13378
|
return version;
|
|
13281
13379
|
} catch {}
|
|
13282
13380
|
}
|
|
13381
|
+
return parseOnnxVersionFromDirectoryPath(libDir);
|
|
13283
13382
|
} catch {}
|
|
13284
13383
|
return null;
|
|
13285
13384
|
}
|
|
@@ -13315,6 +13414,14 @@ function directoryContainsLibrary(dir, libName) {
|
|
|
13315
13414
|
return false;
|
|
13316
13415
|
}
|
|
13317
13416
|
}
|
|
13417
|
+
function resolveCachedOnnxRuntimeDir(ortVersionDir, libName) {
|
|
13418
|
+
if (existsSync5(join6(ortVersionDir, libName)))
|
|
13419
|
+
return ortVersionDir;
|
|
13420
|
+
const libSubdir = join6(ortVersionDir, "lib");
|
|
13421
|
+
if (existsSync5(join6(libSubdir, libName)))
|
|
13422
|
+
return libSubdir;
|
|
13423
|
+
return ortVersionDir;
|
|
13424
|
+
}
|
|
13318
13425
|
function findSystemOnnxRuntime(libName) {
|
|
13319
13426
|
if (!libName)
|
|
13320
13427
|
return null;
|
|
@@ -13360,6 +13467,7 @@ function findSystemOnnxRuntime(libName) {
|
|
|
13360
13467
|
seen.add(key);
|
|
13361
13468
|
return true;
|
|
13362
13469
|
});
|
|
13470
|
+
const unknownVersionPaths = [];
|
|
13363
13471
|
for (const dir of uniquePaths) {
|
|
13364
13472
|
const libPath = join6(dir, libName);
|
|
13365
13473
|
if (process.platform === "win32") {
|
|
@@ -13368,23 +13476,18 @@ function findSystemOnnxRuntime(libName) {
|
|
|
13368
13476
|
} else if (!existsSync5(libPath)) {
|
|
13369
13477
|
continue;
|
|
13370
13478
|
}
|
|
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
|
-
}
|
|
13479
|
+
const version = detectOnnxVersion(dir, libName);
|
|
13480
|
+
if (!version) {
|
|
13481
|
+
unknownVersionPaths.push(dir);
|
|
13482
|
+
continue;
|
|
13483
|
+
}
|
|
13484
|
+
if (!isOnnxVersionCompatible(version)) {
|
|
13485
|
+
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.`);
|
|
13486
|
+
continue;
|
|
13384
13487
|
}
|
|
13385
13488
|
return dir;
|
|
13386
13489
|
}
|
|
13387
|
-
return null;
|
|
13490
|
+
return unknownVersionPaths[0] ?? null;
|
|
13388
13491
|
}
|
|
13389
13492
|
async function downloadFileWithCap(url, destPath) {
|
|
13390
13493
|
const controller = new AbortController;
|
|
@@ -13438,13 +13541,13 @@ function validateExtractedTree(stagingRoot) {
|
|
|
13438
13541
|
const linkTarget = readlinkSync(fullPath);
|
|
13439
13542
|
const resolvedTarget = resolve(dirname3(fullPath), linkTarget);
|
|
13440
13543
|
const rel2 = relative(realRoot, resolvedTarget);
|
|
13441
|
-
if (rel2.startsWith("..") ||
|
|
13544
|
+
if (rel2.startsWith("..") || isAbsolute(rel2) || win32.isAbsolute(rel2)) {
|
|
13442
13545
|
throw new Error(`extracted symlink ${fullPath} points outside staging root: ${linkTarget}`);
|
|
13443
13546
|
}
|
|
13444
13547
|
continue;
|
|
13445
13548
|
}
|
|
13446
13549
|
const rel = relative(realRoot, fullPath);
|
|
13447
|
-
if (rel.startsWith("..") ||
|
|
13550
|
+
if (rel.startsWith("..") || isAbsolute(rel) || win32.isAbsolute(rel)) {
|
|
13448
13551
|
throw new Error(`extracted entry ${fullPath} escapes staging root`);
|
|
13449
13552
|
}
|
|
13450
13553
|
if (lst.isDirectory()) {
|
|
@@ -13547,11 +13650,23 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
|
|
|
13547
13650
|
log(`ORT extract: failed to copy optional ${libFile}: ${copyErr}`);
|
|
13548
13651
|
}
|
|
13549
13652
|
}
|
|
13653
|
+
const targetRoot = realpathSync(targetDir);
|
|
13550
13654
|
for (const link of symlinks) {
|
|
13551
13655
|
const dst = join6(targetDir, link.name);
|
|
13552
13656
|
try {
|
|
13553
13657
|
unlinkSync3(dst);
|
|
13554
13658
|
} catch {}
|
|
13659
|
+
const dstForContainment = join6(targetRoot, link.name);
|
|
13660
|
+
const resolvedTarget = resolve(dirname3(dstForContainment), link.target);
|
|
13661
|
+
if (!isPathInsideRoot(targetRoot, resolvedTarget)) {
|
|
13662
|
+
const message = `ONNX Runtime symlink ${link.name} points outside install dir: ${link.target}`;
|
|
13663
|
+
if (requiredLibs.has(link.name)) {
|
|
13664
|
+
rmSync2(targetDir, { recursive: true, force: true });
|
|
13665
|
+
throw new Error(message);
|
|
13666
|
+
}
|
|
13667
|
+
log(`ORT extract: skipping optional symlink ${link.name}: ${message}`);
|
|
13668
|
+
continue;
|
|
13669
|
+
}
|
|
13555
13670
|
try {
|
|
13556
13671
|
symlinkSync(link.target, dst);
|
|
13557
13672
|
} catch (symlinkErr) {
|
|
@@ -13654,9 +13769,8 @@ ${new Date().toISOString()}
|
|
|
13654
13769
|
}
|
|
13655
13770
|
const age = Date.now() - lockMtimeMs;
|
|
13656
13771
|
const ageWithinFresh = Math.abs(age) < STALE_LOCK_MS;
|
|
13657
|
-
const
|
|
13658
|
-
|
|
13659
|
-
if (skipLiveness ? ageWithinFresh : ownerAlive && ageWithinFresh) {
|
|
13772
|
+
const ownerAlive = owningPid !== null && isProcessAlive(owningPid);
|
|
13773
|
+
if (ownerAlive && ageWithinFresh) {
|
|
13660
13774
|
return false;
|
|
13661
13775
|
}
|
|
13662
13776
|
log(`[onnx] reclaiming install lock (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
|
|
@@ -13697,7 +13811,31 @@ function releaseLock(lockPath) {
|
|
|
13697
13811
|
warn(`[onnx] unexpected error releasing lock ${lockPath}: ${err}`);
|
|
13698
13812
|
}
|
|
13699
13813
|
}
|
|
13814
|
+
function tasklistPidFromCsvLine(line) {
|
|
13815
|
+
const quoted = line.match(/"([^"]*)"/g);
|
|
13816
|
+
if (quoted && quoted.length >= 2)
|
|
13817
|
+
return quoted[1].slice(1, -1);
|
|
13818
|
+
const cells = line.split(",").map((cell) => cell.trim().replace(/^"|"$/g, ""));
|
|
13819
|
+
return cells[1] ?? null;
|
|
13820
|
+
}
|
|
13821
|
+
function isWindowsProcessAlive(pid) {
|
|
13822
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
13823
|
+
return false;
|
|
13824
|
+
try {
|
|
13825
|
+
const output = execFileSync("tasklist.exe", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
13826
|
+
encoding: "utf8",
|
|
13827
|
+
timeout: 2000,
|
|
13828
|
+
windowsHide: true
|
|
13829
|
+
});
|
|
13830
|
+
const expected = String(pid);
|
|
13831
|
+
return output.split(/\r?\n/).some((line) => tasklistPidFromCsvLine(line) === expected);
|
|
13832
|
+
} catch {
|
|
13833
|
+
return false;
|
|
13834
|
+
}
|
|
13835
|
+
}
|
|
13700
13836
|
function isProcessAlive(pid) {
|
|
13837
|
+
if (process.platform === "win32")
|
|
13838
|
+
return isWindowsProcessAlive(pid);
|
|
13701
13839
|
try {
|
|
13702
13840
|
process.kill(pid, 0);
|
|
13703
13841
|
return true;
|
|
@@ -13762,7 +13900,8 @@ class BridgePool {
|
|
|
13762
13900
|
onBashLongRunning: options.onBashLongRunning,
|
|
13763
13901
|
onBashPatternMatch: options.onBashPatternMatch,
|
|
13764
13902
|
errorPrefix: options.errorPrefix,
|
|
13765
|
-
logger: options.logger
|
|
13903
|
+
logger: options.logger,
|
|
13904
|
+
childEnv: options.childEnv
|
|
13766
13905
|
};
|
|
13767
13906
|
this.configOverrides = configOverrides;
|
|
13768
13907
|
if (Number.isFinite(this.idleTimeoutMs)) {
|
|
@@ -13895,6 +14034,9 @@ class BridgePool {
|
|
|
13895
14034
|
_testGetConfigOverrides() {
|
|
13896
14035
|
return { ...this.configOverrides };
|
|
13897
14036
|
}
|
|
14037
|
+
_testGetBridgeOptions() {
|
|
14038
|
+
return { ...this.bridgeOptions };
|
|
14039
|
+
}
|
|
13898
14040
|
}
|
|
13899
14041
|
function normalizeKey(projectRoot) {
|
|
13900
14042
|
const stripped = projectRoot.replace(/[/\\]+$/, "");
|
|
@@ -14632,7 +14774,7 @@ import {
|
|
|
14632
14774
|
// package.json
|
|
14633
14775
|
var package_default = {
|
|
14634
14776
|
name: "@cortexkit/aft-pi",
|
|
14635
|
-
version: "0.
|
|
14777
|
+
version: "0.35.0",
|
|
14636
14778
|
type: "module",
|
|
14637
14779
|
description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
|
|
14638
14780
|
main: "dist/index.js",
|
|
@@ -14651,10 +14793,11 @@ var package_default = {
|
|
|
14651
14793
|
typecheck: "tsc --noEmit",
|
|
14652
14794
|
test: "bun test src/__tests__/",
|
|
14653
14795
|
lint: "biome check src",
|
|
14654
|
-
prepublishOnly: "bun run build"
|
|
14796
|
+
prepublishOnly: "bun run build",
|
|
14797
|
+
"test:unit": "bun test src/__tests__ --path-ignore-patterns 'src/__tests__/e2e/**'"
|
|
14655
14798
|
},
|
|
14656
14799
|
dependencies: {
|
|
14657
|
-
"@cortexkit/aft-bridge": "0.
|
|
14800
|
+
"@cortexkit/aft-bridge": "0.35.0",
|
|
14658
14801
|
"@xterm/headless": "^5.5.0",
|
|
14659
14802
|
"comment-json": "^5.0.0",
|
|
14660
14803
|
diff: "^8.0.4",
|
|
@@ -14662,12 +14805,12 @@ var package_default = {
|
|
|
14662
14805
|
zod: "^4.1.8"
|
|
14663
14806
|
},
|
|
14664
14807
|
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.
|
|
14808
|
+
"@cortexkit/aft-darwin-arm64": "0.35.0",
|
|
14809
|
+
"@cortexkit/aft-darwin-x64": "0.35.0",
|
|
14810
|
+
"@cortexkit/aft-linux-arm64": "0.35.0",
|
|
14811
|
+
"@cortexkit/aft-linux-x64": "0.35.0",
|
|
14812
|
+
"@cortexkit/aft-win32-arm64": "0.35.0",
|
|
14813
|
+
"@cortexkit/aft-win32-x64": "0.35.0"
|
|
14671
14814
|
},
|
|
14672
14815
|
devDependencies: {
|
|
14673
14816
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -28851,6 +28994,8 @@ function date4(params) {
|
|
|
28851
28994
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
28852
28995
|
config(en_default());
|
|
28853
28996
|
// src/config.ts
|
|
28997
|
+
var FOREGROUND_WAIT_WINDOW_DEFAULT_MS = 8000;
|
|
28998
|
+
var FOREGROUND_WAIT_WINDOW_MIN_MS = 5000;
|
|
28854
28999
|
function resolveBashConfig(config2) {
|
|
28855
29000
|
const top = config2.bash;
|
|
28856
29001
|
const legacy = config2.experimental?.bash;
|
|
@@ -28858,13 +29003,16 @@ function resolveBashConfig(config2) {
|
|
|
28858
29003
|
const surfaceDefaultEnabled = surface !== "minimal";
|
|
28859
29004
|
const reminderEnabled = (typeof top === "object" && top !== null ? top.long_running_reminder_enabled : undefined) ?? legacy?.long_running_reminder_enabled;
|
|
28860
29005
|
const reminderInterval = (typeof top === "object" && top !== null ? top.long_running_reminder_interval_ms : undefined) ?? legacy?.long_running_reminder_interval_ms;
|
|
29006
|
+
const rawForegroundWait = typeof top === "object" && top !== null ? top.foreground_wait_window_ms : undefined;
|
|
29007
|
+
const foregroundWaitWindowMs = Math.max(FOREGROUND_WAIT_WINDOW_MIN_MS, rawForegroundWait ?? FOREGROUND_WAIT_WINDOW_DEFAULT_MS);
|
|
28861
29008
|
const base = {
|
|
28862
29009
|
enabled: false,
|
|
28863
29010
|
rewrite: false,
|
|
28864
29011
|
compress: false,
|
|
28865
29012
|
background: false,
|
|
28866
29013
|
long_running_reminder_enabled: reminderEnabled,
|
|
28867
|
-
long_running_reminder_interval_ms: reminderInterval
|
|
29014
|
+
long_running_reminder_interval_ms: reminderInterval,
|
|
29015
|
+
foreground_wait_window_ms: foregroundWaitWindowMs
|
|
28868
29016
|
};
|
|
28869
29017
|
if (top === false)
|
|
28870
29018
|
return base;
|
|
@@ -28918,20 +29066,22 @@ var CheckerEnum = exports_external.enum([
|
|
|
28918
29066
|
"staticcheck",
|
|
28919
29067
|
"none"
|
|
28920
29068
|
]);
|
|
29069
|
+
var ConfigureWarningsDeliveryEnum = exports_external.enum(["toast", "log", "chat"]);
|
|
28921
29070
|
var SemanticConfigSchema = exports_external.object({
|
|
28922
29071
|
backend: exports_external.enum(["fastembed", "openai_compatible", "ollama"]).optional(),
|
|
28923
29072
|
model: exports_external.string().trim().min(1).optional(),
|
|
28924
29073
|
base_url: exports_external.string().trim().min(1).optional(),
|
|
28925
29074
|
api_key_env: exports_external.string().trim().min(1).optional(),
|
|
28926
29075
|
timeout_ms: exports_external.number().int().positive().optional(),
|
|
28927
|
-
max_batch_size: exports_external.number().int().positive().optional()
|
|
29076
|
+
max_batch_size: exports_external.number().int().positive().optional(),
|
|
29077
|
+
max_files: exports_external.number().int().positive().optional()
|
|
28928
29078
|
});
|
|
28929
29079
|
var LspExtensionSchema = exports_external.string().trim().min(1).refine((value) => value.replace(/^\.+/, "").length > 0, {
|
|
28930
29080
|
message: "Extension must include characters other than leading dots"
|
|
28931
29081
|
});
|
|
28932
29082
|
var LspServerEntrySchema = exports_external.object({
|
|
28933
|
-
extensions: exports_external.array(LspExtensionSchema).min(1),
|
|
28934
|
-
binary: exports_external.string().trim().min(1),
|
|
29083
|
+
extensions: exports_external.array(LspExtensionSchema).min(1).optional(),
|
|
29084
|
+
binary: exports_external.string().trim().min(1).optional(),
|
|
28935
29085
|
args: exports_external.array(exports_external.string()).optional().default([]),
|
|
28936
29086
|
root_markers: exports_external.array(exports_external.string().trim().min(1)).optional().default([".git"]),
|
|
28937
29087
|
disabled: exports_external.boolean().optional().default(false),
|
|
@@ -28965,7 +29115,8 @@ var BashFeaturesSchema = exports_external.object({
|
|
|
28965
29115
|
compress: exports_external.boolean().optional(),
|
|
28966
29116
|
background: exports_external.boolean().optional(),
|
|
28967
29117
|
long_running_reminder_enabled: exports_external.boolean().optional(),
|
|
28968
|
-
long_running_reminder_interval_ms: exports_external.number().int().positive().optional()
|
|
29118
|
+
long_running_reminder_interval_ms: exports_external.number().int().positive().optional(),
|
|
29119
|
+
foreground_wait_window_ms: exports_external.number().int().positive().optional()
|
|
28969
29120
|
});
|
|
28970
29121
|
var BashConfigSchema = exports_external.union([exports_external.boolean(), BashFeaturesSchema]);
|
|
28971
29122
|
var InspectConfigSchema = exports_external.object({
|
|
@@ -28993,6 +29144,7 @@ var AftConfigSchema = exports_external.object({
|
|
|
28993
29144
|
validate_on_edit: exports_external.enum(["syntax", "full"]).optional(),
|
|
28994
29145
|
formatter: exports_external.record(exports_external.string(), FormatterEnum).optional(),
|
|
28995
29146
|
checker: exports_external.record(exports_external.string(), CheckerEnum).optional(),
|
|
29147
|
+
configure_warnings_delivery: ConfigureWarningsDeliveryEnum.optional(),
|
|
28996
29148
|
tool_surface: exports_external.enum(["minimal", "recommended", "all"]).optional(),
|
|
28997
29149
|
disabled_tools: exports_external.array(exports_external.string()).optional(),
|
|
28998
29150
|
restrict_to_project_root: exports_external.boolean().optional(),
|
|
@@ -29031,12 +29183,16 @@ function resolveLspConfigForConfigure(config2) {
|
|
|
29031
29183
|
const servers = Object.entries(config2.lsp?.servers ?? {}).map(([id, server]) => {
|
|
29032
29184
|
const entry = {
|
|
29033
29185
|
id,
|
|
29034
|
-
extensions: server.extensions.map(normalizeLspExtension),
|
|
29035
|
-
binary: server.binary,
|
|
29036
29186
|
args: server.args,
|
|
29037
29187
|
root_markers: server.root_markers,
|
|
29038
29188
|
disabled: server.disabled
|
|
29039
29189
|
};
|
|
29190
|
+
if (server.extensions && server.extensions.length > 0) {
|
|
29191
|
+
entry.extensions = server.extensions.map(normalizeLspExtension);
|
|
29192
|
+
}
|
|
29193
|
+
if (server.binary) {
|
|
29194
|
+
entry.binary = server.binary;
|
|
29195
|
+
}
|
|
29040
29196
|
if (server.env && Object.keys(server.env).length > 0) {
|
|
29041
29197
|
entry.env = server.env;
|
|
29042
29198
|
}
|
|
@@ -29284,6 +29440,8 @@ function mergeSemanticConfig(base, override) {
|
|
|
29284
29440
|
projectSafe.timeout_ms = override.timeout_ms;
|
|
29285
29441
|
if (override?.max_batch_size !== undefined)
|
|
29286
29442
|
projectSafe.max_batch_size = override.max_batch_size;
|
|
29443
|
+
if (override?.max_files !== undefined)
|
|
29444
|
+
projectSafe.max_files = override.max_files;
|
|
29287
29445
|
const semantic = { ...base, ...projectSafe };
|
|
29288
29446
|
if (Object.values(semantic).every((v) => v === undefined))
|
|
29289
29447
|
return;
|
|
@@ -29381,6 +29539,7 @@ var PROJECT_SAFE_TOP_LEVEL_FIELDS = new Set([
|
|
|
29381
29539
|
"tool_surface",
|
|
29382
29540
|
"format_on_edit",
|
|
29383
29541
|
"validate_on_edit",
|
|
29542
|
+
"configure_warnings_delivery",
|
|
29384
29543
|
"search_index",
|
|
29385
29544
|
"semantic_search",
|
|
29386
29545
|
"inspect",
|
|
@@ -31230,6 +31389,21 @@ function sendFeatureAnnouncement(version2, features, footer, storageDir) {
|
|
|
31230
31389
|
markAnnouncementSeen(storageDir, "pi", version2);
|
|
31231
31390
|
}
|
|
31232
31391
|
|
|
31392
|
+
// src/status-bar-inject.ts
|
|
31393
|
+
var DEFAULT_SESSION = "__default__";
|
|
31394
|
+
var emitStateBySession = new Map;
|
|
31395
|
+
function statusBarBlockForSession(sessionID, counts) {
|
|
31396
|
+
if (!counts)
|
|
31397
|
+
return;
|
|
31398
|
+
const key = sessionID ?? DEFAULT_SESSION;
|
|
31399
|
+
let state = emitStateBySession.get(key);
|
|
31400
|
+
if (!state) {
|
|
31401
|
+
state = createStatusBarEmitState();
|
|
31402
|
+
emitStateBySession.set(key, state);
|
|
31403
|
+
}
|
|
31404
|
+
return shouldEmitStatusBar(state, counts) ? formatStatusBar(counts) : undefined;
|
|
31405
|
+
}
|
|
31406
|
+
|
|
31233
31407
|
// src/shared/pty-cache.ts
|
|
31234
31408
|
var import_headless = __toESM(require_xterm_headless(), 1);
|
|
31235
31409
|
import * as fs2 from "node:fs/promises";
|
|
@@ -31368,1757 +31542,1811 @@ function registerShutdownCleanup(fn) {
|
|
|
31368
31542
|
|
|
31369
31543
|
// src/tools/ast.ts
|
|
31370
31544
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
31371
|
-
import { Type as
|
|
31545
|
+
import { Type as Type3 } from "typebox";
|
|
31372
31546
|
|
|
31373
|
-
// src/tools/
|
|
31547
|
+
// src/tools/hoisted.ts
|
|
31548
|
+
import { stat } from "node:fs/promises";
|
|
31374
31549
|
import { homedir as homedir8 } from "node:os";
|
|
31375
|
-
import {
|
|
31550
|
+
import { isAbsolute as isAbsolute2, relative as relative3, resolve as resolve3, sep } from "node:path";
|
|
31551
|
+
import {
|
|
31552
|
+
renderDiff
|
|
31553
|
+
} from "@earendil-works/pi-coding-agent";
|
|
31376
31554
|
import { Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
31377
|
-
|
|
31378
|
-
|
|
31379
|
-
|
|
31380
|
-
|
|
31381
|
-
|
|
31555
|
+
import { Type as Type2 } from "typebox";
|
|
31556
|
+
|
|
31557
|
+
// src/tools/diff-format.ts
|
|
31558
|
+
import { diffLines } from "diff";
|
|
31559
|
+
var DEFAULT_CONTEXT_LINES = 4;
|
|
31560
|
+
function formatDiffForPi(oldContent, newContent, contextLines = DEFAULT_CONTEXT_LINES) {
|
|
31561
|
+
const parts = diffLines(oldContent, newContent);
|
|
31562
|
+
const output = [];
|
|
31563
|
+
const oldLines = oldContent.split(`
|
|
31564
|
+
`);
|
|
31565
|
+
const newLines = newContent.split(`
|
|
31566
|
+
`);
|
|
31567
|
+
const maxLineNum = Math.max(oldLines.length, newLines.length);
|
|
31568
|
+
const lineNumWidth = String(maxLineNum).length;
|
|
31569
|
+
const pad = (n) => String(n).padStart(lineNumWidth, " ");
|
|
31570
|
+
const blank = " ".repeat(lineNumWidth);
|
|
31571
|
+
let oldLineNum = 1;
|
|
31572
|
+
let newLineNum = 1;
|
|
31573
|
+
let lastWasChange = false;
|
|
31574
|
+
let firstChangedLine;
|
|
31575
|
+
for (let i = 0;i < parts.length; i++) {
|
|
31576
|
+
const part = parts[i];
|
|
31577
|
+
const raw = part.value.split(`
|
|
31578
|
+
`);
|
|
31579
|
+
if (raw[raw.length - 1] === "")
|
|
31580
|
+
raw.pop();
|
|
31581
|
+
if (part.added || part.removed) {
|
|
31582
|
+
if (firstChangedLine === undefined)
|
|
31583
|
+
firstChangedLine = newLineNum;
|
|
31584
|
+
for (const line of raw) {
|
|
31585
|
+
if (part.added) {
|
|
31586
|
+
output.push(`+${pad(newLineNum)} ${line}`);
|
|
31587
|
+
newLineNum++;
|
|
31588
|
+
} else {
|
|
31589
|
+
output.push(`-${pad(oldLineNum)} ${line}`);
|
|
31590
|
+
oldLineNum++;
|
|
31591
|
+
}
|
|
31592
|
+
}
|
|
31593
|
+
lastWasChange = true;
|
|
31594
|
+
continue;
|
|
31595
|
+
}
|
|
31596
|
+
const nextIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
|
31597
|
+
const hasLeading = lastWasChange;
|
|
31598
|
+
const hasTrailing = nextIsChange;
|
|
31599
|
+
if (hasLeading && hasTrailing) {
|
|
31600
|
+
if (raw.length <= contextLines * 2) {
|
|
31601
|
+
for (const line of raw) {
|
|
31602
|
+
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
31603
|
+
oldLineNum++;
|
|
31604
|
+
newLineNum++;
|
|
31605
|
+
}
|
|
31606
|
+
} else {
|
|
31607
|
+
for (const line of raw.slice(0, contextLines)) {
|
|
31608
|
+
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
31609
|
+
oldLineNum++;
|
|
31610
|
+
newLineNum++;
|
|
31611
|
+
}
|
|
31612
|
+
const skipped = raw.length - contextLines * 2;
|
|
31613
|
+
output.push(` ${blank} ...`);
|
|
31614
|
+
oldLineNum += skipped;
|
|
31615
|
+
newLineNum += skipped;
|
|
31616
|
+
for (const line of raw.slice(raw.length - contextLines)) {
|
|
31617
|
+
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
31618
|
+
oldLineNum++;
|
|
31619
|
+
newLineNum++;
|
|
31620
|
+
}
|
|
31621
|
+
}
|
|
31622
|
+
} else if (hasLeading) {
|
|
31623
|
+
const shown = raw.slice(0, contextLines);
|
|
31624
|
+
for (const line of shown) {
|
|
31625
|
+
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
31626
|
+
oldLineNum++;
|
|
31627
|
+
newLineNum++;
|
|
31628
|
+
}
|
|
31629
|
+
const skipped = raw.length - shown.length;
|
|
31630
|
+
if (skipped > 0) {
|
|
31631
|
+
output.push(` ${blank} ...`);
|
|
31632
|
+
oldLineNum += skipped;
|
|
31633
|
+
newLineNum += skipped;
|
|
31634
|
+
}
|
|
31635
|
+
} else if (hasTrailing) {
|
|
31636
|
+
const shownCount = Math.min(contextLines, raw.length);
|
|
31637
|
+
const shown = raw.slice(raw.length - shownCount);
|
|
31638
|
+
const skipped = raw.length - shown.length;
|
|
31639
|
+
if (skipped > 0) {
|
|
31640
|
+
output.push(` ${blank} ...`);
|
|
31641
|
+
oldLineNum += skipped;
|
|
31642
|
+
newLineNum += skipped;
|
|
31643
|
+
}
|
|
31644
|
+
for (const line of shown) {
|
|
31645
|
+
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
31646
|
+
oldLineNum++;
|
|
31647
|
+
newLineNum++;
|
|
31648
|
+
}
|
|
31649
|
+
} else {
|
|
31650
|
+
oldLineNum += raw.length;
|
|
31651
|
+
newLineNum += raw.length;
|
|
31652
|
+
}
|
|
31653
|
+
lastWasChange = false;
|
|
31654
|
+
}
|
|
31655
|
+
return { diff: output.join(`
|
|
31656
|
+
`), firstChangedLine };
|
|
31382
31657
|
}
|
|
31383
|
-
|
|
31384
|
-
|
|
31385
|
-
|
|
31386
|
-
|
|
31387
|
-
return
|
|
31658
|
+
|
|
31659
|
+
// src/tools/hoisted.ts
|
|
31660
|
+
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.";
|
|
31661
|
+
function diagnosticsOnEditDefault(ctx) {
|
|
31662
|
+
return ctx.config.lsp?.diagnostics_on_edit ?? false;
|
|
31388
31663
|
}
|
|
31389
|
-
function
|
|
31390
|
-
const
|
|
31391
|
-
|
|
31392
|
-
text.setText(`${theme.fg("toolTitle", theme.bold(toolName))}${suffix}`);
|
|
31393
|
-
return text;
|
|
31664
|
+
function containsPath(parent, child) {
|
|
31665
|
+
const rel = relative3(parent, child);
|
|
31666
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
31394
31667
|
}
|
|
31395
|
-
function
|
|
31396
|
-
if (!path2)
|
|
31397
|
-
return
|
|
31398
|
-
|
|
31668
|
+
function expandTilde(path2) {
|
|
31669
|
+
if (!path2 || !path2.startsWith("~"))
|
|
31670
|
+
return path2;
|
|
31671
|
+
if (path2 === "~")
|
|
31672
|
+
return homedir8();
|
|
31673
|
+
if (path2.startsWith(`~${sep}`) || path2.startsWith("~/")) {
|
|
31674
|
+
return resolve3(homedir8(), path2.slice(2));
|
|
31675
|
+
}
|
|
31676
|
+
return path2;
|
|
31399
31677
|
}
|
|
31400
|
-
function
|
|
31401
|
-
|
|
31402
|
-
|
|
31678
|
+
function externalDirectoryPromptTimeoutMs() {
|
|
31679
|
+
const raw = process.env.AFT_PI_EXTERNAL_PROMPT_TIMEOUT_MS;
|
|
31680
|
+
if (raw === undefined)
|
|
31681
|
+
return 30000;
|
|
31682
|
+
const parsed = Number.parseInt(raw, 10);
|
|
31683
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 30000;
|
|
31403
31684
|
}
|
|
31404
|
-
function
|
|
31405
|
-
if (
|
|
31406
|
-
return result.details;
|
|
31407
|
-
const text = collectTextContent(result);
|
|
31408
|
-
if (!text)
|
|
31685
|
+
async function assertExternalDirectoryPermission(extCtx, target, action = "modify", options = {}) {
|
|
31686
|
+
if (!target)
|
|
31409
31687
|
return;
|
|
31410
|
-
|
|
31411
|
-
|
|
31412
|
-
|
|
31688
|
+
const expanded = expandTilde(target);
|
|
31689
|
+
const absoluteTarget = isAbsolute2(expanded) ? expanded : resolve3(extCtx.cwd, expanded);
|
|
31690
|
+
if (containsPath(extCtx.cwd, absoluteTarget))
|
|
31691
|
+
return;
|
|
31692
|
+
if (options.restrictToProjectRoot === false)
|
|
31413
31693
|
return;
|
|
31694
|
+
const confirmFn = extCtx.ui?.confirm;
|
|
31695
|
+
if (extCtx.hasUI === false || !confirmFn) {
|
|
31696
|
+
throw new Error(`Permission denied: cannot prompt for ${action} outside the project (${absoluteTarget}).`);
|
|
31414
31697
|
}
|
|
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));
|
|
31698
|
+
const timeoutMs = externalDirectoryPromptTimeoutMs();
|
|
31699
|
+
let timer;
|
|
31700
|
+
const timeoutPromise = new Promise((resolve4) => {
|
|
31701
|
+
timer = setTimeout(() => resolve4("timeout"), timeoutMs);
|
|
31434
31702
|
});
|
|
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
31703
|
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;
|
|
31704
|
+
const result = await Promise.race([
|
|
31705
|
+
confirmFn("Allow external directory access?", `AFT wants to ${action} outside the project: ${absoluteTarget}`),
|
|
31706
|
+
timeoutPromise
|
|
31707
|
+
]);
|
|
31708
|
+
if (result === true)
|
|
31709
|
+
return;
|
|
31710
|
+
if (result === "timeout") {
|
|
31711
|
+
throw new Error(`Permission denied: external directory prompt timed out after ${timeoutMs}ms.`);
|
|
31525
31712
|
}
|
|
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;
|
|
31713
|
+
throw new Error("Permission denied: external directory access was cancelled.");
|
|
31714
|
+
} finally {
|
|
31715
|
+
if (timer !== undefined)
|
|
31716
|
+
clearTimeout(timer);
|
|
31541
31717
|
}
|
|
31542
31718
|
}
|
|
31543
|
-
|
|
31544
|
-
|
|
31545
|
-
|
|
31546
|
-
|
|
31719
|
+
var ReadParams = Type2.Object({
|
|
31720
|
+
path: Type2.String({
|
|
31721
|
+
description: "Path to the file to read (absolute or relative to project root)"
|
|
31722
|
+
}),
|
|
31723
|
+
offset: optionalInt(1, Number.MAX_SAFE_INTEGER),
|
|
31724
|
+
limit: optionalInt(1, Number.MAX_SAFE_INTEGER)
|
|
31547
31725
|
});
|
|
31548
|
-
var
|
|
31549
|
-
|
|
31550
|
-
description: "
|
|
31726
|
+
var WriteParams = Type2.Object({
|
|
31727
|
+
filePath: Type2.String({
|
|
31728
|
+
description: "Path to the file to write (absolute or relative to project root)"
|
|
31551
31729
|
}),
|
|
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" }))
|
|
31730
|
+
content: Type2.String({ description: "Full file contents to write" }),
|
|
31731
|
+
diagnostics: Type2.Optional(Type2.Boolean({ description: DIAGNOSTICS_PARAM_DESCRIPTION }))
|
|
31556
31732
|
});
|
|
31557
|
-
var
|
|
31558
|
-
|
|
31559
|
-
|
|
31560
|
-
|
|
31561
|
-
|
|
31562
|
-
|
|
31563
|
-
|
|
31733
|
+
var EditParams = Type2.Object({
|
|
31734
|
+
filePath: Type2.String({
|
|
31735
|
+
description: "Path to the file to edit (absolute or relative to project root)"
|
|
31736
|
+
}),
|
|
31737
|
+
oldString: Type2.Optional(Type2.String({ description: "Text to find (exact match, fuzzy fallback)" })),
|
|
31738
|
+
newString: Type2.Optional(Type2.String({ description: "Replacement text (omit to delete match)" })),
|
|
31739
|
+
replaceAll: Type2.Optional(Type2.Boolean({ description: "Replace every occurrence" })),
|
|
31740
|
+
occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER),
|
|
31741
|
+
appendContent: Type2.Optional(Type2.String({
|
|
31742
|
+
description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). When set, oldString/newString are ignored."
|
|
31743
|
+
})),
|
|
31744
|
+
diagnostics: Type2.Optional(Type2.Boolean({ description: DIAGNOSTICS_PARAM_DESCRIPTION }))
|
|
31564
31745
|
});
|
|
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
|
-
|
|
31746
|
+
var GrepParams = Type2.Object({
|
|
31747
|
+
pattern: Type2.String({ description: "Regex pattern to search for" }),
|
|
31748
|
+
path: Type2.Optional(Type2.String({
|
|
31749
|
+
description: "Path scope (file or directory; absolute or relative to project root)"
|
|
31750
|
+
})),
|
|
31751
|
+
include: Type2.Optional(Type2.String({ description: "Glob filter for included files (e.g. '*.ts,*.tsx')" })),
|
|
31752
|
+
caseSensitive: Type2.Optional(Type2.Boolean({ description: "Case-sensitive matching" })),
|
|
31753
|
+
contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER)
|
|
31754
|
+
});
|
|
31755
|
+
function registerHoistedTools(pi, ctx, surface) {
|
|
31756
|
+
if (surface.hoistRead) {
|
|
31757
|
+
pi.registerTool({
|
|
31758
|
+
name: "read",
|
|
31759
|
+
label: "read",
|
|
31760
|
+
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.",
|
|
31761
|
+
promptSnippet: "Read file contents (supports offset/limit for large files)",
|
|
31762
|
+
promptGuidelines: ["Use read to examine files instead of cat or sed."],
|
|
31763
|
+
parameters: ReadParams,
|
|
31764
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
31765
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
31766
|
+
const offset = coerceOptionalInt(params.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
|
|
31767
|
+
const limit = coerceOptionalInt(params.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
|
|
31768
|
+
const req = { file: params.path };
|
|
31769
|
+
if (offset !== undefined) {
|
|
31770
|
+
req.start_line = offset;
|
|
31771
|
+
if (limit !== undefined) {
|
|
31772
|
+
req.end_line = offset + limit - 1;
|
|
31773
|
+
}
|
|
31774
|
+
} else if (limit !== undefined) {
|
|
31775
|
+
req.end_line = limit;
|
|
31776
|
+
}
|
|
31777
|
+
const response = await callBridge(bridge, "read", req, extCtx);
|
|
31778
|
+
if (Array.isArray(response.entries)) {
|
|
31779
|
+
return textResult(response.entries.join(`
|
|
31780
|
+
`));
|
|
31781
|
+
}
|
|
31782
|
+
let text = response.content ?? "";
|
|
31783
|
+
const agentSpecifiedRange = offset !== undefined || limit !== undefined;
|
|
31784
|
+
const footer = formatReadFooter(agentSpecifiedRange, response);
|
|
31785
|
+
if (footer)
|
|
31786
|
+
text += footer;
|
|
31787
|
+
return textResult(text);
|
|
31788
|
+
}
|
|
31789
|
+
});
|
|
31601
31790
|
}
|
|
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)}`);
|
|
31791
|
+
if (surface.hoistWrite) {
|
|
31792
|
+
pi.registerTool({
|
|
31793
|
+
name: "write",
|
|
31794
|
+
label: "write",
|
|
31795
|
+
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.",
|
|
31796
|
+
promptSnippet: "Create or overwrite files (uses filePath; auto-formats; diagnostics follow lsp.diagnostics_on_edit unless overridden)",
|
|
31797
|
+
promptGuidelines: ["Use write only for new files or complete rewrites."],
|
|
31798
|
+
parameters: WriteParams,
|
|
31799
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
31800
|
+
await assertExternalDirectoryPermission(extCtx, params.filePath, "modify", {
|
|
31801
|
+
restrictToProjectRoot: surface.restrictToProjectRoot
|
|
31615
31802
|
});
|
|
31803
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
31804
|
+
const response = await callBridge(bridge, "write", {
|
|
31805
|
+
file: params.filePath,
|
|
31806
|
+
content: params.content,
|
|
31807
|
+
diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
|
|
31808
|
+
include_diff_content: true
|
|
31809
|
+
}, extCtx);
|
|
31810
|
+
return buildMutationResult(params.filePath, response);
|
|
31811
|
+
},
|
|
31812
|
+
renderCall(args, theme, context) {
|
|
31813
|
+
return renderMutationCall("write", args?.filePath, theme, context);
|
|
31814
|
+
},
|
|
31815
|
+
renderResult(result, _options, theme, context) {
|
|
31816
|
+
return renderMutationResult(result, theme, context);
|
|
31616
31817
|
}
|
|
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
31818
|
});
|
|
31624
|
-
sections.push(lines.join(`
|
|
31625
|
-
`));
|
|
31626
31819
|
}
|
|
31627
|
-
|
|
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
|
-
}
|
|
31651
|
-
files.forEach((fileResult) => {
|
|
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) {
|
|
31820
|
+
if (surface.hoistEdit) {
|
|
31692
31821
|
pi.registerTool({
|
|
31693
|
-
name: "
|
|
31694
|
-
label: "
|
|
31695
|
-
description: "
|
|
31696
|
-
|
|
31822
|
+
name: "edit",
|
|
31823
|
+
label: "edit",
|
|
31824
|
+
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.",
|
|
31825
|
+
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.",
|
|
31826
|
+
promptGuidelines: [
|
|
31827
|
+
"Prefer edit over write when changing part of an existing file.",
|
|
31828
|
+
"Include enough surrounding context in oldString to make the match unique, or set replaceAll/occurrence explicitly.",
|
|
31829
|
+
"Use appendContent (instead of read+write) when adding text to the end of a file."
|
|
31830
|
+
],
|
|
31831
|
+
parameters: EditParams,
|
|
31697
31832
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
31833
|
+
await assertExternalDirectoryPermission(extCtx, params.filePath, "modify", {
|
|
31834
|
+
restrictToProjectRoot: surface.restrictToProjectRoot
|
|
31835
|
+
});
|
|
31698
31836
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
31837
|
+
if (typeof params.appendContent === "string") {
|
|
31838
|
+
const req2 = {
|
|
31839
|
+
op: "append",
|
|
31840
|
+
file: params.filePath,
|
|
31841
|
+
append_content: params.appendContent,
|
|
31842
|
+
diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
|
|
31843
|
+
include_diff_content: true
|
|
31844
|
+
};
|
|
31845
|
+
const response2 = await callBridge(bridge, "edit_match", req2, extCtx);
|
|
31846
|
+
return buildMutationResult(params.filePath, response2);
|
|
31847
|
+
}
|
|
31699
31848
|
const req = {
|
|
31700
|
-
|
|
31701
|
-
|
|
31849
|
+
file: params.filePath,
|
|
31850
|
+
match: params.oldString ?? "",
|
|
31851
|
+
replacement: params.newString ?? "",
|
|
31852
|
+
diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
|
|
31853
|
+
include_diff_content: true
|
|
31702
31854
|
};
|
|
31703
|
-
if (
|
|
31704
|
-
req.
|
|
31705
|
-
|
|
31706
|
-
|
|
31707
|
-
|
|
31708
|
-
|
|
31709
|
-
|
|
31710
|
-
return textResult(response.text ?? JSON.stringify(response));
|
|
31855
|
+
if (params.replaceAll === true)
|
|
31856
|
+
req.replace_all = true;
|
|
31857
|
+
const occurrence = coerceOptionalInt(params.occurrence, "occurrence", 0, Number.MAX_SAFE_INTEGER);
|
|
31858
|
+
if (occurrence !== undefined)
|
|
31859
|
+
req.occurrence = occurrence;
|
|
31860
|
+
const response = await callBridge(bridge, "edit_match", req, extCtx);
|
|
31861
|
+
return buildMutationResult(params.filePath, response);
|
|
31711
31862
|
},
|
|
31712
31863
|
renderCall(args, theme, context) {
|
|
31713
|
-
return
|
|
31864
|
+
return renderMutationCall("edit", args?.filePath, theme, context);
|
|
31714
31865
|
},
|
|
31715
31866
|
renderResult(result, _options, theme, context) {
|
|
31716
|
-
return
|
|
31867
|
+
return renderMutationResult(result, theme, context);
|
|
31717
31868
|
}
|
|
31718
31869
|
});
|
|
31719
31870
|
}
|
|
31720
|
-
if (surface.
|
|
31871
|
+
if (surface.hoistGrep) {
|
|
31721
31872
|
pi.registerTool({
|
|
31722
|
-
name: "
|
|
31723
|
-
label: "
|
|
31724
|
-
description: "
|
|
31725
|
-
|
|
31873
|
+
name: "grep",
|
|
31874
|
+
label: "grep",
|
|
31875
|
+
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.",
|
|
31876
|
+
promptSnippet: "Fast regex search across files (trigram-indexed inside the project root)",
|
|
31877
|
+
promptGuidelines: ["Prefer grep over bash-invoked find/rg for in-project searches."],
|
|
31878
|
+
parameters: GrepParams,
|
|
31726
31879
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
31727
31880
|
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);
|
|
31881
|
+
const req = { pattern: params.pattern };
|
|
31882
|
+
if (params.path) {
|
|
31883
|
+
await assertExternalDirectoryPermission(extCtx, params.path, "search", {
|
|
31884
|
+
restrictToProjectRoot: surface.restrictToProjectRoot
|
|
31885
|
+
});
|
|
31886
|
+
req.path = await resolvePathArg(extCtx.cwd, params.path);
|
|
31887
|
+
}
|
|
31888
|
+
if (params.include)
|
|
31889
|
+
req.include = splitIncludeGlobs(params.include);
|
|
31890
|
+
if (params.caseSensitive !== undefined)
|
|
31891
|
+
req.case_sensitive = params.caseSensitive;
|
|
31892
|
+
const contextLines = coerceOptionalInt(params.contextLines, "contextLines", 1, Number.MAX_SAFE_INTEGER);
|
|
31893
|
+
if (contextLines !== undefined)
|
|
31894
|
+
req.context_lines = contextLines;
|
|
31895
|
+
const response = await callBridge(bridge, "grep", req, extCtx);
|
|
31896
|
+
const text = response.text ?? "";
|
|
31897
|
+
return textResult(text);
|
|
31746
31898
|
}
|
|
31747
31899
|
});
|
|
31748
31900
|
}
|
|
31749
31901
|
}
|
|
31902
|
+
function buildMutationResult(filePath, response) {
|
|
31903
|
+
const diffObj = response.diff;
|
|
31904
|
+
const additions = diffObj?.additions ?? 0;
|
|
31905
|
+
const deletions = diffObj?.deletions ?? 0;
|
|
31906
|
+
const replacements = response.replacements;
|
|
31907
|
+
const diagnostics = response.lsp_diagnostics;
|
|
31908
|
+
const truncated = diffObj?.truncated === true;
|
|
31909
|
+
const noOp = response.no_op === true;
|
|
31910
|
+
const formatted = response.formatted;
|
|
31911
|
+
const formatSkippedReason = response.format_skipped_reason;
|
|
31912
|
+
const globFormatSkipReasons = response.format_skip_reasons;
|
|
31913
|
+
let diffText;
|
|
31914
|
+
let firstChangedLine;
|
|
31915
|
+
if (diffObj && !truncated && typeof diffObj.before === "string" && typeof diffObj.after === "string") {
|
|
31916
|
+
const piDiff = formatDiffForPi(diffObj.before, diffObj.after);
|
|
31917
|
+
diffText = piDiff.diff;
|
|
31918
|
+
firstChangedLine = piDiff.firstChangedLine;
|
|
31919
|
+
}
|
|
31920
|
+
const summaryHeader = replacements !== undefined ? `Edited ${filePath} (+${additions}/-${deletions}, ${replacements} replacement${replacements === 1 ? "" : "s"})` : `Wrote ${filePath} (+${additions}/-${deletions})`;
|
|
31921
|
+
let text = summaryHeader;
|
|
31922
|
+
if (noOp) {
|
|
31923
|
+
text += `
|
|
31750
31924
|
|
|
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");
|
|
31925
|
+
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
31926
|
}
|
|
31843
|
-
|
|
31844
|
-
|
|
31845
|
-
|
|
31846
|
-
|
|
31847
|
-
|
|
31848
|
-
|
|
31849
|
-
|
|
31850
|
-
|
|
31851
|
-
|
|
31852
|
-
|
|
31853
|
-
|
|
31854
|
-
|
|
31855
|
-
|
|
31856
|
-
|
|
31857
|
-
|
|
31858
|
-
|
|
31859
|
-
|
|
31860
|
-
|
|
31861
|
-
|
|
31862
|
-
|
|
31863
|
-
|
|
31864
|
-
|
|
31865
|
-
|
|
31866
|
-
|
|
31867
|
-
|
|
31868
|
-
|
|
31869
|
-
|
|
31870
|
-
|
|
31871
|
-
|
|
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);
|
|
31927
|
+
const skipNote = formatSkipReasonNote(formatSkippedReason);
|
|
31928
|
+
if (skipNote)
|
|
31929
|
+
text += `
|
|
31930
|
+
|
|
31931
|
+
${skipNote}`;
|
|
31932
|
+
const globSkipNote = formatGlobSkipReasonsNote(globFormatSkipReasons);
|
|
31933
|
+
if (globSkipNote)
|
|
31934
|
+
text += `
|
|
31935
|
+
|
|
31936
|
+
${globSkipNote}`;
|
|
31937
|
+
if (diagnostics && diagnostics.length > 0) {
|
|
31938
|
+
text += `
|
|
31939
|
+
|
|
31940
|
+
LSP diagnostics:
|
|
31941
|
+
${formatDiagnosticsText(diagnostics)}`;
|
|
31942
|
+
}
|
|
31943
|
+
return {
|
|
31944
|
+
content: [{ type: "text", text }],
|
|
31945
|
+
details: {
|
|
31946
|
+
diff: diffText,
|
|
31947
|
+
firstChangedLine,
|
|
31948
|
+
additions,
|
|
31949
|
+
deletions,
|
|
31950
|
+
replacements,
|
|
31951
|
+
diagnostics,
|
|
31952
|
+
truncated: truncated || undefined,
|
|
31953
|
+
formatted,
|
|
31954
|
+
formatSkippedReason,
|
|
31955
|
+
noOp: noOp || undefined
|
|
31956
31956
|
}
|
|
31957
|
-
}
|
|
31958
|
-
pi.registerTool(createBashStatusTool(ctx));
|
|
31959
|
-
pi.registerTool(createBashWatchTool(ctx));
|
|
31960
|
-
pi.registerTool(createBashWriteTool(ctx));
|
|
31961
|
-
pi.registerTool(createBashKillTool(ctx));
|
|
31957
|
+
};
|
|
31962
31958
|
}
|
|
31963
|
-
function
|
|
31964
|
-
if (
|
|
31965
|
-
return
|
|
31966
|
-
|
|
31967
|
-
|
|
31959
|
+
function formatGlobSkipReasonsNote(reasons) {
|
|
31960
|
+
if (!Array.isArray(reasons))
|
|
31961
|
+
return;
|
|
31962
|
+
const actionable = reasons.filter((reason) => typeof reason === "string").filter((reason) => ["formatter_not_installed", "formatter_excluded_path", "timeout", "error"].includes(reason));
|
|
31963
|
+
if (actionable.length === 0)
|
|
31964
|
+
return;
|
|
31965
|
+
return `Note: formatter skipped some glob edit result file(s): ${[...new Set(actionable)].sort().join(", ")}. See per-file format_skipped_reason values for details.`;
|
|
31968
31966
|
}
|
|
31969
|
-
function
|
|
31970
|
-
|
|
31971
|
-
|
|
31967
|
+
function formatSkipReasonNote(reason) {
|
|
31968
|
+
switch (reason) {
|
|
31969
|
+
case "formatter_not_installed":
|
|
31970
|
+
return "Note: formatter binary not installed; file written unformatted.";
|
|
31971
|
+
case "timeout":
|
|
31972
|
+
return "Note: formatter timed out; file written unformatted. Raise formatter_timeout_secs or check the formatter for hangs.";
|
|
31973
|
+
case "formatter_excluded_path":
|
|
31974
|
+
return "Note: formatter is configured to ignore this path (e.g. biome.json files.includes, .prettierignore). File written unformatted.";
|
|
31975
|
+
case "error":
|
|
31976
|
+
return "Note: formatter exited with an unrecognized error; file written unformatted.";
|
|
31977
|
+
default:
|
|
31978
|
+
return;
|
|
31979
|
+
}
|
|
31972
31980
|
}
|
|
31973
|
-
function
|
|
31974
|
-
|
|
31975
|
-
|
|
31976
|
-
|
|
31977
|
-
|
|
31978
|
-
|
|
31979
|
-
|
|
31980
|
-
|
|
31981
|
-
|
|
31982
|
-
|
|
31981
|
+
function formatDiagnosticsText(diagnostics) {
|
|
31982
|
+
try {
|
|
31983
|
+
return diagnostics.map((d) => {
|
|
31984
|
+
if (d && typeof d === "object") {
|
|
31985
|
+
const obj = d;
|
|
31986
|
+
const line = obj.line ?? obj.startLine ?? "?";
|
|
31987
|
+
const severity = obj.severity ?? "info";
|
|
31988
|
+
const msg = obj.message ?? JSON.stringify(obj);
|
|
31989
|
+
return ` [${severity}] line ${line}: ${msg}`;
|
|
31990
|
+
}
|
|
31991
|
+
return ` ${String(d)}`;
|
|
31992
|
+
}).join(`
|
|
31993
|
+
`);
|
|
31994
|
+
} catch {
|
|
31995
|
+
return JSON.stringify(diagnostics, null, 2);
|
|
31983
31996
|
}
|
|
31984
|
-
|
|
31985
|
-
|
|
31986
|
-
|
|
31997
|
+
}
|
|
31998
|
+
function reuseText(last) {
|
|
31999
|
+
return last instanceof Text ? last : new Text("", 0, 0);
|
|
32000
|
+
}
|
|
32001
|
+
function reuseContainer(last) {
|
|
32002
|
+
return last instanceof Container ? last : new Container;
|
|
32003
|
+
}
|
|
32004
|
+
function renderMutationCall(toolName, filePath, theme, context) {
|
|
32005
|
+
const text = reuseText(context.lastComponent);
|
|
32006
|
+
const pathDisplay = filePath ? theme.fg("accent", shortenPath(filePath)) : theme.fg("toolOutput", "...");
|
|
32007
|
+
text.setText(`${theme.fg("toolTitle", theme.bold(toolName))} ${pathDisplay}`);
|
|
32008
|
+
return text;
|
|
32009
|
+
}
|
|
32010
|
+
function renderMutationResult(result, theme, context) {
|
|
32011
|
+
if (context.isError) {
|
|
32012
|
+
const errorText = result.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join(`
|
|
32013
|
+
`).trim();
|
|
32014
|
+
const text = reuseText(context.lastComponent);
|
|
32015
|
+
text.setText(`
|
|
32016
|
+
${theme.fg("error", errorText || "edit failed")}`);
|
|
32017
|
+
return text;
|
|
31987
32018
|
}
|
|
31988
|
-
|
|
31989
|
-
|
|
31990
|
-
|
|
32019
|
+
const details = result.details;
|
|
32020
|
+
const diff = typeof details?.diff === "string" ? details.diff : undefined;
|
|
32021
|
+
if (!diff) {
|
|
32022
|
+
const additions = details?.additions ?? 0;
|
|
32023
|
+
const deletions = details?.deletions ?? 0;
|
|
32024
|
+
const text = reuseText(context.lastComponent);
|
|
32025
|
+
const summary = theme.fg("success", `+${additions}/-${deletions}`);
|
|
32026
|
+
let suffix = "";
|
|
32027
|
+
if (details?.truncated) {
|
|
32028
|
+
suffix = ` ${theme.fg("muted", "(diff truncated)")}`;
|
|
32029
|
+
} else if (details?.noOp) {
|
|
32030
|
+
suffix = ` ${theme.fg("muted", "(no net change)")}`;
|
|
32031
|
+
}
|
|
32032
|
+
text.setText(`
|
|
32033
|
+
${summary}${suffix}`);
|
|
32034
|
+
return text;
|
|
31991
32035
|
}
|
|
31992
|
-
|
|
32036
|
+
const container = reuseContainer(context.lastComponent);
|
|
32037
|
+
container.clear();
|
|
32038
|
+
container.addChild(new Spacer(1));
|
|
32039
|
+
container.addChild(new Text(renderDiff(diff), 1, 0));
|
|
32040
|
+
return container;
|
|
31993
32041
|
}
|
|
31994
|
-
function
|
|
31995
|
-
|
|
32042
|
+
function shortenPath(path2) {
|
|
32043
|
+
const home = homedir8();
|
|
32044
|
+
if (path2.startsWith(home))
|
|
32045
|
+
return `~${path2.slice(home.length)}`;
|
|
32046
|
+
return path2;
|
|
31996
32047
|
}
|
|
31997
|
-
function
|
|
31998
|
-
|
|
31999
|
-
|
|
32000
|
-
|
|
32001
|
-
|
|
32002
|
-
|
|
32003
|
-
|
|
32004
|
-
|
|
32005
|
-
|
|
32006
|
-
const data = await bashStatusSnapshot(bridge, extCtx, params.task_id, params.output_mode);
|
|
32007
|
-
const details = data;
|
|
32008
|
-
return bashStatusResult(await formatBashStatus(extCtx, params.task_id, details, params.output_mode), details);
|
|
32009
|
-
}
|
|
32010
|
-
};
|
|
32048
|
+
async function resolvePathArg(cwd, path2) {
|
|
32049
|
+
const expanded = expandTilde(path2);
|
|
32050
|
+
const abs = isAbsolute2(expanded) ? expanded : resolve3(cwd, expanded);
|
|
32051
|
+
try {
|
|
32052
|
+
await stat(abs);
|
|
32053
|
+
return abs;
|
|
32054
|
+
} catch {
|
|
32055
|
+
return expanded;
|
|
32056
|
+
}
|
|
32011
32057
|
}
|
|
32012
|
-
function
|
|
32013
|
-
|
|
32014
|
-
|
|
32015
|
-
|
|
32016
|
-
|
|
32017
|
-
|
|
32018
|
-
|
|
32019
|
-
|
|
32020
|
-
|
|
32021
|
-
const waitFor = parseWaitPattern(params.pattern);
|
|
32022
|
-
if (params.background === true) {
|
|
32023
|
-
if (!waitFor) {
|
|
32024
|
-
throw new Error("invalid_request: Use auto-reminder; bash_watch without pattern in async mode is redundant");
|
|
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);
|
|
32058
|
+
function splitIncludeGlobs(include) {
|
|
32059
|
+
const out = [];
|
|
32060
|
+
let depth = 0;
|
|
32061
|
+
let buf = "";
|
|
32062
|
+
for (const ch of include) {
|
|
32063
|
+
if (ch === "{") {
|
|
32064
|
+
depth++;
|
|
32065
|
+
buf += ch;
|
|
32066
|
+
continue;
|
|
32056
32067
|
}
|
|
32057
|
-
|
|
32058
|
-
|
|
32059
|
-
|
|
32060
|
-
|
|
32061
|
-
|
|
32062
|
-
label: "bash_write",
|
|
32063
|
-
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).",
|
|
32064
|
-
promptSnippet: "Write keystrokes/input to a PTY bash task",
|
|
32065
|
-
parameters: BashWriteParams,
|
|
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);
|
|
32068
|
+
if (ch === "}") {
|
|
32069
|
+
if (depth > 0)
|
|
32070
|
+
depth--;
|
|
32071
|
+
buf += ch;
|
|
32072
|
+
continue;
|
|
32070
32073
|
}
|
|
32071
|
-
|
|
32072
|
-
|
|
32073
|
-
|
|
32074
|
-
|
|
32075
|
-
|
|
32076
|
-
|
|
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);
|
|
32074
|
+
if (ch === "," && depth === 0) {
|
|
32075
|
+
const trimmed = buf.trim();
|
|
32076
|
+
if (trimmed.length > 0)
|
|
32077
|
+
out.push(trimmed);
|
|
32078
|
+
buf = "";
|
|
32079
|
+
continue;
|
|
32092
32080
|
}
|
|
32093
|
-
|
|
32081
|
+
buf += ch;
|
|
32082
|
+
}
|
|
32083
|
+
const tail2 = buf.trim();
|
|
32084
|
+
if (tail2.length > 0)
|
|
32085
|
+
out.push(tail2);
|
|
32086
|
+
return out;
|
|
32094
32087
|
}
|
|
32095
|
-
function
|
|
32096
|
-
|
|
32097
|
-
|
|
32098
|
-
|
|
32099
|
-
|
|
32100
|
-
|
|
32101
|
-
|
|
32102
|
-
|
|
32103
|
-
|
|
32104
|
-
|
|
32105
|
-
|
|
32106
|
-
|
|
32088
|
+
function formatReadFooter(agentSpecifiedRange, data) {
|
|
32089
|
+
if (agentSpecifiedRange)
|
|
32090
|
+
return "";
|
|
32091
|
+
if (!data.truncated)
|
|
32092
|
+
return "";
|
|
32093
|
+
const startLine = data.start_line;
|
|
32094
|
+
const endLine = data.end_line;
|
|
32095
|
+
const totalLines = data.total_lines;
|
|
32096
|
+
if (startLine === undefined || endLine === undefined || totalLines === undefined) {
|
|
32097
|
+
return "";
|
|
32098
|
+
}
|
|
32099
|
+
return `
|
|
32100
|
+
(Showing lines ${startLine}-${endLine} of ${totalLines}. Use offset/limit to read other sections.)`;
|
|
32107
32101
|
}
|
|
32108
|
-
|
|
32109
|
-
|
|
32110
|
-
|
|
32111
|
-
|
|
32112
|
-
|
|
32102
|
+
|
|
32103
|
+
// src/tools/render-helpers.ts
|
|
32104
|
+
import { homedir as homedir9 } from "node:os";
|
|
32105
|
+
import { renderDiff as renderDiff2 } from "@earendil-works/pi-coding-agent";
|
|
32106
|
+
import { Container as Container2, Spacer as Spacer2, Text as Text2 } from "@earendil-works/pi-tui";
|
|
32107
|
+
function reuseText2(last) {
|
|
32108
|
+
return last instanceof Text2 ? last : new Text2("", 0, 0);
|
|
32113
32109
|
}
|
|
32114
|
-
function
|
|
32115
|
-
return
|
|
32116
|
-
content: [{ type: "text", text: output }],
|
|
32117
|
-
details
|
|
32118
|
-
};
|
|
32110
|
+
function reuseContainer2(last) {
|
|
32111
|
+
return last instanceof Container2 ? last : new Container2;
|
|
32119
32112
|
}
|
|
32120
|
-
|
|
32121
|
-
|
|
32113
|
+
function shortenPath2(path2) {
|
|
32114
|
+
const home = homedir9();
|
|
32115
|
+
if (path2.startsWith(home))
|
|
32116
|
+
return `~${path2.slice(home.length)}`;
|
|
32117
|
+
return path2;
|
|
32122
32118
|
}
|
|
32123
|
-
|
|
32124
|
-
const
|
|
32125
|
-
const
|
|
32126
|
-
|
|
32127
|
-
|
|
32128
|
-
let scanBaseOffset = 0;
|
|
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;
|
|
32137
|
-
try {
|
|
32138
|
-
while (true) {
|
|
32139
|
-
const data = await bashStatusSnapshot(bridge, extCtx, taskId, outputMode, bridgeOptions);
|
|
32140
|
-
const terminal = isTerminalStatus(data.status);
|
|
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
|
-
}
|
|
32183
|
-
}
|
|
32119
|
+
function renderToolCall(toolName, summary, theme, context) {
|
|
32120
|
+
const text = reuseText2(context.lastComponent);
|
|
32121
|
+
const suffix = summary ? ` ${summary}` : "";
|
|
32122
|
+
text.setText(`${theme.fg("toolTitle", theme.bold(toolName))}${suffix}`);
|
|
32123
|
+
return text;
|
|
32184
32124
|
}
|
|
32185
|
-
|
|
32186
|
-
|
|
32187
|
-
|
|
32188
|
-
|
|
32189
|
-
|
|
32190
|
-
|
|
32191
|
-
|
|
32192
|
-
|
|
32193
|
-
|
|
32194
|
-
|
|
32195
|
-
|
|
32196
|
-
return
|
|
32197
|
-
|
|
32198
|
-
|
|
32199
|
-
nextCursor: { output: state.offset, stderr: 0, combined: state.offset }
|
|
32200
|
-
};
|
|
32201
|
-
}
|
|
32202
|
-
const stderrPath = data.stderr_path;
|
|
32203
|
-
if (!outputPath && !stderrPath)
|
|
32125
|
+
function accentPath(theme, path2) {
|
|
32126
|
+
if (!path2)
|
|
32127
|
+
return theme.fg("toolOutput", "...");
|
|
32128
|
+
return theme.fg("accent", shortenPath2(path2));
|
|
32129
|
+
}
|
|
32130
|
+
function collectTextContent(result) {
|
|
32131
|
+
return result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join(`
|
|
32132
|
+
`).trim();
|
|
32133
|
+
}
|
|
32134
|
+
function extractStructuredPayload(result) {
|
|
32135
|
+
if (result.details !== undefined)
|
|
32136
|
+
return result.details;
|
|
32137
|
+
const text = collectTextContent(result);
|
|
32138
|
+
if (!text)
|
|
32204
32139
|
return;
|
|
32205
|
-
|
|
32206
|
-
|
|
32207
|
-
|
|
32208
|
-
if (bytesRead === 0)
|
|
32140
|
+
try {
|
|
32141
|
+
return JSON.parse(text);
|
|
32142
|
+
} catch {
|
|
32209
32143
|
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
|
-
};
|
|
32219
|
-
}
|
|
32220
|
-
async function readFileBytesFrom(outputPath, cursor) {
|
|
32221
|
-
const handle = await fs3.open(outputPath, "r");
|
|
32222
|
-
try {
|
|
32223
|
-
const chunks = [];
|
|
32224
|
-
let offset = cursor;
|
|
32225
|
-
while (true) {
|
|
32226
|
-
const buffer2 = Buffer.allocUnsafe(64 * 1024);
|
|
32227
|
-
const { bytesRead } = await handle.read(buffer2, 0, buffer2.length, offset);
|
|
32228
|
-
if (bytesRead === 0)
|
|
32229
|
-
break;
|
|
32230
|
-
chunks.push(Buffer.from(buffer2.subarray(0, bytesRead)));
|
|
32231
|
-
offset += bytesRead;
|
|
32232
|
-
}
|
|
32233
|
-
return Buffer.concat(chunks);
|
|
32234
|
-
} finally {
|
|
32235
|
-
await handle.close().catch(() => {
|
|
32236
|
-
return;
|
|
32237
|
-
});
|
|
32238
32144
|
}
|
|
32239
32145
|
}
|
|
32240
|
-
function
|
|
32241
|
-
|
|
32242
|
-
|
|
32243
|
-
|
|
32244
|
-
|
|
32245
|
-
return;
|
|
32146
|
+
function renderErrorResult(result, fallback, theme, context) {
|
|
32147
|
+
const text = reuseText2(context.lastComponent);
|
|
32148
|
+
const message = collectTextContent(result) || fallback;
|
|
32149
|
+
text.setText(`
|
|
32150
|
+
${theme.fg("error", message)}`);
|
|
32151
|
+
return text;
|
|
32246
32152
|
}
|
|
32247
|
-
function
|
|
32248
|
-
|
|
32153
|
+
function renderSections(sections, context) {
|
|
32154
|
+
const container = reuseContainer2(context.lastComponent);
|
|
32155
|
+
container.clear();
|
|
32156
|
+
const visibleSections = sections.filter((section) => section.trim().length > 0);
|
|
32157
|
+
if (visibleSections.length === 0)
|
|
32158
|
+
return container;
|
|
32159
|
+
container.addChild(new Spacer2(1));
|
|
32160
|
+
visibleSections.forEach((section, index) => {
|
|
32161
|
+
if (index > 0)
|
|
32162
|
+
container.addChild(new Spacer2(1));
|
|
32163
|
+
container.addChild(new Text2(section, 0, 0));
|
|
32164
|
+
});
|
|
32165
|
+
return container;
|
|
32249
32166
|
}
|
|
32250
|
-
function
|
|
32251
|
-
if (
|
|
32252
|
-
|
|
32253
|
-
|
|
32254
|
-
}
|
|
32255
|
-
pattern.value.lastIndex = 0;
|
|
32256
|
-
const match = pattern.value.exec(text);
|
|
32257
|
-
return match ? { text: match[0], index: match.index } : undefined;
|
|
32167
|
+
function asRecord2(value) {
|
|
32168
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
32169
|
+
return;
|
|
32170
|
+
return value;
|
|
32258
32171
|
}
|
|
32259
|
-
function
|
|
32260
|
-
return
|
|
32172
|
+
function asRecords(value) {
|
|
32173
|
+
return Array.isArray(value) ? value.map(asRecord2).filter(Boolean) : [];
|
|
32261
32174
|
}
|
|
32262
|
-
function
|
|
32263
|
-
|
|
32264
|
-
|
|
32265
|
-
|
|
32266
|
-
|
|
32267
|
-
|
|
32175
|
+
function asString(value) {
|
|
32176
|
+
return typeof value === "string" ? value : undefined;
|
|
32177
|
+
}
|
|
32178
|
+
function asNumber(value) {
|
|
32179
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
32180
|
+
}
|
|
32181
|
+
function asBoolean(value) {
|
|
32182
|
+
return typeof value === "boolean" ? value : undefined;
|
|
32183
|
+
}
|
|
32184
|
+
function formatValue(value) {
|
|
32185
|
+
if (Array.isArray(value))
|
|
32186
|
+
return value.map(formatValue).join(", ");
|
|
32187
|
+
if (typeof value === "string")
|
|
32188
|
+
return value;
|
|
32189
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
32190
|
+
return String(value);
|
|
32191
|
+
if (value === null || value === undefined)
|
|
32192
|
+
return "—";
|
|
32193
|
+
try {
|
|
32194
|
+
return JSON.stringify(value);
|
|
32195
|
+
} catch {
|
|
32196
|
+
return String(value);
|
|
32268
32197
|
}
|
|
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
32198
|
}
|
|
32272
|
-
|
|
32273
|
-
const
|
|
32274
|
-
|
|
32275
|
-
|
|
32276
|
-
|
|
32277
|
-
|
|
32278
|
-
|
|
32279
|
-
|
|
32280
|
-
|
|
32281
|
-
|
|
32282
|
-
|
|
32283
|
-
|
|
32284
|
-
|
|
32199
|
+
function groupByFile(items, getFile) {
|
|
32200
|
+
const groups = new Map;
|
|
32201
|
+
items.forEach((item) => {
|
|
32202
|
+
const file2 = getFile(item) ?? "(unknown file)";
|
|
32203
|
+
const current = groups.get(file2) ?? [];
|
|
32204
|
+
current.push(item);
|
|
32205
|
+
groups.set(file2, current);
|
|
32206
|
+
});
|
|
32207
|
+
return groups;
|
|
32208
|
+
}
|
|
32209
|
+
function formatTimestamp(value) {
|
|
32210
|
+
if (typeof value === "string" && value.length > 0)
|
|
32211
|
+
return value;
|
|
32212
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
32213
|
+
return;
|
|
32214
|
+
const millis = value > 1000000000000 ? value : value * 1000;
|
|
32215
|
+
const date5 = new Date(millis);
|
|
32216
|
+
if (Number.isNaN(date5.getTime()))
|
|
32217
|
+
return String(value);
|
|
32218
|
+
return date5.toISOString().replace("T", " ").replace(".000Z", "Z");
|
|
32219
|
+
}
|
|
32220
|
+
function formatUnifiedDiffForPi(unifiedDiff) {
|
|
32221
|
+
if (!unifiedDiff.trim())
|
|
32222
|
+
return "";
|
|
32223
|
+
const entries = [];
|
|
32224
|
+
const hunkHeader = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
|
|
32225
|
+
let oldLine = 1;
|
|
32226
|
+
let newLine = 1;
|
|
32227
|
+
for (const line of unifiedDiff.split(`
|
|
32228
|
+
`)) {
|
|
32229
|
+
if (!line)
|
|
32230
|
+
continue;
|
|
32231
|
+
if (line.startsWith("--- ") || line.startsWith("+++ "))
|
|
32232
|
+
continue;
|
|
32233
|
+
if (line.startsWith("\"))
|
|
32234
|
+
continue;
|
|
32235
|
+
const headerMatch = line.match(hunkHeader);
|
|
32236
|
+
if (headerMatch) {
|
|
32237
|
+
oldLine = Number(headerMatch[1]);
|
|
32238
|
+
newLine = Number(headerMatch[2]);
|
|
32239
|
+
continue;
|
|
32285
32240
|
}
|
|
32286
|
-
if (!
|
|
32287
|
-
text
|
|
32288
|
-
|
|
32241
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
32242
|
+
entries.push({ prefix: "+", line: newLine, text: line.slice(1) });
|
|
32243
|
+
newLine += 1;
|
|
32244
|
+
continue;
|
|
32245
|
+
}
|
|
32246
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
32247
|
+
entries.push({ prefix: "-", line: oldLine, text: line.slice(1) });
|
|
32248
|
+
oldLine += 1;
|
|
32249
|
+
continue;
|
|
32250
|
+
}
|
|
32251
|
+
if (line.startsWith(" ")) {
|
|
32252
|
+
entries.push({ prefix: " ", line: oldLine, text: line.slice(1) });
|
|
32253
|
+
oldLine += 1;
|
|
32254
|
+
newLine += 1;
|
|
32289
32255
|
}
|
|
32290
32256
|
}
|
|
32291
|
-
|
|
32257
|
+
if (entries.length === 0)
|
|
32258
|
+
return "";
|
|
32259
|
+
const width = String(entries.reduce((max, entry) => Math.max(max, entry.line), 1)).length;
|
|
32260
|
+
return entries.map((entry) => `${entry.prefix}${String(entry.line).padStart(width, " ")} ${entry.text}`).join(`
|
|
32261
|
+
`);
|
|
32292
32262
|
}
|
|
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);
|
|
32263
|
+
function renderUnifiedDiff(unifiedDiff) {
|
|
32264
|
+
const piDiff = formatUnifiedDiffForPi(unifiedDiff);
|
|
32265
|
+
if (!piDiff)
|
|
32266
|
+
return "";
|
|
32267
|
+
try {
|
|
32268
|
+
return renderDiff2(piDiff);
|
|
32269
|
+
} catch {
|
|
32270
|
+
return piDiff;
|
|
32319
32271
|
}
|
|
32320
|
-
return suffix;
|
|
32321
32272
|
}
|
|
32322
|
-
|
|
32323
|
-
|
|
32324
|
-
|
|
32325
|
-
|
|
32326
|
-
}
|
|
32327
|
-
|
|
32328
|
-
|
|
32273
|
+
|
|
32274
|
+
// src/tools/ast.ts
|
|
32275
|
+
var AstLang = StringEnum(["typescript", "tsx", "javascript", "python", "rust", "go"], {
|
|
32276
|
+
description: "Target language"
|
|
32277
|
+
});
|
|
32278
|
+
var SearchParams = Type3.Object({
|
|
32279
|
+
pattern: Type3.String({
|
|
32280
|
+
description: "AST pattern with meta-variables (`$VAR` matches one node, `$$$` matches many). Must be a complete AST node."
|
|
32281
|
+
}),
|
|
32282
|
+
lang: AstLang,
|
|
32283
|
+
paths: Type3.Optional(Type3.Array(Type3.String(), { description: "Paths to search (default: ['.'])" })),
|
|
32284
|
+
globs: Type3.Optional(Type3.Array(Type3.String(), { description: "Include/exclude globs (prefix `!` to exclude)" })),
|
|
32285
|
+
contextLines: Type3.Optional(Type3.Number({ description: "Number of context lines around each match" }))
|
|
32286
|
+
});
|
|
32287
|
+
var ReplaceParams = Type3.Object({
|
|
32288
|
+
pattern: Type3.String({ description: "AST pattern with meta-variables" }),
|
|
32289
|
+
rewrite: Type3.String({ description: "Replacement pattern, can reference $VAR from pattern" }),
|
|
32290
|
+
lang: AstLang,
|
|
32291
|
+
paths: Type3.Optional(Type3.Array(Type3.String(), { description: "Paths (default: ['.'])" })),
|
|
32292
|
+
globs: Type3.Optional(Type3.Array(Type3.String(), { description: "Include/exclude globs" })),
|
|
32293
|
+
dryRun: Type3.Optional(Type3.Boolean({ description: "Preview without applying (default: false)" }))
|
|
32294
|
+
});
|
|
32295
|
+
function appendHintSection(response, sections, theme) {
|
|
32296
|
+
const hint = asString(response.hint);
|
|
32297
|
+
if (hint && hint.length > 0) {
|
|
32298
|
+
sections.push(theme.fg("warning", hint));
|
|
32299
|
+
}
|
|
32329
32300
|
}
|
|
32330
|
-
function
|
|
32331
|
-
|
|
32301
|
+
function appendScopeSections(response, sections, theme) {
|
|
32302
|
+
if (response.no_files_matched_scope === true) {
|
|
32303
|
+
sections.push(theme.fg("warning", "No files matched the scope (paths/globs resolved to zero files)"));
|
|
32304
|
+
}
|
|
32305
|
+
const warnings = asRecords(response.scope_warnings);
|
|
32306
|
+
const warningStrings = Array.isArray(response.scope_warnings) ? response.scope_warnings.filter((w) => typeof w === "string") : warnings.map((w) => asString(w.warning) ?? "").filter(Boolean);
|
|
32307
|
+
if (warningStrings.length > 0) {
|
|
32308
|
+
sections.push(`${theme.fg("muted", "Scope warnings:")}
|
|
32309
|
+
${warningStrings.map((w) => ` ${w}`).join(`
|
|
32310
|
+
`)}`);
|
|
32311
|
+
}
|
|
32332
32312
|
}
|
|
32333
|
-
function
|
|
32334
|
-
|
|
32313
|
+
async function resolveAstPaths(extCtx, paths) {
|
|
32314
|
+
if (isEmptyParam(paths) || !Array.isArray(paths))
|
|
32315
|
+
return;
|
|
32316
|
+
return Promise.all(paths.filter((path2) => typeof path2 === "string").map((path2) => resolvePathArg(extCtx.cwd, path2)));
|
|
32335
32317
|
}
|
|
32336
|
-
function
|
|
32337
|
-
|
|
32338
|
-
|
|
32339
|
-
|
|
32340
|
-
|
|
32318
|
+
async function assertAstPathsPermission(extCtx, paths, action, restrictToProjectRoot) {
|
|
32319
|
+
if (paths === undefined || paths.length === 0)
|
|
32320
|
+
return;
|
|
32321
|
+
const checked = new Set;
|
|
32322
|
+
for (const path2 of paths) {
|
|
32323
|
+
if (checked.has(path2))
|
|
32324
|
+
continue;
|
|
32325
|
+
checked.add(path2);
|
|
32326
|
+
await assertExternalDirectoryPermission(extCtx, path2, action, { restrictToProjectRoot });
|
|
32327
|
+
}
|
|
32341
32328
|
}
|
|
32342
|
-
function
|
|
32343
|
-
|
|
32344
|
-
|
|
32345
|
-
|
|
32346
|
-
|
|
32347
|
-
|
|
32348
|
-
|
|
32349
|
-
|
|
32350
|
-
|
|
32351
|
-
|
|
32352
|
-
|
|
32353
|
-
|
|
32354
|
-
|
|
32355
|
-
|
|
32356
|
-
|
|
32357
|
-
|
|
32358
|
-
|
|
32359
|
-
|
|
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));
|
|
32367
|
-
}
|
|
32368
|
-
if (exitCode !== undefined) {
|
|
32369
|
-
const exitColor = exitCode === 0 ? "success" : "error";
|
|
32370
|
-
const exitText = theme.fg(exitColor, `exit ${exitCode}`);
|
|
32371
|
-
container.addChild(new Text2(exitText, 1, 0));
|
|
32372
|
-
}
|
|
32373
|
-
if (bgCompletions.length > 0) {
|
|
32374
|
-
container.addChild(new Spacer2(1));
|
|
32375
|
-
for (const bg of bgCompletions) {
|
|
32376
|
-
const cmdPreview = bg.command ? bg.command.slice(0, 60) : "unknown command";
|
|
32377
|
-
const suffix = (bg.command?.length ?? 0) > 60 ? "..." : "";
|
|
32378
|
-
const exitInfo = bg.exit_code !== undefined ? `exit ${bg.exit_code}` : bg.status;
|
|
32379
|
-
const statusColor = bg.status === "completed" && bg.exit_code === 0 ? "success" : "warning";
|
|
32380
|
-
const line = theme.fg(statusColor, `Background task ${bg.task_id} completed (${exitInfo}): ${cmdPreview}${suffix}`);
|
|
32381
|
-
container.addChild(new Text2(line, 1, 0));
|
|
32382
|
-
}
|
|
32383
|
-
}
|
|
32384
|
-
if (details?.duration_ms !== undefined) {
|
|
32385
|
-
container.addChild(new Spacer2(1));
|
|
32386
|
-
const durationText = theme.fg("muted", `${details.duration_ms}ms`);
|
|
32387
|
-
container.addChild(new Text2(durationText, 1, 0));
|
|
32329
|
+
function buildAstSearchSections(payload, theme) {
|
|
32330
|
+
const response = asRecord2(payload);
|
|
32331
|
+
if (!response)
|
|
32332
|
+
return [theme.fg("muted", "No AST search results.")];
|
|
32333
|
+
const matches = asRecords(response.matches);
|
|
32334
|
+
const totalMatches = asNumber(response.total_matches) ?? matches.length;
|
|
32335
|
+
const filesWithMatches = asNumber(response.files_with_matches) ?? groupByFile(matches, (match) => asString(match.file)).size;
|
|
32336
|
+
const filesSearched = asNumber(response.files_searched);
|
|
32337
|
+
const header = [
|
|
32338
|
+
theme.fg("success", `${totalMatches} match${totalMatches === 1 ? "" : "es"}`),
|
|
32339
|
+
theme.fg("accent", `${filesWithMatches} file${filesWithMatches === 1 ? "" : "s"}`),
|
|
32340
|
+
filesSearched !== undefined ? theme.fg("muted", `${filesSearched} searched`) : undefined
|
|
32341
|
+
].filter(Boolean).join(" · ");
|
|
32342
|
+
if (matches.length === 0) {
|
|
32343
|
+
const sections2 = [header, theme.fg("muted", "No AST matches found.")];
|
|
32344
|
+
appendScopeSections(response, sections2, theme);
|
|
32345
|
+
appendHintSection(response, sections2, theme);
|
|
32346
|
+
return sections2;
|
|
32388
32347
|
}
|
|
32389
|
-
|
|
32390
|
-
|
|
32391
|
-
|
|
32392
|
-
|
|
32348
|
+
const grouped = groupByFile(matches, (match) => asString(match.file));
|
|
32349
|
+
const sections = [header];
|
|
32350
|
+
for (const [file2, fileMatches] of grouped.entries()) {
|
|
32351
|
+
const lines = [theme.fg("accent", shortenPath2(file2))];
|
|
32352
|
+
fileMatches.forEach((match, index) => {
|
|
32353
|
+
const line = asNumber(match.line) ?? 0;
|
|
32354
|
+
const column = asNumber(match.column) ?? 0;
|
|
32355
|
+
const snippet = asString(match.text)?.trim() || "(empty match)";
|
|
32356
|
+
lines.push(` ${index + 1}. ${theme.fg("muted", `${line}:${column}`)} ${snippet}`);
|
|
32357
|
+
const metaVars = asRecord2(match.meta_variables);
|
|
32358
|
+
if (metaVars && Object.keys(metaVars).length > 0) {
|
|
32359
|
+
Object.entries(metaVars).forEach(([name, value]) => {
|
|
32360
|
+
lines.push(` ${theme.fg("muted", `${name} =`)} ${formatValue(value)}`);
|
|
32361
|
+
});
|
|
32362
|
+
}
|
|
32363
|
+
const context = asRecords(match.context);
|
|
32364
|
+
context.forEach((ctxLine) => {
|
|
32365
|
+
const ctxNumber = asNumber(ctxLine.line) ?? 0;
|
|
32366
|
+
const prefix = ctxLine.is_match === true ? theme.fg("accent", ">") : theme.fg("muted", "|");
|
|
32367
|
+
lines.push(` ${prefix} ${ctxNumber}: ${asString(ctxLine.text) ?? ""}`);
|
|
32368
|
+
});
|
|
32369
|
+
});
|
|
32370
|
+
sections.push(lines.join(`
|
|
32371
|
+
`));
|
|
32393
32372
|
}
|
|
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);
|
|
32373
|
+
return sections;
|
|
32407
32374
|
}
|
|
32408
|
-
function
|
|
32409
|
-
const
|
|
32410
|
-
if (!
|
|
32411
|
-
return ["No
|
|
32412
|
-
const
|
|
32413
|
-
const
|
|
32414
|
-
const
|
|
32415
|
-
|
|
32375
|
+
function buildAstReplaceSections(payload, theme) {
|
|
32376
|
+
const response = asRecord2(payload);
|
|
32377
|
+
if (!response)
|
|
32378
|
+
return [theme.fg("muted", "No AST replace results.")];
|
|
32379
|
+
const files = asRecords(response.files);
|
|
32380
|
+
const totalReplacements = asNumber(response.total_replacements) ?? 0;
|
|
32381
|
+
const totalFiles = asNumber(response.total_files) ?? files.length;
|
|
32382
|
+
const filesWithMatches = asNumber(response.files_with_matches);
|
|
32383
|
+
const dryRun = response.dry_run === true;
|
|
32384
|
+
const headerParts = [
|
|
32385
|
+
dryRun ? theme.fg("warning", "[dry run]") : theme.fg("success", "[applied]"),
|
|
32386
|
+
`${totalReplacements} replacement${totalReplacements === 1 ? "" : "s"}`,
|
|
32387
|
+
`${totalFiles} file${totalFiles === 1 ? "" : "s"}`,
|
|
32388
|
+
filesWithMatches !== undefined ? theme.fg("muted", `${filesWithMatches} matched`) : undefined
|
|
32416
32389
|
];
|
|
32417
|
-
|
|
32390
|
+
const sections = [headerParts.filter(Boolean).join(" ")];
|
|
32391
|
+
if (files.length === 0) {
|
|
32392
|
+
sections.push(theme.fg("muted", "No files changed."));
|
|
32393
|
+
appendScopeSections(response, sections, theme);
|
|
32394
|
+
appendHintSection(response, sections, theme);
|
|
32418
32395
|
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);
|
|
32396
|
+
}
|
|
32397
|
+
files.forEach((fileResult) => {
|
|
32398
|
+
const file2 = shortenPath2(asString(fileResult.file) ?? "(unknown file)");
|
|
32399
|
+
const replacements = asNumber(fileResult.replacements) ?? 0;
|
|
32400
|
+
const error50 = asString(fileResult.error);
|
|
32401
|
+
const diff = asString(fileResult.diff);
|
|
32402
|
+
const lines = [
|
|
32403
|
+
`${theme.fg("accent", file2)} ${theme.fg("muted", `(${replacements} replacement${replacements === 1 ? "" : "s"})`)}`
|
|
32404
|
+
];
|
|
32405
|
+
if (error50) {
|
|
32406
|
+
lines.push(theme.fg("error", error50));
|
|
32407
|
+
} else if (diff) {
|
|
32408
|
+
const rendered = renderUnifiedDiff(diff);
|
|
32409
|
+
lines.push(rendered || theme.fg("muted", "No diff available."));
|
|
32410
|
+
} else {
|
|
32411
|
+
const backupId = asString(fileResult.backup_id);
|
|
32412
|
+
lines.push(backupId ? `${theme.fg("success", "saved")} ${theme.fg("muted", backupId)}` : theme.fg("success", "saved"));
|
|
32447
32413
|
}
|
|
32414
|
+
sections.push(lines.join(`
|
|
32415
|
+
`));
|
|
32448
32416
|
});
|
|
32417
|
+
return sections;
|
|
32449
32418
|
}
|
|
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);
|
|
32419
|
+
function renderAstCall(toolName, args, theme, context) {
|
|
32420
|
+
const lang = theme.fg("accent", args.lang);
|
|
32421
|
+
const summary = toolName === "ast_grep_replace" ? `${lang} ${theme.fg("toolOutput", `${args.pattern} → ${args.rewrite}`)}` : `${lang} ${theme.fg("toolOutput", args.pattern)}`;
|
|
32422
|
+
return renderToolCall(toolName === "ast_grep_replace" ? "ast replace" : "ast search", summary, theme, context);
|
|
32478
32423
|
}
|
|
32479
|
-
function
|
|
32424
|
+
function renderAstResult(toolName, result, theme, context) {
|
|
32480
32425
|
if (context.isError) {
|
|
32481
32426
|
return renderErrorResult(result, `${toolName} failed`, theme, context);
|
|
32482
32427
|
}
|
|
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);
|
|
32428
|
+
const payload = extractStructuredPayload(result);
|
|
32429
|
+
if (!payload) {
|
|
32430
|
+
const text = collectTextContent(result);
|
|
32431
|
+
return renderSections([text || theme.fg("muted", "No result.")], context);
|
|
32500
32432
|
}
|
|
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);
|
|
32433
|
+
const sections = toolName === "ast_grep_replace" ? buildAstReplaceSections(payload, theme) : buildAstSearchSections(payload, theme);
|
|
32434
|
+
return renderSections(sections, context);
|
|
32506
32435
|
}
|
|
32507
|
-
function
|
|
32508
|
-
if (surface.
|
|
32436
|
+
function registerAstTools(pi, ctx, surface) {
|
|
32437
|
+
if (surface.astSearch) {
|
|
32509
32438
|
pi.registerTool({
|
|
32510
|
-
name: "
|
|
32511
|
-
label: "
|
|
32512
|
-
description: "
|
|
32513
|
-
parameters:
|
|
32439
|
+
name: "ast_grep_search",
|
|
32440
|
+
label: "ast search",
|
|
32441
|
+
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.).",
|
|
32442
|
+
parameters: SearchParams,
|
|
32514
32443
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32444
|
+
const paths = await resolveAstPaths(extCtx, params.paths);
|
|
32445
|
+
await assertAstPathsPermission(extCtx, paths, "search", ctx.config.restrict_to_project_root ?? false);
|
|
32515
32446
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32516
|
-
const
|
|
32517
|
-
|
|
32518
|
-
|
|
32519
|
-
}
|
|
32520
|
-
|
|
32521
|
-
|
|
32522
|
-
|
|
32523
|
-
|
|
32524
|
-
|
|
32525
|
-
|
|
32526
|
-
|
|
32527
|
-
|
|
32528
|
-
|
|
32529
|
-
|
|
32530
|
-
|
|
32531
|
-
|
|
32532
|
-
|
|
32533
|
-
|
|
32534
|
-
|
|
32535
|
-
|
|
32536
|
-
renderCall(args, theme, context) {
|
|
32537
|
-
return renderFsCall("aft_delete", args, theme, context);
|
|
32538
|
-
},
|
|
32539
|
-
renderResult(result, _options, theme, context) {
|
|
32540
|
-
return renderFsResult("aft_delete", context.args, result, theme, context);
|
|
32541
|
-
}
|
|
32542
|
-
});
|
|
32447
|
+
const req = {
|
|
32448
|
+
pattern: params.pattern,
|
|
32449
|
+
lang: params.lang
|
|
32450
|
+
};
|
|
32451
|
+
if (!isEmptyParam(paths))
|
|
32452
|
+
req.paths = paths;
|
|
32453
|
+
if (!isEmptyParam(params.globs))
|
|
32454
|
+
req.globs = params.globs;
|
|
32455
|
+
if (params.contextLines !== undefined)
|
|
32456
|
+
req.context_lines = params.contextLines;
|
|
32457
|
+
const response = await callBridge(bridge, "ast_search", req, extCtx);
|
|
32458
|
+
return textResult(response.text ?? JSON.stringify(response));
|
|
32459
|
+
},
|
|
32460
|
+
renderCall(args, theme, context) {
|
|
32461
|
+
return renderAstCall("ast_grep_search", args, theme, context);
|
|
32462
|
+
},
|
|
32463
|
+
renderResult(result, _options, theme, context) {
|
|
32464
|
+
return renderAstResult("ast_grep_search", result, theme, context);
|
|
32465
|
+
}
|
|
32466
|
+
});
|
|
32543
32467
|
}
|
|
32544
|
-
if (surface.
|
|
32468
|
+
if (surface.astReplace) {
|
|
32545
32469
|
pi.registerTool({
|
|
32546
|
-
name: "
|
|
32547
|
-
label: "
|
|
32548
|
-
description: "
|
|
32549
|
-
parameters:
|
|
32470
|
+
name: "ast_grep_replace",
|
|
32471
|
+
label: "ast replace",
|
|
32472
|
+
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.",
|
|
32473
|
+
parameters: ReplaceParams,
|
|
32550
32474
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32475
|
+
const paths = await resolveAstPaths(extCtx, params.paths);
|
|
32476
|
+
await assertAstPathsPermission(extCtx, paths, params.dryRun === true ? "search" : "modify", ctx.config.restrict_to_project_root ?? false);
|
|
32551
32477
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32552
|
-
const
|
|
32553
|
-
|
|
32554
|
-
|
|
32555
|
-
|
|
32556
|
-
|
|
32478
|
+
const req = {
|
|
32479
|
+
pattern: params.pattern,
|
|
32480
|
+
rewrite: params.rewrite,
|
|
32481
|
+
lang: params.lang
|
|
32482
|
+
};
|
|
32483
|
+
if (!isEmptyParam(paths))
|
|
32484
|
+
req.paths = paths;
|
|
32485
|
+
if (!isEmptyParam(params.globs))
|
|
32486
|
+
req.globs = params.globs;
|
|
32487
|
+
req.dry_run = params.dryRun === true;
|
|
32488
|
+
const response = await callBridge(bridge, "ast_replace", req, extCtx);
|
|
32489
|
+
return textResult(response.text ?? JSON.stringify(response));
|
|
32557
32490
|
},
|
|
32558
32491
|
renderCall(args, theme, context) {
|
|
32559
|
-
return
|
|
32492
|
+
return renderAstCall("ast_grep_replace", args, theme, context);
|
|
32560
32493
|
},
|
|
32561
32494
|
renderResult(result, _options, theme, context) {
|
|
32562
|
-
return
|
|
32495
|
+
return renderAstResult("ast_grep_replace", result, theme, context);
|
|
32563
32496
|
}
|
|
32564
32497
|
});
|
|
32565
32498
|
}
|
|
32566
32499
|
}
|
|
32567
32500
|
|
|
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";
|
|
32501
|
+
// src/tools/bash.ts
|
|
32502
|
+
import * as fs3 from "node:fs/promises";
|
|
32575
32503
|
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
|
-
|
|
32504
|
+
import { Type as Type4 } from "typebox";
|
|
32505
|
+
var FOREGROUND_POLL_INTERVAL_MS = 100;
|
|
32506
|
+
var BASH_WAIT_POLL_INTERVAL_MS = 100;
|
|
32507
|
+
var DEFAULT_BASH_STATUS_WAIT_TIMEOUT_MS = 30000;
|
|
32508
|
+
var MAX_BASH_STATUS_WAIT_TIMEOUT_MS = 300000;
|
|
32509
|
+
var BASH_TRANSPORT_TIMEOUT_MS = 30000;
|
|
32510
|
+
var BashParams = Type4.Object({
|
|
32511
|
+
command: Type4.String({
|
|
32512
|
+
description: "Shell command to execute. Supports pipes, redirections, and shell syntax."
|
|
32513
|
+
}),
|
|
32514
|
+
timeout: optionalInt(1, Number.MAX_SAFE_INTEGER),
|
|
32515
|
+
workdir: Type4.Optional(Type4.String({
|
|
32516
|
+
description: "Working directory for command execution. Relative paths resolve against the project root. Defaults to the current session's working directory."
|
|
32517
|
+
})),
|
|
32518
|
+
description: Type4.Optional(Type4.String({
|
|
32519
|
+
description: "Human-readable description shown in UI logs. Helps users understand what the command does without reading shell syntax."
|
|
32520
|
+
})),
|
|
32521
|
+
background: Type4.Optional(Type4.Boolean({
|
|
32522
|
+
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."
|
|
32523
|
+
})),
|
|
32524
|
+
compressed: Type4.Optional(Type4.Boolean({
|
|
32525
|
+
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."
|
|
32526
|
+
})),
|
|
32527
|
+
pty: Type4.Optional(Type4.Boolean({
|
|
32528
|
+
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.'
|
|
32529
|
+
})),
|
|
32530
|
+
ptyRows: optionalInt(1, 60),
|
|
32531
|
+
ptyCols: optionalInt(1, 140)
|
|
32532
|
+
});
|
|
32533
|
+
var BashTaskParams = Type4.Object({
|
|
32534
|
+
task_id: Type4.String({
|
|
32535
|
+
description: "Background bash task id returned by bash({ background: true })."
|
|
32536
|
+
})
|
|
32537
|
+
});
|
|
32538
|
+
var BashStatusParams = Type4.Object({
|
|
32539
|
+
task_id: Type4.String({
|
|
32540
|
+
description: "Background bash task id returned by bash({ background: true })."
|
|
32541
|
+
}),
|
|
32542
|
+
output_mode: Type4.Optional(Type4.Union([Type4.Literal("screen"), Type4.Literal("raw"), Type4.Literal("both")], {
|
|
32543
|
+
description: "PTY output rendering mode. Defaults to screen for PTY tasks and preserves existing behavior for piped tasks when omitted."
|
|
32544
|
+
}))
|
|
32545
|
+
});
|
|
32546
|
+
var BashWatchParams = Type4.Object({
|
|
32547
|
+
task_id: Type4.String({
|
|
32548
|
+
description: "Background bash task id returned by bash({ background: true })."
|
|
32549
|
+
}),
|
|
32550
|
+
pattern: Type4.Optional(Type4.Union([Type4.String(), Type4.Object({ regex: Type4.String() })])),
|
|
32551
|
+
background: Type4.Optional(Type4.Boolean()),
|
|
32552
|
+
timeout_ms: optionalInt(1, MAX_BASH_STATUS_WAIT_TIMEOUT_MS),
|
|
32553
|
+
once: Type4.Optional(Type4.Boolean())
|
|
32554
|
+
});
|
|
32555
|
+
var BashWriteParams = Type4.Object({
|
|
32556
|
+
task_id: Type4.String({
|
|
32557
|
+
description: "Background PTY task id returned by bash({ pty: true, background: true })."
|
|
32558
|
+
}),
|
|
32559
|
+
input: Type4.Union([
|
|
32560
|
+
Type4.String(),
|
|
32561
|
+
Type4.Array(Type4.Union([
|
|
32562
|
+
Type4.String(),
|
|
32563
|
+
Type4.Object({
|
|
32564
|
+
key: Type4.String({
|
|
32565
|
+
description: "Named control key, e.g. 'esc', 'enter', 'up', 'ctrl-c'. Case-insensitive."
|
|
32566
|
+
})
|
|
32567
|
+
})
|
|
32568
|
+
]))
|
|
32569
|
+
], {
|
|
32570
|
+
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."
|
|
32571
|
+
})
|
|
32572
|
+
});
|
|
32573
|
+
function truncateToVisualLines(text, maxLines) {
|
|
32574
|
+
const lines = text.split(`
|
|
32587
32575
|
`);
|
|
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(`
|
|
32576
|
+
if (lines.length <= maxLines)
|
|
32577
|
+
return text;
|
|
32578
|
+
return lines.slice(-maxLines).join(`
|
|
32599
32579
|
`);
|
|
32600
|
-
if (raw[raw.length - 1] === "")
|
|
32601
|
-
raw.pop();
|
|
32602
|
-
if (part.added || part.removed) {
|
|
32603
|
-
if (firstChangedLine === undefined)
|
|
32604
|
-
firstChangedLine = newLineNum;
|
|
32605
|
-
for (const line of raw) {
|
|
32606
|
-
if (part.added) {
|
|
32607
|
-
output.push(`+${pad(newLineNum)} ${line}`);
|
|
32608
|
-
newLineNum++;
|
|
32609
|
-
} else {
|
|
32610
|
-
output.push(`-${pad(oldLineNum)} ${line}`);
|
|
32611
|
-
oldLineNum++;
|
|
32612
|
-
}
|
|
32613
|
-
}
|
|
32614
|
-
lastWasChange = true;
|
|
32615
|
-
continue;
|
|
32616
|
-
}
|
|
32617
|
-
const nextIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
|
32618
|
-
const hasLeading = lastWasChange;
|
|
32619
|
-
const hasTrailing = nextIsChange;
|
|
32620
|
-
if (hasLeading && hasTrailing) {
|
|
32621
|
-
if (raw.length <= contextLines * 2) {
|
|
32622
|
-
for (const line of raw) {
|
|
32623
|
-
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
32624
|
-
oldLineNum++;
|
|
32625
|
-
newLineNum++;
|
|
32626
|
-
}
|
|
32627
|
-
} else {
|
|
32628
|
-
for (const line of raw.slice(0, contextLines)) {
|
|
32629
|
-
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
32630
|
-
oldLineNum++;
|
|
32631
|
-
newLineNum++;
|
|
32632
|
-
}
|
|
32633
|
-
const skipped = raw.length - contextLines * 2;
|
|
32634
|
-
output.push(` ${blank} ...`);
|
|
32635
|
-
oldLineNum += skipped;
|
|
32636
|
-
newLineNum += skipped;
|
|
32637
|
-
for (const line of raw.slice(raw.length - contextLines)) {
|
|
32638
|
-
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
32639
|
-
oldLineNum++;
|
|
32640
|
-
newLineNum++;
|
|
32641
|
-
}
|
|
32642
|
-
}
|
|
32643
|
-
} else if (hasLeading) {
|
|
32644
|
-
const shown = raw.slice(0, contextLines);
|
|
32645
|
-
for (const line of shown) {
|
|
32646
|
-
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
32647
|
-
oldLineNum++;
|
|
32648
|
-
newLineNum++;
|
|
32649
|
-
}
|
|
32650
|
-
const skipped = raw.length - shown.length;
|
|
32651
|
-
if (skipped > 0) {
|
|
32652
|
-
output.push(` ${blank} ...`);
|
|
32653
|
-
oldLineNum += skipped;
|
|
32654
|
-
newLineNum += skipped;
|
|
32655
|
-
}
|
|
32656
|
-
} else if (hasTrailing) {
|
|
32657
|
-
const shownCount = Math.min(contextLines, raw.length);
|
|
32658
|
-
const shown = raw.slice(raw.length - shownCount);
|
|
32659
|
-
const skipped = raw.length - shown.length;
|
|
32660
|
-
if (skipped > 0) {
|
|
32661
|
-
output.push(` ${blank} ...`);
|
|
32662
|
-
oldLineNum += skipped;
|
|
32663
|
-
newLineNum += skipped;
|
|
32664
|
-
}
|
|
32665
|
-
for (const line of shown) {
|
|
32666
|
-
output.push(` ${pad(oldLineNum)} ${line}`);
|
|
32667
|
-
oldLineNum++;
|
|
32668
|
-
newLineNum++;
|
|
32669
|
-
}
|
|
32670
|
-
} else {
|
|
32671
|
-
oldLineNum += raw.length;
|
|
32672
|
-
newLineNum += raw.length;
|
|
32673
|
-
}
|
|
32674
|
-
lastWasChange = false;
|
|
32675
|
-
}
|
|
32676
|
-
return { diff: output.join(`
|
|
32677
|
-
`), firstChangedLine };
|
|
32678
32580
|
}
|
|
32679
|
-
|
|
32680
|
-
|
|
32681
|
-
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.";
|
|
32682
|
-
function diagnosticsOnEditDefault(ctx) {
|
|
32683
|
-
return ctx.config.lsp?.diagnostics_on_edit ?? false;
|
|
32581
|
+
function reuseText3(last) {
|
|
32582
|
+
return last instanceof Text3 ? last : new Text3("", 0, 0);
|
|
32684
32583
|
}
|
|
32685
|
-
function
|
|
32686
|
-
|
|
32687
|
-
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
32584
|
+
function reuseContainer3(last) {
|
|
32585
|
+
return last instanceof Container3 ? last : new Container3;
|
|
32688
32586
|
}
|
|
32689
|
-
function
|
|
32690
|
-
|
|
32691
|
-
|
|
32692
|
-
|
|
32693
|
-
return homedir9();
|
|
32694
|
-
if (path2.startsWith(`~${sep}`) || path2.startsWith("~/")) {
|
|
32695
|
-
return resolve3(homedir9(), path2.slice(2));
|
|
32587
|
+
function getBashSpawnHook(pi) {
|
|
32588
|
+
const api2 = pi;
|
|
32589
|
+
if (typeof api2.getHook === "function") {
|
|
32590
|
+
return api2.getHook("bashSpawn");
|
|
32696
32591
|
}
|
|
32697
|
-
return
|
|
32698
|
-
}
|
|
32699
|
-
function externalDirectoryPromptTimeoutMs() {
|
|
32700
|
-
const raw = process.env.AFT_PI_EXTERNAL_PROMPT_TIMEOUT_MS;
|
|
32701
|
-
if (raw === undefined)
|
|
32702
|
-
return 30000;
|
|
32703
|
-
const parsed = Number.parseInt(raw, 10);
|
|
32704
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : 30000;
|
|
32592
|
+
return api2.hooks?.bashSpawn;
|
|
32705
32593
|
}
|
|
32706
|
-
|
|
32707
|
-
|
|
32708
|
-
|
|
32709
|
-
|
|
32710
|
-
|
|
32711
|
-
|
|
32712
|
-
|
|
32713
|
-
|
|
32714
|
-
|
|
32715
|
-
|
|
32716
|
-
|
|
32717
|
-
|
|
32718
|
-
|
|
32719
|
-
|
|
32720
|
-
|
|
32721
|
-
|
|
32722
|
-
|
|
32723
|
-
|
|
32724
|
-
|
|
32725
|
-
|
|
32726
|
-
|
|
32727
|
-
|
|
32728
|
-
|
|
32729
|
-
|
|
32730
|
-
|
|
32731
|
-
|
|
32732
|
-
|
|
32594
|
+
function registerBashTool(pi, ctx) {
|
|
32595
|
+
const spawnHook = getBashSpawnHook(pi);
|
|
32596
|
+
pi.registerTool({
|
|
32597
|
+
name: "bash",
|
|
32598
|
+
label: "bash",
|
|
32599
|
+
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`.',
|
|
32600
|
+
promptSnippet: "Run shell commands (timeout in milliseconds; supports workdir, background tasks, compressed output, PTY mode)",
|
|
32601
|
+
promptGuidelines: [
|
|
32602
|
+
"Use bash only when a dedicated AFT tool is not a better fit.",
|
|
32603
|
+
"Set compressed: false when you need ANSI color codes in the output."
|
|
32604
|
+
],
|
|
32605
|
+
parameters: BashParams,
|
|
32606
|
+
async execute(_toolCallId, params, _signal, onUpdate, extCtx) {
|
|
32607
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32608
|
+
const foregroundWaitMs = resolveBashConfig(ctx.config).foreground_wait_window_ms;
|
|
32609
|
+
const timeout = coerceOptionalInt(params.timeout, "timeout", 1, Number.MAX_SAFE_INTEGER);
|
|
32610
|
+
const ptyRows = coerceOptionalInt(params.ptyRows, "ptyRows", 1, 60);
|
|
32611
|
+
const ptyCols = coerceOptionalInt(params.ptyCols, "ptyCols", 1, 140);
|
|
32612
|
+
const effectiveBackground = params.background === true || params.pty === true;
|
|
32613
|
+
let spawnContext = {
|
|
32614
|
+
command: params.command,
|
|
32615
|
+
cwd: params.workdir
|
|
32616
|
+
};
|
|
32617
|
+
if (spawnHook) {
|
|
32618
|
+
try {
|
|
32619
|
+
spawnContext = await spawnHook(spawnContext);
|
|
32620
|
+
} catch (hookErr) {
|
|
32621
|
+
throw new Error(`BashSpawnHook failed: ${hookErr instanceof Error ? hookErr.message : String(hookErr)}`);
|
|
32622
|
+
}
|
|
32623
|
+
}
|
|
32624
|
+
let streamed = "";
|
|
32625
|
+
const response = await callBridge(bridge, "bash", {
|
|
32626
|
+
command: spawnContext.command,
|
|
32627
|
+
timeout,
|
|
32628
|
+
workdir: spawnContext.cwd ?? params.workdir,
|
|
32629
|
+
env: spawnContext.env,
|
|
32630
|
+
description: params.description,
|
|
32631
|
+
background: effectiveBackground,
|
|
32632
|
+
notify_on_completion: effectiveBackground,
|
|
32633
|
+
compressed: params.compressed,
|
|
32634
|
+
pty: params.pty,
|
|
32635
|
+
pty_rows: ptyRows,
|
|
32636
|
+
pty_cols: ptyCols
|
|
32637
|
+
}, extCtx, {
|
|
32638
|
+
transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS,
|
|
32639
|
+
keepBridgeOnTimeout: true,
|
|
32640
|
+
onProgress: ({ text }) => {
|
|
32641
|
+
streamed += text;
|
|
32642
|
+
const displayText = truncateToVisualLines(streamed, 100);
|
|
32643
|
+
onUpdate?.(bashResult(displayText, { streaming: true }));
|
|
32644
|
+
}
|
|
32645
|
+
}).catch((err) => {
|
|
32646
|
+
if (err instanceof Error && err.message.includes("permission_required")) {
|
|
32647
|
+
throw new Error("Permission ask reached Pi adapter — this is a bug. Pi has no permission system.");
|
|
32648
|
+
}
|
|
32649
|
+
throw err;
|
|
32650
|
+
});
|
|
32651
|
+
if (response.success === false) {
|
|
32652
|
+
throw new Error(response.message ?? "bash failed");
|
|
32653
|
+
}
|
|
32654
|
+
const taskId = response.task_id;
|
|
32655
|
+
if (response.status === "running" && taskId) {
|
|
32656
|
+
if (effectiveBackground) {
|
|
32657
|
+
trackBgTask(resolveSessionId(extCtx), taskId);
|
|
32658
|
+
return bashResult(formatBackgroundLaunch(taskId, params.pty === true), {
|
|
32659
|
+
task_id: taskId
|
|
32660
|
+
});
|
|
32661
|
+
}
|
|
32662
|
+
const waitTimeoutMs = timeout !== undefined ? Math.min(timeout, foregroundWaitMs) : foregroundWaitMs;
|
|
32663
|
+
const startedAt = Date.now();
|
|
32664
|
+
while (true) {
|
|
32665
|
+
const status = await callBridge(bridge, "bash_status", { task_id: taskId }, extCtx);
|
|
32666
|
+
if (status.success === false) {
|
|
32667
|
+
throw new Error(status.message ?? "bash_status failed");
|
|
32668
|
+
}
|
|
32669
|
+
if (isTerminalStatus(status.status)) {
|
|
32670
|
+
return bashResult(withBashHints(formatForegroundResult(status), params.command), {
|
|
32671
|
+
exit_code: status.exit_code,
|
|
32672
|
+
duration_ms: status.duration_ms,
|
|
32673
|
+
truncated: status.output_truncated,
|
|
32674
|
+
output_path: status.output_path,
|
|
32675
|
+
task_id: taskId
|
|
32676
|
+
});
|
|
32677
|
+
}
|
|
32678
|
+
if (Date.now() - startedAt >= waitTimeoutMs) {
|
|
32679
|
+
const promoted = await callBridge(bridge, "bash_promote", { task_id: taskId }, extCtx);
|
|
32680
|
+
if (promoted.success === false) {
|
|
32681
|
+
throw new Error(promoted.message ?? "bash_promote failed");
|
|
32682
|
+
}
|
|
32683
|
+
trackBgTask(resolveSessionId(extCtx), taskId);
|
|
32684
|
+
return bashResult(formatPromotionMessage(taskId, params.timeout, foregroundWaitMs), {
|
|
32685
|
+
task_id: taskId
|
|
32686
|
+
});
|
|
32687
|
+
}
|
|
32688
|
+
await sleep(FOREGROUND_POLL_INTERVAL_MS);
|
|
32689
|
+
}
|
|
32690
|
+
}
|
|
32691
|
+
const details = {
|
|
32692
|
+
exit_code: response.exit_code,
|
|
32693
|
+
duration_ms: response.duration_ms,
|
|
32694
|
+
truncated: response.truncated,
|
|
32695
|
+
output_path: response.output_path,
|
|
32696
|
+
task_id: taskId
|
|
32697
|
+
};
|
|
32698
|
+
const output = response.output ?? "";
|
|
32699
|
+
return bashResult(withBashHints(output, params.command), details);
|
|
32700
|
+
},
|
|
32701
|
+
renderCall(args, theme, context) {
|
|
32702
|
+
return renderBashCall(args?.command, args?.description, theme, context);
|
|
32703
|
+
},
|
|
32704
|
+
renderResult(result, _options, theme, context) {
|
|
32705
|
+
return renderBashResult(result, theme, context);
|
|
32733
32706
|
}
|
|
32734
|
-
|
|
32735
|
-
|
|
32736
|
-
|
|
32737
|
-
|
|
32707
|
+
});
|
|
32708
|
+
pi.registerTool(createBashStatusTool(ctx));
|
|
32709
|
+
pi.registerTool(createBashWatchTool(ctx));
|
|
32710
|
+
pi.registerTool(createBashWriteTool(ctx));
|
|
32711
|
+
pi.registerTool(createBashKillTool(ctx));
|
|
32712
|
+
}
|
|
32713
|
+
function formatBackgroundLaunch(taskId, isPty) {
|
|
32714
|
+
if (isPty) {
|
|
32715
|
+
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.`;
|
|
32738
32716
|
}
|
|
32717
|
+
return `Background task started: ${taskId}. A completion reminder will be delivered automatically; don't poll bash_status.`;
|
|
32739
32718
|
}
|
|
32740
|
-
|
|
32741
|
-
|
|
32742
|
-
|
|
32743
|
-
|
|
32744
|
-
|
|
32745
|
-
|
|
32746
|
-
}
|
|
32747
|
-
|
|
32748
|
-
|
|
32749
|
-
|
|
32750
|
-
|
|
32751
|
-
|
|
32752
|
-
|
|
32753
|
-
|
|
32754
|
-
|
|
32755
|
-
|
|
32756
|
-
|
|
32757
|
-
|
|
32758
|
-
|
|
32759
|
-
|
|
32760
|
-
|
|
32761
|
-
|
|
32762
|
-
|
|
32763
|
-
|
|
32764
|
-
|
|
32765
|
-
|
|
32766
|
-
}
|
|
32767
|
-
|
|
32768
|
-
|
|
32769
|
-
|
|
32770
|
-
|
|
32771
|
-
|
|
32772
|
-
|
|
32773
|
-
|
|
32774
|
-
|
|
32775
|
-
}
|
|
32776
|
-
function
|
|
32777
|
-
|
|
32778
|
-
|
|
32779
|
-
|
|
32780
|
-
|
|
32781
|
-
|
|
32782
|
-
|
|
32783
|
-
|
|
32784
|
-
|
|
32785
|
-
|
|
32786
|
-
|
|
32787
|
-
|
|
32788
|
-
|
|
32789
|
-
|
|
32790
|
-
|
|
32791
|
-
|
|
32792
|
-
|
|
32793
|
-
|
|
32794
|
-
|
|
32795
|
-
|
|
32796
|
-
|
|
32719
|
+
function formatPromotionMessage(taskId, timeout, waitWindowMs) {
|
|
32720
|
+
const waited = timeout !== undefined ? Math.min(timeout, waitWindowMs) : waitWindowMs;
|
|
32721
|
+
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.`;
|
|
32722
|
+
}
|
|
32723
|
+
function formatSeconds(ms) {
|
|
32724
|
+
return `${Number((ms / 1000).toFixed(1))}s`;
|
|
32725
|
+
}
|
|
32726
|
+
function withBashHints(output, command) {
|
|
32727
|
+
let result = maybeAppendConflictsHint(output);
|
|
32728
|
+
result = maybeAppendGrepHint(result, command);
|
|
32729
|
+
return result;
|
|
32730
|
+
}
|
|
32731
|
+
function formatForegroundResult(data) {
|
|
32732
|
+
const output = data.output_preview ?? "";
|
|
32733
|
+
const outputPath = data.output_path;
|
|
32734
|
+
const truncated = data.output_truncated === true;
|
|
32735
|
+
const status = data.status;
|
|
32736
|
+
const exit = data.exit_code;
|
|
32737
|
+
let rendered = output;
|
|
32738
|
+
if (truncated && outputPath) {
|
|
32739
|
+
rendered += `
|
|
32740
|
+
[output truncated; full output at ${outputPath}]`;
|
|
32741
|
+
}
|
|
32742
|
+
if (status === "timed_out") {
|
|
32743
|
+
rendered += `
|
|
32744
|
+
[command timed out]`;
|
|
32745
|
+
}
|
|
32746
|
+
if (typeof exit === "number" && exit !== 0) {
|
|
32747
|
+
rendered += `
|
|
32748
|
+
[exit code: ${exit}]`;
|
|
32749
|
+
}
|
|
32750
|
+
return rendered;
|
|
32751
|
+
}
|
|
32752
|
+
function sleep(ms) {
|
|
32753
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
32754
|
+
}
|
|
32755
|
+
function createBashStatusTool(ctx) {
|
|
32756
|
+
return {
|
|
32757
|
+
name: "bash_status",
|
|
32758
|
+
label: "bash_status",
|
|
32759
|
+
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.",
|
|
32760
|
+
promptSnippet: "Inspect a background bash task by task_id",
|
|
32761
|
+
parameters: BashStatusParams,
|
|
32762
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32763
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32764
|
+
const data = await bashStatusSnapshot(bridge, extCtx, params.task_id, params.output_mode);
|
|
32765
|
+
const details = data;
|
|
32766
|
+
return bashStatusResult(await formatBashStatus(extCtx, params.task_id, details, params.output_mode), details);
|
|
32767
|
+
}
|
|
32768
|
+
};
|
|
32769
|
+
}
|
|
32770
|
+
function createBashWatchTool(ctx) {
|
|
32771
|
+
return {
|
|
32772
|
+
name: "bash_watch",
|
|
32773
|
+
label: "bash_watch",
|
|
32774
|
+
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.",
|
|
32775
|
+
promptSnippet: "Wait for or watch a background bash task",
|
|
32776
|
+
parameters: BashWatchParams,
|
|
32777
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32778
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32779
|
+
const waitFor = parseWaitPattern(params.pattern);
|
|
32780
|
+
if (params.background === true) {
|
|
32781
|
+
if (!waitFor) {
|
|
32782
|
+
throw new Error("invalid_request: Use auto-reminder; bash_watch without pattern in async mode is redundant");
|
|
32797
32783
|
}
|
|
32798
|
-
const
|
|
32799
|
-
|
|
32800
|
-
|
|
32801
|
-
|
|
32784
|
+
const notifyParams = {
|
|
32785
|
+
task_id: params.task_id,
|
|
32786
|
+
once: params.once !== false
|
|
32787
|
+
};
|
|
32788
|
+
if (waitFor.kind === "regex")
|
|
32789
|
+
notifyParams.regex = waitFor.source;
|
|
32790
|
+
else
|
|
32791
|
+
notifyParams.pattern = waitFor.value;
|
|
32792
|
+
const sessionId = resolveSessionId(extCtx);
|
|
32793
|
+
markExplicitControl(sessionId, params.task_id, false);
|
|
32794
|
+
let registered;
|
|
32795
|
+
try {
|
|
32796
|
+
registered = await callBridge(bridge, "bash_notify", notifyParams, extCtx);
|
|
32797
|
+
} catch (err) {
|
|
32798
|
+
unmarkExplicitControl(sessionId, params.task_id);
|
|
32799
|
+
throw err;
|
|
32802
32800
|
}
|
|
32803
|
-
|
|
32804
|
-
|
|
32805
|
-
|
|
32806
|
-
|
|
32807
|
-
|
|
32808
|
-
|
|
32801
|
+
if (registered.success === false) {
|
|
32802
|
+
unmarkExplicitControl(sessionId, params.task_id);
|
|
32803
|
+
const message = String(registered.message ?? "bash_notify failed");
|
|
32804
|
+
throw new Error(`${String(registered.code ?? "invalid_request")}: ${message}`);
|
|
32805
|
+
}
|
|
32806
|
+
markExplicitControl(sessionId, params.task_id);
|
|
32807
|
+
const watchDetails = { registered: true, watchId: registered.watch_id };
|
|
32808
|
+
return textResult(`Watch registered: ${registered.watch_id} on task ${params.task_id}
|
|
32809
|
+
A notification will fire when the pattern matches or the task exits.`, watchDetails);
|
|
32810
|
+
}
|
|
32811
|
+
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));
|
|
32812
|
+
const text = await formatBashStatus(extCtx, params.task_id, data, undefined);
|
|
32813
|
+
return textResult(text, data);
|
|
32814
|
+
}
|
|
32815
|
+
};
|
|
32816
|
+
}
|
|
32817
|
+
function createBashWriteTool(ctx) {
|
|
32818
|
+
return {
|
|
32819
|
+
name: "bash_write",
|
|
32820
|
+
label: "bash_write",
|
|
32821
|
+
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).",
|
|
32822
|
+
promptSnippet: "Write keystrokes/input to a PTY bash task",
|
|
32823
|
+
parameters: BashWriteParams,
|
|
32824
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32825
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32826
|
+
const data = await callBridge(bridge, "bash_write", { task_id: params.task_id, input: params.input }, extCtx);
|
|
32827
|
+
return textResult(JSON.stringify({ bytes_written: data.bytes_written }, null, 2), data);
|
|
32828
|
+
}
|
|
32829
|
+
};
|
|
32830
|
+
}
|
|
32831
|
+
function createBashKillTool(ctx) {
|
|
32832
|
+
return {
|
|
32833
|
+
name: "bash_kill",
|
|
32834
|
+
label: "bash_kill",
|
|
32835
|
+
description: "Terminate a running background bash task spawned with bash({ background: true }).",
|
|
32836
|
+
promptSnippet: "Kill a background bash task by task_id",
|
|
32837
|
+
parameters: BashTaskParams,
|
|
32838
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32839
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32840
|
+
const data = await callBridge(bridge, "bash_kill", { task_id: params.task_id }, extCtx);
|
|
32841
|
+
if (data.success === false) {
|
|
32842
|
+
throw new Error(data.message ?? "bash_kill failed");
|
|
32809
32843
|
}
|
|
32810
|
-
|
|
32811
|
-
|
|
32812
|
-
|
|
32813
|
-
|
|
32814
|
-
name: "write",
|
|
32815
|
-
label: "write",
|
|
32816
|
-
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.",
|
|
32817
|
-
promptSnippet: "Create or overwrite files (uses filePath; auto-formats; diagnostics follow lsp.diagnostics_on_edit unless overridden)",
|
|
32818
|
-
promptGuidelines: ["Use write only for new files or complete rewrites."],
|
|
32819
|
-
parameters: WriteParams,
|
|
32820
|
-
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
32821
|
-
await assertExternalDirectoryPermission(extCtx, params.filePath, "modify", {
|
|
32822
|
-
restrictToProjectRoot: surface.restrictToProjectRoot
|
|
32823
|
-
});
|
|
32824
|
-
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
32825
|
-
const response = await callBridge(bridge, "write", {
|
|
32826
|
-
file: params.filePath,
|
|
32827
|
-
content: params.content,
|
|
32828
|
-
diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
|
|
32829
|
-
include_diff_content: true
|
|
32830
|
-
}, extCtx);
|
|
32831
|
-
return buildMutationResult(params.filePath, response);
|
|
32832
|
-
},
|
|
32833
|
-
renderCall(args, theme, context) {
|
|
32834
|
-
return renderMutationCall("write", args?.filePath, theme, context);
|
|
32835
|
-
},
|
|
32836
|
-
renderResult(result, _options, theme, context) {
|
|
32837
|
-
return renderMutationResult(result, theme, context);
|
|
32844
|
+
await disposePtyTerminal(ptyCacheKey(extCtx, params.task_id));
|
|
32845
|
+
const details = data;
|
|
32846
|
+
if (details.kill_signaled === true) {
|
|
32847
|
+
return bashKillResult(`Task ${params.task_id}: kill_signaled`, details);
|
|
32838
32848
|
}
|
|
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
|
-
|
|
32849
|
+
return bashKillResult(`Task ${params.task_id}: ${details.status}`, details);
|
|
32850
|
+
}
|
|
32851
|
+
};
|
|
32852
|
+
}
|
|
32853
|
+
function bashResult(output, details) {
|
|
32854
|
+
return {
|
|
32855
|
+
content: [{ type: "text", text: output }],
|
|
32856
|
+
details: {
|
|
32857
|
+
exit_code: details.exit_code,
|
|
32858
|
+
duration_ms: details.duration_ms,
|
|
32859
|
+
truncated: details.truncated,
|
|
32860
|
+
output_path: details.output_path,
|
|
32861
|
+
task_id: details.task_id,
|
|
32862
|
+
bg_completions: details.bg_completions
|
|
32863
|
+
}
|
|
32864
|
+
};
|
|
32865
|
+
}
|
|
32866
|
+
function bashStatusResult(output, details) {
|
|
32867
|
+
return {
|
|
32868
|
+
content: [{ type: "text", text: output }],
|
|
32869
|
+
details
|
|
32870
|
+
};
|
|
32871
|
+
}
|
|
32872
|
+
function bashKillResult(output, details) {
|
|
32873
|
+
return {
|
|
32874
|
+
content: [{ type: "text", text: output }],
|
|
32875
|
+
details
|
|
32876
|
+
};
|
|
32877
|
+
}
|
|
32878
|
+
async function bashStatusSnapshot(bridge, extCtx, taskId, outputMode, options) {
|
|
32879
|
+
return await callBridge(bridge, "bash_status", { task_id: taskId, output_mode: outputMode }, extCtx, options);
|
|
32880
|
+
}
|
|
32881
|
+
async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFor, waitForExit, effectiveWaitMs) {
|
|
32882
|
+
const startedAt = Date.now();
|
|
32883
|
+
const deadline = startedAt + effectiveWaitMs;
|
|
32884
|
+
let spillCursor = { output: 0, stderr: 0, combined: 0 };
|
|
32885
|
+
let scanText = "";
|
|
32886
|
+
let scanBaseOffset = 0;
|
|
32887
|
+
const bridgeOptions = {
|
|
32888
|
+
keepBridgeOnTimeout: true,
|
|
32889
|
+
transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS
|
|
32890
|
+
};
|
|
32891
|
+
const sessionId = resolveSessionId(extCtx);
|
|
32892
|
+
if (waitForExit)
|
|
32893
|
+
markTaskWaiting(sessionId, taskId);
|
|
32894
|
+
let sawTerminal = false;
|
|
32895
|
+
try {
|
|
32896
|
+
while (true) {
|
|
32897
|
+
const data = await bashStatusSnapshot(bridge, extCtx, taskId, outputMode, bridgeOptions);
|
|
32898
|
+
const terminal = isTerminalStatus(data.status);
|
|
32899
|
+
if (waitFor) {
|
|
32900
|
+
const scan = await readNewTaskOutput(extCtx, taskId, data, spillCursor);
|
|
32901
|
+
if (scan) {
|
|
32902
|
+
spillCursor = scan.nextCursor;
|
|
32903
|
+
if (scanText.length === 0)
|
|
32904
|
+
scanBaseOffset = scan.baseOffset;
|
|
32905
|
+
scanText += scan.text;
|
|
32906
|
+
const match = findWaitMatch(scanText, waitFor);
|
|
32907
|
+
if (match) {
|
|
32908
|
+
if (waitForExit && terminal) {
|
|
32909
|
+
sawTerminal = true;
|
|
32910
|
+
consumeBgCompletion(sessionId, taskId);
|
|
32911
|
+
await markBgCompletionDelivered({ ctx, directory: extCtx.cwd, sessionID: sessionId }, taskId);
|
|
32912
|
+
}
|
|
32913
|
+
return withWaited(data, {
|
|
32914
|
+
reason: "matched",
|
|
32915
|
+
elapsed_ms: Date.now() - startedAt,
|
|
32916
|
+
match: match.text,
|
|
32917
|
+
match_offset: scanBaseOffset + Buffer.byteLength(scanText.slice(0, match.index), "utf8")
|
|
32918
|
+
});
|
|
32919
|
+
}
|
|
32868
32920
|
}
|
|
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
32921
|
}
|
|
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);
|
|
32922
|
+
if (terminal) {
|
|
32923
|
+
if (waitForExit) {
|
|
32924
|
+
sawTerminal = true;
|
|
32925
|
+
consumeBgCompletion(sessionId, taskId);
|
|
32926
|
+
await markBgCompletionDelivered({ ctx, directory: extCtx.cwd, sessionID: sessionId }, taskId);
|
|
32908
32927
|
}
|
|
32909
|
-
|
|
32910
|
-
req.include = splitIncludeGlobs(params.include);
|
|
32911
|
-
if (params.caseSensitive !== undefined)
|
|
32912
|
-
req.case_sensitive = params.caseSensitive;
|
|
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);
|
|
32928
|
+
return withWaited(data, { reason: "exited", elapsed_ms: Date.now() - startedAt });
|
|
32919
32929
|
}
|
|
32920
|
-
|
|
32930
|
+
if (Date.now() >= deadline) {
|
|
32931
|
+
return withWaited(data, { reason: "timeout", elapsed_ms: Date.now() - startedAt });
|
|
32932
|
+
}
|
|
32933
|
+
await sleep(Math.min(BASH_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
|
|
32934
|
+
}
|
|
32935
|
+
} finally {
|
|
32936
|
+
if (waitForExit && !sawTerminal)
|
|
32937
|
+
unmarkTaskWaiting(sessionId, taskId);
|
|
32938
|
+
if (waitFor) {
|
|
32939
|
+
await disposePtyTerminal(watchPtyCacheKey(extCtx, taskId));
|
|
32940
|
+
}
|
|
32921
32941
|
}
|
|
32922
32942
|
}
|
|
32923
|
-
function
|
|
32924
|
-
const
|
|
32925
|
-
|
|
32926
|
-
|
|
32927
|
-
|
|
32928
|
-
|
|
32929
|
-
|
|
32930
|
-
|
|
32931
|
-
|
|
32932
|
-
|
|
32933
|
-
|
|
32934
|
-
|
|
32935
|
-
|
|
32936
|
-
|
|
32937
|
-
|
|
32938
|
-
|
|
32939
|
-
firstChangedLine = piDiff.firstChangedLine;
|
|
32940
|
-
}
|
|
32941
|
-
const summaryHeader = replacements !== undefined ? `Edited ${filePath} (+${additions}/-${deletions}, ${replacements} replacement${replacements === 1 ? "" : "s"})` : `Wrote ${filePath} (+${additions}/-${deletions})`;
|
|
32942
|
-
let text = summaryHeader;
|
|
32943
|
-
if (noOp) {
|
|
32944
|
-
text += `
|
|
32945
|
-
|
|
32946
|
-
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.`;
|
|
32947
|
-
}
|
|
32948
|
-
const skipNote = formatSkipReasonNote(formatSkippedReason);
|
|
32949
|
-
if (skipNote)
|
|
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)}`;
|
|
32943
|
+
async function readNewTaskOutput(extCtx, taskId, data, cursor) {
|
|
32944
|
+
const outputPath = data.output_path;
|
|
32945
|
+
if (data.mode === "pty") {
|
|
32946
|
+
if (!outputPath)
|
|
32947
|
+
return;
|
|
32948
|
+
const { rows, cols } = ptyDimensions(data);
|
|
32949
|
+
const state = await getOrCreatePtyTerminal(watchPtyCacheKey(extCtx, taskId), outputPath, rows, cols);
|
|
32950
|
+
const baseOffset = state.offset;
|
|
32951
|
+
const bytes = await readPtyBytes(state);
|
|
32952
|
+
if (bytes.length === 0)
|
|
32953
|
+
return;
|
|
32954
|
+
return {
|
|
32955
|
+
text: bytes.toString("utf8"),
|
|
32956
|
+
baseOffset,
|
|
32957
|
+
nextCursor: { output: state.offset, stderr: 0, combined: state.offset }
|
|
32958
|
+
};
|
|
32963
32959
|
}
|
|
32960
|
+
const stderrPath = data.stderr_path;
|
|
32961
|
+
if (!outputPath && !stderrPath)
|
|
32962
|
+
return;
|
|
32963
|
+
const stdoutBytes = outputPath ? await readFileBytesFrom(outputPath, cursor.output) : Buffer.alloc(0);
|
|
32964
|
+
const stderrBytes = stderrPath ? await readFileBytesFrom(stderrPath, cursor.stderr) : Buffer.alloc(0);
|
|
32965
|
+
const bytesRead = stdoutBytes.length + stderrBytes.length;
|
|
32966
|
+
if (bytesRead === 0)
|
|
32967
|
+
return;
|
|
32964
32968
|
return {
|
|
32965
|
-
|
|
32966
|
-
|
|
32967
|
-
|
|
32968
|
-
|
|
32969
|
-
|
|
32970
|
-
|
|
32971
|
-
replacements,
|
|
32972
|
-
diagnostics,
|
|
32973
|
-
truncated: truncated || undefined,
|
|
32974
|
-
formatted,
|
|
32975
|
-
formatSkippedReason,
|
|
32976
|
-
noOp: noOp || undefined
|
|
32969
|
+
text: Buffer.concat([stdoutBytes, stderrBytes]).toString("utf8"),
|
|
32970
|
+
baseOffset: cursor.combined,
|
|
32971
|
+
nextCursor: {
|
|
32972
|
+
output: cursor.output + stdoutBytes.length,
|
|
32973
|
+
stderr: cursor.stderr + stderrBytes.length,
|
|
32974
|
+
combined: cursor.combined + bytesRead
|
|
32977
32975
|
}
|
|
32978
32976
|
};
|
|
32979
32977
|
}
|
|
32980
|
-
function
|
|
32981
|
-
|
|
32982
|
-
|
|
32983
|
-
|
|
32984
|
-
|
|
32985
|
-
|
|
32986
|
-
|
|
32987
|
-
}
|
|
32988
|
-
|
|
32989
|
-
|
|
32990
|
-
|
|
32991
|
-
|
|
32992
|
-
|
|
32993
|
-
|
|
32994
|
-
|
|
32995
|
-
|
|
32996
|
-
case "error":
|
|
32997
|
-
return "Note: formatter exited with an unrecognized error; file written unformatted.";
|
|
32998
|
-
default:
|
|
32978
|
+
async function readFileBytesFrom(outputPath, cursor) {
|
|
32979
|
+
const handle = await fs3.open(outputPath, "r");
|
|
32980
|
+
try {
|
|
32981
|
+
const chunks = [];
|
|
32982
|
+
let offset = cursor;
|
|
32983
|
+
while (true) {
|
|
32984
|
+
const buffer2 = Buffer.allocUnsafe(64 * 1024);
|
|
32985
|
+
const { bytesRead } = await handle.read(buffer2, 0, buffer2.length, offset);
|
|
32986
|
+
if (bytesRead === 0)
|
|
32987
|
+
break;
|
|
32988
|
+
chunks.push(Buffer.from(buffer2.subarray(0, bytesRead)));
|
|
32989
|
+
offset += bytesRead;
|
|
32990
|
+
}
|
|
32991
|
+
return Buffer.concat(chunks);
|
|
32992
|
+
} finally {
|
|
32993
|
+
await handle.close().catch(() => {
|
|
32999
32994
|
return;
|
|
32995
|
+
});
|
|
33000
32996
|
}
|
|
33001
32997
|
}
|
|
33002
|
-
function
|
|
33003
|
-
|
|
33004
|
-
return
|
|
33005
|
-
|
|
33006
|
-
|
|
33007
|
-
|
|
33008
|
-
|
|
33009
|
-
|
|
33010
|
-
|
|
33011
|
-
|
|
33012
|
-
|
|
33013
|
-
|
|
33014
|
-
|
|
33015
|
-
|
|
33016
|
-
return JSON.stringify(diagnostics, null, 2);
|
|
32998
|
+
function parseWaitPattern(value) {
|
|
32999
|
+
if (typeof value === "string")
|
|
33000
|
+
return { kind: "substring", value };
|
|
33001
|
+
if (isRegexWaitObject(value))
|
|
33002
|
+
return { kind: "regex", value: new RegExp(value.regex), source: value.regex };
|
|
33003
|
+
return;
|
|
33004
|
+
}
|
|
33005
|
+
function isRegexWaitObject(value) {
|
|
33006
|
+
return typeof value === "object" && value !== null && "regex" in value && typeof value.regex === "string";
|
|
33007
|
+
}
|
|
33008
|
+
function findWaitMatch(text, pattern) {
|
|
33009
|
+
if (pattern.kind === "substring") {
|
|
33010
|
+
const index = text.indexOf(pattern.value);
|
|
33011
|
+
return index >= 0 ? { text: pattern.value, index } : undefined;
|
|
33017
33012
|
}
|
|
33013
|
+
pattern.value.lastIndex = 0;
|
|
33014
|
+
const match = pattern.value.exec(text);
|
|
33015
|
+
return match ? { text: match[0], index: match.index } : undefined;
|
|
33018
33016
|
}
|
|
33019
|
-
function
|
|
33020
|
-
return
|
|
33017
|
+
function withWaited(data, waited) {
|
|
33018
|
+
return { ...data, waited };
|
|
33021
33019
|
}
|
|
33022
|
-
function
|
|
33023
|
-
|
|
33020
|
+
function formatWaitSummary(waited, details) {
|
|
33021
|
+
if (waited.reason === "matched") {
|
|
33022
|
+
return `Waited ${waited.elapsed_ms}ms; matched ${JSON.stringify(waited.match ?? "")} at offset ${waited.match_offset ?? 0}.`;
|
|
33023
|
+
}
|
|
33024
|
+
if (waited.reason === "timeout") {
|
|
33025
|
+
return `Waited ${waited.elapsed_ms}ms; timeout reached without match.`;
|
|
33026
|
+
}
|
|
33027
|
+
const exit = typeof details.exit_code === "number" ? `, exit ${details.exit_code}` : "";
|
|
33028
|
+
return `Waited ${waited.elapsed_ms}ms; task exited (${details.status}${exit}).`;
|
|
33024
33029
|
}
|
|
33025
|
-
function
|
|
33030
|
+
async function formatBashStatus(extCtx, taskId, details, requestedOutputMode) {
|
|
33031
|
+
const exit = typeof details.exit_code === "number" ? ` (exit ${details.exit_code})` : "";
|
|
33032
|
+
const dur = typeof details.duration_ms === "number" ? ` ${Math.round(details.duration_ms / 1000)}s` : "";
|
|
33033
|
+
let text = `Task ${taskId}: ${details.status}${exit}${dur}`;
|
|
33034
|
+
if (details.waited)
|
|
33035
|
+
text += `
|
|
33036
|
+
${formatWaitSummary(details.waited, details)}`;
|
|
33037
|
+
if (details.mode === "pty") {
|
|
33038
|
+
text += await formatPtyStatus(extCtx, taskId, details, requestedOutputMode);
|
|
33039
|
+
} else {
|
|
33040
|
+
if (isTerminalStatus(details.status) && details.output_preview) {
|
|
33041
|
+
text += `
|
|
33042
|
+
${details.output_preview.slice(0, 2000)}`;
|
|
33043
|
+
}
|
|
33044
|
+
if (!isTerminalStatus(details.status)) {
|
|
33045
|
+
text += `
|
|
33046
|
+
A completion reminder will be delivered automatically; don't poll.`;
|
|
33047
|
+
}
|
|
33048
|
+
}
|
|
33049
|
+
return text;
|
|
33050
|
+
}
|
|
33051
|
+
async function formatPtyStatus(extCtx, taskId, details, requestedOutputMode) {
|
|
33052
|
+
if (!details.output_path)
|
|
33053
|
+
return `
|
|
33054
|
+
[PTY output path unavailable]`;
|
|
33055
|
+
const key = ptyCacheKey(extCtx, taskId);
|
|
33056
|
+
const { rows, cols } = ptyDimensions(details);
|
|
33057
|
+
const state = await getOrCreatePtyTerminal(key, details.output_path, rows, cols);
|
|
33058
|
+
const raw = await readPtyBytes(state);
|
|
33059
|
+
const outputMode = requestedOutputMode ?? "screen";
|
|
33060
|
+
let suffix = "";
|
|
33061
|
+
if (outputMode === "raw") {
|
|
33062
|
+
suffix = raw.length > 0 ? `
|
|
33063
|
+
${raw.toString("utf8")}` : "";
|
|
33064
|
+
} else if (outputMode === "both") {
|
|
33065
|
+
suffix = `
|
|
33066
|
+
${JSON.stringify({ screen: renderScreen(state, rows, cols), raw: raw.toString("utf8") }, null, 2)}`;
|
|
33067
|
+
} else {
|
|
33068
|
+
const screen = renderScreen(state, rows, cols);
|
|
33069
|
+
suffix = screen ? `
|
|
33070
|
+
${screen}` : "";
|
|
33071
|
+
}
|
|
33072
|
+
if (!isTerminalStatus(details.status)) {
|
|
33073
|
+
suffix += `
|
|
33074
|
+
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.`;
|
|
33075
|
+
} else {
|
|
33076
|
+
await disposePtyTerminal(key);
|
|
33077
|
+
}
|
|
33078
|
+
return suffix;
|
|
33079
|
+
}
|
|
33080
|
+
function ptyDimensions(data) {
|
|
33081
|
+
const rows = typeof data.pty_rows === "number" ? data.pty_rows : 24;
|
|
33082
|
+
const cols = typeof data.pty_cols === "number" ? data.pty_cols : 80;
|
|
33083
|
+
return { rows, cols };
|
|
33084
|
+
}
|
|
33085
|
+
function ptyCacheKey(extCtx, taskId) {
|
|
33086
|
+
return `${extCtx.cwd}::${resolveSessionId(extCtx) ?? "__default__"}::${taskId}`;
|
|
33087
|
+
}
|
|
33088
|
+
function watchPtyCacheKey(extCtx, taskId) {
|
|
33089
|
+
return `${ptyCacheKey(extCtx, taskId)}::watch`;
|
|
33090
|
+
}
|
|
33091
|
+
function isTerminalStatus(status) {
|
|
33092
|
+
return status === "completed" || status === "failed" || status === "killed" || status === "timed_out";
|
|
33093
|
+
}
|
|
33094
|
+
function renderBashCall(command, description, theme, context) {
|
|
33026
33095
|
const text = reuseText3(context.lastComponent);
|
|
33027
|
-
const
|
|
33028
|
-
text.setText(`${theme.fg("toolTitle", theme.bold(
|
|
33096
|
+
const display = description ?? (command ? shortenCommand(command) : "...");
|
|
33097
|
+
text.setText(`${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg("accent", display)}`);
|
|
33029
33098
|
return text;
|
|
33030
33099
|
}
|
|
33031
|
-
function
|
|
33032
|
-
if (context.isError) {
|
|
33033
|
-
const errorText = result.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join(`
|
|
33034
|
-
`).trim();
|
|
33035
|
-
const text = reuseText3(context.lastComponent);
|
|
33036
|
-
text.setText(`
|
|
33037
|
-
${theme.fg("error", errorText || "
|
|
33038
|
-
return text;
|
|
33039
|
-
}
|
|
33040
|
-
const details = result.details;
|
|
33041
|
-
const
|
|
33042
|
-
|
|
33043
|
-
|
|
33044
|
-
|
|
33045
|
-
|
|
33046
|
-
|
|
33047
|
-
|
|
33048
|
-
|
|
33049
|
-
|
|
33050
|
-
|
|
33051
|
-
|
|
33100
|
+
function renderBashResult(result, theme, context) {
|
|
33101
|
+
if (context.isError) {
|
|
33102
|
+
const errorText = result.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join(`
|
|
33103
|
+
`).trim();
|
|
33104
|
+
const text = reuseText3(context.lastComponent);
|
|
33105
|
+
text.setText(`
|
|
33106
|
+
${theme.fg("error", errorText || "bash failed")}`);
|
|
33107
|
+
return text;
|
|
33108
|
+
}
|
|
33109
|
+
const details = result.details;
|
|
33110
|
+
const exitCode = details?.exit_code;
|
|
33111
|
+
const bgCompletions = details?.bg_completions ?? [];
|
|
33112
|
+
const container = reuseContainer3(context.lastComponent);
|
|
33113
|
+
container.clear();
|
|
33114
|
+
container.addChild(new Spacer3(1));
|
|
33115
|
+
const rawOutput = result.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join(`
|
|
33116
|
+
`).trim();
|
|
33117
|
+
if (rawOutput) {
|
|
33118
|
+
const lines = rawOutput.split(`
|
|
33119
|
+
`);
|
|
33120
|
+
const preview = lines.length > 25 ? `... (${lines.length - 25} lines omitted)
|
|
33121
|
+
${lines.slice(-25).join(`
|
|
33122
|
+
`)}` : rawOutput;
|
|
33123
|
+
container.addChild(new Text3(preview, 1, 0));
|
|
33124
|
+
container.addChild(new Spacer3(1));
|
|
33125
|
+
}
|
|
33126
|
+
if (exitCode !== undefined) {
|
|
33127
|
+
const exitColor = exitCode === 0 ? "success" : "error";
|
|
33128
|
+
const exitText = theme.fg(exitColor, `exit ${exitCode}`);
|
|
33129
|
+
container.addChild(new Text3(exitText, 1, 0));
|
|
33130
|
+
}
|
|
33131
|
+
if (bgCompletions.length > 0) {
|
|
33132
|
+
container.addChild(new Spacer3(1));
|
|
33133
|
+
for (const bg of bgCompletions) {
|
|
33134
|
+
const cmdPreview = bg.command ? bg.command.slice(0, 60) : "unknown command";
|
|
33135
|
+
const suffix = (bg.command?.length ?? 0) > 60 ? "..." : "";
|
|
33136
|
+
const exitInfo = bg.exit_code !== undefined ? `exit ${bg.exit_code}` : bg.status;
|
|
33137
|
+
const statusColor = bg.status === "completed" && bg.exit_code === 0 ? "success" : "warning";
|
|
33138
|
+
const line = theme.fg(statusColor, `Background task ${bg.task_id} completed (${exitInfo}): ${cmdPreview}${suffix}`);
|
|
33139
|
+
container.addChild(new Text3(line, 1, 0));
|
|
33140
|
+
}
|
|
33141
|
+
}
|
|
33142
|
+
if (details?.duration_ms !== undefined) {
|
|
33143
|
+
container.addChild(new Spacer3(1));
|
|
33144
|
+
const durationText = theme.fg("muted", `${details.duration_ms}ms`);
|
|
33145
|
+
container.addChild(new Text3(durationText, 1, 0));
|
|
33146
|
+
}
|
|
33147
|
+
if (details?.truncated) {
|
|
33148
|
+
container.addChild(new Spacer3(1));
|
|
33149
|
+
const truncText = theme.fg("warning", "(output truncated)");
|
|
33150
|
+
container.addChild(new Text3(truncText, 1, 0));
|
|
33151
|
+
}
|
|
33152
|
+
return container;
|
|
33153
|
+
}
|
|
33154
|
+
function shortenCommand(command) {
|
|
33155
|
+
if (command.length <= 60)
|
|
33156
|
+
return command;
|
|
33157
|
+
return `${command.slice(0, 57)}...`;
|
|
33158
|
+
}
|
|
33159
|
+
|
|
33160
|
+
// src/tools/conflicts.ts
|
|
33161
|
+
import { Type as Type5 } from "typebox";
|
|
33162
|
+
var ConflictsParams = Type5.Object({
|
|
33163
|
+
path: Type5.Optional(Type5.String({
|
|
33164
|
+
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."
|
|
33165
|
+
}))
|
|
33166
|
+
});
|
|
33167
|
+
function renderConflictCall(theme, context) {
|
|
33168
|
+
return renderToolCall("conflicts", undefined, theme, context);
|
|
33169
|
+
}
|
|
33170
|
+
function buildConflictSections(text) {
|
|
33171
|
+
const trimmed = text.trim();
|
|
33172
|
+
if (!trimmed)
|
|
33173
|
+
return ["No merge conflicts found."];
|
|
33174
|
+
const [header, ...rest] = trimmed.split(/\n\n+/);
|
|
33175
|
+
const match = header.match(/^(\d+)\s+files?,\s+(\d+)\s+conflicts?/i);
|
|
33176
|
+
const sections = [
|
|
33177
|
+
match ? `${match[1]} conflicted file${match[1] === "1" ? "" : "s"} · ${match[2]} region${match[2] === "1" ? "" : "s"}` : header
|
|
33178
|
+
];
|
|
33179
|
+
if (rest.length === 0)
|
|
33180
|
+
return sections;
|
|
33181
|
+
sections.push(...rest.map((section) => section.trim()).filter(Boolean));
|
|
33182
|
+
return sections;
|
|
33183
|
+
}
|
|
33184
|
+
function renderConflictResult(text, theme, context) {
|
|
33185
|
+
const sections = buildConflictSections(text).map((section, index) => index === 0 ? theme.fg("warning", section) : section);
|
|
33186
|
+
return renderSections(sections, context);
|
|
33187
|
+
}
|
|
33188
|
+
function renderConflictToolResult(result, theme, context) {
|
|
33189
|
+
if (context.isError)
|
|
33190
|
+
return renderErrorResult(result, "conflicts failed", theme, context);
|
|
33191
|
+
return renderConflictResult(collectTextContent(result), theme, context);
|
|
33192
|
+
}
|
|
33193
|
+
function registerConflictsTool(pi, ctx) {
|
|
33194
|
+
pi.registerTool({
|
|
33195
|
+
name: "aft_conflicts",
|
|
33196
|
+
label: "conflicts",
|
|
33197
|
+
description: "Show all git merge conflicts across the repository — returns line-numbered conflict regions with context for every conflicted file in a single call.",
|
|
33198
|
+
parameters: ConflictsParams,
|
|
33199
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33200
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
33201
|
+
const reqParams = {};
|
|
33202
|
+
const path2 = params?.path;
|
|
33203
|
+
if (typeof path2 === "string" && path2.trim() !== "")
|
|
33204
|
+
reqParams.path = path2;
|
|
33205
|
+
const response = await callBridge(bridge, "git_conflicts", reqParams, extCtx);
|
|
33206
|
+
return textResult(response.text ?? JSON.stringify(response, null, 2));
|
|
33207
|
+
},
|
|
33208
|
+
renderCall(_args, theme, context) {
|
|
33209
|
+
return renderConflictCall(theme, context);
|
|
33210
|
+
},
|
|
33211
|
+
renderResult(result, _options, theme, context) {
|
|
33212
|
+
return renderConflictToolResult(result, theme, context);
|
|
33052
33213
|
}
|
|
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;
|
|
33214
|
+
});
|
|
33068
33215
|
}
|
|
33069
|
-
|
|
33070
|
-
|
|
33071
|
-
|
|
33072
|
-
|
|
33073
|
-
|
|
33074
|
-
|
|
33075
|
-
|
|
33076
|
-
|
|
33216
|
+
|
|
33217
|
+
// src/tools/fs.ts
|
|
33218
|
+
import { Type as Type6 } from "typebox";
|
|
33219
|
+
var DeleteParams = Type6.Object({
|
|
33220
|
+
files: Type6.Array(Type6.String(), {
|
|
33221
|
+
description: "Paths to delete (one or more). May include directories when recursive=true.",
|
|
33222
|
+
minItems: 1
|
|
33223
|
+
}),
|
|
33224
|
+
recursive: Type6.Optional(Type6.Boolean({
|
|
33225
|
+
description: "Required to delete a directory and its contents. Defaults to false; passing a directory without this returns an error."
|
|
33226
|
+
}))
|
|
33227
|
+
});
|
|
33228
|
+
var MoveParams = Type6.Object({
|
|
33229
|
+
filePath: Type6.String({
|
|
33230
|
+
description: "Source file path to move (absolute or relative to project root)"
|
|
33231
|
+
}),
|
|
33232
|
+
destination: Type6.String({
|
|
33233
|
+
description: "Destination file path (absolute or relative to project root)"
|
|
33234
|
+
})
|
|
33235
|
+
});
|
|
33236
|
+
function renderFsCall(toolName, args, theme, context) {
|
|
33237
|
+
if (toolName === "aft_delete") {
|
|
33238
|
+
const files = args.files;
|
|
33239
|
+
const summary = files.length === 1 ? accentPath(theme, files[0]) : `${theme.fg("accent", String(files.length))} ${theme.fg("muted", "files")}`;
|
|
33240
|
+
return renderToolCall("delete", summary, theme, context);
|
|
33077
33241
|
}
|
|
33242
|
+
const moveArgs = args;
|
|
33243
|
+
return renderToolCall("move", `${accentPath(theme, moveArgs.filePath)} ${theme.fg("muted", "→")} ${accentPath(theme, moveArgs.destination)}`, theme, context);
|
|
33078
33244
|
}
|
|
33079
|
-
function
|
|
33080
|
-
|
|
33081
|
-
|
|
33082
|
-
|
|
33083
|
-
|
|
33084
|
-
|
|
33085
|
-
|
|
33086
|
-
|
|
33087
|
-
|
|
33245
|
+
function renderFsResult(toolName, args, result, theme, context) {
|
|
33246
|
+
if (context.isError) {
|
|
33247
|
+
return renderErrorResult(result, `${toolName} failed`, theme, context);
|
|
33248
|
+
}
|
|
33249
|
+
if (toolName === "aft_delete") {
|
|
33250
|
+
const files = args.files;
|
|
33251
|
+
const data = result?.details ?? {};
|
|
33252
|
+
const deletedPaths = data.deleted ?? files;
|
|
33253
|
+
const skipped = data.skipped_files ?? [];
|
|
33254
|
+
const lines = [];
|
|
33255
|
+
for (const entry of deletedPaths) {
|
|
33256
|
+
lines.push(`${theme.fg("success", "✓ deleted")} ${theme.fg("accent", shortenPath2(entry))}`);
|
|
33088
33257
|
}
|
|
33089
|
-
|
|
33090
|
-
|
|
33091
|
-
depth--;
|
|
33092
|
-
buf += ch;
|
|
33093
|
-
continue;
|
|
33258
|
+
for (const entry of skipped) {
|
|
33259
|
+
lines.push(`${theme.fg("error", "✗ skipped")} ${theme.fg("accent", shortenPath2(entry.file))} ${theme.fg("muted", `(${entry.reason})`)}`);
|
|
33094
33260
|
}
|
|
33095
|
-
if (
|
|
33096
|
-
|
|
33097
|
-
if (trimmed.length > 0)
|
|
33098
|
-
out.push(trimmed);
|
|
33099
|
-
buf = "";
|
|
33100
|
-
continue;
|
|
33261
|
+
if (lines.length === 0) {
|
|
33262
|
+
lines.push(theme.fg("muted", "(no files deleted)"));
|
|
33101
33263
|
}
|
|
33102
|
-
|
|
33264
|
+
return renderSections([lines.join(`
|
|
33265
|
+
`)], context);
|
|
33103
33266
|
}
|
|
33104
|
-
const
|
|
33105
|
-
|
|
33106
|
-
|
|
33107
|
-
|
|
33267
|
+
const moveArgs = args;
|
|
33268
|
+
return renderSections([
|
|
33269
|
+
`${theme.fg("success", "✓ moved")} ${theme.fg("accent", shortenPath2(moveArgs.filePath))}`,
|
|
33270
|
+
`${theme.fg("muted", "to")} ${theme.fg("accent", shortenPath2(moveArgs.destination))}`
|
|
33271
|
+
], context);
|
|
33108
33272
|
}
|
|
33109
|
-
function
|
|
33110
|
-
if (
|
|
33111
|
-
|
|
33112
|
-
|
|
33113
|
-
|
|
33114
|
-
|
|
33115
|
-
|
|
33116
|
-
|
|
33117
|
-
|
|
33118
|
-
|
|
33273
|
+
function registerFsTools(pi, ctx, surface) {
|
|
33274
|
+
if (surface.delete) {
|
|
33275
|
+
pi.registerTool({
|
|
33276
|
+
name: "aft_delete",
|
|
33277
|
+
label: "delete",
|
|
33278
|
+
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.",
|
|
33279
|
+
parameters: DeleteParams,
|
|
33280
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33281
|
+
const files = await Promise.all(params.files.map((file2) => resolvePathArg(extCtx.cwd, file2)));
|
|
33282
|
+
const checked = new Set;
|
|
33283
|
+
for (const file2 of files) {
|
|
33284
|
+
if (checked.has(file2))
|
|
33285
|
+
continue;
|
|
33286
|
+
checked.add(file2);
|
|
33287
|
+
await assertExternalDirectoryPermission(extCtx, file2, "modify", {
|
|
33288
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
33289
|
+
});
|
|
33290
|
+
}
|
|
33291
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
33292
|
+
const response = await callBridge(bridge, "delete_file", {
|
|
33293
|
+
files,
|
|
33294
|
+
recursive: params.recursive === true
|
|
33295
|
+
}, extCtx);
|
|
33296
|
+
const deletedEntries = response.deleted ?? [];
|
|
33297
|
+
const skipped = response.skipped_files ?? [];
|
|
33298
|
+
const deleted = deletedEntries.map((entry) => entry.file);
|
|
33299
|
+
if (deleted.length === 0 && skipped.length > 0) {
|
|
33300
|
+
throw new Error(`delete failed for all ${skipped.length} file(s):
|
|
33301
|
+
` + skipped.map((entry) => ` ${entry.file}: ${entry.reason}`).join(`
|
|
33302
|
+
`));
|
|
33303
|
+
}
|
|
33304
|
+
const summary = deleted.length === 1 && skipped.length === 0 ? `Deleted ${deleted[0]}` : `Deleted ${deleted.length}/${params.files.length} file(s)`;
|
|
33305
|
+
return textResult(summary, {
|
|
33306
|
+
success: true,
|
|
33307
|
+
complete: skipped.length === 0,
|
|
33308
|
+
deleted,
|
|
33309
|
+
skipped_files: skipped
|
|
33310
|
+
});
|
|
33311
|
+
},
|
|
33312
|
+
renderCall(args, theme, context) {
|
|
33313
|
+
return renderFsCall("aft_delete", args, theme, context);
|
|
33314
|
+
},
|
|
33315
|
+
renderResult(result, _options, theme, context) {
|
|
33316
|
+
return renderFsResult("aft_delete", context.args, result, theme, context);
|
|
33317
|
+
}
|
|
33318
|
+
});
|
|
33319
|
+
}
|
|
33320
|
+
if (surface.move) {
|
|
33321
|
+
pi.registerTool({
|
|
33322
|
+
name: "aft_move",
|
|
33323
|
+
label: "move",
|
|
33324
|
+
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.",
|
|
33325
|
+
parameters: MoveParams,
|
|
33326
|
+
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33327
|
+
const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
|
|
33328
|
+
const destination = await resolvePathArg(extCtx.cwd, params.destination);
|
|
33329
|
+
const checked = new Set([filePath, destination]);
|
|
33330
|
+
for (const file2 of checked) {
|
|
33331
|
+
await assertExternalDirectoryPermission(extCtx, file2, "modify", {
|
|
33332
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
33333
|
+
});
|
|
33334
|
+
}
|
|
33335
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
33336
|
+
const response = await callBridge(bridge, "move_file", {
|
|
33337
|
+
file: filePath,
|
|
33338
|
+
destination
|
|
33339
|
+
}, extCtx);
|
|
33340
|
+
return textResult(`Moved ${params.filePath} → ${params.destination}`, response);
|
|
33341
|
+
},
|
|
33342
|
+
renderCall(args, theme, context) {
|
|
33343
|
+
return renderFsCall("aft_move", args, theme, context);
|
|
33344
|
+
},
|
|
33345
|
+
renderResult(result, _options, theme, context) {
|
|
33346
|
+
return renderFsResult("aft_move", context.args, result, theme, context);
|
|
33347
|
+
}
|
|
33348
|
+
});
|
|
33119
33349
|
}
|
|
33120
|
-
return `
|
|
33121
|
-
(Showing lines ${startLine}-${endLine} of ${totalLines}. Use offset/limit to read other sections.)`;
|
|
33122
33350
|
}
|
|
33123
33351
|
|
|
33124
33352
|
// src/tools/imports.ts
|
|
@@ -33128,8 +33356,20 @@ var ImportParams = Type7.Object({
|
|
|
33128
33356
|
op: StringEnum2(["add", "remove", "organize"], { description: "Import operation" }),
|
|
33129
33357
|
filePath: Type7.String({ description: "Path to the file (absolute or relative to project root)" }),
|
|
33130
33358
|
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
|
-
|
|
33359
|
+
names: Type7.Optional(Type7.Array(Type7.String(), {
|
|
33360
|
+
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']"
|
|
33361
|
+
})),
|
|
33362
|
+
defaultImport: Type7.Optional(Type7.String({ description: "Default import name, ES only (e.g. 'React')" })),
|
|
33363
|
+
namespace: Type7.Optional(Type7.String({
|
|
33364
|
+
description: "Namespace binding: `import * as ns from 'mod'` (ES), `* as N from \"./X.sol\"` (Solidity)"
|
|
33365
|
+
})),
|
|
33366
|
+
alias: Type7.Optional(Type7.String({ description: 'Whole-module alias. Solidity: `import "./X.sol" as X`' })),
|
|
33367
|
+
modifiers: Type7.Optional(Type7.Array(Type7.String(), {
|
|
33368
|
+
description: "Statement-level modifiers, language-validated: Java/C# 'static', C# 'global'/'unsafe', Java/Kotlin/Scala 'wildcard', Swift '@testable'"
|
|
33369
|
+
})),
|
|
33370
|
+
importKind: Type7.Optional(Type7.String({
|
|
33371
|
+
description: "Symbol-kind import: PHP 'function'/'const', Swift 'struct'/'class'/'enum', Scala 'given'"
|
|
33372
|
+
})),
|
|
33133
33373
|
removeName: Type7.Optional(Type7.String({ description: "Named import to remove; omit to remove entire import" })),
|
|
33134
33374
|
typeOnly: Type7.Optional(Type7.Boolean({ description: "Type-only import (TS only)" })),
|
|
33135
33375
|
validate: Type7.Optional(StringEnum2(["syntax", "full"], {
|
|
@@ -33150,16 +33390,19 @@ function buildImportSections(args, payload, theme) {
|
|
|
33150
33390
|
];
|
|
33151
33391
|
}
|
|
33152
33392
|
if (args.op === "add") {
|
|
33153
|
-
const
|
|
33393
|
+
const moduleName2 = asString(response.module) ?? args.module ?? "(module)";
|
|
33154
33394
|
const status = response.already_present === true ? theme.fg("warning", "already present") : theme.fg("success", "added");
|
|
33155
33395
|
return [
|
|
33156
|
-
`${status} ${theme.fg("accent",
|
|
33396
|
+
`${status} ${theme.fg("accent", moduleName2)}`,
|
|
33157
33397
|
`${theme.fg("muted", "file")} ${theme.fg("accent", asString(response.file) ?? args.filePath)}`,
|
|
33158
33398
|
`${theme.fg("muted", "group")} ${asString(response.group) ?? "—"}`
|
|
33159
33399
|
];
|
|
33160
33400
|
}
|
|
33401
|
+
const moduleName = asString(response.module) ?? args.module ?? "(module)";
|
|
33402
|
+
const didRemove = response.removed !== false;
|
|
33403
|
+
const removeStatus = didRemove ? `${theme.fg("success", "removed")} ${theme.fg("accent", moduleName)}` : `${theme.fg("warning", "not present")} ${theme.fg("accent", moduleName)}`;
|
|
33161
33404
|
return [
|
|
33162
|
-
|
|
33405
|
+
removeStatus,
|
|
33163
33406
|
`${theme.fg("muted", "file")} ${theme.fg("accent", asString(response.file) ?? args.filePath)}`,
|
|
33164
33407
|
args.removeName ? `${theme.fg("muted", "name")} ${args.removeName}` : `${theme.fg("muted", "scope")} entire import`
|
|
33165
33408
|
];
|
|
@@ -33182,25 +33425,37 @@ function registerImportTools(pi, ctx) {
|
|
|
33182
33425
|
pi.registerTool({
|
|
33183
33426
|
name: "aft_import",
|
|
33184
33427
|
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.",
|
|
33428
|
+
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
33429
|
parameters: ImportParams,
|
|
33187
33430
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33188
|
-
if ((params.op === "add" || params.op === "remove") &&
|
|
33431
|
+
if ((params.op === "add" || params.op === "remove") && isEmptyParam(params.module)) {
|
|
33189
33432
|
throw new Error(`op='${params.op}' requires 'module'`);
|
|
33190
33433
|
}
|
|
33434
|
+
const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
|
|
33435
|
+
await assertExternalDirectoryPermission(extCtx, filePath, "modify", {
|
|
33436
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
33437
|
+
});
|
|
33191
33438
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
33192
33439
|
const commandMap = {
|
|
33193
33440
|
add: "add_import",
|
|
33194
33441
|
remove: "remove_import",
|
|
33195
33442
|
organize: "organize_imports"
|
|
33196
33443
|
};
|
|
33197
|
-
const req = { file:
|
|
33444
|
+
const req = { file: filePath };
|
|
33198
33445
|
if (params.module !== undefined)
|
|
33199
33446
|
req.module = params.module;
|
|
33200
33447
|
if (params.names !== undefined)
|
|
33201
33448
|
req.names = params.names;
|
|
33202
33449
|
if (params.defaultImport !== undefined)
|
|
33203
33450
|
req.default_import = params.defaultImport;
|
|
33451
|
+
if (params.namespace !== undefined)
|
|
33452
|
+
req.namespace = params.namespace;
|
|
33453
|
+
if (params.alias !== undefined)
|
|
33454
|
+
req.alias = params.alias;
|
|
33455
|
+
if (params.modifiers !== undefined)
|
|
33456
|
+
req.modifiers = params.modifiers;
|
|
33457
|
+
if (params.importKind !== undefined)
|
|
33458
|
+
req.import_kind = params.importKind;
|
|
33204
33459
|
if (params.removeName !== undefined)
|
|
33205
33460
|
req.name = params.removeName;
|
|
33206
33461
|
if (params.typeOnly !== undefined)
|
|
@@ -33243,6 +33498,22 @@ var lastTier2TriggerAtByBridge = new WeakMap;
|
|
|
33243
33498
|
function normalizeStringOrArray(value) {
|
|
33244
33499
|
return isEmptyParam(value) ? undefined : value;
|
|
33245
33500
|
}
|
|
33501
|
+
async function resolveAndGateScope(extCtx, ctx, scope) {
|
|
33502
|
+
if (scope === undefined)
|
|
33503
|
+
return;
|
|
33504
|
+
const values = Array.isArray(scope) ? scope : [scope];
|
|
33505
|
+
const resolved = await Promise.all(values.filter((value) => typeof value === "string" && value.length > 0).map((value) => resolvePathArg(extCtx.cwd, value)));
|
|
33506
|
+
const checked = new Set;
|
|
33507
|
+
for (const target of resolved) {
|
|
33508
|
+
if (checked.has(target))
|
|
33509
|
+
continue;
|
|
33510
|
+
checked.add(target);
|
|
33511
|
+
await assertExternalDirectoryPermission(extCtx, target, "read", {
|
|
33512
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
33513
|
+
});
|
|
33514
|
+
}
|
|
33515
|
+
return Array.isArray(scope) ? resolved : resolved[0];
|
|
33516
|
+
}
|
|
33246
33517
|
function validateOptionalTopK(value) {
|
|
33247
33518
|
if (value === undefined || value === null || value === "")
|
|
33248
33519
|
return;
|
|
@@ -33323,6 +33594,45 @@ function tier2SummaryPart(summary, key, label) {
|
|
|
33323
33594
|
const status = asString(section?.status);
|
|
33324
33595
|
return `${label} ${status ?? "unavailable"}`;
|
|
33325
33596
|
}
|
|
33597
|
+
function shortDupOccurrence(entry) {
|
|
33598
|
+
const [path2] = entry.split(":");
|
|
33599
|
+
return path2?.split("/").pop() ?? entry;
|
|
33600
|
+
}
|
|
33601
|
+
function tier2TopPreview(summary, theme) {
|
|
33602
|
+
const lines = [];
|
|
33603
|
+
const dup = asRecord2(summary?.duplicates);
|
|
33604
|
+
const dupTop = Array.isArray(dup?.top) ? dup.top : [];
|
|
33605
|
+
for (const group of dupTop) {
|
|
33606
|
+
const record2 = asRecord2(group);
|
|
33607
|
+
const files = Array.isArray(record2?.files) ? record2.files : [];
|
|
33608
|
+
const cost = asNumber(record2?.cost);
|
|
33609
|
+
if (files.length < 2)
|
|
33610
|
+
continue;
|
|
33611
|
+
const a = shortDupOccurrence(String(files[0]));
|
|
33612
|
+
const b = shortDupOccurrence(String(files[1]));
|
|
33613
|
+
lines.push(` dup ${a} ↔ ${b}${cost !== undefined ? ` (${cost})` : ""}`);
|
|
33614
|
+
}
|
|
33615
|
+
for (const [key, label] of [
|
|
33616
|
+
["dead_code", "dead"],
|
|
33617
|
+
["unused_exports", "unused"]
|
|
33618
|
+
]) {
|
|
33619
|
+
const section = asRecord2(summary?.[key]);
|
|
33620
|
+
const top = Array.isArray(section?.top) ? section.top : [];
|
|
33621
|
+
for (const item of top) {
|
|
33622
|
+
const record2 = asRecord2(item);
|
|
33623
|
+
const file2 = asString(record2?.file);
|
|
33624
|
+
const symbol2 = asString(record2?.symbol);
|
|
33625
|
+
if (!file2 || !symbol2)
|
|
33626
|
+
continue;
|
|
33627
|
+
lines.push(` ${label} ${symbol2} (${file2.split("/").pop()})`);
|
|
33628
|
+
}
|
|
33629
|
+
}
|
|
33630
|
+
if (lines.length === 0)
|
|
33631
|
+
return;
|
|
33632
|
+
return `${theme.fg("muted", "top findings:")}
|
|
33633
|
+
${lines.join(`
|
|
33634
|
+
`)}`;
|
|
33635
|
+
}
|
|
33326
33636
|
function tier2RefreshCategories(response) {
|
|
33327
33637
|
const scannerState = asRecord2(response.scanner_state);
|
|
33328
33638
|
const categories = new Set;
|
|
@@ -33391,6 +33701,9 @@ function buildInspectSections(payload, theme) {
|
|
|
33391
33701
|
if (stale > 0 || pending > 0) {
|
|
33392
33702
|
sections.push(theme.fg("warning", `scanner state: ${stale} stale · ${pending} pending`));
|
|
33393
33703
|
}
|
|
33704
|
+
const topPreview = tier2TopPreview(summary, theme);
|
|
33705
|
+
if (topPreview)
|
|
33706
|
+
sections.push(topPreview);
|
|
33394
33707
|
const details = asRecord2(response.details);
|
|
33395
33708
|
if (details) {
|
|
33396
33709
|
const names = Object.keys(details);
|
|
@@ -33426,7 +33739,7 @@ function registerInspectTool(pi, ctx) {
|
|
|
33426
33739
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33427
33740
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
33428
33741
|
const sections = normalizeStringOrArray(params.sections);
|
|
33429
|
-
const scope = normalizeStringOrArray(params.scope);
|
|
33742
|
+
const scope = await resolveAndGateScope(extCtx, ctx, normalizeStringOrArray(params.scope));
|
|
33430
33743
|
const topK = validateOptionalTopK(params.topK);
|
|
33431
33744
|
const response = await callBridge(bridge, "inspect", { sections, scope, topK }, extCtx);
|
|
33432
33745
|
runPendingTier2Categories(bridge, tier2RefreshCategories(response), extCtx);
|
|
@@ -33468,7 +33781,7 @@ function treeLine(depth, text) {
|
|
|
33468
33781
|
}
|
|
33469
33782
|
function renderCallTreeNode(node, depth, lines) {
|
|
33470
33783
|
const name = asString(node.name) ?? "(unknown)";
|
|
33471
|
-
const file2 =
|
|
33784
|
+
const file2 = shortenPath2(asString(node.file) ?? "(unknown file)");
|
|
33472
33785
|
const line = asNumber(node.line);
|
|
33473
33786
|
lines.push(treeLine(depth, `${name} ${line !== undefined ? `[${file2}:${line}]` : `[${file2}]`}`));
|
|
33474
33787
|
asRecords(node.children).forEach((child) => {
|
|
@@ -33487,7 +33800,7 @@ function renderTracePath(path2, index, lines) {
|
|
|
33487
33800
|
lines.push(`Path ${index + 1}`);
|
|
33488
33801
|
asRecords(path2.hops).forEach((hop, hopIndex) => {
|
|
33489
33802
|
const symbol2 = asString(hop.symbol) ?? "(unknown)";
|
|
33490
|
-
const file2 =
|
|
33803
|
+
const file2 = shortenPath2(asString(hop.file) ?? "(unknown file)");
|
|
33491
33804
|
const line = asNumber(hop.line);
|
|
33492
33805
|
const entry = hop.is_entry_point === true ? " [entry]" : "";
|
|
33493
33806
|
lines.push(treeLine(hopIndex + 1, `${symbol2}${entry} ${line !== undefined ? `[${file2}:${line}]` : `[${file2}]`}`));
|
|
@@ -33512,7 +33825,7 @@ function buildNavigateSections(args, payload, theme) {
|
|
|
33512
33825
|
`${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
33826
|
];
|
|
33514
33827
|
groups.forEach((group) => {
|
|
33515
|
-
const file2 =
|
|
33828
|
+
const file2 = shortenPath2(asString(group.file) ?? "(unknown file)");
|
|
33516
33829
|
const lines = [theme.fg("accent", file2)];
|
|
33517
33830
|
asRecords(group.callers).forEach((caller) => {
|
|
33518
33831
|
lines.push(` ↳ ${asString(caller.symbol) ?? "(unknown)"} ${theme.fg("muted", `line ${asNumber(caller.line) ?? "?"}`)}`);
|
|
@@ -33533,7 +33846,7 @@ function buildNavigateSections(args, payload, theme) {
|
|
|
33533
33846
|
const lines = [theme.fg("success", `${path2.length} hop${path2.length === 1 ? "" : "s"}`)];
|
|
33534
33847
|
path2.forEach((hop, index) => {
|
|
33535
33848
|
const symbol2 = asString(hop.symbol) ?? "(unknown)";
|
|
33536
|
-
const file2 =
|
|
33849
|
+
const file2 = shortenPath2(asString(hop.file) ?? "(unknown file)");
|
|
33537
33850
|
const line = asNumber(hop.line);
|
|
33538
33851
|
lines.push(treeLine(index + 1, `${symbol2} ${line !== undefined ? `[${file2}:${line}]` : `[${file2}]`}`));
|
|
33539
33852
|
});
|
|
@@ -33564,7 +33877,7 @@ function buildNavigateSections(args, payload, theme) {
|
|
|
33564
33877
|
if (callers.length === 0)
|
|
33565
33878
|
sections2.push(theme.fg("muted", "No impacted callers found."));
|
|
33566
33879
|
callers.forEach((caller) => {
|
|
33567
|
-
const file2 =
|
|
33880
|
+
const file2 = shortenPath2(asString(caller.caller_file) ?? "(unknown file)");
|
|
33568
33881
|
const symbol2 = asString(caller.caller_symbol) ?? "(unknown)";
|
|
33569
33882
|
const line = asNumber(caller.line) ?? 0;
|
|
33570
33883
|
const entry = caller.is_entry_point === true ? ` ${theme.fg("warning", "[entry]")}` : "";
|
|
@@ -33587,7 +33900,7 @@ function buildNavigateSections(args, payload, theme) {
|
|
|
33587
33900
|
if (hops.length === 0)
|
|
33588
33901
|
sections.push(theme.fg("muted", "No data-flow hops found."));
|
|
33589
33902
|
hops.forEach((hop, index) => {
|
|
33590
|
-
const file2 =
|
|
33903
|
+
const file2 = shortenPath2(asString(hop.file) ?? "(unknown file)");
|
|
33591
33904
|
const symbol2 = asString(hop.symbol) ?? "(unknown)";
|
|
33592
33905
|
const variable = asString(hop.variable) ?? "(unknown)";
|
|
33593
33906
|
const line = asNumber(hop.line) ?? 0;
|
|
@@ -33617,27 +33930,44 @@ function registerNavigateTool(pi, ctx) {
|
|
|
33617
33930
|
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
33931
|
parameters: navigateParamsSchema(),
|
|
33619
33932
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33620
|
-
if (params.
|
|
33933
|
+
if (isEmptyParam(params.filePath)) {
|
|
33934
|
+
throw new Error(`op='${params.op}' requires a \`filePath\``);
|
|
33935
|
+
}
|
|
33936
|
+
if (isEmptyParam(params.symbol)) {
|
|
33937
|
+
throw new Error(`op='${params.op}' requires a \`symbol\``);
|
|
33938
|
+
}
|
|
33939
|
+
if (params.op === "trace_data" && isEmptyParam(params.expression)) {
|
|
33621
33940
|
throw new Error("op='trace_data' requires an `expression`");
|
|
33622
33941
|
}
|
|
33623
|
-
if (params.op === "trace_to_symbol" &&
|
|
33942
|
+
if (params.op === "trace_to_symbol" && isEmptyParam(params.toSymbol)) {
|
|
33624
33943
|
throw new Error("op='trace_to_symbol' requires a `toSymbol`");
|
|
33625
33944
|
}
|
|
33945
|
+
const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
|
|
33946
|
+
const toFile = !isEmptyParam(params.toFile) ? await resolvePathArg(extCtx.cwd, params.toFile) : undefined;
|
|
33947
|
+
const checked = new Set;
|
|
33948
|
+
for (const target of [filePath, ...toFile !== undefined ? [toFile] : []]) {
|
|
33949
|
+
if (checked.has(target))
|
|
33950
|
+
continue;
|
|
33951
|
+
checked.add(target);
|
|
33952
|
+
await assertExternalDirectoryPermission(extCtx, target, "read", {
|
|
33953
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
33954
|
+
});
|
|
33955
|
+
}
|
|
33626
33956
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
33627
33957
|
const req = {
|
|
33628
33958
|
op: params.op,
|
|
33629
|
-
file:
|
|
33959
|
+
file: filePath,
|
|
33630
33960
|
symbol: params.symbol
|
|
33631
33961
|
};
|
|
33632
33962
|
const depth = coerceOptionalInt(params.depth, "depth", 1, Number.MAX_SAFE_INTEGER);
|
|
33633
33963
|
if (depth !== undefined)
|
|
33634
33964
|
req.depth = depth;
|
|
33635
|
-
if (params.expression
|
|
33965
|
+
if (!isEmptyParam(params.expression))
|
|
33636
33966
|
req.expression = params.expression;
|
|
33637
|
-
if (params.toSymbol
|
|
33967
|
+
if (!isEmptyParam(params.toSymbol))
|
|
33638
33968
|
req.toSymbol = params.toSymbol;
|
|
33639
|
-
if (
|
|
33640
|
-
req.toFile =
|
|
33969
|
+
if (toFile !== undefined)
|
|
33970
|
+
req.toFile = toFile;
|
|
33641
33971
|
const response = await callBridge(bridge, params.op, req, extCtx);
|
|
33642
33972
|
return textResult(JSON.stringify(response, null, 2));
|
|
33643
33973
|
},
|
|
@@ -33652,7 +33982,6 @@ function registerNavigateTool(pi, ctx) {
|
|
|
33652
33982
|
|
|
33653
33983
|
// src/tools/reading.ts
|
|
33654
33984
|
import { stat as stat2 } from "node:fs/promises";
|
|
33655
|
-
import { resolve as resolve4 } from "node:path";
|
|
33656
33985
|
import { Type as Type10 } from "typebox";
|
|
33657
33986
|
var OutlineParams = Type10.Object({
|
|
33658
33987
|
target: Type10.Union([Type10.String(), Type10.Array(Type10.String())], {
|
|
@@ -33677,11 +34006,26 @@ var ZoomParams = Type10.Object({
|
|
|
33677
34006
|
targets: Type10.Optional(Type10.Union([ZoomTarget, Type10.Array(ZoomTarget)], {
|
|
33678
34007
|
description: "Cross-file batch: `{ filePath, symbol }` or an array of them. Mutually exclusive with filePath/url/symbols."
|
|
33679
34008
|
})),
|
|
33680
|
-
contextLines: Type10.Optional(Type10.Number({ description: "Lines of context before/after (default: 3)" }))
|
|
34009
|
+
contextLines: Type10.Optional(Type10.Number({ description: "Lines of context before/after (default: 3)" })),
|
|
34010
|
+
callgraph: Type10.Optional(Type10.Boolean({
|
|
34011
|
+
description: "Include call-graph annotations (calls-out / called-by within the same file). Default false; off keeps zoom output minimal."
|
|
34012
|
+
}))
|
|
33681
34013
|
});
|
|
33682
34014
|
function isUrl(s) {
|
|
33683
34015
|
return s.startsWith("http://") || s.startsWith("https://");
|
|
33684
34016
|
}
|
|
34017
|
+
async function assertReadPathPermissions(extCtx, ctx, paths) {
|
|
34018
|
+
const targets = Array.isArray(paths) ? paths : [paths];
|
|
34019
|
+
const checked = new Set;
|
|
34020
|
+
for (const target of targets) {
|
|
34021
|
+
if (!target || checked.has(target))
|
|
34022
|
+
continue;
|
|
34023
|
+
checked.add(target);
|
|
34024
|
+
await assertExternalDirectoryPermission(extCtx, target, "read", {
|
|
34025
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
34026
|
+
});
|
|
34027
|
+
}
|
|
34028
|
+
}
|
|
33685
34029
|
function zoomTargetLabel(args) {
|
|
33686
34030
|
return args.filePath ?? args.url ?? "(no target)";
|
|
33687
34031
|
}
|
|
@@ -33698,22 +34042,24 @@ function buildOutlineSections(text, theme) {
|
|
|
33698
34042
|
}
|
|
33699
34043
|
function buildZoomSections(args, payload, theme) {
|
|
33700
34044
|
const batch = asRecord2(payload);
|
|
33701
|
-
|
|
33702
|
-
|
|
33703
|
-
const
|
|
34045
|
+
const batchItems = Array.isArray(batch?.symbols) ? batch.symbols : Array.isArray(batch?.entries) ? batch.entries : null;
|
|
34046
|
+
if (batchItems) {
|
|
34047
|
+
const header = batch?.complete === false ? [theme.fg("warning", "Incomplete zoom results")] : [];
|
|
33704
34048
|
return [
|
|
33705
34049
|
...header,
|
|
33706
|
-
...
|
|
34050
|
+
...batchItems.map((item) => {
|
|
33707
34051
|
const record2 = asRecord2(item);
|
|
33708
34052
|
if (!record2)
|
|
33709
34053
|
return theme.fg("muted", "No zoom result available.");
|
|
33710
34054
|
const name = asString(record2.name) ?? "(unknown symbol)";
|
|
34055
|
+
const itemTargetLabel = asString(record2.targetLabel) ?? zoomTargetLabel(args);
|
|
33711
34056
|
if (record2.success === false) {
|
|
33712
|
-
|
|
34057
|
+
const location = record2.targetLabel ? ` in ${shortenPath2(itemTargetLabel)}` : "";
|
|
34058
|
+
return theme.fg("error", `Symbol "${name}" not found${location}: ${asString(record2.error) ?? "zoom failed"}`);
|
|
33713
34059
|
}
|
|
33714
34060
|
const content = asString(record2.content);
|
|
33715
34061
|
return [
|
|
33716
|
-
`${theme.fg("accent", name)} ${theme.fg("muted",
|
|
34062
|
+
`${theme.fg("accent", name)} ${theme.fg("muted", shortenPath2(itemTargetLabel))}`,
|
|
33717
34063
|
content
|
|
33718
34064
|
].filter(Boolean).join(`
|
|
33719
34065
|
`);
|
|
@@ -33733,7 +34079,7 @@ function buildZoomSections(args, payload, theme) {
|
|
|
33733
34079
|
const startLine = range && typeof range.start_line === "number" ? range.start_line : undefined;
|
|
33734
34080
|
const endLine = range && typeof range.end_line === "number" ? range.end_line : undefined;
|
|
33735
34081
|
const targetLabel = zoomTargetLabel(args);
|
|
33736
|
-
const location = startLine !== undefined ? `${
|
|
34082
|
+
const location = startLine !== undefined ? `${shortenPath2(targetLabel)}:${startLine}${endLine && endLine !== startLine ? `-${endLine}` : ""}` : shortenPath2(targetLabel);
|
|
33737
34083
|
const lines = [`${theme.fg("accent", name)} ${theme.fg("muted", `[${kind}] ${location}`)}`];
|
|
33738
34084
|
const content = asString(record2.content);
|
|
33739
34085
|
if (content) {
|
|
@@ -33810,7 +34156,9 @@ Pass a single \`target\`:
|
|
|
33810
34156
|
if (target.length === 0) {
|
|
33811
34157
|
throw new Error("'target' must be a non-empty string or array of strings");
|
|
33812
34158
|
}
|
|
33813
|
-
const
|
|
34159
|
+
const resolvedTargets = await Promise.all(target.map((entry) => resolvePathArg(extCtx.cwd, entry)));
|
|
34160
|
+
await assertReadPathPermissions(extCtx, ctx, resolvedTargets);
|
|
34161
|
+
const response3 = await callBridge(bridge, "outline", { target: resolvedTargets, files: true }, extCtx);
|
|
33814
34162
|
if (response3.success === false) {
|
|
33815
34163
|
throw new Error(response3.message || "outline failed");
|
|
33816
34164
|
}
|
|
@@ -33819,13 +34167,14 @@ Pass a single \`target\`:
|
|
|
33819
34167
|
if (typeof target !== "string" || target.length === 0) {
|
|
33820
34168
|
throw new Error("'target' must be a non-empty string or array of strings");
|
|
33821
34169
|
}
|
|
34170
|
+
const resolvedTarget2 = await resolvePathArg(extCtx.cwd, target);
|
|
33822
34171
|
let isDirectory2 = false;
|
|
33823
34172
|
try {
|
|
33824
|
-
const
|
|
33825
|
-
const st = await stat2(resolved);
|
|
34173
|
+
const st = await stat2(resolvedTarget2);
|
|
33826
34174
|
isDirectory2 = st.isDirectory();
|
|
33827
34175
|
} catch {}
|
|
33828
|
-
|
|
34176
|
+
await assertReadPathPermissions(extCtx, ctx, resolvedTarget2);
|
|
34177
|
+
const request = isDirectory2 ? { directory: resolvedTarget2, files: true } : { file: resolvedTarget2, files: true };
|
|
33829
34178
|
const response2 = await callBridge(bridge, "outline", request, extCtx);
|
|
33830
34179
|
if (response2.success === false) {
|
|
33831
34180
|
throw new Error(response2.message || "outline failed");
|
|
@@ -33840,24 +34189,26 @@ Pass a single \`target\`:
|
|
|
33840
34189
|
return textResult(formatOutlineText(response2));
|
|
33841
34190
|
}
|
|
33842
34191
|
if (isArray) {
|
|
33843
|
-
const
|
|
34192
|
+
const resolvedTargets = await Promise.all(target.map((entry) => resolvePathArg(extCtx.cwd, entry)));
|
|
34193
|
+
await assertReadPathPermissions(extCtx, ctx, resolvedTargets);
|
|
34194
|
+
const response2 = await callBridge(bridge, "outline", { files: resolvedTargets }, extCtx);
|
|
33844
34195
|
return textResult(formatOutlineText(response2));
|
|
33845
34196
|
}
|
|
33846
34197
|
if (typeof target !== "string" || target.length === 0) {
|
|
33847
34198
|
throw new Error("'target' must be a non-empty string or array of strings");
|
|
33848
34199
|
}
|
|
34200
|
+
const resolvedTarget = await resolvePathArg(extCtx.cwd, target);
|
|
33849
34201
|
let isDirectory = false;
|
|
33850
34202
|
try {
|
|
33851
|
-
const
|
|
33852
|
-
const st = await stat2(resolved);
|
|
34203
|
+
const st = await stat2(resolvedTarget);
|
|
33853
34204
|
isDirectory = st.isDirectory();
|
|
33854
34205
|
} catch {}
|
|
34206
|
+
await assertReadPathPermissions(extCtx, ctx, resolvedTarget);
|
|
33855
34207
|
if (isDirectory) {
|
|
33856
|
-
const
|
|
33857
|
-
const response2 = await callBridge(bridge, "outline", { directory: dirPath }, extCtx);
|
|
34208
|
+
const response2 = await callBridge(bridge, "outline", { directory: resolvedTarget }, extCtx);
|
|
33858
34209
|
return textResult(JSON.stringify(response2, null, 2), response2);
|
|
33859
34210
|
}
|
|
33860
|
-
const response = await callBridge(bridge, "outline", { file:
|
|
34211
|
+
const response = await callBridge(bridge, "outline", { file: resolvedTarget }, extCtx);
|
|
33861
34212
|
return textResult(formatOutlineText(response));
|
|
33862
34213
|
},
|
|
33863
34214
|
renderCall(args, theme, context) {
|
|
@@ -33872,7 +34223,7 @@ Pass a single \`target\`:
|
|
|
33872
34223
|
pi.registerTool({
|
|
33873
34224
|
name: "aft_zoom",
|
|
33874
34225
|
label: "zoom",
|
|
33875
|
-
description: "Inspect code symbols or documentation sections. For code, returns the full source of a symbol
|
|
34226
|
+
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
34227
|
parameters: ZoomParams,
|
|
33877
34228
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33878
34229
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
@@ -33896,6 +34247,7 @@ Pass a single \`target\`:
|
|
|
33896
34247
|
const hasUrl = !isEmptyParam(params.url);
|
|
33897
34248
|
const hasTargets = hasTargetsProvided(params.targets);
|
|
33898
34249
|
const hasSymbols = !isEmptyParam(params.symbols);
|
|
34250
|
+
const wantCallgraph = params.callgraph === true;
|
|
33899
34251
|
if (hasTargets) {
|
|
33900
34252
|
if (hasFilePath || hasUrl || hasSymbols) {
|
|
33901
34253
|
throw new Error("'targets' is mutually exclusive with 'filePath', 'url', and 'symbols'");
|
|
@@ -33912,10 +34264,17 @@ Pass a single \`target\`:
|
|
|
33912
34264
|
throw new Error(`targets[${i}].symbol must be a non-empty string`);
|
|
33913
34265
|
}
|
|
33914
34266
|
}
|
|
33915
|
-
const
|
|
33916
|
-
|
|
34267
|
+
const resolvedTargets = await Promise.all(targets.map((t) => resolvePathArg(extCtx.cwd, t.filePath)));
|
|
34268
|
+
await assertReadPathPermissions(extCtx, ctx, resolvedTargets);
|
|
34269
|
+
const responses = await Promise.all(targets.map((t, index) => {
|
|
34270
|
+
const req2 = {
|
|
34271
|
+
file: resolvedTargets[index],
|
|
34272
|
+
symbol: t.symbol
|
|
34273
|
+
};
|
|
33917
34274
|
if (params.contextLines !== undefined)
|
|
33918
34275
|
req2.context_lines = params.contextLines;
|
|
34276
|
+
if (wantCallgraph)
|
|
34277
|
+
req2.callgraph = true;
|
|
33919
34278
|
return callBridge(bridge, "zoom", req2, extCtx).catch((err) => ({
|
|
33920
34279
|
success: false,
|
|
33921
34280
|
message: err instanceof Error ? err.message : String(err)
|
|
@@ -33935,7 +34294,9 @@ Pass a single \`target\`:
|
|
|
33935
34294
|
if (hasFilePath && hasUrl) {
|
|
33936
34295
|
throw new Error("Provide exactly ONE of 'filePath' or 'url' — not both");
|
|
33937
34296
|
}
|
|
33938
|
-
const file2 = hasUrl ? params.url : params.filePath;
|
|
34297
|
+
const file2 = hasUrl ? params.url : await resolvePathArg(extCtx.cwd, params.filePath);
|
|
34298
|
+
if (!hasUrl)
|
|
34299
|
+
await assertReadPathPermissions(extCtx, ctx, file2);
|
|
33939
34300
|
const targetLabel = (hasUrl ? params.url : params.filePath) ?? file2;
|
|
33940
34301
|
const symbolsArray = hasSymbols ? typeof params.symbols === "string" ? [params.symbols] : params.symbols : undefined;
|
|
33941
34302
|
if (symbolsArray) {
|
|
@@ -33943,6 +34304,8 @@ Pass a single \`target\`:
|
|
|
33943
34304
|
const req2 = { file: file2, symbol: sym };
|
|
33944
34305
|
if (params.contextLines !== undefined)
|
|
33945
34306
|
req2.context_lines = params.contextLines;
|
|
34307
|
+
if (wantCallgraph)
|
|
34308
|
+
req2.callgraph = true;
|
|
33946
34309
|
return callBridge(bridge, "zoom", req2, extCtx).catch((err) => ({
|
|
33947
34310
|
success: false,
|
|
33948
34311
|
message: err instanceof Error ? err.message : String(err)
|
|
@@ -33961,6 +34324,8 @@ Pass a single \`target\`:
|
|
|
33961
34324
|
const req = { file: file2 };
|
|
33962
34325
|
if (params.contextLines !== undefined)
|
|
33963
34326
|
req.context_lines = params.contextLines;
|
|
34327
|
+
if (wantCallgraph)
|
|
34328
|
+
req.callgraph = true;
|
|
33964
34329
|
const response = await callBridge(bridge, "zoom", req, extCtx);
|
|
33965
34330
|
if (response.success === false) {
|
|
33966
34331
|
throw new Error(response.message || "zoom failed");
|
|
@@ -34073,21 +34438,21 @@ function buildRefactorSections(args, payload, theme) {
|
|
|
34073
34438
|
`${theme.fg("success", "moved symbol")} ${theme.fg("toolOutput", args.symbol ?? "(symbol)")}`,
|
|
34074
34439
|
`${theme.fg("muted", "files modified")} ${asNumber(response.files_modified) ?? results.length}`,
|
|
34075
34440
|
`${theme.fg("muted", "consumers updated")} ${asNumber(response.consumers_updated) ?? 0}`,
|
|
34076
|
-
results.length > 0 ? results.map((entry) => ` ↳ ${
|
|
34441
|
+
results.length > 0 ? results.map((entry) => ` ↳ ${shortenPath2(asString(entry.file) ?? "(unknown file)")}`).join(`
|
|
34077
34442
|
`) : theme.fg("muted", "No files reported.")
|
|
34078
34443
|
];
|
|
34079
34444
|
}
|
|
34080
34445
|
if (args.op === "extract") {
|
|
34081
34446
|
return [
|
|
34082
34447
|
`${theme.fg("success", "extracted")} ${theme.fg("toolOutput", asString(response.name) ?? args.name ?? "(function)")}`,
|
|
34083
|
-
`${theme.fg("muted", "file")} ${theme.fg("accent",
|
|
34448
|
+
`${theme.fg("muted", "file")} ${theme.fg("accent", shortenPath2(asString(response.file) ?? args.filePath))}`,
|
|
34084
34449
|
`${theme.fg("muted", "params")} ${Array.isArray(response.parameters) ? response.parameters.join(", ") || "none" : "none"}`,
|
|
34085
34450
|
`${theme.fg("muted", "return type")} ${asString(response.return_type) ?? "unknown"}`
|
|
34086
34451
|
];
|
|
34087
34452
|
}
|
|
34088
34453
|
return [
|
|
34089
34454
|
`${theme.fg("success", "inlined")} ${theme.fg("toolOutput", asString(response.symbol) ?? args.symbol ?? "(symbol)")}`,
|
|
34090
|
-
`${theme.fg("muted", "file")} ${theme.fg("accent",
|
|
34455
|
+
`${theme.fg("muted", "file")} ${theme.fg("accent", shortenPath2(asString(response.file) ?? args.filePath))}`,
|
|
34091
34456
|
`${theme.fg("muted", "context")} ${asString(response.call_context) ?? "unknown"}`,
|
|
34092
34457
|
`${theme.fg("muted", "substitutions")} ${asNumber(response.substitutions) ?? 0}`
|
|
34093
34458
|
];
|
|
@@ -34112,7 +34477,6 @@ function registerRefactorTool(pi, ctx) {
|
|
|
34112
34477
|
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
34478
|
parameters: RefactorParams,
|
|
34114
34479
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
34115
|
-
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
34116
34480
|
const commandMap = {
|
|
34117
34481
|
move: "move_symbol",
|
|
34118
34482
|
extract: "extract_function",
|
|
@@ -34127,18 +34491,40 @@ function registerRefactorTool(pi, ctx) {
|
|
|
34127
34491
|
if (params.op === "extract" && isEmptyParam(params.name)) {
|
|
34128
34492
|
throw new Error("'name' is required for 'extract' op");
|
|
34129
34493
|
}
|
|
34130
|
-
const
|
|
34494
|
+
const startLine = coerceOptionalInt(params.startLine, "startLine", 1, Number.MAX_SAFE_INTEGER);
|
|
34495
|
+
const endLine = coerceOptionalInt(params.endLine, "endLine", 1, Number.MAX_SAFE_INTEGER);
|
|
34496
|
+
const callSiteLine = coerceOptionalInt(params.callSiteLine, "callSiteLine", 1, Number.MAX_SAFE_INTEGER);
|
|
34497
|
+
if (params.op === "extract") {
|
|
34498
|
+
if (startLine === undefined)
|
|
34499
|
+
throw new Error("'startLine' is required for 'extract' op");
|
|
34500
|
+
if (endLine === undefined)
|
|
34501
|
+
throw new Error("'endLine' is required for 'extract' op");
|
|
34502
|
+
}
|
|
34503
|
+
if (params.op === "inline" && callSiteLine === undefined) {
|
|
34504
|
+
throw new Error("'callSiteLine' is required for 'inline' op");
|
|
34505
|
+
}
|
|
34506
|
+
const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
|
|
34507
|
+
const destination = !isEmptyParam(params.destination) ? await resolvePathArg(extCtx.cwd, params.destination) : undefined;
|
|
34508
|
+
const permissionTargets = params.op === "move" && destination !== undefined ? [filePath, destination] : [filePath];
|
|
34509
|
+
const checked = new Set;
|
|
34510
|
+
for (const target of permissionTargets) {
|
|
34511
|
+
if (checked.has(target))
|
|
34512
|
+
continue;
|
|
34513
|
+
checked.add(target);
|
|
34514
|
+
await assertExternalDirectoryPermission(extCtx, target, "modify", {
|
|
34515
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
34516
|
+
});
|
|
34517
|
+
}
|
|
34518
|
+
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
34519
|
+
const req = { file: filePath };
|
|
34131
34520
|
if (!isEmptyParam(params.symbol))
|
|
34132
34521
|
req.symbol = params.symbol;
|
|
34133
|
-
if (
|
|
34134
|
-
req.destination =
|
|
34522
|
+
if (destination !== undefined)
|
|
34523
|
+
req.destination = destination;
|
|
34135
34524
|
if (!isEmptyParam(params.scope))
|
|
34136
34525
|
req.scope = params.scope;
|
|
34137
34526
|
if (!isEmptyParam(params.name))
|
|
34138
34527
|
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
34528
|
if (startLine !== undefined)
|
|
34143
34529
|
req.start_line = startLine;
|
|
34144
34530
|
if (endLine !== undefined) {
|
|
@@ -34161,6 +34547,9 @@ function registerRefactorTool(pi, ctx) {
|
|
|
34161
34547
|
// src/tools/safety.ts
|
|
34162
34548
|
import { StringEnum as StringEnum5 } from "@earendil-works/pi-ai";
|
|
34163
34549
|
import { Type as Type12 } from "typebox";
|
|
34550
|
+
function responsePaths(response) {
|
|
34551
|
+
return Array.isArray(response.paths) ? response.paths.filter((path2) => typeof path2 === "string" && path2.length > 0) : [];
|
|
34552
|
+
}
|
|
34164
34553
|
var SafetyParams = Type12.Object({
|
|
34165
34554
|
op: StringEnum5(["undo", "history", "checkpoint", "restore", "list"], {
|
|
34166
34555
|
description: "Safety operation"
|
|
@@ -34185,14 +34574,14 @@ function buildSafetySections(args, payload, theme) {
|
|
|
34185
34574
|
];
|
|
34186
34575
|
}
|
|
34187
34576
|
return [
|
|
34188
|
-
`${theme.fg("success", "restored")} ${theme.fg("accent",
|
|
34577
|
+
`${theme.fg("success", "restored")} ${theme.fg("accent", shortenPath2(asString(response.path) ?? args.filePath ?? "(file)"))}`,
|
|
34189
34578
|
`${theme.fg("muted", "backup")} ${asString(response.backup_id) ?? "—"}`
|
|
34190
34579
|
];
|
|
34191
34580
|
}
|
|
34192
34581
|
if (args.op === "history") {
|
|
34193
34582
|
const entries = asRecords(response.entries);
|
|
34194
34583
|
const sections2 = [
|
|
34195
|
-
theme.fg("accent",
|
|
34584
|
+
theme.fg("accent", shortenPath2(asString(response.file) ?? args.filePath ?? "(file)"))
|
|
34196
34585
|
];
|
|
34197
34586
|
if (entries.length === 0) {
|
|
34198
34587
|
sections2.push(theme.fg("muted", "No history entries."));
|
|
@@ -34214,7 +34603,7 @@ function buildSafetySections(args, payload, theme) {
|
|
|
34214
34603
|
`${theme.fg("success", "checkpoint created")} ${theme.fg("accent", asString(response.name) ?? args.name ?? "(checkpoint)")}`,
|
|
34215
34604
|
`${theme.fg("muted", "files")} ${asNumber(response.file_count) ?? 0}`,
|
|
34216
34605
|
skipped.length > 0 ? `${theme.fg("warning", "skipped")}
|
|
34217
|
-
${skipped.map((entry) => ` ↳ ${
|
|
34606
|
+
${skipped.map((entry) => ` ↳ ${shortenPath2(asString(entry.file) ?? "(file)")}: ${asString(entry.error) ?? "unknown error"}`).join(`
|
|
34218
34607
|
`)}` : theme.fg("muted", "No skipped files.")
|
|
34219
34608
|
];
|
|
34220
34609
|
}
|
|
@@ -34264,7 +34653,43 @@ function registerSafetyTool(pi, ctx) {
|
|
|
34264
34653
|
if ((params.op === "checkpoint" || params.op === "restore") && !params.name) {
|
|
34265
34654
|
throw new Error(`op='${params.op}' requires 'name'`);
|
|
34266
34655
|
}
|
|
34656
|
+
const filePath = params.filePath ? await resolvePathArg(extCtx.cwd, params.filePath) : undefined;
|
|
34657
|
+
const files = params.files ? await Promise.all(params.files.map((file2) => resolvePathArg(extCtx.cwd, file2))) : undefined;
|
|
34267
34658
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
34659
|
+
const restrictToProjectRoot = ctx.config.restrict_to_project_root ?? false;
|
|
34660
|
+
if (params.op === "undo") {
|
|
34661
|
+
const previewReq = {};
|
|
34662
|
+
if (filePath)
|
|
34663
|
+
previewReq.file = filePath;
|
|
34664
|
+
const preview = await callBridge(bridge, "undo_preview", previewReq, extCtx);
|
|
34665
|
+
for (const file2 of new Set(responsePaths(preview))) {
|
|
34666
|
+
await assertExternalDirectoryPermission(extCtx, file2, "modify", {
|
|
34667
|
+
restrictToProjectRoot
|
|
34668
|
+
});
|
|
34669
|
+
}
|
|
34670
|
+
}
|
|
34671
|
+
if (params.op === "checkpoint") {
|
|
34672
|
+
const checkpointFiles = files ?? (filePath ? [filePath] : undefined);
|
|
34673
|
+
if (Array.isArray(checkpointFiles)) {
|
|
34674
|
+
const checked = new Set;
|
|
34675
|
+
for (const file2 of checkpointFiles) {
|
|
34676
|
+
if (checked.has(file2))
|
|
34677
|
+
continue;
|
|
34678
|
+
checked.add(file2);
|
|
34679
|
+
await assertExternalDirectoryPermission(extCtx, file2, "modify", {
|
|
34680
|
+
restrictToProjectRoot
|
|
34681
|
+
});
|
|
34682
|
+
}
|
|
34683
|
+
}
|
|
34684
|
+
}
|
|
34685
|
+
if (params.op === "restore" && params.name) {
|
|
34686
|
+
const preview = await callBridge(bridge, "checkpoint_paths", { name: params.name }, extCtx);
|
|
34687
|
+
for (const file2 of new Set(responsePaths(preview))) {
|
|
34688
|
+
await assertExternalDirectoryPermission(extCtx, file2, "modify", {
|
|
34689
|
+
restrictToProjectRoot
|
|
34690
|
+
});
|
|
34691
|
+
}
|
|
34692
|
+
}
|
|
34268
34693
|
const commandMap = {
|
|
34269
34694
|
undo: "undo",
|
|
34270
34695
|
history: "edit_history",
|
|
@@ -34276,16 +34701,16 @@ function registerSafetyTool(pi, ctx) {
|
|
|
34276
34701
|
if (params.name)
|
|
34277
34702
|
req.name = params.name;
|
|
34278
34703
|
if (params.op === "checkpoint") {
|
|
34279
|
-
if (
|
|
34280
|
-
req.files =
|
|
34281
|
-
} else if (
|
|
34282
|
-
req.files = [
|
|
34704
|
+
if (files) {
|
|
34705
|
+
req.files = files;
|
|
34706
|
+
} else if (filePath) {
|
|
34707
|
+
req.files = [filePath];
|
|
34283
34708
|
}
|
|
34284
34709
|
} else {
|
|
34285
|
-
if (
|
|
34286
|
-
req.file =
|
|
34287
|
-
if (
|
|
34288
|
-
req.files =
|
|
34710
|
+
if (filePath)
|
|
34711
|
+
req.file = filePath;
|
|
34712
|
+
if (files)
|
|
34713
|
+
req.files = files;
|
|
34289
34714
|
}
|
|
34290
34715
|
const response = await callBridge(bridge, commandMap[params.op], req, extCtx);
|
|
34291
34716
|
return textResult(JSON.stringify(response, null, 2));
|
|
@@ -34362,7 +34787,7 @@ function buildSemanticSections(args, payload, theme) {
|
|
|
34362
34787
|
}
|
|
34363
34788
|
const grouped = groupByFile(results, (result) => asString(result.file));
|
|
34364
34789
|
for (const [file2, fileResults] of grouped.entries()) {
|
|
34365
|
-
const lines = [theme.fg("accent",
|
|
34790
|
+
const lines = [theme.fg("accent", shortenPath2(file2))];
|
|
34366
34791
|
fileResults.forEach((result) => {
|
|
34367
34792
|
if (asString(result.kind) === "GrepLine") {
|
|
34368
34793
|
const line = asNumber(result.line);
|
|
@@ -34516,8 +34941,12 @@ function registerStructureTool(pi, ctx) {
|
|
|
34516
34941
|
parameters: TransformParams,
|
|
34517
34942
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
34518
34943
|
validateTransformParams(params);
|
|
34944
|
+
const filePath = await resolvePathArg(extCtx.cwd, params.filePath);
|
|
34945
|
+
await assertExternalDirectoryPermission(extCtx, filePath, "modify", {
|
|
34946
|
+
restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
|
|
34947
|
+
});
|
|
34519
34948
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
34520
|
-
const req = { file:
|
|
34949
|
+
const req = { file: filePath };
|
|
34521
34950
|
if (params.container !== undefined)
|
|
34522
34951
|
req.scope = params.container;
|
|
34523
34952
|
if (params.code !== undefined)
|
|
@@ -34554,32 +34983,32 @@ function registerStructureTool(pi, ctx) {
|
|
|
34554
34983
|
function validateTransformParams(params) {
|
|
34555
34984
|
const op = params.op;
|
|
34556
34985
|
if (op === "add_member") {
|
|
34557
|
-
if (
|
|
34986
|
+
if (isEmptyParam(params.container)) {
|
|
34558
34987
|
throw new Error("'container' is required for 'add_member' op");
|
|
34559
34988
|
}
|
|
34560
|
-
if (
|
|
34989
|
+
if (isEmptyParam(params.code)) {
|
|
34561
34990
|
throw new Error("'code' is required for 'add_member' op");
|
|
34562
34991
|
}
|
|
34563
34992
|
}
|
|
34564
34993
|
if (op === "add_derive" || op === "wrap_try_catch" || op === "add_decorator" || op === "add_struct_tags") {
|
|
34565
|
-
if (
|
|
34994
|
+
if (isEmptyParam(params.target)) {
|
|
34566
34995
|
throw new Error(`'target' is required for '${op}' op`);
|
|
34567
34996
|
}
|
|
34568
34997
|
}
|
|
34569
|
-
if (op === "add_derive" &&
|
|
34998
|
+
if (op === "add_derive" && isEmptyParam(params.derives)) {
|
|
34570
34999
|
throw new Error("'derives' array is required for 'add_derive' op");
|
|
34571
35000
|
}
|
|
34572
|
-
if (op === "add_decorator" &&
|
|
35001
|
+
if (op === "add_decorator" && isEmptyParam(params.decorator)) {
|
|
34573
35002
|
throw new Error("'decorator' is required for 'add_decorator' op");
|
|
34574
35003
|
}
|
|
34575
35004
|
if (op === "add_struct_tags") {
|
|
34576
|
-
if (
|
|
35005
|
+
if (isEmptyParam(params.field)) {
|
|
34577
35006
|
throw new Error("'field' is required for 'add_struct_tags' op");
|
|
34578
35007
|
}
|
|
34579
|
-
if (
|
|
35008
|
+
if (isEmptyParam(params.tag)) {
|
|
34580
35009
|
throw new Error("'tag' is required for 'add_struct_tags' op");
|
|
34581
35010
|
}
|
|
34582
|
-
if (
|
|
35011
|
+
if (isEmptyParam(params.value)) {
|
|
34583
35012
|
throw new Error("'value' is required for 'add_struct_tags' op");
|
|
34584
35013
|
}
|
|
34585
35014
|
}
|
|
@@ -34611,6 +35040,7 @@ function buildWorkflowHints(opts) {
|
|
|
34611
35040
|
}
|
|
34612
35041
|
if (hasInspect) {
|
|
34613
35042
|
sections.push("**Codebase health & diagnostics**: AFT does not surface compile/type errors automatically after edits — pull them with `aft_inspect`. Run it after a batch of edits and before you run tests or commit, when starting in unfamiliar code, or before a refactor/review. One call summarizes diagnostics (compile/type errors), TODOs, metrics, dead code, unused exports, and duplicates; pass `sections` for focused drill-down and `scope` to actively pull diagnostics for a specific file or directory. Its diagnostics are a fast checkpoint, not the authority — a clean `tsc` / `cargo check` / `pyright` run is the real gate. Treat `stale_categories` as a genuine stale-cache signal while an async Tier 2 refresh catches up.");
|
|
35043
|
+
sections.push("**AFT status bar**: tool results may end with a one-line health bar `[AFT E<errors> W<warnings> | D<dead-code> U<unused-exports> C<clone/dup-groups> | T<todos>]` — an IDE-style glance that appears when a count changes. `E`/`W` are live LSP diagnostics for files touched this session (your universal compile-error signal across every language with an LSP). A `~` before `D` means the dead-code/unused/dup counts predate your latest edit — run `aft_inspect` for current numbers and detail. When `E>0`, you likely just introduced errors; investigate before moving on.");
|
|
34614
35044
|
}
|
|
34615
35045
|
if (hasNavigate) {
|
|
34616
35046
|
sections.push([
|
|
@@ -34720,15 +35150,16 @@ var PLUGIN_VERSION = (() => {
|
|
|
34720
35150
|
return "0.0.0";
|
|
34721
35151
|
}
|
|
34722
35152
|
})();
|
|
34723
|
-
var ANNOUNCEMENT_VERSION = "0.
|
|
35153
|
+
var ANNOUNCEMENT_VERSION = "0.35.0";
|
|
34724
35154
|
var ANNOUNCEMENT_FEATURES = [
|
|
34725
|
-
"
|
|
34726
|
-
"
|
|
34727
|
-
"`
|
|
34728
|
-
"
|
|
34729
|
-
"
|
|
35155
|
+
"Leaner semantic indexing — local embedding now uses about half the CPU and a fraction of the peak memory, with no change to results and no re-index needed.",
|
|
35156
|
+
"New IDE-style status bar on tool results — live LSP errors/warnings plus dead-code, unused-export, duplicate, and TODO counts at a glance.",
|
|
35157
|
+
"`aft_inspect` is more accurate (dead-code reachability, duplicate collapse, ranked findings) and a background scheduler keeps its counts fresh during long sessions.",
|
|
35158
|
+
"The `aft_conflicts` and `grep` bash-output hints now fire on Pi too.",
|
|
35159
|
+
"`npx @cortexkit/aft --version` reports CLI, binary, and per-harness versions; `doctor --issue` can scope its report to a single session.",
|
|
35160
|
+
"New SCSS support and `.inc` files parsed as PHP."
|
|
34730
35161
|
];
|
|
34731
|
-
var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/
|
|
35162
|
+
var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
|
|
34732
35163
|
var ALL_ONLY_TOOLS = new Set([
|
|
34733
35164
|
"aft_callgraph",
|
|
34734
35165
|
"aft_delete",
|
|
@@ -35033,7 +35464,7 @@ ${lines}
|
|
|
35033
35464
|
if (onnxRuntimePromise) {
|
|
35034
35465
|
await Promise.race([
|
|
35035
35466
|
onnxRuntimePromise,
|
|
35036
|
-
new Promise((
|
|
35467
|
+
new Promise((resolve4) => setTimeout(() => resolve4(null), 60000))
|
|
35037
35468
|
]);
|
|
35038
35469
|
}
|
|
35039
35470
|
const bridge = pool.getBridge(cwd);
|
|
@@ -35089,8 +35520,14 @@ ${lines}
|
|
|
35089
35520
|
registerWorkflowHints(pi, config2, surface);
|
|
35090
35521
|
registerStatusCommand(pi, ctx);
|
|
35091
35522
|
pi.on("tool_result", async (event, extCtx) => {
|
|
35092
|
-
const
|
|
35093
|
-
|
|
35523
|
+
const sessionID = resolveSessionId(extCtx);
|
|
35524
|
+
const bgContent = await appendToolResultBgCompletions({ ctx, directory: extCtx.cwd, sessionID }, event.content);
|
|
35525
|
+
let content = bgContent ?? event.content;
|
|
35526
|
+
const activeBridge = pool.getActiveBridgeForRoot(extCtx.cwd);
|
|
35527
|
+
const bar = statusBarBlockForSession(sessionID, activeBridge?.getStatusBar());
|
|
35528
|
+
if (bar)
|
|
35529
|
+
content = [...content, { type: "text", text: bar }];
|
|
35530
|
+
if (content === event.content)
|
|
35094
35531
|
return;
|
|
35095
35532
|
return { content, details: event.details, isError: event.isError };
|
|
35096
35533
|
});
|