shrinker-ai 0.9.0 → 0.11.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.
|
@@ -127,13 +127,14 @@ async function spawnAndCapture(executable, args, viaCmdProxy) {
|
|
|
127
127
|
return await new Promise((resolve, reject) => {
|
|
128
128
|
const spawnCommand = viaCmdProxy ? "cmd.exe" : executable;
|
|
129
129
|
const spawnArgs = viaCmdProxy
|
|
130
|
-
? ["/d", "/s", "/c",
|
|
130
|
+
? ["/d", "/s", "/c", `"${quoteForCmd(executable)} ${args.map(quoteForCmd).join(" ")}"`]
|
|
131
131
|
: args;
|
|
132
132
|
const child = spawn(spawnCommand, spawnArgs, {
|
|
133
133
|
cwd: process.cwd(),
|
|
134
134
|
env: process.env,
|
|
135
135
|
shell: false,
|
|
136
136
|
windowsHide: true,
|
|
137
|
+
windowsVerbatimArguments: viaCmdProxy,
|
|
137
138
|
});
|
|
138
139
|
const stdout = [];
|
|
139
140
|
const stderr = [];
|
|
@@ -160,11 +160,15 @@ export function filterGitLog(input, options) {
|
|
|
160
160
|
flags.some((flag) => !isSafeCompactFlag(flag)) ||
|
|
161
161
|
lines.some((line) => line.startsWith("diff --git ")) ||
|
|
162
162
|
hasStructuredLogDetails(lines)) {
|
|
163
|
+
const limited = limitLines(lines, options.maxLines);
|
|
163
164
|
return {
|
|
164
|
-
output:
|
|
165
|
+
output: limited.lines.join("\n"),
|
|
165
166
|
kind: "git-log",
|
|
166
|
-
omitted:
|
|
167
|
-
|
|
167
|
+
omitted: limited.omitted > 0,
|
|
168
|
+
...(limited.omitted > 0 ? { recovery: "always" } : {}),
|
|
169
|
+
notes: limited.omitted > 0
|
|
170
|
+
? ["explicit Git output format preserved", `omitted ${limited.omitted} output lines`]
|
|
171
|
+
: ["explicit Git output format preserved"],
|
|
168
172
|
};
|
|
169
173
|
}
|
|
170
174
|
if (lines.length > 0 && lines.every((line) => !line.trim() || ONELINE_PATTERN.test(line))) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { createServer } from "node:http";
|
|
3
|
+
import { createServer, request as httpRequest } from "node:http";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { getInputCostPerMillionTokens } from "./stats-store.js";
|
|
@@ -8,11 +8,19 @@ import { DASHBOARD_STATS_PLACEHOLDER, DASHBOARD_TEMPLATE_HTML } from "./dashboar
|
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
9
9
|
// Identifies a running server as ours without depending on user-visible copy.
|
|
10
10
|
const DASHBOARD_MARKER = 'name="generator" content="shrinker-dashboard"';
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
function isSnapshotPath(value) {
|
|
12
|
+
return Boolean(value && /(?:^|[\\/])(?:snapshot|snapshot:)(?:[\\/]|$)/i.test(value));
|
|
13
|
+
}
|
|
14
|
+
export function getCliRelaunchCommand(args, runtime = {
|
|
15
|
+
execPath: process.execPath,
|
|
16
|
+
argv: process.argv,
|
|
17
|
+
packaged: Boolean(process.pkg),
|
|
18
|
+
}) {
|
|
19
|
+
const scriptPath = runtime.argv[1];
|
|
20
|
+
if (runtime.packaged || !scriptPath || isSnapshotPath(scriptPath)) {
|
|
21
|
+
return { command: runtime.execPath, args };
|
|
14
22
|
}
|
|
15
|
-
return { command:
|
|
23
|
+
return { command: runtime.execPath, args: [scriptPath, ...args] };
|
|
16
24
|
}
|
|
17
25
|
// Neutralizes `</script>` inside string fields so the payload cannot break out of the JSON block.
|
|
18
26
|
function serializePayload(summary) {
|
|
@@ -79,16 +87,42 @@ export function serveStatsDashboard(getSummary, port = 4317) {
|
|
|
79
87
|
}
|
|
80
88
|
async function isShrinkerDashboard(url) {
|
|
81
89
|
try {
|
|
82
|
-
const response = await
|
|
90
|
+
const response = await requestLocal(url);
|
|
83
91
|
if (!response.ok)
|
|
84
92
|
return false;
|
|
85
|
-
const body =
|
|
93
|
+
const body = response.body;
|
|
86
94
|
return body.includes(DASHBOARD_MARKER) || body.includes("Shrinker stats");
|
|
87
95
|
}
|
|
88
96
|
catch {
|
|
89
97
|
return false;
|
|
90
98
|
}
|
|
91
99
|
}
|
|
100
|
+
function requestLocal(url, method = "GET", timeoutMs = 500) {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const parsed = new URL(url);
|
|
103
|
+
const request = httpRequest({
|
|
104
|
+
hostname: parsed.hostname,
|
|
105
|
+
port: parsed.port,
|
|
106
|
+
path: `${parsed.pathname}${parsed.search}`,
|
|
107
|
+
method,
|
|
108
|
+
timeout: timeoutMs,
|
|
109
|
+
}, (response) => {
|
|
110
|
+
const chunks = [];
|
|
111
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
112
|
+
response.on("end", () => {
|
|
113
|
+
const statusCode = response.statusCode ?? 0;
|
|
114
|
+
resolve({
|
|
115
|
+
ok: statusCode >= 200 && statusCode < 300,
|
|
116
|
+
statusCode,
|
|
117
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
request.on("error", reject);
|
|
122
|
+
request.on("timeout", () => request.destroy(new Error(`Timed out connecting to ${url}`)));
|
|
123
|
+
request.end();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
92
126
|
async function findListeningProcessId(port) {
|
|
93
127
|
try {
|
|
94
128
|
if (process.platform === "win32") {
|
|
@@ -133,10 +167,8 @@ export async function startStatsDashboard(port = 4317, restart = false) {
|
|
|
133
167
|
if (restart) {
|
|
134
168
|
let response;
|
|
135
169
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
signal: AbortSignal.timeout(500),
|
|
139
|
-
});
|
|
170
|
+
const shutdown = await requestLocal(`${url}/__shrinker_shutdown`, "POST");
|
|
171
|
+
response = { ok: shutdown.ok };
|
|
140
172
|
}
|
|
141
173
|
catch { }
|
|
142
174
|
if (response && !response.ok) {
|
|
@@ -154,7 +186,23 @@ export async function startStatsDashboard(port = 4317, restart = false) {
|
|
|
154
186
|
}
|
|
155
187
|
const command = getCliRelaunchCommand(["stats", "--dashboard", "--dashboard-server", "--port", String(port)]);
|
|
156
188
|
const child = spawn(command.command, command.args, { detached: true, stdio: "ignore" });
|
|
189
|
+
await new Promise((resolve, reject) => {
|
|
190
|
+
child.once("error", reject);
|
|
191
|
+
child.once("spawn", resolve);
|
|
192
|
+
});
|
|
157
193
|
child.unref();
|
|
194
|
+
if (!(await waitForDashboard(url))) {
|
|
195
|
+
throw new Error(`Dashboard server did not start at ${url}`);
|
|
196
|
+
}
|
|
158
197
|
return { pid: child.pid ?? 0, reused: false, restarted };
|
|
159
198
|
}
|
|
199
|
+
async function waitForDashboard(url, timeoutMs = 3000) {
|
|
200
|
+
const deadline = Date.now() + timeoutMs;
|
|
201
|
+
while (Date.now() < deadline) {
|
|
202
|
+
if (await isShrinkerDashboard(url))
|
|
203
|
+
return true;
|
|
204
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
160
208
|
//# sourceMappingURL=dashboard.js.map
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
function estimatedTokens(text) {
|
|
2
2
|
return Math.ceil(text.length / 4);
|
|
3
3
|
}
|
|
4
|
+
export function reductionPercent(rawEstimatedTokens, outputEstimatedTokens) {
|
|
5
|
+
if (rawEstimatedTokens === 0)
|
|
6
|
+
return 0;
|
|
7
|
+
const rounded = Math.max(0, Math.round((1 - outputEstimatedTokens / rawEstimatedTokens) * 100));
|
|
8
|
+
return outputEstimatedTokens > 0 && rounded === 100 ? 99 : rounded;
|
|
9
|
+
}
|
|
4
10
|
export function measure(raw, output) {
|
|
5
11
|
const rawBytes = Buffer.byteLength(raw);
|
|
6
12
|
const outputBytes = Buffer.byteLength(output);
|
|
7
13
|
const rawEstimatedTokens = estimatedTokens(raw);
|
|
8
14
|
const outputEstimatedTokens = estimatedTokens(output);
|
|
9
15
|
const estimatedTokensSaved = Math.max(0, rawEstimatedTokens - outputEstimatedTokens);
|
|
10
|
-
const
|
|
11
|
-
? 0
|
|
12
|
-
: Math.max(0, Math.round((1 - outputEstimatedTokens / rawEstimatedTokens) * 100));
|
|
16
|
+
const percent = reductionPercent(rawEstimatedTokens, outputEstimatedTokens);
|
|
13
17
|
return {
|
|
14
18
|
rawBytes,
|
|
15
19
|
outputBytes,
|
|
16
20
|
rawEstimatedTokens,
|
|
17
21
|
outputEstimatedTokens,
|
|
18
22
|
estimatedTokensSaved,
|
|
19
|
-
reductionPercent,
|
|
23
|
+
reductionPercent: percent,
|
|
20
24
|
};
|
|
21
25
|
}
|
|
22
26
|
export function formatMeasurements(measurements, durationMs) {
|
|
@@ -2,6 +2,7 @@ import { mkdirSync } from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { reductionPercent } from "./measure.js";
|
|
5
6
|
import { isCoverageTrackingEnabled, sanitizeToken, } from "./coverage.js";
|
|
6
7
|
const DEFAULT_INPUT_COST_PER_MILLION_TOKENS = 5;
|
|
7
8
|
function inputCostPerMillionTokens() {
|
|
@@ -156,7 +157,7 @@ function toStatsRow(row, filterKind = "all") {
|
|
|
156
157
|
outputEstimatedTokens: output,
|
|
157
158
|
estimatedTokensSaved,
|
|
158
159
|
estimatedInputCostSavedUsd: (estimatedTokensSaved / 1_000_000) * inputCostPerMillionTokens(),
|
|
159
|
-
reductionPercent: raw
|
|
160
|
+
reductionPercent: reductionPercent(raw, output),
|
|
160
161
|
};
|
|
161
162
|
}
|
|
162
163
|
const AGGREGATE = `
|