@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
package/dist/setup.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { apiUrl } from './contracts.js';
|
|
3
|
+
import { telemetryEnabled } from './telemetry.js';
|
|
4
|
+
import { loadSetupSession, parseStoredSetup, saveSetupSession, setupProjectFingerprint, SetupError, } from './setup-store.js';
|
|
5
|
+
import { parseSetupCapabilities, parseSetupDiscovery, parseSetupStartInput, parseSetupStartResult, parseSetupExchangeInput, parseSetupExchangeResult, parseSetupSession, parseSetupPollInput, parseSetupLocalPlanInput, parseSetupClaimInput, parseSetupClaimResult, parseSetupStartCommandInput, parseSetupCommandResultInput, parseSetupHeartbeatInput, parseSetupReconcileInput, parseSetupSampleReadInput, parseSetupSampleReadResult, parseSetupSourceInfoInput, parseSetupSourceInfo, parseSetupSourcePlanInput, parseSetupSourceUploadInput, } from './setup-wire.js';
|
|
6
|
+
export class BrowserSetupUnavailable extends SetupError {
|
|
7
|
+
constructor() { super('browser_setup_unavailable', 'This API has not enabled browser setup v1. Continue with the existing terminal wizard.'); }
|
|
8
|
+
}
|
|
9
|
+
const terminal = new Set(['complete', 'cancelled', 'expired']);
|
|
10
|
+
const retryable = new Set([429, 502, 503, 504]);
|
|
11
|
+
function retryableNetwork(error) {
|
|
12
|
+
if (!(error instanceof Error))
|
|
13
|
+
return false;
|
|
14
|
+
const code = error.code ?? error.cause?.code;
|
|
15
|
+
if (code && !['ECONNRESET', 'ETIMEDOUT', 'EAI_AGAIN', 'ECONNREFUSED', 'ENETUNREACH', 'UND_ERR_CONNECT_TIMEOUT'].includes(code))
|
|
16
|
+
return false;
|
|
17
|
+
return error.name === 'TimeoutError' || error.name === 'AbortError' || error instanceof TypeError && error.message === 'fetch failed';
|
|
18
|
+
}
|
|
19
|
+
const failure = (status, data) => {
|
|
20
|
+
const code = typeof data === 'object' && data !== null ? data.error : null;
|
|
21
|
+
const nested = typeof code === 'object' && code !== null ? code.code : code;
|
|
22
|
+
const safe = typeof nested === 'string' && /^[a-z][a-z0-9_]{0,79}$/.test(nested) ? nested : 'setup_request_failed';
|
|
23
|
+
return new SetupError(safe, `Setup request failed (${status}, ${safe}). Resume this setup or review its status; no general CLI login was changed.`);
|
|
24
|
+
};
|
|
25
|
+
async function json(response) {
|
|
26
|
+
if (!response.headers.get('content-type')?.toLowerCase().startsWith('application/json') || !response.body)
|
|
27
|
+
throw new SetupError('invalid_setup_response', 'The setup service returned an invalid response.');
|
|
28
|
+
const reader = response.body.getReader(), chunks = [];
|
|
29
|
+
let size = 0;
|
|
30
|
+
try {
|
|
31
|
+
while (true) {
|
|
32
|
+
const item = await reader.read();
|
|
33
|
+
if (item.done)
|
|
34
|
+
break;
|
|
35
|
+
size += item.value.byteLength;
|
|
36
|
+
if (size > 65_536) {
|
|
37
|
+
await reader.cancel();
|
|
38
|
+
throw new SetupError('setup_response_too_large', 'The setup response exceeded 64 KiB.');
|
|
39
|
+
}
|
|
40
|
+
chunks.push(item.value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
reader.releaseLock();
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new SetupError('invalid_setup_response', 'The setup service returned invalid JSON.');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function delay(ms, options) {
|
|
54
|
+
options.signal?.throwIfAborted();
|
|
55
|
+
if (options.sleep) {
|
|
56
|
+
await options.sleep(ms);
|
|
57
|
+
options.signal?.throwIfAborted();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await new Promise((resolve, reject) => {
|
|
61
|
+
const timer = setTimeout(() => { options.signal?.removeEventListener('abort', aborted); resolve(); }, ms);
|
|
62
|
+
const aborted = () => { clearTimeout(timer); reject(options.signal?.reason ?? new Error('Setup cancelled')); };
|
|
63
|
+
options.signal?.addEventListener('abort', aborted, { once: true });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function request(origin, operation, body, token, options, retries = 0) {
|
|
67
|
+
if (!['discovery', 'start', 'exchange', 'poll', 'plan', 'claim', 'start-command', 'report', 'heartbeat', 'reconcile', 'sample-read', 'source-info', 'source-plan', 'source-upload'].includes(operation))
|
|
68
|
+
throw new SetupError('invalid_setup_operation', 'Unsupported setup operation.');
|
|
69
|
+
const encoded = body === undefined ? undefined : JSON.stringify(body);
|
|
70
|
+
if (encoded && Buffer.byteLength(encoded) > 65_536)
|
|
71
|
+
throw new SetupError('setup_request_too_large', 'Setup request exceeds 64 KiB.');
|
|
72
|
+
const url = `${apiUrl(origin)}${operation === 'discovery' ? '/onboarding/v1/auth-config?setup=1' : `/onboarding/v1/setup/${operation}`}`;
|
|
73
|
+
// Preference reads are optional and bounded. Any read failure suppresses
|
|
74
|
+
// measurement, while the core setup request still proceeds normally.
|
|
75
|
+
const measurementDisabled = options.measurementDisabled === true || !await telemetryEnabled().catch(() => false);
|
|
76
|
+
for (let attempt = 0;; attempt++) {
|
|
77
|
+
options.signal?.throwIfAborted();
|
|
78
|
+
let response;
|
|
79
|
+
try {
|
|
80
|
+
response = await (options.fetcher ?? fetch)(url, {
|
|
81
|
+
method: encoded === undefined ? 'GET' : 'POST', redirect: 'error',
|
|
82
|
+
headers: { Accept: 'application/json', ...(encoded === undefined ? {} : { 'Content-Type': 'application/json' }), ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(measurementDisabled ? { 'x-ur-measurement-disabled': '1' } : {}) },
|
|
83
|
+
...(encoded === undefined ? {} : { body: encoded }),
|
|
84
|
+
signal: AbortSignal.any([AbortSignal.timeout(operation === 'source-upload' ? 50_000 : 10_000), ...(options.signal ? [options.signal] : [])]),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
options.signal?.throwIfAborted();
|
|
89
|
+
if (attempt < retries && retryableNetwork(error)) {
|
|
90
|
+
options.onProgress?.('retrying');
|
|
91
|
+
await delay(Math.min(15_000, 2_000 * 2 ** attempt), options);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
throw new SetupError('setup_network_failed', 'Unable to reach the setup service. Retry this setup; local results and ordinary CLI credentials are unchanged.');
|
|
95
|
+
}
|
|
96
|
+
if (!response.ok && retryable.has(response.status) && attempt < retries) {
|
|
97
|
+
await response.body?.cancel();
|
|
98
|
+
options.onProgress?.('retrying');
|
|
99
|
+
await delay(Math.min(15_000, 2_000 * 2 ** attempt), options);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const data = await json(response);
|
|
103
|
+
if (!response.ok)
|
|
104
|
+
throw failure(response.status, data);
|
|
105
|
+
return data;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export async function discoverBrowserSetup(origin, options = {}) {
|
|
109
|
+
const data = await request(origin, 'discovery', undefined, undefined, options, 2);
|
|
110
|
+
// Released servers ignore ?setup=1 and return old auth-config. Only that exact
|
|
111
|
+
// legacy shape is unsupported; malformed claimed protocol data is an error.
|
|
112
|
+
if (data && typeof data === 'object' && !Array.isArray(data) && !('browser_setup' in data) && 'client_id' in data) {
|
|
113
|
+
const old = data;
|
|
114
|
+
let valid = false;
|
|
115
|
+
try {
|
|
116
|
+
valid = Object.keys(old).every(key => ['client_id', 'authkit_domain', 'environment'].includes(key)) &&
|
|
117
|
+
typeof old.client_id === 'string' && /^client_[A-Za-z0-9]{1,230}$/.test(old.client_id) &&
|
|
118
|
+
typeof old.authkit_domain === 'string' && apiUrl(old.authkit_domain) === old.authkit_domain && old.authkit_domain.startsWith('https://') &&
|
|
119
|
+
['production', 'development', 'local'].includes(String(old.environment));
|
|
120
|
+
}
|
|
121
|
+
catch { /* Invalid old metadata is not a supported legacy response. */ }
|
|
122
|
+
if (valid)
|
|
123
|
+
throw new BrowserSetupUnavailable();
|
|
124
|
+
}
|
|
125
|
+
const discovery = parseSetupDiscovery(data);
|
|
126
|
+
if (!discovery.browser_setup.enabled)
|
|
127
|
+
throw new BrowserSetupUnavailable();
|
|
128
|
+
return discovery;
|
|
129
|
+
}
|
|
130
|
+
function browserOrigin(origin, configured) {
|
|
131
|
+
if (configured)
|
|
132
|
+
return apiUrl(configured, 'Setup browser origin');
|
|
133
|
+
if (origin === 'https://api.userreason.com')
|
|
134
|
+
return 'https://dash.userreason.com';
|
|
135
|
+
if (origin === 'https://api-dev.userreason.com')
|
|
136
|
+
return 'https://dash-dev.userreason.com';
|
|
137
|
+
throw new SetupError('setup_browser_origin_required', 'Configure a trusted dashboard origin for this nonstandard API before connecting browser setup.');
|
|
138
|
+
}
|
|
139
|
+
export function validateSetupBrowserUrl(value, expectedOrigin, challenge, now) {
|
|
140
|
+
let target;
|
|
141
|
+
try {
|
|
142
|
+
target = new URL(value.browser_url);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
throw new SetupError('invalid_setup_response', 'Invalid setup browser URL.');
|
|
146
|
+
}
|
|
147
|
+
const query = [...target.searchParams], fragment = [...new URLSearchParams(target.hash.slice(1))];
|
|
148
|
+
const expiry = Date.parse(value.expires_at);
|
|
149
|
+
if (target.origin !== apiUrl(expectedOrigin) || target.username || target.password || target.pathname !== '/install' ||
|
|
150
|
+
query.length !== 1 || query[0]?.[0] !== 'setup' || query[0]?.[1] !== value.setup_id ||
|
|
151
|
+
fragment.length !== 1 || fragment[0]?.[0] !== 'request' || fragment[0]?.[1] !== value.ticket ||
|
|
152
|
+
expiry <= now || expiry > now + 901_000 || value.code !== challenge.slice(0, 8).toUpperCase())
|
|
153
|
+
throw new SetupError('invalid_setup_response', 'Invalid setup browser URL, verification code or expiry.');
|
|
154
|
+
return target.href;
|
|
155
|
+
}
|
|
156
|
+
/** The handshake never installs packages, edits source or grants management access. */
|
|
157
|
+
export async function connectBrowserSetup(origin, input, onBrowser, options = {}) {
|
|
158
|
+
origin = apiUrl(origin);
|
|
159
|
+
const trustedBrowser = browserOrigin(origin, input.browserOrigin);
|
|
160
|
+
await discoverBrowserSetup(origin, options);
|
|
161
|
+
const verifier = randomBytes(32).toString('hex'), challenge = createHash('sha256').update(verifier).digest('hex');
|
|
162
|
+
const fingerprint = await setupProjectFingerprint(input.directory), now = options.now ?? Date.now;
|
|
163
|
+
const started = parseSetupStartResult(await request(origin, 'start', parseSetupStartInput({ schema_version: 1, challenge, project_fingerprint: fingerprint, capabilities: parseSetupCapabilities(input.capabilities) }), undefined, options));
|
|
164
|
+
const url = validateSetupBrowserUrl(started, trustedBrowser, challenge, now());
|
|
165
|
+
const pending = { schema_version: 1, state: 'pending', setup_id: started.setup_id, api_url: origin, project_fingerprint: fingerprint, created_at: new Date(now()).toISOString(), expires_at: started.expires_at, ticket: started.ticket, verifier };
|
|
166
|
+
await saveSetupSession(pending);
|
|
167
|
+
await onBrowser(url, started.code);
|
|
168
|
+
return finishBrowserSetup(pending, options);
|
|
169
|
+
}
|
|
170
|
+
async function finishBrowserSetup(pending, options) {
|
|
171
|
+
const now = options.now ?? Date.now, deadline = Math.min(Date.parse(pending.expires_at), now() + 600_000);
|
|
172
|
+
let attempts = 0;
|
|
173
|
+
while (now() < deadline && attempts++ < 301) {
|
|
174
|
+
const result = parseSetupExchangeResult(await request(pending.api_url, 'exchange', parseSetupExchangeInput({ schema_version: 1, setup_id: pending.setup_id, ticket: pending.ticket, verifier: pending.verifier }), undefined, options, 2));
|
|
175
|
+
if (result.status === 'rejected')
|
|
176
|
+
throw new SetupError('setup_rejected', 'Browser setup was declined. Existing installations, drafts and CLI credentials are unchanged.');
|
|
177
|
+
if (result.status === 'approved') {
|
|
178
|
+
if (result.session.setup_id !== pending.setup_id || result.session.project_fingerprint !== pending.project_fingerprint ||
|
|
179
|
+
Date.parse(result.expires_at) <= now() || Date.parse(result.expires_at) > now() + 7_201_000 ||
|
|
180
|
+
Date.parse(result.expires_at) > Date.parse(result.session.absolute_expires_at) || terminal.has(result.session.phase) || !/^ur-setup\.v1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(result.access_token))
|
|
181
|
+
throw new SetupError('invalid_setup_response', 'The approved setup response does not match this project or active session.');
|
|
182
|
+
const connected = { schema_version: 1, state: 'connected', setup_id: pending.setup_id, api_url: pending.api_url, project_fingerprint: pending.project_fingerprint, created_at: new Date(now()).toISOString(), expires_at: result.expires_at, access_token: result.access_token };
|
|
183
|
+
await saveSetupSession(connected);
|
|
184
|
+
return { client: new BrowserSetupClient(connected, options), session: result.session };
|
|
185
|
+
}
|
|
186
|
+
options.onProgress?.('waiting');
|
|
187
|
+
await delay(Math.max(0, Math.min(2_000, deadline - now())), options);
|
|
188
|
+
}
|
|
189
|
+
throw new SetupError('setup_expired', 'Browser setup approval timed out. Resume the same unexpired setup or start a new one.');
|
|
190
|
+
}
|
|
191
|
+
export async function resumeBrowserSetup(origin, setupId, directory, options = {}) {
|
|
192
|
+
const saved = await loadSetupSession(origin, setupId, directory, (options.now ?? Date.now)());
|
|
193
|
+
if (saved.state === 'pending')
|
|
194
|
+
return finishBrowserSetup(saved, options);
|
|
195
|
+
const client = new BrowserSetupClient(saved, options);
|
|
196
|
+
return { client, session: await client.poll() };
|
|
197
|
+
}
|
|
198
|
+
export class BrowserSetupClient {
|
|
199
|
+
#stored;
|
|
200
|
+
#options;
|
|
201
|
+
constructor(stored, options = {}) {
|
|
202
|
+
const valid = parseStoredSetup(stored);
|
|
203
|
+
if (valid.state !== 'connected')
|
|
204
|
+
throw new SetupError('invalid_setup_session', 'Expected a project-scoped setup capability, never a general CLI login.');
|
|
205
|
+
this.#stored = { ...valid };
|
|
206
|
+
this.#options = options;
|
|
207
|
+
}
|
|
208
|
+
/** Nonsecret identity for the local executor journal; never exposes the bearer. */
|
|
209
|
+
get identity() { const { setup_id, api_url, project_fingerprint, expires_at } = this.#stored; return { setup_id, api_url, project_fingerprint, expires_at }; }
|
|
210
|
+
async call(operation, body, retries = 0) {
|
|
211
|
+
if (Date.parse(this.#stored.expires_at) <= (this.#options.now ?? Date.now)())
|
|
212
|
+
throw new SetupError('setup_expired', 'Setup access expired. Start a new setup; it cannot be refreshed into ordinary CLI access.');
|
|
213
|
+
return request(this.#stored.api_url, operation, body, this.#stored.access_token, this.#options, retries);
|
|
214
|
+
}
|
|
215
|
+
scoped(input) {
|
|
216
|
+
const value = input;
|
|
217
|
+
if (value.setup_id !== this.#stored.setup_id || (value.project_fingerprint && value.project_fingerprint !== this.#stored.project_fingerprint) ||
|
|
218
|
+
(value.plan?.project_fingerprint && value.plan.project_fingerprint !== this.#stored.project_fingerprint))
|
|
219
|
+
throw new SetupError('setup_binding_mismatch', 'Setup operation targets another project or session.');
|
|
220
|
+
}
|
|
221
|
+
view(value) {
|
|
222
|
+
const session = parseSetupSession(value);
|
|
223
|
+
this.scoped(session);
|
|
224
|
+
return session;
|
|
225
|
+
}
|
|
226
|
+
async poll() { return this.view(await this.call('poll', parseSetupPollInput({ schema_version: 1, setup_id: this.#stored.setup_id }), 2)); }
|
|
227
|
+
async discovery() { return discoverBrowserSetup(this.#stored.api_url, this.#options); }
|
|
228
|
+
async sourceInfo() {
|
|
229
|
+
const result = parseSetupSourceInfo(await this.call('source-info', parseSetupSourceInfoInput({ schema_version: 1, setup_id: this.#stored.setup_id }), 2));
|
|
230
|
+
this.scoped(result.session);
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
async sourcePlan(input) { const body = parseSetupSourcePlanInput(input); this.scoped(body); return this.view(await this.call('source-plan', body)); }
|
|
234
|
+
async sourceUpload(input) { const body = parseSetupSourceUploadInput(input); this.scoped(body); return this.view(await this.call('source-upload', body)); }
|
|
235
|
+
async plan(input) { const body = parseSetupLocalPlanInput(input); this.scoped(body); return this.view(await this.call('plan', body)); }
|
|
236
|
+
async claim(input) {
|
|
237
|
+
const body = parseSetupClaimInput(input);
|
|
238
|
+
this.scoped(body);
|
|
239
|
+
const result = parseSetupClaimResult(await this.call('claim', body));
|
|
240
|
+
this.scoped(result.session);
|
|
241
|
+
if (result.command && result.command.executor_generation !== body.executor_generation)
|
|
242
|
+
throw new SetupError('invalid_setup_response', 'Claimed setup command has the wrong executor generation.');
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
async startCommand(input) { const body = parseSetupStartCommandInput(input); this.scoped(body); return this.view(await this.call('start-command', body)); }
|
|
246
|
+
async report(input) { const body = parseSetupCommandResultInput(input); this.scoped(body); return this.view(await this.call('report', body)); }
|
|
247
|
+
async heartbeat(input) { const body = parseSetupHeartbeatInput(input); this.scoped(body); return this.view(await this.call('heartbeat', body, 2)); }
|
|
248
|
+
async reconcile(input) { const body = parseSetupReconcileInput(input); this.scoped(body); return this.view(await this.call('reconcile', body)); }
|
|
249
|
+
async sample(requestId = randomUUID()) {
|
|
250
|
+
const active = await this.poll();
|
|
251
|
+
if (active.phase !== 'agent_test' || !['waiting', 'received'].includes(active.sample.status))
|
|
252
|
+
throw new SetupError('setup_sample_unavailable', 'This setup is not waiting for its optional sample read. Open that step in the browser first.');
|
|
253
|
+
const body = parseSetupSampleReadInput({ schema_version: 1, setup_id: this.#stored.setup_id, request_id: requestId });
|
|
254
|
+
const result = parseSetupSampleReadResult(await this.call('sample-read', body, 2));
|
|
255
|
+
this.scoped(result);
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { UserReasonClient } from './client.js';
|
|
2
|
+
import { type FirstSurveyPreparation } from './first-survey-preparation.js';
|
|
3
|
+
import { type FirstSurveyUI } from './first-survey.js';
|
|
4
|
+
export type StarterGoal = 'improve' | 'friction' | 'satisfaction';
|
|
5
|
+
type Definition = Record<string, unknown>;
|
|
6
|
+
export declare function starterDefinition(goal: StarterGoal, product: string, focus?: string): Definition;
|
|
7
|
+
/** Guided draft creation. Templates work without AI, source consent or provider availability. */
|
|
8
|
+
export declare function starterSurvey(client: UserReasonClient, root: string, workspace: string, site: string, ui: FirstSurveyUI, options?: {
|
|
9
|
+
product?: string;
|
|
10
|
+
preparation?: FirstSurveyPreparation;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
state: string;
|
|
13
|
+
prompt_id: string;
|
|
14
|
+
result: unknown;
|
|
15
|
+
} | {
|
|
16
|
+
state: string;
|
|
17
|
+
goal?: undefined;
|
|
18
|
+
} | {
|
|
19
|
+
state: string;
|
|
20
|
+
goal: StarterGoal;
|
|
21
|
+
}>;
|
|
22
|
+
export {};
|