SLOPSHOPPER

orangu-god

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

newpaneguardcommandstatustool
★ 2v0.1.0MITupdated 2026-10-24NissanOhana/orangu/god
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · orangu-god
│ ┃ orangu god ✕ › fix the failing auth test and add an audit log call │ ┃ ◉ orangu god keys: prompt · ctrl+x tab: │ ┃ pane ⏺ Read(src/auth.ts) │ ┃ ──────────────────────────────────────────── ⎿ Read 6 lines │ ┃ ──────────── ⏺ Update(src/auth.ts) │ ┃ Reading sessions. ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /god │ ⎿ orangu-god: The god pane is open. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · orangu god
◉ orangu god keys: prompt · ctrl+x tab: pane ──────────────────────────────────────────────────────── Reading sessions.
README

<img src="https://raw.githubusercontent.com/NissanOhana/orangu/main/design/brand/mascot-main-transparent.png" width="140" alt="orangu" />

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 youWhat orangu shows
"compacted"which turns filled the context window, and with what
a subagent's final answerits full tree: tools, tokens, time, errors
that a skill or MCP server is installedwhether it ever fired, and what it weighs in context
nothing about repetitionthe 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.

Get started

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.

The improve loop

  1. Observe. orangu parses, redacts, counts, and ranks each session against 44 deterministic rules. It uses no model.
  2. See. The report and orangu serve show quality, time, and tokens as separate axes, with the evidence behind each finding.
  3. Propose. /orangu:improve or /orangu:harness drafts one proposal for each change, in one of nine change classes. A proposal changes no file.
  4. Apply. You approve each change by its id. Then /orangu:apply <id> makes it and records which checks ran.
  5. Verify. orangu compares your later sessions in the same workspace with the sessions before the change. The change counts as verified only when they beat the earlier sessions beyond chance. No model is in the measurement. /orangu:improve --verify <id> shows the comparison and records a final verdict.
  6. Remember. orangu keeps each verdict and each reason that you pick. The next proposal reads them. 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.

Skills

/plugin marketplace add NissanOhana/orangu
/plugin install orangu
SkillWhat it does
/orangu:analyzeExplain what happened in one session, finished or still running, from local evidence. Open the report of the current session
/orangu:improveStart here. Find the next change, draft one reviewable proposal, and check each change that you apply
/orangu:interviewResearch outside options for one suggestion, then interview you before its proposal or a second opinion (/orangu:interview <sg_id>)
/orangu:applyApply one reviewed session or repo proposal and save the receipt
/orangu:harnessReview the whole harness: CLAUDE.md, memory, skills, agents, hooks, MCP. A rule that did not hold gets a hook, not another note
/orangu:show-meTurn 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:feedbackSend 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.

Not a wrapper

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.

Design principles

  • No model in the measurement path. Counting, reconciling, and ranking are plain code. Skills only interpret the result.
  • Zero network from reports. Every report ships 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.
  • Redacted by default. orangu scrubs secrets. It omits prompt text unless you ask with --include-text.
  • Same transcript in, byte-identical analysis out. The analyzer uses no clock, no randomness, and no network.
  • Components, never a composite score. Quality, time, and tokens stay separate. orangu never ranks people.
  • Tokens only. orangu counts input, cache-read, cache-write, and output tokens. It never converts them into another unit.

Where it sits

Raw transcriptsContain the record, but leave the reading and synthesis to you.
Usage counters (ccusage and friends)Sum tokens across time and sources.
Instrumented trace platformsCapture traces and evaluations from applications that you instrument.
oranguReads the transcript you already have, explains it, and turns findings into reviewed changes with receipts.

Privacy and support

  • Redaction is on by default. Add --strip-paths before you share a report. Review every export first.
  • orangu counts unknown transcript records and shows them in Coverage. They never crash the analysis.
  • Feedback stays on localhost until you review the exact title and body and open GitHub's issue composer yourself.

Read the privacy model, supported limits, and security policy before sharing a report or JSON export. Are you an LLM? Read llms.txt.

Documentation

GuidePurpose
Documentation indexAll user, integration, design, and contributor references
UsageCLI commands, app surfaces, inputs, and limits
DeterminismLocal evidence, AI-skill authority, and lifecycle rules
ArchitecturePipeline, modules, build, and correctness gates
Data contractsAnalysis, evidence, app, proposal, and receipt shapes
Beta feedbackLocalhost intake, exact-review consent, and GitHub handoff
ContributingDevelopment workflow and project layout

Develop

git clone https://github.com/NissanOhana/orangu && cd orangu
npm ci && npm run verify:release

License

MIT © Nissan Ohana. The mascot is CC0.

Source 2 files
hooks/god.mjs 6042 lines
1// 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 lines
1// 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