@ulrichc1/sparn 1.2.2 → 1.4.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/PRIVACY.md +1 -1
- package/README.md +136 -642
- package/SECURITY.md +1 -1
- package/dist/cli/dashboard.cjs +3977 -0
- package/dist/cli/dashboard.cjs.map +1 -0
- package/dist/cli/dashboard.d.cts +17 -0
- package/dist/cli/dashboard.d.ts +17 -0
- package/dist/cli/dashboard.js +3932 -0
- package/dist/cli/dashboard.js.map +1 -0
- package/dist/cli/index.cjs +3853 -484
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +3810 -457
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/index.cjs +411 -99
- package/dist/daemon/index.cjs.map +1 -1
- package/dist/daemon/index.js +423 -103
- package/dist/daemon/index.js.map +1 -1
- package/dist/hooks/post-tool-result.cjs +115 -266
- package/dist/hooks/post-tool-result.cjs.map +1 -1
- package/dist/hooks/post-tool-result.js +115 -266
- package/dist/hooks/post-tool-result.js.map +1 -1
- package/dist/hooks/pre-prompt.cjs +197 -268
- package/dist/hooks/pre-prompt.cjs.map +1 -1
- package/dist/hooks/pre-prompt.js +182 -268
- package/dist/hooks/pre-prompt.js.map +1 -1
- package/dist/hooks/stop-docs-refresh.cjs +123 -0
- package/dist/hooks/stop-docs-refresh.cjs.map +1 -0
- package/dist/hooks/stop-docs-refresh.d.cts +1 -0
- package/dist/hooks/stop-docs-refresh.d.ts +1 -0
- package/dist/hooks/stop-docs-refresh.js +126 -0
- package/dist/hooks/stop-docs-refresh.js.map +1 -0
- package/dist/index.cjs +1754 -337
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +539 -40
- package/dist/index.d.ts +539 -40
- package/dist/index.js +1737 -329
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.cjs +304 -71
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +308 -71
- package/dist/mcp/index.js.map +1 -1
- package/package.json +10 -3
|
@@ -6,10 +6,15 @@ import { homedir } from "os";
|
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
|
|
8
8
|
// src/utils/tokenizer.ts
|
|
9
|
+
import { encode } from "gpt-tokenizer";
|
|
10
|
+
var usePrecise = false;
|
|
9
11
|
function estimateTokens(text) {
|
|
10
12
|
if (!text || text.length === 0) {
|
|
11
13
|
return 0;
|
|
12
14
|
}
|
|
15
|
+
if (usePrecise) {
|
|
16
|
+
return encode(text).length;
|
|
17
|
+
}
|
|
13
18
|
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
14
19
|
const wordCount = words.length;
|
|
15
20
|
const charCount = text.length;
|
|
@@ -21,283 +26,85 @@ function estimateTokens(text) {
|
|
|
21
26
|
// src/hooks/post-tool-result.ts
|
|
22
27
|
var DEBUG = process.env["SPARN_DEBUG"] === "true";
|
|
23
28
|
var LOG_FILE = process.env["SPARN_LOG_FILE"] || join(homedir(), ".sparn-hook.log");
|
|
29
|
+
var SUMMARY_THRESHOLD = 3e3;
|
|
24
30
|
function log(message) {
|
|
25
31
|
if (DEBUG) {
|
|
26
32
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
27
|
-
appendFileSync(LOG_FILE, `[${timestamp}] [post-tool
|
|
33
|
+
appendFileSync(LOG_FILE, `[${timestamp}] [post-tool] ${message}
|
|
28
34
|
`);
|
|
29
35
|
}
|
|
30
36
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
var COMPRESSION_THRESHOLD = 5e3;
|
|
36
|
-
var TOOL_PATTERNS = {
|
|
37
|
-
fileRead: /<file_path>(.*?)<\/file_path>[\s\S]*?<content>([\s\S]*?)<\/content>/,
|
|
38
|
-
grepResult: /<pattern>(.*?)<\/pattern>[\s\S]*?<matches>([\s\S]*?)<\/matches>/,
|
|
39
|
-
gitDiff: /^diff --git/m,
|
|
40
|
-
buildOutput: /(error|warning|failed|failure)/i,
|
|
41
|
-
npmInstall: /^(npm|pnpm|yarn) (install|add|i)/m,
|
|
42
|
-
dockerLogs: /^\[?\d{4}-\d{2}-\d{2}/m,
|
|
43
|
-
testResults: /(PASS|FAIL|SKIP).*?\.test\./i,
|
|
44
|
-
typescriptErrors: /^.*\(\d+,\d+\): error TS\d+:/m,
|
|
45
|
-
webpackBuild: /webpack \d+\.\d+\.\d+/i
|
|
46
|
-
};
|
|
47
|
-
function compressFileRead(content, maxLines = 100) {
|
|
48
|
-
const lines = content.split("\n");
|
|
49
|
-
if (lines.length <= maxLines * 2) {
|
|
50
|
-
return content;
|
|
37
|
+
function extractText(response) {
|
|
38
|
+
if (typeof response === "string") return response;
|
|
39
|
+
if (response && typeof response === "object") {
|
|
40
|
+
return JSON.stringify(response);
|
|
51
41
|
}
|
|
52
|
-
|
|
53
|
-
const tail = lines.slice(-maxLines);
|
|
54
|
-
const omitted = lines.length - maxLines * 2;
|
|
55
|
-
return [...head, "", `... [${omitted} lines omitted] ...`, "", ...tail].join("\n");
|
|
42
|
+
return String(response ?? "");
|
|
56
43
|
}
|
|
57
|
-
function
|
|
58
|
-
const lines =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (!fileMatches.has(file)) {
|
|
67
|
-
fileMatches.set(file, []);
|
|
68
|
-
}
|
|
69
|
-
fileMatches.get(file)?.push(` Line ${lineNum}: ${text.trim()}`);
|
|
44
|
+
function summarizeBash(text, command) {
|
|
45
|
+
const lines = text.split("\n");
|
|
46
|
+
if (/\d+ (pass|fail|skip)/i.test(text) || /Tests?:/i.test(text)) {
|
|
47
|
+
const resultLines = lines.filter(
|
|
48
|
+
(l) => /(pass|fail|skip|error|Tests?:|Test Suites?:)/i.test(l) || /^\s*(PASS|FAIL)\s/.test(l)
|
|
49
|
+
);
|
|
50
|
+
if (resultLines.length > 0) {
|
|
51
|
+
return `[sparn] Test output summary (${lines.length} lines):
|
|
52
|
+
${resultLines.slice(0, 15).join("\n")}`;
|
|
70
53
|
}
|
|
71
54
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
} else {
|
|
78
|
-
compressed.push(...matches.slice(0, maxMatchesPerFile));
|
|
79
|
-
compressed.push(` ... and ${matches.length - maxMatchesPerFile} more matches`);
|
|
55
|
+
if (/(error|warning|failed)/i.test(text)) {
|
|
56
|
+
const errorLines = lines.filter((l) => /(error|warning|failed|fatal)/i.test(l));
|
|
57
|
+
if (errorLines.length > 0) {
|
|
58
|
+
return `[sparn] Build output summary (${errorLines.length} errors/warnings from ${lines.length} lines):
|
|
59
|
+
${errorLines.slice(0, 10).join("\n")}`;
|
|
80
60
|
}
|
|
81
|
-
compressed.push("");
|
|
82
61
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
let currentFile = "";
|
|
89
|
-
for (const line of lines) {
|
|
90
|
-
if (line.startsWith("diff --git")) {
|
|
91
|
-
const match = line.match(/diff --git a\/(.*?) b\/(.*)/);
|
|
92
|
-
if (match) {
|
|
93
|
-
currentFile = match[2] || "";
|
|
94
|
-
files.set(currentFile, { added: 0, removed: 0 });
|
|
95
|
-
}
|
|
96
|
-
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
97
|
-
const stats = files.get(currentFile);
|
|
98
|
-
if (stats) stats.added++;
|
|
99
|
-
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
100
|
-
const stats = files.get(currentFile);
|
|
101
|
-
if (stats) stats.removed++;
|
|
62
|
+
if (/^diff --git/m.test(text)) {
|
|
63
|
+
const files = [];
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
const match = line.match(/^diff --git a\/(.*?) b\/(.*)/);
|
|
66
|
+
if (match?.[2]) files.push(match[2]);
|
|
102
67
|
}
|
|
68
|
+
return `[sparn] Git diff: ${files.length} files changed: ${files.join(", ")}`;
|
|
103
69
|
}
|
|
104
|
-
|
|
105
|
-
for (const [file, stats] of files.entries()) {
|
|
106
|
-
summary.push(` ${file}: +${stats.added} -${stats.removed}`);
|
|
107
|
-
}
|
|
108
|
-
return summary.join("\n");
|
|
70
|
+
return `[sparn] Command \`${command}\` produced ${lines.length} lines of output. First 3: ${lines.slice(0, 3).join(" | ")}`;
|
|
109
71
|
}
|
|
110
|
-
function
|
|
111
|
-
const lines =
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return ["Build errors/warnings:", ...important].join("\n");
|
|
122
|
-
}
|
|
123
|
-
function compressNpmInstall(content) {
|
|
124
|
-
const lines = content.split("\n");
|
|
125
|
-
const summary = [];
|
|
126
|
-
const warnings = [];
|
|
127
|
-
const errors = [];
|
|
128
|
-
for (const line of lines) {
|
|
129
|
-
if (/added \d+ packages?/i.test(line)) {
|
|
130
|
-
summary.push(line.trim());
|
|
131
|
-
}
|
|
132
|
-
if (/warn/i.test(line)) {
|
|
133
|
-
warnings.push(line.trim());
|
|
134
|
-
}
|
|
135
|
-
if (/error/i.test(line)) {
|
|
136
|
-
errors.push(line.trim());
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
if (errors.length > 0) {
|
|
140
|
-
return ["Package installation errors:", ...errors.slice(0, 5)].join("\n");
|
|
141
|
-
}
|
|
142
|
-
if (warnings.length > 0) {
|
|
143
|
-
return [
|
|
144
|
-
"Package installation completed with warnings:",
|
|
145
|
-
...warnings.slice(0, 3),
|
|
146
|
-
warnings.length > 3 ? `... and ${warnings.length - 3} more warnings` : ""
|
|
147
|
-
].filter(Boolean).join("\n");
|
|
148
|
-
}
|
|
149
|
-
return summary.length > 0 ? summary.join("\n") : "Package installation completed successfully";
|
|
150
|
-
}
|
|
151
|
-
function compressDockerLogs(content) {
|
|
152
|
-
const lines = content.split("\n");
|
|
153
|
-
const logMap = /* @__PURE__ */ new Map();
|
|
154
|
-
for (const line of lines) {
|
|
155
|
-
const normalized = line.replace(/^\[?\d{4}-\d{2}-\d{2}.*?\]\s*/, "").trim();
|
|
156
|
-
if (normalized) {
|
|
157
|
-
logMap.set(normalized, (logMap.get(normalized) || 0) + 1);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const summary = ["Docker logs (deduplicated):"];
|
|
161
|
-
for (const [log2, count] of Array.from(logMap.entries()).slice(0, 20)) {
|
|
162
|
-
if (count > 1) {
|
|
163
|
-
summary.push(` [${count}x] ${log2}`);
|
|
164
|
-
} else {
|
|
165
|
-
summary.push(` ${log2}`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (logMap.size > 20) {
|
|
169
|
-
summary.push(` ... and ${logMap.size - 20} more unique log lines`);
|
|
72
|
+
function summarizeFileRead(text, filePath) {
|
|
73
|
+
const lines = text.split("\n");
|
|
74
|
+
const tokens = estimateTokens(text);
|
|
75
|
+
const exports = lines.filter((l) => /^export\s/.test(l.trim()));
|
|
76
|
+
const functions = lines.filter((l) => /function\s+\w+/.test(l));
|
|
77
|
+
const classes = lines.filter((l) => /class\s+\w+/.test(l));
|
|
78
|
+
const parts = [`[sparn] File ${filePath}: ${lines.length} lines, ~${tokens} tokens.`];
|
|
79
|
+
if (exports.length > 0) {
|
|
80
|
+
parts.push(
|
|
81
|
+
`Exports: ${exports.slice(0, 5).map((e) => e.trim().substring(0, 60)).join("; ")}`
|
|
82
|
+
);
|
|
170
83
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
let passed = 0;
|
|
176
|
-
let failed = 0;
|
|
177
|
-
let skipped = 0;
|
|
178
|
-
const failures = [];
|
|
179
|
-
for (const line of lines) {
|
|
180
|
-
if (/PASS/i.test(line)) passed++;
|
|
181
|
-
if (/FAIL/i.test(line)) {
|
|
182
|
-
failed++;
|
|
183
|
-
failures.push(line.trim());
|
|
184
|
-
}
|
|
185
|
-
if (/SKIP/i.test(line)) skipped++;
|
|
84
|
+
if (functions.length > 0) {
|
|
85
|
+
parts.push(
|
|
86
|
+
`Functions: ${functions.slice(0, 5).map((f) => f.trim().substring(0, 40)).join(", ")}`
|
|
87
|
+
);
|
|
186
88
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
summary.push("", "Failed tests:");
|
|
190
|
-
summary.push(...failures.slice(0, 10));
|
|
191
|
-
if (failures.length > 10) {
|
|
192
|
-
summary.push(`... and ${failures.length - 10} more failures`);
|
|
193
|
-
}
|
|
89
|
+
if (classes.length > 0) {
|
|
90
|
+
parts.push(`Classes: ${classes.map((c) => c.trim().substring(0, 40)).join(", ")}`);
|
|
194
91
|
}
|
|
195
|
-
return
|
|
92
|
+
return parts.join(" ");
|
|
196
93
|
}
|
|
197
|
-
function
|
|
198
|
-
const lines =
|
|
199
|
-
const
|
|
94
|
+
function summarizeSearch(text, pattern) {
|
|
95
|
+
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
96
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
200
97
|
for (const line of lines) {
|
|
201
|
-
const match = line.match(/^(.*?)
|
|
202
|
-
if (match) {
|
|
203
|
-
|
|
204
|
-
const errorCode = match[2] || "TS0000";
|
|
205
|
-
const key = `${file}:${errorCode}`;
|
|
206
|
-
if (!errorMap.has(key)) {
|
|
207
|
-
errorMap.set(key, []);
|
|
208
|
-
}
|
|
209
|
-
errorMap.get(key)?.push(line);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const summary = ["TypeScript Errors (grouped by file):"];
|
|
213
|
-
for (const [key, errors] of errorMap.entries()) {
|
|
214
|
-
summary.push(` ${key} (${errors.length} errors)`);
|
|
215
|
-
summary.push(` ${errors[0]}`);
|
|
216
|
-
}
|
|
217
|
-
return summary.join("\n");
|
|
218
|
-
}
|
|
219
|
-
function compressToolResult(input) {
|
|
220
|
-
const tokens = estimateTokens(input);
|
|
221
|
-
log(`Tool result tokens: ${tokens}`);
|
|
222
|
-
if (tokens < COMPRESSION_THRESHOLD) {
|
|
223
|
-
log(`Under compression threshold (${tokens} < ${COMPRESSION_THRESHOLD}), passing through`);
|
|
224
|
-
return input;
|
|
225
|
-
}
|
|
226
|
-
log(`Over threshold! Compressing ${tokens} token tool result`);
|
|
227
|
-
if (TOOL_PATTERNS.fileRead.test(input)) {
|
|
228
|
-
log("Detected: File read");
|
|
229
|
-
const match = input.match(TOOL_PATTERNS.fileRead);
|
|
230
|
-
if (match?.[2]) {
|
|
231
|
-
const content = match[2];
|
|
232
|
-
const compressed = compressFileRead(content);
|
|
233
|
-
const afterTokens = estimateTokens(compressed);
|
|
234
|
-
log(`Compressed file read: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
235
|
-
return input.replace(content, compressed);
|
|
98
|
+
const match = line.match(/^(.*?):\d+:/);
|
|
99
|
+
if (match?.[1]) {
|
|
100
|
+
fileMap.set(match[1], (fileMap.get(match[1]) || 0) + 1);
|
|
236
101
|
}
|
|
237
102
|
}
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (match?.[2]) {
|
|
242
|
-
const matches = match[2];
|
|
243
|
-
const compressed = compressGrepResults(matches);
|
|
244
|
-
const afterTokens = estimateTokens(compressed);
|
|
245
|
-
log(`Compressed grep: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
246
|
-
return input.replace(matches, compressed);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
if (TOOL_PATTERNS.gitDiff.test(input)) {
|
|
250
|
-
log("Detected: Git diff");
|
|
251
|
-
const compressed = compressGitDiff(input);
|
|
252
|
-
const afterTokens = estimateTokens(compressed);
|
|
253
|
-
log(`Compressed git diff: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
254
|
-
return compressed;
|
|
255
|
-
}
|
|
256
|
-
if (TOOL_PATTERNS.buildOutput.test(input)) {
|
|
257
|
-
log("Detected: Build output");
|
|
258
|
-
const compressed = compressBuildOutput(input);
|
|
259
|
-
const afterTokens = estimateTokens(compressed);
|
|
260
|
-
log(`Compressed build: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
261
|
-
return compressed;
|
|
262
|
-
}
|
|
263
|
-
if (TOOL_PATTERNS.npmInstall.test(input)) {
|
|
264
|
-
log("Detected: NPM install");
|
|
265
|
-
const compressed = compressNpmInstall(input);
|
|
266
|
-
const afterTokens = estimateTokens(compressed);
|
|
267
|
-
log(`Compressed npm: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
268
|
-
return compressed;
|
|
103
|
+
if (fileMap.size > 0) {
|
|
104
|
+
const summary = Array.from(fileMap.entries()).slice(0, 5).map(([f, c]) => `${f} (${c})`).join(", ");
|
|
105
|
+
return `[sparn] Search for "${pattern}": ${lines.length} matches across ${fileMap.size} files. Top files: ${summary}`;
|
|
269
106
|
}
|
|
270
|
-
|
|
271
|
-
log("Detected: Docker logs");
|
|
272
|
-
const compressed = compressDockerLogs(input);
|
|
273
|
-
const afterTokens = estimateTokens(compressed);
|
|
274
|
-
log(`Compressed docker: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
275
|
-
return compressed;
|
|
276
|
-
}
|
|
277
|
-
if (TOOL_PATTERNS.testResults.test(input)) {
|
|
278
|
-
log("Detected: Test results");
|
|
279
|
-
const compressed = compressTestResults(input);
|
|
280
|
-
const afterTokens = estimateTokens(compressed);
|
|
281
|
-
log(`Compressed tests: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
282
|
-
return compressed;
|
|
283
|
-
}
|
|
284
|
-
if (TOOL_PATTERNS.typescriptErrors.test(input)) {
|
|
285
|
-
log("Detected: TypeScript errors");
|
|
286
|
-
const compressed = compressTypescriptErrors(input);
|
|
287
|
-
const afterTokens = estimateTokens(compressed);
|
|
288
|
-
log(`Compressed TS errors: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
289
|
-
return compressed;
|
|
290
|
-
}
|
|
291
|
-
log("No pattern matched, applying generic compression");
|
|
292
|
-
const lines = input.split("\n");
|
|
293
|
-
if (lines.length > 200) {
|
|
294
|
-
const compressed = compressFileRead(input, 100);
|
|
295
|
-
const afterTokens = estimateTokens(compressed);
|
|
296
|
-
log(`Generic compression: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
297
|
-
return compressed;
|
|
298
|
-
}
|
|
299
|
-
log("No compression needed");
|
|
300
|
-
return input;
|
|
107
|
+
return `[sparn] Search for "${pattern}": ${lines.length} result lines`;
|
|
301
108
|
}
|
|
302
109
|
async function main() {
|
|
303
110
|
try {
|
|
@@ -305,19 +112,61 @@ async function main() {
|
|
|
305
112
|
for await (const chunk of process.stdin) {
|
|
306
113
|
chunks.push(chunk);
|
|
307
114
|
}
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
115
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
116
|
+
let input;
|
|
117
|
+
try {
|
|
118
|
+
input = JSON.parse(raw);
|
|
119
|
+
} catch {
|
|
120
|
+
log("Failed to parse JSON input");
|
|
121
|
+
process.exit(0);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const toolName = input.tool_name ?? "unknown";
|
|
125
|
+
const text = extractText(input.tool_response);
|
|
126
|
+
const tokens = estimateTokens(text);
|
|
127
|
+
log(`Tool: ${toolName}, response tokens: ~${tokens}`);
|
|
128
|
+
if (tokens < SUMMARY_THRESHOLD) {
|
|
129
|
+
log("Under threshold, no summary needed");
|
|
130
|
+
process.exit(0);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
let summary = "";
|
|
134
|
+
switch (toolName) {
|
|
135
|
+
case "Bash": {
|
|
136
|
+
const command = String(input.tool_input?.["command"] ?? "");
|
|
137
|
+
summary = summarizeBash(text, command);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case "Read": {
|
|
141
|
+
const filePath = String(input.tool_input?.["file_path"] ?? "");
|
|
142
|
+
summary = summarizeFileRead(text, filePath);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case "Grep": {
|
|
146
|
+
const pattern = String(input.tool_input?.["pattern"] ?? "");
|
|
147
|
+
summary = summarizeSearch(text, pattern);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
default: {
|
|
151
|
+
const lines = text.split("\n");
|
|
152
|
+
summary = `[sparn] ${toolName} output: ${lines.length} lines, ~${tokens} tokens`;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
318
155
|
}
|
|
319
|
-
|
|
320
|
-
|
|
156
|
+
if (summary) {
|
|
157
|
+
log(`Summary: ${summary.substring(0, 100)}`);
|
|
158
|
+
const output = JSON.stringify({
|
|
159
|
+
hookSpecificOutput: {
|
|
160
|
+
hookEventName: "PostToolUse",
|
|
161
|
+
additionalContext: summary
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
process.stdout.write(output);
|
|
165
|
+
}
|
|
166
|
+
process.exit(0);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
log(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
169
|
+
process.exit(0);
|
|
321
170
|
}
|
|
322
171
|
}
|
|
323
172
|
main();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/post-tool-result.ts","../../src/utils/tokenizer.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Post-Tool-Result Hook - Claude Code hook for compressing verbose tool output\n *\n * Compresses large tool results using type-specific strategies:\n * - File reads: Truncate long files, show first/last N lines\n * - Grep results: Group by file, show match count + samples\n * - Git diffs: Summarize file changes, show stats\n * - Build output: Extract errors/warnings only\n *\n * CRITICAL: Always exits 0 (never disrupts Claude Code).\n * Falls through unmodified on error or if already small.\n */\n\nimport { appendFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { estimateTokens } from '../utils/tokenizer.js';\n\n// Debug logging (optional, set via env var)\nconst DEBUG = process.env['SPARN_DEBUG'] === 'true';\nconst LOG_FILE = process.env['SPARN_LOG_FILE'] || join(homedir(), '.sparn-hook.log');\n\nfunction log(message: string): void {\n if (DEBUG) {\n const timestamp = new Date().toISOString();\n appendFileSync(LOG_FILE, `[${timestamp}] [post-tool-result] ${message}\\n`);\n }\n}\n\n// Exit 0 wrapper for all errors\nfunction exitSuccess(output: string): void {\n process.stdout.write(output);\n process.exit(0);\n}\n\n// Compression threshold (only compress if over this many tokens)\nconst COMPRESSION_THRESHOLD = 5000;\n\n// Tool result patterns\nconst TOOL_PATTERNS = {\n fileRead: /<file_path>(.*?)<\\/file_path>[\\s\\S]*?<content>([\\s\\S]*?)<\\/content>/,\n grepResult: /<pattern>(.*?)<\\/pattern>[\\s\\S]*?<matches>([\\s\\S]*?)<\\/matches>/,\n gitDiff: /^diff --git/m,\n buildOutput: /(error|warning|failed|failure)/i,\n npmInstall: /^(npm|pnpm|yarn) (install|add|i)/m,\n dockerLogs: /^\\[?\\d{4}-\\d{2}-\\d{2}/m,\n testResults: /(PASS|FAIL|SKIP).*?\\.test\\./i,\n typescriptErrors: /^.*\\(\\d+,\\d+\\): error TS\\d+:/m,\n webpackBuild: /webpack \\d+\\.\\d+\\.\\d+/i,\n};\n\n/**\n * Compress file read results\n */\nfunction compressFileRead(content: string, maxLines = 100): string {\n const lines = content.split('\\n');\n\n if (lines.length <= maxLines * 2) {\n return content; // Already small enough\n }\n\n const head = lines.slice(0, maxLines);\n const tail = lines.slice(-maxLines);\n const omitted = lines.length - maxLines * 2;\n\n return [...head, '', `... [${omitted} lines omitted] ...`, '', ...tail].join('\\n');\n}\n\n/**\n * Compress grep results\n */\nfunction compressGrepResults(content: string, maxMatchesPerFile = 5): string {\n const lines = content.split('\\n');\n const fileMatches = new Map<string, string[]>();\n\n // Group matches by file\n for (const line of lines) {\n const match = line.match(/^(.*?):(\\d+):(.*)/);\n if (match?.[1] && match[2] && match[3]) {\n const file = match[1];\n const lineNum = match[2];\n const text = match[3];\n if (!fileMatches.has(file)) {\n fileMatches.set(file, []);\n }\n fileMatches.get(file)?.push(` Line ${lineNum}: ${text.trim()}`);\n }\n }\n\n // Build compressed output\n const compressed: string[] = [];\n\n for (const [file, matches] of fileMatches.entries()) {\n compressed.push(`${file} (${matches.length} matches):`);\n\n if (matches.length <= maxMatchesPerFile) {\n compressed.push(...matches);\n } else {\n compressed.push(...matches.slice(0, maxMatchesPerFile));\n compressed.push(` ... and ${matches.length - maxMatchesPerFile} more matches`);\n }\n\n compressed.push('');\n }\n\n return compressed.join('\\n');\n}\n\n/**\n * Compress git diff results\n */\nfunction compressGitDiff(content: string): string {\n const lines = content.split('\\n');\n const files = new Map<string, { added: number; removed: number }>();\n let currentFile = '';\n\n for (const line of lines) {\n if (line.startsWith('diff --git')) {\n const match = line.match(/diff --git a\\/(.*?) b\\/(.*)/);\n if (match) {\n currentFile = match[2] || '';\n files.set(currentFile, { added: 0, removed: 0 });\n }\n } else if (line.startsWith('+') && !line.startsWith('+++')) {\n const stats = files.get(currentFile);\n if (stats) stats.added++;\n } else if (line.startsWith('-') && !line.startsWith('---')) {\n const stats = files.get(currentFile);\n if (stats) stats.removed++;\n }\n }\n\n // Build summary\n const summary: string[] = ['Git diff summary:'];\n\n for (const [file, stats] of files.entries()) {\n summary.push(` ${file}: +${stats.added} -${stats.removed}`);\n }\n\n return summary.join('\\n');\n}\n\n/**\n * Compress build output (extract errors/warnings only)\n */\nfunction compressBuildOutput(content: string): string {\n const lines = content.split('\\n');\n const important: string[] = [];\n\n for (const line of lines) {\n if (/(error|warning|failed|failure|fatal)/i.test(line)) {\n important.push(line);\n }\n }\n\n if (important.length === 0) {\n return 'Build output: No errors or warnings found';\n }\n\n return ['Build errors/warnings:', ...important].join('\\n');\n}\n\n/**\n * Compress npm/pnpm install output\n */\nfunction compressNpmInstall(content: string): string {\n const lines = content.split('\\n');\n const summary: string[] = [];\n\n // Extract package count and warnings/errors\n const warnings: string[] = [];\n const errors: string[] = [];\n\n for (const line of lines) {\n if (/added \\d+ packages?/i.test(line)) {\n summary.push(line.trim());\n }\n if (/warn/i.test(line)) {\n warnings.push(line.trim());\n }\n if (/error/i.test(line)) {\n errors.push(line.trim());\n }\n }\n\n if (errors.length > 0) {\n return ['Package installation errors:', ...errors.slice(0, 5)].join('\\n');\n }\n\n if (warnings.length > 0) {\n return [\n 'Package installation completed with warnings:',\n ...warnings.slice(0, 3),\n warnings.length > 3 ? `... and ${warnings.length - 3} more warnings` : '',\n ]\n .filter(Boolean)\n .join('\\n');\n }\n\n return summary.length > 0 ? summary.join('\\n') : 'Package installation completed successfully';\n}\n\n/**\n * Compress Docker logs\n */\nfunction compressDockerLogs(content: string): string {\n const lines = content.split('\\n');\n const logMap = new Map<string, number>();\n\n // Deduplicate and count repeated lines\n for (const line of lines) {\n // Strip timestamps for deduplication\n const normalized = line.replace(/^\\[?\\d{4}-\\d{2}-\\d{2}.*?\\]\\s*/, '').trim();\n if (normalized) {\n logMap.set(normalized, (logMap.get(normalized) || 0) + 1);\n }\n }\n\n const summary: string[] = ['Docker logs (deduplicated):'];\n\n for (const [log, count] of Array.from(logMap.entries()).slice(0, 20)) {\n if (count > 1) {\n summary.push(` [${count}x] ${log}`);\n } else {\n summary.push(` ${log}`);\n }\n }\n\n if (logMap.size > 20) {\n summary.push(` ... and ${logMap.size - 20} more unique log lines`);\n }\n\n return summary.join('\\n');\n}\n\n/**\n * Compress test results\n */\nfunction compressTestResults(content: string): string {\n const lines = content.split('\\n');\n let passed = 0;\n let failed = 0;\n let skipped = 0;\n const failures: string[] = [];\n\n for (const line of lines) {\n if (/PASS/i.test(line)) passed++;\n if (/FAIL/i.test(line)) {\n failed++;\n failures.push(line.trim());\n }\n if (/SKIP/i.test(line)) skipped++;\n }\n\n const summary = [`Test Results: ${passed} passed, ${failed} failed, ${skipped} skipped`];\n\n if (failures.length > 0) {\n summary.push('', 'Failed tests:');\n summary.push(...failures.slice(0, 10));\n if (failures.length > 10) {\n summary.push(`... and ${failures.length - 10} more failures`);\n }\n }\n\n return summary.join('\\n');\n}\n\n/**\n * Compress TypeScript errors\n */\nfunction compressTypescriptErrors(content: string): string {\n const lines = content.split('\\n');\n const errorMap = new Map<string, string[]>();\n\n for (const line of lines) {\n const match = line.match(/^(.*?)\\(\\d+,\\d+\\): error (TS\\d+):/);\n if (match) {\n const file = match[1] || 'unknown';\n const errorCode = match[2] || 'TS0000';\n const key = `${file}:${errorCode}`;\n\n if (!errorMap.has(key)) {\n errorMap.set(key, []);\n }\n errorMap.get(key)?.push(line);\n }\n }\n\n const summary = ['TypeScript Errors (grouped by file):'];\n\n for (const [key, errors] of errorMap.entries()) {\n summary.push(` ${key} (${errors.length} errors)`);\n summary.push(` ${errors[0]}`);\n }\n\n return summary.join('\\n');\n}\n\n/**\n * Main compression logic\n */\nfunction compressToolResult(input: string): string {\n const tokens = estimateTokens(input);\n log(`Tool result tokens: ${tokens}`);\n\n // Only compress if over threshold\n if (tokens < COMPRESSION_THRESHOLD) {\n log(`Under compression threshold (${tokens} < ${COMPRESSION_THRESHOLD}), passing through`);\n return input;\n }\n\n log(`Over threshold! Compressing ${tokens} token tool result`);\n\n // Detect tool result type and compress accordingly\n if (TOOL_PATTERNS.fileRead.test(input)) {\n log('Detected: File read');\n const match = input.match(TOOL_PATTERNS.fileRead);\n if (match?.[2]) {\n const content = match[2];\n const compressed = compressFileRead(content);\n const afterTokens = estimateTokens(compressed);\n log(`Compressed file read: ${tokens} → ${afterTokens} tokens`);\n return input.replace(content, compressed);\n }\n }\n\n if (TOOL_PATTERNS.grepResult.test(input)) {\n log('Detected: Grep results');\n const match = input.match(TOOL_PATTERNS.grepResult);\n if (match?.[2]) {\n const matches = match[2];\n const compressed = compressGrepResults(matches);\n const afterTokens = estimateTokens(compressed);\n log(`Compressed grep: ${tokens} → ${afterTokens} tokens`);\n return input.replace(matches, compressed);\n }\n }\n\n if (TOOL_PATTERNS.gitDiff.test(input)) {\n log('Detected: Git diff');\n const compressed = compressGitDiff(input);\n const afterTokens = estimateTokens(compressed);\n log(`Compressed git diff: ${tokens} → ${afterTokens} tokens`);\n return compressed;\n }\n\n if (TOOL_PATTERNS.buildOutput.test(input)) {\n log('Detected: Build output');\n const compressed = compressBuildOutput(input);\n const afterTokens = estimateTokens(compressed);\n log(`Compressed build: ${tokens} → ${afterTokens} tokens`);\n return compressed;\n }\n\n if (TOOL_PATTERNS.npmInstall.test(input)) {\n log('Detected: NPM install');\n const compressed = compressNpmInstall(input);\n const afterTokens = estimateTokens(compressed);\n log(`Compressed npm: ${tokens} → ${afterTokens} tokens`);\n return compressed;\n }\n\n if (TOOL_PATTERNS.dockerLogs.test(input)) {\n log('Detected: Docker logs');\n const compressed = compressDockerLogs(input);\n const afterTokens = estimateTokens(compressed);\n log(`Compressed docker: ${tokens} → ${afterTokens} tokens`);\n return compressed;\n }\n\n if (TOOL_PATTERNS.testResults.test(input)) {\n log('Detected: Test results');\n const compressed = compressTestResults(input);\n const afterTokens = estimateTokens(compressed);\n log(`Compressed tests: ${tokens} → ${afterTokens} tokens`);\n return compressed;\n }\n\n if (TOOL_PATTERNS.typescriptErrors.test(input)) {\n log('Detected: TypeScript errors');\n const compressed = compressTypescriptErrors(input);\n const afterTokens = estimateTokens(compressed);\n log(`Compressed TS errors: ${tokens} → ${afterTokens} tokens`);\n return compressed;\n }\n\n // Unknown type or no compression pattern matched\n log('No pattern matched, applying generic compression');\n // Apply generic truncation as fallback\n const lines = input.split('\\n');\n if (lines.length > 200) {\n const compressed = compressFileRead(input, 100);\n const afterTokens = estimateTokens(compressed);\n log(`Generic compression: ${tokens} → ${afterTokens} tokens`);\n return compressed;\n }\n\n log('No compression needed');\n return input;\n}\n\n// Main hook logic\nasync function main(): Promise<void> {\n try {\n // Read stdin (tool result)\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const input = Buffer.concat(chunks).toString('utf-8');\n\n // Compress if needed\n const output = compressToolResult(input);\n\n exitSuccess(output);\n } catch (error) {\n // On any error, pass through original input\n log(\n `Error in post-tool-result hook: ${error instanceof Error ? error.message : String(error)}`,\n );\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const input = Buffer.concat(chunks).toString('utf-8');\n exitSuccess(input);\n }\n}\n\n// Run hook\nmain();\n","/**\n * Token estimation utilities.\n * Uses whitespace heuristic (~90% accuracy vs GPT tokenizer).\n */\n\n/**\n * Estimate token count for text using heuristic.\n *\n * Approximation: 1 token ≈ 4 chars or 0.75 words\n * Provides ~90% accuracy compared to GPT tokenizer, sufficient for optimization heuristics.\n *\n * @param text - Text to count\n * @returns Estimated token count\n *\n * @example\n * ```typescript\n * const tokens = estimateTokens('Hello world');\n * console.log(tokens); // ~2\n * ```\n */\nexport function estimateTokens(text: string): number {\n if (!text || text.length === 0) {\n return 0;\n }\n\n // Split on whitespace to get words\n const words = text.split(/\\s+/).filter((w) => w.length > 0);\n const wordCount = words.length;\n\n // Character-based estimate\n const charCount = text.length;\n const charEstimate = Math.ceil(charCount / 4);\n\n // Word-based estimate\n const wordEstimate = Math.ceil(wordCount * 0.75);\n\n // Return the maximum of both estimates (more conservative)\n return Math.max(wordEstimate, charEstimate);\n}\n"],"mappings":"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","names":["log"]}
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/post-tool-result.ts","../../src/utils/tokenizer.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * PostToolUse Hook - Compresses verbose tool output\n *\n * After tools like Bash, Read, Grep execute, this hook checks if\n * the output is very large and adds a compressed summary as\n * additionalContext so Claude can quickly reference key information.\n *\n * CRITICAL: Always exits 0 (never disrupts Claude Code).\n */\n\nimport { appendFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { estimateTokens } from '../utils/tokenizer.js';\n\nconst DEBUG = process.env['SPARN_DEBUG'] === 'true';\nconst LOG_FILE = process.env['SPARN_LOG_FILE'] || join(homedir(), '.sparn-hook.log');\n\n// Only add summaries for outputs over this many estimated tokens\nconst SUMMARY_THRESHOLD = 3000;\n\nfunction log(message: string): void {\n if (DEBUG) {\n const timestamp = new Date().toISOString();\n appendFileSync(LOG_FILE, `[${timestamp}] [post-tool] ${message}\\n`);\n }\n}\n\ninterface HookInput {\n session_id?: string;\n hook_event_name?: string;\n tool_name?: string;\n tool_use_id?: string;\n tool_input?: Record<string, unknown>;\n tool_response?: unknown;\n}\n\nfunction extractText(response: unknown): string {\n if (typeof response === 'string') return response;\n if (response && typeof response === 'object') {\n return JSON.stringify(response);\n }\n return String(response ?? '');\n}\n\n/**\n * Summarize large bash output\n */\nfunction summarizeBash(text: string, command: string): string {\n const lines = text.split('\\n');\n\n // Check for test results\n if (/\\d+ (pass|fail|skip)/i.test(text) || /Tests?:/i.test(text)) {\n const resultLines = lines.filter(\n (l) => /(pass|fail|skip|error|Tests?:|Test Suites?:)/i.test(l) || /^\\s*(PASS|FAIL)\\s/.test(l),\n );\n if (resultLines.length > 0) {\n return `[sparn] Test output summary (${lines.length} lines):\\n${resultLines.slice(0, 15).join('\\n')}`;\n }\n }\n\n // Check for build errors\n if (/(error|warning|failed)/i.test(text)) {\n const errorLines = lines.filter((l) => /(error|warning|failed|fatal)/i.test(l));\n if (errorLines.length > 0) {\n return `[sparn] Build output summary (${errorLines.length} errors/warnings from ${lines.length} lines):\\n${errorLines.slice(0, 10).join('\\n')}`;\n }\n }\n\n // Check for git diff\n if (/^diff --git/m.test(text)) {\n const files: string[] = [];\n for (const line of lines) {\n const match = line.match(/^diff --git a\\/(.*?) b\\/(.*)/);\n if (match?.[2]) files.push(match[2]);\n }\n return `[sparn] Git diff: ${files.length} files changed: ${files.join(', ')}`;\n }\n\n // Generic: show line count and first/last few lines\n return `[sparn] Command \\`${command}\\` produced ${lines.length} lines of output. First 3: ${lines.slice(0, 3).join(' | ')}`;\n}\n\n/**\n * Summarize large file read\n */\nfunction summarizeFileRead(text: string, filePath: string): string {\n const lines = text.split('\\n');\n const tokens = estimateTokens(text);\n\n // Find key structures\n const exports = lines.filter((l) => /^export\\s/.test(l.trim()));\n const functions = lines.filter((l) => /function\\s+\\w+/.test(l));\n const classes = lines.filter((l) => /class\\s+\\w+/.test(l));\n\n const parts = [`[sparn] File ${filePath}: ${lines.length} lines, ~${tokens} tokens.`];\n\n if (exports.length > 0) {\n parts.push(\n `Exports: ${exports\n .slice(0, 5)\n .map((e) => e.trim().substring(0, 60))\n .join('; ')}`,\n );\n }\n if (functions.length > 0) {\n parts.push(\n `Functions: ${functions\n .slice(0, 5)\n .map((f) => f.trim().substring(0, 40))\n .join(', ')}`,\n );\n }\n if (classes.length > 0) {\n parts.push(`Classes: ${classes.map((c) => c.trim().substring(0, 40)).join(', ')}`);\n }\n\n return parts.join(' ');\n}\n\n/**\n * Summarize grep/search results\n */\nfunction summarizeSearch(text: string, pattern: string): string {\n const lines = text.split('\\n').filter((l) => l.trim().length > 0);\n const fileMap = new Map<string, number>();\n\n for (const line of lines) {\n const match = line.match(/^(.*?):\\d+:/);\n if (match?.[1]) {\n fileMap.set(match[1], (fileMap.get(match[1]) || 0) + 1);\n }\n }\n\n if (fileMap.size > 0) {\n const summary = Array.from(fileMap.entries())\n .slice(0, 5)\n .map(([f, c]) => `${f} (${c})`)\n .join(', ');\n return `[sparn] Search for \"${pattern}\": ${lines.length} matches across ${fileMap.size} files. Top files: ${summary}`;\n }\n\n return `[sparn] Search for \"${pattern}\": ${lines.length} result lines`;\n}\n\nasync function main(): Promise<void> {\n try {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const raw = Buffer.concat(chunks).toString('utf-8');\n\n let input: HookInput;\n try {\n input = JSON.parse(raw);\n } catch {\n log('Failed to parse JSON input');\n process.exit(0);\n return;\n }\n\n const toolName = input.tool_name ?? 'unknown';\n const text = extractText(input.tool_response);\n const tokens = estimateTokens(text);\n\n log(`Tool: ${toolName}, response tokens: ~${tokens}`);\n\n if (tokens < SUMMARY_THRESHOLD) {\n log('Under threshold, no summary needed');\n process.exit(0);\n return;\n }\n\n let summary = '';\n\n switch (toolName) {\n case 'Bash': {\n const command = String(input.tool_input?.['command'] ?? '');\n summary = summarizeBash(text, command);\n break;\n }\n case 'Read': {\n const filePath = String(input.tool_input?.['file_path'] ?? '');\n summary = summarizeFileRead(text, filePath);\n break;\n }\n case 'Grep': {\n const pattern = String(input.tool_input?.['pattern'] ?? '');\n summary = summarizeSearch(text, pattern);\n break;\n }\n default: {\n const lines = text.split('\\n');\n summary = `[sparn] ${toolName} output: ${lines.length} lines, ~${tokens} tokens`;\n break;\n }\n }\n\n if (summary) {\n log(`Summary: ${summary.substring(0, 100)}`);\n const output = JSON.stringify({\n hookSpecificOutput: {\n hookEventName: 'PostToolUse',\n additionalContext: summary,\n },\n });\n process.stdout.write(output);\n }\n\n process.exit(0);\n } catch (error) {\n log(`Error: ${error instanceof Error ? error.message : String(error)}`);\n process.exit(0);\n }\n}\n\nmain();\n","/**\n * Token estimation utilities.\n * Supports both heuristic (~90% accuracy) and precise (GPT tokenizer, ~95%+ accuracy) modes.\n */\n\nimport { encode } from 'gpt-tokenizer';\n\n/** Module-level flag for precise token counting */\nlet usePrecise = false;\n\n/**\n * Enable or disable precise token counting using GPT tokenizer.\n * When enabled, estimateTokens() uses gpt-tokenizer for ~95%+ accuracy on code.\n * When disabled (default), uses fast whitespace heuristic.\n *\n * @param enabled - Whether to use precise counting\n */\nexport function setPreciseTokenCounting(enabled: boolean): void {\n usePrecise = enabled;\n}\n\n/**\n * Count tokens precisely using GPT tokenizer.\n *\n * @param text - Text to count\n * @returns Exact token count\n */\nexport function countTokensPrecise(text: string): number {\n if (!text || text.length === 0) {\n return 0;\n }\n return encode(text).length;\n}\n\n/**\n * Estimate token count for text.\n *\n * In default (heuristic) mode: ~90% accuracy, very fast.\n * In precise mode: ~95%+ accuracy using GPT tokenizer.\n *\n * @param text - Text to count\n * @returns Estimated token count\n */\nexport function estimateTokens(text: string): number {\n if (!text || text.length === 0) {\n return 0;\n }\n\n if (usePrecise) {\n return encode(text).length;\n }\n\n // Split on whitespace to get words\n const words = text.split(/\\s+/).filter((w) => w.length > 0);\n const wordCount = words.length;\n\n // Character-based estimate\n const charCount = text.length;\n const charEstimate = Math.ceil(charCount / 4);\n\n // Word-based estimate\n const wordEstimate = Math.ceil(wordCount * 0.75);\n\n // Return the maximum of both estimates (more conservative)\n return Math.max(wordEstimate, charEstimate);\n}\n"],"mappings":"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","names":[]}
|