shrinker-ai 0.10.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", `${quoteForCmd(executable)} ${args.map(quoteForCmd).join(" ")}`]
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 = [];
@@ -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 getCliRelaunchCommand(args) {
12
- if (process.pkg || !process.argv[1]) {
13
- return { command: process.execPath, args };
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: process.execPath, args: [process.argv[1], ...args] };
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 fetch(url, { signal: AbortSignal.timeout(500) });
90
+ const response = await requestLocal(url);
83
91
  if (!response.ok)
84
92
  return false;
85
- const body = await response.text();
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
- response = await fetch(`${url}/__shrinker_shutdown`, {
137
- method: "POST",
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 reductionPercent = rawEstimatedTokens === 0
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 === 0 ? 0 : Math.max(0, Math.round((1 - output / raw) * 100)),
160
+ reductionPercent: reductionPercent(raw, output),
160
161
  };
161
162
  }
162
163
  const AGGREGATE = `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shrinker-ai",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Local deterministic command-output shrinker for coding agents",
5
5
  "type": "module",
6
6
  "bin": {