@diegopetrucci/pi-extensions 0.1.78 → 0.1.80
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/prompt.ts +2 -8
- package/extensions/annotate-last-message/web/index.html +0 -27
- package/extensions/code-reviewer/index.ts +2 -46
- 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 +5 -55
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dynamic-context-pruning/README.md +24 -7
- package/extensions/dynamic-context-pruning/index.ts +21 -130
- 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 +8 -46
- 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 +5 -55
- 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/project-mcp-json/config.ts +2 -1
- package/extensions/project-mcp-json/package.json +1 -1
- package/extensions/quiet-tools/index.ts +15 -6
- package/extensions/quiet-tools/package.json +1 -1
- package/extensions/review/README.md +2 -0
- package/extensions/review/package.json +1 -1
- package/extensions/triage-comments/index.ts +1 -22
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ A collection of [pi](https://pi.dev) agent extensions I made. (Btw, [I am also b
|
|
|
31
31
|
|
|
32
32
|
### Context management
|
|
33
33
|
|
|
34
|
-
- [`context-cap`](./extensions/context-cap): Caps effective model context windows at
|
|
34
|
+
- [`context-cap`](./extensions/context-cap): Caps effective model context windows at 350k tokens by default so pi avoids the `dumb zone`; toggle temporarily with `/context-cap`.
|
|
35
35
|
- [`context-inspector`](./extensions/context-inspector): Adds `/context`, a local self-contained HTML dashboard that breaks down where the current session context is going, with category overview, top offenders, and drilldown search.
|
|
36
36
|
- [`dynamic-context-pruning`](./extensions/dynamic-context-pruning): Automatically prunes duplicate tool results, stale errored inputs, and superseded file-op outputs from long sessions behind a cache-aware net-benefit gate, with a `/prune` picker and `/context-pruning` status/control commands.
|
|
37
37
|
|
|
@@ -63,10 +63,8 @@ type AuditDetails = {
|
|
|
63
63
|
toolCalls: ToolCall[];
|
|
64
64
|
startedAt: number;
|
|
65
65
|
endedAt?: number;
|
|
66
|
-
error?: string;
|
|
67
66
|
initialGitStatus?: string;
|
|
68
67
|
finalGitStatus?: string;
|
|
69
|
-
reportLength?: number;
|
|
70
68
|
toolCallCount?: number;
|
|
71
69
|
};
|
|
72
70
|
|
|
@@ -250,14 +248,14 @@ function renderCollapsedReport(report: string, lineLimit = COLLAPSED_REPORT_LINE
|
|
|
250
248
|
].join("\n");
|
|
251
249
|
}
|
|
252
250
|
|
|
253
|
-
function summarizeGitStatus(status: string | undefined): {
|
|
251
|
+
function summarizeGitStatus(status: string | undefined): { dirtyCount: number } {
|
|
254
252
|
const lines =
|
|
255
253
|
status
|
|
256
254
|
?.split("\n")
|
|
257
255
|
.map((line) => line.trim())
|
|
258
256
|
.filter(Boolean) ?? [];
|
|
259
257
|
const dirty = lines.filter((line) => !line.startsWith("##"));
|
|
260
|
-
return {
|
|
258
|
+
return { dirtyCount: dirty.length };
|
|
261
259
|
}
|
|
262
260
|
|
|
263
261
|
function appendRunBoundary(report: string, details: AuditDetails): string {
|
|
@@ -373,12 +371,6 @@ function getReadPathCandidates(resolved: string): string[] {
|
|
|
373
371
|
];
|
|
374
372
|
}
|
|
375
373
|
|
|
376
|
-
function resolveToolPath(cwd: string, rawPath: unknown): string {
|
|
377
|
-
const input = typeof rawPath === "string" && rawPath.trim() ? rawPath.trim() : ".";
|
|
378
|
-
const normalized = input.startsWith("@") ? input.slice(1) : input;
|
|
379
|
-
return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(cwd, normalized);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
374
|
function isInside(parent: string, child: string): boolean {
|
|
383
375
|
const parentResolved = path.resolve(parent);
|
|
384
376
|
const childResolved = path.resolve(child);
|
|
@@ -399,11 +391,6 @@ async function assertToolPathInsideCwd(
|
|
|
399
391
|
// where realpath fails and the lexical fallback starts with the alias prefix.
|
|
400
392
|
const root = await fs.realpath(cwd).catch(() => path.resolve(cwd));
|
|
401
393
|
const lexicalCwd = path.resolve(cwd);
|
|
402
|
-
const resolved = resolveToolPath(cwd, rawPath);
|
|
403
|
-
const realPath = await fs.realpath(resolved).catch(() => resolved);
|
|
404
|
-
if (!isInside(root, realPath) && !isInside(lexicalCwd, realPath))
|
|
405
|
-
return `${toolName} is limited to the audited checkout: ${realPath}`;
|
|
406
|
-
// Pi-effective path check (monotonic: block if either resolution is outside).
|
|
407
394
|
// For the read tool, also check Pi's resolveReadPathAsync fallback variants so
|
|
408
395
|
// a missing file whose variant resolves outside is not silently allowed.
|
|
409
396
|
try {
|
|
@@ -1157,7 +1144,7 @@ function hasInlineFlag(tokens: string[], flags: string[]): boolean {
|
|
|
1157
1144
|
function getBlockedInterpreterReason(commandName: string, tokens: string[]): string | undefined {
|
|
1158
1145
|
const lowerTokens = tokens.map((token) => token.toLowerCase());
|
|
1159
1146
|
if (
|
|
1160
|
-
["node", "
|
|
1147
|
+
["node", "bun"].includes(commandName) &&
|
|
1161
1148
|
hasInlineFlag(lowerTokens, [
|
|
1162
1149
|
"-e",
|
|
1163
1150
|
"--eval",
|
|
@@ -1718,17 +1705,14 @@ async function runAudit(
|
|
|
1718
1705
|
throw new Error(outcome.reason);
|
|
1719
1706
|
}
|
|
1720
1707
|
details.endedAt = Date.now();
|
|
1721
|
-
details.reportLength = lastContent.length;
|
|
1722
1708
|
emit();
|
|
1723
1709
|
return { report: lastContent, details };
|
|
1724
1710
|
} catch (error) {
|
|
1725
1711
|
const failure = classifyRunFailure(error, abortController.callerAborted);
|
|
1726
1712
|
const { message } = failure;
|
|
1727
1713
|
details.status = failure.status;
|
|
1728
|
-
details.error = failure.error;
|
|
1729
1714
|
details.endedAt = Date.now();
|
|
1730
1715
|
lastContent = message;
|
|
1731
|
-
details.reportLength = lastContent.length;
|
|
1732
1716
|
emit();
|
|
1733
1717
|
return { report: `## Agent Workflow Audit failed\n\n${message}`, details };
|
|
1734
1718
|
} finally {
|
|
@@ -1866,7 +1850,6 @@ export default function agentWorkflowAuditExtension(pi: ExtensionAPI) {
|
|
|
1866
1850
|
if (!details) return;
|
|
1867
1851
|
details.finalGitStatus = await gitStatusShort(pi, ctx.cwd, ctx.signal).catch(() => undefined);
|
|
1868
1852
|
report = appendRunBoundary(report, details);
|
|
1869
|
-
details.reportLength = report.length;
|
|
1870
1853
|
const parentDetails: AuditDetails = {
|
|
1871
1854
|
...details,
|
|
1872
1855
|
toolCallCount: details.toolCalls.length,
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
declare module "glimpseui" {
|
|
2
|
-
import { EventEmitter } from "node:events";
|
|
3
|
-
|
|
4
2
|
export type FollowMode = "snap" | "spring";
|
|
5
3
|
export type CursorAnchor =
|
|
6
4
|
| "top-left"
|
|
@@ -32,67 +30,9 @@ declare module "glimpseui" {
|
|
|
32
30
|
timeout?: number;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
visibleY?: number;
|
|
41
|
-
visibleWidth?: number;
|
|
42
|
-
visibleHeight?: number;
|
|
43
|
-
x?: number;
|
|
44
|
-
y?: number;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface GlimpseAppearanceInfo {
|
|
48
|
-
darkMode: boolean;
|
|
49
|
-
accentColor: string;
|
|
50
|
-
reduceMotion: boolean;
|
|
51
|
-
increaseContrast: boolean;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface GlimpseCursorInfo {
|
|
55
|
-
x: number;
|
|
56
|
-
y: number;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface GlimpseCursorTip {
|
|
60
|
-
x: number;
|
|
61
|
-
y: number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface GlimpseInfo {
|
|
65
|
-
screen: GlimpseScreenInfo;
|
|
66
|
-
screens: GlimpseScreenInfo[];
|
|
67
|
-
appearance: GlimpseAppearanceInfo;
|
|
68
|
-
cursor: GlimpseCursorInfo;
|
|
69
|
-
cursorTip: GlimpseCursorTip | null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export class GlimpseWindow extends EventEmitter {
|
|
73
|
-
on(event: "ready", listener: (info: GlimpseInfo) => void): this;
|
|
74
|
-
on(event: "message", listener: (data: unknown) => void): this;
|
|
75
|
-
on(event: "info", listener: (info: GlimpseInfo) => void): this;
|
|
76
|
-
on(event: "closed", listener: () => void): this;
|
|
77
|
-
on(event: "error", listener: (error: Error) => void): this;
|
|
78
|
-
once(event: "ready", listener: (info: GlimpseInfo) => void): this;
|
|
79
|
-
once(event: "message", listener: (data: unknown) => void): this;
|
|
80
|
-
once(event: "info", listener: (info: GlimpseInfo) => void): this;
|
|
81
|
-
once(event: "closed", listener: () => void): this;
|
|
82
|
-
once(event: "error", listener: (error: Error) => void): this;
|
|
83
|
-
send(js: string): void;
|
|
84
|
-
setHTML(html: string): void;
|
|
85
|
-
show(options?: { title?: string }): void;
|
|
86
|
-
close(): void;
|
|
87
|
-
loadFile(path: string): void;
|
|
88
|
-
get info(): GlimpseInfo | null;
|
|
89
|
-
getInfo(): void;
|
|
90
|
-
followCursor(enabled: boolean, anchor?: CursorAnchor, mode?: FollowMode): void;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function open(html: string, options?: GlimpseOpenOptions): GlimpseWindow;
|
|
94
|
-
export function prompt<T = unknown>(
|
|
95
|
-
html: string,
|
|
96
|
-
options?: GlimpseOpenOptions,
|
|
97
|
-
): Promise<T | null>;
|
|
33
|
+
export function getNativeHostInfo(): {
|
|
34
|
+
path: string;
|
|
35
|
+
extraArgs?: string[];
|
|
36
|
+
buildHint?: string;
|
|
37
|
+
};
|
|
98
38
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-annotate-git-diff",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "A standalone pi extension that adds /annotate-git-diff, a native Glimpse UI for annotating git diffs and sending explicit feedback directly to the agent; close recovery keeps unsent drafts in the editor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
declare module "glimpseui" {
|
|
2
|
-
import { EventEmitter } from "node:events";
|
|
3
|
-
|
|
4
2
|
export type FollowMode = "snap" | "spring";
|
|
5
3
|
export type CursorAnchor =
|
|
6
4
|
| "top-left"
|
|
@@ -32,67 +30,9 @@ declare module "glimpseui" {
|
|
|
32
30
|
timeout?: number;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
visibleY?: number;
|
|
41
|
-
visibleWidth?: number;
|
|
42
|
-
visibleHeight?: number;
|
|
43
|
-
x?: number;
|
|
44
|
-
y?: number;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface GlimpseAppearanceInfo {
|
|
48
|
-
darkMode: boolean;
|
|
49
|
-
accentColor: string;
|
|
50
|
-
reduceMotion: boolean;
|
|
51
|
-
increaseContrast: boolean;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface GlimpseCursorInfo {
|
|
55
|
-
x: number;
|
|
56
|
-
y: number;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface GlimpseCursorTip {
|
|
60
|
-
x: number;
|
|
61
|
-
y: number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface GlimpseInfo {
|
|
65
|
-
screen: GlimpseScreenInfo;
|
|
66
|
-
screens: GlimpseScreenInfo[];
|
|
67
|
-
appearance: GlimpseAppearanceInfo;
|
|
68
|
-
cursor: GlimpseCursorInfo;
|
|
69
|
-
cursorTip: GlimpseCursorTip | null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export class GlimpseWindow extends EventEmitter {
|
|
73
|
-
on(event: "ready", listener: (info: GlimpseInfo) => void): this;
|
|
74
|
-
on(event: "message", listener: (data: unknown) => void): this;
|
|
75
|
-
on(event: "info", listener: (info: GlimpseInfo) => void): this;
|
|
76
|
-
on(event: "closed", listener: () => void): this;
|
|
77
|
-
on(event: "error", listener: (error: Error) => void): this;
|
|
78
|
-
once(event: "ready", listener: (info: GlimpseInfo) => void): this;
|
|
79
|
-
once(event: "message", listener: (data: unknown) => void): this;
|
|
80
|
-
once(event: "info", listener: (info: GlimpseInfo) => void): this;
|
|
81
|
-
once(event: "closed", listener: () => void): this;
|
|
82
|
-
once(event: "error", listener: (error: Error) => void): this;
|
|
83
|
-
send(js: string): void;
|
|
84
|
-
setHTML(html: string): void;
|
|
85
|
-
show(options?: { title?: string }): void;
|
|
86
|
-
close(): void;
|
|
87
|
-
loadFile(path: string): void;
|
|
88
|
-
get info(): GlimpseInfo | null;
|
|
89
|
-
getInfo(): void;
|
|
90
|
-
followCursor(enabled: boolean, anchor?: CursorAnchor, mode?: FollowMode): void;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function open(html: string, options?: GlimpseOpenOptions): GlimpseWindow;
|
|
94
|
-
export function prompt<T = unknown>(
|
|
95
|
-
html: string,
|
|
96
|
-
options?: GlimpseOpenOptions,
|
|
97
|
-
): Promise<T | null>;
|
|
33
|
+
export function getNativeHostInfo(): {
|
|
34
|
+
path: string;
|
|
35
|
+
extraArgs?: string[];
|
|
36
|
+
buildHint?: string;
|
|
37
|
+
};
|
|
98
38
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-annotate-last-message",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "A standalone pi extension that adds /annotate-last-message, a native Glimpse UI for annotating the latest assistant reply.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AnnotateLastMessageInlineComment,
|
|
3
|
-
AnnotateLastMessageSectionComment,
|
|
4
3
|
AnnotateLastMessageSubmitPayload,
|
|
5
4
|
LastAssistantMessageData,
|
|
6
5
|
LastAssistantMessageLine,
|
|
@@ -34,10 +33,7 @@ function formatSectionLineRange(section: LastAssistantMessageSection): string {
|
|
|
34
33
|
: `lines ${section.startLine}-${section.endLine}`;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
function formatSectionComment(
|
|
38
|
-
comment: AnnotateLastMessageSectionComment,
|
|
39
|
-
section: LastAssistantMessageSection | undefined,
|
|
40
|
-
): string {
|
|
36
|
+
function formatSectionComment(section: LastAssistantMessageSection | undefined): string {
|
|
41
37
|
if (section == null) {
|
|
42
38
|
return "Unknown section";
|
|
43
39
|
}
|
|
@@ -90,9 +86,7 @@ export function composeAnnotateLastMessagePrompt(
|
|
|
90
86
|
if (sectionComments.length > 0) {
|
|
91
87
|
lines.push("## Section comments");
|
|
92
88
|
sectionComments.forEach((comment, index) => {
|
|
93
|
-
lines.push(
|
|
94
|
-
`${index + 1}. ${formatSectionComment(comment, sectionMap.get(comment.sectionId))}`,
|
|
95
|
-
);
|
|
89
|
+
lines.push(`${index + 1}. ${formatSectionComment(sectionMap.get(comment.sectionId))}`);
|
|
96
90
|
lines.push(` ${comment.body.trim()}`);
|
|
97
91
|
lines.push("");
|
|
98
92
|
});
|
|
@@ -314,9 +314,6 @@
|
|
|
314
314
|
margin: 0;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
.markdown-content p { margin: 0; }
|
|
318
|
-
.markdown-content p + p { margin-top: 4px; }
|
|
319
|
-
|
|
320
317
|
.markdown-content a {
|
|
321
318
|
color: var(--mdLink);
|
|
322
319
|
text-decoration: underline;
|
|
@@ -337,21 +334,6 @@
|
|
|
337
334
|
font-family: inherit;
|
|
338
335
|
}
|
|
339
336
|
|
|
340
|
-
.markdown-content pre {
|
|
341
|
-
background: transparent;
|
|
342
|
-
margin: 4px 0;
|
|
343
|
-
overflow-x: auto;
|
|
344
|
-
border-left: 3px solid var(--mdCodeBlockBorder);
|
|
345
|
-
padding-left: 8px;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
.markdown-content pre code {
|
|
349
|
-
display: block;
|
|
350
|
-
background: none;
|
|
351
|
-
color: var(--mdCodeBlock, var(--text));
|
|
352
|
-
padding: 0;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
337
|
.markdown-content blockquote {
|
|
356
338
|
border-left: 3px solid var(--mdQuoteBorder);
|
|
357
339
|
padding-left: 8px;
|
|
@@ -360,15 +342,6 @@
|
|
|
360
342
|
font-style: italic;
|
|
361
343
|
}
|
|
362
344
|
|
|
363
|
-
.markdown-content ul,
|
|
364
|
-
.markdown-content ol {
|
|
365
|
-
margin: 4px 0;
|
|
366
|
-
padding-left: 1.5em;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
.markdown-content li { margin: 0; }
|
|
370
|
-
.markdown-content li::marker { color: var(--mdListBullet); }
|
|
371
|
-
|
|
372
345
|
.markdown-content hr {
|
|
373
346
|
border: none;
|
|
374
347
|
border-top: 1px solid var(--mdHr);
|
|
@@ -155,13 +155,11 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
155
155
|
"gpt-6-luna ",
|
|
156
156
|
"gpt-6-luna",
|
|
157
157
|
"gpt-5.6-luna",
|
|
158
|
-
"gpt-5.5",
|
|
159
158
|
"claude-opus-5.5 ",
|
|
160
159
|
"claude-opus-5.5",
|
|
161
160
|
"claude-opus-5",
|
|
162
161
|
"gpt-5.4",
|
|
163
162
|
"gpt-5.3-codex",
|
|
164
|
-
"gpt-5-mini",
|
|
165
163
|
"grok-4.7 ",
|
|
166
164
|
"grok-4.7",
|
|
167
165
|
"grok-4.6",
|
|
@@ -187,7 +185,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
187
185
|
"gpt-5.4",
|
|
188
186
|
"gpt-5.3-codex",
|
|
189
187
|
],
|
|
190
|
-
"opencode-go": ["grok-4.7 ", "grok-4.7"
|
|
188
|
+
"opencode-go": ["grok-4.7 ", "grok-4.7"],
|
|
191
189
|
openrouter: [
|
|
192
190
|
"openai/gpt-6-astra ",
|
|
193
191
|
"openai/gpt-6-astra",
|
|
@@ -258,8 +256,6 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
258
256
|
"gpt-6-luna ",
|
|
259
257
|
"gpt-6-luna",
|
|
260
258
|
"gpt-5.6-luna",
|
|
261
|
-
"gpt-5.5",
|
|
262
|
-
"gpt-5.3-codex-spark",
|
|
263
259
|
],
|
|
264
260
|
xai: ["grok-4.7 ", "grok-4.7", "grok-4.6", "grok-4.5"],
|
|
265
261
|
"vercel-ai-gateway": [
|
|
@@ -503,12 +499,8 @@ const CodeReviewerParams = Type.Object({
|
|
|
503
499
|
});
|
|
504
500
|
|
|
505
501
|
type ToolCall = {
|
|
506
|
-
id: string;
|
|
507
502
|
name: string;
|
|
508
503
|
args: unknown;
|
|
509
|
-
startedAt: number;
|
|
510
|
-
endedAt?: number;
|
|
511
|
-
isError?: boolean;
|
|
512
504
|
};
|
|
513
505
|
|
|
514
506
|
type ReviewStatus = "running" | "done" | "error" | "aborted";
|
|
@@ -877,12 +869,6 @@ function addAssistantMessageUsage(total: Usage, message: unknown): void {
|
|
|
877
869
|
addUsage(total, assistant.usage as Usage);
|
|
878
870
|
}
|
|
879
871
|
|
|
880
|
-
function aggregateAssistantUsage(messages: unknown[]): Usage {
|
|
881
|
-
const total = createEmptyUsage();
|
|
882
|
-
for (const message of messages) addAssistantMessageUsage(total, message);
|
|
883
|
-
return total;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
872
|
function addSessionEventUsage(total: Usage, event: unknown): void {
|
|
887
873
|
const sessionEvent = event as { type?: string; message?: unknown; result?: { usage?: Usage } };
|
|
888
874
|
if (sessionEvent?.type === "message_end")
|
|
@@ -1033,12 +1019,6 @@ function getReadPathCandidates(resolved: string): string[] {
|
|
|
1033
1019
|
];
|
|
1034
1020
|
}
|
|
1035
1021
|
|
|
1036
|
-
function resolveToolPath(cwd: string, rawPath: string | undefined): string {
|
|
1037
|
-
const input = rawPath?.trim() || ".";
|
|
1038
|
-
const normalized = input.startsWith("@") ? input.slice(1) : input;
|
|
1039
|
-
return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(cwd, normalized);
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
1022
|
async function assertToolPathInsideCwd(
|
|
1043
1023
|
cwd: string,
|
|
1044
1024
|
rawPath: unknown,
|
|
@@ -1046,24 +1026,11 @@ async function assertToolPathInsideCwd(
|
|
|
1046
1026
|
): Promise<string | undefined> {
|
|
1047
1027
|
if (rawPath !== undefined && typeof rawPath !== "string")
|
|
1048
1028
|
return `${toolName} path must be a string.`;
|
|
1049
|
-
if (typeof rawPath === "string") {
|
|
1050
|
-
const normalizedInput = rawPath.trim().startsWith("@")
|
|
1051
|
-
? rawPath.trim().slice(1)
|
|
1052
|
-
: rawPath.trim();
|
|
1053
|
-
if (/(^|[/\\])\.\.(?:[/\\]|$)/.test(normalizedInput)) {
|
|
1054
|
-
return `${toolName} path must not traverse outside the local checkout.`;
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
1029
|
// A path is allowed if it resolves inside EITHER the realpath'd root (root) OR
|
|
1058
1030
|
// the lexical cwd (lexicalCwd). This handles missing files in symlinked workspaces
|
|
1059
1031
|
// where realpath fails and the lexical fallback starts with the alias prefix.
|
|
1060
1032
|
const root = await fs.realpath(cwd).catch(() => path.resolve(cwd));
|
|
1061
1033
|
const lexicalCwd = path.resolve(cwd);
|
|
1062
|
-
const resolved = resolveToolPath(cwd, rawPath);
|
|
1063
|
-
const realPath = await fs.realpath(resolved).catch(() => resolved);
|
|
1064
|
-
if (!isInside(root, realPath) && !isInside(lexicalCwd, realPath))
|
|
1065
|
-
return `${toolName} is limited to the local checkout: ${realPath}`;
|
|
1066
|
-
// Pi-effective path check (monotonic: block if either resolution is outside).
|
|
1067
1034
|
// For the read tool, also check Pi's resolveReadPathAsync fallback variants so
|
|
1068
1035
|
// a missing file whose variant resolves outside is not silently allowed.
|
|
1069
1036
|
try {
|
|
@@ -1780,7 +1747,7 @@ export const __test__ = {
|
|
|
1780
1747
|
normalizeThinkingLevel,
|
|
1781
1748
|
resolveThinkingLevel,
|
|
1782
1749
|
selectCodeReviewerModel,
|
|
1783
|
-
|
|
1750
|
+
createEmptyUsage,
|
|
1784
1751
|
addAssistantMessageUsage,
|
|
1785
1752
|
addSessionEventUsage,
|
|
1786
1753
|
inspectFinalAssistant,
|
|
@@ -1929,25 +1896,14 @@ export default function codeReviewerExtension(pi: ExtensionAPI) {
|
|
|
1929
1896
|
break;
|
|
1930
1897
|
case "tool_execution_start":
|
|
1931
1898
|
details.toolCalls.push({
|
|
1932
|
-
id: event.toolCallId,
|
|
1933
1899
|
name: event.toolName,
|
|
1934
1900
|
args: event.args,
|
|
1935
|
-
startedAt: Date.now(),
|
|
1936
1901
|
});
|
|
1937
1902
|
if (details.toolCalls.length > MAX_TOOL_CALLS_TO_KEEP) {
|
|
1938
1903
|
details.toolCalls.splice(0, details.toolCalls.length - MAX_TOOL_CALLS_TO_KEEP);
|
|
1939
1904
|
}
|
|
1940
1905
|
emit();
|
|
1941
1906
|
break;
|
|
1942
|
-
case "tool_execution_end": {
|
|
1943
|
-
const call = details.toolCalls.find((item) => item.id === event.toolCallId);
|
|
1944
|
-
if (call) {
|
|
1945
|
-
call.endedAt = Date.now();
|
|
1946
|
-
call.isError = event.isError;
|
|
1947
|
-
}
|
|
1948
|
-
emit();
|
|
1949
|
-
break;
|
|
1950
|
-
}
|
|
1951
1907
|
case "message_update":
|
|
1952
1908
|
if (event.assistantMessageEvent?.type === "text_delta") {
|
|
1953
1909
|
lastContent += event.assistantMessageEvent.delta ?? "";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# context-cap
|
|
2
2
|
|
|
3
|
-
A pi extension that treats large-context models as having an effective
|
|
3
|
+
A pi extension that treats large-context models as having an effective 350k-token context window, so pi's built-in auto-compaction starts earlier, and avoids the dumb zone.
|
|
4
4
|
|
|
5
5
|
By default, pi auto-compacts when:
|
|
6
6
|
|
|
@@ -11,10 +11,10 @@ contextTokens > model.contextWindow - reserveTokens
|
|
|
11
11
|
This extension changes the active model's in-memory `contextWindow` to:
|
|
12
12
|
|
|
13
13
|
```text
|
|
14
|
-
min(originalContextWindow,
|
|
14
|
+
min(originalContextWindow, 350000)
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
With pi's default `reserveTokens` of 16,384, models larger than
|
|
17
|
+
With pi's default `reserveTokens` of 16,384, models larger than 350k will proactively compact around 333,616 tokens.
|
|
18
18
|
|
|
19
19
|
## Commands
|
|
20
20
|
|
|
@@ -65,21 +65,21 @@ Then reload pi:
|
|
|
65
65
|
|
|
66
66
|
### Settings guard and alternative
|
|
67
67
|
|
|
68
|
-
- On Pi hosts exposing `getSettings()`, the extension skips the cap whenever `compaction.modelOverrides` contains the active model's key (`provider/model-id`), whatever fields that entry sets; it does not read `reserveTokens` from the override. Otherwise it uses `compaction.reserveTokens`, then the default `16,384`, and skips the cap when that value is at least `
|
|
69
|
-
- Pi's provider request path also derives an output-token budget from the effective window (`contextWindow - contextTokens - 4096`, with any provider-specific clamp applied afterward). The cap can therefore reduce response or thinking-token budgets near the limit, in addition to causing earlier compaction; Pi's overflow handling may also treat a request above
|
|
70
|
-
- As a session-scoped alternative, configure Pi's own `compaction.modelOverrides` instead of this extension. The exact key is `provider/model-id`; to match this extension's default
|
|
68
|
+
- On Pi hosts exposing `getSettings()`, the extension skips the cap whenever `compaction.modelOverrides` contains the active model's key (`provider/model-id`), whatever fields that entry sets; it does not read `reserveTokens` from the override. Otherwise it uses `compaction.reserveTokens`, then the default `16,384`, and skips the cap when that value is at least `350,000` (which would make the threshold zero or negative). Hosts without `getSettings()` use the older cap behavior without this guard.
|
|
69
|
+
- Pi's provider request path also derives an output-token budget from the effective window (`contextWindow - contextTokens - 4096`, with any provider-specific clamp applied afterward). The cap can therefore reduce response or thinking-token budgets near the limit, in addition to causing earlier compaction; Pi's overflow handling may also treat a request above 350k as overflow and retry after compaction. Use `/context-cap off` when you need the full model window temporarily.
|
|
70
|
+
- As a session-scoped alternative, configure Pi's own `compaction.modelOverrides` instead of this extension. The exact key is `provider/model-id`; to match this extension's default 350k threshold for a model with original window `W`, let `R` be the configured `reserveTokens` value (default `16,384`) and use:
|
|
71
71
|
|
|
72
72
|
```text
|
|
73
|
-
reserveTokens = W -
|
|
73
|
+
reserveTokens = W - 350000 + R
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
For example, with the default `R`, a 1,000,000-token model uses `
|
|
76
|
+
For example, with the default `R`, a 1,000,000-token model uses `666384`, yielding the same `333616` threshold as `350000 - 16384`:
|
|
77
77
|
|
|
78
78
|
```json
|
|
79
79
|
{
|
|
80
80
|
"compaction": {
|
|
81
81
|
"modelOverrides": {
|
|
82
|
-
"provider/model-id": { "reserveTokens":
|
|
82
|
+
"provider/model-id": { "reserveTokens": 666384 }
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Model, Api } from "@earendil-works/pi-ai";
|
|
3
3
|
|
|
4
|
-
const DEFAULT_MAX_CONTEXT_WINDOW =
|
|
4
|
+
const DEFAULT_MAX_CONTEXT_WINDOW = 350_000;
|
|
5
5
|
const DEFAULT_RESERVE_TOKENS = 16_384;
|
|
6
6
|
const PROCESS_STATE_KEY = Symbol.for("pi-extensions.context-cap.model-holds.v1");
|
|
7
7
|
type AnyModel = Model<Api> | Model<any>;
|
|
@@ -224,7 +224,7 @@ function unavailableReason(result: ApplyResult | undefined): string | undefined
|
|
|
224
224
|
return "physical model has no compatible context-window limit";
|
|
225
225
|
}
|
|
226
226
|
if (result?.reason === "reserve") {
|
|
227
|
-
return `compaction reserveTokens (${formatTokens(result.reserveTokens ?? DEFAULT_MAX_CONTEXT_WINDOW)}) is at least
|
|
227
|
+
return `compaction reserveTokens (${formatTokens(result.reserveTokens ?? DEFAULT_MAX_CONTEXT_WINDOW)}) is at least ${formatTokens(DEFAULT_MAX_CONTEXT_WINDOW)}; the auto-compaction threshold would be zero or negative`;
|
|
228
228
|
}
|
|
229
229
|
if (result?.reason === "modelOverride") {
|
|
230
230
|
return `compaction.modelOverrides[\"${result.key}\"] is configured; leaving the model window unchanged`;
|
|
@@ -317,7 +317,7 @@ export default function contextCapExtension(pi: ExtensionAPI) {
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
pi.registerCommand("context-cap", {
|
|
320
|
-
description:
|
|
320
|
+
description: `Toggle the ${formatTokens(DEFAULT_MAX_CONTEXT_WINDOW)} effective context-window cap for auto-compaction`,
|
|
321
321
|
getArgumentCompletions: (prefix) => {
|
|
322
322
|
const commands = ["on", "off", "toggle", "status"];
|
|
323
323
|
const matches = commands.filter((command) => command.startsWith(prefix.trim()));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-context-cap",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A pi extension that caps effective model context windows at
|
|
3
|
+
"version": "0.1.16",
|
|
4
|
+
"description": "A pi extension that caps effective model context windows at 350k tokens for earlier auto-compaction.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"pi",
|