@diegopetrucci/pi-extensions 0.1.78 → 0.1.79
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 +1 -1
- package/extensions/agent-workflow-audit/index.ts +3 -20
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/glimpseui.d.ts +5 -65
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-last-message/glimpseui.d.ts +5 -65
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/web/index.html +0 -27
- package/extensions/code-reviewer/index.ts +0 -34
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/context-cap/README.md +9 -9
- package/extensions/context-cap/index.ts +3 -3
- package/extensions/context-cap/package.json +2 -2
- package/extensions/context-inspector/index.ts +2 -46
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/index.ts +1 -27
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dynamic-context-pruning/README.md +16 -7
- package/extensions/dynamic-context-pruning/index.ts +9 -114
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +16 -9
- package/extensions/fast/index.ts +9 -5
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/index.ts +2 -16
- package/extensions/git-footer/package.json +1 -1
- package/extensions/librarian/index.ts +7 -39
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/README.md +5 -5
- package/extensions/minimal-footer/index.ts +3 -17
- package/extensions/minimal-footer/minimal-footer.example.json +1 -1
- package/extensions/minimal-footer/openai-usage.ts +2 -11
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/oracle/index.ts +1 -27
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/index.ts +31 -63
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/review/README.md +2 -0
- package/extensions/review/package.json +1 -1
- package/extensions/triage-comments/index.ts +0 -15
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +1 -1
|
@@ -13,10 +13,6 @@ type GitStatusSnapshot = {
|
|
|
13
13
|
|
|
14
14
|
type PullRequestSnapshot = {
|
|
15
15
|
number?: number | string;
|
|
16
|
-
state?: string;
|
|
17
|
-
isDraft?: boolean;
|
|
18
|
-
url?: string;
|
|
19
|
-
title?: string;
|
|
20
16
|
};
|
|
21
17
|
|
|
22
18
|
type CommandResult = {
|
|
@@ -74,7 +70,7 @@ const GIT_STATUS_ARGS = [
|
|
|
74
70
|
"--porcelain=v2",
|
|
75
71
|
"--branch",
|
|
76
72
|
] as const;
|
|
77
|
-
const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number
|
|
73
|
+
const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number"] as const;
|
|
78
74
|
|
|
79
75
|
function createEmptyGitStatus(): GitStatusSnapshot {
|
|
80
76
|
return {
|
|
@@ -202,10 +198,6 @@ function parsePullRequestJson(stdout: string): PullRequestSnapshot | undefined {
|
|
|
202
198
|
if (typeof record.number === "number" || typeof record.number === "string") {
|
|
203
199
|
snapshot.number = record.number;
|
|
204
200
|
}
|
|
205
|
-
if (typeof record.state === "string") snapshot.state = record.state;
|
|
206
|
-
if (typeof record.isDraft === "boolean") snapshot.isDraft = record.isDraft;
|
|
207
|
-
if (typeof record.url === "string") snapshot.url = record.url;
|
|
208
|
-
if (typeof record.title === "string") snapshot.title = record.title;
|
|
209
201
|
return snapshot;
|
|
210
202
|
}
|
|
211
203
|
|
|
@@ -228,13 +220,7 @@ function pullRequestSnapshotsEqual(
|
|
|
228
220
|
left: PullRequestSnapshot | undefined,
|
|
229
221
|
right: PullRequestSnapshot | undefined,
|
|
230
222
|
): boolean {
|
|
231
|
-
return
|
|
232
|
-
left?.number === right?.number &&
|
|
233
|
-
left?.state === right?.state &&
|
|
234
|
-
left?.isDraft === right?.isDraft &&
|
|
235
|
-
left?.url === right?.url &&
|
|
236
|
-
left?.title === right?.title
|
|
237
|
-
);
|
|
223
|
+
return left?.number === right?.number;
|
|
238
224
|
}
|
|
239
225
|
|
|
240
226
|
function defaultRunner(
|
|
@@ -766,11 +766,6 @@ async function writeLibrarianPreferences(preferences: LibrarianPreferences): Pro
|
|
|
766
766
|
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
767
767
|
}
|
|
768
768
|
|
|
769
|
-
async function writeCachePreference(preference: CacheMode): Promise<void> {
|
|
770
|
-
const preferences = await readLibrarianPreferences();
|
|
771
|
-
await writeLibrarianPreferences({ ...preferences, cacheMode: preference });
|
|
772
|
-
}
|
|
773
|
-
|
|
774
769
|
function resolveCacheDecision(preference: CacheMode): { enabled: boolean; reason: string } {
|
|
775
770
|
if (preference === "enabled") {
|
|
776
771
|
return { enabled: true, reason: "cache preference enabled; using cached local checkouts" };
|
|
@@ -1045,7 +1040,7 @@ function tryCurlyQuotePathVariant(filePath: string): string {
|
|
|
1045
1040
|
}
|
|
1046
1041
|
/**
|
|
1047
1042
|
* Returns all read path candidates including Pi's resolveReadPathAsync fallback
|
|
1048
|
-
* variants.
|
|
1043
|
+
* variants.
|
|
1049
1044
|
*/
|
|
1050
1045
|
function getReadPathCandidates(resolved: string): string[] {
|
|
1051
1046
|
const nfd = tryNFDPathVariant(resolved);
|
|
@@ -1058,11 +1053,6 @@ function getReadPathCandidates(resolved: string): string[] {
|
|
|
1058
1053
|
];
|
|
1059
1054
|
}
|
|
1060
1055
|
|
|
1061
|
-
function resolveToolPath(cwd: string, rawPath: string): string {
|
|
1062
|
-
const normalized = rawPath.startsWith("@") ? rawPath.slice(1) : rawPath;
|
|
1063
|
-
return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(cwd, normalized);
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
1056
|
function getBlockedBashReason(
|
|
1067
1057
|
command: string,
|
|
1068
1058
|
options: { workspace: string; cacheRoot: string; cacheEnabled: boolean },
|
|
@@ -1165,17 +1155,9 @@ function createLibrarianRuntimeGuardExtension(options: {
|
|
|
1165
1155
|
(options.cacheEnabled && (isInside(options.cacheRoot, p) || isInside(realCacheRoot, p)))
|
|
1166
1156
|
);
|
|
1167
1157
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
return {
|
|
1172
|
-
block: true,
|
|
1173
|
-
reason: `Librarian read is limited to its workspace/cache: ${realPath}`,
|
|
1174
|
-
};
|
|
1175
|
-
// Pi-effective path check (monotonic: block if either resolution is
|
|
1176
|
-
// outside). For the read tool, also check Pi's resolveReadPathAsync
|
|
1177
|
-
// fallback variants so a missing file whose variant resolves outside is
|
|
1178
|
-
// not silently allowed.
|
|
1158
|
+
// For the read tool, also check Pi's resolveReadPathAsync fallback
|
|
1159
|
+
// variants so a missing file whose variant resolves outside is not
|
|
1160
|
+
// silently allowed.
|
|
1179
1161
|
try {
|
|
1180
1162
|
const piResolved = resolveToCwd(input.path, options.workspace);
|
|
1181
1163
|
const candidates = getReadPathCandidates(piResolved);
|
|
@@ -1482,7 +1464,6 @@ export const __test__ = {
|
|
|
1482
1464
|
parseModelPreference,
|
|
1483
1465
|
resolveThinkingLevel,
|
|
1484
1466
|
aggregateAssistantUsage,
|
|
1485
|
-
addAssistantMessageUsage,
|
|
1486
1467
|
addSessionEventUsage,
|
|
1487
1468
|
inspectFinalAssistant,
|
|
1488
1469
|
classifyRunFailure,
|
|
@@ -1490,14 +1471,12 @@ export const __test__ = {
|
|
|
1490
1471
|
createLibrarianAbortController,
|
|
1491
1472
|
cleanupExpiredCache,
|
|
1492
1473
|
cleanupExpiredRepos,
|
|
1493
|
-
cleanupLockOwnershipError,
|
|
1494
1474
|
tryAcquireCleanupLock,
|
|
1495
1475
|
heartbeatCleanupLock,
|
|
1496
1476
|
releaseCleanupLock,
|
|
1497
1477
|
CLEANUP_LOCK_FILE,
|
|
1498
1478
|
CLEANUP_LOCK_STALE_MS,
|
|
1499
1479
|
CLEANUP_LOCK_MAX_LEASE_MS,
|
|
1500
|
-
isCleanupLockLeaseExpired,
|
|
1501
1480
|
};
|
|
1502
1481
|
|
|
1503
1482
|
export default function librarianExtension(pi: ExtensionAPI) {
|
|
@@ -1668,17 +1647,6 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1668
1647
|
const cacheRoot = getUserCacheReposRoot();
|
|
1669
1648
|
const configPath = getCacheConfigPath();
|
|
1670
1649
|
|
|
1671
|
-
const setPreference = async (mode: CacheMode): Promise<string | undefined> => {
|
|
1672
|
-
cachePreference = mode;
|
|
1673
|
-
try {
|
|
1674
|
-
await writeCachePreference(mode);
|
|
1675
|
-
return undefined;
|
|
1676
|
-
} catch (error) {
|
|
1677
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1678
|
-
return `Preference changed for this process, but could not save ${configPath}: ${message}`;
|
|
1679
|
-
}
|
|
1680
|
-
};
|
|
1681
|
-
|
|
1682
1650
|
const formatSetMessage = (mode: CacheMode, warning?: string): string => {
|
|
1683
1651
|
const main =
|
|
1684
1652
|
mode === "enabled"
|
|
@@ -1688,20 +1656,20 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1688
1656
|
};
|
|
1689
1657
|
|
|
1690
1658
|
if (action === "on" || action === "enable") {
|
|
1691
|
-
const warning = await
|
|
1659
|
+
const warning = await savePreferences({ ...currentPreferences(), cacheMode: "enabled" });
|
|
1692
1660
|
notifyCommand(ctx, formatSetMessage("enabled", warning), warning ? "warning" : "info");
|
|
1693
1661
|
return;
|
|
1694
1662
|
}
|
|
1695
1663
|
|
|
1696
1664
|
if (action === "off" || action === "disable") {
|
|
1697
|
-
const warning = await
|
|
1665
|
+
const warning = await savePreferences({ ...currentPreferences(), cacheMode: "disabled" });
|
|
1698
1666
|
notifyCommand(ctx, formatSetMessage("disabled", warning), warning ? "warning" : "info");
|
|
1699
1667
|
return;
|
|
1700
1668
|
}
|
|
1701
1669
|
|
|
1702
1670
|
if (action === "toggle") {
|
|
1703
1671
|
const next = cachePreference === "enabled" ? "disabled" : "enabled";
|
|
1704
|
-
const warning = await
|
|
1672
|
+
const warning = await savePreferences({ ...currentPreferences(), cacheMode: next });
|
|
1705
1673
|
notifyCommand(ctx, formatSetMessage(next, warning), warning ? "warning" : "info");
|
|
1706
1674
|
return;
|
|
1707
1675
|
}
|
|
@@ -10,7 +10,7 @@ It replaces pi's built-in footer with a cleaner two-line layout that focuses on
|
|
|
10
10
|
- TLH-style git dirty counts, ahead/behind counts, and optional PR number
|
|
11
11
|
- current repo name
|
|
12
12
|
- current context percentage
|
|
13
|
-
- red `DUMB ZONE` indicator when context usage is above
|
|
13
|
+
- red `DUMB ZONE` indicator when context usage is above 350k tokens
|
|
14
14
|
- current model and thinking level
|
|
15
15
|
- OpenAI Codex usage windows, labeled from the durations reported by OpenAI
|
|
16
16
|
- `xp` marker when Pi experimental features are enabled
|
|
@@ -39,7 +39,7 @@ main · +1 ~2 ?3 ↑1 • PR #42
|
|
|
39
39
|
|
|
40
40
|
The markers are `!` conflicts, `+` staged, `~` unstaged, `?` untracked, `↑` ahead, and `↓` behind. PR numbers are fetched best-effort with `gh pr view`. Git polling is skipped until Pi reports that the project is trusted, and `git status` is run with fsmonitor disabled.
|
|
41
41
|
|
|
42
|
-
When context usage is above
|
|
42
|
+
When context usage is above 350k tokens, the bottom-left line includes a red warning:
|
|
43
43
|
|
|
44
44
|
```text
|
|
45
45
|
44.1% · DUMB ZONE
|
|
@@ -115,7 +115,7 @@ Example:
|
|
|
115
115
|
"showPercent": true,
|
|
116
116
|
"dumbZone": {
|
|
117
117
|
"enabled": true,
|
|
118
|
-
"thresholdTokens":
|
|
118
|
+
"thresholdTokens": 350000,
|
|
119
119
|
"label": "DUMB ZONE",
|
|
120
120
|
"color": "error"
|
|
121
121
|
}
|
|
@@ -231,7 +231,7 @@ Disable git dirty/ahead/PR status:
|
|
|
231
231
|
|
|
232
232
|
- **Top left:** current git branch plus dirty/ahead/PR status when available
|
|
233
233
|
- **Top right:** current repo directory name
|
|
234
|
-
- **Bottom left:** current context usage percentage, plus red `DUMB ZONE` above
|
|
234
|
+
- **Bottom left:** current context usage percentage, plus red `DUMB ZONE` above 350k context tokens
|
|
235
235
|
- **Bottom left on `openai-codex`:** current context usage percentage plus the Codex usage windows reported for the account
|
|
236
236
|
- **Bottom left on `openai` ChatGPT OAuth:** current context usage percentage plus `usage unsupported`; no legacy WHAM request is attempted
|
|
237
237
|
- **Bottom left with `PI_EXPERIMENTAL=1`:** current context usage percentage plus `xp`
|
|
@@ -246,7 +246,7 @@ This extension also lives inside the broader [`pi-extensions`](../../README.md)
|
|
|
246
246
|
- Replaces pi's built-in footer entirely.
|
|
247
247
|
- Uses pi footer data for git branch updates, plus background cached git/gh checks for dirty counts and PR number after project trust is granted.
|
|
248
248
|
- Shows only context percentage, not context window size.
|
|
249
|
-
- Shows `DUMB ZONE` only while context usage is above
|
|
249
|
+
- Shows `DUMB ZONE` only while context usage is above 350k tokens.
|
|
250
250
|
- Shows the model id rather than a provider-specific display label.
|
|
251
251
|
- Shows `xp` when `PI_EXPERIMENTAL=1`.
|
|
252
252
|
- For `openai-codex`, reads pi's stored OAuth login and fetches usage from ChatGPT's backend usage endpoint. Window labels are inferred from each window's reported duration; missing or unrecognized durations use neutral `usage` labels rather than guessing from window position.
|
|
@@ -24,7 +24,7 @@ const DEFAULT_CONFIG: MinimalFooterConfig = {
|
|
|
24
24
|
showPercent: true,
|
|
25
25
|
dumbZone: {
|
|
26
26
|
enabled: true,
|
|
27
|
-
thresholdTokens:
|
|
27
|
+
thresholdTokens: 350_000,
|
|
28
28
|
label: "DUMB ZONE",
|
|
29
29
|
color: "error",
|
|
30
30
|
},
|
|
@@ -148,10 +148,6 @@ type GitStatusSnapshot = {
|
|
|
148
148
|
|
|
149
149
|
type PullRequestSnapshot = {
|
|
150
150
|
number?: number | string;
|
|
151
|
-
state?: string;
|
|
152
|
-
isDraft?: boolean;
|
|
153
|
-
url?: string;
|
|
154
|
-
title?: string;
|
|
155
151
|
};
|
|
156
152
|
|
|
157
153
|
type CommandResult = {
|
|
@@ -200,7 +196,7 @@ const GIT_STATUS_ARGS = [
|
|
|
200
196
|
"--porcelain=v2",
|
|
201
197
|
"--branch",
|
|
202
198
|
] as const;
|
|
203
|
-
const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number
|
|
199
|
+
const GH_PR_VIEW_ARGS = ["pr", "view", "--json", "number"] as const;
|
|
204
200
|
|
|
205
201
|
function readConfigFile(path: string): RecursivePartial<MinimalFooterConfig> {
|
|
206
202
|
if (!existsSync(path)) return {};
|
|
@@ -468,10 +464,6 @@ function parsePullRequestJson(stdout: string): PullRequestSnapshot | undefined {
|
|
|
468
464
|
if (typeof record.number === "number" || typeof record.number === "string") {
|
|
469
465
|
snapshot.number = record.number;
|
|
470
466
|
}
|
|
471
|
-
if (typeof record.state === "string") snapshot.state = record.state;
|
|
472
|
-
if (typeof record.isDraft === "boolean") snapshot.isDraft = record.isDraft;
|
|
473
|
-
if (typeof record.url === "string") snapshot.url = record.url;
|
|
474
|
-
if (typeof record.title === "string") snapshot.title = record.title;
|
|
475
467
|
return snapshot;
|
|
476
468
|
}
|
|
477
469
|
|
|
@@ -494,13 +486,7 @@ function pullRequestSnapshotsEqual(
|
|
|
494
486
|
left: PullRequestSnapshot | undefined,
|
|
495
487
|
right: PullRequestSnapshot | undefined,
|
|
496
488
|
): boolean {
|
|
497
|
-
return
|
|
498
|
-
left?.number === right?.number &&
|
|
499
|
-
left?.state === right?.state &&
|
|
500
|
-
left?.isDraft === right?.isDraft &&
|
|
501
|
-
left?.url === right?.url &&
|
|
502
|
-
left?.title === right?.title
|
|
503
|
-
);
|
|
489
|
+
return left?.number === right?.number;
|
|
504
490
|
}
|
|
505
491
|
|
|
506
492
|
function defaultRunner(
|
|
@@ -15,7 +15,6 @@ export type UsageAvailability = "unsupported" | "unavailable";
|
|
|
15
15
|
|
|
16
16
|
interface WhamUsageWindow {
|
|
17
17
|
limit_window_seconds?: number;
|
|
18
|
-
reset_at?: number;
|
|
19
18
|
used_percent?: number;
|
|
20
19
|
}
|
|
21
20
|
|
|
@@ -29,7 +28,6 @@ interface WhamUsageResponse {
|
|
|
29
28
|
export interface UsageWindow {
|
|
30
29
|
usedPercent?: number;
|
|
31
30
|
windowSeconds?: number;
|
|
32
|
-
resetAt?: number;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
export interface UsageSnapshot {
|
|
@@ -59,19 +57,12 @@ function normalizeWindowSeconds(value?: number): number | undefined {
|
|
|
59
57
|
return value;
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
function normalizeResetAt(value?: number): number | undefined {
|
|
63
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
64
|
-
return value * 1000;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
60
|
function parseUsageWindow(window?: WhamUsageWindow): UsageWindow | undefined {
|
|
68
61
|
if (!window) return undefined;
|
|
69
62
|
const usedPercent = normalizeUsedPercent(window.used_percent);
|
|
70
63
|
const windowSeconds = normalizeWindowSeconds(window.limit_window_seconds);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return undefined;
|
|
74
|
-
return { usedPercent, windowSeconds, resetAt };
|
|
64
|
+
if (usedPercent === undefined && windowSeconds === undefined) return undefined;
|
|
65
|
+
return { usedPercent, windowSeconds };
|
|
75
66
|
}
|
|
76
67
|
|
|
77
68
|
function parseUsageSnapshot(data: WhamUsageResponse): Omit<UsageSnapshot, "fetchedAt"> {
|
|
@@ -1810,8 +1810,7 @@ function createOracleExtension(pi: ExtensionAPI, deps: OracleExtensionDeps = {})
|
|
|
1810
1810
|
onUpdate?.(partial);
|
|
1811
1811
|
};
|
|
1812
1812
|
|
|
1813
|
-
let
|
|
1814
|
-
for (let index = 0; index < attempts.length; index++) {
|
|
1813
|
+
for (let index = 0; ; index++) {
|
|
1815
1814
|
const previous = index > 0 ? attempts[index - 1] : undefined;
|
|
1816
1815
|
const attempt = previous
|
|
1817
1816
|
? withFallbackReason(attempts[index], previous)
|
|
@@ -1835,7 +1834,6 @@ function createOracleExtension(pi: ExtensionAPI, deps: OracleExtensionDeps = {})
|
|
|
1835
1834
|
});
|
|
1836
1835
|
}
|
|
1837
1836
|
|
|
1838
|
-
lastErrorResult = result;
|
|
1839
1837
|
const canFallBack = index < attempts.length - 1 && isModelAvailabilityError(result.error);
|
|
1840
1838
|
if (!canFallBack) {
|
|
1841
1839
|
return finalizeResult({
|
|
@@ -1844,30 +1842,6 @@ function createOracleExtension(pi: ExtensionAPI, deps: OracleExtensionDeps = {})
|
|
|
1844
1842
|
});
|
|
1845
1843
|
}
|
|
1846
1844
|
}
|
|
1847
|
-
|
|
1848
|
-
return finalizeResult({
|
|
1849
|
-
content: [
|
|
1850
|
-
{
|
|
1851
|
-
type: "text",
|
|
1852
|
-
text: lastErrorResult?.error ?? "Oracle could not select an available model.",
|
|
1853
|
-
},
|
|
1854
|
-
],
|
|
1855
|
-
details: lastErrorResult?.details ?? {
|
|
1856
|
-
modelRef: "",
|
|
1857
|
-
provider: ctx.model?.provider ?? "unknown",
|
|
1858
|
-
modelId: "",
|
|
1859
|
-
modelName: undefined,
|
|
1860
|
-
thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
1861
|
-
autoSelected: true,
|
|
1862
|
-
selectionReason: "No authenticated models were available to run the oracle.",
|
|
1863
|
-
includeBash: params.includeBash ?? false,
|
|
1864
|
-
usage: createEmptyUsage(),
|
|
1865
|
-
stderr: "",
|
|
1866
|
-
exitCode: 1,
|
|
1867
|
-
durationMs: 0,
|
|
1868
|
-
cwd: params.cwd ?? ctx.cwd,
|
|
1869
|
-
},
|
|
1870
|
-
});
|
|
1871
1845
|
} finally {
|
|
1872
1846
|
activeRuns.delete(toolCallId);
|
|
1873
1847
|
updateOracleUi(ctx, activeRuns);
|
|
@@ -77,17 +77,9 @@ function normalizeWindowsShellPathLocal(filePath: string): string {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
* Mirrors Pi's normalizePath (dist/utils/paths.js).
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* normalizeUnicodeSpaces — replace Unicode space characters with ASCII space.
|
|
84
|
-
* stripAtPrefix — strip a leading "@" path sigil.
|
|
85
|
-
* expandTilde — expand "~"/"~/" to homedir (default true).
|
|
86
|
-
*
|
|
87
|
-
* On win32, Git Bash / WSL drive-path normalisation always runs (not gated
|
|
88
|
-
* by expandTilde); only the tilde expansion itself is gated by expandTilde.
|
|
89
|
-
* Always resolves "file://" URLs via fileURLToPath; throws for non-local hosts
|
|
90
|
-
* (e.g. file://hostname/…) or invalid URLs.
|
|
80
|
+
* Mirrors Pi's normalizePath (dist/utils/paths.js). On win32, Git Bash / WSL
|
|
81
|
+
* drive-path normalisation always runs; only tilde expansion is gated by
|
|
82
|
+
* expandTilde.
|
|
91
83
|
*/
|
|
92
84
|
function normalizePath(
|
|
93
85
|
input: string,
|
|
@@ -797,6 +789,28 @@ async function confirmProtectedPathAction(
|
|
|
797
789
|
);
|
|
798
790
|
}
|
|
799
791
|
|
|
792
|
+
async function confirmLexicalOrResolvedPath(
|
|
793
|
+
toolName: "write" | "edit",
|
|
794
|
+
normalizedPath: NormalizedPath,
|
|
795
|
+
ctx: ConfirmationContext & { cwd?: string },
|
|
796
|
+
): Promise<GuardDecision> {
|
|
797
|
+
// Monotonic hardening: also check via Pi-style path resolution against ctx.cwd.
|
|
798
|
+
// OR logic: protected if EITHER the lexical check OR the Pi-resolved check matches.
|
|
799
|
+
// When only the Pi-resolved check catches a path, show the resolved absolute path in
|
|
800
|
+
// the reason so the user understands why the plain-looking path is protected.
|
|
801
|
+
const lexicalProtected = isProtectedPath(normalizedPath);
|
|
802
|
+
const piResult = checkPiResolvedPath(normalizedPath.original, ctx.cwd ?? process.cwd());
|
|
803
|
+
const piAlsoProtected = piResult.status === "protected" || piResult.status === "error";
|
|
804
|
+
const displaySuffix = lexicalProtected
|
|
805
|
+
? undefined
|
|
806
|
+
: piResult.status === "protected"
|
|
807
|
+
? ` (resolves to ${piResult.resolvedPath})`
|
|
808
|
+
: piResult.status === "error"
|
|
809
|
+
? " (path could not be resolved)"
|
|
810
|
+
: undefined;
|
|
811
|
+
return confirmProtectedPathAction(toolName, normalizedPath, ctx, piAlsoProtected, displaySuffix);
|
|
812
|
+
}
|
|
813
|
+
|
|
800
814
|
export default function (pi: ExtensionAPI) {
|
|
801
815
|
pi.on("tool_call", async (event, ctx) => {
|
|
802
816
|
if (event.toolName === "bash" || event.toolName === "powershell") {
|
|
@@ -824,60 +838,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
824
838
|
return undefined;
|
|
825
839
|
}
|
|
826
840
|
|
|
827
|
-
if (event.toolName === "write") {
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
}
|
|
832
|
-
// Monotonic hardening: also check via Pi-style path resolution against ctx.cwd.
|
|
833
|
-
// OR logic: protected if EITHER the lexical check OR the Pi-resolved check matches.
|
|
834
|
-
// When only the Pi-resolved check catches a path, show the resolved absolute path in
|
|
835
|
-
// the reason so the user understands why the plain-looking path is protected.
|
|
836
|
-
const lexicalProtected = isProtectedPath(normalizedPath);
|
|
837
|
-
const piResult = checkPiResolvedPath(normalizedPath.original, ctx.cwd ?? process.cwd());
|
|
838
|
-
const piAlsoProtected = piResult.status === "protected" || piResult.status === "error";
|
|
839
|
-
const displaySuffix = lexicalProtected
|
|
840
|
-
? undefined
|
|
841
|
-
: piResult.status === "protected"
|
|
842
|
-
? ` (resolves to ${piResult.resolvedPath})`
|
|
843
|
-
: piResult.status === "error"
|
|
844
|
-
? " (path could not be resolved)"
|
|
845
|
-
: undefined;
|
|
846
|
-
return confirmProtectedPathAction(
|
|
847
|
-
"write",
|
|
848
|
-
normalizedPath,
|
|
849
|
-
ctx,
|
|
850
|
-
piAlsoProtected,
|
|
851
|
-
displaySuffix,
|
|
852
|
-
);
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
if (event.toolName === "edit") {
|
|
856
|
-
const normalizedPath = validateEditInput(event.input);
|
|
841
|
+
if (event.toolName === "write" || event.toolName === "edit") {
|
|
842
|
+
const toolName = event.toolName;
|
|
843
|
+
const normalizedPath =
|
|
844
|
+
toolName === "write" ? validateWriteInput(event.input) : validateEditInput(event.input);
|
|
857
845
|
if (!normalizedPath) {
|
|
858
|
-
return { block: true, reason:
|
|
846
|
+
return { block: true, reason: `Malformed ${toolName} input blocked` };
|
|
859
847
|
}
|
|
860
|
-
|
|
861
|
-
// OR logic: protected if EITHER the lexical check OR the Pi-resolved check matches.
|
|
862
|
-
// When only the Pi-resolved check catches a path, show the resolved absolute path in
|
|
863
|
-
// the reason so the user understands why the plain-looking path is protected.
|
|
864
|
-
const lexicalProtected = isProtectedPath(normalizedPath);
|
|
865
|
-
const piResult = checkPiResolvedPath(normalizedPath.original, ctx.cwd ?? process.cwd());
|
|
866
|
-
const piAlsoProtected = piResult.status === "protected" || piResult.status === "error";
|
|
867
|
-
const displaySuffix = lexicalProtected
|
|
868
|
-
? undefined
|
|
869
|
-
: piResult.status === "protected"
|
|
870
|
-
? ` (resolves to ${piResult.resolvedPath})`
|
|
871
|
-
: piResult.status === "error"
|
|
872
|
-
? " (path could not be resolved)"
|
|
873
|
-
: undefined;
|
|
874
|
-
return confirmProtectedPathAction(
|
|
875
|
-
"edit",
|
|
876
|
-
normalizedPath,
|
|
877
|
-
ctx,
|
|
878
|
-
piAlsoProtected,
|
|
879
|
-
displaySuffix,
|
|
880
|
-
);
|
|
848
|
+
return confirmLexicalOrResolvedPath(toolName, normalizedPath, ctx);
|
|
881
849
|
}
|
|
882
850
|
|
|
883
851
|
return undefined;
|
|
@@ -29,7 +29,9 @@ Then reload pi:
|
|
|
29
29
|
|
|
30
30
|
## Notes
|
|
31
31
|
|
|
32
|
+
- `/review` requires interactive mode (TUI); `/end-review` requires a UI.
|
|
32
33
|
- This fork adds loop fixing, custom review instructions persisted in the session, a blocking-findings parser, and `/end-review` handoff modes (return only, return and summarize, or return and fix findings). Loop fixing cycles until the parser reports no blocking findings: tagged P0–P2 items, or a "needs attention" verdict when no priority tags are present. Commit review is excluded from loop fixing.
|
|
34
|
+
- Loop fixing always uses Empty branch mode and skips the Empty branch / Current session choice.
|
|
33
35
|
- PR review requires `gh` access and a clean working tree for tracked files.
|
|
34
36
|
- If the project is trusted and a `REVIEW_GUIDELINES.md` file exists next to the repo's `<pi-config-dir>` directory, its contents are appended to the review prompt. `<pi-config-dir>` is Pi's runtime config directory name (`CONFIG_DIR_NAME`; `.pi` by default).
|
|
35
37
|
|
|
@@ -272,8 +272,6 @@ type ToolCall = {
|
|
|
272
272
|
id: string;
|
|
273
273
|
name: string;
|
|
274
274
|
args: unknown;
|
|
275
|
-
startedAt: number;
|
|
276
|
-
endedAt?: number;
|
|
277
275
|
isError?: boolean;
|
|
278
276
|
};
|
|
279
277
|
|
|
@@ -736,12 +734,6 @@ function getReadPathCandidates(resolved: string): string[] {
|
|
|
736
734
|
];
|
|
737
735
|
}
|
|
738
736
|
|
|
739
|
-
function resolveToolPath(cwd: string, rawPath: string | undefined): string {
|
|
740
|
-
const input = rawPath?.trim() || ".";
|
|
741
|
-
const normalized = input.startsWith("@") ? input.slice(1) : input;
|
|
742
|
-
return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(cwd, normalized);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
737
|
async function assertToolPathInsideCwd(
|
|
746
738
|
cwd: string,
|
|
747
739
|
rawPath: unknown,
|
|
@@ -754,11 +746,6 @@ async function assertToolPathInsideCwd(
|
|
|
754
746
|
// where realpath fails and the lexical fallback starts with the alias prefix.
|
|
755
747
|
const root = await fs.realpath(cwd).catch(() => path.resolve(cwd));
|
|
756
748
|
const lexicalCwd = path.resolve(cwd);
|
|
757
|
-
const resolved = resolveToolPath(cwd, rawPath);
|
|
758
|
-
const realPath = await fs.realpath(resolved).catch(() => resolved);
|
|
759
|
-
if (!isInside(root, realPath) && !isInside(lexicalCwd, realPath))
|
|
760
|
-
return `${toolName} is limited to the local checkout: ${realPath}`;
|
|
761
|
-
// Pi-effective path check (monotonic: block if either resolution is outside).
|
|
762
749
|
// For the read tool, also check Pi's resolveReadPathAsync fallback variants so
|
|
763
750
|
// a missing file whose variant resolves outside is not silently allowed.
|
|
764
751
|
try {
|
|
@@ -2841,7 +2828,6 @@ export default function triageCommentsExtension(pi: ExtensionAPI) {
|
|
|
2841
2828
|
id: event.toolCallId,
|
|
2842
2829
|
name: event.toolName,
|
|
2843
2830
|
args: event.args,
|
|
2844
|
-
startedAt: Date.now(),
|
|
2845
2831
|
});
|
|
2846
2832
|
if (details.toolCalls.length > MAX_TOOL_CALLS_TO_KEEP) {
|
|
2847
2833
|
details.toolCalls.splice(0, details.toolCalls.length - MAX_TOOL_CALLS_TO_KEEP);
|
|
@@ -2851,7 +2837,6 @@ export default function triageCommentsExtension(pi: ExtensionAPI) {
|
|
|
2851
2837
|
case "tool_execution_end": {
|
|
2852
2838
|
const call = details.toolCalls.find((item) => item.id === event.toolCallId);
|
|
2853
2839
|
if (call) {
|
|
2854
|
-
call.endedAt = Date.now();
|
|
2855
2840
|
call.isError = event.isError;
|
|
2856
2841
|
}
|
|
2857
2842
|
emit();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.79",
|
|
4
4
|
"description": "A collection of pi extensions and skills for annotation UIs, context management, workflow audits, contrarian review, review-comment triage, notifications, brrr push alerts, safety guards, GitHub research, repo-local knowledge, todos, tool rendering, model/provider helpers, and Xiaohei-style article illustrations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|