@soleri/cli 1.7.0 → 1.8.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/dist/commands/create.js +6 -0
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/extend.d.ts +2 -0
- package/dist/commands/extend.js +167 -0
- package/dist/commands/extend.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.map +1 -1
- package/dist/prompts/archetypes.d.ts +21 -0
- package/dist/prompts/archetypes.js +153 -0
- package/dist/prompts/archetypes.js.map +1 -0
- package/dist/prompts/create-wizard.d.ts +1 -1
- package/dist/prompts/create-wizard.js +299 -76
- package/dist/prompts/create-wizard.js.map +1 -1
- package/dist/prompts/playbook.d.ts +63 -0
- package/dist/prompts/playbook.js +154 -0
- package/dist/prompts/playbook.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/create.ts +6 -0
- package/src/commands/extend.ts +197 -0
- package/src/main.ts +2 -0
- package/src/prompts/archetypes.ts +212 -0
- package/src/prompts/create-wizard.ts +345 -69
- package/src/prompts/playbook.ts +301 -0
|
@@ -1,31 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Interactive create wizard using @clack/prompts.
|
|
3
|
+
*
|
|
4
|
+
* Guided flow with archetypes, multiselects, and playbook-assisted
|
|
5
|
+
* custom fields. Happy path: 1 typed field (name), everything else
|
|
6
|
+
* is Enter / arrow keys / space bar.
|
|
3
7
|
*/
|
|
4
8
|
import * as p from '@clack/prompts';
|
|
5
9
|
import type { AgentConfig } from '@soleri/forge/lib';
|
|
10
|
+
import { ARCHETYPES, type Archetype } from './archetypes.js';
|
|
11
|
+
import {
|
|
12
|
+
DOMAIN_OPTIONS,
|
|
13
|
+
CUSTOM_DOMAIN_GUIDANCE,
|
|
14
|
+
PRINCIPLE_CATEGORIES,
|
|
15
|
+
CUSTOM_PRINCIPLE_GUIDANCE,
|
|
16
|
+
SKILL_CATEGORIES,
|
|
17
|
+
CORE_SKILLS,
|
|
18
|
+
ALL_OPTIONAL_SKILLS,
|
|
19
|
+
TONE_OPTIONS,
|
|
20
|
+
CUSTOM_ROLE_GUIDANCE,
|
|
21
|
+
CUSTOM_DESCRIPTION_GUIDANCE,
|
|
22
|
+
CUSTOM_GREETING_GUIDANCE,
|
|
23
|
+
} from './playbook.js';
|
|
24
|
+
|
|
25
|
+
/** Slugify a display name into a kebab-case ID. */
|
|
26
|
+
function slugify(name: string): string {
|
|
27
|
+
return name
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
30
|
+
.replace(/^-|-$/g, '');
|
|
31
|
+
}
|
|
6
32
|
|
|
7
33
|
/**
|
|
8
34
|
* Run the interactive create wizard and return an AgentConfig.
|
|
9
|
-
* Returns null if the user cancels.
|
|
35
|
+
* Returns null if the user cancels at any point.
|
|
10
36
|
*/
|
|
11
37
|
export async function runCreateWizard(initialName?: string): Promise<AgentConfig | null> {
|
|
12
38
|
p.intro('Create a new Soleri agent');
|
|
13
39
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
((
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
// ─── Step 1: Archetype ────────────────────────────────────
|
|
41
|
+
const archetypeChoices = [
|
|
42
|
+
...ARCHETYPES.map((a) => ({
|
|
43
|
+
value: a.value,
|
|
44
|
+
label: a.label,
|
|
45
|
+
hint: a.hint,
|
|
46
|
+
})),
|
|
47
|
+
{
|
|
48
|
+
value: '_custom',
|
|
49
|
+
label: '\u2726 Create Custom',
|
|
50
|
+
hint: "I'll guide you through defining your own agent type",
|
|
51
|
+
},
|
|
52
|
+
];
|
|
23
53
|
|
|
24
|
-
|
|
54
|
+
const archetypeValue = await p.select({
|
|
55
|
+
message: 'What kind of agent are you building?',
|
|
56
|
+
options: archetypeChoices,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (p.isCancel(archetypeValue)) return null;
|
|
60
|
+
|
|
61
|
+
const archetype: Archetype | undefined = ARCHETYPES.find((a) => a.value === archetypeValue);
|
|
62
|
+
const isCustom = archetypeValue === '_custom';
|
|
63
|
+
|
|
64
|
+
// ─── Step 2: Display name ─────────────────────────────────
|
|
65
|
+
const nameDefault = archetype ? archetype.label : undefined;
|
|
25
66
|
|
|
26
67
|
const name = (await p.text({
|
|
27
68
|
message: 'Display name',
|
|
28
|
-
placeholder: 'My Agent',
|
|
69
|
+
placeholder: nameDefault ?? 'My Agent',
|
|
70
|
+
initialValue: initialName ?? nameDefault,
|
|
29
71
|
validate: (v) => {
|
|
30
72
|
if (!v || v.length > 50) return 'Required (max 50 chars)';
|
|
31
73
|
},
|
|
@@ -33,81 +75,313 @@ export async function runCreateWizard(initialName?: string): Promise<AgentConfig
|
|
|
33
75
|
|
|
34
76
|
if (p.isCancel(name)) return null;
|
|
35
77
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
78
|
+
// ─── Step 3: Agent ID (auto-derived, confirm or edit) ─────
|
|
79
|
+
const autoId = slugify(name);
|
|
80
|
+
|
|
81
|
+
const id = (await p.text({
|
|
82
|
+
message: 'Agent ID (auto-generated, press Enter to accept)',
|
|
83
|
+
placeholder: autoId,
|
|
84
|
+
initialValue: autoId,
|
|
85
|
+
validate: (v = '') => {
|
|
86
|
+
if (!/^[a-z][a-z0-9-]*$/.test(v)) return 'Must be kebab-case (e.g., "my-agent")';
|
|
41
87
|
},
|
|
42
88
|
})) as string;
|
|
43
89
|
|
|
44
|
-
if (p.isCancel(
|
|
90
|
+
if (p.isCancel(id)) return null;
|
|
45
91
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
placeholder: 'This agent helps developers with...',
|
|
49
|
-
validate: (v) => {
|
|
50
|
-
if (!v || v.length < 10 || v.length > 500) return 'Required (10-500 chars)';
|
|
51
|
-
},
|
|
52
|
-
})) as string;
|
|
92
|
+
// ─── Step 4: Role ─────────────────────────────────────────
|
|
93
|
+
let role: string;
|
|
53
94
|
|
|
54
|
-
if (
|
|
95
|
+
if (isCustom) {
|
|
96
|
+
p.note(
|
|
97
|
+
[
|
|
98
|
+
CUSTOM_ROLE_GUIDANCE.instruction,
|
|
99
|
+
'',
|
|
100
|
+
'Examples:',
|
|
101
|
+
...CUSTOM_ROLE_GUIDANCE.examples.map((e) => ` "${e}"`),
|
|
102
|
+
].join('\n'),
|
|
103
|
+
'\u2726 Custom Agent Playbook',
|
|
104
|
+
);
|
|
55
105
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.filter(Boolean);
|
|
64
|
-
if (parts.length === 0) return 'At least one domain required';
|
|
65
|
-
for (const d of parts) {
|
|
66
|
-
if (!/^[a-z][a-z0-9-]*$/.test(d)) return `Invalid domain "${d}" — must be kebab-case`;
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
})) as string;
|
|
106
|
+
const customRole = (await p.text({
|
|
107
|
+
message: 'What does your agent do? (one sentence)',
|
|
108
|
+
placeholder: 'Validates GraphQL schemas against federation rules',
|
|
109
|
+
validate: (v) => {
|
|
110
|
+
if (!v || v.length > 100) return 'Required (max 100 chars)';
|
|
111
|
+
},
|
|
112
|
+
})) as string;
|
|
70
113
|
|
|
71
|
-
|
|
114
|
+
if (p.isCancel(customRole)) return null;
|
|
115
|
+
role = customRole;
|
|
116
|
+
} else {
|
|
117
|
+
const prefilledRole = archetype!.defaults.role;
|
|
118
|
+
const editedRole = (await p.text({
|
|
119
|
+
message: 'Role (pre-filled, press Enter to accept)',
|
|
120
|
+
initialValue: prefilledRole,
|
|
121
|
+
validate: (v) => {
|
|
122
|
+
if (!v || v.length > 100) return 'Required (max 100 chars)';
|
|
123
|
+
},
|
|
124
|
+
})) as string;
|
|
72
125
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
.filter(Boolean);
|
|
126
|
+
if (p.isCancel(editedRole)) return null;
|
|
127
|
+
role = editedRole;
|
|
128
|
+
}
|
|
77
129
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
130
|
+
// ─── Step 5: Description ──────────────────────────────────
|
|
131
|
+
let description: string;
|
|
132
|
+
|
|
133
|
+
if (isCustom) {
|
|
134
|
+
p.note(
|
|
135
|
+
[
|
|
136
|
+
CUSTOM_DESCRIPTION_GUIDANCE.instruction,
|
|
137
|
+
'',
|
|
138
|
+
'Example:',
|
|
139
|
+
...CUSTOM_DESCRIPTION_GUIDANCE.examples.map((e) => ` "${e}"`),
|
|
140
|
+
].join('\n'),
|
|
141
|
+
'\u2726 Description',
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const customDesc = (await p.text({
|
|
145
|
+
message: 'Describe your agent in detail',
|
|
146
|
+
placeholder: 'This agent helps developers with...',
|
|
147
|
+
validate: (v) => {
|
|
148
|
+
if (!v || v.length < 10 || v.length > 500) return 'Required (10-500 chars)';
|
|
149
|
+
},
|
|
150
|
+
})) as string;
|
|
151
|
+
|
|
152
|
+
if (p.isCancel(customDesc)) return null;
|
|
153
|
+
description = customDesc;
|
|
154
|
+
} else {
|
|
155
|
+
const prefilledDesc = archetype!.defaults.description;
|
|
156
|
+
const editedDesc = (await p.text({
|
|
157
|
+
message: 'Description (pre-filled, press Enter to accept)',
|
|
158
|
+
initialValue: prefilledDesc,
|
|
159
|
+
validate: (v) => {
|
|
160
|
+
if (!v || v.length < 10 || v.length > 500) return 'Required (10-500 chars)';
|
|
161
|
+
},
|
|
162
|
+
})) as string;
|
|
163
|
+
|
|
164
|
+
if (p.isCancel(editedDesc)) return null;
|
|
165
|
+
description = editedDesc;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Step 6: Domains (multiselect) ────────────────────────
|
|
169
|
+
const preselectedDomains = new Set(archetype?.defaults.domains ?? []);
|
|
170
|
+
|
|
171
|
+
const domainChoices = [
|
|
172
|
+
...DOMAIN_OPTIONS.map((d) => ({
|
|
173
|
+
value: d.value,
|
|
174
|
+
label: d.label,
|
|
175
|
+
hint: d.hint,
|
|
176
|
+
})),
|
|
177
|
+
{
|
|
178
|
+
value: '_custom',
|
|
179
|
+
label: '\u2726 Add custom domain...',
|
|
180
|
+
hint: 'Define your own domain with playbook guidance',
|
|
88
181
|
},
|
|
89
|
-
|
|
182
|
+
];
|
|
90
183
|
|
|
91
|
-
|
|
184
|
+
// Pre-select archetype domains via initialValues
|
|
185
|
+
const domainSelection = await p.multiselect({
|
|
186
|
+
message: 'Select domains (areas of expertise)',
|
|
187
|
+
options: domainChoices,
|
|
188
|
+
initialValues: [...preselectedDomains],
|
|
189
|
+
required: true,
|
|
190
|
+
});
|
|
92
191
|
|
|
93
|
-
|
|
94
|
-
.split('\n')
|
|
95
|
-
.map((s) => s.trim())
|
|
96
|
-
.filter(Boolean);
|
|
192
|
+
if (p.isCancel(domainSelection)) return null;
|
|
97
193
|
|
|
98
|
-
const
|
|
194
|
+
const domains = (domainSelection as string[]).filter((d) => d !== '_custom');
|
|
195
|
+
const wantsCustomDomain = (domainSelection as string[]).includes('_custom');
|
|
196
|
+
|
|
197
|
+
if (wantsCustomDomain) {
|
|
198
|
+
p.note(
|
|
199
|
+
[
|
|
200
|
+
CUSTOM_DOMAIN_GUIDANCE.instruction,
|
|
201
|
+
'',
|
|
202
|
+
'Examples:',
|
|
203
|
+
...CUSTOM_DOMAIN_GUIDANCE.examples.map((e) => ` ${e}`),
|
|
204
|
+
'',
|
|
205
|
+
'Avoid:',
|
|
206
|
+
...CUSTOM_DOMAIN_GUIDANCE.antiExamples.map((e) => ` \u2717 ${e}`),
|
|
207
|
+
].join('\n'),
|
|
208
|
+
'\u2726 Custom Domain',
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const customDomain = (await p.text({
|
|
212
|
+
message: 'Custom domain name (kebab-case)',
|
|
213
|
+
placeholder: 'graphql-federation',
|
|
214
|
+
validate: (v = '') => {
|
|
215
|
+
if (!/^[a-z][a-z0-9-]*$/.test(v)) return 'Must be kebab-case';
|
|
216
|
+
if (domains.includes(v)) return 'Already selected';
|
|
217
|
+
},
|
|
218
|
+
})) as string;
|
|
219
|
+
|
|
220
|
+
if (!p.isCancel(customDomain)) {
|
|
221
|
+
domains.push(customDomain);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (domains.length === 0) {
|
|
226
|
+
p.log.error('At least one domain is required');
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── Step 7: Principles (multiselect) ─────────────────────
|
|
231
|
+
const preselectedPrinciples = new Set(archetype?.defaults.principles ?? []);
|
|
232
|
+
|
|
233
|
+
// Flatten categories into a single options list with group labels
|
|
234
|
+
const principleChoices = PRINCIPLE_CATEGORIES.flatMap((cat) => cat.options.map((o) => ({
|
|
235
|
+
value: o.value,
|
|
236
|
+
label: o.label,
|
|
237
|
+
hint: cat.label,
|
|
238
|
+
})));
|
|
239
|
+
|
|
240
|
+
principleChoices.push({
|
|
241
|
+
value: '_custom',
|
|
242
|
+
label: '\u2726 Add custom principle...',
|
|
243
|
+
hint: 'Write your own guiding principle',
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const principleSelection = await p.multiselect({
|
|
247
|
+
message: 'Select guiding principles',
|
|
248
|
+
options: principleChoices,
|
|
249
|
+
initialValues: [...preselectedPrinciples],
|
|
250
|
+
required: true,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (p.isCancel(principleSelection)) return null;
|
|
254
|
+
|
|
255
|
+
const principles = (principleSelection as string[]).filter((p) => p !== '_custom');
|
|
256
|
+
const wantsCustomPrinciple = (principleSelection as string[]).includes('_custom');
|
|
257
|
+
|
|
258
|
+
if (wantsCustomPrinciple) {
|
|
259
|
+
p.note(
|
|
260
|
+
[
|
|
261
|
+
CUSTOM_PRINCIPLE_GUIDANCE.instruction,
|
|
262
|
+
'',
|
|
263
|
+
'Good principles are specific and actionable:',
|
|
264
|
+
...CUSTOM_PRINCIPLE_GUIDANCE.examples.map((e) => ` \u2713 "${e}"`),
|
|
265
|
+
'',
|
|
266
|
+
'Avoid vague principles:',
|
|
267
|
+
...CUSTOM_PRINCIPLE_GUIDANCE.antiExamples.map((e) => ` \u2717 ${e}`),
|
|
268
|
+
].join('\n'),
|
|
269
|
+
'\u2726 Custom Principle',
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const customPrinciple = (await p.text({
|
|
273
|
+
message: 'Your custom principle',
|
|
274
|
+
placeholder: 'Every public API must have a deprecation path',
|
|
275
|
+
validate: (v) => {
|
|
276
|
+
if (!v) return 'Required';
|
|
277
|
+
if (v.length > 100) return 'Max 100 chars';
|
|
278
|
+
},
|
|
279
|
+
})) as string;
|
|
280
|
+
|
|
281
|
+
if (!p.isCancel(customPrinciple)) {
|
|
282
|
+
principles.push(customPrinciple);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (principles.length === 0) {
|
|
287
|
+
p.log.error('At least one principle is required');
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ─── Step 8: Communication tone ───────────────────────────
|
|
292
|
+
const defaultTone = archetype?.defaults.tone ?? 'pragmatic';
|
|
293
|
+
|
|
294
|
+
const tone = await p.select({
|
|
295
|
+
message: 'Communication tone',
|
|
296
|
+
options: TONE_OPTIONS.map((t) => ({
|
|
297
|
+
value: t.value,
|
|
298
|
+
label: t.label,
|
|
299
|
+
hint: t.hint,
|
|
300
|
+
})),
|
|
301
|
+
initialValue: defaultTone,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (p.isCancel(tone)) return null;
|
|
305
|
+
|
|
306
|
+
// ─── Step 9: Skills (multiselect) ─────────────────────────
|
|
307
|
+
const preselectedSkills = new Set(archetype?.defaults.skills ?? []);
|
|
308
|
+
|
|
309
|
+
p.note(`Always included: ${CORE_SKILLS.join(', ')}`, 'Core Skills');
|
|
310
|
+
|
|
311
|
+
const skillChoices = SKILL_CATEGORIES.flatMap((cat) =>
|
|
312
|
+
cat.options.map((o) => ({
|
|
313
|
+
value: o.value,
|
|
314
|
+
label: o.label,
|
|
315
|
+
hint: `${o.hint} (${cat.label})`,
|
|
316
|
+
})),
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const skillSelection = await p.multiselect({
|
|
320
|
+
message: 'Select additional skills',
|
|
321
|
+
options: skillChoices,
|
|
322
|
+
initialValues: [...preselectedSkills].filter((s) => ALL_OPTIONAL_SKILLS.includes(s)),
|
|
323
|
+
required: false,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (p.isCancel(skillSelection)) return null;
|
|
327
|
+
|
|
328
|
+
const selectedSkills = [...CORE_SKILLS, ...(skillSelection as string[])];
|
|
329
|
+
|
|
330
|
+
// ─── Step 10: Greeting (auto or custom) ───────────────────
|
|
331
|
+
const autoGreeting = archetype
|
|
332
|
+
? archetype.defaults.greetingTemplate(name)
|
|
333
|
+
: `Hello! I'm ${name}. I ${role[0].toLowerCase()}${role.slice(1)}.`;
|
|
334
|
+
|
|
335
|
+
const greetingChoice = await p.select({
|
|
99
336
|
message: 'Greeting message',
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
337
|
+
options: [
|
|
338
|
+
{
|
|
339
|
+
value: 'auto',
|
|
340
|
+
label: `Auto \u2014 "${autoGreeting.length > 70 ? autoGreeting.slice(0, 67) + '...' : autoGreeting}"`,
|
|
341
|
+
hint: 'Generated from name + role',
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
value: 'custom',
|
|
345
|
+
label: '\u2726 Custom \u2014 Write your own greeting',
|
|
346
|
+
hint: 'Opens playbook-guided text field',
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
initialValue: 'auto',
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
if (p.isCancel(greetingChoice)) return null;
|
|
353
|
+
|
|
354
|
+
let greeting: string;
|
|
355
|
+
|
|
356
|
+
if (greetingChoice === 'custom') {
|
|
357
|
+
p.note(
|
|
358
|
+
[
|
|
359
|
+
CUSTOM_GREETING_GUIDANCE.instruction,
|
|
360
|
+
'',
|
|
361
|
+
'Examples:',
|
|
362
|
+
...CUSTOM_GREETING_GUIDANCE.examples.map((e) => ` "${e}"`),
|
|
363
|
+
].join('\n'),
|
|
364
|
+
'\u2726 Custom Greeting',
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const customGreeting = (await p.text({
|
|
368
|
+
message: 'Your greeting',
|
|
369
|
+
placeholder: `Hello! I'm ${name}...`,
|
|
370
|
+
validate: (v) => {
|
|
371
|
+
if (!v || v.length < 10 || v.length > 300) return 'Required (10-300 chars)';
|
|
372
|
+
},
|
|
373
|
+
})) as string;
|
|
105
374
|
|
|
106
|
-
|
|
375
|
+
if (p.isCancel(customGreeting)) return null;
|
|
376
|
+
greeting = customGreeting;
|
|
377
|
+
} else {
|
|
378
|
+
greeting = autoGreeting;
|
|
379
|
+
}
|
|
107
380
|
|
|
381
|
+
// ─── Step 11: Output directory ────────────────────────────
|
|
108
382
|
const outputDir = (await p.text({
|
|
109
383
|
message: 'Output directory',
|
|
110
|
-
|
|
384
|
+
initialValue: process.cwd(),
|
|
111
385
|
placeholder: process.cwd(),
|
|
112
386
|
validate: (v) => {
|
|
113
387
|
if (!v) return 'Required';
|
|
@@ -123,7 +397,9 @@ export async function runCreateWizard(initialName?: string): Promise<AgentConfig
|
|
|
123
397
|
description,
|
|
124
398
|
domains,
|
|
125
399
|
principles,
|
|
400
|
+
tone: tone as 'precise' | 'mentor' | 'pragmatic',
|
|
126
401
|
greeting,
|
|
127
402
|
outputDir,
|
|
403
|
+
skills: selectedSkills,
|
|
128
404
|
};
|
|
129
405
|
}
|