@userreason/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +301 -41
  2. package/dist/authorize.d.ts +29 -0
  3. package/dist/authorize.js +23 -0
  4. package/dist/bin.js +565 -50
  5. package/dist/capture.d.ts +18 -0
  6. package/dist/capture.js +256 -0
  7. package/dist/catalog.d.ts +16 -0
  8. package/dist/catalog.js +206 -0
  9. package/dist/client.d.ts +79 -2
  10. package/dist/client.js +140 -17
  11. package/dist/config.d.ts +4 -0
  12. package/dist/config.js +35 -12
  13. package/dist/contracts.d.ts +13 -3
  14. package/dist/contracts.js +4 -4
  15. package/dist/credentials.d.ts +12 -2
  16. package/dist/credentials.js +110 -26
  17. package/dist/docs.d.ts +51 -0
  18. package/dist/docs.js +143 -0
  19. package/dist/errors.d.ts +11 -0
  20. package/dist/errors.js +38 -0
  21. package/dist/event-policy.d.ts +2 -0
  22. package/dist/event-policy.js +9 -0
  23. package/dist/first-survey-preparation.d.ts +8 -0
  24. package/dist/first-survey-preparation.js +29 -0
  25. package/dist/first-survey-source.d.ts +60 -0
  26. package/dist/first-survey-source.js +289 -0
  27. package/dist/first-survey-terminal.d.ts +5 -0
  28. package/dist/first-survey-terminal.js +16 -0
  29. package/dist/first-survey.d.ts +88 -0
  30. package/dist/first-survey.js +227 -0
  31. package/dist/help.d.ts +13 -0
  32. package/dist/help.js +74 -0
  33. package/dist/index.d.ts +23 -0
  34. package/dist/index.js +23 -0
  35. package/dist/lock.js +19 -5
  36. package/dist/profile.d.ts +20 -0
  37. package/dist/profile.js +57 -0
  38. package/dist/project.d.ts +9 -0
  39. package/dist/project.js +87 -0
  40. package/dist/recommended-surveys.d.ts +30 -0
  41. package/dist/recommended-surveys.js +201 -0
  42. package/dist/setup-executor.d.ts +35 -0
  43. package/dist/setup-executor.js +329 -0
  44. package/dist/setup-files.d.ts +11 -0
  45. package/dist/setup-files.js +96 -0
  46. package/dist/setup-runner.d.ts +14 -0
  47. package/dist/setup-runner.js +142 -0
  48. package/dist/setup-source.d.ts +31 -0
  49. package/dist/setup-source.js +48 -0
  50. package/dist/setup-store.d.ts +37 -0
  51. package/dist/setup-store.js +256 -0
  52. package/dist/setup-terminal.d.ts +3 -0
  53. package/dist/setup-terminal.js +6 -0
  54. package/dist/setup-wire.d.ts +1237 -0
  55. package/dist/setup-wire.js +175 -0
  56. package/dist/setup.d.ts +1223 -0
  57. package/dist/setup.js +258 -0
  58. package/dist/starter-survey.d.ts +22 -0
  59. package/dist/starter-survey.js +457 -0
  60. package/dist/telemetry.d.ts +67 -0
  61. package/dist/telemetry.js +304 -0
  62. package/dist/ui.d.ts +75 -0
  63. package/dist/ui.js +107 -0
  64. package/dist/views.d.ts +51 -0
  65. package/dist/views.js +93 -0
  66. package/dist/workos.d.ts +1 -1
  67. package/dist/workos.js +9 -4
  68. package/package.json +29 -12
  69. package/skills/userreason/SKILL.md +88 -8
  70. package/skills/userreason/references/management.md +67 -0
