@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/README.md +94 -41
- package/dist/authorize.d.ts +29 -0
- package/dist/authorize.js +23 -0
- package/dist/bin.js +479 -49
- package/dist/capture.d.ts +18 -0
- package/dist/capture.js +256 -0
- package/dist/catalog.d.ts +16 -0
- package/dist/catalog.js +180 -0
- package/dist/client.d.ts +72 -1
- package/dist/client.js +120 -12
- package/dist/config.d.ts +4 -0
- package/dist/config.js +35 -12
- package/dist/contracts.d.ts +13 -3
- package/dist/contracts.js +4 -4
- package/dist/credentials.d.ts +12 -2
- package/dist/credentials.js +110 -26
- package/dist/docs.d.ts +51 -0
- package/dist/docs.js +143 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +38 -0
- package/dist/event-policy.d.ts +2 -0
- package/dist/event-policy.js +9 -0
- package/dist/help.d.ts +13 -0
- package/dist/help.js +74 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/lock.js +19 -5
- package/dist/profile.d.ts +20 -0
- package/dist/profile.js +57 -0
- package/dist/project.d.ts +9 -0
- package/dist/project.js +87 -0
- package/dist/ui.d.ts +75 -0
- package/dist/ui.js +107 -0
- package/dist/views.d.ts +51 -0
- package/dist/views.js +93 -0
- package/dist/workos.js +5 -2
- package/package.json +28 -12
- package/skills/userreason/SKILL.md +45 -8
- package/skills/userreason/references/management.md +61 -0
package/dist/capture.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { access, lstat, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
/** External, pinned capture tool; no SingleFile source is bundled into the UserReason package. */
|
|
6
|
+
export const CAPTURE_ENGINE = 'single-file-cli@2.0.77';
|
|
7
|
+
export class CaptureError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function captureUrl(value) {
|
|
15
|
+
let url;
|
|
16
|
+
try {
|
|
17
|
+
url = new URL(value);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error('Capture requires an absolute HTTP(S) URL');
|
|
21
|
+
}
|
|
22
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password)
|
|
23
|
+
throw new Error('Capture requires an HTTP(S) URL without embedded credentials');
|
|
24
|
+
return url;
|
|
25
|
+
}
|
|
26
|
+
export function captureArguments(url, output, options = {}) {
|
|
27
|
+
captureUrl(url);
|
|
28
|
+
const width = options.width ?? 1440, height = options.height ?? 900;
|
|
29
|
+
if (!Number.isInteger(width) || width < 240 || width > 2560 || !Number.isInteger(height) || height < 320 || height > 2560)
|
|
30
|
+
throw new Error('Capture dimensions must be 240–2560 wide and 320–2560 high');
|
|
31
|
+
return [url, output, `--browser-width=${width}`, `--browser-height=${height}`,
|
|
32
|
+
'--browser-wait-until=DOMContentLoaded', '--browser-wait-until-delay=1000',
|
|
33
|
+
'--browser-wait-until-fallback=false', `--browser-load-max-time=${options.interactive ? 300000 : 30000}`, '--browser-capture-max-time=60000',
|
|
34
|
+
'--remove-hidden-elements=false', '--remove-unused-styles=false', '--remove-unused-fonts=false',
|
|
35
|
+
'--remove-alternative-fonts=false', '--remove-alternative-medias=false', '--remove-alternative-images=false',
|
|
36
|
+
'--block-alternative-images=false', '--compress-content=false', '--self-extracting-archive=false',
|
|
37
|
+
'--filename-conflict-action=overwrite', ...(options.browser ? [`--browser-executable-path=${options.browser}`] : [])];
|
|
38
|
+
}
|
|
39
|
+
async function launcher(engine) {
|
|
40
|
+
if (engine)
|
|
41
|
+
return { command: resolve(engine), prefix: [] };
|
|
42
|
+
const candidates = [
|
|
43
|
+
...(process.env.npm_execpath ? [join(dirname(process.env.npm_execpath), 'npx-cli.js')] : []),
|
|
44
|
+
join(dirname(process.execPath), '../lib/node_modules/npm/bin/npx-cli.js'),
|
|
45
|
+
join(dirname(process.execPath), 'node_modules/npm/bin/npx-cli.js'),
|
|
46
|
+
'/usr/share/nodejs/npm/bin/npx-cli.js',
|
|
47
|
+
];
|
|
48
|
+
for (const file of candidates) {
|
|
49
|
+
try {
|
|
50
|
+
await access(file);
|
|
51
|
+
return { command: process.execPath, prefix: [file, '--yes', `--package=${CAPTURE_ENGINE}`, 'single-file'] };
|
|
52
|
+
}
|
|
53
|
+
catch { /* Try the next standard npm location. */ }
|
|
54
|
+
}
|
|
55
|
+
if (process.platform !== 'win32')
|
|
56
|
+
return { command: 'npx', prefix: ['--yes', `--package=${CAPTURE_ENGINE}`, 'single-file'] };
|
|
57
|
+
throw new Error('Install SingleFile CLI and pass its executable with --capture-bin');
|
|
58
|
+
}
|
|
59
|
+
async function run(command, args, timeoutMs, signal) {
|
|
60
|
+
signal.throwIfAborted();
|
|
61
|
+
await new Promise((done, reject) => {
|
|
62
|
+
const child = spawn(command, args, { stdio: 'ignore', shell: false, detached: process.platform !== 'win32' });
|
|
63
|
+
let timedOut = false;
|
|
64
|
+
const stop = () => {
|
|
65
|
+
try {
|
|
66
|
+
if (child.pid && process.platform !== 'win32')
|
|
67
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
68
|
+
else
|
|
69
|
+
child.kill('SIGKILL');
|
|
70
|
+
}
|
|
71
|
+
catch { /* The capture process already exited. */ }
|
|
72
|
+
};
|
|
73
|
+
const timer = setTimeout(() => { timedOut = true; stop(); }, timeoutMs);
|
|
74
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
75
|
+
if (signal.aborted)
|
|
76
|
+
stop();
|
|
77
|
+
const cleanup = () => { clearTimeout(timer); signal.removeEventListener('abort', stop); };
|
|
78
|
+
child.once('error', () => { cleanup(); reject(new Error('Could not start the capture engine. Install npm or pass --capture-bin with a SingleFile executable')); });
|
|
79
|
+
child.once('close', (code) => {
|
|
80
|
+
cleanup();
|
|
81
|
+
if (signal.aborted)
|
|
82
|
+
reject(signal.reason);
|
|
83
|
+
else if (timedOut)
|
|
84
|
+
reject(new CaptureError('capture_timeout', 'Capture timed out. The page may still be loading or waiting for input; try --interactive to inspect it.'));
|
|
85
|
+
else if (code !== 0)
|
|
86
|
+
reject(new Error('Page capture failed; check the URL, Chrome installation and access to the page'));
|
|
87
|
+
else
|
|
88
|
+
done();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Own the browser lifecycle: sandbox on, loopback CDP, isolated profile, no user cookies. */
|
|
93
|
+
async function startBrowser(directory, options) {
|
|
94
|
+
const candidates = options.browser ? [resolve(options.browser)] : process.platform === 'darwin'
|
|
95
|
+
? ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Chromium.app/Contents/MacOS/Chromium', '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge']
|
|
96
|
+
: process.platform === 'win32'
|
|
97
|
+
? [join(process.env.PROGRAMFILES ?? 'C:\\Program Files', 'Google/Chrome/Application/chrome.exe'), join(process.env.LOCALAPPDATA ?? '', 'Google/Chrome/Application/chrome.exe')]
|
|
98
|
+
: ['/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/opt/google/chrome/chrome'];
|
|
99
|
+
let executable;
|
|
100
|
+
for (const candidate of candidates) {
|
|
101
|
+
try {
|
|
102
|
+
await access(candidate);
|
|
103
|
+
executable = candidate;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
catch { /* Try next installed browser. */ }
|
|
107
|
+
}
|
|
108
|
+
if (!executable)
|
|
109
|
+
throw new Error('Chrome was not found. Install Chrome or use --browser-executable PATH');
|
|
110
|
+
const profile = join(directory, 'browser');
|
|
111
|
+
const child = spawn(executable, [...(options.interactive ? [] : ['--headless=new']), '--remote-debugging-port=0', '--remote-debugging-address=127.0.0.1', `--user-data-dir=${profile}`, '--no-first-run', '--no-default-browser-check', '--disable-background-networking', '--disable-component-update', '--disable-sync', '--disable-extensions', 'about:blank'], { stdio: 'ignore', detached: process.platform !== 'win32', shell: false });
|
|
112
|
+
let exited = false;
|
|
113
|
+
child.once('error', () => { exited = true; });
|
|
114
|
+
child.once('exit', () => { exited = true; });
|
|
115
|
+
const close = () => {
|
|
116
|
+
process.off('SIGINT', close);
|
|
117
|
+
process.off('SIGTERM', close);
|
|
118
|
+
try {
|
|
119
|
+
if (child.pid && process.platform !== 'win32')
|
|
120
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
121
|
+
else
|
|
122
|
+
child.kill('SIGKILL');
|
|
123
|
+
}
|
|
124
|
+
catch { /* Already closed. */ }
|
|
125
|
+
};
|
|
126
|
+
process.once('SIGINT', close);
|
|
127
|
+
process.once('SIGTERM', close);
|
|
128
|
+
const deadline = Date.now() + 15000;
|
|
129
|
+
while (!exited && Date.now() < deadline) {
|
|
130
|
+
try {
|
|
131
|
+
const [port = '', path] = (await readFile(join(profile, 'DevToolsActivePort'), 'utf8')).split('\n');
|
|
132
|
+
if (/^[0-9]{1,5}$/.test(port) && Number(port) > 0 && Number(port) < 65536)
|
|
133
|
+
return { server: `http://127.0.0.1:${port}`, debuggerUrl: path?.startsWith('/devtools/browser/') ? `ws://127.0.0.1:${port}${path}` : undefined, close };
|
|
134
|
+
}
|
|
135
|
+
catch { /* Chrome writes the port file once CDP is listening. */ }
|
|
136
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
137
|
+
}
|
|
138
|
+
close();
|
|
139
|
+
throw new Error('Chrome could not start its sandboxed capture session');
|
|
140
|
+
}
|
|
141
|
+
/** Watch new page targets before navigation so even an early prompt is observable. */
|
|
142
|
+
async function watchDialogs(url, options, controller) {
|
|
143
|
+
const socket = new WebSocket(url);
|
|
144
|
+
let nextId = 0, closed = false;
|
|
145
|
+
const pending = new Map();
|
|
146
|
+
const fail = () => { if (!closed)
|
|
147
|
+
controller.abort(new CaptureError('capture_browser_closed', 'The capture browser closed before capture finished.')); };
|
|
148
|
+
const call = (method, params = {}, sessionId) => new Promise((resolve, reject) => {
|
|
149
|
+
const id = ++nextId;
|
|
150
|
+
pending.set(id, { resolve, reject });
|
|
151
|
+
socket.send(JSON.stringify({ id, method, params, sessionId }));
|
|
152
|
+
});
|
|
153
|
+
socket.addEventListener('message', event => {
|
|
154
|
+
const message = JSON.parse(String(event.data));
|
|
155
|
+
if (message.id) {
|
|
156
|
+
const item = pending.get(message.id);
|
|
157
|
+
pending.delete(message.id);
|
|
158
|
+
if (message.error)
|
|
159
|
+
item?.reject(new Error('Could not observe the capture browser'));
|
|
160
|
+
else
|
|
161
|
+
item?.resolve();
|
|
162
|
+
}
|
|
163
|
+
else if (message.method === 'Target.attachedToTarget') {
|
|
164
|
+
const sessionId = message.params.sessionId;
|
|
165
|
+
void (async () => { await call('Page.enable', {}, sessionId); await call('Runtime.runIfWaitingForDebugger', {}, sessionId); })().catch(fail);
|
|
166
|
+
}
|
|
167
|
+
else if (message.method === 'Page.javascriptDialogOpening') {
|
|
168
|
+
if (options.interactive)
|
|
169
|
+
options.onProgress?.('The page is waiting for input. Answer the dialog in the capture browser.');
|
|
170
|
+
else
|
|
171
|
+
controller.abort(new CaptureError('capture_input_required', 'The page opened a browser dialog and needs input. Retry with --interactive to answer it in the capture browser.'));
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
socket.addEventListener('close', fail);
|
|
175
|
+
socket.addEventListener('error', fail);
|
|
176
|
+
const close = () => {
|
|
177
|
+
closed = true;
|
|
178
|
+
socket.close();
|
|
179
|
+
for (const item of pending.values())
|
|
180
|
+
item.reject(new Error('Capture browser observation ended'));
|
|
181
|
+
pending.clear();
|
|
182
|
+
};
|
|
183
|
+
let setupTimer;
|
|
184
|
+
try {
|
|
185
|
+
await Promise.race([
|
|
186
|
+
(async () => {
|
|
187
|
+
await new Promise((resolve, reject) => {
|
|
188
|
+
socket.addEventListener('open', () => resolve(), { once: true });
|
|
189
|
+
socket.addEventListener('error', () => reject(new Error('Could not connect to the capture browser')), { once: true });
|
|
190
|
+
});
|
|
191
|
+
await call('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true, filter: [{ type: 'page', exclude: false }, { exclude: true }] });
|
|
192
|
+
})(),
|
|
193
|
+
new Promise((_, reject) => { setupTimer = setTimeout(() => reject(new Error('Could not connect to the capture browser')), 5000); }),
|
|
194
|
+
]);
|
|
195
|
+
return close;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
close();
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
clearTimeout(setupTimer);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
export async function capturePage(value, options = {}) {
|
|
206
|
+
const url = captureUrl(value);
|
|
207
|
+
const timeout = options.timeoutMs ?? (options.interactive ? 300000 : 120000);
|
|
208
|
+
if (!Number.isInteger(timeout) || timeout < 1000 || timeout > 300000)
|
|
209
|
+
throw new Error('Capture timeout must be 1000–300000 milliseconds');
|
|
210
|
+
// Validate before starting an external process or touching a temporary directory.
|
|
211
|
+
captureArguments(url.href, 'capture.html', options);
|
|
212
|
+
const directory = await mkdtemp(join(tmpdir(), 'userreason-capture-'));
|
|
213
|
+
let browser;
|
|
214
|
+
let stopWatching;
|
|
215
|
+
const controller = new AbortController();
|
|
216
|
+
const interrupted = () => controller.abort(new CaptureError('capture_cancelled', 'Capture cancelled. Nothing was uploaded.'));
|
|
217
|
+
process.once('SIGINT', interrupted);
|
|
218
|
+
process.once('SIGTERM', interrupted);
|
|
219
|
+
try {
|
|
220
|
+
const output = join(directory, 'capture.html');
|
|
221
|
+
const hidePrompt = join(directory, 'hide-prompt.css');
|
|
222
|
+
// Removing a live SDK host can trigger its mount observer repeatedly.
|
|
223
|
+
await writeFile(hidePrompt, '[data-userreason] { display: none !important; }', { mode: 0o600 });
|
|
224
|
+
// Ownership remains private even if the external tool replaces the file contents.
|
|
225
|
+
await writeFile(output, '', { mode: 0o600 });
|
|
226
|
+
const { command, prefix } = await launcher(options.engine);
|
|
227
|
+
options.onProgress?.(options.interactive ? 'Opening capture browser. Complete any sign-in or dialog there.' : 'Starting isolated Chrome capture.');
|
|
228
|
+
browser = await startBrowser(directory, options);
|
|
229
|
+
if (controller.signal.aborted)
|
|
230
|
+
throw controller.signal.reason;
|
|
231
|
+
if (browser.debuggerUrl)
|
|
232
|
+
stopWatching = await watchDialogs(browser.debuggerUrl, options, controller);
|
|
233
|
+
options.onProgress?.('Loading page and embedding assets; first use may download SingleFile.');
|
|
234
|
+
const progressTimer = setInterval(() => options.onProgress?.('Still capturing the page and its assets…'), 10000);
|
|
235
|
+
try {
|
|
236
|
+
await run(command, [...prefix, ...captureArguments(url.href, output, options), `--browser-server=${browser.server}`, `--browser-stylesheet=${hidePrompt}`], timeout, controller.signal);
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
clearInterval(progressTimer);
|
|
240
|
+
}
|
|
241
|
+
const info = await lstat(output);
|
|
242
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size < 1 || info.size > 4 * 1024 * 1024)
|
|
243
|
+
throw new Error('Captured HTML is empty or exceeds the 4 MiB demo limit');
|
|
244
|
+
return new Uint8Array(await readFile(output));
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
throw controller.signal.aborted ? controller.signal.reason : error;
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
stopWatching?.();
|
|
251
|
+
browser?.close();
|
|
252
|
+
process.off('SIGINT', interrupted);
|
|
253
|
+
process.off('SIGTERM', interrupted);
|
|
254
|
+
await rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Scope } from './contracts.js';
|
|
2
|
+
/** The description written for exactly this command or noun, if there is one. */
|
|
3
|
+
export declare const commandSummary: (command: string) => string | undefined;
|
|
4
|
+
export interface CommandReference {
|
|
5
|
+
command: string;
|
|
6
|
+
usage: string;
|
|
7
|
+
summary: string;
|
|
8
|
+
scopes: readonly Scope[];
|
|
9
|
+
notes: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare const commandCatalog: CommandReference[];
|
|
12
|
+
export declare const globalHelp = "Workspace and site default to the SDK initialization in the current project.\nExplicit --workspace / --site override it. No project configuration file is created.\nSurveys use the existing prompt command and API names.\nGlobal options: --dev, --api-url ORIGIN, --client-id CLIENT_ID, --json\nCredential storage: --credential-store file|keychain, or UR_CREDENTIAL_STORE (default file)\nLogin saves the selected store for this API environment. Existing Keychain sessions require --credential-store keychain.\nDocs: --docs-url ORIGIN, --offline (bundled CLI reference), --live (require hosted docs)\nCreation options: --request-id UUID, --workspace-id UUID / --site-id UUID\nSite environment: --environment development|production (default production)\nOrigins are unrestricted by default; --origin adds an exact restriction.\nFile credentials are plaintext under the user config directory with owner-only permissions (0600).\nKeychain is optional and never silently falls back to a file. WorkOS API keys are never required.";
|
|
13
|
+
export declare const cliHelp: () => string;
|
|
14
|
+
export declare function getCommandReference(command: string): CommandReference;
|
|
15
|
+
export declare function searchCommands(query: string, limit?: number): CommandReference[];
|
|
16
|
+
export declare function bundledCliMarkdown(): string;
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { SCOPES } from './contracts.js';
|
|
2
|
+
// Shared by terminal help, offline command discovery, and the bundled CLI reference.
|
|
3
|
+
const usages = `userreason login [--open]
|
|
4
|
+
userreason access [--open] [--no-wait]
|
|
5
|
+
userreason logout
|
|
6
|
+
userreason whoami
|
|
7
|
+
userreason deletion status --id UUID --workspace UUID
|
|
8
|
+
userreason workspace delete UUID [--preview] [--confirm NAME --revision N --confirmation-token TOKEN]
|
|
9
|
+
userreason site delete UUID --workspace UUID [--preview] [--confirm NAME --revision N --confirmation-token TOKEN]
|
|
10
|
+
userreason prompt delete UUID --workspace UUID --site UUID [--preview] [--confirm NAME --revision N --confirmation-token TOKEN]
|
|
11
|
+
userreason workspace list
|
|
12
|
+
userreason workspace create --name NAME
|
|
13
|
+
userreason site list --workspace UUID
|
|
14
|
+
userreason site create --workspace UUID --name NAME [--origin URL]
|
|
15
|
+
userreason grant request --scope site:read [--scope site:create] [--workspace UUID] [--wait]
|
|
16
|
+
userreason grant status --id UUID [--wait]
|
|
17
|
+
userreason prompt list --workspace UUID --site UUID
|
|
18
|
+
userreason prompt get UUID --workspace UUID --site UUID
|
|
19
|
+
userreason prompt validate --file PROMPT.json [--context-file EXAMPLE.json] --workspace UUID --site UUID
|
|
20
|
+
userreason preview capture URL --dev --workspace UUID --site UUID [--name NAME] [--width 1440] [--height 900] [--interactive] [--open] [--capture-bin PATH] [--browser-executable PATH]
|
|
21
|
+
userreason prompt create --file PROMPT.json --workspace UUID --site UUID [--id UUID]
|
|
22
|
+
userreason prompt update UUID --file PROMPT.json --revision N --workspace UUID --site UUID
|
|
23
|
+
userreason prompt publish|pause|resume|archive UUID --revision N --workspace UUID --site UUID
|
|
24
|
+
userreason prompt responses UUID --workspace UUID --site UUID [--limit 50] [--after UUID]
|
|
25
|
+
userreason audience list --workspace UUID --site UUID
|
|
26
|
+
userreason audience get UUID --workspace UUID --site UUID
|
|
27
|
+
userreason audience create --file AUDIENCE.json --workspace UUID --site UUID [--id UUID]
|
|
28
|
+
userreason audience update UUID --file AUDIENCE.json --revision N --workspace UUID --site UUID
|
|
29
|
+
userreason audience publish|discard|archive UUID --revision N --workspace UUID --site UUID
|
|
30
|
+
userreason results overview --workspace UUID --site UUID [--days 1|7|30|90] [--time-zone ZONE]
|
|
31
|
+
userreason results prompt UUID --workspace UUID --site UUID [--days N] [--country NL] [--segment browser=Chrome]
|
|
32
|
+
userreason results experiments UUID --workspace UUID --site UUID [--run UUID] [--all-attempts] [--reported-only]
|
|
33
|
+
userreason results breakdown --by country|language|device_type|os|browser --workspace UUID --site UUID [--prompt-id UUID]
|
|
34
|
+
userreason responses list --workspace UUID --site UUID [--prompt-id UUID] [--run UUID] [--variant ID] [--user UUID] [--search TEXT] [--days N] [--cursor CURSOR]
|
|
35
|
+
userreason user list --workspace UUID --site UUID [--kind all|identified|anonymous|responded] [--search TEXT] [--cursor CURSOR]
|
|
36
|
+
userreason user get UUID --workspace UUID --site UUID
|
|
37
|
+
userreason user eligibility UUID --workspace UUID --site UUID [--file AUDIENCE.json]
|
|
38
|
+
userreason event list|overview --workspace UUID --site UUID [--days N]
|
|
39
|
+
userreason event create --file EVENT.json [--purpose business|activity] --workspace UUID --site UUID [--id UUID]
|
|
40
|
+
userreason event update UUID --file EVENT.json --revision N --workspace UUID --site UUID
|
|
41
|
+
userreason site events preview --mode explicit|registered|all [--file LIMITS.json] --workspace UUID --site UUID
|
|
42
|
+
userreason site events set --mode explicit|registered|all --revision N [--confirmation-token TOKEN] [--file LIMITS.json] --workspace UUID --site UUID
|
|
43
|
+
userreason site activity --workspace UUID --site UUID
|
|
44
|
+
userreason site frequency get --workspace UUID --site UUID
|
|
45
|
+
userreason site frequency set --file POLICY.json --revision N --workspace UUID --site UUID
|
|
46
|
+
userreason site frequency reset --revision N --workspace UUID --site UUID
|
|
47
|
+
userreason prompt history --workspace UUID --site UUID
|
|
48
|
+
userreason member list --workspace UUID
|
|
49
|
+
userreason command --file COMMAND.json
|
|
50
|
+
userreason query --workspace UUID --resource surveys [--site UUID] [--limit 50] [--after UUID]
|
|
51
|
+
userreason docs search "QUESTION" [--limit 5]
|
|
52
|
+
userreason docs get URL_OR_PATH
|
|
53
|
+
userreason cli search "QUESTION" [--limit 5]
|
|
54
|
+
userreason cli get "COMMAND"
|
|
55
|
+
userreason cli list`;
|
|
56
|
+
const descriptions = {
|
|
57
|
+
preview: 'Capture a webpage locally and upload a responsive HTML background to the development preview lab.',
|
|
58
|
+
deletion: 'Read the status of a permanent deletion job.',
|
|
59
|
+
'workspace delete': 'Preview or explicitly confirm permanent removal of a workspace and all its data.',
|
|
60
|
+
'site delete': 'Preview or explicitly confirm permanent removal of a site and all its data.',
|
|
61
|
+
'prompt delete': 'Preview or explicitly confirm permanent removal of a survey, versions and responses.',
|
|
62
|
+
login: 'Sign in and approve workspace access and permissions in the browser.',
|
|
63
|
+
access: 'Renew or change approved workspace access and permissions.',
|
|
64
|
+
logout: 'Remove the session from the selected local credential store.',
|
|
65
|
+
whoami: 'Inspect the signed-in account, API, workspace grants and project site.',
|
|
66
|
+
workspace: 'List or create workspaces.',
|
|
67
|
+
site: 'List or create sites, inspect SDK connection activity, or manage survey frequency limits.',
|
|
68
|
+
grant: 'Request deliberately restricted access or inspect approval status.',
|
|
69
|
+
prompt: 'Manage feedback surveys and their questions.',
|
|
70
|
+
'prompt validate': 'Rehearse survey targeting, event collection and SDK requirements without saving or publishing.',
|
|
71
|
+
'prompt create': 'Create a draft survey to collect feedback from visitors.',
|
|
72
|
+
'prompt update': 'Edit an existing survey draft with a complete question definition.',
|
|
73
|
+
'prompt publish': 'Publish an approved feedback survey to deliver it to matching visitors.',
|
|
74
|
+
'prompt pause': 'Pause a published survey to stop collecting new feedback.',
|
|
75
|
+
'prompt resume': 'Resume delivery of a paused feedback survey.',
|
|
76
|
+
'prompt archive': 'Retire a feedback survey while preserving published versions and responses.',
|
|
77
|
+
'prompt responses': 'Read answers submitted to one feedback survey.',
|
|
78
|
+
'prompt history': 'Read when the site began recording survey history.',
|
|
79
|
+
audience: 'Manage saved audiences, reusable targeting rules and segments.',
|
|
80
|
+
results: 'Analyze feedback results, response rates, funnels and breakdowns.',
|
|
81
|
+
'results experiments': 'Read experiment runs, allocation, comparisons and answer positions. See /prompts/experiments/ in the selected documentation site.',
|
|
82
|
+
responses: 'Read submitted feedback answers and filter responses.',
|
|
83
|
+
user: 'Inspect visitors and eligibility; explain why a person does or does not see a survey.',
|
|
84
|
+
'user eligibility': 'Explain why a visitor does or does not see a survey, including targeting and frequency checks.',
|
|
85
|
+
event: 'Inspect or register tracked events and their collection settings.',
|
|
86
|
+
'site activity': 'Check whether the SDK is connected and receiving sessions, events and displays.',
|
|
87
|
+
member: 'Read workspace members and roles.',
|
|
88
|
+
command: 'Submit a complete versioned management command JSON file.',
|
|
89
|
+
query: 'Query workspace and site records with pagination.',
|
|
90
|
+
docs: 'Search public SDK documentation or read a Markdown page. Use --offline for bundled CLI documentation.',
|
|
91
|
+
cli: 'Discover installed CLI commands and exact usage locally without login or network access.',
|
|
92
|
+
};
|
|
93
|
+
/** The description written for exactly this command or noun, if there is one. */
|
|
94
|
+
export const commandSummary = (command) => descriptions[command];
|
|
95
|
+
function scopeFor(command) {
|
|
96
|
+
const [group, action] = command.split(' ');
|
|
97
|
+
if (group === 'preview')
|
|
98
|
+
return ['site:read', 'survey:write'];
|
|
99
|
+
if (action === 'delete')
|
|
100
|
+
return ['resource:delete'];
|
|
101
|
+
if (group === 'workspace')
|
|
102
|
+
return [action === 'create' ? 'workspace:create' : 'workspace:read'];
|
|
103
|
+
if (group === 'member')
|
|
104
|
+
return ['workspace:read'];
|
|
105
|
+
if (group === 'site')
|
|
106
|
+
return [action === 'create' ? 'site:create' : command.startsWith('site events set') || command.startsWith('site frequency s') || command.endsWith('reset') ? 'site:update' : 'site:read'];
|
|
107
|
+
if (['prompt', 'audience', 'event'].includes(group)) {
|
|
108
|
+
if (['publish', 'pause', 'resume', 'archive'].includes(action))
|
|
109
|
+
return ['campaign:publish'];
|
|
110
|
+
if (['create', 'update', 'discard'].includes(action))
|
|
111
|
+
return ['survey:write'];
|
|
112
|
+
return ['site:read'];
|
|
113
|
+
}
|
|
114
|
+
if (['results', 'responses', 'user'].includes(group))
|
|
115
|
+
return ['site:read'];
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
export const commandCatalog = usages.split('\n').flatMap(line => {
|
|
119
|
+
const withoutBinary = line.replace(/^userreason /, '');
|
|
120
|
+
const path = withoutBinary.match(/^[a-z]+(?:[| ][a-z]+)*/)?.[0].trim() ?? '';
|
|
121
|
+
const parts = path.split(' '), last = parts.pop();
|
|
122
|
+
return last.split('|').map(action => {
|
|
123
|
+
const command = [...parts, action].join(' ');
|
|
124
|
+
const targeted = ['preview', 'site', 'prompt', 'audience', 'results', 'responses', 'user', 'event', 'query'].includes(parts[0] ?? action);
|
|
125
|
+
const notes = targeted ? ['Workspace and site default to the project SDK initialization. Explicit flags override detection. Missing access never selects another site.'] : [];
|
|
126
|
+
if (command === 'preview capture')
|
|
127
|
+
notes.push('Development API only. Requires Chrome; first use downloads a pinned external SingleFile CLI via npm. Captures use a fresh browser profile, preserve responsive assets and expire after seven days. Use --interactive to answer browser dialogs in a visible capture window. Returns links.preview.');
|
|
128
|
+
if (['create', 'update'].includes(action) && ['prompt', 'audience'].includes(parts[0]))
|
|
129
|
+
notes.push('The JSON file is a complete definition, not a patch. Saving a draft does not publish it.');
|
|
130
|
+
if (parts[0] === 'prompt' && ['create', 'update', 'publish', 'pause', 'resume', 'archive'].includes(action))
|
|
131
|
+
notes.push(`Share the returned links.${['create', 'update'].includes(action) ? 'edit' : 'results'} so the user can review the survey; state whether changes are draft or published.`);
|
|
132
|
+
if (withoutBinary.includes('--revision'))
|
|
133
|
+
notes.push('Read the current revision before changing a resource. On conflict, reread; do not retry blindly.');
|
|
134
|
+
if (action === 'delete')
|
|
135
|
+
notes.push('Without --confirm, only previews impact. Permanent deletion requires the exact name, reviewed token and revision, plus explicit user authorization. Use deletion status to track cleanup.');
|
|
136
|
+
if (action === 'publish')
|
|
137
|
+
notes.push('Publishing changes live delivery. Publish only the survey or audience the user approved.');
|
|
138
|
+
if (command === 'command' || command === 'query')
|
|
139
|
+
notes.push('The server derives the required permission from the requested operation and resource.');
|
|
140
|
+
return { command, usage: line.replace(path, command), summary: descriptions[command] ?? descriptions[parts[0] ?? action], scopes: scopeFor(command), notes };
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
export const globalHelp = `Workspace and site default to the SDK initialization in the current project.
|
|
144
|
+
Explicit --workspace / --site override it. No project configuration file is created.
|
|
145
|
+
Surveys use the existing prompt command and API names.
|
|
146
|
+
Global options: --dev, --api-url ORIGIN, --client-id CLIENT_ID, --json
|
|
147
|
+
Credential storage: --credential-store file|keychain, or UR_CREDENTIAL_STORE (default file)
|
|
148
|
+
Login saves the selected store for this API environment. Existing Keychain sessions require --credential-store keychain.
|
|
149
|
+
Docs: --docs-url ORIGIN, --offline (bundled CLI reference), --live (require hosted docs)
|
|
150
|
+
Creation options: --request-id UUID, --workspace-id UUID / --site-id UUID
|
|
151
|
+
Site environment: --environment development|production (default production)
|
|
152
|
+
Origins are unrestricted by default; --origin adds an exact restriction.
|
|
153
|
+
File credentials are plaintext under the user config directory with owner-only permissions (0600).
|
|
154
|
+
Keychain is optional and never silently falls back to a file. WorkOS API keys are never required.`;
|
|
155
|
+
export const cliHelp = () => 'UserReason CLI\n\n' + commandCatalog.map(item => item.usage).join('\n') + '\n\n' + globalHelp + '\n';
|
|
156
|
+
export function getCommandReference(command) {
|
|
157
|
+
const key = command.trim().replace(/^userreason\s+/, '').replace(/\s+/g, ' ');
|
|
158
|
+
const found = commandCatalog.find(item => item.command === key);
|
|
159
|
+
if (!found)
|
|
160
|
+
throw new Error('Unknown CLI command. Use userreason cli search "QUESTION" or userreason cli list.');
|
|
161
|
+
return found;
|
|
162
|
+
}
|
|
163
|
+
export function searchCommands(query, limit = 5) {
|
|
164
|
+
if (!query.trim() || query.length > 500)
|
|
165
|
+
throw new Error('Command search needs a question of 1–500 characters');
|
|
166
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 20)
|
|
167
|
+
throw new Error('Command search limit must be an integer from 1 to 20');
|
|
168
|
+
const words = query.toLowerCase().match(/[a-z0-9]+/g)?.filter(word => !['a', 'an', 'the', 'to', 'for', 'i', 'want', 'how', 'do', 'my'].includes(word)) ?? [];
|
|
169
|
+
return commandCatalog.map(item => {
|
|
170
|
+
const path = item.command.toLowerCase(), text = (item.summary + ' ' + item.usage).toLowerCase();
|
|
171
|
+
const score = words.reduce((sum, word) => sum + (path.includes(word) ? 6 : text.includes(word) ? 1 : 0), 0);
|
|
172
|
+
return { item, score };
|
|
173
|
+
}).filter(item => item.score > 0).sort((a, b) => b.score - a.score || a.item.command.localeCompare(b.item.command)).slice(0, limit).map(({ item }) => item);
|
|
174
|
+
}
|
|
175
|
+
export function bundledCliMarkdown() {
|
|
176
|
+
return '# UserReason CLI command reference\n\nThis reference ships with the installed CLI and works offline. For live SDK documentation, use docs search or docs get with --live.\n\n' +
|
|
177
|
+
globalHelp + '\n\n## Access and retries\n\nLogin opens browser consent for all current and future workspaces and available permissions; the human can narrow them. Current membership and role still limit each operation. Use access --open for missing or expired grants. Local credential/network errors are not grant denials. Valid tokens are read without a lock; refreshing needs local write and network access. Never change grants to fix a local sandbox denial.\n\n' +
|
|
178
|
+
'Permissions: ' + SCOPES.join(', ') + '.\n\nUse the same request ID and unchanged input when retrying an uncertain mutation. Definitions are complete JSON files. Read each resource before changing its revision.\n\n' +
|
|
179
|
+
commandCatalog.map(item => '## ' + item.command + '\n\n' + item.summary + '\n\n```sh\n' + item.usage + '\n```\n\n' + (item.scopes.length ? 'Required scope: ' + item.scopes.join(', ') + '.\n\n' : '') + item.notes.join(' ') + '\n').join('\n');
|
|
180
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { type Account, type Credentials, type GrantRequest, type Scope, type Site, type Workspace } from "./contracts.js";
|
|
2
2
|
import type { ClientConfig } from "./config.js";
|
|
3
3
|
import { type Fetch } from "./workos.js";
|
|
4
|
+
export type PromptAction = 'create' | 'update' | 'publish' | 'pause' | 'resume' | 'archive';
|
|
5
|
+
export type AudienceAction = 'create' | 'update' | 'publish' | 'discard' | 'archive';
|
|
6
|
+
/** What the dashboard reads beside records: results, the response feed, users, events and members. */
|
|
7
|
+
export declare const READ_OPERATIONS: readonly ["insights.overview", "insights.prompt", "insights.experiments", "insights.breakdown", "responses.list", "users.list", "users.get", "users.eligibility", "prompts.validate", "prompts.history", "events.overview", "site.activity", "members.list"];
|
|
8
|
+
export type ReadOperation = typeof READ_OPERATIONS[number];
|
|
4
9
|
export declare class ApiError extends Error {
|
|
5
10
|
status: number;
|
|
6
11
|
code: string;
|
|
@@ -18,23 +23,30 @@ export declare class UserReasonClient {
|
|
|
18
23
|
});
|
|
19
24
|
private token;
|
|
20
25
|
request(path: string, body?: unknown): Promise<unknown>;
|
|
26
|
+
private authenticatedRequest;
|
|
27
|
+
previewCaptures(workspaceId: string, siteId: string, forUpload?: boolean): Promise<unknown>;
|
|
28
|
+
uploadPreview(workspaceId: string, siteId: string, name: string, html: Uint8Array): Promise<unknown>;
|
|
29
|
+
deletion(operation: 'preview' | 'request' | 'status', input: Record<string, unknown>): Promise<unknown>;
|
|
21
30
|
account(): Promise<Account>;
|
|
22
31
|
createWorkspace(name: string, options?: {
|
|
23
32
|
requestId?: string;
|
|
24
33
|
workspaceId?: string;
|
|
25
34
|
}): Promise<Workspace>;
|
|
26
35
|
sites(workspaceId: string): Promise<Site[]>;
|
|
36
|
+
siteById(siteId: string): Promise<Site>;
|
|
27
37
|
createSite(input: {
|
|
28
38
|
workspaceId: string;
|
|
29
39
|
name: string;
|
|
30
40
|
environment: Site["environment"];
|
|
31
|
-
origins
|
|
41
|
+
origins?: string[];
|
|
32
42
|
requestId?: string;
|
|
33
43
|
siteId?: string;
|
|
34
44
|
}): Promise<Site>;
|
|
35
45
|
requestGrant(input: {
|
|
36
46
|
workspaceId?: string;
|
|
37
47
|
siteIds?: string[];
|
|
48
|
+
allWorkspaces?: boolean;
|
|
49
|
+
workspaceIds?: string[];
|
|
38
50
|
scopes: Scope[];
|
|
39
51
|
requestId?: string;
|
|
40
52
|
}): Promise<GrantRequest>;
|
|
@@ -45,8 +57,67 @@ export declare class UserReasonClient {
|
|
|
45
57
|
workspaceId: string;
|
|
46
58
|
siteId?: string;
|
|
47
59
|
resource: string;
|
|
60
|
+
eventCaptureMode?: string;
|
|
61
|
+
eventCollectionLimits?: unknown;
|
|
48
62
|
limit?: number;
|
|
49
63
|
after?: string;
|
|
64
|
+
id?: string;
|
|
65
|
+
promptId?: string;
|
|
66
|
+
}): Promise<unknown>;
|
|
67
|
+
/** A read-only view of one workspace (members) or one of its sites (everything else). Empty values are left out. */
|
|
68
|
+
read(operation: ReadOperation, input: {
|
|
69
|
+
workspaceId: string;
|
|
70
|
+
siteId?: string;
|
|
71
|
+
} & Record<string, unknown>): Promise<unknown>;
|
|
72
|
+
/** One site as stored, with its revision and frequency policy (null while it follows the starting values). */
|
|
73
|
+
siteRecord(input: {
|
|
74
|
+
workspaceId: string;
|
|
75
|
+
siteId: string;
|
|
76
|
+
}): Promise<Record<string, unknown>>;
|
|
77
|
+
prompts(input: {
|
|
78
|
+
workspaceId: string;
|
|
79
|
+
siteId: string;
|
|
80
|
+
limit?: number;
|
|
81
|
+
after?: string;
|
|
82
|
+
}): Promise<unknown>;
|
|
83
|
+
prompt(input: {
|
|
84
|
+
workspaceId: string;
|
|
85
|
+
siteId: string;
|
|
86
|
+
promptId: string;
|
|
87
|
+
}): Promise<Record<string, unknown>>;
|
|
88
|
+
promptCommand(input: {
|
|
89
|
+
workspaceId: string;
|
|
90
|
+
siteId: string;
|
|
91
|
+
action: PromptAction;
|
|
92
|
+
promptId: string;
|
|
93
|
+
requestId: string;
|
|
94
|
+
revision?: string;
|
|
95
|
+
definition?: Record<string, unknown>;
|
|
96
|
+
}): Promise<unknown>;
|
|
97
|
+
/** Saved audiences: named conditions that several prompts of a site share. */
|
|
98
|
+
audiences(input: {
|
|
99
|
+
workspaceId: string;
|
|
100
|
+
siteId: string;
|
|
101
|
+
limit?: number;
|
|
102
|
+
after?: string;
|
|
103
|
+
}): Promise<unknown>;
|
|
104
|
+
audience(input: {
|
|
105
|
+
workspaceId: string;
|
|
106
|
+
siteId: string;
|
|
107
|
+
audienceId: string;
|
|
108
|
+
}): Promise<Record<string, unknown>>;
|
|
109
|
+
/**
|
|
110
|
+
* Create and update save a draft from `{ name, description, rule }`. Publish makes every prompt
|
|
111
|
+
* that names the audience follow the new version at once; read `used_by` first.
|
|
112
|
+
*/
|
|
113
|
+
audienceCommand(input: {
|
|
114
|
+
workspaceId: string;
|
|
115
|
+
siteId: string;
|
|
116
|
+
action: AudienceAction;
|
|
117
|
+
audienceId: string;
|
|
118
|
+
requestId: string;
|
|
119
|
+
revision?: string;
|
|
120
|
+
audience?: Record<string, unknown>;
|
|
50
121
|
}): Promise<unknown>;
|
|
51
122
|
waitForGrant(id: string, options?: {
|
|
52
123
|
timeoutMs?: number;
|