@cortexkit/aft-pi 0.32.0 → 0.33.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 +2 -2
- package/dist/config.d.ts +41 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +517 -202
- package/dist/notifications.d.ts.map +1 -1
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/inspect.d.ts +21 -0
- package/dist/tools/inspect.d.ts.map +1 -0
- package/dist/tools/navigate.d.ts +1 -1
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tools/semantic.d.ts +1 -1
- package/dist/tools/semantic.d.ts.map +1 -1
- package/dist/workflow-hints.d.ts +1 -0
- package/dist/workflow-hints.d.ts.map +1 -1
- package/package.json +9 -9
- package/dist/tools/lsp.d.ts +0 -24
- package/dist/tools/lsp.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -11680,6 +11680,7 @@ import { homedir } from "node:os";
|
|
|
11680
11680
|
import { join } from "node:path";
|
|
11681
11681
|
import { StringDecoder } from "node:string_decoder";
|
|
11682
11682
|
var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
|
|
11683
|
+
var BRIDGE_HANG_TIMEOUT_THRESHOLD = 2;
|
|
11683
11684
|
var SEMANTIC_TIMEOUT_SAFETY_MARGIN_MS = 5000;
|
|
11684
11685
|
var MAX_STDOUT_BUFFER = 64 * 1024 * 1024;
|
|
11685
11686
|
function tagStderrLine(line) {
|
|
@@ -11787,8 +11788,11 @@ class BinaryBridge {
|
|
|
11787
11788
|
statusListeners = new Set;
|
|
11788
11789
|
configureWarningClients = new Map;
|
|
11789
11790
|
restartResetTimer = null;
|
|
11791
|
+
lastChildActivityAt = 0;
|
|
11792
|
+
consecutiveRequestTimeouts = 0;
|
|
11790
11793
|
errorPrefix;
|
|
11791
11794
|
logger;
|
|
11795
|
+
childEnv;
|
|
11792
11796
|
constructor(binaryPath, cwd, options, configOverrides) {
|
|
11793
11797
|
this.binaryPath = binaryPath;
|
|
11794
11798
|
this.cwd = cwd;
|
|
@@ -11803,6 +11807,7 @@ class BinaryBridge {
|
|
|
11803
11807
|
this.onBashPatternMatch = options?.onBashPatternMatch;
|
|
11804
11808
|
this.errorPrefix = options?.errorPrefix ?? "[aft-bridge]";
|
|
11805
11809
|
this.logger = options?.logger;
|
|
11810
|
+
this.childEnv = options?.childEnv;
|
|
11806
11811
|
}
|
|
11807
11812
|
logVia(message, meta) {
|
|
11808
11813
|
const logger = this.logger ?? getActiveLogger();
|
|
@@ -11954,20 +11959,41 @@ class BinaryBridge {
|
|
|
11954
11959
|
const line = `${JSON.stringify(request)}
|
|
11955
11960
|
`;
|
|
11956
11961
|
const keepBridgeOnTimeout = options?.keepBridgeOnTimeout === true;
|
|
11962
|
+
let requestSentAt = Date.now();
|
|
11957
11963
|
const response = await new Promise((resolve, reject) => {
|
|
11958
11964
|
const timer = setTimeout(() => {
|
|
11965
|
+
const entry = this.pending.get(id);
|
|
11966
|
+
if (!entry)
|
|
11967
|
+
return;
|
|
11959
11968
|
this.pending.delete(id);
|
|
11960
|
-
|
|
11969
|
+
clearTimeout(entry.timer);
|
|
11970
|
+
if (keepBridgeOnTimeout) {
|
|
11971
|
+
const timeoutMsg2 = `Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`;
|
|
11972
|
+
if (requestSessionId) {
|
|
11973
|
+
this.sessionWarnVia(requestSessionId, timeoutMsg2);
|
|
11974
|
+
} else {
|
|
11975
|
+
this.warnVia(timeoutMsg2);
|
|
11976
|
+
}
|
|
11977
|
+
entry.reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
|
|
11978
|
+
return;
|
|
11979
|
+
}
|
|
11980
|
+
const childActiveSinceRequest = this.lastChildActivityAt > requestSentAt;
|
|
11981
|
+
const consecutiveTimeouts = this.consecutiveRequestTimeouts + 1;
|
|
11982
|
+
this.consecutiveRequestTimeouts = consecutiveTimeouts;
|
|
11983
|
+
const keepWarm = childActiveSinceRequest || consecutiveTimeouts < BRIDGE_HANG_TIMEOUT_THRESHOLD;
|
|
11984
|
+
const restartSuffix = keepWarm ? " — bridge kept warm" : " — restarting bridge";
|
|
11961
11985
|
const timeoutMsg = `Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms${restartSuffix}`;
|
|
11962
11986
|
if (requestSessionId) {
|
|
11963
11987
|
this.sessionWarnVia(requestSessionId, timeoutMsg);
|
|
11964
11988
|
} else {
|
|
11965
11989
|
this.warnVia(timeoutMsg);
|
|
11966
11990
|
}
|
|
11967
|
-
|
|
11968
|
-
|
|
11969
|
-
|
|
11991
|
+
if (keepWarm) {
|
|
11992
|
+
entry.reject(new Error(`${this.errorPrefix} request "${command}" timed out after ${effectiveTimeoutMs}ms (bridge busy/under load); bridge kept warm — retry`));
|
|
11993
|
+
return;
|
|
11970
11994
|
}
|
|
11995
|
+
entry.reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
|
|
11996
|
+
this.handleTimeout(requestSessionId);
|
|
11971
11997
|
}, effectiveTimeoutMs);
|
|
11972
11998
|
this.pending.set(id, { resolve, reject, timer, onProgress: options?.onProgress });
|
|
11973
11999
|
if (!this.process?.stdin?.writable) {
|
|
@@ -11976,6 +12002,7 @@ class BinaryBridge {
|
|
|
11976
12002
|
reject(new Error(`${this.errorPrefix} stdin not writable for command "${command}"`));
|
|
11977
12003
|
return;
|
|
11978
12004
|
}
|
|
12005
|
+
requestSentAt = Date.now();
|
|
11979
12006
|
this.process.stdin.write(line, (err) => {
|
|
11980
12007
|
if (err) {
|
|
11981
12008
|
const entry = this.pending.get(id);
|
|
@@ -12170,6 +12197,15 @@ class BinaryBridge {
|
|
|
12170
12197
|
this.logVia(`ORT_DYLIB_PATH set from managed ONNX Runtime: ${ortLibraryPath}`);
|
|
12171
12198
|
}
|
|
12172
12199
|
}
|
|
12200
|
+
if (this.childEnv) {
|
|
12201
|
+
for (const [key, value] of Object.entries(this.childEnv)) {
|
|
12202
|
+
if (value === undefined) {
|
|
12203
|
+
delete env[key];
|
|
12204
|
+
} else {
|
|
12205
|
+
env[key] = value;
|
|
12206
|
+
}
|
|
12207
|
+
}
|
|
12208
|
+
}
|
|
12173
12209
|
const child = spawn(this.binaryPath, [], {
|
|
12174
12210
|
cwd: this.cwd,
|
|
12175
12211
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -12219,6 +12255,8 @@ class BinaryBridge {
|
|
|
12219
12255
|
this.process = child;
|
|
12220
12256
|
this.stdoutBuffer = "";
|
|
12221
12257
|
this.stderrBuffer = "";
|
|
12258
|
+
this.lastChildActivityAt = 0;
|
|
12259
|
+
this.consecutiveRequestTimeouts = 0;
|
|
12222
12260
|
this.stderrTail = [];
|
|
12223
12261
|
}
|
|
12224
12262
|
pushStderrLine(line) {
|
|
@@ -12274,6 +12312,7 @@ class BinaryBridge {
|
|
|
12274
12312
|
continue;
|
|
12275
12313
|
try {
|
|
12276
12314
|
const response = JSON.parse(line);
|
|
12315
|
+
this.lastChildActivityAt = Date.now();
|
|
12277
12316
|
if (response.type === "progress") {
|
|
12278
12317
|
const requestId = response.request_id;
|
|
12279
12318
|
const entry = requestId ? this.pending.get(requestId) : undefined;
|
|
@@ -12326,6 +12365,7 @@ class BinaryBridge {
|
|
|
12326
12365
|
continue;
|
|
12327
12366
|
this.pending.delete(id);
|
|
12328
12367
|
clearTimeout(entry.timer);
|
|
12368
|
+
this.consecutiveRequestTimeouts = 0;
|
|
12329
12369
|
this.scheduleRestartCountReset();
|
|
12330
12370
|
entry.resolve(response);
|
|
12331
12371
|
} else if (typeof response.type === "string") {
|
|
@@ -12337,6 +12377,7 @@ class BinaryBridge {
|
|
|
12337
12377
|
}
|
|
12338
12378
|
}
|
|
12339
12379
|
handleTimeout(triggeringSessionId) {
|
|
12380
|
+
this.consecutiveRequestTimeouts = 0;
|
|
12340
12381
|
this.rejectAllPending(new Error(`${this.errorPrefix} bridge killed during sibling timeout — request aborted`));
|
|
12341
12382
|
if (this.process) {
|
|
12342
12383
|
this.process.kill("SIGKILL");
|
|
@@ -12415,7 +12456,7 @@ class BinaryBridge {
|
|
|
12415
12456
|
// ../aft-bridge/dist/downloader.js
|
|
12416
12457
|
import { spawnSync } from "node:child_process";
|
|
12417
12458
|
import { createHash, randomUUID } from "node:crypto";
|
|
12418
|
-
import { chmodSync, closeSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
12459
|
+
import { chmodSync, closeSync, copyFileSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
12419
12460
|
import { homedir as homedir2 } from "node:os";
|
|
12420
12461
|
import { join as join2 } from "node:path";
|
|
12421
12462
|
import { Readable } from "node:stream";
|
|
@@ -12582,10 +12623,18 @@ async function downloadBinary(version) {
|
|
|
12582
12623
|
throw new Error(`Checksum mismatch for ${assetName}: expected ${expectedHash}, got ${actualHash}. ` + "The binary may have been tampered with.");
|
|
12583
12624
|
}
|
|
12584
12625
|
log(`Checksum verified (SHA-256: ${actualHash.slice(0, 16)}...)`);
|
|
12585
|
-
if (process.platform
|
|
12626
|
+
if (process.platform === "win32") {
|
|
12627
|
+
copyFileSync(tmpPath, binaryPath);
|
|
12628
|
+
} else {
|
|
12586
12629
|
chmodSync(tmpPath, 493);
|
|
12630
|
+
renameSync(tmpPath, binaryPath);
|
|
12631
|
+
}
|
|
12632
|
+
try {
|
|
12633
|
+
if (existsSync(tmpPath))
|
|
12634
|
+
unlinkSync(tmpPath);
|
|
12635
|
+
} catch {
|
|
12636
|
+
warn(`Could not clean up temporary download file ${tmpPath} — it can be removed manually.`);
|
|
12587
12637
|
}
|
|
12588
|
-
renameSync(tmpPath, binaryPath);
|
|
12589
12638
|
log(`AFT binary ready at ${binaryPath}`);
|
|
12590
12639
|
return binaryPath;
|
|
12591
12640
|
} catch (err) {
|
|
@@ -12756,7 +12805,7 @@ function markAnnouncementSeen(storageRoot, harness, currentVersion) {
|
|
|
12756
12805
|
|
|
12757
12806
|
// ../aft-bridge/dist/resolver.js
|
|
12758
12807
|
import { execSync } from "node:child_process";
|
|
12759
|
-
import { chmodSync as chmodSync2, copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, renameSync as renameSync3 } from "node:fs";
|
|
12808
|
+
import { chmodSync as chmodSync2, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, renameSync as renameSync3, unlinkSync as unlinkSync2 } from "node:fs";
|
|
12760
12809
|
import { createRequire as createRequire2 } from "node:module";
|
|
12761
12810
|
import { homedir as homedir3 } from "node:os";
|
|
12762
12811
|
import { join as join4 } from "node:path";
|
|
@@ -12779,10 +12828,15 @@ function copyToVersionedCache(npmBinaryPath, knownVersion) {
|
|
|
12779
12828
|
}
|
|
12780
12829
|
mkdirSync3(versionedDir, { recursive: true });
|
|
12781
12830
|
const tmpPath = `${cachedPath}.${process.pid}.${Date.now()}.tmp`;
|
|
12782
|
-
|
|
12831
|
+
copyFileSync2(npmBinaryPath, tmpPath);
|
|
12783
12832
|
if (process.platform !== "win32") {
|
|
12784
12833
|
chmodSync2(tmpPath, 493);
|
|
12785
12834
|
}
|
|
12835
|
+
if (process.platform === "win32" && existsSync3(cachedPath)) {
|
|
12836
|
+
try {
|
|
12837
|
+
unlinkSync2(cachedPath);
|
|
12838
|
+
} catch {}
|
|
12839
|
+
}
|
|
12786
12840
|
renameSync3(tmpPath, cachedPath);
|
|
12787
12841
|
log(`Copied npm binary to versioned cache: ${cachedPath}`);
|
|
12788
12842
|
return cachedPath;
|
|
@@ -13034,7 +13088,7 @@ async function ensureStorageMigrated(opts) {
|
|
|
13034
13088
|
// ../aft-bridge/dist/onnx-runtime.js
|
|
13035
13089
|
import { execFileSync } from "node:child_process";
|
|
13036
13090
|
import { createHash as createHash2 } from "node:crypto";
|
|
13037
|
-
import { chmodSync as chmodSync3, closeSync as closeSync2, copyFileSync as
|
|
13091
|
+
import { chmodSync as chmodSync3, closeSync as closeSync2, copyFileSync as copyFileSync3, createWriteStream as createWriteStream2, existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync5, openSync as openSync2, readdirSync, readFileSync as readFileSync3, readlinkSync, realpathSync, rmSync as rmSync2, statSync as statSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
13038
13092
|
import { basename, dirname as dirname3, isAbsolute, join as join6, relative, resolve, win32 } from "node:path";
|
|
13039
13093
|
import { Readable as Readable2 } from "node:stream";
|
|
13040
13094
|
import { pipeline as pipeline2 } from "node:stream/promises";
|
|
@@ -13270,20 +13324,64 @@ function findSystemOnnxRuntime(libName) {
|
|
|
13270
13324
|
} else if (process.platform === "linux") {
|
|
13271
13325
|
searchPaths.push("/usr/lib", "/usr/lib/x86_64-linux-gnu", "/usr/lib/aarch64-linux-gnu", "/usr/local/lib");
|
|
13272
13326
|
} else if (process.platform === "win32") {
|
|
13327
|
+
const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
|
|
13328
|
+
const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
13329
|
+
searchPaths.push(join6(programFiles, "onnxruntime", "lib"), join6(programFiles, "Microsoft ONNX Runtime", "lib"), join6(programFiles, "Microsoft Machine Learning", "lib"), join6(programFilesX86, "onnxruntime", "lib"), ...(() => {
|
|
13330
|
+
const nugetPaths = [];
|
|
13331
|
+
const userProfile = process.env.USERPROFILE ?? "";
|
|
13332
|
+
if (!userProfile)
|
|
13333
|
+
return nugetPaths;
|
|
13334
|
+
const nugetPackageDir = join6(userProfile, ".nuget", "packages", "microsoft.ml.onnxruntime");
|
|
13335
|
+
if (!existsSync5(nugetPackageDir))
|
|
13336
|
+
return nugetPaths;
|
|
13337
|
+
try {
|
|
13338
|
+
for (const entry of readdirSync(nugetPackageDir, { withFileTypes: true })) {
|
|
13339
|
+
if (!entry.isDirectory())
|
|
13340
|
+
continue;
|
|
13341
|
+
if (entry.name === "__globalPackagesFolder" || entry.name.startsWith("."))
|
|
13342
|
+
continue;
|
|
13343
|
+
nugetPaths.push(join6(nugetPackageDir, entry.name, "runtimes", "win-x64", "native"), join6(nugetPackageDir, entry.name, "runtimes", "win-arm64", "native"));
|
|
13344
|
+
}
|
|
13345
|
+
} catch (err) {
|
|
13346
|
+
warn(`Failed to scan NuGet ONNX Runtime cache ${nugetPackageDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
13347
|
+
}
|
|
13348
|
+
return nugetPaths;
|
|
13349
|
+
})());
|
|
13273
13350
|
searchPaths.push(...pathEntriesForPlatform());
|
|
13274
13351
|
}
|
|
13352
|
+
const normalizeCase = process.platform === "win32" || process.platform === "darwin";
|
|
13275
13353
|
const seen = new Set;
|
|
13276
|
-
|
|
13277
|
-
|
|
13278
|
-
|
|
13279
|
-
|
|
13280
|
-
if (
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
|
|
13354
|
+
const uniquePaths = searchPaths.filter((p) => {
|
|
13355
|
+
let key = resolve(p).replace(/[/\\]+$/, "");
|
|
13356
|
+
if (normalizeCase)
|
|
13357
|
+
key = key.toLowerCase();
|
|
13358
|
+
if (seen.has(key))
|
|
13359
|
+
return false;
|
|
13360
|
+
seen.add(key);
|
|
13361
|
+
return true;
|
|
13362
|
+
});
|
|
13363
|
+
for (const dir of uniquePaths) {
|
|
13364
|
+
const libPath = join6(dir, libName);
|
|
13365
|
+
if (process.platform === "win32") {
|
|
13366
|
+
if (!directoryContainsLibrary(dir, libName))
|
|
13367
|
+
continue;
|
|
13368
|
+
} else if (!existsSync5(libPath)) {
|
|
13285
13369
|
continue;
|
|
13286
13370
|
}
|
|
13371
|
+
let skipVersionCheck = false;
|
|
13372
|
+
if (process.platform === "win32") {
|
|
13373
|
+
const dirLower = dir.toLowerCase();
|
|
13374
|
+
const isCommonPath = dirLower.includes("program files") || dirLower.includes("onnxruntime");
|
|
13375
|
+
if (!isCommonPath)
|
|
13376
|
+
skipVersionCheck = true;
|
|
13377
|
+
}
|
|
13378
|
+
if (!skipVersionCheck) {
|
|
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
|
+
}
|
|
13384
|
+
}
|
|
13287
13385
|
return dir;
|
|
13288
13386
|
}
|
|
13289
13387
|
return null;
|
|
@@ -13321,7 +13419,7 @@ async function downloadFileWithCap(url, destPath) {
|
|
|
13321
13419
|
await pipeline2(nodeStream, createWriteStream2(destPath), { signal: controller.signal });
|
|
13322
13420
|
} catch (err) {
|
|
13323
13421
|
try {
|
|
13324
|
-
|
|
13422
|
+
unlinkSync3(destPath);
|
|
13325
13423
|
} catch {}
|
|
13326
13424
|
throw err;
|
|
13327
13425
|
} finally {
|
|
@@ -13382,7 +13480,7 @@ async function downloadOnnxRuntime(info, targetDir) {
|
|
|
13382
13480
|
await extractZipArchive(archivePath, tmpDir);
|
|
13383
13481
|
}
|
|
13384
13482
|
try {
|
|
13385
|
-
|
|
13483
|
+
unlinkSync3(archivePath);
|
|
13386
13484
|
} catch {}
|
|
13387
13485
|
validateExtractedTree(tmpDir);
|
|
13388
13486
|
const extractedDir = join6(tmpDir, info.assetName, "lib");
|
|
@@ -13431,7 +13529,7 @@ async function downloadOnnxRuntime(info, targetDir) {
|
|
|
13431
13529
|
return null;
|
|
13432
13530
|
}
|
|
13433
13531
|
}
|
|
13434
|
-
function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, copyFile =
|
|
13532
|
+
function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, copyFile = copyFileSync3) {
|
|
13435
13533
|
const requiredLibs = new Set([info.libName]);
|
|
13436
13534
|
for (const libFile of realFiles) {
|
|
13437
13535
|
const src = join6(extractedDir, libFile);
|
|
@@ -13452,7 +13550,7 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
|
|
|
13452
13550
|
for (const link of symlinks) {
|
|
13453
13551
|
const dst = join6(targetDir, link.name);
|
|
13454
13552
|
try {
|
|
13455
|
-
|
|
13553
|
+
unlinkSync3(dst);
|
|
13456
13554
|
} catch {}
|
|
13457
13555
|
try {
|
|
13458
13556
|
symlinkSync(link.target, dst);
|
|
@@ -13563,7 +13661,7 @@ ${new Date().toISOString()}
|
|
|
13563
13661
|
}
|
|
13564
13662
|
log(`[onnx] reclaiming install lock (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
|
|
13565
13663
|
try {
|
|
13566
|
-
|
|
13664
|
+
unlinkSync3(lockPath);
|
|
13567
13665
|
} catch {}
|
|
13568
13666
|
return tryClaim();
|
|
13569
13667
|
}
|
|
@@ -13588,7 +13686,7 @@ function releaseLock(lockPath) {
|
|
|
13588
13686
|
return;
|
|
13589
13687
|
}
|
|
13590
13688
|
try {
|
|
13591
|
-
|
|
13689
|
+
unlinkSync3(lockPath);
|
|
13592
13690
|
} catch (unlinkErr) {
|
|
13593
13691
|
const code = unlinkErr.code;
|
|
13594
13692
|
if (code !== "ENOENT") {
|
|
@@ -14534,7 +14632,7 @@ import {
|
|
|
14534
14632
|
// package.json
|
|
14535
14633
|
var package_default = {
|
|
14536
14634
|
name: "@cortexkit/aft-pi",
|
|
14537
|
-
version: "0.
|
|
14635
|
+
version: "0.33.0",
|
|
14538
14636
|
type: "module",
|
|
14539
14637
|
description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
|
|
14540
14638
|
main: "dist/index.js",
|
|
@@ -14542,7 +14640,7 @@ var package_default = {
|
|
|
14542
14640
|
license: "MIT",
|
|
14543
14641
|
repository: {
|
|
14544
14642
|
type: "git",
|
|
14545
|
-
url: "https://github.com/cortexkit/aft"
|
|
14643
|
+
url: "git+https://github.com/cortexkit/aft.git"
|
|
14546
14644
|
},
|
|
14547
14645
|
files: [
|
|
14548
14646
|
"dist",
|
|
@@ -14556,7 +14654,7 @@ var package_default = {
|
|
|
14556
14654
|
prepublishOnly: "bun run build"
|
|
14557
14655
|
},
|
|
14558
14656
|
dependencies: {
|
|
14559
|
-
"@cortexkit/aft-bridge": "0.
|
|
14657
|
+
"@cortexkit/aft-bridge": "0.33.0",
|
|
14560
14658
|
"@xterm/headless": "^5.5.0",
|
|
14561
14659
|
"comment-json": "^5.0.0",
|
|
14562
14660
|
diff: "^8.0.4",
|
|
@@ -14564,12 +14662,12 @@ var package_default = {
|
|
|
14564
14662
|
zod: "^4.1.8"
|
|
14565
14663
|
},
|
|
14566
14664
|
optionalDependencies: {
|
|
14567
|
-
"@cortexkit/aft-darwin-arm64": "0.
|
|
14568
|
-
"@cortexkit/aft-darwin-x64": "0.
|
|
14569
|
-
"@cortexkit/aft-linux-arm64": "0.
|
|
14570
|
-
"@cortexkit/aft-linux-x64": "0.
|
|
14571
|
-
"@cortexkit/aft-win32-arm64": "0.
|
|
14572
|
-
"@cortexkit/aft-win32-x64": "0.
|
|
14665
|
+
"@cortexkit/aft-darwin-arm64": "0.33.0",
|
|
14666
|
+
"@cortexkit/aft-darwin-x64": "0.33.0",
|
|
14667
|
+
"@cortexkit/aft-linux-arm64": "0.33.0",
|
|
14668
|
+
"@cortexkit/aft-linux-x64": "0.33.0",
|
|
14669
|
+
"@cortexkit/aft-win32-arm64": "0.33.0",
|
|
14670
|
+
"@cortexkit/aft-win32-x64": "0.33.0"
|
|
14573
14671
|
},
|
|
14574
14672
|
devDependencies: {
|
|
14575
14673
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -15216,7 +15314,7 @@ function registerStatusCommand(pi, ctx) {
|
|
|
15216
15314
|
|
|
15217
15315
|
// src/config.ts
|
|
15218
15316
|
var import_comment_json = __toESM(require_src2(), 1);
|
|
15219
|
-
import { existsSync as existsSync6, readFileSync as readFileSync4, renameSync as renameSync4, unlinkSync as
|
|
15317
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, renameSync as renameSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
15220
15318
|
import { homedir as homedir6 } from "node:os";
|
|
15221
15319
|
import { join as join8 } from "node:path";
|
|
15222
15320
|
|
|
@@ -28847,6 +28945,7 @@ var LspConfigSchema = exports_external.object({
|
|
|
28847
28945
|
servers: exports_external.record(exports_external.string().trim().min(1), LspServerEntrySchema).optional(),
|
|
28848
28946
|
disabled: exports_external.array(exports_external.string().trim().min(1)).optional(),
|
|
28849
28947
|
python: exports_external.enum(["pyright", "ty", "auto"]).optional(),
|
|
28948
|
+
diagnostics_on_edit: exports_external.boolean().optional(),
|
|
28850
28949
|
auto_install: exports_external.boolean().optional(),
|
|
28851
28950
|
grace_days: exports_external.number().int().positive().optional(),
|
|
28852
28951
|
versions: exports_external.record(exports_external.string().trim().min(1), exports_external.string().trim().min(1)).optional()
|
|
@@ -28869,6 +28968,24 @@ var BashFeaturesSchema = exports_external.object({
|
|
|
28869
28968
|
long_running_reminder_interval_ms: exports_external.number().int().positive().optional()
|
|
28870
28969
|
});
|
|
28871
28970
|
var BashConfigSchema = exports_external.union([exports_external.boolean(), BashFeaturesSchema]);
|
|
28971
|
+
var InspectConfigSchema = exports_external.object({
|
|
28972
|
+
enabled: exports_external.boolean().optional(),
|
|
28973
|
+
tier2_idle_minutes: exports_external.number().min(0).optional(),
|
|
28974
|
+
categories: exports_external.record(exports_external.string(), exports_external.boolean()).optional(),
|
|
28975
|
+
tier2_soft_deadline_ms: exports_external.number().int().positive().optional(),
|
|
28976
|
+
max_drill_down_items: exports_external.number().int().positive().max(100).optional(),
|
|
28977
|
+
duplicates: exports_external.object({
|
|
28978
|
+
lower_bound: exports_external.number().int().positive().optional(),
|
|
28979
|
+
discard_cost: exports_external.number().int().min(0).optional(),
|
|
28980
|
+
anonymize: exports_external.object({
|
|
28981
|
+
variables: exports_external.boolean().optional(),
|
|
28982
|
+
fields: exports_external.boolean().optional(),
|
|
28983
|
+
methods: exports_external.boolean().optional(),
|
|
28984
|
+
types: exports_external.boolean().optional(),
|
|
28985
|
+
literals: exports_external.boolean().optional()
|
|
28986
|
+
}).optional()
|
|
28987
|
+
}).optional()
|
|
28988
|
+
});
|
|
28872
28989
|
var AftConfigSchema = exports_external.object({
|
|
28873
28990
|
$schema: exports_external.string().optional(),
|
|
28874
28991
|
format_on_edit: exports_external.boolean().optional(),
|
|
@@ -28881,6 +28998,7 @@ var AftConfigSchema = exports_external.object({
|
|
|
28881
28998
|
restrict_to_project_root: exports_external.boolean().optional(),
|
|
28882
28999
|
search_index: exports_external.boolean().optional(),
|
|
28883
29000
|
semantic_search: exports_external.boolean().optional(),
|
|
29001
|
+
inspect: InspectConfigSchema.optional(),
|
|
28884
29002
|
bash: BashConfigSchema.optional(),
|
|
28885
29003
|
experimental: ExperimentalConfigSchema.optional(),
|
|
28886
29004
|
lsp: LspConfigSchema.optional(),
|
|
@@ -29091,7 +29209,7 @@ ${serialized}` : serialized;
|
|
|
29091
29209
|
} catch (err) {
|
|
29092
29210
|
if (tmpPath) {
|
|
29093
29211
|
try {
|
|
29094
|
-
|
|
29212
|
+
unlinkSync4(tmpPath);
|
|
29095
29213
|
} catch {}
|
|
29096
29214
|
}
|
|
29097
29215
|
if (isWritableMigrationError(err)) {
|
|
@@ -29175,6 +29293,9 @@ function mergeLspConfig(base, override) {
|
|
|
29175
29293
|
const projectSafe = {};
|
|
29176
29294
|
if (override?.python !== undefined)
|
|
29177
29295
|
projectSafe.python = override.python;
|
|
29296
|
+
if (override?.diagnostics_on_edit !== undefined) {
|
|
29297
|
+
projectSafe.diagnostics_on_edit = override.diagnostics_on_edit;
|
|
29298
|
+
}
|
|
29178
29299
|
const userDisabled = base?.disabled ?? [];
|
|
29179
29300
|
const lsp = {
|
|
29180
29301
|
...base,
|
|
@@ -29185,6 +29306,27 @@ function mergeLspConfig(base, override) {
|
|
|
29185
29306
|
return;
|
|
29186
29307
|
return Object.fromEntries(Object.entries(lsp).filter(([, v]) => v !== undefined));
|
|
29187
29308
|
}
|
|
29309
|
+
function mergeInspectConfig(baseInspect, overrideInspect) {
|
|
29310
|
+
const inspect = {
|
|
29311
|
+
...baseInspect,
|
|
29312
|
+
...overrideInspect,
|
|
29313
|
+
duplicates: baseInspect?.duplicates || overrideInspect?.duplicates ? {
|
|
29314
|
+
...baseInspect?.duplicates,
|
|
29315
|
+
...overrideInspect?.duplicates,
|
|
29316
|
+
anonymize: baseInspect?.duplicates?.anonymize || overrideInspect?.duplicates?.anonymize ? {
|
|
29317
|
+
...baseInspect?.duplicates?.anonymize,
|
|
29318
|
+
...overrideInspect?.duplicates?.anonymize
|
|
29319
|
+
} : undefined
|
|
29320
|
+
} : undefined
|
|
29321
|
+
};
|
|
29322
|
+
if (inspect.duplicates && inspect.duplicates.anonymize === undefined) {
|
|
29323
|
+
delete inspect.duplicates.anonymize;
|
|
29324
|
+
}
|
|
29325
|
+
if (Object.values(inspect).every((value) => value === undefined)) {
|
|
29326
|
+
return;
|
|
29327
|
+
}
|
|
29328
|
+
return Object.fromEntries(Object.entries(inspect).filter(([, value]) => value !== undefined));
|
|
29329
|
+
}
|
|
29188
29330
|
function mergeBashConfig(baseBash, overrideBash) {
|
|
29189
29331
|
if (baseBash === undefined && overrideBash === undefined)
|
|
29190
29332
|
return;
|
|
@@ -29241,6 +29383,7 @@ var PROJECT_SAFE_TOP_LEVEL_FIELDS = new Set([
|
|
|
29241
29383
|
"validate_on_edit",
|
|
29242
29384
|
"search_index",
|
|
29243
29385
|
"semantic_search",
|
|
29386
|
+
"inspect",
|
|
29244
29387
|
"experimental",
|
|
29245
29388
|
"bash"
|
|
29246
29389
|
]);
|
|
@@ -29271,8 +29414,10 @@ function mergeConfigs(base, override) {
|
|
|
29271
29414
|
const lsp = mergeLspConfig(base.lsp, override.lsp);
|
|
29272
29415
|
const experimental = mergeExperimentalConfig(base.experimental, override.experimental);
|
|
29273
29416
|
const bash = mergeBashConfig(base.bash, override.bash);
|
|
29417
|
+
const inspect = mergeInspectConfig(base.inspect, override.inspect);
|
|
29274
29418
|
const safeOverride = pickProjectSafeFields(override);
|
|
29275
29419
|
delete safeOverride.bash;
|
|
29420
|
+
delete safeOverride.inspect;
|
|
29276
29421
|
return {
|
|
29277
29422
|
...base,
|
|
29278
29423
|
...safeOverride,
|
|
@@ -29280,6 +29425,7 @@ function mergeConfigs(base, override) {
|
|
|
29280
29425
|
...Object.keys(checker).length > 0 ? { checker } : {},
|
|
29281
29426
|
...lsp ? { lsp } : {},
|
|
29282
29427
|
...bash !== undefined ? { bash } : {},
|
|
29428
|
+
...inspect !== undefined ? { inspect } : {},
|
|
29283
29429
|
experimental,
|
|
29284
29430
|
semantic,
|
|
29285
29431
|
...disabledTools.length > 0 ? { disabled_tools: [...new Set(disabledTools)] } : {}
|
|
@@ -29331,7 +29477,7 @@ import {
|
|
|
29331
29477
|
openSync as openSync3,
|
|
29332
29478
|
readFileSync as readFileSync5,
|
|
29333
29479
|
statSync as statSync3,
|
|
29334
|
-
unlinkSync as
|
|
29480
|
+
unlinkSync as unlinkSync5,
|
|
29335
29481
|
writeFileSync as writeFileSync4
|
|
29336
29482
|
} from "node:fs";
|
|
29337
29483
|
import { homedir as homedir7 } from "node:os";
|
|
@@ -29461,7 +29607,7 @@ ${new Date().toISOString()}
|
|
|
29461
29607
|
}
|
|
29462
29608
|
log2(`[lsp] reclaiming install lock for ${lockKey} (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
|
|
29463
29609
|
try {
|
|
29464
|
-
|
|
29610
|
+
unlinkSync5(lock);
|
|
29465
29611
|
} catch {}
|
|
29466
29612
|
return tryClaim();
|
|
29467
29613
|
}
|
|
@@ -29498,7 +29644,7 @@ function releaseInstallLock(lockKey) {
|
|
|
29498
29644
|
return;
|
|
29499
29645
|
}
|
|
29500
29646
|
try {
|
|
29501
|
-
|
|
29647
|
+
unlinkSync5(lock);
|
|
29502
29648
|
} catch (unlinkErr) {
|
|
29503
29649
|
const code = unlinkErr.code;
|
|
29504
29650
|
if (code !== "ENOENT") {
|
|
@@ -30154,7 +30300,7 @@ function runAutoInstall(projectRoot, config2, fetchImpl = fetch) {
|
|
|
30154
30300
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
30155
30301
|
import { createHash as createHash4, randomBytes } from "node:crypto";
|
|
30156
30302
|
import {
|
|
30157
|
-
copyFileSync as
|
|
30303
|
+
copyFileSync as copyFileSync4,
|
|
30158
30304
|
createReadStream as createReadStream2,
|
|
30159
30305
|
createWriteStream as createWriteStream3,
|
|
30160
30306
|
existsSync as existsSync8,
|
|
@@ -30167,7 +30313,7 @@ import {
|
|
|
30167
30313
|
renameSync as renameSync6,
|
|
30168
30314
|
rmSync as rmSync4,
|
|
30169
30315
|
statSync as statSync5,
|
|
30170
|
-
unlinkSync as
|
|
30316
|
+
unlinkSync as unlinkSync6,
|
|
30171
30317
|
writeFileSync as writeFileSync5
|
|
30172
30318
|
} from "node:fs";
|
|
30173
30319
|
import { dirname as dirname4, join as join12, relative as relative2, resolve as resolve2 } from "node:path";
|
|
@@ -30529,7 +30675,7 @@ async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
|
|
|
30529
30675
|
await pipeline3(nodeStream, createWriteStream3(destPath), { signal: timeout.signal });
|
|
30530
30676
|
} catch (err) {
|
|
30531
30677
|
try {
|
|
30532
|
-
|
|
30678
|
+
unlinkSync6(destPath);
|
|
30533
30679
|
} catch {}
|
|
30534
30680
|
throw err;
|
|
30535
30681
|
} finally {
|
|
@@ -30761,7 +30907,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
|
|
|
30761
30907
|
} catch (err) {
|
|
30762
30908
|
error2(`[lsp] hash ${spec.id} failed: ${err}`);
|
|
30763
30909
|
try {
|
|
30764
|
-
|
|
30910
|
+
unlinkSync6(archivePath);
|
|
30765
30911
|
} catch {}
|
|
30766
30912
|
return null;
|
|
30767
30913
|
}
|
|
@@ -30772,7 +30918,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
|
|
|
30772
30918
|
if (previousArchiveSha256 !== archiveSha256) {
|
|
30773
30919
|
error2(`[lsp] ${spec.id} ${tag}: TOFU sha256 mismatch — refusing install. ` + `Previously installed archive sha256=${previousArchiveSha256}, downloaded sha256=${archiveSha256}. ` + `This means the published release for tag ${tag} changed. Investigate before proceeding.`);
|
|
30774
30920
|
try {
|
|
30775
|
-
|
|
30921
|
+
unlinkSync6(archivePath);
|
|
30776
30922
|
} catch {}
|
|
30777
30923
|
return null;
|
|
30778
30924
|
}
|
|
@@ -30784,7 +30930,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
|
|
|
30784
30930
|
return null;
|
|
30785
30931
|
} finally {
|
|
30786
30932
|
try {
|
|
30787
|
-
|
|
30933
|
+
unlinkSync6(archivePath);
|
|
30788
30934
|
} catch {}
|
|
30789
30935
|
}
|
|
30790
30936
|
const innerBinaryPath = join12(extractDir, spec.binaryPathInArchive(platform, arch, version2));
|
|
@@ -30795,7 +30941,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
|
|
|
30795
30941
|
const targetBinary = ghBinaryPath(spec, platform);
|
|
30796
30942
|
mkdirSync8(dirname4(targetBinary), { recursive: true });
|
|
30797
30943
|
try {
|
|
30798
|
-
|
|
30944
|
+
copyFileSync4(innerBinaryPath, targetBinary);
|
|
30799
30945
|
if (platform !== "win32") {
|
|
30800
30946
|
const { chmodSync: chmodSync4 } = await import("node:fs");
|
|
30801
30947
|
chmodSync4(targetBinary, 493);
|
|
@@ -30984,13 +31130,18 @@ function sendIgnoredMessage(client, sessionId, text) {
|
|
|
30984
31130
|
}
|
|
30985
31131
|
}
|
|
30986
31132
|
async function readWarnedTools(bridge) {
|
|
31133
|
+
let resp;
|
|
31134
|
+
try {
|
|
31135
|
+
resp = await bridge.send("db_get_state", { key: "warned_tools" });
|
|
31136
|
+
} catch {
|
|
31137
|
+
return null;
|
|
31138
|
+
}
|
|
31139
|
+
if (resp.success === false)
|
|
31140
|
+
return null;
|
|
31141
|
+
const value = resp.data?.value;
|
|
31142
|
+
if (typeof value !== "string")
|
|
31143
|
+
return {};
|
|
30987
31144
|
try {
|
|
30988
|
-
const resp = await bridge.send("db_get_state", { key: "warned_tools" });
|
|
30989
|
-
if (resp.success === false)
|
|
30990
|
-
return {};
|
|
30991
|
-
const value = resp.data?.value;
|
|
30992
|
-
if (typeof value !== "string")
|
|
30993
|
-
return {};
|
|
30994
31145
|
const parsed = JSON.parse(value);
|
|
30995
31146
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
30996
31147
|
return {};
|
|
@@ -30999,12 +31150,16 @@ async function readWarnedTools(bridge) {
|
|
|
30999
31150
|
return {};
|
|
31000
31151
|
}
|
|
31001
31152
|
}
|
|
31002
|
-
async function
|
|
31153
|
+
async function warnedStatus(bridge, key) {
|
|
31003
31154
|
const warned = await readWarnedTools(bridge);
|
|
31004
|
-
|
|
31155
|
+
if (warned === null)
|
|
31156
|
+
return "unknown";
|
|
31157
|
+
return warned[key] === true || typeof warned[key] === "string" ? "warned" : "fresh";
|
|
31005
31158
|
}
|
|
31006
31159
|
async function recordWarning(bridge, key) {
|
|
31007
31160
|
const warned = await readWarnedTools(bridge);
|
|
31161
|
+
if (warned === null)
|
|
31162
|
+
return;
|
|
31008
31163
|
warned[key] = true;
|
|
31009
31164
|
try {
|
|
31010
31165
|
await bridge.send("db_set_state", {
|
|
@@ -31053,7 +31208,7 @@ async function deliverConfigureWarnings(opts, warnings) {
|
|
|
31053
31208
|
return;
|
|
31054
31209
|
for (const warning of warnings) {
|
|
31055
31210
|
const key = warningKey(warning, opts.projectRoot);
|
|
31056
|
-
if (await
|
|
31211
|
+
if (await warnedStatus(opts.bridge, key) !== "fresh")
|
|
31057
31212
|
continue;
|
|
31058
31213
|
if (!sendIgnoredMessage(opts.client, opts.sessionId, formatConfigureWarning(warning)))
|
|
31059
31214
|
continue;
|
|
@@ -31321,24 +31476,6 @@ function groupByFile(items, getFile) {
|
|
|
31321
31476
|
});
|
|
31322
31477
|
return groups;
|
|
31323
31478
|
}
|
|
31324
|
-
function distinctCount(values) {
|
|
31325
|
-
return new Set(values.filter((value) => Boolean(value))).size;
|
|
31326
|
-
}
|
|
31327
|
-
function severityBadge(theme, severity) {
|
|
31328
|
-
const label = severity === "information" ? "info" : severity;
|
|
31329
|
-
switch (severity) {
|
|
31330
|
-
case "error":
|
|
31331
|
-
return theme.fg("error", `[${label}]`);
|
|
31332
|
-
case "warning":
|
|
31333
|
-
return theme.fg("warning", `[${label}]`);
|
|
31334
|
-
case "information":
|
|
31335
|
-
return theme.fg("accent", `[${label}]`);
|
|
31336
|
-
case "hint":
|
|
31337
|
-
return theme.fg("muted", `[${label}]`);
|
|
31338
|
-
default:
|
|
31339
|
-
return theme.fg("muted", `[${label}]`);
|
|
31340
|
-
}
|
|
31341
|
-
}
|
|
31342
31479
|
function formatTimestamp(value) {
|
|
31343
31480
|
if (typeof value === "string" && value.length > 0)
|
|
31344
31481
|
return value;
|
|
@@ -32541,6 +32678,10 @@ function formatDiffForPi(oldContent, newContent, contextLines = DEFAULT_CONTEXT_
|
|
|
32541
32678
|
}
|
|
32542
32679
|
|
|
32543
32680
|
// src/tools/hoisted.ts
|
|
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;
|
|
32684
|
+
}
|
|
32544
32685
|
function containsPath(parent, child) {
|
|
32545
32686
|
const rel = relative3(parent, child);
|
|
32546
32687
|
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
@@ -32607,7 +32748,8 @@ var WriteParams = Type6.Object({
|
|
|
32607
32748
|
filePath: Type6.String({
|
|
32608
32749
|
description: "Path to the file to write (absolute or relative to project root)"
|
|
32609
32750
|
}),
|
|
32610
|
-
content: Type6.String({ description: "Full file contents to write" })
|
|
32751
|
+
content: Type6.String({ description: "Full file contents to write" }),
|
|
32752
|
+
diagnostics: Type6.Optional(Type6.Boolean({ description: DIAGNOSTICS_PARAM_DESCRIPTION }))
|
|
32611
32753
|
});
|
|
32612
32754
|
var EditParams = Type6.Object({
|
|
32613
32755
|
filePath: Type6.String({
|
|
@@ -32619,7 +32761,8 @@ var EditParams = Type6.Object({
|
|
|
32619
32761
|
occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER),
|
|
32620
32762
|
appendContent: Type6.Optional(Type6.String({
|
|
32621
32763
|
description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). When set, oldString/newString are ignored."
|
|
32622
|
-
}))
|
|
32764
|
+
})),
|
|
32765
|
+
diagnostics: Type6.Optional(Type6.Boolean({ description: DIAGNOSTICS_PARAM_DESCRIPTION }))
|
|
32623
32766
|
});
|
|
32624
32767
|
var GrepParams = Type6.Object({
|
|
32625
32768
|
pattern: Type6.String({ description: "Regex pattern to search for" }),
|
|
@@ -32670,8 +32813,8 @@ function registerHoistedTools(pi, ctx, surface) {
|
|
|
32670
32813
|
pi.registerTool({
|
|
32671
32814
|
name: "write",
|
|
32672
32815
|
label: "write",
|
|
32673
|
-
description: "Write a file atomically with per-file backup
|
|
32674
|
-
promptSnippet: "Create or overwrite files (uses filePath; auto-formats;
|
|
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)",
|
|
32675
32818
|
promptGuidelines: ["Use write only for new files or complete rewrites."],
|
|
32676
32819
|
parameters: WriteParams,
|
|
32677
32820
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
@@ -32682,8 +32825,8 @@ function registerHoistedTools(pi, ctx, surface) {
|
|
|
32682
32825
|
const response = await callBridge(bridge, "write", {
|
|
32683
32826
|
file: params.filePath,
|
|
32684
32827
|
content: params.content,
|
|
32685
|
-
diagnostics:
|
|
32686
|
-
|
|
32828
|
+
diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
|
|
32829
|
+
include_diff_content: true
|
|
32687
32830
|
}, extCtx);
|
|
32688
32831
|
return buildMutationResult(params.filePath, response);
|
|
32689
32832
|
},
|
|
@@ -32699,8 +32842,8 @@ function registerHoistedTools(pi, ctx, surface) {
|
|
|
32699
32842
|
pi.registerTool({
|
|
32700
32843
|
name: "edit",
|
|
32701
32844
|
label: "edit",
|
|
32702
|
-
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`.
|
|
32703
|
-
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).",
|
|
32845
|
+
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.",
|
|
32846
|
+
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.",
|
|
32704
32847
|
promptGuidelines: [
|
|
32705
32848
|
"Prefer edit over write when changing part of an existing file.",
|
|
32706
32849
|
"Include enough surrounding context in oldString to make the match unique, or set replaceAll/occurrence explicitly.",
|
|
@@ -32717,8 +32860,8 @@ function registerHoistedTools(pi, ctx, surface) {
|
|
|
32717
32860
|
op: "append",
|
|
32718
32861
|
file: params.filePath,
|
|
32719
32862
|
append_content: params.appendContent,
|
|
32720
|
-
diagnostics:
|
|
32721
|
-
|
|
32863
|
+
diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
|
|
32864
|
+
include_diff_content: true
|
|
32722
32865
|
};
|
|
32723
32866
|
const response2 = await callBridge(bridge, "edit_match", req2, extCtx);
|
|
32724
32867
|
return buildMutationResult(params.filePath, response2);
|
|
@@ -32727,8 +32870,8 @@ function registerHoistedTools(pi, ctx, surface) {
|
|
|
32727
32870
|
file: params.filePath,
|
|
32728
32871
|
match: params.oldString ?? "",
|
|
32729
32872
|
replacement: params.newString ?? "",
|
|
32730
|
-
diagnostics:
|
|
32731
|
-
|
|
32873
|
+
diagnostics: params.diagnostics ?? diagnosticsOnEditDefault(ctx),
|
|
32874
|
+
include_diff_content: true
|
|
32732
32875
|
};
|
|
32733
32876
|
if (params.replaceAll === true)
|
|
32734
32877
|
req.replace_all = true;
|
|
@@ -32797,15 +32940,6 @@ function buildMutationResult(filePath, response) {
|
|
|
32797
32940
|
}
|
|
32798
32941
|
const summaryHeader = replacements !== undefined ? `Edited ${filePath} (+${additions}/-${deletions}, ${replacements} replacement${replacements === 1 ? "" : "s"})` : `Wrote ${filePath} (+${additions}/-${deletions})`;
|
|
32799
32942
|
let text = summaryHeader;
|
|
32800
|
-
if (diffText)
|
|
32801
|
-
text += `
|
|
32802
|
-
|
|
32803
|
-
${diffText}`;
|
|
32804
|
-
if (truncated) {
|
|
32805
|
-
text += `
|
|
32806
|
-
|
|
32807
|
-
(diff truncated — file too large to include before/after content)`;
|
|
32808
|
-
}
|
|
32809
32943
|
if (noOp) {
|
|
32810
32944
|
text += `
|
|
32811
32945
|
|
|
@@ -33085,106 +33219,234 @@ function registerImportTools(pi, ctx) {
|
|
|
33085
33219
|
});
|
|
33086
33220
|
}
|
|
33087
33221
|
|
|
33088
|
-
// src/tools/
|
|
33089
|
-
import { StringEnum as StringEnum3 } from "@earendil-works/pi-ai";
|
|
33222
|
+
// src/tools/inspect.ts
|
|
33090
33223
|
import { Type as Type8 } from "typebox";
|
|
33091
|
-
var
|
|
33092
|
-
|
|
33093
|
-
|
|
33094
|
-
description: "Directory to get diagnostics for (mutually exclusive with filePath)"
|
|
33224
|
+
var InspectParams = Type8.Object({
|
|
33225
|
+
sections: Type8.Optional(Type8.Union([Type8.String(), Type8.Array(Type8.String())], {
|
|
33226
|
+
description: "Categories to include in detailed drill-down (e.g. 'todos' or ['todos', 'dead_code']). Use 'all' for every active category. Omit for summary-only mode."
|
|
33095
33227
|
})),
|
|
33096
|
-
|
|
33097
|
-
description: "
|
|
33228
|
+
scope: Type8.Optional(Type8.Union([Type8.String(), Type8.Array(Type8.String())], {
|
|
33229
|
+
description: "Restrict scan/results to paths under this scope (file or directory, absolute or relative to project root). Tier 1 scopes the scan; Tier 2 scans project-wide and applies scope as a result filter."
|
|
33098
33230
|
})),
|
|
33099
|
-
|
|
33100
|
-
|
|
33231
|
+
topK: Type8.Optional(Type8.Integer({
|
|
33232
|
+
minimum: 1,
|
|
33233
|
+
maximum: 100,
|
|
33234
|
+
default: 20,
|
|
33235
|
+
description: "Max drill-down items per category. Default 20, max 100."
|
|
33101
33236
|
}))
|
|
33102
33237
|
});
|
|
33103
|
-
|
|
33104
|
-
|
|
33105
|
-
|
|
33106
|
-
|
|
33107
|
-
|
|
33108
|
-
|
|
33109
|
-
|
|
33110
|
-
|
|
33111
|
-
|
|
33112
|
-
|
|
33113
|
-
|
|
33114
|
-
if (
|
|
33115
|
-
|
|
33116
|
-
return sections;
|
|
33238
|
+
var TIER2_INSPECT_CATEGORIES = new Set(["dead_code", "unused_exports", "duplicates"]);
|
|
33239
|
+
var INSPECT_TIER2_RUN_TIMEOUT_MS = 5 * 60000;
|
|
33240
|
+
var INSPECT_TIER2_MIN_TRIGGER_INTERVAL_MS = 4 * 60000;
|
|
33241
|
+
var runningTier2Categories = new WeakMap;
|
|
33242
|
+
var lastTier2TriggerAtByBridge = new WeakMap;
|
|
33243
|
+
function normalizeStringOrArray(value) {
|
|
33244
|
+
return isEmptyParam(value) ? undefined : value;
|
|
33245
|
+
}
|
|
33246
|
+
function validateOptionalTopK(value) {
|
|
33247
|
+
if (value === undefined || value === null || value === "")
|
|
33248
|
+
return;
|
|
33249
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
33250
|
+
throw new Error("topK must be an integer between 1 and 100");
|
|
33117
33251
|
}
|
|
33118
|
-
|
|
33119
|
-
|
|
33120
|
-
|
|
33121
|
-
|
|
33122
|
-
|
|
33123
|
-
|
|
33124
|
-
|
|
33125
|
-
|
|
33126
|
-
|
|
33127
|
-
|
|
33128
|
-
|
|
33129
|
-
|
|
33130
|
-
|
|
33131
|
-
|
|
33252
|
+
if (value < 1 || value > 100) {
|
|
33253
|
+
throw new Error("topK must be between 1 and 100");
|
|
33254
|
+
}
|
|
33255
|
+
return value;
|
|
33256
|
+
}
|
|
33257
|
+
function diagnosticsServerSummary(section) {
|
|
33258
|
+
const pending = Array.isArray(section.servers_pending) ? section.servers_pending.filter((item) => typeof item === "string") : [];
|
|
33259
|
+
const notInstalled = Array.isArray(section.servers_not_installed) ? section.servers_not_installed.filter((item) => typeof item === "string") : [];
|
|
33260
|
+
const parts = [];
|
|
33261
|
+
if (pending.length > 0)
|
|
33262
|
+
parts.push(`pending: ${pending.join(", ")}`);
|
|
33263
|
+
if (notInstalled.length > 0)
|
|
33264
|
+
parts.push(`not installed: ${notInstalled.join(", ")}`);
|
|
33265
|
+
return parts.length > 0 ? parts.join("; ") : "none reported";
|
|
33266
|
+
}
|
|
33267
|
+
function diagnosticsSummaryPart(summary) {
|
|
33268
|
+
const section = asRecord2(summary?.diagnostics);
|
|
33269
|
+
if (!section)
|
|
33270
|
+
return;
|
|
33271
|
+
const errors3 = asNumber(section.errors);
|
|
33272
|
+
const warnings = asNumber(section.warnings);
|
|
33273
|
+
const info = asNumber(section.info);
|
|
33274
|
+
const hints = asNumber(section.hints);
|
|
33275
|
+
const hasCounts = [errors3, warnings, info, hints].some((value) => value !== undefined);
|
|
33276
|
+
const counts = `${errors3 ?? 0} errors/${warnings ?? 0} warnings/${info ?? 0} info/${hints ?? 0} hints`;
|
|
33277
|
+
const status = asString(section.status);
|
|
33278
|
+
if (status === "pending") {
|
|
33279
|
+
return hasCounts ? `diagnostics ${counts} so far — still pending (servers: ${diagnosticsServerSummary(section)})` : `diagnostics pending (servers: ${diagnosticsServerSummary(section)})`;
|
|
33280
|
+
}
|
|
33281
|
+
if (status === "incomplete") {
|
|
33282
|
+
return hasCounts ? `diagnostics ${counts} (incomplete — servers: ${diagnosticsServerSummary(section)})` : `diagnostics unavailable (status incomplete; servers: ${diagnosticsServerSummary(section)})`;
|
|
33283
|
+
}
|
|
33284
|
+
if (hasCounts) {
|
|
33285
|
+
return `diagnostics ${counts}`;
|
|
33132
33286
|
}
|
|
33287
|
+
return;
|
|
33288
|
+
}
|
|
33289
|
+
function diagnosticLocation(diagnostic) {
|
|
33290
|
+
const file2 = asString(diagnostic.file) ?? "(unknown file)";
|
|
33291
|
+
const line = asNumber(diagnostic.line);
|
|
33292
|
+
const column = asNumber(diagnostic.column);
|
|
33293
|
+
if (line === undefined)
|
|
33294
|
+
return file2;
|
|
33295
|
+
if (column === undefined)
|
|
33296
|
+
return `${file2}:${line}`;
|
|
33297
|
+
return `${file2}:${line}:${column}`;
|
|
33298
|
+
}
|
|
33299
|
+
function diagnosticsDetailSection(details) {
|
|
33300
|
+
const diagnostics = asRecords(details?.diagnostics);
|
|
33301
|
+
if (diagnostics.length === 0)
|
|
33302
|
+
return;
|
|
33303
|
+
const lines = ["diagnostics"];
|
|
33304
|
+
for (const diagnostic of diagnostics) {
|
|
33305
|
+
const severity = asString(diagnostic.severity) ?? "information";
|
|
33306
|
+
const message = asString(diagnostic.message) ?? "(no message)";
|
|
33307
|
+
const source = asString(diagnostic.source);
|
|
33308
|
+
const suffix = source ? ` [${source}]` : "";
|
|
33309
|
+
lines.push(`- ${diagnosticLocation(diagnostic)} ${severity} ${message}${suffix}`);
|
|
33310
|
+
}
|
|
33311
|
+
return lines.join(`
|
|
33312
|
+
`);
|
|
33313
|
+
}
|
|
33314
|
+
function countFrom(summary, key) {
|
|
33315
|
+
const section = asRecord2(summary?.[key]);
|
|
33316
|
+
return asNumber(section?.count);
|
|
33317
|
+
}
|
|
33318
|
+
function tier2SummaryPart(summary, key, label) {
|
|
33319
|
+
const section = asRecord2(summary?.[key]);
|
|
33320
|
+
const count = asNumber(section?.count);
|
|
33321
|
+
if (count !== undefined)
|
|
33322
|
+
return `${label} ${count}`;
|
|
33323
|
+
const status = asString(section?.status);
|
|
33324
|
+
return `${label} ${status ?? "unavailable"}`;
|
|
33325
|
+
}
|
|
33326
|
+
function tier2RefreshCategories(response) {
|
|
33327
|
+
const scannerState = asRecord2(response.scanner_state);
|
|
33328
|
+
const categories = new Set;
|
|
33329
|
+
for (const key of ["pending_categories", "stale_categories"]) {
|
|
33330
|
+
const values = scannerState?.[key];
|
|
33331
|
+
if (!Array.isArray(values))
|
|
33332
|
+
continue;
|
|
33333
|
+
for (const category of values) {
|
|
33334
|
+
if (typeof category === "string" && TIER2_INSPECT_CATEGORIES.has(category)) {
|
|
33335
|
+
categories.add(category);
|
|
33336
|
+
}
|
|
33337
|
+
}
|
|
33338
|
+
}
|
|
33339
|
+
return [...categories];
|
|
33340
|
+
}
|
|
33341
|
+
function runPendingTier2Categories(bridge, categories, extCtx) {
|
|
33342
|
+
const now = Date.now();
|
|
33343
|
+
const running = runningTier2Categories.get(bridge) ?? new Set;
|
|
33344
|
+
const lastTriggerAt = lastTier2TriggerAtByBridge.get(bridge) ?? new Map;
|
|
33345
|
+
const toRun = categories.filter((category) => {
|
|
33346
|
+
if (running.has(category))
|
|
33347
|
+
return false;
|
|
33348
|
+
const previousTriggerAt = lastTriggerAt.get(category);
|
|
33349
|
+
return previousTriggerAt === undefined || previousTriggerAt + INSPECT_TIER2_MIN_TRIGGER_INTERVAL_MS <= now;
|
|
33350
|
+
});
|
|
33351
|
+
if (toRun.length === 0)
|
|
33352
|
+
return;
|
|
33353
|
+
for (const category of toRun) {
|
|
33354
|
+
running.add(category);
|
|
33355
|
+
lastTriggerAt.set(category, now);
|
|
33356
|
+
}
|
|
33357
|
+
runningTier2Categories.set(bridge, running);
|
|
33358
|
+
lastTier2TriggerAtByBridge.set(bridge, lastTriggerAt);
|
|
33359
|
+
callBridge(bridge, "inspect_tier2_run", { categories: toRun }, extCtx, {
|
|
33360
|
+
transportTimeoutMs: INSPECT_TIER2_RUN_TIMEOUT_MS
|
|
33361
|
+
}).catch(() => {}).finally(() => {
|
|
33362
|
+
const active = runningTier2Categories.get(bridge);
|
|
33363
|
+
if (!active)
|
|
33364
|
+
return;
|
|
33365
|
+
for (const category of toRun) {
|
|
33366
|
+
active.delete(category);
|
|
33367
|
+
}
|
|
33368
|
+
if (active.size === 0) {
|
|
33369
|
+
runningTier2Categories.delete(bridge);
|
|
33370
|
+
}
|
|
33371
|
+
});
|
|
33372
|
+
}
|
|
33373
|
+
function buildInspectSections(payload, theme) {
|
|
33374
|
+
const response = asRecord2(payload);
|
|
33375
|
+
if (!response)
|
|
33376
|
+
return [theme.fg("muted", "No inspect snapshot available.")];
|
|
33377
|
+
const summary = asRecord2(response.summary);
|
|
33378
|
+
const metrics = asRecord2(summary?.metrics);
|
|
33379
|
+
const scannerState = asRecord2(response.scanner_state);
|
|
33380
|
+
const stale = Array.isArray(scannerState?.stale_categories) ? scannerState.stale_categories.length : 0;
|
|
33381
|
+
const pending = Array.isArray(scannerState?.pending_categories) ? scannerState.pending_categories.length : 0;
|
|
33382
|
+
const parts = [
|
|
33383
|
+
`todos ${countFrom(summary, "todos") ?? 0}`,
|
|
33384
|
+
diagnosticsSummaryPart(summary),
|
|
33385
|
+
`metrics ${asNumber(metrics?.files) ?? 0} files/${asNumber(metrics?.symbols) ?? 0} symbols`,
|
|
33386
|
+
tier2SummaryPart(summary, "dead_code", "dead code"),
|
|
33387
|
+
tier2SummaryPart(summary, "unused_exports", "unused exports"),
|
|
33388
|
+
tier2SummaryPart(summary, "duplicates", "duplicates")
|
|
33389
|
+
].filter((part) => Boolean(part));
|
|
33390
|
+
const sections = [theme.fg("accent", parts.join(" · "))];
|
|
33391
|
+
if (stale > 0 || pending > 0) {
|
|
33392
|
+
sections.push(theme.fg("warning", `scanner state: ${stale} stale · ${pending} pending`));
|
|
33393
|
+
}
|
|
33394
|
+
const details = asRecord2(response.details);
|
|
33395
|
+
if (details) {
|
|
33396
|
+
const names = Object.keys(details);
|
|
33397
|
+
sections.push(names.length > 0 ? `details: ${names.join(", ")}` : theme.fg("muted", "No drill-down details returned."));
|
|
33398
|
+
const diagnosticsDetails = diagnosticsDetailSection(details);
|
|
33399
|
+
if (diagnosticsDetails)
|
|
33400
|
+
sections.push(diagnosticsDetails);
|
|
33401
|
+
}
|
|
33402
|
+
const text = asString(response.text);
|
|
33403
|
+
if (text)
|
|
33404
|
+
sections.push(text);
|
|
33133
33405
|
return sections;
|
|
33134
33406
|
}
|
|
33135
|
-
function
|
|
33136
|
-
const
|
|
33137
|
-
const
|
|
33138
|
-
|
|
33139
|
-
|
|
33140
|
-
].filter(Boolean).join(" ");
|
|
33141
|
-
return renderToolCall("lsp diagnostics", summary, theme, context);
|
|
33407
|
+
function renderInspectCall(args, theme, context) {
|
|
33408
|
+
const sections = Array.isArray(args.sections) ? `${args.sections.length} sections` : args.sections;
|
|
33409
|
+
const scope = Array.isArray(args.scope) ? `${args.scope.length} scopes` : args.scope;
|
|
33410
|
+
const summary = [sections, scope, args.topK ? `topK=${args.topK}` : undefined].filter(Boolean).join(" ");
|
|
33411
|
+
return renderToolCall("inspect", summary ? theme.fg("toolOutput", summary) : undefined, theme, context);
|
|
33142
33412
|
}
|
|
33143
|
-
function
|
|
33413
|
+
function renderInspectResult(result, theme, context) {
|
|
33144
33414
|
if (context.isError)
|
|
33145
|
-
return renderErrorResult(result, "
|
|
33146
|
-
return renderSections(
|
|
33415
|
+
return renderErrorResult(result, "inspect failed", theme, context);
|
|
33416
|
+
return renderSections(buildInspectSections(extractStructuredPayload(result), theme), context);
|
|
33147
33417
|
}
|
|
33148
|
-
function
|
|
33418
|
+
function registerInspectTool(pi, ctx) {
|
|
33149
33419
|
pi.registerTool({
|
|
33150
|
-
name: "
|
|
33151
|
-
label: "
|
|
33152
|
-
description: "
|
|
33153
|
-
|
|
33420
|
+
name: "aft_inspect",
|
|
33421
|
+
label: "inspect",
|
|
33422
|
+
description: "Codebase health snapshot. One call returns summary stats for: TODOs, diagnostics, file/symbol metrics, dead code, unused exports, code duplicates. Pass `sections` for per-category drill-down details.\n\n" + "Categories run in tiers — Tier 1 (todos, metrics) return synchronously from cache. Tier 2 (dead_code, unused_exports, duplicates) run asynchronously on demand: when a call sees cold `pending_categories: [...]` or stale `stale_categories: [...]`, Pi quietly starts a background Tier 2 warmup (deduped while in-flight and rate-limited per category to at most once every 4 minutes, matching OpenCode's default idle window). The current call may still return pending results while the cache warms; a later call can use cached data.\n\n" + `Use when: starting work on unfamiliar code, after multi-edit batches to check diagnostics, before a refactor, before review, or to verify cleanup completeness.
|
|
33423
|
+
|
|
33424
|
+
` + "Treat `dead_code` as a hint, not proof: reachability is call-based, so symbols reached only via method dispatch or referenced only in type position may be false positives — verify before deleting.",
|
|
33425
|
+
parameters: InspectParams,
|
|
33154
33426
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33155
|
-
const hasFile = typeof params.filePath === "string" && params.filePath.length > 0;
|
|
33156
|
-
const hasDir = typeof params.directory === "string" && params.directory.length > 0;
|
|
33157
|
-
if (hasFile && hasDir) {
|
|
33158
|
-
throw new Error("'filePath' and 'directory' are mutually exclusive — provide one or neither");
|
|
33159
|
-
}
|
|
33160
33427
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
33161
|
-
const
|
|
33162
|
-
|
|
33163
|
-
|
|
33164
|
-
|
|
33165
|
-
|
|
33166
|
-
|
|
33167
|
-
req.severity = params.severity;
|
|
33168
|
-
if (params.waitMs !== undefined)
|
|
33169
|
-
req.wait_ms = params.waitMs;
|
|
33170
|
-
const response = await callBridge(bridge, "lsp_diagnostics", req, extCtx);
|
|
33171
|
-
return textResult(JSON.stringify(response, null, 2));
|
|
33428
|
+
const sections = normalizeStringOrArray(params.sections);
|
|
33429
|
+
const scope = normalizeStringOrArray(params.scope);
|
|
33430
|
+
const topK = validateOptionalTopK(params.topK);
|
|
33431
|
+
const response = await callBridge(bridge, "inspect", { sections, scope, topK }, extCtx);
|
|
33432
|
+
runPendingTier2Categories(bridge, tier2RefreshCategories(response), extCtx);
|
|
33433
|
+
return textResult(response.text ?? JSON.stringify(response, null, 2), response);
|
|
33172
33434
|
},
|
|
33173
33435
|
renderCall(args, theme, context) {
|
|
33174
|
-
return
|
|
33436
|
+
return renderInspectCall(args, theme, context);
|
|
33175
33437
|
},
|
|
33176
33438
|
renderResult(result, _options, theme, context) {
|
|
33177
|
-
return
|
|
33439
|
+
return renderInspectResult(result, theme, context);
|
|
33178
33440
|
}
|
|
33179
33441
|
});
|
|
33180
33442
|
}
|
|
33181
33443
|
|
|
33182
33444
|
// src/tools/navigate.ts
|
|
33183
|
-
import { StringEnum as
|
|
33445
|
+
import { StringEnum as StringEnum3 } from "@earendil-works/pi-ai";
|
|
33184
33446
|
import { Type as Type9 } from "typebox";
|
|
33185
33447
|
function navigateParamsSchema() {
|
|
33186
33448
|
return Type9.Object({
|
|
33187
|
-
op:
|
|
33449
|
+
op: StringEnum3(["call_tree", "callers", "trace_to", "trace_to_symbol", "impact", "trace_data"], {
|
|
33188
33450
|
description: "Navigation operation"
|
|
33189
33451
|
}),
|
|
33190
33452
|
filePath: Type9.String({
|
|
@@ -33341,7 +33603,7 @@ function renderNavigateCall(args, theme, context) {
|
|
|
33341
33603
|
theme.fg("toolOutput", args.symbol),
|
|
33342
33604
|
args.toSymbol ? theme.fg("toolOutput", `→ ${args.toSymbol}`) : undefined
|
|
33343
33605
|
].filter(Boolean).join(" ");
|
|
33344
|
-
return renderToolCall("
|
|
33606
|
+
return renderToolCall("callgraph", summary, theme, context);
|
|
33345
33607
|
}
|
|
33346
33608
|
function renderNavigateResult(result, args, theme, context) {
|
|
33347
33609
|
if (context.isError)
|
|
@@ -33350,9 +33612,9 @@ function renderNavigateResult(result, args, theme, context) {
|
|
|
33350
33612
|
}
|
|
33351
33613
|
function registerNavigateTool(pi, ctx) {
|
|
33352
33614
|
pi.registerTool({
|
|
33353
|
-
name: "
|
|
33354
|
-
label: "
|
|
33355
|
-
description: "
|
|
33615
|
+
name: "aft_callgraph",
|
|
33616
|
+
label: "callgraph",
|
|
33617
|
+
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.",
|
|
33356
33618
|
parameters: navigateParamsSchema(),
|
|
33357
33619
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33358
33620
|
if (params.op === "trace_data" && !params.expression) {
|
|
@@ -33614,9 +33876,25 @@ Pass a single \`target\`:
|
|
|
33614
33876
|
parameters: ZoomParams,
|
|
33615
33877
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
33616
33878
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
33879
|
+
const hasTargetsProvided = (t) => {
|
|
33880
|
+
if (isEmptyParam(t))
|
|
33881
|
+
return false;
|
|
33882
|
+
const entryEmpty = (entry) => {
|
|
33883
|
+
if (!entry || typeof entry !== "object")
|
|
33884
|
+
return true;
|
|
33885
|
+
const fp = entry.filePath;
|
|
33886
|
+
const sym = entry.symbol;
|
|
33887
|
+
const fpEmpty = typeof fp !== "string" || fp.length === 0;
|
|
33888
|
+
const symEmpty = typeof sym !== "string" || sym.length === 0;
|
|
33889
|
+
return fpEmpty && symEmpty;
|
|
33890
|
+
};
|
|
33891
|
+
if (Array.isArray(t))
|
|
33892
|
+
return !t.every(entryEmpty);
|
|
33893
|
+
return !entryEmpty(t);
|
|
33894
|
+
};
|
|
33617
33895
|
const hasFilePath = !isEmptyParam(params.filePath);
|
|
33618
33896
|
const hasUrl = !isEmptyParam(params.url);
|
|
33619
|
-
const hasTargets =
|
|
33897
|
+
const hasTargets = hasTargetsProvided(params.targets);
|
|
33620
33898
|
const hasSymbols = !isEmptyParam(params.symbols);
|
|
33621
33899
|
if (hasTargets) {
|
|
33622
33900
|
if (hasFilePath || hasUrl || hasSymbols) {
|
|
@@ -33770,10 +34048,10 @@ function formatOutlineFilesText(response) {
|
|
|
33770
34048
|
}
|
|
33771
34049
|
|
|
33772
34050
|
// src/tools/refactor.ts
|
|
33773
|
-
import { StringEnum as
|
|
34051
|
+
import { StringEnum as StringEnum4 } from "@earendil-works/pi-ai";
|
|
33774
34052
|
import { Type as Type11 } from "typebox";
|
|
33775
34053
|
var RefactorParams = Type11.Object({
|
|
33776
|
-
op:
|
|
34054
|
+
op: StringEnum4(["move", "extract", "inline"], { description: "Refactoring operation" }),
|
|
33777
34055
|
filePath: Type11.String({
|
|
33778
34056
|
description: "Source file (absolute or relative to project root)"
|
|
33779
34057
|
}),
|
|
@@ -33881,10 +34159,10 @@ function registerRefactorTool(pi, ctx) {
|
|
|
33881
34159
|
}
|
|
33882
34160
|
|
|
33883
34161
|
// src/tools/safety.ts
|
|
33884
|
-
import { StringEnum as
|
|
34162
|
+
import { StringEnum as StringEnum5 } from "@earendil-works/pi-ai";
|
|
33885
34163
|
import { Type as Type12 } from "typebox";
|
|
33886
34164
|
var SafetyParams = Type12.Object({
|
|
33887
|
-
op:
|
|
34165
|
+
op: StringEnum5(["undo", "history", "checkpoint", "restore", "list"], {
|
|
33888
34166
|
description: "Safety operation"
|
|
33889
34167
|
}),
|
|
33890
34168
|
filePath: Type12.Optional(Type12.String({
|
|
@@ -34023,11 +34301,28 @@ function registerSafetyTool(pi, ctx) {
|
|
|
34023
34301
|
|
|
34024
34302
|
// src/tools/semantic.ts
|
|
34025
34303
|
import { Type as Type13 } from "typebox";
|
|
34304
|
+
function semanticHonestyNote(response, theme) {
|
|
34305
|
+
const notes = [];
|
|
34306
|
+
if (response.more_available === true)
|
|
34307
|
+
notes.push("more results available");
|
|
34308
|
+
if (response.engine_capped === true)
|
|
34309
|
+
notes.push("enumeration capped");
|
|
34310
|
+
if (response.fully_degraded === true)
|
|
34311
|
+
notes.push("fully degraded");
|
|
34312
|
+
if (response.complete === false)
|
|
34313
|
+
notes.push("partial/incomplete");
|
|
34314
|
+
return notes.length > 0 ? theme.fg("warning", `Search status: ${notes.join("; ")}.`) : undefined;
|
|
34315
|
+
}
|
|
34026
34316
|
var SearchParams2 = Type13.Object({
|
|
34027
34317
|
query: Type13.String({
|
|
34028
34318
|
description: "Concept, regex, literal text, filename, or capability to find. Examples: 'fuzzy match with whitespace tolerance', '^export', 'Cargo.lock'."
|
|
34029
34319
|
}),
|
|
34030
|
-
topK: Type13.Optional(Type13.
|
|
34320
|
+
topK: Type13.Optional(Type13.Integer({
|
|
34321
|
+
minimum: 1,
|
|
34322
|
+
maximum: 100,
|
|
34323
|
+
default: 10,
|
|
34324
|
+
description: "Maximum number of results (default: 10, max: 100)"
|
|
34325
|
+
})),
|
|
34031
34326
|
hint: Type13.Optional(Type13.Union([
|
|
34032
34327
|
Type13.Literal("regex"),
|
|
34033
34328
|
Type13.Literal("literal"),
|
|
@@ -34053,6 +34348,9 @@ function buildSemanticSections(args, payload, theme) {
|
|
|
34053
34348
|
sections.push(warnings.map((warning) => theme.fg("warning", `⚠ ${warning}`)).join(`
|
|
34054
34349
|
`));
|
|
34055
34350
|
}
|
|
34351
|
+
const honestyNote = semanticHonestyNote(response, theme);
|
|
34352
|
+
if (honestyNote)
|
|
34353
|
+
sections.push(honestyNote);
|
|
34056
34354
|
const results = asRecords(response.results);
|
|
34057
34355
|
if (status !== "ready" && results.length === 0) {
|
|
34058
34356
|
sections.push(asString(response.text) ?? theme.fg("muted", "Semantic index is not ready."));
|
|
@@ -34070,12 +34368,14 @@ function buildSemanticSections(args, payload, theme) {
|
|
|
34070
34368
|
const line = asNumber(result.line);
|
|
34071
34369
|
const column = asNumber(result.column);
|
|
34072
34370
|
const lineText = asString(result.line_text) ?? "";
|
|
34073
|
-
const
|
|
34074
|
-
lines.push(` ↳ ${theme.fg("muted", `line ${
|
|
34371
|
+
const location2 = line !== undefined ? `${line}${column !== undefined ? `:${column}` : ""}` : "?";
|
|
34372
|
+
lines.push(` ↳ ${theme.fg("muted", `line ${location2}`)} ${lineText}`);
|
|
34075
34373
|
return;
|
|
34076
34374
|
}
|
|
34077
34375
|
const score = asNumber(result.score);
|
|
34078
34376
|
const source = asString(result.source);
|
|
34377
|
+
const kind = asString(result.kind) ?? "symbol";
|
|
34378
|
+
const location = asString(result.location);
|
|
34079
34379
|
if (source === "lexical") {
|
|
34080
34380
|
lines.push(` ↳ ${theme.fg("muted", `[lexical match${score !== undefined ? ` — score ${score.toFixed(3)}` : ""}]`)}`);
|
|
34081
34381
|
const snippet2 = asString(result.snippet);
|
|
@@ -34085,10 +34385,14 @@ function buildSemanticSections(args, payload, theme) {
|
|
|
34085
34385
|
}
|
|
34086
34386
|
return;
|
|
34087
34387
|
}
|
|
34388
|
+
if (kind === "file_summary" || location === "[file summary]") {
|
|
34389
|
+
const summary = asString(result.snippet) ?? asString(result.name) ?? "(no summary)";
|
|
34390
|
+
lines.push(` ↳ ${summary} ${theme.fg("muted", `[file summary${score !== undefined ? ` score ${score.toFixed(3)}` : ""}]`)}`);
|
|
34391
|
+
return;
|
|
34392
|
+
}
|
|
34088
34393
|
const startLine = asNumber(result.start_line);
|
|
34089
34394
|
const endLine = asNumber(result.end_line);
|
|
34090
34395
|
const range = startLine !== undefined ? `${startLine}${endLine && endLine !== startLine ? `-${endLine}` : ""}` : "?";
|
|
34091
|
-
const kind = asString(result.kind) ?? "symbol";
|
|
34092
34396
|
const name = asString(result.name) ?? "(unknown)";
|
|
34093
34397
|
lines.push(` ↳ ${name} ${theme.fg("muted", `[${kind}] lines ${range}${score !== undefined ? ` score ${score.toFixed(3)}` : ""}`)}`);
|
|
34094
34398
|
const snippet = asString(result.snippet);
|
|
@@ -34127,13 +34431,16 @@ function registerSemanticTool(pi, ctx) {
|
|
|
34127
34431
|
"When NOT to use:",
|
|
34128
34432
|
"- You need exhaustive literal enumeration → use grep directly",
|
|
34129
34433
|
"- You want the file/module structure → use aft_outline",
|
|
34130
|
-
"- You're following a call chain → use
|
|
34434
|
+
"- You're following a call chain → use aft_callgraph",
|
|
34131
34435
|
"",
|
|
34132
34436
|
"Set hint to 'regex', 'literal', 'semantic', or 'auto' to override or document routing intent."
|
|
34133
34437
|
].join(`
|
|
34134
34438
|
`),
|
|
34135
34439
|
parameters: SearchParams2,
|
|
34136
34440
|
async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
|
|
34441
|
+
if (isEmptyParam(params.query) || typeof params.query !== "string" || params.query.trim().length === 0) {
|
|
34442
|
+
throw new Error("semantic_search: invalid params: `query` must be a non-empty string");
|
|
34443
|
+
}
|
|
34137
34444
|
const bridge = bridgeFor(ctx, extCtx.cwd);
|
|
34138
34445
|
const req = { query: params.query };
|
|
34139
34446
|
if (params.topK !== undefined)
|
|
@@ -34141,7 +34448,7 @@ function registerSemanticTool(pi, ctx) {
|
|
|
34141
34448
|
if (params.hint !== undefined)
|
|
34142
34449
|
req.hint = params.hint;
|
|
34143
34450
|
const response = await callBridge(bridge, "semantic_search", req, extCtx);
|
|
34144
|
-
return textResult(response.text ?? JSON.stringify(response, null, 2));
|
|
34451
|
+
return textResult(response.text ?? JSON.stringify(response, null, 2), response);
|
|
34145
34452
|
},
|
|
34146
34453
|
renderCall(args, theme, context) {
|
|
34147
34454
|
return renderSemanticCall(args, theme, context);
|
|
@@ -34153,10 +34460,10 @@ function registerSemanticTool(pi, ctx) {
|
|
|
34153
34460
|
}
|
|
34154
34461
|
|
|
34155
34462
|
// src/tools/structure.ts
|
|
34156
|
-
import { StringEnum as
|
|
34463
|
+
import { StringEnum as StringEnum6 } from "@earendil-works/pi-ai";
|
|
34157
34464
|
import { Type as Type14 } from "typebox";
|
|
34158
34465
|
var TransformParams = Type14.Object({
|
|
34159
|
-
op:
|
|
34466
|
+
op: StringEnum6(["add_member", "add_derive", "wrap_try_catch", "add_decorator", "add_struct_tags"], { description: "Transformation operation" }),
|
|
34160
34467
|
filePath: Type14.String({
|
|
34161
34468
|
description: "Path to the source file (absolute or relative to project root)"
|
|
34162
34469
|
}),
|
|
@@ -34172,7 +34479,7 @@ var TransformParams = Type14.Object({
|
|
|
34172
34479
|
position: Type14.Optional(Type14.String({
|
|
34173
34480
|
description: "Position hint: 'first', 'last' (default), 'before:name', 'after:name' for add_member"
|
|
34174
34481
|
})),
|
|
34175
|
-
validate: Type14.Optional(
|
|
34482
|
+
validate: Type14.Optional(StringEnum6(["syntax", "full"], {
|
|
34176
34483
|
description: "Validation level (default: syntax)"
|
|
34177
34484
|
}))
|
|
34178
34485
|
});
|
|
@@ -34288,7 +34595,8 @@ function buildWorkflowHints(opts) {
|
|
|
34288
34595
|
const hasZoom = !opts.absentTools.has("aft_zoom");
|
|
34289
34596
|
const hasGrep = opts.toolSurface !== "minimal" && !opts.absentTools.has(grepName);
|
|
34290
34597
|
const hasSearch = opts.toolSurface !== "minimal" && opts.semanticEnabled && !opts.absentTools.has("aft_search");
|
|
34291
|
-
const hasNavigate = opts.toolSurface === "all" && !opts.absentTools.has("
|
|
34598
|
+
const hasNavigate = opts.toolSurface === "all" && !opts.absentTools.has("aft_callgraph");
|
|
34599
|
+
const hasInspect = opts.toolSurface !== "minimal" && !opts.absentTools.has("aft_inspect");
|
|
34292
34600
|
const hasBgBash = opts.bashBackgroundEnabled && !opts.absentTools.has(bashName) && !opts.absentTools.has("bash_status");
|
|
34293
34601
|
if (hasOutline && hasZoom) {
|
|
34294
34602
|
sections.push(`**Web/URL access**: \`aft_outline({ target: "<url>" })\` first for structure, then \`aft_zoom({ url: "<url>", symbols: "<heading>" })\` for the specific section.`);
|
|
@@ -34296,14 +34604,17 @@ function buildWorkflowHints(opts) {
|
|
|
34296
34604
|
if (hasOutline && hasZoom && (hasGrep || hasSearch)) {
|
|
34297
34605
|
if (hasSearch) {
|
|
34298
34606
|
const grepFallback = hasGrep ? ` Use \`${grepName}\` directly only when you need exhaustive enumeration of literal text (every TODO, every import of X) without ranking.` : "";
|
|
34299
|
-
sections.push(`**Code exploration**: \`aft_search\` is the primary code-search tool. It auto-routes by query shape — exact identifiers, regex, error messages, natural language all use the same call.
|
|
34607
|
+
sections.push(`**Code exploration**: \`aft_search\` is the primary code-search tool. It auto-routes by query shape — exact identifiers, regex, error messages, natural language all use the same call. Very short queries fall back to literal scans; pass \`hint: "regex"\` / \`hint: "literal"\` / \`hint: "semantic"\` to override routing if needed. Then \`aft_outline\` for structure → \`aft_zoom\` for symbol(s).${grepFallback}`);
|
|
34300
34608
|
} else {
|
|
34301
34609
|
sections.push(`**Code exploration**: \`${grepName}\` to locate → \`aft_outline\` for structure → \`aft_zoom\` for symbol(s).`);
|
|
34302
34610
|
}
|
|
34303
34611
|
}
|
|
34612
|
+
if (hasInspect) {
|
|
34613
|
+
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.");
|
|
34614
|
+
}
|
|
34304
34615
|
if (hasNavigate) {
|
|
34305
34616
|
sections.push([
|
|
34306
|
-
"Use `
|
|
34617
|
+
"Use `aft_callgraph` for code-relationship questions instead of grep + read chains:",
|
|
34307
34618
|
"- `callers` — find all call sites before changing a function signature",
|
|
34308
34619
|
"- `impact` — blast radius (which functions/files will need updates)",
|
|
34309
34620
|
"- `trace_to` — how execution reaches this code from entry points (routes, exports, main)",
|
|
@@ -34343,7 +34654,9 @@ function registerWorkflowHints(pi, config2, surface) {
|
|
|
34343
34654
|
if (!surface.semantic)
|
|
34344
34655
|
absent.add("aft_search");
|
|
34345
34656
|
if (!surface.navigate)
|
|
34346
|
-
absent.add("
|
|
34657
|
+
absent.add("aft_callgraph");
|
|
34658
|
+
if (!surface.inspect)
|
|
34659
|
+
absent.add("aft_inspect");
|
|
34347
34660
|
if (!surface.hoistGrep)
|
|
34348
34661
|
absent.add("grep");
|
|
34349
34662
|
if (!surface.hoistBash) {
|
|
@@ -34407,17 +34720,17 @@ var PLUGIN_VERSION = (() => {
|
|
|
34407
34720
|
return "0.0.0";
|
|
34408
34721
|
}
|
|
34409
34722
|
})();
|
|
34410
|
-
var ANNOUNCEMENT_VERSION = "0.
|
|
34723
|
+
var ANNOUNCEMENT_VERSION = "0.33.0";
|
|
34411
34724
|
var ANNOUNCEMENT_FEATURES = [
|
|
34412
|
-
"`
|
|
34413
|
-
|
|
34414
|
-
"
|
|
34415
|
-
"
|
|
34416
|
-
|
|
34725
|
+
"New `aft_inspect` — one call for codebase health: diagnostics, metrics, TODOs, dead code, unused exports, and duplicates.",
|
|
34726
|
+
"Diagnostics now flow through `aft_inspect` (run it after a batch of edits) instead of arriving automatically on every edit.",
|
|
34727
|
+
"`aft_navigate` is renamed to `aft_callgraph`; the Rust call graph now resolves cross-file callers.",
|
|
34728
|
+
"Edits no longer echo the whole file back to the agent — much lower token cost per edit.",
|
|
34729
|
+
"Batch of `aft_search` correctness fixes and undo-history/SSRF/Windows hardening."
|
|
34417
34730
|
];
|
|
34418
34731
|
var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU";
|
|
34419
34732
|
var ALL_ONLY_TOOLS = new Set([
|
|
34420
|
-
"
|
|
34733
|
+
"aft_callgraph",
|
|
34421
34734
|
"aft_delete",
|
|
34422
34735
|
"aft_move",
|
|
34423
34736
|
"aft_transform",
|
|
@@ -34491,6 +34804,7 @@ function resolveToolSurface(config2) {
|
|
|
34491
34804
|
outline: ok("aft_outline"),
|
|
34492
34805
|
zoom: ok("aft_zoom"),
|
|
34493
34806
|
semantic: false,
|
|
34807
|
+
inspect: false,
|
|
34494
34808
|
navigate: false,
|
|
34495
34809
|
conflicts: false,
|
|
34496
34810
|
importTool: false,
|
|
@@ -34499,7 +34813,6 @@ function resolveToolSurface(config2) {
|
|
|
34499
34813
|
move: false,
|
|
34500
34814
|
astSearch: false,
|
|
34501
34815
|
astReplace: false,
|
|
34502
|
-
lspDiagnostics: false,
|
|
34503
34816
|
structure: false,
|
|
34504
34817
|
refactor: false
|
|
34505
34818
|
};
|
|
@@ -34514,6 +34827,7 @@ function resolveToolSurface(config2) {
|
|
|
34514
34827
|
outline: ok("aft_outline"),
|
|
34515
34828
|
zoom: ok("aft_zoom"),
|
|
34516
34829
|
semantic: ok("aft_search") && config2.semantic_search === true,
|
|
34830
|
+
inspect: ok("aft_inspect") && config2.inspect?.enabled !== false,
|
|
34517
34831
|
navigate: false,
|
|
34518
34832
|
conflicts: ok("aft_conflicts"),
|
|
34519
34833
|
importTool: ok("aft_import"),
|
|
@@ -34522,14 +34836,13 @@ function resolveToolSurface(config2) {
|
|
|
34522
34836
|
move: false,
|
|
34523
34837
|
astSearch: ok("ast_grep_search"),
|
|
34524
34838
|
astReplace: ok("ast_grep_replace"),
|
|
34525
|
-
lspDiagnostics: ok("lsp_diagnostics"),
|
|
34526
34839
|
structure: false,
|
|
34527
34840
|
refactor: false
|
|
34528
34841
|
};
|
|
34529
34842
|
if (surface === "all") {
|
|
34530
34843
|
return {
|
|
34531
34844
|
...base,
|
|
34532
|
-
navigate: allOnly("
|
|
34845
|
+
navigate: allOnly("aft_callgraph"),
|
|
34533
34846
|
delete: allOnly("aft_delete"),
|
|
34534
34847
|
move: allOnly("aft_move"),
|
|
34535
34848
|
structure: allOnly("aft_transform"),
|
|
@@ -34577,6 +34890,8 @@ async function src_default(pi) {
|
|
|
34577
34890
|
Object.assign(configOverrides, resolveLspConfigForConfigure(config2));
|
|
34578
34891
|
if (config2.semantic !== undefined)
|
|
34579
34892
|
configOverrides.semantic = config2.semantic;
|
|
34893
|
+
if (config2.inspect !== undefined)
|
|
34894
|
+
configOverrides.inspect = config2.inspect;
|
|
34580
34895
|
if (config2.max_callgraph_files !== undefined)
|
|
34581
34896
|
configOverrides.max_callgraph_files = config2.max_callgraph_files;
|
|
34582
34897
|
if (config2.url_fetch_allow_private !== undefined)
|
|
@@ -34744,6 +35059,9 @@ ${lines}
|
|
|
34744
35059
|
if (surface.semantic) {
|
|
34745
35060
|
registerSemanticTool(pi, ctx);
|
|
34746
35061
|
}
|
|
35062
|
+
if (surface.inspect) {
|
|
35063
|
+
registerInspectTool(pi, ctx);
|
|
35064
|
+
}
|
|
34747
35065
|
if (surface.navigate) {
|
|
34748
35066
|
registerNavigateTool(pi, ctx);
|
|
34749
35067
|
}
|
|
@@ -34762,9 +35080,6 @@ ${lines}
|
|
|
34762
35080
|
if (surface.delete || surface.move) {
|
|
34763
35081
|
registerFsTools(pi, ctx, surface);
|
|
34764
35082
|
}
|
|
34765
|
-
if (surface.lspDiagnostics) {
|
|
34766
|
-
registerLspTools(pi, ctx);
|
|
34767
|
-
}
|
|
34768
35083
|
if (surface.structure) {
|
|
34769
35084
|
registerStructureTool(pi, ctx);
|
|
34770
35085
|
}
|