github-router 0.3.150 → 0.3.152

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.
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "name": "github-router browser bridge",
4
4
  "short_name": "gh-router-browser",
5
- "version": "0.3.150",
5
+ "version": "0.3.152",
6
6
  "description": "Bridge between Claude (via github-router /mcp) and the browser. Implements tab control, navigation, clicks, form fill, downloads, screenshots, devtools eval. Blocks navigation to chrome://settings.",
7
7
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqJElxuBlonBS3TVW9FJN0mGTtShB3L1hoaYf6k39SOr1ogGYmF90EjRxy1i21k9wQQjPf26bcBu/9X67KrQjQV0uB38CaNukgiSeoLjfptN811u+PJHx6BP+jx3Qa6/3VenNPxHC8WEU0GXql8QSjIHEyCwKb6fMASXOK94JyB5Ywov2x8mt/+9ncqBBBMVzf6r5Sagy4PL1XnryLsuADD/vOEkPet8wXgH/Oj7v5tTsQQZ7U1JT51PoDs2BFnXc5v3TkVgZwd32k3ONh+nkDw1Hof+4zwUGOyJE6eMrlYzRlKM4Qxdf9JpavQvqfieAbTRWcyKeclnHeoIfE7cDBQIDAQAB",
8
8
  "background": {
@@ -0,0 +1,6 @@
1
+ import { A as BROWSE_DEFAULT_MODEL, F as appendPlanReminder, I as runWorkerAgent, L as withNoOutputRetry, M as IMPLEMENT_DEFAULT_MODEL, N as PLAN_DEFAULT_MODEL, P as REVIEW_DEFAULT_MODEL, j as DEFAULT_MODEL } from "./peer-mcp-personas-BQVOxB1i.js";
2
+ import "./paths-D0tJ_tms.js";
3
+ import "./lifecycle-DyEXZu2z.js";
4
+ import "./lifecycle-DGvk4z63.js";
5
+
6
+ export { BROWSE_DEFAULT_MODEL, DEFAULT_MODEL, IMPLEMENT_DEFAULT_MODEL, PLAN_DEFAULT_MODEL, REVIEW_DEFAULT_MODEL, appendPlanReminder, runWorkerAgent, withNoOutputRetry };
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as readIteratorWithTimeout, $t as copilotHeaders, At as UPSTREAM_INACTIVITY_TIMEOUT_MS, B as toolbeltSkipSet, Bt as cacheModels, C as repoRoot, Ct as collapsePathKeys, D as resolveSealedGate, Dt as DEFAULT_CODEX_MODEL_FALLBACKS, E as trustRepo, Et as DEFAULT_CODEX_MODEL, Ft as setupCopilotToken, G as ADVISOR_INTERNAL_TOOL_NAME, Gt as resolveModel, H as TOOLBELT_TOOLS, Ht as filterBetaHeader, It as setupGitHubAgentToken, J as injectAdvisorTool, Jt as fetchWithTransientRetry, K as ADVISOR_TOOL_INSTRUCTIONS, Kt as sleep, L as availableToolCommands, Lt as setupGitHubToken, Mt as pickClaudeDefault, Nt as getPackageVersion, O as liveExec, Ot as DEFAULT_PORT, Pt as withInstallLock, Q as logStreamError, Qt as copilotBaseUrl, R as buildToolbeltAwareness, Rt as tryRefreshAndRetry, S as repoFingerprint, St as ArtifactClient, T as stopReviewStateDir, Tt as DEFAULT_CLAUDE_MODEL_FALLBACKS, U as assetFor, Ut as isNullish, V as vscodeRipgrepPath, Vt as cacheVSCodeVersion, W as searchWeb, Wt as resolveCodexModel, X as buildOpenAIErrorEvent, Xt as forwardError, Y as isAdvisorRequested, Yt as HTTPError, Z as isControllerClosedError, Zt as GITHUB_API_BASE_URL, _ as fileBaselineStore, _t as hasSupportedBrowserInstalled, a as buildPeerAwarenessSnippet, at as fleetToolsEnabled, b as fileReviewDebounce, bt as extractZipMember, c as buildSessionBindHookCommand, ct as countTokens, d as decideStopHook, dt as createResponses, en as githubHeaders, et as relayAnthropicStream, f as fileBlockBudget, ft as createChatCompletions, g as stopReviewEnabled, gt as provisionBrowserAssets, h as stopGateId, ht as parseJsonOrDiagnose, i as buildAgentPrompt, it as browserToolsEnabled, jt as generateRandomPort, kt as UPSTREAM_FETCH_TIMEOUT_MS, l as buildStopHookCommand, lt as createMessages, m as launchBaselineKey, mt as readResponseBodyCapped, n as MCP_GROUPS, nt as handleMcpPost, o as personasFor, ot as standInToolEnabled, p as injectStopHookIntoSettingsFile, pt as MAX_RESPONSE_BODY_BYTES, q as buildAdvisorStream, qt as getModels, r as assertMcpToolSurfaceConsistent, rt as agentToolsEnabled, s as buildArtifactOpenHookCommand, st as workerToolsEnabled, t as GROUP_META, tn as state, tt as handleMcpDelete, u as captureLaunchBaseline, ut as getTokenCount, v as fileFindingsStore, vt as provisionAndIndexColbert, w as stopGateEnabledForRepo, wt as toolbeltPathOverride, x as isSubagentContext, xt as shouldUseInsecureTls, y as fileLastPromptStore, yt as extractTarGzMember, z as toolbeltEnabled, zt as cacheCopilotVersion } from "./peer-mcp-personas-D_HyWhUb.js";
2
+ import { $ as logStreamError, $t as copilotBaseUrl, At as UPSTREAM_FETCH_TIMEOUT_MS, B as toolbeltEnabled, Bt as cacheCopilotVersion, C as repoFingerprint, Ct as ArtifactClient, D as trustRepo, Dt as DEFAULT_CODEX_MODEL, E as stopReviewStateDir, Et as DEFAULT_CLAUDE_MODEL_FALLBACKS, Ft as withInstallLock, G as searchWeb, Gt as resolveCodexModel, H as vscodeRipgrepPath, Ht as cacheVSCodeVersion, It as setupCopilotToken, J as buildAdvisorStream, Jt as getModels, K as ADVISOR_INTERNAL_TOOL_NAME, Kt as resolveModel, Lt as setupGitHubAgentToken, Mt as generateRandomPort, Nt as pickClaudeDefault, O as resolveSealedGate, Ot as DEFAULT_CODEX_MODEL_FALLBACKS, Pt as getPackageVersion, Q as isControllerClosedError, Qt as GITHUB_API_BASE_URL, R as availableToolCommands, Rt as setupGitHubToken, S as isSubagentContext, St as shouldUseInsecureTls, T as stopGateEnabledForRepo, Tt as toolbeltPathOverride, U as TOOLBELT_TOOLS, Ut as filterBetaHeader, V as toolbeltSkipSet, Vt as cacheModels, W as assetFor, Wt as isNullish, X as isAdvisorRequested, Xt as HTTPError, Y as injectAdvisorTool, Yt as fetchWithTransientRetry, Z as buildOpenAIErrorEvent, Zt as forwardError, _ as stopReviewEnabled, _t as provisionBrowserAssets, a as buildPeerAwarenessSnippet, at as browserToolsEnabled, b as fileLastPromptStore, bt as extractTarGzMember, c as buildSessionBindHookCommand, ct as workerToolsEnabled, d as decideStopHook, dt as getTokenCount, en as copilotHeaders, et as readIteratorWithTimeout, f as fileBlockBudget, ft as createResponses, g as stopGateId, gt as parseJsonOrDiagnose, h as stopGateDisabled, ht as readResponseBodyCapped, i as buildAgentPrompt, it as agentToolsEnabled, jt as UPSTREAM_INACTIVITY_TIMEOUT_MS, k as liveExec, kt as DEFAULT_PORT, l as buildStopHookCommand, lt as countTokens, m as launchBaselineKey, mt as MAX_RESPONSE_BODY_BYTES, n as MCP_GROUPS, nn as state, nt as handleMcpDelete, o as personasFor, ot as fleetToolsEnabled, p as injectStopHookIntoSettingsFile, pt as createChatCompletions, q as ADVISOR_TOOL_INSTRUCTIONS, qt as sleep, r as assertMcpToolSurfaceConsistent, rt as handleMcpPost, s as buildArtifactOpenHookCommand, st as standInToolEnabled, t as GROUP_META, tn as githubHeaders, tt as relayAnthropicStream, u as captureLaunchBaseline, ut as createMessages, v as fileBaselineStore, vt as hasSupportedBrowserInstalled, w as repoRoot, wt as collapsePathKeys, x as fileReviewDebounce, xt as extractZipMember, y as fileFindingsStore, yt as provisionAndIndexColbert, z as buildToolbeltAwareness, zt as tryRefreshAndRetry } from "./peer-mcp-personas-BQVOxB1i.js";
3
3
  import { a as removeOwnClaudeConfigMirror, i as isUnderClaudeConfigMirror, l as writeArtifactCredsToMirror, n as ensureClaudeConfigMirror, r as ensurePaths, t as PATHS, u as writeRuntimeFileSecure } from "./paths-D0tJ_tms.js";
