@intentic/claude-plugin 1.317.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,927 @@
1
+ import { createRequire as __intenticRequire } from 'node:module'; const require = __intenticRequire(import.meta.url);
2
+
3
+ // src/post-bash.ts
4
+ import { join as join4 } from "node:path";
5
+
6
+ // ../output-cleaners/src/cleaners.mjs
7
+ import { createHash } from "node:crypto";
8
+ import { closeSync, fstatSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ var ANSI = /\x1b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_])/g;
11
+ var collapseCr = (line) => {
12
+ const frames = line.split("\r");
13
+ for (let i = frames.length - 1; i >= 0; i--) {
14
+ if (frames[i] !== "") {
15
+ return frames[i];
16
+ }
17
+ }
18
+ return "";
19
+ };
20
+ var strip = (id, match, patterns) => ({
21
+ id,
22
+ match,
23
+ apply: (lines) => lines.filter((line) => !patterns.some((re) => re.test(line)))
24
+ });
25
+ var invocation = (word) => new RegExp(String.raw`(?<![\w.\-/])(?:${word})(?![\w.-])`);
26
+ var bodyBytes = (lines) => lines.length === 0 ? 0 : lines.reduce((sum, line) => sum + line.length, 0) + lines.length - 1;
27
+ var humanSize = (bytes) => bytes >= 1048576 ? `${(bytes / 1048576).toFixed(1)}M` : bytes >= 1024 ? `${(bytes / 1024).toFixed(1)}K` : `${bytes}B`;
28
+ var EXECUTABLE = /* @__PURE__ */ new Set(["x", "s", "t"]);
29
+ var permsToOctal = (perms) => {
30
+ const triad = (read, write, exec) => (perms[read] === "r" ? 4 : 0) + (perms[write] === "w" ? 2 : 0) + (EXECUTABLE.has(perms[exec]) ? 1 : 0);
31
+ const special = ("sS".includes(perms[3]) ? 4 : 0) + ("sS".includes(perms[6]) ? 2 : 0) + ("tT".includes(perms[9]) ? 1 : 0);
32
+ const mode = `${triad(1, 2, 3)}${triad(4, 5, 6)}${triad(7, 8, 9)}`;
33
+ return special > 0 ? `${special}${mode}` : mode;
34
+ };
35
+ var LS_DATE = /\s(?:(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}\s+(?:\d{4}|\d{1,2}:\d{2})|\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2})\s/;
36
+ var LS_MODE = /^[-dlbcps][rwxSsTt-]{9}[.+@]?$/;
37
+ var parseListingLine = (line) => {
38
+ const date = LS_DATE.exec(line);
39
+ if (date === null) {
40
+ return void 0;
41
+ }
42
+ const head = line.slice(0, date.index).split(/\s+/).filter((token) => token !== "");
43
+ if (head.length < 4 || !LS_MODE.test(head[0])) {
44
+ return void 0;
45
+ }
46
+ const size = head.reduceRight((found, token) => found === void 0 && /^\d+$/.test(token) ? Number(token) : found, void 0);
47
+ return { mode: permsToOctal(head[0]), directory: head[0].startsWith("d"), size, name: line.slice(date.index + date[0].length) };
48
+ };
49
+ var compactListing = (lines) => {
50
+ let parsed = 0;
51
+ const out = lines.flatMap((line) => {
52
+ if (/^total \d+$/.test(line)) {
53
+ return [];
54
+ }
55
+ const entry = parseListingLine(line);
56
+ if (entry === void 0) {
57
+ return [line];
58
+ }
59
+ parsed++;
60
+ if (entry.name === "." || entry.name === "..") {
61
+ return [];
62
+ }
63
+ if (entry.directory) {
64
+ return [`${entry.mode} ${entry.name}/`];
65
+ }
66
+ return [`${entry.mode} ${entry.name}${entry.size === void 0 ? "" : ` ${humanSize(entry.size)}`}`];
67
+ });
68
+ return parsed === 0 ? lines : out;
69
+ };
70
+ var PATH_RUN_MIN = 10;
71
+ var PATH_RUN_DIRS = 60;
72
+ var PATH_RUN_NAMES = 40;
73
+ var isPathLine = (line) => line !== "" && !/\s/.test(line) && !/:\d/.test(line) && line.length < 300;
74
+ var sharedRoot = (directories) => {
75
+ if (directories.length < 2) {
76
+ return "";
77
+ }
78
+ let prefix = directories[0];
79
+ for (const directory of directories) {
80
+ while (prefix !== "" && !directory.startsWith(prefix)) {
81
+ const shorter = prefix.slice(0, prefix.lastIndexOf("/", prefix.length - 2) + 1);
82
+ prefix = shorter === prefix ? "" : shorter;
83
+ }
84
+ }
85
+ return prefix;
86
+ };
87
+ var foldPaths = (run) => {
88
+ if (run.filter((line) => line.includes("/")).length * 5 < run.length * 3) {
89
+ return run;
90
+ }
91
+ const byDirectory = /* @__PURE__ */ new Map();
92
+ for (const path of run) {
93
+ const cut = path.lastIndexOf("/");
94
+ const directory = cut === -1 ? "./" : path.slice(0, cut + 1);
95
+ const names = byDirectory.get(directory);
96
+ if (names === void 0) {
97
+ byDirectory.set(directory, [path.slice(cut + 1)]);
98
+ } else {
99
+ names.push(path.slice(cut + 1));
100
+ }
101
+ }
102
+ const root = sharedRoot([...byDirectory.keys()]);
103
+ const trim = root.length > 8 ? root.length : 0;
104
+ const folded = [`${run.length} paths in ${byDirectory.size} directories${trim === 0 ? "" : ` under ${root}`}:`];
105
+ let shown = 0;
106
+ for (const [directory, names] of [...byDirectory].slice(0, PATH_RUN_DIRS)) {
107
+ const kept = names.slice(0, PATH_RUN_NAMES);
108
+ shown += kept.length;
109
+ folded.push(`${directory.slice(trim) === "" ? "./" : directory.slice(trim)} ${kept.join(" ")}`);
110
+ }
111
+ if (shown < run.length) {
112
+ folded.push(`\u2026 ${run.length - shown} more paths elided`);
113
+ }
114
+ return bodyBytes(folded) < bodyBytes(run) ? folded : run;
115
+ };
116
+ var foldPathRuns = (lines) => {
117
+ const out = [];
118
+ for (let i = 0; i < lines.length; ) {
119
+ let j = i;
120
+ while (j < lines.length && isPathLine(lines[j])) {
121
+ j++;
122
+ }
123
+ if (j === i) {
124
+ out.push(lines[i]);
125
+ i++;
126
+ continue;
127
+ }
128
+ out.push(...j - i >= PATH_RUN_MIN ? foldPaths(lines.slice(i, j)) : lines.slice(i, j));
129
+ i = j;
130
+ }
131
+ return out;
132
+ };
133
+ var HIT_LINE = /^([^\s:][^:]{0,240}):(\d+):(?:(\d+):)?(.*)$/;
134
+ var HIT_RUN_MIN = 6;
135
+ var isHitKey = (key) => key.includes("/") || key.includes(".");
136
+ var parseHit = (line) => {
137
+ const match = HIT_LINE.exec(line);
138
+ return match !== null && isHitKey(match[1]) ? match : void 0;
139
+ };
140
+ var foldHitRun = (run) => {
141
+ const folded = [];
142
+ for (let start = 0; start < run.length; ) {
143
+ const file = parseHit(run[start])[1];
144
+ folded.push(run[start]);
145
+ let next = start + 1;
146
+ while (next < run.length && parseHit(run[next])[1] === file) {
147
+ const hit = parseHit(run[next]);
148
+ folded.push(` ${hit[2]}${hit[3] === void 0 ? "" : `:${hit[3]}`}:${hit[4]}`);
149
+ next++;
150
+ }
151
+ start = next;
152
+ }
153
+ return bodyBytes(folded) < bodyBytes(run) ? folded : run;
154
+ };
155
+ var foldHitRuns = (lines) => {
156
+ const out = [];
157
+ for (let i = 0; i < lines.length; ) {
158
+ let end = i;
159
+ while (end < lines.length && parseHit(lines[end]) !== void 0) {
160
+ end++;
161
+ }
162
+ const stop = Math.max(end, i + 1);
163
+ out.push(...end - i >= HIT_RUN_MIN ? foldHitRun(lines.slice(i, end)) : lines.slice(i, stop));
164
+ i = stop;
165
+ }
166
+ return out;
167
+ };
168
+ var GENERATED_PATH = /(?:^|\/)(?:[^/]*\.lock(?:\.json)?|[^/]*-lock\.json|pnpm-lock\.yaml|go\.sum|[^/]*\.min\.(?:js|css|mjs)|[^/]*\.map)$/;
169
+ var DIFF_HEADER = /^diff --git a\/(\S+) b\/(\S+)$/;
170
+ var isAdded = (line) => line.startsWith("+") && !line.startsWith("+++");
171
+ var isRemoved = (line) => line.startsWith("-") && !line.startsWith("---");
172
+ var foldGeneratedDiffs = (lines) => {
173
+ const out = [];
174
+ let folding = false;
175
+ let added = 0;
176
+ let removed = 0;
177
+ let dropped = 0;
178
+ const flush = () => {
179
+ if (folding && dropped > 0) {
180
+ out.push(`\u2026 ${dropped} lines of generated-file diff elided (+${added} \u2212${removed}) \u2026`);
181
+ }
182
+ folding = false;
183
+ added = 0;
184
+ removed = 0;
185
+ dropped = 0;
186
+ };
187
+ for (const line of lines) {
188
+ const header = DIFF_HEADER.exec(line);
189
+ if (header !== null) {
190
+ flush();
191
+ folding = GENERATED_PATH.test(header[2]);
192
+ out.push(line);
193
+ continue;
194
+ }
195
+ if (!folding) {
196
+ out.push(line);
197
+ continue;
198
+ }
199
+ dropped++;
200
+ added += isAdded(line) ? 1 : 0;
201
+ removed += isRemoved(line) ? 1 : 0;
202
+ }
203
+ flush();
204
+ return bodyBytes(out) < bodyBytes(lines) ? out : lines;
205
+ };
206
+ var COMMAND_CLEANERS = [
207
+ strip("pnpm", invocation(String.raw`pnpm`), [
208
+ /^\s*Progress: /,
209
+ /^Packages: [+-]/,
210
+ /^Downloading /,
211
+ /^\s*[.+]+\s*$/,
212
+ /^Virtual store is at/,
213
+ /^Lockfile is up to date/
214
+ ]),
215
+ // `apt-get` before `apt`: ordered alternation, or the guard rejects the longer spelling at its hyphen.
216
+ strip("apt", invocation(String.raw`apt-get|apt`), [
217
+ /^(?:Get:|Hit:|Ign:|Fetched |Selecting |Preparing to unpack|Unpacking |Setting up |Processing triggers)/
218
+ ]),
219
+ // Test runners: on a passing run (exit 0) per-test PASS lines are noise, dropped, keeping the summary. Failures skip
220
+ // all command cleaners, so failing tests survive verbatim.
221
+ strip("test", invocation(String.raw`bun\s+test|suites|vitest|jest|pytest|rspec|mocha|phpunit|go\s+test|cargo\s+test`), [
222
+ /^\s*[✓√]\s/,
223
+ // per-test pass (vitest/jest/mocha)
224
+ /^\(pass\)\s/,
225
+ // bun test per-test pass
226
+ /^--- PASS:/,
227
+ // go test per-test
228
+ /^\s*test .+\.\.\. ok$/,
229
+ // cargo test per-test
230
+ /^PASS\s+\S/,
231
+ // jest per-file PASS header
232
+ /^\s*[.·]+\s*$/
233
+ // pytest/mocha dot progress
234
+ ]),
235
+ // `diff` reads the whole output's structure, the three after it read runs of lines; folding the generated hunks away
236
+ // first keeps those three from ever looking inside one.
237
+ { id: "diff", apply: foldGeneratedDiffs },
238
+ { id: "ls", apply: compactListing },
239
+ { id: "files", apply: foldPathRuns },
240
+ { id: "hits", apply: foldHitRuns }
241
+ ];
242
+ var CLEANERS = [...COMMAND_CLEANERS.map((cleaner) => cleaner.id), "dedup", "wide", "cap", "redact", "cache"];
243
+ var dedupeRuns = (lines) => {
244
+ const out = [];
245
+ for (let i = 0; i < lines.length; ) {
246
+ let j = i + 1;
247
+ while (j < lines.length && lines[j] === lines[i]) {
248
+ j++;
249
+ }
250
+ const run = j - i;
251
+ out.push(lines[i]);
252
+ if (run >= 3) {
253
+ out.push(` \u2026 (${run - 1} more identical lines)`);
254
+ } else {
255
+ for (let k = 1; k < run; k++) {
256
+ out.push(lines[i]);
257
+ }
258
+ }
259
+ i = j;
260
+ }
261
+ return out;
262
+ };
263
+ var WIDE_RUN = 400;
264
+ var WIDE_HEAD = 240;
265
+ var WIDE_TAIL = 80;
266
+ var elideRun = (run) => `${run.slice(0, WIDE_HEAD)}\u2026 ${run.length - WIDE_HEAD - WIDE_TAIL} chars elided \u2026${run.slice(-WIDE_TAIL)}`;
267
+ var WIDE_PATTERN = new RegExp(String.raw`\S{${WIDE_RUN + 1},}`, "g");
268
+ var elideWideRuns = (lines) => lines.map((line) => line.length > WIDE_RUN ? line.replaceAll(WIDE_PATTERN, elideRun) : line);
269
+ var SECRET_NAME = String.raw`[A-Za-z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|API[_-]?KEY|ACCESS[_-]?KEY|PRIVATE[_-]?KEY)[A-Za-z0-9_]*`;
270
+ var ASSIGN = String.raw`["']?\s*[:=](?![=>])\s*`;
271
+ var QUOTED_VALUE = String.raw`(["'\x60])([^"'\x60\n]{6,})\2`;
272
+ var BARE_VALUE = String.raw`[\w+/=~-]{6,}(?![\w+/=~(-])`;
273
+ var ISSUER_PREFIX = /^(?:sk-|pk-|rk-|ghp_|gho_|ghu_|ghs_|ghr_|github_pat_|xox[abposr]-|AKIA|ASIA|eyJ|AIza|ya29\.|glpat-|dop_v1_|shpat_|SG\.|npm_)/;
274
+ var NUMERIC_VALUE = /^[\d_,.]+$/;
275
+ var STRUCTURAL_VALUE = /[\s/\\${}]/;
276
+ var ENV_NAME_VALUE = /^[A-Z0-9_]+$/;
277
+ var ENTROPIC_VALUE = /^(?=[^\n]*\d)(?=[^\n]*[A-Za-z])/;
278
+ var HANDWRITTEN_MAX = 12;
279
+ var looksLikeCredential = (value) => ISSUER_PREFIX.test(value) || value.length > HANDWRITTEN_MAX && ENTROPIC_VALUE.test(value) && !NUMERIC_VALUE.test(value) && !STRUCTURAL_VALUE.test(value) && !ENV_NAME_VALUE.test(value);
280
+ var SECRET_PATTERNS = [
281
+ [
282
+ new RegExp(`\\b(${SECRET_NAME}${ASSIGN})(?:${QUOTED_VALUE}|(${BARE_VALUE}))`, "gi"),
283
+ // Quoted and bare arrive in different groups because only the quoted one has a quote to put back.
284
+ (match, assignment, quote, quoted, bare) => looksLikeCredential(quoted ?? bare) ? `${assignment}${quote ?? ""}***${quote ?? ""}` : match
285
+ ],
286
+ [/\bAKIA[0-9A-Z]{16}\b/g, "***"],
287
+ // Same rule for a bearer value: prose about tokens carries no digit, every real bearer token does.
288
+ [/\b(Bearer\s+)(?=[\w.-]*\d)[\w.-]{8,}/gi, "$1***"],
289
+ [/\b(https?:\/\/[^:@\s/]+:)[^@\s]+@/gi, "$1***@"]
290
+ ];
291
+ var SECRET_VALUE_MIN = 12;
292
+ var secretValueCache = /* @__PURE__ */ new Map();
293
+ var readIfChanged = (path, name) => {
294
+ try {
295
+ const descriptor = openSync(path, "r");
296
+ try {
297
+ const { mtimeMs, size } = fstatSync(descriptor);
298
+ const stamp = `${mtimeMs}:${size}`;
299
+ const cached = secretValueCache.get(path);
300
+ if (cached?.stamp === stamp) {
301
+ return cached.values;
302
+ }
303
+ const values = harvest(readFileSync(descriptor, "utf8"), path, name);
304
+ secretValueCache.set(path, { stamp, values });
305
+ return values;
306
+ } finally {
307
+ closeSync(descriptor);
308
+ }
309
+ } catch {
310
+ return [];
311
+ }
312
+ };
313
+ var surfaceForms = (value) => {
314
+ const forms = [value];
315
+ const jsonEscaped = JSON.stringify(value).slice(1, -1);
316
+ if (jsonEscaped !== value) {
317
+ forms.push(jsonEscaped);
318
+ }
319
+ try {
320
+ const encoded = encodeURIComponent(value);
321
+ if (encoded !== value) {
322
+ forms.push(encoded);
323
+ }
324
+ } catch {
325
+ }
326
+ return forms;
327
+ };
328
+ var harvest = (text, path, name) => {
329
+ const named = [];
330
+ if (path.endsWith("capability-secrets.json")) {
331
+ for (const [id, entry] of Object.entries(JSON.parse(text))) {
332
+ if (entry === null || typeof entry !== "object") {
333
+ continue;
334
+ }
335
+ for (const [field, value] of Object.entries(entry)) {
336
+ if (typeof value === "string") {
337
+ named.push([name(id, field), value]);
338
+ }
339
+ }
340
+ }
341
+ } else if (path.endsWith(".json")) {
342
+ for (const [key, value] of Object.entries(JSON.parse(text))) {
343
+ if (typeof value === "string") {
344
+ named.push([name(key), value]);
345
+ }
346
+ }
347
+ } else {
348
+ for (const line of text.split("\n")) {
349
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
350
+ if (match !== null) {
351
+ named.push([name(match[1]), (match[2] ?? "").trim().replace(/^(["'])([\s\S]*)\1$/, "$2")]);
352
+ }
353
+ }
354
+ }
355
+ const byTarget = /* @__PURE__ */ new Map();
356
+ const add = (target, replacement) => {
357
+ const trimmed = target.trim();
358
+ if (trimmed.length >= SECRET_VALUE_MIN && !byTarget.has(trimmed)) {
359
+ byTarget.set(trimmed, { target: trimmed, replacement });
360
+ }
361
+ };
362
+ for (const [reference, value] of named) {
363
+ for (const form of surfaceForms(value.trim())) {
364
+ add(form, `{{secret:${reference}}}`);
365
+ }
366
+ if (value.includes("\n")) {
367
+ for (const line of value.split("\n")) {
368
+ add(line, "***");
369
+ }
370
+ }
371
+ }
372
+ return [...byTarget.values()].toSorted((a, b) => b.target.length - a.target.length);
373
+ };
374
+ var secretValues = (env = process.env) => {
375
+ const authRoot = env.AGENT_AUTH_DIR !== void 0 && env.AGENT_AUTH_DIR !== "" ? env.AGENT_AUTH_DIR : join(env.WORKSPACE_ROOT ?? "/work", ".intentic/secrets/auth");
376
+ const repo = join(env.WORKSPACE_ROOT ?? "/work", "desired-state");
377
+ return [
378
+ ...readIfChanged(join(authRoot, "capability-secrets.json"), (id, field) => `${id}/${field}`),
379
+ ...readIfChanged(join(repo, ".env"), (key) => key),
380
+ ...readIfChanged(join(repo, ".secrets.json"), (key) => key)
381
+ ].toSorted((a, b) => b.target.length - a.target.length);
382
+ };
383
+ var redactText = (text, values = []) => text.split("\n").map((line) => redactLine(line, values)).join("\n");
384
+ var redactLine = (line, values = []) => {
385
+ let masked = line;
386
+ for (const { target, replacement } of values) {
387
+ if (masked.includes(target)) {
388
+ masked = masked.split(target).join(replacement);
389
+ }
390
+ }
391
+ return SECRET_PATTERNS.reduce((acc, [pattern, replacement]) => acc.replace(pattern, replacement), masked);
392
+ };
393
+ var parseCleaners = (spec) => {
394
+ if (spec === void 0 || spec.trim() === "") {
395
+ return new Set(CLEANERS);
396
+ }
397
+ const tokens = spec.split(",").map((token) => token.trim()).filter((token) => token !== "" && CLEANERS.includes(token.replace(/^-/, "")));
398
+ if (tokens.length === 0) {
399
+ return new Set(CLEANERS);
400
+ }
401
+ const allowList = tokens.some((token) => !token.startsWith("-"));
402
+ if (allowList) {
403
+ return new Set(tokens.filter((token) => !token.startsWith("-")));
404
+ }
405
+ const disabled = new Set(tokens.map((token) => token.slice(1)));
406
+ return new Set(CLEANERS.filter((cleaner) => !disabled.has(cleaner)));
407
+ };
408
+ var HEAD = 30;
409
+ var TAIL = 50;
410
+ var MAX = 100;
411
+ var FAIL_TAIL = 500;
412
+ var LOG_MAX_BYTES = 16e3;
413
+ var READ_MAX_BYTES = 96e3;
414
+ var BYTE_HEAD_SHARE = 0.375;
415
+ var READ_MAX = 2e3;
416
+ var GIT_OPTIONS = String.raw`(?:(?:-[Cc]|--(?:git-dir|work-tree|namespace|exec-path|super-prefix))\s+\S+\s+|-\S+\s+)*`;
417
+ var READ_COMMAND = new RegExp(
418
+ String.raw`(?<![\w.-])(?:cat|bat|sed\s+-n|awk|git\s+${GIT_OPTIONS}(?:diff|show)\b|git\s+${GIT_OPTIONS}log\s+(?:[^;&|]*\s)?-p)\b`
419
+ );
420
+ var takeWithinBudget = (source, budget) => {
421
+ const kept = [];
422
+ let spent = 0;
423
+ for (const line of source) {
424
+ spent += line.length + 1;
425
+ if (spent > budget) {
426
+ break;
427
+ }
428
+ kept.push(line);
429
+ }
430
+ return kept;
431
+ };
432
+ var byteBudget = (command) => READ_COMMAND.test(command) ? READ_MAX_BYTES : LOG_MAX_BYTES;
433
+ var capOutput = (lines, command) => {
434
+ const isRead = READ_COMMAND.test(command);
435
+ const maxLines = isRead ? READ_MAX : MAX;
436
+ const maxBytes = byteBudget(command);
437
+ if (lines.length > maxLines) {
438
+ return isRead ? [...lines.slice(0, maxLines), `\u2026 ${lines.length - maxLines} more lines elided: narrow the range or use the Read tool \u2026`] : [...lines.slice(0, HEAD), `\u2026 ${lines.length - HEAD - TAIL} lines elided \u2026`, ...lines.slice(-TAIL)];
439
+ }
440
+ if (bodyBytes(lines) <= maxBytes) {
441
+ return lines;
442
+ }
443
+ if (isRead) {
444
+ const head2 = takeWithinBudget(lines, maxBytes);
445
+ return [...head2, `\u2026 ${lines.length - head2.length} more lines elided (${bodyBytes(lines)} bytes): narrow the range or use the Read tool \u2026`];
446
+ }
447
+ const head = takeWithinBudget(lines, Math.round(maxBytes * BYTE_HEAD_SHARE));
448
+ const tail = takeWithinBudget(lines.toReversed(), maxBytes - bodyBytes(head)).toReversed();
449
+ if (head.length === 0 && tail.length === 0) {
450
+ return [`${lines[0].slice(0, maxBytes)}\u2026 line truncated at ${maxBytes} bytes \u2026`];
451
+ }
452
+ return [...head, `\u2026 ${lines.length - head.length - tail.length} lines elided (${bodyBytes(lines)} bytes) \u2026`, ...tail];
453
+ };
454
+ var cleanSucceeded = (lines, command, enabled, ran) => {
455
+ let out = lines;
456
+ for (const cleaner of COMMAND_CLEANERS) {
457
+ if (enabled.has(cleaner.id) && (cleaner.match === void 0 || cleaner.match.test(command))) {
458
+ out = ran(cleaner.id, cleaner.apply(out));
459
+ }
460
+ }
461
+ if (enabled.has("dedup")) {
462
+ out = ran("dedup", dedupeRuns(out));
463
+ }
464
+ if (enabled.has("wide") && bodyBytes(out) > byteBudget(command)) {
465
+ out = ran("wide", elideWideRuns(out));
466
+ }
467
+ if (enabled.has("cap")) {
468
+ const capped = capOutput(out, command);
469
+ if (capped !== out) {
470
+ out = ran("cap", capped);
471
+ }
472
+ }
473
+ return out;
474
+ };
475
+ var cleanFailed = (lines, enabled, ran) => {
476
+ const out = enabled.has("dedup") ? ran("dedup", dedupeRuns(lines)) : lines;
477
+ return out.length > FAIL_TAIL ? (
478
+ // Own id, never `cap`: this fires regardless of the spec, so `cap`'s toggle shouldn't get credit for it.
479
+ ran("failtail", [`\u2026 ${out.length - FAIL_TAIL} earlier lines elided \u2026`, ...out.slice(-FAIL_TAIL)])
480
+ ) : out;
481
+ };
482
+ var cleanLines = (lines, { command, exitCode, enabled, values = [] }) => {
483
+ const stages = [];
484
+ let bytes = bodyBytes(lines);
485
+ const ran = (id, next) => {
486
+ const after = bodyBytes(next);
487
+ stages.push({ id, saved: bytes - after });
488
+ bytes = after;
489
+ return next;
490
+ };
491
+ let out = exitCode === "0" ? cleanSucceeded(lines, command, enabled, ran) : cleanFailed(lines, enabled, ran);
492
+ if (enabled.has("redact")) {
493
+ out = ran(
494
+ "redact",
495
+ out.map((line) => redactLine(line, values))
496
+ );
497
+ }
498
+ return { lines: out, stages };
499
+ };
500
+ var matchedCleaners = (command, enabled) => COMMAND_CLEANERS.filter((cleaner) => cleaner.match !== void 0 && enabled.has(cleaner.id) && cleaner.match.test(command)).map(
501
+ (cleaner) => cleaner.id
502
+ );
503
+ var CACHE_MARKER = "(output identical to ";
504
+ var CACHE_MIN_BYTES = 512;
505
+ var CACHE_COMMAND_MAX = 120;
506
+ var hashText = (text) => createHash("sha1").update(text).digest("hex");
507
+ var sessionKeyFromLog = (logPath) => {
508
+ if (logPath === void 0 || logPath === "") {
509
+ return void 0;
510
+ }
511
+ const base = logPath.split("/").pop() ?? "";
512
+ const match = base.match(/^(.*)-[^-]+\.log$/);
513
+ return match !== null && match[1] !== "" ? match[1] : void 0;
514
+ };
515
+ var openCacheStore = (logsDir, sessionKey) => {
516
+ const file = join(logsDir, "output-cache", `${sessionKey}.json`);
517
+ let map;
518
+ try {
519
+ map = new Map(Object.entries(JSON.parse(readFileSync(file, "utf8"))));
520
+ } catch {
521
+ map = /* @__PURE__ */ new Map();
522
+ }
523
+ return {
524
+ lookup: (key) => map.get(key),
525
+ record: (key, value) => {
526
+ map.set(key, value);
527
+ try {
528
+ mkdirSync(dirname(file), { recursive: true });
529
+ writeFileSync(file, JSON.stringify(Object.fromEntries(map)));
530
+ } catch {
531
+ }
532
+ }
533
+ };
534
+ };
535
+ var collapseCached = (body, command, store, retain = () => void 0) => {
536
+ if (body.length < CACHE_MIN_BYTES) {
537
+ return { body, cached: false };
538
+ }
539
+ const commandHash = `c:${hashText(command)}`;
540
+ const bodyKey = `b:${hashText(body)}`;
541
+ const bodyHash = hashText(body);
542
+ const handle = () => {
543
+ const path = retain();
544
+ return path === void 0 ? "" : ` \xB7 retrieve-output ${path}`;
545
+ };
546
+ if (store.lookup(commandHash) === bodyHash) {
547
+ return { body: `${CACHE_MARKER}a previous run this session${handle()})`, cached: true };
548
+ }
549
+ const earlier = store.lookup(bodyKey);
550
+ if (earlier !== void 0 && earlier !== command) {
551
+ const named = earlier.length > CACHE_COMMAND_MAX ? `${earlier.slice(0, CACHE_COMMAND_MAX)}\u2026` : earlier;
552
+ return { body: `${CACHE_MARKER}the output of \`${named}\` earlier this session${handle()})`, cached: true };
553
+ }
554
+ store.record(commandHash, bodyHash);
555
+ if (earlier === void 0) {
556
+ store.record(bodyKey, command);
557
+ }
558
+ return { body, cached: false };
559
+ };
560
+
561
+ // ../output-cleaners/src/agent-output-filter.mjs
562
+ import { appendFileSync, mkdirSync as mkdirSync2, readdirSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
563
+ import { basename, join as join2 } from "node:path";
564
+ var RETRIEVAL_MIN_DROPPED = 20;
565
+ var SIGPIPE_STATUS = "141";
566
+ var maskedFailure = (exitCode, pipeline) => {
567
+ const statuses = pipeline.trim() === "" ? [] : pipeline.trim().split(/\s+/);
568
+ if (exitCode !== "0" || statuses.length < 2 || !statuses.every((status) => /^\d+$/.test(status))) {
569
+ return void 0;
570
+ }
571
+ return statuses.slice(0, -1).some((status) => status !== "0" && status !== SIGPIPE_STATUS) ? statuses.join(" ") : void 0;
572
+ };
573
+ var RG_REPLACE_CLUSTER = /(?<![\w.\-/])rg\s(?:[^;&|\n]*?\s)?(-[a-zA-Z]*r[a-zA-Z]*)(?=[\s;&|]|$)/;
574
+ var commandNotes = (command, raw) => {
575
+ const cluster = RG_REPLACE_CLUSTER.exec(command)?.[1];
576
+ return cluster !== void 0 && cluster.length > 2 && raw.trim() !== "" ? [`--- note: \`${cluster}\` is not recursive in rg: -r is --replace, so matched text above was rewritten (rg recurses by default)`] : [];
577
+ };
578
+ var filterOutput = (raw, { command = "", exitCode = "0", durationS = "0", retain, enabled = new Set(CLEANERS), cacheStore, values = [], pipeline = "" }) => {
579
+ const input = raw.replaceAll(ANSI, "").split("\n").map(collapseCr);
580
+ if (input.at(-1) === "") {
581
+ input.pop();
582
+ }
583
+ const keep = () => retain?.(input.join("\n"));
584
+ const rawCount = input.length;
585
+ const stages = [{ id: "ansi", saved: raw.length - bodyBytes(input) }];
586
+ const cleaned = cleanLines(input, { command, exitCode, enabled, values });
587
+ const lines = cleaned.lines;
588
+ stages.push(...cleaned.stages);
589
+ let body = lines.join("\n");
590
+ if (exitCode === "0" && body.trim() === "" && raw.trim() !== "") {
591
+ body = "(no notable output)";
592
+ }
593
+ const masked = maskedFailure(exitCode, pipeline);
594
+ const exit = masked === void 0 ? `exit ${exitCode}` : `exit ${exitCode} (pipeline: ${masked} \u2014 an earlier stage failed)`;
595
+ const emitted = (text, id, footed) => {
596
+ stages.push({ id, saved: bodyBytes(lines) - text.length });
597
+ let out = text;
598
+ if (text.length > raw.length) {
599
+ stages.push({ id: "guard", saved: text.length - raw.length });
600
+ out = raw;
601
+ }
602
+ const extra = [...masked === void 0 || footed && out === text ? [] : [`--- [${exit}, ${durationS}s]`], ...commandNotes(command, raw)];
603
+ if (extra.length === 0) {
604
+ return { out, stages };
605
+ }
606
+ const noted = `${out === "" || out.endsWith("\n") ? out : `${out}
607
+ `}${extra.join("\n")}
608
+ `;
609
+ stages.push({ id: "notes", saved: out.length - noted.length });
610
+ return { out: noted, stages };
611
+ };
612
+ if (exitCode === "0" && enabled.has("cache") && cacheStore !== void 0 && body !== "" && body !== "(no notable output)") {
613
+ const collapsed = collapseCached(body, command, cacheStore, keep);
614
+ if (collapsed.cached) {
615
+ return emitted(`${collapsed.body}
616
+ `, "cache", false);
617
+ }
618
+ }
619
+ const kept = body === "(no notable output)" ? 0 : lines.length;
620
+ const dropped = Math.max(0, rawCount - kept);
621
+ const cut = stages.find((stage) => stage.id === "wide")?.saved ?? 0;
622
+ if (dropped === 0 && cut <= 0) {
623
+ return emitted(body === "" ? body : `${body}
624
+ `, "footer", false);
625
+ }
626
+ const retained = dropped >= RETRIEVAL_MIN_DROPPED || cut > 0 ? keep() : void 0;
627
+ const handle = retained === void 0 ? "" : ` \xB7 full: retrieve-output ${retained} [pattern]`;
628
+ const summary = dropped > 0 ? `${rawCount} lines filtered to ${kept}` : "long runs cut inside lines";
629
+ const withFooter = `${body}
630
+ --- [${exit}, ${durationS}s] ${summary}${handle}
631
+ `;
632
+ return withFooter.length <= raw.length ? emitted(withFooter, "footer", true) : emitted(`${body}
633
+ `, "footer", false);
634
+ };
635
+ var RETAIN_FILES = 30;
636
+ var RETAIN_MAX_CHARS = 2e6;
637
+ var retainIn = (dir, name, values) => (text) => {
638
+ try {
639
+ mkdirSync2(dir, { recursive: true });
640
+ const file = join2(dir, name);
641
+ writeFileSync2(file, redactText(text.length > RETAIN_MAX_CHARS ? text.slice(-RETAIN_MAX_CHARS) : text, values));
642
+ const aged = readdirSync(dir).flatMap((entry) => {
643
+ try {
644
+ return [{ entry, at: statSync(join2(dir, entry)).mtimeMs }];
645
+ } catch {
646
+ return [];
647
+ }
648
+ });
649
+ for (const { entry } of aged.toSorted((a, b) => b.at - a.at).slice(RETAIN_FILES)) {
650
+ rmSync(join2(dir, entry), { force: true });
651
+ }
652
+ return file;
653
+ } catch {
654
+ return void 0;
655
+ }
656
+ };
657
+ var filterRun = (raw, {
658
+ command = "",
659
+ exitCode = "0",
660
+ durationS = "0",
661
+ pipeline = "",
662
+ enabled = new Set(CLEANERS),
663
+ holdout = 0,
664
+ logsDir,
665
+ runName,
666
+ sessionKey,
667
+ values = [],
668
+ tags = {},
669
+ random = Math.random,
670
+ now = Date.now
671
+ } = {}) => {
672
+ let out = raw;
673
+ let stages = [];
674
+ let heldOut = false;
675
+ try {
676
+ heldOut = holdout > 0 && random() < holdout;
677
+ const cacheStore = !heldOut && enabled.has("cache") && logsDir !== void 0 && sessionKey !== void 0 ? openCacheStore(logsDir, sessionKey) : void 0;
678
+ if (!heldOut) {
679
+ const retain = logsDir !== void 0 && runName !== void 0 && runName !== "" ? retainIn(join2(logsDir, "raw-output"), runName, values) : void 0;
680
+ const filtered = filterOutput(raw, { command, exitCode, durationS, retain, enabled, cacheStore, values, pipeline });
681
+ out = filtered.out;
682
+ stages = filtered.stages;
683
+ }
684
+ } catch {
685
+ out = raw;
686
+ stages = [];
687
+ }
688
+ if (logsDir !== void 0) {
689
+ try {
690
+ const matched = matchedCleaners(command, enabled);
691
+ const stat = {
692
+ ...tags,
693
+ ts: now(),
694
+ command: command.slice(0, 200),
695
+ exit: exitCode,
696
+ durationS: Number(durationS),
697
+ rawBytes: raw.length,
698
+ emittedBytes: out.length,
699
+ cleaners: [...enabled],
700
+ matched: out.startsWith(CACHE_MARKER) ? [...matched, "cache"] : matched,
701
+ heldOut,
702
+ stageBytes: Object.fromEntries(stages.map((stage) => [stage.id, stage.saved]))
703
+ };
704
+ mkdirSync2(logsDir, { recursive: true });
705
+ appendFileSync(join2(logsDir, "filter-stats.jsonl"), `${JSON.stringify(stat)}
706
+ `);
707
+ } catch {
708
+ }
709
+ }
710
+ try {
711
+ out = redactText(out, values);
712
+ } catch {
713
+ }
714
+ return { out, heldOut, stages };
715
+ };
716
+ var main = async () => {
717
+ const [command = "", exitCode = "0", durationS = "0", logPath = "", pipeline = ""] = process.argv.slice(2);
718
+ const chunks = [];
719
+ for await (const chunk of process.stdin) {
720
+ chunks.push(chunk);
721
+ }
722
+ const raw = Buffer.concat(chunks).toString("utf8");
723
+ let values = [];
724
+ try {
725
+ values = secretValues();
726
+ } catch {
727
+ values = [];
728
+ }
729
+ const terminalsDir = process.env["INTENTIC_TERMINAL_LOGS_DIR"];
730
+ const { out } = filterRun(raw, {
731
+ command,
732
+ exitCode,
733
+ durationS,
734
+ pipeline,
735
+ enabled: parseCleaners(process.env["INTENTIC_OUTPUT_CLEANERS"]),
736
+ holdout: Number(process.env["INTENTIC_OUTPUT_HOLDOUT"] ?? "0"),
737
+ logsDir: terminalsDir !== void 0 && terminalsDir !== "" ? join2(terminalsDir, "..") : void 0,
738
+ // Named after the pane log, which is unique per command: one window, one pane id.
739
+ runName: logPath === "" ? void 0 : basename(logPath),
740
+ // Every command of a session runs in a new pane, so the session is the pane log's name minus the pane.
741
+ sessionKey: sessionKeyFromLog(logPath),
742
+ values
743
+ });
744
+ process.stdout.write(out);
745
+ };
746
+ if (process.argv[1]?.endsWith("agent-output-filter") || process.argv[1]?.endsWith("agent-output-filter.mjs")) {
747
+ await main();
748
+ }
749
+
750
+ // src/features.ts
751
+ var OPTIONS = {
752
+ output_cleaners: {
753
+ type: "boolean",
754
+ title: "Trim Bash output",
755
+ description: "Clean what a successful command printed before Claude reads it (progress bars, install chatter, repeated lines, the middle of huge logs), keep the full text retrievable with `retrieve-output`, and ledger what each cleaner saved.",
756
+ default: true
757
+ },
758
+ cleaners: {
759
+ type: "string",
760
+ title: "Cleaners",
761
+ description: "Which cleaners run, as the sandbox spells it: empty for all, `-cap,-wide` to switch some off, `git,pnpm` to allow only those. `/intentic:stats` lists them by what they saved.",
762
+ default: ""
763
+ },
764
+ output_holdout: {
765
+ type: "number",
766
+ title: "Untrimmed share",
767
+ description: "Share of commands left untrimmed, so the saving can be measured against a real comparison and not only computed. 0 turns the comparison off; the exact per-command saving is reported either way.",
768
+ default: 0,
769
+ min: 0,
770
+ max: 0.5
771
+ },
772
+ project_map: {
773
+ type: "boolean",
774
+ title: "Project map",
775
+ description: "Open each session with a map of the project it starts in: its areas, what each is for, and where Claude is standing. Worked out from the tree every time, never stored, so it cannot go stale.",
776
+ default: true
777
+ },
778
+ field_notes: {
779
+ type: "boolean",
780
+ title: "Field notes",
781
+ description: "Open each session with this project's field notes: the traps past sessions hit and the commands that really verify. Written by `/intentic:field-notes` from your own sessions here; nothing is sent until you run it once.",
782
+ default: true
783
+ },
784
+ iq: {
785
+ type: "boolean",
786
+ title: "iq search",
787
+ description: "Teach Claude the `iq` code search CLI and recall past sessions on a matching prompt. Needs `npm i -g @intentic/iq`; without it this switch does nothing and says so once.",
788
+ default: true
789
+ },
790
+ fileq: {
791
+ type: "boolean",
792
+ title: "fileq",
793
+ description: "Let Claude read documents, PDFs, spreadsheets, slides, notebooks, images, audio and archives as markdown with the bundled `fileq` CLI. Off makes the command refuse; its one-line skill description stays listed, since a plugin cannot hide its own skills.",
794
+ default: true
795
+ },
796
+ shadows: {
797
+ type: "boolean",
798
+ title: "Document shadows",
799
+ description: "Keep a markdown rendering of every document in the project up to date in the background (a sweep at session start and every ten minutes, and each binary file Claude writes), so a read finds one ready. Costs background CPU on a document-heavy project.",
800
+ default: false
801
+ },
802
+ holdout: {
803
+ type: "number",
804
+ title: "Measured share",
805
+ description: "Share of sessions opened without the map, the field notes and the iq teaching, so `/intentic:stats` can show what each changed. 0 sends them to every session and measures nothing.",
806
+ default: 0.1,
807
+ min: 0,
808
+ max: 0.5
809
+ }
810
+ };
811
+ var asBoolean = (raw, fallback) => {
812
+ const value = raw?.trim().toLowerCase();
813
+ if (value === "true" || value === "1" || value === "yes" || value === "on") {
814
+ return true;
815
+ }
816
+ if (value === "false" || value === "0" || value === "no" || value === "off") {
817
+ return false;
818
+ }
819
+ return fallback;
820
+ };
821
+ var asShare = (raw, option) => {
822
+ const value = raw === void 0 || raw.trim() === "" ? Number.NaN : Number(raw);
823
+ return Number.isFinite(value) ? Math.min(option.max, Math.max(option.min, value)) : option.default;
824
+ };
825
+ var optionEnvName = (key) => `CLAUDE_PLUGIN_OPTION_${key.toUpperCase()}`;
826
+ var readOptions = (env = process.env) => {
827
+ const entries = Object.keys(OPTIONS).map((key) => {
828
+ const option = OPTIONS[key];
829
+ const raw = env[optionEnvName(key)];
830
+ if (option.type === "boolean") {
831
+ return [key, asBoolean(raw, option.default)];
832
+ }
833
+ if (option.type === "number") {
834
+ return [key, asShare(raw, option)];
835
+ }
836
+ return [key, raw ?? option.default];
837
+ });
838
+ return Object.fromEntries(entries);
839
+ };
840
+
841
+ // src/hook-io.ts
842
+ import { homedir } from "node:os";
843
+ import { join as join3 } from "node:path";
844
+ var readStdin = async () => {
845
+ const chunks = [];
846
+ for await (const chunk of process.stdin) {
847
+ chunks.push(chunk);
848
+ }
849
+ return Buffer.concat(chunks).toString("utf8");
850
+ };
851
+ var parseHookInput = (text) => {
852
+ try {
853
+ const value = JSON.parse(text);
854
+ return typeof value === "object" && value !== null ? value : void 0;
855
+ } catch {
856
+ return void 0;
857
+ }
858
+ };
859
+ var runHook = async (handle) => {
860
+ try {
861
+ const raw = await readStdin();
862
+ const input = parseHookInput(raw);
863
+ if (input === void 0) {
864
+ return;
865
+ }
866
+ const answer = await handle(input, raw);
867
+ if (typeof answer === "string") {
868
+ process.stdout.write(answer);
869
+ } else if (answer !== void 0) {
870
+ process.stdout.write(`${JSON.stringify(answer)}
871
+ `);
872
+ }
873
+ } catch (error) {
874
+ process.stderr.write(`intentic: ${error instanceof Error ? error.message : String(error)}
875
+ `);
876
+ }
877
+ };
878
+ var dataDir = (env = process.env) => env["CLAUDE_PLUGIN_DATA"] ?? join3(env["CLAUDE_CONFIG_DIR"] ?? join3(homedir(), ".claude"), "plugins", "data", "intentic-intentic");
879
+ var projectDir = (input, env = process.env) => env["CLAUDE_PROJECT_DIR"] ?? input.cwd ?? process.cwd();
880
+
881
+ // src/post-bash.ts
882
+ var outputDir = (data) => join4(data, "output");
883
+ var OWN_REPORTS = /(?:^|[\s/])intentic-(?:stats|notes-evidence)(?:\s|$)/;
884
+ var rawOf = (response) => {
885
+ if (typeof response.stdout !== "string" || response.interrupted === true || response.isImage === true || response.backgroundTaskId !== void 0) {
886
+ return void 0;
887
+ }
888
+ const stderr = typeof response.stderr === "string" ? response.stderr : "";
889
+ return stderr === "" ? response.stdout : `${response.stdout}${response.stdout === "" || response.stdout.endsWith("\n") ? "" : "\n"}${stderr}`;
890
+ };
891
+ var postBash = (input, env = process.env) => {
892
+ const options = readOptions(env);
893
+ const command = input.tool_input?.["command"];
894
+ const response = input.tool_response;
895
+ if (!options.output_cleaners || typeof command !== "string" || OWN_REPORTS.test(command) || typeof response !== "object" || response === null) {
896
+ return void 0;
897
+ }
898
+ const raw = rawOf(response);
899
+ if (raw === void 0 || raw === "") {
900
+ return void 0;
901
+ }
902
+ const session = input.session_id ?? "session";
903
+ const { out } = filterRun(raw, {
904
+ command,
905
+ exitCode: "0",
906
+ durationS: String(Math.round((input.duration_ms ?? 0) / 1e3)),
907
+ enabled: parseCleaners(options.cleaners),
908
+ holdout: options.output_holdout,
909
+ logsDir: outputDir(dataDir(env)),
910
+ // One file per call, named so a person can tell which session it came from.
911
+ runName: `${session}-${input.tool_use_id ?? Date.now().toString(36)}.log`,
912
+ sessionKey: session,
913
+ values: [],
914
+ tags: { project: projectDir(input, env), session }
915
+ });
916
+ if (out === raw) {
917
+ return void 0;
918
+ }
919
+ return { hookSpecificOutput: { hookEventName: "PostToolUse", updatedToolOutput: { ...response, stdout: out, stderr: "" } } };
920
+ };
921
+ if (process.argv[1]?.endsWith("post-bash.mjs")) {
922
+ await runHook((input) => postBash(input));
923
+ }
924
+ export {
925
+ outputDir,
926
+ postBash
927
+ };