@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
|
@@ -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.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { commandCatalog, commandSummary } from './catalog.js';
|
|
2
2
|
/** The overview groups every command noun. A test keeps this list in step with the catalog. */
|
|
3
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']]],
|
|
5
|
-
['Feedback', [['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'], ['user', 'People and their eligibility'], ['event', 'Product events and collection']]],
|
|
6
|
-
['Workspaces and sites', [['workspace', 'list · create · 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']]],
|
|
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
8
|
];
|
|
9
9
|
export const overviewNouns = GROUPS.flatMap(([, items]) => items.map(([name]) => name));
|
|
10
10
|
/** Commands with no action of their own: they never get a per-command help page. */
|
|
@@ -25,7 +25,7 @@ export function overviewHelp(kit, { version, development = false, trademark = fa
|
|
|
25
25
|
for (const [group, items] of GROUPS) {
|
|
26
26
|
lines.push(c.bold(group), ...items.map(([name, text]) => ` ${c.code(name)}${' '.repeat(first - name.length + 2)}${c.muted(text)}`), '');
|
|
27
27
|
}
|
|
28
|
-
lines.push(`${c.muted('Options')} --json --workspace UUID --site UUID --dev --no-color -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>"')}`);
|
|
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
29
|
return lines.join('\n') + '\n';
|
|
30
30
|
}
|
|
31
31
|
/** One noun's actions: exact usage from the catalog, without the options every command shares. */
|
|
@@ -44,7 +44,7 @@ export function nounHelp(kit, noun) {
|
|
|
44
44
|
else
|
|
45
45
|
lines.push(`userreason ${noun}${summary ? ` ${S.dot} ${summary}` : ''}`);
|
|
46
46
|
for (const [title, read] of [['Read', true], ['Write', false]]) {
|
|
47
|
-
const rows = items.filter(item => item.scopes.every(scope => READ_SCOPES.has(scope)) === read);
|
|
47
|
+
const rows = items.filter(item => (item.command !== 'workspace ai set' && item.scopes.every(scope => READ_SCOPES.has(scope))) === read);
|
|
48
48
|
if (!rows.length)
|
|
49
49
|
continue;
|
|
50
50
|
lines.push(c.bold(title));
|
package/dist/index.d.ts
CHANGED
|
@@ -12,3 +12,18 @@ export * from "./catalog.js";
|
|
|
12
12
|
export * from "./ui.js";
|
|
13
13
|
export * from "./help.js";
|
|
14
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-diagnostics.js";
|
|
26
|
+
export * from "./setup-files.js";
|
|
27
|
+
export * from "./setup-terminal.js";
|
|
28
|
+
export * from "./setup-source.js";
|
|
29
|
+
export * from "./setup-runner.js";
|
package/dist/index.js
CHANGED
|
@@ -12,3 +12,18 @@ export * from "./catalog.js";
|
|
|
12
12
|
export * from "./ui.js";
|
|
13
13
|
export * from "./help.js";
|
|
14
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-diagnostics.js";
|
|
26
|
+
export * from "./setup-files.js";
|
|
27
|
+
export * from "./setup-terminal.js";
|
|
28
|
+
export * from "./setup-source.js";
|
|
29
|
+
export * from "./setup-runner.js";
|
package/dist/profile.js
CHANGED
|
@@ -4,7 +4,7 @@ export const PRODUCTION_DOCS = "https://docs.userreason.com";
|
|
|
4
4
|
export const DEVELOPMENT_DOCS = "https://docs-dev.userreason.com";
|
|
5
5
|
export const productionProfile = {
|
|
6
6
|
schemaVersion: 1, kind: "production", apiEnvironment: "production",
|
|
7
|
-
docsUrl: PRODUCTION_DOCS, sdkVersion: "
|
|
7
|
+
docsUrl: PRODUCTION_DOCS, sdkVersion: "0.5.3", cliVersion: "0.2.1",
|
|
8
8
|
};
|
|
9
9
|
export function parsePackageProfile(value) {
|
|
10
10
|
const data = object(value, "package profile");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type Source = {
|
|
2
|
+
path: string;
|
|
3
|
+
text: string;
|
|
4
|
+
};
|
|
5
|
+
type Window = Source & {
|
|
6
|
+
start: number;
|
|
7
|
+
end: number;
|
|
8
|
+
};
|
|
9
|
+
/** Same complete-file/whole-AST-unit policy as the benchmark. An oversized
|
|
10
|
+
* statement is omitted rather than cut in the middle to fill a byte quota. */
|
|
11
|
+
export declare function recommendationSourceWindows(file: Source, requiredHandlers?: string[]): Window[];
|
|
12
|
+
export declare function connectedRecommendationSource(candidates: Source[], maxPacketBytes?: number): Window[];
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/** Benchmark structural source selection. The caller reads and screens files;
|
|
2
|
+
* this pure selector never executes code or expands the approved root. */
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { posix } from 'node:path';
|
|
5
|
+
const bytes = (text) => Buffer.byteLength(text);
|
|
6
|
+
const limits = { roots: 64, total: 164000, file: 26000, span: 16000, workflow: 38000, support: 8000, shared: 5000, excerpts: 200 };
|
|
7
|
+
function analyze(file) {
|
|
8
|
+
const ast = ts.createSourceFile(file.path, file.text, ts.ScriptTarget.Latest, true, /\.[cm]?jsx?$/.test(file.path) ? ts.ScriptKind.JSX : /\.tsx$/.test(file.path) ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
9
|
+
const imports = [], tags = new Set(), handlers = new Set(), templates = [];
|
|
10
|
+
const walk = (node) => {
|
|
11
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
12
|
+
const names = [], bindings = node.importClause?.namedBindings;
|
|
13
|
+
if (node.importClause?.name)
|
|
14
|
+
names.push(node.importClause.name.text);
|
|
15
|
+
if (bindings && ts.isNamedImports(bindings))
|
|
16
|
+
names.push(...bindings.elements.map(element => element.name.text));
|
|
17
|
+
imports.push({ specifier: node.moduleSpecifier.text, names });
|
|
18
|
+
}
|
|
19
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier))
|
|
20
|
+
imports.push({ specifier: node.moduleSpecifier.text, names: [] });
|
|
21
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node))
|
|
22
|
+
tags.add(node.tagName.getText(ast).split('.')[0]);
|
|
23
|
+
if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.getText(ast))) {
|
|
24
|
+
const ids = (child) => { if (ts.isIdentifier(child))
|
|
25
|
+
handlers.add(child.text); ts.forEachChild(child, ids); };
|
|
26
|
+
ts.forEachChild(node, ids);
|
|
27
|
+
}
|
|
28
|
+
if (ts.isPropertyAssignment(node) && node.name.getText(ast) === 'templateUrl' && ts.isStringLiteral(node.initializer))
|
|
29
|
+
templates.push(node.initializer.text);
|
|
30
|
+
ts.forEachChild(node, walk);
|
|
31
|
+
};
|
|
32
|
+
walk(ast);
|
|
33
|
+
if (/\.(?:vue|svelte|html)$/.test(file.path))
|
|
34
|
+
for (const match of file.text.matchAll(/(?:\([\w.:-]+\)|@[\w.:-]+)\s*=\s*["']([^"']+)["']/g))
|
|
35
|
+
for (const call of match[1].matchAll(/([A-Za-z_$][\w$]*)\s*\(/g))
|
|
36
|
+
handlers.add(call[1]);
|
|
37
|
+
return { ast, imports, rendered: new Set(imports.filter(item => item.names.some(name => tags.has(name))).map(item => item.specifier)), handlers, templates };
|
|
38
|
+
}
|
|
39
|
+
/** Same complete-file/whole-AST-unit policy as the benchmark. An oversized
|
|
40
|
+
* statement is omitted rather than cut in the middle to fill a byte quota. */
|
|
41
|
+
export function recommendationSourceWindows(file, requiredHandlers = []) {
|
|
42
|
+
const lines = file.text.split('\n');
|
|
43
|
+
const make = (start, end) => ({ path: file.path, start, end, text: lines.slice(start - 1, end).join('\n') });
|
|
44
|
+
if (bytes(file.text) <= limits.file)
|
|
45
|
+
return [make(1, lines.length)];
|
|
46
|
+
if (!/\.[cm]?[jt]sx?$/.test(file.path))
|
|
47
|
+
return [];
|
|
48
|
+
const a = analyze(file), units = [];
|
|
49
|
+
for (const handler of requiredHandlers)
|
|
50
|
+
a.handlers.add(handler);
|
|
51
|
+
const add = (node, priority) => units.push({ start: a.ast.getLineAndCharacterOfPosition(node.getStart(a.ast)).line + 1, end: a.ast.getLineAndCharacterOfPosition(node.end).line + 1, priority });
|
|
52
|
+
const inspect = (node) => {
|
|
53
|
+
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
|
54
|
+
add(node, 2);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if ((ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && node.body && ts.isBlock(node.body)) {
|
|
58
|
+
for (const statement of node.body.statements) {
|
|
59
|
+
const raw = statement.getText(a.ast);
|
|
60
|
+
const names = ts.isVariableStatement(statement) ? statement.declarationList.declarations.map(declaration => declaration.name.getText(a.ast)) : ts.isFunctionDeclaration(statement) && statement.name ? [statement.name.text] : [];
|
|
61
|
+
add(statement, /return\s*\(?\s*</.test(raw) || /useEffect|useState|useCallback|useMemo/.test(raw) || names.some(name => a.handlers.has(name)) ? 0 : 1);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (ts.isClassDeclaration(node)) {
|
|
66
|
+
for (const member of node.members)
|
|
67
|
+
add(member, member.name && a.handlers.has(member.name.getText(a.ast)) ? 0 : 1);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (ts.isVariableStatement(node)) {
|
|
71
|
+
let expanded = false;
|
|
72
|
+
for (const declaration of node.declarationList.declarations)
|
|
73
|
+
if (declaration.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) {
|
|
74
|
+
inspect(declaration.initializer);
|
|
75
|
+
expanded = true;
|
|
76
|
+
}
|
|
77
|
+
if (!expanded)
|
|
78
|
+
add(node, 2);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
add(node, 2);
|
|
82
|
+
};
|
|
83
|
+
for (const node of a.ast.statements)
|
|
84
|
+
inspect(node);
|
|
85
|
+
const selected = [];
|
|
86
|
+
let retained = 0;
|
|
87
|
+
for (const unit of units.sort((a, b) => a.priority - b.priority || a.start - b.start)) {
|
|
88
|
+
if (selected.some(window => window.start <= unit.start && window.end >= unit.end))
|
|
89
|
+
continue;
|
|
90
|
+
const window = make(unit.start, unit.end), length = bytes(window.text);
|
|
91
|
+
if (length > limits.span || retained + length > limits.file)
|
|
92
|
+
continue;
|
|
93
|
+
selected.push(window);
|
|
94
|
+
retained += length;
|
|
95
|
+
}
|
|
96
|
+
return selected.sort((a, b) => a.start - b.start);
|
|
97
|
+
}
|
|
98
|
+
const shell = (path) => /^(?:app|main|router|routes|layout)\.[cm]?[jt]sx?$/i.test(posix.basename(path)) || /\/app\.component\.(?:ts|html)$/.test(path);
|
|
99
|
+
const rank = (path) => /\/(?:routes|pages|screens|views)\//.test('/' + path) ? 0 : /\.component\.html$/.test(path) ? 1 : 2;
|
|
100
|
+
const family = (path) => { const parts = path.split('/'), index = parts.findIndex(part => ['routes', 'pages', 'screens', 'views'].includes(part)); return index >= 0 ? parts.slice(0, index + 2).join('/') : posix.dirname(path); };
|
|
101
|
+
export function connectedRecommendationSource(candidates, maxPacketBytes = 262144) {
|
|
102
|
+
const files = new Map(candidates.map(file => [file.path, file])), analyses = new Map();
|
|
103
|
+
const record = (path) => { if (!analyses.has(path))
|
|
104
|
+
analyses.set(path, analyze(files.get(path))); return analyses.get(path); };
|
|
105
|
+
const resolve = (from, specifier) => {
|
|
106
|
+
const base = specifier.startsWith('.') ? posix.normalize(posix.join(posix.dirname(from), specifier)) : specifier.startsWith('@/') ? `src/${specifier.slice(2)}` : '';
|
|
107
|
+
if (!base)
|
|
108
|
+
return undefined;
|
|
109
|
+
return [base, ...['.tsx', '.ts', '.jsx', '.js', '.vue', '.svelte', '.html', '/index.tsx', '/index.ts', '/index.jsx', '/index.js'].map(ext => base + ext)].find(path => files.has(path));
|
|
110
|
+
};
|
|
111
|
+
const references = (path) => {
|
|
112
|
+
const a = record(path);
|
|
113
|
+
return [
|
|
114
|
+
...a.imports.flatMap(item => { const target = resolve(path, item.specifier); return target ? [{ target, rendered: a.rendered.has(item.specifier), template: false }] : []; }),
|
|
115
|
+
...a.templates.flatMap(template => { const target = resolve(path, template); return target ? [{ target, rendered: true, template: true }] : []; }),
|
|
116
|
+
...(/\.component\.html$/.test(path) && files.has(path.replace(/\.html$/, '.ts')) ? [{ target: path.replace(/\.html$/, '.ts'), rendered: true, template: true }] : []),
|
|
117
|
+
].sort((a, b) => Number(b.rendered) - Number(a.rendered) || a.target.localeCompare(b.target));
|
|
118
|
+
};
|
|
119
|
+
const buckets = new Map();
|
|
120
|
+
for (const file of candidates.filter(file => rank(file.path) < 2 && !shell(file.path))) {
|
|
121
|
+
const key = family(file.path);
|
|
122
|
+
buckets.set(key, [...(buckets.get(key) ?? []), file]);
|
|
123
|
+
}
|
|
124
|
+
for (const bucket of buckets.values())
|
|
125
|
+
bucket.sort((a, b) => a.path.split('/').length - b.path.split('/').length || a.path.localeCompare(b.path));
|
|
126
|
+
const queues = [...buckets].sort((a, b) => a[0].localeCompare(b[0])), seeds = [];
|
|
127
|
+
for (let i = 0; seeds.length < Math.min(limits.roots, [...buckets.values()].reduce((n, values) => n + values.length, 0)); i++)
|
|
128
|
+
for (const [, bucket] of queues)
|
|
129
|
+
if (bucket[i] && seeds.length < limits.roots)
|
|
130
|
+
seeds.push(bucket[i].path);
|
|
131
|
+
if (!seeds.length)
|
|
132
|
+
seeds.push(...candidates.filter(file => shell(file.path)).map(file => file.path).slice(0, limits.roots));
|
|
133
|
+
if (!seeds.length)
|
|
134
|
+
seeds.push(...candidates.filter(file => /\.(?:[cm]?[jt]sx?|vue|svelte|html)$/.test(file.path)).map(file => file.path).slice(0, limits.roots));
|
|
135
|
+
const rootSet = new Set(seeds), owner = new Map(seeds.map(path => [path, path]));
|
|
136
|
+
const find = (path) => { const seen = new Set(); while (owner.get(path) !== path && !seen.has(path)) {
|
|
137
|
+
seen.add(path);
|
|
138
|
+
path = owner.get(path);
|
|
139
|
+
} return path; };
|
|
140
|
+
for (const seed of seeds)
|
|
141
|
+
for (const edge of references(seed))
|
|
142
|
+
if (edge.rendered && rootSet.has(edge.target) && find(seed) !== find(edge.target))
|
|
143
|
+
owner.set(find(edge.target), find(seed));
|
|
144
|
+
const groups = new Map();
|
|
145
|
+
for (const seed of seeds) {
|
|
146
|
+
const key = find(seed);
|
|
147
|
+
groups.set(key, [...(groups.get(key) ?? []), seed]);
|
|
148
|
+
}
|
|
149
|
+
const workflows = [];
|
|
150
|
+
for (const roots of groups.values()) {
|
|
151
|
+
const windows = [], seen = new Set(), queue = roots.map(path => ({ path, depth: 0 }));
|
|
152
|
+
let retained = 0, support = 0;
|
|
153
|
+
const append = (path, supporting, handlers = []) => {
|
|
154
|
+
for (const window of recommendationSourceWindows(files.get(path), handlers)) {
|
|
155
|
+
const key = `${path}:${window.start}:${window.end}`, length = bytes(window.text);
|
|
156
|
+
if (seen.has(key) || retained + length > limits.workflow || supporting && support + length > limits.support)
|
|
157
|
+
continue;
|
|
158
|
+
seen.add(key);
|
|
159
|
+
windows.push(window);
|
|
160
|
+
retained += length;
|
|
161
|
+
if (supporting)
|
|
162
|
+
support += length;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
for (const root of roots)
|
|
166
|
+
append(root, false);
|
|
167
|
+
const visited = new Set();
|
|
168
|
+
while (queue.length) {
|
|
169
|
+
const item = queue.shift();
|
|
170
|
+
if (visited.has(item.path) || item.depth > 2)
|
|
171
|
+
continue;
|
|
172
|
+
visited.add(item.path);
|
|
173
|
+
for (const edge of references(item.path).slice(0, 12)) {
|
|
174
|
+
if (rootSet.has(edge.target) && !roots.includes(edge.target))
|
|
175
|
+
continue;
|
|
176
|
+
append(edge.target, !roots.includes(edge.target), edge.template ? [...record(item.path).handlers] : []);
|
|
177
|
+
if (edge.rendered || edge.template)
|
|
178
|
+
queue.push({ path: edge.target, depth: item.depth + 1 });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (windows.length)
|
|
182
|
+
workflows.push(windows);
|
|
183
|
+
}
|
|
184
|
+
const selected = [], seen = new Set();
|
|
185
|
+
let retained = 0, wireBytes = 512;
|
|
186
|
+
const append = (window) => {
|
|
187
|
+
const key = `${window.path}:${window.start}:${window.end}`, length = bytes(window.text);
|
|
188
|
+
if (seen.has(key) || selected.length >= limits.excerpts || retained + length > limits.total)
|
|
189
|
+
return;
|
|
190
|
+
const encoded = bytes(JSON.stringify({ kind: 'local_source', id: `source_${selected.length + 1}`, path: window.path, file_hash: '0'.repeat(64), excerpt_hash: '0'.repeat(64), snapshot_id: '00000000-0000-4000-8000-000000000000', start_line: window.start, end_line: window.end, excerpt: window.text })) + 1;
|
|
191
|
+
if (wireBytes + encoded > maxPacketBytes)
|
|
192
|
+
return;
|
|
193
|
+
seen.add(key);
|
|
194
|
+
selected.push(window);
|
|
195
|
+
retained += length;
|
|
196
|
+
wireBytes += encoded;
|
|
197
|
+
};
|
|
198
|
+
let shared = 0;
|
|
199
|
+
for (const file of candidates.filter(file => shell(file.path)).slice(0, 8))
|
|
200
|
+
for (const window of recommendationSourceWindows(file))
|
|
201
|
+
if (shared + bytes(window.text) <= limits.shared) {
|
|
202
|
+
append(window);
|
|
203
|
+
shared += bytes(window.text);
|
|
204
|
+
}
|
|
205
|
+
// Fair interleaving keeps the first large view from consuming all evidence.
|
|
206
|
+
for (let i = 0; i < Math.max(0, ...workflows.map(windows => windows.length)); i++)
|
|
207
|
+
for (const windows of workflows)
|
|
208
|
+
if (windows[i])
|
|
209
|
+
append(windows[i]);
|
|
210
|
+
return selected;
|
|
211
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { UserReasonClient } from "./client.js";
|
|
2
|
+
import { type FirstSurveyPreparation } from "./first-survey-preparation.js";
|
|
3
|
+
import type { FirstSurveyUI } from "./first-survey.js";
|
|
4
|
+
type Receipt = {
|
|
5
|
+
prompt: string;
|
|
6
|
+
request: string;
|
|
7
|
+
definition: Record<string, unknown>;
|
|
8
|
+
result?: unknown;
|
|
9
|
+
};
|
|
10
|
+
/** One optional selection; all drafts stay unpublished and match no audience. */
|
|
11
|
+
export declare function recommendedSurveys(client: UserReasonClient, root: string, workspace: string, site: string, ui: FirstSurveyUI, options?: {
|
|
12
|
+
preparation?: FirstSurveyPreparation;
|
|
13
|
+
sourceConsent?: boolean;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
state: string;
|
|
16
|
+
error?: undefined;
|
|
17
|
+
count?: undefined;
|
|
18
|
+
receipts?: undefined;
|
|
19
|
+
} | {
|
|
20
|
+
state: string;
|
|
21
|
+
error: string;
|
|
22
|
+
count?: undefined;
|
|
23
|
+
receipts?: undefined;
|
|
24
|
+
} | {
|
|
25
|
+
state: string;
|
|
26
|
+
count: number;
|
|
27
|
+
receipts: Record<string, Receipt>;
|
|
28
|
+
error?: undefined;
|
|
29
|
+
}>;
|
|
30
|
+
export {};
|