@habemus-papadum/aiui-util 0.2.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ /** The installed-Chrome release channels a launch can target. */
2
+ export declare const CHROME_CHANNELS: readonly ["stable", "beta", "dev", "canary"];
3
+ export type ChromeChannel = (typeof CHROME_CHANNELS)[number];
4
+ export interface SessionBrowser {
5
+ /** The DevTools endpoint chrome-devtools-mcp attaches to. */
6
+ browserUrl: string;
7
+ port: number;
8
+ }
9
+ /**
10
+ * A live session browser for this profile, if one is running.
11
+ * Never launches anything; safe to call from non-interactive paths.
12
+ */
13
+ export declare function discoverSessionBrowser(userDataDir: string): Promise<SessionBrowser | undefined>;
14
+ /**
15
+ * Which binary a session-browser launch should run: an explicit
16
+ * executablePath (usually the managed Chrome for Testing) wins; otherwise the
17
+ * system install of the requested channel (default stable). Throws when no
18
+ * such browser is installed.
19
+ */
20
+ export declare function sessionBrowserBinary(settings: {
21
+ executablePath?: string;
22
+ channel?: ChromeChannel;
23
+ }): string;
24
+ /**
25
+ * Launch the session browser detached (it deliberately outlives the launching
26
+ * process — it's the user's window too) and wait for its debug endpoint.
27
+ *
28
+ * `debugPort` 0 lets the OS pick a free port; Chrome reports the choice via
29
+ * `DevToolsActivePort`, which is removed first so a stale file from a previous
30
+ * run can't win the poll. Fails fast if the process exits early — the classic
31
+ * cause being an already-running Chrome on the same profile *without* a debug
32
+ * port, which swallows the new invocation as a URL-handoff and exits.
33
+ */
34
+ export declare function launchSessionBrowser(opts: {
35
+ binary: string;
36
+ userDataDir: string;
37
+ debugPort?: number;
38
+ /** Unpacked extensions to load (comma-joined into one `--load-extension`). */
39
+ extensionDirs?: string[];
40
+ headless?: boolean;
41
+ startUrl?: string;
42
+ }): Promise<SessionBrowser>;
43
+ /**
44
+ * Open a URL as a new tab in a session browser, via the DevTools HTTP API
45
+ * (`PUT /json/new` — PUT is required by current Chrome).
46
+ */
47
+ export declare function openInSessionBrowser(browserUrl: string, url: string): Promise<void>;
48
+ /** The force/suppress escape hatches a sidecar's caller can express. */
49
+ export interface BrowserAutoOpenFlags {
50
+ /** Force opening even under CI/SSH/no-display (`--aiui-browser`, `WORKBENCH_BROWSER=1`). */
51
+ browser?: boolean;
52
+ /** Never open a browser for this run (`--aiui-no-browser`, `WORKBENCH_BROWSER=0`). */
53
+ noBrowser?: boolean;
54
+ }
55
+ /** The config a sidecar's project may have voted with (aiui's `chrome` section). */
56
+ export interface BrowserAutoOpenConfig {
57
+ /** `false` opts the project out of browser integration wholesale. */
58
+ enabled?: boolean;
59
+ /** A browser managed elsewhere (usually reverse-tunneled) — attach there. */
60
+ browserUrl?: string;
61
+ }
62
+ /** What a browser sidecar should do once its dev-server URL is known. */
63
+ export type BrowserAction = {
64
+ kind: "open";
65
+ } | {
66
+ kind: "skip";
67
+ } | {
68
+ kind: "hint";
69
+ reason: string;
70
+ };
71
+ /**
72
+ * Decide the browser sidecar's move — pure (env and platform are parameters)
73
+ * so every rung is unit-testable. The ladder, most explicit first, mirrors
74
+ * the aiui CLI's flag-beats-config ordering:
75
+ *
76
+ * 1. Suppress flag → skip, silently. The user said no for this run.
77
+ * 2. Force flag → open, even under CI, over SSH, or with
78
+ * `chrome.enabled: false` — the force flag exists precisely to overrule
79
+ * the defaults (e.g. the dev box is "headless" but its display is a
80
+ * forwarded port away).
81
+ * 3. `chrome.enabled: false` → skip: the config opted this project out of
82
+ * browser integration wholesale, same as it disables the DevTools MCP.
83
+ * 4. A configured `chrome.browserUrl` → open. The browser deliberately lives
84
+ * elsewhere (typically the user's local machine, reverse-tunneled — see
85
+ * docs/guide/remote), so *this* machine being headless is irrelevant:
86
+ * opening a tab there is exactly the point of the setup.
87
+ * 5. CI or headless (see ./environment) → don't launch a browser nobody
88
+ * can see; hand back the reason so the caller can print the
89
+ * port-forwarding hint instead.
90
+ * 6. Otherwise → open.
91
+ */
92
+ export declare function decideBrowserAction(args: BrowserAutoOpenFlags, config?: BrowserAutoOpenConfig, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): BrowserAction;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Where is this process running — CI? over SSH? a machine with no display?
3
+ *
4
+ * Several commands adapt to their surroundings: `aiui claude` leaves the
5
+ * Chrome DevTools MCP off under CI, and `aiui vite` only
6
+ * auto-open their dev server in the session browser when there is a display
7
+ * to show it on. Those decisions share the detection logic here — kept pure
8
+ * (`env` and `platform` are parameters defaulting to the real ones) so every
9
+ * branch is unit-testable without mutating the test process's own
10
+ * environment.
11
+ *
12
+ * The signals, and why each is trusted:
13
+ *
14
+ * - **CI** — effectively every provider (GitHub Actions, GitLab, CircleCI,
15
+ * Travis, Buildkite, …) sets `CI=true`. A CI runner may technically have a
16
+ * display, but nobody is watching it, which is what "headless" means for
17
+ * the decisions built on this module.
18
+ * - **SSH** — sshd sets `SSH_CONNECTION` and `SSH_CLIENT` in every session
19
+ * and `SSH_TTY` in interactive ones. Any of them means this shell lives at
20
+ * the far end of a network connection: a browser launched here would render
21
+ * on the remote box's display (if it even has one), never in front of the
22
+ * user. X11 forwarding (`ssh -X`, which sets `DISPLAY`) is deliberately
23
+ * *not* honored as an exception — it's rare, slow, and almost never where
24
+ * the user wants a Chrome window; `--aiui-browser`-style force flags are
25
+ * the escape hatch.
26
+ * - **Linux display** — X11 clients find their server via `DISPLAY`; Wayland
27
+ * clients via `WAYLAND_DISPLAY`. Neither being set means there is no
28
+ * compositor to render a window into.
29
+ * - **macOS** — has no display env-var convention; WindowServer is always up
30
+ * on a booted Mac, so assume a GUI unless SSH says the shell is remote.
31
+ * - **Other platforms** (Windows) likewise have no display env var: assume a
32
+ * GUI and rely on the CI/SSH signals alone.
33
+ */
34
+ /** Truthy `CI` env var, with the conventional "false"/"0" escape hatches. */
35
+ export declare function isCi(env?: NodeJS.ProcessEnv): boolean;
36
+ /**
37
+ * Whether this shell is an SSH session. Checks all three of sshd's markers —
38
+ * `SSH_TTY` is only set for interactive sessions, and `SSH_CONNECTION` /
39
+ * `SSH_CLIENT` can be scrubbed by a paranoid shell profile, so any one of
40
+ * them is taken at its word. Empty values count as unset.
41
+ */
42
+ export declare function isSsh(env?: NodeJS.ProcessEnv): boolean;
43
+ /**
44
+ * Why this environment is considered headless — a short human-readable
45
+ * fragment for messages like "detected a headless environment (`<reason>`)" —
46
+ * or undefined when a GUI is presumed available. The checks run most-explicit
47
+ * first (CI, then SSH, then the platform's display convention); see the
48
+ * module doc for the reasoning behind each signal.
49
+ */
50
+ export declare function headlessReason(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string | undefined;
51
+ /** {@link headlessReason} as a boolean, for callers that don't print it. */
52
+ export declare function isHeadless(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Talking to an unpacked extension in the session browser, over raw CDP:
3
+ * reload it, and ask it what it is actually running.
4
+ *
5
+ * `chrome.runtime.reload()` is the only thing that makes Chrome re-read an
6
+ * unpacked extension's directory — which is exactly what the CRXJS dev loop
7
+ * needs after every dev-server start (the artifact on disk was just rewritten;
8
+ * Chrome is still holding the previous run's snapshot). Doing it by hand means
9
+ * a trip to chrome://extensions and a click; doing it in the wrong order —
10
+ * while Vite is still writing — is what strands people with a blank panel. So
11
+ * the CLI owns it: wait for the artifact, reload, then *verify* by reading the
12
+ * extension's own dev stamp back out of the browser.
13
+ *
14
+ * Chrome exposes no HTTP verb for any of this, but every extension context can
15
+ * call `chrome.runtime.reload()` and `fetch(chrome.runtime.getURL(…))` itself,
16
+ * and the DevTools protocol can evaluate in any of them. Two ways in, in order:
17
+ *
18
+ * 1. **An existing extension target** — the MV3 service worker, or any open
19
+ * extension page. Cheap and invisible.
20
+ * 2. **A wake page** — MV3 service workers idle-terminate (~30s), so quite
21
+ * often there is *no* extension target at all. We then open one of the
22
+ * extension's own pages in a background tab, evaluate there, and close it.
23
+ * The extension supplies a page that is safe to open for this purpose
24
+ * (aiui-extension ships `reload.html`, which is inert).
25
+ */
26
+ /** A DevTools target as listed by `GET /json/list`. */
27
+ export interface BrowserTarget {
28
+ id: string;
29
+ type: string;
30
+ url: string;
31
+ title?: string;
32
+ webSocketDebuggerUrl?: string;
33
+ }
34
+ /** How to reach a context inside one extension. */
35
+ export interface ExtensionContextOptions {
36
+ /** The extension id (aiui's is pinned by the manifest key). */
37
+ extensionId: string;
38
+ /**
39
+ * An inert page inside the extension, opened in a background tab when no
40
+ * extension context exists to evaluate in. Without it, a sleeping service
41
+ * worker means "not-loaded".
42
+ */
43
+ wakePage?: string;
44
+ /** How long to wait for a reply. */
45
+ timeoutMs?: number;
46
+ }
47
+ /** Why we couldn't run something inside the extension. */
48
+ export type ExtensionFailure =
49
+ /** The browser has no context for this extension — it isn't loaded. */
50
+ {
51
+ ok: false;
52
+ reason: "not-loaded";
53
+ }
54
+ /** The browser's debug endpoint didn't answer. */
55
+ | {
56
+ ok: false;
57
+ reason: "no-browser";
58
+ detail: string;
59
+ }
60
+ /** We reached a context, but the expression threw in it. */
61
+ | {
62
+ ok: false;
63
+ reason: "failed";
64
+ detail: string;
65
+ };
66
+ export type ReloadExtensionResult = {
67
+ ok: true;
68
+ via: "service-worker" | "page" | "wake-page";
69
+ } | ExtensionFailure;
70
+ export type EvaluateInExtensionResult<T> = {
71
+ ok: true;
72
+ value: T;
73
+ } | ExtensionFailure;
74
+ /**
75
+ * Install (or re-point) an unpacked extension in a running browser — CDP's
76
+ * `Extensions.loadUnpacked`, i.e. "Load unpacked" without the human.
77
+ *
78
+ * This is what makes the two-artifact layout painless: Chrome installs an
79
+ * unpacked extension **by path**, so a browser that was launched against `dist/`
80
+ * would otherwise ignore the dev server's output forever, and no amount of
81
+ * reloading could tell it otherwise — only a trip to chrome://extensions. With
82
+ * this, the CLI just points it at the right directory. The extension id is
83
+ * unchanged (it comes from the manifest key, not the path), so nothing that
84
+ * depends on the id — the native-messaging host's `allowed_origins`, above all —
85
+ * needs to know this happened.
86
+ *
87
+ * Best-effort by design: the domain is recent (Chrome ≥ 129, measured working on
88
+ * Chrome for Testing 150) and a browser may refuse. Callers fall back to telling
89
+ * the human what to click.
90
+ */
91
+ export declare function loadUnpackedExtension(browserUrl: string, path: string): Promise<{
92
+ ok: true;
93
+ extensionId: string;
94
+ } | {
95
+ ok: false;
96
+ detail: string;
97
+ }>;
98
+ /** `GET /json/list` — every target the browser is willing to show us. */
99
+ export declare function listBrowserTargets(browserUrl: string): Promise<BrowserTarget[]>;
100
+ /** Targets belonging to one extension (service worker, pages, offscreen docs). */
101
+ export declare function extensionTargets(targets: BrowserTarget[], extensionId: string): BrowserTarget[];
102
+ /**
103
+ * Make the browser re-read the extension's directory.
104
+ *
105
+ * The evaluation destroys the context it runs in, so the reply never arrives —
106
+ * "no answer" is success here, not failure. Callers that need certainty read
107
+ * the extension back afterwards ({@link evaluateInExtension}).
108
+ */
109
+ export declare function reloadExtension(browserUrl: string, options: ExtensionContextOptions): Promise<ReloadExtensionResult>;
110
+ /**
111
+ * Run an expression *inside* the extension and get its value back — how the
112
+ * CLI asks "which artifact are you actually running?" (the extension fetches
113
+ * its own dev stamp; see aiui-webext's dev-stamp module). Promises are awaited.
114
+ */
115
+ export declare function evaluateInExtension<T>(browserUrl: string, options: ExtensionContextOptions & {
116
+ expression: string;
117
+ }): Promise<EvaluateInExtensionResult<T>>;
package/dist/index.d.ts CHANGED
@@ -1,8 +1,15 @@
1
1
  /**
2
- * Shared utilities for the aiui packages.
2
+ * Shared utilities for the aiui packages: cache directories, environment
3
+ * detection (CI / SSH / headless), and the session-browser plumbing that
4
+ * dev-server sidecars build their browser auto-open on.
3
5
  *
4
6
  * @packageDocumentation
5
7
  */
8
+ export * from "./browser";
9
+ export * from "./environment";
10
+ export * from "./extension";
11
+ export * from "./provenance";
12
+ export * from "./socket-url";
6
13
  export interface CacheDirOptions {
7
14
  /**
8
15
  * Create the directory (recursively) if it doesn't exist. Defaults to `true` —
package/dist/index.js CHANGED
@@ -1,20 +1,414 @@
1
- import { mkdirSync as s } from "node:fs";
2
- import { homedir as m } from "node:os";
3
- import { join as c, isAbsolute as u } from "node:path";
4
- const f = "aiui";
5
- function A(r, o = {}) {
6
- const { create: e = !0 } = o, t = r ? c(n(), r) : n();
7
- return e && s(t, { recursive: !0 }), t;
8
- }
9
- function n() {
10
- var t, i;
11
- const r = (t = process.env.AIUI_CACHE) == null ? void 0 : t.trim();
12
- if (r)
1
+ import { mkdirSync as D, rmSync as I, writeFileSync as U, readFileSync as N, existsSync as A } from "node:fs";
2
+ import { join as f, dirname as R, isAbsolute as _ } from "node:path";
3
+ import { computeSystemExecutablePath as j, ChromeReleaseChannel as g, Browser as H } from "@puppeteer/browsers";
4
+ import { execa as O } from "execa";
5
+ import { createRequire as B } from "node:module";
6
+ import { homedir as Y } from "node:os";
7
+ function M(e = process.env) {
8
+ const t = e.CI;
9
+ return t !== void 0 && t !== "" && t !== "0" && t.toLowerCase() !== "false";
10
+ }
11
+ function F(e = process.env) {
12
+ return m(e.SSH_CONNECTION) || m(e.SSH_TTY) || m(e.SSH_CLIENT);
13
+ }
14
+ function T(e = process.env, t = process.platform) {
15
+ if (M(e))
16
+ return "the CI environment variable is set";
17
+ if (F(e))
18
+ return "this is an SSH session";
19
+ if (t === "linux" && !m(e.DISPLAY) && !m(e.WAYLAND_DISPLAY))
20
+ return "Linux with neither DISPLAY nor WAYLAND_DISPLAY set";
21
+ }
22
+ function ue(e = process.env, t = process.platform) {
23
+ return T(e, t) !== void 0;
24
+ }
25
+ function m(e) {
26
+ return e !== void 0 && e !== "";
27
+ }
28
+ const $ = "DevToolsActivePort", v = 2e4, le = ["stable", "beta", "dev", "canary"];
29
+ async function J(e) {
30
+ const t = q(e);
31
+ if (t !== void 0 && await W(t))
32
+ return { browserUrl: `http://127.0.0.1:${t}`, port: t };
33
+ }
34
+ function q(e) {
35
+ try {
36
+ const [t] = N(f(e, $), "utf8").split(`
37
+ `), r = Number(t);
38
+ return Number.isInteger(r) && r > 0 ? r : void 0;
39
+ } catch {
40
+ return;
41
+ }
42
+ }
43
+ async function W(e) {
44
+ try {
45
+ return (await fetch(`http://127.0.0.1:${e}/json/version`, {
46
+ signal: AbortSignal.timeout(1e3)
47
+ })).ok;
48
+ } catch {
49
+ return !1;
50
+ }
51
+ }
52
+ function de(e) {
53
+ return e.executablePath ? e.executablePath : j({
54
+ browser: H.CHROME,
55
+ channel: V[e.channel ?? "stable"]
56
+ });
57
+ }
58
+ const V = {
59
+ stable: g.STABLE,
60
+ beta: g.BETA,
61
+ dev: g.DEV,
62
+ canary: g.CANARY
63
+ };
64
+ async function fe(e) {
65
+ var o;
66
+ D(e.userDataDir, { recursive: !0 }), I(f(e.userDataDir, $), { force: !0 });
67
+ const t = [
68
+ `--remote-debugging-port=${e.debugPort ?? 0}`,
69
+ `--user-data-dir=${e.userDataDir}`,
70
+ "--no-first-run",
71
+ "--no-default-browser-check",
72
+ // Media prompts, pre-answered — this is a *dev* browser (unauthenticated
73
+ // debug port, project-local profile; see docs/guide/warning.md), and the
74
+ // intent tool's dictation + screenshots otherwise re-prompt constantly:
75
+ // Chrome scopes mic permission per-origin (every dev-server PORT is its
76
+ // own origin), and the getDisplayMedia share picker can never be
77
+ // persisted at all.
78
+ // - auto-accept camera/microphone permission prompts (the *default, real*
79
+ // devices — fake devices only come from the separate
80
+ // --use-fake-device-for-media-stream, deliberately not passed). NOT the
81
+ // older --use-fake-ui-for-media-stream: that flag also hijacks the
82
+ // getDisplayMedia picker and auto-selects the ENTIRE SCREEN, which
83
+ // needs macOS Screen Recording permission the CfT binary doesn't have —
84
+ // every capture then dies with NotReadableError ("Could not start
85
+ // video source"), silently defeating the tab-capture flag below (this
86
+ // broke the paint host's screen share; verified against CfT 150).
87
+ "--auto-accept-camera-and-microphone-capture",
88
+ // - auto-accept a current-tab share (getDisplayMedia({ preferCurrentTab:
89
+ // true })) — no picker, no gesture, no OS-level screen-recording grant.
90
+ // No shipped page calls getDisplayMedia any more (the intent client's
91
+ // hosts capture natively), but the flag is kept: it is harmless, and a
92
+ // scratch page an agent writes can rely on it. Verified against CfT
93
+ // 150: the call resolves in ~320ms with userActivation.isActive false.
94
+ // Trap for anyone re-measuring: a Chrome spawned from a process that
95
+ // lacks the macOS Screen Recording grant inherits that lack, and the
96
+ // call HANGS instead — test from a real terminal, or headless.
97
+ "--auto-accept-this-tab-capture",
98
+ // - allow audio PLAYBACK without a user gesture — the outbound half of the
99
+ // same media posture (the two flags above pre-answer capture). The
100
+ // intent client's panels play server-pushed speech (the linter's spoken
101
+ // notes, TTS acks) via Audio.play(), and with keys forwarded from the
102
+ // target tab the panel document may never receive the gesture Chrome's
103
+ // autoplay policy wants — the clip would be refused with
104
+ // NotAllowedError. The SpeechPlayer parks blocked clips and resumes on
105
+ // a gesture (dev-overlay speech.ts), so outside this browser nothing is
106
+ // lost — but in the session browser the linter should simply be HEARD.
107
+ "--autoplay-policy=no-user-gesture-required"
108
+ ];
109
+ (o = e.extensionDirs) != null && o.length && t.push(`--load-extension=${e.extensionDirs.join(",")}`), e.headless && t.push("--headless"), t.push(e.startUrl ?? "about:blank");
110
+ const r = O(e.binary, t, {
111
+ detached: !0,
112
+ stdio: "ignore",
113
+ reject: !1,
114
+ cleanup: !1
115
+ });
116
+ r.unref();
117
+ let n = !1;
118
+ r.then(() => {
119
+ n = !0;
120
+ });
121
+ const s = Date.now() + v;
122
+ for (; Date.now() < s; ) {
123
+ const a = await J(e.userDataDir);
124
+ if (a) {
125
+ try {
126
+ U(
127
+ f(e.userDataDir, "aiui-browser.json"),
128
+ `${JSON.stringify({ pid: r.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() })}
129
+ `
130
+ );
131
+ } catch {
132
+ }
133
+ return a;
134
+ }
135
+ if (n)
136
+ throw new Error(
137
+ "the browser exited before exposing its DevTools endpoint — is another Chrome already running on this profile without a debug port? Close it and retry."
138
+ );
139
+ await G(250);
140
+ }
141
+ throw new Error(
142
+ `the browser did not expose its DevTools endpoint within ${v / 1e3}s`
143
+ );
144
+ }
145
+ async function we(e, t) {
146
+ const r = e.replace(/\/+$/, ""), n = await fetch(`${r}/json/new?${encodeURI(t)}`, {
147
+ method: "PUT",
148
+ signal: AbortSignal.timeout(3e3)
149
+ });
150
+ if (!n.ok)
151
+ throw new Error(`the browser refused to open the tab (${n.status} ${n.statusText})`);
152
+ }
153
+ function he(e, t = {}, r = process.env, n = process.platform) {
154
+ if (e.noBrowser)
155
+ return { kind: "skip" };
156
+ if (e.browser)
157
+ return { kind: "open" };
158
+ if (t.enabled === !1)
159
+ return { kind: "skip" };
160
+ if (t.browserUrl)
161
+ return { kind: "open" };
162
+ const s = T(r, n);
163
+ return s ? { kind: "hint", reason: s } : { kind: "open" };
164
+ }
165
+ function G(e) {
166
+ return new Promise((t) => setTimeout(t, e));
167
+ }
168
+ async function pe(e, t) {
169
+ var s;
170
+ let r;
171
+ try {
172
+ const o = await (await fetch(`${k(e)}/json/version`, { signal: AbortSignal.timeout(3e3) })).json();
173
+ if (!o.webSocketDebuggerUrl)
174
+ return { ok: !1, detail: "the browser exposes no debugger endpoint" };
175
+ r = o.webSocketDebuggerUrl;
176
+ } catch (o) {
177
+ return { ok: !1, detail: o instanceof Error ? o.message : String(o) };
178
+ }
179
+ const n = await S(r);
180
+ if (!n)
181
+ return { ok: !1, detail: "couldn't open a CDP socket to the browser" };
182
+ try {
183
+ const o = await X(n, "Extensions.loadUnpacked", { path: t });
184
+ if (o.error)
185
+ return { ok: !1, detail: o.error };
186
+ const a = (s = o.result) == null ? void 0 : s.id;
187
+ return a ? { ok: !0, extensionId: a } : { ok: !1, detail: "the browser accepted the load but named no extension" };
188
+ } finally {
189
+ b(n);
190
+ }
191
+ }
192
+ function X(e, t, r, n = 1e4) {
193
+ return new Promise((s) => {
194
+ const a = setTimeout(() => s({ error: `no reply to ${t}` }), n), l = (c) => {
195
+ let u;
196
+ try {
197
+ u = JSON.parse(String(c.data));
198
+ } catch {
199
+ return;
200
+ }
201
+ u.id === 1 && (clearTimeout(a), e.removeEventListener("message", l), s(u.error ? { error: u.error.message ?? t } : { result: u.result }));
202
+ };
203
+ e.addEventListener("message", l), e.send(JSON.stringify({ id: 1, method: t, params: r }));
204
+ });
205
+ }
206
+ async function C(e) {
207
+ const t = await fetch(`${k(e)}/json/list`, {
208
+ signal: AbortSignal.timeout(3e3)
209
+ });
210
+ if (!t.ok)
211
+ throw new Error(`the browser's debug endpoint answered ${t.status} ${t.statusText}`);
212
+ return await t.json();
213
+ }
214
+ function z(e, t) {
215
+ const r = `chrome-extension://${t}/`;
216
+ return e.filter((n) => n.url.startsWith(r));
217
+ }
218
+ async function me(e, t) {
219
+ const r = await L(e, t);
220
+ if (!r.ok)
221
+ return r;
222
+ const n = await P(
223
+ r.socket,
224
+ "chrome.runtime.reload()",
225
+ t.timeoutMs ?? 2e3,
226
+ !1
227
+ );
228
+ return await r.release(), n.threw ? r.via === "wake-page" ? { ok: !1, reason: "not-loaded" } : { ok: !1, reason: "failed", detail: n.threw } : { ok: !0, via: r.via };
229
+ }
230
+ async function ge(e, t) {
231
+ const r = await L(e, t);
232
+ if (!r.ok)
13
233
  return r;
14
- const o = (i = process.env.XDG_CACHE_HOME) == null ? void 0 : i.trim(), e = o && u(o) ? o : c(m(), ".cache");
15
- return c(e, f);
234
+ const n = await P(
235
+ r.socket,
236
+ t.expression,
237
+ t.timeoutMs ?? 5e3,
238
+ !0
239
+ );
240
+ return await r.release(), n.threw ? { ok: !1, reason: "failed", detail: n.threw } : { ok: !0, value: n.value };
241
+ }
242
+ async function L(e, t) {
243
+ var p;
244
+ const { extensionId: r, wakePage: n } = t;
245
+ let s;
246
+ try {
247
+ s = await C(e);
248
+ } catch (i) {
249
+ return {
250
+ ok: !1,
251
+ reason: "no-browser",
252
+ detail: i instanceof Error ? i.message : String(i)
253
+ };
254
+ }
255
+ const o = z(s, r).filter((i) => i.webSocketDebuggerUrl), a = o.find((i) => i.type === "service_worker"), l = o.find((i) => i.type === "page" || i.type === "other"), c = a ?? l;
256
+ if (c != null && c.webSocketDebuggerUrl) {
257
+ const i = await S(c.webSocketDebuggerUrl);
258
+ return i ? {
259
+ ok: !0,
260
+ socket: i,
261
+ via: a ? "service-worker" : "page",
262
+ release: async () => b(i)
263
+ } : { ok: !1, reason: "not-loaded" };
264
+ }
265
+ if (!n)
266
+ return { ok: !1, reason: "not-loaded" };
267
+ const u = `chrome-extension://${r}/${n.replace(/^\/+/, "")}`;
268
+ let d;
269
+ try {
270
+ d = await K(e, u);
271
+ } catch {
272
+ return { ok: !1, reason: "not-loaded" };
273
+ }
274
+ const h = d.webSocketDebuggerUrl ?? ((p = await Q(e, d.id)) == null ? void 0 : p.webSocketDebuggerUrl), w = h ? await S(h) : void 0;
275
+ return w ? {
276
+ ok: !0,
277
+ socket: w,
278
+ via: "wake-page",
279
+ release: async () => {
280
+ b(w), await x(e, d.id);
281
+ }
282
+ } : (await x(e, d.id), { ok: !1, reason: "not-loaded" });
283
+ }
284
+ async function P(e, t, r, n) {
285
+ return await new Promise((s) => {
286
+ const o = (l) => {
287
+ clearTimeout(a), s(l);
288
+ }, a = setTimeout(
289
+ () => o(n ? { threw: `no reply within ${r}ms` } : {}),
290
+ r
291
+ );
292
+ e.addEventListener("message", (l) => {
293
+ var h, w, p, i, y;
294
+ let c;
295
+ try {
296
+ c = JSON.parse(String(l.data));
297
+ } catch {
298
+ return;
299
+ }
300
+ if (c.id !== 1)
301
+ return;
302
+ const u = (h = c.result) == null ? void 0 : h.exceptionDetails, d = u ? ((w = u.exception) == null ? void 0 : w.description) ?? u.text : (p = c.error) == null ? void 0 : p.message;
303
+ o(d ? { threw: d } : { value: (y = (i = c.result) == null ? void 0 : i.result) == null ? void 0 : y.value });
304
+ }), e.addEventListener(
305
+ "close",
306
+ () => o(n ? { threw: "the context closed before answering" } : {})
307
+ ), e.addEventListener("error", () => o({ threw: "the CDP socket errored" })), e.send(
308
+ JSON.stringify({
309
+ id: 1,
310
+ method: "Runtime.evaluate",
311
+ params: { expression: t, awaitPromise: !0, returnByValue: !0 }
312
+ })
313
+ );
314
+ });
315
+ }
316
+ function S(e, t = 3e3) {
317
+ return new Promise((r) => {
318
+ const n = new WebSocket(e), s = setTimeout(() => {
319
+ b(n), r(void 0);
320
+ }, t);
321
+ n.addEventListener("open", () => {
322
+ clearTimeout(s), r(n);
323
+ }), n.addEventListener("error", () => {
324
+ clearTimeout(s), r(void 0);
325
+ });
326
+ });
327
+ }
328
+ function b(e) {
329
+ try {
330
+ e.close();
331
+ } catch {
332
+ }
333
+ }
334
+ async function K(e, t) {
335
+ const r = await fetch(`${k(e)}/json/new?${encodeURI(t)}`, {
336
+ method: "PUT",
337
+ signal: AbortSignal.timeout(5e3)
338
+ });
339
+ if (!r.ok)
340
+ throw new Error(`the browser refused to open ${t} (${r.status} ${r.statusText})`);
341
+ return await r.json();
342
+ }
343
+ async function Q(e, t) {
344
+ try {
345
+ return (await C(e)).find((r) => r.id === t);
346
+ } catch {
347
+ return;
348
+ }
349
+ }
350
+ async function x(e, t) {
351
+ try {
352
+ await fetch(`${k(e)}/json/close/${t}`, { signal: AbortSignal.timeout(3e3) });
353
+ } catch {
354
+ }
355
+ }
356
+ function k(e) {
357
+ return e.replace(/\/+$/, "");
358
+ }
359
+ const Z = B(import.meta.url);
360
+ function ee(e) {
361
+ const t = e.split("/");
362
+ for (const r of Z.resolve.paths(e) ?? []) {
363
+ const n = f(r, ...t, "package.json");
364
+ if (A(n))
365
+ return R(n);
366
+ }
367
+ throw new Error(`could not locate the "${e}" package (is it installed?)`);
368
+ }
369
+ function te(e) {
370
+ return A(f(e, "src"));
371
+ }
372
+ function be(e) {
373
+ return te(ee(e));
374
+ }
375
+ function ke(e, t) {
376
+ const r = new URL(e), n = new URL(t);
377
+ return r.protocol = n.protocol === "https:" ? "wss:" : "ws:", r.hostname = n.hostname, r.port = n.port, r.toString();
378
+ }
379
+ const re = "aiui";
380
+ function Se(e, t = {}) {
381
+ const { create: r = !0 } = t, n = e ? f(E(), e) : E();
382
+ return r && D(n, { recursive: !0 }), n;
383
+ }
384
+ function E() {
385
+ var n, s;
386
+ const e = (n = process.env.AIUI_CACHE) == null ? void 0 : n.trim();
387
+ if (e)
388
+ return e;
389
+ const t = (s = process.env.XDG_CACHE_HOME) == null ? void 0 : s.trim(), r = t && _(t) ? t : f(Y(), ".cache");
390
+ return f(r, re);
16
391
  }
17
392
  export {
18
- A as cacheDir
393
+ le as CHROME_CHANNELS,
394
+ Se as cacheDir,
395
+ he as decideBrowserAction,
396
+ J as discoverSessionBrowser,
397
+ ge as evaluateInExtension,
398
+ z as extensionTargets,
399
+ T as headlessReason,
400
+ M as isCi,
401
+ ue as isHeadless,
402
+ F as isSsh,
403
+ fe as launchSessionBrowser,
404
+ C as listBrowserTargets,
405
+ pe as loadUnpackedExtension,
406
+ we as openInSessionBrowser,
407
+ be as packageFromSource,
408
+ ee as packageRoot,
409
+ ke as rehostSocketUrl,
410
+ me as reloadExtension,
411
+ te as runningFromSource,
412
+ de as sessionBrowserBinary
19
413
  };
20
414
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * Shared utilities for the aiui packages.\n *\n * @packageDocumentation\n */\n\nimport { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { isAbsolute, join } from \"node:path\";\n\n/** The application name — the leaf under the platform cache directory. */\nconst APP = \"aiui\";\n\nexport interface CacheDirOptions {\n /**\n * Create the directory (recursively) if it doesn't exist. Defaults to `true` —\n * callers generally want a ready-to-use directory back.\n */\n create?: boolean;\n}\n\n/**\n * Resolve the cache directory for aiui, creating it by default.\n *\n * Resolution order:\n * 1. `$AIUI_CACHE` — an explicit override; used verbatim as the aiui cache root.\n * 2. `$XDG_CACHE_HOME/aiui` — honoring the XDG Base Directory spec (the variable\n * is ignored unless it's an absolute path, per the spec).\n * 3. `~/.cache/aiui` — the XDG default.\n *\n * Pass `namespace` to carve out a subdirectory for a particular kind of cached\n * data, e.g. `cacheDir(\"claude\")` → `~/.cache/aiui/claude`. Different callers\n * cache different things, so each should pick its own namespace rather than\n * writing into the shared root.\n *\n * @example\n * const dir = cacheDir(\"screenshots\"); // created, ready to write into\n * const dir = cacheDir(\"claude\", { create: false }); // resolve the path only\n */\nexport function cacheDir(namespace?: string, options: CacheDirOptions = {}): string {\n const { create = true } = options;\n const dir = namespace ? join(cacheRoot(), namespace) : cacheRoot();\n if (create) {\n mkdirSync(dir, { recursive: true });\n }\n return dir;\n}\n\n/** Resolve the aiui cache root (no directory creation). */\nfunction cacheRoot(): string {\n const override = process.env.AIUI_CACHE?.trim();\n if (override) {\n return override;\n }\n\n const xdg = process.env.XDG_CACHE_HOME?.trim();\n const cacheHome = xdg && isAbsolute(xdg) ? xdg : join(homedir(), \".cache\");\n return join(cacheHome, APP);\n}\n"],"names":["APP","cacheDir","namespace","options","create","dir","join","cacheRoot","mkdirSync","override","_a","xdg","_b","cacheHome","isAbsolute","homedir"],"mappings":";;;AAWA,MAAMA,IAAM;AA4BL,SAASC,EAASC,GAAoBC,IAA2B,IAAY;AAClF,QAAM,EAAE,QAAAC,IAAS,GAAA,IAASD,GACpBE,IAAMH,IAAYI,EAAKC,KAAaL,CAAS,IAAIK,EAAA;AACvD,SAAIH,KACFI,EAAUH,GAAK,EAAE,WAAW,GAAA,CAAM,GAE7BA;AACT;AAGA,SAASE,IAAoB;;AAC3B,QAAME,KAAWC,IAAA,QAAQ,IAAI,eAAZ,gBAAAA,EAAwB;AACzC,MAAID;AACF,WAAOA;AAGT,QAAME,KAAMC,IAAA,QAAQ,IAAI,mBAAZ,gBAAAA,EAA4B,QAClCC,IAAYF,KAAOG,EAAWH,CAAG,IAAIA,IAAML,EAAKS,EAAA,GAAW,QAAQ;AACzE,SAAOT,EAAKO,GAAWb,CAAG;AAC5B;"}
1
+ {"version":3,"file":"index.js","sources":["../src/environment.ts","../src/browser.ts","../src/extension.ts","../src/provenance.ts","../src/socket-url.ts","../src/index.ts"],"sourcesContent":["/**\n * Where is this process running — CI? over SSH? a machine with no display?\n *\n * Several commands adapt to their surroundings: `aiui claude` leaves the\n * Chrome DevTools MCP off under CI, and `aiui vite` only\n * auto-open their dev server in the session browser when there is a display\n * to show it on. Those decisions share the detection logic here — kept pure\n * (`env` and `platform` are parameters defaulting to the real ones) so every\n * branch is unit-testable without mutating the test process's own\n * environment.\n *\n * The signals, and why each is trusted:\n *\n * - **CI** — effectively every provider (GitHub Actions, GitLab, CircleCI,\n * Travis, Buildkite, …) sets `CI=true`. A CI runner may technically have a\n * display, but nobody is watching it, which is what \"headless\" means for\n * the decisions built on this module.\n * - **SSH** — sshd sets `SSH_CONNECTION` and `SSH_CLIENT` in every session\n * and `SSH_TTY` in interactive ones. Any of them means this shell lives at\n * the far end of a network connection: a browser launched here would render\n * on the remote box's display (if it even has one), never in front of the\n * user. X11 forwarding (`ssh -X`, which sets `DISPLAY`) is deliberately\n * *not* honored as an exception — it's rare, slow, and almost never where\n * the user wants a Chrome window; `--aiui-browser`-style force flags are\n * the escape hatch.\n * - **Linux display** — X11 clients find their server via `DISPLAY`; Wayland\n * clients via `WAYLAND_DISPLAY`. Neither being set means there is no\n * compositor to render a window into.\n * - **macOS** — has no display env-var convention; WindowServer is always up\n * on a booted Mac, so assume a GUI unless SSH says the shell is remote.\n * - **Other platforms** (Windows) likewise have no display env var: assume a\n * GUI and rely on the CI/SSH signals alone.\n */\n\n/** Truthy `CI` env var, with the conventional \"false\"/\"0\" escape hatches. */\nexport function isCi(env: NodeJS.ProcessEnv = process.env): boolean {\n const ci = env.CI;\n return ci !== undefined && ci !== \"\" && ci !== \"0\" && ci.toLowerCase() !== \"false\";\n}\n\n/**\n * Whether this shell is an SSH session. Checks all three of sshd's markers —\n * `SSH_TTY` is only set for interactive sessions, and `SSH_CONNECTION` /\n * `SSH_CLIENT` can be scrubbed by a paranoid shell profile, so any one of\n * them is taken at its word. Empty values count as unset.\n */\nexport function isSsh(env: NodeJS.ProcessEnv = process.env): boolean {\n return isSet(env.SSH_CONNECTION) || isSet(env.SSH_TTY) || isSet(env.SSH_CLIENT);\n}\n\n/**\n * Why this environment is considered headless — a short human-readable\n * fragment for messages like \"detected a headless environment (`<reason>`)\" —\n * or undefined when a GUI is presumed available. The checks run most-explicit\n * first (CI, then SSH, then the platform's display convention); see the\n * module doc for the reasoning behind each signal.\n */\nexport function headlessReason(\n env: NodeJS.ProcessEnv = process.env,\n platform: NodeJS.Platform = process.platform,\n): string | undefined {\n if (isCi(env)) {\n return \"the CI environment variable is set\";\n }\n if (isSsh(env)) {\n return \"this is an SSH session\";\n }\n if (platform === \"linux\" && !isSet(env.DISPLAY) && !isSet(env.WAYLAND_DISPLAY)) {\n return \"Linux with neither DISPLAY nor WAYLAND_DISPLAY set\";\n }\n return undefined;\n}\n\n/** {@link headlessReason} as a boolean, for callers that don't print it. */\nexport function isHeadless(\n env: NodeJS.ProcessEnv = process.env,\n platform: NodeJS.Platform = process.platform,\n): boolean {\n return headlessReason(env, platform) !== undefined;\n}\n\n/** Set and non-empty — `VAR=` in a profile shouldn't count as a signal. */\nfunction isSet(value: string | undefined): boolean {\n return value !== undefined && value !== \"\";\n}\n","/**\n * The session browser: one user-visible Chrome shared by the human and the\n * agent.\n *\n * In the aiui CLI's default \"attach\" mode, `aiui claude` launches the browser\n * itself — with a DevTools debug port, the project profile, and the aiui\n * extensions (DevTools panel, intent tool) — and chrome-devtools-mcp\n * *attaches* to it\n * (`--browser-url`) instead of launching a private one. That's what makes the\n * agent's browser the same window the human is looking at: shared tabs,\n * shared state, visible from session start.\n *\n * This module is the shared plumbing under that story — discovery, launch,\n * open-a-tab, and the auto-open decision ladder — so every dev-server sidecar\n * (`aiui vite`) puts its page in the same\n * shared window instead of re-deriving the mechanics. The aiui CLI layers its\n * own affordances on top (config resolution, Chrome for Testing sync, the\n * devtools-extension autoload); nothing here reads config or prompts.\n *\n * There is deliberately no registry file for browsers. Chrome itself writes\n * `DevToolsActivePort` into the user data dir of any instance started with a\n * debug port, and the user data dir is already the profile's identity — so\n * discovery is: read that file, confirm the endpoint answers `/json/version`.\n * A dead file (crash leftovers, stale port) just fails the liveness probe.\n *\n * Security note (documented in docs/guide/warning): the debug endpoint is\n * unauthenticated — any local process can drive the browser through it. It\n * binds to loopback only.\n */\nimport { mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { Browser, ChromeReleaseChannel, computeSystemExecutablePath } from \"@puppeteer/browsers\";\nimport { execa } from \"execa\";\nimport { headlessReason } from \"./environment\";\n\n/** Chrome writes this into the user data dir when debugging is enabled. */\nconst ACTIVE_PORT_FILE = \"DevToolsActivePort\";\n\n/** How long a fresh Chrome gets to bring up its debug endpoint. */\nconst LAUNCH_TIMEOUT_MS = 20_000;\n\n/** The installed-Chrome release channels a launch can target. */\nexport const CHROME_CHANNELS = [\"stable\", \"beta\", \"dev\", \"canary\"] as const;\nexport type ChromeChannel = (typeof CHROME_CHANNELS)[number];\n\nexport interface SessionBrowser {\n /** The DevTools endpoint chrome-devtools-mcp attaches to. */\n browserUrl: string;\n port: number;\n}\n\n/**\n * A live session browser for this profile, if one is running.\n * Never launches anything; safe to call from non-interactive paths.\n */\nexport async function discoverSessionBrowser(\n userDataDir: string,\n): Promise<SessionBrowser | undefined> {\n const port = readActivePort(userDataDir);\n if (port === undefined) {\n return undefined;\n }\n if (!(await debugEndpointAlive(port))) {\n return undefined;\n }\n return { browserUrl: `http://127.0.0.1:${port}`, port };\n}\n\nfunction readActivePort(userDataDir: string): number | undefined {\n try {\n const [first] = readFileSync(join(userDataDir, ACTIVE_PORT_FILE), \"utf8\").split(\"\\n\");\n const port = Number(first);\n return Number.isInteger(port) && port > 0 ? port : undefined;\n } catch {\n return undefined;\n }\n}\n\nasync function debugEndpointAlive(port: number): Promise<boolean> {\n try {\n const res = await fetch(`http://127.0.0.1:${port}/json/version`, {\n signal: AbortSignal.timeout(1000),\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\n/**\n * Which binary a session-browser launch should run: an explicit\n * executablePath (usually the managed Chrome for Testing) wins; otherwise the\n * system install of the requested channel (default stable). Throws when no\n * such browser is installed.\n */\nexport function sessionBrowserBinary(settings: {\n executablePath?: string;\n channel?: ChromeChannel;\n}): string {\n if (settings.executablePath) {\n return settings.executablePath;\n }\n return computeSystemExecutablePath({\n browser: Browser.CHROME,\n channel: RELEASE_CHANNELS[settings.channel ?? \"stable\"],\n });\n}\n\nconst RELEASE_CHANNELS: Record<ChromeChannel, ChromeReleaseChannel> = {\n stable: ChromeReleaseChannel.STABLE,\n beta: ChromeReleaseChannel.BETA,\n dev: ChromeReleaseChannel.DEV,\n canary: ChromeReleaseChannel.CANARY,\n};\n\n/**\n * Launch the session browser detached (it deliberately outlives the launching\n * process — it's the user's window too) and wait for its debug endpoint.\n *\n * `debugPort` 0 lets the OS pick a free port; Chrome reports the choice via\n * `DevToolsActivePort`, which is removed first so a stale file from a previous\n * run can't win the poll. Fails fast if the process exits early — the classic\n * cause being an already-running Chrome on the same profile *without* a debug\n * port, which swallows the new invocation as a URL-handoff and exits.\n */\nexport async function launchSessionBrowser(opts: {\n binary: string;\n userDataDir: string;\n debugPort?: number;\n /** Unpacked extensions to load (comma-joined into one `--load-extension`). */\n extensionDirs?: string[];\n headless?: boolean;\n startUrl?: string;\n}): Promise<SessionBrowser> {\n mkdirSync(opts.userDataDir, { recursive: true });\n rmSync(join(opts.userDataDir, ACTIVE_PORT_FILE), { force: true });\n\n const args = [\n `--remote-debugging-port=${opts.debugPort ?? 0}`,\n `--user-data-dir=${opts.userDataDir}`,\n \"--no-first-run\",\n \"--no-default-browser-check\",\n // Media prompts, pre-answered — this is a *dev* browser (unauthenticated\n // debug port, project-local profile; see docs/guide/warning.md), and the\n // intent tool's dictation + screenshots otherwise re-prompt constantly:\n // Chrome scopes mic permission per-origin (every dev-server PORT is its\n // own origin), and the getDisplayMedia share picker can never be\n // persisted at all.\n // - auto-accept camera/microphone permission prompts (the *default, real*\n // devices — fake devices only come from the separate\n // --use-fake-device-for-media-stream, deliberately not passed). NOT the\n // older --use-fake-ui-for-media-stream: that flag also hijacks the\n // getDisplayMedia picker and auto-selects the ENTIRE SCREEN, which\n // needs macOS Screen Recording permission the CfT binary doesn't have —\n // every capture then dies with NotReadableError (\"Could not start\n // video source\"), silently defeating the tab-capture flag below (this\n // broke the paint host's screen share; verified against CfT 150).\n \"--auto-accept-camera-and-microphone-capture\",\n // - auto-accept a current-tab share (getDisplayMedia({ preferCurrentTab:\n // true })) — no picker, no gesture, no OS-level screen-recording grant.\n // No shipped page calls getDisplayMedia any more (the intent client's\n // hosts capture natively), but the flag is kept: it is harmless, and a\n // scratch page an agent writes can rely on it. Verified against CfT\n // 150: the call resolves in ~320ms with userActivation.isActive false.\n // Trap for anyone re-measuring: a Chrome spawned from a process that\n // lacks the macOS Screen Recording grant inherits that lack, and the\n // call HANGS instead — test from a real terminal, or headless.\n \"--auto-accept-this-tab-capture\",\n // - allow audio PLAYBACK without a user gesture — the outbound half of the\n // same media posture (the two flags above pre-answer capture). The\n // intent client's panels play server-pushed speech (the linter's spoken\n // notes, TTS acks) via Audio.play(), and with keys forwarded from the\n // target tab the panel document may never receive the gesture Chrome's\n // autoplay policy wants — the clip would be refused with\n // NotAllowedError. The SpeechPlayer parks blocked clips and resumes on\n // a gesture (dev-overlay speech.ts), so outside this browser nothing is\n // lost — but in the session browser the linter should simply be HEARD.\n \"--autoplay-policy=no-user-gesture-required\",\n ];\n if (opts.extensionDirs?.length) {\n args.push(`--load-extension=${opts.extensionDirs.join(\",\")}`);\n }\n if (opts.headless) {\n args.push(\"--headless\");\n }\n args.push(opts.startUrl ?? \"about:blank\");\n\n const child = execa(opts.binary, args, {\n detached: true,\n stdio: \"ignore\",\n reject: false,\n cleanup: false,\n });\n child.unref();\n let exited = false;\n void child.then(() => {\n exited = true;\n });\n\n const deadline = Date.now() + LAUNCH_TIMEOUT_MS;\n while (Date.now() < deadline) {\n const found = await discoverSessionBrowser(opts.userDataDir);\n if (found) {\n // Purely informational breadcrumb (pid, when) for humans poking around.\n try {\n writeFileSync(\n join(opts.userDataDir, \"aiui-browser.json\"),\n `${JSON.stringify({ pid: child.pid, startedAt: new Date().toISOString() })}\\n`,\n );\n } catch {}\n return found;\n }\n if (exited) {\n throw new Error(\n \"the browser exited before exposing its DevTools endpoint — is another Chrome \" +\n \"already running on this profile without a debug port? Close it and retry.\",\n );\n }\n await sleep(250);\n }\n throw new Error(\n `the browser did not expose its DevTools endpoint within ${LAUNCH_TIMEOUT_MS / 1000}s`,\n );\n}\n\n/**\n * Open a URL as a new tab in a session browser, via the DevTools HTTP API\n * (`PUT /json/new` — PUT is required by current Chrome).\n */\nexport async function openInSessionBrowser(browserUrl: string, url: string): Promise<void> {\n const base = browserUrl.replace(/\\/+$/, \"\");\n const res = await fetch(`${base}/json/new?${encodeURI(url)}`, {\n method: \"PUT\",\n signal: AbortSignal.timeout(3000),\n });\n if (!res.ok) {\n throw new Error(`the browser refused to open the tab (${res.status} ${res.statusText})`);\n }\n}\n\n/** The force/suppress escape hatches a sidecar's caller can express. */\nexport interface BrowserAutoOpenFlags {\n /** Force opening even under CI/SSH/no-display (`--aiui-browser`, `WORKBENCH_BROWSER=1`). */\n browser?: boolean;\n /** Never open a browser for this run (`--aiui-no-browser`, `WORKBENCH_BROWSER=0`). */\n noBrowser?: boolean;\n}\n\n/** The config a sidecar's project may have voted with (aiui's `chrome` section). */\nexport interface BrowserAutoOpenConfig {\n /** `false` opts the project out of browser integration wholesale. */\n enabled?: boolean;\n /** A browser managed elsewhere (usually reverse-tunneled) — attach there. */\n browserUrl?: string;\n}\n\n/** What a browser sidecar should do once its dev-server URL is known. */\nexport type BrowserAction = { kind: \"open\" } | { kind: \"skip\" } | { kind: \"hint\"; reason: string };\n\n/**\n * Decide the browser sidecar's move — pure (env and platform are parameters)\n * so every rung is unit-testable. The ladder, most explicit first, mirrors\n * the aiui CLI's flag-beats-config ordering:\n *\n * 1. Suppress flag → skip, silently. The user said no for this run.\n * 2. Force flag → open, even under CI, over SSH, or with\n * `chrome.enabled: false` — the force flag exists precisely to overrule\n * the defaults (e.g. the dev box is \"headless\" but its display is a\n * forwarded port away).\n * 3. `chrome.enabled: false` → skip: the config opted this project out of\n * browser integration wholesale, same as it disables the DevTools MCP.\n * 4. A configured `chrome.browserUrl` → open. The browser deliberately lives\n * elsewhere (typically the user's local machine, reverse-tunneled — see\n * docs/guide/remote), so *this* machine being headless is irrelevant:\n * opening a tab there is exactly the point of the setup.\n * 5. CI or headless (see ./environment) → don't launch a browser nobody\n * can see; hand back the reason so the caller can print the\n * port-forwarding hint instead.\n * 6. Otherwise → open.\n */\nexport function decideBrowserAction(\n args: BrowserAutoOpenFlags,\n config: BrowserAutoOpenConfig = {},\n env: NodeJS.ProcessEnv = process.env,\n platform: NodeJS.Platform = process.platform,\n): BrowserAction {\n if (args.noBrowser) {\n return { kind: \"skip\" };\n }\n if (args.browser) {\n return { kind: \"open\" };\n }\n if (config.enabled === false) {\n return { kind: \"skip\" };\n }\n if (config.browserUrl) {\n return { kind: \"open\" };\n }\n const reason = headlessReason(env, platform);\n if (reason) {\n return { kind: \"hint\", reason };\n }\n return { kind: \"open\" };\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * Talking to an unpacked extension in the session browser, over raw CDP:\n * reload it, and ask it what it is actually running.\n *\n * `chrome.runtime.reload()` is the only thing that makes Chrome re-read an\n * unpacked extension's directory — which is exactly what the CRXJS dev loop\n * needs after every dev-server start (the artifact on disk was just rewritten;\n * Chrome is still holding the previous run's snapshot). Doing it by hand means\n * a trip to chrome://extensions and a click; doing it in the wrong order —\n * while Vite is still writing — is what strands people with a blank panel. So\n * the CLI owns it: wait for the artifact, reload, then *verify* by reading the\n * extension's own dev stamp back out of the browser.\n *\n * Chrome exposes no HTTP verb for any of this, but every extension context can\n * call `chrome.runtime.reload()` and `fetch(chrome.runtime.getURL(…))` itself,\n * and the DevTools protocol can evaluate in any of them. Two ways in, in order:\n *\n * 1. **An existing extension target** — the MV3 service worker, or any open\n * extension page. Cheap and invisible.\n * 2. **A wake page** — MV3 service workers idle-terminate (~30s), so quite\n * often there is *no* extension target at all. We then open one of the\n * extension's own pages in a background tab, evaluate there, and close it.\n * The extension supplies a page that is safe to open for this purpose\n * (aiui-extension ships `reload.html`, which is inert).\n */\n\n/** A DevTools target as listed by `GET /json/list`. */\nexport interface BrowserTarget {\n id: string;\n type: string;\n url: string;\n title?: string;\n webSocketDebuggerUrl?: string;\n}\n\n/** How to reach a context inside one extension. */\nexport interface ExtensionContextOptions {\n /** The extension id (aiui's is pinned by the manifest key). */\n extensionId: string;\n /**\n * An inert page inside the extension, opened in a background tab when no\n * extension context exists to evaluate in. Without it, a sleeping service\n * worker means \"not-loaded\".\n */\n wakePage?: string;\n /** How long to wait for a reply. */\n timeoutMs?: number;\n}\n\n/** Why we couldn't run something inside the extension. */\nexport type ExtensionFailure =\n /** The browser has no context for this extension — it isn't loaded. */\n | { ok: false; reason: \"not-loaded\" }\n /** The browser's debug endpoint didn't answer. */\n | { ok: false; reason: \"no-browser\"; detail: string }\n /** We reached a context, but the expression threw in it. */\n | { ok: false; reason: \"failed\"; detail: string };\n\nexport type ReloadExtensionResult =\n | { ok: true; via: \"service-worker\" | \"page\" | \"wake-page\" }\n | ExtensionFailure;\n\nexport type EvaluateInExtensionResult<T> = { ok: true; value: T } | ExtensionFailure;\n\n/**\n * Install (or re-point) an unpacked extension in a running browser — CDP's\n * `Extensions.loadUnpacked`, i.e. \"Load unpacked\" without the human.\n *\n * This is what makes the two-artifact layout painless: Chrome installs an\n * unpacked extension **by path**, so a browser that was launched against `dist/`\n * would otherwise ignore the dev server's output forever, and no amount of\n * reloading could tell it otherwise — only a trip to chrome://extensions. With\n * this, the CLI just points it at the right directory. The extension id is\n * unchanged (it comes from the manifest key, not the path), so nothing that\n * depends on the id — the native-messaging host's `allowed_origins`, above all —\n * needs to know this happened.\n *\n * Best-effort by design: the domain is recent (Chrome ≥ 129, measured working on\n * Chrome for Testing 150) and a browser may refuse. Callers fall back to telling\n * the human what to click.\n */\nexport async function loadUnpackedExtension(\n browserUrl: string,\n path: string,\n): Promise<{ ok: true; extensionId: string } | { ok: false; detail: string }> {\n let endpoint: string;\n try {\n const version = (await (\n await fetch(`${trim(browserUrl)}/json/version`, { signal: AbortSignal.timeout(3000) })\n ).json()) as { webSocketDebuggerUrl?: string };\n if (!version.webSocketDebuggerUrl) {\n return { ok: false, detail: \"the browser exposes no debugger endpoint\" };\n }\n endpoint = version.webSocketDebuggerUrl;\n } catch (error) {\n return { ok: false, detail: error instanceof Error ? error.message : String(error) };\n }\n\n const socket = await connect(endpoint);\n if (!socket) {\n return { ok: false, detail: \"couldn't open a CDP socket to the browser\" };\n }\n try {\n const reply = await request(socket, \"Extensions.loadUnpacked\", { path });\n if (reply.error) {\n return { ok: false, detail: reply.error };\n }\n const id = (reply.result as { id?: string } | undefined)?.id;\n return id\n ? { ok: true, extensionId: id }\n : { ok: false, detail: \"the browser accepted the load but named no extension\" };\n } finally {\n close(socket);\n }\n}\n\n/** One CDP request/response on an open socket. */\nfunction request(\n socket: WebSocket,\n method: string,\n params: unknown,\n timeoutMs = 10_000,\n): Promise<{ result?: unknown; error?: string }> {\n return new Promise((resolve) => {\n const id = 1;\n const timer = setTimeout(() => resolve({ error: `no reply to ${method}` }), timeoutMs);\n const onMessage = (event: MessageEvent) => {\n let reply: { id?: number; result?: unknown; error?: { message?: string } };\n try {\n reply = JSON.parse(String(event.data));\n } catch {\n return;\n }\n if (reply.id !== id) {\n return;\n }\n clearTimeout(timer);\n socket.removeEventListener(\"message\", onMessage);\n resolve(reply.error ? { error: reply.error.message ?? method } : { result: reply.result });\n };\n socket.addEventListener(\"message\", onMessage);\n socket.send(JSON.stringify({ id, method, params }));\n });\n}\n\n/** `GET /json/list` — every target the browser is willing to show us. */\nexport async function listBrowserTargets(browserUrl: string): Promise<BrowserTarget[]> {\n const res = await fetch(`${trim(browserUrl)}/json/list`, {\n signal: AbortSignal.timeout(3000),\n });\n if (!res.ok) {\n throw new Error(`the browser's debug endpoint answered ${res.status} ${res.statusText}`);\n }\n return (await res.json()) as BrowserTarget[];\n}\n\n/** Targets belonging to one extension (service worker, pages, offscreen docs). */\nexport function extensionTargets(targets: BrowserTarget[], extensionId: string): BrowserTarget[] {\n const prefix = `chrome-extension://${extensionId}/`;\n return targets.filter((t) => t.url.startsWith(prefix));\n}\n\n/**\n * Make the browser re-read the extension's directory.\n *\n * The evaluation destroys the context it runs in, so the reply never arrives —\n * \"no answer\" is success here, not failure. Callers that need certainty read\n * the extension back afterwards ({@link evaluateInExtension}).\n */\nexport async function reloadExtension(\n browserUrl: string,\n options: ExtensionContextOptions,\n): Promise<ReloadExtensionResult> {\n const context = await extensionContext(browserUrl, options);\n if (!context.ok) {\n return context;\n }\n const threw = await evaluate(\n context.socket,\n \"chrome.runtime.reload()\",\n options.timeoutMs ?? 2000,\n false,\n );\n await context.release();\n // A wake page that failed to load answers with an exception instead of dying:\n // that means the extension isn't there, not that the reload broke.\n if (threw.threw) {\n return context.via === \"wake-page\"\n ? { ok: false, reason: \"not-loaded\" }\n : { ok: false, reason: \"failed\", detail: threw.threw };\n }\n return { ok: true, via: context.via };\n}\n\n/**\n * Run an expression *inside* the extension and get its value back — how the\n * CLI asks \"which artifact are you actually running?\" (the extension fetches\n * its own dev stamp; see aiui-webext's dev-stamp module). Promises are awaited.\n */\nexport async function evaluateInExtension<T>(\n browserUrl: string,\n options: ExtensionContextOptions & { expression: string },\n): Promise<EvaluateInExtensionResult<T>> {\n const context = await extensionContext(browserUrl, options);\n if (!context.ok) {\n return context;\n }\n const result = await evaluate(\n context.socket,\n options.expression,\n options.timeoutMs ?? 5000,\n true,\n );\n await context.release();\n if (result.threw) {\n return { ok: false, reason: \"failed\", detail: result.threw };\n }\n return { ok: true, value: result.value as T };\n}\n\n/** A live CDP socket into some context of the extension, plus how to let go. */\ntype ExtensionContext =\n | {\n ok: true;\n socket: WebSocket;\n via: \"service-worker\" | \"page\" | \"wake-page\";\n release: () => Promise<void>;\n }\n | ExtensionFailure;\n\nasync function extensionContext(\n browserUrl: string,\n options: ExtensionContextOptions,\n): Promise<ExtensionContext> {\n const { extensionId, wakePage } = options;\n\n let targets: BrowserTarget[];\n try {\n targets = await listBrowserTargets(browserUrl);\n } catch (error) {\n return {\n ok: false,\n reason: \"no-browser\",\n detail: error instanceof Error ? error.message : String(error),\n };\n }\n\n const own = extensionTargets(targets, extensionId).filter((t) => t.webSocketDebuggerUrl);\n const worker = own.find((t) => t.type === \"service_worker\");\n const page = own.find((t) => t.type === \"page\" || t.type === \"other\");\n const existing = worker ?? page;\n if (existing?.webSocketDebuggerUrl) {\n const socket = await connect(existing.webSocketDebuggerUrl);\n return socket\n ? {\n ok: true,\n socket,\n via: worker ? \"service-worker\" : \"page\",\n release: async () => close(socket),\n }\n : { ok: false, reason: \"not-loaded\" };\n }\n\n if (!wakePage) {\n return { ok: false, reason: \"not-loaded\" };\n }\n\n // No context to evaluate in (an idle MV3 worker leaves none): open one. A tab\n // at an extension URL only loads if the extension is installed — a failure\n // here IS \"not loaded\".\n const url = `chrome-extension://${extensionId}/${wakePage.replace(/^\\/+/, \"\")}`;\n let opened: BrowserTarget;\n try {\n opened = await openTarget(browserUrl, url);\n } catch {\n return { ok: false, reason: \"not-loaded\" };\n }\n const ws =\n opened.webSocketDebuggerUrl ?? (await findTarget(browserUrl, opened.id))?.webSocketDebuggerUrl;\n const socket = ws ? await connect(ws) : undefined;\n if (!socket) {\n await closeTarget(browserUrl, opened.id);\n return { ok: false, reason: \"not-loaded\" };\n }\n return {\n ok: true,\n socket,\n via: \"wake-page\",\n release: async () => {\n close(socket);\n // Chrome tears extension pages down on reload, so this often 404s — the\n // tab is already gone. Best-effort either way.\n await closeTarget(browserUrl, opened.id);\n },\n };\n}\n\n/**\n * Evaluate an expression in a target. `expectReply: false` means the expression\n * is expected to kill its own context (`chrome.runtime.reload()`), so a closed\n * socket is the success signal, not an error.\n */\nasync function evaluate(\n socket: WebSocket,\n expression: string,\n timeoutMs: number,\n expectReply: boolean,\n): Promise<{ value?: unknown; threw?: string }> {\n return await new Promise((resolve) => {\n const done = (result: { value?: unknown; threw?: string }) => {\n clearTimeout(timer);\n resolve(result);\n };\n const timer = setTimeout(\n () => done(expectReply ? { threw: `no reply within ${timeoutMs}ms` } : {}),\n timeoutMs,\n );\n socket.addEventListener(\"message\", (event) => {\n let reply: {\n id?: number;\n error?: { message?: string };\n result?: {\n result?: { value?: unknown };\n exceptionDetails?: { exception?: { description?: string }; text?: string };\n };\n };\n try {\n reply = JSON.parse(String(event.data));\n } catch {\n return;\n }\n if (reply.id !== 1) {\n return; // an unrelated CDP event on the same socket\n }\n const ex = reply.result?.exceptionDetails;\n const threw = ex ? (ex.exception?.description ?? ex.text) : reply.error?.message;\n done(threw ? { threw } : { value: reply.result?.result?.value });\n });\n socket.addEventListener(\"close\", () =>\n done(expectReply ? { threw: \"the context closed before answering\" } : {}),\n );\n socket.addEventListener(\"error\", () => done({ threw: \"the CDP socket errored\" }));\n socket.send(\n JSON.stringify({\n id: 1,\n method: \"Runtime.evaluate\",\n params: { expression, awaitPromise: true, returnByValue: true },\n }),\n );\n });\n}\n\nfunction connect(url: string, timeoutMs = 3000): Promise<WebSocket | undefined> {\n return new Promise((resolve) => {\n const socket = new WebSocket(url);\n const timer = setTimeout(() => {\n close(socket);\n resolve(undefined);\n }, timeoutMs);\n socket.addEventListener(\"open\", () => {\n clearTimeout(timer);\n resolve(socket);\n });\n socket.addEventListener(\"error\", () => {\n clearTimeout(timer);\n resolve(undefined);\n });\n });\n}\n\nfunction close(socket: WebSocket): void {\n try {\n socket.close();\n } catch {}\n}\n\n/** `PUT /json/new?<url>` — the DevTools HTTP API's \"open a tab\". */\nasync function openTarget(browserUrl: string, url: string): Promise<BrowserTarget> {\n const res = await fetch(`${trim(browserUrl)}/json/new?${encodeURI(url)}`, {\n method: \"PUT\",\n signal: AbortSignal.timeout(5000),\n });\n if (!res.ok) {\n throw new Error(`the browser refused to open ${url} (${res.status} ${res.statusText})`);\n }\n return (await res.json()) as BrowserTarget;\n}\n\nasync function findTarget(browserUrl: string, id: string): Promise<BrowserTarget | undefined> {\n try {\n return (await listBrowserTargets(browserUrl)).find((t) => t.id === id);\n } catch {\n return undefined;\n }\n}\n\nasync function closeTarget(browserUrl: string, id: string): Promise<void> {\n try {\n await fetch(`${trim(browserUrl)}/json/close/${id}`, { signal: AbortSignal.timeout(3000) });\n } catch {}\n}\n\nfunction trim(browserUrl: string): string {\n return browserUrl.replace(/\\/+$/, \"\");\n}\n","/**\n * Was a package obtained as an editable **source checkout** (this monorepo) or\n * **installed** from a published tarball? `resolvePackageCli` (in the aiui CLI)\n * uses this to decide how to spawn a workspace CLI: through tsx straight from\n * `src/` in a dev checkout, or plain `node` on `dist/` once installed.\n *\n * The signal is a filesystem fact, not an env var: a published tarball ships\n * only its `dist/` (the `files` allowlist excludes `src/`), so a package directory\n * that still carries a `src/` folder is a dev checkout. Locate a package's root\n * with {@link packageRoot}, then ask {@link runningFromSource} about it — or use\n * {@link packageFromSource} for both in one call.\n */\nimport { existsSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\n\n// Resolve modules the way this package would at runtime.\nconst nodeRequire = createRequire(import.meta.url);\n\n/**\n * Absolute root directory of an installed (or workspace-linked) dependency.\n *\n * Rather than resolve the package *through* the module system — which would hit\n * its `exports` map (forcing a built `dist/`, and normally blocking access to\n * `package.json`) — we ask Node for the `node_modules` dirs it would search and\n * read `package.json` straight off disk. This needs nothing special from the\n * target package (no `exports` entry) and works even when it has not been built,\n * so dev iteration requires no compile step.\n */\nexport function packageRoot(packageName: string): string {\n const segments = packageName.split(\"/\");\n for (const base of nodeRequire.resolve.paths(packageName) ?? []) {\n const manifest = join(base, ...segments, \"package.json\");\n if (existsSync(manifest)) {\n return dirname(manifest);\n }\n }\n throw new Error(`could not locate the \"${packageName}\" package (is it installed?)`);\n}\n\n/**\n * Whether a package directory is an editable source checkout (it still carries\n * a `src/` folder) rather than an installed tarball (which ships only `dist/`).\n * Pass the package's own root, e.g. from {@link packageRoot}.\n */\nexport function runningFromSource(packageDir: string): boolean {\n return existsSync(join(packageDir, \"src\"));\n}\n\n/**\n * {@link runningFromSource} for a package resolved by name — the common case\n * (\"is `@habemus-papadum/aiui-dev-overlay` a source checkout here?\").\n */\nexport function packageFromSource(packageName: string): boolean {\n return runningFromSource(packageRoot(packageName));\n}\n","/**\n * Re-host a Chrome DevTools websocket URL onto the endpoint that answered.\n *\n * Chrome reports its browser socket as `ws://127.0.0.1:<its own port>/…`, which\n * is a lie from anywhere but that machine — a tunneled remote browser\n * (docs/guide/remote) is reached at the forwarded host:port we just fetched\n * from. Keep the path (the browser's session id) and take the authority from\n * the endpoint that answered.\n */\nexport function rehostSocketUrl(webSocketDebuggerUrl: string, browserUrl: string): string {\n const socket = new URL(webSocketDebuggerUrl);\n const endpoint = new URL(browserUrl);\n socket.protocol = endpoint.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n // hostname + port, never `host`: assigning a host with no port component\n // leaves the OLD port in place, so a default-port endpoint would inherit\n // Chrome's self-reported one.\n socket.hostname = endpoint.hostname;\n socket.port = endpoint.port;\n return socket.toString();\n}\n","/**\n * Shared utilities for the aiui packages: cache directories, environment\n * detection (CI / SSH / headless), and the session-browser plumbing that\n * dev-server sidecars build their browser auto-open on.\n *\n * @packageDocumentation\n */\n\nexport * from \"./browser\";\nexport * from \"./environment\";\nexport * from \"./extension\";\nexport * from \"./provenance\";\nexport * from \"./socket-url\";\n\nimport { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { isAbsolute, join } from \"node:path\";\n\n/** The application name — the leaf under the platform cache directory. */\nconst APP = \"aiui\";\n\nexport interface CacheDirOptions {\n /**\n * Create the directory (recursively) if it doesn't exist. Defaults to `true` —\n * callers generally want a ready-to-use directory back.\n */\n create?: boolean;\n}\n\n/**\n * Resolve the cache directory for aiui, creating it by default.\n *\n * Resolution order:\n * 1. `$AIUI_CACHE` — an explicit override; used verbatim as the aiui cache root.\n * 2. `$XDG_CACHE_HOME/aiui` — honoring the XDG Base Directory spec (the variable\n * is ignored unless it's an absolute path, per the spec).\n * 3. `~/.cache/aiui` — the XDG default.\n *\n * Pass `namespace` to carve out a subdirectory for a particular kind of cached\n * data, e.g. `cacheDir(\"claude\")` → `~/.cache/aiui/claude`. Different callers\n * cache different things, so each should pick its own namespace rather than\n * writing into the shared root.\n *\n * @example\n * const dir = cacheDir(\"screenshots\"); // created, ready to write into\n * const dir = cacheDir(\"claude\", { create: false }); // resolve the path only\n */\nexport function cacheDir(namespace?: string, options: CacheDirOptions = {}): string {\n const { create = true } = options;\n const dir = namespace ? join(cacheRoot(), namespace) : cacheRoot();\n if (create) {\n mkdirSync(dir, { recursive: true });\n }\n return dir;\n}\n\n/** Resolve the aiui cache root (no directory creation). */\nfunction cacheRoot(): string {\n const override = process.env.AIUI_CACHE?.trim();\n if (override) {\n return override;\n }\n\n const xdg = process.env.XDG_CACHE_HOME?.trim();\n const cacheHome = xdg && isAbsolute(xdg) ? xdg : join(homedir(), \".cache\");\n return join(cacheHome, APP);\n}\n"],"names":["isCi","env","ci","isSsh","isSet","headlessReason","platform","isHeadless","value","ACTIVE_PORT_FILE","LAUNCH_TIMEOUT_MS","CHROME_CHANNELS","discoverSessionBrowser","userDataDir","port","readActivePort","debugEndpointAlive","first","readFileSync","join","sessionBrowserBinary","settings","computeSystemExecutablePath","Browser","RELEASE_CHANNELS","ChromeReleaseChannel","launchSessionBrowser","opts","mkdirSync","rmSync","args","_a","child","execa","exited","deadline","found","writeFileSync","sleep","openInSessionBrowser","browserUrl","url","base","res","decideBrowserAction","config","reason","ms","resolve","loadUnpackedExtension","path","endpoint","version","trim","error","socket","connect","reply","request","id","close","method","params","timeoutMs","timer","onMessage","event","listBrowserTargets","extensionTargets","targets","extensionId","prefix","t","reloadExtension","options","context","extensionContext","threw","evaluate","evaluateInExtension","result","wakePage","own","worker","page","existing","opened","openTarget","ws","findTarget","closeTarget","expression","expectReply","done","ex","_b","_c","_e","_d","nodeRequire","createRequire","packageRoot","packageName","segments","manifest","existsSync","dirname","runningFromSource","packageDir","packageFromSource","rehostSocketUrl","webSocketDebuggerUrl","APP","cacheDir","namespace","create","dir","cacheRoot","override","xdg","cacheHome","isAbsolute","homedir"],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Absolute root directory of an installed (or workspace-linked) dependency.
3
+ *
4
+ * Rather than resolve the package *through* the module system — which would hit
5
+ * its `exports` map (forcing a built `dist/`, and normally blocking access to
6
+ * `package.json`) — we ask Node for the `node_modules` dirs it would search and
7
+ * read `package.json` straight off disk. This needs nothing special from the
8
+ * target package (no `exports` entry) and works even when it has not been built,
9
+ * so dev iteration requires no compile step.
10
+ */
11
+ export declare function packageRoot(packageName: string): string;
12
+ /**
13
+ * Whether a package directory is an editable source checkout (it still carries
14
+ * a `src/` folder) rather than an installed tarball (which ships only `dist/`).
15
+ * Pass the package's own root, e.g. from {@link packageRoot}.
16
+ */
17
+ export declare function runningFromSource(packageDir: string): boolean;
18
+ /**
19
+ * {@link runningFromSource} for a package resolved by name — the common case
20
+ * ("is `@habemus-papadum/aiui-dev-overlay` a source checkout here?").
21
+ */
22
+ export declare function packageFromSource(packageName: string): boolean;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Re-host a Chrome DevTools websocket URL onto the endpoint that answered.
3
+ *
4
+ * Chrome reports its browser socket as `ws://127.0.0.1:<its own port>/…`, which
5
+ * is a lie from anywhere but that machine — a tunneled remote browser
6
+ * (docs/guide/remote) is reached at the forwarded host:port we just fetched
7
+ * from. Keep the path (the browser's session id) and take the authority from
8
+ * the endpoint that answered.
9
+ */
10
+ export declare function rehostSocketUrl(webSocketDebuggerUrl: string, browserUrl: string): string;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * serve-client-surface — the one place a channel sidecar decides HOW to serve
3
+ * its web client, by {@link SidecarContext.mode}.
4
+ *
5
+ * - **dev** → a Vite dev server in middleware mode (HMR, source-first), rooted
6
+ * at the sidecar package's own Vite config. HMR rides the channel's ONE port
7
+ * via a never-listening shim: the channel offers each unclaimed websocket
8
+ * upgrade to the sidecar, and the returned {@link ClientSurface.handleUpgrade}
9
+ * forwards this surface's HMR path into the shim, from which Vite completes
10
+ * the handshake.
11
+ * - **prod** → static file serving from a prebuilt bundle. No Vite at runtime;
12
+ * an installed package needs neither Vite nor the dev toolchain.
13
+ *
14
+ * Extracted from the intent sidecar so every HTML-serving sidecar (intent,
15
+ * pencil, …) serves the same way, differing only by `(viteRoot, distDir)`. Vite
16
+ * is imported LAZILY and only in dev, so `import("vite")` is never reached in a
17
+ * prod install (where it isn't present).
18
+ *
19
+ * The helper only owns the *client-serving* middleware; a sidecar registers its
20
+ * own routes (a discovery `/info`, a proxy) on the app BEFORE calling this so
21
+ * they take precedence, and composes the returned `handleUpgrade`/`dispose` with
22
+ * its own (e.g. the intent sidecar's CDP bridge).
23
+ */
24
+ import { type IncomingMessage } from "node:http";
25
+ import type { Duplex } from "node:stream";
26
+ import type { Express } from "express";
27
+ export interface ServeClientSurfaceOptions {
28
+ /** Dev serves from Vite; prod serves the prebuilt static bundle. */
29
+ mode: "dev" | "prod";
30
+ /** Base path the client mounts under, no trailing slash (e.g. `"/intent"`). */
31
+ prefix: string;
32
+ /**
33
+ * Dev only: the directory Vite roots at. Its own `vite.config` is auto-resolved
34
+ * from here (plugins, entry html) unless {@link viteConfigFile} overrides.
35
+ * Required in dev.
36
+ */
37
+ viteRoot?: string;
38
+ /**
39
+ * Dev only: an explicit Vite config file, when the root's auto-resolved config
40
+ * isn't the one to serve with (e.g. pencil's `lab/vite.config.ts` mounts a
41
+ * whole lab rig; the sidecar wants the client-only build config instead).
42
+ * Omitted → Vite auto-resolves from {@link viteRoot}.
43
+ */
44
+ viteConfigFile?: string;
45
+ /**
46
+ * Dev only: the html file served at the base (`prefix/`), when it isn't
47
+ * `index.html` (Vite's convention). E.g. pencil's client entry is
48
+ * `client.html`; a request to `/pencil/` is rewritten to `/pencil/client.html`
49
+ * before Vite. Prod always serves `<distDir>/index.html` (the built artifact).
50
+ */
51
+ devEntry?: string;
52
+ /** Prod only: the prebuilt static bundle served under `prefix`. Required in prod. */
53
+ distDir?: string;
54
+ /**
55
+ * Vite's `appType` in dev (default `"mpa"`, matching the intent panel). Use
56
+ * `"spa"` for a single-page client that wants an index.html fallback.
57
+ */
58
+ appType?: "mpa" | "spa";
59
+ /** Relative HMR websocket path segment under `prefix` (default `"hmr"`). */
60
+ hmrPath?: string;
61
+ /** Prod: shown (503) when `distDir` has no `index.html` — the build command to run. */
62
+ notBuiltHint?: string;
63
+ /** Diagnostic sink (stderr). */
64
+ log?: (message: string) => void;
65
+ }
66
+ /** What a sidecar composes into its {@link MountedSidecar} handle. */
67
+ export interface ClientSurface {
68
+ /** Claim this surface's HMR upgrade (dev only); `false` otherwise. */
69
+ handleUpgrade?(req: IncomingMessage, socket: Duplex, head: Buffer): boolean;
70
+ /** Close the Vite server + HMR shim (dev); a no-op in prod. */
71
+ dispose?(): void | Promise<void>;
72
+ }
73
+ /**
74
+ * Mount a client surface on `app` under `options.prefix`, choosing Vite (dev) or
75
+ * static serving (prod). Returns the HMR upgrade handler + disposer to compose
76
+ * into the sidecar's {@link MountedSidecar}.
77
+ */
78
+ export declare function serveClientSurface(app: Express, options: ServeClientSurfaceOptions): Promise<ClientSurface>;
@@ -0,0 +1,100 @@
1
+ import { existsSync as x, statSync as g, readFileSync as y } from "node:fs";
2
+ import { createServer as w } from "node:http";
3
+ import { resolve as v, join as $, normalize as S } from "node:path";
4
+ const C = {
5
+ ".html": "text/html; charset=utf-8",
6
+ ".js": "text/javascript; charset=utf-8",
7
+ ".mjs": "text/javascript; charset=utf-8",
8
+ ".css": "text/css; charset=utf-8",
9
+ ".map": "application/json",
10
+ ".json": "application/json",
11
+ ".svg": "image/svg+xml",
12
+ ".png": "image/png",
13
+ ".ico": "image/x-icon",
14
+ ".woff2": "font/woff2"
15
+ };
16
+ async function H(d, e) {
17
+ const f = e.log ?? (() => {
18
+ });
19
+ return e.mode === "dev" ? j(d, e, f) : T(d, e, f);
20
+ }
21
+ async function j(d, e, f) {
22
+ if (e.viteRoot === void 0)
23
+ throw new Error(`serveClientSurface("${e.prefix}"): dev mode needs a viteRoot`);
24
+ const { createServer: l } = await import("vite"), a = w(), m = e.hmrPath ?? "hmr", u = await l({
25
+ root: e.viteRoot,
26
+ // Explicit config when the root's auto-resolved one isn't the serving config
27
+ // (pencil); `undefined` keeps Vite's auto-resolution (intent).
28
+ configFile: e.viteConfigFile,
29
+ base: `${e.prefix}/`,
30
+ appType: e.appType ?? "mpa",
31
+ server: { middlewareMode: !0, hmr: { server: a, path: m } },
32
+ clearScreen: !1,
33
+ logLevel: "warn"
34
+ }), { prefix: s } = e, h = e.devEntry ?? "index.html", r = `${s}/${h}`;
35
+ d.use((t, n, c) => {
36
+ const [o, p] = (t.url ?? "").split("?");
37
+ if (o !== s && !o.startsWith(`${s}/`)) {
38
+ c();
39
+ return;
40
+ }
41
+ h !== "index.html" && (o === s || o === `${s}/`) && (t.url = p !== void 0 ? `${r}?${p}` : r), u.middlewares(t, n, c);
42
+ }), f(`${s}: vite middleware (dev, source-first, HMR) rooted at ${e.viteRoot}`);
43
+ const i = `${e.prefix}/${m}`;
44
+ return {
45
+ handleUpgrade: (t, n, c) => (t.url ?? "").split("?")[0] === i ? (a.emit("upgrade", t, n, c), !0) : !1,
46
+ dispose: async () => {
47
+ await u.close(), a.close();
48
+ }
49
+ };
50
+ }
51
+ function T(d, e, f) {
52
+ if (e.distDir === void 0)
53
+ throw new Error(`serveClientSurface("${e.prefix}"): prod mode needs a distDir`);
54
+ const l = v(e.distDir), { prefix: a } = e, m = e.appType === "spa", u = (r) => {
55
+ r.statusCode = 503, r.setHeader("Content-Type", "text/plain; charset=utf-8"), r.end(
56
+ `The ${a} client has not been built.
57
+
58
+ ` + (e.notBuiltHint ? ` ${e.notBuiltHint}
59
+
60
+ ` : "") + `(serving from ${l})
61
+ `
62
+ );
63
+ }, s = (r, i) => {
64
+ const t = i.lastIndexOf("."), n = t >= 0 ? C[i.slice(t)] : void 0;
65
+ r.statusCode = 200, n && r.setHeader("Content-Type", n), r.end(y(i));
66
+ }, h = (r, i) => {
67
+ if ((r.method ?? "GET") !== "GET")
68
+ return !1;
69
+ let t;
70
+ try {
71
+ t = new URL(r.url ?? "/", "http://localhost").pathname;
72
+ } catch {
73
+ return !1;
74
+ }
75
+ if (t !== a && !t.startsWith(`${a}/`))
76
+ return !1;
77
+ let n = t === a ? "/" : t.slice(a.length);
78
+ (n === "/" || n === "") && (n = "/index.html");
79
+ const c = v($(l, S(n)));
80
+ if (!c.startsWith(l))
81
+ return i.statusCode = 403, i.end(), !0;
82
+ if (!x(c) || !g(c).isFile()) {
83
+ if (n === "/index.html")
84
+ return u(i), !0;
85
+ if (m && !n.slice(n.lastIndexOf("/") + 1).includes(".")) {
86
+ const o = v($(l, "index.html"));
87
+ return x(o) ? s(i, o) : u(i), !0;
88
+ }
89
+ return !1;
90
+ }
91
+ return s(i, c), !0;
92
+ };
93
+ return d.use((r, i, t) => {
94
+ h(r, i) || t();
95
+ }), f(`${a}: static bundle (prod) from ${l}`), {};
96
+ }
97
+ export {
98
+ H as serveClientSurface
99
+ };
100
+ //# sourceMappingURL=web-surface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-surface.js","sources":["../src/web-surface.ts"],"sourcesContent":["/**\n * serve-client-surface — the one place a channel sidecar decides HOW to serve\n * its web client, by {@link SidecarContext.mode}.\n *\n * - **dev** → a Vite dev server in middleware mode (HMR, source-first), rooted\n * at the sidecar package's own Vite config. HMR rides the channel's ONE port\n * via a never-listening shim: the channel offers each unclaimed websocket\n * upgrade to the sidecar, and the returned {@link ClientSurface.handleUpgrade}\n * forwards this surface's HMR path into the shim, from which Vite completes\n * the handshake.\n * - **prod** → static file serving from a prebuilt bundle. No Vite at runtime;\n * an installed package needs neither Vite nor the dev toolchain.\n *\n * Extracted from the intent sidecar so every HTML-serving sidecar (intent,\n * pencil, …) serves the same way, differing only by `(viteRoot, distDir)`. Vite\n * is imported LAZILY and only in dev, so `import(\"vite\")` is never reached in a\n * prod install (where it isn't present).\n *\n * The helper only owns the *client-serving* middleware; a sidecar registers its\n * own routes (a discovery `/info`, a proxy) on the app BEFORE calling this so\n * they take precedence, and composes the returned `handleUpgrade`/`dispose` with\n * its own (e.g. the intent sidecar's CDP bridge).\n */\n\nimport { existsSync, readFileSync, statSync } from \"node:fs\";\nimport {\n createServer as createHttpServer,\n type IncomingMessage,\n type ServerResponse,\n} from \"node:http\";\nimport { join, normalize, resolve } from \"node:path\";\nimport type { Duplex } from \"node:stream\";\nimport type { Express } from \"express\";\n\nexport interface ServeClientSurfaceOptions {\n /** Dev serves from Vite; prod serves the prebuilt static bundle. */\n mode: \"dev\" | \"prod\";\n /** Base path the client mounts under, no trailing slash (e.g. `\"/intent\"`). */\n prefix: string;\n /**\n * Dev only: the directory Vite roots at. Its own `vite.config` is auto-resolved\n * from here (plugins, entry html) unless {@link viteConfigFile} overrides.\n * Required in dev.\n */\n viteRoot?: string;\n /**\n * Dev only: an explicit Vite config file, when the root's auto-resolved config\n * isn't the one to serve with (e.g. pencil's `lab/vite.config.ts` mounts a\n * whole lab rig; the sidecar wants the client-only build config instead).\n * Omitted → Vite auto-resolves from {@link viteRoot}.\n */\n viteConfigFile?: string;\n /**\n * Dev only: the html file served at the base (`prefix/`), when it isn't\n * `index.html` (Vite's convention). E.g. pencil's client entry is\n * `client.html`; a request to `/pencil/` is rewritten to `/pencil/client.html`\n * before Vite. Prod always serves `<distDir>/index.html` (the built artifact).\n */\n devEntry?: string;\n /** Prod only: the prebuilt static bundle served under `prefix`. Required in prod. */\n distDir?: string;\n /**\n * Vite's `appType` in dev (default `\"mpa\"`, matching the intent panel). Use\n * `\"spa\"` for a single-page client that wants an index.html fallback.\n */\n appType?: \"mpa\" | \"spa\";\n /** Relative HMR websocket path segment under `prefix` (default `\"hmr\"`). */\n hmrPath?: string;\n /** Prod: shown (503) when `distDir` has no `index.html` — the build command to run. */\n notBuiltHint?: string;\n /** Diagnostic sink (stderr). */\n log?: (message: string) => void;\n}\n\n/** What a sidecar composes into its {@link MountedSidecar} handle. */\nexport interface ClientSurface {\n /** Claim this surface's HMR upgrade (dev only); `false` otherwise. */\n handleUpgrade?(req: IncomingMessage, socket: Duplex, head: Buffer): boolean;\n /** Close the Vite server + HMR shim (dev); a no-op in prod. */\n dispose?(): void | Promise<void>;\n}\n\nconst MIME: Record<string, string> = {\n \".html\": \"text/html; charset=utf-8\",\n \".js\": \"text/javascript; charset=utf-8\",\n \".mjs\": \"text/javascript; charset=utf-8\",\n \".css\": \"text/css; charset=utf-8\",\n \".map\": \"application/json\",\n \".json\": \"application/json\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".ico\": \"image/x-icon\",\n \".woff2\": \"font/woff2\",\n};\n\n/**\n * Mount a client surface on `app` under `options.prefix`, choosing Vite (dev) or\n * static serving (prod). Returns the HMR upgrade handler + disposer to compose\n * into the sidecar's {@link MountedSidecar}.\n */\nexport async function serveClientSurface(\n app: Express,\n options: ServeClientSurfaceOptions,\n): Promise<ClientSurface> {\n const log = options.log ?? (() => {});\n return options.mode === \"dev\" ? serveDev(app, options, log) : serveProd(app, options, log);\n}\n\nasync function serveDev(\n app: Express,\n options: ServeClientSurfaceOptions,\n log: (message: string) => void,\n): Promise<ClientSurface> {\n if (options.viteRoot === undefined) {\n throw new Error(`serveClientSurface(\"${options.prefix}\"): dev mode needs a viteRoot`);\n }\n // Lazy + dev-only: a prod install has no Vite, and never reaches this branch.\n const { createServer } = await import(\"vite\");\n const hmrShim = createHttpServer();\n const hmrSeg = options.hmrPath ?? \"hmr\";\n // Vite composes the HMR ws path as base + hmr.path, so hand it the RELATIVE\n // segment (`hmr`) — a full `/prefix/hmr` here makes the client dial\n // `/prefix/prefix/hmr`. The client ends up dialing `${prefix}/${hmrSeg}`.\n const vite = await createServer({\n root: options.viteRoot,\n // Explicit config when the root's auto-resolved one isn't the serving config\n // (pencil); `undefined` keeps Vite's auto-resolution (intent).\n configFile: options.viteConfigFile,\n base: `${options.prefix}/`,\n appType: options.appType ?? \"mpa\",\n server: { middlewareMode: true, hmr: { server: hmrShim, path: hmrSeg } },\n clearScreen: false,\n logLevel: \"warn\",\n });\n // One gated middleware, because this Vite server SHARES the channel's Express\n // app with sibling sidecars mounted after it. Vite's stack ends in a terminal\n // 404 (appType mpa/spa) that answers every request it can't serve instead of\n // calling next() — so an ungated `app.use(vite.middlewares)` would swallow all\n // downstream sidecars' routes (found live: intent starved /bar and /pencil).\n // The gate hands Vite ONLY requests under our prefix (every dev URL it emits\n // — `@vite/client`, `/@fs`, dep-optimizer, source — is base-prefixed, so this\n // loses nothing) and lets everything else fall through untouched.\n const { prefix } = options;\n const devEntry = options.devEntry ?? \"index.html\";\n const entry = `${prefix}/${devEntry}`;\n app.use((req, res, next) => {\n const [path, query] = (req.url ?? \"\").split(\"?\");\n if (path !== prefix && !path.startsWith(`${prefix}/`)) {\n next(); // not ours — a sibling sidecar or the channel owns it\n return;\n }\n // When the base entry isn't `index.html` (Vite's convention), point the bare\n // mount path at it so Vite transforms and serves that html. Assets keep\n // their own URLs.\n if (devEntry !== \"index.html\" && (path === prefix || path === `${prefix}/`)) {\n req.url = query !== undefined ? `${entry}?${query}` : entry;\n }\n vite.middlewares(req, res, next);\n });\n log(`${prefix}: vite middleware (dev, source-first, HMR) rooted at ${options.viteRoot}`);\n\n const hmrFull = `${options.prefix}/${hmrSeg}`;\n return {\n handleUpgrade: (req, socket, head) => {\n if ((req.url ?? \"\").split(\"?\")[0] === hmrFull) {\n hmrShim.emit(\"upgrade\", req, socket, head);\n return true;\n }\n return false;\n },\n dispose: async () => {\n await vite.close();\n hmrShim.close();\n },\n };\n}\n\nfunction serveProd(\n app: Express,\n options: ServeClientSurfaceOptions,\n log: (message: string) => void,\n): ClientSurface {\n if (options.distDir === undefined) {\n throw new Error(`serveClientSurface(\"${options.prefix}\"): prod mode needs a distDir`);\n }\n const root = resolve(options.distDir);\n const { prefix } = options;\n const spa = options.appType === \"spa\";\n\n const notBuilt = (res: ServerResponse): void => {\n res.statusCode = 503;\n res.setHeader(\"Content-Type\", \"text/plain; charset=utf-8\");\n res.end(\n `The ${prefix} client has not been built.\\n\\n` +\n (options.notBuiltHint ? ` ${options.notBuiltHint}\\n\\n` : \"\") +\n `(serving from ${root})\\n`,\n );\n };\n\n const sendFile = (res: ServerResponse, file: string): void => {\n const dot = file.lastIndexOf(\".\");\n const type = dot >= 0 ? MIME[file.slice(dot)] : undefined;\n res.statusCode = 200;\n if (type) {\n res.setHeader(\"Content-Type\", type);\n }\n res.end(readFileSync(file));\n };\n\n // Hand-rolled rather than `express.static`: the helper receives an Express app\n // but must not import Express at runtime (it would resolve a second copy\n // beside the channel's own), and a few lines with a traversal guard is cheaper\n // than that risk. When the bundle hasn't been built, `<prefix>/` answers 503\n // with the build command, not a bare 404 that reads as \"the sidecar is broken\".\n const serve = (req: IncomingMessage, res: ServerResponse): boolean => {\n if ((req.method ?? \"GET\") !== \"GET\") {\n return false;\n }\n let pathname: string;\n try {\n pathname = new URL(req.url ?? \"/\", \"http://localhost\").pathname;\n } catch {\n return false;\n }\n if (pathname !== prefix && !pathname.startsWith(`${prefix}/`)) {\n return false;\n }\n let rel = pathname === prefix ? \"/\" : pathname.slice(prefix.length);\n if (rel === \"/\" || rel === \"\") {\n rel = \"/index.html\";\n }\n const file = resolve(join(root, normalize(rel)));\n if (!file.startsWith(root)) {\n res.statusCode = 403;\n res.end();\n return true; // a traversal attempt is OURS to refuse, not to pass along\n }\n if (!existsSync(file) || !statSync(file).isFile()) {\n if (rel === \"/index.html\") {\n notBuilt(res);\n return true;\n }\n // SPA fallback: a client ROUTE (a path with no file extension, e.g.\n // `/__aiui/debug`) has no file of its own — serve the app's index.html\n // and let the client router take it. Asset paths (with an extension)\n // keep falling through: an unknown `.js` may belong to a sibling.\n if (spa && !rel.slice(rel.lastIndexOf(\"/\") + 1).includes(\".\")) {\n const index = resolve(join(root, \"index.html\"));\n if (existsSync(index)) {\n sendFile(res, index);\n } else {\n notBuilt(res);\n }\n return true;\n }\n return false; // an unknown asset path may belong to someone else\n }\n sendFile(res, file);\n return true;\n };\n\n app.use((req, res, next) => {\n if (!serve(req, res)) {\n next();\n }\n });\n log(`${prefix}: static bundle (prod) from ${root}`);\n return {};\n}\n"],"names":["MIME","serveClientSurface","app","options","log","serveDev","serveProd","createServer","hmrShim","createHttpServer","hmrSeg","vite","prefix","devEntry","entry","req","res","next","path","query","hmrFull","socket","head","root","resolve","spa","notBuilt","sendFile","file","dot","type","readFileSync","serve","pathname","rel","join","normalize","existsSync","statSync","index"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@habemus-papadum/aiui-util",
3
- "version": "0.2.0",
4
- "description": "Shared utilities for aiui packages (cache dirs, etc.).",
3
+ "version": "0.6.0",
4
+ "description": "Shared utilities for aiui packages (cache dirs, environment detection, session-browser plumbing).",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -13,7 +13,13 @@
13
13
  "exports": {
14
14
  ".": {
15
15
  "types": "./dist/index.d.ts",
16
- "import": "./dist/index.js"
16
+ "import": "./dist/index.js",
17
+ "default": "./dist/index.js"
18
+ },
19
+ "./web-surface": {
20
+ "types": "./dist/web-surface.d.ts",
21
+ "import": "./dist/web-surface.js",
22
+ "default": "./dist/web-surface.js"
17
23
  }
18
24
  },
19
25
  "main": "./dist/index.js",
@@ -25,6 +31,15 @@
25
31
  "publishConfig": {
26
32
  "access": "public"
27
33
  },
34
+ "dependencies": {
35
+ "@puppeteer/browsers": "^2.10.5",
36
+ "execa": "^9.6.1"
37
+ },
38
+ "devDependencies": {
39
+ "@types/express": "^5.0.0",
40
+ "express": "^5.1.0",
41
+ "vite": "^6.4.1"
42
+ },
28
43
  "scripts": {
29
44
  "build": "tsc --emitDeclarationOnly -p tsconfig.json && vite build",
30
45
  "typecheck": "tsc --noEmit -p tsconfig.json",