shrinker-ai 0.1.0 โ 0.3.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/README.md +363 -295
- package/dist/src/cli.js +18 -18
- package/dist/src/metrics/dashboard.js +65 -65
- package/dist/src/metrics/stats-store.js +43 -43
- package/integrations/macos/install.sh +152 -0
- package/integrations/macos/shrinker-profile.zsh +98 -0
- package/integrations/macos/uninstall.sh +70 -0
- package/integrations/windows/install.ps1 +113 -0
- package/integrations/{shrinker-profile.ps1 โ windows/shrinker-profile.ps1} +136 -136
- package/integrations/windows/uninstall.ps1 +57 -0
- package/package.json +39 -31
- package/templates/agent-rules.md +18 -18
- package/.copilot-instructions.md +0 -2
- package/CLAUDE.md +0 -2
- package/integrations/install-shrinker.ps1 +0 -166
- package/integrations/install.ps1 +0 -46
- package/integrations/uninstall-shrinker.ps1 +0 -79
- package/integrations/uninstall.ps1 +0 -35
package/dist/src/cli.js
CHANGED
|
@@ -9,24 +9,24 @@ import { formatMeasurements, measure } from "./metrics/measure.js";
|
|
|
9
9
|
import { openStatsDashboard, writeStatsDashboard } from "./metrics/dashboard.js";
|
|
10
10
|
import { defaultStatsPath, formatStats, formatStatsChart, getStats, recordRun } from "./metrics/stats-store.js";
|
|
11
11
|
function usage() {
|
|
12
|
-
return `Usage:
|
|
13
|
-
shrinker <command> [args...]
|
|
14
|
-
shrinker exec [options] [--] <command> [args...]
|
|
15
|
-
shrinker pipe [options]
|
|
16
|
-
shrinker stats [--json] [--chart] [--dashboard]
|
|
17
|
-
shrinker last [--path]
|
|
18
|
-
shrinker raw <capture-id> [--path]
|
|
19
|
-
shrinker help
|
|
20
|
-
|
|
21
|
-
Options:
|
|
22
|
-
--kind <auto|git-status|git-diff|git-log|git-list|npm|tail|find|rg|docker|kubectl|cat|gh|test|log>
|
|
23
|
-
--max-lines <number> default: 120
|
|
24
|
-
--per-file-lines <number> default: 40
|
|
25
|
-
--raw bypass filtering
|
|
26
|
-
--metrics print per-run savings and duration
|
|
27
|
-
--no-save do not save omitted raw output
|
|
28
|
-
--no-stats do not record this run
|
|
29
|
-
--dashboard write a browser dashboard to ~/.shrinker/dashboard.html
|
|
12
|
+
return `Usage:
|
|
13
|
+
shrinker <command> [args...]
|
|
14
|
+
shrinker exec [options] [--] <command> [args...]
|
|
15
|
+
shrinker pipe [options]
|
|
16
|
+
shrinker stats [--json] [--chart] [--dashboard]
|
|
17
|
+
shrinker last [--path]
|
|
18
|
+
shrinker raw <capture-id> [--path]
|
|
19
|
+
shrinker help
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--kind <auto|git-status|git-diff|git-log|git-list|npm|tail|find|rg|docker|kubectl|cat|gh|test|log>
|
|
23
|
+
--max-lines <number> default: 120
|
|
24
|
+
--per-file-lines <number> default: 40
|
|
25
|
+
--raw bypass filtering
|
|
26
|
+
--metrics print per-run savings and duration
|
|
27
|
+
--no-save do not save omitted raw output
|
|
28
|
+
--no-stats do not record this run
|
|
29
|
+
--dashboard write a browser dashboard to ~/.shrinker/dashboard.html
|
|
30
30
|
--help`;
|
|
31
31
|
}
|
|
32
32
|
function parsePositiveInteger(value, option) {
|
|
@@ -45,14 +45,14 @@ function makeChart(summary) {
|
|
|
45
45
|
if (points.length === 0) {
|
|
46
46
|
return `<div class="empty-chart">No recorded runs in the last 30 days.</div>`;
|
|
47
47
|
}
|
|
48
|
-
return `<svg viewBox="0 0 ${width} ${height}" role="img" aria-label="Daily estimated tokens saved over the last 30 days">
|
|
49
|
-
<text class="axis-title" x="18" y="${top + plotHeight / 2}" transform="rotate(-90 18 ${top + plotHeight / 2})">Tokens saved</text>
|
|
50
|
-
${grid}
|
|
51
|
-
<line class="axis" x1="${left}" y1="${top + plotHeight}" x2="${width - right}" y2="${top + plotHeight}" />
|
|
52
|
-
<polyline class="trend" points="${line}" />
|
|
53
|
-
${dots}
|
|
54
|
-
${labels}
|
|
55
|
-
<text class="axis-title" x="${left + plotWidth / 2}" y="${height - 6}" text-anchor="middle">Date</text>
|
|
48
|
+
return `<svg viewBox="0 0 ${width} ${height}" role="img" aria-label="Daily estimated tokens saved over the last 30 days">
|
|
49
|
+
<text class="axis-title" x="18" y="${top + plotHeight / 2}" transform="rotate(-90 18 ${top + plotHeight / 2})">Tokens saved</text>
|
|
50
|
+
${grid}
|
|
51
|
+
<line class="axis" x1="${left}" y1="${top + plotHeight}" x2="${width - right}" y2="${top + plotHeight}" />
|
|
52
|
+
<polyline class="trend" points="${line}" />
|
|
53
|
+
${dots}
|
|
54
|
+
${labels}
|
|
55
|
+
<text class="axis-title" x="${left + plotWidth / 2}" y="${height - 6}" text-anchor="middle">Date</text>
|
|
56
56
|
</svg>`;
|
|
57
57
|
}
|
|
58
58
|
export function defaultDashboardPath(databasePath) {
|
|
@@ -62,63 +62,63 @@ export function writeStatsDashboard(summary, outputPath = defaultDashboardPath(s
|
|
|
62
62
|
const filters = summary.byFilter.length === 0
|
|
63
63
|
? `<p class="muted">No filter data yet.</p>`
|
|
64
64
|
: summary.byFilter.map((row) => `<div class="filter-row"><span>${escapeHtml(row.filterKind)}</span><strong>${formatInteger(row.estimatedTokensSaved)}</strong><small>${row.reductionPercent}% reduction</small></div>`).join("");
|
|
65
|
-
const html = `<!doctype html>
|
|
66
|
-
<html lang="en">
|
|
67
|
-
<head>
|
|
68
|
-
<meta charset="utf-8">
|
|
69
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
70
|
-
<title>Shrinker stats dashboard</title>
|
|
71
|
-
<style>
|
|
72
|
-
:root { color-scheme: light; --ink: #17202a; --muted: #66727f; --line: #dce3e8; --blue: #2774d9; --blue-soft: #eaf2ff; --green: #16856b; --surface: #ffffff; --background: #f4f7fa; }
|
|
73
|
-
* { box-sizing: border-box; }
|
|
74
|
-
body { margin: 0; color: var(--ink); background: var(--background); font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif; }
|
|
75
|
-
main { max-width: 1180px; margin: 0 auto; padding: 42px 28px 56px; }
|
|
76
|
-
.eyebrow { margin: 0 0 8px; color: var(--blue); font-size: 12px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }
|
|
77
|
-
h1 { margin: 0; font-size: clamp(28px, 4vw, 44px); letter-spacing: -.03em; }
|
|
78
|
-
.subtitle { margin: 8px 0 30px; color: var(--muted); }
|
|
79
|
-
.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-bottom: 20px; }
|
|
80
|
-
.card, .panel { border: 1px solid var(--line); border-radius: 8px; background: var(--surface); box-shadow: 0 8px 24px #26394d0d; }
|
|
81
|
-
.card { padding: 18px 20px; }
|
|
82
|
-
.card label { display: block; color: var(--muted); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; }
|
|
83
|
-
.card strong { display: block; margin-top: 6px; font-size: 27px; }
|
|
84
|
-
.panel { padding: 22px; }
|
|
85
|
-
.panel h2 { margin: 0 0 4px; font-size: 18px; }
|
|
86
|
-
.panel p { margin: 0 0 14px; color: var(--muted); }
|
|
87
|
-
.chart { overflow: hidden; }
|
|
88
|
-
svg { display: block; width: 100%; min-width: 620px; height: auto; }
|
|
89
|
-
.chart { overflow-x: auto; }
|
|
90
|
-
.grid { stroke: var(--line); stroke-width: 1; }
|
|
91
|
-
.axis { stroke: #9aa9b5; stroke-width: 1; }
|
|
92
|
-
.axis-label, .axis-title { fill: var(--muted); font-size: 12px; }
|
|
93
|
-
.trend { fill: none; stroke: var(--blue); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
|
|
94
|
-
.point { fill: var(--surface); stroke: var(--blue); stroke-width: 3; }
|
|
95
|
-
.point:hover { fill: var(--blue); r: 6; }
|
|
96
|
-
.lower { display: grid; grid-template-columns: 1.2fr .8fr; gap: 20px; margin-top: 20px; }
|
|
97
|
-
.filter-row { display: grid; grid-template-columns: 1fr auto auto; gap: 18px; align-items: baseline; padding: 11px 0; border-bottom: 1px solid var(--line); }
|
|
98
|
-
.filter-row:last-child { border-bottom: 0; }
|
|
99
|
-
.filter-row small { color: var(--green); }
|
|
100
|
-
.muted { color: var(--muted); }
|
|
101
|
-
@media (max-width: 720px) { main { padding: 28px 16px 40px; } .cards, .lower { grid-template-columns: 1fr; } .card strong { font-size: 24px; } }
|
|
102
|
-
</style>
|
|
103
|
-
</head>
|
|
104
|
-
<body>
|
|
105
|
-
<main>
|
|
106
|
-
<p class="eyebrow">Local activity</p>
|
|
107
|
-
<h1>Shrinker stats</h1>
|
|
108
|
-
<p class="subtitle">Token reduction over the last 30 days</p>
|
|
109
|
-
<section class="cards">
|
|
110
|
-
<div class="card"><label>All-time saved</label><strong>${formatInteger(summary.total.estimatedTokensSaved)}</strong></div>
|
|
111
|
-
<div class="card"><label>Runs this week</label><strong>${formatInteger(summary.last7Days.runs)}</strong></div>
|
|
112
|
-
<div class="card"><label>Average reduction</label><strong>${summary.total.reductionPercent}%</strong></div>
|
|
113
|
-
</section>
|
|
114
|
-
<section class="panel chart"><h2>Tokens saved over time</h2><p>Estimated savings from recorded command runs.</p>${makeChart(summary)}</section>
|
|
115
|
-
<section class="lower">
|
|
116
|
-
<section class="panel"><h2>By filter</h2><p>Where the savings come from.</p>${filters}</section>
|
|
117
|
-
<section class="panel"><h2>Storage</h2><p>Stats stay on this machine.</p><p class="muted">${escapeHtml(summary.databasePath)}</p></section>
|
|
118
|
-
</section>
|
|
119
|
-
</main>
|
|
120
|
-
</body>
|
|
121
|
-
</html>
|
|
65
|
+
const html = `<!doctype html>
|
|
66
|
+
<html lang="en">
|
|
67
|
+
<head>
|
|
68
|
+
<meta charset="utf-8">
|
|
69
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
70
|
+
<title>Shrinker stats dashboard</title>
|
|
71
|
+
<style>
|
|
72
|
+
:root { color-scheme: light; --ink: #17202a; --muted: #66727f; --line: #dce3e8; --blue: #2774d9; --blue-soft: #eaf2ff; --green: #16856b; --surface: #ffffff; --background: #f4f7fa; }
|
|
73
|
+
* { box-sizing: border-box; }
|
|
74
|
+
body { margin: 0; color: var(--ink); background: var(--background); font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif; }
|
|
75
|
+
main { max-width: 1180px; margin: 0 auto; padding: 42px 28px 56px; }
|
|
76
|
+
.eyebrow { margin: 0 0 8px; color: var(--blue); font-size: 12px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }
|
|
77
|
+
h1 { margin: 0; font-size: clamp(28px, 4vw, 44px); letter-spacing: -.03em; }
|
|
78
|
+
.subtitle { margin: 8px 0 30px; color: var(--muted); }
|
|
79
|
+
.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-bottom: 20px; }
|
|
80
|
+
.card, .panel { border: 1px solid var(--line); border-radius: 8px; background: var(--surface); box-shadow: 0 8px 24px #26394d0d; }
|
|
81
|
+
.card { padding: 18px 20px; }
|
|
82
|
+
.card label { display: block; color: var(--muted); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; }
|
|
83
|
+
.card strong { display: block; margin-top: 6px; font-size: 27px; }
|
|
84
|
+
.panel { padding: 22px; }
|
|
85
|
+
.panel h2 { margin: 0 0 4px; font-size: 18px; }
|
|
86
|
+
.panel p { margin: 0 0 14px; color: var(--muted); }
|
|
87
|
+
.chart { overflow: hidden; }
|
|
88
|
+
svg { display: block; width: 100%; min-width: 620px; height: auto; }
|
|
89
|
+
.chart { overflow-x: auto; }
|
|
90
|
+
.grid { stroke: var(--line); stroke-width: 1; }
|
|
91
|
+
.axis { stroke: #9aa9b5; stroke-width: 1; }
|
|
92
|
+
.axis-label, .axis-title { fill: var(--muted); font-size: 12px; }
|
|
93
|
+
.trend { fill: none; stroke: var(--blue); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
|
|
94
|
+
.point { fill: var(--surface); stroke: var(--blue); stroke-width: 3; }
|
|
95
|
+
.point:hover { fill: var(--blue); r: 6; }
|
|
96
|
+
.lower { display: grid; grid-template-columns: 1.2fr .8fr; gap: 20px; margin-top: 20px; }
|
|
97
|
+
.filter-row { display: grid; grid-template-columns: 1fr auto auto; gap: 18px; align-items: baseline; padding: 11px 0; border-bottom: 1px solid var(--line); }
|
|
98
|
+
.filter-row:last-child { border-bottom: 0; }
|
|
99
|
+
.filter-row small { color: var(--green); }
|
|
100
|
+
.muted { color: var(--muted); }
|
|
101
|
+
@media (max-width: 720px) { main { padding: 28px 16px 40px; } .cards, .lower { grid-template-columns: 1fr; } .card strong { font-size: 24px; } }
|
|
102
|
+
</style>
|
|
103
|
+
</head>
|
|
104
|
+
<body>
|
|
105
|
+
<main>
|
|
106
|
+
<p class="eyebrow">Local activity</p>
|
|
107
|
+
<h1>Shrinker stats</h1>
|
|
108
|
+
<p class="subtitle">Token reduction over the last 30 days</p>
|
|
109
|
+
<section class="cards">
|
|
110
|
+
<div class="card"><label>All-time saved</label><strong>${formatInteger(summary.total.estimatedTokensSaved)}</strong></div>
|
|
111
|
+
<div class="card"><label>Runs this week</label><strong>${formatInteger(summary.last7Days.runs)}</strong></div>
|
|
112
|
+
<div class="card"><label>Average reduction</label><strong>${summary.total.reductionPercent}%</strong></div>
|
|
113
|
+
</section>
|
|
114
|
+
<section class="panel chart"><h2>Tokens saved over time</h2><p>Estimated savings from recorded command runs.</p>${makeChart(summary)}</section>
|
|
115
|
+
<section class="lower">
|
|
116
|
+
<section class="panel"><h2>By filter</h2><p>Where the savings come from.</p>${filters}</section>
|
|
117
|
+
<section class="panel"><h2>Storage</h2><p>Stats stay on this machine.</p><p class="muted">${escapeHtml(summary.databasePath)}</p></section>
|
|
118
|
+
</section>
|
|
119
|
+
</main>
|
|
120
|
+
</body>
|
|
121
|
+
</html>
|
|
122
122
|
`;
|
|
123
123
|
mkdirSync(path.dirname(outputPath), { recursive: true, mode: 0o700 });
|
|
124
124
|
writeFileSync(outputPath, html, "utf8");
|
|
@@ -8,27 +8,27 @@ export function defaultStatsPath() {
|
|
|
8
8
|
function openDatabase(databasePath) {
|
|
9
9
|
mkdirSync(path.dirname(databasePath), { recursive: true, mode: 0o700 });
|
|
10
10
|
const database = new DatabaseSync(databasePath);
|
|
11
|
-
database.exec(`
|
|
12
|
-
PRAGMA journal_mode = WAL;
|
|
13
|
-
PRAGMA busy_timeout = 2000;
|
|
14
|
-
CREATE TABLE IF NOT EXISTS runs (
|
|
15
|
-
id INTEGER PRIMARY KEY,
|
|
16
|
-
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
17
|
-
mode TEXT NOT NULL,
|
|
18
|
-
filter_kind TEXT NOT NULL,
|
|
19
|
-
command_name TEXT NOT NULL,
|
|
20
|
-
raw_bytes INTEGER NOT NULL,
|
|
21
|
-
output_bytes INTEGER NOT NULL,
|
|
22
|
-
raw_estimated_tokens INTEGER NOT NULL,
|
|
23
|
-
output_estimated_tokens INTEGER NOT NULL,
|
|
24
|
-
estimated_tokens_saved INTEGER NOT NULL,
|
|
25
|
-
reduction_percent INTEGER NOT NULL,
|
|
26
|
-
duration_ms INTEGER,
|
|
27
|
-
omitted INTEGER NOT NULL,
|
|
28
|
-
exit_code INTEGER
|
|
29
|
-
);
|
|
30
|
-
CREATE INDEX IF NOT EXISTS runs_created_at_idx ON runs(created_at);
|
|
31
|
-
CREATE INDEX IF NOT EXISTS runs_filter_kind_idx ON runs(filter_kind);
|
|
11
|
+
database.exec(`
|
|
12
|
+
PRAGMA journal_mode = WAL;
|
|
13
|
+
PRAGMA busy_timeout = 2000;
|
|
14
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
15
|
+
id INTEGER PRIMARY KEY,
|
|
16
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
17
|
+
mode TEXT NOT NULL,
|
|
18
|
+
filter_kind TEXT NOT NULL,
|
|
19
|
+
command_name TEXT NOT NULL,
|
|
20
|
+
raw_bytes INTEGER NOT NULL,
|
|
21
|
+
output_bytes INTEGER NOT NULL,
|
|
22
|
+
raw_estimated_tokens INTEGER NOT NULL,
|
|
23
|
+
output_estimated_tokens INTEGER NOT NULL,
|
|
24
|
+
estimated_tokens_saved INTEGER NOT NULL,
|
|
25
|
+
reduction_percent INTEGER NOT NULL,
|
|
26
|
+
duration_ms INTEGER,
|
|
27
|
+
omitted INTEGER NOT NULL,
|
|
28
|
+
exit_code INTEGER
|
|
29
|
+
);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS runs_created_at_idx ON runs(created_at);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS runs_filter_kind_idx ON runs(filter_kind);
|
|
32
32
|
`);
|
|
33
33
|
return database;
|
|
34
34
|
}
|
|
@@ -36,12 +36,12 @@ export function recordRun(statistic, databasePath = defaultStatsPath()) {
|
|
|
36
36
|
const database = openDatabase(databasePath);
|
|
37
37
|
try {
|
|
38
38
|
database
|
|
39
|
-
.prepare(`
|
|
40
|
-
INSERT INTO runs (
|
|
41
|
-
mode, filter_kind, command_name, raw_bytes, output_bytes,
|
|
42
|
-
raw_estimated_tokens, output_estimated_tokens, estimated_tokens_saved,
|
|
43
|
-
reduction_percent, duration_ms, omitted, exit_code
|
|
44
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
39
|
+
.prepare(`
|
|
40
|
+
INSERT INTO runs (
|
|
41
|
+
mode, filter_kind, command_name, raw_bytes, output_bytes,
|
|
42
|
+
raw_estimated_tokens, output_estimated_tokens, estimated_tokens_saved,
|
|
43
|
+
reduction_percent, duration_ms, omitted, exit_code
|
|
44
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
45
45
|
`)
|
|
46
46
|
.run(statistic.mode, statistic.filterKind, statistic.commandName, statistic.measurements.rawBytes, statistic.measurements.outputBytes, statistic.measurements.rawEstimatedTokens, statistic.measurements.outputEstimatedTokens, statistic.measurements.estimatedTokensSaved, statistic.measurements.reductionPercent, statistic.durationMs ?? null, statistic.omitted ? 1 : 0, statistic.exitCode ?? null);
|
|
47
47
|
}
|
|
@@ -61,11 +61,11 @@ function toStatsRow(row, filterKind = "all") {
|
|
|
61
61
|
reductionPercent: raw === 0 ? 0 : Math.max(0, Math.round((1 - output / raw) * 100)),
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
const AGGREGATE = `
|
|
65
|
-
COUNT(*) AS runs,
|
|
66
|
-
COALESCE(SUM(raw_estimated_tokens), 0) AS raw_tokens,
|
|
67
|
-
COALESCE(SUM(output_estimated_tokens), 0) AS output_tokens,
|
|
68
|
-
COALESCE(SUM(estimated_tokens_saved), 0) AS tokens_saved
|
|
64
|
+
const AGGREGATE = `
|
|
65
|
+
COUNT(*) AS runs,
|
|
66
|
+
COALESCE(SUM(raw_estimated_tokens), 0) AS raw_tokens,
|
|
67
|
+
COALESCE(SUM(output_estimated_tokens), 0) AS output_tokens,
|
|
68
|
+
COALESCE(SUM(estimated_tokens_saved), 0) AS tokens_saved
|
|
69
69
|
`;
|
|
70
70
|
export function getStats(databasePath = defaultStatsPath()) {
|
|
71
71
|
const database = openDatabase(databasePath);
|
|
@@ -77,20 +77,20 @@ export function getStats(databasePath = defaultStatsPath()) {
|
|
|
77
77
|
.prepare(`SELECT ${AGGREGATE} FROM runs WHERE created_at >= datetime('now', '-7 days')`)
|
|
78
78
|
.get();
|
|
79
79
|
const byFilter = database
|
|
80
|
-
.prepare(`
|
|
81
|
-
SELECT filter_kind, ${AGGREGATE}
|
|
82
|
-
FROM runs
|
|
83
|
-
GROUP BY filter_kind
|
|
84
|
-
ORDER BY tokens_saved DESC, filter_kind ASC
|
|
80
|
+
.prepare(`
|
|
81
|
+
SELECT filter_kind, ${AGGREGATE}
|
|
82
|
+
FROM runs
|
|
83
|
+
GROUP BY filter_kind
|
|
84
|
+
ORDER BY tokens_saved DESC, filter_kind ASC
|
|
85
85
|
`)
|
|
86
86
|
.all();
|
|
87
87
|
const daily = database
|
|
88
|
-
.prepare(`
|
|
89
|
-
SELECT substr(created_at, 1, 10) AS date, ${AGGREGATE}
|
|
90
|
-
FROM runs
|
|
91
|
-
WHERE created_at >= datetime('now', '-30 days')
|
|
92
|
-
GROUP BY substr(created_at, 1, 10)
|
|
93
|
-
ORDER BY date ASC
|
|
88
|
+
.prepare(`
|
|
89
|
+
SELECT substr(created_at, 1, 10) AS date, ${AGGREGATE}
|
|
90
|
+
FROM runs
|
|
91
|
+
WHERE created_at >= datetime('now', '-30 days')
|
|
92
|
+
GROUP BY substr(created_at, 1, 10)
|
|
93
|
+
ORDER BY date ASC
|
|
94
94
|
`)
|
|
95
95
|
.all();
|
|
96
96
|
return {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
PACKAGE_NAME="shrinker-ai"
|
|
5
|
+
REGISTRY="https://registry.npmjs.org"
|
|
6
|
+
VERSION=""
|
|
7
|
+
LOCAL=0
|
|
8
|
+
SKIP_NPM_INSTALL=0
|
|
9
|
+
SKIP_BUILD=0
|
|
10
|
+
SKIP_LINK=0
|
|
11
|
+
ENABLE_PROFILE_ROUTING=0
|
|
12
|
+
SKIP_PROFILE=0
|
|
13
|
+
SKIP_AGENT_RULES=0
|
|
14
|
+
COPILOT_ONLY=0
|
|
15
|
+
CLAUDE_ONLY=0
|
|
16
|
+
PROFILE_PATH="${PROFILE_PATH:-$HOME/.zshrc}"
|
|
17
|
+
STEP=0
|
|
18
|
+
|
|
19
|
+
print_message() {
|
|
20
|
+
local emoji="$1"
|
|
21
|
+
local message="$2"
|
|
22
|
+
STEP=$((STEP + 1))
|
|
23
|
+
printf '%d. %s %s\n' "$STEP" "$emoji" "$message"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
while [[ $# -gt 0 ]]; do
|
|
27
|
+
case "$1" in
|
|
28
|
+
--local) LOCAL=1 ;;
|
|
29
|
+
--package-name) PACKAGE_NAME="${2:-}"; shift ;;
|
|
30
|
+
--registry) REGISTRY="${2:-}"; shift ;;
|
|
31
|
+
--version) VERSION="${2:-}"; shift ;;
|
|
32
|
+
--skip-npm-install) SKIP_NPM_INSTALL=1 ;;
|
|
33
|
+
--skip-build) SKIP_BUILD=1 ;;
|
|
34
|
+
--skip-link) SKIP_LINK=1 ;;
|
|
35
|
+
--enable-profile-routing) ENABLE_PROFILE_ROUTING=1 ;;
|
|
36
|
+
--skip-profile) SKIP_PROFILE=1 ;;
|
|
37
|
+
--skip-agent-rules) SKIP_AGENT_RULES=1 ;;
|
|
38
|
+
--copilot-only) COPILOT_ONLY=1 ;;
|
|
39
|
+
--claude-only) CLAUDE_ONLY=1 ;;
|
|
40
|
+
--profile-path) PROFILE_PATH="${2:-}"; shift ;;
|
|
41
|
+
*) print_message "โ" "Unknown option: $1" >&2; exit 1 ;;
|
|
42
|
+
esac
|
|
43
|
+
shift
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
if (( ENABLE_PROFILE_ROUTING == 1 && SKIP_PROFILE == 1 )); then print_message "โ" "Use either --enable-profile-routing or --skip-profile, not both." >&2; exit 1; fi
|
|
47
|
+
if (( COPILOT_ONLY == 1 && CLAUDE_ONLY == 1 )); then print_message "โ" "Use either --copilot-only or --claude-only, not both." >&2; exit 1; fi
|
|
48
|
+
|
|
49
|
+
if (( LOCAL == 0 )); then
|
|
50
|
+
package_spec="$PACKAGE_NAME"
|
|
51
|
+
[[ -z "$VERSION" ]] || package_spec="$PACKAGE_NAME@$VERSION"
|
|
52
|
+
print_message "๐ฆ" "Installing $package_spec from $REGISTRY..."
|
|
53
|
+
npm install --silent --global "$package_spec" "--registry=$REGISTRY"
|
|
54
|
+
package_root="$(npm root --global)/$PACKAGE_NAME"
|
|
55
|
+
entrypoint="$package_root/integrations/macos/install.sh"
|
|
56
|
+
[[ -f "$entrypoint" ]] || { print_message "โ" "Installed package installer not found: $entrypoint" >&2; exit 1; }
|
|
57
|
+
bash "$entrypoint" --local \
|
|
58
|
+
$( (( SKIP_NPM_INSTALL )) && echo --skip-npm-install ) \
|
|
59
|
+
$( (( SKIP_BUILD )) && echo --skip-build ) \
|
|
60
|
+
$( (( SKIP_LINK )) && echo --skip-link ) \
|
|
61
|
+
$( (( ENABLE_PROFILE_ROUTING )) && echo --enable-profile-routing ) \
|
|
62
|
+
$( (( SKIP_PROFILE )) && echo --skip-profile ) \
|
|
63
|
+
$( (( SKIP_AGENT_RULES )) && echo --skip-agent-rules ) \
|
|
64
|
+
$( (( COPILOT_ONLY )) && echo --copilot-only ) \
|
|
65
|
+
$( (( CLAUDE_ONLY )) && echo --claude-only ) \
|
|
66
|
+
--profile-path "$PROFILE_PATH"
|
|
67
|
+
exit 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
71
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
72
|
+
TEMPLATE_PATH="$REPO_ROOT/templates/agent-rules.md"
|
|
73
|
+
INTEGRATION_PATH="$SCRIPT_DIR/shrinker-profile.zsh"
|
|
74
|
+
BLOCK_START="<!-- shrinker agent rules start -->"
|
|
75
|
+
BLOCK_END="<!-- shrinker agent rules end -->"
|
|
76
|
+
|
|
77
|
+
node_version_raw="$(node -v 2>/dev/null || true)"
|
|
78
|
+
[[ -n "$node_version_raw" ]] || { print_message "โ" "Node.js was not found on PATH. Install Node.js 22.13+ first." >&2; exit 1; }
|
|
79
|
+
node_version="${node_version_raw#v}"
|
|
80
|
+
IFS='.' read -r node_major node_minor _ <<< "$node_version"
|
|
81
|
+
(( node_major > 22 || (node_major == 22 && node_minor >= 13) )) || { print_message "โ" "Node.js 22.13+ is required. Found $node_version_raw" >&2; exit 1; }
|
|
82
|
+
|
|
83
|
+
set_agent_rules() {
|
|
84
|
+
local target="$1"
|
|
85
|
+
local body="$2"
|
|
86
|
+
local block="$BLOCK_START"$'\n'"$body"$'\n'"$BLOCK_END"
|
|
87
|
+
mkdir -p "$(dirname "$target")"
|
|
88
|
+
if [[ -f "$target" ]] && grep -Fq "$BLOCK_START" "$target" && grep -Fq "$BLOCK_END" "$target"; then
|
|
89
|
+
local block_file
|
|
90
|
+
block_file="$(mktemp "$(dirname "$target")/.shrinker-rules.XXXXXX")"
|
|
91
|
+
printf '%s\n' "$block" > "$block_file"
|
|
92
|
+
awk -v start="$BLOCK_START" -v end="$BLOCK_END" -v block_file="$block_file" '
|
|
93
|
+
function print_block(line) {
|
|
94
|
+
while ((getline line < block_file) > 0) print line
|
|
95
|
+
close(block_file)
|
|
96
|
+
}
|
|
97
|
+
{
|
|
98
|
+
if (index($0, start) > 0) {
|
|
99
|
+
if (!replaced) print_block()
|
|
100
|
+
replaced = 1
|
|
101
|
+
inblock = 1
|
|
102
|
+
next
|
|
103
|
+
}
|
|
104
|
+
if (inblock && index($0, end) > 0) {
|
|
105
|
+
inblock = 0
|
|
106
|
+
next
|
|
107
|
+
}
|
|
108
|
+
if (!inblock) print
|
|
109
|
+
}
|
|
110
|
+
' "$target" > "$target.tmp"
|
|
111
|
+
rm -f "$block_file"
|
|
112
|
+
mv "$target.tmp" "$target"
|
|
113
|
+
elif [[ -f "$target" ]]; then printf '\n%s\n' "$block" >> "$target"
|
|
114
|
+
else printf '%s\n' "$block" > "$target"; fi
|
|
115
|
+
print_message "๐" "Installed managed rules in: $target"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
add_profile_integration() {
|
|
119
|
+
local profile_file="$1"
|
|
120
|
+
mkdir -p "$(dirname "$profile_file")"; touch "$profile_file"
|
|
121
|
+
if ! grep -Fq '# >>> shrinker integration >>>' "$profile_file"; then
|
|
122
|
+
printf '\n# >>> shrinker integration >>>\nsource "%s"\n# <<< shrinker integration <<<\n\n' "$INTEGRATION_PATH" >> "$profile_file"
|
|
123
|
+
fi
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
print_message "๐ฆ" "Installing shrinker from: $REPO_ROOT"
|
|
127
|
+
pushd "$REPO_ROOT" >/dev/null
|
|
128
|
+
if (( SKIP_NPM_INSTALL == 0 )); then
|
|
129
|
+
print_message "๐ฅ" "Installing dependencies..."
|
|
130
|
+
npm install --silent
|
|
131
|
+
fi
|
|
132
|
+
if (( SKIP_BUILD == 0 )); then
|
|
133
|
+
print_message "๐๏ธ" "Building shrinker..."
|
|
134
|
+
npm run build --silent
|
|
135
|
+
fi
|
|
136
|
+
if (( SKIP_LINK == 0 )); then
|
|
137
|
+
print_message "๐" "Linking shrinker globally..."
|
|
138
|
+
npm link --silent
|
|
139
|
+
fi
|
|
140
|
+
popd >/dev/null
|
|
141
|
+
|
|
142
|
+
if (( SKIP_PROFILE == 0 && ENABLE_PROFILE_ROUTING == 1 )); then add_profile_integration "$PROFILE_PATH"; fi
|
|
143
|
+
if (( SKIP_AGENT_RULES == 0 )); then
|
|
144
|
+
[[ -f "$TEMPLATE_PATH" ]] || { print_message "โ" "Agent rules template not found: $TEMPLATE_PATH" >&2; exit 1; }
|
|
145
|
+
rules_body="$(cat "$TEMPLATE_PATH")"
|
|
146
|
+
(( CLAUDE_ONLY )) || set_agent_rules "$HOME/.copilot/copilot-instructions.md" "$rules_body"
|
|
147
|
+
(( COPILOT_ONLY )) || set_agent_rules "$HOME/.claude/CLAUDE.md" "$rules_body"
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
print_message "โ
" "Install complete."
|
|
151
|
+
echo ""
|
|
152
|
+
echo "๐ก Try: shrinker help"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
typeset -gA SHRINK_RULES
|
|
2
|
+
SHRINK_RULES=(
|
|
3
|
+
git "status diff log show reflog branch tag stash"
|
|
4
|
+
npm "test t install i ci ls list"
|
|
5
|
+
docker "ps logs images compose"
|
|
6
|
+
kubectl "get describe logs"
|
|
7
|
+
gh "pr issue run"
|
|
8
|
+
rg "*"
|
|
9
|
+
find "*"
|
|
10
|
+
tail "*"
|
|
11
|
+
cat "*"
|
|
12
|
+
ls "*"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
typeset -gA SHRINK_OPTION_VALUE_FLAGS
|
|
16
|
+
SHRINK_OPTION_VALUE_FLAGS=(
|
|
17
|
+
git "-C -c --git-dir --work-tree --namespace"
|
|
18
|
+
npm "--prefix --cache --registry --workspace --userconfig -w -C"
|
|
19
|
+
docker "-H --host --context --config"
|
|
20
|
+
kubectl "-n --namespace -o --output --context --kubeconfig --cluster --user"
|
|
21
|
+
gh "-R --repo"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_shrink_get_subcommand() {
|
|
25
|
+
local cmd="$1"
|
|
26
|
+
shift
|
|
27
|
+
|
|
28
|
+
local -a args
|
|
29
|
+
args=("$@")
|
|
30
|
+
|
|
31
|
+
local -a value_flags
|
|
32
|
+
value_flags=(${=SHRINK_OPTION_VALUE_FLAGS[$cmd]})
|
|
33
|
+
|
|
34
|
+
local i part
|
|
35
|
+
i=1
|
|
36
|
+
while (( i <= ${#args[@]} )); do
|
|
37
|
+
part="${args[$i]}"
|
|
38
|
+
if [[ -z "$part" ]]; then
|
|
39
|
+
(( i++ ))
|
|
40
|
+
continue
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if (( ${value_flags[(Ie)$part]} > 0 )); then
|
|
44
|
+
(( i += 2 ))
|
|
45
|
+
continue
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
if [[ "$part" == -* ]]; then
|
|
49
|
+
(( i++ ))
|
|
50
|
+
continue
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
print -r -- "${part:l}"
|
|
54
|
+
return 0
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
print -r -- ""
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_shrink_should_route() {
|
|
61
|
+
local cmd="$1"
|
|
62
|
+
shift
|
|
63
|
+
|
|
64
|
+
local rules="${SHRINK_RULES[$cmd]}"
|
|
65
|
+
[[ -z "$rules" ]] && return 1
|
|
66
|
+
[[ "$rules" == *"*"* ]] && return 0
|
|
67
|
+
|
|
68
|
+
local subcommand
|
|
69
|
+
subcommand="$(_shrink_get_subcommand "$cmd" "$@")"
|
|
70
|
+
[[ -z "$subcommand" ]] && return 1
|
|
71
|
+
|
|
72
|
+
local -a allowlist
|
|
73
|
+
allowlist=(${=rules})
|
|
74
|
+
(( ${allowlist[(Ie)$subcommand]} > 0 ))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_shrink_invoke_or_native() {
|
|
78
|
+
local cmd="$1"
|
|
79
|
+
shift
|
|
80
|
+
|
|
81
|
+
if _shrink_should_route "$cmd" "$@" && command -v shrinker >/dev/null 2>&1; then
|
|
82
|
+
command shrinker "$cmd" "$@"
|
|
83
|
+
return $?
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
command "$cmd" "$@"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
git() { _shrink_invoke_or_native git "$@"; }
|
|
90
|
+
npm() { _shrink_invoke_or_native npm "$@"; }
|
|
91
|
+
docker() { _shrink_invoke_or_native docker "$@"; }
|
|
92
|
+
kubectl() { _shrink_invoke_or_native kubectl "$@"; }
|
|
93
|
+
gh() { _shrink_invoke_or_native gh "$@"; }
|
|
94
|
+
rg() { _shrink_invoke_or_native rg "$@"; }
|
|
95
|
+
find() { _shrink_invoke_or_native find "$@"; }
|
|
96
|
+
tail() { _shrink_invoke_or_native tail "$@"; }
|
|
97
|
+
cat() { _shrink_invoke_or_native cat "$@"; }
|
|
98
|
+
ls() { _shrink_invoke_or_native ls "$@"; }
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
INSTALL_DIR="${HOME}/.shrinker-src"
|
|
5
|
+
REMOVE_INSTALL_DIR=0
|
|
6
|
+
SKIP_UNLINK=0
|
|
7
|
+
SKIP_AGENT_RULES=0
|
|
8
|
+
COPILOT_ONLY=0
|
|
9
|
+
CLAUDE_ONLY=0
|
|
10
|
+
PROFILE_PATH="${PROFILE_PATH:-$HOME/.zshrc}"
|
|
11
|
+
UNINSTALL_STEP=0
|
|
12
|
+
|
|
13
|
+
print_step() {
|
|
14
|
+
local emoji="$1" message="$2"
|
|
15
|
+
UNINSTALL_STEP=$((UNINSTALL_STEP + 1))
|
|
16
|
+
printf '%d. %s %s\n' "$UNINSTALL_STEP" "$emoji" "$message"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
while [[ $# -gt 0 ]]; do
|
|
20
|
+
case "$1" in
|
|
21
|
+
--skip-unlink) SKIP_UNLINK=1 ;;
|
|
22
|
+
--skip-agent-rules) SKIP_AGENT_RULES=1 ;;
|
|
23
|
+
--copilot-only) COPILOT_ONLY=1 ;;
|
|
24
|
+
--claude-only) CLAUDE_ONLY=1 ;;
|
|
25
|
+
--profile-path) PROFILE_PATH="${2:-}"; shift ;;
|
|
26
|
+
--install-dir) INSTALL_DIR="${2:-}"; shift ;;
|
|
27
|
+
--remove-install-dir) REMOVE_INSTALL_DIR=1 ;;
|
|
28
|
+
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
29
|
+
esac
|
|
30
|
+
shift
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
if (( COPILOT_ONLY == 1 && CLAUDE_ONLY == 1 )); then echo "Use either --copilot-only or --claude-only, not both." >&2; exit 1; fi
|
|
34
|
+
BLOCK_START="<!-- shrinker agent rules start -->"
|
|
35
|
+
BLOCK_END="<!-- shrinker agent rules end -->"
|
|
36
|
+
|
|
37
|
+
remove_agent_rules() {
|
|
38
|
+
local target="$1"
|
|
39
|
+
[[ -f "$target" ]] || return 0
|
|
40
|
+
awk -v start="$BLOCK_START" -v end="$BLOCK_END" 'BEGIN{inblock=0} {if(index($0,start)>0){inblock=1;next} if(inblock&&index($0,end)>0){inblock=0;next} if(!inblock)print}' "$target" > "$target.tmp"
|
|
41
|
+
mv "$target.tmp" "$target"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
remove_profile_integration() {
|
|
45
|
+
local profile_file="$1"
|
|
46
|
+
[[ -f "$profile_file" ]] || return 0
|
|
47
|
+
awk -v start='# >>> shrinker integration >>>' -v end='# <<< shrinker integration <<<' 'BEGIN{inblock=0} {if(index($0,start)>0){inblock=1;next} if(inblock&&index($0,end)>0){inblock=0;next} if(!inblock)print}' "$profile_file" > "$profile_file.tmp"
|
|
48
|
+
mv "$profile_file.tmp" "$profile_file"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (( SKIP_UNLINK == 0 )); then
|
|
52
|
+
print_step "๐" "Unlinking shrinker globally..."
|
|
53
|
+
npm unlink --silent --global shrinker-ai
|
|
54
|
+
else
|
|
55
|
+
print_step "โญ๏ธ" "Skipped global npm unlink."
|
|
56
|
+
fi
|
|
57
|
+
print_step "๐ง" "Removing shell profile integration..."
|
|
58
|
+
remove_profile_integration "$PROFILE_PATH"
|
|
59
|
+
if (( SKIP_AGENT_RULES == 0 )); then
|
|
60
|
+
(( CLAUDE_ONLY )) || remove_agent_rules "$HOME/.copilot/copilot-instructions.md"
|
|
61
|
+
(( COPILOT_ONLY )) || remove_agent_rules "$HOME/.claude/CLAUDE.md"
|
|
62
|
+
else
|
|
63
|
+
print_step "โญ๏ธ" "Skipped managed agent rules."
|
|
64
|
+
fi
|
|
65
|
+
print_step "โ
" "Uninstall complete."
|
|
66
|
+
|
|
67
|
+
if (( REMOVE_INSTALL_DIR == 1 )) && [[ -d "$INSTALL_DIR" ]]; then
|
|
68
|
+
rm -rf "$INSTALL_DIR"
|
|
69
|
+
print_step "๐งน" "Removed install directory: $INSTALL_DIR"
|
|
70
|
+
fi
|