@@ -0,0 +1,88 @@
1
+ import { UserReasonClient } from './client.js';
2
+ import { type LocalEvidence } from './first-survey-source.js';
3
+ export interface FirstSurveyUI {
4
+ show(message: string): void;
5
+ confirm(message: string): Promise<boolean>;
6
+ choose(message: string, options: {
7
+ value: string;
8
+ label: string;
9
+ }[], initialValue?: string): Promise<string>;
10
+ answer(message: string): Promise<string>;
11
+ signal?: AbortSignal;
12
+ chooseMany?(message: string, options: {
13
+ value: string;
14
+ label: string;
15
+ hint?: string;
16
+ }[], initialValues: string[]): Promise<string[]>;
17
+ loading?<T>(message: string, work: () => Promise<T>): Promise<T>;
18
+ }
19
+ type Candidate = {
20
+ decision?: string;
21
+ objective?: string;
22
+ audience?: {
23
+ claim: string;
24
+ };
25
+ exclusions?: {
26
+ claim: string;
27
+ };
28
+ opportunity?: {
29
+ claim: string;
30
+ };
31
+ availability?: string;
32
+ frequency?: string;
33
+ readiness?: string;
34
+ warnings?: string[];
35
+ id: string;
36
+ question: string;
37
+ definition: Record<string, unknown>;
38
+ instrumentation: unknown[];
39
+ missing: string[];
40
+ };
41
+ type Plan = {
42
+ outcome: string;
43
+ candidates: Candidate[];
44
+ clarifications: {
45
+ id: string;
46
+ question: string;
47
+ reason: string;
48
+ }[];
49
+ };
50
+ /** Show all respondent copy and branching before the author approves a draft. */
51
+ export declare function renderFirstSurveyQuestions(definition: Record<string, unknown>): string[];
52
+ export declare function renderFirstSurveyCandidate(c: Candidate): string;
53
+ type Session = {
54
+ version: 1;
55
+ root: string;
56
+ api: string;
57
+ workspace: string;
58
+ site: string;
59
+ id: string;
60
+ expires: number;
61
+ state: string;
62
+ sources: Pick<LocalEvidence, 'path' | 'file_hash'>[];
63
+ plan?: Plan;
64
+ digest?: string;
65
+ saves: Record<string, {
66
+ prompt: string;
67
+ request: string;
68
+ definition: Record<string, unknown>;
69
+ result?: unknown;
70
+ }>;
71
+ };
72
+ export declare function firstSurveyHandoff(root: string, api: string, workspace: string, site: string, id?: string): string;
73
+ /** Separate from installation state. Owner-only, seven days, no source excerpts or credentials. */
74
+ export declare function firstSurvey(client: UserReasonClient, root: string, workspace: string, site: string, ui: FirstSurveyUI, mode?: string): Promise<Session | {
75
+ state: string;
76
+ session_id?: undefined;
77
+ saves?: undefined;
78
+ } | {
79
+ session_id: string;
80
+ state: string;
81
+ saves: Record<string, {
82
+ prompt: string;
83
+ request: string;
84
+ definition: Record<string, unknown>;
85
+ result?: unknown;
86
+ }>;
87
+ }>;
88
+ export {};
@@ -0,0 +1,227 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, writeFile, rename, open, unlink, lstat } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { configDirectory } from './config.js';
5
+ import { object } from './contracts.js';
6
+ import { collectFirstSurveySource, collectFirstSurveyHistory, verifyFirstSurveySource, sourceHash } from './first-survey-source.js';
7
+ const record = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
8
+ function describeCondition(value) {
9
+ const condition = record(value);
10
+ if (Array.isArray(condition.conditions))
11
+ return condition.conditions.map(describeCondition).join(condition.type === 'any' ? ' OR ' : ' AND ');
12
+ const comparison = record(condition.comparison);
13
+ const operand = Array.isArray(comparison.value) ? comparison.value.join(', ') : comparison.value ?? (comparison.min !== undefined ? `${comparison.min}–${comparison.max}` : '');
14
+ return `${String(condition.question_id ?? 'earlier answer')} ${String(comparison.operator ?? 'matches')} ${String(operand)}`.trim();
15
+ }
16
+ /** Show all respondent copy and branching before the author approves a draft. */
17
+ export function renderFirstSurveyQuestions(definition) {
18
+ const content = record(definition.content), questions = content.questions;
19
+ if (!Array.isArray(questions))
20
+ return ['Full question definition unavailable; validation is required before saving.'];
21
+ return questions.flatMap((value, index) => {
22
+ const q = record(value), logic = record(q.logic);
23
+ const lines = [`${index + 1}. ${String(q.prompt ?? 'Missing question')} [${String(q.kind ?? 'unknown type')}; ${q.required === true ? 'required' : 'optional'}]`];
24
+ if (q.kind === 'rating') {
25
+ const labels = Array.isArray(q.labels) ? q.labels : [];
26
+ lines.push(labels.length ? ` Scale: ${labels.map((label, i) => `${Number(q.min ?? 0) + i}: ${String(label)}`).join(' · ')}` : ` Scale: ${String(q.min)} (${String(q.min_label ?? '')}) to ${String(q.max)} (${String(q.max_label ?? '')})`);
27
+ }
28
+ if (Array.isArray(q.options))
29
+ for (const value of q.options) {
30
+ const option = record(value);
31
+ lines.push(` - ${String(option.label ?? 'Missing label')}${option.other ? ' (asks for their own words)' : ''}`);
32
+ }
33
+ if (q.kind === 'multiple_choice')
34
+ lines.push(` Select ${String(q.min_selections)}–${String(q.max_selections)} choices.`);
35
+ if (q.answer_order)
36
+ lines.push(` Answer order: ${typeof q.answer_order === 'string' ? q.answer_order : JSON.stringify(q.answer_order)}`);
37
+ if (logic.show_when)
38
+ lines.push(` Show when: ${describeCondition(logic.show_when)}.`);
39
+ if (Array.isArray(logic.rules))
40
+ for (const value of logic.rules) {
41
+ const rule = record(value), to = record(rule.to);
42
+ lines.push(` When ${describeCondition(rule.when)} → ${String(to.id ?? to.type)}.`);
43
+ }
44
+ if (logic.otherwise) {
45
+ const to = record(logic.otherwise);
46
+ lines.push(` Otherwise → ${String(to.id ?? to.type)}.`);
47
+ }
48
+ return lines;
49
+ });
50
+ }
51
+ export function renderFirstSurveyCandidate(c) {
52
+ return [`Decision: ${c.decision ?? 'Review with the author'}`, `Learning objective: ${c.objective ?? 'Not supplied'}`, `Question: ${c.question}`, ...renderFirstSurveyQuestions(c.definition), `Audience: ${c.audience?.claim ?? 'Needs review'}`, `Exclude: ${c.exclusions?.claim ?? 'Needs review'}`, `Timing: ${c.opportunity?.claim ?? 'Needs review'}`, `Availability: ${c.availability ?? 'Unverified'}`, `Frequency: ${c.frequency ?? 'Inherited from site'}`, `Missing signals: ${c.missing.join('; ') || 'None reported (not proof of readiness)'}`, `Readiness: ${c.readiness ?? 'Review required'}; browser behavior unverified`, ...(c.warnings ?? []).map(w => 'Warning: ' + w)].join('\n');
53
+ }
54
+ export function firstSurveyHandoff(root, api, workspace, site, id) {
55
+ return `Use the installed UserReason skill to propose up to three first surveys. Application root: ${JSON.stringify(root)}. API: ${api}; workspace: ${workspace}; site: ${site}${id ? `; session: ${id}` : ''}. Confirm installed SDK and scope. The coding agent may send source to its own provider: obtain separate source-processing consent. Reuse application facts and semantic success events; clicks are not completion and missing history is unknown. Cite relative paths, full-file hashes and line ranges. Include decision, objective, one neutral question, observable audience/exclusions, safe opportunity, availability, inherited site frequency and missing signals. Do not infer deployment or visitor history from source. Preview instrumentation with before-file hashes; obtain explicit edit approval and verify behavior, cleanup, deduplication and overlapping availability owners. Unsupported edits require a manual diff. Obtain separate approval before prompt validate and prompt create; use stable prompt/request IDs and report authoritative IDs, revisions and links.edit. A local file is not a draft. Never publish without separate approval of the exact saved revision. Configuration reachability is not browser verification. Continue interactively with userreason first-survey start --workspace ${workspace} --site ${site}.`;
56
+ }
57
+ /** Separate from installation state. Owner-only, seven days, no source excerpts or credentials. */
58
+ export async function firstSurvey(client, root, workspace, site, ui, mode = 'start') {
59
+ ui.signal?.throwIfAborted();
60
+ const check = () => ui.signal?.throwIfAborted();
61
+ const directory = join(configDirectory(), 'first-survey');
62
+ await mkdir(directory, { recursive: true, mode: 0o700 });
63
+ const key = sourceHash(JSON.stringify([root, client.config.apiUrl, workspace, site]));
64
+ const path = join(directory, key + '.json');
65
+ const lock = await open(path + '.lock', 'wx', 0o600).catch(() => { throw new Error('Another first-survey process owns this session. Retry after it exits; remove a stale .lock only after verifying the process stopped.'); });
66
+ let state = { version: 1, root, api: client.config.apiUrl, workspace, site, id: randomUUID(), expires: Date.now() + 7 * 86400000, state: 'source_review', sources: [], saves: {} };
67
+ const save = async () => { const temporary = path + '.' + randomUUID() + '.tmp'; await writeFile(temporary, JSON.stringify(state), { mode: 0o600, flag: 'wx' }); await rename(temporary, path); };
68
+ const lookup = () => ({ schema_version: 1, scope: { api: state.api, workspace_id: workspace, site_id: site }, session_id: state.id });
69
+ try {
70
+ try {
71
+ if ((await lstat(path)).isSymbolicLink())
72
+ throw new Error('Unsafe session');
73
+ const old = JSON.parse(await readFile(path, 'utf8'));
74
+ if (old.version !== 1 || old.root !== root || old.api !== state.api || old.workspace !== workspace || old.site !== site)
75
+ throw new Error('Session scope changed');
76
+ if (old.expires > Date.now())
77
+ state = old;
78
+ else
79
+ await unlink(path);
80
+ }
81
+ catch (e) {
82
+ if (e.code !== 'ENOENT')
83
+ throw e;
84
+ }
85
+ if (mode === 'delete') {
86
+ await client.request('/onboarding/v1/planning/delete', lookup());
87
+ await unlink(path).catch(() => { });
88
+ return { state: 'deleted' };
89
+ }
90
+ if (mode === 'cancel') {
91
+ state.state = 'cancelled';
92
+ await save();
93
+ await client.request('/onboarding/v1/planning/cancel', lookup()).catch(() => ui.show('Local cancellation saved. Remote cancellation could not be confirmed; retry cancel when connected.'));
94
+ return state;
95
+ }
96
+ if (mode === 'handoff') {
97
+ ui.show(firstSurveyHandoff(root, state.api, workspace, site, state.id));
98
+ return { state: 'handoff' };
99
+ }
100
+ if (['cancelled', 'failed'].includes(state.state)) {
101
+ state = { ...state, id: randomUUID(), state: 'source_review', plan: undefined, digest: undefined, saves: {} };
102
+ }
103
+ const metadata = object(await client.request('/onboarding/v1/planning/describe', lookup()), 'planning destination');
104
+ const limits = { input_tokens: 12000, output_tokens: 4000, deadline_ms: 35000 };
105
+ if (!state.plan) {
106
+ if (state.state === 'analyzing') {
107
+ const existing = object(await client.request('/onboarding/v1/planning/get', lookup()), 'plan');
108
+ if (existing.status === 'proposed') {
109
+ state.plan = existing.result;
110
+ state.digest = sourceHash(JSON.stringify(state.plan));
111
+ state.state = 'proposed';
112
+ await save();
113
+ }
114
+ else
115
+ throw new Error('Analysis is pending or failed. Cancel this session before starting again.');
116
+ }
117
+ if (!state.plan) {
118
+ ui.show('Only the chosen application root is inspected. No sibling packages are included. Secret detection is best effort; review every excerpt.');
119
+ const selection = await ui.answer('Relative files/directories to include, comma separated (blank: bounded application root; no siblings)');
120
+ const preview = await collectFirstSurveySource({ root, signal: ui.signal, selected: selection.trim() ? selection.split(',').map(p => p.trim()) : undefined });
121
+ ui.show(JSON.stringify({ root, packages: preview.packages, installed_sdk: preview.packages.length ? 'metadata found, runtime unverified' : 'unknown: verify using installed skill', manifest: preview.evidence.map(({ excerpt, ...rest }) => rest), omitted: preview.omitted, truncated: preview.truncated, destination: metadata.destination, provider_model: metadata.model, provider_prices: metadata.prices, provider_policy: metadata.policy, configuration_fingerprint: metadata.configuration_fingerprint, retention: 'Receipt and private plan: seven days, deleted on next planning activity; submitted source packets are not stored by UserReason; generated plans can quote source. Provider policy must be reviewed separately.', budget: `${limits.input_tokens} input / ${limits.output_tokens} completion tokens (including reasoning); ${limits.deadline_ms / 1000} seconds`, capabilities: preview.capabilities, capability_status: preview.capability_status }));
122
+ ui.show(JSON.stringify(preview.evidence));
123
+ if (!await ui.confirm('Approve uploading exactly this source packet to the displayed provider?')) {
124
+ ui.show('Manual path: use prompt validate with your own evidence-labelled definition, then explicitly approve prompt create. No source processing is required.');
125
+ ui.show(firstSurveyHandoff(root, state.api, workspace, site, state.id));
126
+ return { state: 'declined' };
127
+ }
128
+ const git = await ui.confirm('Separately include up to 20 local Git commit subjects within 90 days? (No authors, diffs or remote fetch)');
129
+ const history = await collectFirstSurveyHistory(root, git);
130
+ if (history.warning)
131
+ ui.show(history.warning);
132
+ let evidence = [...preview.evidence, ...history.evidence];
133
+ if (history.evidence.length) {
134
+ ui.show(JSON.stringify(history.evidence));
135
+ if (!await ui.confirm('Approve this additional Git packet?'))
136
+ evidence = preview.evidence;
137
+ }
138
+ await verifyFirstSurveySource(root, preview.evidence);
139
+ state.sources = preview.evidence.map(({ path, file_hash }) => ({ path, file_hash }));
140
+ for (let round = 0; round < 2; round++) {
141
+ check();
142
+ state.state = 'analyzing';
143
+ await save();
144
+ const response = object(await client.request('/onboarding/v1/planning/analyze', { schema_version: 1, session_id: state.id, request_id: randomUUID(), scope: lookup().scope, packages: preview.packages, capabilities: preview.capabilities, evidence, previous_plan_digest: state.digest ?? null, limits, consent: { packet_digest: sourceHash(JSON.stringify(evidence)), destination: metadata.destination, policy_version: 'first-survey-v1', configuration_fingerprint: metadata.configuration_fingerprint, authorized_at: new Date().toISOString(), git_authorized: git } }, ui.signal), 'analysis');
145
+ check();
146
+ state.plan = response.result;
147
+ state.digest = sourceHash(JSON.stringify(state.plan));
148
+ state.state = 'proposed';
149
+ await save();
150
+ if (state.plan.outcome !== 'needs_clarification' || round === 1)
151
+ break;
152
+ ui.show('Clarification needed before proposing a survey.');
153
+ const answers = [];
154
+ for (const q of state.plan.clarifications) {
155
+ const answer = await ui.answer(q.question + ' — ' + q.reason);
156
+ answers.push({ kind: 'clarification', id: 'answer_' + q.id, question_id: q.id, answer });
157
+ }
158
+ if (!await ui.confirm('Approve sending these clarification answers to the same provider?'))
159
+ return { state: 'clarification_declined' };
160
+ evidence = [...evidence, ...answers];
161
+ state.id = randomUUID();
162
+ state.plan = undefined;
163
+ await verifyFirstSurveySource(root, state.sources);
164
+ }
165
+ }
166
+ }
167
+ const plan = state.plan;
168
+ ui.show(`Analysis: ${plan.outcome}. ${plan.candidates.length} candidates for review.`);
169
+ if (plan.outcome !== 'proposed') {
170
+ ui.show(firstSurveyHandoff(root, state.api, workspace, site, state.id));
171
+ return { state: plan.outcome };
172
+ }
173
+ if (sourceHash(JSON.stringify(plan)) !== state.digest)
174
+ throw new Error('Plan changed; new review required');
175
+ await verifyFirstSurveySource(root, state.sources);
176
+ for (;;) {
177
+ const id = await ui.choose('Select one candidate to review/save (or finish)', [...plan.candidates.map(c => ({ value: c.id, label: c.question })), { value: 'finish', label: 'Finish without more drafts' }]);
178
+ if (id === 'finish')
179
+ break;
180
+ check();
181
+ const c = plan.candidates.find(c => c.id === id);
182
+ if (!c)
183
+ throw new Error('Unknown candidate');
184
+ ui.show(renderFirstSurveyCandidate(c));
185
+ const diagnostics = await client.read('prompts.validate', { workspaceId: workspace, siteId: site, definition: c.definition });
186
+ const validation = object(diagnostics, 'validation');
187
+ ui.show(`Definition: ${validation.valid === false ? 'invalid' : 'validated'}; readiness: ${String(validation.status ?? 'unverified')}. Integration: ${c.instrumentation.length ? 'blocked pending manual preview, edit approval and verification' : 'not browser verified'}. Unpublished.`);
188
+ if (Array.isArray(validation.warnings))
189
+ for (const warning of validation.warnings)
190
+ ui.show('Warning: ' + String(warning));
191
+ if (object(diagnostics, 'validation').valid === false) {
192
+ ui.show('Definition invalid; correct it using the coding-agent handoff before saving.');
193
+ continue;
194
+ }
195
+ if (!await ui.confirm(`Create this real unpublished draft${c.instrumentation.length ? ' with blocked integration readiness' : ''}? No edits or publication are authorized.`))
196
+ continue;
197
+ check();
198
+ await verifyFirstSurveySource(root, state.sources);
199
+ const receipt = state.saves[id] ?? { prompt: randomUUID(), request: randomUUID(), definition: c.definition };
200
+ state.saves[id] = receipt;
201
+ await save();
202
+ if (sourceHash(JSON.stringify(receipt.definition)) !== sourceHash(JSON.stringify(c.definition)))
203
+ throw new Error('Reviewed definition changed');
204
+ if (!receipt.result)
205
+ receipt.result = await client.promptCommand({ workspaceId: workspace, siteId: site, action: 'create', promptId: receipt.prompt, requestId: receipt.request, definition: receipt.definition });
206
+ state.state = 'saved';
207
+ await save();
208
+ const saved = object(receipt.result, 'draft');
209
+ const links = saved.links && typeof saved.links === 'object' ? saved.links : {};
210
+ ui.show(`Draft saved: ${String(saved.id ?? receipt.prompt)}; revision ${String(saved.revision ?? 'see editor')}. Edit: ${String(links.edit ?? 'Open the workspace survey editor')}. Unpublished; integration unverified.`);
211
+ }
212
+ return { session_id: state.id, state: state.state, saves: state.saves };
213
+ }
214
+ catch (e) {
215
+ if (ui.signal?.aborted) {
216
+ state.state = 'cancelled';
217
+ await save();
218
+ await client.request('/onboarding/v1/planning/cancel', lookup()).catch(() => { });
219
+ }
220
+ ui.show(firstSurveyHandoff(root, state.api, workspace, site, state.id));
221
+ throw e;
222
+ }
223
+ finally {
224
+ await lock.close();
225
+ await unlink(path + '.lock');
226
+ }
227
+ }
package/dist/help.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { UiKit } from './ui.js';
2
+ export declare const overviewNouns: string[];
3
+ export interface HelpOptions {
4
+ version?: string;
5
+ development?: boolean;
6
+ trademark?: boolean;
7
+ }
8
+ /** Four groups of nouns, one line each. Plain (piped) output is the same text without colour or rules. */
9
+ export declare function overviewHelp(kit: UiKit, { version, development, trademark }?: HelpOptions): string;
10
+ /** One noun's actions: exact usage from the catalog, without the options every command shares. */
11
+ export declare function nounHelp(kit: UiKit, noun: string): string | undefined;
12
+ /** The closest command name within two edits, for "Did you mean". */
13
+ export declare function suggestCommand(word: string): string | undefined;
package/dist/help.js ADDED
@@ -0,0 +1,74 @@
1
+ import { commandCatalog, commandSummary } from './catalog.js';
2
+ /** The overview groups every command noun. A test keeps this list in step with the catalog. */
3
+ const GROUPS = [
4
+ ['Get started', [['login', 'Sign in in the browser'], ['access', 'Review or renew what this CLI may do'], ['whoami', "Account, grants and this project's site"], ['logout', 'Remove local credentials'], ['setup', 'Read an active setup’s simulated sample answers']]],
5
+ ['Feedback', [['first-survey', 'Review evidence-backed first-survey suggestions'], ['prompt', 'Create, validate, publish and read surveys'], ['preview', 'Capture a site for the development preview lab'], ['audience', 'Choose who sees a survey'], ['results', 'Summaries and breakdowns'], ['responses', 'Read answers'], ['conversation', 'Read conversation transcripts'], ['user', 'People and their eligibility'], ['event', 'Product events and collection']]],
6
+ ['Workspaces and sites', [['workspace', 'list · create · ai · delete'], ['site', 'list · create · activity · events · frequency · delete'], ['member', 'list'], ['grant', 'Request and check access'], ['deletion', 'Check a deletion request']]],
7
+ ['Reference', [['docs', 'Search and read the documentation'], ['cli', "Search this CLI's commands"], ['query', 'Raw management API for agents'], ['command', 'Send a management command file'], ['telemetry', 'Enable, disable or inspect usage measurement']]],
8
+ ];
9
+ export const overviewNouns = GROUPS.flatMap(([, items]) => items.map(([name]) => name));
10
+ /** Commands with no action of their own: they never get a per-command help page. */
11
+ const NO_ACTION = new Set(['login', 'access', 'logout', 'whoami', 'command', 'query']);
12
+ const READ_SCOPES = new Set(['workspace:read', 'site:read']);
13
+ /** Four groups of nouns, one line each. Plain (piped) output is the same text without colour or rules. */
14
+ export function overviewHelp(kit, { version, development = false, trademark = false } = {}) {
15
+ const { c, S } = kit;
16
+ const lines = [];
17
+ const environment = development ? `(development API)` : '';
18
+ const meta = [version ? `v${version}` : '', environment].filter(Boolean).join(' ');
19
+ if (kit.level > 0)
20
+ lines.push(`${c.bold(c.accent(trademark ? 'UserReason™ CLI' : 'userreason'))}${meta ? ` ${c.muted(meta)}` : ''}`, c.faint(S.rule.repeat(54)));
21
+ else
22
+ lines.push(`userreason${meta ? ` ${meta}` : ''}`);
23
+ lines.push(`${c.muted('Usage')} userreason <command> [options]`, '');
24
+ const first = Math.max(...overviewNouns.map(name => name.length));
25
+ for (const [group, items] of GROUPS) {
26
+ lines.push(c.bold(group), ...items.map(([name, text]) => ` ${c.code(name)}${' '.repeat(first - name.length + 2)}${c.muted(text)}`), '');
27
+ }
28
+ lines.push(`${c.muted('Options')} --json --workspace UUID --site UUID --dev --no-color --no-telemetry -h`, `${c.muted('Target')} this project's SDK site; override with --workspace and --site`, `${c.muted('More')} ${c.code('userreason <command> --help')} ${c.code('userreason --help --all')} ${c.code('userreason cli search "<task>"')}`);
29
+ return lines.join('\n') + '\n';
30
+ }
31
+ /** One noun's actions: exact usage from the catalog, without the options every command shares. */
32
+ export function nounHelp(kit, noun) {
33
+ if (NO_ACTION.has(noun))
34
+ return undefined;
35
+ const items = commandCatalog.filter(item => item.command === noun || item.command.startsWith(`${noun} `));
36
+ if (!items.length)
37
+ return undefined;
38
+ const { c, S } = kit;
39
+ const short = (usage) => usage.replace(/^userreason /, '').replace(/ --workspace UUID/g, '').replace(/ --site UUID/g, '').replace(`${noun} `, '');
40
+ const lines = [];
41
+ const summary = commandSummary(noun);
42
+ if (kit.level > 0)
43
+ lines.push(`${c.bold(c.accent(`userreason ${noun}`))}${summary ? ` ${c.muted(summary)}` : ''}`, c.faint(S.rule.repeat(54)));
44
+ else
45
+ lines.push(`userreason ${noun}${summary ? ` ${S.dot} ${summary}` : ''}`);
46
+ for (const [title, read] of [['Read', true], ['Write', false]]) {
47
+ const rows = items.filter(item => (item.command !== 'workspace ai set' && item.scopes.every(scope => READ_SCOPES.has(scope))) === read);
48
+ if (!rows.length)
49
+ continue;
50
+ lines.push(c.bold(title));
51
+ for (const item of rows) {
52
+ lines.push(` ${c.code(short(item.usage))}`);
53
+ const specific = commandSummary(item.command);
54
+ if (specific && item.command !== noun)
55
+ lines.push(` ${c.muted(specific)}`);
56
+ }
57
+ }
58
+ lines.push(`${c.muted('Options')} --workspace UUID --site UUID ${c.muted("(default: this project's site)")} --json`);
59
+ return lines.join('\n') + '\n';
60
+ }
61
+ /** The closest command name within two edits, for "Did you mean". */
62
+ export function suggestCommand(word) {
63
+ const distance = (a, b) => {
64
+ const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
65
+ for (let j = 1; j <= b.length; j++)
66
+ d[0][j] = j;
67
+ for (let i = 1; i <= a.length; i++)
68
+ for (let j = 1; j <= b.length; j++)
69
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
70
+ return d[a.length][b.length];
71
+ };
72
+ const [best] = overviewNouns.map(name => [name, distance(word.toLowerCase(), name)]).sort((a, b) => a[1] - b[1]);
73
+ return best && best[1] <= 2 ? best[0] : undefined;
74
+ }
package/dist/index.d.ts CHANGED
@@ -2,4 +2,27 @@ export * from "./client.js";
2
2
  export * from "./config.js";
