@ulrichc1/sparn 1.2.1 → 1.2.2
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 +1 -1
- package/dist/cli/index.cjs +2 -2
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/hooks/post-tool-result.cjs +66 -11
- package/dist/hooks/post-tool-result.cjs.map +1 -1
- package/dist/hooks/post-tool-result.js +66 -11
- package/dist/hooks/post-tool-result.js.map +1 -1
- package/dist/hooks/pre-prompt.cjs +38 -3
- package/dist/hooks/pre-prompt.cjs.map +1 -1
- package/dist/hooks/pre-prompt.js +39 -4
- package/dist/hooks/pre-prompt.js.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.cjs +2 -2
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
+
// src/hooks/post-tool-result.ts
|
|
5
|
+
var import_node_fs = require("fs");
|
|
6
|
+
var import_node_os = require("os");
|
|
7
|
+
var import_node_path = require("path");
|
|
8
|
+
|
|
4
9
|
// src/utils/tokenizer.ts
|
|
5
10
|
function estimateTokens(text) {
|
|
6
11
|
if (!text || text.length === 0) {
|
|
@@ -15,6 +20,15 @@ function estimateTokens(text) {
|
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
// src/hooks/post-tool-result.ts
|
|
23
|
+
var DEBUG = process.env["SPARN_DEBUG"] === "true";
|
|
24
|
+
var LOG_FILE = process.env["SPARN_LOG_FILE"] || (0, import_node_path.join)((0, import_node_os.homedir)(), ".sparn-hook.log");
|
|
25
|
+
function log(message) {
|
|
26
|
+
if (DEBUG) {
|
|
27
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
28
|
+
(0, import_node_fs.appendFileSync)(LOG_FILE, `[${timestamp}] [post-tool-result] ${message}
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
18
32
|
function exitSuccess(output) {
|
|
19
33
|
process.stdout.write(output);
|
|
20
34
|
process.exit(0);
|
|
@@ -145,11 +159,11 @@ function compressDockerLogs(content) {
|
|
|
145
159
|
}
|
|
146
160
|
}
|
|
147
161
|
const summary = ["Docker logs (deduplicated):"];
|
|
148
|
-
for (const [
|
|
162
|
+
for (const [log2, count] of Array.from(logMap.entries()).slice(0, 20)) {
|
|
149
163
|
if (count > 1) {
|
|
150
|
-
summary.push(` [${count}x] ${
|
|
164
|
+
summary.push(` [${count}x] ${log2}`);
|
|
151
165
|
} else {
|
|
152
|
-
summary.push(` ${
|
|
166
|
+
summary.push(` ${log2}`);
|
|
153
167
|
}
|
|
154
168
|
}
|
|
155
169
|
if (logMap.size > 20) {
|
|
@@ -205,47 +219,85 @@ function compressTypescriptErrors(content) {
|
|
|
205
219
|
}
|
|
206
220
|
function compressToolResult(input) {
|
|
207
221
|
const tokens = estimateTokens(input);
|
|
222
|
+
log(`Tool result tokens: ${tokens}`);
|
|
208
223
|
if (tokens < COMPRESSION_THRESHOLD) {
|
|
224
|
+
log(`Under compression threshold (${tokens} < ${COMPRESSION_THRESHOLD}), passing through`);
|
|
209
225
|
return input;
|
|
210
226
|
}
|
|
227
|
+
log(`Over threshold! Compressing ${tokens} token tool result`);
|
|
211
228
|
if (TOOL_PATTERNS.fileRead.test(input)) {
|
|
229
|
+
log("Detected: File read");
|
|
212
230
|
const match = input.match(TOOL_PATTERNS.fileRead);
|
|
213
231
|
if (match?.[2]) {
|
|
214
232
|
const content = match[2];
|
|
215
233
|
const compressed = compressFileRead(content);
|
|
234
|
+
const afterTokens = estimateTokens(compressed);
|
|
235
|
+
log(`Compressed file read: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
216
236
|
return input.replace(content, compressed);
|
|
217
237
|
}
|
|
218
238
|
}
|
|
219
239
|
if (TOOL_PATTERNS.grepResult.test(input)) {
|
|
240
|
+
log("Detected: Grep results");
|
|
220
241
|
const match = input.match(TOOL_PATTERNS.grepResult);
|
|
221
242
|
if (match?.[2]) {
|
|
222
243
|
const matches = match[2];
|
|
223
244
|
const compressed = compressGrepResults(matches);
|
|
245
|
+
const afterTokens = estimateTokens(compressed);
|
|
246
|
+
log(`Compressed grep: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
224
247
|
return input.replace(matches, compressed);
|
|
225
248
|
}
|
|
226
249
|
}
|
|
227
250
|
if (TOOL_PATTERNS.gitDiff.test(input)) {
|
|
228
|
-
|
|
251
|
+
log("Detected: Git diff");
|
|
252
|
+
const compressed = compressGitDiff(input);
|
|
253
|
+
const afterTokens = estimateTokens(compressed);
|
|
254
|
+
log(`Compressed git diff: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
255
|
+
return compressed;
|
|
229
256
|
}
|
|
230
257
|
if (TOOL_PATTERNS.buildOutput.test(input)) {
|
|
231
|
-
|
|
258
|
+
log("Detected: Build output");
|
|
259
|
+
const compressed = compressBuildOutput(input);
|
|
260
|
+
const afterTokens = estimateTokens(compressed);
|
|
261
|
+
log(`Compressed build: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
262
|
+
return compressed;
|
|
232
263
|
}
|
|
233
264
|
if (TOOL_PATTERNS.npmInstall.test(input)) {
|
|
234
|
-
|
|
265
|
+
log("Detected: NPM install");
|
|
266
|
+
const compressed = compressNpmInstall(input);
|
|
267
|
+
const afterTokens = estimateTokens(compressed);
|
|
268
|
+
log(`Compressed npm: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
269
|
+
return compressed;
|
|
235
270
|
}
|
|
236
271
|
if (TOOL_PATTERNS.dockerLogs.test(input)) {
|
|
237
|
-
|
|
272
|
+
log("Detected: Docker logs");
|
|
273
|
+
const compressed = compressDockerLogs(input);
|
|
274
|
+
const afterTokens = estimateTokens(compressed);
|
|
275
|
+
log(`Compressed docker: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
276
|
+
return compressed;
|
|
238
277
|
}
|
|
239
278
|
if (TOOL_PATTERNS.testResults.test(input)) {
|
|
240
|
-
|
|
279
|
+
log("Detected: Test results");
|
|
280
|
+
const compressed = compressTestResults(input);
|
|
281
|
+
const afterTokens = estimateTokens(compressed);
|
|
282
|
+
log(`Compressed tests: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
283
|
+
return compressed;
|
|
241
284
|
}
|
|
242
285
|
if (TOOL_PATTERNS.typescriptErrors.test(input)) {
|
|
243
|
-
|
|
286
|
+
log("Detected: TypeScript errors");
|
|
287
|
+
const compressed = compressTypescriptErrors(input);
|
|
288
|
+
const afterTokens = estimateTokens(compressed);
|
|
289
|
+
log(`Compressed TS errors: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
290
|
+
return compressed;
|
|
244
291
|
}
|
|
292
|
+
log("No pattern matched, applying generic compression");
|
|
245
293
|
const lines = input.split("\n");
|
|
246
294
|
if (lines.length > 200) {
|
|
247
|
-
|
|
295
|
+
const compressed = compressFileRead(input, 100);
|
|
296
|
+
const afterTokens = estimateTokens(compressed);
|
|
297
|
+
log(`Generic compression: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
298
|
+
return compressed;
|
|
248
299
|
}
|
|
300
|
+
log("No compression needed");
|
|
249
301
|
return input;
|
|
250
302
|
}
|
|
251
303
|
async function main() {
|
|
@@ -257,7 +309,10 @@ async function main() {
|
|
|
257
309
|
const input = Buffer.concat(chunks).toString("utf-8");
|
|
258
310
|
const output = compressToolResult(input);
|
|
259
311
|
exitSuccess(output);
|
|
260
|
-
} catch (
|
|
312
|
+
} catch (error) {
|
|
313
|
+
log(
|
|
314
|
+
`Error in post-tool-result hook: ${error instanceof Error ? error.message : String(error)}`
|
|
315
|
+
);
|
|
261
316
|
const chunks = [];
|
|
262
317
|
for await (const chunk of process.stdin) {
|
|
263
318
|
chunks.push(chunk);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/tokenizer.ts","../../src/hooks/post-tool-result.ts"],"sourcesContent":["/**\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","#!/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 { estimateTokens } from '../utils/tokenizer.js';\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\n // Only compress if over threshold\n if (tokens < COMPRESSION_THRESHOLD) {\n return input;\n }\n\n // Detect tool result type and compress accordingly\n if (TOOL_PATTERNS.fileRead.test(input)) {\n const match = input.match(TOOL_PATTERNS.fileRead);\n if (match?.[2]) {\n const content = match[2];\n const compressed = compressFileRead(content);\n return input.replace(content, compressed);\n }\n }\n\n if (TOOL_PATTERNS.grepResult.test(input)) {\n const match = input.match(TOOL_PATTERNS.grepResult);\n if (match?.[2]) {\n const matches = match[2];\n const compressed = compressGrepResults(matches);\n return input.replace(matches, compressed);\n }\n }\n\n if (TOOL_PATTERNS.gitDiff.test(input)) {\n return compressGitDiff(input);\n }\n\n if (TOOL_PATTERNS.buildOutput.test(input)) {\n return compressBuildOutput(input);\n }\n\n if (TOOL_PATTERNS.npmInstall.test(input)) {\n return compressNpmInstall(input);\n }\n\n if (TOOL_PATTERNS.dockerLogs.test(input)) {\n return compressDockerLogs(input);\n }\n\n if (TOOL_PATTERNS.testResults.test(input)) {\n return compressTestResults(input);\n }\n\n if (TOOL_PATTERNS.typescriptErrors.test(input)) {\n return compressTypescriptErrors(input);\n }\n\n // Unknown type or no compression pattern matched\n // Apply generic truncation as fallback\n const lines = input.split('\\n');\n if (lines.length > 200) {\n return compressFileRead(input, 100);\n }\n\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 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"],"mappings":"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","names":[]}
|
|
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,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/hooks/post-tool-result.ts
|
|
4
|
+
import { appendFileSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
|
|
3
8
|
// src/utils/tokenizer.ts
|
|
4
9
|
function estimateTokens(text) {
|
|
5
10
|
if (!text || text.length === 0) {
|
|
@@ -14,6 +19,15 @@ function estimateTokens(text) {
|
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
// src/hooks/post-tool-result.ts
|
|
22
|
+
var DEBUG = process.env["SPARN_DEBUG"] === "true";
|
|
23
|
+
var LOG_FILE = process.env["SPARN_LOG_FILE"] || join(homedir(), ".sparn-hook.log");
|
|
24
|
+
function log(message) {
|
|
25
|
+
if (DEBUG) {
|
|
26
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
27
|
+
appendFileSync(LOG_FILE, `[${timestamp}] [post-tool-result] ${message}
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
17
31
|
function exitSuccess(output) {
|
|
18
32
|
process.stdout.write(output);
|
|
19
33
|
process.exit(0);
|
|
@@ -144,11 +158,11 @@ function compressDockerLogs(content) {
|
|
|
144
158
|
}
|
|
145
159
|
}
|
|
146
160
|
const summary = ["Docker logs (deduplicated):"];
|
|
147
|
-
for (const [
|
|
161
|
+
for (const [log2, count] of Array.from(logMap.entries()).slice(0, 20)) {
|
|
148
162
|
if (count > 1) {
|
|
149
|
-
summary.push(` [${count}x] ${
|
|
163
|
+
summary.push(` [${count}x] ${log2}`);
|
|
150
164
|
} else {
|
|
151
|
-
summary.push(` ${
|
|
165
|
+
summary.push(` ${log2}`);
|
|
152
166
|
}
|
|
153
167
|
}
|
|
154
168
|
if (logMap.size > 20) {
|
|
@@ -204,47 +218,85 @@ function compressTypescriptErrors(content) {
|
|
|
204
218
|
}
|
|
205
219
|
function compressToolResult(input) {
|
|
206
220
|
const tokens = estimateTokens(input);
|
|
221
|
+
log(`Tool result tokens: ${tokens}`);
|
|
207
222
|
if (tokens < COMPRESSION_THRESHOLD) {
|
|
223
|
+
log(`Under compression threshold (${tokens} < ${COMPRESSION_THRESHOLD}), passing through`);
|
|
208
224
|
return input;
|
|
209
225
|
}
|
|
226
|
+
log(`Over threshold! Compressing ${tokens} token tool result`);
|
|
210
227
|
if (TOOL_PATTERNS.fileRead.test(input)) {
|
|
228
|
+
log("Detected: File read");
|
|
211
229
|
const match = input.match(TOOL_PATTERNS.fileRead);
|
|
212
230
|
if (match?.[2]) {
|
|
213
231
|
const content = match[2];
|
|
214
232
|
const compressed = compressFileRead(content);
|
|
233
|
+
const afterTokens = estimateTokens(compressed);
|
|
234
|
+
log(`Compressed file read: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
215
235
|
return input.replace(content, compressed);
|
|
216
236
|
}
|
|
217
237
|
}
|
|
218
238
|
if (TOOL_PATTERNS.grepResult.test(input)) {
|
|
239
|
+
log("Detected: Grep results");
|
|
219
240
|
const match = input.match(TOOL_PATTERNS.grepResult);
|
|
220
241
|
if (match?.[2]) {
|
|
221
242
|
const matches = match[2];
|
|
222
243
|
const compressed = compressGrepResults(matches);
|
|
244
|
+
const afterTokens = estimateTokens(compressed);
|
|
245
|
+
log(`Compressed grep: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
223
246
|
return input.replace(matches, compressed);
|
|
224
247
|
}
|
|
225
248
|
}
|
|
226
249
|
if (TOOL_PATTERNS.gitDiff.test(input)) {
|
|
227
|
-
|
|
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;
|
|
228
255
|
}
|
|
229
256
|
if (TOOL_PATTERNS.buildOutput.test(input)) {
|
|
230
|
-
|
|
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;
|
|
231
262
|
}
|
|
232
263
|
if (TOOL_PATTERNS.npmInstall.test(input)) {
|
|
233
|
-
|
|
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;
|
|
234
269
|
}
|
|
235
270
|
if (TOOL_PATTERNS.dockerLogs.test(input)) {
|
|
236
|
-
|
|
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;
|
|
237
276
|
}
|
|
238
277
|
if (TOOL_PATTERNS.testResults.test(input)) {
|
|
239
|
-
|
|
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;
|
|
240
283
|
}
|
|
241
284
|
if (TOOL_PATTERNS.typescriptErrors.test(input)) {
|
|
242
|
-
|
|
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;
|
|
243
290
|
}
|
|
291
|
+
log("No pattern matched, applying generic compression");
|
|
244
292
|
const lines = input.split("\n");
|
|
245
293
|
if (lines.length > 200) {
|
|
246
|
-
|
|
294
|
+
const compressed = compressFileRead(input, 100);
|
|
295
|
+
const afterTokens = estimateTokens(compressed);
|
|
296
|
+
log(`Generic compression: ${tokens} \u2192 ${afterTokens} tokens`);
|
|
297
|
+
return compressed;
|
|
247
298
|
}
|
|
299
|
+
log("No compression needed");
|
|
248
300
|
return input;
|
|
249
301
|
}
|
|
250
302
|
async function main() {
|
|
@@ -256,7 +308,10 @@ async function main() {
|
|
|
256
308
|
const input = Buffer.concat(chunks).toString("utf-8");
|
|
257
309
|
const output = compressToolResult(input);
|
|
258
310
|
exitSuccess(output);
|
|
259
|
-
} catch (
|
|
311
|
+
} catch (error) {
|
|
312
|
+
log(
|
|
313
|
+
`Error in post-tool-result hook: ${error instanceof Error ? error.message : String(error)}`
|
|
314
|
+
);
|
|
260
315
|
const chunks = [];
|
|
261
316
|
for await (const chunk of process.stdin) {
|
|
262
317
|
chunks.push(chunk);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/tokenizer.ts","../../src/hooks/post-tool-result.ts"],"sourcesContent":["/**\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","#!/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 { estimateTokens } from '../utils/tokenizer.js';\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\n // Only compress if over threshold\n if (tokens < COMPRESSION_THRESHOLD) {\n return input;\n }\n\n // Detect tool result type and compress accordingly\n if (TOOL_PATTERNS.fileRead.test(input)) {\n const match = input.match(TOOL_PATTERNS.fileRead);\n if (match?.[2]) {\n const content = match[2];\n const compressed = compressFileRead(content);\n return input.replace(content, compressed);\n }\n }\n\n if (TOOL_PATTERNS.grepResult.test(input)) {\n const match = input.match(TOOL_PATTERNS.grepResult);\n if (match?.[2]) {\n const matches = match[2];\n const compressed = compressGrepResults(matches);\n return input.replace(matches, compressed);\n }\n }\n\n if (TOOL_PATTERNS.gitDiff.test(input)) {\n return compressGitDiff(input);\n }\n\n if (TOOL_PATTERNS.buildOutput.test(input)) {\n return compressBuildOutput(input);\n }\n\n if (TOOL_PATTERNS.npmInstall.test(input)) {\n return compressNpmInstall(input);\n }\n\n if (TOOL_PATTERNS.dockerLogs.test(input)) {\n return compressDockerLogs(input);\n }\n\n if (TOOL_PATTERNS.testResults.test(input)) {\n return compressTestResults(input);\n }\n\n if (TOOL_PATTERNS.typescriptErrors.test(input)) {\n return compressTypescriptErrors(input);\n }\n\n // Unknown type or no compression pattern matched\n // Apply generic truncation as fallback\n const lines = input.split('\\n');\n if (lines.length > 200) {\n return compressFileRead(input, 100);\n }\n\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 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"],"mappings":"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","names":[]}
|
|
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"]}
|
|
@@ -238,6 +238,15 @@ function createEntry(content, type, baseTime) {
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
// src/hooks/pre-prompt.ts
|
|
241
|
+
var DEBUG = process.env["SPARN_DEBUG"] === "true";
|
|
242
|
+
var LOG_FILE = process.env["SPARN_LOG_FILE"] || (0, import_node_path.join)((0, import_node_os.homedir)(), ".sparn-hook.log");
|
|
243
|
+
function log(message) {
|
|
244
|
+
if (DEBUG) {
|
|
245
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
246
|
+
(0, import_node_fs.appendFileSync)(LOG_FILE, `[${timestamp}] [pre-prompt] ${message}
|
|
247
|
+
`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
241
250
|
function exitSuccess(output) {
|
|
242
251
|
process.stdout.write(output);
|
|
243
252
|
process.exit(0);
|
|
@@ -250,31 +259,57 @@ async function main() {
|
|
|
250
259
|
}
|
|
251
260
|
const input = Buffer.concat(chunks).toString("utf-8");
|
|
252
261
|
const tokens = estimateTokens(input);
|
|
253
|
-
|
|
262
|
+
log(`Input tokens: ${tokens}`);
|
|
263
|
+
const projectConfigPath = (0, import_node_path.join)(process.cwd(), ".sparn", "config.yaml");
|
|
264
|
+
const globalConfigPath = (0, import_node_path.join)((0, import_node_os.homedir)(), ".sparn", "config.yaml");
|
|
254
265
|
let config;
|
|
266
|
+
let configPath;
|
|
267
|
+
if ((0, import_node_fs.existsSync)(projectConfigPath)) {
|
|
268
|
+
configPath = projectConfigPath;
|
|
269
|
+
log(`Using project config: ${configPath}`);
|
|
270
|
+
} else if ((0, import_node_fs.existsSync)(globalConfigPath)) {
|
|
271
|
+
configPath = globalConfigPath;
|
|
272
|
+
log(`Using global config: ${configPath}`);
|
|
273
|
+
} else {
|
|
274
|
+
log("No config found, passing through");
|
|
275
|
+
exitSuccess(input);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
255
278
|
try {
|
|
256
279
|
const configYAML = (0, import_node_fs.readFileSync)(configPath, "utf-8");
|
|
257
280
|
config = (0, import_js_yaml.load)(configYAML);
|
|
258
|
-
} catch {
|
|
281
|
+
} catch (err) {
|
|
282
|
+
log(`Config parse error: ${err}`);
|
|
259
283
|
exitSuccess(input);
|
|
260
284
|
return;
|
|
261
285
|
}
|
|
262
286
|
const { autoOptimizeThreshold, tokenBudget } = config.realtime;
|
|
287
|
+
log(`Threshold: ${autoOptimizeThreshold}, Budget: ${tokenBudget}`);
|
|
263
288
|
if (tokens < autoOptimizeThreshold) {
|
|
289
|
+
log(`Under threshold (${tokens} < ${autoOptimizeThreshold}), passing through`);
|
|
264
290
|
exitSuccess(input);
|
|
265
291
|
return;
|
|
266
292
|
}
|
|
293
|
+
log(`Over threshold! Optimizing ${tokens} tokens to fit ${tokenBudget} budget`);
|
|
267
294
|
const entries = parseClaudeCodeContext(input);
|
|
295
|
+
log(`Parsed ${entries.length} context entries`);
|
|
268
296
|
if (entries.length === 0) {
|
|
297
|
+
log("No entries to optimize, passing through");
|
|
269
298
|
exitSuccess(input);
|
|
270
299
|
return;
|
|
271
300
|
}
|
|
272
301
|
const pruner = createBudgetPrunerFromConfig(config.realtime, config.decay, config.states);
|
|
273
302
|
const result = pruner.pruneToFit(entries, tokenBudget);
|
|
303
|
+
const outputTokens = estimateTokens(result.kept.map((e) => e.content).join("\n\n"));
|
|
304
|
+
const saved = tokens - outputTokens;
|
|
305
|
+
const reduction = (saved / tokens * 100).toFixed(1);
|
|
306
|
+
log(`Optimization complete: ${tokens} \u2192 ${outputTokens} tokens (${reduction}% reduction)`);
|
|
307
|
+
log(`Kept ${result.kept.length}/${entries.length} entries`);
|
|
274
308
|
const sorted = [...result.kept].sort((a, b) => a.timestamp - b.timestamp);
|
|
275
309
|
const optimizedContext = sorted.map((e) => e.content).join("\n\n");
|
|
276
310
|
exitSuccess(optimizedContext);
|
|
277
|
-
} catch (
|
|
311
|
+
} catch (error) {
|
|
312
|
+
log(`Error in pre-prompt hook: ${error instanceof Error ? error.message : String(error)}`);
|
|
278
313
|
const chunks = [];
|
|
279
314
|
for await (const chunk of process.stdin) {
|
|
280
315
|
chunks.push(chunk);
|