scenescout 1.0.0 → 1.2.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,185 @@
1
+ /**
2
+ * Which browsers SceneScout can drive, which builds `scenescout install`
3
+ * downloads for them, and which build a given launch needs.
4
+ *
5
+ * Kept free of Playwright so the rules can be table-tested: the CLI and the
6
+ * engine pass in the paths Playwright reports and get decisions back.
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ /** A browser the engine can launch. */
11
+ export const BROWSER_ENGINES = ["chromium", "firefox", "webkit"];
12
+ /**
13
+ * A build Playwright downloads. Chromium comes as two: the full browser, which
14
+ * a headed run opens, and the headless shell, which every headless run uses.
15
+ * Installing "chromium" brings both, which is what install has always done.
16
+ */
17
+ export const INSTALL_TARGETS = ["chromium", "chromium-headless-shell", "firefox", "webkit"];
18
+ export const DEFAULT_ENGINE = "chromium";
19
+ /** Environment variable naming the browser an attach uses when it does not name one. */
20
+ export const DEFAULT_ENGINE_ENV = "SCENESCOUT_BROWSER";
21
+ /** Approximate size on disk, so the install output can say what it is about to fetch. */
22
+ export const APPROX_DISK_MB = {
23
+ chromium: 550,
24
+ "chromium-headless-shell": 200,
25
+ firefox: 270,
26
+ webkit: 290,
27
+ };
28
+ export function isBrowserEngine(value) {
29
+ return BROWSER_ENGINES.includes(value);
30
+ }
31
+ /** The engine an attach uses when the caller names none: the environment's choice, else Chromium. */
32
+ export function defaultEngine(env) {
33
+ const raw = env[DEFAULT_ENGINE_ENV]?.trim().toLowerCase();
34
+ if (!raw)
35
+ return DEFAULT_ENGINE;
36
+ if (!isBrowserEngine(raw)) {
37
+ throw new Error(`${DEFAULT_ENGINE_ENV}="${env[DEFAULT_ENGINE_ENV]}" is not a browser SceneScout can drive. Use one of: ${BROWSER_ENGINES.join(", ")}.`);
38
+ }
39
+ return raw;
40
+ }
41
+ /**
42
+ * Read the value of `--browsers`. Absent means what install has always done.
43
+ * `all` is every engine; a comma-separated list picks several. "chromium"
44
+ * already includes the headless shell, so naming both is the same as naming it.
45
+ */
46
+ export function parseBrowserSelection(value) {
47
+ if (value === undefined)
48
+ return { targets: ["chromium"] };
49
+ const names = value
50
+ .split(",")
51
+ .map((s) => s.trim().toLowerCase())
52
+ .filter(Boolean);
53
+ const choices = `${INSTALL_TARGETS.join(", ")}, all`;
54
+ if (names.length === 0)
55
+ return { error: `--browsers needs a value. Choose from: ${choices}.` };
56
+ const picked = new Set();
57
+ for (const name of names) {
58
+ if (name === "all") {
59
+ for (const t of ["chromium", "firefox", "webkit"])
60
+ picked.add(t);
61
+ }
62
+ else if (INSTALL_TARGETS.includes(name)) {
63
+ picked.add(name);
64
+ }
65
+ else {
66
+ return { error: `"${name}" is not a browser SceneScout can install. Choose from: ${choices}.` };
67
+ }
68
+ }
69
+ if (picked.has("chromium"))
70
+ picked.delete("chromium-headless-shell");
71
+ return { targets: INSTALL_TARGETS.filter((t) => picked.has(t)) };
72
+ }
73
+ /** The arguments for Playwright's own installer. Target names are Playwright's names. */
74
+ export function playwrightInstallArgs(targets) {
75
+ return ["install", ...targets];
76
+ }
77
+ /** The build a launch needs on disk. */
78
+ export function launchTarget(engine, headed) {
79
+ if (engine !== "chromium")
80
+ return engine;
81
+ return headed ? "chromium" : "chromium-headless-shell";
82
+ }
83
+ /** The engine each build belongs to. */
84
+ export function engineOf(target) {
85
+ return target === "chromium-headless-shell" ? "chromium" : target;
86
+ }
87
+ /**
88
+ * Where the headless shell lives, worked out from the full browser's path.
89
+ * Playwright exposes no path for the shell, but it keeps the two side by side
90
+ * under one revision: `chromium-1234/…` next to `chromium_headless_shell-1234/`.
91
+ * Null when the path does not have that shape.
92
+ */
93
+ export function headlessShellDir(chromiumExecutable) {
94
+ if (!chromiumExecutable)
95
+ return null;
96
+ const parts = chromiumExecutable.split(/[\\/]/);
97
+ // The LAST such segment: a cache kept under a directory that happens to be
98
+ // named the same way must not be mistaken for the build directory.
99
+ let at = -1;
100
+ for (let i = parts.length - 1; i >= 0 && at < 0; i--)
101
+ if (/^chromium-\d+$/.test(parts[i]))
102
+ at = i;
103
+ if (at < 0)
104
+ return null;
105
+ const sep = chromiumExecutable.includes("\\") && !chromiumExecutable.includes("/") ? "\\" : "/";
106
+ return [...parts.slice(0, at), parts[at].replace(/^chromium-/, "chromium_headless_shell-")].join(sep);
107
+ }
108
+ /**
109
+ * Which builds are on disk. `executables` is what Playwright reports for each
110
+ * engine; a path Playwright names is not proof the file is there. The shell is
111
+ * counted only once Playwright has marked its download complete.
112
+ */
113
+ export function browserPresence(executables, exists = fs.existsSync) {
114
+ const at = (p) => ({ installed: !!p && exists(p), path: p });
115
+ const shellDir = headlessShellDir(executables.chromium);
116
+ const shellMarker = shellDir ? path.join(shellDir, "INSTALLATION_COMPLETE") : null;
117
+ const chromium = at(executables.chromium);
118
+ return {
119
+ chromium: { installed: chromium.installed && !!shellMarker && exists(shellMarker), path: chromium.path },
120
+ "chromium-headless-shell": { installed: !!shellMarker && exists(shellMarker), path: shellDir },
121
+ firefox: at(executables.firefox),
122
+ webkit: at(executables.webkit),
123
+ };
124
+ }
125
+ /** The command that downloads one build, for the way this copy of SceneScout was installed. */
126
+ export function installCommandFor(target, fromCheckout) {
127
+ const flags = `--browser-only --browsers ${target}`;
128
+ return fromCheckout ? `node dist/cli.js install ${flags}` : `npx -y scenescout install ${flags}`;
129
+ }
130
+ /**
131
+ * Whether pages may register service workers in this browser.
132
+ *
133
+ * The write policy works by intercepting requests. Only Chromium lets the
134
+ * driver intercept a request a service worker issues; in Firefox and WebKit
135
+ * such a request goes straight to the network, so an app that syncs its writes
136
+ * from a worker would send a DELETE through read-only mode with nothing
137
+ * logged. Blocking registration there makes the page send those requests
138
+ * itself, where the policy sees them.
139
+ */
140
+ export function serviceWorkerPolicy(engine) {
141
+ return engine === "chromium" ? "allow" : "block";
142
+ }
143
+ /**
144
+ * The key that moves keyboard focus to the next control, links and buttons
145
+ * included. WebKit on macOS follows Safari: plain Tab stops only at text
146
+ * fields, and Option+Tab stops everywhere. A focus audit that pressed Tab
147
+ * there would walk past every button and report nothing.
148
+ */
149
+ export function focusAdvanceKey(engine, platform) {
150
+ return engine === "webkit" && platform === "darwin" ? "Alt+Tab" : "Tab";
151
+ }
152
+ /**
153
+ * Run in every page before its own scripts: takes shared workers away.
154
+ *
155
+ * A request issued by a shared worker cannot be intercepted in any browser, so
156
+ * a write sent from one passes the policy unseen and unlogged. Removing the
157
+ * constructor makes feature detection fail, and an app then does that work on
158
+ * the page, where the policy sees it. Not applied in `destructive` mode, where
159
+ * the policy blocks nothing and the person has opted in to everything.
160
+ */
161
+ export const REMOVE_SHARED_WORKER_SCRIPT = `(() => {
162
+ try { delete globalThis.SharedWorker; } catch {}
163
+ if ("SharedWorker" in globalThis) {
164
+ try { Object.defineProperty(globalThis, "SharedWorker", { value: undefined, configurable: true, writable: true }); } catch {}
165
+ }
166
+ })()`;
167
+ /** Whether pages may use shared workers in this write mode. */
168
+ export function sharedWorkersAllowed(mode) {
169
+ return mode === "destructive";
170
+ }
171
+ /**
172
+ * What install says when the browsers it was asked for do not include the one
173
+ * a default attach launches, and that one is not on disk either. Without it,
174
+ * `install --browsers firefox` on a fresh machine ends in "ready" and the first
175
+ * attach fails. Null when there is nothing to say.
176
+ */
177
+ export function defaultAttachNote(opts) {
178
+ if (opts.defaultInstalled || opts.selected.length === 0)
179
+ return null;
180
+ if (opts.selected.some((t) => engineOf(t) === opts.defaultEngine))
181
+ return null;
182
+ const engine = engineOf(opts.selected[0]);
183
+ return (`an attach drives ${opts.defaultEngine} unless told otherwise, and that build is not installed. ` +
184
+ `Pass browser: "${engine}" when attaching, or set ${DEFAULT_ENGINE_ENV}=${engine} in the MCP server's environment.`);
185
+ }
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * scenescout scan <projectPath> Print project discovery results
6
6
  * scenescout serve Run the MCP server on stdio
