@userreason/cli 0.1.1 → 0.2.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 +217 -0
- package/dist/bin.js +93 -8
- package/dist/catalog.js +29 -3
- package/dist/client.d.ts +8 -2
- package/dist/client.js +26 -11
- package/dist/first-survey-preparation.d.ts +8 -0
- package/dist/first-survey-preparation.js +29 -0
- package/dist/first-survey-source.d.ts +61 -0
- package/dist/first-survey-source.js +298 -0
- package/dist/first-survey-terminal.d.ts +5 -0
- package/dist/first-survey-terminal.js +16 -0
- package/dist/first-survey.d.ts +88 -0
- package/dist/first-survey.js +227 -0
- package/dist/help.js +6 -6
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/profile.js +1 -1
- package/dist/recommendation-source.d.ts +13 -0
- package/dist/recommendation-source.js +211 -0
- package/dist/recommended-surveys.d.ts +30 -0
- package/dist/recommended-surveys.js +201 -0
- package/dist/setup-diagnostics.d.ts +2 -0
- package/dist/setup-diagnostics.js +26 -0
- package/dist/setup-executor.d.ts +42 -0
- package/dist/setup-executor.js +350 -0
- package/dist/setup-files.d.ts +11 -0
- package/dist/setup-files.js +96 -0
- package/dist/setup-heartbeat.d.ts +268 -0
- package/dist/setup-heartbeat.js +120 -0
- package/dist/setup-runner.d.ts +16 -0
- package/dist/setup-runner.js +191 -0
- package/dist/setup-source.d.ts +32 -0
- package/dist/setup-source.js +48 -0
- package/dist/setup-store.d.ts +37 -0
- package/dist/setup-store.js +266 -0
- package/dist/setup-terminal.d.ts +3 -0
- package/dist/setup-terminal.js +6 -0
- package/dist/setup-wire.d.ts +1367 -0
- package/dist/setup-wire.js +190 -0
- package/dist/setup.d.ts +1423 -0
- package/dist/setup.js +264 -0
- package/dist/starter-survey.d.ts +22 -0
- package/dist/starter-survey.js +457 -0
- package/dist/telemetry.d.ts +67 -0
- package/dist/telemetry.js +304 -0
- package/dist/workos.d.ts +1 -1
- package/dist/workos.js +4 -2
- package/package.json +3 -2
- package/skills/userreason/SKILL.md +43 -0
- package/skills/userreason/references/management.md +6 -0
package/README.md
CHANGED
|
@@ -121,3 +121,220 @@ site's environment is separate from the API environment.
|
|
|
121
121
|
|
|
122
122
|
See [troubleshooting](https://docs.userreason.com/get-started/troubleshooting/)
|
|
123
123
|
for installation, sign-in and configuration help.
|
|
124
|
+
|
|
125
|
+
## Optional usage measurement
|
|
126
|
+
|
|
127
|
+
The CLI reports canonical command types, success/failure counts, durations,
|
|
128
|
+
execution mode and its version to the UserReason API. It never reports command
|
|
129
|
+
arguments, documentation searches, file paths, source code, command output,
|
|
130
|
+
raw errors, email addresses or credentials as analytics properties. Signed-in
|
|
131
|
+
requests can be associated with the verified UserReason account and workspace;
|
|
132
|
+
anonymous usage uses a random local installation ID. Switching accounts never
|
|
133
|
+
attributes an earlier account's queued activity to the next account.
|
|
134
|
+
|
|
135
|
+
Usage is batched in the per-user UserReason configuration directory, outside
|
|
136
|
+
your project, with a seven-day expiry, bounded storage and an upload deadline of
|
|
137
|
+
500 ms for anonymous batches or 1.5 seconds for authenticated batches. Failed
|
|
138
|
+
uploads do not change command output or exit status.
|
|
139
|
+
`--offline` sends no analytics requests. Local command discovery (`cli`) also
|
|
140
|
+
stays offline. The queue is retried on later online commands; aggregated command
|
|
141
|
+
counts, rather than the number of batches, describe usage.
|
|
142
|
+
|
|
143
|
+
Disable for one invocation with `--no-telemetry`, for a process or CI job with
|
|
144
|
+
`UR_TELEMETRY_DISABLED=1` (or `DO_NOT_TRACK=1`), or persistently with:
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
userreason telemetry disable
|
|
148
|
+
userreason telemetry status
|
|
149
|
+
userreason telemetry enable
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The persistent setting is shared with the installation wizard. Disabling it
|
|
153
|
+
also removes locally queued analytics and the random installation identity.
|
|
154
|
+
|
|
155
|
+
### Schedule a survey and set collection targets
|
|
156
|
+
|
|
157
|
+
The JSON definition accepted by `prompt create` and `prompt update` supports an
|
|
158
|
+
optional `rules.collection` object. For example:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"starts_at": "2026-10-20T07:00:00.000Z",
|
|
163
|
+
"ends_at": "2026-10-27T16:00:00.000Z",
|
|
164
|
+
"time_zone": "Europe/Amsterdam",
|
|
165
|
+
"response_target": 500,
|
|
166
|
+
"daily_response_target": 50
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Publish the saved draft through the normal prompt command. Use null for an unset
|
|
171
|
+
timestamp or unlimited target, or omit the object for no schedule/targets. Targets
|
|
172
|
+
are shared across the campaign's versions, experiments and enrollments. They stop
|
|
173
|
+
new admissions while allowing already-admitted surveys to finish. `prompt list`
|
|
174
|
+
and prompt reads include the current collection state and completed counts.
|
|
175
|
+
|
|
176
|
+
## Conversational surveys (development)
|
|
177
|
+
|
|
178
|
+
The development API accepts a terminal conversation as a text question with a
|
|
179
|
+
versioned `conversation` setting. Use the existing `prompt create`, `update`,
|
|
180
|
+
`validate` and `publish` commands with the complete definition. Read
|
|
181
|
+
`https://docs-dev.userreason.com/prompts/conversations/` for the current schema.
|
|
182
|
+
A conversation stands alone or starts an experiment version; it cannot follow a
|
|
183
|
+
rating. Quick, Focused and In-depth presets limit follow-ups and time. Review and
|
|
184
|
+
Send remain separate: only the visitor's accepted answer is a response.
|
|
185
|
+
|
|
186
|
+
```sh
|
|
187
|
+
userreason workspace ai get --dev --workspace WORKSPACE_UUID
|
|
188
|
+
userreason workspace ai get --dev --workspace WORKSPACE_UUID --site SITE_UUID
|
|
189
|
+
userreason workspace ai set --dev --workspace WORKSPACE_UUID --file AI-SETTINGS.json
|
|
190
|
+
userreason results conversations PROMPT_UUID --dev --workspace WORKSPACE_UUID --site SITE_UUID
|
|
191
|
+
userreason conversation get CONVERSATION_UUID --dev --workspace WORKSPACE_UUID --site SITE_UUID
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`AI-SETTINGS.json` contains only the fields to change, for example
|
|
195
|
+
`{"enabled":true,"voice_enabled":false}`. Owners and admins manage allowance
|
|
196
|
+
controls. To change site context, pass one `--site` and a file such as
|
|
197
|
+
`{"product_context":"Scheduled reports email PDF or CSV files."}`; editors can
|
|
198
|
+
update that site context. Missing settings keep their current value.
|
|
199
|
+
|
|
200
|
+
Typed, spoken and live-test sessions share workspace AI allowance. Scripted
|
|
201
|
+
preview uses no allowance. Accepted text, transcript, and optional generated
|
|
202
|
+
analysis have different provenance; generated analysis is not a visitor answer.
|
|
203
|
+
These commands require the matching development backend and private development
|
|
204
|
+
package set. Public npm `latest` remains unchanged by this rollout.
|
|
205
|
+
|
|
206
|
+
## Browser setup client and local execution
|
|
207
|
+
|
|
208
|
+
The exported setup-v1 client provides a proof-bound handshake, strict bounded
|
|
209
|
+
protocol reads, heartbeat/claim/result adapters and `runSetupExecutor` with a per-project lock for the
|
|
210
|
+
browser-driven wizard. It uses a separate owner-only `setup-v1` store
|
|
211
|
+
outside the project; it neither reads nor replaces normal login credentials.
|
|
212
|
+
Loading a selected expired setup removes its capability. New setup saves lazily
|
|
213
|
+
inspect at most 128 directory entries and remove validated setup records at
|
|
214
|
+
least seven days old; later saves handle remaining batches. There is no cleanup
|
|
215
|
+
daemon. Unknown files, symlinks, project files, locks and normal login credentials
|
|
216
|
+
are left alone.
|
|
217
|
+
The server must explicitly advertise `browser_setup` v1 as enabled. Old or
|
|
218
|
+
disabled servers return an unsupported result before local effects. A started
|
|
219
|
+
setup never silently falls back to a different installation flow.
|
|
220
|
+
|
|
221
|
+
Setup requests also propagate `--no-telemetry`, the shared saved opt-out,
|
|
222
|
+
`UR_TELEMETRY_DISABLED` and `DO_NOT_TRACK` to optional server-side setup
|
|
223
|
+
measurement. Unreadable preference state suppresses measurement without blocking
|
|
224
|
+
the setup request. Browser measurement preferences remain separate.
|
|
225
|
+
|
|
226
|
+
An active setup can request its simulated sample using:
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
userreason setup sample --setup SETUP_UUID --directory /path/to/original/project --json
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Use `--api-url` or `--dev` for the same API as that setup. This operation requires
|
|
233
|
+
its unexpired local setup capability and an open sample-test step. It reads a
|
|
234
|
+
clearly labelled static sample and records only that setup's read receipt; it
|
|
235
|
+
does not read real feedback, create responses, consume response quota, publish
|
|
236
|
+
surveys or authorize ongoing CLI access. The ID in the command is not a bearer
|
|
237
|
+
credential. Setup credentials are never printed. `--request-id UUID` permits an
|
|
238
|
+
explicit idempotent retry of the read.
|
|
239
|
+
|
|
240
|
+
The executor accepts only locally prepared, browser-approved typed operations.
|
|
241
|
+
It writes a private intent before starting, verifies exact source/manifest hashes,
|
|
242
|
+
and records the result before reporting. Package-manager lockfiles are declared
|
|
243
|
+
managed outputs: their contents are generated during dependency resolution and
|
|
244
|
+
their actual hashes are saved in the local receipt, rather than predicted.
|
|
245
|
+
After an interruption it reconciles known output; it never blindly reruns an
|
|
246
|
+
unknown command. `recoverSetupProjectLock` explicitly releases a crashed owner's
|
|
247
|
+
lock only after that process is gone. Local receipts contain prepared file bytes,
|
|
248
|
+
stay outside the project with owner-only permissions, and expire after seven
|
|
249
|
+
days. Lazy cleanup is bounded; receipts never restore expired remote access.
|
|
250
|
+
The browser executor requires durable file synchronization (`fsync`). Node 24
|
|
251
|
+
permission mode disables that operation even for allowed paths, so setup stops
|
|
252
|
+
before server calls or application changes under those flags. It does not remove
|
|
253
|
+
permission flags or fall back to nondurable receipts.
|
|
254
|
+
|
|
255
|
+
`detectSetupTerminal` emits fixed platform/app codes only. Installing the project
|
|
256
|
+
agent skill does not grant continuing management access. Ordinary CLI login,
|
|
257
|
+
terminal installation and first-survey commands are unchanged.
|
|
258
|
+
|
|
259
|
+
`runBrowserSetup` connects this executor to the optional browser onboarding flow.
|
|
260
|
+
The server's source-analysis flag is checked before collecting any source. The
|
|
261
|
+
local collector prepares a bounded, memory-only application packet with no Git
|
|
262
|
+
history. `onSourcePrepared` can display the exact excerpts locally; the initial
|
|
263
|
+
`source-plan` request contains only relative paths, line ranges, hashes and package
|
|
264
|
+
metadata. The browser separately approves that packet digest and configured
|
|
265
|
+
providers. Immediately before `source-upload`, the runner rechecks all full-file
|
|
266
|
+
hashes, recollects the same snapshot and validates installed package metadata.
|
|
267
|
+
Source text is never saved in setup receipts. Resuming rebuilds the original
|
|
268
|
+
snapshot ID and refuses changed content until the browser reviews a fresh packet.
|
|
269
|
+
|
|
270
|
+
Heartbeat continues while the bounded analysis request runs. A dropped response
|
|
271
|
+
is reconciled by polling, not a repeated provider call. A failed preparation or
|
|
272
|
+
analysis needs a browser retry or skip. The browser owns candidate selection,
|
|
273
|
+
inactive draft creation, sample skip, finish and cancellation. The local runner
|
|
274
|
+
cannot create drafts, publish surveys or change ongoing CLI grants. Server rollout
|
|
275
|
+
flags remain off by default; these are prepared clients for a compatible release.
|
|
276
|
+
|
|
277
|
+
## Optional first-survey guidance
|
|
278
|
+
|
|
279
|
+
The development wizard also uses the exported `starterSurvey` flow. It provides
|
|
280
|
+
one-question starters for improvement, workflow friction and satisfaction, with
|
|
281
|
+
optional background personalization. Local preparation after sign-in is
|
|
282
|
+
memory-only; provider processing still needs approval of the exact source packet.
|
|
283
|
+
The model returns wording and cited rationale, while the Worker compiles the
|
|
284
|
+
inactive draft. Users keep control of their edits and choose delivery separately.
|
|
285
|
+
An unconfigured starter matches nobody, even if it is later published unchanged.
|
|
286
|
+
Draft save receipts survive a lost response so retries use the same identifiers.
|
|
287
|
+
This does not change the legacy `first-survey start` command.
|
|
288
|
+
|
|
289
|
+
`userreason first-survey start --workspace WORKSPACE_UUID --site SITE_UUID
|
|
290
|
+
--directory /absolute/app` offers interactive, evidence-backed suggestions and
|
|
291
|
+
individual unpublished draft creation. Repeating the command resumes the private
|
|
292
|
+
session. `handoff` prints a coding-agent task; `cancel` prevents further work;
|
|
293
|
+
`delete` removes the retained plan (not already-created drafts). No publication
|
|
294
|
+
or source edits are automatic. Noninteractive/JSON runs cannot authorize analysis.
|
|
295
|
+
|
|
296
|
+
Only a reviewed packet from the explicit application root is uploaded. Default
|
|
297
|
+
bounds: 200 files, 500 KiB local text, 10 KiB source packet, 12,000 input and 4,000
|
|
298
|
+
output tokens, one clarification round. Symlinks, ignored/generated files and
|
|
299
|
+
files with detected secrets are omitted. Detection is best effort: review every
|
|
300
|
+
excerpt. Git subjects need separate consent and are limited to 20 commits/90 days;
|
|
301
|
+
no authors, diffs or network fetch. Narrow the selected paths when truncated.
|
|
302
|
+
|
|
303
|
+
Before saving, the review shows every question, rating label, answer option and
|
|
304
|
+
conditional route in the returned definition. The analysis deadline is 35 seconds;
|
|
305
|
+
the transport allows the backend's maximum deadline without cutting it off early.
|
|
306
|
+
Draft validation remains authoritative: unsupported survey behavior cannot be
|
|
307
|
+
silently converted or published by this flow.
|
|
308
|
+
|
|
309
|
+
The provider destination and configured policy are shown before approval. Local
|
|
310
|
+
sessions contain hashes, private proposals and mutation receipts, never submitted source
|
|
311
|
+
packets or credentials (generated plans can quote source); protect their owner-only config directory. They expire
|
|
312
|
+
after seven days. Worker plans expire after seven days and are purged on subsequent
|
|
313
|
+
planning activity; provider retention is separately configured/disclosed. Drafts
|
|
314
|
+
have their normal account retention. Deletion retains a content-free budget
|
|
315
|
+
receipt until expiry so it cannot reset allowance. No first-survey content is sent
|
|
316
|
+
to product telemetry.
|
|
317
|
+
|
|
318
|
+
Analysis requires an ongoing connection with `site:read` and `survey:write` on the
|
|
319
|
+
exact site; installation grants do not suffice. Use `access --open` if necessary,
|
|
320
|
+
reviewing replacement-grant semantics rather than broadening installation access.
|
|
321
|
+
Provider errors do not undo installation. Use the agent/manual workflow when
|
|
322
|
+
analysis is unavailable. Integration and browser eligibility remain unverified
|
|
323
|
+
until tested: reachable configuration is not browser verification. Instrumentation
|
|
324
|
+
requires a reviewed, hash-guarded diff and separate edit approval using the agent
|
|
325
|
+
skill; unsupported host transforms are never executed automatically.
|
|
326
|
+
|
|
327
|
+
The 0.2.1 production installation profile selects `@userreason/js@0.5.3` and
|
|
328
|
+
`@userreason/cli@0.2.1` exactly. Browser setup remains an explicit wizard entry
|
|
329
|
+
and requires an enabled compatible server; ordinary CLI authentication and
|
|
330
|
+
management commands retain their existing authorization flow.
|
|
331
|
+
|
|
332
|
+
Local setup execution failures retain bounded, redacted diagnostic text in the
|
|
333
|
+
private execution receipt. `onOperationError` displays it on failure and resume.
|
|
334
|
+
The detailed text never enters setup API requests or telemetry; the server still
|
|
335
|
+
receives only the typed outcome code.
|
|
336
|
+
|
|
337
|
+
A new run automatically recovers an existing project lock only after verifying
|
|
338
|
+
that its recorded process no longer exists. It still acquires the lock exclusively,
|
|
339
|
+
keeps all installation receipts for reconciliation, and refuses live, unreadable
|
|
340
|
+
or malformed locks. A live-lock error identifies the process to finish or stop.
|
package/dist/bin.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { firstSurvey } from './first-survey.js';
|
|
3
|
+
import { firstSurveyTerminalUI } from './first-survey-terminal.js';
|
|
4
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import { realpath } from 'node:fs/promises';
|
|
2
6
|
import { eventCollection } from './event-policy.js';
|
|
3
7
|
import { CaptureError, capturePage, captureUrl } from './capture.js';
|
|
4
8
|
import { randomUUID } from "node:crypto";
|
|
@@ -19,8 +23,14 @@ import { docsOrigin, packageProfile } from "./profile.js";
|
|
|
19
23
|
import { createSpinner, createUiKit, detectAgentHarness, detectColorLevel } from './ui.js';
|
|
20
24
|
import { nounHelp, overviewHelp, overviewNouns, suggestCommand } from './help.js';
|
|
21
25
|
import { grantSummary, promptsView, resultsOverviewView, sitesView, whoamiView, workspacesView } from './views.js';
|
|
26
|
+
import { ToolTelemetry, telemetryCommand, telemetryEnabled, setTelemetryEnabled } from './telemetry.js';
|
|
27
|
+
import { resumeBrowserSetup } from './setup.js';
|
|
28
|
+
import { SetupError } from './setup-store.js';
|
|
22
29
|
/** Terminal styling applies only to a person at a terminal: never when piped, with --json, or in an agent harness. */
|
|
23
30
|
let errorStyle;
|
|
31
|
+
let analytics;
|
|
32
|
+
let analyticsCommand = 'unknown';
|
|
33
|
+
const analyticsStarted = performance.now();
|
|
24
34
|
const cliVersion = () => { try {
|
|
25
35
|
return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
26
36
|
}
|
|
@@ -97,22 +107,35 @@ async function readJsonFile(file) {
|
|
|
97
107
|
}
|
|
98
108
|
async function main() {
|
|
99
109
|
const parsed = parseArgs({ allowPositionals: true, strict: true, options: {
|
|
100
|
-
all: { type: "boolean" }, human: { type: "boolean" }, "no-color": { type: "boolean" },
|
|
110
|
+
directory: { type: "string" }, all: { type: "boolean" }, human: { type: "boolean" }, "no-color": { type: "boolean" }, "no-telemetry": { type: 'boolean' },
|
|
101
111
|
width: { type: 'string' }, height: { type: 'string' }, 'capture-bin': { type: 'string' }, 'browser-executable': { type: 'string' }, interactive: { type: 'boolean' },
|
|
102
112
|
preview: { type: "boolean" }, confirm: { type: "string" }, help: { type: "boolean", short: "h" }, json: { type: "boolean" }, dev: { type: "boolean" }, open: { type: "boolean" }, wait: { type: "boolean" }, "no-wait": { type: "boolean" },
|
|
103
113
|
offline: { type: "boolean" }, live: { type: "boolean" }, "credential-store": { type: "string" }, "api-url": { type: "string" }, "docs-url": { type: "string" }, "client-id": { type: "string" }, name: { type: "string" }, workspace: { type: "string" }, id: { type: "string" },
|
|
104
114
|
origin: { type: "string", multiple: true }, scope: { type: "string", multiple: true }, site: { type: "string", multiple: true }, environment: { type: "string" },
|
|
105
|
-
"request-id": { type: "string" }, "workspace-id": { type: "string" }, "site-id": { type: "string" },
|
|
115
|
+
"request-id": { type: "string" }, setup: { type: "string" }, "workspace-id": { type: "string" }, "site-id": { type: "string" },
|
|
106
116
|
file: { type: "string" }, "context-file": { type: "string" }, purpose: { type: "string" }, mode: { type: "string" }, "confirmation-token": { type: "string" }, resource: { type: "string" }, limit: { type: "string" }, after: { type: "string" }, revision: { type: "string" }, "prompt-id": { type: "string" },
|
|
107
117
|
days: { type: "string" }, "time-zone": { type: "string" }, country: { type: "string" }, segment: { type: "string" }, by: { type: "string" }, user: { type: "string" }, kind: { type: "string" }, search: { type: "string" }, cursor: { type: "string" },
|
|
108
118
|
run: { type: 'string' }, variant: { type: 'string' }, 'all-attempts': { type: 'boolean' }, 'reported-only': { type: 'boolean' },
|
|
109
119
|
} });
|
|
110
120
|
const values = parsed.values, [command, action, positionalId] = parsed.positionals;
|
|
121
|
+
if (command === 'telemetry') {
|
|
122
|
+
if (action === 'enable' || action === 'disable')
|
|
123
|
+
await setTelemetryEnabled(action === 'enable');
|
|
124
|
+
else if (action !== 'status')
|
|
125
|
+
throw new Error('Use userreason telemetry enable, disable or status');
|
|
126
|
+
process.stdout.write(JSON.stringify({ enabled: await telemetryEnabled() }) + '\n');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
111
129
|
if (action === 'delete' && positionalId && command === 'workspace')
|
|
112
130
|
values.workspace = uuid(positionalId);
|
|
113
131
|
if (action === 'delete' && positionalId && command === 'site')
|
|
114
132
|
values.site = [uuid(positionalId)];
|
|
115
133
|
const harness = detectAgentHarness();
|
|
134
|
+
analyticsCommand = telemetryCommand(parsed.positionals, values.help);
|
|
135
|
+
try {
|
|
136
|
+
analytics = new ToolTelemetry({ surface: 'cli', releaseVersion: cliVersion() ?? 'unknown', executionMode: process.env.CI ? 'ci' : harness ? 'agent' : process.stdin.isTTY ? 'interactive' : 'noninteractive', apiUrl: values['api-url'] ?? process.env.UR_API_URL ?? ((values.dev ?? developmentApi()) ? 'https://api-dev.userreason.com' : 'https://api.userreason.com'), disabled: values['no-telemetry'] || values.help || !command, offline: values.offline || command === 'cli' });
|
|
137
|
+
}
|
|
138
|
+
catch { /* Optional telemetry never changes command validation. */ }
|
|
116
139
|
const humanOut = Boolean(process.stdout.isTTY) && !values.json && (!harness || Boolean(values.human));
|
|
117
140
|
const humanErr = Boolean(process.stderr.isTTY) && !values.json && (!harness || Boolean(values.human));
|
|
118
141
|
const kit = createUiKit({ level: detectColorLevel({ stream: process.stdout, off: values["no-color"] }) });
|
|
@@ -123,7 +146,7 @@ async function main() {
|
|
|
123
146
|
process.stdout.write(values.all ? cliHelp() : noun ?? overviewHelp(kit, { version: cliVersion(), development: values.dev ?? developmentApi(), trademark: humanOut }));
|
|
124
147
|
return;
|
|
125
148
|
}
|
|
126
|
-
if (!overviewNouns.includes(command)) {
|
|
149
|
+
if (command !== "first-survey" && !overviewNouns.includes(command)) {
|
|
127
150
|
const guess = suggestCommand(command);
|
|
128
151
|
throw new UnknownCommand(command, guess);
|
|
129
152
|
}
|
|
@@ -145,6 +168,19 @@ async function main() {
|
|
|
145
168
|
}
|
|
146
169
|
if (values.offline && values.live)
|
|
147
170
|
throw new Error('Choose --offline or --live');
|
|
171
|
+
if (command === 'setup') {
|
|
172
|
+
if (action !== 'sample' || parsed.positionals.length !== 2)
|
|
173
|
+
throw new Error('Use userreason setup sample --setup UUID [--directory PATH]');
|
|
174
|
+
if (values.offline)
|
|
175
|
+
throw new Error('Reading the setup sample requires the setup service; omit --offline.');
|
|
176
|
+
if (values.workspace || values.site || values['client-id'] || values['credential-store'])
|
|
177
|
+
throw new Error('The setup sample uses only the selected setup session, not workspace flags or a general CLI login.');
|
|
178
|
+
const origin = values['api-url'] ?? process.env.UR_API_URL ?? ((values.dev ?? developmentApi()) ? 'https://api-dev.userreason.com' : 'https://api.userreason.com');
|
|
179
|
+
const { client } = await resumeBrowserSetup(origin, uuid(required('setup')), values.directory ?? process.cwd(), { measurementDisabled: values['no-telemetry'] });
|
|
180
|
+
const sample = await client.sample(values['request-id'] ? uuid(values['request-id']) : undefined);
|
|
181
|
+
process.stdout.write(JSON.stringify(sample, null, values.json ? undefined : 2) + '\n');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
148
184
|
if (command === "docs") {
|
|
149
185
|
// Public documentation: no login, saved configuration or credential store is involved.
|
|
150
186
|
let projectDevelopment;
|
|
@@ -178,7 +214,7 @@ async function main() {
|
|
|
178
214
|
project = await detectProjectTarget();
|
|
179
215
|
}
|
|
180
216
|
catch (error) {
|
|
181
|
-
if (!values.workspace && !values.site)
|
|
217
|
+
if (!values.workspace && !values.site && !(command === 'whoami' && error.code === 'ERR_ACCESS_DENIED'))
|
|
182
218
|
throw error;
|
|
183
219
|
}
|
|
184
220
|
const explicitApi = values["api-url"] ?? process.env.UR_API_URL ?? (values.dev ? 'https://api-dev.userreason.com' : undefined);
|
|
@@ -187,8 +223,10 @@ async function main() {
|
|
|
187
223
|
const selectedApi = explicitApi ?? project?.apiUrl;
|
|
188
224
|
const development = values.dev ?? (selectedApi === 'https://api-dev.userreason.com' ? true : selectedApi === 'https://api.userreason.com' ? false : packageProfile().apiEnvironment === 'development');
|
|
189
225
|
const config = await resolveConfig({ development, apiUrl: selectedApi, clientId: values["client-id"], credentialStore: values["credential-store"] });
|
|
190
|
-
|
|
191
|
-
const
|
|
226
|
+
analytics?.configure({ apiUrl: config.apiUrl });
|
|
227
|
+
const rawCredentials = resolveCredentials(config);
|
|
228
|
+
const credentials = analytics?.observeCredentials(rawCredentials) ?? rawCredentials, client = new UserReasonClient(config, credentials);
|
|
229
|
+
const targeting = ['conversation', 'preview', 'prompt', 'audience', 'results', 'responses', 'user', 'event', 'member', 'query', 'site'].includes(command);
|
|
192
230
|
let targetName;
|
|
193
231
|
if (targeting && !values.workspace) {
|
|
194
232
|
const siteId = values.site?.length === 1 ? values.site[0] : project?.siteId;
|
|
@@ -204,8 +242,29 @@ async function main() {
|
|
|
204
242
|
: `Using ${printable(site.name)} (${site.id}) on ${config.apiUrl}\n`);
|
|
205
243
|
}
|
|
206
244
|
}
|
|
245
|
+
analytics?.configure({ context: { ...(values.workspace ? { workspace_id: values.workspace } : {}), ...(values.site?.length === 1 ? { site_id: values.site[0] } : {}), ...(values.environment === 'development' || values.environment === 'production' ? { source_site_environment: values.environment } : {}) } });
|
|
207
246
|
let result;
|
|
208
|
-
if (command ===
|
|
247
|
+
if (command === 'first-survey') {
|
|
248
|
+
if (values.site?.length !== 1)
|
|
249
|
+
throw new Error('Select exactly one site');
|
|
250
|
+
if (!['start', 'handoff', 'cancel', 'delete'].includes(action ?? ''))
|
|
251
|
+
throw new Error('Use first-survey start, handoff, cancel or delete');
|
|
252
|
+
if (action === 'start' && (!process.stdin.isTTY || values.json))
|
|
253
|
+
throw new Error('Guided source consent requires an interactive terminal; use first-survey handoff for an agent task');
|
|
254
|
+
const abort = new AbortController();
|
|
255
|
+
const cancel = () => abort.abort();
|
|
256
|
+
process.once('SIGINT', cancel);
|
|
257
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
258
|
+
rl.on('SIGINT', cancel);
|
|
259
|
+
try {
|
|
260
|
+
result = await firstSurvey(client, await realpath(values.directory ?? process.cwd()), uuid(required('workspace')), uuid(values.site[0]), firstSurveyTerminalUI(rl, process.stderr, abort.signal), action);
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
rl.close();
|
|
264
|
+
process.off('SIGINT', cancel);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (command === "login" || command === "access") {
|
|
209
268
|
const open = (url) => {
|
|
210
269
|
if (!values.open)
|
|
211
270
|
return;
|
|
@@ -328,6 +387,25 @@ async function main() {
|
|
|
328
387
|
result = await client.deletion('request', { ...target, request_id, expected_revision: required('revision'), confirmation_token: required('confirmation-token'), confirm_name: values.confirm });
|
|
329
388
|
}
|
|
330
389
|
}
|
|
390
|
+
else if (command === 'workspace' && action === 'ai') {
|
|
391
|
+
const context = { workspaceId: uuid(required('workspace')), ...(values.site?.length === 1 ? { siteId: uuid(values.site[0]) } : {}) };
|
|
392
|
+
if (values.site && values.site.length !== 1)
|
|
393
|
+
throw new Error('AI settings accept at most one --site');
|
|
394
|
+
const sub = parsed.positionals[2];
|
|
395
|
+
if (sub === 'get')
|
|
396
|
+
result = await client.read('workspace.ai', context);
|
|
397
|
+
else if (sub === 'set')
|
|
398
|
+
result = await client.updateWorkspaceAi({ ...context, settings: await readJsonFile(required('file')) });
|
|
399
|
+
else
|
|
400
|
+
throw new Error('Use workspace ai get or workspace ai set --file SETTINGS.json');
|
|
401
|
+
}
|
|
402
|
+
else if (command === 'conversation') {
|
|
403
|
+
if (action !== 'get')
|
|
404
|
+
throw new Error('Use conversation get UUID');
|
|
405
|
+
if (values.site?.length !== 1)
|
|
406
|
+
throw new Error('This command requires one --site');
|
|
407
|
+
result = await client.read('conversation.transcript', { workspaceId: uuid(required('workspace')), siteId: uuid(values.site[0]), conversation_id: uuid(positionalId ?? required('id')) });
|
|
408
|
+
}
|
|
331
409
|
else if (command === "workspace" && action === "list")
|
|
332
410
|
result = (await client.account()).workspaces;
|
|
333
411
|
else if (command === "workspace" && action === "create")
|
|
@@ -364,6 +442,8 @@ async function main() {
|
|
|
364
442
|
result = await client.read('insights.overview', { ...context, ...range, ...filters });
|
|
365
443
|
else if (command === "results" && action === "prompt")
|
|
366
444
|
result = await client.read('insights.prompt', { ...context, ...range, ...filters, prompt_id: uuid(positionalId ?? required("prompt-id")) });
|
|
445
|
+
else if (command === 'results' && action === 'conversations')
|
|
446
|
+
result = await client.read('insights.conversations', { ...context, ...range, ...filters, prompt_id: uuid(positionalId ?? required('prompt-id')) });
|
|
367
447
|
else if (command === "results" && action === "experiments")
|
|
368
448
|
result = await client.read('insights.experiments', { ...context, prompt_id: uuid(positionalId ?? required('prompt-id')), run_id: values.run, all_attempts: values['all-attempts'] ?? false, reported_only: values['reported-only'] ?? false });
|
|
369
449
|
else if (command === "results" && action === "breakdown")
|
|
@@ -498,6 +578,8 @@ async function main() {
|
|
|
498
578
|
// A person at a terminal has just read the progress lines; the JSON result needs --json or a pipe.
|
|
499
579
|
if ((command === 'login' || command === 'access') && humanOut && !values["no-wait"])
|
|
500
580
|
return;
|
|
581
|
+
if (command === 'first-survey' && !values.json)
|
|
582
|
+
return;
|
|
501
583
|
const view = humanOut ? humanView(kit, command, action, result) : undefined;
|
|
502
584
|
process.stdout.write((view ? view.join('\n') : humanOut ? kit.json(result) : JSON.stringify(result, null, values.json ? undefined : 2)) + "\n");
|
|
503
585
|
function humanView(ui, name, verb, data) {
|
|
@@ -519,7 +601,7 @@ async function main() {
|
|
|
519
601
|
}
|
|
520
602
|
main().catch(error => {
|
|
521
603
|
const json = process.argv.includes("--json");
|
|
522
|
-
const detail = { error: error instanceof ApiError || error instanceof DocsError || error instanceof CredentialStoreError || error instanceof NetworkError || error instanceof AuthenticationError || error instanceof ConfigurationError || error instanceof CaptureError ? error.code : "command_failed", message: error instanceof Error ? error.message : "Command failed", ...(error instanceof ApiError && error.approvalUrl ? { approval_url: error.approvalUrl } : {}), ...(retry ? { retry } : {}) };
|
|
604
|
+
const detail = { error: error instanceof ApiError || error instanceof DocsError || error instanceof CredentialStoreError || error instanceof NetworkError || error instanceof AuthenticationError || error instanceof ConfigurationError || error instanceof CaptureError || error instanceof SetupError ? error.code : "command_failed", message: error instanceof Error ? error.message : "Command failed", ...(error instanceof ApiError && error.approvalUrl ? { approval_url: error.approvalUrl } : {}), ...(retry ? { retry } : {}) };
|
|
523
605
|
const exitCode = error instanceof CaptureError && error.code === 'capture_cancelled' ? 130 : 1;
|
|
524
606
|
if (error instanceof ApiError && error.code === 'authorization_required')
|
|
525
607
|
detail.message += '. The current login lacks access to this target. Run userreason access --open to review workspace access and permissions.';
|
|
@@ -530,4 +612,7 @@ main().catch(error => {
|
|
|
530
612
|
}
|
|
531
613
|
process.stderr.write((json ? JSON.stringify(detail) : detail.message + (retry ? `\nRetry with the same input, --id ${retry.prompt_id} --request-id ${retry.request_id}; read the survey first if changing the request.` : "")) + "\n");
|
|
532
614
|
process.exitCode = exitCode;
|
|
615
|
+
}).finally(async () => {
|
|
616
|
+
analytics?.command(analyticsCommand, !process.exitCode, performance.now() - analyticsStarted);
|
|
617
|
+
await analytics?.flush();
|
|
533
618
|
});
|
package/dist/catalog.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { SCOPES } from './contracts.js';
|
|
2
2
|
// Shared by terminal help, offline command discovery, and the bundled CLI reference.
|
|
3
|
-
const usages = `userreason
|
|
3
|
+
const usages = `userreason telemetry enable|disable|status
|
|
4
|
+
userreason setup sample --setup UUID [--directory PATH] [--request-id UUID]
|
|
5
|
+
userreason first-survey start|handoff|cancel|delete --workspace UUID --site UUID [--directory PATH]
|
|
6
|
+
userreason login [--open]
|
|
4
7
|
userreason access [--open] [--no-wait]
|
|
5
8
|
userreason logout
|
|
6
9
|
userreason whoami
|
|
@@ -8,6 +11,9 @@ userreason deletion status --id UUID --workspace UUID
|
|
|
8
11
|
userreason workspace delete UUID [--preview] [--confirm NAME --revision N --confirmation-token TOKEN]
|
|
9
12
|
userreason site delete UUID --workspace UUID [--preview] [--confirm NAME --revision N --confirmation-token TOKEN]
|
|
10
13
|
userreason prompt delete UUID --workspace UUID --site UUID [--preview] [--confirm NAME --revision N --confirmation-token TOKEN]
|
|
14
|
+
userreason workspace ai get --workspace UUID [--site UUID]
|
|
15
|
+
userreason workspace ai set --file SETTINGS.json --workspace UUID [--site UUID]
|
|
16
|
+
userreason conversation get UUID --workspace UUID --site UUID
|
|
11
17
|
userreason workspace list
|
|
12
18
|
userreason workspace create --name NAME
|
|
13
19
|
userreason site list --workspace UUID
|
|
@@ -29,6 +35,7 @@ userreason audience update UUID --file AUDIENCE.json --revision N --workspace UU
|
|
|
29
35
|
userreason audience publish|discard|archive UUID --revision N --workspace UUID --site UUID
|
|
30
36
|
userreason results overview --workspace UUID --site UUID [--days 1|7|30|90] [--time-zone ZONE]
|
|
31
37
|
userreason results prompt UUID --workspace UUID --site UUID [--days N] [--country NL] [--segment browser=Chrome]
|
|
38
|
+
userreason results conversations UUID --workspace UUID --site UUID [--days N]
|
|
32
39
|
userreason results experiments UUID --workspace UUID --site UUID [--run UUID] [--all-attempts] [--reported-only]
|
|
33
40
|
userreason results breakdown --by country|language|device_type|os|browser --workspace UUID --site UUID [--prompt-id UUID]
|
|
34
41
|
userreason responses list --workspace UUID --site UUID [--prompt-id UUID] [--run UUID] [--variant ID] [--user UUID] [--search TEXT] [--days N] [--cursor CURSOR]
|
|
@@ -54,16 +61,23 @@ userreason cli search "QUESTION" [--limit 5]
|
|
|
54
61
|
userreason cli get "COMMAND"
|
|
55
62
|
userreason cli list`;
|
|
56
63
|
const descriptions = {
|
|
64
|
+
setup: 'Read the clearly simulated answers for one active browser setup using its separate local setup capability; no ordinary login or real response data.',
|
|
65
|
+
telemetry: 'Enable, disable or inspect optional anonymous usage measurement. UR_TELEMETRY_DISABLED=1 and --no-telemetry disable it too.',
|
|
57
66
|
preview: 'Capture a webpage locally and upload a responsive HTML background to the development preview lab.',
|
|
58
67
|
deletion: 'Read the status of a permanent deletion job.',
|
|
59
68
|
'workspace delete': 'Preview or explicitly confirm permanent removal of a workspace and all its data.',
|
|
60
69
|
'site delete': 'Preview or explicitly confirm permanent removal of a site and all its data.',
|
|
61
70
|
'prompt delete': 'Preview or explicitly confirm permanent removal of a survey, versions and responses.',
|
|
71
|
+
'first-survey': 'Review consented source-backed proposals and explicitly save real unpublished drafts; handoff requires no upload.',
|
|
62
72
|
login: 'Sign in and approve workspace access and permissions in the browser.',
|
|
63
73
|
access: 'Renew or change approved workspace access and permissions.',
|
|
64
74
|
logout: 'Remove the session from the selected local credential store.',
|
|
65
75
|
whoami: 'Inspect the signed-in account, API, workspace grants and project site.',
|
|
66
76
|
workspace: 'List or create workspaces.',
|
|
77
|
+
'workspace ai get': 'Read the shared conversation allowance and optional site product context.',
|
|
78
|
+
'workspace ai set': 'Update authorized conversation controls or site product context from a complete settings file.',
|
|
79
|
+
conversation: 'Read a conversation transcript and reviewed answer with source provenance.',
|
|
80
|
+
'results conversations': 'Read conversational survey results, completion, timing and answer evidence.',
|
|
67
81
|
site: 'List or create sites, inspect SDK connection activity, or manage survey frequency limits.',
|
|
68
82
|
grant: 'Request deliberately restricted access or inspect approval status.',
|
|
69
83
|
prompt: 'Manage feedback surveys and their questions.',
|
|
@@ -94,10 +108,16 @@ const descriptions = {
|
|
|
94
108
|
export const commandSummary = (command) => descriptions[command];
|
|
95
109
|
function scopeFor(command) {
|
|
96
110
|
const [group, action] = command.split(' ');
|
|
111
|
+
if (group === 'first-survey')
|
|
112
|
+
return action === 'handoff' ? [] : ['site:read', 'survey:write'];
|
|
97
113
|
if (group === 'preview')
|
|
98
114
|
return ['site:read', 'survey:write'];
|
|
99
115
|
if (action === 'delete')
|
|
100
116
|
return ['resource:delete'];
|
|
117
|
+
if (command === 'workspace ai set')
|
|
118
|
+
return ['workspace:read'];
|
|
119
|
+
if (group === 'conversation')
|
|
120
|
+
return ['site:read'];
|
|
101
121
|
if (group === 'workspace')
|
|
102
122
|
return [action === 'create' ? 'workspace:create' : 'workspace:read'];
|
|
103
123
|
if (group === 'member')
|
|
@@ -117,14 +137,20 @@ function scopeFor(command) {
|
|
|
117
137
|
}
|
|
118
138
|
export const commandCatalog = usages.split('\n').flatMap(line => {
|
|
119
139
|
const withoutBinary = line.replace(/^userreason /, '');
|
|
120
|
-
const path = withoutBinary.match(/^[a-z]+(?:[| ][a-z]+)*/)?.[0].trim() ?? '';
|
|
140
|
+
const path = withoutBinary.match(/^[a-z]+(?:-[a-z]+)*(?:[| ][a-z]+(?:-[a-z]+)*)*/)?.[0].trim() ?? '';
|
|
121
141
|
const parts = path.split(' '), last = parts.pop();
|
|
122
142
|
return last.split('|').map(action => {
|
|
123
143
|
const command = [...parts, action].join(' ');
|
|
124
|
-
const targeted = ['preview', 'site', 'prompt', 'audience', 'results', 'responses', 'user', 'event', 'query'].includes(parts[0] ?? action);
|
|
144
|
+
const targeted = ['conversation', 'preview', 'site', 'prompt', 'audience', 'results', 'responses', 'user', 'event', 'query'].includes(parts[0] ?? action);
|
|
125
145
|
const notes = targeted ? ['Workspace and site default to the project SDK initialization. Explicit flags override detection. Missing access never selects another site.'] : [];
|
|
146
|
+
if (command === 'workspace ai set')
|
|
147
|
+
notes.push('Owner/admin is required for workspace allowance controls. Site product context accepts editors. Updating one setting does not reset omitted settings.');
|
|
148
|
+
if (command.startsWith('conversation') || command === 'results conversations')
|
|
149
|
+
notes.push('Development feature. Accepted answers are separate from generated analysis; retain speaker, typed/spoken and recorded/live provenance.');
|
|
126
150
|
if (command === 'preview capture')
|
|
127
151
|
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.');
|
|
152
|
+
if (command === 'setup sample')
|
|
153
|
+
notes.push('Requires an explicitly selected setup ID and its unexpired local capability for this API/project. Records only a setup-scoped sample-read receipt; never creates real responses, consumes response quota or grants ongoing access. Browser setup must be enabled by the server.');
|
|
128
154
|
if (['create', 'update'].includes(action) && ['prompt', 'audience'].includes(parts[0]))
|
|
129
155
|
notes.push('The JSON file is a complete definition, not a patch. Saving a draft does not publish it.');
|
|
130
156
|
if (parts[0] === 'prompt' && ['create', 'update', 'publish', 'pause', 'resume', 'archive'].includes(action))
|
package/dist/client.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { type Fetch } from "./workos.js";
|
|
|
4
4
|
export type PromptAction = 'create' | 'update' | 'publish' | 'pause' | 'resume' | 'archive';
|
|
5
5
|
export type AudienceAction = 'create' | 'update' | 'publish' | 'discard' | 'archive';
|
|
6
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"];
|
|
7
|
+
export declare const READ_OPERATIONS: readonly ["workspace.ai", "conversation.transcript", "insights.conversations", "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
8
|
export type ReadOperation = typeof READ_OPERATIONS[number];
|
|
9
9
|
export declare class ApiError extends Error {
|
|
10
10
|
status: number;
|
|
@@ -22,7 +22,7 @@ export declare class UserReasonClient {
|
|
|
22
22
|
lock?: <T>(operation: () => Promise<T>) => Promise<T>;
|
|
23
23
|
});
|
|
24
24
|
private token;
|
|
25
|
-
request(path: string, body?: unknown): Promise<unknown>;
|
|
25
|
+
request(path: string, body?: unknown, signal?: AbortSignal): Promise<unknown>;
|
|
26
26
|
private authenticatedRequest;
|
|
27
27
|
previewCaptures(workspaceId: string, siteId: string, forUpload?: boolean): Promise<unknown>;
|
|
28
28
|
uploadPreview(workspaceId: string, siteId: string, name: string, html: Uint8Array): Promise<unknown>;
|
|
@@ -69,6 +69,12 @@ export declare class UserReasonClient {
|
|
|
69
69
|
workspaceId: string;
|
|
70
70
|
siteId?: string;
|
|
71
71
|
} & Record<string, unknown>): Promise<unknown>;
|
|
72
|
+
/** Workspace allowance settings and optional site product context; server enforces the caller's role. */
|
|
73
|
+
updateWorkspaceAi(input: {
|
|
74
|
+
workspaceId: string;
|
|
75
|
+
siteId?: string;
|
|
76
|
+
settings: Record<string, unknown>;
|
|
77
|
+
}): Promise<unknown>;
|
|
72
78
|
/** One site as stored, with its revision and frequency policy (null while it follows the starting values). */
|
|
73
79
|
siteRecord(input: {
|
|
74
80
|
workspaceId: string;
|
package/dist/client.js
CHANGED
|
@@ -4,7 +4,7 @@ import { refresh, AuthenticationError } from "./workos.js";
|
|
|
4
4
|
import { withSessionLock } from "./lock.js";
|
|
5
5
|
import { fetchWithErrors } from './errors.js';
|
|
6
6
|
/** What the dashboard reads beside records: results, the response feed, users, events and members. */
|
|
7
|
-
export const READ_OPERATIONS = ['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'];
|
|
7
|
+
export const READ_OPERATIONS = ['workspace.ai', 'conversation.transcript', 'insights.conversations', '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
8
|
export class ApiError extends Error {
|
|
9
9
|
status;
|
|
10
10
|
code;
|
|
@@ -27,8 +27,9 @@ export class UserReasonClient {
|
|
|
27
27
|
this.options = options;
|
|
28
28
|
this.fetcher = options.fetcher ?? fetch;
|
|
29
29
|
}
|
|
30
|
-
async token(forceRefresh = false, previous) {
|
|
30
|
+
async token(forceRefresh = false, previous, signal) {
|
|
31
31
|
const read = async () => {
|
|
32
|
+
signal?.throwIfAborted();
|
|
32
33
|
const current = await this.credentials.load();
|
|
33
34
|
if (!current)
|
|
34
35
|
throw new AuthenticationError('login_required', 'Run userreason login --open first. Existing Keychain logins can be used with --credential-store keychain.');
|
|
@@ -49,20 +50,25 @@ export class UserReasonClient {
|
|
|
49
50
|
return current;
|
|
50
51
|
if (current.accessToken.startsWith('ur-install.'))
|
|
51
52
|
throw new AuthenticationError('installation_expired', 'Installation access expired or was revoked; rerun the wizard to reconnect');
|
|
52
|
-
return refresh(this.config, this.credentials, current, this.fetcher);
|
|
53
|
+
return refresh(this.config, this.credentials, current, this.fetcher, signal);
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
|
-
async request(path, body) {
|
|
56
|
-
return this.authenticatedRequest(path, body === undefined ? undefined : JSON.stringify(body), 'application/json');
|
|
56
|
+
async request(path, body, signal) {
|
|
57
|
+
return this.authenticatedRequest(path, body === undefined ? undefined : JSON.stringify(body), 'application/json', signal);
|
|
57
58
|
}
|
|
58
|
-
async authenticatedRequest(path, body, contentType) {
|
|
59
|
+
async authenticatedRequest(path, body, contentType, signal) {
|
|
59
60
|
if (!path.startsWith("/onboarding/v1/") || path.includes("//") || path.includes("\\"))
|
|
60
61
|
throw new Error("Invalid UserReason API path");
|
|
61
|
-
|
|
62
|
+
// Planning may use the API's full 40-second deadline. One transport budget
|
|
63
|
+
// also bounds authentication refresh; cancellation must survive a retry.
|
|
64
|
+
const requestSignal = AbortSignal.any([AbortSignal.timeout(path === '/onboarding/v1/planning/analyze' ? 45_000 : 20_000), ...(signal ? [signal] : [])]);
|
|
65
|
+
requestSignal.throwIfAborted();
|
|
66
|
+
let session = await this.token(false, undefined, requestSignal);
|
|
62
67
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
63
|
-
|
|
68
|
+
requestSignal.throwIfAborted();
|
|
69
|
+
const response = await fetchWithErrors(this.fetcher, this.config.apiUrl + path, { method: body === undefined ? "GET" : "POST", redirect: "error", headers: { Authorization: `Bearer ${session.accessToken}`, "x-ur-client": "cli", "Content-Type": contentType }, ...(body === undefined ? {} : { body: body }), signal: requestSignal }, 'UserReason API request');
|
|
64
70
|
if (response.status === 401 && attempt === 0) {
|
|
65
|
-
session = await this.token(true, session.accessToken);
|
|
71
|
+
session = await this.token(true, session.accessToken, requestSignal);
|
|
66
72
|
continue;
|
|
67
73
|
}
|
|
68
74
|
let result;
|
|
@@ -167,18 +173,27 @@ export class UserReasonClient {
|
|
|
167
173
|
if (!READ_OPERATIONS.includes(operation))
|
|
168
174
|
throw new Error("Unsupported read operation");
|
|
169
175
|
const { workspaceId, siteId, ...rest } = input;
|
|
170
|
-
if (
|
|
176
|
+
if (!['members.list', 'workspace.ai'].includes(operation) && !siteId)
|
|
171
177
|
throw new Error("This read requires a site");
|
|
172
178
|
if (rest.run_id !== undefined)
|
|
173
179
|
uuid(string(rest.run_id, 'Experiment run ID'));
|
|
174
180
|
if (rest.variant_id && !rest.run_id)
|
|
175
181
|
throw new Error('A variant filter requires an experiment run');
|
|
176
|
-
const body = { workspace_id: uuid(workspaceId), ...(operation === 'members.list' ? {} : { site_id: uuid(siteId) }) };
|
|
182
|
+
const body = { workspace_id: uuid(workspaceId), ...(operation === 'members.list' || !siteId ? {} : { site_id: uuid(siteId) }) };
|
|
177
183
|
for (const [key, value] of Object.entries(rest))
|
|
178
184
|
if (value !== undefined && value !== null && value !== '')
|
|
179
185
|
body[key] = value;
|
|
180
186
|
return this.request(`/onboarding/v1/read/${operation}`, body);
|
|
181
187
|
}
|
|
188
|
+
/** Workspace allowance settings and optional site product context; server enforces the caller's role. */
|
|
189
|
+
async updateWorkspaceAi(input) {
|
|
190
|
+
const allowed = new Set(['enabled', 'voice_enabled', 'monthly_limit_microusd', 'concurrent_limit', 'preview_daily_limit', 'product_context']);
|
|
191
|
+
if (!Object.keys(input.settings).length || Object.keys(input.settings).some(key => !allowed.has(key)))
|
|
192
|
+
throw new Error('AI settings require supported setting fields only');
|
|
193
|
+
if ('product_context' in input.settings && !input.siteId)
|
|
194
|
+
throw new Error('Product context requires a site');
|
|
195
|
+
return this.request('/onboarding/v1/workspace/ai/update', { ...input.settings, workspace_id: uuid(input.workspaceId), ...(input.siteId ? { site_id: uuid(input.siteId) } : {}) });
|
|
196
|
+
}
|
|
182
197
|
/** One site as stored, with its revision and frequency policy (null while it follows the starting values). */
|
|
183
198
|
async siteRecord(input) {
|
|
184
199
|
const page = object(await this.query({ ...input, resource: 'sites', id: input.siteId, limit: 1 }), 'sites');
|