3
3
  export * from "./contracts.js";
4
4
  export * from "./credentials.js";
5
+ export * from "./docs.js";
6
+ export * from "./profile.js";
5
7
  export * from "./workos.js";
8
+ export * from "./authorize.js";
9
+ export * from "./project.js";
10
+ export * from "./errors.js";
11
+ export * from "./catalog.js";
12
+ export * from "./ui.js";
13
+ export * from "./help.js";
14
+ export * from "./views.js";
15
+ export * from "./telemetry.js";
16
+ export * from "./first-survey-source.js";
17
+ export * from "./first-survey.js";
18
+ export * from "./first-survey-preparation.js";
19
+ export * from "./starter-survey.js";
20
+ export * from "./recommended-surveys.js";
21
+ export * from "./setup.js";
22
+ export * from "./setup-store.js";
23
+ export * from "./setup-wire.js";
24
+ export * from "./setup-executor.js";
25
+ export * from "./setup-files.js";
26
+ export * from "./setup-terminal.js";
27
+ export * from "./setup-source.js";
28
+ export * from "./setup-runner.js";
package/dist/index.js CHANGED
@@ -2,4 +2,27 @@ export * from "./client.js";
2
2
  export * from "./config.js";
3
3
  export * from "./contracts.js";
4
4
  export * from "./credentials.js";
