@userreason/cli 0.1.1 → 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.
- package/README.md +207 -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 +60 -0
- package/dist/first-survey-source.js +289 -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 +14 -0
- package/dist/index.js +14 -0
- package/dist/profile.js +1 -1
- package/dist/recommended-surveys.d.ts +30 -0
- package/dist/recommended-surveys.js +201 -0
- package/dist/setup-executor.d.ts +35 -0
- package/dist/setup-executor.js +329 -0
- package/dist/setup-files.d.ts +11 -0
- package/dist/setup-files.js +96 -0
- package/dist/setup-runner.d.ts +14 -0
- package/dist/setup-runner.js +142 -0
- package/dist/setup-source.d.ts +31 -0
- package/dist/setup-source.js +48 -0
- package/dist/setup-store.d.ts +37 -0
- package/dist/setup-store.js +256 -0
- package/dist/setup-terminal.d.ts +3 -0
- package/dist/setup-terminal.js +6 -0
- package/dist/setup-wire.d.ts +1237 -0
- package/dist/setup-wire.js +175 -0
- package/dist/setup.d.ts +1223 -0
- package/dist/setup.js +258 -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,457 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { mkdir, open, rename, unlink, lstat, writeFile, realpath } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { configDirectory } from './config.js';
|
|
6
|
+
import { sourceHash, verifyFirstSurveySource } from './first-survey-source.js';
|
|
7
|
+
import { prepareFirstSurveyLocally } from './first-survey-preparation.js';
|
|
8
|
+
const goals = [
|
|
9
|
+
{ value: 'improve', label: 'Find opportunities to improve' },
|
|
10
|
+
{ value: 'friction', label: 'Understand difficulties in a workflow' },
|
|
11
|
+
{ value: 'satisfaction', label: 'Measure satisfaction' },
|
|
12
|
+
];
|
|
13
|
+
const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
14
|
+
const blockedAudience = () => ({ type: 'not', rule: { type: 'always' } });
|
|
15
|
+
const cleanText = (text, maximum) => text.trim().length > 0 && text.trim().length <= maximum && !/[\u0000-\u001f\u007f]/.test(text);
|
|
16
|
+
const digest = (value) => sourceHash(JSON.stringify(value));
|
|
17
|
+
export function starterDefinition(goal, product, focus = 'this workflow') {
|
|
18
|
+
const question = goal === 'satisfaction'
|
|
19
|
+
? `How satisfied are you with ${product} overall?`
|
|
20
|
+
: goal === 'friction'
|
|
21
|
+
? `What, if anything, made ${focus} difficult?`
|
|
22
|
+
: `What, if anything, would make ${product} more useful to you?`;
|
|
23
|
+
return {
|
|
24
|
+
schema_version: 1,
|
|
25
|
+
name: `${goals.find((g) => g.value === goal).label}: ${product}`.slice(0, 240),
|
|
26
|
+
priority: 0,
|
|
27
|
+
sampling_percent: 10,
|
|
28
|
+
enrollment_epoch: 'first-survey-v1',
|
|
29
|
+
content: {
|
|
30
|
+
schema_version: 1,
|
|
31
|
+
title: 'Help us improve',
|
|
32
|
+
description: null,
|
|
33
|
+
thank_you: null,
|
|
34
|
+
questions: [
|
|
35
|
+
goal === 'satisfaction'
|
|
36
|
+
? {
|
|
37
|
+
id: 'first',
|
|
38
|
+
kind: 'rating',
|
|
39
|
+
prompt: question,
|
|
40
|
+
required: false,
|
|
41
|
+
min: 1,
|
|
42
|
+
max: 5,
|
|
43
|
+
min_label: 'Very dissatisfied',
|
|
44
|
+
max_label: 'Very satisfied',
|
|
45
|
+
labels: [
|
|
46
|
+
'Very dissatisfied',
|
|
47
|
+
'Dissatisfied',
|
|
48
|
+
'Neither satisfied nor dissatisfied',
|
|
49
|
+
'Satisfied',
|
|
50
|
+
'Very satisfied',
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
: { id: 'first', kind: 'text', prompt: question, required: false, max_length: 2000 },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
// A saved starter cannot match anyone until the author configures targeting.
|
|
57
|
+
rules: {
|
|
58
|
+
schema_version: 1,
|
|
59
|
+
audience: blockedAudience(),
|
|
60
|
+
triggers: [{ type: 'page_load' }],
|
|
61
|
+
frequency: { mode: 'site_default' },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function suggestion(definition, reason, sources = []) {
|
|
66
|
+
const content = record(definition.content), questions = content.questions;
|
|
67
|
+
if (!Array.isArray(questions) || questions.length !== 1)
|
|
68
|
+
throw Error('A starter needs exactly one question');
|
|
69
|
+
const q = record(questions[0]);
|
|
70
|
+
if (typeof q.prompt !== 'string' ||
|
|
71
|
+
!cleanText(q.prompt, 300) ||
|
|
72
|
+
!['text', 'rating'].includes(String(q.kind)))
|
|
73
|
+
throw Error('Unsupported starter question');
|
|
74
|
+
const copy = structuredClone(definition);
|
|
75
|
+
// AI proposes wording, not verified runtime targeting. The author configures that separately.
|
|
76
|
+
copy.rules = {
|
|
77
|
+
schema_version: 1,
|
|
78
|
+
audience: blockedAudience(),
|
|
79
|
+
triggers: [{ type: 'page_load' }],
|
|
80
|
+
frequency: { mode: 'site_default' },
|
|
81
|
+
};
|
|
82
|
+
return { question: q.prompt, definition: copy, reason, sources };
|
|
83
|
+
}
|
|
84
|
+
function preview(s) {
|
|
85
|
+
const q = record(record(s.definition.content).questions[0]);
|
|
86
|
+
const response = q.kind === 'rating' && Array.isArray(q.labels)
|
|
87
|
+
? `Optional rating: ${q.labels.join(' · ')}`
|
|
88
|
+
: 'Optional written answer';
|
|
89
|
+
return `${s.reason}\nQuestion: ${s.question}\n${response}`;
|
|
90
|
+
}
|
|
91
|
+
function abortable(work, signal) {
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const aborted = () => reject(signal.reason ?? new Error('Cancelled'));
|
|
94
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
95
|
+
work.then((value) => {
|
|
96
|
+
signal.removeEventListener('abort', aborted);
|
|
97
|
+
resolve(value);
|
|
98
|
+
}, (error) => {
|
|
99
|
+
signal.removeEventListener('abort', aborted);
|
|
100
|
+
reject(error);
|
|
101
|
+
});
|
|
102
|
+
if (signal.aborted)
|
|
103
|
+
aborted();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** Guided draft creation. Templates work without AI, source consent or provider availability. */
|
|
107
|
+
export async function starterSurvey(client, root, workspace, site, ui, options = {}) {
|
|
108
|
+
ui.signal?.throwIfAborted();
|
|
109
|
+
root = await realpath(root);
|
|
110
|
+
const preparation = options.preparation?.root === root ? options.preparation : prepareFirstSurveyLocally(root);
|
|
111
|
+
const directory = join(configDirectory(), 'starter-survey');
|
|
112
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
113
|
+
if ((await lstat(directory)).isSymbolicLink())
|
|
114
|
+
throw Error('Unsafe starter session directory');
|
|
115
|
+
const path = join(directory, digest([root, client.config.apiUrl, workspace, site]) + '.json');
|
|
116
|
+
const lock = await open(path + '.lock', 'wx', 0o600).catch(() => {
|
|
117
|
+
preparation.cancel();
|
|
118
|
+
throw Error('Another first-survey process owns this session');
|
|
119
|
+
});
|
|
120
|
+
let state = {
|
|
121
|
+
version: 1,
|
|
122
|
+
root,
|
|
123
|
+
api: client.config.apiUrl,
|
|
124
|
+
workspace,
|
|
125
|
+
site,
|
|
126
|
+
expires: Date.now() + 7 * 86400000,
|
|
127
|
+
product: options.product && cleanText(options.product, 120) ? options.product.trim() : 'this product',
|
|
128
|
+
focus: 'this workflow',
|
|
129
|
+
};
|
|
130
|
+
const save = async () => {
|
|
131
|
+
const temp = path + '.' + randomUUID() + '.tmp';
|
|
132
|
+
await writeFile(temp, JSON.stringify(state), { flag: 'wx', mode: 0o600 });
|
|
133
|
+
await rename(temp, path);
|
|
134
|
+
};
|
|
135
|
+
let ai = {
|
|
136
|
+
status: 'idle',
|
|
137
|
+
suggestions: [],
|
|
138
|
+
};
|
|
139
|
+
let work, controller, analysisId;
|
|
140
|
+
const check = () => ui.signal?.throwIfAborted();
|
|
141
|
+
const scope = { api: client.config.apiUrl, workspace_id: workspace, site_id: site };
|
|
142
|
+
const stopAI = async () => {
|
|
143
|
+
controller?.abort();
|
|
144
|
+
if (analysisId && ai.status !== 'ready') {
|
|
145
|
+
await client
|
|
146
|
+
.request('/onboarding/v1/planning/cancel', { schema_version: 1, session_id: analysisId, scope }, AbortSignal.timeout(1500))
|
|
147
|
+
.catch(() => { });
|
|
148
|
+
}
|
|
149
|
+
await work;
|
|
150
|
+
analysisId = undefined;
|
|
151
|
+
ai = { status: 'idle', suggestions: [] };
|
|
152
|
+
};
|
|
153
|
+
const text = async (message, fallback, max = 300) => {
|
|
154
|
+
for (;;) {
|
|
155
|
+
check();
|
|
156
|
+
const answer = (await ui.answer(message)).trim() || fallback;
|
|
157
|
+
if (cleanText(answer, max))
|
|
158
|
+
return answer;
|
|
159
|
+
ui.show(`Use between 1 and ${max} characters on one line.`);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const showSaved = (result, id) => {
|
|
163
|
+
const value = record(result), links = record(value.links);
|
|
164
|
+
ui.show(`Draft saved. ${String(links.edit ?? id)}\nIt is unpublished. Review the audience and timing in the editor before launching.`);
|
|
165
|
+
};
|
|
166
|
+
const create = async () => {
|
|
167
|
+
check();
|
|
168
|
+
const receipt = state.receipt;
|
|
169
|
+
if (!receipt.result)
|
|
170
|
+
receipt.result = await client.promptCommand({
|
|
171
|
+
workspaceId: workspace,
|
|
172
|
+
siteId: site,
|
|
173
|
+
action: 'create',
|
|
174
|
+
promptId: receipt.prompt,
|
|
175
|
+
requestId: receipt.request,
|
|
176
|
+
definition: receipt.definition,
|
|
177
|
+
});
|
|
178
|
+
await save();
|
|
179
|
+
showSaved(receipt.result, receipt.prompt);
|
|
180
|
+
return { state: 'saved', prompt_id: receipt.prompt, result: receipt.result };
|
|
181
|
+
};
|
|
182
|
+
try {
|
|
183
|
+
try {
|
|
184
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
185
|
+
try {
|
|
186
|
+
const stat = await file.stat();
|
|
187
|
+
if (stat.size > 128000)
|
|
188
|
+
throw Error('Starter session too large');
|
|
189
|
+
const old = JSON.parse(await file.readFile('utf8'));
|
|
190
|
+
if (old.version !== 1 ||
|
|
191
|
+
old.root !== root ||
|
|
192
|
+
old.api !== state.api ||
|
|
193
|
+
old.workspace !== workspace ||
|
|
194
|
+
old.site !== site)
|
|
195
|
+
throw Error('Starter session scope changed');
|
|
196
|
+
if (old.expires > Date.now())
|
|
197
|
+
state = old;
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
await file.close();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (error.code !== 'ENOENT')
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
if (state.receipt) {
|
|
208
|
+
if (state.receipt.result) {
|
|
209
|
+
showSaved(state.receipt.result, state.receipt.prompt);
|
|
210
|
+
return { state: 'saved', prompt_id: state.receipt.prompt, result: state.receipt.result };
|
|
211
|
+
}
|
|
212
|
+
ui.show('The previous save was not confirmed. Retrying uses the same draft and request IDs.');
|
|
213
|
+
if (!(await ui.confirm('Retry saving the already-reviewed draft?')))
|
|
214
|
+
return { state: 'save_pending' };
|
|
215
|
+
return await create();
|
|
216
|
+
}
|
|
217
|
+
if (!state.goal || !state.selected) {
|
|
218
|
+
const goal = await ui.choose('What would you like to learn?', [
|
|
219
|
+
...goals,
|
|
220
|
+
{ value: 'finish', label: 'Finish setup for now' },
|
|
221
|
+
]);
|
|
222
|
+
if (goal === 'finish')
|
|
223
|
+
return { state: 'skipped' };
|
|
224
|
+
if (!goals.some((g) => g.value === goal))
|
|
225
|
+
throw Error('Unknown learning goal');
|
|
226
|
+
state.goal = goal;
|
|
227
|
+
if (state.goal === 'friction')
|
|
228
|
+
state.focus = await text('Which workflow? For example, creating a report (blank: using this product)', `using ${state.product}`, 120);
|
|
229
|
+
state.selected = suggestion(starterDefinition(state.goal, state.product, state.focus), 'A starter question. You can use or edit it now.');
|
|
230
|
+
await save();
|
|
231
|
+
}
|
|
232
|
+
for (;;) {
|
|
233
|
+
check();
|
|
234
|
+
const selected = state.selected;
|
|
235
|
+
ui.show(preview(selected));
|
|
236
|
+
const action = await ui.choose('Your first survey', [
|
|
237
|
+
{ value: 'use', label: 'Use this question' },
|
|
238
|
+
{ value: 'edit', label: 'Edit the question' },
|
|
239
|
+
...(ai.status === 'abstained' ? [] : ai.status === 'ready'
|
|
240
|
+
? [{ value: 'review_ai', label: 'Review AI suggestions' }]
|
|
241
|
+
: ai.status === 'pending'
|
|
242
|
+
? [{ value: 'check_ai', label: 'Check personalization — starter remains available' }]
|
|
243
|
+
: [
|
|
244
|
+
{
|
|
245
|
+
value: 'personalize',
|
|
246
|
+
label: ai.status === 'failed'
|
|
247
|
+
? 'Retry AI personalization (optional)'
|
|
248
|
+
: 'Personalize with AI (optional)',
|
|
249
|
+
},
|
|
250
|
+
]),
|
|
251
|
+
{ value: 'more', label: 'Other goals or finish later' },
|
|
252
|
+
]);
|
|
253
|
+
if (action === 'edit') {
|
|
254
|
+
const question = await text('Your question (blank: keep current wording)', selected.question);
|
|
255
|
+
const definition = structuredClone(selected.definition), content = record(definition.content);
|
|
256
|
+
content.questions[0].prompt = question;
|
|
257
|
+
state.selected = suggestion(definition, 'Your edited question.', selected.sources);
|
|
258
|
+
await save();
|
|
259
|
+
}
|
|
260
|
+
else if (action === 'personalize') {
|
|
261
|
+
try {
|
|
262
|
+
const metadata = record(await client.request('/onboarding/v1/planning/describe', { schema_version: 1, session_id: randomUUID(), scope }, ui.signal));
|
|
263
|
+
const preview = await preparation.take(ui.signal);
|
|
264
|
+
if (!preview.evidence.length) {
|
|
265
|
+
ui.show('No suitable source was found. Your starter question is ready to use.');
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const evidence = [
|
|
269
|
+
...preview.evidence,
|
|
270
|
+
{
|
|
271
|
+
kind: 'clarification',
|
|
272
|
+
id: 'onboarding_goal',
|
|
273
|
+
question_id: 'onboarding_goal',
|
|
274
|
+
answer: state.goal,
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
kind: 'clarification',
|
|
278
|
+
id: 'onboarding_focus',
|
|
279
|
+
question_id: 'onboarding_focus',
|
|
280
|
+
answer: state.focus,
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
kind: 'clarification',
|
|
284
|
+
id: 'onboarding_product',
|
|
285
|
+
question_id: 'onboarding_product',
|
|
286
|
+
answer: state.product,
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
ui.show(`Optional personalization: ${String(metadata.destination)}\nModel: ${String(metadata.model ?? 'configured provider model')}\n${String(metadata.policy)}\nConfigured prices: ${JSON.stringify(metadata.prices ?? {})}. Limit: 12,000 source-input / 4,000 output tokens; 30 seconds.\nReview the following exact source packet and learning goal for private material before sending. Only this application root is included. No Git history is included.\n${JSON.stringify(evidence, null, 2)}`);
|
|
290
|
+
if (!(await ui.confirm('Approve sending this exact packet for AI personalization?')))
|
|
291
|
+
continue;
|
|
292
|
+
await verifyFirstSurveySource(root, preview.evidence);
|
|
293
|
+
check();
|
|
294
|
+
await stopAI();
|
|
295
|
+
controller = new AbortController();
|
|
296
|
+
const signal = AbortSignal.any([
|
|
297
|
+
controller.signal,
|
|
298
|
+
AbortSignal.timeout(35000),
|
|
299
|
+
...(ui.signal ? [ui.signal] : []),
|
|
300
|
+
]);
|
|
301
|
+
analysisId = randomUUID();
|
|
302
|
+
ai = { status: 'pending', suggestions: [] };
|
|
303
|
+
const current = ai;
|
|
304
|
+
const request = {
|
|
305
|
+
schema_version: 1,
|
|
306
|
+
session_id: analysisId,
|
|
307
|
+
request_id: randomUUID(),
|
|
308
|
+
scope,
|
|
309
|
+
packages: preview.packages,
|
|
310
|
+
capabilities: [...preview.capabilities, 'guided_starter:v1'],
|
|
311
|
+
evidence,
|
|
312
|
+
previous_plan_digest: null,
|
|
313
|
+
limits: { input_tokens: 12000, output_tokens: 4000, deadline_ms: 30000 },
|
|
314
|
+
consent: {
|
|
315
|
+
packet_digest: digest(evidence),
|
|
316
|
+
destination: metadata.destination,
|
|
317
|
+
policy_version: 'first-survey-v1',
|
|
318
|
+
configuration_fingerprint: metadata.configuration_fingerprint,
|
|
319
|
+
authorized_at: new Date().toISOString(),
|
|
320
|
+
git_authorized: false,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
work = abortable(client.request('/onboarding/v1/planning/analyze', request, signal), signal)
|
|
324
|
+
.then(async (response) => {
|
|
325
|
+
signal.throwIfAborted();
|
|
326
|
+
const candidates = record(record(response).result).candidates;
|
|
327
|
+
if (!Array.isArray(candidates))
|
|
328
|
+
throw Error('Invalid personalization result');
|
|
329
|
+
await verifyFirstSurveySource(root, preview.evidence);
|
|
330
|
+
signal.throwIfAborted();
|
|
331
|
+
current.suggestions = candidates.slice(0, 3).flatMap((value) => {
|
|
332
|
+
const c = record(value);
|
|
333
|
+
try {
|
|
334
|
+
const candidate = suggestion(record(c.definition), String(record(c.rationale).claim ?? 'A suggestion based on the reviewed source.'), preview.evidence.map(({ path, file_hash }) => ({ path, file_hash })));
|
|
335
|
+
const q = record(record(candidate.definition.content).questions[0]);
|
|
336
|
+
return q.kind === (state.goal === 'satisfaction' ? 'rating' : 'text') ? [candidate] : [];
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
current.status = current.suggestions.length ? 'ready' : 'abstained';
|
|
343
|
+
})
|
|
344
|
+
.catch(() => {
|
|
345
|
+
current.status = 'failed';
|
|
346
|
+
});
|
|
347
|
+
ui.show('Personalization is running. You can use or edit the starter without waiting.');
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
check();
|
|
351
|
+
ai.status = 'failed';
|
|
352
|
+
ui.show('Personalization is unavailable. Your starter question is still ready to use.');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else if (action === 'check_ai') {
|
|
356
|
+
ui.show(ai.status === 'pending'
|
|
357
|
+
? 'Still working. You can continue with the starter.'
|
|
358
|
+
: ai.status === 'ready'
|
|
359
|
+
? 'Your AI suggestions are ready to review.'
|
|
360
|
+
: 'No suitable personalization was returned. The starter remains available.');
|
|
361
|
+
}
|
|
362
|
+
else if (action === 'review_ai' && ai.status === 'ready') {
|
|
363
|
+
const pick = await ui.choose('Choose a suggestion or keep your current question', [
|
|
364
|
+
...ai.suggestions.map((s, i) => ({ value: String(i), label: s.question })),
|
|
365
|
+
{ value: 'keep', label: 'Keep my current question' },
|
|
366
|
+
]);
|
|
367
|
+
if (pick !== 'keep') {
|
|
368
|
+
const candidate = ai.suggestions[Number(pick)];
|
|
369
|
+
if (!candidate)
|
|
370
|
+
throw Error('Unknown suggestion');
|
|
371
|
+
await verifyFirstSurveySource(root, candidate.sources);
|
|
372
|
+
state.selected = candidate;
|
|
373
|
+
await save();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else if (action === 'more') {
|
|
377
|
+
const next = await ui.choose('Continue when you are ready', [
|
|
378
|
+
{ value: 'back', label: 'Back to my question' },
|
|
379
|
+
{ value: 'goal', label: 'Choose another learning goal' },
|
|
380
|
+
{ value: 'finish', label: 'Finish later — keep this draft locally' },
|
|
381
|
+
]);
|
|
382
|
+
if (next === 'finish') {
|
|
383
|
+
ui.show('Your question is saved locally. Rerun the development wizard to resume it.');
|
|
384
|
+
return { state: 'draft', goal: state.goal };
|
|
385
|
+
}
|
|
386
|
+
if (next === 'goal') {
|
|
387
|
+
await stopAI();
|
|
388
|
+
const goal = await ui.choose('What would you like to learn?', goals);
|
|
389
|
+
if (!goals.some((g) => g.value === goal))
|
|
390
|
+
throw Error('Unknown learning goal');
|
|
391
|
+
state.goal = goal;
|
|
392
|
+
if (state.goal === 'friction')
|
|
393
|
+
state.focus = await text('Which workflow?', `using ${state.product}`, 120);
|
|
394
|
+
state.selected = suggestion(starterDefinition(state.goal, state.product, state.focus), 'A starter question.');
|
|
395
|
+
await save();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
else if (action === 'use') {
|
|
399
|
+
await stopAI();
|
|
400
|
+
check();
|
|
401
|
+
if (selected.sources.length)
|
|
402
|
+
await verifyFirstSurveySource(root, selected.sources);
|
|
403
|
+
const definition = structuredClone(selected.definition);
|
|
404
|
+
const when = await ui.choose('When should this survey be offered?', [
|
|
405
|
+
{ value: 'editor', label: 'Set audience and timing in the editor' },
|
|
406
|
+
{ value: 'event', label: 'After a verified workflow-completion event' },
|
|
407
|
+
]);
|
|
408
|
+
if (when === 'event') {
|
|
409
|
+
const event = await text('Existing completion event name', '', 80);
|
|
410
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(event)) {
|
|
411
|
+
ui.show('Use a valid existing event name.');
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (!(await ui.confirm('Have you verified this event fires only after the relevant experience, for people who can answer this question?')))
|
|
415
|
+
continue;
|
|
416
|
+
definition.rules = {
|
|
417
|
+
schema_version: 1,
|
|
418
|
+
audience: {
|
|
419
|
+
type: 'local_event',
|
|
420
|
+
version: 1,
|
|
421
|
+
event_key: event,
|
|
422
|
+
dimension_key: '',
|
|
423
|
+
minimum: 1,
|
|
424
|
+
delay_ms: 0,
|
|
425
|
+
},
|
|
426
|
+
triggers: [{ type: 'event', event_key: event }],
|
|
427
|
+
frequency: { mode: 'site_default' },
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
else if (when !== 'editor')
|
|
431
|
+
throw Error('Unknown timing choice');
|
|
432
|
+
ui.show(when === 'editor'
|
|
433
|
+
? 'The saved draft will match nobody until you configure its audience and timing.'
|
|
434
|
+
: 'The draft will use your confirmed completion event. Test it before launch.');
|
|
435
|
+
const validation = record(await client.read('prompts.validate', { workspaceId: workspace, siteId: site, definition }));
|
|
436
|
+
if (validation.valid !== true) {
|
|
437
|
+
ui.show('This draft needs changes before it can be saved. Your question is kept locally.');
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (!(await ui.confirm('Save this reviewed question as an unpublished draft?')))
|
|
441
|
+
continue;
|
|
442
|
+
check();
|
|
443
|
+
if (selected.sources.length)
|
|
444
|
+
await verifyFirstSurveySource(root, selected.sources);
|
|
445
|
+
state.receipt = { prompt: randomUUID(), request: randomUUID(), definition };
|
|
446
|
+
await save();
|
|
447
|
+
return await create();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
await stopAI();
|
|
453
|
+
preparation.cancel();
|
|
454
|
+
await lock.close();
|
|
455
|
+
await unlink(path + '.lock');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type Credentials, type Session } from './contracts.js';
|
|
2
|
+
export declare const WIZARD_FLOW_VERSION = "2026-10-26.1";
|
|
3
|
+
export declare const WIZARD_CONTENT_VERSION = "2026-10-26.1";
|
|
4
|
+
export declare const WIZARD_STEPS: readonly ["run", "authentication", "target_selection", "options", "sdk_install", "cli_install", "initialization", "configuration_check", "agent_note", "agent_skill", "cli_access", "global_cli"];
|
|
5
|
+
export declare const WIZARD_PHASES: readonly ["eligible", "offered", "selected", "declined", "applied", "failed", "skipped", "already_present", "cancelled", "started", "completed"];
|
|
6
|
+
export declare const TELEMETRY_REASONS: readonly ["not_requested", "installation_pending", "already_present", "up_to_date", "unsupported", "existing_content_preserved", "permission_denied", "validation_failed", "network_error", "operation_failed", "user_cancelled", "explicit_skip", "packages_current"];
|
|
7
|
+
export type ExecutionMode = 'interactive' | 'agent' | 'ci' | 'noninteractive';
|
|
8
|
+
type Step = typeof WIZARD_STEPS[number];
|
|
9
|
+
type Phase = typeof WIZARD_PHASES[number];
|
|
10
|
+
type Reason = typeof TELEMETRY_REASONS[number];
|
|
11
|
+
type WizardDetails = {
|
|
12
|
+
duration_ms?: number;
|
|
13
|
+
reason?: Reason;
|
|
14
|
+
selection_source?: 'prompt' | 'flag' | 'default';
|
|
15
|
+
framework?: 'react' | 'next' | 'vue' | 'vanilla';
|
|
16
|
+
package_manager?: 'npm' | 'pnpm' | 'yarn';
|
|
17
|
+
resumed?: boolean;
|
|
18
|
+
guidance_file?: 'AGENTS.md' | 'CLAUDE.md';
|
|
19
|
+
};
|
|
20
|
+
type Context = {
|
|
21
|
+
workspace_id?: string;
|
|
22
|
+
site_id?: string;
|
|
23
|
+
source_site_environment?: 'development' | 'production';
|
|
24
|
+
};
|
|
25
|
+
export declare function telemetryEnabled(): Promise<boolean>;
|
|
26
|
+
export declare function setTelemetryEnabled(enabled: boolean): Promise<void>;
|
|
27
|
+
export declare function telemetryCommand(positionals: readonly string[], help?: boolean): string;
|
|
28
|
+
/** A best-effort recorder. Credentials stay in memory and never enter the spool or payload. */
|
|
29
|
+
export declare class ToolTelemetry {
|
|
30
|
+
private options;
|
|
31
|
+
readonly runId: `${string}-${string}-${string}-${string}-${string}`;
|
|
32
|
+
private pending;
|
|
33
|
+
private session;
|
|
34
|
+
private context;
|
|
35
|
+
private wizardContext;
|
|
36
|
+
private active;
|
|
37
|
+
private fetcher;
|
|
38
|
+
private origin;
|
|
39
|
+
constructor(options: {
|
|
40
|
+
surface: 'wizard' | 'cli';
|
|
41
|
+
releaseVersion: string;
|
|
42
|
+
executionMode: ExecutionMode;
|
|
43
|
+
apiUrl: string;
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
offline?: boolean;
|
|
46
|
+
fetcher?: typeof fetch;
|
|
47
|
+
minimumIntervalMs?: number;
|
|
48
|
+
});
|
|
49
|
+
configure(input: {
|
|
50
|
+
apiUrl?: string;
|
|
51
|
+
context?: Context;
|
|
52
|
+
wizard?: WizardDetails;
|
|
53
|
+
}): void;
|
|
54
|
+
setSession(session: Session | null): void;
|
|
55
|
+
observeCredentials(credentials: Credentials): Credentials;
|
|
56
|
+
private scope;
|
|
57
|
+
private common;
|
|
58
|
+
step(step_id: Step, phase: Phase, details?: WizardDetails): void;
|
|
59
|
+
command(command: string, success: boolean, durationMs: number): void;
|
|
60
|
+
/** Save a bounded checkpoint without introducing a network call into a wizard step. */
|
|
61
|
+
checkpoint(): Promise<void>;
|
|
62
|
+
flush(force?: boolean, storeOnly?: boolean): Promise<void>;
|
|
63
|
+
private aggregate;
|
|
64
|
+
private installationId;
|
|
65
|
+
private cleanup;
|
|
66
|
+
}
|
|
67
|
+
export {};
|