@cortexkit/aft-pi 0.22.1 → 0.24.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.
Files changed (44) hide show
  1. package/dist/bg-notifications.d.ts +0 -1
  2. package/dist/bg-notifications.d.ts.map +1 -1
  3. package/dist/commands/aft-status.d.ts +5 -4
  4. package/dist/commands/aft-status.d.ts.map +1 -1
  5. package/dist/dialogs/status-dialog.d.ts +19 -0
  6. package/dist/dialogs/status-dialog.d.ts.map +1 -0
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +454 -58
  10. package/dist/shared/status.d.ts +10 -0
  11. package/dist/shared/status.d.ts.map +1 -1
  12. package/dist/tools/_shared.d.ts +1 -1
  13. package/dist/tools/_shared.d.ts.map +1 -1
  14. package/dist/tools/ast.d.ts +17 -17
  15. package/dist/tools/ast.d.ts.map +1 -1
  16. package/dist/tools/bash.d.ts +8 -8
  17. package/dist/tools/bash.d.ts.map +1 -1
  18. package/dist/tools/conflicts.d.ts +4 -4
  19. package/dist/tools/conflicts.d.ts.map +1 -1
  20. package/dist/tools/fs.d.ts +9 -9
  21. package/dist/tools/fs.d.ts.map +1 -1
  22. package/dist/tools/hoisted.d.ts +1 -1
  23. package/dist/tools/hoisted.d.ts.map +1 -1
  24. package/dist/tools/imports.d.ts +13 -13
  25. package/dist/tools/imports.d.ts.map +1 -1
  26. package/dist/tools/lsp.d.ts +9 -9
  27. package/dist/tools/lsp.d.ts.map +1 -1
  28. package/dist/tools/navigate.d.ts +10 -10
  29. package/dist/tools/navigate.d.ts.map +1 -1
  30. package/dist/tools/reading.d.ts +14 -14
  31. package/dist/tools/reading.d.ts.map +1 -1
  32. package/dist/tools/refactor.d.ts +14 -14
  33. package/dist/tools/refactor.d.ts.map +1 -1
  34. package/dist/tools/render-helpers.d.ts +2 -2
  35. package/dist/tools/render-helpers.d.ts.map +1 -1
  36. package/dist/tools/safety.d.ts +9 -9
  37. package/dist/tools/safety.d.ts.map +1 -1
  38. package/dist/tools/semantic.d.ts +7 -7
  39. package/dist/tools/semantic.d.ts.map +1 -1
  40. package/dist/tools/structure.d.ts +18 -18
  41. package/dist/tools/structure.d.ts.map +1 -1
  42. package/dist/workflow-hints.d.ts +1 -1
  43. package/dist/workflow-hints.d.ts.map +1 -1
  44. package/package.json +14 -14
package/dist/index.js CHANGED
@@ -30623,6 +30623,8 @@ class BinaryBridge {
30623
30623
  onConfigureWarnings;
30624
30624
  onBashCompletion;
30625
30625
  onBashLongRunning;
30626
+ cachedStatus = null;
30627
+ statusListeners = new Set;
30626
30628
  configureWarningClients = new Map;
30627
30629
  restartResetTimer = null;
30628
30630
  errorPrefix;
@@ -30671,6 +30673,21 @@ class BinaryBridge {
30671
30673
  hasPendingRequests() {
30672
30674
  return this.pending.size > 0;
30673
30675
  }
30676
+ getCachedStatus() {
30677
+ return this.cachedStatus;
30678
+ }
30679
+ subscribeStatus(listener) {
30680
+ this.statusListeners.add(listener);
30681
+ if (this.cachedStatus !== null) {
30682
+ this.deliverStatusSnapshot(listener, this.cachedStatus);
30683
+ }
30684
+ return () => {
30685
+ this.statusListeners.delete(listener);
30686
+ };
30687
+ }
30688
+ cacheStatusSnapshot(snapshot) {
30689
+ this.cachedStatus = snapshot;
30690
+ }
30674
30691
  async send(command, params = {}, options) {
30675
30692
  if (this._shuttingDown) {
30676
30693
  throw new Error(`${this.errorPrefix} Bridge is shutting down, cannot send "${command}"`);
@@ -30807,6 +30824,23 @@ class BinaryBridge {
30807
30824
  }
30808
30825
  }
30809
30826
  }
30827
+ handleStatusChangedFrame(frame) {
30828
+ const snapshot = frame.snapshot;
30829
+ if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot))
30830
+ return;
30831
+ this.cachedStatus = snapshot;
30832
+ log("Received status_changed push frame; cached AFT status snapshot");
30833
+ for (const listener of this.statusListeners) {
30834
+ this.deliverStatusSnapshot(listener, this.cachedStatus);
30835
+ }
30836
+ }
30837
+ deliverStatusSnapshot(listener, snapshot) {
30838
+ try {
30839
+ listener(snapshot);
30840
+ } catch (err) {
30841
+ warn(`status listener threw: ${err instanceof Error ? err.message : String(err)}`);
30842
+ }
30843
+ }
30810
30844
  async shutdown() {
30811
30845
  this._shuttingDown = true;
30812
30846
  this.clearRestartResetTimer();
@@ -31011,6 +31045,10 @@ class BinaryBridge {
31011
31045
  });
31012
31046
  continue;
31013
31047
  }
31048
+ if (response.type === "status_changed") {
31049
+ this.handleStatusChangedFrame(response);
31050
+ continue;
31051
+ }
31014
31052
  const id = response.id;
