greprag 5.48.0 → 5.49.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.
Files changed (37) hide show
  1. package/dist/commands/checkpoint-helpers.js +6 -1
  2. package/dist/commands/checkpoint-helpers.js.map +1 -1
  3. package/dist/commands/crush.d.ts +9 -0
  4. package/dist/commands/crush.js +39 -0
  5. package/dist/commands/crush.js.map +1 -1
  6. package/dist/commands/fix.js +7 -3
  7. package/dist/commands/fix.js.map +1 -1
  8. package/dist/commands/init.js +47 -49
  9. package/dist/commands/init.js.map +1 -1
  10. package/dist/commands/opencode-relay.d.ts +14 -8
  11. package/dist/commands/opencode-relay.js +15 -19
  12. package/dist/commands/opencode-relay.js.map +1 -1
  13. package/dist/commands/watcher-registry.js +4 -2
  14. package/dist/commands/watcher-registry.js.map +1 -1
  15. package/dist/crush/code-compressor.d.ts +61 -0
  16. package/dist/crush/code-compressor.js +359 -0
  17. package/dist/crush/code-compressor.js.map +1 -0
  18. package/dist/crush/crush-types.d.ts +1 -1
  19. package/dist/crush/index.d.ts +1 -0
  20. package/dist/crush/index.js +6 -1
  21. package/dist/crush/index.js.map +1 -1
  22. package/dist/index.js +8 -8
  23. package/dist/index.js.map +1 -1
  24. package/dist/opencode-plugin-crush.d.ts +137 -0
  25. package/dist/opencode-plugin-crush.js +278 -0
  26. package/dist/opencode-plugin-crush.js.map +1 -0
  27. package/dist/opencode-plugin.bundle.js +2447 -0
  28. package/dist/opencode-plugin.d.ts +6 -0
  29. package/dist/opencode-plugin.js +124 -72
  30. package/dist/opencode-plugin.js.map +1 -1
  31. package/dist/session-id.d.ts +9 -0
  32. package/dist/session-id.js +15 -0
  33. package/dist/session-id.js.map +1 -1
  34. package/dist/worktree-state.js +5 -5
  35. package/dist/worktree-state.js.map +1 -1
  36. package/package.json +3 -2
  37. package/scripts/bundle-opencode-plugin.mjs +54 -0