7
- * scenescout install Install the skill, download Chromium, register the MCP server
7
+ * scenescout install Install the skill, download the browser, register the MCP server
8
8
  * scenescout doctor Check every piece of the setup and say how to fix what is missing
9
9
  */
10
10
  import { spawnSync } from "node:child_process";
@@ -13,6 +13,8 @@ import { createRequire } from "node:module";
13
13
  import os from "node:os";
14
14
  import path from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
+ import { APPROX_DISK_MB, BROWSER_ENGINES, browserPresence, defaultAttachNote, defaultEngine, launchTarget, parseBrowserSelection, playwrightInstallArgs, } from "./browsers.js";
17
+ import { CLIENT_LABELS, firstMessageHint, manualFor, parseClients, registerWithClient, vscodeBinary } from "./clients.js";
16
18
  import { diagnose, installSkill, launchCommand, manualRegisterCommand, registerMcp, resolveClaudeDir, spawnRunner } from "./installer.js";
17
19
  import { LEGACY_MEMORY_DIRNAME, MEMORY_DIRNAME } from "./engine/memory.js";
18
20
  import { formatScan, scanProject } from "./scan.js";
@@ -26,7 +28,12 @@ Usage:
26
28
  scenescout serve Run the MCP server (stdio)
27
29
  scenescout install One-step setup: skill + Chromium + MCP registration
