@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.
- package/.claude-plugin/plugin.json +82 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/bin/fileq +9 -0
- package/bin/intentic-notes-evidence +3 -0
- package/bin/intentic-stats +3 -0
- package/bin/retrieve-output +3 -0
- package/dist/fileq.mjs +147667 -0
- package/dist/notes-evidence.mjs +749 -0
- package/dist/post-bash.mjs +927 -0
- package/dist/post-edit.mjs +252 -0
- package/dist/prompt-submit.mjs +220 -0
- package/dist/retrieve-output.mjs +38 -0
- package/dist/session-start.mjs +1658 -0
- package/dist/stats.mjs +1847 -0
- package/generated/iq-nudge.txt +3 -0
- package/generated/output-styles/intentic.md +13 -0
- package/generated/skills/field-notes/SKILL.md +40 -0
- package/generated/skills/fileq/SKILL.md +56 -0
- package/generated/skills/iq/SKILL.md +38 -0
- package/generated/skills/stats/SKILL.md +11 -0
- package/hooks/hooks.json +49 -0
- package/package.json +60 -0
package/dist/stats.mjs
ADDED
|
@@ -0,0 +1,1847 @@
|
|
|
1
|
+
import { createRequire as __intenticRequire } from 'node:module'; const require = __intenticRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// src/stats.ts
|
|
4
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
5
|
+
import { join as join8 } from "node:path";
|
|
6
|
+
|
|
7
|
+
// ../agent-context/src/arm-stats.ts
|
|
8
|
+
var MIN_ARM_TURNS = 30;
|
|
9
|
+
var Z_95 = 1.96;
|
|
10
|
+
var round1 = (value) => Math.round(value * 10) / 10;
|
|
11
|
+
var SEARCH_CALLS = { name: "searchCalls", of: (turn) => turn.searchCalls, round: round1 };
|
|
12
|
+
var OPENING_SEARCHES = { name: "openingSearches", of: (turn) => turn.openingSearches, round: round1 };
|
|
13
|
+
var ROOT_LISTINGS = { name: "openingListings", of: (turn) => turn.openingListings, round: round1 };
|
|
14
|
+
var CALLS_BEFORE_TARGET = { name: "callsBeforeTarget", of: (turn) => turn.callsBeforeTarget, round: round1 };
|
|
15
|
+
var FAILED_CALLS = { name: "failedCalls", of: (turn) => turn.failedCalls, round: round1 };
|
|
16
|
+
var armOfValues = (values) => {
|
|
17
|
+
if (values.length === 0) {
|
|
18
|
+
return { turns: 0, mean: 0, variance: 0 };
|
|
19
|
+
}
|
|
20
|
+
const mean = values.reduce((sum2, value) => sum2 + value, 0) / values.length;
|
|
21
|
+
const variance = values.length < 2 ? 0 : values.reduce((sum2, value) => sum2 + (value - mean) ** 2, 0) / (values.length - 1);
|
|
22
|
+
return { turns: values.length, mean, variance };
|
|
23
|
+
};
|
|
24
|
+
var RESOLVING_MARGIN_PCT = 10;
|
|
25
|
+
var controlTurnsNeededFor = (offTurns, marginPct) => {
|
|
26
|
+
if (marginPct <= RESOLVING_MARGIN_PCT) {
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
return Math.ceil(offTurns * (marginPct / RESOLVING_MARGIN_PCT) ** 2) - offTurns;
|
|
30
|
+
};
|
|
31
|
+
var readingOfArms = (on, off, metric, claimRealizedSaving) => {
|
|
32
|
+
const arms = {
|
|
33
|
+
metric: metric.name,
|
|
34
|
+
on: { turns: on.turns, mean: metric.round(on.mean) },
|
|
35
|
+
off: { turns: off.turns, mean: metric.round(off.mean) }
|
|
36
|
+
};
|
|
37
|
+
if (on.turns < MIN_ARM_TURNS || off.turns < MIN_ARM_TURNS || off.mean === 0) {
|
|
38
|
+
return arms;
|
|
39
|
+
}
|
|
40
|
+
const standardError = Math.sqrt(on.variance / on.turns + off.variance / off.turns);
|
|
41
|
+
const deltaPct = round1((on.mean - off.mean) / off.mean * 100);
|
|
42
|
+
const marginPct = round1(Z_95 * standardError / off.mean * 100);
|
|
43
|
+
if (Math.abs(deltaPct) <= marginPct) {
|
|
44
|
+
const controlTurnsNeeded = controlTurnsNeededFor(off.turns, marginPct);
|
|
45
|
+
return { ...arms, marginPct, ...controlTurnsNeeded !== void 0 ? { controlTurnsNeeded } : {} };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...arms,
|
|
49
|
+
marginPct,
|
|
50
|
+
deltaPct,
|
|
51
|
+
...claimRealizedSaving ? { saved: metric.round((off.mean - on.mean) * on.turns) } : {}
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
var meanOfTurns = (turns, metric) => {
|
|
55
|
+
const measured = turns.map(metric.of).filter((value) => value !== void 0);
|
|
56
|
+
return measured.length === 0 ? void 0 : measured.reduce((sum2, value) => sum2 + value, 0) / measured.length;
|
|
57
|
+
};
|
|
58
|
+
var openingTurn = (turns, metric) => {
|
|
59
|
+
const opening = turns.filter((turn) => turn.turnIndex === 0).toSorted((left, right) => left.at - right.at)[0];
|
|
60
|
+
return opening === void 0 ? void 0 : metric.of(opening);
|
|
61
|
+
};
|
|
62
|
+
var MECHANISMS = {
|
|
63
|
+
// The search teaching rides every turn, so every turn is evidence.
|
|
64
|
+
search: { metrics: [SEARCH_CALLS, OPENING_SEARCHES], sampleUnit: "conversations", sample: meanOfTurns },
|
|
65
|
+
// The map is sent once and judged on the turn it was sent to; `callsBeforeTarget` exists only on turns that edited.
|
|
66
|
+
map: { metrics: [ROOT_LISTINGS, CALLS_BEFORE_TARGET], sampleUnit: "opening turns", sample: openingTurn },
|
|
67
|
+
// The brief rides the whole session, so turn 40 is as much evidence as turn 1.
|
|
68
|
+
notes: { metrics: [FAILED_CALLS, CALLS_BEFORE_TARGET], sampleUnit: "conversations", sample: meanOfTurns },
|
|
69
|
+
// Judged on what the long form was written to prevent: calls that fail, and calls spent before reaching the work.
|
|
70
|
+
guidance: { metrics: [FAILED_CALLS, CALLS_BEFORE_TARGET], sampleUnit: "conversations", sample: meanOfTurns }
|
|
71
|
+
};
|
|
72
|
+
var measureExperiment = (turns, design) => {
|
|
73
|
+
const latest = turns.filter((turn) => design.arm(turn) !== void 0 && design.cohort(turn) !== void 0).toSorted((left, right) => right.at - left.at)[0];
|
|
74
|
+
const cohort = latest === void 0 ? void 0 : design.cohort(latest);
|
|
75
|
+
const cohortTurns = cohort === void 0 ? turns.filter((turn) => design.cohort(turn) === void 0) : turns.filter((turn) => design.cohort(turn) === cohort);
|
|
76
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const turn of cohortTurns) {
|
|
78
|
+
const arm = design.arm(turn);
|
|
79
|
+
if (turn.conversationId === void 0 || arm === void 0) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const current = grouped.get(turn.conversationId);
|
|
83
|
+
if (current === void 0) {
|
|
84
|
+
grouped.set(turn.conversationId, { arm, valid: true, turns: [turn] });
|
|
85
|
+
} else {
|
|
86
|
+
current.valid &&= current.arm === arm;
|
|
87
|
+
current.turns.push(turn);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const samples = [...grouped.values()].filter((entry) => entry.valid).map((entry) => ({ arm: entry.arm, turns: entry.turns }));
|
|
91
|
+
if (samples.length === 0) {
|
|
92
|
+
return void 0;
|
|
93
|
+
}
|
|
94
|
+
const reading = (metric) => {
|
|
95
|
+
const values = (arm) => samples.flatMap((sample) => {
|
|
96
|
+
const value = sample.arm === arm ? design.sample(sample.turns, metric) : void 0;
|
|
97
|
+
return value === void 0 ? [] : [value];
|
|
98
|
+
});
|
|
99
|
+
return readingOfArms(armOfValues(values(true)), armOfValues(values(false)), metric, false);
|
|
100
|
+
};
|
|
101
|
+
const [headline, ...rest] = design.metrics;
|
|
102
|
+
return {
|
|
103
|
+
metrics: [reading(headline), ...rest.map(reading)],
|
|
104
|
+
minTurns: MIN_ARM_TURNS,
|
|
105
|
+
sampleUnit: design.sampleUnit,
|
|
106
|
+
...cohort !== void 0 ? { cohort } : {}
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ../agent-context/src/claude-transcript.ts
|
|
111
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
112
|
+
import { basename, dirname, join } from "node:path";
|
|
113
|
+
|
|
114
|
+
// ../agent-context/src/tool-calls.ts
|
|
115
|
+
import { isAbsolute, relative } from "node:path";
|
|
116
|
+
var DISPLAY_NAMES = {
|
|
117
|
+
bash: "Bash",
|
|
118
|
+
edit: "Edit",
|
|
119
|
+
write: "Write",
|
|
120
|
+
read: "Read",
|
|
121
|
+
grep: "Grep",
|
|
122
|
+
glob: "Glob",
|
|
123
|
+
list: "LS",
|
|
124
|
+
webfetch: "WebFetch",
|
|
125
|
+
websearch: "WebSearch",
|
|
126
|
+
task: "Task",
|
|
127
|
+
patch: "Edit"
|
|
128
|
+
};
|
|
129
|
+
var BROWSER_TOOL = /^mcp__.+__browser_(.+)$/;
|
|
130
|
+
var BROWSER_VERB_NAMES = { take_screenshot: "screenshot" };
|
|
131
|
+
var browserDisplayName = (raw) => {
|
|
132
|
+
const verb = BROWSER_TOOL.exec(raw)?.[1];
|
|
133
|
+
return verb === void 0 ? void 0 : `Browser ${(BROWSER_VERB_NAMES[verb] ?? verb).replaceAll("_", " ")}`;
|
|
134
|
+
};
|
|
135
|
+
var displayNameOf = (raw) => DISPLAY_NAMES[raw] ?? browserDisplayName(raw) ?? raw;
|
|
136
|
+
var CATEGORIES = [
|
|
137
|
+
["read", "read"],
|
|
138
|
+
["edit", "edit"],
|
|
139
|
+
["write", "edit"],
|
|
140
|
+
["multiedit", "edit"],
|
|
141
|
+
["notebookedit", "edit"],
|
|
142
|
+
["bash", "execute"],
|
|
143
|
+
["bashoutput", "execute"],
|
|
144
|
+
["killshell", "execute"],
|
|
145
|
+
["grep", "search"],
|
|
146
|
+
["glob", "search"],
|
|
147
|
+
["ls", "search"],
|
|
148
|
+
["websearch", "search"],
|
|
149
|
+
["webfetch", "fetch"],
|
|
150
|
+
["task", "other"]
|
|
151
|
+
];
|
|
152
|
+
var CATEGORY_BY_NAME = new Map(CATEGORIES);
|
|
153
|
+
var MCP_VERBS = [
|
|
154
|
+
["edit", "edit"],
|
|
155
|
+
["write", "edit"],
|
|
156
|
+
["read", "read"],
|
|
157
|
+
["search", "search"],
|
|
158
|
+
["fetch", "fetch"],
|
|
159
|
+
["delete", "delete"],
|
|
160
|
+
["move", "move"],
|
|
161
|
+
["run", "execute"],
|
|
162
|
+
["exec", "execute"]
|
|
163
|
+
];
|
|
164
|
+
var BROWSER_VERB_KINDS = {
|
|
165
|
+
navigate: "fetch",
|
|
166
|
+
navigate_back: "fetch",
|
|
167
|
+
snapshot: "read",
|
|
168
|
+
take_screenshot: "read",
|
|
169
|
+
console_messages: "read",
|
|
170
|
+
network_requests: "read",
|
|
171
|
+
network_request: "read",
|
|
172
|
+
tabs: "read"
|
|
173
|
+
};
|
|
174
|
+
var toolCategoryOf = (name) => {
|
|
175
|
+
const exact = CATEGORY_BY_NAME.get(name.toLowerCase());
|
|
176
|
+
if (exact !== void 0) {
|
|
177
|
+
return exact;
|
|
178
|
+
}
|
|
179
|
+
const browserVerb = BROWSER_TOOL.exec(name)?.[1];
|
|
180
|
+
if (browserVerb !== void 0) {
|
|
181
|
+
return BROWSER_VERB_KINDS[browserVerb] ?? "execute";
|
|
182
|
+
}
|
|
183
|
+
const segment = name.includes("__") ? name.split("__").pop() : name.includes(".") ? name.split(".").pop() : void 0;
|
|
184
|
+
if (segment === void 0) {
|
|
185
|
+
return "other";
|
|
186
|
+
}
|
|
187
|
+
const verb = segment.toLowerCase();
|
|
188
|
+
for (const [suffix, kind] of MCP_VERBS) {
|
|
189
|
+
if (verb === suffix || verb.endsWith(suffix)) {
|
|
190
|
+
return kind;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return "other";
|
|
194
|
+
};
|
|
195
|
+
var SEARCH_COMMANDS = /* @__PURE__ */ new Set(["iq", "grep", "rg", "ag", "ack", "find", "fd", "fdfind", "locate", "ls", "tree"]);
|
|
196
|
+
var FILE_WORK_COMMANDS = /* @__PURE__ */ new Set(["cat", "sed", "head", "tail", "less", "more", "bat", "awk"]);
|
|
197
|
+
var statementsOf = (command) => command.split(/&&|\|\||;|\n/);
|
|
198
|
+
var commandHeads = (command) => statementsOf(command).map((statement) => {
|
|
199
|
+
const head = statement.trim().split(/\s+/).find((word) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(word)) ?? "";
|
|
200
|
+
return head.split("/").pop() ?? head;
|
|
201
|
+
});
|
|
202
|
+
var isSearchCall = (call) => {
|
|
203
|
+
if (call.category === "search") {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if (call.category !== "execute" || call.target === void 0) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
return commandHeads(call.target).some((head) => SEARCH_COMMANDS.has(head));
|
|
210
|
+
};
|
|
211
|
+
var LISTING_COMMANDS = /* @__PURE__ */ new Set(["ls", "tree"]);
|
|
212
|
+
var LISTING_TOOLS = /* @__PURE__ */ new Set(["ls", "list"]);
|
|
213
|
+
var depthBelow = (raw, root) => {
|
|
214
|
+
const path = raw.replace(/^["']|["']$/g, "").replace(/\/+$/, "");
|
|
215
|
+
if (path === "" || path === "." || path === "./") {
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
const rel = isAbsolute(path) ? relative(root, path) : path.replace(/^\.\//, "");
|
|
219
|
+
if (rel === "") {
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
return rel === ".." || rel.startsWith("../") ? void 0 : rel.split("/").length;
|
|
223
|
+
};
|
|
224
|
+
var isRootListing = (call, root) => {
|
|
225
|
+
const shallow = (path) => {
|
|
226
|
+
const depth = depthBelow(path, root);
|
|
227
|
+
return depth !== void 0 && depth <= 1;
|
|
228
|
+
};
|
|
229
|
+
if (call.name !== void 0 && LISTING_TOOLS.has(call.name.toLowerCase())) {
|
|
230
|
+
return call.target === void 0 || shallow(call.target);
|
|
231
|
+
}
|
|
232
|
+
if (call.category !== "execute" || call.target === void 0) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
return statementsOf(call.target).some((statement) => {
|
|
236
|
+
const words = (statement.split("|")[0] ?? "").trim().split(/\s+/);
|
|
237
|
+
const head = words.find((word) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(word)) ?? "";
|
|
238
|
+
if (!LISTING_COMMANDS.has(head.split("/").pop() ?? head)) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
const args = words.slice(words.indexOf(head) + 1).filter((word) => word !== "" && !word.startsWith("-"));
|
|
242
|
+
return args.length === 0 ? true : !args.some((arg) => arg.includes("*")) && args.some(shallow);
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
var isFileWorkCall = (call) => {
|
|
246
|
+
if (call.category === "read" || call.category === "edit") {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
if (call.category !== "execute" || call.target === void 0) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
return commandHeads(call.target).some((head) => FILE_WORK_COMMANDS.has(head));
|
|
253
|
+
};
|
|
254
|
+
var searchPrecedesFileWork = (call) => {
|
|
255
|
+
if (call.category === "search") {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
if (call.category !== "execute" || call.target === void 0) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
const heads = commandHeads(call.target);
|
|
262
|
+
const searchAt = heads.findIndex((head) => SEARCH_COMMANDS.has(head));
|
|
263
|
+
const workAt = heads.findIndex((head) => FILE_WORK_COMMANDS.has(head));
|
|
264
|
+
return searchAt !== -1 && (workAt === -1 || searchAt < workAt);
|
|
265
|
+
};
|
|
266
|
+
var TARGET_KEYS = ["file_path", "filePath", "notebook_path", "command", "pattern", "url", "element", "path", "query"];
|
|
267
|
+
var toolTarget = (input) => {
|
|
268
|
+
if (typeof input !== "object" || input === null) {
|
|
269
|
+
return void 0;
|
|
270
|
+
}
|
|
271
|
+
const record = input;
|
|
272
|
+
for (const key of TARGET_KEYS) {
|
|
273
|
+
const value = record[key];
|
|
274
|
+
if (typeof value === "string") {
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return void 0;
|
|
279
|
+
};
|
|
280
|
+
var PATH_KEYS = ["file_path", "filePath", "notebook_path", "path"];
|
|
281
|
+
var toolPathsUnder = (input, root) => {
|
|
282
|
+
if (typeof input !== "object" || input === null) {
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
285
|
+
const record = input;
|
|
286
|
+
for (const key of PATH_KEYS) {
|
|
287
|
+
const value = record[key];
|
|
288
|
+
if (typeof value !== "string") {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const rel = isAbsolute(value) ? relative(root, value) : value;
|
|
292
|
+
return rel === "" || rel === "." || rel === ".." || rel.startsWith("../") || isAbsolute(rel) ? void 0 : [{ path: rel.split("\\").join("/") }];
|
|
293
|
+
}
|
|
294
|
+
return void 0;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// ../agent-context/src/claude-transcript.ts
|
|
298
|
+
var FAILURE_TEXT_CHARS = 400;
|
|
299
|
+
var stampOf = (line) => {
|
|
300
|
+
const at = line.timestamp === void 0 ? Number.NaN : Date.parse(line.timestamp);
|
|
301
|
+
return Number.isNaN(at) ? void 0 : at;
|
|
302
|
+
};
|
|
303
|
+
var textOf = (content) => {
|
|
304
|
+
if (typeof content === "string") {
|
|
305
|
+
return content;
|
|
306
|
+
}
|
|
307
|
+
if (!Array.isArray(content)) {
|
|
308
|
+
return "";
|
|
309
|
+
}
|
|
310
|
+
return content.map((block) => block["type"] === "text" && typeof block["text"] === "string" ? block["text"] : "").filter((text) => text !== "").join("\n");
|
|
311
|
+
};
|
|
312
|
+
var parse = (raw) => {
|
|
313
|
+
if (!raw.includes(`"type":"user"`) && !raw.includes(`"type":"assistant"`)) {
|
|
314
|
+
return void 0;
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
return JSON.parse(raw);
|
|
318
|
+
} catch {
|
|
319
|
+
return void 0;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
var isMissing = (error) => {
|
|
323
|
+
const code = typeof error === "object" && error !== null ? error.code : void 0;
|
|
324
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
325
|
+
};
|
|
326
|
+
var promptOf = (line) => {
|
|
327
|
+
if (line.type !== "user" || line.isMeta === true || line.isCompactSummary === true) {
|
|
328
|
+
return void 0;
|
|
329
|
+
}
|
|
330
|
+
const content = line.message?.content;
|
|
331
|
+
if (Array.isArray(content) && content.some((block) => block["type"] === "tool_result")) {
|
|
332
|
+
return void 0;
|
|
333
|
+
}
|
|
334
|
+
const text = textOf(content);
|
|
335
|
+
return text.startsWith("<local-command-") ? void 0 : text;
|
|
336
|
+
};
|
|
337
|
+
var callsOf = (line, options, subagent) => {
|
|
338
|
+
const content = line.message?.content;
|
|
339
|
+
if (line.type !== "assistant" || !Array.isArray(content)) {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
const locate = options.locate ?? toolPathsUnder;
|
|
343
|
+
const at = stampOf(line);
|
|
344
|
+
return content.flatMap((block, index) => {
|
|
345
|
+
const raw = block["name"];
|
|
346
|
+
if (block["type"] !== "tool_use" || typeof raw !== "string") {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
const name = displayNameOf(raw);
|
|
350
|
+
const target = toolTarget(block["input"]);
|
|
351
|
+
const locations = locate(block["input"], options.root);
|
|
352
|
+
return [
|
|
353
|
+
{
|
|
354
|
+
id: typeof block["id"] === "string" ? block["id"] : `${at ?? 0}-${index}`,
|
|
355
|
+
name,
|
|
356
|
+
category: toolCategoryOf(name),
|
|
357
|
+
at,
|
|
358
|
+
subagent,
|
|
359
|
+
...target !== void 0 ? { target } : {},
|
|
360
|
+
...locations !== void 0 ? { locations } : {}
|
|
361
|
+
}
|
|
362
|
+
];
|
|
363
|
+
});
|
|
364
|
+
};
|
|
365
|
+
var failuresOf = (line) => {
|
|
366
|
+
const content = line.message?.content;
|
|
367
|
+
if (line.type !== "user" || !Array.isArray(content)) {
|
|
368
|
+
return [];
|
|
369
|
+
}
|
|
370
|
+
return content.flatMap(
|
|
371
|
+
(block) => block["type"] === "tool_result" && block["is_error"] === true && typeof block["tool_use_id"] === "string" ? [{ id: block["tool_use_id"], text: (typeof block["content"] === "string" ? block["content"] : textOf(block["content"])).slice(0, FAILURE_TEXT_CHARS) }] : []
|
|
372
|
+
);
|
|
373
|
+
};
|
|
374
|
+
var readClaudeTranscript = (text, options) => {
|
|
375
|
+
const turns = [];
|
|
376
|
+
let sessionId;
|
|
377
|
+
let cwd;
|
|
378
|
+
for (const raw of text.split("\n")) {
|
|
379
|
+
const line = parse(raw);
|
|
380
|
+
if (line === void 0) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
sessionId ??= line.sessionId;
|
|
384
|
+
cwd ??= line.cwd;
|
|
385
|
+
const prompt = promptOf(line);
|
|
386
|
+
if (prompt !== void 0) {
|
|
387
|
+
turns.push({ index: turns.length, at: stampOf(line) ?? turns.at(-1)?.at ?? 0, prompt, calls: [], failures: [] });
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const turn = turns.at(-1);
|
|
391
|
+
if (turn === void 0) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
turn.calls.push(...callsOf(line, options, false));
|
|
395
|
+
turn.failures.push(...failuresOf(line));
|
|
396
|
+
}
|
|
397
|
+
return { sessionId, cwd, turns };
|
|
398
|
+
};
|
|
399
|
+
var subagentActivity = (file, options) => {
|
|
400
|
+
const dir = join(dirname(file), basename(file, ".jsonl"), "subagents");
|
|
401
|
+
let names;
|
|
402
|
+
try {
|
|
403
|
+
names = readdirSync(dir).filter((name) => name.endsWith(".jsonl"));
|
|
404
|
+
} catch {
|
|
405
|
+
return { calls: [], failures: [] };
|
|
406
|
+
}
|
|
407
|
+
const calls = [];
|
|
408
|
+
const failures = [];
|
|
409
|
+
for (const name of names) {
|
|
410
|
+
let text;
|
|
411
|
+
try {
|
|
412
|
+
text = readFileSync(join(dir, name), "utf8");
|
|
413
|
+
} catch {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
for (const raw of text.split("\n")) {
|
|
417
|
+
const line = parse(raw);
|
|
418
|
+
if (line === void 0) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
calls.push(...callsOf(line, options, true));
|
|
422
|
+
failures.push(...failuresOf(line).map((failure) => ({ ...failure, at: stampOf(line) })));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return { calls, failures };
|
|
426
|
+
};
|
|
427
|
+
var turnAt = (turns, at) => {
|
|
428
|
+
if (at === void 0) {
|
|
429
|
+
return void 0;
|
|
430
|
+
}
|
|
431
|
+
let found;
|
|
432
|
+
for (const turn of turns) {
|
|
433
|
+
if (turn.at <= at) {
|
|
434
|
+
found = turn;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return found;
|
|
438
|
+
};
|
|
439
|
+
var readClaudeSession = (file, options) => {
|
|
440
|
+
let text;
|
|
441
|
+
try {
|
|
442
|
+
text = readFileSync(file, "utf8");
|
|
443
|
+
} catch (error) {
|
|
444
|
+
if (isMissing(error)) {
|
|
445
|
+
return void 0;
|
|
446
|
+
}
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
const session = readClaudeTranscript(text, options);
|
|
450
|
+
if (session.turns.length === 0) {
|
|
451
|
+
return session;
|
|
452
|
+
}
|
|
453
|
+
const activity = subagentActivity(file, options);
|
|
454
|
+
for (const call of activity.calls) {
|
|
455
|
+
turnAt(session.turns, call.at)?.calls.push(call);
|
|
456
|
+
}
|
|
457
|
+
for (const { at, ...failure } of activity.failures) {
|
|
458
|
+
turnAt(session.turns, at)?.failures.push(failure);
|
|
459
|
+
}
|
|
460
|
+
for (const turn of session.turns) {
|
|
461
|
+
turn.calls.sort((left, right) => (left.at ?? 0) - (right.at ?? 0));
|
|
462
|
+
}
|
|
463
|
+
return session;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// ../../_tools/base/src/outside-text.ts
|
|
467
|
+
var CONTROL_TAGS = ["system-reminder", "task-notification", "command-name", "command-message", "command-args", "local-command-stdout"];
|
|
468
|
+
var CONTROL = new RegExp(String.raw`<\s*/?\s*(?:${CONTROL_TAGS.join("|")})\b[^>]*>`, "gi");
|
|
469
|
+
|
|
470
|
+
// ../../_tools/constants/src/hosted-tiers.ts
|
|
471
|
+
var HOSTED_TIERS = [
|
|
472
|
+
{ id: "free", name: "Free", cpuKind: "shared", cpus: 4, memoryMb: 4096, volumeGb: 10, monthlyHours: 40, priceUsd: 0, flyHourUsd: 0.0329 },
|
|
473
|
+
{ id: "standard", name: "Standard", cpuKind: "shared", cpus: 8, memoryMb: 8192, volumeGb: 25, monthlyHours: 220, priceUsd: 20, flyHourUsd: 0.0657 },
|
|
474
|
+
{ id: "max", name: "Max", cpuKind: "shared", cpus: 8, memoryMb: 16384, volumeGb: 50, monthlyHours: 320, priceUsd: 50, flyHourUsd: 0.1234 }
|
|
475
|
+
];
|
|
476
|
+
var FREE_TIER = HOSTED_TIERS[0];
|
|
477
|
+
var PAID_TIERS = HOSTED_TIERS.filter((tier) => tier.priceUsd > 0);
|
|
478
|
+
|
|
479
|
+
// ../../_tools/constants/src/index.ts
|
|
480
|
+
var STATE_DIR = ".intentic";
|
|
481
|
+
var GOOGLE_CLIENT_ID = "481795963975-cq9msl6higcd91joidrfp8mjlkuq5fk3.apps.googleusercontent.com";
|
|
482
|
+
var GOOGLE_TOKEN_STORAGE_KEY = `intentic.gid.${GOOGLE_CLIENT_ID}`;
|
|
483
|
+
|
|
484
|
+
// ../fileq/src/lib/sidecar.ts
|
|
485
|
+
var DERIVED_DIR = `${STATE_DIR}/local/cache/derived`;
|
|
486
|
+
|
|
487
|
+
// ../agent-context/src/turn-metrics.ts
|
|
488
|
+
var createTurnMetrics = (root) => {
|
|
489
|
+
let calls = 0;
|
|
490
|
+
let searchCalls = 0;
|
|
491
|
+
let openingSearches = 0;
|
|
492
|
+
let openingListings = 0;
|
|
493
|
+
let reachedTheWork = false;
|
|
494
|
+
const failed = /* @__PURE__ */ new Set();
|
|
495
|
+
const firstTouch = /* @__PURE__ */ new Map();
|
|
496
|
+
const noteTouches = (locations) => {
|
|
497
|
+
for (const { path } of locations ?? []) {
|
|
498
|
+
firstTouch.set(path, firstTouch.get(path) ?? calls - 1);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
return {
|
|
502
|
+
call: (call) => {
|
|
503
|
+
calls += 1;
|
|
504
|
+
const searched = isSearchCall(call);
|
|
505
|
+
if (searched) {
|
|
506
|
+
searchCalls += 1;
|
|
507
|
+
}
|
|
508
|
+
if (searched && !reachedTheWork && searchPrecedesFileWork(call)) {
|
|
509
|
+
openingSearches += 1;
|
|
510
|
+
}
|
|
511
|
+
if (!reachedTheWork && isRootListing(call, root)) {
|
|
512
|
+
openingListings += 1;
|
|
513
|
+
}
|
|
514
|
+
noteTouches(call.locations);
|
|
515
|
+
if (isFileWorkCall(call)) {
|
|
516
|
+
reachedTheWork = true;
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
failed: (id) => {
|
|
520
|
+
failed.add(id);
|
|
521
|
+
},
|
|
522
|
+
calls: () => calls,
|
|
523
|
+
reading: (edited) => {
|
|
524
|
+
const reached = edited.map((path) => firstTouch.get(path)).filter((at) => at !== void 0);
|
|
525
|
+
return {
|
|
526
|
+
searchCalls,
|
|
527
|
+
openingSearches,
|
|
528
|
+
openingListings,
|
|
529
|
+
failedCalls: failed.size,
|
|
530
|
+
...reached.length > 0 ? { callsBeforeTarget: Math.min(...reached) } : {}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// ../output-cleaners/src/filter-stats.mjs
|
|
537
|
+
var sum = (rows, key) => rows.reduce((total, row) => total + (row[key] ?? 0), 0);
|
|
538
|
+
var tokens = (bytes) => Math.round(bytes / 4);
|
|
539
|
+
var READ_VERBS = /* @__PURE__ */ new Set(["cat", "bat", "sed", "awk", "head", "tail", "less", "more"]);
|
|
540
|
+
var isPrefixWord = (word, previous) => /^[A-Za-z_]\w*=/.test(word) || word === "sudo" || word === "timeout" || previous === "timeout" && /^\d+[smhd]?$/.test(word);
|
|
541
|
+
var SHELL_NOISE = /* @__PURE__ */ new Set(["cd", "echo", "true", "set", "export", "source", "time", "for", "do", "done", "while", "until", "if", "then", "else", "elif", "fi", "case", "esac"]);
|
|
542
|
+
var SUBCOMMAND_VERBS = /* @__PURE__ */ new Set(["git", "pnpm", "npm", "npx", "yarn", "docker", "cargo", "go", "gh", "kubectl", "systemctl", "apt", "apt-get", "pip", "poetry"]);
|
|
543
|
+
var segmentVerb = (segment) => {
|
|
544
|
+
const words = segment.trim().split(/\s+/);
|
|
545
|
+
let index = 0;
|
|
546
|
+
while (index < words.length && isPrefixWord(words[index], words[index - 1])) {
|
|
547
|
+
index++;
|
|
548
|
+
}
|
|
549
|
+
const word = (words[index] ?? "").replace(/^[({]+/, "");
|
|
550
|
+
const verb = word.slice(word.lastIndexOf("/") + 1);
|
|
551
|
+
if (verb === "" || SHELL_NOISE.has(verb)) {
|
|
552
|
+
return "";
|
|
553
|
+
}
|
|
554
|
+
const next = words[index + 1];
|
|
555
|
+
return SUBCOMMAND_VERBS.has(verb) && next !== void 0 && /^[a-z][\w-]*$/.test(next) ? `${verb} ${next}` : verb;
|
|
556
|
+
};
|
|
557
|
+
var commandSignature = (command) => {
|
|
558
|
+
for (const segment of String(command).split(/[|;&]+/)) {
|
|
559
|
+
const verb = segmentVerb(segment);
|
|
560
|
+
if (verb !== "") {
|
|
561
|
+
return verb;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return "";
|
|
565
|
+
};
|
|
566
|
+
var sortedBytes = (rows, key) => rows.map((row) => row[key] ?? 0).toSorted((a, b) => a - b);
|
|
567
|
+
var median = (sorted) => sorted[sorted.length >> 1] ?? 0;
|
|
568
|
+
var mannWhitneyZ = (first, second) => {
|
|
569
|
+
const n1 = first.length;
|
|
570
|
+
const n2 = second.length;
|
|
571
|
+
const total = n1 + n2;
|
|
572
|
+
if (n1 === 0 || n2 === 0) {
|
|
573
|
+
return 0;
|
|
574
|
+
}
|
|
575
|
+
const merged = [...first.map((value) => ({ value, isFirst: true })), ...second.map((value) => ({ value, isFirst: false }))].toSorted(
|
|
576
|
+
(left, right) => left.value - right.value
|
|
577
|
+
);
|
|
578
|
+
let rankSum = 0;
|
|
579
|
+
let tieCorrection = 0;
|
|
580
|
+
let start = 0;
|
|
581
|
+
let firstsInGroup = 0;
|
|
582
|
+
let previous;
|
|
583
|
+
const closeGroup = (end) => {
|
|
584
|
+
const tied = end - start;
|
|
585
|
+
if (tied > 1) {
|
|
586
|
+
tieCorrection += tied ** 3 - tied;
|
|
587
|
+
}
|
|
588
|
+
rankSum += firstsInGroup * ((start + end + 1) / 2);
|
|
589
|
+
};
|
|
590
|
+
merged.forEach((entry, position) => {
|
|
591
|
+
if (previous !== void 0 && entry.value !== previous) {
|
|
592
|
+
closeGroup(position);
|
|
593
|
+
start = position;
|
|
594
|
+
firstsInGroup = 0;
|
|
595
|
+
}
|
|
596
|
+
previous = entry.value;
|
|
597
|
+
if (entry.isFirst) {
|
|
598
|
+
firstsInGroup++;
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
closeGroup(total);
|
|
602
|
+
const u = rankSum - n1 * (n1 + 1) / 2;
|
|
603
|
+
const deviation = Math.sqrt(n1 * n2 / 12 * (total + 1 - tieCorrection / (total * (total - 1))));
|
|
604
|
+
return deviation === 0 ? 0 : (u - n1 * n2 / 2) / deviation;
|
|
605
|
+
};
|
|
606
|
+
var MIN_HELD_COMMANDS = 30;
|
|
607
|
+
var Z_952 = 1.96;
|
|
608
|
+
var GAP_MIN_BYTES = 2e3;
|
|
609
|
+
var summarizeGaps = (cleaned) => {
|
|
610
|
+
const byCommand = /* @__PURE__ */ new Map();
|
|
611
|
+
for (const row of cleaned) {
|
|
612
|
+
if (row.matched !== void 0 && row.matched.length > 0 || (row.emittedBytes ?? 0) <= GAP_MIN_BYTES) {
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const signature = commandSignature(String(row.command ?? ""));
|
|
616
|
+
if (READ_VERBS.has(signature)) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
const current = byCommand.get(signature) ?? { commands: 0, bytes: 0 };
|
|
620
|
+
byCommand.set(signature, { commands: current.commands + 1, bytes: current.bytes + (row.emittedBytes ?? 0) });
|
|
621
|
+
}
|
|
622
|
+
return [...byCommand.entries()].map(([command, entry]) => ({ command, commands: entry.commands, tokens: tokens(entry.bytes) })).toSorted((a, b) => b.tokens - a.tokens).slice(0, 15);
|
|
623
|
+
};
|
|
624
|
+
var summarizeStats = (rows) => {
|
|
625
|
+
const cleaned = rows.filter((row) => row.heldOut !== true);
|
|
626
|
+
const held = rows.filter((row) => row.heldOut === true);
|
|
627
|
+
const cleanedRaw = sum(cleaned, "rawBytes");
|
|
628
|
+
const cleanedEmitted = sum(cleaned, "emittedBytes");
|
|
629
|
+
const savedPct = cleanedRaw === 0 ? 0 : Math.round((cleanedRaw - cleanedEmitted) / cleanedRaw * 100);
|
|
630
|
+
const stages = /* @__PURE__ */ new Map();
|
|
631
|
+
for (const row of cleaned) {
|
|
632
|
+
for (const [id, bytes] of Object.entries(row.stageBytes ?? {})) {
|
|
633
|
+
const current = stages.get(id) ?? { commands: 0, bytes: 0 };
|
|
634
|
+
stages.set(id, { commands: current.commands + 1, bytes: current.bytes + bytes });
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const perCleaner = [...stages.entries()].map(([id, entry]) => ({ id, commands: entry.commands, savedTokens: tokens(entry.bytes) })).toSorted((a, b) => b.savedTokens - a.savedTokens);
|
|
638
|
+
const heldRaw = sortedBytes(held, "rawBytes");
|
|
639
|
+
const cleanedEmittedSorted = sortedBytes(cleaned, "emittedBytes");
|
|
640
|
+
const measurable = held.length >= MIN_HELD_COMMANDS && median(heldRaw) > 0 && Math.abs(mannWhitneyZ(cleanedEmittedSorted, heldRaw)) > Z_952;
|
|
641
|
+
const measuredSavedPct = measurable ? Math.round((1 - median(cleanedEmittedSorted) / median(heldRaw)) * 100) : void 0;
|
|
642
|
+
return {
|
|
643
|
+
commands: rows.length,
|
|
644
|
+
rawTokens: tokens(sum(rows, "rawBytes")),
|
|
645
|
+
emittedTokens: tokens(sum(rows, "emittedBytes")),
|
|
646
|
+
savedPct,
|
|
647
|
+
perCleaner,
|
|
648
|
+
holdout: { cleaned: cleaned.length, heldOut: held.length, ...measuredSavedPct !== void 0 ? { measuredSavedPct } : {} },
|
|
649
|
+
gaps: summarizeGaps(cleaned)
|
|
650
|
+
};
|
|
651
|
+
};
|
|
652
|
+
var parseStatsFile = (text) => text.split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
|
|
653
|
+
try {
|
|
654
|
+
return [JSON.parse(line)];
|
|
655
|
+
} catch {
|
|
656
|
+
return [];
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// src/features.ts
|
|
661
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
662
|
+
import { join as join2 } from "node:path";
|
|
663
|
+
|
|
664
|
+
// src/runtime.ts
|
|
665
|
+
var isMissing2 = (error) => {
|
|
666
|
+
const code = typeof error === "object" && error !== null ? error.code : void 0;
|
|
667
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
668
|
+
};
|
|
669
|
+
var projectSlug = (project) => project.replace(/[^a-zA-Z0-9]/g, "-");
|
|
670
|
+
|
|
671
|
+
// src/features.ts
|
|
672
|
+
var OPTIONS = {
|
|
673
|
+
output_cleaners: {
|
|
674
|
+
type: "boolean",
|
|
675
|
+
title: "Trim Bash output",
|
|
676
|
+
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.",
|
|
677
|
+
default: true
|
|
678
|
+
},
|
|
679
|
+
cleaners: {
|
|
680
|
+
type: "string",
|
|
681
|
+
title: "Cleaners",
|
|
682
|
+
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.",
|
|
683
|
+
default: ""
|
|
684
|
+
},
|
|
685
|
+
output_holdout: {
|
|
686
|
+
type: "number",
|
|
687
|
+
title: "Untrimmed share",
|
|
688
|
+
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.",
|
|
689
|
+
default: 0,
|
|
690
|
+
min: 0,
|
|
691
|
+
max: 0.5
|
|
692
|
+
},
|
|
693
|
+
project_map: {
|
|
694
|
+
type: "boolean",
|
|
695
|
+
title: "Project map",
|
|
696
|
+
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.",
|
|
697
|
+
default: true
|
|
698
|
+
},
|
|
699
|
+
field_notes: {
|
|
700
|
+
type: "boolean",
|
|
701
|
+
title: "Field notes",
|
|
702
|
+
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.",
|
|
703
|
+
default: true
|
|
704
|
+
},
|
|
705
|
+
iq: {
|
|
706
|
+
type: "boolean",
|
|
707
|
+
title: "iq search",
|
|
708
|
+
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.",
|
|
709
|
+
default: true
|
|
710
|
+
},
|
|
711
|
+
fileq: {
|
|
712
|
+
type: "boolean",
|
|
713
|
+
title: "fileq",
|
|
714
|
+
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.",
|
|
715
|
+
default: true
|
|
716
|
+
},
|
|
717
|
+
shadows: {
|
|
718
|
+
type: "boolean",
|
|
719
|
+
title: "Document shadows",
|
|
720
|
+
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.",
|
|
721
|
+
default: false
|
|
722
|
+
},
|
|
723
|
+
holdout: {
|
|
724
|
+
type: "number",
|
|
725
|
+
title: "Measured share",
|
|
726
|
+
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.",
|
|
727
|
+
default: 0.1,
|
|
728
|
+
min: 0,
|
|
729
|
+
max: 0.5
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
var asBoolean = (raw, fallback) => {
|
|
733
|
+
const value = raw?.trim().toLowerCase();
|
|
734
|
+
if (value === "true" || value === "1" || value === "yes" || value === "on") {
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
if (value === "false" || value === "0" || value === "no" || value === "off") {
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
return fallback;
|
|
741
|
+
};
|
|
742
|
+
var asShare = (raw, option) => {
|
|
743
|
+
const value = raw === void 0 || raw.trim() === "" ? Number.NaN : Number(raw);
|
|
744
|
+
return Number.isFinite(value) ? Math.min(option.max, Math.max(option.min, value)) : option.default;
|
|
745
|
+
};
|
|
746
|
+
var optionEnvName = (key) => `CLAUDE_PLUGIN_OPTION_${key.toUpperCase()}`;
|
|
747
|
+
var readOptions = (env = process.env) => {
|
|
748
|
+
const entries = Object.keys(OPTIONS).map((key) => {
|
|
749
|
+
const option = OPTIONS[key];
|
|
750
|
+
const raw = env[optionEnvName(key)];
|
|
751
|
+
if (option.type === "boolean") {
|
|
752
|
+
return [key, asBoolean(raw, option.default)];
|
|
753
|
+
}
|
|
754
|
+
if (option.type === "number") {
|
|
755
|
+
return [key, asShare(raw, option)];
|
|
756
|
+
}
|
|
757
|
+
return [key, raw ?? option.default];
|
|
758
|
+
});
|
|
759
|
+
return Object.fromEntries(entries);
|
|
760
|
+
};
|
|
761
|
+
var optionsFile = (data) => join2(data, "options.json");
|
|
762
|
+
var loadOptions = (data) => {
|
|
763
|
+
let text;
|
|
764
|
+
try {
|
|
765
|
+
text = readFileSync2(optionsFile(data), "utf8");
|
|
766
|
+
} catch (error) {
|
|
767
|
+
if (isMissing2(error)) {
|
|
768
|
+
return readOptions({});
|
|
769
|
+
}
|
|
770
|
+
throw error;
|
|
771
|
+
}
|
|
772
|
+
let saved;
|
|
773
|
+
try {
|
|
774
|
+
saved = JSON.parse(text);
|
|
775
|
+
} catch {
|
|
776
|
+
saved = {};
|
|
777
|
+
}
|
|
778
|
+
const values = typeof saved === "object" && saved !== null ? saved : {};
|
|
779
|
+
return readOptions(Object.fromEntries(Object.keys(OPTIONS).flatMap((key) => key in values ? [[optionEnvName(key), String(values[key])]] : [])));
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
// src/post-bash.ts
|
|
783
|
+
import { join as join6 } from "node:path";
|
|
784
|
+
|
|
785
|
+
// ../output-cleaners/src/cleaners.mjs
|
|
786
|
+
import { createHash } from "node:crypto";
|
|
787
|
+
import { closeSync, fstatSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
788
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
789
|
+
var ANSI = /\x1b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_])/g;
|
|
790
|
+
var collapseCr = (line) => {
|
|
791
|
+
const frames = line.split("\r");
|
|
792
|
+
for (let i = frames.length - 1; i >= 0; i--) {
|
|
793
|
+
if (frames[i] !== "") {
|
|
794
|
+
return frames[i];
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return "";
|
|
798
|
+
};
|
|
799
|
+
var strip = (id, match, patterns) => ({
|
|
800
|
+
id,
|
|
801
|
+
match,
|
|
802
|
+
apply: (lines) => lines.filter((line) => !patterns.some((re) => re.test(line)))
|
|
803
|
+
});
|
|
804
|
+
var invocation = (word) => new RegExp(String.raw`(?<![\w.\-/])(?:${word})(?![\w.-])`);
|
|
805
|
+
var bodyBytes = (lines) => lines.length === 0 ? 0 : lines.reduce((sum2, line) => sum2 + line.length, 0) + lines.length - 1;
|
|
806
|
+
var humanSize = (bytes) => bytes >= 1048576 ? `${(bytes / 1048576).toFixed(1)}M` : bytes >= 1024 ? `${(bytes / 1024).toFixed(1)}K` : `${bytes}B`;
|
|
807
|
+
var EXECUTABLE = /* @__PURE__ */ new Set(["x", "s", "t"]);
|
|
808
|
+
var permsToOctal = (perms) => {
|
|
809
|
+
const triad = (read, write, exec) => (perms[read] === "r" ? 4 : 0) + (perms[write] === "w" ? 2 : 0) + (EXECUTABLE.has(perms[exec]) ? 1 : 0);
|
|
810
|
+
const special = ("sS".includes(perms[3]) ? 4 : 0) + ("sS".includes(perms[6]) ? 2 : 0) + ("tT".includes(perms[9]) ? 1 : 0);
|
|
811
|
+
const mode = `${triad(1, 2, 3)}${triad(4, 5, 6)}${triad(7, 8, 9)}`;
|
|
812
|
+
return special > 0 ? `${special}${mode}` : mode;
|
|
813
|
+
};
|
|
814
|
+
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/;
|
|
815
|
+
var LS_MODE = /^[-dlbcps][rwxSsTt-]{9}[.+@]?$/;
|
|
816
|
+
var parseListingLine = (line) => {
|
|
817
|
+
const date = LS_DATE.exec(line);
|
|
818
|
+
if (date === null) {
|
|
819
|
+
return void 0;
|
|
820
|
+
}
|
|
821
|
+
const head = line.slice(0, date.index).split(/\s+/).filter((token) => token !== "");
|
|
822
|
+
if (head.length < 4 || !LS_MODE.test(head[0])) {
|
|
823
|
+
return void 0;
|
|
824
|
+
}
|
|
825
|
+
const size = head.reduceRight((found, token) => found === void 0 && /^\d+$/.test(token) ? Number(token) : found, void 0);
|
|
826
|
+
return { mode: permsToOctal(head[0]), directory: head[0].startsWith("d"), size, name: line.slice(date.index + date[0].length) };
|
|
827
|
+
};
|
|
828
|
+
var compactListing = (lines) => {
|
|
829
|
+
let parsed = 0;
|
|
830
|
+
const out = lines.flatMap((line) => {
|
|
831
|
+
if (/^total \d+$/.test(line)) {
|
|
832
|
+
return [];
|
|
833
|
+
}
|
|
834
|
+
const entry = parseListingLine(line);
|
|
835
|
+
if (entry === void 0) {
|
|
836
|
+
return [line];
|
|
837
|
+
}
|
|
838
|
+
parsed++;
|
|
839
|
+
if (entry.name === "." || entry.name === "..") {
|
|
840
|
+
return [];
|
|
841
|
+
}
|
|
842
|
+
if (entry.directory) {
|
|
843
|
+
return [`${entry.mode} ${entry.name}/`];
|
|
844
|
+
}
|
|
845
|
+
return [`${entry.mode} ${entry.name}${entry.size === void 0 ? "" : ` ${humanSize(entry.size)}`}`];
|
|
846
|
+
});
|
|
847
|
+
return parsed === 0 ? lines : out;
|
|
848
|
+
};
|
|
849
|
+
var PATH_RUN_MIN = 10;
|
|
850
|
+
var PATH_RUN_DIRS = 60;
|
|
851
|
+
var PATH_RUN_NAMES = 40;
|
|
852
|
+
var isPathLine = (line) => line !== "" && !/\s/.test(line) && !/:\d/.test(line) && line.length < 300;
|
|
853
|
+
var sharedRoot = (directories) => {
|
|
854
|
+
if (directories.length < 2) {
|
|
855
|
+
return "";
|
|
856
|
+
}
|
|
857
|
+
let prefix = directories[0];
|
|
858
|
+
for (const directory of directories) {
|
|
859
|
+
while (prefix !== "" && !directory.startsWith(prefix)) {
|
|
860
|
+
const shorter = prefix.slice(0, prefix.lastIndexOf("/", prefix.length - 2) + 1);
|
|
861
|
+
prefix = shorter === prefix ? "" : shorter;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return prefix;
|
|
865
|
+
};
|
|
866
|
+
var foldPaths = (run) => {
|
|
867
|
+
if (run.filter((line) => line.includes("/")).length * 5 < run.length * 3) {
|
|
868
|
+
return run;
|
|
869
|
+
}
|
|
870
|
+
const byDirectory = /* @__PURE__ */ new Map();
|
|
871
|
+
for (const path of run) {
|
|
872
|
+
const cut = path.lastIndexOf("/");
|
|
873
|
+
const directory = cut === -1 ? "./" : path.slice(0, cut + 1);
|
|
874
|
+
const names = byDirectory.get(directory);
|
|
875
|
+
if (names === void 0) {
|
|
876
|
+
byDirectory.set(directory, [path.slice(cut + 1)]);
|
|
877
|
+
} else {
|
|
878
|
+
names.push(path.slice(cut + 1));
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const root = sharedRoot([...byDirectory.keys()]);
|
|
882
|
+
const trim = root.length > 8 ? root.length : 0;
|
|
883
|
+
const folded = [`${run.length} paths in ${byDirectory.size} directories${trim === 0 ? "" : ` under ${root}`}:`];
|
|
884
|
+
let shown = 0;
|
|
885
|
+
for (const [directory, names] of [...byDirectory].slice(0, PATH_RUN_DIRS)) {
|
|
886
|
+
const kept = names.slice(0, PATH_RUN_NAMES);
|
|
887
|
+
shown += kept.length;
|
|
888
|
+
folded.push(`${directory.slice(trim) === "" ? "./" : directory.slice(trim)} ${kept.join(" ")}`);
|
|
889
|
+
}
|
|
890
|
+
if (shown < run.length) {
|
|
891
|
+
folded.push(`\u2026 ${run.length - shown} more paths elided`);
|
|
892
|
+
}
|
|
893
|
+
return bodyBytes(folded) < bodyBytes(run) ? folded : run;
|
|
894
|
+
};
|
|
895
|
+
var foldPathRuns = (lines) => {
|
|
896
|
+
const out = [];
|
|
897
|
+
for (let i = 0; i < lines.length; ) {
|
|
898
|
+
let j = i;
|
|
899
|
+
while (j < lines.length && isPathLine(lines[j])) {
|
|
900
|
+
j++;
|
|
901
|
+
}
|
|
902
|
+
if (j === i) {
|
|
903
|
+
out.push(lines[i]);
|
|
904
|
+
i++;
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
out.push(...j - i >= PATH_RUN_MIN ? foldPaths(lines.slice(i, j)) : lines.slice(i, j));
|
|
908
|
+
i = j;
|
|
909
|
+
}
|
|
910
|
+
return out;
|
|
911
|
+
};
|
|
912
|
+
var HIT_LINE = /^([^\s:][^:]{0,240}):(\d+):(?:(\d+):)?(.*)$/;
|
|
913
|
+
var HIT_RUN_MIN = 6;
|
|
914
|
+
var isHitKey = (key) => key.includes("/") || key.includes(".");
|
|
915
|
+
var parseHit = (line) => {
|
|
916
|
+
const match = HIT_LINE.exec(line);
|
|
917
|
+
return match !== null && isHitKey(match[1]) ? match : void 0;
|
|
918
|
+
};
|
|
919
|
+
var foldHitRun = (run) => {
|
|
920
|
+
const folded = [];
|
|
921
|
+
for (let start = 0; start < run.length; ) {
|
|
922
|
+
const file = parseHit(run[start])[1];
|
|
923
|
+
folded.push(run[start]);
|
|
924
|
+
let next = start + 1;
|
|
925
|
+
while (next < run.length && parseHit(run[next])[1] === file) {
|
|
926
|
+
const hit = parseHit(run[next]);
|
|
927
|
+
folded.push(` ${hit[2]}${hit[3] === void 0 ? "" : `:${hit[3]}`}:${hit[4]}`);
|
|
928
|
+
next++;
|
|
929
|
+
}
|
|
930
|
+
start = next;
|
|
931
|
+
}
|
|
932
|
+
return bodyBytes(folded) < bodyBytes(run) ? folded : run;
|
|
933
|
+
};
|
|
934
|
+
var foldHitRuns = (lines) => {
|
|
935
|
+
const out = [];
|
|
936
|
+
for (let i = 0; i < lines.length; ) {
|
|
937
|
+
let end = i;
|
|
938
|
+
while (end < lines.length && parseHit(lines[end]) !== void 0) {
|
|
939
|
+
end++;
|
|
940
|
+
}
|
|
941
|
+
const stop = Math.max(end, i + 1);
|
|
942
|
+
out.push(...end - i >= HIT_RUN_MIN ? foldHitRun(lines.slice(i, end)) : lines.slice(i, stop));
|
|
943
|
+
i = stop;
|
|
944
|
+
}
|
|
945
|
+
return out;
|
|
946
|
+
};
|
|
947
|
+
var GENERATED_PATH = /(?:^|\/)(?:[^/]*\.lock(?:\.json)?|[^/]*-lock\.json|pnpm-lock\.yaml|go\.sum|[^/]*\.min\.(?:js|css|mjs)|[^/]*\.map)$/;
|
|
948
|
+
var DIFF_HEADER = /^diff --git a\/(\S+) b\/(\S+)$/;
|
|
949
|
+
var isAdded = (line) => line.startsWith("+") && !line.startsWith("+++");
|
|
950
|
+
var isRemoved = (line) => line.startsWith("-") && !line.startsWith("---");
|
|
951
|
+
var foldGeneratedDiffs = (lines) => {
|
|
952
|
+
const out = [];
|
|
953
|
+
let folding = false;
|
|
954
|
+
let added = 0;
|
|
955
|
+
let removed = 0;
|
|
956
|
+
let dropped = 0;
|
|
957
|
+
const flush = () => {
|
|
958
|
+
if (folding && dropped > 0) {
|
|
959
|
+
out.push(`\u2026 ${dropped} lines of generated-file diff elided (+${added} \u2212${removed}) \u2026`);
|
|
960
|
+
}
|
|
961
|
+
folding = false;
|
|
962
|
+
added = 0;
|
|
963
|
+
removed = 0;
|
|
964
|
+
dropped = 0;
|
|
965
|
+
};
|
|
966
|
+
for (const line of lines) {
|
|
967
|
+
const header = DIFF_HEADER.exec(line);
|
|
968
|
+
if (header !== null) {
|
|
969
|
+
flush();
|
|
970
|
+
folding = GENERATED_PATH.test(header[2]);
|
|
971
|
+
out.push(line);
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
if (!folding) {
|
|
975
|
+
out.push(line);
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
dropped++;
|
|
979
|
+
added += isAdded(line) ? 1 : 0;
|
|
980
|
+
removed += isRemoved(line) ? 1 : 0;
|
|
981
|
+
}
|
|
982
|
+
flush();
|
|
983
|
+
return bodyBytes(out) < bodyBytes(lines) ? out : lines;
|
|
984
|
+
};
|
|
985
|
+
var COMMAND_CLEANERS = [
|
|
986
|
+
strip("pnpm", invocation(String.raw`pnpm`), [
|
|
987
|
+
/^\s*Progress: /,
|
|
988
|
+
/^Packages: [+-]/,
|
|
989
|
+
/^Downloading /,
|
|
990
|
+
/^\s*[.+]+\s*$/,
|
|
991
|
+
/^Virtual store is at/,
|
|
992
|
+
/^Lockfile is up to date/
|
|
993
|
+
]),
|
|
994
|
+
// `apt-get` before `apt`: ordered alternation, or the guard rejects the longer spelling at its hyphen.
|
|
995
|
+
strip("apt", invocation(String.raw`apt-get|apt`), [
|
|
996
|
+
/^(?:Get:|Hit:|Ign:|Fetched |Selecting |Preparing to unpack|Unpacking |Setting up |Processing triggers)/
|
|
997
|
+
]),
|
|
998
|
+
// Test runners: on a passing run (exit 0) per-test PASS lines are noise, dropped, keeping the summary. Failures skip
|
|
999
|
+
// all command cleaners, so failing tests survive verbatim.
|
|
1000
|
+
strip("test", invocation(String.raw`bun\s+test|suites|vitest|jest|pytest|rspec|mocha|phpunit|go\s+test|cargo\s+test`), [
|
|
1001
|
+
/^\s*[✓√]\s/,
|
|
1002
|
+
// per-test pass (vitest/jest/mocha)
|
|
1003
|
+
/^\(pass\)\s/,
|
|
1004
|
+
// bun test per-test pass
|
|
1005
|
+
/^--- PASS:/,
|
|
1006
|
+
// go test per-test
|
|
1007
|
+
/^\s*test .+\.\.\. ok$/,
|
|
1008
|
+
// cargo test per-test
|
|
1009
|
+
/^PASS\s+\S/,
|
|
1010
|
+
// jest per-file PASS header
|
|
1011
|
+
/^\s*[.·]+\s*$/
|
|
1012
|
+
// pytest/mocha dot progress
|
|
1013
|
+
]),
|
|
1014
|
+
// `diff` reads the whole output's structure, the three after it read runs of lines; folding the generated hunks away
|
|
1015
|
+
// first keeps those three from ever looking inside one.
|
|
1016
|
+
{ id: "diff", apply: foldGeneratedDiffs },
|
|
1017
|
+
{ id: "ls", apply: compactListing },
|
|
1018
|
+
{ id: "files", apply: foldPathRuns },
|
|
1019
|
+
{ id: "hits", apply: foldHitRuns }
|
|
1020
|
+
];
|
|
1021
|
+
var CLEANERS = [...COMMAND_CLEANERS.map((cleaner) => cleaner.id), "dedup", "wide", "cap", "redact", "cache"];
|
|
1022
|
+
var dedupeRuns = (lines) => {
|
|
1023
|
+
const out = [];
|
|
1024
|
+
for (let i = 0; i < lines.length; ) {
|
|
1025
|
+
let j = i + 1;
|
|
1026
|
+
while (j < lines.length && lines[j] === lines[i]) {
|
|
1027
|
+
j++;
|
|
1028
|
+
}
|
|
1029
|
+
const run = j - i;
|
|
1030
|
+
out.push(lines[i]);
|
|
1031
|
+
if (run >= 3) {
|
|
1032
|
+
out.push(` \u2026 (${run - 1} more identical lines)`);
|
|
1033
|
+
} else {
|
|
1034
|
+
for (let k = 1; k < run; k++) {
|
|
1035
|
+
out.push(lines[i]);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
i = j;
|
|
1039
|
+
}
|
|
1040
|
+
return out;
|
|
1041
|
+
};
|
|
1042
|
+
var WIDE_RUN = 400;
|
|
1043
|
+
var WIDE_HEAD = 240;
|
|
1044
|
+
var WIDE_TAIL = 80;
|
|
1045
|
+
var elideRun = (run) => `${run.slice(0, WIDE_HEAD)}\u2026 ${run.length - WIDE_HEAD - WIDE_TAIL} chars elided \u2026${run.slice(-WIDE_TAIL)}`;
|
|
1046
|
+
var WIDE_PATTERN = new RegExp(String.raw`\S{${WIDE_RUN + 1},}`, "g");
|
|
1047
|
+
var elideWideRuns = (lines) => lines.map((line) => line.length > WIDE_RUN ? line.replaceAll(WIDE_PATTERN, elideRun) : line);
|
|
1048
|
+
var SECRET_NAME = String.raw`[A-Za-z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|API[_-]?KEY|ACCESS[_-]?KEY|PRIVATE[_-]?KEY)[A-Za-z0-9_]*`;
|
|
1049
|
+
var ASSIGN = String.raw`["']?\s*[:=](?![=>])\s*`;
|
|
1050
|
+
var QUOTED_VALUE = String.raw`(["'\x60])([^"'\x60\n]{6,})\2`;
|
|
1051
|
+
var BARE_VALUE = String.raw`[\w+/=~-]{6,}(?![\w+/=~(-])`;
|
|
1052
|
+
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_)/;
|
|
1053
|
+
var NUMERIC_VALUE = /^[\d_,.]+$/;
|
|
1054
|
+
var STRUCTURAL_VALUE = /[\s/\\${}]/;
|
|
1055
|
+
var ENV_NAME_VALUE = /^[A-Z0-9_]+$/;
|
|
1056
|
+
var ENTROPIC_VALUE = /^(?=[^\n]*\d)(?=[^\n]*[A-Za-z])/;
|
|
1057
|
+
var HANDWRITTEN_MAX = 12;
|
|
1058
|
+
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);
|
|
1059
|
+
var SECRET_PATTERNS = [
|
|
1060
|
+
[
|
|
1061
|
+
new RegExp(`\\b(${SECRET_NAME}${ASSIGN})(?:${QUOTED_VALUE}|(${BARE_VALUE}))`, "gi"),
|
|
1062
|
+
// Quoted and bare arrive in different groups because only the quoted one has a quote to put back.
|
|
1063
|
+
(match, assignment, quote, quoted, bare) => looksLikeCredential(quoted ?? bare) ? `${assignment}${quote ?? ""}***${quote ?? ""}` : match
|
|
1064
|
+
],
|
|
1065
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, "***"],
|
|
1066
|
+
// Same rule for a bearer value: prose about tokens carries no digit, every real bearer token does.
|
|
1067
|
+
[/\b(Bearer\s+)(?=[\w.-]*\d)[\w.-]{8,}/gi, "$1***"],
|
|
1068
|
+
[/\b(https?:\/\/[^:@\s/]+:)[^@\s]+@/gi, "$1***@"]
|
|
1069
|
+
];
|
|
1070
|
+
var SECRET_VALUE_MIN = 12;
|
|
1071
|
+
var secretValueCache = /* @__PURE__ */ new Map();
|
|
1072
|
+
var readIfChanged = (path, name) => {
|
|
1073
|
+
try {
|
|
1074
|
+
const descriptor = openSync(path, "r");
|
|
1075
|
+
try {
|
|
1076
|
+
const { mtimeMs, size } = fstatSync(descriptor);
|
|
1077
|
+
const stamp = `${mtimeMs}:${size}`;
|
|
1078
|
+
const cached = secretValueCache.get(path);
|
|
1079
|
+
if (cached?.stamp === stamp) {
|
|
1080
|
+
return cached.values;
|
|
1081
|
+
}
|
|
1082
|
+
const values = harvest(readFileSync3(descriptor, "utf8"), path, name);
|
|
1083
|
+
secretValueCache.set(path, { stamp, values });
|
|
1084
|
+
return values;
|
|
1085
|
+
} finally {
|
|
1086
|
+
closeSync(descriptor);
|
|
1087
|
+
}
|
|
1088
|
+
} catch {
|
|
1089
|
+
return [];
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
var surfaceForms = (value) => {
|
|
1093
|
+
const forms = [value];
|
|
1094
|
+
const jsonEscaped = JSON.stringify(value).slice(1, -1);
|
|
1095
|
+
if (jsonEscaped !== value) {
|
|
1096
|
+
forms.push(jsonEscaped);
|
|
1097
|
+
}
|
|
1098
|
+
try {
|
|
1099
|
+
const encoded = encodeURIComponent(value);
|
|
1100
|
+
if (encoded !== value) {
|
|
1101
|
+
forms.push(encoded);
|
|
1102
|
+
}
|
|
1103
|
+
} catch {
|
|
1104
|
+
}
|
|
1105
|
+
return forms;
|
|
1106
|
+
};
|
|
1107
|
+
var harvest = (text, path, name) => {
|
|
1108
|
+
const named = [];
|
|
1109
|
+
if (path.endsWith("capability-secrets.json")) {
|
|
1110
|
+
for (const [id, entry] of Object.entries(JSON.parse(text))) {
|
|
1111
|
+
if (entry === null || typeof entry !== "object") {
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
for (const [field, value] of Object.entries(entry)) {
|
|
1115
|
+
if (typeof value === "string") {
|
|
1116
|
+
named.push([name(id, field), value]);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
} else if (path.endsWith(".json")) {
|
|
1121
|
+
for (const [key, value] of Object.entries(JSON.parse(text))) {
|
|
1122
|
+
if (typeof value === "string") {
|
|
1123
|
+
named.push([name(key), value]);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
} else {
|
|
1127
|
+
for (const line of text.split("\n")) {
|
|
1128
|
+
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
|
|
1129
|
+
if (match !== null) {
|
|
1130
|
+
named.push([name(match[1]), (match[2] ?? "").trim().replace(/^(["'])([\s\S]*)\1$/, "$2")]);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
1135
|
+
const add = (target, replacement) => {
|
|
1136
|
+
const trimmed = target.trim();
|
|
1137
|
+
if (trimmed.length >= SECRET_VALUE_MIN && !byTarget.has(trimmed)) {
|
|
1138
|
+
byTarget.set(trimmed, { target: trimmed, replacement });
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
for (const [reference, value] of named) {
|
|
1142
|
+
for (const form of surfaceForms(value.trim())) {
|
|
1143
|
+
add(form, `{{secret:${reference}}}`);
|
|
1144
|
+
}
|
|
1145
|
+
if (value.includes("\n")) {
|
|
1146
|
+
for (const line of value.split("\n")) {
|
|
1147
|
+
add(line, "***");
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return [...byTarget.values()].toSorted((a, b) => b.target.length - a.target.length);
|
|
1152
|
+
};
|
|
1153
|
+
var secretValues = (env = process.env) => {
|
|
1154
|
+
const authRoot = env.AGENT_AUTH_DIR !== void 0 && env.AGENT_AUTH_DIR !== "" ? env.AGENT_AUTH_DIR : join3(env.WORKSPACE_ROOT ?? "/work", ".intentic/secrets/auth");
|
|
1155
|
+
const repo = join3(env.WORKSPACE_ROOT ?? "/work", "desired-state");
|
|
1156
|
+
return [
|
|
1157
|
+
...readIfChanged(join3(authRoot, "capability-secrets.json"), (id, field) => `${id}/${field}`),
|
|
1158
|
+
...readIfChanged(join3(repo, ".env"), (key) => key),
|
|
1159
|
+
...readIfChanged(join3(repo, ".secrets.json"), (key) => key)
|
|
1160
|
+
].toSorted((a, b) => b.target.length - a.target.length);
|
|
1161
|
+
};
|
|
1162
|
+
var redactText = (text, values = []) => text.split("\n").map((line) => redactLine(line, values)).join("\n");
|
|
1163
|
+
var redactLine = (line, values = []) => {
|
|
1164
|
+
let masked = line;
|
|
1165
|
+
for (const { target, replacement } of values) {
|
|
1166
|
+
if (masked.includes(target)) {
|
|
1167
|
+
masked = masked.split(target).join(replacement);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return SECRET_PATTERNS.reduce((acc, [pattern, replacement]) => acc.replace(pattern, replacement), masked);
|
|
1171
|
+
};
|
|
1172
|
+
var parseCleaners = (spec) => {
|
|
1173
|
+
if (spec === void 0 || spec.trim() === "") {
|
|
1174
|
+
return new Set(CLEANERS);
|
|
1175
|
+
}
|
|
1176
|
+
const tokens3 = spec.split(",").map((token) => token.trim()).filter((token) => token !== "" && CLEANERS.includes(token.replace(/^-/, "")));
|
|
1177
|
+
if (tokens3.length === 0) {
|
|
1178
|
+
return new Set(CLEANERS);
|
|
1179
|
+
}
|
|
1180
|
+
const allowList = tokens3.some((token) => !token.startsWith("-"));
|
|
1181
|
+
if (allowList) {
|
|
1182
|
+
return new Set(tokens3.filter((token) => !token.startsWith("-")));
|
|
1183
|
+
}
|
|
1184
|
+
const disabled = new Set(tokens3.map((token) => token.slice(1)));
|
|
1185
|
+
return new Set(CLEANERS.filter((cleaner) => !disabled.has(cleaner)));
|
|
1186
|
+
};
|
|
1187
|
+
var HEAD = 30;
|
|
1188
|
+
var TAIL = 50;
|
|
1189
|
+
var MAX = 100;
|
|
1190
|
+
var FAIL_TAIL = 500;
|
|
1191
|
+
var LOG_MAX_BYTES = 16e3;
|
|
1192
|
+
var READ_MAX_BYTES = 96e3;
|
|
1193
|
+
var BYTE_HEAD_SHARE = 0.375;
|
|
1194
|
+
var READ_MAX = 2e3;
|
|
1195
|
+
var GIT_OPTIONS = String.raw`(?:(?:-[Cc]|--(?:git-dir|work-tree|namespace|exec-path|super-prefix))\s+\S+\s+|-\S+\s+)*`;
|
|
1196
|
+
var READ_COMMAND = new RegExp(
|
|
1197
|
+
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`
|
|
1198
|
+
);
|
|
1199
|
+
var takeWithinBudget = (source, budget) => {
|
|
1200
|
+
const kept = [];
|
|
1201
|
+
let spent = 0;
|
|
1202
|
+
for (const line of source) {
|
|
1203
|
+
spent += line.length + 1;
|
|
1204
|
+
if (spent > budget) {
|
|
1205
|
+
break;
|
|
1206
|
+
}
|
|
1207
|
+
kept.push(line);
|
|
1208
|
+
}
|
|
1209
|
+
return kept;
|
|
1210
|
+
};
|
|
1211
|
+
var byteBudget = (command) => READ_COMMAND.test(command) ? READ_MAX_BYTES : LOG_MAX_BYTES;
|
|
1212
|
+
var capOutput = (lines, command) => {
|
|
1213
|
+
const isRead = READ_COMMAND.test(command);
|
|
1214
|
+
const maxLines = isRead ? READ_MAX : MAX;
|
|
1215
|
+
const maxBytes = byteBudget(command);
|
|
1216
|
+
if (lines.length > maxLines) {
|
|
1217
|
+
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)];
|
|
1218
|
+
}
|
|
1219
|
+
if (bodyBytes(lines) <= maxBytes) {
|
|
1220
|
+
return lines;
|
|
1221
|
+
}
|
|
1222
|
+
if (isRead) {
|
|
1223
|
+
const head2 = takeWithinBudget(lines, maxBytes);
|
|
1224
|
+
return [...head2, `\u2026 ${lines.length - head2.length} more lines elided (${bodyBytes(lines)} bytes): narrow the range or use the Read tool \u2026`];
|
|
1225
|
+
}
|
|
1226
|
+
const head = takeWithinBudget(lines, Math.round(maxBytes * BYTE_HEAD_SHARE));
|
|
1227
|
+
const tail = takeWithinBudget(lines.toReversed(), maxBytes - bodyBytes(head)).toReversed();
|
|
1228
|
+
if (head.length === 0 && tail.length === 0) {
|
|
1229
|
+
return [`${lines[0].slice(0, maxBytes)}\u2026 line truncated at ${maxBytes} bytes \u2026`];
|
|
1230
|
+
}
|
|
1231
|
+
return [...head, `\u2026 ${lines.length - head.length - tail.length} lines elided (${bodyBytes(lines)} bytes) \u2026`, ...tail];
|
|
1232
|
+
};
|
|
1233
|
+
var cleanSucceeded = (lines, command, enabled, ran) => {
|
|
1234
|
+
let out = lines;
|
|
1235
|
+
for (const cleaner of COMMAND_CLEANERS) {
|
|
1236
|
+
if (enabled.has(cleaner.id) && (cleaner.match === void 0 || cleaner.match.test(command))) {
|
|
1237
|
+
out = ran(cleaner.id, cleaner.apply(out));
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (enabled.has("dedup")) {
|
|
1241
|
+
out = ran("dedup", dedupeRuns(out));
|
|
1242
|
+
}
|
|
1243
|
+
if (enabled.has("wide") && bodyBytes(out) > byteBudget(command)) {
|
|
1244
|
+
out = ran("wide", elideWideRuns(out));
|
|
1245
|
+
}
|
|
1246
|
+
if (enabled.has("cap")) {
|
|
1247
|
+
const capped = capOutput(out, command);
|
|
1248
|
+
if (capped !== out) {
|
|
1249
|
+
out = ran("cap", capped);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return out;
|
|
1253
|
+
};
|
|
1254
|
+
var cleanFailed = (lines, enabled, ran) => {
|
|
1255
|
+
const out = enabled.has("dedup") ? ran("dedup", dedupeRuns(lines)) : lines;
|
|
1256
|
+
return out.length > FAIL_TAIL ? (
|
|
1257
|
+
// Own id, never `cap`: this fires regardless of the spec, so `cap`'s toggle shouldn't get credit for it.
|
|
1258
|
+
ran("failtail", [`\u2026 ${out.length - FAIL_TAIL} earlier lines elided \u2026`, ...out.slice(-FAIL_TAIL)])
|
|
1259
|
+
) : out;
|
|
1260
|
+
};
|
|
1261
|
+
var cleanLines = (lines, { command, exitCode, enabled, values = [] }) => {
|
|
1262
|
+
const stages = [];
|
|
1263
|
+
let bytes = bodyBytes(lines);
|
|
1264
|
+
const ran = (id, next) => {
|
|
1265
|
+
const after = bodyBytes(next);
|
|
1266
|
+
stages.push({ id, saved: bytes - after });
|
|
1267
|
+
bytes = after;
|
|
1268
|
+
return next;
|
|
1269
|
+
};
|
|
1270
|
+
let out = exitCode === "0" ? cleanSucceeded(lines, command, enabled, ran) : cleanFailed(lines, enabled, ran);
|
|
1271
|
+
if (enabled.has("redact")) {
|
|
1272
|
+
out = ran(
|
|
1273
|
+
"redact",
|
|
1274
|
+
out.map((line) => redactLine(line, values))
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
return { lines: out, stages };
|
|
1278
|
+
};
|
|
1279
|
+
var matchedCleaners = (command, enabled) => COMMAND_CLEANERS.filter((cleaner) => cleaner.match !== void 0 && enabled.has(cleaner.id) && cleaner.match.test(command)).map(
|
|
1280
|
+
(cleaner) => cleaner.id
|
|
1281
|
+
);
|
|
1282
|
+
var CACHE_MARKER = "(output identical to ";
|
|
1283
|
+
var CACHE_MIN_BYTES = 512;
|
|
1284
|
+
var CACHE_COMMAND_MAX = 120;
|
|
1285
|
+
var hashText = (text) => createHash("sha1").update(text).digest("hex");
|
|
1286
|
+
var sessionKeyFromLog = (logPath) => {
|
|
1287
|
+
if (logPath === void 0 || logPath === "") {
|
|
1288
|
+
return void 0;
|
|
1289
|
+
}
|
|
1290
|
+
const base = logPath.split("/").pop() ?? "";
|
|
1291
|
+
const match = base.match(/^(.*)-[^-]+\.log$/);
|
|
1292
|
+
return match !== null && match[1] !== "" ? match[1] : void 0;
|
|
1293
|
+
};
|
|
1294
|
+
var openCacheStore = (logsDir, sessionKey) => {
|
|
1295
|
+
const file = join3(logsDir, "output-cache", `${sessionKey}.json`);
|
|
1296
|
+
let map;
|
|
1297
|
+
try {
|
|
1298
|
+
map = new Map(Object.entries(JSON.parse(readFileSync3(file, "utf8"))));
|
|
1299
|
+
} catch {
|
|
1300
|
+
map = /* @__PURE__ */ new Map();
|
|
1301
|
+
}
|
|
1302
|
+
return {
|
|
1303
|
+
lookup: (key) => map.get(key),
|
|
1304
|
+
record: (key, value) => {
|
|
1305
|
+
map.set(key, value);
|
|
1306
|
+
try {
|
|
1307
|
+
mkdirSync2(dirname2(file), { recursive: true });
|
|
1308
|
+
writeFileSync2(file, JSON.stringify(Object.fromEntries(map)));
|
|
1309
|
+
} catch {
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
};
|
|
1314
|
+
var collapseCached = (body, command, store, retain = () => void 0) => {
|
|
1315
|
+
if (body.length < CACHE_MIN_BYTES) {
|
|
1316
|
+
return { body, cached: false };
|
|
1317
|
+
}
|
|
1318
|
+
const commandHash = `c:${hashText(command)}`;
|
|
1319
|
+
const bodyKey = `b:${hashText(body)}`;
|
|
1320
|
+
const bodyHash = hashText(body);
|
|
1321
|
+
const handle = () => {
|
|
1322
|
+
const path = retain();
|
|
1323
|
+
return path === void 0 ? "" : ` \xB7 retrieve-output ${path}`;
|
|
1324
|
+
};
|
|
1325
|
+
if (store.lookup(commandHash) === bodyHash) {
|
|
1326
|
+
return { body: `${CACHE_MARKER}a previous run this session${handle()})`, cached: true };
|
|
1327
|
+
}
|
|
1328
|
+
const earlier = store.lookup(bodyKey);
|
|
1329
|
+
if (earlier !== void 0 && earlier !== command) {
|
|
1330
|
+
const named = earlier.length > CACHE_COMMAND_MAX ? `${earlier.slice(0, CACHE_COMMAND_MAX)}\u2026` : earlier;
|
|
1331
|
+
return { body: `${CACHE_MARKER}the output of \`${named}\` earlier this session${handle()})`, cached: true };
|
|
1332
|
+
}
|
|
1333
|
+
store.record(commandHash, bodyHash);
|
|
1334
|
+
if (earlier === void 0) {
|
|
1335
|
+
store.record(bodyKey, command);
|
|
1336
|
+
}
|
|
1337
|
+
return { body, cached: false };
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
// ../output-cleaners/src/agent-output-filter.mjs
|
|
1341
|
+
import { appendFileSync, mkdirSync as mkdirSync3, readdirSync as readdirSync2, rmSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1342
|
+
import { basename as basename2, join as join4 } from "node:path";
|
|
1343
|
+
var RETRIEVAL_MIN_DROPPED = 20;
|
|
1344
|
+
var SIGPIPE_STATUS = "141";
|
|
1345
|
+
var maskedFailure = (exitCode, pipeline) => {
|
|
1346
|
+
const statuses = pipeline.trim() === "" ? [] : pipeline.trim().split(/\s+/);
|
|
1347
|
+
if (exitCode !== "0" || statuses.length < 2 || !statuses.every((status) => /^\d+$/.test(status))) {
|
|
1348
|
+
return void 0;
|
|
1349
|
+
}
|
|
1350
|
+
return statuses.slice(0, -1).some((status) => status !== "0" && status !== SIGPIPE_STATUS) ? statuses.join(" ") : void 0;
|
|
1351
|
+
};
|
|
1352
|
+
var RG_REPLACE_CLUSTER = /(?<![\w.\-/])rg\s(?:[^;&|\n]*?\s)?(-[a-zA-Z]*r[a-zA-Z]*)(?=[\s;&|]|$)/;
|
|
1353
|
+
var commandNotes = (command, raw) => {
|
|
1354
|
+
const cluster = RG_REPLACE_CLUSTER.exec(command)?.[1];
|
|
1355
|
+
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)`] : [];
|
|
1356
|
+
};
|
|
1357
|
+
var filterOutput = (raw, { command = "", exitCode = "0", durationS = "0", retain, enabled = new Set(CLEANERS), cacheStore, values = [], pipeline = "" }) => {
|
|
1358
|
+
const input = raw.replaceAll(ANSI, "").split("\n").map(collapseCr);
|
|
1359
|
+
if (input.at(-1) === "") {
|
|
1360
|
+
input.pop();
|
|
1361
|
+
}
|
|
1362
|
+
const keep = () => retain?.(input.join("\n"));
|
|
1363
|
+
const rawCount = input.length;
|
|
1364
|
+
const stages = [{ id: "ansi", saved: raw.length - bodyBytes(input) }];
|
|
1365
|
+
const cleaned = cleanLines(input, { command, exitCode, enabled, values });
|
|
1366
|
+
const lines = cleaned.lines;
|
|
1367
|
+
stages.push(...cleaned.stages);
|
|
1368
|
+
let body = lines.join("\n");
|
|
1369
|
+
if (exitCode === "0" && body.trim() === "" && raw.trim() !== "") {
|
|
1370
|
+
body = "(no notable output)";
|
|
1371
|
+
}
|
|
1372
|
+
const masked = maskedFailure(exitCode, pipeline);
|
|
1373
|
+
const exit = masked === void 0 ? `exit ${exitCode}` : `exit ${exitCode} (pipeline: ${masked} \u2014 an earlier stage failed)`;
|
|
1374
|
+
const emitted = (text, id, footed) => {
|
|
1375
|
+
stages.push({ id, saved: bodyBytes(lines) - text.length });
|
|
1376
|
+
let out = text;
|
|
1377
|
+
if (text.length > raw.length) {
|
|
1378
|
+
stages.push({ id: "guard", saved: text.length - raw.length });
|
|
1379
|
+
out = raw;
|
|
1380
|
+
}
|
|
1381
|
+
const extra = [...masked === void 0 || footed && out === text ? [] : [`--- [${exit}, ${durationS}s]`], ...commandNotes(command, raw)];
|
|
1382
|
+
if (extra.length === 0) {
|
|
1383
|
+
return { out, stages };
|
|
1384
|
+
}
|
|
1385
|
+
const noted = `${out === "" || out.endsWith("\n") ? out : `${out}
|
|
1386
|
+
`}${extra.join("\n")}
|
|
1387
|
+
`;
|
|
1388
|
+
stages.push({ id: "notes", saved: out.length - noted.length });
|
|
1389
|
+
return { out: noted, stages };
|
|
1390
|
+
};
|
|
1391
|
+
if (exitCode === "0" && enabled.has("cache") && cacheStore !== void 0 && body !== "" && body !== "(no notable output)") {
|
|
1392
|
+
const collapsed = collapseCached(body, command, cacheStore, keep);
|
|
1393
|
+
if (collapsed.cached) {
|
|
1394
|
+
return emitted(`${collapsed.body}
|
|
1395
|
+
`, "cache", false);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
const kept = body === "(no notable output)" ? 0 : lines.length;
|
|
1399
|
+
const dropped = Math.max(0, rawCount - kept);
|
|
1400
|
+
const cut = stages.find((stage) => stage.id === "wide")?.saved ?? 0;
|
|
1401
|
+
if (dropped === 0 && cut <= 0) {
|
|
1402
|
+
return emitted(body === "" ? body : `${body}
|
|
1403
|
+
`, "footer", false);
|
|
1404
|
+
}
|
|
1405
|
+
const retained = dropped >= RETRIEVAL_MIN_DROPPED || cut > 0 ? keep() : void 0;
|
|
1406
|
+
const handle = retained === void 0 ? "" : ` \xB7 full: retrieve-output ${retained} [pattern]`;
|
|
1407
|
+
const summary = dropped > 0 ? `${rawCount} lines filtered to ${kept}` : "long runs cut inside lines";
|
|
1408
|
+
const withFooter = `${body}
|
|
1409
|
+
--- [${exit}, ${durationS}s] ${summary}${handle}
|
|
1410
|
+
`;
|
|
1411
|
+
return withFooter.length <= raw.length ? emitted(withFooter, "footer", true) : emitted(`${body}
|
|
1412
|
+
`, "footer", false);
|
|
1413
|
+
};
|
|
1414
|
+
var RETAIN_FILES = 30;
|
|
1415
|
+
var RETAIN_MAX_CHARS = 2e6;
|
|
1416
|
+
var retainIn = (dir, name, values) => (text) => {
|
|
1417
|
+
try {
|
|
1418
|
+
mkdirSync3(dir, { recursive: true });
|
|
1419
|
+
const file = join4(dir, name);
|
|
1420
|
+
writeFileSync3(file, redactText(text.length > RETAIN_MAX_CHARS ? text.slice(-RETAIN_MAX_CHARS) : text, values));
|
|
1421
|
+
const aged = readdirSync2(dir).flatMap((entry) => {
|
|
1422
|
+
try {
|
|
1423
|
+
return [{ entry, at: statSync(join4(dir, entry)).mtimeMs }];
|
|
1424
|
+
} catch {
|
|
1425
|
+
return [];
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
for (const { entry } of aged.toSorted((a, b) => b.at - a.at).slice(RETAIN_FILES)) {
|
|
1429
|
+
rmSync(join4(dir, entry), { force: true });
|
|
1430
|
+
}
|
|
1431
|
+
return file;
|
|
1432
|
+
} catch {
|
|
1433
|
+
return void 0;
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
var filterRun = (raw, {
|
|
1437
|
+
command = "",
|
|
1438
|
+
exitCode = "0",
|
|
1439
|
+
durationS = "0",
|
|
1440
|
+
pipeline = "",
|
|
1441
|
+
enabled = new Set(CLEANERS),
|
|
1442
|
+
holdout = 0,
|
|
1443
|
+
logsDir,
|
|
1444
|
+
runName,
|
|
1445
|
+
sessionKey,
|
|
1446
|
+
values = [],
|
|
1447
|
+
tags = {},
|
|
1448
|
+
random = Math.random,
|
|
1449
|
+
now = Date.now
|
|
1450
|
+
} = {}) => {
|
|
1451
|
+
let out = raw;
|
|
1452
|
+
let stages = [];
|
|
1453
|
+
let heldOut = false;
|
|
1454
|
+
try {
|
|
1455
|
+
heldOut = holdout > 0 && random() < holdout;
|
|
1456
|
+
const cacheStore = !heldOut && enabled.has("cache") && logsDir !== void 0 && sessionKey !== void 0 ? openCacheStore(logsDir, sessionKey) : void 0;
|
|
1457
|
+
if (!heldOut) {
|
|
1458
|
+
const retain = logsDir !== void 0 && runName !== void 0 && runName !== "" ? retainIn(join4(logsDir, "raw-output"), runName, values) : void 0;
|
|
1459
|
+
const filtered = filterOutput(raw, { command, exitCode, durationS, retain, enabled, cacheStore, values, pipeline });
|
|
1460
|
+
out = filtered.out;
|
|
1461
|
+
stages = filtered.stages;
|
|
1462
|
+
}
|
|
1463
|
+
} catch {
|
|
1464
|
+
out = raw;
|
|
1465
|
+
stages = [];
|
|
1466
|
+
}
|
|
1467
|
+
if (logsDir !== void 0) {
|
|
1468
|
+
try {
|
|
1469
|
+
const matched = matchedCleaners(command, enabled);
|
|
1470
|
+
const stat = {
|
|
1471
|
+
...tags,
|
|
1472
|
+
ts: now(),
|
|
1473
|
+
command: command.slice(0, 200),
|
|
1474
|
+
exit: exitCode,
|
|
1475
|
+
durationS: Number(durationS),
|
|
1476
|
+
rawBytes: raw.length,
|
|
1477
|
+
emittedBytes: out.length,
|
|
1478
|
+
cleaners: [...enabled],
|
|
1479
|
+
matched: out.startsWith(CACHE_MARKER) ? [...matched, "cache"] : matched,
|
|
1480
|
+
heldOut,
|
|
1481
|
+
stageBytes: Object.fromEntries(stages.map((stage) => [stage.id, stage.saved]))
|
|
1482
|
+
};
|
|
1483
|
+
mkdirSync3(logsDir, { recursive: true });
|
|
1484
|
+
appendFileSync(join4(logsDir, "filter-stats.jsonl"), `${JSON.stringify(stat)}
|
|
1485
|
+
`);
|
|
1486
|
+
} catch {
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
try {
|
|
1490
|
+
out = redactText(out, values);
|
|
1491
|
+
} catch {
|
|
1492
|
+
}
|
|
1493
|
+
return { out, heldOut, stages };
|
|
1494
|
+
};
|
|
1495
|
+
var main = async () => {
|
|
1496
|
+
const [command = "", exitCode = "0", durationS = "0", logPath = "", pipeline = ""] = process.argv.slice(2);
|
|
1497
|
+
const chunks = [];
|
|
1498
|
+
for await (const chunk of process.stdin) {
|
|
1499
|
+
chunks.push(chunk);
|
|
1500
|
+
}
|
|
1501
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
1502
|
+
let values = [];
|
|
1503
|
+
try {
|
|
1504
|
+
values = secretValues();
|
|
1505
|
+
} catch {
|
|
1506
|
+
values = [];
|
|
1507
|
+
}
|
|
1508
|
+
const terminalsDir = process.env["INTENTIC_TERMINAL_LOGS_DIR"];
|
|
1509
|
+
const { out } = filterRun(raw, {
|
|
1510
|
+
command,
|
|
1511
|
+
exitCode,
|
|
1512
|
+
durationS,
|
|
1513
|
+
pipeline,
|
|
1514
|
+
enabled: parseCleaners(process.env["INTENTIC_OUTPUT_CLEANERS"]),
|
|
1515
|
+
holdout: Number(process.env["INTENTIC_OUTPUT_HOLDOUT"] ?? "0"),
|
|
1516
|
+
logsDir: terminalsDir !== void 0 && terminalsDir !== "" ? join4(terminalsDir, "..") : void 0,
|
|
1517
|
+
// Named after the pane log, which is unique per command: one window, one pane id.
|
|
1518
|
+
runName: logPath === "" ? void 0 : basename2(logPath),
|
|
1519
|
+
// Every command of a session runs in a new pane, so the session is the pane log's name minus the pane.
|
|
1520
|
+
sessionKey: sessionKeyFromLog(logPath),
|
|
1521
|
+
values
|
|
1522
|
+
});
|
|
1523
|
+
process.stdout.write(out);
|
|
1524
|
+
};
|
|
1525
|
+
if (process.argv[1]?.endsWith("agent-output-filter") || process.argv[1]?.endsWith("agent-output-filter.mjs")) {
|
|
1526
|
+
await main();
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// src/hook-io.ts
|
|
1530
|
+
import { homedir } from "node:os";
|
|
1531
|
+
import { join as join5 } from "node:path";
|
|
1532
|
+
var readStdin = async () => {
|
|
1533
|
+
const chunks = [];
|
|
1534
|
+
for await (const chunk of process.stdin) {
|
|
1535
|
+
chunks.push(chunk);
|
|
1536
|
+
}
|
|
1537
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1538
|
+
};
|
|
1539
|
+
var parseHookInput = (text) => {
|
|
1540
|
+
try {
|
|
1541
|
+
const value = JSON.parse(text);
|
|
1542
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
1543
|
+
} catch {
|
|
1544
|
+
return void 0;
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
var runHook = async (handle) => {
|
|
1548
|
+
try {
|
|
1549
|
+
const raw = await readStdin();
|
|
1550
|
+
const input = parseHookInput(raw);
|
|
1551
|
+
if (input === void 0) {
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
const answer = await handle(input, raw);
|
|
1555
|
+
if (typeof answer === "string") {
|
|
1556
|
+
process.stdout.write(answer);
|
|
1557
|
+
} else if (answer !== void 0) {
|
|
1558
|
+
process.stdout.write(`${JSON.stringify(answer)}
|
|
1559
|
+
`);
|
|
1560
|
+
}
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
process.stderr.write(`intentic: ${error instanceof Error ? error.message : String(error)}
|
|
1563
|
+
`);
|
|
1564
|
+
}
|
|
1565
|
+
};
|
|
1566
|
+
var dataDir = (env = process.env) => env["CLAUDE_PLUGIN_DATA"] ?? join5(env["CLAUDE_CONFIG_DIR"] ?? join5(homedir(), ".claude"), "plugins", "data", "intentic-intentic");
|
|
1567
|
+
var projectDir = (input, env = process.env) => env["CLAUDE_PROJECT_DIR"] ?? input.cwd ?? process.cwd();
|
|
1568
|
+
|
|
1569
|
+
// src/post-bash.ts
|
|
1570
|
+
var outputDir = (data) => join6(data, "output");
|
|
1571
|
+
var OWN_REPORTS = /(?:^|[\s/])intentic-(?:stats|notes-evidence)(?:\s|$)/;
|
|
1572
|
+
var rawOf = (response) => {
|
|
1573
|
+
if (typeof response.stdout !== "string" || response.interrupted === true || response.isImage === true || response.backgroundTaskId !== void 0) {
|
|
1574
|
+
return void 0;
|
|
1575
|
+
}
|
|
1576
|
+
const stderr = typeof response.stderr === "string" ? response.stderr : "";
|
|
1577
|
+
return stderr === "" ? response.stdout : `${response.stdout}${response.stdout === "" || response.stdout.endsWith("\n") ? "" : "\n"}${stderr}`;
|
|
1578
|
+
};
|
|
1579
|
+
var postBash = (input, env = process.env) => {
|
|
1580
|
+
const options = readOptions(env);
|
|
1581
|
+
const command = input.tool_input?.["command"];
|
|
1582
|
+
const response = input.tool_response;
|
|
1583
|
+
if (!options.output_cleaners || typeof command !== "string" || OWN_REPORTS.test(command) || typeof response !== "object" || response === null) {
|
|
1584
|
+
return void 0;
|
|
1585
|
+
}
|
|
1586
|
+
const raw = rawOf(response);
|
|
1587
|
+
if (raw === void 0 || raw === "") {
|
|
1588
|
+
return void 0;
|
|
1589
|
+
}
|
|
1590
|
+
const session = input.session_id ?? "session";
|
|
1591
|
+
const { out } = filterRun(raw, {
|
|
1592
|
+
command,
|
|
1593
|
+
exitCode: "0",
|
|
1594
|
+
durationS: String(Math.round((input.duration_ms ?? 0) / 1e3)),
|
|
1595
|
+
enabled: parseCleaners(options.cleaners),
|
|
1596
|
+
holdout: options.output_holdout,
|
|
1597
|
+
logsDir: outputDir(dataDir(env)),
|
|
1598
|
+
// One file per call, named so a person can tell which session it came from.
|
|
1599
|
+
runName: `${session}-${input.tool_use_id ?? Date.now().toString(36)}.log`,
|
|
1600
|
+
sessionKey: session,
|
|
1601
|
+
values: [],
|
|
1602
|
+
tags: { project: projectDir(input, env), session }
|
|
1603
|
+
});
|
|
1604
|
+
if (out === raw) {
|
|
1605
|
+
return void 0;
|
|
1606
|
+
}
|
|
1607
|
+
return { hookSpecificOutput: { hookEventName: "PostToolUse", updatedToolOutput: { ...response, stdout: out, stderr: "" } } };
|
|
1608
|
+
};
|
|
1609
|
+
if (process.argv[1]?.endsWith("post-bash.mjs")) {
|
|
1610
|
+
await runHook((input) => postBash(input));
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// src/sessions.ts
|
|
1614
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
1615
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
1616
|
+
var sessionsFile = (data) => join7(data, "sessions.jsonl");
|
|
1617
|
+
var readSessions = (data) => {
|
|
1618
|
+
let text;
|
|
1619
|
+
try {
|
|
1620
|
+
text = readFileSync4(sessionsFile(data), "utf8");
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
if (isMissing2(error)) {
|
|
1623
|
+
return [];
|
|
1624
|
+
}
|
|
1625
|
+
throw error;
|
|
1626
|
+
}
|
|
1627
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1628
|
+
for (const line of text.split("\n")) {
|
|
1629
|
+
if (line.trim() === "") {
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
try {
|
|
1633
|
+
const row = JSON.parse(line);
|
|
1634
|
+
if (typeof row.session === "string" && typeof row.project === "string") {
|
|
1635
|
+
byId.set(row.session, row);
|
|
1636
|
+
}
|
|
1637
|
+
} catch {
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
return [...byId.values()];
|
|
1642
|
+
};
|
|
1643
|
+
|
|
1644
|
+
// src/stats.ts
|
|
1645
|
+
var MAX_SESSIONS = 150;
|
|
1646
|
+
var tokens2 = (count) => {
|
|
1647
|
+
if (count >= 1e6) {
|
|
1648
|
+
return `${(count / 1e6).toFixed(1)}M`;
|
|
1649
|
+
}
|
|
1650
|
+
if (count >= 1e4) {
|
|
1651
|
+
return `${Math.round(count / 1e3)}k`;
|
|
1652
|
+
}
|
|
1653
|
+
return count >= 1e3 ? `${(count / 1e3).toFixed(1)}k` : String(count);
|
|
1654
|
+
};
|
|
1655
|
+
var counted = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
1656
|
+
var readText = (path) => {
|
|
1657
|
+
try {
|
|
1658
|
+
return readFileSync5(path, "utf8");
|
|
1659
|
+
} catch (error) {
|
|
1660
|
+
if (isMissing2(error)) {
|
|
1661
|
+
return void 0;
|
|
1662
|
+
}
|
|
1663
|
+
throw error;
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
var cleanerLines = (summary, options) => {
|
|
1667
|
+
if (summary.commands === 0) {
|
|
1668
|
+
return [options.output_cleaners ? "No commands have been trimmed yet." : "Switched off (`output_cleaners` in /config)."];
|
|
1669
|
+
}
|
|
1670
|
+
const cleaned = summary.holdout.cleaned;
|
|
1671
|
+
const lines = [
|
|
1672
|
+
`${counted(cleaned, "command")} trimmed: ${tokens2(summary.rawTokens)} tokens of output became ${tokens2(summary.emittedTokens)} (${summary.savedPct}% saved, exact per command).`
|
|
1673
|
+
];
|
|
1674
|
+
const stages = summary.perCleaner.filter((stage) => stage.savedTokens > 0).slice(0, 6);
|
|
1675
|
+
if (stages.length > 0) {
|
|
1676
|
+
lines.push(`Biggest cleaners: ${stages.map((stage) => `${stage.id} ${tokens2(stage.savedTokens)} (${counted(stage.commands, "command")})`).join(", ")}.`);
|
|
1677
|
+
}
|
|
1678
|
+
const { heldOut, measuredSavedPct } = summary.holdout;
|
|
1679
|
+
if (measuredSavedPct !== void 0) {
|
|
1680
|
+
lines.push(`Against ${counted(heldOut, "untrimmed command")}, the median output is ${measuredSavedPct}% smaller.`);
|
|
1681
|
+
} else if (heldOut > 0) {
|
|
1682
|
+
lines.push(`The untrimmed comparison has ${counted(heldOut, "command")}; it reports once it is large enough and the difference is clear.`);
|
|
1683
|
+
} else {
|
|
1684
|
+
lines.push("No commands are left untrimmed (`output_holdout` is 0), so the saving is exact per command but has no control group.");
|
|
1685
|
+
}
|
|
1686
|
+
const gaps = summary.gaps.slice(0, 3);
|
|
1687
|
+
if (gaps.length > 0) {
|
|
1688
|
+
lines.push(`Untouched and still costly: ${gaps.map((gap) => `\`${gap.command}\` ${tokens2(gap.tokens)} over ${gap.commands}`).join(", ")}.`);
|
|
1689
|
+
}
|
|
1690
|
+
return lines;
|
|
1691
|
+
};
|
|
1692
|
+
var turnsOf = (row) => {
|
|
1693
|
+
if (row.transcript === void 0) {
|
|
1694
|
+
return [];
|
|
1695
|
+
}
|
|
1696
|
+
const session = readClaudeSession(row.transcript, { root: row.project });
|
|
1697
|
+
return (session?.turns ?? []).map((turn) => {
|
|
1698
|
+
const metrics = createTurnMetrics(row.project);
|
|
1699
|
+
const edited = [];
|
|
1700
|
+
for (const call of turn.calls) {
|
|
1701
|
+
metrics.call(call);
|
|
1702
|
+
if (call.category === "edit") {
|
|
1703
|
+
edited.push(...(call.locations ?? []).map((location) => location.path));
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
for (const failure of turn.failures) {
|
|
1707
|
+
metrics.failed(failure.id);
|
|
1708
|
+
}
|
|
1709
|
+
return {
|
|
1710
|
+
conversationId: row.session,
|
|
1711
|
+
at: turn.at,
|
|
1712
|
+
turnIndex: turn.index,
|
|
1713
|
+
...metrics.reading(edited),
|
|
1714
|
+
mapArm: row.arms.map,
|
|
1715
|
+
notesArm: row.arms.notes,
|
|
1716
|
+
iqArm: row.arms.iq,
|
|
1717
|
+
notesCohort: row.notesRevision
|
|
1718
|
+
};
|
|
1719
|
+
});
|
|
1720
|
+
};
|
|
1721
|
+
var DESIGNS = [
|
|
1722
|
+
{ title: "Project map", option: "project_map", design: { ...MECHANISMS.map, arm: (turn) => turn.mapArm, cohort: () => void 0 }, reads: "root listings in the opening turn" },
|
|
1723
|
+
{
|
|
1724
|
+
title: "Field notes",
|
|
1725
|
+
option: "field_notes",
|
|
1726
|
+
design: { ...MECHANISMS.notes, arm: (turn) => turn.notesArm, cohort: (turn) => turn.notesCohort },
|
|
1727
|
+
reads: "failed tool calls per turn"
|
|
1728
|
+
},
|
|
1729
|
+
{ title: "iq teaching", option: "iq", design: { ...MECHANISMS.search, arm: (turn) => turn.iqArm, cohort: () => void 0 }, reads: "search calls per turn" }
|
|
1730
|
+
];
|
|
1731
|
+
var readingLine = (reading, unit) => {
|
|
1732
|
+
const arms = `${reading.on.mean} with it vs ${reading.off.mean} without, over ${reading.on.turns} and ${reading.off.turns} ${unit}`;
|
|
1733
|
+
if (reading.deltaPct !== void 0 && reading.marginPct !== void 0) {
|
|
1734
|
+
return `${reading.deltaPct > 0 ? "+" : ""}${reading.deltaPct}% \xB1${reading.marginPct}pp (95%): ${arms}.`;
|
|
1735
|
+
}
|
|
1736
|
+
if (reading.marginPct !== void 0) {
|
|
1737
|
+
return `no difference yet beyond \xB1${reading.marginPct}pp (95%): ${arms}.`;
|
|
1738
|
+
}
|
|
1739
|
+
return `measuring, ${Math.min(reading.on.turns, reading.off.turns)} of ${MIN_ARM_TURNS} ${unit} in the smaller arm: ${arms}.`;
|
|
1740
|
+
};
|
|
1741
|
+
var mechanismLine = (title, reads, on, experiment, holdout) => {
|
|
1742
|
+
if (!on) {
|
|
1743
|
+
return `${title}: switched off.`;
|
|
1744
|
+
}
|
|
1745
|
+
if (holdout === 0) {
|
|
1746
|
+
return `${title}: on for every session; nothing is held out (\`holdout\` is 0), so there is nothing to compare.`;
|
|
1747
|
+
}
|
|
1748
|
+
if (experiment === void 0) {
|
|
1749
|
+
return `${title}: no measured sessions yet.`;
|
|
1750
|
+
}
|
|
1751
|
+
return `${title}, ${reads}: ${readingLine(experiment.metrics[0], experiment.sampleUnit ?? "conversations")}`;
|
|
1752
|
+
};
|
|
1753
|
+
var countShadows = (dir) => {
|
|
1754
|
+
let count = 0;
|
|
1755
|
+
const walk = (current) => {
|
|
1756
|
+
let entries;
|
|
1757
|
+
try {
|
|
1758
|
+
entries = readdirSync3(current, { withFileTypes: true });
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
if (isMissing2(error)) {
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
throw error;
|
|
1764
|
+
}
|
|
1765
|
+
for (const entry of entries) {
|
|
1766
|
+
if (entry.isDirectory()) {
|
|
1767
|
+
walk(join8(current, entry.name));
|
|
1768
|
+
} else if (entry.name.endsWith(".md")) {
|
|
1769
|
+
count += 1;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
walk(dir);
|
|
1774
|
+
return count;
|
|
1775
|
+
};
|
|
1776
|
+
var lastSweep = (data, project) => {
|
|
1777
|
+
const text = readText(join8(data, "shadows", `${projectSlug(project)}.sweep.log`));
|
|
1778
|
+
try {
|
|
1779
|
+
const summary = JSON.parse(text ?? "");
|
|
1780
|
+
return typeof summary.derived === "number" && typeof summary.fresh === "number" ? ` (last sweep: ${summary.derived} derived, ${summary.fresh} already fresh${typeof summary.pruned === "number" && summary.pruned > 0 ? `, ${summary.pruned} pruned` : ""})` : "";
|
|
1781
|
+
} catch {
|
|
1782
|
+
return "";
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
var shadowLine = (args) => {
|
|
1786
|
+
if (!args.options.shadows) {
|
|
1787
|
+
return "Document shadows: switched off.";
|
|
1788
|
+
}
|
|
1789
|
+
if (args.project === void 0) {
|
|
1790
|
+
return "Document shadows: reported per project.";
|
|
1791
|
+
}
|
|
1792
|
+
const count = countShadows(join8(args.project, DERIVED_DIR));
|
|
1793
|
+
return `Document shadows: ${count} documents have a markdown shadow${lastSweep(args.data, args.project)}. Nothing measures what they save yet.`;
|
|
1794
|
+
};
|
|
1795
|
+
var inScope = (args, project) => args.project === void 0 || project === args.project;
|
|
1796
|
+
var statsReport = (args) => {
|
|
1797
|
+
const rows = parseStatsFile(readText(join8(outputDir(args.data), "filter-stats.jsonl")) ?? "").filter((row) => inScope(args, row.project));
|
|
1798
|
+
const sessions = readSessions(args.data).filter((row) => inScope(args, row.project)).toSorted((left, right) => right.ts - left.ts).slice(0, MAX_SESSIONS);
|
|
1799
|
+
const turns = sessions.flatMap(turnsOf);
|
|
1800
|
+
const switches = Object.keys(OPTIONS).map((key) => `${key}=${String(args.options[key])}`).join(" ");
|
|
1801
|
+
return [
|
|
1802
|
+
`# intentic: what the plugin saved`,
|
|
1803
|
+
``,
|
|
1804
|
+
`Scope: ${args.project === void 0 ? "every project" : `\`${args.project}\``} \xB7 ${sessions.length} recorded sessions \xB7 switches as the last session opened: ${switches}`,
|
|
1805
|
+
``,
|
|
1806
|
+
`## Bash output`,
|
|
1807
|
+
...cleanerLines(summarizeStats(rows), args.options),
|
|
1808
|
+
``,
|
|
1809
|
+
`## Session context`,
|
|
1810
|
+
...DESIGNS.map(
|
|
1811
|
+
({ title, option, design, reads }) => mechanismLine(title, reads, args.options[option] === true, measureExperiment(turns, design), args.options.holdout)
|
|
1812
|
+
),
|
|
1813
|
+
``,
|
|
1814
|
+
shadowLine(args),
|
|
1815
|
+
``
|
|
1816
|
+
].join("\n");
|
|
1817
|
+
};
|
|
1818
|
+
var parseStatsArgs = (argv) => {
|
|
1819
|
+
let data = "";
|
|
1820
|
+
let project;
|
|
1821
|
+
let all = false;
|
|
1822
|
+
const overrides = {};
|
|
1823
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1824
|
+
const arg = argv[index] ?? "";
|
|
1825
|
+
if (arg === "--data") {
|
|
1826
|
+
data = argv[index += 1] ?? "";
|
|
1827
|
+
} else if (arg === "--project") {
|
|
1828
|
+
project = argv[index += 1];
|
|
1829
|
+
} else if (arg === "--option") {
|
|
1830
|
+
const [key, ...value] = (argv[index += 1] ?? "").split("=");
|
|
1831
|
+
overrides[optionEnvName(key ?? "")] = value.join("=");
|
|
1832
|
+
} else if (arg === "all") {
|
|
1833
|
+
all = true;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
const saved = loadOptions(data);
|
|
1837
|
+
const env = Object.fromEntries(Object.keys(OPTIONS).map((key) => [optionEnvName(key), String(saved[key])]));
|
|
1838
|
+
return { data, project: all ? void 0 : project, options: readOptions({ ...env, ...overrides }) };
|
|
1839
|
+
};
|
|
1840
|
+
if (process.argv[1]?.endsWith("stats.mjs")) {
|
|
1841
|
+
process.stdout.write(statsReport(parseStatsArgs(process.argv.slice(2))));
|
|
1842
|
+
}
|
|
1843
|
+
export {
|
|
1844
|
+
parseStatsArgs,
|
|
1845
|
+
statsReport,
|
|
1846
|
+
turnsOf
|
|
1847
|
+
};
|