@agentguard-run/burn 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,7 @@ const claude_transcript_1 = require("../history/claude-transcript");
27
27
  const reservations_1 = require("../state/reservations");
28
28
  const session_1 = require("../state/session");
29
29
  const defaults_1 = require("../defaults");
30
+ const render_1 = require("../replay/render");
30
31
  const SPAWN_TOOLS = new Set(['Agent', 'Task']);
31
32
  function sessionFile(home, sessionId) {
32
33
  return (0, node_path_1.join)(home, 'sessions', `${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
@@ -145,10 +146,10 @@ function handlePreToolUse(input, home, now = Date.now()) {
145
146
  }
146
147
  return { continue: true, suppressOutput: true };
147
148
  }
148
- function buildDenyReason(report, reservation) {
149
- const lead = report.findings.find((f) => f.verdict === 'STOP')?.summary ?? `Fan-out ceiling reached (${reservation.effectiveSpawns} effective spawns).`;
150
- const rx = report.prescriptions.slice(0, 3).map((p, i) => `${i + 1}. ${p}`).join(' ');
151
- return `AgentGuard STOP: blocked this agent spawn. ${lead} ${rx} Override once with: agentguard-burn resume --once --reason "..."`;
149
+ function buildDenyReason(report, _reservation) {
150
+ // Claude Code shows this reason to the user. A box reads as an alarm; a
151
+ // sentence reads as a log line. Colour is off: the host decides rendering.
152
+ return (0, render_1.renderStop)(report, { colour: false });
152
153
  }
153
154
  function deny(reason) {
154
155
  return {
@@ -1,11 +1,17 @@
1
1
  /**
2
- * Terminal rendering for replay. This output is the marketing artifact, so it
3
- * has to survive a screenshot: pure ASCII box drawing, ANSI colour that
4
- * degrades cleanly, nothing that depends on a font.
2
+ * Terminal rendering. This output is the distribution artifact, so it is
3
+ * designed to be screenshotted: one giant number, a curve you can see the
4
+ * runaway in, one line worth quoting. Pure ASCII/ANSI, degrades cleanly.
5
5
  */
6
+ import type { BurnReport } from '../types';
6
7
  import type { ReplaySummary, SessionReplay } from './simulate';
8
+ export declare function sparkline(curve: number[], stopAt: number | null, on: boolean): string;
9
+ export declare function comparison(tokens: number): string;
7
10
  export declare function renderReplay(summary: ReplaySummary, opts?: {
8
11
  colour?: boolean;
9
12
  top?: number;
10
13
  }): string;
14
+ export declare function renderStop(report: BurnReport, opts?: {
15
+ colour?: boolean;
16
+ }): string;
11
17
  export declare function renderSessionRow(s: SessionReplay): string;
@@ -1,11 +1,14 @@
1
1
  "use strict";
2
2
  /**
3
- * Terminal rendering for replay. This output is the marketing artifact, so it
4
- * has to survive a screenshot: pure ASCII box drawing, ANSI colour that
5
- * degrades cleanly, nothing that depends on a font.
3
+ * Terminal rendering. This output is the distribution artifact, so it is
4
+ * designed to be screenshotted: one giant number, a curve you can see the
5
+ * runaway in, one line worth quoting. Pure ASCII/ANSI, degrades cleanly.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.sparkline = sparkline;
9
+ exports.comparison = comparison;
8
10
  exports.renderReplay = renderReplay;
11
+ exports.renderStop = renderStop;
9
12
  exports.renderSessionRow = renderSessionRow;
10
13
  const evaluate_1 = require("../detectors/evaluate");
11
14
  const C = {
@@ -16,74 +19,201 @@ const C = {
16
19
  yellow: '\x1b[33m',
17
20
  green: '\x1b[32m',
18
21
  cyan: '\x1b[36m',
22
+ magenta: '\x1b[35m',
23
+ bgRed: '\x1b[41m',
24
+ white: '\x1b[97m',
19
25
  };
20
- function colour(enabled, code, text) {
21
- return enabled ? `${code}${text}${C.reset}` : text;
26
+ const W = 72;
27
+ function paint(on, code, text) {
28
+ return on ? `${code}${text}${C.reset}` : text;
22
29
  }
23
- const W = 70;
24
- function line(text = '') {
25
- const clean = text.replace(/\x1b\[[0-9;]*m/g, '');
26
- const pad = Math.max(0, W - 2 - clean.length);
30
+ function strip(text) {
31
+ return text.replace(/\x1b\[[0-9;]*m/g, '');
32
+ }
33
+ function row(text = '') {
34
+ const pad = Math.max(0, W - 2 - strip(text).length);
27
35
  return `│ ${text}${' '.repeat(pad)} │`;
28
36
  }
29
37
  const TOP = `╭${'─'.repeat(W)}╮`;
30
38
  const MID = `├${'─'.repeat(W)}┤`;
31
39
  const BOT = `╰${'─'.repeat(W)}╯`;
32
- /** API-list equivalent. Labelled as a scenario, never as the user's bill. */
40
+ // ---------------------------------------------------------------------------
41
+ // Block digits. Five rows, three columns each, for 0-9 . % B K M and space.
42
+ // ---------------------------------------------------------------------------
43
+ const GLYPHS = {
44
+ '0': ['█▀▀█', '█ █', '█ █', '█ █', '▀▀▀▀'],
45
+ '1': [' ▀█', ' █', ' █', ' █', ' ▀'],
46
+ '2': ['▀▀▀█', ' █', '█▀▀▀', '█ ', '▀▀▀▀'],
47
+ '3': ['▀▀▀█', ' █', ' ▀▀█', ' █', '▀▀▀▀'],
48
+ '4': ['█ █', '█ █', '▀▀▀█', ' █', ' ▀'],
49
+ '5': ['█▀▀▀', '█ ', '▀▀▀█', ' █', '▀▀▀▀'],
50
+ '6': ['█▀▀▀', '█ ', '█▀▀█', '█ █', '▀▀▀▀'],
51
+ '7': ['▀▀▀█', ' █', ' █', ' █', ' ▀'],
52
+ '8': ['█▀▀█', '█ █', '█▀▀█', '█ █', '▀▀▀▀'],
53
+ '9': ['█▀▀█', '█ █', '▀▀▀█', ' █', '▀▀▀▀'],
54
+ '.': [' ', ' ', ' ', ' ', ' ▀ '],
55
+ '%': ['▀▀ █', ' █ ', ' █ ', ' █ ', '█ ▀▀'],
56
+ B: ['█▀▀▄', '█ █', '█▀▀▄', '█ █', '▀▀▀ '],
57
+ M: ['█▄ ▄█', '█ ▀ █', '█ █', '█ █', '▀ ▀'],
58
+ K: ['█ █', '█ █ ', '██ ', '█ █ ', '▀ ▀'],
59
+ ' ': [' ', ' ', ' ', ' ', ' '],
60
+ };
61
+ function blockDigits(text) {
62
+ const rows = ['', '', '', '', ''];
63
+ for (const ch of text) {
64
+ const g = GLYPHS[ch] ?? GLYPHS[' '];
65
+ for (let r = 0; r < 5; r++)
66
+ rows[r] += g[r] + ' ';
67
+ }
68
+ return rows.map((r) => r.trimEnd());
69
+ }
70
+ // ---------------------------------------------------------------------------
71
+ // Sparkline with the STOP point marked.
72
+ // ---------------------------------------------------------------------------
73
+ const BARS = '▁▂▃▄▅▆▇█';
74
+ function sparkline(curve, stopAt, on) {
75
+ if (curve.length === 0)
76
+ return '';
77
+ let out = '';
78
+ for (let i = 0; i < curve.length; i++) {
79
+ const level = Math.min(BARS.length - 1, Math.max(0, Math.round(curve[i] * (BARS.length - 1))));
80
+ const bar = BARS[level];
81
+ if (stopAt !== null && i >= stopAt) {
82
+ // Everything after the stop is the tail enforcement would have cut.
83
+ out += paint(on, C.red, i === stopAt ? '┃' : bar);
84
+ }
85
+ else {
86
+ out += paint(on, C.green, bar);
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+ // ---------------------------------------------------------------------------
92
+ // The quotable comparison. Tokens to something a human can picture.
93
+ // ---------------------------------------------------------------------------
94
+ const COMPARISONS = [
95
+ { tokens: 7_000_000_000, label: 'all of English Wikipedia' },
96
+ { tokens: 1_200_000_000, label: 'the entire Harry Potter series, 1,000 times' },
97
+ { tokens: 200_000_000, label: 'the complete works of Shakespeare, 200 times' },
98
+ { tokens: 40_000_000, label: 'the King James Bible, 50 times' },
99
+ { tokens: 130_000, label: 'Slaughterhouse-Five' },
100
+ ];
101
+ function comparison(tokens) {
102
+ for (const c of COMPARISONS) {
103
+ const times = tokens / c.tokens;
104
+ if (times >= 1) {
105
+ const n = times >= 10 ? Math.round(times) : Math.round(times * 10) / 10;
106
+ return `≈ ${c.label}${n > 1 ? `, ${n}×` : ''}`;
107
+ }
108
+ }
109
+ return '';
110
+ }
111
+ // ---------------------------------------------------------------------------
112
+ // Replay
113
+ // ---------------------------------------------------------------------------
33
114
  function scenarioUsd(tokens, cacheShare) {
34
115
  const cached = tokens * cacheShare;
35
116
  return (cached * 0.3 + (tokens - cached) * 5) / 1e6;
36
117
  }
37
118
  function renderReplay(summary, opts = {}) {
38
119
  const on = opts.colour ?? Boolean(process.stdout.isTTY);
39
- const top = opts.top ?? 8;
120
+ const top = opts.top ?? 6;
40
121
  const out = [];
41
- const cacheShare = summary.sessions.length
42
- ? summary.sessions.reduce((s, r) => s + r.cacheReadRatio * r.totalTokens, 0) / Math.max(summary.totalTokens, 1)
122
+ const cacheShare = summary.totalTokens
123
+ ? summary.sessions.reduce((s, r) => s + r.cacheReadRatio * r.totalTokens, 0) / summary.totalTokens
43
124
  : 0;
125
+ const share = Math.round(summary.catchableShare * 100);
44
126
  out.push(TOP);
45
- out.push(line(colour(on, C.bold, 'AGENTGUARD REPLAY')));
46
- out.push(line(colour(on, C.dim, 'Your agent history, before enforcement was installed')));
127
+ out.push(row(paint(on, C.bold + C.magenta, 'AGENTGUARD') + ' ' + paint(on, C.dim, 'replay · your agent history, before enforcement')));
47
128
  out.push(MID);
48
- out.push(line(`${summary.sessions.length} sessions ${summary.totalSpawns} spawns ${(0, evaluate_1.fmt)(summary.totalTokens)} tokens observed`));
49
- out.push(line(colour(on, C.dim, `${Math.round(cacheShare * 100)}% cache-read. Shown as explanation; it decided nothing.`)));
129
+ // Hero: the one number. Rendered huge.
130
+ out.push(row(''));
131
+ for (const line of blockDigits(`${share}%`))
132
+ out.push(row(' ' + paint(on, C.bold + C.green, line)));
133
+ out.push(row(''));
134
+ out.push(row(' ' + paint(on, C.bold, 'of everything you burned came after a point it would have blocked')));
135
+ out.push(row(''));
136
+ // Three supporting numbers, card-style.
137
+ const cards = [
138
+ { n: (0, evaluate_1.fmt)(summary.totalTokens), l: 'observed', c: C.white },
139
+ { n: (0, evaluate_1.fmt)(summary.catchableTail), l: 'after STOP', c: C.red },
140
+ { n: `$${Math.round(scenarioUsd(summary.catchableTail, cacheShare)).toLocaleString()}`, l: 'API-list, scenario', c: C.yellow },
141
+ ];
142
+ const COL = 20;
143
+ out.push(row(' ' + cards.map((k) => paint(on, C.bold + k.c, k.n.padEnd(COL))).join('')));
144
+ out.push(row(' ' + cards.map((k) => paint(on, C.dim, k.l.padEnd(COL))).join('')));
145
+ const comp = comparison(summary.catchableTail);
146
+ if (comp)
147
+ out.push(row(' ' + paint(on, C.dim, comp)));
50
148
  out.push(MID);
51
- out.push(line(colour(on, C.bold, 'WHAT ENFORCEMENT WOULD HAVE INTERCEPTED')));
52
- out.push(line(''));
53
- const share = Math.round(summary.catchableShare * 100);
54
- out.push(line(`${colour(on, C.bold, (0, evaluate_1.fmt)(summary.catchableTail))} tokens observed after the first STOP boundary`));
55
- out.push(line(`${share}% of everything you burned ≈ $${Math.round(scenarioUsd(summary.catchableTail, cacheShare)).toLocaleString()} at API list, as a scenario`));
56
- out.push(line(''));
57
- out.push(line(colour(on, C.dim, 'Upper bound. It assumes you would not have overridden or restarted.')));
58
- out.push(MID);
59
- out.push(line(`${colour(on, C.red, `${summary.stops} STOP`)} ${colour(on, C.yellow, `${summary.warns} WARN`)} ${colour(on, C.green, `${summary.clean} clean`)}`));
149
+ out.push(row(`${paint(on, C.bold + C.red, `${summary.stops} STOP`)} ${paint(on, C.bold + C.yellow, `${summary.warns} WARN`)} ${paint(on, C.bold + C.green, `${summary.clean} clean`)}` +
150
+ paint(on, C.dim, ` across ${summary.sessions.length} sessions, ${summary.totalSpawns} spawns`)));
60
151
  out.push(MID);
61
152
  for (const s of summary.sessions.slice(0, top)) {
62
- out.push(line(colour(on, C.bold, `session ${s.sessionId.slice(0, 8)}`) + ` ${(0, evaluate_1.fmt)(s.totalTokens)} tokens · ${s.spawns} spawns · depth ${s.maxDepth}`));
63
- if (s.fanoutStop) {
64
- out.push(line(` ${colour(on, C.red, 'FAN-OUT STOP')} before spawn ${s.fanoutStop.atSpawn} at ${(0, evaluate_1.fmt)(s.fanoutStop.tokensAtStop)}`));
65
- }
66
- if (s.sustainedStop) {
67
- out.push(line(` ${colour(on, C.red, 'SUSTAINED STOP')} near ${(0, evaluate_1.fmt)(s.sustainedStop.tokensAtStop)}`));
68
- }
69
- if (!s.fanoutStop && !s.sustainedStop) {
70
- if (s.firstWarn)
71
- out.push(line(` ${colour(on, C.yellow, 'WARN')} (${s.firstWarn.detector}) at ${(0, evaluate_1.fmt)(s.firstWarn.tokensAt)} · no stop`));
72
- else
73
- out.push(line(` ${colour(on, C.green, 'clean')}`));
74
- }
75
- if (s.catchableTail > 0) {
76
- out.push(line(` observed tail after stop: ${colour(on, C.bold, (0, evaluate_1.fmt)(s.catchableTail))}`));
77
- }
153
+ const tag = s.fanoutStop ? paint(on, C.red, 'FAN-OUT STOP') : s.sustainedStop ? paint(on, C.red, 'SUSTAINED STOP') : s.firstWarn ? paint(on, C.yellow, 'WARN') : paint(on, C.green, 'clean');
154
+ out.push(row(`${sparkline(s.curve, s.stopAtIndex, on)} ${tag}`));
155
+ const detail = s.fanoutStop
156
+ ? `before spawn ${s.fanoutStop.atSpawn}, tail ${(0, evaluate_1.fmt)(s.catchableTail)}`
157
+ : s.sustainedStop
158
+ ? `near ${(0, evaluate_1.fmt)(s.sustainedStop.tokensAtStop)}, tail ${(0, evaluate_1.fmt)(s.catchableTail)}`
159
+ : `${(0, evaluate_1.fmt)(s.totalTokens)} · ${s.spawns} spawns`;
160
+ out.push(row(paint(on, C.dim, `${s.sessionId.slice(0, 8)} ${(0, evaluate_1.fmt)(s.totalTokens).padStart(6)} · ${String(s.spawns).padStart(3)} spawns ${detail}`)));
78
161
  }
79
162
  if (summary.sessions.length > top) {
80
- out.push(line(colour(on, C.dim, `… ${summary.sessions.length - top} more sessions below the warning line`)));
163
+ out.push(row(paint(on, C.dim, `… ${summary.sessions.length - top} more, all clean`)));
81
164
  }
82
165
  out.push(MID);
83
- out.push(line(colour(on, C.cyan, 'Next: agentguard-burn init (shadow mode; blocks nothing yet)')));
166
+ out.push(row(paint(on, C.dim, 'Upper bound; assumes no override or restart. Nothing left this machine.')));
167
+ out.push(row(paint(on, C.cyan, 'next agentguard-burn init') + paint(on, C.dim, ' shadow mode; blocks nothing until you say so')));
84
168
  out.push(BOT);
85
169
  return out.join('\n');
86
170
  }
171
+ // ---------------------------------------------------------------------------
172
+ // The mid-session STOP box. What Claude shows the user when a spawn is denied.
173
+ // ---------------------------------------------------------------------------
174
+ function renderStop(report, opts = {}) {
175
+ const on = opts.colour ?? false;
176
+ const lead = report.findings.find((f) => f.verdict === 'STOP')?.summary ?? 'Fan-out ceiling reached.';
177
+ const w = 64;
178
+ const bar = '━'.repeat(w);
179
+ const lines = [];
180
+ lines.push(paint(on, C.red, `┏${bar}┓`));
181
+ lines.push(paint(on, C.red, '┃ ') + paint(on, C.bold + C.bgRed + C.white, ' AGENTGUARD STOP ') + paint(on, C.bold, ' agent spawn blocked') + paint(on, C.red, ' '.repeat(w - 40) + '┃'));
182
+ lines.push(paint(on, C.red, `┣${bar}┫`));
183
+ lines.push(paint(on, C.red, '┃ ') + lead.padEnd(w - 1).slice(0, w - 1) + paint(on, C.red, '┃'));
184
+ lines.push(paint(on, C.red, '┃ ') + paint(on, C.dim, `${(0, evaluate_1.fmt)(report.totals.tokens)} tokens · ${report.totals.spawns} spawns · depth ${report.totals.maxDepth}`.padEnd(w - 1)) + paint(on, C.red, '┃'));
185
+ lines.push(paint(on, C.red, `┣${bar}┫`));
186
+ lines.push(paint(on, C.red, '┃ ') + paint(on, C.bold, 'DO NOW'.padEnd(w - 1)) + paint(on, C.red, '┃'));
187
+ report.prescriptions.slice(0, 3).forEach((p, i) => {
188
+ const wrapped = wrap(`${i + 1}. ${p}`, w - 2, ' ');
189
+ for (const line of wrapped) {
190
+ lines.push(paint(on, C.red, '┃ ') + line.padEnd(w - 1) + paint(on, C.red, '┃'));
191
+ }
192
+ });
193
+ lines.push(paint(on, C.red, `┣${bar}┫`));
194
+ lines.push(paint(on, C.red, '┃ ') + paint(on, C.dim, 'override once: agentguard-burn resume --once --reason "..."'.padEnd(w - 1)) + paint(on, C.red, '┃'));
195
+ lines.push(paint(on, C.red, `┗${bar}┛`));
196
+ return lines.join('\n');
197
+ }
198
+ /** Word-aware wrap. Continuation lines are indented. */
199
+ function wrap(text, width, indent) {
200
+ const words = text.split(' ');
201
+ const lines = [];
202
+ let current = '';
203
+ for (const word of words) {
204
+ const prefix = lines.length ? indent : '';
205
+ if ((current + ' ' + word).trim().length + prefix.length > width && current) {
206
+ lines.push((lines.length ? indent : '') + current);
207
+ current = word;
208
+ }
209
+ else {
210
+ current = current ? `${current} ${word}` : word;
211
+ }
212
+ }
213
+ if (current)
214
+ lines.push((lines.length ? indent : '') + current);
215
+ return lines;
216
+ }
87
217
  function renderSessionRow(s) {
88
218
  return `${s.sessionId.slice(0, 8)} ${(0, evaluate_1.fmt)(s.totalTokens).padStart(8)} ${String(s.spawns).padStart(4)} spawns ${s.finalVerdict}`;
89
219
  }
@@ -37,6 +37,10 @@ export interface SessionReplay {
37
37
  tokensAt: number;
38
38
  spawnsAt: number;
39
39
  } | null;
40
+ /** Cumulative tokens sampled over active time, for a sparkline. 0..1 normalised. */
41
+ curve: number[];
42
+ /** Index into curve where the earliest STOP fired, or null. */
43
+ stopAtIndex: number | null;
40
44
  }
41
45
  export declare function discoverTranscripts(root?: string): string[];
42
46
  export declare function replaySession(path: string, thresholds: Thresholds): SessionReplay | null;
@@ -60,6 +60,9 @@ function replayEvents(sessionId, path, events, thresholds) {
60
60
  let sustainedStop = null;
61
61
  let firstWarn = null;
62
62
  let tokensAtEarliestStop = null;
63
+ // Cumulative tokens after every event, for the sparkline.
64
+ const timeline = [];
65
+ let stopEventIndex = null;
63
66
  for (const event of events) {
64
67
  // A hook boundary exists only where a spawn was attempted. Evaluate the
65
68
  // *proposal* before applying the event, then apply it.
@@ -80,6 +83,7 @@ function replayEvents(sessionId, path, events, thresholds) {
80
83
  }
81
84
  if ((fanoutStop || sustainedStop) && tokensAtEarliestStop === null) {
82
85
  tokensAtEarliestStop = state.totalTokens;
86
+ stopEventIndex = timeline.length;
83
87
  }
84
88
  }
85
89
  // The sustained plane also has a boundary at every tool call, not just
@@ -92,12 +96,16 @@ function replayEvents(sessionId, path, events, thresholds) {
92
96
  const hit = report.findings.find((f) => f.verdict === 'STOP' && (f.detector === 'sustained_burn' || f.detector === 'burn_debt'));
93
97
  if (hit) {
94
98
  sustainedStop = { atSpawn: state.spawnCount, tokensAtStop: state.totalTokens };
95
- if (tokensAtEarliestStop === null)
99
+ if (tokensAtEarliestStop === null) {
96
100
  tokensAtEarliestStop = state.totalTokens;
101
+ stopEventIndex = timeline.length;
102
+ }
97
103
  }
98
104
  }
99
105
  (0, session_1.applyEvent)(state, event);
106
+ timeline.push(state.totalTokens);
100
107
  }
108
+ const { curve, stopAtIndex } = downsample(timeline, stopEventIndex, 32);
101
109
  const final = (0, evaluate_1.evaluate)(state, thresholds, null);
102
110
  return {
103
111
  sessionId,
@@ -112,8 +120,23 @@ function replayEvents(sessionId, path, events, thresholds) {
112
120
  sustainedStop,
113
121
  catchableTail: tokensAtEarliestStop === null ? 0 : Math.max(0, state.totalTokens - tokensAtEarliestStop),
114
122
  firstWarn,
123
+ curve,
124
+ stopAtIndex,
115
125
  };
116
126
  }
127
+ /** Compress a cumulative timeline into N normalised buckets, carrying the stop index across. */
128
+ function downsample(timeline, stopIndex, buckets) {
129
+ if (timeline.length === 0)
130
+ return { curve: [], stopAtIndex: null };
131
+ const max = timeline[timeline.length - 1] || 1;
132
+ const curve = [];
133
+ for (let b = 0; b < buckets; b++) {
134
+ const i = Math.min(timeline.length - 1, Math.floor(((b + 1) / buckets) * timeline.length) - 1);
135
+ curve.push(Math.max(0, timeline[Math.max(0, i)]) / max);
136
+ }
137
+ const stopAtIndex = stopIndex === null ? null : Math.min(buckets - 1, Math.floor((stopIndex / timeline.length) * buckets));
138
+ return { curve, stopAtIndex };
139
+ }
117
140
  function replayAll(paths, thresholds, minTokens = 0) {
118
141
  const sessions = [];
119
142
  for (const path of paths) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentguard-run/burn",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Local runaway-agent circuit breaker for AI coding agents. Detects fan-out storms and sustained token burn, blocks the next spawn, and proves what happened. Nothing leaves the machine.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",
@@ -39,4 +39,4 @@
39
39
  "runaway",
40
40
  "fan-out"
41
41
  ]
42
- }
42
+ }