setup-agent 0.0.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 +38 -0
- package/bin/setup-agent.js +4 -0
- package/dist/cli.d.ts +64 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +636 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +26 -0
- package/dist/client.js.map +1 -0
- package/dist/host.d.ts +12 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +291 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/presets.d.ts +32 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +175 -0
- package/dist/presets.js.map +1 -0
- package/dist/protocol.d.ts +289 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +184 -0
- package/dist/protocol.js.map +1 -0
- package/package.json +60 -0
- package/src/cli.ts +799 -0
- package/src/client.ts +35 -0
- package/src/host.ts +435 -0
- package/src/index.ts +26 -0
- package/src/presets.ts +185 -0
- package/src/protocol.ts +479 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { StatewireClient, StatewireHttp } from "statewire";
|
|
2
|
+
import type { Checkout } from "./protocol";
|
|
3
|
+
|
|
4
|
+
export type CheckoutClient = StatewireClient<
|
|
5
|
+
Checkout.State | undefined,
|
|
6
|
+
Checkout.Commands
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
/** Attach to a checkout by its base URL and resolve once its state has arrived. */
|
|
10
|
+
export const connectCheckout = (url: string): Promise<CheckoutClient> =>
|
|
11
|
+
new Promise((resolve, reject) => {
|
|
12
|
+
const client: CheckoutClient = new StatewireClient({
|
|
13
|
+
transport: StatewireHttp({ url }),
|
|
14
|
+
onError: (error) => reject(error),
|
|
15
|
+
});
|
|
16
|
+
const settle = () => {
|
|
17
|
+
if (client.state === undefined) {
|
|
18
|
+
if (client.connection.status !== "stopped") return;
|
|
19
|
+
unsubscribe();
|
|
20
|
+
client.dispose();
|
|
21
|
+
reject(
|
|
22
|
+
new Error(
|
|
23
|
+
`checkout unreachable: ${client.connection.reason}${
|
|
24
|
+
client.connection.message ? ` (${client.connection.message})` : ""
|
|
25
|
+
}`,
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
unsubscribe();
|
|
31
|
+
resolve(client);
|
|
32
|
+
};
|
|
33
|
+
const unsubscribe = client.subscribe(settle);
|
|
34
|
+
settle();
|
|
35
|
+
});
|
package/src/host.ts
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { resource } from "@assistant-ui/tap";
|
|
2
|
+
import {
|
|
3
|
+
StatewireReject,
|
|
4
|
+
getStateHost,
|
|
5
|
+
useStatewireCommands,
|
|
6
|
+
useStatewireState,
|
|
7
|
+
} from "statewire/host";
|
|
8
|
+
import {
|
|
9
|
+
classifyChoiceAnswer,
|
|
10
|
+
currentPlan,
|
|
11
|
+
initialCheckoutState,
|
|
12
|
+
isAgentPresent,
|
|
13
|
+
isClosed,
|
|
14
|
+
isValidModelAnswer,
|
|
15
|
+
parsePreviewUrl,
|
|
16
|
+
} from "./protocol";
|
|
17
|
+
import type { Checkout } from "./protocol";
|
|
18
|
+
|
|
19
|
+
const reject = (reason: Checkout.RejectionReason, message: string) =>
|
|
20
|
+
new StatewireReject(message, { payload: { reason } });
|
|
21
|
+
|
|
22
|
+
const useCheckoutHost = (restored: unknown) => {
|
|
23
|
+
const [state] = useStatewireState<Checkout.State>(() => {
|
|
24
|
+
const initial = initialCheckoutState();
|
|
25
|
+
if (restored === undefined) return initial;
|
|
26
|
+
const snapshot = restored as Partial<Checkout.State>;
|
|
27
|
+
if (snapshot.version !== 2) return initial;
|
|
28
|
+
return {
|
|
29
|
+
...initial,
|
|
30
|
+
...snapshot,
|
|
31
|
+
agent: { ...initial.agent, ...snapshot.agent },
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const requireOpen = () => {
|
|
36
|
+
if (state.createdAt === null) {
|
|
37
|
+
throw reject("not-created", "the checkout has not been created yet");
|
|
38
|
+
}
|
|
39
|
+
if (isClosed(state)) {
|
|
40
|
+
throw reject("closed", `the checkout is ${state.status}`);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const requireStarted = () => {
|
|
45
|
+
requireOpen();
|
|
46
|
+
if (state.status === "waiting") {
|
|
47
|
+
throw reject(
|
|
48
|
+
"planning-not-started",
|
|
49
|
+
'ask the user to return to their browser and click "Begin plan"; wait with "wait start"',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const requireInstalling = () => {
|
|
55
|
+
requireOpen();
|
|
56
|
+
if (state.status !== "installing") {
|
|
57
|
+
throw reject(
|
|
58
|
+
"plan-required",
|
|
59
|
+
"steps run once the user has approved a plan; submit one with plan",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const findStep = (stepId: string) => {
|
|
65
|
+
const step = state.steps.find((candidate) => candidate.id === stepId);
|
|
66
|
+
if (!step) throw reject("unknown-step", `no step "${stepId}"`);
|
|
67
|
+
return step;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const findInput = (inputId: string) => {
|
|
71
|
+
const input = state.inputs.find((candidate) => candidate.id === inputId);
|
|
72
|
+
if (!input) throw reject("unknown-input", `no input "${inputId}"`);
|
|
73
|
+
if (input.status !== "open") {
|
|
74
|
+
throw reject("input-closed", `input "${inputId}" is ${input.status}`);
|
|
75
|
+
}
|
|
76
|
+
return input;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const touchAgent = () => {
|
|
80
|
+
state.agent.lastSeenAt = Date.now();
|
|
81
|
+
state.agent.connected = true;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const ask = (seed: Checkout.InputSeed & { stepId?: string }) => {
|
|
85
|
+
const kind = seed.kind ?? "text";
|
|
86
|
+
if (kind === "product") {
|
|
87
|
+
if (!seed.product) {
|
|
88
|
+
throw reject("invalid-input", "a product input names a product");
|
|
89
|
+
}
|
|
90
|
+
if (state.products.some((product) => product.slug === seed.product)) {
|
|
91
|
+
throw reject(
|
|
92
|
+
"invalid-input",
|
|
93
|
+
`"${seed.product}" is already part of this checkout`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
} else if (kind !== "text" && !seed.options?.length) {
|
|
97
|
+
throw reject("invalid-input", `a ${kind} input needs options`);
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
seed.multiple &&
|
|
101
|
+
(kind !== "choice" || seed.options?.some((option) => option.variants))
|
|
102
|
+
) {
|
|
103
|
+
throw reject(
|
|
104
|
+
"invalid-input",
|
|
105
|
+
"only a choice whose options have no variants accepts several answers",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (
|
|
109
|
+
seed.default !== undefined &&
|
|
110
|
+
!seed.options?.some((option) => option.id === seed.default)
|
|
111
|
+
) {
|
|
112
|
+
throw reject(
|
|
113
|
+
"invalid-input",
|
|
114
|
+
`no option "${seed.default}" to default to`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (seed.stepId !== undefined) findStep(seed.stepId);
|
|
118
|
+
const id = `q${state.inputs.length + 1}`;
|
|
119
|
+
const stepId =
|
|
120
|
+
seed.stepId ?? state.steps.find((step) => step.status === "active")?.id;
|
|
121
|
+
state.inputs.push({
|
|
122
|
+
id,
|
|
123
|
+
phase: state.status,
|
|
124
|
+
kind,
|
|
125
|
+
...(seed.preset !== undefined && { preset: seed.preset }),
|
|
126
|
+
prompt: seed.prompt,
|
|
127
|
+
...(seed.placeholder !== undefined && { placeholder: seed.placeholder }),
|
|
128
|
+
...(seed.options !== undefined && { options: seed.options }),
|
|
129
|
+
...(seed.multiple && { multiple: true }),
|
|
130
|
+
...(kind === "product" && { product: seed.product }),
|
|
131
|
+
...(seed.default !== undefined && { default: seed.default }),
|
|
132
|
+
...(seed.help !== undefined && { help: seed.help }),
|
|
133
|
+
optional: seed.optional ?? false,
|
|
134
|
+
status: "open",
|
|
135
|
+
...(stepId !== undefined && { stepId }),
|
|
136
|
+
createdAt: Date.now(),
|
|
137
|
+
});
|
|
138
|
+
return { inputId: id };
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const dismissOpenInputs = () => {
|
|
142
|
+
for (const input of state.inputs) {
|
|
143
|
+
if (input.status === "open") {
|
|
144
|
+
input.status = "dismissed";
|
|
145
|
+
input.answeredAt = Date.now();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const log = (text: string, stepId?: string) => {
|
|
151
|
+
state.log.push({
|
|
152
|
+
id: `l${state.log.length + 1}`,
|
|
153
|
+
role: "agent",
|
|
154
|
+
phase: state.status,
|
|
155
|
+
at: Date.now(),
|
|
156
|
+
text,
|
|
157
|
+
...(stepId !== undefined && { stepId }),
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const updateStep = (
|
|
162
|
+
step: Checkout.Step,
|
|
163
|
+
status: Checkout.StepStatus,
|
|
164
|
+
note?: string,
|
|
165
|
+
) => {
|
|
166
|
+
const changed = step.status !== status;
|
|
167
|
+
step.status = status;
|
|
168
|
+
if (note !== undefined) step.note = note;
|
|
169
|
+
if (changed && (status === "done" || status === "skipped")) {
|
|
170
|
+
log(
|
|
171
|
+
`${status === "done" ? "Completed" : "Skipped"}: ${step.title}${step.note ? `\n\n${step.note}` : ""}`,
|
|
172
|
+
step.id,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const commands = useStatewireCommands<Checkout.Commands>({
|
|
178
|
+
"checkout/create": ({ id, products, instructions }) => {
|
|
179
|
+
if (state.id === id) {
|
|
180
|
+
throw reject("already-created", "the checkout already exists");
|
|
181
|
+
}
|
|
182
|
+
const fresh = initialCheckoutState();
|
|
183
|
+
if (!isAgentPresent(state)) state.agent = fresh.agent;
|
|
184
|
+
state.id = id;
|
|
185
|
+
state.status = fresh.status;
|
|
186
|
+
delete state.completion;
|
|
187
|
+
state.plans = fresh.plans;
|
|
188
|
+
state.steps = fresh.steps;
|
|
189
|
+
state.inputs = fresh.inputs;
|
|
190
|
+
state.log = fresh.log;
|
|
191
|
+
state.createdAt = Date.now();
|
|
192
|
+
state.instructions = instructions?.trim() ?? "";
|
|
193
|
+
state.products = products.map((product) => ({
|
|
194
|
+
slug: product.slug,
|
|
195
|
+
name: product.name,
|
|
196
|
+
...(product.guide !== undefined && { guide: product.guide }),
|
|
197
|
+
}));
|
|
198
|
+
},
|
|
199
|
+
"checkout/begin-plan": () => {
|
|
200
|
+
requireOpen();
|
|
201
|
+
if (state.status === "waiting") state.status = "planning";
|
|
202
|
+
},
|
|
203
|
+
"checkout/answer": ({ inputId, answer, note }) => {
|
|
204
|
+
requireOpen();
|
|
205
|
+
const input = findInput(inputId);
|
|
206
|
+
if (
|
|
207
|
+
input.kind === "choice" &&
|
|
208
|
+
classifyChoiceAnswer(input, answer) === "invalid"
|
|
209
|
+
) {
|
|
210
|
+
throw reject(
|
|
211
|
+
"invalid-answer",
|
|
212
|
+
input.multiple
|
|
213
|
+
? `"${answer}" is not a JSON array of option ids`
|
|
214
|
+
: `"${answer}" names an option without a valid variant`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (input.kind === "model" && !isValidModelAnswer(input, answer)) {
|
|
218
|
+
throw reject(
|
|
219
|
+
"invalid-answer",
|
|
220
|
+
"a model answer is JSON with provider and model",
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (input.kind === "product") {
|
|
224
|
+
throw reject(
|
|
225
|
+
"invalid-answer",
|
|
226
|
+
"a product input is answered with checkout/add-product",
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (input.kind === "text" && answer.trim() === "") {
|
|
230
|
+
throw reject("invalid-answer", "an answer cannot be empty");
|
|
231
|
+
}
|
|
232
|
+
input.status = "answered";
|
|
233
|
+
input.answeredAt = Date.now();
|
|
234
|
+
input.answer = answer;
|
|
235
|
+
if (note !== undefined && note.trim() !== "") input.note = note.trim();
|
|
236
|
+
},
|
|
237
|
+
"checkout/add-product": ({ inputId, product }) => {
|
|
238
|
+
requireOpen();
|
|
239
|
+
const input = findInput(inputId);
|
|
240
|
+
if (input.kind !== "product" || input.product !== product.slug) {
|
|
241
|
+
throw reject(
|
|
242
|
+
"invalid-answer",
|
|
243
|
+
`input "${inputId}" does not propose "${product.slug}"`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (!state.products.some((entry) => entry.slug === product.slug)) {
|
|
247
|
+
state.products.push({
|
|
248
|
+
slug: product.slug,
|
|
249
|
+
name: product.name,
|
|
250
|
+
...(product.guide !== undefined && { guide: product.guide }),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
input.status = "answered";
|
|
254
|
+
input.answer = "added";
|
|
255
|
+
input.answeredAt = Date.now();
|
|
256
|
+
},
|
|
257
|
+
"checkout/message": ({ text }) => {
|
|
258
|
+
requireOpen();
|
|
259
|
+
if (text.trim() === "")
|
|
260
|
+
throw reject("empty-message", "a message cannot be empty");
|
|
261
|
+
const stepId = state.steps.find((step) => step.status === "active")?.id;
|
|
262
|
+
state.log.push({
|
|
263
|
+
id: `l${state.log.length + 1}`,
|
|
264
|
+
role: "user",
|
|
265
|
+
phase: state.status,
|
|
266
|
+
...(stepId !== undefined && { stepId }),
|
|
267
|
+
at: Date.now(),
|
|
268
|
+
text: text.trim(),
|
|
269
|
+
});
|
|
270
|
+
},
|
|
271
|
+
"agent/ack": ({ messageId }) => {
|
|
272
|
+
requireOpen();
|
|
273
|
+
const message = state.log.find(
|
|
274
|
+
(entry) => entry.id === messageId && entry.role === "user",
|
|
275
|
+
);
|
|
276
|
+
if (!message)
|
|
277
|
+
throw reject("unknown-message", `no user message "${messageId}"`);
|
|
278
|
+
message.acknowledgedAt ??= Date.now();
|
|
279
|
+
},
|
|
280
|
+
"checkout/dismiss": ({ inputId }) => {
|
|
281
|
+
requireOpen();
|
|
282
|
+
const input = findInput(inputId);
|
|
283
|
+
input.status = "dismissed";
|
|
284
|
+
input.answeredAt = Date.now();
|
|
285
|
+
},
|
|
286
|
+
"checkout/plan": (params) => {
|
|
287
|
+
requireOpen();
|
|
288
|
+
const plan = currentPlan(state);
|
|
289
|
+
if (plan === undefined) {
|
|
290
|
+
throw reject("no-plan", "the agent has not proposed a plan yet");
|
|
291
|
+
}
|
|
292
|
+
if (plan.status !== "proposed") {
|
|
293
|
+
throw reject("plan-decided", `the plan is already ${plan.status}`);
|
|
294
|
+
}
|
|
295
|
+
if (params.decision === "approve") {
|
|
296
|
+
plan.status = "approved";
|
|
297
|
+
plan.decidedAt = Date.now();
|
|
298
|
+
state.status = "installing";
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (params.feedback.trim() === "") {
|
|
302
|
+
throw reject("feedback-required", "say what should change");
|
|
303
|
+
}
|
|
304
|
+
plan.status = "changes-requested";
|
|
305
|
+
plan.decidedAt = Date.now();
|
|
306
|
+
plan.feedback = params.feedback.trim();
|
|
307
|
+
},
|
|
308
|
+
"checkout/cancel": () => {
|
|
309
|
+
if (state.status === "cancelled") return;
|
|
310
|
+
state.status = "cancelled";
|
|
311
|
+
dismissOpenInputs();
|
|
312
|
+
},
|
|
313
|
+
"checkout/finish": () => {
|
|
314
|
+
requireOpen();
|
|
315
|
+
if (state.completion === undefined) {
|
|
316
|
+
throw reject(
|
|
317
|
+
"finish-not-proposed",
|
|
318
|
+
"the agent has not proposed to finish yet",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
dismissOpenInputs();
|
|
322
|
+
state.status = "done";
|
|
323
|
+
log("Setup complete.");
|
|
324
|
+
},
|
|
325
|
+
"agent/intro": ({ kind } = {}) => {
|
|
326
|
+
requireOpen();
|
|
327
|
+
if (state.agent.introducedAt === null) {
|
|
328
|
+
state.agent.introducedAt = Date.now();
|
|
329
|
+
}
|
|
330
|
+
if (kind !== undefined) state.agent.kind = kind;
|
|
331
|
+
},
|
|
332
|
+
"agent/hello": ({ cwd, kind } = {}) => {
|
|
333
|
+
touchAgent();
|
|
334
|
+
if (cwd !== undefined) state.agent.cwd = cwd;
|
|
335
|
+
if (kind !== undefined) state.agent.kind = kind;
|
|
336
|
+
},
|
|
337
|
+
"agent/heartbeat": () => {
|
|
338
|
+
touchAgent();
|
|
339
|
+
},
|
|
340
|
+
"agent/bye": () => {
|
|
341
|
+
state.agent.connected = false;
|
|
342
|
+
},
|
|
343
|
+
"agent/plan": ({ markdown }) => {
|
|
344
|
+
requireStarted();
|
|
345
|
+
touchAgent();
|
|
346
|
+
if (markdown.trim() === "") {
|
|
347
|
+
throw reject("empty-plan", "the plan is empty");
|
|
348
|
+
}
|
|
349
|
+
if (state.status === "installing") {
|
|
350
|
+
throw reject("plan-decided", "the plan is already approved");
|
|
351
|
+
}
|
|
352
|
+
const revision = state.plans.length + 1;
|
|
353
|
+
state.plans.push({
|
|
354
|
+
revision,
|
|
355
|
+
markdown: markdown.trim(),
|
|
356
|
+
status: "proposed",
|
|
357
|
+
submittedAt: Date.now(),
|
|
358
|
+
});
|
|
359
|
+
return { revision };
|
|
360
|
+
},
|
|
361
|
+
"agent/add-step": ({ title, detail, product, active }) => {
|
|
362
|
+
requireInstalling();
|
|
363
|
+
if (
|
|
364
|
+
product !== undefined &&
|
|
365
|
+
!state.products.some((candidate) => candidate.slug === product)
|
|
366
|
+
) {
|
|
367
|
+
throw reject("unknown-product", `no product "${product}"`);
|
|
368
|
+
}
|
|
369
|
+
const id = `s${state.steps.length + 1}`;
|
|
370
|
+
state.steps.push({
|
|
371
|
+
id,
|
|
372
|
+
title,
|
|
373
|
+
...(detail !== undefined && { detail }),
|
|
374
|
+
status: active ? "active" : "pending",
|
|
375
|
+
...(product !== undefined && { product }),
|
|
376
|
+
createdAt: Date.now(),
|
|
377
|
+
});
|
|
378
|
+
return { stepId: id };
|
|
379
|
+
},
|
|
380
|
+
"agent/step": ({ stepId, status, note }) => {
|
|
381
|
+
requireInstalling();
|
|
382
|
+
const step = findStep(stepId);
|
|
383
|
+
updateStep(step, status, note);
|
|
384
|
+
},
|
|
385
|
+
"agent/ask": (seed) => {
|
|
386
|
+
requireStarted();
|
|
387
|
+
touchAgent();
|
|
388
|
+
return ask(seed);
|
|
389
|
+
},
|
|
390
|
+
"agent/log": ({ text }) => {
|
|
391
|
+
requireOpen();
|
|
392
|
+
touchAgent();
|
|
393
|
+
const stepId = state.steps.find((step) => step.status === "active")?.id;
|
|
394
|
+
log(text, stepId);
|
|
395
|
+
},
|
|
396
|
+
"agent/done": ({ summary, preview } = {}) => {
|
|
397
|
+
requireInstalling();
|
|
398
|
+
const previewUrl = parsePreviewUrl(preview?.trim());
|
|
399
|
+
if (preview !== undefined && !previewUrl) {
|
|
400
|
+
throw reject(
|
|
401
|
+
"invalid-preview",
|
|
402
|
+
"a preview is an http URL on localhost, such as http://localhost:3000",
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
touchAgent();
|
|
406
|
+
for (const step of state.steps) {
|
|
407
|
+
if (step.status === "pending" || step.status === "active") {
|
|
408
|
+
updateStep(step, "done");
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
log(summary?.trim() || "Everything is installed.");
|
|
412
|
+
state.completion = {
|
|
413
|
+
proposedAt: Date.now(),
|
|
414
|
+
...(previewUrl && { preview: previewUrl.href }),
|
|
415
|
+
};
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const source = getStateHost(state)!;
|
|
420
|
+
return {
|
|
421
|
+
state,
|
|
422
|
+
commands,
|
|
423
|
+
snapshot: source.snapshot,
|
|
424
|
+
subscribe: (listener: () => void) => {
|
|
425
|
+
source.setOnChange(listener);
|
|
426
|
+
return () => source.setOnChange(null);
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
/** The checkout statewire host; pass the durable object's restored snapshot. */
|
|
432
|
+
export const CheckoutHost = (restored?: unknown) =>
|
|
433
|
+
resource(useCheckoutHost)(restored);
|
|
434
|
+
|
|
435
|
+
export type { Checkout } from "./protocol";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type { Checkout, OptionIcon } from "./protocol";
|
|
2
|
+
export {
|
|
3
|
+
AGENT_HEARTBEAT_MS,
|
|
4
|
+
AGENT_PRESENCE_MS,
|
|
5
|
+
OPTION_ICONS,
|
|
6
|
+
classifyChoiceAnswer,
|
|
7
|
+
currentPlan,
|
|
8
|
+
initialCheckoutState,
|
|
9
|
+
finishProposed,
|
|
10
|
+
followedUpSinceProposal,
|
|
11
|
+
isAgentPresent,
|
|
12
|
+
isClosed,
|
|
13
|
+
isOptionIcon,
|
|
14
|
+
isValidModelAnswer,
|
|
15
|
+
openInputs,
|
|
16
|
+
parseChoiceAnswer,
|
|
17
|
+
parseModelAnswer,
|
|
18
|
+
parseMultipleAnswer,
|
|
19
|
+
parsePreviewUrl,
|
|
20
|
+
planNeedsReview,
|
|
21
|
+
stepProgress,
|
|
22
|
+
} from "./protocol";
|
|
23
|
+
export { INPUT_PRESETS, isPresetId, presetInput } from "./presets";
|
|
24
|
+
export type { PresetId, PresetOverrides } from "./presets";
|
|
25
|
+
export { connectCheckout } from "./client";
|
|
26
|
+
export type { CheckoutClient } from "./client";
|
package/src/presets.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { Checkout } from "./protocol";
|
|
2
|
+
|
|
3
|
+
const TYPESCRIPT = { id: "typescript", label: "TypeScript" };
|
|
4
|
+
const PYTHON = { id: "python", label: "Python" };
|
|
5
|
+
|
|
6
|
+
const DOCS = "https://www.assistant-ui.com";
|
|
7
|
+
|
|
8
|
+
export type PresetId = "framework" | "llm-provider" | "project";
|
|
9
|
+
|
|
10
|
+
export type PresetOverrides = {
|
|
11
|
+
/** Restrict the options to these ids, in this order; one id locks the choice in. */
|
|
12
|
+
only?: readonly string[];
|
|
13
|
+
/** Options the agent found itself, listed before the preset's own. */
|
|
14
|
+
found?: readonly { id: string; description?: string }[];
|
|
15
|
+
default?: string;
|
|
16
|
+
optional?: boolean;
|
|
17
|
+
stepId?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type Preset = {
|
|
21
|
+
kind: Checkout.InputKind;
|
|
22
|
+
prompt: string;
|
|
23
|
+
options: Checkout.ChoiceOption[];
|
|
24
|
+
default?: string;
|
|
25
|
+
help: Checkout.InputHelp;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** The standard inputs an agent can ask with `ask --preset <id>`. */
|
|
29
|
+
export const INPUT_PRESETS: Record<PresetId, Preset> = {
|
|
30
|
+
framework: {
|
|
31
|
+
kind: "choice",
|
|
32
|
+
prompt: "Which agent framework runs your backend?",
|
|
33
|
+
options: [
|
|
34
|
+
{
|
|
35
|
+
id: "ai-sdk",
|
|
36
|
+
label: "Vercel AI SDK",
|
|
37
|
+
description: "Shortest path for Next.js and React",
|
|
38
|
+
icon: "vercel",
|
|
39
|
+
variants: [TYPESCRIPT],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "mastra",
|
|
43
|
+
label: "Mastra",
|
|
44
|
+
description: "Workflows, memory and tools",
|
|
45
|
+
icon: "mastra",
|
|
46
|
+
variants: [TYPESCRIPT],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "langgraph",
|
|
50
|
+
label: "LangGraph",
|
|
51
|
+
description: "Stateful graphs, Python or TypeScript",
|
|
52
|
+
icon: "langgraph",
|
|
53
|
+
variants: [PYTHON, TYPESCRIPT],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
help: {
|
|
57
|
+
summary:
|
|
58
|
+
"Already on one of these? Pick it. Starting fresh in Next.js, the AI SDK is the shortest path. Mastra adds workflows and memory in TypeScript. LangGraph fits a Python backend or multi-step graphs that pause for a human.",
|
|
59
|
+
href: `${DOCS}/docs/runtimes/pick-a-runtime`,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
"llm-provider": {
|
|
63
|
+
kind: "model",
|
|
64
|
+
prompt: "Which model should the assistant use?",
|
|
65
|
+
options: [
|
|
66
|
+
{
|
|
67
|
+
id: "openai",
|
|
68
|
+
label: "OpenAI",
|
|
69
|
+
description: "OPENAI_API_KEY",
|
|
70
|
+
icon: "openai",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "anthropic",
|
|
74
|
+
label: "Anthropic",
|
|
75
|
+
description: "ANTHROPIC_API_KEY",
|
|
76
|
+
icon: "anthropic",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "google",
|
|
80
|
+
label: "Google Gemini",
|
|
81
|
+
description: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
82
|
+
icon: "google",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "openrouter",
|
|
86
|
+
label: "OpenRouter",
|
|
87
|
+
description: "OPENROUTER_API_KEY",
|
|
88
|
+
icon: "openrouter",
|
|
89
|
+
},
|
|
90
|
+
{ id: "xai", label: "xAI", description: "XAI_API_KEY", icon: "xai" },
|
|
91
|
+
{
|
|
92
|
+
id: "mistral",
|
|
93
|
+
label: "Mistral",
|
|
94
|
+
description: "MISTRAL_API_KEY",
|
|
95
|
+
icon: "mistral",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "deepseek",
|
|
99
|
+
label: "DeepSeek",
|
|
100
|
+
description: "DEEPSEEK_API_KEY",
|
|
101
|
+
icon: "deepseek",
|
|
102
|
+
},
|
|
103
|
+
{ id: "groq", label: "Groq", description: "GROQ_API_KEY", icon: "groq" },
|
|
104
|
+
{
|
|
105
|
+
id: "fireworks",
|
|
106
|
+
label: "Fireworks AI",
|
|
107
|
+
description: "FIREWORKS_API_KEY",
|
|
108
|
+
icon: "fireworks",
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
default: "openai",
|
|
112
|
+
help: {
|
|
113
|
+
summary:
|
|
114
|
+
"Any of them works with every framework here. Pick the one whose API key you already have, or OpenRouter to reach many models with one key.",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
project: {
|
|
118
|
+
kind: "choice",
|
|
119
|
+
prompt: "Which project should this go into?",
|
|
120
|
+
options: [
|
|
121
|
+
{
|
|
122
|
+
id: "new",
|
|
123
|
+
label: "New project",
|
|
124
|
+
description: "Start a fresh React app here",
|
|
125
|
+
variants: [
|
|
126
|
+
{ id: "next", label: "Next.js" },
|
|
127
|
+
{ id: "vite", label: "Vite" },
|
|
128
|
+
{ id: "react-router", label: "React Router" },
|
|
129
|
+
{ id: "tanstack-start", label: "TanStack Start" },
|
|
130
|
+
{ id: "expo", label: "Expo" },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
help: {
|
|
135
|
+
summary:
|
|
136
|
+
"Pick the app the assistant should live in. For a new project, Next.js is the shortest path: the assistant-ui CLI scaffolds the chat route with it. Pick Vite, React Router or TanStack Start when the rest of your stack already uses them, and Expo for a native app.",
|
|
137
|
+
href: `${DOCS}/docs/installation`,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const isPresetId = (id: string): id is PresetId => id in INPUT_PRESETS;
|
|
143
|
+
|
|
144
|
+
/** Build the seed for a standard input, throwing on an unknown preset, option or default. */
|
|
145
|
+
export const presetInput = (
|
|
146
|
+
id: PresetId,
|
|
147
|
+
overrides: PresetOverrides = {},
|
|
148
|
+
): Checkout.InputSeed & { stepId?: string } => {
|
|
149
|
+
const preset = INPUT_PRESETS[id];
|
|
150
|
+
const listed = overrides.only
|
|
151
|
+
? overrides.only.map((optionId) => {
|
|
152
|
+
const option = preset.options.find((c) => c.id === optionId);
|
|
153
|
+
if (!option)
|
|
154
|
+
throw new Error(`preset "${id}" has no option "${optionId}"`);
|
|
155
|
+
return option;
|
|
156
|
+
})
|
|
157
|
+
: preset.options;
|
|
158
|
+
const found = (overrides.found ?? []).map((option) => {
|
|
159
|
+
if (
|
|
160
|
+
option.id.includes(":") ||
|
|
161
|
+
preset.options.some((o) => o.id === option.id)
|
|
162
|
+
) {
|
|
163
|
+
throw new Error(`preset "${id}" cannot list "${option.id}" as an option`);
|
|
164
|
+
}
|
|
165
|
+
return { ...option, label: option.id };
|
|
166
|
+
});
|
|
167
|
+
const options = [...found, ...listed];
|
|
168
|
+
const fallback = options.some((o) => o.id === preset.default)
|
|
169
|
+
? preset.default
|
|
170
|
+
: undefined;
|
|
171
|
+
const chosen = overrides.default ?? fallback;
|
|
172
|
+
if (chosen !== undefined && !options.some((o) => o.id === chosen)) {
|
|
173
|
+
throw new Error(`preset "${id}" has no option "${chosen}" to default to`);
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
kind: preset.kind,
|
|
177
|
+
preset: id,
|
|
178
|
+
prompt: preset.prompt,
|
|
179
|
+
options,
|
|
180
|
+
...(chosen !== undefined && { default: chosen }),
|
|
181
|
+
help: preset.help,
|
|
182
|
+
optional: overrides.optional ?? false,
|
|
183
|
+
...(overrides.stepId !== undefined && { stepId: overrides.stepId }),
|
|
184
|
+
};
|
|
185
|
+
};
|