28
30
  (--skip-browser, --no-register to opt out of a step;
29
- --browser-only when the skill and server came from a plugin)
31
+ --browser-only when the skill and server came from a plugin;
32
+ --browsers <list> to choose what to download: chromium (default),
33
+ chromium-headless-shell, firefox, webkit, all — comma-separated)
34
+ (--client <list> to set up another MCP client instead of, or as well as,
35
+ Claude Code: claude-code (default), cursor, vscode, codex, gemini,
36
+ copilot, windsurf — comma-separated)
30
37
  scenescout doctor Check the setup and print the fix for anything missing
31
38
  (--engine: only node, the build and the browser — for plugin
32
39
  installs and other MCP clients)
@@ -109,23 +116,83 @@ function status(projectPath) {
109
116
  }
110
117
  }
111
118
  }
112
- /** Where Playwright expects its Chromium build; null when playwright cannot say. */
113
- async function chromiumPath() {
119
+ /** Which browser builds are on disk, going by the paths Playwright reports for the version we depend on. */
120
+ async function presentBrowsers() {
121
+ const executables = { chromium: null, firefox: null, webkit: null };
114
122
  try {
115
- const { chromium } = await import("playwright");
116
- return chromium.executablePath() || null;
123
+ const playwright = await import("playwright");
124
+ for (const name of BROWSER_ENGINES)
125
+ executables[name] = playwright[name].executablePath() || null;
117
126
  }
118
127
  catch {
119
- return null;
128
+ // Playwright cannot be loaded: every build reads as absent, which is what doctor should say.
120
129
  }
130
+ return browserPresence(executables);
121
131
  }
122
- /** Download Chromium through the playwright CLI that ships with our own dependency. */
123
- function downloadChromium() {
132
+ /** Download browser builds through the playwright CLI that ships with our own dependency. */
133
+ function downloadBrowsers(targets) {
124
134
  const require = createRequire(import.meta.url);
125
135
  const cli = path.join(path.dirname(require.resolve("playwright/package.json")), "cli.js");
126
- const r = spawnSync(process.execPath, [cli, "install", "chromium"], { stdio: "inherit" });
136
+ const r = spawnSync(process.execPath, [cli, ...playwrightInstallArgs(targets)], { stdio: "inherit" });
127
137
  return r.status === 0;
128
138
  }
139
+ /**
140
+ * The value of a flag written as `--name x` or `--name=x`. Undefined when the
141
+ * flag is absent; empty when it was given no value, which includes being
142
+ * followed by another flag.
143
+ */
144
+ function flagValue(flags, name) {
145
+ const inline = flags.find((f) => f.startsWith(`${name}=`));
146
+ if (inline)
147
+ return inline.slice(name.length + 1);
148
+ const at = flags.indexOf(name);
149
+ if (at < 0)
150
+ return undefined;
151
+ const next = flags[at + 1];
152
+ return next === undefined || next.startsWith("--") ? "" : next;
153
+ }
154
+ function browsersFlag(flags) {
155
+ // `--browser-only` is a different flag; a bare `--browser` is a slip that would otherwise be ignored and download Chromium.
156
+ const slip = flags.find((f) => f === "--browser" || f.startsWith("--browser="));
157
+ if (slip)
158
+ throw new Error(`unknown flag ${slip.split("=")[0]} — did you mean --browsers?`);
159
+ return flagValue(flags, "--browsers");
160
+ }
161
+ /** The `code` command on PATH, with its real path: the real path is what tells VS Code from a fork. */
162
+ function codeOnPath() {
163
+ const names = process.platform === "win32" ? ["code.cmd", "code.exe"] : ["code"];
164
+ for (const dir of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
165
+ for (const name of names) {
166
+ const candidate = path.join(dir, name);
167
+ try {
168
+ if (fs.existsSync(candidate))
169
+ return { command: candidate, realPath: fs.realpathSync(candidate) };
170
+ }
171
+ catch {
172
+ // An unreadable PATH entry is not a VS Code install.
173
+ }
174
+ }
175
+ }
176
+ return null;
177
+ }
178
+ /** Every value given for a flag, across repeats and both spellings, joined the way one comma-separated value would be. */
179
+ function flagValues(flags, names) {
180
+ const values = [];
181
+ flags.forEach((f, i) => {
182
+ for (const name of names) {
183
+ if (f.startsWith(`${name}=`))
184
+ values.push(f.slice(name.length + 1));
185
+ else if (f === name) {
186
+ const next = flags[i + 1];
187
+ values.push(next === undefined || next.startsWith("--") ? "" : next);
188
+ }
189
+ }
190
+ });
191
+ if (values.length === 0)
192
+ return undefined;
193
+ // One occurrence without a value is a mistake even when another has one.
194
+ return values.some((v) => v.trim() === "") ? "" : values.join(",");
195
+ }
129
196
  async function install(flags) {
130
197
  const serverPath = path.join(packageRoot, "dist", "mcp-server.js");
131
198
  if (!fs.existsSync(serverPath)) {
@@ -137,7 +204,17 @@ async function install(flags) {
137
204
  // A plugin install already brings the skill and the server registration; the
138
205
  // only thing it cannot bring is the browser download.
139
206
  const browserOnly = flags.includes("--browser-only");
140
- if (!browserOnly) {
207
+ // Read the choice before doing anything, so a typo costs nothing.
208
+ const selection = parseBrowserSelection(browsersFlag(flags));
209
+ if ("error" in selection)
210
+ throw new Error(selection.error);
211
+ const chosen = parseClients(flagValues(flags, ["--client", "--clients"]));
212
+ if ("error" in chosen)
213
+ throw new Error(chosen.error);
214
+ const forClaude = chosen.clients.includes("claude-code");
215
+ const others = chosen.clients.filter((c) => c !== "claude-code");
216
+ // The skill is Claude Code's way of receiving the method; every other client gets it from the server.
217
+ if (!browserOnly && forClaude) {
141
218
  const skill = installSkill({ packageRoot, claudeDir: resolveClaudeDir(process.env, os.homedir()) });
142
219
  for (const note of skill.notes)
143
220
  console.log(`· ${note}`);
@@ -149,45 +226,85 @@ async function install(flags) {
149
226
  console.log("· Browser download skipped (--skip-browser).");
150
227
  }
151
228
  else {
152
- const existing = await chromiumPath();
153
- if (existing && fs.existsSync(existing)) {
154
- console.log(`✓ Chromium already present: ${existing}`);
155
- }
156
- else {
157
- console.log("· Downloading Chromium (one-time, ~150 MB)…");
158
- if (downloadChromium())
159
- console.log("✓ Chromium downloaded.");
229
+ const present = await presentBrowsers();
230
+ const missing = selection.targets.filter((t) => !present[t].installed);
231
+ for (const t of selection.targets)
232
+ if (present[t].installed)
233
+ console.log(`✓ ${t} already present: ${present[t].path}`);
234
+ if (missing.length > 0) {
235
+ const size = missing.reduce((sum, t) => sum + APPROX_DISK_MB[t], 0);
236
+ console.log( Downloading ${missing.join(", ")} (one-time, about ${size} MB on disk)…`);
237
+ if (downloadBrowsers(missing))
238
+ console.log(`✓ Downloaded: ${missing.join(", ")}.`);
160
239
  else {
161
240
  failed = true;
162
- console.log("✗ Chromium download failed — run `npx playwright install chromium` and check your network/proxy.");
241
+ console.log(`✗ Browser download failed — run \`npx playwright install ${missing.join(" ")}\` and check your network/proxy.`);
163
242
  }
164
243
  }
165
244
  }
245
+ // Downloading a browser the server will not launch leaves the first attach failing with no hint why.
246
+ const engine = defaultEngine(process.env);
247
+ const note = defaultAttachNote({
248
+ selected: flags.includes("--skip-browser") ? [] : selection.targets,
249
+ defaultEngine: engine,
250
+ defaultInstalled: (await presentBrowsers())[launchTarget(engine, false)].installed,
251
+ });
252
+ if (note)
253
+ console.log(`· Note: ${note}`);
254
+ const launch = launchCommand({ packageRoot, nodePath: process.execPath, serverPath });
166
255
  if (browserOnly) {
167
256
  // nothing to register
168
257
  }
169
258
  else if (flags.includes("--no-register")) {
170
- console.log( MCP registration skipped (--no-register). To do it by hand:\n\n ${manualRegisterCommand(launchCommand({ packageRoot, nodePath: process.execPath, serverPath }))}\n`);
259
+ console.log( MCP registration skipped (--no-register). To do it by hand:\n");
260
+ if (forClaude)
261
+ console.log(` ${manualRegisterCommand(launch)}`);
262
+ for (const client of others)
263
+ console.log(` ${CLIENT_LABELS[client]}: ${manualFor(client, launch, os.homedir())}`);
264
+ console.log("");
171
265
  }
172
266
  else {
173
- const reg = registerMcp({ launch: launchCommand({ packageRoot, nodePath: process.execPath, serverPath }), serverPath, run: spawnRunner });
174
- if (reg.status === "registered") {
175
- console.log(`✓ MCP server ${reg.replaced ? "re-registered (paths refreshed)" : "registered"} with Claude Code at user scope.`);
176
- for (const name of reg.removedLegacy)
177
- console.log( removed the pre-rename MCP registration "${name}" (it pointed at this same server).`);
178
- for (const note of reg.notes)
179
- console.log( ${note}`);
267
+ if (forClaude) {
268
+ const reg = registerMcp({ launch, serverPath, run: spawnRunner });
269
+ if (reg.status === "registered") {
270
+ console.log(`✓ MCP server ${reg.replaced ? "re-registered (paths refreshed)" : "registered"} with Claude Code at user scope.`);
271
+ for (const name of reg.removedLegacy)
272
+ console.log(`· removed the pre-rename MCP registration "${name}" (it pointed at this same server).`);
273
+ for (const note of reg.notes)
274
+ console.log(`· ${note}`);
275
+ }
276
+ else {
277
+ failed = true;
278
+ console.log(reg.status === "claude-missing"
279
+ ? "· `claude` is not on this shell's PATH, so the MCP server was not registered."
280
+ : `✗ \`claude mcp add\` failed: ${reg.detail}`);
281
+ console.log(` Run this once from a terminal where \`claude\` works:\n\n ${reg.manual}\n`);
282
+ }
180
283
  }
181
- else {
182
- failed = true;
183
- console.log(reg.status === "claude-missing"
184
- ? `claude` is not on this shell's PATH, so the MCP server was not registered."
185
- : `✗ \`claude mcp add\` failed: ${reg.detail}`);
186
- console.log(` Run this once from a terminal where \`claude\` works:\n\n ${reg.manual}\n`);
284
+ const vscode = others.includes("vscode")
285
+ ? vscodeBinary({ platform: process.platform, home: os.homedir(), exists: fs.existsSync, codeOnPath: codeOnPath() })
286
+ : null;
287
+ for (const client of others) {
288
+ const reg = registerWithClient(client, { launch, home: os.homedir(), run: spawnRunner, vscode });
289
+ const label = CLIENT_LABELS[client];
290
+ if (reg.status === "registered") {
291
+ console.log(`✓ MCP server ${reg.replaced ? "re-registered" : "registered"} with ${label} (${reg.where}).`);
292
+ for (const note of reg.notes)
293
+ console.log(`· ${note}`);
294
+ }
295
+ else {
296
+ failed = true;
297
+ // On Windows a client installed through npm is a .cmd shim, which node cannot start directly.
298
+ const windowsNote = process.platform === "win32" ? " (or it is installed as a .cmd shim, which cannot be started from here)" : "";
299
+ console.log(reg.status === "client-missing"
300
+ ? `· ${label} was not found on this machine${windowsNote}, so nothing was registered with it.`
301
+ : `✗ Registering with ${label} failed: ${reg.detail}`);
302
+ console.log(` To do it by hand, ${reg.manual}\n`);
303
+ }
187
304
  }
188
305
  }
189
306
  if (failed) {
190
- console.log("\nSetup is incomplete — fix the lines marked ✗ or · above, then run: npm run doctor");
307
+ console.log(`\nSetup is incomplete — fix the lines marked ✗ or · above, then run: scenescout doctor${forClaude ? "" : " --engine"}`);
191
308
  process.exitCode = 1;
192
309
  return;
193
310
  }
@@ -195,8 +312,12 @@ async function install(flags) {
195
312
  console.log("\nThe browser is ready — attach again.");
196
313
  return;
197
314
  }
198
- console.log("\nStart a FRESH Claude Code session, then in any project run: /scenescout");
199
- console.log("Something off? Run: npm run doctor");
315
+ if (forClaude)
316
+ console.log("\nStart a FRESH Claude Code session, then in any project run: /scenescout");
317
+ // Telling someone to restart a client nothing was registered with sends them looking for a server that is not there.
318
+ if (others.length > 0 && !flags.includes("--no-register"))
319
+ console.log(`\n${firstMessageHint(others)}`);
320
+ console.log(`Something off? Run: scenescout doctor${forClaude ? "" : " --engine"}`);
200
321
  }
201
322
  async function doctor(flags) {
202
323
  const checks = diagnose({
@@ -204,7 +325,12 @@ async function doctor(flags) {
204
325
  packageRoot,
205
326
  claudeDir: resolveClaudeDir(process.env, os.homedir()),
206
327
  nodeVersion: process.version,
207
- chromiumPath: await chromiumPath(),
328
+ // What a default attach launches: the headless build of the default browser.
329
+ defaultBrowser: await (async () => {
330
+ const target = launchTarget(defaultEngine(process.env), false);
331
+ const found = (await presentBrowsers())[target];
332
+ return { target, path: found.installed ? found.path : null, expected: found.path };
333
+ })(),
208
334
  run: spawnRunner,
209
335
  });
210
336
  for (const c of checks) {
@@ -214,7 +340,9 @@ async function doctor(flags) {
214
340
  }
215
341
  if (checks.some((c) => !c.ok))
216
342
  process.exit(1);
217
- console.log("\nAll good. In any project, run: /scenescout");
343
+ console.log(flags.includes("--engine")
344
+ ? "\nAll good. Ask your agent: Use SceneScout to test http://localhost:3000"
345
+ : "\nAll good. In any project, run: /scenescout (or ask: Use SceneScout to test http://localhost:3000)");
218
346
  }
219
347
  const [, , command, ...args] = process.argv;
220
348
  // A CLI's failure mode should be a sentence, not a stack trace. `scan` on a