Show each live Claude Code session of this machine in one pane. The pane tells you which session needs you now.

<img src="https://raw.githubusercontent.com/NissanOhana/orangu/main/design/brand/mascot-main-transparent.png" width="140" alt="orangu" />
Improve your agent harness, one checked change at a time.
Local, offline, deterministic observability for Claude Code, Cowork, and Desktop sessions. Optional Claude Code skills turn the evidence into reviewable changes.
sample report · repo sample · docs · npm · npx orangu
You run coding agents for hours. The transcript on disk records what they did. orangu reads it and shows you:
| What the session does not tell you | What orangu shows |
|---|---|
| "compacted" | which turns filled the context window, and with what |
| a subagent's final answer | its full tree: tools, tokens, time, errors |
| that a skill or MCP server is installed | whether it ever fired, and what it weighs in context |
| nothing about repetition | the same file read again and again, and the same context re-read in every request |
One real number from one local session: 34.9M of the 37.0M tokens the model processed were cache reads. The model re-read one 223k-token context 156 times. Tokens are the only usage metric orangu has, because they are the only one the transcript records.
ccusage counts your token usage. orangu tells you what to change.
Read-only. orangu reads the session files already on your disk and writes one HTML file. It needs no SDK, proxy, account, instrumentation, upload, or telemetry. It can analyze your whole history from the minute you install it, not only new sessions.
npx orangu # start here: choose a report, or see what to improve next
npx orangu serve # the local app: every session, live, and your improvements
npx orangu report # only the latest session, as one HTML report
In Claude Code, start with /orangu:improve after you install the skills. It finds the next change to make, and it checks each change that you apply.
Use the arrow keys or j/k and Enter in the dashboard. In a pipe or CI, bare orangu keeps the compact latest-session summary instead of waiting for input. orangu help all lists every command and flag.
orangu needs Node.js 20 or newer and no runtime dependency. The usage guide has more commands, inputs, and limits.
orangu serve show quality, time, and tokens as separate axes, with the evidence behind each finding./orangu:improve or /orangu:harness drafts one proposal for each change, in one of nine change classes. A proposal changes no file./orangu:apply <id> makes it and records which checks ran./orangu:improve --verify <id> shows the comparison and records a final verdict.orangu serve lists them under Improvements.orangu never changes your harness by itself. Global (whole-machine) changes stay proposals. A later comparison is not causal proof. The full boundary is in determinism and skill authority.
/plugin marketplace add NissanOhana/orangu
/plugin install orangu
| Skill | What it does |
|---|---|
/orangu:analyze | Explain what happened in one session, finished or still running, from local evidence. Open the report of the current session |
/orangu:improve | Start here. Find the next change, draft one reviewable proposal, and check each change that you apply |
/orangu:interview | Research outside options for one suggestion, then interview you before its proposal or a second opinion (/orangu:interview <sg_id>) |
/orangu:apply | Apply one reviewed session or repo proposal and save the receipt |
/orangu:harness | Review the whole harness: CLAUDE.md, memory, skills, agents, hooks, MCP. A rule that did not hold gets a hook, not another note |
/orangu:show-me | Turn the evidence of a session, a repository or all sessions into a slide deck and a written report, as offline HTML. Claude writes only 3 text values, and orangu show-me writes both files from the data |
/orangu:feedback | Send reviewed beta feedback from a localhost form |
Skills read orangu ... --json and orangu evidence, never the raw .jsonl. Each skill shows a token estimate before a large read. The shipped instructions live in plugin/skills/. Codex can also host the improvement skills: codex plugin marketplace add NissanOhana/orangu then codex plugin add orangu@orangu installs $orangu-improve, $orangu-interview, $orangu-apply, and $orangu-feedback. orangu does not ingest Codex transcripts.
orangu is not a proxy, an SDK, a hook you install before the session, or a model that summarizes your transcript. It is a parser plus a rule set over the .jsonl files Claude Code already writes. Nothing runs during your session, nothing is sent anywhere, and the measurement never asks a model what it thinks.
default-src 'none' and makes no request. serve binds to 127.0.0.1 and places every route behind a fresh process capability in its launched URL.--include-text.| Raw transcripts | Contain the record, but leave the reading and synthesis to you. |
| Usage counters (ccusage and friends) | Sum tokens across time and sources. |
| Instrumented trace platforms | Capture traces and evaluations from applications that you instrument. |
| orangu | Reads the transcript you already have, explains it, and turns findings into reviewed changes with receipts. |
--strip-paths before you share a report. Review every export first.Read the privacy model, supported limits, and security policy before sharing a report or JSON export. Are you an LLM? Read llms.txt.
| Guide | Purpose |
|---|---|
| Documentation index | All user, integration, design, and contributor references |
| Usage | CLI commands, app surfaces, inputs, and limits |
| Determinism | Local evidence, AI-skill authority, and lifecycle rules |
| Architecture | Pipeline, modules, build, and correctness gates |
| Data contracts | Analysis, evidence, app, proposal, and receipt shapes |
| Beta feedback | Localhost intake, exact-review consent, and GitHub handoff |
| Contributing | Development workflow and project layout |
git clone https://github.com/NissanOhana/orangu && cd orangu
npm ci && npm run verify:release
MIT © Nissan Ohana. The mascot is CC0.
hooks/god.mjs 6042 lines1// GENERATED by scripts/build.mjs from src/god. Do not edit.
2
3// src/god/register.tsx
4import { read as read6, update as update6 } from "claude-code";
5
6// src/god/alert-keys.tsx
7import { read, update } from "claude-code";
8
9// src/god/act/alert-policy.ts
10var ALERT_WINDOW_MS = 5 * 6e4;
11var NO_ALERTS = { alertAt: {}, doneAt: {} };
12var KIND_OF_LEVEL = { "needs-you": "needs-you", stuck: "stuck", "your-turn": "done" };
13var TONE_ORDER = ["needs-you", "stuck", "done"];
14var holdsBack = (time, now) => typeof time === "number" && Number.isFinite(time) && time <= now && now - time <= ALERT_WINDOW_MS;
15function recentTimes(times, now, added = []) {
16 const kept = Object.entries(times).filter(([, time]) => holdsBack(time, now));
17 return Object.fromEntries([...kept, ...added.map((sessionId) => [sessionId, now])]);
18}
19var isBase = (last, lastRefreshAt, now) => last !== void 0 && last.firstRefreshDone && lastRefreshAt !== void 0 && now - lastRefreshAt <= ALERT_WINDOW_MS;
20function changes(last, next) {
21 const before = new Map(last.sessions.map((session) => [session.sessionId, session.level]));
22 return next.sessions.flatMap(({ sessionId, level }) => {
23 const levelBefore = before.get(sessionId);
24 const kind = KIND_OF_LEVEL[level];
25 if (kind === void 0 || levelBefore === level || levelBefore === void 0 && kind === "done") return [];
26 return [{ sessionId, kind }];
27 });
28}
29function decideAlerts(input) {
30 const { last, next, store, now } = input;
31 const alertAt = recentTimes(input.state.alertAt, now);
32 const doneAt = recentTimes(input.state.doneAt, now);
33 if (!isBase(last, input.lastRefreshAt, now)) return { alerts: [], state: { alertAt, doneAt } };
34 const muted = new Set(store.muted);
35 const isHeldBack = (sessionId, kind) => Object.hasOwn(alertAt, sessionId) || kind === "done" && Object.hasOwn(doneAt, sessionId);
36 const due = changes(last, next).filter(({ sessionId, kind }) => !muted.has(sessionId) && !isHeldBack(sessionId, kind)).sort((a, b) => TONE_ORDER.indexOf(a.kind) - TONE_ORDER.indexOf(b.kind));
37 let hasTone = store.sound && input.isPaneOpen;
38 const alerts = due.flatMap(({ sessionId, kind }) => {
39 const playsTone = hasTone;
40 const notifies = kind !== "done";
41 if (!playsTone && !notifies) return [];
42 if (playsTone) hasTone = false;
43 return [{ sessionId, kind, playsTone, notifies }];
44 });
45 const idsOf = (isDone) => alerts.filter((alert) => alert.kind === "done" === isDone).map((alert) => alert.sessionId);
46 return { alerts, state: { alertAt: recentTimes(alertAt, now, idsOf(false)), doneAt: recentTimes(doneAt, now, idsOf(true)) } };
47}
48
49// src/god/copy.ts
50var GOD_COMMAND_DESCRIPTION = "Show the live Claude Code sessions of this machine in one pane.";
51var GOD_ARGUMENT_HINT = "[all | repos <name>... | stats]";
52var PANE_TITLE = "orangu god";
53var KEYS_LABEL = "keys:";
54var KEYS_HOLDER = {
55 pane: { holder: "pane", handover: "Esc: prompt" },
56 prompt: { holder: "prompt", handover: "ctrl+x tab: pane" }
57};
58var HELP_TITLE = "What the pane shows. Press h or Esc to close this help.";
59var HELP_LEVELS = {
60 "needs-you": "The session waits for your answer to a question or a permission prompt.",
61 "your-turn": "The session finished its turn and waits for your next prompt.",
62 stuck: "The session is busy, but it wrote nothing for a long time, or it got an API error.",
63 working: "The session works on its task now.",
64 idle: "The session waits for a prompt, and you saw its last turn.",
65 stale: "The session is idle for a long time. Press Enter on the heading to show its group."
66};
67var HELP_SAME_FILE = "This session and another session wrote the same file in the last 2 h.";
68var HELP_KEYS = [
69 ["\u2191\u2193", "Select a session."],
70 ["Enter", "Open the selected session."],
71 ["b", "Go back to the board from the detail of a narrow pane."],
72 ["j", "Jump to the cmux tab of the session."],
73 ["n", "Select the next session that needs you."],
74 ["1-9", "Pick an option of the open question. Then press y to send it."],
75 ["p", "Go back 1 question in a dialog with 2 or more single-select questions."],
76 ["y c", "Send, or cancel, what the confirm bar shows."],
77 ["r", "Write a prompt. The session reads it as your prompt."],
78 ["m", "Write a peer note. The session reads it as a note from a peer, not as your prompt."],
79 ["t", "Show the next tab of the detail."],
80 ["w z", "Show the lines above or below a cut text."],
81 ["x", "Make a Recap of the session. This key shows only on the Recap tab."],
82 ["s", "Turn all tones off, or on again."],
83 ["u", "Mute the selected session, or unmute it."],
84 ["f", "Show every session, or only the sessions of the selected repos."],
85 ["g", "Group the rows by level or by repo."],
86 ["h", "Show this help, or close it."],
87 ["Esc", "Give the keys to the prompt."],
88 ["ctrl+x tab", "Give the keys to the pane."]
89];
90function sharesFolderText(count2, names, left) {
91 return `Shares its folder with ${count2} working ${count2 === 1 ? "session" : "sessions"}: ${names.join(", ")}${left > 0 ? ` and ${left} more` : ""}`;
92}
93var GOD_OPENED = "The god pane is open.";
94var GOD_NOT_PLACED = "The god pane waits for more room. Make the terminal wider.";
95var GOD_SHOWS_ALL = "The god pane shows every live session.";
96var godShowsRepos = (names) => `The god pane shows the sessions of these repos: ${names.join(", ")}.`;
97var reposSetText = (names) => names.length === 0 ? "No repo is selected. Run /god repos <name> to select one." : `The repos mode shows these repos: ${names.join(", ")}.`;
98var NO_REPO_SET = "No repo is selected. Type /god repos and a repo name.";
99var GOD_USAGE = "Run /god to open the pane. Run /god all to show every session. Run /god repos <name> to show the sessions of a repo. Run /god stats to show the time of the last refresh.";
100var GOD_FAILED = "The god command failed. Run /god again.";
101var needYouStatus = (count2) => `god: ${count2} ${count2 === 1 ? "needs" : "need"} you`;
102var GOD_STATS_OFF = "The god pane does not run now. Run /god to start it.";
103var GOD_STATS_READING = "The god pane reads the sessions now. Run /god stats again in 2 s.";
104function godStats(last, mostComputeMs, refreshes) {
105 const sessions = `${last.sessions} ${last.sessions === 1 ? "session" : "sessions"}`;
106 const kept = `${refreshes} ${refreshes === 1 ? "refresh" : "refreshes"}`;
107 return `The last refresh took ${last.computeMs} ms of engine work and ${last.waitMs} ms of wait for host commands, for ${sessions}. The most engine work in the last ${kept} was ${mostComputeMs} ms.`;
108}
109function alertCountsText(counts) {
110 const tones = `${counts.tones} ${counts.tones === 1 ? "tone" : "tones"}`;
111 const notifications = `${counts.notifications} ${counts.notifications === 1 ? "notification" : "notifications"}`;
112 return `The pane played ${tones} since it started, and the notification channel of Claude Code wrote ${notifications}. A Notification hook can show more.`;
113}
114function alertBody(name, kind) {
115 return kind === "needs-you" ? `The session ${name} needs you.` : `The session ${name} is stuck.`;
116}
117var RECAP_SYSTEM = "You write a recap of 1 Claude Code session for the person who runs it. Write 3 short sentences or fewer. Say what the person asked, what the session did last, and what it waits for now. Use only the facts that follow, and write no secret.";
118var RECAP_LABEL = {
119 session: "Session",
120 title: "Title",
121 level: "Level",
122 waits: "Waits for",
123 question: "Open question",
124 tool: "Last tool",
125 prompt: "Prompt",
126 reply: "Reply"
127};
128var BOARD_TOOL_DESCRIPTION = "This tool shows 1 line for each live Claude Code session on this machine, in board order: the sessions that need the person come first. Each line gives the level and its time, the session name, the repo, the context percent, the summary, the title and the row name. The row name is the name that the pane shows for the session. The tool only reads, and it changes nothing.";
129var SESSION_TOOL_DESCRIPTION = "This tool shows the detail of the live session with the name that you give. The detail has the open question, the last prompt, the last reply, the activity, the agents and the overlap flags. Get the names from the board tool. The tool only reads, and it changes nothing.";
130var SESSION_TOOL_NAME_HINT = "The session name or the row name of the session, as the board tool shows them.";
131var TOOL_NOT_RUNNING = "The god pane does not run in this session now. Ask the person to run /god.";
132var TOOL_FAILED = "The orangu god tool failed. Call it again.";
133var TASK_RUNNING = "The session runs a background task or a subagent that can open a dialog, so the pane sent nothing.";
134var NOT_A_PROMPT = "The pane does not type a shell command, the help or an exit. Press j and type it in its tab.";
135var CONTROL_CHAR = "The text has a control character or a character that does not show, so the pane sent nothing. An emoji can hold such a character.";
136var answerAction = (digits) => `Press ${digits}, then y to send.`;
137var pickAction = (at, count2, digits) => at < count2 ? `Question ${at} of ${count2}. Press ${digits} to pick, then go on to the next question.` : `Question ${at} of ${count2}. Press ${digits} to pick, then y to send.`;
138var pickBackLabel = (at) => `back to question ${at}`;
139var ANSWER_NO_CMUX = "cmux is not on this machine, so answer the question in its tab.";
140var ANSWERS_PARTIAL = "The pane stopped after 1 or more answers, so the dialog stays open with some picks in it. Press j to answer the rest in its tab.";
141var answersAsk = (name, picks) => `Send to ${name}? ${picks.join(" \xB7 ")}`;
142
143// src/god/view/cells.ts
144var ZERO_WIDTH = /^[\p{Mn}\p{Me}\p{Cf}\p{Cc}\p{Default_Ignorable_Code_Point}]/u;
145var EMOJI = /\p{Emoji_Presentation}|️/u;
146var WIDE_RANGES = [
147 [4352, 4447],
148 [11904, 42191],
149 [44032, 55203],
150 [63744, 64255],
151 [65072, 65103],
152 [65280, 65376],
153 [65504, 65510],
154 [127744, 128591],
155 [129280, 129535],
156 [131072, 262141]
157];
158var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
159var isWide = (codePoint) => WIDE_RANGES.some(([low, high]) => codePoint >= low && codePoint <= high);
160function graphemes(text) {
161 return Array.from(segmenter.segment(text), (part) => part.segment);
162}
163function graphemeWidth(grapheme) {
164 const codePoint = grapheme.codePointAt(0) ?? 0;
165 if (codePoint >= 32 && codePoint <= 126) return 1;
166 if (ZERO_WIDTH.test(grapheme)) return 0;
167 return EMOJI.test(grapheme) || isWide(codePoint) ? 2 : 1;
168}
169function cellWidth(text) {
170 let width = 0;
171 for (const grapheme of graphemes(text)) width += graphemeWidth(grapheme);
172 return width;
173}
174function breakWord(word, width) {
175 const pieces = [];
176 let piece = "";
177 for (const grapheme of graphemes(word)) {
178 if (piece !== "" && cellWidth(piece) + graphemeWidth(grapheme) > width) {
179 pieces.push(piece);
180 piece = "";
181 }
182 piece += grapheme;
183 }
184 return piece === "" ? pieces : [...pieces, piece];
185}
186function wrapWords(text, width) {
187 const lines = [];
188 let line = "";
189 for (const word of text.split(/\s+/).filter((part) => part !== "")) {
190 const pieces = cellWidth(word) > width ? breakWord(word, width) : [word];
191 for (const piece of pieces) {
192 if (line !== "" && cellWidth(line) + 1 + cellWidth(piece) <= width) {
193 line += ` ${piece}`;
194 continue;
195 }
196 if (line !== "") lines.push(line);
197 line = piece;
198 }
199 }
200 return line === "" ? lines : [...lines, line];
201}
202function flowItems(items, width) {
203 const lines = [];
204 let used = 0;
205 for (const item of items) {
206 const itemWidth = item.reduce((sum, segment) => sum + cellWidth(segment.text), 0);
207 const line = lines.at(-1);
208 if (line !== void 0 && used + 2 + itemWidth <= width) {
209 line.push({ text: " " }, ...item);
210 used += 2 + itemWidth;
211 continue;
212 }
213 lines.push([{ text: " " }, ...item]);
214 used = 1 + itemWidth;
215 }
216 return lines.map((segments) => ({ segments }));
217}
218
219// src/god/view/wrap.ts
220var LINE_BREAK = /\r\n|[\n\r\u2028\u2029\u0085\v\f]/;
221var flat = (text) => text.replace(/\s+/g, " ").trim();
222var styleOf = ({ text: _text, ...style }) => style;
223var isWhole = (style) => style.press !== void 0 || style.isPainted === true;
224var sameStyle = (a, b) => !isWhole(a) && !isWhole(b) && a.token === b.token && a.bold === b.bold && a.dim === b.dim;
225function merged(segments) {
226 const out = [];
227 for (const segment of segments) {
228 if (segment.text === "") continue;
229 const last = out.at(-1);
230 if (last !== void 0 && sameStyle(styleOf(last), styleOf(segment))) out[out.length - 1] = { ...last, text: last.text + segment.text };
231 else out.push({ ...segment });
232 }
233 return out;
234}
235function splitAt(text, room) {
236 const chars = graphemes(text);
237 let head = chars[0] ?? "";
238 let index = 1;
239 while (index < chars.length && cellWidth(head + chars[index]) <= room) head += chars[index++];
240 return [head, chars.slice(index).join("")];
241}
242function wrapSegments(segments, width, indent2 = 1, hang = indent2) {
243 const clean = segments.map((segment) => ({ ...segment, text: segment.text.replace(/\t/g, " ").replace(new RegExp(LINE_BREAK.source, "g"), " ") }));
244 if (indent2 + clean.reduce((sum, segment) => sum + cellWidth(segment.text), 0) <= width) return [{ segments: merged([{ text: " ".repeat(indent2) }, ...clean]) }];
245 const words = clean.flatMap((segment) => isWhole(segment) ? [segment] : segment.text.split(/\s+/).filter((word) => word !== "").map((word) => ({ ...segment, text: word })));
246 const lines = [];
247 let line = [];
248 let used = 0;
249 const room = () => width - (lines.length === 0 ? indent2 : hang);
250 const flush = () => {
251 lines.push(line);
252 line = [];
253 used = 0;
254 };
255 for (const word of words) {
256 let rest = word.text;
257 while (rest !== "") {
258 const gap = line.length > 0 ? 1 : 0;
259 if (used + gap + cellWidth(rest) <= room()) {
260 const last = line.at(-1);
261 if (last !== void 0) line.push({ ...sameStyle(styleOf(last), styleOf(word)) ? styleOf(word) : {}, text: " " });
262 line.push({ ...word, text: rest });
263 used += gap + cellWidth(rest);
264 rest = "";
265 } else if (line.length > 0) {
266 flush();
267 } else {
268 const [head, tail] = splitAt(rest, room());
269 line.push({ ...word, text: head });
270 rest = tail;
271 flush();
272 }
273 }
274 }
275 if (line.length > 0) lines.push(line);
276 return lines.map((parts, index) => ({ segments: merged([{ text: " ".repeat(index === 0 ? indent2 : hang) }, ...parts]) }));
277}
278function textLines(text, width, indent2, style = {}) {
279 const out = [];
280 for (const source of text.split(LINE_BREAK)) {
281 if (source.trim() === "") {
282 if (out.length > 0 && out.at(-1).segments.length > 0) out.push({ segments: [] });
283 continue;
284 }
285 out.push(...wrapSegments([{ ...style, text: source.trimEnd() }], width, indent2));
286 }
287 while (out.at(-1)?.segments.length === 0) out.pop();
288 return out;
289}
290function keptLines(text, width, indent2, isTrimmed = false) {
291 const rows = text.replace(/\t/g, " ").split(LINE_BREAK).map((row) => isTrimmed ? row.trimEnd() : row);
292 while (rows.length > 0 && rows.at(-1).trim() === "") rows.pop();
293 const pad = " ".repeat(indent2);
294 return rows.flatMap((row) => {
295 if (row === "") return [{ segments: [] }];
296 const pieces = [];
297 let rest = row;
298 while (rest !== "") {
299 const [head, tail] = splitAt(rest, width - indent2);
300 pieces.push(head);
301 rest = tail;
302 }
303 return pieces.map((piece) => ({ segments: [{ text: pad + piece }] }));
304 });
305}
306var screenLines = (text, width) => keptLines(text, width, 1, true);
307function appendItem(lines, item, width) {
308 const last = lines.at(-1);
309 const used = last === void 0 ? 0 : last.segments.reduce((sum, segment) => sum + cellWidth(segment.text), 0);
310 const itemWidth = item.reduce((sum, segment) => sum + cellWidth(segment.text), 0);
311 if (last !== void 0 && used + 2 + itemWidth <= width) return [...lines.slice(0, -1), { segments: [...last.segments, { text: " " }, ...item] }];
312 return [...lines, ...wrapSegments(item, width)];
313}
314
315// src/god/view/names.ts
316var STATUS_GLYPH = /^(?:[✳◐◑◒◓]\u{FE0F}?|[\u{2800}-\u{28FF}])\s*/u;
317var DEFAULT_TAB_TITLE = "Claude Code";
318function rowName(session) {
319 const tab = flat(session.tab?.surfaceTitle ?? "").replace(STATUS_GLYPH, "");
320 return (tab === DEFAULT_TAB_TITLE ? "" : tab) || flat(session.transcript?.title ?? "") || flat(session.name);
321}
322
323// src/god/model/screen-match.ts
324var OPTION_ROW = /^( *)(?:(❯) | {2})([1-9])\. (.*)$/u;
325var PROMPT_ROW = /^ *❯(?:[ \u00a0](.*))?$/u;
326var CHIP_ROW = /^ *(?:← +)?[☐☒] +\S/u;
327var RULE_ROW = /^ *─{3,}/u;
328var FULL_RULE = /^ *─+$/u;
329var TYPE_SOMETHING = /^Type something\.?$/;
330var BASH_TITLE = /^Bash command(?: \([^()]*\))?$/;
331var DASHED_RULE = /^ *╌{3,}$/u;
332var GUTTER_ROW = /^ │(?: (.*))?$/u;
333var REQUEST_ROWS = 40;
334var WRAP_MARGIN = 6;
335var NARROW = [[32, 126], [160, 767], [880, 1154], [1162, 1327], [1488, 1514], [8208, 8231], [8240, 8286], [8592, 8703], [9472, 9599], [9744, 9746], [10004, 10004], [10095, 10095]];
336var WIDE = [[4352, 4447], [11904, 12350], [12353, 13311], [13312, 19903], [19968, 40959], [40960, 42191], [44032, 55203], [63744, 64255], [65072, 65103], [65280, 65376], [65504, 65510], [131072, 262141]];
337var trimEnd = (text) => text.replace(/ +$/, "");
338var trim = (text) => text.replace(/^ +| +$/g, "");
339var isBlank = (row) => row === void 0 || row === "";
340var indent = (row) => row.length - row.replace(/^ +/, "").length;
341var inRanges = (code, ranges) => ranges.some(([from, to]) => code >= from && code <= to);
342function columns(text) {
343 let total = 0;
344 for (const char of text) {
345 const code = char.codePointAt(0) ?? 0;
346 if (inRanges(code, NARROW)) total += 1;
347 else if (inRanges(code, WIDE)) total += 2;
348 else return void 0;
349 }
350 return total;
351}
352function optionRow(rows, i) {
353 const match = OPTION_ROW.exec(rows[i] ?? "");
354 if (!match) return void 0;
355 return { digit: Number(match[3]), label: match[4] ?? "", descriptionRows: 0, hasCursor: match[2] === "\u276F", isWhole: false, line: i, column: (match[1] ?? "").length + 2 };
356}
357function isWholeAt(rows, i, width) {
358 const next = rows[i + 1];
359 if (next === void 0) return false;
360 if (next === "" || RULE_ROW.test(next) || optionRow(rows, i + 1)) return true;
361 const label = columns(rows[i] ?? "");
362 const word = columns(trim(next).split(" ")[0] ?? "");
363 return width > 0 && label !== void 0 && word !== void 0 && label + 1 + word <= width - WRAP_MARGIN;
364}
365function readOptions(rows, start2, width) {
366 const options = [];
367 let column = 0;
368 let end = start2 - 1;
369 for (let i = start2; i < rows.length; i++) {
370 const row = optionRow(rows, i);
371 const last = options[options.length - 1];
372 if (row && row.digit === options.length + 1) {
373 const { column: rowColumn, ...option } = row;
374 options.push({ ...option, isWhole: isWholeAt(rows, i, width) });
375 column = rowColumn;
376 end = i;
377 } else if (!row && last && !isBlank(rows[i]) && indent(rows[i] ?? "") > column && !RULE_ROW.test(rows[i] ?? "")) {
378 last.description = [last.description, trim(rows[i] ?? "")].filter(Boolean).join(" ");
379 last.descriptionRows += 1;
380 end = i;
381 } else if (isBlank(rows[i]) && last && optionRow(rows, i + 1)?.digit === options.length + 1) {
382 continue;
383 } else break;
384 }
385 return { options, end };
386}
387function skipBlank(rows, i, most) {
388 let at = i;
389 while (at < rows.length && isBlank(rows[at]) && at - i < most) at++;
390 return isBlank(rows[at]) ? -1 : at;
391}
392function questionAt(rows, c, width) {
393 const chipRow = rows[c] ?? "";
394 const questionRows = [];
395 let i = skipBlank(rows, c + 1, 2);
396 while (i >= 0 && i < rows.length && !isBlank(rows[i]) && !optionRow(rows, i) && !RULE_ROW.test(rows[i] ?? "")) questionRows.push(trim(rows[i++] ?? ""));
397 const first = questionRows.length === 0 ? -1 : skipBlank(rows, i, 2);
398 if (first < 0 || optionRow(rows, first)?.digit !== 1) return void 0;
399 const { options: numbered, end } = readOptions(rows, first, width);
400 const last = numbered[numbered.length - 1];
401 const other = last && TYPE_SOMETHING.test(last.label) ? last : void 0;
402 let lastLine = end;
403 let chat;
404 for (let j = end + 1; j < Math.min(rows.length, end + 4); j++) {
405 if (isBlank(rows[j]) || RULE_ROW.test(rows[j] ?? "")) continue;
406 const row = optionRow(rows, j);
407 if (row && row.digit === numbered.length + 1 && row.label === "Chat about this") {
408 const { column: _column, ...option } = row;
409 chat = option;
410 lastLine = j;
411 }
412 break;
413 }
414 return {
415 kind: "question",
416 chips: (chipRow.match(/[☐☒]/gu) ?? []).length,
417 hasTabs: /[←→]/u.test(chipRow) || /✔ *Submit/u.test(chipRow),
418 header: /[☐☒] +(.*?)(?= *(?:[☐☒✔→]|$))/u.exec(chipRow)?.[1] ?? "",
419 question: questionRows.join(" "),
420 options: other ? numbered.slice(0, -1) : numbered,
421 ...other ? { other } : {},
422 isMultiSelect: other !== void 0 && !other.label.endsWith("."),
423 ...chat ? { chat } : {},
424 firstLine: c,
425 lastLine
426 };
427}
428function requestRows(rows, q) {
429 for (let i = q - 1; i >= Math.max(0, q - REQUEST_ROWS); i--) {
430 const row = rows[i] ?? "";
431 if (RULE_ROW.test(row)) return rows.slice(i + 1, q);
432 if (optionRow(rows, i) || PROMPT_ROW.test(row)) return void 0;
433 }
434 return void 0;
435}
436function permissionAt(rows, i, width) {
437 const q = isBlank(rows[i - 1]) ? i - 2 : i - 1;
438 const question = trim(rows[q] ?? "");
439 if (q < 0 || !question.endsWith("?") || optionRow(rows, q)) return void 0;
440 const { options, end } = readOptions(rows, i, width);
441 if (options.length < 2 || !options.some((option) => option.hasCursor)) return void 0;
442 const request = requestRows(rows, q);
443 return { kind: "permission", ...request ? { request } : {}, question, options, firstLine: q, lastLine: end };
444}
445function readScreen(text) {
446 const rows = text.split("\n").map(trimEnd);
447 const width = Math.max(0, ...rows.filter((row) => FULL_RULE.test(row)).map((row) => row.length));
448 const dialogs = [];
449 let lastMark = -1;
450 let promptLine = -1;
451 let promptText = "";
452 for (let i = 0; i < rows.length; i++) {
453 const row = rows[i] ?? "";
454 const option = optionRow(rows, i);
455 const claimed = dialogs.some((dialog2) => i >= dialog2.firstLine && i <= dialog2.lastLine);
456 const dialog = claimed ? void 0 : CHIP_ROW.test(row) ? questionAt(rows, i, width) : option?.digit === 1 ? permissionAt(rows, i, width) : void 0;
457 if (dialog) {
458 dialogs.push(dialog);
459 lastMark = Math.max(lastMark, dialog.lastLine);
460 }
461 if (option) lastMark = Math.max(lastMark, i);
462 const prompt = option ? void 0 : PROMPT_ROW.exec(row);
463 if (prompt) {
464 promptLine = i;
465 promptText = prompt[1] ?? "";
466 }
467 }
468 const isPromptFramed = promptLine > 0 && RULE_ROW.test(rows[promptLine - 1] ?? "") && RULE_ROW.test(rows[promptLine + 1] ?? "");
469 return { dialogs, promptLine, promptText, isPromptFramed, lastMark };
470}
471function soleDialog(view) {
472 const dialog = view.dialogs[view.dialogs.length - 1];
473 if (!dialog || view.dialogs.length !== 1) return void 0;
474 return view.promptLine > dialog.lastLine || view.lastMark > dialog.lastLine ? void 0 : dialog;
475}
476var refuse = (refusal) => ({ isMatch: false, refusal });
477function showsOption(row, expected) {
478 if (!expected || row.label !== expected.label || !row.isWhole) return false;
479 return expected.description ? row.descriptionRows === 1 && row.description === expected.description : row.descriptionRows === 0;
480}
481function matchAnswer(view, want) {
482 const dialog = soleDialog(view);
483 if (dialog?.kind !== "question") return refuse("screen-changed");
484 if (dialog.chips !== 1 || dialog.hasTabs || dialog.isMultiSelect) return refuse("multi-question");
485 if (dialog.header !== want.header || dialog.question !== want.question || !dialog.other) return refuse("screen-changed");
486 const cursors = [...dialog.options, dialog.other, ...dialog.chat ? [dialog.chat] : []].filter((row) => row.hasCursor);
487 if (cursors.length !== 1 || !dialog.options.includes(cursors[0])) return refuse("screen-changed");
488 if (dialog.options.length !== want.options.length || !dialog.options.every((row, k) => showsOption(row, want.options[k]))) return refuse("screen-changed");
489 const option = dialog.options.find((row) => row.digit === want.option);
490 if (!option || option.label !== want.label) return refuse("screen-changed");
491 return { isMatch: true, digit: option.digit };
492}
493var linesOf = (text) => text.split("\n").map(trim).filter((row) => row !== "");
494function requestOf(tool, input) {
495 if (tool !== "Bash" || input === void 0) return void 0;
496 let parsed;
497 try {
498 parsed = JSON.parse(input);
499 } catch {
500 return void 0;
501 }
502 const { command, description } = parsed ?? {};
503 if (typeof command !== "string") return void 0;
504 const commandRows = linesOf(command);
505 return commandRows.length > 0 ? { description: typeof description === "string" ? linesOf(description) : [], command: commandRows } : void 0;
506}
507var isAutoModeTip = (row) => row === 'Tip: auto mode handles these prompts for you \u2014 choose "switch to auto mode" below';
508var sameRows = (rows, expected) => rows.length === expected.length && rows.every((row, k) => row === expected[k]);
509function showsHead(rows, expected) {
510 const [title, ...rest] = rows.map(trim).filter((row) => row !== "");
511 if (title === void 0 || !BASH_TITLE.test(title)) return false;
512 return sameRows(isAutoModeTip(rest[0]) ? rest.slice(1) : rest, expected);
513}
514function boxLines(rows, width) {
515 const full = width - 1;
516 const widths = rows.map(columns);
517 if (widths.some((cells) => cells !== void 0 && cells > full)) return void 0;
518 const fills = (k) => widths[k] === full && GUTTER_ROW.test(rows[k] ?? "");
519 if (!rows.some((_, k) => fills(k))) return rows.map(trim).filter((row) => row !== "");
520 if (rows.some((row) => !isBlank(row) && !GUTTER_ROW.test(row))) return void 0;
521 const lines = [];
522 let line = "";
523 for (const [k, row] of rows.entries()) {
524 line += GUTTER_ROW.exec(row)?.[1] ?? "";
525 if (fills(k) && GUTTER_ROW.test(rows[k + 1] ?? "")) continue;
526 lines.push(trim(line));
527 line = "";
528 }
529 return lines.filter((text) => text !== "");
530}
531function boxOf(rows) {
532 const rules = rows.flatMap((row, k) => DASHED_RULE.test(row) ? [k] : []);
533 if (rules.length === 0) return "none";
534 const [open = -1, close = -1] = rules;
535 const width = columns(rows[open] ?? "");
536 if (rules.length !== 2 || width === void 0 || columns(rows[close] ?? "") !== width || rows.slice(close + 1).some((row) => !isBlank(row))) return void 0;
537 return { open, close, width };
538}
539function rawRequestRows(dialog, raw) {
540 const request = dialog.request ?? [];
541 const start2 = dialog.firstLine - request.length;
542 if (start2 < 0) return void 0;
543 const rows = raw.text.split("\n").map(trimEnd).slice(start2, dialog.firstLine + 1);
544 const cleanRow = (row) => trimEnd(raw.clean(row ?? ""));
545 if (rows.length !== request.length + 1 || !request.every((row, k) => cleanRow(rows[k]) === row) || trim(cleanRow(rows[request.length])) !== dialog.question) return void 0;
546 return rows.slice(0, request.length);
547}
548function noBoxRows(rows) {
549 const [title, ...rest] = rows.flatMap((row, k) => isBlank(row) ? [] : [k]);
550 if (title === void 0 || !BASH_TITLE.test(trim(rows[title] ?? ""))) return void 0;
551 const parts = { description: [], command: [] };
552 for (const k of isAutoModeTip(trim(rows[rest[0] ?? -1] ?? "")) ? rest.slice(1) : rest) {
553 const cells = indent(rows[k] ?? "");
554 if (cells === 1 && parts.command.length === 0) parts.description.push(k);
555 else if (cells >= 3) parts.command.push(k);
556 else return void 0;
557 }
558 return parts;
559}
560function showsRequest(dialog, expected, raw) {
561 const rows = dialog.request;
562 if (dialog.question !== "Do you want to proceed?" || rows === void 0) return false;
563 const box = boxOf(rows);
564 const rawRows = raw === void 0 ? void 0 : rawRequestRows(dialog, raw);
565 if (box === void 0 || raw !== void 0 && rawRows === void 0) return false;
566 const source = rawRows ?? rows;
567 const want = raw === void 0 ? expected.command : linesOf(raw.command);
568 if (want.length === 0) return false;
569 if (box === "none") {
570 const parts = noBoxRows(rows);
571 const at = (from) => (k) => trim(from[k] ?? "");
572 return parts !== void 0 && sameRows(parts.description.map(at(rows)), expected.description) && sameRows(parts.command.map(at(source)), want);
573 }
574 const lines = boxLines(source.slice(box.open + 1, box.close), box.width);
575 return showsHead(rows.slice(0, box.open), expected.description) && lines !== void 0 && sameRows(lines, want);
576}
577function matchPermission(view, want, raw) {
578 const dialog = soleDialog(view);
579 const expected = requestOf(want.tool, want.input);
580 if (dialog?.kind !== "permission" || !expected || !showsRequest(dialog, expected, raw)) return refuse("screen-changed");
581 if (dialog.options.filter((row) => row.hasCursor).length !== 1) return refuse("screen-changed");
582 const label = want.choice === "yes" ? "Yes" : "No";
583 const hits = dialog.options.filter((row) => row.label === label);
584 if (hits.length !== 1 || !hits[0]) return refuse("label-not-found");
585 return hits[0].isWhole ? { isMatch: true, digit: hits[0].digit } : refuse("screen-changed");
586}
587function matchPrompt(view) {
588 if (view.dialogs.length > 0 || view.promptLine < 0 || !view.isPromptFramed || view.lastMark > view.promptLine || view.promptText !== "") return "screen-changed";
589 return void 0;
590}
591var squeeze = (text) => text.replace(/\s+/gu, "");
592function inputBox(rows, promptLine, promptText) {
593 if (promptLine < 1 || !RULE_ROW.test(rows[promptLine - 1] ?? "")) return void 0;
594 const next = [];
595 for (let i = promptLine + 1; i < rows.length; i++) {
596 const row = rows[i] ?? "";
597 if (RULE_ROW.test(row)) return { text: [promptText, ...next].join("\n"), end: i };
598 next.push(row.replace(/^[ \u00a0]{1,2}/u, ""));
599 }
600 return void 0;
601}
602function matchTyped(text, typed) {
603 const view = readScreen(text);
604 if (view.dialogs.length > 0 || view.promptLine < 0) return "screen-changed";
605 const box = inputBox(text.split("\n").map(trimEnd), view.promptLine, view.promptText);
606 if (box === void 0 || view.lastMark > box.end) return "screen-changed";
607 return squeeze(box.text) === squeeze(typed) ? void 0 : "screen-changed";
608}
609
610// src/god/model/question-dialog.ts
611var MOST_OPTIONS = 8;
612var TABS_ROW = /^ *← +(.*?) +✔ Submit +→$/u;
613var CHIP = /^([☐☒]) +(\S.*)$/u;
614var REVIEW_TITLE = "Review your answers";
615var REVIEW_READY = "Ready to submit your answers?";
616var SUBMIT_ROWS = [/^❯? *1\. Submit answers$/u, /^❯? *2\. Cancel$/u];
617var MOST_BLANKS = 2;
618var refuse2 = (refusal) => ({ isMatch: false, refusal });
619var CHANGED = refuse2("screen-changed");
620var trimEnd2 = (text) => text.replace(/ +$/, "");
621var trim2 = (text) => text.replace(/^ +| +$/g, "");
622function isSteppable(questions) {
623 return questions.length >= 2 && questions.every((question) => !question.multiSelect && question.options.length >= 1 && question.options.length <= MOST_OPTIONS);
624}
625function chipsOf(row) {
626 const middle = TABS_ROW.exec(row)?.[1];
627 if (middle === void 0) return void 0;
628 const chips = middle.split(/ +(?=[☐☒])/u).map((part) => CHIP.exec(part));
629 return chips.every((chip) => chip !== null) ? chips.map((chip) => ({ isAnswered: chip[1] === "\u2612", header: chip[2] ?? "" })) : void 0;
630}
631function showsChips(row, want, answered) {
632 const chips = chipsOf(row ?? "");
633 return chips !== void 0 && chips.length === want.questions.length && chips.every((chip, k) => chip.header === want.questions[k]?.header && chip.isAnswered === k < answered);
634}
635function isWanted(rows, want) {
636 if (!isSteppable(want.questions) || want.answers.length !== want.questions.length) return false;
637 const sameAnswers = want.answers.every((answer, k) => {
638 const question = want.questions[k];
639 return question !== void 0 && answer.header === question.header && answer.question === question.text && question.options[answer.option - 1]?.label === answer.label;
640 });
641 return sameAnswers && rows.filter((row) => TABS_ROW.test(row)).length === 1;
642}
643function matchQuestion(view, rows, want, k) {
644 const dialog = soleDialog(view);
645 const question = want.questions[k];
646 const answer = want.answers[k];
647 if (dialog?.kind !== "question" || question === void 0 || answer === void 0 || dialog.isMultiSelect || dialog.other === void 0) return CHANGED;
648 if (!showsChips(rows[dialog.firstLine], want, k) || dialog.question !== question.text) return CHANGED;
649 const cursors = [...dialog.options, dialog.other, ...dialog.chat ? [dialog.chat] : []].filter((row) => row.hasCursor);
650 if (cursors.length !== 1 || !dialog.options.includes(cursors[0])) return CHANGED;
651 if (dialog.options.length !== question.options.length || !dialog.options.every((row, at) => showsOption(row, question.options[at]))) return CHANGED;
652 const option = dialog.options.find((row) => row.digit === answer.option);
653 return option !== void 0 && option.label === answer.label ? { isMatch: true, digit: option.digit } : CHANGED;
654}
655function partsFrom(rows, from) {
656 const parts = [];
657 let blanks2 = 0;
658 for (let line = from; line < rows.length; line++) {
659 const text = trim2(rows[line] ?? "");
660 blanks2 = text === "" ? blanks2 + 1 : 0;
661 if (blanks2 > MOST_BLANKS) return parts.length === 0 ? void 0 : parts;
662 if (text !== "") parts.push({ text, line });
663 }
664 return parts;
665}
666function matchReview(view, rows, want) {
667 const chipLine = rows.findIndex((row) => TABS_ROW.test(row));
668 const parts = partsFrom(rows, chipLine + 1);
669 if (chipLine < 0 || parts === void 0 || !showsChips(rows[chipLine], want, want.questions.length) || parts[0]?.text !== REVIEW_TITLE) return CHANGED;
670 let at = 1;
671 const takeText = (mark2) => {
672 const first = parts[at]?.text;
673 if (first === void 0 || !first.startsWith(`${mark2} `)) return void 0;
674 const texts = [first.slice(mark2.length + 1)];
675 for (at += 1; at < parts.length && !/^(?:●|→) /u.test(parts[at]?.text ?? "") && parts[at]?.text !== REVIEW_READY; at++) texts.push(parts[at]?.text ?? "");
676 return texts.join(" ");
677 };
678 for (const answer of want.answers) {
679 if (takeText("\u25CF") !== answer.question || takeText("\u2192") !== answer.label) return CHANGED;
680 }
681 const [ready, submit, cancel] = parts.slice(at, at + 3);
682 if (ready?.text !== REVIEW_READY || submit === void 0 || cancel === void 0 || !SUBMIT_ROWS[0].test(submit.text) || !SUBMIT_ROWS[1].test(cancel.text)) return CHANGED;
683 if (!submit.text.startsWith("\u276F") || cancel.text.startsWith("\u276F")) return CHANGED;
684 const isInside = view.dialogs.length <= 1 && view.dialogs.every((dialog) => dialog.firstLine >= chipLine && dialog.lastLine <= cancel.line);
685 if (!isInside || view.promptLine > chipLine || view.lastMark > cancel.line) return CHANGED;
686 return { isMatch: true, digit: 1 };
687}
688function matchAnswersStep(text, want, step) {
689 const rows = text.split("\n").map(trimEnd2);
690 if (!Number.isInteger(step) || step < 0 || step > want.questions.length || !isWanted(rows, want)) return CHANGED;
691 const view = readScreen(text);
692 return step < want.questions.length ? matchQuestion(view, rows, want, step) : matchReview(view, rows, want);
693}
694
695// src/redact/origins.ts
696function originsAfter(text, origins, edits, length) {
697 const next = new Int32Array(length);
698 const origin = (i) => origins ? origins[i] : i;
699 let at = 0;
700 let j = 0;
701 for (const [from, to, by] of edits) {
702 while (at < from) next[j++] = origin(at++);
703 let head = 0;
704 while (head < by.length && head < to - from && by[head] === text[from + head]) head++;
705 let tail = 0;
706 while (tail < by.length - head && tail < to - from - head && by[by.length - 1 - tail] === text[to - 1 - tail]) tail++;
707 for (let k = 0; k < by.length; k++) next[j++] = k < head ? origin(from + k) : k >= by.length - tail ? origin(to - by.length + k) : -1;
708 at = to;
709 }
710 while (at < text.length) next[j++] = origin(at++);
711 if (j !== length) throw new Error(`the edits give ${j} characters, not ${length}`);
712 return next;
713}
714function applyEdits(text, edits) {
715 let out = "";
716 let at = 0;
717 for (const [from, to, by] of edits) {
718 out += text.slice(at, from) + by;
719 at = to;
720 }
721 return out + text.slice(at);
722}
723
724// src/redact/secret-name.ts
725var secretWords = (sep) => `password|passwd|passphrase|secret[${sep}]?keys?|secret|token|api[${sep}]?keys?|access[${sep}]?keys?|private[${sep}]?keys?|authorization|auth`;
726var NAME_SEPARATORS = "_./-";
727var NAME_END_SEPARATORS = "_-";
728var KEY_SEPARATORS = " _.:/-";
729var wordAtEnd = (sep) => new RegExp(`(?:${secretWords(sep)})$`, "i");
730var [NAME_WORD, KEY_WORD] = [wordAtEnd(NAME_SEPARATORS), wordAtEnd(KEY_SEPARATORS)];
731var ALPHANUMERIC = /[A-Za-z0-9]/;
732function endsWithSecretName(name, sep, word) {
733 let end = name.length;
734 while (end > 0) {
735 if (word.test(name.slice(Math.max(0, end - 16), end))) return true;
736 let start2 = end;
737 while (start2 > 0 && ALPHANUMERIC.test(name[start2 - 1])) start2--;
738 if (start2 === end || start2 === 0 || !sep.includes(name[start2 - 1])) return false;
739 end = start2 - 1;
740 }
741 return false;
742}
743var isSecretName = (run) => endsWithSecretName(run, NAME_END_SEPARATORS, NAME_WORD);
744var endsWithSecretWord = (text) => KEY_WORD.test(text.slice(-16));
745var answers = /* @__PURE__ */ new Map();
746function isSecretKey(key) {
747 const known = answers.get(key);
748 if (known !== void 0) return known;
749 const answer = endsWithSecretName(key.trim().replace(/:$/, "").trim(), KEY_SEPARATORS, KEY_WORD);
750 if (key.length <= 256) {
751 if (answers.size >= 4096) answers.clear();
752 answers.set(key, answer);
753 }
754 return answer;
755}
756
757// src/redact/scan.ts
758var MASK = "\u2039redacted\u203A";
759var NAME_CHAR = /[A-Za-z0-9_.\/-]/;
760var SPACE = /\s/;
761var LINE_END = /[\n\r]/;
762var VALUE_CHAR = /[^\s'"‹]/;
763function quotedTail(text, end) {
764 const quote2 = text[end];
765 if (quote2 !== '"' && quote2 !== "'" || text[end - 1] !== "=" && text[end - 1] !== ":") return end;
766 let at = end + 1;
767 while (at < text.length && !LINE_END.test(text[at]) && text[at] !== quote2) at++;
768 return text[at] === quote2 ? at + 1 : at;
769}
770function lineEnd(text, at, quote2) {
771 let end = at;
772 while (end < text.length && !LINE_END.test(text[end]) && text[end] !== quote2) end++;
773 return end;
774}
775function twoWordName(text, start2, end, done) {
776 if (text[start2 - 1] !== " " || start2 - 1 <= done) return false;
777 let word = start2 - 1;
778 while (word > done && /[A-Za-z]/.test(text[word - 1])) word--;
779 return word < start2 - 1 && endsWithSecretWord(text.slice(word, end));
780}
781function skipSpaces(text, at) {
782 while (at < text.length && SPACE.test(text[at])) at++;
783 return at;
784}
785function crossesLine(text, from, to) {
786 for (let at = from; at < to; at++) if (LINE_END.test(text[at])) return true;
787 return false;
788}
789var URL_PASSWORD = "\u2039redacted\u203A@";
790function valueCharsEnd(text, at) {
791 let end = at;
792 for (; ; ) {
793 while (end < text.length && VALUE_CHAR.test(text[end])) end++;
794 if (!text.startsWith(URL_PASSWORD, end)) return end;
795 end += URL_PASSWORD.length;
796 }
797}
798var valueRunEnd = (text, at) => quotedTail(text, valueCharsEnd(text, at));
799var opensValue = (c) => c !== void 0 && (VALUE_CHAR.test(c) || c === '"' || c === "'");
800var isSeparator = (c) => c === "=" || c === ":";
801var BLANK = /[^\S\n\r]/;
802function namesSecretAt(text, end) {
803 let start2 = end;
804 while (start2 > 0 && NAME_CHAR.test(text[start2 - 1])) start2--;
805 return start2 < end && isSecretName(text.slice(start2, end));
806}
807function endOfName(text, end) {
808 while (end > 0 && BLANK.test(text[end - 1])) end--;
809 return end;
810}
811function endsWithInnerName(text, end) {
812 const at = endOfName(text, text[end - 1] === '"' || text[end - 1] === "'" ? end - 1 : end);
813 return isSeparator(text[at - 1]) && namesSecretAt(text, endOfName(text, at - 1));
814}
815function longValueAt(text, at) {
816 const from = text[at] === '"' || text[at] === "'" ? at + 1 : at;
817 let end = from;
818 while (end < from + 6 && VALUE_CHAR.test(text[end] ?? " ")) end++;
819 return end === from + 6;
820}
821function withInnerPairs(text, end) {
822 const later = [];
823 let first = -1;
824 let start2 = 0;
825 for (; ; ) {
826 let blank = end;
827 while (BLANK.test(text[blank] ?? "")) blank++;
828 const next = skipSpaces(text, end);
829 let inner = -1;
830 const after = isSeparator(text[next]) ? skipSpaces(text, next + 1) : -1;
831 if (after >= 0 && (!crossesLine(text, end, after) || namesSecretAt(text, endOfName(text, end)) && longValueAt(text, after))) inner = after;
832 else if (opensValue(text[next]) && endsWithInnerName(text, end) && (next === blank || longValueAt(text, next))) inner = next;
833 else if (isSeparator(text[end - 1]) && blank > end && opensValue(text[blank])) inner = blank;
834 if (inner < 0) break;
835 const from = text[inner] === '"' || text[inner] === "'" ? inner + 1 : inner;
836 if (crossesLine(text, end, inner)) {
837 if (first < 0) first = end;
838 else later.push([start2, end]);
839 start2 = from;
840 end = valueCharsEnd(text, from);
841 } else end = valueRunEnd(text, from);
842 }
843 if (first < 0) return { end, later };
844 later.push([start2, end]);
845 return { end: first, later };
846}
847function maskLater(text, at, pairs, mark2) {
848 let out = "";
849 for (const [from, to] of pairs.later) {
850 out += text.slice(at, from) + MASK;
851 mark2(from, to, MASK);
852 at = to;
853 }
854 return out;
855}
856var lastEnd = (pairs) => pairs.later.at(-1)?.[1] ?? pairs.end;
857function holdsInnerName(text, at, end) {
858 for (let sep = at; sep < end; sep++) if (isSeparator(text[sep]) && namesSecretAt(text, sep)) return true;
859 return false;
860}
861function maskAssignments(text, mark2) {
862 let out = "";
863 let done = 0;
864 for (let at = 0; at < text.length; at++) {
865 if (text[at] !== "=" && text[at] !== ":") continue;
866 let nameEnd = at;
867 while (nameEnd > done && SPACE.test(text[nameEnd - 1])) nameEnd--;
868 let nameStart = nameEnd;
869 while (nameStart > done && NAME_CHAR.test(text[nameStart - 1])) nameStart--;
870 if (nameStart === nameEnd || !(isSecretName(text.slice(nameStart, nameEnd)) || twoWordName(text, nameStart, nameEnd, done))) continue;
871 const value = skipSpaces(text, at + 1);
872 const opened = text[value] === '"' || text[value] === "'";
873 const later = crossesLine(text, at + 1, value);
874 const quote2 = opened && !later ? text[value] : "";
875 const from = opened && later ? value + 1 : value;
876 const close = quote2 ? lineEnd(text, value + 1, quote2) : -1;
877 const end = quote2 ? text[close] === quote2 ? close + 1 : close : later ? valueCharsEnd(text, from) : valueRunEnd(text, from);
878 if ((quote2 ? close - value - 1 : end - from) < 6) continue;
879 const pairs = withInnerPairs(text, end);
880 const keep = later ? from : crossesLine(text, nameEnd, at) ? at : nameEnd;
881 const by = later ? MASK : "=" + MASK;
882 out += text.slice(done, keep) + by;
883 mark2(keep, pairs.end, by);
884 out += maskLater(text, pairs.end, pairs, mark2);
885 done = lastEnd(pairs);
886 at = done - 1;
887 }
888 return out + text.slice(done);
889}
890var QUOTED_KEY = /(\\{0,7}["'])([^"'\\\n]+)\1([ \t]*[:=][ \t]*)/g;
891var OPEN_QUOTE = /\\{0,7}["']/y;
892var UNQUOTED_VALUES = [/[^\s'"\\,;}\]‹]{6,}/y, /[^\s'"‹]{6,}/y];
893function quotedValue(text, at) {
894 OPEN_QUOTE.lastIndex = at;
895 const quote2 = OPEN_QUOTE.exec(text)?.[0];
896 if (!quote2 || text[at + quote2.length] === "\u2039") return void 0;
897 let items = 0;
898 let k = at + quote2.length;
899 while (k < text.length && !LINE_END.test(text[k])) {
900 if (text.startsWith(quote2, k)) return items >= 6 ? { end: k + quote2.length, mask: quote2 + MASK + quote2 } : void 0;
901 k += text[k] === "\\" && text[k + 1] !== void 0 && !LINE_END.test(text[k + 1]) ? 2 : 1;
902 items++;
903 }
904 return items >= 6 ? { end: k, mask: quote2 + MASK } : void 0;
905}
906function unquotedValue(text, at) {
907 for (const value of UNQUOTED_VALUES) {
908 value.lastIndex = at;
909 if (value.exec(text)) return { end: holdsInnerName(text, at, value.lastIndex) ? valueRunEnd(text, at) : value.lastIndex, mask: MASK };
910 }
911 return void 0;
912}
913function maskQuotedPairs(text, mark2) {
914 let out = "";
915 let done = 0;
916 QUOTED_KEY.lastIndex = 0;
917 for (let m = QUOTED_KEY.exec(text); m; m = QUOTED_KEY.exec(text)) {
918 if (!isSecretKey(m[2])) continue;
919 const at = m.index + m[0].length;
920 const value = quotedValue(text, at) ?? unquotedValue(text, at);
921 if (!value) continue;
922 const pairs = withInnerPairs(text, quotedTail(text, value.end));
923 out += text.slice(done, at) + value.mask;
924 mark2(at, pairs.end, value.mask);
925 out += maskLater(text, pairs.end, pairs, mark2);
926 done = QUOTED_KEY.lastIndex = lastEnd(pairs);
927 }
928 return out + text.slice(done);
929}
930var AUTHORIZATION = /\b((?:proxy-)?authorization\\{0,7}["']?[ \t]*[:=][ \t]*\\{0,7}["']?[a-z][a-z0-9_-]*)[ \t]+([A-Za-z0-9+/._~@-]{4,})(?![A-Za-z0-9+/._~-]|@[A-Za-z0-9]|[ \t]*:|[ \t]+=|=+[ \t]*["']?[A-Za-z0-9+/._~-])={0,2}/dgi;
931var TRAILING_NAME = /[A-Za-z0-9_.\/-]*$/;
932function maskAuthorizations(text, mark2) {
933 let out = "";
934 let done = 0;
935 AUTHORIZATION.lastIndex = 0;
936 for (let m = AUTHORIZATION.exec(text); m; m = AUTHORIZATION.exec(text)) {
937 const credential = m.indices[2][1];
938 const name = TRAILING_NAME.exec(m[2])[0];
939 const pairs = isSecretName(name) ? withInnerPairs(text, credential) : { end: 0, later: [] };
940 const end = Math.max(m.index + m[0].length, pairs.end);
941 const by = `${m[1]} \u2039token\u203A`;
942 out += text.slice(done, m.index) + by;
943 mark2(m.index, end, by);
944 out += maskLater(text, end, pairs, mark2);
945 done = AUTHORIZATION.lastIndex = Math.max(end, lastEnd(pairs));
946 }
947 return out + text.slice(done);
948}
949var LOCAL_CHAR = /[A-Za-z0-9._%+-]/;
950var DOMAIN_CHAR = /[A-Za-z0-9.-]/;
951var LETTER = /[A-Za-z]/;
952function addressEnd(text, from) {
953 let end = from;
954 while (end < text.length && DOMAIN_CHAR.test(text[end])) end++;
955 for (let dot = end - 1; dot > from; dot--) {
956 if (text[dot] !== ".") continue;
957 let last = dot + 1;
958 while (last < end && LETTER.test(text[last])) last++;
959 if (last - dot > 2 && (last === text.length || !WORD_CHAR.test(text[last]))) return last;
960 }
961 return -1;
962}
963function maskEmails(text, mark2) {
964 let out = "";
965 let done = 0;
966 for (let at = 0; at < text.length; ) {
967 const boundary = WORD_CHAR.test(text[at]) !== (at > 0 && WORD_CHAR.test(text[at - 1]));
968 if (!boundary || !LOCAL_CHAR.test(text[at])) {
969 at++;
970 continue;
971 }
972 let local = at;
973 while (local < text.length && LOCAL_CHAR.test(text[local])) local++;
974 const end = text[local] === "@" ? addressEnd(text, local + 1) : -1;
975 if (end < 0) {
976 at = text[local] === "@" ? local + 1 : Math.max(local, at + 1);
977 continue;
978 }
979 out += text.slice(done, at) + "\u2039email\u203A";
980 mark2(at, end, "\u2039email\u203A");
981 done = at = end;
982 }
983 return out + text.slice(done);
984}
985var TOKEN_CHAR = /[A-Za-z0-9_-]/;
986var WORD_CHAR = /\w/;
987var runEnd = (text, from) => {
988 let at = from;
989 while (at < text.length && TOKEN_CHAR.test(text[at])) at++;
990 return at;
991};
992function maskJwts(text, mark2) {
993 let out = "";
994 let done = 0;
995 let skipTo = 0;
996 for (let at = text.indexOf("eyJ"); at >= 0; at = text.indexOf("eyJ", at + 1)) {
997 if (at < skipTo || at > 0 && WORD_CHAR.test(text[at - 1])) continue;
998 const first = runEnd(text, at + 3);
999 const second = text[first] === "." ? runEnd(text, first + 1) : first;
1000 const third = text[second] === "." && second !== first ? runEnd(text, second + 1) : second;
1001 if (first - at < 13 || second - first - 1 < 10 || text[second] !== "." || third - second - 1 < 5) {
1002 skipTo = first;
1003 continue;
1004 }
1005 out += text.slice(done, at) + "\u2039jwt\u203A";
1006 mark2(at, third, "\u2039jwt\u203A");
1007 done = skipTo = third;
1008 at = third - 1;
1009 }
1010 return out + text.slice(done);
1011}
1012
1013// src/redact/rules.ts
1014var PATTERNS = [
1015 // key material first: its body would otherwise be cut into the blobs and hashes below
1016 [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, "\u2039private-key\u203A"],
1017 // API keys / tokens (do these before generic ones). Each key rule, the JWT rule, Bearer and Authorization start at a
1018 // word start: a rule that starts inside a word splits a JWT, an address, a value or a blob (`helpdesk-…@…`,
1019 // `password=Xy9sk-…`), and the rule that would mask it whole then sees only a part of it.
1020 [/\bsk-ant-[A-Za-z0-9_-]{10,}/g, "\u2039anthropic-key\u203A"],
1021 [/\bsk-proj-[A-Za-z0-9_-]{20,}/g, "\u2039openai-project-key\u203A"],
1022 [/\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}/g, "\u2039stripe-key\u203A"],
1023 [/\bwhsec_[A-Za-z0-9]{16,}/g, "\u2039stripe-webhook-secret\u203A"],
1024 [/\bsk-[A-Za-z0-9]{20,}/g, "\u2039api-key\u203A"],
1025 [/\b(?:ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9_]{20,}/g, "\u2039github-token\u203A"],
1026 [/\bglpat-[A-Za-z0-9_-]{20,}/g, "\u2039gitlab-token\u203A"],
1027 [/\bnpm_[A-Za-z0-9]{30,}/g, "\u2039npm-token\u203A"],
1028 [/\bhf_[A-Za-z0-9]{30,}/g, "\u2039huggingface-token\u203A"],
1029 [/\bSG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/g, "\u2039sendgrid-key\u203A"],
1030 [/\bdop_v1_[A-Za-z0-9]{60,}/g, "\u2039digitalocean-token\u203A"],
1031 [/\bxox[baprs]-[A-Za-z0-9-]{10,}/g, "\u2039slack-token\u203A"],
1032 [/\bAKIA[0-9A-Z]{16}\b/g, "\u2039aws-key\u203A"],
1033 [/\bAIza[0-9A-Za-z_-]{30,}/g, "\u2039google-key\u203A"],
1034 maskJwts,
1035 [/\b[A-Fa-f0-9]{40}\b/g, "\u2039hash40\u203A"],
1036 // connection strings
1037 [/\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s'"]+/gi, "\u2039db-url\u203A"],
1038 // the password of any other URL (scheme://user:<password>@host): the scheme, the user, the host and the path stay.
1039 // The password runs to the last @ before the host, so a password that holds an @ goes whole.
1040 [/\b([a-z][a-z0-9+.-]{0,31}:\/\/[^\s:/?#@'"]*):(?!‹)[^\s/?#'"]+@/gi, "$1:\u2039redacted\u203A@"],
1041 // bearer/authorization
1042 [/\bBearer\s+[A-Za-z0-9._-]{12,}/g, "Bearer \u2039token\u203A"],
1043 // the credential after any other scheme word of an Authorization header (Basic, Token, Negotiate, …), as a scanner
1044 // so that a credential that names a secret takes its inner pair with it
1045 maskAuthorizations,
1046 // a quoted key and its value, then key=value assignments with env-style names around the secret word
1047 // (AWS_SECRET_ACCESS_KEY=, DB_PASSWORD=, _authToken=). A value a rule above already masked (‹…›) keeps its marker,
1048 // but a URL password that the URL rule masked inside a longer value goes with that value.
1049 maskQuotedPairs,
1050 maskAssignments,
1051 // email, as a scanner that reads each run once
1052 maskEmails,
1053 // long hex/base64 blobs
1054 [/\b[A-Za-z0-9+/]{60,}={0,2}\b/g, "\u2039blob\u203A"]
1055];
1056function replaceRule(text, [re, rep], mark2) {
1057 return text.replace(re, (...m) => {
1058 const by = rep.replace(/\$(\d)/g, (_, i) => String(m[Number(i)] ?? ""));
1059 const at = m[m.length - 2];
1060 mark2(at, at + m[0].length, by);
1061 return by;
1062 });
1063}
1064var applyRule = (text, rule, mark2) => typeof rule === "function" ? rule(text, mark2) : replaceRule(text, rule, mark2);
1065function scrubWithOrigins(s, home) {
1066 let text = s;
1067 let origins;
1068 const homeRules = [homeRegExp(home), homeSlugRegExp(home)].flatMap((re) => re ? [[re, "~"]] : []);
1069 for (const rule of [...PATTERNS, ...homeRules]) {
1070 const edits = [];
1071 const next = applyRule(text, rule, (from, to, by) => void edits.push([from, to, by]));
1072 if (!edits.length) continue;
1073 origins = originsAfter(text, origins, edits, next.length);
1074 text = next;
1075 }
1076 return { text, origins };
1077}
1078function homeRegExp(home) {
1079 if (home.length < 2) return null;
1080 const escaped = home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1081 return new RegExp(escaped + "(?![A-Za-z0-9_.-])", "g");
1082}
1083function homeSlugRegExp(home) {
1084 if (home.length < 2) return null;
1085 const slug = home.replace(/[^A-Za-z0-9-]/g, "-");
1086 if (slug.length < 2 || !/[A-Za-z0-9]/.test(slug)) return null;
1087 const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1088 return new RegExp(escaped + "(?![A-Za-z0-9_.])", "g");
1089}
1090
1091// src/god/sanitize.ts
1092var ESC = String.fromCharCode(27);
1093var CONTROL = "[\\u0000-\\u0008\\u000b-\\u001f\\u007f-\\u009f\\u00ad\\u034f\\u061c\\u115f\\u1160\\u17b4\\u17b5\\u180b-\\u180f\\u200b-\\u200f\\u202a-\\u202e\\u2060-\\u2064\\u2066-\\u206f\\u3164\\ufe00-\\ufe0f\\ufeff\\uffa0\\ufff9-\\ufffb]|\\udb40[\\udc00-\\udc7f\\udd00-\\uddef]";
1094var CSI = `(?:${ESC}\\[|\\u009b)(?:[\\u0030-\\u003f]+[\\u0020-\\u002f]*|[\\u0021-\\u002f]*)[\\u0040-\\u007e]`;
1095var STRING_TEXT = "[^\\u0000-\\u001f\\u007f-\\u009f]*";
1096var STRING_END = `(?:\\u0007|${ESC}\\\\|\\u009c)`;
1097var STRING = `${ESC}[\\]PX^_]${STRING_TEXT}${STRING_END}?|[\\u0090\\u0098\\u009d-\\u009f]${STRING_TEXT}${STRING_END}`;
1098var ESCAPE = `${ESC}[\\u0021-\\u002f]*[\\u0030-\\u007e]`;
1099var SEQUENCES = new RegExp(`(?:${CSI}|${STRING}|${ESCAPE}|${CONTROL})+`, "g");
1100var CONTROLS = new RegExp(`(?:${CONTROL})+`, "g");
1101var C0_INSIDE = "[\\u0000-\\u0009\\u000b-\\u0017\\u0019\\u001c-\\u001f]";
1102var LOOSE_STRING = `(?:${ESC}[\\]PX^_]|[\\u0090\\u0098\\u009d-\\u009f])${STRING_TEXT}${STRING_END}?`;
1103function looseSequences(c0Inside, final) {
1104 const inside = c0Inside ? `|${C0_INSIDE}` : "";
1105 const csi = `(?:${ESC}\\[|\\u009b)(?:[\\u0030-\\u003f]${inside})*(?:[\\u0020-\\u002f]${inside})*${final ? "[\\u0040-\\u007e]?" : ""}`;
1106 const escape = `${ESC}(?:[\\u0020-\\u002f]${inside})*${final ? "[\\u0030-\\u007e]?" : ""}`;
1107 return new RegExp(`(?:${csi}|${LOOSE_STRING}|${escape}|${CONTROL})+`, "g");
1108}
1109var LOOSE = [true, false].flatMap((c0Inside) => [true, false].map((final) => looseSequences(c0Inside, final)));
1110var SEQUENCE_START = new RegExp(`[${ESC}\\u0090\\u0098\\u009b\\u009d-\\u009f]`);
1111var NARROW_CONTROLS = new RegExp("[\\u0000-\\u0008\\u000b-\\u001f\\u007f-\\u009f\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069]+", "g");
1112var REDACTED = "\u2039redacted\u203A";
1113function spaceBetween(text, at, run) {
1114 return at > 0 && at + run.length < text.length && !/\s/.test(text[at - 1]) && !/\s/.test(text[at + run.length]) ? " " : "";
1115}
1116function removeControls(text) {
1117 return text.replace(SEQUENCES, (run, at) => spaceBetween(text, at, run));
1118}
1119function cutsOf(text, pattern, put) {
1120 return Array.from(text.matchAll(pattern), (m) => [m.index, m.index + m[0].length, put(m.index, m[0])]);
1121}
1122function readingOf(text, cuts) {
1123 const read7 = applyEdits(text, cuts);
1124 return { text: read7, origins: originsAfter(text, void 0, cuts, read7.length) };
1125}
1126var sameCuts = (a, b) => a.length === b.length && a.every(([from, to, by], i) => from === b[i][0] && to === b[i][1] && by === b[i][2]);
1127function readings(text) {
1128 const patterns = [CONTROLS, ...SEQUENCE_START.test(text) ? LOOSE : []];
1129 const all = [...patterns.flatMap((pattern) => [cutsOf(text, pattern, () => ""), cutsOf(text, pattern, () => " ")]), cutsOf(text, NARROW_CONTROLS, () => "")];
1130 return all.filter((cuts, i) => !all.slice(0, i).some((seen) => sameCuts(seen, cuts))).map((cuts) => readingOf(text, cuts));
1131}
1132function markHidden(reading, home, hidden) {
1133 const { origins } = scrubWithOrigins(reading.text, home);
1134 if (!origins) return;
1135 const kept = new Uint8Array(reading.text.length);
1136 for (const at of origins) if (at >= 0) kept[at] = 1;
1137 for (let i = 0; i < kept.length; i++) if (!kept[i] && reading.origins[i] >= 0) hidden[reading.origins[i]] = 1;
1138}
1139var MARKER_OR_RUN = /‹[^›]*›|[A-Za-z0-9]+/g;
1140function hideIn(text, isHidden) {
1141 const hide = new Uint8Array(text.length);
1142 for (let at = 0; at < text.length; at++) if (isHidden(at) && !/\s/.test(text[at])) hide[at] = 1;
1143 if (!hide.includes(1)) return text;
1144 for (const m of text.matchAll(MARKER_OR_RUN)) if (hide.subarray(m.index, m.index + m[0].length).includes(1)) hide.fill(1, m.index, m.index + m[0].length);
1145 let out = "";
1146 for (let at = 0; at < text.length; ) {
1147 let end = at;
1148 while (end < text.length && hide[end] === hide[at]) end++;
1149 out += hide[at] ? REDACTED : text.slice(at, end);
1150 at = end;
1151 }
1152 return out;
1153}
1154function cleanControls(text, home) {
1155 const pane = readingOf(text, cutsOf(text, SEQUENCES, (at, run) => spaceBetween(text, at, run)));
1156 const { text: out, origins } = scrubWithOrigins(pane.text, home);
1157 if (pane.text === text) return out;
1158 const hidden = new Uint8Array(text.length);
1159 for (const reading of readings(text)) markHidden(reading, home, hidden);
1160 return hideIn(out, (at) => {
1161 const from = origins ? origins[at] : at;
1162 return from >= 0 && pane.origins[from] >= 0 && hidden[pane.origins[from]] === 1;
1163 });
1164}
1165var MAX_READ_CHARS = 262144;
1166var MAX_SHOW_CHARS = 65536;
1167var CUT_CHAR = /[\s"'`,;(){}[\]<>]/;
1168function cutBefore(text, limit) {
1169 let at = limit;
1170 while (at > 0 && !CUT_CHAR.test(text[at])) at--;
1171 return at;
1172}
1173var CHECK_CHARS = 4096;
1174var MOVE_CHARS = 2 * MAX_READ_CHARS;
1175function readCut(text, clean) {
1176 let at = cutBefore(text, MAX_READ_CHARS);
1177 const wide = clean(text.slice(0, at + CHECK_CHARS));
1178 let kept = clean(text.slice(0, at));
1179 for (let moved = 0; at > 0 && !wide.startsWith(kept); moved += at) {
1180 let same = 0;
1181 while (kept[same] === wide[same]) same++;
1182 const from = at - (kept.length - same);
1183 const asItCameIn = from >= 0 && text.slice(from, at) === kept.slice(same);
1184 at = cutBefore(text, asItCameIn ? from : Math.floor(at / 2));
1185 if (moved + at > MOVE_CHARS) at = 0;
1186 kept = clean(text.slice(0, at));
1187 }
1188 return { at, kept };
1189}
1190function shown(cleaned, lost) {
1191 if (cleaned.length <= MAX_SHOW_CHARS && lost === 0) return cleaned;
1192 let at = Math.min(cleaned.length, MAX_SHOW_CHARS);
1193 const open = cleaned.lastIndexOf("\u2039", at - 1);
1194 if (open >= 0 && cleaned.indexOf("\u203A", open) >= at) at = open;
1195 if (at > 0 && /[\ud800-\udbff]/.test(cleaned[at - 1])) at--;
1196 return `${cleaned.slice(0, at)}\u2026+${cleaned.length - at + lost} chars`;
1197}
1198var CACHE_ENTRIES = 4096;
1199var CACHE_CHARS = 8388608;
1200var cache = /* @__PURE__ */ new Map();types/index.d.ts 51 lines1// The engine state contract of the orangu-god plugin: the names and the JSON shapes of the values that its pane
2// keeps in `$.state`. The engine validator reads the names here. The code of the plugin (src/god/state.tsx) gives
3// each value its full type, and no other plugin depends on these values.
4
5/** The board snapshot of the god pane, as plain JSON. */
6export type OranguGodBoard = { readonly [field: string]: unknown }
7
8/** 1 value of the key path (an intent, an outcome, the check after a send), as plain JSON. */
9export type OranguGodDeliverValue = { readonly [field: string]: unknown }
10
11/** The Recap of 1 session, as plain JSON: running, answered with its model text and tokens, or failed with its reason. */
12export type OranguGodRecap = { readonly [field: string]: unknown }
13
14declare module 'claude-code' {
15 interface PluginState {
16 /** the values of the god pane in the engine state */
17 'orangu-god': {
18 /** the last board snapshot, null before the first one */
19 board: OranguGodBoard | null
20 /** the key of the selected row, '' for none */
21 selected: string
22 /** the keys of the groups that the person opened */
23 openGroups: readonly string[]
24 /** the color set of the session theme */
25 theme: 'dark' | 'light' | 'theme-keys'
26 /** the open tab of the detail, the first line of its content, and whether the narrow pane shows the detail */
27 detail: { tab: 'now' | 'history' | 'agents' | 'screen' | 'recap'; scroll: number; isOpen: boolean }
28 /** the screen state of the Screen tab: the session, the last good read of its screen, and whether the newest read failed */
29 screen: { sessionId: string; read?: { sessionId: string; workspaceRef: string; surfaceRef: string; text: string; readAt: number }; isFailed: boolean } | null
30 /** the note of the last j that did not jump: the session and the reason */
31 jump: { sessionId: string; reason: 'no-tab' | 'failed' } | null
32 /** the key path: the intent of the confirm bar and its scroll, the draft of r or m, the picks so far in a dialog with 2 or more questions, the last outcome, the check of the refreshes after a send, and whether the keys go out now */
33 deliver: {
34 intent?: OranguGodDeliverValue
35 scroll: number
36 draft?: { kind: 'prompt' | 'peer'; target: { sessionId: string; name: string }; lines: readonly string[] }
37 picks?: { sessionId: string; toolUseId: string; digits: readonly number[] }
38 outcome?: OranguGodDeliverValue
39 check?: OranguGodDeliverValue
40 isSending?: boolean
41 }
42 /** the times of the last alert and the last done tone of each session in the last 5 min */
43 alerts: { alertAt: { readonly [sessionId: string]: number }; doneAt: { readonly [sessionId: string]: number } }
44 /** the last Recap, of any session, null before the first x */
45 recap: OranguGodRecap | null
46 /** the working sessions of each minute of the last 30 min, oldest first, for the sparkline */
47 spark: { minutes: readonly { minute: number; ids: readonly string[] }[] }
48 }
49 }
50}
51