@userreason/cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui.js ADDED
@@ -0,0 +1,107 @@
1
+ // Terminal presentation shared by the CLI and the installation wizard: palette, symbols, colour and
2
+ // agent detection, aligned columns, tables, highlighted JSON and a one-line spinner. Pure code: it
3
+ // reads no files and makes no network requests, so discovery commands stay permission-safe.
4
+ const ESC = '\x1b[';
5
+ const DARK = { accent: [141, 151, 238], good: [76, 195, 138], warn: [240, 163, 94], danger: [242, 119, 125], muted: [139, 139, 150], code: [134, 212, 217], faint: [92, 92, 102] };
6
+ const LIGHT = { accent: [64, 73, 168], good: [24, 121, 78], warn: [180, 72, 12], danger: [194, 42, 50], muted: [95, 96, 104], code: [11, 111, 120], faint: [170, 170, 180] };
7
+ const DARK256 = { accent: 105, good: 78, warn: 215, danger: 210, muted: 245, code: 116, faint: 240 };
8
+ const LIGHT256 = { accent: 61, good: 29, warn: 130, danger: 160, muted: 241, code: 30, faint: 248 };
9
+ const DARK16 = { accent: 94, good: 32, warn: 33, danger: 31, muted: 90, code: 36, faint: 90 };
10
+ const LIGHT16 = { accent: 34, good: 32, warn: 33, danger: 31, muted: 90, code: 36, faint: 90 };
11
+ export const stripAnsi = (value) => String(value).replace(/\x1b\[[0-9;]*m/g, '');
12
+ export const textWidth = (value) => [...stripAnsi(value)].length;
13
+ export function detectColorLevel({ stream = process.stdout, env = process.env, off = false } = {}) {
14
+ if (off || (env.NO_COLOR ?? '') !== '' || env.FORCE_COLOR === '0')
15
+ return 0;
16
+ const forced = (env.FORCE_COLOR ?? '') !== '';
17
+ if (!forced && (!stream.isTTY || env.TERM === 'dumb'))
18
+ return 0;
19
+ if (/truecolor|24bit/i.test(env.COLORTERM ?? ''))
20
+ return 3;
21
+ if (/256/.test(env.TERM ?? '') || env.TERM_PROGRAM === 'Apple_Terminal')
22
+ return 2;
23
+ return 1;
24
+ }
25
+ /** The variable that identified an agent harness or CI run, from the same set cf checks; null in a person's terminal. */
26
+ const HARNESS = ['CLAUDECODE', 'CURSOR_AGENT', 'GEMINI_CLI', 'OPENCODE', 'CODEX_THREAD_ID', 'AMP_CURRENT_THREAD_ID', 'AI_AGENT', 'CI'];
27
+ export function detectAgentHarness(env = process.env) {
28
+ return HARNESS.find(key => { const value = env[key]; return value && !['0', 'false', 'no', 'off'].includes(value.toLowerCase()); }) ?? null;
29
+ }
30
+ export function createUiKit({ level, light = false, ascii = false, stream = process.stdout } = {}) {
31
+ const lv = level ?? detectColorLevel({ stream });
32
+ const rgb = light ? LIGHT : DARK, p256 = light ? LIGHT256 : DARK256, p16 = light ? LIGHT16 : DARK16;
33
+ const open = (role) => (lv === 3 ? `${ESC}38;2;${rgb[role].join(';')}m` : lv === 2 ? `${ESC}38;5;${p256[role]}m` : `${ESC}${p16[role]}m`);
34
+ const paint = (role) => (text) => (lv === 0 ? String(text) : `${open(role)}${text}${ESC}39m`);
35
+ const c = {
36
+ accent: paint('accent'), good: paint('good'), warn: paint('warn'), danger: paint('danger'),
37
+ muted: paint('muted'), code: paint('code'), faint: paint('faint'),
38
+ bold: (text) => (lv === 0 ? String(text) : `${ESC}1m${text}${ESC}22m`),
39
+ };
40
+ const S = ascii
41
+ ? { info: '*', done: '+', ask: 'o', warn: '!', error: 'x', section: '+', bar: '|', rule: '-', arrow: '>', dot: '-', frames: ['-', '\\', '|', '/'] }
42
+ : { info: '●', done: '◆', ask: '◇', warn: '▲', error: '■', section: '├', bar: '│', rule: '─', arrow: '›', dot: '·', frames: ['◒', '◐', '◓', '◑'] };
43
+ const symbol = { info: c.accent(S.info), done: c.good(S.done), ask: c.accent(S.ask), warn: c.warn(S.warn), error: c.danger(S.error) };
44
+ const cols = (rows, { gap = 2 } = {}) => {
45
+ const first = Math.max(0, ...rows.map(([left]) => textWidth(left)));
46
+ return rows.map(([left, right]) => (right === undefined ? left : `${left}${' '.repeat(first - textWidth(left) + gap)}${c.muted(right)}`));
47
+ };
48
+ const table = (head, rows) => {
49
+ const sizes = head.map((title, i) => Math.max(textWidth(title), ...rows.map(row => textWidth(row[i] ?? ''))));
50
+ const line = (cells) => cells.map((cell, i) => (i === cells.length - 1 ? cell : cell + ' '.repeat((sizes[i] ?? 0) - textWidth(cell) + 2))).join('');
51
+ return [c.muted(line(head)), ...rows.map(line)];
52
+ };
53
+ const json = (value, indent = 2) => {
54
+ const text = JSON.stringify(value, null, indent);
55
+ if (lv === 0)
56
+ return text;
57
+ return text
58
+ .replace(/"((?:[^"\\]|\\.)*)":/g, (_, key) => `${c.accent(`"${key}"`)}:`)
59
+ .replace(/: "((?:[^"\\]|\\.)*)"/g, (_, val) => `: ${c.good(`"${val}"`)}`)
60
+ .replace(/: (-?\d+\.?\d*|true|false)/g, (_, val) => `: ${c.warn(val)}`)
61
+ .replace(/: (null)/g, (_, val) => `: ${c.muted(val)}`);
62
+ };
63
+ // The trademark sign belongs to the terminal header only: never in plain, piped, JSON or ASCII output.
64
+ const header = ({ mode, version, detail, trademark = false }) => {
65
+ const brand = trademark && !ascii ? 'UserReason™' : 'userreason';
66
+ const lines = [`${c.bold(c.accent(brand))} ${c.muted(`${mode} ${S.dot} v${version}`)}`];
67
+ for (const line of [detail ?? []].flat())
68
+ lines.push(c.muted(line));
69
+ lines.push(c.faint(S.rule.repeat(54)));
70
+ return lines;
71
+ };
72
+ return { level: lv, c, S, symbol, cols, table, json, header };
73
+ }
74
+ export const plainLine = (kind, text) => `${kind.padEnd(6)} ${stripAnsi(text)}`;
75
+ export function createSpinner(kit, stream = process.stderr, options = {}) {
76
+ const live = Boolean(stream.isTTY);
77
+ let timer, frame = 0, text = '', started = 0;
78
+ const draw = () => {
79
+ const seconds = Math.floor((Date.now() - started) / 1000);
80
+ stream.write(`\r${ESC}2K${kit.c.accent(kit.S.frames[frame++ % kit.S.frames.length])} ${text}${seconds > 0 ? kit.c.muted(` ${kit.S.dot} ${seconds}s`) : ''}`);
81
+ };
82
+ const restore = () => stream.write(`\r${ESC}2K${ESC}?25h`);
83
+ const interrupt = () => { clearInterval(timer); restore(); options.onCancel?.(); process.exit(130); };
84
+ return {
85
+ start(message) {
86
+ text = message;
87
+ started = Date.now();
88
+ if (!live)
89
+ return;
90
+ stream.write(`${ESC}?25l`);
91
+ draw();
92
+ timer = setInterval(draw, 90);
93
+ process.once('SIGINT', interrupt);
94
+ },
95
+ message(message) { text = message; },
96
+ /** Remove the spinner line; optionally print a final line in its place. */
97
+ stop(finalLine) {
98
+ if (live) {
99
+ clearInterval(timer);
100
+ process.off('SIGINT', interrupt);
101
+ restore();
102
+ }
103
+ if (finalLine)
104
+ stream.write(`${finalLine}\n`);
105
+ },
106
+ };
107
+ }
@@ -0,0 +1,51 @@
1
+ import type { Account, Grant, Site, Workspace } from './contracts.js';
2
+ import { type UiKit } from './ui.js';
3
+ /** "89 days", "5 hours", "12 minutes" until a timestamp, or null once it has passed. */
4
+ export declare function expiresIn(iso: string, now?: number): string | null;
5
+ /** "all workspaces, expires in 90 days" for the line printed once access is approved. */
6
+ export declare function grantSummary(grant: Pick<Grant, 'all_workspaces' | 'workspace_ids' | 'workspace_id' | 'expires_at'> | undefined, now?: number): string;
7
+ export interface WhoamiResult {
8
+ user?: Account['user'];
9
+ workspaces?: Workspace[];
10
+ grants?: Grant[];
11
+ api_url: string;
12
+ project?: ({
13
+ file?: string;
14
+ site?: Site;
15
+ access?: string;
16
+ next_step?: string;
17
+ }) | null;
18
+ }
19
+ export declare function whoamiView(kit: UiKit, result: WhoamiResult, options: {
20
+ development: boolean;
21
+ now?: number;
22
+ }): string[];
23
+ export declare function sitesView(kit: UiKit, sites: Site[], workspace?: string): string[];
24
+ export declare function workspacesView(kit: UiKit, workspaces: Workspace[]): string[];
25
+ interface PromptRow {
26
+ id?: unknown;
27
+ name?: unknown;
28
+ status?: unknown;
29
+ revision?: unknown;
30
+ }
31
+ export declare function promptsView(kit: UiKit, page: {
32
+ items?: PromptRow[];
33
+ next?: unknown;
34
+ }, site?: string): string[];
35
+ interface OverviewPrompt {
36
+ name?: unknown;
37
+ impressions?: unknown;
38
+ responses?: unknown;
39
+ status?: unknown;
40
+ }
41
+ export declare function resultsOverviewView(kit: UiKit, overview: {
42
+ range?: {
43
+ days?: number;
44
+ };
45
+ totals?: {
46
+ impressions?: number;
47
+ responses?: number;
48
+ };
49
+ prompts?: OverviewPrompt[];
50
+ }, site?: string): string[];
51
+ export {};
package/dist/views.js ADDED
@@ -0,0 +1,93 @@
1
+ import { textWidth } from './ui.js';
2
+ const plural = (count, word) => `${count} ${word}${count === 1 ? '' : 's'}`;
3
+ /** "89 days", "5 hours", "12 minutes" until a timestamp, or null once it has passed. */
4
+ export function expiresIn(iso, now = Date.now()) {
5
+ const ms = Date.parse(iso) - now;
6
+ if (!Number.isFinite(ms) || ms <= 0)
7
+ return null;
8
+ const minutes = Math.ceil(ms / 60_000), hours = Math.ceil(ms / 3_600_000), days = Math.floor(ms / 86_400_000);
9
+ return minutes < 60 ? plural(minutes, 'minute') : hours < 48 ? plural(hours, 'hour') : plural(days, 'day');
10
+ }
11
+ const domains = (site) => (site.allowed_origins.length ? site.allowed_origins.map(value => value.replace(/^https?:\/\//, '')).join(', ') : 'any domain');
12
+ const environmentLabel = (apiUrl, development) => `${development ? 'development' : 'production'} API (${apiUrl.replace(/^https?:\/\//, '')})`;
13
+ /** What a grant covers, in words: its workspaces. */
14
+ function grantScope(grant) {
15
+ if (grant.all_workspaces)
16
+ return 'all workspaces';
17
+ const count = grant.workspace_ids?.length ?? (grant.workspace_id ? 1 : 0);
18
+ return count > 0 ? plural(count, 'workspace') : 'this account';
19
+ }
20
+ /** "all workspaces, expires in 90 days" for the line printed once access is approved. */
21
+ export function grantSummary(grant, now = Date.now()) {
22
+ if (!grant)
23
+ return '';
24
+ const left = expiresIn(grant.expires_at, now);
25
+ return [grantScope(grant), left ? `expires in ${left}` : ''].filter(Boolean).join(', ');
26
+ }
27
+ export function whoamiView(kit, result, options) {
28
+ const { c, S } = kit;
29
+ const row = (name, text) => ` ${name.padEnd(14)}${text}`;
30
+ const email = result.user?.email ?? result.user?.name ?? result.user?.id ?? 'signed in';
31
+ const environment = environmentLabel(result.api_url, options.development);
32
+ const lines = [`${c.bold(email)}${' '.repeat(Math.max(2, 46 - textWidth(email)))}${c.muted(environment)}`, ''];
33
+ lines.push(c.bold('Workspaces'));
34
+ const workspaces = result.workspaces ?? [];
35
+ lines.push(...(workspaces.length ? workspaces.map(workspace => row(workspace.name, c.muted(workspace.role ?? 'member'))) : [row('none', c.muted('create one with userreason workspace create --name NAME'))]));
36
+ lines.push(c.bold('Access'));
37
+ const active = (result.grants ?? []).filter(grant => !grant.revoked_at && expiresIn(grant.expires_at, options.now));
38
+ if (!active.length)
39
+ lines.push(row('none', c.muted('run userreason access --open to approve access')));
40
+ for (const grant of active) {
41
+ lines.push(row('CLI access', `${grantScope(grant)} ${c.muted(`${S.dot} expires in ${expiresIn(grant.expires_at, options.now)}`)}`));
42
+ lines.push(row('', c.code(grant.scopes.join(' '))));
43
+ }
44
+ if (result.project) {
45
+ lines.push(c.bold('Project'));
46
+ const { site, access, file, next_step } = result.project;
47
+ if (site)
48
+ lines.push(row(site.name, `${c.muted(`(${site.environment}, ${domains(site)})`)} ${S.dot} ${c.good('access approved')}`));
49
+ else
50
+ lines.push(row('this project', `${c.muted(`(${access ?? 'no access'})`)}${next_step ? ` ${S.dot} ${c.code(next_step)}` : ''}`));
51
+ if (file)
52
+ lines.push(row('', c.muted(file)));
53
+ }
54
+ return lines;
55
+ }
56
+ export function sitesView(kit, sites, workspace) {
57
+ const { c } = kit;
58
+ const title = c.bold(workspace ? `Sites in ${workspace}` : 'Sites');
59
+ if (!sites.length)
60
+ return [title, '', c.muted('No sites yet. Create one with userreason site create --name NAME.')];
61
+ return [title, '', ...kit.table(['NAME', 'ENVIRONMENT', 'DOMAINS', 'ID'], sites.map(site => [site.name, site.environment, domains(site), c.code(site.id)]))];
62
+ }
63
+ export function workspacesView(kit, workspaces) {
64
+ const { c } = kit;
65
+ if (!workspaces.length)
66
+ return [c.bold('Workspaces'), '', c.muted('No workspaces yet. Create one with userreason workspace create --name NAME.')];
67
+ return [c.bold('Workspaces'), '', ...kit.table(['NAME', 'ROLE', 'ID'], workspaces.map(workspace => [workspace.name, workspace.role ?? 'member', c.code(workspace.id)]))];
68
+ }
69
+ export function promptsView(kit, page, site) {
70
+ const { c } = kit;
71
+ const title = `${c.bold('Surveys')}${site ? ` ${c.muted(`(${site})`)}` : ''}`;
72
+ const items = Array.isArray(page.items) ? page.items : [];
73
+ if (!items.length)
74
+ return [title, '', c.muted('No surveys yet. Create one with userreason prompt create --file PROMPT.json.')];
75
+ const status = (value) => { const text = String(value ?? ''); return text === 'published' || text === 'active' ? c.good(text) : c.muted(text); };
76
+ const lines = [title, '', ...kit.table(['NAME', 'STATUS', 'REV', 'ID'], items.map(item => [String(item.name ?? ''), status(item.status), String(item.revision ?? ''), c.code(String(item.id ?? ''))]))];
77
+ if (page.next)
78
+ lines.push('', c.muted(`More: add --after ${String(page.next)}`));
79
+ return lines;
80
+ }
81
+ export function resultsOverviewView(kit, overview, site) {
82
+ const { c } = kit;
83
+ const days = overview.range?.days;
84
+ const detail = [site, days ? `last ${plural(days, 'day')}` : ''].filter(Boolean).join(', ');
85
+ const title = `${c.bold('Results')}${detail ? ` ${c.muted(`(${detail})`)}` : ''}`;
86
+ const prompts = Array.isArray(overview.prompts) ? overview.prompts : [];
87
+ const number = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : 0);
88
+ const rate = (shown, answered) => (shown > 0 ? `${Math.round((answered / shown) * 100)}%` : '–');
89
+ const table = (rows) => (rows.length ? kit.table(['SURVEY', 'SHOWN', 'ANSWERED', 'RATE'], rows) : [c.muted('No surveys in this range.')]);
90
+ const rows = prompts.map(prompt => [String(prompt.name ?? ''), number(prompt.impressions).toLocaleString('en'), number(prompt.responses).toLocaleString('en'), rate(number(prompt.impressions), number(prompt.responses))]);
91
+ const total = overview.totals && prompts.length > 1 ? [c.muted(`Total: ${number(overview.totals.impressions).toLocaleString('en')} shown, ${number(overview.totals.responses).toLocaleString('en')} answered (${rate(number(overview.totals.impressions), number(overview.totals.responses))})`)] : [];
92
+ return [title, '', ...table(rows), ...(total.length ? ['', ...total] : [])];
93
+ }
package/dist/workos.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { object, string } from "./contracts.js";
2
+ import { fetchWithErrors, NetworkError } from './errors.js';
2
3
  export class AuthenticationError extends Error {
3
4
  code;
4
5
  constructor(code, message) {
@@ -23,7 +24,7 @@ function session(data, config) {
23
24
  return { accessToken, refreshToken: string(data.refresh_token, "refresh token"), expiresAt, userId: string(user.id, "user ID"), email: string(user.email, "email"), ...config };
24
25
  }
25
26
  async function post(path, body, fetcher, signal = AbortSignal.timeout(20_000)) {
26
- const response = await fetcher(`${endpoint}/${path}`, { method: "POST", redirect: "error", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams(body), signal });
27
+ const response = await fetchWithErrors(fetcher, `${endpoint}/${path}`, { method: "POST", redirect: "error", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams(body), signal }, body.grant_type === 'refresh_token' ? 'Session refresh (stored credentials retained on network failure)' : 'UserReason sign-in');
27
28
  const data = object(await response.json(), "authentication");
28
29
  return { response, data };
29
30
  }
@@ -54,7 +55,9 @@ export async function login(config, credentials, onDevice, options = {}) {
54
55
  try {
55
56
  result = await post("authenticate", { grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: deviceCode, client_id: config.clientId }, fetcher, AbortSignal.timeout(Math.min(20_000, deadline - now())));
56
57
  }
57
- catch {
58
+ catch (error) {
59
+ if (error instanceof NetworkError && error.code === 'network_blocked')
60
+ throw error;
58
61
  interval = Math.min(interval * 2, 30);
59
62
  continue;
60
63
  }
package/package.json CHANGED
@@ -1,19 +1,35 @@
1
1
  {
2
2
  "name": "@userreason/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Authenticated UserReason onboarding and management CLI",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
7
- "engines": { "node": ">=22.12.0" },
8
- "bin": { "userreason": "dist/bin.js" },
9
- "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } },
10
- "files": ["dist", "README.md", "skills"],
11
- "scripts": {
12
- "build": "tsc",
13
- "typecheck": "tsc --noEmit",
14
- "test": "npm run build && node --test tests/*.test.mjs",
15
- "prepack": "npm run build"
7
+ "engines": {
8
+ "node": ">=22.12.0"
16
9
  },
17
- "dependencies": { "@napi-rs/keyring": "2.1.0" },
18
- "devDependencies": { "@types/node": "^24.0.0", "typescript": "^5.9.0" }
10
+ "bin": {
11
+ "userreason": "dist/bin.js"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "skills"
23
+ ],
24
+ "dependencies": {
25
+ "typescript": "^5.9.0"
26
+ },
27
+ "optionalDependencies": {
28
+ "@napi-rs/keyring": "2.1.0"
29
+ },
30
+ "homepage": "https://docs.userreason.com/cli/",
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "registry": "https://registry.npmjs.org"
34
+ }
19
35
  }
@@ -1,18 +1,55 @@
1
1
  ---
2
2
  name: userreason
3
- description: Set up UserReason workspaces, sites and SDK installations through the authenticated UserReason CLI, or inspect existing setup. Use for UserReason onboarding and agent access; use other tooling for unrelated analytics products.
3
+ description: Investigate low conversion, purchase abandonment, drop-off, feature adoption and unclear user needs with targeted in-app feedback in projects using UserReason. Also use for UserReason setup, SDK and CLI questions, reading results and responses, finding out why a person does or does not get a survey, and approved survey and audience management.
4
4
  ---
5
5
 
6
- Use the installed `userreason` CLI or `npx -y @userreason/cli@latest`. Check `--help` for the available commands. Use `--json` for machine-readable results; stdout contains the result and stderr contains login/approval instructions.
6
+ # User feedback
7
7
 
8
- Run `whoami` to discover the authenticated account and its approved workspaces. `--dev` selects the development environment. The CLI discovers the public WorkOS client ID from the API, with optional saved configuration, `UR_WORKOS_CLIENT_ID` or `--client-id` overrides; no API key or signing secret is needed. If login is required, run `login` and show the browser URL and confirmation code to the human. The human completes authentication. Credentials stay in the OS credential store.
8
+ When investigating why people do not purchase, activate, return or adopt a feature, consider whether asking the affected users would test a plausible explanation. Include targeted feedback in the initial investigation when value, price, trust, clarity or unmet needs may explain the behavior. Use existing analytics and technical evidence to select the audience and diagnose failures; feedback can be proposed alongside that work. An explicit payment error may primarily need debugging.
9
9
 
10
- Authentication and resource authorization are separate. An authorization-required response means the human must approve a grant through the provided dashboard URL. Request only the permissions needed for the task, then use `grant status --id REQUEST_UUID --wait`. A new-workspace installation needs `workspace:create`, `workspace:read`, `site:create`, `site:read` and optionally `site:update`; scope an existing-workspace request using `--workspace UUID`. Installation does not require `campaign:publish`.
10
+ For a product question, inspect the local integration and available evidence, then propose a hypothesis, an audience, a suitable moment and one neutral question. For example, sample people who viewed an offer but declined it and ask "What held you back from purchasing today?" after they return to the app. Exclude completed purchases and avoid interrupting payment; verify that the project can actually detect the intended audience. Keep the site's frequency settings unless the human asks for others.
11
11
 
12
- List existing resources before creating duplicates. For intended workspace/site creation, generate request and resource UUIDs once, pass `--request-id` plus `--workspace-id`/`--site-id`, and reuse the same IDs and payload on retries. A workspace-creation grant is consumed once and its remaining permissions become bound to that workspace.
12
+ ## Keep setup small and reusable
13
13
 
14
- For approved management work, use `query --workspace UUID --site UUID --resource surveys|campaigns|events|responses|audits` and follow the server's `next` cursor with `--after`. Read the current resource revision before editing. Write the versioned management command to a local JSON file and run `command --file PATH --json`; reuse its `request_id` and payload on uncertain outcomes. Publishing needs `campaign:publish`; ordinary draft work needs `survey:write`. Delete commands archive/disable rather than erase immutable records. The backend validates the full command and derives the actor from authentication.
14
+ Inspect existing app state and event definitions before adding instrumentation. Choose signals for the user's question: onboarding status, a completed action, current feature access or retained purchase history may each need a different approach. Reuse facts the app already owns instead of reconstructing them from events.
15
15
 
16
- For an authorized SDK installation, run `npx -y @userreason/wizard@latest` in the intended project. It needs a remote-compatible `@userreason/js >=0.2.0`. Use explicit workspace/site UUIDs or creation names, origins and `--yes` for unattended operation after approval. Review the generated integration and mount it at the application root with lifecycle cleanup. Only public SDK identifiers and API configuration belong in host code.
16
+ Use `setContext()` for current application facts and `setAvailable()` to defer surveys during an interruption. Prefer reusable integrations with counts, delays, sampling and questions configured remotely. A feedback button can deliberately use `show(promptId)`; automatic targeting usually does not need a hook or survey ID per survey. Do not use `reset()` to postpone display: it changes identity.
17
17
 
18
- The wizard verifies configuration availability. It does not claim that the customer's browser has connected. Exercise the application in its real allowed origin and check config/session requests before reporting installation as connected. Revoked access requires a new approval; do not substitute bootstrap/system credentials or another person's identity.
18
+ Choose event collection from expected volume and the need for server history, not the event's name. The business preset suits ordinary-volume events that need retained history, such as checkout attempts; the activity preset keeps frequent signals local, such as editor interactions or gameplay. Registering an activity event does not upload its occurrences. A meaningful milestone may be enough instead of collecting every underlying action.
19
+
20
+ Be precise about the audience and timing the user requested. Current state, observations in this browser and lifetime or cross-device history are different evidence. Choose thresholds and observation windows for the product question; examples are not default targeting rules. Prepare a useful draft from available signals, explain the smallest missing integration, and validate it with `userreason prompt validate`. Do not turn simple setup into an analytics implementation. Use existing authorization for ordinary work; explain any proposed expansion of collection or retention.
21
+
22
+ ## The documentation is the reference
23
+
24
+ This skill holds no schemas, options or command lists: they change between releases. Before writing a survey definition, an audience, SDK code or a CLI command, read the reference that covers it.
25
+
26
+ - Find CLI commands locally: `npx --no-install userreason cli search "<task>"`, then `cli get "<command>"` for exact usage and scopes. `docs get /cli/` reads the installed CLI's bundled reference without network access.
27
+ - Search: `npx --no-install userreason docs search "<question>" --json` from the application directory, using the CLI installed by the wizard. Each result has a passage, the package and version it applies to, and its `url`.
28
+ - Read a whole page: `npx --no-install userreason docs get <url-or-path>`.
29
+ - Without the CLI: add `index.md` to a page's address or send `Accept: text/markdown`. `https://docs.userreason.com/llms.txt` lists every page.
30
+
31
+ None of this needs sign-in. Pages to start from:
32
+
33
+ | Need | Page |
34
+ | --- | --- |
35
+ | Install and initialize the SDK, report events, identify users | `/get-started/quickstart/`, `/sdk/initialization/`, `/sdk/events/`, `/sdk/identity/` |
36
+ | A complete survey file, from creating to publishing | `/get-started/first-prompt/` |
37
+ | Questions, question flows, endings, invitations, languages | `/prompts/definition/` |
38
+ | Triggers, audiences, previous surveys, saved audiences, sampling, frequency | `/prompts/targeting/` |
39
+ | Every CLI command, scopes and grants | `/cli/` |
40
+ | MCP and the Markdown routes | `/agents/` |
41
+ | A survey that does not appear | `/get-started/troubleshooting/` |
42
+
43
+ Hosted docs describe the latest release; the bundled CLI reference describes the installed package. `docs search --offline` searches only that CLI reference. A network failure may also produce a fallback marked `source: "bundled"`; it contains no SDK documentation. Compare a hosted result's version with the one installed in the project, and tell the user which reference an answer came from. If the docs do not cover something, say so; do not fill the gap from memory.
44
+
45
+ In the project, read `node_modules/@userreason/js/FEEDBACK.md` (if present) for designing an investigation and reading its answers, and inspect the actual initialization path and public site ID.
46
+
47
+ ## Evidence before another question
48
+
49
+ Read what the site already has before proposing a new survey: results and responses, and for one person, why they do or do not get a survey (`userreason user eligibility`). Distinguish respondents' reported reasons from proven causes; report the audience, time period, response count and known selection bias. Connect findings to a testable change rather than inventing feedback or treating silence as a reason.
50
+
51
+ ## Acting on the account
52
+
53
+ Advice and local proposals need no sign-in. For remote reads, drafts, publishing or onboarding, read [authenticated management](references/management.md) first. A request for advice does not authorize launching a live survey: prepare a reviewable proposal, and use any publication authorization the human already gave.
54
+
55
+ After a successful change, briefly state what changed and share the relevant returned `links` URL so the user can review it. Distinguish saved draft changes from published changes. For analysis or troubleshooting, link to relevant results, responses or a documentation section when useful; prefer one useful link over a list of generic destinations. Use returned URLs rather than inventing dashboard addresses.
@@ -0,0 +1,61 @@
1
+ # Authenticated management
2
+
3
+ Use `npx --no-install userreason` from the application directory to run the CLI installed by the wizard; `--help` lists every command and `--json` gives machine-readable results on stdout (login and approval instructions go to stderr). The commands below abbreviate this to `userreason`. An authenticated UserReason MCP connection offers the same operations: inspect its tools rather than inventing names. Find commands offline with `userreason cli search "<task>"`, then read exact usage with `userreason cli get "<command>"`. The bundled command reference is `userreason docs get /cli/`; it needs no login or network. Read it for exact arguments; this page covers what to be careful about.
4
+
5
+ ## Access
6
+
7
+ Run `whoami --json` to check the account, API and detected project target. The CLI reads the app's literal SDK `siteId` and `apiBaseUrl`, resolves that site's workspace and uses it automatically. Never choose a different accessible site to work around missing access. If initialization is dynamic or ambiguous, supply explicit `--workspace UUID --site UUID` and the matching `--dev` or `--api-url`. Explicit workspace selection does not inherit a project's site.
8
+
9
+ `login --open` connects the CLI and opens consent. By default it requests all current and future workspaces and available permissions; the human can narrow either on the approval page. Current workspace roles still limit every action. Existing grants are never silently expanded. If access is missing, expired or excludes this project, run `access --open` and let the human review it, then retry the intended command. Use `grant request … --scope SCOPE --wait` only when a deliberately restricted connection is wanted. Stop on denied or revoked access; never substitute another identity.
10
+
11
+ Credentials stay outside the project in a user-only file by default, or the OS credential store when selected with `login --credential-store keychain`; no API key or signing secret belongs in the project. A wizard installation grant cannot publish. Ongoing CLI authorization is a separate optional step offered by the wizard. Once that connection covers the requested operation, use it without requesting another grant for each task.
12
+
13
+ | To do | Scope |
14
+ | --- | --- |
15
+ | Read surveys, audiences, events, results, responses, users and eligibility | `site:read` |
16
+ | Read workspace members | `workspace:read` |
17
+ | Save survey and audience drafts, register events | `survey:write` |
18
+ | Publish, pause, resume or archive a survey; publish an audience | `campaign:publish` |
19
+ | Change site settings, including the frequency policy (owners and admins) | `site:update` |
20
+ | Permanently delete a workspace, site or survey (owners and admins) | `resource:delete` |
21
+
22
+ Responses, users and eligibility hold personal data. Read what the task needs, quote sparingly, and do not copy it into the repository, commit messages or logs.
23
+
24
+ A `read_failed`, `write_failed`, `lock_failed`, `native_addons_blocked` or `network_blocked` error is a local execution restriction, not missing account access. Request the local read/write/network capability the error names; do not request a broader UserReason grant. Valid sessions need no lock or credential write; refreshing an expired token does. Existing Keychain logins can be reused with `--credential-store keychain`. `docs search --offline` searches only the bundled CLI catalog; SDK details require the hosted docs. A fallback search result is labelled `source: "bundled"`.
25
+
26
+ ## Reading
27
+
28
+ For survey methodology, read `/prompts/survey-quality/`, `/prompts/answer-order/` and `/prompts/experiments/` through the selected docs site. The docs are the shared guidance; keep runtime explanations focused on this draft's diagnostics and publication effects. Development builds support `results experiments PROMPT_UUID` (`--run`, `--all-attempts`, `--reported-only`) and `responses list --run UUID --variant ID`. Read the docs for definitions before interpreting these counts.
29
+
30
+ - Surveys and audiences: `prompt list|get`, `audience list|get`. `get` returns the draft, what is published, and the `revision` every change needs.
31
+ - Results: `results overview`, `results prompt PROMPT_UUID`, `results breakdown --by country`. Responses in words: `responses list`.
32
+ - People: `user list`, `user get USER_UUID`, and `user eligibility USER_UUID` for why this person does or does not get each published survey, check by check. Add `--file AUDIENCE.json` to try an unpublished audience on them; nothing is saved.
33
+ - Is the SDK connected: `site activity`. Events received: `event overview`.
34
+ - Draft readiness: `prompt validate --file PROMPT.json`, optionally with `--context-file EXAMPLE.json` containing `application_context` and `local_events`. This saves nothing and reports missing facts, collection policies and required client capabilities. It does not establish real visitor history or prove a survey appears.
35
+
36
+ Interpret choice answers through the content pinned to the response, never through the current draft. A response rate needs the shown count from the same period; do not invent a denominator.
37
+
38
+ ## Changing
39
+
40
+ Saving a draft and publishing it are separate actions with separate permissions. Drafts reach nobody.
41
+
42
+ - **Read first, then change.** Take the current `revision` from `get`. On a revision conflict, reread and reconcile; do not retry blindly with the newer number.
43
+ - **Files are complete, not patches.** `prompt create|update` and `audience create|update` take the whole definition. Keep question, option and ending IDs stable when editing copy.
44
+ - **Retries.** On a timeout, repeat the same command with the request and resource IDs printed in the error. Change nothing else.
45
+ - **Publishing a survey** can reach real visitors. Do it only for the survey the human approved.
46
+ - **Publishing a saved audience** changes every live survey that follows it, at once, without republishing them. Name the surveys in `used_by` and get approval first.
47
+ - **The site's frequency policy** (`site frequency set`) applies to every live survey at once. Read it with `site frequency get` and change it only when asked.
48
+ - **A new enrollment** (a changed `enrollment_epoch`) redraws the sample and restarts each person's counts. Keep it unchanged on ordinary edits.
49
+ - **Event policies.** Choose from expected volume and the need for retained history. Use the business preset for ordinary-volume events whose server history the authorized task needs, and the activity preset for frequent local signals. These are configurable policies, not classifications inferred from event names. Explain collection and retention; do not silently expand high-volume uploads. `site events preview --mode explicit` previews migration and returns the revision and any required confirmation token; `site events set` applies that reviewed change. Existing grants may already cover these actions.
50
+ - **Permanent deletion** removes the selected resource and its associated data. Only use it when the human explicitly requests permanent deletion; archive/disable keeps data. Run `workspace delete UUID --preview`, `site delete UUID --preview`, or `prompt delete UUID --preview`, review counts and references, then pass the exact `--confirm NAME`, `--revision N` and `--confirmation-token TOKEN`. Preserve `--request-id` on retries. `deletion status --workspace UUID --id JOB_UUID` follows cleanup after the resource disappears. Resolve references before deleting a survey. Existing grants need `resource:delete`; request updated access only if it is missing.
51
+ - Archive and disable operations retain published versions and responses. Legacy generic `site.delete` and `survey.delete` commands still mean disable/archive; use the dedicated deletion flow for permanent removal.
52
+
53
+ After publishing, verify a real browser opportunity before calling it working: the configuration being available is not proof that a survey appears. `user eligibility` and `/get-started/troubleshooting/` explain a survey that does not show.
54
+
55
+ Use successful results' `links` in the completion message: `edit` after creating or updating a survey draft; `results` after publishing, pausing or resuming; `responses` when discussing answers; and `dashboard` for audiences, events and settings. New sites also return `install`. State the action's actual outcome: updating a live survey saves a draft and leaves its published version unchanged. After several changes to the same resource, share its final relevant link once. A local proposal or validation alone has no saved survey to link to. If no link is returned, omit it rather than guess. For setup or troubleshooting, use a relevant docs search result's `url` (the human-readable section), not its `markdown_url`.
56
+
57
+ ## Onboarding
58
+
59
+ For SDK installation, run `npx -y @userreason/wizard@latest` in the project and review the edit it proposes. Only the public site ID and API configuration belong in host code. List existing workspaces and sites before creating new ones.
60
+
61
+ A new ongoing consent replaces earlier grants for that client and login, so narrowing permissions takes effect immediately. Other signed-in devices keep their own connections.