pi-squeeze 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +115 -2
- package/extensions/command/parse.ts +82 -0
- package/extensions/core/adb.ts +231 -0
- package/extensions/core/audit.ts +53 -0
- package/extensions/core/blame.ts +32 -0
- package/extensions/core/command.ts +86 -0
- package/extensions/core/compact.ts +332 -0
- package/extensions/core/config.ts +109 -0
- package/extensions/core/diff.ts +93 -0
- package/extensions/core/docker.ts +58 -0
- package/extensions/core/dust.ts +24 -0
- package/extensions/core/elide.ts +31 -0
- package/extensions/core/files.ts +29 -0
- package/extensions/core/generic.ts +40 -0
- package/extensions/core/gitlog.ts +75 -0
- package/extensions/core/gitstatus.ts +52 -0
- package/extensions/core/gradle.ts +269 -0
- package/extensions/core/gradlereports.ts +78 -0
- package/extensions/core/install.ts +122 -0
- package/extensions/core/javac.ts +44 -0
- package/extensions/core/jest.ts +38 -0
- package/extensions/core/jvm.ts +18 -0
- package/extensions/core/lint.ts +199 -0
- package/extensions/core/logs.ts +106 -0
- package/extensions/core/lsd.ts +64 -0
- package/extensions/core/maven.ts +122 -0
- package/extensions/core/paths.ts +50 -0
- package/extensions/core/pytest.ts +37 -0
- package/extensions/core/rg.ts +126 -0
- package/extensions/core/rtk.ts +50 -0
- package/extensions/core/runners.ts +50 -0
- package/extensions/core/rustc.ts +99 -0
- package/extensions/core/save.ts +11 -0
- package/extensions/core/stats.ts +31 -0
- package/extensions/core/tests.ts +46 -0
- package/extensions/core/tree.ts +51 -0
- package/extensions/core/tsc.ts +49 -0
- package/extensions/core/xcode.ts +123 -0
- package/extensions/squeeze.ts +122 -0
- package/package.json +32 -4
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `xcodebuild` and `swift build` / `swift test`:
|
|
3
|
+
*
|
|
4
|
+
* * build steps (`CompileSwift …`, `Ld …`, `CodeSign …` with their indented command lines)
|
|
5
|
+
* are counted, not listed; so are progress lines (`[12/40] Compiling …`);
|
|
6
|
+
* * warnings are grouped by message, one location each, without their source frame;
|
|
7
|
+
* * errors keep their frame and their notes;
|
|
8
|
+
* * passing tests (XCTest and Swift Testing) are counted; failures, issues and totals stay;
|
|
9
|
+
* * the `** BUILD SUCCEEDED **` / `** TEST FAILED **` result stays.
|
|
10
|
+
*/
|
|
11
|
+
import { capLines, cutLine, DEFAULT_LIMITS, type Limits, type Squeezed } from "./elide.ts";
|
|
12
|
+
import { stripAnsi } from "./generic.ts";
|
|
13
|
+
import { renderDiags, type Diag } from "./lint.ts";
|
|
14
|
+
|
|
15
|
+
const DIAG = /^(.+?):(\d+)(?::(\d+))?: (error|warning|note): (.*)$/;
|
|
16
|
+
/** `CompileSwift normal arm64 …`, `WriteAuxiliaryFile …`: a CamelCase verb, or a one-word one. */
|
|
17
|
+
const ACTION = /^(?:[A-Z][a-z]+[A-Z]\w*|Ld|Copy|Touch|Libtool|Strip|Lipo|Ditto|Validate)(?=[ \\]|$)/;
|
|
18
|
+
const DROP = [
|
|
19
|
+
// `2026-11-02 01:38:45.358 xcodebuild[40118:44955631] …`: tool chatter.
|
|
20
|
+
/^\d{4}-\d\d-\d\d [\d:.]+ [\w.-]+\[\d+:\d+\] /,
|
|
21
|
+
/^(Details|Object|Method|Thread): {1,3}\S/,
|
|
22
|
+
/^Please file a bug at https:\/\/feedbackassistant/,
|
|
23
|
+
/^\{ platform:/,
|
|
24
|
+
/^(Command line invocation|Resolved source packages):$/,
|
|
25
|
+
/^(Resolve Package Graph|Prepare packages|Testing started)$/,
|
|
26
|
+
/^Build description (signature|path): /,
|
|
27
|
+
/^Building for (debugging|production)\.\.\.$/,
|
|
28
|
+
/^\[Planning deferred tasks\]$/,
|
|
29
|
+
// SwiftPM progress; the spaces are narrow no-break ones.
|
|
30
|
+
/^\[\d+\s?\/\s?\d+\]/,
|
|
31
|
+
/^(Fetching|Fetched|Computing version for|Computed|Creating working copy for|Working copy of|Updating|Updated) /,
|
|
32
|
+
/^note: (Building targets|Target dependency graph|Run script build phase|Using codesigning|Removed stale)/,
|
|
33
|
+
/^Test Case '.*' started\.$/,
|
|
34
|
+
/^Test Suite '.*' (started|passed|failed) at /,
|
|
35
|
+
/^◇ /,
|
|
36
|
+
/^↳ (Testing Library Version|Target Platform): /,
|
|
37
|
+
/^\[#[\w-]+\]: <https?:/,
|
|
38
|
+
];
|
|
39
|
+
/** A source frame line under a diagnostic. */
|
|
40
|
+
const FRAME = /^\s*(\d+ )?\|/;
|
|
41
|
+
|
|
42
|
+
export function compactXcode(output: string, limits: Limits = DEFAULT_LIMITS): Squeezed | undefined {
|
|
43
|
+
const lines = stripAnsi(output).replace(/\u200b|XCTestOutputBarrier/g, "").split("\n");
|
|
44
|
+
while (lines.length && lines.at(-1) === "") lines.pop();
|
|
45
|
+
if (!lines.some((l) => /^\*\* \w+ (SUCCEEDED|FAILED) \*\*$|^Build complete!|^error: (Build failed|fatalError)|^Test Suite '|^✘ Test run|^✔ Test run/.test(l))) return undefined;
|
|
46
|
+
|
|
47
|
+
const actions = new Map<string, number>();
|
|
48
|
+
const warnings: Diag[] = [];
|
|
49
|
+
const seen = new Set<string>();
|
|
50
|
+
const out: string[] = [];
|
|
51
|
+
let executed: string | undefined;
|
|
52
|
+
let passed = 0;
|
|
53
|
+
let indented = false;
|
|
54
|
+
let last: "error" | "warning" | "other" = "other";
|
|
55
|
+
let skipNext = false;
|
|
56
|
+
for (const l of lines) {
|
|
57
|
+
if (skipNext) {
|
|
58
|
+
skipNext = false;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// Each suite repeats the totals; the last one (all tests) is the one that counts.
|
|
62
|
+
if (/^\s+Executed \d+ tests?/.test(l)) {
|
|
63
|
+
executed = l.trim();
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (indented && /^\s+\S/.test(l)) continue;
|
|
67
|
+
indented = false;
|
|
68
|
+
const action = ACTION.exec(l);
|
|
69
|
+
if (action) {
|
|
70
|
+
actions.set(action[0], (actions.get(action[0]) ?? 0) + 1);
|
|
71
|
+
indented = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (/^(Command line invocation|Resolved source packages):$|^error: .* Command line:/.test(l)) indented = true;
|
|
75
|
+
if (DROP.some((d) => d.test(l))) {
|
|
76
|
+
indented = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (/^Test Case '.*' passed \(/.test(l) || /^✔ Test /.test(l)) {
|
|
80
|
+
passed++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const d = DIAG.exec(l);
|
|
84
|
+
if (d) {
|
|
85
|
+
const severity = d[4]!;
|
|
86
|
+
if (severity === "warning") {
|
|
87
|
+
const key = `${d[1]}:${d[2]}:${d[3]}:${d[5]}`;
|
|
88
|
+
if (!seen.has(key)) warnings.push({ file: d[1]!, line: d[2]!, col: d[3], severity, message: d[5]! });
|
|
89
|
+
seen.add(key);
|
|
90
|
+
last = "warning";
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (severity === "note" && last !== "error") continue;
|
|
94
|
+
if (severity === "error") last = "error";
|
|
95
|
+
out.push(cutLine(l, limits.lineChars));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (FRAME.test(l) || /^\s+`- /.test(l)) {
|
|
99
|
+
if (last === "error") out.push(cutLine(l, limits.lineChars));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
last = "other";
|
|
103
|
+
if (l === "Failed frontend command:") skipNext = true;
|
|
104
|
+
if (l.trim() === "" && (out.length === 0 || out.at(-1) === "")) continue;
|
|
105
|
+
out.push(cutLine(l.trim() === "" ? "" : l, limits.lineChars));
|
|
106
|
+
}
|
|
107
|
+
while (out.at(-1) === "") out.pop();
|
|
108
|
+
if (executed) out.push(executed);
|
|
109
|
+
|
|
110
|
+
const steps = [...actions].sort((a, b) => b[1] - a[1]);
|
|
111
|
+
const total = steps.reduce((n, [, c]) => n + c, 0);
|
|
112
|
+
const head: string[] = [];
|
|
113
|
+
if (total) head.push(`build: ${total} steps (${steps.slice(0, 4).map(([k, v]) => `${v} ${k}`).join(", ")}${steps.length > 4 ? ", …" : ""})`);
|
|
114
|
+
if (passed) head.push(`tests: ${passed} passed (not listed)`);
|
|
115
|
+
const warned = warnings.length ? ["", ...renderDiags({ tool: "swift", diags: warnings, notes: [] }, limits).text.split("\n")] : [];
|
|
116
|
+
const capped = capLines([...head, ...out, ...warned], limits.total);
|
|
117
|
+
return { text: capped.text, truncated: capped.truncated || warnings.length > 0 };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** For sniffing: only with xcodebuild's own `** BUILD SUCCEEDED **`-style result line. */
|
|
121
|
+
export function compactXcodeSniffed(output: string, limits: Limits = DEFAULT_LIMITS): Squeezed | undefined {
|
|
122
|
+
return /^\*\* [A-Z ]+ (SUCCEEDED|FAILED) \*\*$/m.test(output) ? compactXcode(output, limits) : undefined;
|
|
123
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-squeeze: Squeezes tool output so the agent's context stays small.
|
|
3
|
+
*
|
|
4
|
+
* Pure logic lives in core/ and command/ (tested directly); this file only wires it to pi.
|
|
5
|
+
* Output is only ever replaced when the result is clearly smaller, and a truncated result
|
|
6
|
+
* says where the full output was saved. When pi itself cut a long output to its tail, the
|
|
7
|
+
* full output pi saved is compacted instead (a build's tail alone often misses its errors). Settings persist in <agent dir>/squeeze.json.
|
|
8
|
+
*/
|
|
9
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { completions, parseCommand } from "./command/parse.ts";
|
|
11
|
+
import { compact, KINDS } from "./core/compact.ts";
|
|
12
|
+
import { type Config, DEFAULT_CONFIG, optionsOf, readConfig, saveConfig } from "./core/config.ts";
|
|
13
|
+
import { rewrite } from "./core/rtk.ts";
|
|
14
|
+
import { fileIn, readFullOutput } from "./core/files.ts";
|
|
15
|
+
import { saveFull } from "./core/save.ts";
|
|
16
|
+
import { emptyStats, record, saved, summary, tokens } from "./core/stats.ts";
|
|
17
|
+
|
|
18
|
+
type StatusUi = { ui: { setStatus(key: string, text: string): void } };
|
|
19
|
+
|
|
20
|
+
export const settingsText = (c: Config): string =>
|
|
21
|
+
[c.enabled ? "on" : "off", `rtk ${c.rtk ? "on" : "off"}`, ...KINDS.map((k) => `${k} ${c.kinds[k] ? "on" : "off"}`), `min ${c.minChars} chars`].join(" · ");
|
|
22
|
+
|
|
23
|
+
export default function (pi: ExtensionAPI) {
|
|
24
|
+
let config: Config = { ...DEFAULT_CONFIG, kinds: { ...DEFAULT_CONFIG.kinds } };
|
|
25
|
+
const stats = emptyStats();
|
|
26
|
+
if (process.env.PI_SQUEEZE === "off") config.enabled = false;
|
|
27
|
+
|
|
28
|
+
const showStatus = (ctx: StatusUi) => {
|
|
29
|
+
const n = tokens(saved(stats));
|
|
30
|
+
ctx.ui.setStatus("squeeze", config.enabled && config.showStatus && n > 0 ? `squeeze: ~${n >= 1000 ? `${(n / 1000).toFixed(1)}k` : n} tokens saved` : "");
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
34
|
+
const { config: loaded, problem } = await readConfig();
|
|
35
|
+
config = loaded;
|
|
36
|
+
if (process.env.PI_SQUEEZE === "off") config.enabled = false;
|
|
37
|
+
if (problem) ctx.ui.notify(`squeeze: ${problem}; using the defaults`, "warning");
|
|
38
|
+
showStatus(ctx);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
pi.on("tool_call", async (event) => {
|
|
42
|
+
if (!config.enabled || !config.rtk || event.toolName !== "bash") return;
|
|
43
|
+
const input = event.input as { command?: unknown };
|
|
44
|
+
if (typeof input.command !== "string") return;
|
|
45
|
+
const rewritten = rewrite(input.command);
|
|
46
|
+
if (rewritten) input.command = rewritten;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
50
|
+
if (!config.enabled || event.toolName !== "bash") return;
|
|
51
|
+
const command = (event.input as { command?: unknown }).command;
|
|
52
|
+
if (typeof command !== "string") return;
|
|
53
|
+
const blocks = event.content;
|
|
54
|
+
if (blocks.length !== 1 || blocks[0]!.type !== "text") return;
|
|
55
|
+
const shown = blocks[0]!.text;
|
|
56
|
+
// pi cut this one to its tail and saved the whole of it: compact the whole, not the tail.
|
|
57
|
+
const details = event.details as { truncation?: unknown; fullOutputPath?: unknown } | undefined;
|
|
58
|
+
const fullPath = details?.truncation ? details.fullOutputPath : undefined;
|
|
59
|
+
let original = shown;
|
|
60
|
+
if (details?.truncation) {
|
|
61
|
+
const full = readFullOutput(fullPath);
|
|
62
|
+
if (full === undefined) return;
|
|
63
|
+
// The exit status pi appends to a failed run's text.
|
|
64
|
+
original = full + (/\n\nCommand exited with code \d+$/.exec(shown)?.[0] ?? "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const r = compact(command, original, { ...optionsOf(config), failed: event.isError, isFile: fileIn(ctx.cwd) });
|
|
68
|
+
// Never more than pi would have shown.
|
|
69
|
+
if (!r || r.text.length >= shown.length) return;
|
|
70
|
+
record(stats, { ...r, before: shown.length });
|
|
71
|
+
showStatus(ctx);
|
|
72
|
+
const where = fullPath ?? (r.truncated ? saveFull(original) : undefined);
|
|
73
|
+
const text = where ? `${r.text}\n[squeeze: output shortened; full output: ${where}]` : r.text;
|
|
74
|
+
return { content: [{ type: "text", text }] };
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
pi.registerCommand("squeeze", {
|
|
78
|
+
description: "pi-squeeze: status, on, off, <compactor>|rtk on|off, min <chars>",
|
|
79
|
+
getArgumentCompletions: (prefix: string) => completions(prefix),
|
|
80
|
+
handler: async (args, ctx) => {
|
|
81
|
+
const cmd = parseCommand(args);
|
|
82
|
+
// The setting applies to this session either way; a failed save only means it is not remembered.
|
|
83
|
+
const persist = async () => {
|
|
84
|
+
try {
|
|
85
|
+
await saveConfig(config);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
ctx.ui.notify(`squeeze: could not save the setting (${(error as Error)?.message ?? String(error)}); it applies to this session only`, "warning");
|
|
88
|
+
}
|
|
89
|
+
showStatus(ctx);
|
|
90
|
+
};
|
|
91
|
+
switch (cmd.kind) {
|
|
92
|
+
case "status":
|
|
93
|
+
ctx.ui.notify(`squeeze ${settingsText(config)}\n${summary(stats)}`, "info");
|
|
94
|
+
return;
|
|
95
|
+
case "on":
|
|
96
|
+
case "off":
|
|
97
|
+
config.enabled = cmd.kind === "on";
|
|
98
|
+
await persist();
|
|
99
|
+
ctx.ui.notify(`squeeze ${cmd.kind}`, "info");
|
|
100
|
+
return;
|
|
101
|
+
case "rtk":
|
|
102
|
+
config.rtk = cmd.on;
|
|
103
|
+
await persist();
|
|
104
|
+
ctx.ui.notify(`squeeze: rtk ${cmd.on ? "on" : "off"}`, "info");
|
|
105
|
+
return;
|
|
106
|
+
case "compactor":
|
|
107
|
+
config.kinds[cmd.compactor] = cmd.on;
|
|
108
|
+
await persist();
|
|
109
|
+
ctx.ui.notify(`squeeze: ${cmd.compactor} ${cmd.on ? "on" : "off"}`, "info");
|
|
110
|
+
return;
|
|
111
|
+
case "min":
|
|
112
|
+
config.minChars = cmd.chars;
|
|
113
|
+
await persist();
|
|
114
|
+
ctx.ui.notify(`squeeze: output under ${cmd.chars} characters is left alone`, "info");
|
|
115
|
+
return;
|
|
116
|
+
case "invalid":
|
|
117
|
+
ctx.ui.notify(cmd.message, "warning");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-squeeze",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Shrinks bash output before it reaches the agent's context: native compactors for git, search, listings, tests, builds (cargo, gradle, Maven, javac, Xcode, tsc, docker), Android (adb, lint, detekt), linters, package installs and logs, plus rtk when installed. Full output stays one temp file away.",
|
|
5
|
+
"keywords": ["pi-package", "pi-extension", "pi", "tokens", "context", "compaction", "rtk"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Sorin Albu-Irimies",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/sorinirimies/pi-squeeze.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/sorinirimies/pi-squeeze#readme",
|
|
13
|
+
"files": ["extensions", "README.md", "LICENSE"],
|
|
14
|
+
"pi": {
|
|
15
|
+
"extensions": ["./extensions/squeeze.ts"]
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "bun test",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"check": "nu scripts/ci/quality_gate.nu"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
24
|
+
},
|
|
25
|
+
"overrides": {
|
|
26
|
+
"undici": "8.12.3",
|
|
27
|
+
"minimatch": "11.0.3"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@earendil-works/pi-coding-agent": "^1.4.0",
|
|
31
|
+
"@types/bun": "^1.4.4",
|
|
32
|
+
"typescript": "^7.0.2"
|
|
33
|
+
}
|
|
34
|
+
}
|