5
+ export * from "./docs.js";
6
+ export * from "./profile.js";
5
7
  export * from "./workos.js";
8
+ export * from "./authorize.js";
9
+ export * from "./project.js";
10
+ export * from "./errors.js";
11
+ export * from "./catalog.js";
12
+ export * from "./ui.js";
13
+ export * from "./help.js";
14
+ export * from "./views.js";
15
+ export * from "./telemetry.js";
16
+ export * from "./first-survey-source.js";
17
+ export * from "./first-survey.js";
18
+ export * from "./first-survey-preparation.js";
19
+ export * from "./starter-survey.js";
20
+ export * from "./recommended-surveys.js";
21
+ export * from "./setup.js";
22
+ export * from "./setup-store.js";
23
+ export * from "./setup-wire.js";
24
+ export * from "./setup-executor.js";
25
+ export * from "./setup-files.js";
26
+ export * from "./setup-terminal.js";
27
+ export * from "./setup-source.js";
28
+ export * from "./setup-runner.js";
package/dist/lock.js CHANGED
@@ -2,22 +2,36 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { mkdir, open, readFile, unlink } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { configDirectory } from "./config.js";
5
+ import { CredentialStoreError } from './credentials.js';
5
6
  // Serialize rotating refresh tokens across CLI/wizard processes. No credentials enter this file.