@@ -0,0 +1,2447 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/opencode-plugin.ts
26
+ var crypto2 = __toESM(require("crypto"));
27
+ var fs2 = __toESM(require("fs"));
28
+ var os = __toESM(require("os"));
29
+ var path2 = __toESM(require("path"));
30
+
31
+ // src/opencode-plugin-helpers.ts
32
+ var crypto = __toESM(require("crypto"));
33
+ var fs = __toESM(require("fs"));
34
+ var path = __toESM(require("path"));
35
+
36
+ // src/proc.ts
37
+ var import_child_process = require("child_process");
38
+ function safeExecSync(command, options) {
39
+ return (0, import_child_process.execSync)(command, { windowsHide: true, ...options });
40
+ }
41
+ function safeSpawn(command, argsOrOptions, maybeOptions) {
42
+ if (Array.isArray(argsOrOptions)) {
43
+ return (0, import_child_process.spawn)(command, argsOrOptions, { windowsHide: true, ...maybeOptions });
44
+ }
45
+ return (0, import_child_process.spawn)(command, { windowsHide: true, ...argsOrOptions });
46
+ }
47
+
48
+ // src/opencode-plugin-helpers.ts
49
+ var HOME = process.env.HOME || process.env.USERPROFILE || "";
50
+ function readAnchorFile(filePath) {
51
+ try {
52
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
53
+ if (!raw || typeof raw !== "object")
54
+ return null;
55
+ const notify = raw.inbox_notify;
56
+ const inboxNotify = notify === "off" || notify === "session_start_only" ? notify : "every_turn";
57
+ return {
58
+ projectId: typeof raw.project_id === "string" ? raw.project_id : void 0,
59
+ projectName: typeof raw.project_name === "string" ? raw.project_name : void 0,
60
+ memoryCapture: raw.memory_capture !== false,
61
+ sessionStartRecap: raw.session_start_recap !== false,
62
+ inboxNotify
63
+ };
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+ function findExistingAnchorFile(startDir) {
69
+ const globalAnchorPath = path.join(HOME, ".greprag", "project.json");
70
+ const legacyGlobalAnchorPath = path.join(HOME, ".claude", "project.json");
71
+ let dir = path.resolve(startDir);
72
+ while (true) {
73
+ for (const subdir of [".greprag", ".claude", ".opencode"]) {
74
+ const candidate = path.join(dir, subdir, "project.json");
75
+ if (candidate !== globalAnchorPath && candidate !== legacyGlobalAnchorPath && fs.existsSync(candidate)) {
76
+ return candidate;
77
+ }
78
+ }
79
+ const parent = path.dirname(dir);
80
+ if (parent === dir)
81
+ return null;
82
+ dir = parent;
83
+ }
84
+ }
85
+ function formatUuid(hashHex) {
86
+ return [
87
+ hashHex.slice(0, 8),
88
+ hashHex.slice(8, 12),
89
+ "4" + hashHex.slice(13, 16),
90
+ "8" + hashHex.slice(17, 20),
91
+ hashHex.slice(20, 32)
92
+ ].join("-");
93
+ }
94
+ function computeGitDerivedProjectId(cwd) {
95
+ try {
96
+ const out = safeExecSync("git rev-list --max-parents=0 HEAD", {
97
+ cwd,
98
+ encoding: "utf-8",
99
+ stdio: ["pipe", "pipe", "pipe"]
100
+ });
101
+ const roots = out.trim().split(/\s+/).filter(Boolean).sort();
102
+ if (roots.length === 0)
103
+ return null;
104
+ const hash = crypto.createHash("sha256").update(roots.join("\n")).digest("hex");
105
+ return formatUuid(hash);
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+ function deterministicProjectId(cwd) {
111
+ const normalized = path.resolve(cwd).toLowerCase();
112
+ const hash = crypto.createHash("sha256").update(normalized).digest("hex");
113
+ return formatUuid(hash);
114
+ }
115
+ function isEphemeralCwd(cwd) {
116
+ const norm = path.resolve(cwd).replace(/\\/g, "/").toLowerCase();
117
+ if (norm.includes("/appdata/roaming/claude/local-agent-mode-sessions/"))
118
+ return true;
119
+ if (norm.includes("/appdata/local/claude/local-agent-mode-sessions/"))
120
+ return true;
121
+ if (norm.startsWith("/tmp/"))
122
+ return true;
123
+ if (norm.startsWith("/var/tmp/"))
124
+ return true;
125
+ if (norm.startsWith("/private/tmp/"))
126
+ return true;
127
+ return false;
128
+ }
129
+ function readAnchor(worktree) {
130
+ const filePath = findExistingAnchorFile(worktree);
131
+ const file = filePath ? readAnchorFile(filePath) : null;
132
+ const root = path.resolve(worktree);
133
+ if (file && file.projectId && file.projectName) {
134
+ return {
135
+ projectId: file.projectId,
136
+ projectName: file.projectName,
137
+ memoryCapture: file.memoryCapture,
138
+ sessionStartRecap: file.sessionStartRecap,
139
+ inboxNotify: file.inboxNotify
140
+ };
141
+ }
142
+ const gitId = computeGitDerivedProjectId(worktree);
143
+ if (gitId) {
144
+ return {
145
+ projectId: gitId,
146
+ projectName: file?.projectName || path.basename(root).toLowerCase(),
147
+ memoryCapture: file?.memoryCapture ?? true,
148
+ sessionStartRecap: file?.sessionStartRecap ?? true,
149
+ inboxNotify: file?.inboxNotify ?? "every_turn"
150
+ };
151
+ }
152
+ if (isEphemeralCwd(worktree)) {
153
+ const canonicalGlobal = path.join(HOME, ".greprag", "project.json");
154
+ const legacyGlobal = path.join(HOME, ".claude", "project.json");
155
+ const globalPath = fs.existsSync(canonicalGlobal) ? canonicalGlobal : legacyGlobal;
156
+ const globalFile = fs.existsSync(globalPath) ? readAnchorFile(globalPath) : null;
157
+ if (globalFile && globalFile.projectId && globalFile.projectName) {
158
+ return {
159
+ projectId: globalFile.projectId,
160
+ projectName: globalFile.projectName,
161
+ memoryCapture: globalFile.memoryCapture,
162
+ sessionStartRecap: globalFile.sessionStartRecap,
163
+ inboxNotify: globalFile.inboxNotify
164
+ };
165
+ }
166
+ }
167
+ return {
168
+ projectId: deterministicProjectId(worktree),
169
+ projectName: file?.projectName || path.basename(root).toLowerCase(),
170
+ memoryCapture: file?.memoryCapture ?? true,
171
+ sessionStartRecap: file?.sessionStartRecap ?? true,
172
+ inboxNotify: file?.inboxNotify ?? "every_turn"
173
+ };
174
+ }
175
+ function isPidAlive(pid) {
176
+ if (!Number.isFinite(pid) || pid <= 0)
177
+ return false;
178
+ try {
179
+ process.kill(pid, 0);
180
+ return true;
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+ var RELAY_LOCK_PREFIX = "relay.";
186
+ var RELAY_STALE_MS = 3e4;
187
+ function relayLockPath(sessionId) {
188
+ const eight = sessionId.replace(/[^0-9a-f]/gi, "").slice(0, 8) || "unknown";
189
+ return path.join(HOME, ".greprag", `${RELAY_LOCK_PREFIX}${eight}.pid`);
190
+ }
191
+ function tryClaimRelayLock(lockPath) {
192
+ try {
193
+ const fd = fs.openSync(lockPath, "wx");
194
+ return { ok: true, fd };
195
+ } catch (err) {
196
+ if (err.code !== "EEXIST") {
197
+ return { ok: false, reason: "lock-create-failed" };
198
+ }
199
+ try {
200
+ const stat = fs.statSync(lockPath);
201
+ const ageMs = Date.now() - stat.mtimeMs;
202
+ let pidAlive = false;
203
+ try {
204
+ const pid = parseInt(fs.readFileSync(lockPath, "utf-8").trim(), 10);
205
+ pidAlive = pid > 0 && isPidAlive(pid);
206
+ } catch {
207
+ }
208
+ if (ageMs < RELAY_STALE_MS && pidAlive) {
209
+ return { ok: false, reason: "busy" };
210
+ }
211
+ try {
212
+ fs.unlinkSync(lockPath);
213
+ } catch {
214
+ }
215
+ const fd = fs.openSync(lockPath, "wx");
216
+ return { ok: true, fd };
217
+ } catch (err2) {
218
+ return { ok: false, reason: "lock-stale-replace-failed" };
219
+ }
220
+ }
221
+ }
222
+ var HOURLY_CAP = 3;
223
+ function stripRecapNoise(content, type) {
224
+ const target = type === "daily" ? "Shipped:" : "Open:";
225
+ const lines = content.split("\n");
226
+ const cutAt = lines.findIndex((line) => line.trim().startsWith(target));
227
+ const kept = cutAt === -1 ? lines : lines.slice(0, cutAt);
228
+ while (kept.length > 0 && kept[kept.length - 1].trim() === "")
229
+ kept.pop();
230
+ return kept.join("\n");
231
+ }
232
+ function fmtHoursAgo(windowEndIso, now) {
233
+ const diffMs = now.getTime() - new Date(windowEndIso).getTime();
234
+ const diffMin = Math.floor(diffMs / 6e4);
235
+ if (diffMin < 1)
236
+ return "just now";
237
+ if (diffMin < 60)
238
+ return `${diffMin} min ago`;
239
+ const diffHrs = Math.round(diffMs / 36e5);
240
+ return diffHrs === 1 ? "1 hour ago" : `${diffHrs} hours ago`;
241
+ }
242
+ async function buildRecapBody(apiUrl, apiKey, anchor, now = /* @__PURE__ */ new Date()) {
243
+ if (!anchor.projectId)
244
+ return "";
245
+ const hourlyFromIso = new Date(now.getTime() - 2 * 864e5).toISOString();
246
+ const toIso = now.toISOString();
247
+ const hourlyCutoffMs = now.getTime() - 24 * 36e5;
248
+ const url = `${apiUrl}/v1/memory/by-period?projectId=${encodeURIComponent(anchor.projectId)}&from=${encodeURIComponent(hourlyFromIso)}&to=${encodeURIComponent(toIso)}&type=hourly&limit=50`;
249
+ let hourlies = [];
250
+ try {
251
+ const res = await fetch(url, { headers: { "Authorization": `Bearer ${apiKey}` } });
252
+ if (res.ok) {
253
+ const data = await res.json();
254
+ hourlies = data.memories || [];
255
+ }
256
+ } catch {
257
+ }
258
+ const recent = hourlies.filter((h) => h.windowEnd && new Date(h.windowEnd).getTime() >= hourlyCutoffMs).slice(-HOURLY_CAP);
259
+ if (recent.length === 0)
260
+ return "";
261
+ const parts = [];
262
+ parts.push(`[GrepRAG memory: ${anchor.projectName}]`);
263
+ parts.push("");
264
+ parts.push("Recent sessions:");
265
+ for (const h of recent) {
266
+ if (!h.windowEnd)
267
+ continue;
268
+ parts.push("");
269
+ parts.push(`${fmtHoursAgo(h.windowEnd, now)}:`);
270
+ parts.push(stripRecapNoise(h.content.trim(), "hourly"));
271
+ }
272
+ return parts.join("\n");
273
+ }
274
+
275
+ // src/crush/crush-types.ts
276
+ function estimateTokens(text) {
277
+ return Math.ceil(text.length / 4);
278
+ }
279
+ function makePassthrough(content, sourceType, reason, units = 0) {
280
+ return {
281
+ output: content,
282
+ stats: {
283
+ sourceType,
284
+ originalChars: content.length,
285
+ compressedChars: content.length,
286
+ compressionRatio: 1,
287
+ originalUnits: units,
288
+ keptUnits: units,
289
+ passthrough: true,
290
+ passthroughReason: reason
291
+ }
292
+ };
293
+ }
294
+
295
+ // src/crush/adaptive-sizer.ts
296
+ function fnv1a(s, seed) {
297
+ let h = (2166136261 ^ seed) >>> 0;
298
+ for (let i = 0; i < s.length; i++) {
299
+ h ^= s.charCodeAt(i);
300
+ h = Math.imul(h, 16777619);
301
+ }
302
+ return h >>> 0;
303
+ }
304
+ function simhash(s) {
305
+ const votes1 = new Array(32).fill(0);
306
+ const votes2 = new Array(32).fill(0);
307
+ const text = s.length < 4 ? s + " ".repeat(4 - s.length) : s;
308
+ for (let i = 0; i + 4 <= text.length; i++) {
309
+ const gram = text.slice(i, i + 4);
310
+ const h1 = fnv1a(gram, 0);
311
+ const h2 = fnv1a(gram, 2654435769);
312
+ for (let b = 0; b < 32; b++) {
313
+ votes1[b] += h1 >>> b & 1 ? 1 : -1;
314
+ votes2[b] += h2 >>> b & 1 ? 1 : -1;
315
+ }
316
+ }
317
+ let out = "";
318
+ for (let b = 0; b < 32; b++)
319
+ out += votes1[b] >= 0 ? "1" : "0";
320
+ for (let b = 0; b < 32; b++)
321
+ out += votes2[b] >= 0 ? "1" : "0";
322
+ return out;
323
+ }
324
+ function countUniqueSimhash(items) {
325
+ const seen = /* @__PURE__ */ new Set();
326
+ for (const item of items)
327
+ seen.add(simhash(item));
328
+ return seen.size;
329
+ }
330
+ function computeUniqueBigramCurve(items) {
331
+ const seen = /* @__PURE__ */ new Set();
332
+ const curve = [];
333
+ for (const item of items) {
334
+ const words = item.split(/\s+/).filter((w) => w.length > 0);
335
+ if (words.length === 0) {
336
+ seen.add(" ");
337
+ } else if (words.length === 1) {
338
+ seen.add(words[0] + " ");
339
+ } else {
340
+ for (let i = 0; i + 1 < words.length; i++) {
341
+ seen.add(words[i] + " " + words[i + 1]);
342
+ }
343
+ }
344
+ curve.push(seen.size);
345
+ }
346
+ return curve;
347
+ }
348
+ function findKnee(curve) {
349
+ const n = curve.length;
350
+ if (n < 3)
351
+ return null;
352
+ const yMin = curve[0];
353
+ const yMax = curve[n - 1];
354
+ if (Math.abs(yMax - yMin) < Number.EPSILON)
355
+ return 1;
356
+ const xRange = n - 1;
357
+ const yRange = yMax - yMin;
358
+ let maxDiff = -1;
359
+ let kneeIdx = null;
360
+ for (let i = 0; i < n; i++) {
361
+ const xNorm = i / xRange;
362
+ const yNorm = (curve[i] - yMin) / yRange;
363
+ const diff = yNorm - xNorm;
364
+ if (diff > maxDiff) {
365
+ maxDiff = diff;
366
+ kneeIdx = i;
367
+ }
368
+ }
369
+ if (maxDiff < 0.05)
370
+ return null;
371
+ return kneeIdx === null ? null : kneeIdx + 1;
372
+ }
373
+ function computeOptimalK(items, bias, minK, maxK) {
374
+ const n = items.length;
375
+ const effectiveMax = maxK ?? n;
376
+ if (n <= 8)
377
+ return n;
378
+ const uniqueCount = countUniqueSimhash(items);
379
+ if (uniqueCount <= 3) {
380
+ return Math.min(Math.max(minK, uniqueCount), effectiveMax);
381
+ }
382
+ const curve = computeUniqueBigramCurve(items);
383
+ let knee = findKnee(curve);
384
+ const diversityRatio = uniqueCount / n;
385
+ if (knee === null) {
386
+ const keepFraction = 0.3 + 0.7 * diversityRatio;
387
+ knee = Math.max(minK, Math.floor(n * keepFraction));
388
+ } else if (diversityRatio > 0.7) {
389
+ const floor = Math.max(minK, Math.floor(n * (0.3 + 0.7 * diversityRatio)));
390
+ knee = Math.max(knee, floor);
391
+ }
392
+ let k = Math.max(minK, Math.floor(knee * bias));
393
+ k = Math.min(k, effectiveMax);
394
+ return Math.max(minK, Math.min(k, effectiveMax));
395
+ }
396
+
397
+ // src/crush/crush-keywords.ts
398
+ var ERROR_KEYWORDS = [
399
+ "error",
400
+ "exception",
401
+ "fail",
402
+ "failed",
403
+ "failure",
404
+ "fatal",
405
+ "critical",
406
+ "crash",
407
+ "panic",
408
+ "abort",
409
+ "timeout",
410
+ "denied",
411
+ "rejected"
412
+ ];
413
+ var JSON_ERROR_KEYWORDS = [
414
+ "error",
415
+ "exception",
416
+ "failed",
417
+ "failure",
418
+ "critical",
419
+ "fatal",
420
+ "crash",
421
+ "panic",
422
+ "abort",
423
+ "timeout",
424
+ "denied",
425
+ "rejected"
426
+ ];
427
+ var WARNING_KEYWORDS = ["warn", "warning"];
428
+ var IMPORTANCE_KEYWORDS = [
429
+ "important",
430
+ "note",
431
+ "todo",
432
+ "fixme",
433
+ "hack",
434
+ "xxx",
435
+ "bug",
436
+ "fix"
437
+ ];
438
+ function isWordChar(ch) {
439
+ return /[A-Za-z0-9_]/.test(ch);
440
+ }
441
+ function containsKeyword(line, keyword) {
442
+ const haystack = line.toLowerCase();
443
+ const needle = keyword.toLowerCase();
444
+ let from = 0;
445
+ for (; ; ) {
446
+ const idx = haystack.indexOf(needle, from);
447
+ if (idx === -1)
448
+ return false;
449
+ const leftOk = idx === 0 || !isWordChar(haystack[idx - 1]);
450
+ const right = idx + needle.length;
451
+ const rightOk = right === haystack.length || !isWordChar(haystack[right]);
452
+ if (leftOk && rightOk)
453
+ return true;
454
+ from = idx + 1;
455
+ }
456
+ }
457
+ function containsAnyKeyword(line, keywords) {
458
+ return keywords.some((kw) => containsKeyword(line, kw));
459
+ }
460
+ function classifyImportance(line) {
461
+ if (containsAnyKeyword(line, ERROR_KEYWORDS))
462
+ return "error";
463
+ if (containsAnyKeyword(line, WARNING_KEYWORDS))
464
+ return "warning";
465
+ if (containsAnyKeyword(line, IMPORTANCE_KEYWORDS))
466
+ return "importance";
467
+ return null;
468
+ }
469
+
470
+ // src/crush/search-compressor.ts
471
+ var DEFAULT_SEARCH_CONFIG = {
472
+ maxMatchesPerFile: 5,
473
+ alwaysKeepFirst: true,
474
+ alwaysKeepLast: true,
475
+ maxTotalMatches: 30,
476
+ maxFiles: 15,
477
+ contextKeywords: [],
478
+ boostErrors: true,
479
+ bias: 1
480
+ };
481
+ function parseMatchLine(line) {
482
+ const isDrivePrefixed = line.length >= 3 && /[A-Za-z]/.test(line[0]) && line[1] === ":" && (line[2] === "\\" || line[2] === "/");
483
+ let i = isDrivePrefixed ? 2 : 0;
484
+ for (; i < line.length; i++) {
485
+ const ch = line[i];
486
+ if (ch !== ":" && ch !== "-")
487
+ continue;
488
+ const prev = line[i - 1];
489
+ if (i > 0 && (prev === ":" || prev === "-"))
490
+ continue;
491
+ const digitsStart = i + 1;
492
+ let j = digitsStart;
493
+ while (j < line.length && line[j] >= "0" && line[j] <= "9")
494
+ j++;
495
+ if (j > digitsStart && j < line.length && (line[j] === ":" || line[j] === "-")) {
496
+ if (i === 0)
497
+ return null;
498
+ const lineNumber = parseInt(line.slice(digitsStart, j), 10);
499
+ if (!Number.isFinite(lineNumber))
500
+ return null;
501
+ return { file: line.slice(0, i), lineNumber, content: line.slice(j + 1) };
502
+ }
503
+ }
504
+ return null;
505
+ }
506
+ function parseSearchResults(content) {
507
+ const out = /* @__PURE__ */ new Map();
508
+ for (const raw of content.split("\n")) {
509
+ const line = raw.trim();
510
+ if (!line)
511
+ continue;
512
+ const parsed = parseMatchLine(line);
513
+ if (!parsed)
514
+ continue;
515
+ let fm = out.get(parsed.file);
516
+ if (!fm) {
517
+ fm = { file: parsed.file, matches: [] };
518
+ out.set(parsed.file, fm);
519
+ }
520
+ fm.matches.push({ ...parsed, score: 0 });
521
+ }
522
+ return out;
523
+ }
524
+ function scoreMatches(files, query, config) {
525
+ const queryWords = query.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
526
+ for (const fm of files.values()) {
527
+ for (const m of fm.matches) {
528
+ let score = 0;
529
+ const lower = m.content.toLowerCase();
530
+ for (const w of queryWords) {
531
+ if (lower.includes(w))
532
+ score += 0.3;
533
+ }
534
+ if (config.boostErrors) {
535
+ const cat = classifyImportance(m.content);
536
+ if (cat === "error")
537
+ score += 0.5;
538
+ else if (cat === "warning")
539
+ score += 0.4;
540
+ else if (cat === "importance")
541
+ score += 0.3;
542
+ }
543
+ for (const kw of config.contextKeywords) {
544
+ if (lower.includes(kw.toLowerCase()))
545
+ score += 0.4;
546
+ }
547
+ m.score = Math.min(score, 1);
548
+ }
549
+ }
550
+ }
551
+ function totalScore(fm) {
552
+ return fm.matches.reduce((s, m) => s + m.score, 0);
553
+ }
554
+ function selectMatches(files, config) {
555
+ let byScore = [...files.values()].sort((a, b) => totalScore(b) - totalScore(a));
556
+ if (byScore.length > config.maxFiles)
557
+ byScore = byScore.slice(0, config.maxFiles);
558
+ const allMatchStrings = byScore.flatMap(
559
+ (fm) => fm.matches.map((m) => `${m.file}:${m.lineNumber}:${m.content}`)
560
+ );
561
+ const adaptiveTotal = computeOptimalK(allMatchStrings, config.bias, 5, config.maxTotalMatches);
562
+ const selected = /* @__PURE__ */ new Map();
563
+ let totalSelected = 0;
564
+ for (const fm of byScore) {
565
+ if (totalSelected >= adaptiveTotal)
566
+ continue;
567
+ const sorted = [...fm.matches].sort((a, b) => b.score - a.score || a.lineNumber - b.lineNumber);
568
+ const fileSelected = [];
569
+ const seen = /* @__PURE__ */ new Set();
570
+ const remainingCap = Math.min(config.maxMatchesPerFile, adaptiveTotal - totalSelected);
571
+ const pushUnique = (m) => {
572
+ const key = `${m.lineNumber}${m.content}`;
573
+ if (!seen.has(key)) {
574
+ seen.add(key);
575
+ fileSelected.push(m);
576
+ }
577
+ };
578
+ if (config.alwaysKeepFirst && fm.matches.length > 0 && fileSelected.length < remainingCap) {
579
+ pushUnique(fm.matches[0]);
580
+ }
581
+ if (config.alwaysKeepLast && fm.matches.length > 1 && fileSelected.length < remainingCap) {
582
+ pushUnique(fm.matches[fm.matches.length - 1]);
583
+ }
584
+ for (const m of sorted) {
585
+ if (fileSelected.length >= remainingCap)
586
+ break;
587
+ pushUnique(m);
588
+ }
589
+ fileSelected.sort((a, b) => a.lineNumber - b.lineNumber);
590
+ totalSelected += fileSelected.length;
591
+ selected.set(fm.file, { file: fm.file, matches: fileSelected });
592
+ }
593
+ return selected;
594
+ }
595
+ function formatOutput(selected, original) {
596
+ const lines = [];
597
+ for (const [file, fm] of selected) {
598
+ for (const m of fm.matches) {
599
+ lines.push(`${m.file}:${m.lineNumber}:${m.content}`);
600
+ }
601
+ const orig = original.get(file);
602
+ if (orig && orig.matches.length > fm.matches.length) {
603
+ lines.push(`[... and ${orig.matches.length - fm.matches.length} more matches in ${file}]`);
604
+ }
605
+ }
606
+ return lines.join("\n");
607
+ }
608
+ function crushSearch(content, query = "", config = {}) {
609
+ const cfg = { ...DEFAULT_SEARCH_CONFIG, ...config };
610
+ const parsed = parseSearchResults(content);
611
+ const originalCount = [...parsed.values()].reduce((s, fm) => s + fm.matches.length, 0);
612
+ if (originalCount === 0) {
613
+ return makePassthrough(content, "search", "no parseable matches");
614
+ }
615
+ scoreMatches(parsed, query, cfg);
616
+ const selected = selectMatches(parsed, cfg);
617
+ const output = formatOutput(selected, parsed);
618
+ const keptCount = [...selected.values()].reduce((s, fm) => s + fm.matches.length, 0);
619
+ return {
620
+ output,
621
+ stats: {
622
+ sourceType: "search",
623
+ originalChars: content.length,
624
+ compressedChars: output.length,
625
+ compressionRatio: output.length / Math.max(content.length, 1),
626
+ originalUnits: originalCount,
627
+ keptUnits: keptCount,
628
+ passthrough: false
629
+ }
630
+ };
631
+ }
632
+
633
+ // src/crush/log-compressor.ts
634
+ var DEFAULT_LOG_CONFIG = {
635
+ maxErrors: 10,
636
+ errorContextLines: 3,
637
+ keepFirstError: true,
638
+ keepLastError: true,
639
+ maxStackTraces: 3,
640
+ stackTraceMaxLines: 20,
641
+ maxWarnings: 5,
642
+ dedupeWarnings: true,
643
+ keepSummaryLines: true,
644
+ maxTotalLines: 100,
645
+ minLinesToCompress: 50,
646
+ bias: 1
647
+ };
648
+ var LEVEL_PATTERNS = [
649
+ ["error", /\b(?:ERROR|error|Error|FATAL|fatal|Fatal|CRITICAL|critical)\b/],
650
+ ["fail", /\b(?:FAIL|FAILED|fail|failed|Fail|Failed)\b/],
651
+ ["warn", /\b(?:WARN|WARNING|warn|warning|Warn|Warning)\b/],
652
+ ["info", /\b(?:INFO|info|Info)\b/],
653
+ ["debug", /\b(?:DEBUG|debug|Debug)\b/],
654
+ ["trace", /\b(?:TRACE|trace|Trace)\b/]
655
+ ];
656
+ function classifyLevel(line) {
657
+ for (const [level, re] of LEVEL_PATTERNS) {
658
+ if (re.test(line))
659
+ return level;
660
+ }
661
+ return "unknown";
662
+ }
663
+ function hasLineColSuffix(s) {
664
+ return /:\d+:\d/.test(s);
665
+ }
666
+ function traceFlavorFor(line) {
667
+ const t = line.trimStart();
668
+ if (t.startsWith("Traceback (most recent call last)"))
669
+ return "python";
670
+ if (t.startsWith('File "') && t.includes('", line ') && /\d$/.test(t))
671
+ return "python";
672
+ if (t.startsWith("at ") && t.includes("(") && t.includes(")") && hasLineColSuffix(t))
673
+ return "js";
674
+ if (t.startsWith("at ") && t.includes("(")) {
675
+ const body = t.slice(3, t.indexOf("("));
676
+ if (body.length > 0 && /^[A-Za-z0-9._$]+$/.test(body))
677
+ return "java";
678
+ }
679
+ if (t.startsWith("--> ") && hasLineColSuffix(t))
680
+ return "rust";
681
+ if (/^\s*\d+:\s*0x[0-9a-fA-F]/.test(line))
682
+ return "go";
683
+ return null;
684
+ }
685
+ function traceTerminates(flavor, line) {
686
+ const t = line.trimStart();
687
+ switch (flavor) {
688
+ case "python": {
689
+ const indentedOrBlank = line.startsWith(" ") || line.startsWith(" ") || line.length === 0;
690
+ const continuation = t.startsWith("Traceback") || t.startsWith("File ") || t.startsWith("During handling") || t.startsWith("The above exception");
691
+ if (indentedOrBlank || continuation)
692
+ return false;
693
+ return !/^[A-Z]/.test(t);
694
+ }
695
+ case "js":
696
+ case "java":
697
+ return !t.startsWith("at ") && line.length > 0;
698
+ case "rust":
699
+ return !t.startsWith("--> ") && line.length > 0;
700
+ case "go":
701
+ return !/^\d/.test(t) && line.length > 0;
702
+ }
703
+ }
704
+ function isSummaryLine(line) {
705
+ if (line.startsWith("===") || line.startsWith("---"))
706
+ return true;
707
+ const m = line.match(/^(\d+) /);
708
+ if (m) {
709
+ const rest = line.slice(m[1].length + 1);
710
+ if (/^(passed|failed|skipped|error|warning)/.test(rest))
711
+ return true;
712
+ }
713
+ for (const prefix of ["Test ", "Tests ", "Tests:", "Test:", "Suite ", "Suites ", "Suites:", "Suite:"]) {
714
+ if (line.startsWith(prefix)) {
715
+ const rest = line.slice(prefix.length).trimStart();
716
+ return rest.length > 0 && rest[0] >= "0" && rest[0] <= "9";
717
+ }
718
+ }
719
+ if (/^(TOTAL|Total|Summary)/.test(line))
720
+ return true;
721
+ if (/^(Build|Compile|Test)/.test(line) && /(succeeded|failed|complete)/.test(line))
722
+ return true;
723
+ return false;
724
+ }
725
+ function scoreLogLine(line) {
726
+ let levelScore;
727
+ switch (line.level) {
728
+ case "error":
729
+ case "fail":
730
+ levelScore = 1;
731
+ break;
732
+ case "warn":
733
+ levelScore = 0.5;
734
+ break;
735
+ case "debug":
736
+ levelScore = 0.05;
737
+ break;
738
+ case "trace":
739
+ levelScore = 0.02;
740
+ break;
741
+ default:
742
+ levelScore = 0.1;
743
+ }
744
+ const stackBoost = line.isStackTrace ? 0.3 : 0;
745
+ const summaryBoost = line.isSummary ? 0.4 : 0;
746
+ return Math.min(levelScore + stackBoost + summaryBoost, 1);
747
+ }
748
+ function parseLogLines(lines, config) {
749
+ const out = [];
750
+ let active = null;
751
+ let traceLines = 0;
752
+ for (let i = 0; i < lines.length; i++) {
753
+ const raw = lines[i];
754
+ const entry = {
755
+ lineNumber: i,
756
+ content: raw,
757
+ level: classifyLevel(raw),
758
+ isStackTrace: false,
759
+ isSummary: isSummaryLine(raw),
760
+ score: 0
761
+ };
762
+ if (active !== null) {
763
+ if (traceLines >= config.stackTraceMaxLines || traceTerminates(active, raw)) {
764
+ active = null;
765
+ traceLines = 0;
766
+ const next = traceFlavorFor(raw);
767
+ if (next) {
768
+ active = next;
769
+ traceLines = 1;
770
+ entry.isStackTrace = true;
771
+ }
772
+ } else {
773
+ entry.isStackTrace = true;
774
+ traceLines++;
775
+ }
776
+ } else {
777
+ const flavor = traceFlavorFor(raw);
778
+ if (flavor) {
779
+ active = flavor;
780
+ traceLines = 1;
781
+ entry.isStackTrace = true;
782
+ }
783
+ }
784
+ entry.score = scoreLogLine(entry);
785
+ out.push(entry);
786
+ }
787
+ return out;
788
+ }
789
+ function normalizeForDedupe(content) {
790
+ const splitAt = content.search(/[:=]/);
791
+ const at = splitAt === -1 ? content.length : splitAt;
792
+ const prefix = content.slice(0, at);
793
+ const suffix = content.slice(at).replace(/0x[0-9a-fA-F]+/g, "ADDR").replace(/\d+/g, "N").replace(/\/[\w/]+\//g, "/PATH/");
794
+ return prefix + suffix;
795
+ }
796
+ function dedupeSimilar(lines) {
797
+ const seen = /* @__PURE__ */ new Set();
798
+ const out = [];
799
+ for (const line of lines) {
800
+ const key = normalizeForDedupe(line.content);
801
+ if (!seen.has(key)) {
802
+ seen.add(key);
803
+ out.push(line);
804
+ }
805
+ }
806
+ return out;
807
+ }
808
+ function selectWithFirstLast(lines, maxCount, config) {
809
+ if (lines.length <= maxCount)
810
+ return [...lines];
811
+ const out = [];
812
+ const seen = /* @__PURE__ */ new Set();
813
+ const push = (l) => {
814
+ if (!seen.has(l.lineNumber)) {
815
+ seen.add(l.lineNumber);
816
+ out.push(l);
817
+ }
818
+ };
819
+ if (config.keepFirstError)
820
+ push(lines[0]);
821
+ if (config.keepLastError)
822
+ push(lines[lines.length - 1]);
823
+ const byScore = [...lines].sort((a, b) => b.score - a.score || a.lineNumber - b.lineNumber);
824
+ for (const l of byScore) {
825
+ if (out.length >= maxCount)
826
+ break;
827
+ push(l);
828
+ }
829
+ return out;
830
+ }
831
+ function selectLines(logLines, config) {
832
+ const adaptiveMax = computeOptimalK(
833
+ logLines.map((l) => l.content),
834
+ config.bias,
835
+ 10,
836
+ config.maxTotalLines
837
+ );
838
+ const errors = [];
839
+ const fails = [];
840
+ let warnings = [];
841
+ const summaries = [];
842
+ const stackTraces = [];
843
+ let currentStack = [];
844
+ for (const line of logLines) {
845
+ if (line.level === "error")
846
+ errors.push(line);
847
+ else if (line.level === "fail")
848
+ fails.push(line);
849
+ else if (line.level === "warn")
850
+ warnings.push(line);
851
+ if (line.isStackTrace) {
852
+ currentStack.push(line);
853
+ } else if (currentStack.length > 0) {
854
+ stackTraces.push(currentStack);
855
+ currentStack = [];
856
+ }
857
+ if (line.isSummary)
858
+ summaries.push(line);
859
+ }
860
+ if (currentStack.length > 0)
861
+ stackTraces.push(currentStack);
862
+ const selected = /* @__PURE__ */ new Map();
863
+ const add = (l) => selected.set(l.lineNumber, l);
864
+ for (const l of selectWithFirstLast(errors, config.maxErrors, config))
865
+ add(l);
866
+ for (const l of selectWithFirstLast(fails, config.maxErrors, config))
867
+ add(l);
868
+ if (config.dedupeWarnings)
869
+ warnings = dedupeSimilar(warnings);
870
+ for (const l of warnings.slice(0, config.maxWarnings))
871
+ add(l);
872
+ for (const stack of stackTraces.slice(0, config.maxStackTraces)) {
873
+ for (const l of stack.slice(0, config.stackTraceMaxLines))
874
+ add(l);
875
+ }
876
+ if (config.keepSummaryLines) {
877
+ for (const l of summaries)
878
+ add(l);
879
+ }
880
+ const anchorIndices = [...selected.keys()];
881
+ for (const idx of anchorIndices) {
882
+ const lo = Math.max(0, idx - config.errorContextLines);
883
+ const hi = Math.min(logLines.length, idx + config.errorContextLines + 1);
884
+ for (let i = lo; i < hi; i++) {
885
+ if (!selected.has(i))
886
+ add(logLines[i]);
887
+ }
888
+ }
889
+ let ordered = [...selected.values()].sort((a, b) => a.lineNumber - b.lineNumber);
890
+ if (ordered.length > adaptiveMax) {
891
+ ordered.sort((a, b) => b.score - a.score || a.lineNumber - b.lineNumber);
892
+ ordered = ordered.slice(0, adaptiveMax);
893
+ ordered.sort((a, b) => a.lineNumber - b.lineNumber);
894
+ }
895
+ return ordered;
896
+ }
897
+ function formatLogOutput(selected, allLines) {
898
+ const count = (level) => allLines.filter((l) => l.level === level).length;
899
+ const output = selected.map((l) => l.content);
900
+ const omitted = allLines.length - selected.length;
901
+ if (omitted > 0) {
902
+ const parts = [];
903
+ for (const [label, level] of [
904
+ ["ERROR", "error"],
905
+ ["FAIL", "fail"],
906
+ ["WARN", "warn"],
907
+ ["INFO", "info"]
908
+ ]) {
909
+ const n = count(level);
910
+ if (n > 0)
911
+ parts.push(`${n} ${label}`);
912
+ }
913
+ if (parts.length > 0) {
914
+ output.push(`[${omitted} lines omitted: ${parts.join(", ")}]`);
915
+ } else {
916
+ output.push(`[${omitted} lines omitted]`);
917
+ }
918
+ }
919
+ return output.join("\n");
920
+ }
921
+ function crushLog(content, config = {}) {
922
+ const cfg = { ...DEFAULT_LOG_CONFIG, ...config };
923
+ const lines = content.split("\n");
924
+ if (lines.length < cfg.minLinesToCompress) {
925
+ return makePassthrough(content, "log", "below min line count", lines.length);
926
+ }
927
+ const logLines = parseLogLines(lines, cfg);
928
+ const selected = selectLines(logLines, cfg);
929
+ const output = formatLogOutput(selected, logLines);
930
+ return {
931
+ output,
932
+ stats: {
933
+ sourceType: "log",
934
+ originalChars: content.length,
935
+ compressedChars: output.length,
936
+ compressionRatio: output.length / Math.max(content.length, 1),
937
+ originalUnits: lines.length,
938
+ keptUnits: selected.length,
939
+ passthrough: false
940
+ }
941
+ };
942
+ }
943
+
944
+ // src/crush/json-detectors.ts
945
+ function classifyArray(items) {
946
+ if (items.length === 0)
947
+ return "empty";
948
+ let hasBool = false, hasNumber = false, hasString = false, hasObject = false, hasArray = false, hasNull = false;
949
+ for (const item of items) {
950
+ if (item === null)
951
+ hasNull = true;
952
+ else if (typeof item === "boolean")
953
+ hasBool = true;
954
+ else if (typeof item === "number")
955
+ hasNumber = true;
956
+ else if (typeof item === "string")
957
+ hasString = true;
958
+ else if (Array.isArray(item))
959
+ hasArray = true;
960
+ else
961
+ hasObject = true;
962
+ }
963
+ if (hasBool && !hasNumber && !hasString && !hasObject && !hasArray && !hasNull)
964
+ return "bool_array";
965
+ if (hasObject && !hasBool && !hasNumber && !hasString && !hasArray && !hasNull)
966
+ return "dict_array";
967
+ if (hasString && !hasBool && !hasNumber && !hasObject && !hasArray && !hasNull)
968
+ return "string_array";
969
+ if (hasNumber && !hasBool && !hasString && !hasObject && !hasArray && !hasNull)
970
+ return "number_array";
971
+ if (hasArray && !hasBool && !hasNumber && !hasString && !hasObject && !hasNull)
972
+ return "nested_array";
973
+ return "mixed_array";
974
+ }
975
+ function isPlainObject(v) {
976
+ return typeof v === "object" && v !== null && !Array.isArray(v);
977
+ }
978
+ function stringifyScalar(v) {
979
+ if (typeof v === "string")
980
+ return v;
981
+ return JSON.stringify(v);
982
+ }
983
+ function detectSequentialPattern(values) {
984
+ if (values.length < 5)
985
+ return false;
986
+ const nums = [];
987
+ let hadNonStringNumeric = false;
988
+ for (const v of values) {
989
+ if (typeof v === "number") {
990
+ nums.push(v);
991
+ hadNonStringNumeric = true;
992
+ } else if (typeof v === "string" && /^[+-]?\d+$/.test(v.trim())) {
993
+ nums.push(parseInt(v.trim(), 10));
994
+ }
995
+ }
996
+ if (nums.length < 5)
997
+ return false;
998
+ if (!hadNonStringNumeric)
999
+ return false;
1000
+ const sorted = [...nums].sort((a, b) => a - b);
1001
+ const diffs = [];
1002
+ for (let i = 1; i < sorted.length; i++)
1003
+ diffs.push(sorted[i] - sorted[i - 1]);
1004
+ if (diffs.length === 0)
1005
+ return false;
1006
+ const avg = diffs.reduce((s, d) => s + d, 0) / diffs.length;
1007
+ if (avg < 0.5 || avg > 2)
1008
+ return false;
1009
+ const consistent = diffs.filter((d) => d >= 0.5 && d <= 2).length;
1010
+ return consistent / diffs.length > 0.8;
1011
+ }
1012
+ function detectRareStatusValues(items, commonFields) {
1013
+ const outliers = [];
1014
+ const sortedFields = [...commonFields].sort();
1015
+ for (const field of sortedFields) {
1016
+ const values = [];
1017
+ for (const item of items) {
1018
+ if (isPlainObject(item) && field in item)
1019
+ values.push(item[field]);
1020
+ }
1021
+ const uniqueValues = /* @__PURE__ */ new Set();
1022
+ for (const v of values) {
1023
+ if (v !== null)
1024
+ uniqueValues.add(stringifyScalar(v));
1025
+ }
1026
+ if (uniqueValues.size < 2 || uniqueValues.size > 50)
1027
+ continue;
1028
+ const counts = /* @__PURE__ */ new Map();
1029
+ for (const v of values) {
1030
+ const key = v === null ? "__none__" : stringifyScalar(v);
1031
+ counts.set(key, (counts.get(key) ?? 0) + 1);
1032
+ }
1033
+ if (counts.size === 0)
1034
+ continue;
1035
+ const sortedCounts = [...counts.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
1036
+ const threshold = Math.ceil(values.length * 0.8);
1037
+ let cumulative = 0;
1038
+ const topK = /* @__PURE__ */ new Set();
1039
+ for (const [value, count] of sortedCounts) {
1040
+ cumulative += count;
1041
+ topK.add(value);
1042
+ if (cumulative >= threshold)
1043
+ break;
1044
+ }
1045
+ if (topK.size > 5)
1046
+ continue;
1047
+ for (let i = 0; i < items.length; i++) {
1048
+ const item = items[i];
1049
+ if (!isPlainObject(item) || !(field in item))
1050
+ continue;
1051
+ const v = item[field];
1052
+ const key = v === null ? "__none__" : stringifyScalar(v);
1053
+ if (!topK.has(key))
1054
+ outliers.push(i);
1055
+ }
1056
+ }
1057
+ return outliers;
1058
+ }
1059
+ function detectStructuralOutliers(items) {
1060
+ if (items.length < 5)
1061
+ return [];
1062
+ const fieldCounts = /* @__PURE__ */ new Map();
1063
+ for (const item of items) {
1064
+ if (!isPlainObject(item))
1065
+ continue;
1066
+ for (const key of Object.keys(item)) {
1067
+ fieldCounts.set(key, (fieldCounts.get(key) ?? 0) + 1);
1068
+ }
1069
+ }
1070
+ const n = items.length;
1071
+ const commonFields = /* @__PURE__ */ new Set();
1072
+ const rareFields = /* @__PURE__ */ new Set();
1073
+ for (const [field, count] of fieldCounts) {
1074
+ if (count >= n * 0.8)
1075
+ commonFields.add(field);
1076
+ if (count < n * 0.2)
1077
+ rareFields.add(field);
1078
+ }
1079
+ const outliers = /* @__PURE__ */ new Set();
1080
+ for (let i = 0; i < items.length; i++) {
1081
+ const item = items[i];
1082
+ if (!isPlainObject(item))
1083
+ continue;
1084
+ if (Object.keys(item).some((k) => rareFields.has(k)))
1085
+ outliers.add(i);
1086
+ }
1087
+ for (const idx of detectRareStatusValues(items, commonFields))
1088
+ outliers.add(idx);
1089
+ return [...outliers].sort((a, b) => a - b);
1090
+ }
1091
+ function detectErrorItems(items) {
1092
+ const out = [];
1093
+ for (let i = 0; i < items.length; i++) {
1094
+ const item = items[i];
1095
+ if (!isPlainObject(item))
1096
+ continue;
1097
+ const serialized = JSON.stringify(item).toLowerCase();
1098
+ if (JSON_ERROR_KEYWORDS.some((kw) => serialized.includes(kw)))
1099
+ out.push(i);
1100
+ }
1101
+ return out;
1102
+ }
1103
+
1104
+ // src/crush/json-crusher.ts
1105
+ var DEFAULT_JSON_CONFIG = {
1106
+ minItemsToAnalyze: 10,
1107
+ maxItemsAfterCrush: 15,
1108
+ firstFraction: 0.3,
1109
+ lastFraction: 0.15,
1110
+ bias: 1
1111
+ };
1112
+ function computeKSplit(kTotal, firstFraction, lastFraction) {
1113
+ if (kTotal <= 1)
1114
+ return { first: kTotal, last: 0 };
1115
+ const first = Math.max(1, Math.round(kTotal * firstFraction));
1116
+ const last = Math.max(1, Math.round(kTotal * lastFraction));
1117
+ if (first + last > kTotal)
1118
+ return { first: Math.max(1, kTotal - 1), last: kTotal > 1 ? 1 : 0 };
1119
+ return { first, last };
1120
+ }
1121
+ function sequentialIdField(items) {
1122
+ const first = items[0];
1123
+ if (typeof first !== "object" || first === null || Array.isArray(first))
1124
+ return null;
1125
+ for (const key of Object.keys(first)) {
1126
+ if (!/^(id|.*_id|index|seq|n)$/i.test(key))
1127
+ continue;
1128
+ const values = items.map(
1129
+ (it) => typeof it === "object" && it !== null && !Array.isArray(it) ? it[key] ?? null : null
1130
+ );
1131
+ if (detectSequentialPattern(values))
1132
+ return key;
1133
+ }
1134
+ return null;
1135
+ }
1136
+ function omittedMarker(droppedCount, items, droppedIndices) {
1137
+ const idField = sequentialIdField(items);
1138
+ if (idField && droppedIndices.length > 0) {
1139
+ const firstDropped = items[droppedIndices[0]];
1140
+ const lastDropped = items[droppedIndices[droppedIndices.length - 1]];
1141
+ const idOf = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v[idField] ?? null : null;
1142
+ const lo = idOf(firstDropped);
1143
+ const hi = idOf(lastDropped);
1144
+ if (lo !== null && hi !== null) {
1145
+ return `[... ${droppedCount} items omitted (${idField} ${lo}..${hi}) ...]`;
1146
+ }
1147
+ }
1148
+ return `[... ${droppedCount} items omitted ...]`;
1149
+ }
1150
+ function crushDictArray(items, config) {
1151
+ const n = items.length;
1152
+ const serialized = items.map((it) => JSON.stringify(it));
1153
+ const budget = computeOptimalK(serialized, config.bias, 5, config.maxItemsAfterCrush);
1154
+ const keep = /* @__PURE__ */ new Set();
1155
+ for (const idx of detectErrorItems(items))
1156
+ keep.add(idx);
1157
+ for (const idx of detectStructuralOutliers(items))
1158
+ keep.add(idx);
1159
+ const remaining = Math.max(0, budget - keep.size);
1160
+ if (remaining > 0) {
1161
+ const { first, last } = computeKSplit(remaining, config.firstFraction, config.lastFraction);
1162
+ for (let i = 0; i < Math.min(first, n); i++)
1163
+ keep.add(i);
1164
+ for (let i = Math.max(0, n - last); i < n; i++)
1165
+ keep.add(i);
1166
+ let leftover = budget - keep.size;
1167
+ if (leftover > 0) {
1168
+ const step = Math.max(1, Math.floor(n / (leftover + 1)));
1169
+ for (let i = step; i < n && leftover > 0; i += step) {
1170
+ if (!keep.has(i)) {
1171
+ keep.add(i);
1172
+ leftover--;
1173
+ }
1174
+ }
1175
+ }
1176
+ }
1177
+ if (keep.size >= n) {
1178
+ return { items: [...items], originalCount: n, keptCount: n, crushed: false };
1179
+ }
1180
+ const keptIndices = [...keep].sort((a, b) => a - b);
1181
+ const droppedIndices = [];
1182
+ for (let i = 0; i < n; i++) {
1183
+ if (!keep.has(i))
1184
+ droppedIndices.push(i);
1185
+ }
1186
+ const out = [];
1187
+ let markerPlaced = false;
1188
+ for (let i = 0; i < n; i++) {
1189
+ if (keep.has(i)) {
1190
+ out.push(items[i]);
1191
+ } else if (!markerPlaced) {
1192
+ out.push(omittedMarker(droppedIndices.length, items, droppedIndices));
1193
+ markerPlaced = true;
1194
+ }
1195
+ }
1196
+ return { items: out, originalCount: n, keptCount: keptIndices.length, crushed: true };
1197
+ }
1198
+ function crushScalarArray(items, config) {
1199
+ const n = items.length;
1200
+ const serialized = items.map((it) => JSON.stringify(it));
1201
+ const budget = computeOptimalK(serialized, config.bias, 5, config.maxItemsAfterCrush);
1202
+ if (budget >= n) {
1203
+ return { items: [...items], originalCount: n, keptCount: n, crushed: false };
1204
+ }
1205
+ const keep = /* @__PURE__ */ new Set();
1206
+ const numbers = items.every((it) => typeof it === "number") ? items : null;
1207
+ if (numbers) {
1208
+ const mean = numbers.reduce((s, v) => s + v, 0) / n;
1209
+ const variance = numbers.reduce((s, v) => s + (v - mean) ** 2, 0) / n;
1210
+ const std = Math.sqrt(variance);
1211
+ if (std > 0) {
1212
+ for (let i = 0; i < n; i++) {
1213
+ if (Math.abs(numbers[i] - mean) > 2 * std)
1214
+ keep.add(i);
1215
+ }
1216
+ }
1217
+ }
1218
+ if (!numbers) {
1219
+ for (let i = 0; i < n; i++) {
1220
+ const it = items[i];
1221
+ if (typeof it === "string") {
1222
+ const lower = it.toLowerCase();
1223
+ if (/(error|exception|failed|failure|fatal|panic|timeout)/.test(lower))
1224
+ keep.add(i);
1225
+ }
1226
+ }
1227
+ }
1228
+ const remaining = Math.max(0, budget - keep.size);
1229
+ if (remaining > 0) {
1230
+ const { first, last } = computeKSplit(remaining, config.firstFraction, config.lastFraction);
1231
+ for (let i = 0; i < Math.min(first, n); i++)
1232
+ keep.add(i);
1233
+ for (let i = Math.max(0, n - last); i < n; i++)
1234
+ keep.add(i);
1235
+ }
1236
+ if (keep.size >= n) {
1237
+ return { items: [...items], originalCount: n, keptCount: n, crushed: false };
1238
+ }
1239
+ const out = [];
1240
+ let dropped = 0;
1241
+ let markerAt = -1;
1242
+ for (let i = 0; i < n; i++) {
1243
+ if (keep.has(i)) {
1244
+ out.push(items[i]);
1245
+ } else {
1246
+ dropped++;
1247
+ if (markerAt === -1) {
1248
+ markerAt = out.length;
1249
+ out.push("");
1250
+ }
1251
+ }
1252
+ }
1253
+ if (markerAt >= 0)
1254
+ out[markerAt] = `[... ${dropped} items omitted ...]`;
1255
+ return { items: out, originalCount: n, keptCount: n - dropped, crushed: true };
1256
+ }
1257
+ function walkAndCrush(value, config, totals) {
1258
+ if (Array.isArray(value)) {
1259
+ const recursed = value.map((v) => walkAndCrush(v, config, totals));
1260
+ if (recursed.length < config.minItemsToAnalyze)
1261
+ return recursed;
1262
+ const kind = classifyArray(recursed);
1263
+ let outcome = null;
1264
+ if (kind === "dict_array")
1265
+ outcome = crushDictArray(recursed, config);
1266
+ else if (kind === "string_array" || kind === "number_array")
1267
+ outcome = crushScalarArray(recursed, config);
1268
+ if (outcome && outcome.crushed) {
1269
+ totals.original += outcome.originalCount;
1270
+ totals.kept += outcome.keptCount;
1271
+ totals.crushedArrays++;
1272
+ return outcome.items;
1273
+ }
1274
+ totals.original += recursed.length;
1275
+ totals.kept += recursed.length;
1276
+ return recursed;
1277
+ }
1278
+ if (typeof value === "object" && value !== null) {
1279
+ const out = {};
1280
+ for (const [k, v] of Object.entries(value)) {
1281
+ out[k] = walkAndCrush(v, config, totals);
1282
+ }
1283
+ return out;
1284
+ }
1285
+ return value;
1286
+ }
1287
+ function crushJson(content, config = {}) {
1288
+ const cfg = { ...DEFAULT_JSON_CONFIG, ...config };
1289
+ let parsed;
1290
+ try {
1291
+ parsed = JSON.parse(content);
1292
+ } catch {
1293
+ return makePassthrough(content, "json", "not valid JSON");
1294
+ }
1295
+ const totals = { original: 0, kept: 0, crushedArrays: 0 };
1296
+ const crushedValue = walkAndCrush(parsed, cfg, totals);
1297
+ if (totals.crushedArrays === 0) {
1298
+ return makePassthrough(content, "json", "no qualifying arrays", totals.original);
1299
+ }
1300
+ const output = JSON.stringify(crushedValue);
1301
+ return {
1302
+ output,
1303
+ stats: {
1304
+ sourceType: "json",
1305
+ originalChars: content.length,
1306
+ compressedChars: output.length,
1307
+ compressionRatio: output.length / Math.max(content.length, 1),
1308
+ originalUnits: totals.original,
1309
+ keptUnits: totals.kept,
1310
+ passthrough: false
1311
+ }
1312
+ };
1313
+ }
1314
+
1315
+ // src/crush/code-compressor.ts
1316
+ var DEFAULT_CODE_CONFIG = {
1317
+ minLines: 8,
1318
+ maxBodyLines: 3
1319
+ };
1320
+ function detectCodeLanguage(content) {
1321
+ const braces = (content.match(/[{}]/g) || []).length;
1322
+ let pyDef = 0;
1323
+ let colonBlock = 0;
1324
+ for (const raw of content.split("\n")) {
1325
+ const t = raw.trim();
1326
+ if (/^(?:async\s+def|def)\s+\w+\s*\(/.test(t))
1327
+ pyDef++;
1328
+ if (/:\s*(?:#.*)?$/.test(t) && /^(?:def|class|if|elif|else|for|while|try|except|finally|with|async\s+def|match|case)\b/.test(t)) {
1329
+ colonBlock++;
1330
+ }
1331
+ }
1332
+ if (pyDef > 0 && braces <= pyDef)
1333
+ return "python";
1334
+ if (braces === 0 && colonBlock > 0)
1335
+ return "python";
1336
+ return "cfamily";
1337
+ }
1338
+ var DECL_RE = /^\s*(?:export\s+|default\s+|pub\s+|public\s+|private\s+|protected\s+|static\s+|final\s+|abstract\s+|async\s+)*(?:import\b|from\s+\S+\s+import\b|require\s*\(|use\s+\w|#include\b|package\s+\w|using\s+\w|namespace\s+\w|function\b|func\s+\w|fn\s+\w|def\s+\w|class\s+\w|interface\s+\w|type\s+\w+\s*[=<]|struct\s+\w|enum\s+\w|impl\b|trait\s+\w|const\s+\w|let\s+\w|var\s+\w|val\s+\w)|^\s*@\w[\w.]*\s*(?:\(|$)/;
1339
+ var LOG_LINE_RE = /^\s*(?:\[?\d{4}-\d\d-\d\d|\d\d:\d\d:\d\d|(?:INFO|DEBUG|WARN|WARNING|ERROR|TRACE|FATAL|CRITICAL)\b|npm (?:ERR!|WARN|info)|at\s+\S+\s*\(|Traceback\b|File ")/;
1340
+ function looksProse(t) {
1341
+ if (/[{};=<>]/.test(t))
1342
+ return false;
1343
+ if (!/[.!?]['")]?$/.test(t))
1344
+ return false;
1345
+ return t.split(/\s+/).filter(Boolean).length >= 5;
1346
+ }
1347
+ function looksLikeCode(text) {
1348
+ const lines = text.split("\n").filter((l) => l.trim() !== "").slice(0, 100);
1349
+ const n = lines.length;
1350
+ if (n < 4)
1351
+ return false;
1352
+ let decl = 0;
1353
+ let struct = 0;
1354
+ let log = 0;
1355
+ let prose = 0;
1356
+ for (const l of lines) {
1357
+ const t = l.trim();
1358
+ if (DECL_RE.test(l))
1359
+ decl++;
1360
+ if (/[{};]\s*$/.test(t) || /=>\s*\{?\s*$/.test(t) || /:\s*(?:#.*)?$/.test(t) && /^(?:def|class|if|elif|else|for|while|try|except|finally|with|async\s+def|match|case)\b/.test(t)) {
1361
+ struct++;
1362
+ }
1363
+ if (LOG_LINE_RE.test(l))
1364
+ log++;
1365
+ if (looksProse(t))
1366
+ prose++;
1367
+ }
1368
+ if (log / n >= 0.3)
1369
+ return false;
1370
+ if (prose / n >= 0.5)
1371
+ return false;
1372
+ if (decl < 2)
1373
+ return false;
1374
+ return (decl + struct) / n >= 0.4;
1375
+ }
1376
+ var CONTROL_HEAD_RE = /^(?:if|for|while|switch|catch|do|else|try|finally|with|when|return|throw|=>)\b/;
1377
+ function lineOpensFunc(line) {
1378
+ const t = line.trim();
1379
+ const brace = t.indexOf("{");
1380
+ if (brace === -1)
1381
+ return false;
1382
+ const sig = t.slice(0, brace).trim();
1383
+ if (sig === "")
1384
+ return false;
1385
+ if (sig.startsWith("}"))
1386
+ return false;
1387
+ if (CONTROL_HEAD_RE.test(sig))
1388
+ return false;
1389
+ if (/\bfunction\b/.test(sig))
1390
+ return true;
1391
+ if (/^(?:pub\s+|async\s+|public\s+|private\s+|protected\s+|static\s+|final\s+|export\s+|default\s+|unsafe\s+|inline\s+|virtual\s+|override\s+)*(?:func|fn)\b/.test(sig)) {
1392
+ return true;
1393
+ }
1394
+ if (/=>\s*$/.test(sig))
1395
+ return true;
1396
+ if (/\)\s*(?::\s*[^{]+)?$/.test(sig) && (/\w\s*\(/.test(sig) || sig === ")"))
1397
+ return true;
1398
+ return false;
1399
+ }
1400
+ function braceEvents(line, st) {
1401
+ const events = [];
1402
+ let inBlock = st.inBlockComment;
1403
+ let quote = null;
1404
+ let i = 0;
1405
+ const n = line.length;
1406
+ while (i < n) {
1407
+ const c = line[i];
1408
+ const c2 = i + 1 < n ? line[i + 1] : "";
1409
+ if (inBlock) {
1410
+ if (c === "*" && c2 === "/") {
1411
+ inBlock = false;
1412
+ i += 2;
1413
+ continue;
1414
+ }
1415
+ i++;
1416
+ continue;
1417
+ }
1418
+ if (quote) {
1419
+ if (c === "\\") {
1420
+ i += 2;
1421
+ continue;
1422
+ }
1423
+ if (c === quote)
1424
+ quote = null;
1425
+ i++;
1426
+ continue;
1427
+ }
1428
+ if (c === "/" && c2 === "/")
1429
+ break;
1430
+ if (c === "/" && c2 === "*") {
1431
+ inBlock = true;
1432
+ i += 2;
1433
+ continue;
1434
+ }
1435
+ if (c === '"' || c === "'" || c === "`") {
1436
+ quote = c;
1437
+ i++;
1438
+ continue;
1439
+ }
1440
+ if (c === "{") {
1441
+ events.push("{");
1442
+ i++;
1443
+ continue;
1444
+ }
1445
+ if (c === "}") {
1446
+ events.push("}");
1447
+ i++;
1448
+ continue;
1449
+ }
1450
+ i++;
1451
+ }
1452
+ return { events, st: { inBlockComment: inBlock } };
1453
+ }
1454
+ function leadingWhitespace(line) {
1455
+ return line.slice(0, line.length - line.trimStart().length);
1456
+ }
1457
+ var CodeOutput = class {
1458
+ constructor(commentPrefix) {
1459
+ this.commentPrefix = commentPrefix;
1460
+ this.lines = [];
1461
+ this.kept = 0;
1462
+ this.omitted = [];
1463
+ }
1464
+ keep(line) {
1465
+ this.flush();
1466
+ this.lines.push(line);
1467
+ this.kept++;
1468
+ }
1469
+ drop(line) {
1470
+ this.omitted.push(line);
1471
+ }
1472
+ flush() {
1473
+ if (this.omitted.length === 0)
1474
+ return;
1475
+ const anchor = this.omitted.find((l) => l.trim() !== "") ?? this.omitted[0];
1476
+ const indent = leadingWhitespace(anchor);
1477
+ this.lines.push(`${indent}${this.commentPrefix} [${this.omitted.length} lines omitted]`);
1478
+ this.omitted = [];
1479
+ }
1480
+ };
1481
+ function compressBraces(lines, cfg, commentPrefix) {
1482
+ const out = new CodeOutput(commentPrefix);
1483
+ const stack = [];
1484
+ let scan = { inBlockComment: false };
1485
+ for (const line of lines) {
1486
+ const innermost = innermostFunc(stack);
1487
+ const insideFunc = innermost !== null;
1488
+ const trimmed = line.trim();
1489
+ const inComment = scan.inBlockComment;
1490
+ const { events, st } = braceEvents(line, scan);
1491
+ scan = st;
1492
+ let firstPush = true;
1493
+ let closedFunc = false;
1494
+ const opensFunc = lineOpensFunc(line);
1495
+ for (const ev of events) {
1496
+ if (ev === "{") {
1497
+ const kind = firstPush && opensFunc ? "func" : "other";
1498
+ firstPush = false;
1499
+ stack.push({ kind, budget: kind === "func" ? cfg.maxBodyLines : 0 });
1500
+ } else {
1501
+ const f = stack.pop();
1502
+ if (f && f.kind === "func")
1503
+ closedFunc = true;
1504
+ }
1505
+ }
1506
+ let keep;
1507
+ if (!insideFunc || inComment)
1508
+ keep = true;
1509
+ else if (closedFunc)
1510
+ keep = true;
1511
+ else if (trimmed === "")
1512
+ keep = innermost.budget > 0;
1513
+ else if (innermost.budget > 0) {
1514
+ keep = true;
1515
+ innermost.budget--;
1516
+ } else
1517
+ keep = false;
1518
+ if (keep)
1519
+ out.keep(line);
1520
+ else
1521
+ out.drop(line);
1522
+ }
1523
+ out.flush();
1524
+ return out;
1525
+ }
1526
+ function innermostFunc(stack) {
1527
+ for (let i = stack.length - 1; i >= 0; i--) {
1528
+ if (stack[i].kind === "func")
1529
+ return stack[i];
1530
+ }
1531
+ return null;
1532
+ }
1533
+ var PY_STRUCT_RE = /^(?:@|async\s+def\b|def\b|class\b|import\b|from\b)/;
1534
+ var PY_DEF_RE = /^(?:async\s+def|def)\b/;
1535
+ function compressPython(lines, cfg, commentPrefix) {
1536
+ const out = new CodeOutput(commentPrefix);
1537
+ const stack = [];
1538
+ for (const line of lines) {
1539
+ const stripped = line.trim();
1540
+ if (stripped === "") {
1541
+ const top = stack[stack.length - 1];
1542
+ out_keepOrDrop(out, line, top ? top.budget > 0 : true);
1543
+ continue;
1544
+ }
1545
+ const indent = leadingWhitespace(line).length;
1546
+ while (stack.length && indent <= stack[stack.length - 1].indent)
1547
+ stack.pop();
1548
+ const inside = stack.length > 0;
1549
+ let keep;
1550
+ if (PY_STRUCT_RE.test(stripped))
1551
+ keep = true;
1552
+ else if (!inside)
1553
+ keep = true;
1554
+ else {
1555
+ const top = stack[stack.length - 1];
1556
+ if (top.budget > 0) {
1557
+ keep = true;
1558
+ top.budget--;
1559
+ } else
1560
+ keep = false;
1561
+ }
1562
+ out_keepOrDrop(out, line, keep);
1563
+ if (PY_DEF_RE.test(stripped))
1564
+ stack.push({ indent, budget: cfg.maxBodyLines });
1565
+ }
1566
+ out.flush();
1567
+ return out;
1568
+ }
1569
+ function out_keepOrDrop(out, line, keep) {
1570
+ if (keep)
1571
+ out.keep(line);
1572
+ else
1573
+ out.drop(line);
1574
+ }
1575
+ function crushCode(content, config = {}) {
1576
+ const cfg = { ...DEFAULT_CODE_CONFIG, ...config };
1577
+ const lines = content.split("\n");
1578
+ if (lines.length < cfg.minLines) {
1579
+ return makePassthrough(content, "code", "below min line count", lines.length);
1580
+ }
1581
+ const lang = cfg.language ?? detectCodeLanguage(content);
1582
+ const commentPrefix = cfg.commentPrefix ?? (lang === "python" ? "#" : "//");
1583
+ const out = lang === "python" ? compressPython(lines, cfg, commentPrefix) : compressBraces(lines, cfg, commentPrefix);
1584
+ if (out.kept >= lines.length) {
1585
+ return makePassthrough(content, "code", "no compressible bodies", lines.length);
1586
+ }
1587
+ const output = out.lines.join("\n");
1588
+ return {
1589
+ output,
1590
+ stats: {
1591
+ sourceType: "code",
1592
+ originalChars: content.length,
1593
+ compressedChars: output.length,
1594
+ compressionRatio: output.length / Math.max(content.length, 1),
1595
+ originalUnits: lines.length,
1596
+ keptUnits: out.kept,
1597
+ passthrough: false
1598
+ }
1599
+ };
1600
+ }
1601
+
1602
+ // src/opencode-plugin-crush.ts
1603
+ var CCR_MARKER_HASH_LEN = 16;
1604
+ function ccrMarker(hash) {
1605
+ return `<<ccr:${hash.slice(0, CCR_MARKER_HASH_LEN)}>>`;
1606
+ }
1607
+ var CCR_MARKER_RE = /<<ccr:/;
1608
+ var FINAL_MARKER_OVERHEAD = 1 + "<<ccr:".length + CCR_MARKER_HASH_LEN + ">>".length;
1609
+ var DEFAULT_THRESHOLD_BYTES = 2048;
1610
+ function memoKey(content) {
1611
+ let h1 = 2166136261;
1612
+ let h2 = 5381;
1613
+ for (let i = 0; i < content.length; i++) {
1614
+ const c = content.charCodeAt(i);
1615
+ h1 ^= c;
1616
+ h1 = Math.imul(h1, 16777619);
1617
+ h2 = Math.imul(h2, 33) + c | 0;
1618
+ }
1619
+ return `${content.length.toString(36)}:${(h1 >>> 0).toString(36)}:${(h2 >>> 0).toString(36)}`;
1620
+ }
1621
+ function classifyContent(text) {
1622
+ const t = text.trimStart();
1623
+ const head = t[0];
1624
+ if (head === "{" || head === "[")
1625
+ return "json";
1626
+ const lines = t.split("\n", 21).map((l) => l.trim()).filter(Boolean).slice(0, 20);
1627
+ if (lines.length) {
1628
+ const matchLike = lines.filter((l) => /^.+?[:\-]\d+[:\-]/.test(l)).length;
1629
+ if (matchLike / lines.length >= 0.6)
1630
+ return "search";
1631
+ }
1632
+ if (looksLikeCode(t))
1633
+ return "code";
1634
+ return "log";
1635
+ }
1636
+ function runEngine(type, content) {
1637
+ switch (type) {
1638
+ case "search":
1639
+ return crushSearch(content, "");
1640
+ case "json":
1641
+ return crushJson(content);
1642
+ case "code":
1643
+ return crushCode(content);
1644
+ default:
1645
+ return crushLog(content);
1646
+ }
1647
+ }
1648
+ function crushField(content, stash, thresholdBytes = DEFAULT_THRESHOLD_BYTES) {
1649
+ if (typeof content !== "string")
1650
+ return null;
1651
+ if (CCR_MARKER_RE.test(content))
1652
+ return null;
1653
+ if (Buffer.byteLength(content, "utf8") < thresholdBytes)
1654
+ return null;
1655
+ const type = classifyContent(content);
1656
+ const result = runEngine(type, content);
1657
+ if (result.stats.passthrough)
1658
+ return null;
1659
+ const crushed = result.output;
1660
+ const finalTokens = Math.ceil((crushed.length + FINAL_MARKER_OVERHEAD) / 4);
1661
+ if (finalTokens >= estimateTokens(content))
1662
+ return null;
1663
+ const hash = stash(content, type);
1664
+ return `${crushed}
1665
+ ${ccrMarker(hash)}`;
1666
+ }
1667
+ function crushAndApply(obj, key, stash, thresholdBytes, stats, memo) {
1668
+ const content = obj[key];
1669
+ if (typeof content !== "string")
1670
+ return;
1671
+ const bytes = Buffer.byteLength(content, "utf8");
1672
+ if (bytes < thresholdBytes) {
1673
+ stats.partsSkipped++;
1674
+ return;
1675
+ }
1676
+ if (CCR_MARKER_RE.test(content)) {
1677
+ stats.partsSkipped++;
1678
+ return;
1679
+ }
1680
+ stats.bytesBefore += bytes;
1681
+ let replacement;
1682
+ if (memo) {
1683
+ const k = memoKey(content);
1684
+ if (memo.has(k)) {
1685
+ replacement = memo.get(k) ?? null;
1686
+ stats.memoHits++;
1687
+ } else {
1688
+ replacement = crushField(content, stash, thresholdBytes);
1689
+ stats.crushFieldCalls++;
1690
+ memo.set(k, replacement);
1691
+ }
1692
+ } else {
1693
+ replacement = crushField(content, stash, thresholdBytes);
1694
+ stats.crushFieldCalls++;
1695
+ }
1696
+ if (replacement === null) {
1697
+ stats.partsReverted++;
1698
+ stats.bytesAfter += bytes;
1699
+ return;
1700
+ }
1701
+ obj[key] = replacement;
1702
+ stats.partsCrushed++;
1703
+ stats.bytesAfter += Buffer.byteLength(replacement, "utf8");
1704
+ }
1705
+ function processPart(part, stash, thresholdBytes, stats, memo) {
1706
+ if (!part || typeof part !== "object")
1707
+ return;
1708
+ const p = part;
1709
+ if (p.type === "text" && typeof p.text === "string") {
1710
+ crushAndApply(p, "text", stash, thresholdBytes, stats, memo);
1711
+ return;
1712
+ }
1713
+ if (p.type === "tool" && p.state && typeof p.state === "object") {
1714
+ const state = p.state;
1715
+ if (state.status === "completed" && typeof state.output === "string") {
1716
+ crushAndApply(state, "output", stash, thresholdBytes, stats, memo);
1717
+ }
1718
+ }
1719
+ }
1720
+ function crushMessages(messages, opts) {
1721
+ const stats = {
1722
+ partsCrushed: 0,
1723
+ partsSkipped: 0,
1724
+ partsReverted: 0,
1725
+ bytesBefore: 0,
1726
+ bytesAfter: 0,
1727
+ crushFieldCalls: 0,
1728
+ memoHits: 0
1729
+ };
1730
+ const thresholdBytes = opts.thresholdBytes ?? DEFAULT_THRESHOLD_BYTES;
1731
+ if (!Array.isArray(messages))
1732
+ return stats;
1733
+ for (const msg of messages) {
1734
+ const parts = msg && typeof msg === "object" ? msg.parts : null;
1735
+ if (!Array.isArray(parts))
1736
+ continue;
1737
+ for (const part of parts) {
1738
+ try {
1739
+ processPart(part, opts.stash, thresholdBytes, stats, opts.memo);
1740
+ } catch {
1741
+ stats.partsSkipped++;
1742
+ }
1743
+ }
1744
+ }
1745
+ return stats;
1746
+ }
1747
+
1748
+ // src/opencode-plugin.ts
1749
+ var DEBUG_LOG_PATH = path2.join(os.homedir(), ".greprag", "opencode-plugin-debug.log");
1750
+ var _debugLogReady = false;
1751
+ function dlogInit() {
1752
+ if (_debugLogReady)
1753
+ return;
1754
+ _debugLogReady = true;
1755
+ try {
1756
+ fs2.writeFileSync(DEBUG_LOG_PATH, "");
1757
+ } catch {
1758
+ }
1759
+ }
1760
+ function dlog(msg) {
1761
+ dlogInit();
1762
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [greprag-memory] ${msg}
1763
+ `;
1764
+ try {
1765
+ fs2.appendFileSync(DEBUG_LOG_PATH, line);
1766
+ } catch {
1767
+ }
1768
+ }
1769
+ dlog(`module top reached: pid=${process.pid} argv0=${process.argv[0]} distPath=${__filename}`);
1770
+ var API_URL = "https://api.greprag.com";
1771
+ function loadEnvFile(filePath) {
1772
+ try {
1773
+ if (!fs2.existsSync(filePath))
1774
+ return;
1775
+ const raw = fs2.readFileSync(filePath, "utf-8");
1776
+ for (const line of raw.split(/\r?\n/)) {
1777
+ const trimmed = line.trim();
1778
+ if (!trimmed || trimmed.startsWith("#"))
1779
+ continue;
1780
+ const idx = trimmed.indexOf("=");
1781
+ if (idx < 1)
1782
+ continue;
1783
+ const key = trimmed.slice(0, idx).trim();
1784
+ let value = trimmed.slice(idx + 1).trim();
1785
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1786
+ value = value.slice(1, -1);
1787
+ }
1788
+ if (!process.env[key])
1789
+ process.env[key] = value;
1790
+ }
1791
+ } catch {
1792
+ }
1793
+ }
1794
+ function loadGrepragEnv() {
1795
+ loadEnvFile(path2.join(HOME, ".greprag", ".env"));
1796
+ try {
1797
+ const p = path2.join(HOME, ".claude", "settings.json");
1798
+ if (!fs2.existsSync(p))
1799
+ return;
1800
+ const data = JSON.parse(fs2.readFileSync(p, "utf-8"));
1801
+ if (data && data.env && typeof data.env === "object") {
1802
+ for (const [key, val] of Object.entries(data.env)) {
1803
+ if (!process.env[key] && typeof val === "string") {
1804
+ process.env[key] = val;
1805
+ }
1806
+ }
1807
+ }
1808
+ } catch {
1809
+ }
1810
+ }
1811
+ loadGrepragEnv();
1812
+ dlog(`env loaded: MEMORY_HOOK_ENABLED=${process.env.MEMORY_HOOK_ENABLED || "<unset>"} GREPRAG_API_KEY=${process.env.GREPRAG_API_KEY ? "set" : "<unset>"} GREPRAG_OPENCODE_CAPTURE=${process.env.GREPRAG_OPENCODE_CAPTURE || "<unset>"}`);
1813
+ var WATCHER_LOCK = path2.join(HOME || os.homedir(), ".greprag", "opencode-watch.lock");
1814
+ var WATCHER_STALE_MS = 3e4;
1815
+ function findGrepragBinary() {
1816
+ const override = process.env.GREPRAG_BIN;
1817
+ if (override && fs2.existsSync(override))
1818
+ return override;
1819
+ const binaryName = process.platform === "win32" ? "greprag.cmd" : "greprag";
1820
+ try {
1821
+ const out = safeExecSync(`where ${binaryName}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
1822
+ for (const line of out.split(/\r?\n/)) {
1823
+ const candidate = line.trim();
1824
+ if (!candidate)
1825
+ continue;
1826
+ if (!fs2.existsSync(candidate))
1827
+ continue;
1828
+ if (process.platform === "win32") {
1829
+ const ext = path2.extname(candidate).toLowerCase();
1830
+ if (ext === ".cmd" || ext === ".exe" || ext === ".bat")
1831
+ return candidate;
1832
+ const cmdSibling = candidate + ".cmd";
1833
+ if (fs2.existsSync(cmdSibling))
1834
+ return cmdSibling;
1835
+ continue;
1836
+ }
1837
+ return candidate;
1838
+ }
1839
+ } catch {
1840
+ }
1841
+ const candidates = process.platform === "win32" ? [
1842
+ path2.join(HOME, "AppData", "Roaming", "npm", "greprag.cmd"),
1843
+ path2.join(HOME, "AppData", "Roaming", "npm", "greprag"),
1844
+ path2.join(HOME, "AppData", "Local", "Yarn", "bin", "greprag.cmd"),
1845
+ path2.join(HOME, "AppData", "Local", "pnpm", "bin", "greprag.cmd")
1846
+ ] : [
1847
+ "/usr/local/bin/greprag",
1848
+ "/opt/homebrew/bin/greprag",
1849
+ path2.join(HOME || "", ".local", "bin", "greprag"),
1850
+ path2.join(HOME || "", ".yarn", "bin", "greprag")
1851
+ ];
1852
+ for (const c of candidates) {
1853
+ try {
1854
+ if (c && fs2.existsSync(c))
1855
+ return c;
1856
+ } catch {
1857
+ }
1858
+ }
1859
+ return null;
1860
+ }
1861
+ function tryClaimWatcherLock() {
1862
+ try {
1863
+ const fd = fs2.openSync(WATCHER_LOCK, "wx");
1864
+ return { ok: true, fd };
1865
+ } catch (err) {
1866
+ if (err.code !== "EEXIST") {
1867
+ return { ok: false, reason: "lock-create-failed" };
1868
+ }
1869
+ try {
1870
+ const stat = fs2.statSync(WATCHER_LOCK);
1871
+ const ageMs = Date.now() - stat.mtimeMs;
1872
+ let pidAlive = false;
1873
+ try {
1874
+ const pid = parseInt(fs2.readFileSync(WATCHER_LOCK, "utf-8").trim(), 10);
1875
+ pidAlive = pid > 0 && isPidAlive(pid);
1876
+ } catch {
1877
+ }
1878
+ if (ageMs < WATCHER_STALE_MS && pidAlive) {
1879
+ return { ok: false, reason: "busy" };
1880
+ }
1881
+ try {
1882
+ fs2.unlinkSync(WATCHER_LOCK);
1883
+ } catch {
1884
+ }
1885
+ const fd = fs2.openSync(WATCHER_LOCK, "wx");
1886
+ return { ok: true, fd };
1887
+ } catch (err2) {
1888
+ return { ok: false, reason: "lock-stale-replace-failed" };
1889
+ }
1890
+ }
1891
+ }
1892
+ function startCaptureWatcher() {
1893
+ dlog(`startCaptureWatcher: entry CAPTURE=${process.env.GREPRAG_OPENCODE_CAPTURE || "<unset>"} HOOK=${process.env.MEMORY_HOOK_ENABLED || "<unset>"} KEY=${process.env.GREPRAG_API_KEY ? "set" : "<unset>"}`);
1894
+ if (process.env.GREPRAG_OPENCODE_CAPTURE === "0") {
1895
+ dlog("startCaptureWatcher: bail \u2014 GREPRAG_OPENCODE_CAPTURE=0");
1896
+ return;
1897
+ }
1898
+ if (process.env.MEMORY_HOOK_ENABLED !== "true") {
1899
+ dlog("startCaptureWatcher: bail \u2014 MEMORY_HOOK_ENABLED!=true");
1900
+ return;
1901
+ }
1902
+ if (!process.env.GREPRAG_API_KEY) {
1903
+ dlog("startCaptureWatcher: bail \u2014 no GREPRAG_API_KEY");
1904
+ return;
1905
+ }
1906
+ const grepragBin = findGrepragBinary();
1907
+ dlog(`startCaptureWatcher: findGrepragBinary -> ${grepragBin || "<null>"}`);
1908
+ if (!grepragBin) {
1909
+ process.stderr.write(
1910
+ "[greprag-memory] opencode watch could not start: greprag binary not in PATH. Run `npm install -g @greprag/cli` or set GREPRAG_BIN.\n"
1911
+ );
1912
+ return;
1913
+ }
1914
+ const claim = tryClaimWatcherLock();
1915
+ dlog(`startCaptureWatcher: lock claim ok=${claim.ok}${claim.ok ? "" : " reason=" + claim.reason}`);
1916
+ if (!claim.ok) {
1917
+ if (claim.reason === "busy") {
1918
+ process.stderr.write("[greprag-memory] opencode watch already running; not spawning\n");
1919
+ } else {
1920
+ process.stderr.write(`[greprag-memory] opencode watch could not claim lock: ${claim.reason}
1921
+ `);
1922
+ }
1923
+ return;
1924
+ }
1925
+ try {
1926
+ fs2.writeSync(claim.fd, String(process.pid));
1927
+ fs2.closeSync(claim.fd);
1928
+ } catch {
1929
+ try {
1930
+ fs2.unlinkSync(WATCHER_LOCK);
1931
+ } catch {
1932
+ }
1933
+ return;
1934
+ }
1935
+ let stopped = false;
1936
+ let currentChildPid = null;
1937
+ let currentChild = null;
1938
+ let respawnCount = 0;
1939
+ let childStartTime = 0;
1940
+ const MAX_RESPAWNS = 10;
1941
+ const MAX_BACKOFF_MS = 3e4;
1942
+ function tryReclaimLockForRespawn() {
1943
+ const claim2 = tryClaimWatcherLock();
1944
+ if (claim2.ok) {
1945
+ try {
1946
+ fs2.writeSync(claim2.fd, String(process.pid));
1947
+ fs2.closeSync(claim2.fd);
1948
+ } catch {
1949
+ try {
1950
+ fs2.unlinkSync(WATCHER_LOCK);
1951
+ } catch {
1952
+ }
1953
+ return false;
1954
+ }
1955
+ return true;
1956
+ }
1957
+ if (claim2.reason === "busy") {
1958
+ process.stderr.write(
1959
+ "[greprag-memory] lockfile claimed by another opencode instance; not respawning\n"
1960
+ );
1961
+ } else {
1962
+ process.stderr.write(
1963
+ `[greprag-memory] respawn lockfile error: ${claim2.reason}
1964
+ `
1965
+ );
1966
+ }
1967
+ return false;
1968
+ }
1969
+ function spawnWatcher() {
1970
+ if (stopped)
1971
+ return;
1972
+ if (!grepragBin)
1973
+ return;
1974
+ let child;
1975
+ try {
1976
+ child = safeSpawn(grepragBin, ["opencode", "watch"], {
1977
+ stdio: ["ignore", "pipe", "pipe"],
1978
+ env: process.env,
1979
+ windowsHide: true,
1980
+ shell: process.platform === "win32"
1981
+ });
1982
+ } catch (err) {
1983
+ process.stderr.write(`[greprag-memory] spawn failed: ${err.message}
1984
+ `);
1985
+ scheduleRespawn();
1986
+ return;
1987
+ }
1988
+ currentChild = child;
1989
+ currentChildPid = child.pid ?? null;
1990
+ childStartTime = Date.now();
1991
+ dlog(`startCaptureWatcher: spawned 'greprag opencode watch' pid=${child.pid ?? "<none>"}`);
1992
+ try {
1993
+ fs2.writeFileSync(WATCHER_LOCK, String(child.pid));
1994
+ } catch {
1995
+ }
1996
+ child.stderr?.on("data", (chunk) => {
1997
+ process.stderr.write(`[greprag-watch] ${chunk.toString("utf-8")}`);
1998
+ });
1999
+ child.stdout?.on("data", (chunk) => {
2000
+ process.stderr.write(`[greprag-watch] ${chunk.toString("utf-8")}`);
2001
+ });
2002
+ child.on("exit", (code, signal) => {
2003
+ dlog(`startCaptureWatcher: child pid=${child.pid ?? "<none>"} exited code=${code} signal=${signal || "none"}`);
2004
+ try {
2005
+ const current = fs2.readFileSync(WATCHER_LOCK, "utf-8").trim();
2006
+ if (current === String(child.pid))
2007
+ fs2.unlinkSync(WATCHER_LOCK);
2008
+ } catch {
2009
+ }
2010
+ currentChild = null;
2011
+ currentChildPid = null;
2012
+ if (stopped)
2013
+ return;
2014
+ const cleanShutdown = code === 0 || signal === "SIGTERM" || signal === "SIGKILL";
2015
+ if (!cleanShutdown) {
2016
+ process.stderr.write(
2017
+ `[greprag-memory] opencode watch exited code=${code} signal=${signal || "none"}, respawning
2018
+ `
2019
+ );
2020
+ }
2021
+ scheduleRespawn();
2022
+ });
2023
+ }
2024
+ function scheduleRespawn() {
2025
+ if (stopped)
2026
+ return;
2027
+ if (Date.now() - childStartTime > 3e4)
2028
+ respawnCount = 0;
2029
+ respawnCount++;
2030
+ if (respawnCount > MAX_RESPAWNS) {
2031
+ process.stderr.write(
2032
+ `[greprag-memory] opencode watch respawned ${respawnCount} times; giving up. Run \`greprag doctor\` to investigate.
2033
+ `
2034
+ );
2035
+ try {
2036
+ fs2.unlinkSync(WATCHER_LOCK);
2037
+ } catch {
2038
+ }
2039
+ return;
2040
+ }
2041
+ if (!tryReclaimLockForRespawn())
2042
+ return;
2043
+ const backoff = Math.min(MAX_BACKOFF_MS, 1e3 * Math.pow(2, Math.min(respawnCount, 5)));
2044
+ process.stderr.write(
2045
+ `[greprag-memory] respawning opencode watch in ${Math.round(backoff / 1e3)}s (attempt ${respawnCount}/${MAX_RESPAWNS})
2046
+ `
2047
+ );
2048
+ setTimeout(spawnWatcher, backoff).unref();
2049
+ }
2050
+ spawnWatcher();
2051
+ const cleanup = () => {
2052
+ if (stopped)
2053
+ return;
2054
+ stopped = true;
2055
+ if (currentChild) {
2056
+ try {
2057
+ currentChild.kill("SIGTERM");
2058
+ } catch {
2059
+ }
2060
+ setTimeout(() => {
2061
+ try {
2062
+ currentChild?.kill("SIGKILL");
2063
+ } catch {
2064
+ }
2065
+ }, 2e3).unref();
2066
+ }
2067
+ try {
2068
+ fs2.unlinkSync(WATCHER_LOCK);
2069
+ } catch {
2070
+ }
2071
+ };
2072
+ process.on("exit", cleanup);
2073
+ process.on("SIGINT", () => {
2074
+ cleanup();
2075
+ process.exit(0);
2076
+ });
2077
+ process.on("SIGTERM", () => {
2078
+ cleanup();
2079
+ process.exit(0);
2080
+ });
2081
+ }
2082
+ startCaptureWatcher();
2083
+ function getEnv(key) {
2084
+ return process.env[key] || "";
2085
+ }
2086
+ async function fetchRecapInbox(anchor) {
2087
+ const apiKey = getEnv("GREPRAG_API_KEY");
2088
+ if (!apiKey) {
2089
+ dlog("fetchRecapInbox: no API key \u2014 returning empty");
2090
+ return [];
2091
+ }
2092
+ if (!anchor.sessionStartRecap) {
2093
+ dlog(`sessionStartRecap=false on anchor \u2014 skipping memory fetch (per-project opt-out)`);
2094
+ return [];
2095
+ }
2096
+ const body = await buildRecapBody(API_URL, apiKey, anchor);
2097
+ dlog(`buildRecapBody returned ${body.length} chars (projectId=${anchor.projectId})`);
2098
+ if (!body)
2099
+ return [];
2100
+ return [body];
2101
+ }
2102
+ var relayArmedBySession = /* @__PURE__ */ new Set();
2103
+ var cachedRecap = null;
2104
+ var RECAP_CACHE_TTL_MS = 5 * 6e4;
2105
+ function startSessionRelay(sessionId, serverUrl) {
2106
+ if (process.env.GREPRAG_OPENCODE_RELAY === "0")
2107
+ return;
2108
+ if (process.env.MEMORY_HOOK_ENABLED !== "true")
2109
+ return;
2110
+ if (!process.env.GREPRAG_API_KEY)
2111
+ return;
2112
+ if (relayArmedBySession.has(sessionId))
2113
+ return;
2114
+ relayArmedBySession.add(sessionId);
2115
+ dlog(`startSessionRelay: arming sid=${sessionId.replace(/[^0-9a-f]/gi, "").slice(0, 8)} url=${serverUrl}`);
2116
+ const grepragBin = findGrepragBinary();
2117
+ dlog(`startSessionRelay: findGrepragBinary -> ${grepragBin || "<null>"}`);
2118
+ if (!grepragBin) {
2119
+ process.stderr.write(
2120
+ "[greprag-memory] relay could not start: greprag binary not in PATH. Run `npm install -g @greprag/cli` or set GREPRAG_BIN.\n"
2121
+ );
2122
+ return;
2123
+ }
2124
+ const lockPath = relayLockPath(sessionId);
2125
+ const claim = tryClaimRelayLock(lockPath);
2126
+ if (!claim.ok) {
2127
+ if (claim.reason === "busy") {
2128
+ process.stderr.write(
2129
+ `[greprag-memory] relay for session ${sessionId} already running; not spawning
2130
+ `
2131
+ );
2132
+ } else {
2133
+ process.stderr.write(
2134
+ `[greprag-memory] relay lockfile error: ${claim.reason}
2135
+ `
2136
+ );
2137
+ }
2138
+ return;
2139
+ }
2140
+ try {
2141
+ fs2.writeSync(claim.fd, String(process.pid));
2142
+ fs2.closeSync(claim.fd);
2143
+ } catch {
2144
+ try {
2145
+ fs2.unlinkSync(lockPath);
2146
+ } catch {
2147
+ }
2148
+ return;
2149
+ }
2150
+ let stopped = false;
2151
+ let currentChild = null;
2152
+ let respawnCount = 0;
2153
+ let childStartTime = 0;
2154
+ const MAX_RESPAWNS = 10;
2155
+ const MAX_BACKOFF_MS = 3e4;
2156
+ function tryReclaimLockForRespawn() {
2157
+ const claim2 = tryClaimRelayLock(lockPath);
2158
+ if (claim2.ok) {
2159
+ try {
2160
+ fs2.writeSync(claim2.fd, String(process.pid));
2161
+ fs2.closeSync(claim2.fd);
2162
+ } catch {
2163
+ try {
2164
+ fs2.unlinkSync(lockPath);
2165
+ } catch {
2166
+ }
2167
+ return false;
2168
+ }
2169
+ return true;
2170
+ }
2171
+ if (claim2.reason === "busy") {
2172
+ process.stderr.write(
2173
+ `[greprag-memory] relay lockfile claimed by another process; not respawning
2174
+ `
2175
+ );
2176
+ }
2177
+ return false;
2178
+ }
2179
+ function spawnRelay() {
2180
+ if (stopped)
2181
+ return;
2182
+ let child;
2183
+ try {
2184
+ child = safeSpawn(grepragBin, ["opencode", "relay", "--session", sessionId, "--opencode-url", serverUrl], {
2185
+ stdio: ["ignore", "pipe", "pipe"],
2186
+ env: process.env,
2187
+ windowsHide: true,
2188
+ shell: process.platform === "win32"
2189
+ });
2190
+ } catch (err) {
2191
+ process.stderr.write(`[greprag-relay] spawn failed: ${err.message}
2192
+ `);
2193
+ scheduleRespawn();
2194
+ return;
2195
+ }
2196
+ currentChild = child;
2197
+ childStartTime = Date.now();
2198
+ dlog(`startSessionRelay: spawned 'greprag opencode relay' pid=${child.pid ?? "<none>"}`);
2199
+ try {
2200
+ fs2.writeFileSync(lockPath, String(child.pid));
2201
+ } catch {
2202
+ }
2203
+ child.stderr?.on("data", (chunk) => {
2204
+ process.stderr.write(`[greprag-relay] ${chunk.toString("utf-8")}`);
2205
+ });
2206
+ child.stdout?.on("data", (chunk) => {
2207
+ process.stderr.write(`[greprag-relay] ${chunk.toString("utf-8")}`);
2208
+ });
2209
+ child.on("exit", (code, signal) => {
2210
+ try {
2211
+ const current = fs2.readFileSync(lockPath, "utf-8").trim();
2212
+ if (current === String(child.pid))
2213
+ fs2.unlinkSync(lockPath);
2214
+ } catch {
2215
+ }
2216
+ currentChild = null;
2217
+ if (stopped)
2218
+ return;
2219
+ if (code !== 0 && signal !== "SIGTERM" && signal !== "SIGKILL") {
2220
+ process.stderr.write(
2221
+ `[greprag-memory] relay for ${sessionId} exited code=${code} signal=${signal || "none"}, respawning
2222
+ `
2223
+ );
2224
+ }
2225
+ scheduleRespawn();
2226
+ });
2227
+ }
2228
+ function scheduleRespawn() {
2229
+ if (stopped)
2230
+ return;
2231
+ if (Date.now() - childStartTime > 3e4)
2232
+ respawnCount = 0;
2233
+ respawnCount++;
2234
+ if (respawnCount > MAX_RESPAWNS) {
2235
+ process.stderr.write(
2236
+ `[greprag-memory] relay for ${sessionId} respawned ${respawnCount} times; giving up. Run \`greprag doctor\` to investigate.
2237
+ `
2238
+ );
2239
+ try {
2240
+ fs2.unlinkSync(lockPath);
2241
+ } catch {
2242
+ }
2243
+ return;
2244
+ }
2245
+ if (!tryReclaimLockForRespawn())
2246
+ return;
2247
+ const backoff = Math.min(MAX_BACKOFF_MS, 1e3 * Math.pow(2, Math.min(respawnCount, 5)));
2248
+ process.stderr.write(
2249
+ `[greprag-memory] respawning relay for ${sessionId} in ${Math.round(backoff / 1e3)}s (attempt ${respawnCount}/${MAX_RESPAWNS})
2250
+ `
2251
+ );
2252
+ setTimeout(spawnRelay, backoff).unref();
2253
+ }
2254
+ spawnRelay();
2255
+ const cleanup = () => {
2256
+ if (stopped)
2257
+ return;
2258
+ stopped = true;
2259
+ if (currentChild) {
2260
+ try {
2261
+ currentChild.kill("SIGTERM");
2262
+ } catch {
2263
+ }
2264
+ setTimeout(() => {
2265
+ try {
2266
+ currentChild?.kill("SIGKILL");
2267
+ } catch {
2268
+ }
2269
+ }, 2e3).unref();
2270
+ }
2271
+ try {
2272
+ fs2.unlinkSync(lockPath);
2273
+ } catch {
2274
+ }
2275
+ };
2276
+ process.on("exit", cleanup);
2277
+ process.on("SIGINT", () => {
2278
+ cleanup();
2279
+ process.exit(0);
2280
+ });
2281
+ process.on("SIGTERM", () => {
2282
+ cleanup();
2283
+ process.exit(0);
2284
+ });
2285
+ }
2286
+ var _cachedGrepragBin;
2287
+ function grepragBinCached() {
2288
+ if (_cachedGrepragBin === void 0)
2289
+ _cachedGrepragBin = findGrepragBinary();
2290
+ return _cachedGrepragBin;
2291
+ }
2292
+ var _crushShapeLogged = false;
2293
+ function inlineCrushEnabled() {
2294
+ const v = process.env.GREPRAG_OPENCODE_CRUSH;
2295
+ return v === "true" || v === "1";
2296
+ }
2297
+ function makeCcrStash() {
2298
+ return (original, sourceType) => {
2299
+ const hash = crypto2.createHash("sha256").update(original, "utf8").digest("hex").slice(0, 16);
2300
+ const bin = grepragBinCached();
2301
+ if (bin) {
2302
+ try {
2303
+ const child = safeSpawn(bin, ["ccr", "put", "--type", sourceType], {
2304
+ stdio: ["pipe", "ignore", "ignore"],
2305
+ env: process.env,
2306
+ windowsHide: true,
2307
+ shell: process.platform === "win32"
2308
+ });
2309
+ child.on("error", () => {
2310
+ });
2311
+ if (child.stdin) {
2312
+ child.stdin.on("error", () => {
2313
+ });
2314
+ child.stdin.write(original);
2315
+ child.stdin.end();
2316
+ }
2317
+ child.unref();
2318
+ } catch {
2319
+ }
2320
+ }
2321
+ return hash;
2322
+ };
2323
+ }
2324
+ var GrepRAGMemoryPlugin = async (ctx) => {
2325
+ dlog(`plugin function INVOKED (this is the entry opencode calls)`);
2326
+ const apiKey = getEnv("GREPRAG_API_KEY");
2327
+ const enabled = getEnv("MEMORY_HOOK_ENABLED") === "true";
2328
+ dlog(`plugin invoked: enabled=${enabled} apiKeySet=${!!apiKey} worktree=${ctx.worktree} directory=${ctx.directory}`);
2329
+ if (!enabled || !apiKey) {
2330
+ dlog(`bailing: env gate closed (enabled=${enabled} apiKeySet=${!!apiKey}). No hooks will register.`);
2331
+ return {};
2332
+ }
2333
+ const client = ctx.client;
2334
+ const fallbackAnchor = readAnchor(ctx.worktree);
2335
+ const fallbackWorkingDir = ctx.directory || ctx.worktree;
2336
+ async function resolveSessionContext(sessionID) {
2337
+ try {
2338
+ const session = await client.session.get({ path: { id: sessionID } });
2339
+ const dir = session && (session.directory || session.path);
2340
+ if (typeof dir === "string" && dir) {
2341
+ return { anchor: readAnchor(dir), workingDir: dir };
2342
+ }
2343
+ } catch {
2344
+ }
2345
+ return { anchor: fallbackAnchor, workingDir: fallbackWorkingDir };
2346
+ }
2347
+ return {
2348
+ "experimental.chat.system.transform": async (input, output) => {
2349
+ const sid = input && input.sessionID;
2350
+ const sidShort = sid ? sid.replace(/[^0-9a-f]/gi, "").slice(0, 8) || sid.slice(0, 8) : "<none>";
2351
+ dlog(`hook fired sid=${sidShort}\u2026 model=${input?.model?.modelID || input?.model?.id || "?"}`);
2352
+ if (sid && !relayArmedBySession.has(sid)) {
2353
+ startSessionRelay(sid, ctx.serverUrl.toString());
2354
+ }
2355
+ const anchor = sid ? (await resolveSessionContext(sid)).anchor : fallbackAnchor;
2356
+ let body;
2357
+ if (cachedRecap && cachedRecap.projectId === anchor.projectId && Date.now() - cachedRecap.fetchedAt < RECAP_CACHE_TTL_MS) {
2358
+ body = cachedRecap.body;
2359
+ dlog(`recap cache hit for projectId=${anchor.projectId} (${body.length} chars, age=${Math.round((Date.now() - cachedRecap.fetchedAt) / 1e3)}s)`);
2360
+ } else {
2361
+ dlog(
2362
+ `resolving recap: projectId=${anchor.projectId} projectName=${anchor.projectName} sessionStartRecap=${anchor.sessionStartRecap} inboxNotify=${anchor.inboxNotify}`
2363
+ );
2364
+ const recap = await fetchRecapInbox(anchor);
2365
+ body = recap.join("\n");
2366
+ cachedRecap = { projectId: anchor.projectId || "", body, fetchedAt: Date.now() };
2367
+ dlog(`buildRecapBody returned ${body.length} chars (projectId=${anchor.projectId})`);
2368
+ if (body) {
2369
+ dlog(`recap content (${body.length} chars):
2370
+ ---
2371
+ ${body}
2372
+ ---`);
2373
+ }
2374
+ }
2375
+ if (body) {
2376
+ pushSystemPrompt(output, body);
2377
+ dlog(`pushed recap to system prompt for sid=${sidShort}\u2026 (output.system is ${Array.isArray(output.system) ? "string[]" : typeof output.system}, body=${body.length} chars)`);
2378
+ } else {
2379
+ dlog(`recap empty for sid=${sidShort}\u2026 (no memories in window, no unread inbox)`);
2380
+ }
2381
+ if (sid) {
2382
+ const sid8 = sidShort && sidShort !== "<none>" ? sidShort : sid.slice(0, 8);
2383
+ pushSystemPrompt(
2384
+ output,
2385
+ `Your greprag session id: ${sid8}
2386
+ Reply-to: travis@greprag.com/${sid8}
2387
+ ALWAYS pass \`--from-session ${sid8}\` when you run \`greprag send\`, so recipients can reply to your session:
2388
+ greprag send "msg" --to <handle>@greprag.com/<their-8hex> --from-session ${sid8}`
2389
+ );
2390
+ dlog(`pushed session-id awareness for sid=${sid8}`);
2391
+ }
2392
+ },
2393
+ // Inline outbound-context crusher. Fires before every LLM call with the
2394
+ // full outbound array in `output.messages`. Crushes its bulky parts in
2395
+ // place (reversible via CCR, token-validate-or-revert, idempotent). The
2396
+ // ENTIRE hook is wrapped so a crush failure can NEVER break the turn —
2397
+ // on any error we simply leave the messages as opencode handed them.
2398
+ // Contract: we mutate ONLY this request-scoped `output.messages` copy,
2399
+ // never opencode's persisted session history. adr:
2400
+ // adr/opencode-context-compressor.md
2401
+ "experimental.chat.messages.transform": async (_input, output) => {
2402
+ try {
2403
+ if (!inlineCrushEnabled())
2404
+ return;
2405
+ const msgs = output && output.messages;
2406
+ if (!Array.isArray(msgs)) {
2407
+ dlog(`messages.transform: output.messages is ${typeof msgs}, not an array \u2014 skip`);
2408
+ return;
2409
+ }
2410
+ if (!_crushShapeLogged) {
2411
+ _crushShapeLogged = true;
2412
+ try {
2413
+ dlog(`messages.transform: live shape sample = ${JSON.stringify(msgs[0]).slice(0, 600)}`);
2414
+ } catch {
2415
+ }
2416
+ }
2417
+ const thresholdEnv = parseInt(process.env.GREPRAG_OPENCODE_CRUSH_MIN_BYTES || "", 10);
2418
+ const stats = crushMessages(msgs, {
2419
+ stash: makeCcrStash(),
2420
+ thresholdBytes: Number.isFinite(thresholdEnv) && thresholdEnv > 0 ? thresholdEnv : void 0
2421
+ });
2422
+ if (stats.partsCrushed > 0 || stats.partsReverted > 0) {
2423
+ dlog(
2424
+ `messages.transform: crushed=${stats.partsCrushed} reverted=${stats.partsReverted} skipped=${stats.partsSkipped} bytes ${stats.bytesBefore}->${stats.bytesAfter}`
2425
+ );
2426
+ }
2427
+ } catch (e) {
2428
+ dlog(`messages.transform: swallowed error (turn unaffected): ${e.message}`);
2429
+ }
2430
+ }
2431
+ };
2432
+ };
2433
+ function pushSystemPrompt(output, body) {
2434
+ if (Array.isArray(output.system)) {
2435
+ output.system.push(body);
2436
+ } else if (typeof output.system === "string" && output.system.length > 0) {
2437
+ output.system = `${output.system}
2438
+
2439
+ ${body}`;
2440
+ } else {
2441
+ output.system = body;
2442
+ }
2443
+ }
2444
+ module.exports = {
2445
+ id: "greprag-memory",
2446
+ server: GrepRAGMemoryPlugin
2447
+ };