31015
31053
  if (id && this.pending.has(id)) {
31016
31054
  const entry = this.pending.get(id);
@@ -31972,9 +32010,9 @@ import { chmodSync as chmodSync3, copyFileSync as copyFileSync2, existsSync as e
31972
32010
  import { createRequire as createRequire2 } from "node:module";
31973
32011
  import { homedir as homedir4 } from "node:os";
31974
32012
  import { join as join4 } from "node:path";
31975
- function copyToVersionedCache(npmBinaryPath) {
32013
+ function readBinaryVersion(binaryPath) {
31976
32014
  try {
31977
- const result = spawnSync(npmBinaryPath, ["--version"], {
32015
+ const result = spawnSync(binaryPath, ["--version"], {
31978
32016
  encoding: "utf-8",
31979
32017
  stdio: ["pipe", "pipe", "pipe"],
31980
32018
  timeout: 5000
@@ -31982,7 +32020,16 @@ function copyToVersionedCache(npmBinaryPath) {
31982
32020
  const rawVersion = result.stdout?.trim();
31983
32021
  if (!rawVersion)
31984
32022
  return null;
31985
- const version = rawVersion.replace(/^aft\s+/, "");
32023
+ return rawVersion.replace(/^aft\s+/, "");
32024
+ } catch {
32025
+ return null;
32026
+ }
32027
+ }
32028
+ function copyToVersionedCache(npmBinaryPath, knownVersion) {
32029
+ try {
32030
+ const version = knownVersion ?? readBinaryVersion(npmBinaryPath);
32031
+ if (!version)
32032
+ return null;
31986
32033
  const tag = version.startsWith("v") ? version : `v${version}`;
31987
32034
  const cacheDir = getCacheDir();
31988
32035
  const versionedDir = join4(cacheDir, tag);
@@ -32037,8 +32084,13 @@ function findBinarySync(expectedVersion) {
32037
32084
  const req = createRequire2(import.meta.url);
32038
32085
  const resolved = req.resolve(packageBin);
32039
32086
  if (existsSync3(resolved)) {
32040
- const copied = copyToVersionedCache(resolved);
32041
- return copied ?? resolved;
32087
+ const npmVersion = readBinaryVersion(resolved);
32088
+ if (pluginVersion && npmVersion && npmVersion !== pluginVersion) {
32089
+ warn(`npm platform package binary v${npmVersion} does not match plugin v${pluginVersion}; skipping (continuing to PATH lookup)`);
32090
+ } else {
32091
+ const copied = copyToVersionedCache(resolved, npmVersion ?? undefined);
32092
+ return copied ?? resolved;
32093
+ }
32042
32094
  }
32043
32095
  } catch {}
32044
32096
  try {
@@ -32736,8 +32788,6 @@ async function handleTurnEndBgCompletions(drainContext) {
32736
32788
  }
32737
32789
  async function triggerWakeIfPending(drainContext, skipDrain) {
32738
32790
  const state = stateFor(drainContext.sessionID);
32739
- if (drainContext.isActive?.())
32740
- return;
32741
32791
  if (!skipDrain && state.outstandingTaskIds.size > 0) {
32742
32792
  await drainCompletions(drainContext);
32743
32793
  }
@@ -32781,9 +32831,8 @@ ${formatLongRunningReminder(longRunning)}`;
32781
32831
  async function drainCompletions({ ctx, directory, sessionID }) {
32782
32832
  try {
32783
32833
  const bridge = ctx.pool.getActiveBridgeForRoot(directory) ?? ctx.pool.getBridge(directory);
32784
- if (!sessionID)
32785
- return;
32786
- const response = await bridge.send("bash_drain_completions", { session_id: sessionID });
32834
+ const params = sessionID ? { session_id: sessionID } : {};
32835
+ const response = await bridge.send("bash_drain_completions", params);
32787
32836
  if (response.success === false) {
32788
32837
  sessionWarn2(sessionID ?? "", `${LOG_PREFIX} drain failed: ${String(response.message ?? "unknown error")}`);
32789
32838
  return;
@@ -32941,6 +32990,74 @@ function shorten(value, limit) {
32941
32990
  return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
32942
32991
  }
32943
32992
 
32993
+ // src/dialogs/status-dialog.ts
32994
+ import {
32995
+ matchesKey,
32996
+ truncateToWidth,
32997
+ visibleWidth
32998
+ } from "@earendil-works/pi-tui";
32999
+ // package.json
33000
+ var package_default = {
33001
+ name: "@cortexkit/aft-pi",
33002
+ version: "0.24.0",
33003
+ type: "module",
33004
+ description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
33005
+ main: "dist/index.js",
33006
+ types: "dist/index.d.ts",
33007
+ license: "MIT",
33008
+ repository: {
33009
+ type: "git",
33010
+ url: "https://github.com/cortexkit/aft"
33011
+ },
33012
+ files: [
33013
+ "dist",
33014
+ "README.md"
33015
+ ],
33016
+ scripts: {
33017
+ build: "bun build src/index.ts --outdir dist --target node --format esm --external @earendil-works/pi-coding-agent --external @earendil-works/pi-ai --external @earendil-works/pi-tui --external typebox --external diff && tsc --emitDeclarationOnly",
33018
+ typecheck: "tsc --noEmit",
33019
+ test: "bun test src/__tests__/",
33020
+ lint: "biome check src",
33021
+ prepublishOnly: "bun run build"
33022
+ },
33023
+ dependencies: {
33024
+ "@cortexkit/aft-bridge": "0.24.0",
33025
+ typebox: "^1.1.24",
33026
+ "comment-json": "^5.0.0",
33027
+ diff: "^8.0.4",
33028
+ zod: "^4.1.8"
33029
+ },
33030
+ optionalDependencies: {
33031
+ "@cortexkit/aft-darwin-arm64": "0.24.0",
33032
+ "@cortexkit/aft-darwin-x64": "0.24.0",
33033
+ "@cortexkit/aft-linux-arm64": "0.24.0",
33034
+ "@cortexkit/aft-linux-x64": "0.24.0",
33035
+ "@cortexkit/aft-win32-x64": "0.24.0"
33036
+ },
33037
+ devDependencies: {
33038
+ "@earendil-works/pi-coding-agent": "*",
33039
+ "@earendil-works/pi-ai": "*",
33040
+ "@earendil-works/pi-tui": "*",
33041
+ "@types/node": "^22.0.0",
33042
+ typescript: "^5.8.0"
33043
+ },
33044
+ peerDependencies: {
33045
+ "@earendil-works/pi-coding-agent": "*",
33046
+ "@earendil-works/pi-tui": "*"
33047
+ },
33048
+ exports: {
33049
+ ".": {
33050
+ types: "./dist/index.d.ts",
33051
+ import: "./dist/index.js"
33052
+ }
33053
+ },
33054
+ pi: {
33055
+ extensions: [
33056
+ "./dist/index.js"
33057
+ ]
33058
+ }
33059
+ };
33060
+
32944
33061
  // src/shared/status.ts
32945
33062
  function asRecord(value) {
32946
33063
  return typeof value === "object" && value !== null ? value : {};
@@ -32989,9 +33106,12 @@ function coerceAftStatus(response) {
32989
33106
  };
32990
33107
  const disk = asRecord(response.disk);
32991
33108
  const symbolCache = asRecord(response.symbol_cache);
33109
+ const session = asRecord(response.session);
32992
33110
  return {
32993
33111
  version: readString(response.version, "unknown"),
32994
33112
  project_root: readNullableString(response.project_root),
33113
+ canonical_root: readNullableString(response.canonical_root),
33114
+ cache_role: readString(response.cache_role, "not_initialized"),
32995
33115
  features: {
32996
33116
  format_on_edit: readBoolean(features.format_on_edit),
32997
33117
  validate_on_edit: readString(features.validate_on_edit, "off"),
@@ -33026,13 +33146,21 @@ function coerceAftStatus(response) {
33026
33146
  local_entries: readNumber(symbolCache.local_entries),
33027
33147
  warm_entries: readNumber(symbolCache.warm_entries)
33028
33148
  },
33029
- storage_dir: readNullableString(response.storage_dir)
33149
+ storage_dir: readNullableString(response.storage_dir),
33150
+ checkpoints_total: readNumber(response.checkpoints_total),
33151
+ session: {
33152
+ id: readString(session.id, "__default__"),
33153
+ tracked_files: readNumber(session.tracked_files),
33154
+ checkpoints: readNumber(session.checkpoints)
33155
+ }
33030
33156
  };
33031
33157
  }
33032
33158
  function formatStatusDialogMessage(status) {
33033
33159
  const lines = [
33034
33160
  `AFT version: ${status.version}`,
33035
33161
  `Project root: ${status.project_root ?? "(not configured)"}`,
33162
+ `Canonical root: ${status.canonical_root ?? "(not configured)"}`,
33163
+ `Cache role: ${status.cache_role}`,
33036
33164
  "",
33037
33165
  "Enabled features",
33038
33166
  `- format_on_edit: ${formatFlag(status.features.format_on_edit)}`,
@@ -33125,26 +33253,261 @@ function textResult(text, details) {
33125
33253
  };
33126
33254
  }
33127
33255
 
33256
+ // src/dialogs/status-dialog.ts
33257
+ var REFRESH_INTERVAL_MS = 1500;
33258
+ var OVERLAY_WIDTH = 84;
33259
+ async function showAftStatusDialog(pi, extCtx, pluginCtx) {
33260
+ await extCtx.ui.custom((tui, theme, _keybindings, done) => new AftStatusDialogComponent({
33261
+ pi,
33262
+ extCtx,
33263
+ pluginCtx,
33264
+ theme,
33265
+ tui,
33266
+ done
33267
+ }), {
33268
+ overlay: true,
33269
+ overlayOptions: { anchor: "center", width: OVERLAY_WIDTH }
33270
+ });
33271
+ }
33272
+
33273
+ class AftStatusDialogComponent {
33274
+ props;
33275
+ snapshot = null;
33276
+ errorMessage = null;
33277
+ refreshTimer = null;
33278
+ closed = false;
33279
+ constructor(props) {
33280
+ this.props = props;
33281
+ this.fetchOnce();
33282
+ this.refreshTimer = setInterval(() => {
33283
+ if (this.closed)
33284
+ return;
33285
+ this.fetchOnce();
33286
+ }, REFRESH_INTERVAL_MS);
33287
+ }
33288
+ async fetchOnce() {
33289
+ try {
33290
+ const bridge = bridgeFor(this.props.pluginCtx, this.props.extCtx.cwd);
33291
+ const cached = bridge.getCachedStatus();
33292
+ const response = cached ? { success: true, ...cached } : await callBridge(bridge, "status", {}, this.props.extCtx);
33293
+ if (!cached) {
33294
+ bridge.cacheStatusSnapshot(response);
33295
+ }
33296
+ if (this.closed)
33297
+ return;
33298
+ this.snapshot = coerceAftStatus(response);
33299
+ this.errorMessage = null;
33300
+ this.props.tui.requestRender();
33301
+ } catch (err) {
33302
+ if (this.closed)
33303
+ return;
33304
+ this.errorMessage = err instanceof Error ? err.message : String(err);
33305
+ this.props.tui.requestRender();
33306
+ }
33307
+ }
33308
+ handleInput(data) {
33309
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c") || matchesKey(data, "return")) {
33310
+ this.close();
33311
+ }
33312
+ }
33313
+ close() {
33314
+ if (this.closed)
33315
+ return;
33316
+ this.closed = true;
33317
+ if (this.refreshTimer) {
33318
+ clearInterval(this.refreshTimer);
33319
+ this.refreshTimer = null;
33320
+ }
33321
+ this.props.done(undefined);
33322
+ }
33323
+ invalidate() {}
33324
+ render(width) {
33325
+ const innerWidth = Math.max(40, width - 4);
33326
+ const inner = renderInner(this.snapshot, this.errorMessage, this.props.theme, innerWidth);
33327
+ return drawBorder(inner, width, this.props.theme);
33328
+ }
33329
+ dispose() {
33330
+ if (this.refreshTimer) {
33331
+ clearInterval(this.refreshTimer);
33332
+ this.refreshTimer = null;
33333
+ }
33334
+ }
33335
+ }
33336
+ function renderInner(s, error3, theme, innerWidth) {
33337
+ const lines = [];
33338
+ lines.push(`${theme.fg("accent", theme.bold("⚡ AFT Status"))} ${theme.fg("muted", `v${s?.version ?? package_default.version}`)}`);
33339
+ lines.push("");
33340
+ if (error3 && !s) {
33341
+ lines.push(theme.fg("warning", error3));
33342
+ lines.push("");
33343
+ lines.push(theme.fg("muted", "Press Escape to close"));
33344
+ return lines;
33345
+ }
33346
+ if (!s) {
33347
+ lines.push(theme.fg("muted", "Connecting to AFT…"));
33348
+ return lines;
33349
+ }
33350
+ lines.push(rowFull("Project root", s.project_root ?? "(not configured)", theme, innerWidth));
33351
+ lines.push(rowFull("Canonical root", s.canonical_root ?? "(not configured)", theme, innerWidth));
33352
+ const cacheTone = s.cache_role === "main" ? "accent" : s.cache_role === "worktree" ? "warning" : "muted";
33353
+ lines.push(rowFull("Cache role", theme.fg(cacheTone, s.cache_role), theme, innerWidth));
33354
+ lines.push("");
33355
+ const colWidth = Math.floor((innerWidth - 2) / 2);
33356
+ const left = [];
33357
+ const right = [];
33358
+ left.push(theme.fg("muted", "Search index"));
33359
+ left.push(kv("status", colorStatus(s.search_index.status, theme), theme));
33360
+ left.push(kv("files", formatCountShort(s.search_index.files), theme));
33361
+ left.push(kv("trigrams", formatCountShort(s.search_index.trigrams), theme));
33362
+ left.push(kv("disk", formatBytes(s.disk.trigram_disk_bytes), theme));
33363
+ right.push(theme.fg("muted", "Semantic index"));
33364
+ right.push(kv("status", colorStatus(s.semantic_index.status, theme), theme));
33365
+ right.push(kv("entries", formatCountShort(s.semantic_index.entries), theme));
33366
+ if (s.semantic_index.backend)
33367
+ right.push(kv("backend", s.semantic_index.backend, theme));
33368
+ if (s.semantic_index.model)
33369
+ right.push(kv("model", s.semantic_index.model, theme));
33370
+ if (s.semantic_index.dimension != null) {
33371
+ right.push(kv("dimension", String(s.semantic_index.dimension), theme));
33372
+ }
33373
+ right.push(kv("disk", formatBytes(s.disk.semantic_disk_bytes), theme));
33374
+ for (const line of renderColumns(left, right, colWidth))
33375
+ lines.push(line);
33376
+ lines.push("");
33377
+ const left2 = [];
33378
+ const right2 = [];
33379
+ left2.push(theme.fg("muted", "Runtime"));
33380
+ left2.push(kv("LSP servers", String(s.lsp_servers), theme));
33381
+ left2.push(kv("symbol cache", `${formatCountShort(s.symbol_cache.local_entries)} local · ${formatCountShort(s.symbol_cache.warm_entries)} warm`, theme));
33382
+ right2.push(theme.fg("muted", "Current session"));
33383
+ right2.push(kv("tracked files", String(s.session.tracked_files), theme));
33384
+ right2.push(kv("checkpoints", String(s.session.checkpoints), theme));
33385
+ right2.push(kv("all-session", String(s.checkpoints_total), theme));
33386
+ for (const line of renderColumns(left2, right2, colWidth))
33387
+ lines.push(line);
33388
+ lines.push("");
33389
+ lines.push(theme.fg("muted", "Features"));
33390
+ lines.push(` ${featureBadge("format_on_edit", s.features.format_on_edit, theme)} ${featureBadge("search_index", s.features.search_index, theme)} ${featureBadge("semantic_search", s.features.semantic_search, theme)}`);
33391
+ if (s.semantic_index.stage) {
33392
+ lines.push("");
33393
+ lines.push(theme.fg("muted", "Semantic build progress"));
33394
+ lines.push(kv("stage", s.semantic_index.stage, theme));
33395
+ if (s.semantic_index.files != null) {
33396
+ lines.push(kv("files seen", formatCountShort(s.semantic_index.files), theme));
33397
+ }
33398
+ if (s.semantic_index.entries_done != null || s.semantic_index.entries_total != null) {
33399
+ lines.push(kv("progress", `${formatCountShort(s.semantic_index.entries_done ?? null)} / ${formatCountShort(s.semantic_index.entries_total ?? null)}`, theme));
33400
+ }
33401
+ }
33402
+ if (s.semantic_index.error) {
33403
+ lines.push("");
33404
+ lines.push(theme.fg("error", `⚠ ${s.semantic_index.error}`));
33405
+ }
33406
+ if (error3) {
33407
+ lines.push("");
33408
+ lines.push(theme.fg("warning", `⚠ ${error3}`));
33409
+ }
33410
+ lines.push("");
33411
+ lines.push(theme.fg("muted", "Press Escape to close"));
33412
+ return lines;
33413
+ }
33414
+ function colorStatus(status, theme) {
33415
+ switch (status) {
33416
+ case "ready":
33417
+ try {
33418
+ return theme.fg("success", status);
33419
+ } catch {
33420
+ return theme.fg("accent", status);
33421
+ }
33422
+ case "loading":
33423
+ case "building":
33424
+ return theme.fg("warning", status);
33425
+ case "failed":
33426
+ case "error":
33427
+ return theme.fg("error", status);
33428
+ case "disabled":
33429
+ return theme.fg("muted", status);
33430
+ default:
33431
+ return status;
33432
+ }
33433
+ }
33434
+ function featureBadge(name, enabled, theme) {
33435
+ const indicator = enabled ? theme.fg("accent", "●") : theme.fg("muted", "○");
33436
+ const label = enabled ? name : theme.fg("muted", name);
33437
+ return `${indicator} ${label}`;
33438
+ }
33439
+ function kv(label, value, theme) {
33440
+ return ` ${theme.fg("muted", `${label}:`)} ${value}`;
33441
+ }
33442
+ function rowFull(label, value, theme, innerWidth) {
33443
+ const labelText = `${label}: `;
33444
+ const remaining = Math.max(10, innerWidth - visibleWidth(labelText));
33445
+ const truncated = truncateToWidth(value, remaining, "…");
33446
+ return `${theme.fg("muted", labelText)}${truncated}`;
33447
+ }
33448
+ function renderColumns(left, right, colWidth) {
33449
+ const rows = Math.max(left.length, right.length);
33450
+ const out = [];
33451
+ for (let i = 0;i < rows; i++) {
33452
+ const l = left[i] ?? "";
33453
+ const r = right[i] ?? "";
33454
+ const visible = visibleWidth(l);
33455
+ const pad = " ".repeat(Math.max(0, colWidth - visible));
33456
+ out.push(`${l}${pad} ${r}`);
33457
+ }
33458
+ return out;
33459
+ }
33460
+ function drawBorder(inner, width, theme) {
33461
+ const innerWidth = Math.max(40, width - 4);
33462
+ const border = (s) => theme.fg("borderMuted", s);
33463
+ const top = border(`╭${"─".repeat(innerWidth + 2)}╮`);
33464
+ const bottom = border(`╰${"─".repeat(innerWidth + 2)}╯`);
33465
+ const side = border("│");
33466
+ const out = [];
33467
+ out.push(top);
33468
+ for (const raw of inner) {
33469
+ const line = truncateToWidth(raw, innerWidth, "…");
33470
+ const visible = visibleWidth(line);
33471
+ const pad = " ".repeat(Math.max(0, innerWidth - visible));
33472
+ out.push(`${side} ${line}${pad} ${side}`);
33473
+ }
33474
+ out.push(bottom);
33475
+ return out;
33476
+ }
33477
+ function formatCountShort(value) {
33478
+ if (value == null || !Number.isFinite(value))
33479
+ return "—";
33480
+ if (value >= 1e6)
33481
+ return `${(value / 1e6).toFixed(1)}M`;
33482
+ if (value >= 1000)
33483
+ return `${Math.round(value / 1000)}K`;
33484
+ return String(value);
33485
+ }
33486
+
33128
33487
  // src/commands/aft-status.ts
33129
33488
  function registerStatusCommand(pi, ctx) {
33130
33489
  pi.registerCommand("aft-status", {
33131
33490
  description: "Show AFT plugin status (search/semantic indexes, LSP, storage)",
33132
33491
  handler: async (_args, extCtx) => {
33133
33492
  try {
33493
+ if (extCtx.hasUI) {
33494
+ await showAftStatusDialog(pi, extCtx, ctx);
33495
+ return;
33496
+ }
33134
33497
  const bridge = bridgeFor(ctx, extCtx.cwd);
33135
- const response = await callBridge(bridge, "status", {}, extCtx);
33498
+ const cached = bridge.getCachedStatus();
33499
+ const response = cached ? { success: true, ...cached } : await callBridge(bridge, "status", {}, extCtx);
33500
+ if (!cached) {
33501
+ bridge.cacheStatusSnapshot(response);
33502
+ }
33136
33503
  const snapshot = coerceAftStatus(response);
33137
33504
  const text = formatStatusDialogMessage(snapshot);
33138
- if (extCtx.hasUI) {
33139
- await extCtx.ui.input("AFT Status", text);
33140
- } else {
33141
- extCtx.ui.notify(text, "info");
33142
- }
33505
+ extCtx.ui.notify(text, "info");
33143
33506
  } catch (err) {
33144
33507
  const message = `AFT status failed: ${err instanceof Error ? err.message : String(err)}`;
33145
- if (extCtx.hasUI) {
33508
+ try {
33146
33509
  extCtx.ui.notify(message, "error");
33147
- } else {
33510
+ } catch {
33148
33511
  console.error(`[aft-plugin] ${message}`);
33149
33512
  }
33150
33513
  }
@@ -48890,13 +49253,13 @@ function registerShutdownCleanup(fn) {
48890
49253
  }
48891
49254
 
48892
49255
  // src/tools/ast.ts
48893
- import { StringEnum } from "@mariozechner/pi-ai";
48894
- import { Type } from "@sinclair/typebox";
49256
+ import { StringEnum } from "@earendil-works/pi-ai";
49257
+ import { Type } from "typebox";
48895
49258
 
48896
49259
  // src/tools/render-helpers.ts
48897
49260
  import { homedir as homedir7 } from "node:os";
48898
- import { renderDiff } from "@mariozechner/pi-coding-agent";
48899
- import { Container, Spacer, Text } from "@mariozechner/pi-tui";
49261
+ import { renderDiff } from "@earendil-works/pi-coding-agent";
49262
+ import { Container, Spacer, Text } from "@earendil-works/pi-tui";
48900
49263
  function reuseText(last) {
48901
49264
  return last instanceof Text ? last : new Text("", 0, 0);
48902
49265
  }
@@ -49290,8 +49653,8 @@ function registerAstTools(pi, ctx, surface) {
49290
49653
  }
49291
49654
 
49292
49655
  // src/tools/bash.ts
49293
- import { Container as Container2, Spacer as Spacer2, Text as Text2 } from "@mariozechner/pi-tui";
49294
- import { Type as Type2 } from "@sinclair/typebox";
49656
+ import { Container as Container2, Spacer as Spacer2, Text as Text2 } from "@earendil-works/pi-tui";
49657
+ import { Type as Type2 } from "typebox";
49295
49658
  var FOREGROUND_WAIT_WINDOW_MS = 5000;
49296
49659
  var FOREGROUND_POLL_INTERVAL_MS = 100;
49297
49660
  var BASH_TRANSPORT_TIMEOUT_MS = 30000;
@@ -49625,7 +49988,7 @@ function shortenCommand(command) {
49625
49988
  }
49626
49989
 
49627
49990
  // src/tools/conflicts.ts
49628
- import { Type as Type3 } from "@sinclair/typebox";
49991
+ import { Type as Type3 } from "typebox";
49629
49992
  var ConflictsParams = Type3.Object({});
49630
49993
  function renderConflictCall(theme, context) {
49631
49994
  return renderToolCall("conflicts", undefined, theme, context);
@@ -49674,7 +50037,7 @@ function registerConflictsTool(pi, ctx) {
49674
50037
  }
49675
50038
 
49676
50039
  // src/tools/fs.ts
49677
- import { Type as Type4 } from "@sinclair/typebox";
50040
+ import { Type as Type4 } from "typebox";
49678
50041
  var DeleteParams = Type4.Object({
49679
50042
  files: Type4.Array(Type4.String(), {
49680
50043
  description: "Paths to delete (one or more). Single-file callers pass a single-element array.",
@@ -49796,12 +50159,12 @@ function registerFsTools(pi, ctx, surface) {
49796
50159
  // src/tools/hoisted.ts
49797
50160
  import { stat } from "node:fs/promises";
49798
50161
  import { homedir as homedir8 } from "node:os";
49799
- import { resolve as resolve3 } from "node:path";
50162
+ import { isAbsolute, relative as relative3, resolve as resolve3 } from "node:path";
49800
50163
  import {
49801
50164
  renderDiff as renderDiff2
49802
- } from "@mariozechner/pi-coding-agent";
49803
- import { Container as Container3, Spacer as Spacer3, Text as Text3 } from "@mariozechner/pi-tui";
49804
- import { Type as Type5 } from "@sinclair/typebox";
50165
+ } from "@earendil-works/pi-coding-agent";
50166
+ import { Container as Container3, Spacer as Spacer3, Text as Text3 } from "@earendil-works/pi-tui";
50167
+ import { Type as Type5 } from "typebox";
49805
50168
 
49806
50169
  // src/tools/diff-format.ts
49807
50170
  import { diffLines } from "diff";
@@ -49906,6 +50269,21 @@ function formatDiffForPi(oldContent, newContent, contextLines = DEFAULT_CONTEXT_
49906
50269
  }
49907
50270
 
49908
50271
  // src/tools/hoisted.ts
50272
+ function containsPath(parent, child) {
50273
+ const rel = relative3(parent, child);
50274
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
50275
+ }
50276
+ async function assertExternalDirectoryPermission(extCtx, target, action = "modify") {
50277
+ if (!target)
50278
+ return;
50279
+ const absoluteTarget = isAbsolute(target) ? target : resolve3(extCtx.cwd, target);
50280
+ if (containsPath(extCtx.cwd, absoluteTarget))
50281
+ return;
50282
+ const confirmed = await extCtx.ui?.confirm?.("Allow external directory access?", `AFT wants to ${action} outside the project: ${absoluteTarget}`);
50283
+ if (confirmed === true)
50284
+ return;
50285
+ throw new Error("Permission denied: external directory access was cancelled.");
50286
+ }
49909
50287
  var ReadParams = Type5.Object({
49910
50288
  path: Type5.String({ description: "Path to the file to read (relative or absolute)" }),
49911
50289
  offset: Type5.Optional(Type5.Number({ description: "Line number to start reading from (1-indexed)" })),
@@ -49977,6 +50355,7 @@ function registerHoistedTools(pi, ctx, surface) {
49977
50355
  promptGuidelines: ["Use write only for new files or complete rewrites."],
49978
50356
  parameters: WriteParams,
49979
50357
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
50358
+ await assertExternalDirectoryPermission(extCtx, params.filePath);
49980
50359
  const bridge = bridgeFor(ctx, extCtx.cwd);
49981
50360
  const response = await callBridge(bridge, "write", {
49982
50361
  file: params.filePath,
@@ -50007,6 +50386,7 @@ function registerHoistedTools(pi, ctx, surface) {
50007
50386
  ],
50008
50387
  parameters: EditParams,
50009
50388
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
50389
+ await assertExternalDirectoryPermission(extCtx, params.filePath);
50010
50390
  const bridge = bridgeFor(ctx, extCtx.cwd);
50011
50391
  if (typeof params.appendContent === "string") {
50012
50392
  const req2 = {
@@ -50052,8 +50432,10 @@ function registerHoistedTools(pi, ctx, surface) {
50052
50432
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
50053
50433
  const bridge = bridgeFor(ctx, extCtx.cwd);
50054
50434
  const req = { pattern: params.pattern };
50055
- if (params.path)
50435
+ if (params.path) {
50436
+ await assertExternalDirectoryPermission(extCtx, params.path, "search");
50056
50437
  req.path = await resolvePathArg(extCtx.cwd, params.path);
50438
+ }
50057
50439
  if (params.include)
50058
50440
  req.include = splitIncludeGlobs(params.include);
50059
50441
  if (params.caseSensitive !== undefined)
@@ -50265,8 +50647,8 @@ function formatReadFooter(agentSpecifiedRange, data) {
50265
50647
  }
50266
50648
 
50267
50649
  // src/tools/imports.ts
50268
- import { StringEnum as StringEnum2 } from "@mariozechner/pi-ai";
50269
- import { Type as Type6 } from "@sinclair/typebox";
50650
+ import { StringEnum as StringEnum2 } from "@earendil-works/pi-ai";
50651
+ import { Type as Type6 } from "typebox";
50270
50652
  var ImportParams = Type6.Object({
50271
50653
  op: StringEnum2(["add", "remove", "organize"], { description: "Import operation" }),
50272
50654
  filePath: Type6.String({ description: "Path to the file" }),
@@ -50363,8 +50745,8 @@ function registerImportTools(pi, ctx) {
50363
50745
  }
50364
50746
 
50365
50747
  // src/tools/lsp.ts
50366
- import { StringEnum as StringEnum3 } from "@mariozechner/pi-ai";
50367
- import { Type as Type7 } from "@sinclair/typebox";
50748
+ import { StringEnum as StringEnum3 } from "@earendil-works/pi-ai";
50749
+ import { Type as Type7 } from "typebox";
50368
50750
  var LspDiagnosticsParams = Type7.Object({
50369
50751
  filePath: Type7.Optional(Type7.String({ description: "File to get diagnostics for (mutually exclusive with directory)" })),
50370
50752
  directory: Type7.Optional(Type7.String({
@@ -50457,8 +50839,8 @@ function registerLspTools(pi, ctx) {
50457
50839
  }
50458
50840
 
50459
50841
  // src/tools/navigate.ts
50460
- import { StringEnum as StringEnum4 } from "@mariozechner/pi-ai";
50461
- import { Type as Type8 } from "@sinclair/typebox";
50842
+ import { StringEnum as StringEnum4 } from "@earendil-works/pi-ai";
50843
+ import { Type as Type8 } from "typebox";
50462
50844
  function navigateParamsSchema() {
50463
50845
  return Type8.Object({
50464
50846
  op: StringEnum4(["call_tree", "callers", "trace_to", "impact", "trace_data"], {
@@ -50620,7 +51002,7 @@ function registerNavigateTool(pi, ctx) {
50620
51002
  // src/tools/reading.ts
50621
51003
  import { stat as stat2 } from "node:fs/promises";
50622
51004
  import { resolve as resolve4 } from "node:path";
50623
- import { Type as Type9 } from "@sinclair/typebox";
51005
+ import { Type as Type9 } from "typebox";
50624
51006
  var OutlineParams = Type9.Object({
50625
51007
  target: Type9.Union([Type9.String(), Type9.Array(Type9.String())], {
50626
51008
  description: "What to outline: a file path, directory path, URL (http:// or https://), or array of file paths. The mode is auto-detected: URLs by `http://`/`https://` prefix, directories by stat, arrays as multi-file. Directory walks cap at 200 files."
@@ -50880,8 +51262,8 @@ ${lines}`;
50880
51262
  }
50881
51263
 
50882
51264
  // src/tools/refactor.ts
50883
- import { StringEnum as StringEnum5 } from "@mariozechner/pi-ai";
50884
- import { Type as Type10 } from "@sinclair/typebox";
51265
+ import { StringEnum as StringEnum5 } from "@earendil-works/pi-ai";
51266
+ import { Type as Type10 } from "typebox";
50885
51267
  var RefactorParams = Type10.Object({
50886
51268
  op: StringEnum5(["move", "extract", "inline"], { description: "Refactoring operation" }),
50887
51269
  filePath: Type10.String({ description: "Source file" }),
@@ -50977,8 +51359,8 @@ function registerRefactorTool(pi, ctx) {
50977
51359
  }
50978
51360
 
50979
51361
  // src/tools/safety.ts
50980
- import { StringEnum as StringEnum6 } from "@mariozechner/pi-ai";
50981
- import { Type as Type11 } from "@sinclair/typebox";
51362
+ import { StringEnum as StringEnum6 } from "@earendil-works/pi-ai";
51363
+ import { Type as Type11 } from "typebox";
50982
51364
  var SafetyParams = Type11.Object({
50983
51365
  op: StringEnum6(["undo", "history", "checkpoint", "restore", "list"], {
50984
51366
  description: "Safety operation"
@@ -51110,7 +51492,7 @@ function registerSafetyTool(pi, ctx) {
51110
51492
  }
51111
51493
 
51112
51494
  // src/tools/semantic.ts
51113
- import { Type as Type12 } from "@sinclair/typebox";
51495
+ import { Type as Type12 } from "typebox";
51114
51496
  var SearchParams2 = Type12.Object({
51115
51497
  query: Type12.String({
51116
51498
  description: "Concept or capability to find, phrased as a programmer would describe the code. Examples: 'fuzzy match with whitespace tolerance', 'undo backup before edit', 'retry failed network request'."
@@ -51139,6 +51521,16 @@ function buildSemanticSections(args, payload, theme) {
51139
51521
  const lines = [theme.fg("accent", shortenPath(file2))];
51140
51522
  fileResults.forEach((result) => {
51141
51523
  const score = asNumber(result.score);
51524
+ const source = asString(result.source);
51525
+ if (source === "lexical") {
51526
+ lines.push(` ↳ ${theme.fg("muted", `[lexical match${score !== undefined ? ` — score ${score.toFixed(3)}` : ""}]`)}`);
51527
+ const snippet2 = asString(result.snippet);
51528
+ if (snippet2) {
51529
+ lines.push(...snippet2.split(`
51530
+ `).map((line) => ` ${line}`));
51531
+ }
51532
+ return;
51533
+ }
51142
51534
  const startLine = asNumber(result.start_line);
51143
51535
  const endLine = asNumber(result.end_line);
51144
51536
  const range = startLine !== undefined ? `${startLine}${endLine && endLine !== startLine ? `-${endLine}` : ""}` : "?";
@@ -51169,21 +51561,21 @@ function registerSemanticTool(pi, ctx) {
51169
51561
  name: "aft_search",
51170
51562
  label: "semantic search",
51171
51563
  description: [
51172
- "Find symbols by concept when grep keywords fall short. Returns ranked code matches with similarity scores.",
51564
+ "Find symbols by concept using hybrid semantic + lexical search. Returns ranked code matches with similarity scores and provenance tags.",
51173
51565
  "",
51174
51566
  "When to reach for it:",
51175
51567
  "- Exploring an unfamiliar area: 'where is rate limiting handled', 'how does auth flow work'",
51176
51568
  "- Concept doesn't appear as a literal string: 'retry logic', 'cache invalidation', 'graceful shutdown'",
51569
+ "- Filename-shaped concepts: 'the bridge spawn helper', 'the session detection module'",
51177
51570
  "- After 2+ grep attempts that came back empty or noisy",
51178
51571
  "- You know roughly what the function does but not what it's named",
51179
51572
  "",
51180
51573
  "When NOT to use:",
51181
- "- You have a specific symbol name → use grep",
51182
51574
  "- You have an error message or stack trace → use grep",
51183
51575
  "- You want the file/module structure → use aft_outline",
51184
51576
  "- You're following a call chain → use aft_navigate",
51185
51577
  "",
51186
- "Scores below ~0.4 are usually weak matches; treat them as 'maybe relevant' and verify with read."
51578
+ "Each result tags `source` as one of: 'semantic' (embedding match only), 'lexical' (trigram exact-token match the embedding lane missed), or 'hybrid' (both lanes agreed — strongest signal)."
51187
51579
  ].join(`
51188
51580
  `),
51189
51581
  parameters: SearchParams2,
@@ -51205,8 +51597,8 @@ function registerSemanticTool(pi, ctx) {
51205
51597
  }
51206
51598
 
51207
51599
  // src/tools/structure.ts
51208
- import { StringEnum as StringEnum7 } from "@mariozechner/pi-ai";
51209
- import { Type as Type13 } from "@sinclair/typebox";
51600
+ import { StringEnum as StringEnum7 } from "@earendil-works/pi-ai";
51601
+ import { Type as Type13 } from "typebox";
51210
51602
  var TransformParams = Type13.Object({
51211
51603
  op: StringEnum7(["add_member", "add_derive", "wrap_try_catch", "add_decorator", "add_struct_tags"], { description: "Transformation operation" }),
51212
51604
  filePath: Type13.String({ description: "Path to the source file" }),
@@ -51344,8 +51736,8 @@ function buildWorkflowHints(opts) {
51344
51736
  sections.push(`**Web/URL access**: \`aft_outline({ url })\` first for structure, then \`aft_zoom({ url, symbol: "<heading>" })\` for the specific section.`);
51345
51737
  }
51346
51738
  if (hasOutline && hasZoom && (hasGrep || hasSearch)) {
51347
- const locator = hasGrep && hasSearch ? `\`${grepName}\` or \`aft_search\`` : hasGrep ? `\`${grepName}\`` : "`aft_search`";
51348
- sections.push(`**Code exploration**: ${locator} to locate → \`aft_outline\` for structure → \`aft_zoom\` for symbol(s).`);
51739
+ const locator = hasGrep ? `\`${grepName}\`` : "`aft_search`";
51740
+ sections.push(hasGrep && hasSearch ? `**Code exploration**: For exact identifiers (\`useState\`, function names, env vars), error messages, or path-shaped queries → \`${grepName}\` first. For broad concepts ('where is X handled', 'how does Y work') → \`aft_search\`. Then use \`aft_outline\` for structure → \`aft_zoom\` for symbol(s).` : `**Code exploration**: ${locator} to locate → \`aft_outline\` for structure → \`aft_zoom\` for symbol(s).`);
51349
51741
  }
51350
51742
  if (hasNavigate) {
51351
51743
  sections.push([
@@ -51546,7 +51938,7 @@ async function src_default(pi) {
51546
51938
  log2(`AFT extension loading (plugin v${PLUGIN_VERSION})`);
51547
51939
  let binaryPath;
51548
51940
  try {
51549
- binaryPath = await findBinary();
51941
+ binaryPath = await findBinary(PLUGIN_VERSION);
51550
51942
  } catch (err) {
51551
51943
  warn2(`Failed to resolve AFT binary: ${err instanceof Error ? err.message : String(err)}. ` + "Tools will not be registered.");
51552
51944
  return;
@@ -51662,22 +52054,20 @@ ${lines}
51662
52054
  pluginVersion: PLUGIN_VERSION
51663
52055
  });
51664
52056
  },
51665
- onBashCompletion: (completion, bridge) => {
52057
+ onBashCompletion: (completion) => {
51666
52058
  handlePushedBgCompletion({
51667
52059
  ctx,
51668
52060
  directory: process.cwd(),
51669
52061
  sessionID: completion.session_id,
51670
- runtime: pi,
51671
- isActive: () => bridge.hasPendingRequests()
52062
+ runtime: pi
51672
52063
  }, completion);
51673
52064
  },
51674
- onBashLongRunning: (reminder, bridge) => {
52065
+ onBashLongRunning: (reminder) => {
51675
52066
  handlePushedBgLongRunning({
51676
52067
  ctx,
51677
52068
  directory: process.cwd(),
51678
52069
  sessionID: reminder.session_id,
51679
- runtime: pi,
51680
- isActive: () => bridge.hasPendingRequests()
52070
+ runtime: pi
51681
52071
  }, reminder);
51682
52072
  }
51683
52073
  };
@@ -51702,6 +52092,12 @@ ${lines}
51702
52092
  log2(`Eager configure skipped: cwd=${cwd} is the user home directory. ` + `The first real tool call will warm the correct project bridge.`);
51703
52093
  return;
51704
52094
  }
52095
+ if (onnxRuntimePromise) {
52096
+ await Promise.race([
52097
+ onnxRuntimePromise,
52098
+ new Promise((resolve5) => setTimeout(() => resolve5(null), 60000))
52099
+ ]);
52100
+ }
51705
52101
  const bridge = pool.getBridge(cwd);
51706
52102
  await bridge.send("status", {});
51707
52103
  } catch (err) {