4
4
  import { c as killManagedTree, d as runCommandCapture, f as runCommandVoid, l as parseBoolEnv, s as killChildProcessTree, u as resolveExecutable } from "./lifecycle-DyEXZu2z.js";
5
5
  import { a as sweepRegistry } from "./lifecycle-DGvk4z63.js";
@@ -2036,7 +2036,7 @@ async function discoverGateCommands(cwd, opts) {
2036
2036
  if (files.length === 0) return null;
2037
2037
  let result;
2038
2038
  try {
2039
- const { runWorkerAgent } = await import("./engine-Dtu1TDpQ.js");
2039
+ const { runWorkerAgent } = await import("./engine-CvY9mR3b.js");
2040
2040
  result = await runWorkerAgent({
2041
2041
  mode: "explore",
2042
2042
  workspace: root,
@@ -2729,13 +2729,13 @@ const ARTIFACT_REVIEW_SKILL = {
2729
2729
  name: "gh-artifact-review",
2730
2730
  md: `---
2731
2731
  name: gh-artifact-review
2732
- description: Review plans and artifacts in the ai-or-die panel. Default to authoring a self-contained HTML artifact (rich, annotatable) and opening THAT for the human, then poll for feedback, revise, and end the loop. Use when running inside an ai-or-die tab and you have a plan, comparison, diagram, table, diff, or report the user should see before proceeding.
2732
+ description: Review plans and artifacts in the ai-or-die panel. Default to authoring a self-contained HTML artifact (rich, annotatable, optionally interactive) and opening THAT for the human, then drain feedback with artifact_await, revise, and end the loop. Use when running inside an ai-or-die tab and you have a plan, comparison, diagram, table, diff, or report the user should see before proceeding.
2733
2733
  user-invocable: true
2734
2734
  ---
2735
2735
 
2736
2736
  # gh-artifact-review: human review in the ai-or-die panel
2737
2737
 
2738
- Use this when you finish a plan or produce something a user should review and you are inside an ai-or-die tab (the \`mcp__peers__artifact_*\` tools drive a live panel). The human can click any block or select text to attach a comment that comes back to you.
2738
+ Use this when you finish a plan or produce something a user should review and you are inside an ai-or-die tab (the \`mcp__peers__artifact_*\` tools drive a live panel). The human can click any block or select text to attach a comment, and can click declarative action controls you emit — both come back to you as typed events.
2739
2739
 
2740
2740
  ## Default: present HTML, not raw markdown
2741
2741
 
@@ -2763,14 +2763,36 @@ Artifacts stay portable (they must render identically opened standalone), so do
2763
2763
 
2764
2764
  ## Loop
2765
2765
 
2766
- 1. Open: \`mcp__peers__artifact_open\` with the absolute path of the \`.html\` (or the file). Relay the returned \`viewUrl\` and tell the user to review in the panel — they can click a block or select text to comment.
2767
- 2. Poll: \`mcp__peers__artifact_poll\`. If status is waiting, poll again. Each returned prompt may carry a \`selector\`, quoted \`text\`, and \`sourceLine\` pinpointing what the comment is about act on that exact spot.
2768
- 3. Apply: make the requested edits, then \`mcp__peers__artifact_reply\` with a concise summary of what changed.
2769
- 4. Repeat 2-3 until the user is satisfied; then \`mcp__peers__artifact_end\`.
2766
+ 1. Open: \`mcp__peers__artifact_open\` with the absolute path of the \`.html\` (or the file). Pass \`mode:"interactive"\` when the HTML carries \`data-aod-*\` action controls (below). Relay the returned \`viewUrl\` and tell the user to review in the panel.
2767
+ 2. Drain: \`mcp__peers__artifact_await\`. It long-holds for the human's next events and returns \`{events, status, cursor}\`. **Pass the returned \`cursor\` on your next call** so you only receive newer events; if \`events\` is empty, call again with that cursor.
2768
+ 3. Act on each event by \`kind\`:
2769
+ - \`comment\` — a free-text note anchored to the artifact (\`selector\`, quoted \`text\`, \`sourceLine\`); apply the requested change at that exact spot.
2770
+ - \`action\` — the human clicked a control you emitted (\`action\` verb, \`elementId\`, optional \`value\`; a multi-select submit arrives as one action carrying the selected set). Do what the verb means (approve a step, choose an option, apply a toggle set).
2771
+ 4. Reply: \`mcp__peers__artifact_reply\` with a concise summary of what you changed. Optionally \`mcp__peers__artifact_update({file})\` or \`({html})\` to replace the artifact content in place, or \`mcp__peers__artifact_refresh\` to reload it from disk.
2772
+ 5. Repeat 2-4 until the user is satisfied; then \`mcp__peers__artifact_end\`. Use \`mcp__peers__artifact_dismiss\` to hide the panel while keeping the review alive (queued feedback preserved) if the user wants it out of the way without ending.
2773
+
2774
+ ### Push arrival (you do not have to be polling)
2775
+
2776
+ When you are idle at the prompt, panel feedback can arrive on its own as a new turn (the tab injects it). Structured actions that answer a pending decision are routed to you directly. Either way, the durable record is the \`artifact_await\` drain, so when in doubt call \`artifact_await\` (with your last cursor) to reconcile — it replays anything you missed.
2777
+
2778
+ ### \`artifact_poll\` (frozen legacy)
2779
+
2780
+ \`mcp__peers__artifact_poll\` still resolves for back-compat but returns the OLD payload (human comments only, no structured actions). Prefer \`artifact_await\`.
2781
+
2782
+ ## Interactive controls (data-aod-* authoring)
2783
+
2784
+ To let the human act on the artifact (not just comment), emit declarative controls; the panel wires them and delivers a typed \`action\` event. No JS in the artifact.
2785
+
2786
+ - **choose-one (fires immediately):** each option is a button that posts the moment it is clicked.
2787
+ \`<button data-aod-action="choose" data-aod-group="decision-1" data-aod-id="opt-jwt" data-aod-value="jwt">JWT</button>\`
2788
+ - **multi-select (toggle then submit):** checkboxes sharing a \`data-aod-group\`, plus one submit button with the same group. The submit delivers ONE action carrying \`{group, selected:[{elementId, value?}]}\`.
2789
+ \`<input type="checkbox" data-aod-action="check" data-aod-group="opts" data-aod-id="a" data-aod-value="a">\` … \`<button data-aod-action="submit" data-aod-group="opts" data-aod-id="opts-submit">Apply</button>\`
2790
+ - **plan steps:** \`<li class="aod-step" data-aod-id="plan-step-3" data-source-line="14">\` with per-step \`data-aod-action="approve"/"skip"\` buttons.
2791
+ - **Required attrs:** \`data-aod-action\` (verb), \`data-aod-id\` (stable, echoed back as \`elementId\`; a control missing it is ignored), optional \`data-aod-value\`. Keep \`data-source-line\` for comment mapping.
2770
2792
 
2771
2793
  ## Honest limits
2772
2794
 
2773
- - If a tool errors (e.g. \`NOT_IN_AIORDIE_TAB\`, \`UNREACHABLE\`), report the code/message verbatim; do not claim the panel opened.
2795
+ - If a tool errors (e.g. \`NOT_IN_AIORDIE_TAB\`, \`UNREACHABLE\`, \`INVALID_REQUEST\`), report the code/message verbatim; do not claim the panel opened.
2774
2796
  - The panel is a review surface, not an approver: outward/irreversible actions still need explicit user confirmation.
2775
2797
  `
2776
2798
  };
@@ -2796,7 +2818,7 @@ const TOOLBELT_MARKER_OPEN = "<!-- gh-router toolbelt awareness — auto-injecte
2796
2818
  const TOOLBELT_MARKER_CLOSE = "<!-- /gh-router toolbelt awareness -->";
2797
2819
  const ARTIFACT_MARKER_OPEN = "<!-- gh-router artifact-panel directive — auto-injected when in an ai-or-die tab -->";
2798
2820
  const ARTIFACT_MARKER_CLOSE = "<!-- /gh-router artifact-panel directive -->";
2799
- const ARTIFACT_PANEL_DIRECTIVE = "## Review in the artifact panel (HTML by default)\n\nYou are running inside an ai-or-die tab, so the `mcp__peers__artifact_open/poll/reply/end` tools drive a live human-review panel. Default to an HTML artifact for anything the user should review before you proceed, not just plans but also design proposals, comparisons / trade-offs, decisions that need their input, diagrams, tables, code diffs, and reports. Author a self-contained `.html` (inline CSS, no external deps, readable typography) and open it with `mcp__peers__artifact_open`; tell the user to review (they can click a block or select text to comment), then `mcp__peers__artifact_poll` for feedback, revise, `mcp__peers__artifact_reply`, and `mcp__peers__artifact_end` when done. Plan-mode plans are auto-rendered to HTML and auto-opened for you; raw markdown is only a fallback. Skip the panel only for trivial one-line answers.\n\nMake the HTML good: match the subject project's design system (its Tailwind / theme / tokens) when the artifact represents a specific app, otherwise clean readable defaults. Per-type cheatsheet. plan: goal, current state, proposed approach, risks / open questions. comparison: options as columns with trade-off rows and a recommendation. table: scannable rows with a sticky header. diagram: boxes + arrows (SVG/CSS or Mermaid). code / diff: `<pre>` with before/after. The `gh-artifact-review` skill carries the fuller playbook.";
2821
+ const ARTIFACT_PANEL_DIRECTIVE = "## Review in the artifact panel (HTML by default)\n\nYou are running inside an ai-or-die tab, so the `mcp__peers__artifact_*` tools drive a live human-review panel. Default to an HTML artifact for anything the user should review before you proceed, not just plans but also design proposals, comparisons / trade-offs, decisions that need their input, diagrams, tables, code diffs, and reports. Author a self-contained `.html` (inline CSS, no external deps, readable typography) and open it with `mcp__peers__artifact_open` (pass `mode:\"interactive\"` if it carries `data-aod-*` action controls); tell the user to review, then drain their feedback with `mcp__peers__artifact_await` (pass back the returned `cursor` each call), revise, `mcp__peers__artifact_reply`, and `mcp__peers__artifact_end` when done. `artifact_await` returns typed events: `comment` (free-text anchored by selector/text/sourceLine) and `action` (the human clicked a control you emitted). Use `mcp__peers__artifact_update`/`artifact_refresh` to change the shown content and `mcp__peers__artifact_dismiss` to hide the panel while keeping the review alive. `mcp__peers__artifact_poll` is a frozen legacy alias (comments only). Plan-mode plans are auto-rendered to HTML and auto-opened for you; raw markdown is only a fallback. Skip the panel only for trivial one-line answers.\n\nMake the HTML good: match the subject project's design system (its Tailwind / theme / tokens) when the artifact represents a specific app, otherwise clean readable defaults. Per-type cheatsheet. plan: goal, current state, proposed approach, risks / open questions. comparison: options as columns with trade-off rows and a recommendation. table: scannable rows with a sticky header. diagram: boxes + arrows (SVG/CSS or Mermaid). code / diff: `<pre>` with before/after. To let the user act (not just comment), emit declarative controls (no JS): `data-aod-action` (verb) + `data-aod-id` (stable) [+ `data-aod-value`] — a choose-one option fires on click, a multi-select is checkboxes sharing `data-aod-group` plus a submit button with that group. The `gh-artifact-review` skill carries the fuller playbook.";
2800
2822
  /**
2801
2823
  * Writing / communication style directive injected at the TOP of the
2802
2824
  * mirrored CLAUDE.md so every spawned agent (main, Agent-tool subagent,
@@ -3717,7 +3739,7 @@ function initProxyFromEnv() {
3717
3739
  //#endregion
3718
3740
  //#region package.json
3719
3741
  var name = "github-router";
3720
- var version$1 = "0.3.150";
3742
+ var version$1 = "0.3.152";
3721
3743
 
3722
3744
  //#endregion
3723
3745
  //#region src/lib/approval.ts
@@ -5765,6 +5787,11 @@ const claude = defineCommand({
5765
5787
  default: false,
5766
5788
  description: "Explicitly record consent for the structural Stop-gate in THIS repo (pinned to the repo's root-commit). The gate is ON BY DEFAULT when a harness is detected (consent-by-launching), so this is now mostly redundant; it stays for explicit/scripted use. Disable the gate entirely with GH_ROUTER_DISABLE_STOP_GATE=1."
5767
5789
  },
5790
+ "no-stop-gate": {
5791
+ type: "boolean",
5792
+ default: false,
5793
+ description: "Disable the structural Stop-gate for THIS session (same effect as GH_ROUTER_DISABLE_STOP_GATE=1). Intended for driven/automated sessions where a blocking Stop hook would hang the turn-end while a fleet driver waits."
5794
+ },
5768
5795
  "auto-update": {
5769
5796
  type: "boolean",
5770
5797
  default: true,
@@ -5952,7 +5979,7 @@ const claude = defineCommand({
5952
5979
  } catch (err) {
5953
5980
  consola.warn(`Could not record gate trust: ${String(err)}`);
5954
5981
  }
5955
- const gateDisabled = parseBoolEnv(process$1.env.GH_ROUTER_DISABLE_STOP_GATE) === true;
5982
+ const gateDisabled = stopGateDisabled(args);
5956
5983
  const forceEnabled = parseBoolEnv(process$1.env.GH_ROUTER_ENABLE_STOP_GATE) === true;
5957
5984
  const includeTests = parseBoolEnv(process$1.env.GH_ROUTER_STOP_GATE_RUN_TESTS) === true;
5958
5985
  const gateRoot = await repoRoot(sessionCwd).catch(() => sessionCwd);
@@ -6636,15 +6663,9 @@ function renderMarkdownBody(source) {
6636
6663
  * readable typography (works even before ai-or-die injects its annotation SDK).
6637
6664
  */
6638
6665
  function renderPlanHtml(source, title = "Plan") {
6639
- const body = renderMarkdownBody(source);
6640
- return `<!doctype html>
6641
- <html lang="en">
6642
- <head>
6643
- <meta charset="utf-8">
6644
- <meta name="viewport" content="width=device-width, initial-scale=1">
6645
- <title>${escapeHtml(title)}</title>
6646
- <style>
6647
- :root {
6666
+ return wrapArtifactDocument(renderMarkdownBody(source), title);
6667
+ }
6668
+ const BASE_STYLE = ` :root {
6648
6669
  --bg: #0f1115; --fg: #f7f3ea; --muted: #aeb6c6; --border: #2a2f3a;
6649
6670
  --accent: #f4c95d; --code-bg: #171a21;
6650
6671
  --serif: 'Iowan Old Style', Georgia, 'Times New Roman', serif;
@@ -6673,7 +6694,22 @@ function renderPlanHtml(source, title = "Plan") {
6673
6694
  th, td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }
6674
6695
  th { background: var(--code-bg); }
6675
6696
  hr { border: 0; border-top: 1px solid var(--border); margin: 1.5em 0; }
6676
- ul, ol { padding-left: 1.4em; }
6697
+ ul, ol { padding-left: 1.4em; }`;
6698
+ /**
6699
+ * Wrap rendered inner HTML in the self-contained document shell. `extraStyle` is
6700
+ * appended to the inline stylesheet so the interactive variant can add its own
6701
+ * rules without a second `<style>` block. Static callers pass "" (default), so
6702
+ * the static document is byte-identical to the pre-refactor output.
6703
+ */
6704
+ function wrapArtifactDocument(body, title, extraStyle = "") {
6705
+ return `<!doctype html>
6706
+ <html lang="en">
6707
+ <head>
6708
+ <meta charset="utf-8">
6709
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6710
+ <title>${escapeHtml(title)}</title>
6711
+ <style>
6712
+ ${BASE_STYLE}${extraStyle}
6677
6713
  </style>
6678
6714
  </head>
6679
6715
  <body>