6
7
  export async function withSessionLock(config, operation) {
7
8
  const directory = join(configDirectory(), "locks");
8
- await mkdir(directory, { recursive: true, mode: 0o700 });
9
+ try {
10
+ await mkdir(directory, { recursive: true, mode: 0o700 });
11
+ }
12
+ catch {
13
+ throw new CredentialStoreError('lock_failed', `Refreshing the UserReason session requires local write access to ${directory}. Allow that access and retry; workspace grants are unrelated.`);
14
+ }
9
15
  const path = join(directory, createHash("sha256").update(`${config.apiUrl}\n${config.clientId}`).digest("hex") + ".lock");
10
16
  const id = randomUUID(), deadline = Date.now() + 35_000;
11
17
  while (true) {
12
18
  try {
13
19
  const file = await open(path, "wx", 0o600);
14
- await file.writeFile(JSON.stringify({ id, createdAt: Date.now() }));
15
- await file.close();
20
+ try {
21
+ await file.writeFile(JSON.stringify({ id, createdAt: Date.now() }));
22
+ }
23
+ catch (error) {
24
+ await unlink(path).catch(() => { });
25
+ throw error;
26
+ }
27
+ finally {
28
+ await file.close();
29
+ }
16
30
  break;
17
31
  }
18
32
  catch (error) {
19
33
  if (error.code !== "EEXIST")
20
- throw new Error("Unable to lock the UserReason credential store");
34
+ throw new CredentialStoreError('lock_failed', `Unable to lock UserReason credentials for refresh at ${path}. Allow local write access and retry.`);
21
35
  try {
22
36
  const old = JSON.parse(await readFile(path, "utf8"));
23
37
  if (typeof old.createdAt === "number" && Date.now() - old.createdAt > 120_000) {
@@ -27,7 +41,7 @@ export async function withSessionLock(config, operation) {
27
41
  }
28
42
  catch { /* Another process may be writing or releasing its lock. */ }
29
43
  if (Date.now() >= deadline)
30
- throw new Error("Another UserReason process is refreshing credentials; try again shortly");
44
+ throw new CredentialStoreError('lock_timeout', "Another UserReason process is refreshing credentials; try again shortly");
31
45
  await new Promise(resolve => setTimeout(resolve, 150));
32
46
  }
33
47
  }
@@ -0,0 +1,20 @@
1
+ export interface PackageProfile {
2
+ schemaVersion: 1;
3
+ kind: "production" | "local-development";
4
+ apiEnvironment: "production" | "development";
5
+ docsUrl: string;
6
+ sdkVersion: string;
7
+ cliVersion: string;
8
+ wizardVersion?: string;
9
+ buildId?: string;
10
+ registry?: string;
11
+ }
12
+ export declare const PRODUCTION_DOCS = "https://docs.userreason.com";
13
+ export declare const DEVELOPMENT_DOCS = "https://docs-dev.userreason.com";
14
+ export declare const productionProfile: PackageProfile;
15
+ export declare function parsePackageProfile(value: unknown): PackageProfile;
16
+ /** The same optional file travels beside the CLI modules and the wizard's bundled modules. */
17
+ export declare function packageProfile(): PackageProfile;
18
+ export declare function docsOrigin(development?: boolean): string;
19
+ /** Ask the foreground builder for a complete current set, including changes still being compiled. */
20
+ export declare function currentPackageProfile(profile?: PackageProfile, fetcher?: typeof fetch): Promise<PackageProfile>;
@@ -0,0 +1,57 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { apiUrl, object } from "./contracts.js";
3
+ export const PRODUCTION_DOCS = "https://docs.userreason.com";
4
+ export const DEVELOPMENT_DOCS = "https://docs-dev.userreason.com";
5
+ export const productionProfile = {
6
+ schemaVersion: 1, kind: "production", apiEnvironment: "production",
7
+ docsUrl: PRODUCTION_DOCS, sdkVersion: "0.5.0", cliVersion: "0.2.0",
8
+ };
9
+ export function parsePackageProfile(value) {
10
+ const data = object(value, "package profile");
11
+ if (data.schemaVersion !== 1 || data.kind !== "local-development" || data.apiEnvironment !== "development" ||
12
+ data.docsUrl !== DEVELOPMENT_DOCS || typeof data.buildId !== "string" || !/^[a-f0-9]{64}$/.test(data.buildId) ||
13
+ [data.sdkVersion, data.cliVersion, data.wizardVersion].some(version => typeof version !== "string" || !/^\d+\.\d+\.\d+-dev\.[0-9]+$/.test(version)) ||
14
+ typeof data.registry !== "string")
15
+ throw new Error("Invalid local UserReason package profile");
16
+ const registry = apiUrl(data.registry), url = new URL(registry);
17
+ if (url.protocol !== "http:" || url.hostname !== "127.0.0.1")
18
+ throw new Error("Local UserReason packages require a loopback registry");
19
+ return { schemaVersion: 1, kind: "local-development", apiEnvironment: "development", docsUrl: DEVELOPMENT_DOCS,
20
+ sdkVersion: data.sdkVersion, cliVersion: data.cliVersion, wizardVersion: data.wizardVersion,
21
+ buildId: data.buildId, registry };
22
+ }
23
+ /** The same optional file travels beside the CLI modules and the wizard's bundled modules. */
24
+ export function packageProfile() {
25
+ try {
26
+ return parsePackageProfile(JSON.parse(readFileSync(new URL("./package-profile.json", import.meta.url), "utf8")));
27
+ }
28
+ catch (error) {
29
+ if (error.code === "ENOENT")
30
+ return { ...productionProfile };
31
+ throw error;
32
+ }
33
+ }
34
+ export function docsOrigin(development = packageProfile().apiEnvironment === "development") {
35
+ return development ? DEVELOPMENT_DOCS : PRODUCTION_DOCS;
36
+ }
37
+ /** Ask the foreground builder for a complete current set, including changes still being compiled. */
38
+ export async function currentPackageProfile(profile = packageProfile(), fetcher = fetch) {
39
+ if (profile.kind !== "local-development")
40
+ return profile;
41
+ let response;
42
+ try {
43
+ response = await fetcher(`${profile.registry}/-/userreason/dev-profile`, { redirect: "error", signal: AbortSignal.timeout(120_000), headers: { Accept: "application/json" } });
44
+ }
45
+ catch {
46
+ throw new Error("Local UserReason builder is unavailable. Start npm run dev:packages in ur-platform and retry.");
47
+ }
48
+ if (!response.ok)
49
+ throw new Error("The current local UserReason build failed. Check the dev:packages terminal and retry after fixing it.");
50
+ const text = await response.text();
51
+ if (text.length > 8192)
52
+ throw new Error("Invalid local UserReason build response");
53
+ const current = parsePackageProfile(JSON.parse(text));
54
+ if (current.registry !== profile.registry)
55
+ throw new Error("Local UserReason build registry does not match this wizard");
56
+ return current;
57
+ }
@@ -0,0 +1,9 @@
1
+ export interface ProjectTarget {
2
+ directory: string;
3
+ file: string;
4
+ siteId: string;
5
+ apiUrl: string;
6
+ workspaceId?: string;
7
+ }
8
+ /** Parse static SDK initialization only. Never execute application code or select an arbitrary account site. */
9
+ export declare function detectProjectTarget(directory?: string): Promise<ProjectTarget | undefined>;