create-expert 0.0.8 → 0.0.10
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 +42 -5
- package/dist/bin/cli.js +1871 -0
- package/dist/bin/cli.js.map +1 -0
- package/package.json +1 -1
- package/dist/cli.js +0 -800
- package/dist/cli.js.map +0 -1
package/dist/cli.js
DELETED
|
@@ -1,800 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
#!/usr/bin/env node
|
|
3
|
-
|
|
4
|
-
// bin/cli.ts
|
|
5
|
-
import { spawn } from "child_process";
|
|
6
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
7
|
-
import { join } from "path";
|
|
8
|
-
import { Command } from "commander";
|
|
9
|
-
import { config } from "dotenv";
|
|
10
|
-
|
|
11
|
-
// src/lib/agents-md-template.ts
|
|
12
|
-
function generateAgentsMd(options) {
|
|
13
|
-
const { provider, model, runtime } = options;
|
|
14
|
-
const isNonDefaultRuntime = runtime && runtime !== "default";
|
|
15
|
-
const runtimeSection = isNonDefaultRuntime ? `runtime = "${runtime}"` : "";
|
|
16
|
-
return `# AGENTS.md
|
|
17
|
-
|
|
18
|
-
## What is Perstack
|
|
19
|
-
|
|
20
|
-
Perstack is a package manager and runtime for agent-first development. It enables you to define, test, and share modular AI agents called "Experts".
|
|
21
|
-
|
|
22
|
-
Key concepts:
|
|
23
|
-
- **Experts**: Modular micro-agents defined in TOML
|
|
24
|
-
- **Runtime**: Executes Experts with isolation, observability, and sandbox support
|
|
25
|
-
- **Registry**: Public registry for sharing and reusing Experts
|
|
26
|
-
|
|
27
|
-
## Project Configuration
|
|
28
|
-
|
|
29
|
-
This project uses:
|
|
30
|
-
- Provider: ${provider}
|
|
31
|
-
- Model: ${model}
|
|
32
|
-
${isNonDefaultRuntime ? `- Runtime: ${runtime}` : "- Runtime: docker (default)"}
|
|
33
|
-
|
|
34
|
-
## CLI Reference
|
|
35
|
-
|
|
36
|
-
### Running Experts
|
|
37
|
-
|
|
38
|
-
**\`perstack start\`** - Interactive workbench for developing and testing Experts
|
|
39
|
-
\`\`\`bash
|
|
40
|
-
perstack start [expertKey] [query]
|
|
41
|
-
\`\`\`
|
|
42
|
-
|
|
43
|
-
**\`perstack run\`** - Headless execution for production and automation
|
|
44
|
-
\`\`\`bash
|
|
45
|
-
perstack run <expertKey> <query> [options]
|
|
46
|
-
\`\`\`
|
|
47
|
-
|
|
48
|
-
### Common Options
|
|
49
|
-
|
|
50
|
-
| Option | Description | Default |
|
|
51
|
-
|--------|-------------|---------|
|
|
52
|
-
| \`--provider <provider>\` | LLM provider | \`anthropic\` |
|
|
53
|
-
| \`--model <model>\` | Model name | \`claude-sonnet-4-5\` |
|
|
54
|
-
| \`--max-steps <n>\` | Maximum steps | unlimited |
|
|
55
|
-
| \`--runtime <runtime>\` | Execution runtime | \`docker\` |
|
|
56
|
-
|
|
57
|
-
### Available Runtimes
|
|
58
|
-
|
|
59
|
-
- \`docker\` \u2014 Containerized runtime with network isolation (default)
|
|
60
|
-
- \`local\` \u2014 Built-in runtime without isolation
|
|
61
|
-
- \`cursor\` \u2014 Cursor CLI (experimental)
|
|
62
|
-
- \`claude-code\` \u2014 Claude Code CLI (experimental)
|
|
63
|
-
- \`gemini\` \u2014 Gemini CLI (experimental)
|
|
64
|
-
|
|
65
|
-
## perstack.toml Format
|
|
66
|
-
|
|
67
|
-
\`\`\`toml
|
|
68
|
-
model = "${model}"
|
|
69
|
-
${runtimeSection}
|
|
70
|
-
|
|
71
|
-
[provider]
|
|
72
|
-
providerName = "${provider}"
|
|
73
|
-
|
|
74
|
-
[experts."my-expert"]
|
|
75
|
-
version = "1.0.0"
|
|
76
|
-
description = "Brief description"
|
|
77
|
-
instruction = """
|
|
78
|
-
Detailed instructions for the expert.
|
|
79
|
-
"""
|
|
80
|
-
|
|
81
|
-
[experts."my-expert".skills."@perstack/base"]
|
|
82
|
-
type = "mcpStdioSkill"
|
|
83
|
-
command = "npx"
|
|
84
|
-
packageName = "@perstack/base"
|
|
85
|
-
\`\`\`
|
|
86
|
-
|
|
87
|
-
## Best Practices for Creating Experts
|
|
88
|
-
|
|
89
|
-
### 1. Do One Thing Well
|
|
90
|
-
|
|
91
|
-
Bad:
|
|
92
|
-
\`\`\`toml
|
|
93
|
-
[experts."assistant"]
|
|
94
|
-
description = "Handles inquiries, reports, meetings, and expenses"
|
|
95
|
-
\`\`\`
|
|
96
|
-
|
|
97
|
-
Good:
|
|
98
|
-
\`\`\`toml
|
|
99
|
-
[experts."customer-support"]
|
|
100
|
-
description = "Answers customer questions about products and orders"
|
|
101
|
-
\`\`\`
|
|
102
|
-
|
|
103
|
-
### 2. Trust the LLM, Define Domain Knowledge
|
|
104
|
-
|
|
105
|
-
Bad (procedural):
|
|
106
|
-
\`\`\`toml
|
|
107
|
-
instruction = """
|
|
108
|
-
1. First, greet the customer
|
|
109
|
-
2. Ask for their order number
|
|
110
|
-
3. Look up the order
|
|
111
|
-
"""
|
|
112
|
-
\`\`\`
|
|
113
|
-
|
|
114
|
-
Good (declarative):
|
|
115
|
-
\`\`\`toml
|
|
116
|
-
instruction = """
|
|
117
|
-
You are a customer support specialist.
|
|
118
|
-
|
|
119
|
-
Key policies:
|
|
120
|
-
- Orders ship within 2 business days
|
|
121
|
-
- Free returns within 30 days
|
|
122
|
-
- VIP customers get priority handling
|
|
123
|
-
|
|
124
|
-
Tone: Friendly but professional.
|
|
125
|
-
"""
|
|
126
|
-
\`\`\`
|
|
127
|
-
|
|
128
|
-
### 3. Let Them Collaborate
|
|
129
|
-
|
|
130
|
-
Use delegation for complex workflows:
|
|
131
|
-
\`\`\`toml
|
|
132
|
-
[experts."coordinator"]
|
|
133
|
-
delegates = ["researcher", "writer", "reviewer"]
|
|
134
|
-
|
|
135
|
-
[experts."researcher"]
|
|
136
|
-
description = "Gathers information from various sources"
|
|
137
|
-
\`\`\`
|
|
138
|
-
|
|
139
|
-
### 4. Keep It Verifiable
|
|
140
|
-
|
|
141
|
-
Write clear, predictable instructions:
|
|
142
|
-
\`\`\`toml
|
|
143
|
-
instruction = """
|
|
144
|
-
Approval rules:
|
|
145
|
-
- Under $100: Auto-approve with receipt
|
|
146
|
-
- $100-$500: Approve if business purpose is clear
|
|
147
|
-
- Over $500: Flag for manager review
|
|
148
|
-
"""
|
|
149
|
-
\`\`\`
|
|
150
|
-
|
|
151
|
-
### 5. Ship Early
|
|
152
|
-
|
|
153
|
-
Start minimal, expand based on real usage. Don't over-engineer.
|
|
154
|
-
|
|
155
|
-
## Finding Skills (MCP Servers)
|
|
156
|
-
|
|
157
|
-
Skills extend Experts with external capabilities via MCP (Model Context Protocol).
|
|
158
|
-
|
|
159
|
-
### MCP Registry
|
|
160
|
-
|
|
161
|
-
Search for MCP servers at: https://registry.modelcontextprotocol.io
|
|
162
|
-
|
|
163
|
-
**API Reference:**
|
|
164
|
-
\`\`\`bash
|
|
165
|
-
# List all servers
|
|
166
|
-
curl "https://registry.modelcontextprotocol.io/v0.1/servers"
|
|
167
|
-
|
|
168
|
-
# Search by name
|
|
169
|
-
curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=filesystem"
|
|
170
|
-
|
|
171
|
-
# Get specific server
|
|
172
|
-
curl "https://registry.modelcontextprotocol.io/v0.1/servers/{serverName}/versions/{version}"
|
|
173
|
-
\`\`\`
|
|
174
|
-
|
|
175
|
-
### Using MCP Skills
|
|
176
|
-
|
|
177
|
-
**npm packages (most common):**
|
|
178
|
-
\`\`\`toml
|
|
179
|
-
[experts."my-expert".skills."web-search"]
|
|
180
|
-
type = "mcpStdioSkill"
|
|
181
|
-
command = "npx"
|
|
182
|
-
packageName = "exa-mcp-server"
|
|
183
|
-
requiredEnv = ["EXA_API_KEY"]
|
|
184
|
-
pick = ["web_search_exa"] # Optional: whitelist tools
|
|
185
|
-
\`\`\`
|
|
186
|
-
|
|
187
|
-
**Remote servers (SSE):**
|
|
188
|
-
\`\`\`toml
|
|
189
|
-
[experts."my-expert".skills."remote-api"]
|
|
190
|
-
type = "mcpSseSkill"
|
|
191
|
-
endpoint = "https://api.example.com/mcp"
|
|
192
|
-
\`\`\`
|
|
193
|
-
|
|
194
|
-
### Built-in Base Skill
|
|
195
|
-
|
|
196
|
-
\`@perstack/base\` provides essential tools:
|
|
197
|
-
- File operations: \`readTextFile\`, \`writeTextFile\`, \`editTextFile\`, etc.
|
|
198
|
-
- Shell execution: \`exec\`
|
|
199
|
-
- Control flow: \`attemptCompletion\`, \`think\`, \`todo\`
|
|
200
|
-
|
|
201
|
-
\`\`\`toml
|
|
202
|
-
[experts."my-expert".skills."@perstack/base"]
|
|
203
|
-
type = "mcpStdioSkill"
|
|
204
|
-
command = "npx"
|
|
205
|
-
packageName = "@perstack/base"
|
|
206
|
-
\`\`\`
|
|
207
|
-
|
|
208
|
-
## Testing Experts
|
|
209
|
-
|
|
210
|
-
1. Start with \`perstack start\` for interactive development
|
|
211
|
-
2. Test happy path scenarios first
|
|
212
|
-
3. Test edge cases and error scenarios
|
|
213
|
-
4. Verify outputs match expectations
|
|
214
|
-
5. Use \`--max-steps\` to limit runaway executions
|
|
215
|
-
|
|
216
|
-
## Project Files
|
|
217
|
-
|
|
218
|
-
- \`perstack.toml\` - Expert definitions and runtime config
|
|
219
|
-
- \`AGENTS.md\` - This file, for AI agent context
|
|
220
|
-
- \`.env\` - Environment variables (API keys)
|
|
221
|
-
- \`perstack/\` - Execution history (auto-managed)
|
|
222
|
-
`;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// src/lib/create-expert-toml.ts
|
|
226
|
-
function generateCreateExpertToml(options) {
|
|
227
|
-
const runtimeLine = options.runtime && options.runtime !== "default" ? `runtime = "${options.runtime}"
|
|
228
|
-
` : "";
|
|
229
|
-
return `model = "${options.model}"
|
|
230
|
-
${runtimeLine}
|
|
231
|
-
[provider]
|
|
232
|
-
providerName = "${options.provider}"
|
|
233
|
-
|
|
234
|
-
[experts."create-expert"]
|
|
235
|
-
version = "1.0.0"
|
|
236
|
-
description = "Creates and tests new Perstack Experts based on user requirements"
|
|
237
|
-
instruction = """
|
|
238
|
-
You are an Expert creator for Perstack. Your job is to create well-designed Experts based on user requirements.
|
|
239
|
-
|
|
240
|
-
## Your Workflow
|
|
241
|
-
|
|
242
|
-
1. **Understand Requirements**: Analyze what the user wants the Expert to do
|
|
243
|
-
2. **Design the Expert**: Plan the structure, skills needed, and delegation patterns
|
|
244
|
-
3. **Implement**: Write the Expert definition in perstack.toml
|
|
245
|
-
4. **Test**: Run the Expert with realistic test cases
|
|
246
|
-
5. **Iterate**: Fix issues and improve based on test results
|
|
247
|
-
|
|
248
|
-
## Design Principles
|
|
249
|
-
|
|
250
|
-
1. **Do One Thing Well**: Each Expert should focus on a single responsibility
|
|
251
|
-
2. **Define Domain Knowledge**: Write declarative instructions with policies, not procedures
|
|
252
|
-
3. **Use Delegation**: Break complex tasks into collaborating Experts
|
|
253
|
-
4. **Keep It Verifiable**: Write clear, predictable behavior
|
|
254
|
-
5. **Start Simple**: Begin minimal, expand based on real needs
|
|
255
|
-
|
|
256
|
-
## Expert Structure
|
|
257
|
-
|
|
258
|
-
[experts."expert-name"]
|
|
259
|
-
version = "1.0.0"
|
|
260
|
-
description = "Brief description visible to delegators"
|
|
261
|
-
instruction = "Domain knowledge, policies, and constraints."
|
|
262
|
-
delegates = ["other-expert"]
|
|
263
|
-
|
|
264
|
-
[experts."expert-name".skills."skill-name"]
|
|
265
|
-
type = "mcpStdioSkill"
|
|
266
|
-
command = "npx"
|
|
267
|
-
packageName = "package-name"
|
|
268
|
-
|
|
269
|
-
## Finding Skills
|
|
270
|
-
|
|
271
|
-
Search MCP Registry for available skills:
|
|
272
|
-
- Browse: https://registry.modelcontextprotocol.io
|
|
273
|
-
- API: GET /v0.1/servers?search=<query>
|
|
274
|
-
|
|
275
|
-
Common skills:
|
|
276
|
-
- \`@perstack/base\` - File operations, shell, control flow
|
|
277
|
-
- \`exa-mcp-server\` - Web search (requires EXA_API_KEY)
|
|
278
|
-
- \`@anthropic/mcp-server-memory\` - Persistent memory
|
|
279
|
-
|
|
280
|
-
## Testing Protocol
|
|
281
|
-
|
|
282
|
-
For each Expert you create:
|
|
283
|
-
|
|
284
|
-
1. **Happy Path Tests**
|
|
285
|
-
- Test the primary use case with normal inputs
|
|
286
|
-
- Verify the output matches expectations
|
|
287
|
-
|
|
288
|
-
2. **Edge Case Tests**
|
|
289
|
-
- Empty or minimal inputs
|
|
290
|
-
- Unusual but valid inputs
|
|
291
|
-
- Boundary conditions
|
|
292
|
-
|
|
293
|
-
3. **Error Handling Tests**
|
|
294
|
-
- Invalid inputs
|
|
295
|
-
- Missing required data
|
|
296
|
-
- Skill failures
|
|
297
|
-
|
|
298
|
-
Use \`npx -y perstack run\` to execute tests:
|
|
299
|
-
\`\`\`bash
|
|
300
|
-
npx -y perstack run expert-name "test query"
|
|
301
|
-
\`\`\`
|
|
302
|
-
|
|
303
|
-
## Iteration Process
|
|
304
|
-
|
|
305
|
-
After each test:
|
|
306
|
-
1. Review the output
|
|
307
|
-
2. Identify issues or improvements
|
|
308
|
-
3. Update the Expert definition
|
|
309
|
-
4. Re-run tests to verify fixes
|
|
310
|
-
|
|
311
|
-
Continue until all tests pass and the Expert behaves as expected.
|
|
312
|
-
"""
|
|
313
|
-
|
|
314
|
-
[experts."create-expert".skills."@perstack/base"]
|
|
315
|
-
type = "mcpStdioSkill"
|
|
316
|
-
command = "npx"
|
|
317
|
-
packageName = "@perstack/base"
|
|
318
|
-
`;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// src/lib/detect-llm.ts
|
|
322
|
-
var LLM_CONFIGS = {
|
|
323
|
-
anthropic: {
|
|
324
|
-
envVar: "ANTHROPIC_API_KEY",
|
|
325
|
-
displayName: "Anthropic (Claude)",
|
|
326
|
-
defaultModel: "claude-sonnet-4-5"
|
|
327
|
-
},
|
|
328
|
-
openai: { envVar: "OPENAI_API_KEY", displayName: "OpenAI", defaultModel: "gpt-4o" },
|
|
329
|
-
google: {
|
|
330
|
-
envVar: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
331
|
-
displayName: "Google (Gemini)",
|
|
332
|
-
defaultModel: "gemini-2.5-pro"
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
function detectLLM(provider) {
|
|
336
|
-
const config2 = LLM_CONFIGS[provider];
|
|
337
|
-
return {
|
|
338
|
-
provider,
|
|
339
|
-
envVar: config2.envVar,
|
|
340
|
-
available: Boolean(process.env[config2.envVar]),
|
|
341
|
-
displayName: config2.displayName,
|
|
342
|
-
defaultModel: config2.defaultModel
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
function detectAllLLMs() {
|
|
346
|
-
return Object.keys(LLM_CONFIGS).map(detectLLM);
|
|
347
|
-
}
|
|
348
|
-
function getDefaultModel(provider) {
|
|
349
|
-
return LLM_CONFIGS[provider].defaultModel;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// src/lib/detect-runtime.ts
|
|
353
|
-
import { execSync } from "child_process";
|
|
354
|
-
function checkCommand(command) {
|
|
355
|
-
try {
|
|
356
|
-
const result = execSync(`${command} --version`, {
|
|
357
|
-
encoding: "utf-8",
|
|
358
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
359
|
-
timeout: 5e3
|
|
360
|
-
});
|
|
361
|
-
const firstLine = result.trim().split("\n")[0];
|
|
362
|
-
return { available: true, version: firstLine };
|
|
363
|
-
} catch {
|
|
364
|
-
return { available: false };
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
function detectCursor() {
|
|
368
|
-
const result = checkCommand("cursor");
|
|
369
|
-
return { type: "cursor", ...result };
|
|
370
|
-
}
|
|
371
|
-
function detectClaudeCode() {
|
|
372
|
-
const result = checkCommand("claude");
|
|
373
|
-
return { type: "claude-code", ...result };
|
|
374
|
-
}
|
|
375
|
-
function detectGemini() {
|
|
376
|
-
const result = checkCommand("gemini");
|
|
377
|
-
return { type: "gemini", ...result };
|
|
378
|
-
}
|
|
379
|
-
function detectAllRuntimes() {
|
|
380
|
-
return [detectCursor(), detectClaudeCode(), detectGemini()];
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// src/tui/wizard/render.tsx
|
|
384
|
-
import { render } from "ink";
|
|
385
|
-
|
|
386
|
-
// src/tui/wizard/app.tsx
|
|
387
|
-
import { Box, Text, useApp, useInput } from "ink";
|
|
388
|
-
import { useEffect, useState } from "react";
|
|
389
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
390
|
-
function SelectableList({
|
|
391
|
-
items,
|
|
392
|
-
selectedIndex,
|
|
393
|
-
renderItem
|
|
394
|
-
}) {
|
|
395
|
-
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: items.map((item, index) => {
|
|
396
|
-
const isSelected = index === selectedIndex;
|
|
397
|
-
if (renderItem) {
|
|
398
|
-
return /* @__PURE__ */ jsx(Box, { children: renderItem(item, isSelected) }, item.key);
|
|
399
|
-
}
|
|
400
|
-
return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { color: item.disabled ? "gray" : isSelected ? "cyan" : "white", children: [
|
|
401
|
-
isSelected ? "\u276F " : " ",
|
|
402
|
-
item.label,
|
|
403
|
-
item.disabled ? " (not available)" : ""
|
|
404
|
-
] }) }, item.key);
|
|
405
|
-
}) });
|
|
406
|
-
}
|
|
407
|
-
function TextInputComponent({
|
|
408
|
-
value,
|
|
409
|
-
onChange,
|
|
410
|
-
onSubmit,
|
|
411
|
-
placeholder,
|
|
412
|
-
isSecret
|
|
413
|
-
}) {
|
|
414
|
-
useInput((input, key) => {
|
|
415
|
-
if (key.return) {
|
|
416
|
-
onSubmit();
|
|
417
|
-
} else if (key.backspace || key.delete) {
|
|
418
|
-
onChange(value.slice(0, -1));
|
|
419
|
-
} else if (!key.ctrl && !key.meta && input) {
|
|
420
|
-
onChange(value + input);
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
const displayValue = isSecret ? "\u2022".repeat(value.length) : value;
|
|
424
|
-
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
425
|
-
/* @__PURE__ */ jsx(Text, { color: "cyan", children: displayValue || /* @__PURE__ */ jsx(Text, { color: "gray", children: placeholder }) }),
|
|
426
|
-
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u2588" })
|
|
427
|
-
] });
|
|
428
|
-
}
|
|
429
|
-
function getRuntimeDisplayName(type) {
|
|
430
|
-
switch (type) {
|
|
431
|
-
case "cursor":
|
|
432
|
-
return "Cursor";
|
|
433
|
-
case "claude-code":
|
|
434
|
-
return "Claude Code";
|
|
435
|
-
case "gemini":
|
|
436
|
-
return "Gemini CLI";
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
function App({ llms, runtimes, onComplete, isImprovement, improvementTarget }) {
|
|
440
|
-
const { exit } = useApp();
|
|
441
|
-
const [step, setStep] = useState("detecting");
|
|
442
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
443
|
-
const [result, setResult] = useState({});
|
|
444
|
-
const [apiKeyInput, setApiKeyInput] = useState("");
|
|
445
|
-
const [expertDescInput, setExpertDescInput] = useState(improvementTarget || "");
|
|
446
|
-
const availableLLMs = llms.filter((l) => l.available);
|
|
447
|
-
const availableRuntimes = runtimes.filter((r) => r.available);
|
|
448
|
-
const runtimeOptions = [
|
|
449
|
-
{ key: "default", type: "default", label: "Default (built-in)" },
|
|
450
|
-
...availableRuntimes.map((r) => ({
|
|
451
|
-
key: r.type,
|
|
452
|
-
type: r.type,
|
|
453
|
-
label: `${getRuntimeDisplayName(r.type)}${r.version ? ` (${r.version})` : ""}`,
|
|
454
|
-
version: r.version
|
|
455
|
-
}))
|
|
456
|
-
];
|
|
457
|
-
const llmOptionsWithOther = [
|
|
458
|
-
...llms.map((l) => ({
|
|
459
|
-
key: l.provider,
|
|
460
|
-
label: `${l.displayName}${l.available ? " \u2713" : ""}`,
|
|
461
|
-
provider: l.provider,
|
|
462
|
-
available: l.available,
|
|
463
|
-
defaultModel: l.defaultModel
|
|
464
|
-
})),
|
|
465
|
-
{
|
|
466
|
-
key: "other",
|
|
467
|
-
label: "Other (configure new provider)",
|
|
468
|
-
provider: null,
|
|
469
|
-
available: false,
|
|
470
|
-
defaultModel: ""
|
|
471
|
-
}
|
|
472
|
-
];
|
|
473
|
-
useEffect(() => {
|
|
474
|
-
if (step === "detecting") {
|
|
475
|
-
const timer = setTimeout(() => {
|
|
476
|
-
if (isImprovement) {
|
|
477
|
-
const llm = availableLLMs[0];
|
|
478
|
-
if (llm) {
|
|
479
|
-
setResult({
|
|
480
|
-
runtime: "default",
|
|
481
|
-
provider: llm.provider,
|
|
482
|
-
model: llm.defaultModel
|
|
483
|
-
});
|
|
484
|
-
setStep("input-expert-description");
|
|
485
|
-
} else {
|
|
486
|
-
setStep("select-runtime");
|
|
487
|
-
}
|
|
488
|
-
} else {
|
|
489
|
-
setStep("select-runtime");
|
|
490
|
-
}
|
|
491
|
-
}, 500);
|
|
492
|
-
return () => clearTimeout(timer);
|
|
493
|
-
}
|
|
494
|
-
return void 0;
|
|
495
|
-
}, [step, isImprovement, availableLLMs]);
|
|
496
|
-
useInput((_, key) => {
|
|
497
|
-
if (key.escape) {
|
|
498
|
-
exit();
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
if (step === "input-api-key" || step === "input-expert-description") {
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
if (key.upArrow) {
|
|
505
|
-
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
506
|
-
} else if (key.downArrow) {
|
|
507
|
-
setSelectedIndex((prev) => {
|
|
508
|
-
const maxIndex = getMaxIndex();
|
|
509
|
-
return Math.min(maxIndex, prev + 1);
|
|
510
|
-
});
|
|
511
|
-
} else if (key.return) {
|
|
512
|
-
handleSelect();
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
function getMaxIndex() {
|
|
516
|
-
switch (step) {
|
|
517
|
-
case "select-runtime":
|
|
518
|
-
return runtimeOptions.length - 1;
|
|
519
|
-
case "select-llm":
|
|
520
|
-
return llmOptionsWithOther.length - 1;
|
|
521
|
-
case "select-provider":
|
|
522
|
-
return llms.length - 1;
|
|
523
|
-
default:
|
|
524
|
-
return 0;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
function handleSelect() {
|
|
528
|
-
switch (step) {
|
|
529
|
-
case "select-runtime": {
|
|
530
|
-
const selected = runtimeOptions[selectedIndex];
|
|
531
|
-
if (!selected) break;
|
|
532
|
-
if (selected.type === "default") {
|
|
533
|
-
setResult({ runtime: "default" });
|
|
534
|
-
if (availableLLMs.length > 0) {
|
|
535
|
-
setStep("select-llm");
|
|
536
|
-
} else {
|
|
537
|
-
setStep("select-provider");
|
|
538
|
-
}
|
|
539
|
-
} else {
|
|
540
|
-
setResult({ runtime: selected.type });
|
|
541
|
-
setStep("input-expert-description");
|
|
542
|
-
}
|
|
543
|
-
setSelectedIndex(0);
|
|
544
|
-
break;
|
|
545
|
-
}
|
|
546
|
-
case "select-llm": {
|
|
547
|
-
const selected = llmOptionsWithOther[selectedIndex];
|
|
548
|
-
if (!selected) break;
|
|
549
|
-
if (selected.key === "other") {
|
|
550
|
-
setStep("select-provider");
|
|
551
|
-
} else if (selected.available && selected.provider) {
|
|
552
|
-
setResult((prev) => ({
|
|
553
|
-
...prev,
|
|
554
|
-
provider: selected.provider,
|
|
555
|
-
model: selected.defaultModel
|
|
556
|
-
}));
|
|
557
|
-
setStep("input-expert-description");
|
|
558
|
-
} else if (selected.provider) {
|
|
559
|
-
setResult((prev) => ({ ...prev, provider: selected.provider }));
|
|
560
|
-
setStep("input-api-key");
|
|
561
|
-
}
|
|
562
|
-
setSelectedIndex(0);
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
565
|
-
case "select-provider": {
|
|
566
|
-
const selected = llms[selectedIndex];
|
|
567
|
-
if (!selected) break;
|
|
568
|
-
setResult((prev) => ({ ...prev, provider: selected.provider }));
|
|
569
|
-
setStep("input-api-key");
|
|
570
|
-
setSelectedIndex(0);
|
|
571
|
-
break;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
function handleApiKeySubmit() {
|
|
576
|
-
if (apiKeyInput.trim()) {
|
|
577
|
-
const selectedLlm = llms.find((l) => l.provider === result.provider) ?? llms[0];
|
|
578
|
-
if (!selectedLlm) return;
|
|
579
|
-
setResult((prev) => ({
|
|
580
|
-
...prev,
|
|
581
|
-
provider: selectedLlm.provider,
|
|
582
|
-
model: selectedLlm.defaultModel,
|
|
583
|
-
apiKey: apiKeyInput.trim()
|
|
584
|
-
}));
|
|
585
|
-
setStep("input-expert-description");
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
function handleExpertDescSubmit() {
|
|
589
|
-
if (expertDescInput.trim()) {
|
|
590
|
-
const finalResult = {
|
|
591
|
-
runtime: result.runtime || "default",
|
|
592
|
-
provider: result.provider,
|
|
593
|
-
model: result.model,
|
|
594
|
-
apiKey: result.apiKey,
|
|
595
|
-
expertDescription: expertDescInput.trim()
|
|
596
|
-
};
|
|
597
|
-
onComplete(finalResult);
|
|
598
|
-
setStep("done");
|
|
599
|
-
exit();
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
|
|
603
|
-
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "\u{1F680} Create Expert Wizard" }) }),
|
|
604
|
-
step === "detecting" && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "Detecting available runtimes..." }) }),
|
|
605
|
-
step === "select-runtime" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
606
|
-
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { children: "Select a runtime:" }) }),
|
|
607
|
-
/* @__PURE__ */ jsx(
|
|
608
|
-
SelectableList,
|
|
609
|
-
{
|
|
610
|
-
items: runtimeOptions.map((r) => ({ key: r.key, label: r.label })),
|
|
611
|
-
selectedIndex
|
|
612
|
-
}
|
|
613
|
-
),
|
|
614
|
-
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "\u2191\u2193 to move, Enter to select, Esc to exit" }) })
|
|
615
|
-
] }),
|
|
616
|
-
step === "select-llm" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
617
|
-
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { children: "Select an LLM provider:" }) }),
|
|
618
|
-
/* @__PURE__ */ jsx(
|
|
619
|
-
SelectableList,
|
|
620
|
-
{
|
|
621
|
-
items: llmOptionsWithOther.map((l) => ({ key: l.key, label: l.label })),
|
|
622
|
-
selectedIndex
|
|
623
|
-
}
|
|
624
|
-
),
|
|
625
|
-
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "\u2191\u2193 to move, Enter to select" }) })
|
|
626
|
-
] }),
|
|
627
|
-
step === "select-provider" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
628
|
-
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u26A0 No LLM API keys found." }) }),
|
|
629
|
-
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { children: "Perstack requires an API key from one of these providers:" }) }),
|
|
630
|
-
/* @__PURE__ */ jsx(Box, { flexDirection: "column", marginBottom: 1, children: llms.map((l) => /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
631
|
-
"\u2022 ",
|
|
632
|
-
l.displayName,
|
|
633
|
-
" (",
|
|
634
|
-
l.envVar,
|
|
635
|
-
")"
|
|
636
|
-
] }, l.provider)) }),
|
|
637
|
-
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { children: "Select a provider to configure:" }) }),
|
|
638
|
-
/* @__PURE__ */ jsx(
|
|
639
|
-
SelectableList,
|
|
640
|
-
{
|
|
641
|
-
items: llms.map((l) => ({ key: l.provider, label: l.displayName })),
|
|
642
|
-
selectedIndex
|
|
643
|
-
}
|
|
644
|
-
),
|
|
645
|
-
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "\u2191\u2193 to move, Enter to select" }) })
|
|
646
|
-
] }),
|
|
647
|
-
step === "input-api-key" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
648
|
-
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
649
|
-
"Enter your ",
|
|
650
|
-
llms.find((l) => l.provider === result.provider)?.displayName || "API",
|
|
651
|
-
" ",
|
|
652
|
-
"key:"
|
|
653
|
-
] }) }),
|
|
654
|
-
/* @__PURE__ */ jsx(
|
|
655
|
-
TextInputComponent,
|
|
656
|
-
{
|
|
657
|
-
value: apiKeyInput,
|
|
658
|
-
onChange: setApiKeyInput,
|
|
659
|
-
onSubmit: handleApiKeySubmit,
|
|
660
|
-
placeholder: "sk-...",
|
|
661
|
-
isSecret: true
|
|
662
|
-
}
|
|
663
|
-
),
|
|
664
|
-
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "Type your API key and press Enter" }) })
|
|
665
|
-
] }),
|
|
666
|
-
step === "input-expert-description" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
667
|
-
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, children: isImprovement ? "What improvements do you want?" : "What kind of Expert do you want to create?" }) }),
|
|
668
|
-
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "Describe the Expert's purpose, capabilities, or domain knowledge." }) }),
|
|
669
|
-
/* @__PURE__ */ jsx(
|
|
670
|
-
TextInputComponent,
|
|
671
|
-
{
|
|
672
|
-
value: expertDescInput,
|
|
673
|
-
onChange: setExpertDescInput,
|
|
674
|
-
onSubmit: handleExpertDescSubmit,
|
|
675
|
-
placeholder: "e.g., A code reviewer that checks for TypeScript best practices"
|
|
676
|
-
}
|
|
677
|
-
),
|
|
678
|
-
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "Type your description and press Enter" }) })
|
|
679
|
-
] }),
|
|
680
|
-
step === "done" && /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { color: "green", children: "\u2713 Configuration complete! Starting Expert creation..." }) })
|
|
681
|
-
] });
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// src/tui/wizard/render.tsx
|
|
685
|
-
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
686
|
-
async function renderWizard(options) {
|
|
687
|
-
return new Promise((resolve) => {
|
|
688
|
-
let result = null;
|
|
689
|
-
const { waitUntilExit } = render(
|
|
690
|
-
/* @__PURE__ */ jsx2(
|
|
691
|
-
App,
|
|
692
|
-
{
|
|
693
|
-
llms: options.llms,
|
|
694
|
-
runtimes: options.runtimes,
|
|
695
|
-
isImprovement: options.isImprovement,
|
|
696
|
-
improvementTarget: options.improvementTarget,
|
|
697
|
-
onComplete: (wizardResult) => {
|
|
698
|
-
result = wizardResult;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
)
|
|
702
|
-
);
|
|
703
|
-
waitUntilExit().then(() => resolve(result));
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// bin/cli.ts
|
|
708
|
-
config();
|
|
709
|
-
function getEnvVarName(provider) {
|
|
710
|
-
switch (provider) {
|
|
711
|
-
case "anthropic":
|
|
712
|
-
return "ANTHROPIC_API_KEY";
|
|
713
|
-
case "openai":
|
|
714
|
-
return "OPENAI_API_KEY";
|
|
715
|
-
case "google":
|
|
716
|
-
return "GOOGLE_GENERATIVE_AI_API_KEY";
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
var program = new Command().name("create-expert").description("Create Perstack Experts interactively").version("0.0.1").argument("[expertName]", "Expert name to improve (for improvement mode)").argument("[improvements]", "Improvement description (for improvement mode)").option("--cwd <path>", "Working directory", process.cwd()).action(async (expertName, improvements, options) => {
|
|
720
|
-
const cwd = options?.cwd || process.cwd();
|
|
721
|
-
const isImprovement = Boolean(expertName);
|
|
722
|
-
const improvementTarget = improvements || "";
|
|
723
|
-
const perstackTomlPath = join(cwd, "perstack.toml");
|
|
724
|
-
const agentsMdPath = join(cwd, "AGENTS.md");
|
|
725
|
-
const envPath = join(cwd, ".env");
|
|
726
|
-
const llms = detectAllLLMs();
|
|
727
|
-
const runtimes = detectAllRuntimes();
|
|
728
|
-
const wizardResult = await renderWizard({
|
|
729
|
-
llms,
|
|
730
|
-
runtimes,
|
|
731
|
-
isImprovement,
|
|
732
|
-
improvementTarget
|
|
733
|
-
});
|
|
734
|
-
if (!wizardResult) {
|
|
735
|
-
console.log("Wizard cancelled.");
|
|
736
|
-
process.exit(0);
|
|
737
|
-
}
|
|
738
|
-
if (wizardResult.apiKey && wizardResult.provider) {
|
|
739
|
-
const envVarName = getEnvVarName(wizardResult.provider);
|
|
740
|
-
const envContent = `${envVarName}=${wizardResult.apiKey}
|
|
741
|
-
`;
|
|
742
|
-
if (existsSync(envPath)) {
|
|
743
|
-
const existing = readFileSync(envPath, "utf-8");
|
|
744
|
-
const hasEnvVar = new RegExp(`^${envVarName}=`, "m").test(existing);
|
|
745
|
-
if (!hasEnvVar) {
|
|
746
|
-
writeFileSync(envPath, `${existing}
|
|
747
|
-
${envContent}`);
|
|
748
|
-
console.log(`\u2713 Added ${envVarName} to .env`);
|
|
749
|
-
}
|
|
750
|
-
} else {
|
|
751
|
-
writeFileSync(envPath, envContent);
|
|
752
|
-
console.log(`\u2713 Created .env with ${envVarName}`);
|
|
753
|
-
}
|
|
754
|
-
process.env[envVarName] = wizardResult.apiKey;
|
|
755
|
-
}
|
|
756
|
-
const isDefaultRuntime = wizardResult.runtime === "default";
|
|
757
|
-
if (!isImprovement) {
|
|
758
|
-
if (isDefaultRuntime) {
|
|
759
|
-
const provider = wizardResult.provider || "anthropic";
|
|
760
|
-
const model = wizardResult.model || getDefaultModel(provider);
|
|
761
|
-
const agentsMd = generateAgentsMd({ provider, model });
|
|
762
|
-
writeFileSync(agentsMdPath, agentsMd);
|
|
763
|
-
console.log("\u2713 Created AGENTS.md");
|
|
764
|
-
const createExpertToml = generateCreateExpertToml({ provider, model });
|
|
765
|
-
writeFileSync(perstackTomlPath, createExpertToml);
|
|
766
|
-
console.log("\u2713 Created perstack.toml with create-expert Expert");
|
|
767
|
-
} else {
|
|
768
|
-
const provider = wizardResult.provider || "anthropic";
|
|
769
|
-
const model = wizardResult.model || getDefaultModel(provider);
|
|
770
|
-
const agentsMd = generateAgentsMd({
|
|
771
|
-
provider,
|
|
772
|
-
model,
|
|
773
|
-
runtime: wizardResult.runtime
|
|
774
|
-
});
|
|
775
|
-
writeFileSync(agentsMdPath, agentsMd);
|
|
776
|
-
console.log("\u2713 Created AGENTS.md");
|
|
777
|
-
const createExpertToml = generateCreateExpertToml({
|
|
778
|
-
provider,
|
|
779
|
-
model,
|
|
780
|
-
runtime: wizardResult.runtime
|
|
781
|
-
});
|
|
782
|
-
writeFileSync(perstackTomlPath, createExpertToml);
|
|
783
|
-
console.log("\u2713 Created perstack.toml with create-expert Expert");
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
const expertDescription = wizardResult.expertDescription || "";
|
|
787
|
-
const query = isImprovement ? `Improve the Expert "${expertName}": ${expertDescription}` : `Create a new Expert based on these requirements: ${expertDescription}`;
|
|
788
|
-
const runtimeArg = isDefaultRuntime ? [] : ["--runtime", wizardResult.runtime];
|
|
789
|
-
const args = ["perstack", "start", "create-expert", query, ...runtimeArg];
|
|
790
|
-
const proc = spawn("npx", args, {
|
|
791
|
-
cwd,
|
|
792
|
-
env: process.env,
|
|
793
|
-
stdio: "inherit"
|
|
794
|
-
});
|
|
795
|
-
proc.on("exit", (code) => {
|
|
796
|
-
process.exit(code || 0);
|
|
797
|
-
});
|
|
798
|
-
});
|
|
799
|
-
program.parse();
|
|
800
|
-
//# sourceMappingURL=cli.js.map
|