@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.
@@ -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 [log, count] of Array.from(logMap.entries()).slice(0, 20)) {
162
+ for (const [log2, count] of Array.from(logMap.entries()).slice(0, 20)) {
149
163
  if (count > 1) {
150
- summary.push(` [${count}x] ${log}`);
164
+ summary.push(` [${count}x] ${log2}`);
151
165
  } else {
152
- summary.push(` ${log}`);
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
- return compressGitDiff(input);
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
- return compressBuildOutput(input);
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
- return compressNpmInstall(input);
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
- return compressDockerLogs(input);
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
- return compressTestResults(input);
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
- return compressTypescriptErrors(input);
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
- return compressFileRead(input, 100);
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 (_error) {
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 [log, count] of Array.from(logMap.entries()).slice(0, 20)) {
161
+ for (const [log2, count] of Array.from(logMap.entries()).slice(0, 20)) {
148
162
  if (count > 1) {
149
- summary.push(` [${count}x] ${log}`);
163
+ summary.push(` [${count}x] ${log2}`);
150
164
  } else {
151
- summary.push(` ${log}`);
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
- return compressGitDiff(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;
228
255
  }
229
256
  if (TOOL_PATTERNS.buildOutput.test(input)) {
230
- return compressBuildOutput(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;
231
262
  }
232
263
  if (TOOL_PATTERNS.npmInstall.test(input)) {
233
- return compressNpmInstall(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;
234
269
  }
235
270
  if (TOOL_PATTERNS.dockerLogs.test(input)) {
236
- return compressDockerLogs(input);
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
- return compressTestResults(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;
240
283
  }
241
284
  if (TOOL_PATTERNS.typescriptErrors.test(input)) {
242
- return compressTypescriptErrors(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;
243
290
  }
291
+ log("No pattern matched, applying generic compression");
244
292
  const lines = input.split("\n");
245
293
  if (lines.length > 200) {
246
- return compressFileRead(input, 100);
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 (_error) {
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":";;;AAcA,SAAS,sBAAsB;AAC/B,SAAS,eAAe;AACxB,SAAS,YAAY;;;ACId,SAAS,eAAe,MAAsB;AACnD,MAAI,CAAC,QAAQ,KAAK,WAAW,GAAG;AAC9B,WAAO;AAAA,EACT;AAGA,QAAM,QAAQ,KAAK,MAAM,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC1D,QAAM,YAAY,MAAM;AAGxB,QAAM,YAAY,KAAK;AACvB,QAAM,eAAe,KAAK,KAAK,YAAY,CAAC;AAG5C,QAAM,eAAe,KAAK,KAAK,YAAY,IAAI;AAG/C,SAAO,KAAK,IAAI,cAAc,YAAY;AAC5C;;;ADlBA,IAAM,QAAQ,QAAQ,IAAI,aAAa,MAAM;AAC7C,IAAM,WAAW,QAAQ,IAAI,gBAAgB,KAAK,KAAK,QAAQ,GAAG,iBAAiB;AAEnF,SAAS,IAAI,SAAuB;AAClC,MAAI,OAAO;AACT,UAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AACzC,mBAAe,UAAU,IAAI,SAAS,wBAAwB,OAAO;AAAA,CAAI;AAAA,EAC3E;AACF;AAGA,SAAS,YAAY,QAAsB;AACzC,UAAQ,OAAO,MAAM,MAAM;AAC3B,UAAQ,KAAK,CAAC;AAChB;AAGA,IAAM,wBAAwB;AAG9B,IAAM,gBAAgB;AAAA,EACpB,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,cAAc;AAChB;AAKA,SAAS,iBAAiB,SAAiB,WAAW,KAAa;AACjE,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAEhC,MAAI,MAAM,UAAU,WAAW,GAAG;AAChC,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,MAAM,MAAM,GAAG,QAAQ;AACpC,QAAM,OAAO,MAAM,MAAM,CAAC,QAAQ;AAClC,QAAM,UAAU,MAAM,SAAS,WAAW;AAE1C,SAAO,CAAC,GAAG,MAAM,IAAI,QAAQ,OAAO,uBAAuB,IAAI,GAAG,IAAI,EAAE,KAAK,IAAI;AACnF;AAKA,SAAS,oBAAoB,SAAiB,oBAAoB,GAAW;AAC3E,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,QAAM,cAAc,oBAAI,IAAsB;AAG9C,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,KAAK,MAAM,mBAAmB;AAC5C,QAAI,QAAQ,CAAC,KAAK,MAAM,CAAC,KAAK,MAAM,CAAC,GAAG;AACtC,YAAM,OAAO,MAAM,CAAC;AACpB,YAAM,UAAU,MAAM,CAAC;AACvB,YAAM,OAAO,MAAM,CAAC;AACpB,UAAI,CAAC,YAAY,IAAI,IAAI,GAAG;AAC1B,oBAAY,IAAI,MAAM,CAAC,CAAC;AAAA,MAC1B;AACA,kBAAY,IAAI,IAAI,GAAG,KAAK,UAAU,OAAO,KAAK,KAAK,KAAK,CAAC,EAAE;AAAA,IACjE;AAAA,EACF;AAGA,QAAM,aAAuB,CAAC;AAE9B,aAAW,CAAC,MAAM,OAAO,KAAK,YAAY,QAAQ,GAAG;AACnD,eAAW,KAAK,GAAG,IAAI,KAAK,QAAQ,MAAM,YAAY;AAEtD,QAAI,QAAQ,UAAU,mBAAmB;AACvC,iBAAW,KAAK,GAAG,OAAO;AAAA,IAC5B,OAAO;AACL,iBAAW,KAAK,GAAG,QAAQ,MAAM,GAAG,iBAAiB,CAAC;AACtD,iBAAW,KAAK,aAAa,QAAQ,SAAS,iBAAiB,eAAe;AAAA,IAChF;AAEA,eAAW,KAAK,EAAE;AAAA,EACpB;AAEA,SAAO,WAAW,KAAK,IAAI;AAC7B;AAKA,SAAS,gBAAgB,SAAyB;AAChD,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,QAAM,QAAQ,oBAAI,IAAgD;AAClE,MAAI,cAAc;AAElB,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,WAAW,YAAY,GAAG;AACjC,YAAM,QAAQ,KAAK,MAAM,6BAA6B;AACtD,UAAI,OAAO;AACT,sBAAc,MAAM,CAAC,KAAK;AAC1B,cAAM,IAAI,aAAa,EAAE,OAAO,GAAG,SAAS,EAAE,CAAC;AAAA,MACjD;AAAA,IACF,WAAW,KAAK,WAAW,GAAG,KAAK,CAAC,KAAK,WAAW,KAAK,GAAG;AAC1D,YAAM,QAAQ,MAAM,IAAI,WAAW;AACnC,UAAI,MAAO,OAAM;AAAA,IACnB,WAAW,KAAK,WAAW,GAAG,KAAK,CAAC,KAAK,WAAW,KAAK,GAAG;AAC1D,YAAM,QAAQ,MAAM,IAAI,WAAW;AACnC,UAAI,MAAO,OAAM;AAAA,IACnB;AAAA,EACF;AAGA,QAAM,UAAoB,CAAC,mBAAmB;AAE9C,aAAW,CAAC,MAAM,KAAK,KAAK,MAAM,QAAQ,GAAG;AAC3C,YAAQ,KAAK,KAAK,IAAI,MAAM,MAAM,KAAK,KAAK,MAAM,OAAO,EAAE;AAAA,EAC7D;AAEA,SAAO,QAAQ,KAAK,IAAI;AAC1B;AAKA,SAAS,oBAAoB,SAAyB;AACpD,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,QAAM,YAAsB,CAAC;AAE7B,aAAW,QAAQ,OAAO;AACxB,QAAI,wCAAwC,KAAK,IAAI,GAAG;AACtD,gBAAU,KAAK,IAAI;AAAA,IACrB;AAAA,EACF;AAEA,MAAI,UAAU,WAAW,GAAG;AAC1B,WAAO;AAAA,EACT;AAEA,SAAO,CAAC,0BAA0B,GAAG,SAAS,EAAE,KAAK,IAAI;AAC3D;AAKA,SAAS,mBAAmB,SAAyB;AACnD,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,QAAM,UAAoB,CAAC;AAG3B,QAAM,WAAqB,CAAC;AAC5B,QAAM,SAAmB,CAAC;AAE1B,aAAW,QAAQ,OAAO;AACxB,QAAI,uBAAuB,KAAK,IAAI,GAAG;AACrC,cAAQ,KAAK,KAAK,KAAK,CAAC;AAAA,IAC1B;AACA,QAAI,QAAQ,KAAK,IAAI,GAAG;AACtB,eAAS,KAAK,KAAK,KAAK,CAAC;AAAA,IAC3B;AACA,QAAI,SAAS,KAAK,IAAI,GAAG;AACvB,aAAO,KAAK,KAAK,KAAK,CAAC;AAAA,IACzB;AAAA,EACF;AAEA,MAAI,OAAO,SAAS,GAAG;AACrB,WAAO,CAAC,gCAAgC,GAAG,OAAO,MAAM,GAAG,CAAC,CAAC,EAAE,KAAK,IAAI;AAAA,EAC1E;AAEA,MAAI,SAAS,SAAS,GAAG;AACvB,WAAO;AAAA,MACL;AAAA,MACA,GAAG,SAAS,MAAM,GAAG,CAAC;AAAA,MACtB,SAAS,SAAS,IAAI,WAAW,SAAS,SAAS,CAAC,mBAAmB;AAAA,IACzE,EACG,OAAO,OAAO,EACd,KAAK,IAAI;AAAA,EACd;AAEA,SAAO,QAAQ,SAAS,IAAI,QAAQ,KAAK,IAAI,IAAI;AACnD;AAKA,SAAS,mBAAmB,SAAyB;AACnD,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,QAAM,SAAS,oBAAI,IAAoB;AAGvC,aAAW,QAAQ,OAAO;AAExB,UAAM,aAAa,KAAK,QAAQ,iCAAiC,EAAE,EAAE,KAAK;AAC1E,QAAI,YAAY;AACd,aAAO,IAAI,aAAa,OAAO,IAAI,UAAU,KAAK,KAAK,CAAC;AAAA,IAC1D;AAAA,EACF;AAEA,QAAM,UAAoB,CAAC,6BAA6B;AAExD,aAAW,CAACA,MAAK,KAAK,KAAK,MAAM,KAAK,OAAO,QAAQ,CAAC,EAAE,MAAM,GAAG,EAAE,GAAG;AACpE,QAAI,QAAQ,GAAG;AACb,cAAQ,KAAK,MAAM,KAAK,MAAMA,IAAG,EAAE;AAAA,IACrC,OAAO;AACL,cAAQ,KAAK,KAAKA,IAAG,EAAE;AAAA,IACzB;AAAA,EACF;AAEA,MAAI,OAAO,OAAO,IAAI;AACpB,YAAQ,KAAK,aAAa,OAAO,OAAO,EAAE,wBAAwB;AAAA,EACpE;AAEA,SAAO,QAAQ,KAAK,IAAI;AAC1B;AAKA,SAAS,oBAAoB,SAAyB;AACpD,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,MAAI,SAAS;AACb,MAAI,SAAS;AACb,MAAI,UAAU;AACd,QAAM,WAAqB,CAAC;AAE5B,aAAW,QAAQ,OAAO;AACxB,QAAI,QAAQ,KAAK,IAAI,EAAG;AACxB,QAAI,QAAQ,KAAK,IAAI,GAAG;AACtB;AACA,eAAS,KAAK,KAAK,KAAK,CAAC;AAAA,IAC3B;AACA,QAAI,QAAQ,KAAK,IAAI,EAAG;AAAA,EAC1B;AAEA,QAAM,UAAU,CAAC,iBAAiB,MAAM,YAAY,MAAM,YAAY,OAAO,UAAU;AAEvF,MAAI,SAAS,SAAS,GAAG;AACvB,YAAQ,KAAK,IAAI,eAAe;AAChC,YAAQ,KAAK,GAAG,SAAS,MAAM,GAAG,EAAE,CAAC;AACrC,QAAI,SAAS,SAAS,IAAI;AACxB,cAAQ,KAAK,WAAW,SAAS,SAAS,EAAE,gBAAgB;AAAA,IAC9D;AAAA,EACF;AAEA,SAAO,QAAQ,KAAK,IAAI;AAC1B;AAKA,SAAS,yBAAyB,SAAyB;AACzD,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,QAAM,WAAW,oBAAI,IAAsB;AAE3C,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,KAAK,MAAM,mCAAmC;AAC5D,QAAI,OAAO;AACT,YAAM,OAAO,MAAM,CAAC,KAAK;AACzB,YAAM,YAAY,MAAM,CAAC,KAAK;AAC9B,YAAM,MAAM,GAAG,IAAI,IAAI,SAAS;AAEhC,UAAI,CAAC,SAAS,IAAI,GAAG,GAAG;AACtB,iBAAS,IAAI,KAAK,CAAC,CAAC;AAAA,MACtB;AACA,eAAS,IAAI,GAAG,GAAG,KAAK,IAAI;AAAA,IAC9B;AAAA,EACF;AAEA,QAAM,UAAU,CAAC,sCAAsC;AAEvD,aAAW,CAAC,KAAK,MAAM,KAAK,SAAS,QAAQ,GAAG;AAC9C,YAAQ,KAAK,KAAK,GAAG,KAAK,OAAO,MAAM,UAAU;AACjD,YAAQ,KAAK,OAAO,OAAO,CAAC,CAAC,EAAE;AAAA,EACjC;AAEA,SAAO,QAAQ,KAAK,IAAI;AAC1B;AAKA,SAAS,mBAAmB,OAAuB;AACjD,QAAM,SAAS,eAAe,KAAK;AACnC,MAAI,uBAAuB,MAAM,EAAE;AAGnC,MAAI,SAAS,uBAAuB;AAClC,QAAI,gCAAgC,MAAM,MAAM,qBAAqB,oBAAoB;AACzF,WAAO;AAAA,EACT;AAEA,MAAI,+BAA+B,MAAM,oBAAoB;AAG7D,MAAI,cAAc,SAAS,KAAK,KAAK,GAAG;AACtC,QAAI,qBAAqB;AACzB,UAAM,QAAQ,MAAM,MAAM,cAAc,QAAQ;AAChD,QAAI,QAAQ,CAAC,GAAG;AACd,YAAM,UAAU,MAAM,CAAC;AACvB,YAAM,aAAa,iBAAiB,OAAO;AAC3C,YAAM,cAAc,eAAe,UAAU;AAC7C,UAAI,yBAAyB,MAAM,WAAM,WAAW,SAAS;AAC7D,aAAO,MAAM,QAAQ,SAAS,UAAU;AAAA,IAC1C;AAAA,EACF;AAEA,MAAI,cAAc,WAAW,KAAK,KAAK,GAAG;AACxC,QAAI,wBAAwB;AAC5B,UAAM,QAAQ,MAAM,MAAM,cAAc,UAAU;AAClD,QAAI,QAAQ,CAAC,GAAG;AACd,YAAM,UAAU,MAAM,CAAC;AACvB,YAAM,aAAa,oBAAoB,OAAO;AAC9C,YAAM,cAAc,eAAe,UAAU;AAC7C,UAAI,oBAAoB,MAAM,WAAM,WAAW,SAAS;AACxD,aAAO,MAAM,QAAQ,SAAS,UAAU;AAAA,IAC1C;AAAA,EACF;AAEA,MAAI,cAAc,QAAQ,KAAK,KAAK,GAAG;AACrC,QAAI,oBAAoB;AACxB,UAAM,aAAa,gBAAgB,KAAK;AACxC,UAAM,cAAc,eAAe,UAAU;AAC7C,QAAI,wBAAwB,MAAM,WAAM,WAAW,SAAS;AAC5D,WAAO;AAAA,EACT;AAEA,MAAI,cAAc,YAAY,KAAK,KAAK,GAAG;AACzC,QAAI,wBAAwB;AAC5B,UAAM,aAAa,oBAAoB,KAAK;AAC5C,UAAM,cAAc,eAAe,UAAU;AAC7C,QAAI,qBAAqB,MAAM,WAAM,WAAW,SAAS;AACzD,WAAO;AAAA,EACT;AAEA,MAAI,cAAc,WAAW,KAAK,KAAK,GAAG;AACxC,QAAI,uBAAuB;AAC3B,UAAM,aAAa,mBAAmB,KAAK;AAC3C,UAAM,cAAc,eAAe,UAAU;AAC7C,QAAI,mBAAmB,MAAM,WAAM,WAAW,SAAS;AACvD,WAAO;AAAA,EACT;AAEA,MAAI,cAAc,WAAW,KAAK,KAAK,GAAG;AACxC,QAAI,uBAAuB;AAC3B,UAAM,aAAa,mBAAmB,KAAK;AAC3C,UAAM,cAAc,eAAe,UAAU;AAC7C,QAAI,sBAAsB,MAAM,WAAM,WAAW,SAAS;AAC1D,WAAO;AAAA,EACT;AAEA,MAAI,cAAc,YAAY,KAAK,KAAK,GAAG;AACzC,QAAI,wBAAwB;AAC5B,UAAM,aAAa,oBAAoB,KAAK;AAC5C,UAAM,cAAc,eAAe,UAAU;AAC7C,QAAI,qBAAqB,MAAM,WAAM,WAAW,SAAS;AACzD,WAAO;AAAA,EACT;AAEA,MAAI,cAAc,iBAAiB,KAAK,KAAK,GAAG;AAC9C,QAAI,6BAA6B;AACjC,UAAM,aAAa,yBAAyB,KAAK;AACjD,UAAM,cAAc,eAAe,UAAU;AAC7C,QAAI,yBAAyB,MAAM,WAAM,WAAW,SAAS;AAC7D,WAAO;AAAA,EACT;AAGA,MAAI,kDAAkD;AAEtD,QAAM,QAAQ,MAAM,MAAM,IAAI;AAC9B,MAAI,MAAM,SAAS,KAAK;AACtB,UAAM,aAAa,iBAAiB,OAAO,GAAG;AAC9C,UAAM,cAAc,eAAe,UAAU;AAC7C,QAAI,wBAAwB,MAAM,WAAM,WAAW,SAAS;AAC5D,WAAO;AAAA,EACT;AAEA,MAAI,uBAAuB;AAC3B,SAAO;AACT;AAGA,eAAe,OAAsB;AACnC,MAAI;AAEF,UAAM,SAAmB,CAAC;AAC1B,qBAAiB,SAAS,QAAQ,OAAO;AACvC,aAAO,KAAK,KAAK;AAAA,IACnB;AACA,UAAM,QAAQ,OAAO,OAAO,MAAM,EAAE,SAAS,OAAO;AAGpD,UAAM,SAAS,mBAAmB,KAAK;AAEvC,gBAAY,MAAM;AAAA,EACpB,SAAS,OAAO;AAEd;AAAA,MACE,mCAAmC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,IAC3F;AACA,UAAM,SAAmB,CAAC;AAC1B,qBAAiB,SAAS,QAAQ,OAAO;AACvC,aAAO,KAAK,KAAK;AAAA,IACnB;AACA,UAAM,QAAQ,OAAO,OAAO,MAAM,EAAE,SAAS,OAAO;AACpD,gBAAY,KAAK;AAAA,EACnB;AACF;AAGA,KAAK;","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
- const configPath = (0, import_node_path.join)((0, import_node_os.homedir)(), ".sparn", "config.yaml");
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 (_error) {
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);