@agentic-patterns/cli 0.1.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/LICENSE +21 -0
- package/assets/dashboard/assets/index-Bv9u9q_K.js +93 -0
- package/assets/dashboard/assets/index-whvaenaU.css +1 -0
- package/assets/dashboard/index.html +13 -0
- package/dist/cli.js +1266 -0
- package/dist/cli.js.map +1 -0
- package/package.json +53 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1266 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { parseArgs } from "util";
|
|
5
|
+
|
|
6
|
+
// src/commands/agents.ts
|
|
7
|
+
import path from "path";
|
|
8
|
+
var BOLD = "\x1B[1m";
|
|
9
|
+
var DIM = "\x1B[2m";
|
|
10
|
+
var RESET = "\x1B[0m";
|
|
11
|
+
var YELLOW = "\x1B[33m";
|
|
12
|
+
function runAgentsCommand(input) {
|
|
13
|
+
const { agents, loadErrors, root } = input;
|
|
14
|
+
process.stdout.write("\n");
|
|
15
|
+
if (agents.length === 0) {
|
|
16
|
+
process.stdout.write(
|
|
17
|
+
` ${DIM}no agents discovered. Drop a file at ./agents/<name>/agent.ts that default-exports { id, name, agent }.${RESET}
|
|
18
|
+
|
|
19
|
+
`
|
|
20
|
+
);
|
|
21
|
+
} else {
|
|
22
|
+
process.stdout.write(
|
|
23
|
+
` ${BOLD}${agents.length} agent${agents.length === 1 ? "" : "s"}${RESET}
|
|
24
|
+
|
|
25
|
+
`
|
|
26
|
+
);
|
|
27
|
+
const idCol = Math.max(...agents.map((a) => a.id.length), 8);
|
|
28
|
+
for (const a of agents) {
|
|
29
|
+
const rel = path.relative(root, a.file);
|
|
30
|
+
process.stdout.write(` ${a.id.padEnd(idCol)} ${a.name} ${DIM}${rel}${RESET}
|
|
31
|
+
`);
|
|
32
|
+
if (a.description) {
|
|
33
|
+
process.stdout.write(` ${"".padEnd(idCol)} ${DIM}${a.description}${RESET}
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
process.stdout.write("\n");
|
|
38
|
+
}
|
|
39
|
+
if (loadErrors.length > 0) {
|
|
40
|
+
process.stdout.write(` ${YELLOW}${loadErrors.length} load error(s):${RESET}
|
|
41
|
+
`);
|
|
42
|
+
for (const err of loadErrors) {
|
|
43
|
+
process.stdout.write(` ${YELLOW}!${RESET} ${err.file}: ${err.error.message}
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
process.stdout.write("\n");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/commands/config.ts
|
|
51
|
+
import fs from "fs";
|
|
52
|
+
import path2 from "path";
|
|
53
|
+
import { isCancel, password, select, text } from "@clack/prompts";
|
|
54
|
+
var BOLD2 = "\x1B[1m";
|
|
55
|
+
var DIM2 = "\x1B[2m";
|
|
56
|
+
var RESET2 = "\x1B[0m";
|
|
57
|
+
var GREEN = "\x1B[32m";
|
|
58
|
+
var TRACKED_ENV = [
|
|
59
|
+
{ key: "ANTHROPIC_API_KEY", label: "Anthropic API key", secret: true },
|
|
60
|
+
{ key: "OPENAI_API_KEY", label: "OpenAI API key", secret: true },
|
|
61
|
+
{ key: "GOOGLE_GENERATIVE_AI_API_KEY", label: "Google API key", secret: true },
|
|
62
|
+
{ key: "GROQ_API_KEY", label: "Groq API key", secret: true },
|
|
63
|
+
{ key: "MISTRAL_API_KEY", label: "Mistral API key", secret: true },
|
|
64
|
+
{ key: "XAI_API_KEY", label: "xAI API key", secret: true },
|
|
65
|
+
{ key: "DEEPSEEK_API_KEY", label: "DeepSeek API key", secret: true },
|
|
66
|
+
{ key: "OPENROUTER_API_KEY", label: "OpenRouter API key", secret: true },
|
|
67
|
+
{ key: "OLLAMA_HOST", label: "Ollama host URL", secret: false },
|
|
68
|
+
{ key: "AGENT_TIER", label: "Default tier (opus | sonnet | haiku)", secret: false }
|
|
69
|
+
];
|
|
70
|
+
function runConfigStatusCommand(input) {
|
|
71
|
+
const { config } = input;
|
|
72
|
+
const envFile = path2.join(config.root, ".env");
|
|
73
|
+
const envExists = fs.existsSync(envFile);
|
|
74
|
+
process.stdout.write("\n");
|
|
75
|
+
process.stdout.write(` ${BOLD2}config${RESET2}
|
|
76
|
+
|
|
77
|
+
`);
|
|
78
|
+
process.stdout.write(
|
|
79
|
+
` .env ${envExists ? `${GREEN}loaded${RESET2} ${DIM2}from ${path2.relative(process.cwd(), envFile)}${RESET2}` : `${DIM2}not present${RESET2}`}
|
|
80
|
+
`
|
|
81
|
+
);
|
|
82
|
+
const longestKey = Math.max(...TRACKED_ENV.map((e) => e.key.length));
|
|
83
|
+
for (const spec of TRACKED_ENV) {
|
|
84
|
+
const value = process.env[spec.key];
|
|
85
|
+
const padded = spec.key.padEnd(longestKey);
|
|
86
|
+
if (value) {
|
|
87
|
+
const display = spec.secret ? maskSecret(value) : value;
|
|
88
|
+
process.stdout.write(` ${padded} ${GREEN}\u2713${RESET2} ${DIM2}${display}${RESET2}
|
|
89
|
+
`);
|
|
90
|
+
} else {
|
|
91
|
+
process.stdout.write(` ${padded} ${DIM2}\u2014 not set${RESET2}
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
process.stdout.write("\n");
|
|
96
|
+
process.stdout.write(` ${DIM2}ap config set to edit interactively${RESET2}
|
|
97
|
+
|
|
98
|
+
`);
|
|
99
|
+
}
|
|
100
|
+
async function runConfigSetCommand(input) {
|
|
101
|
+
const { config } = input;
|
|
102
|
+
const envFile = path2.join(config.root, ".env");
|
|
103
|
+
const choice = await select({
|
|
104
|
+
message: "Which env var?",
|
|
105
|
+
options: TRACKED_ENV.map((spec2) => {
|
|
106
|
+
const current = process.env[spec2.key];
|
|
107
|
+
const hint = current ? spec2.secret ? maskSecret(current) : current : "not set";
|
|
108
|
+
return {
|
|
109
|
+
value: spec2.key,
|
|
110
|
+
label: spec2.key,
|
|
111
|
+
hint
|
|
112
|
+
};
|
|
113
|
+
})
|
|
114
|
+
});
|
|
115
|
+
if (isCancel(choice)) {
|
|
116
|
+
process.stdout.write(`
|
|
117
|
+
${DIM2}cancelled${RESET2}
|
|
118
|
+
`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const spec = TRACKED_ENV.find((e) => e.key === choice);
|
|
122
|
+
if (!spec) return;
|
|
123
|
+
const prompt = spec.secret ? password : text;
|
|
124
|
+
const value = await prompt({
|
|
125
|
+
message: `${spec.label} (${spec.key}):`,
|
|
126
|
+
placeholder: process.env[spec.key] ?? ""
|
|
127
|
+
});
|
|
128
|
+
if (isCancel(value)) {
|
|
129
|
+
process.stdout.write(`
|
|
130
|
+
${DIM2}cancelled${RESET2}
|
|
131
|
+
`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
upsertEnvFile(envFile, spec.key, String(value));
|
|
135
|
+
process.stdout.write(
|
|
136
|
+
`
|
|
137
|
+
${GREEN}\u2713${RESET2} wrote ${BOLD2}${spec.key}${RESET2} to ${path2.relative(process.cwd(), envFile)}
|
|
138
|
+
|
|
139
|
+
`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
function maskSecret(v) {
|
|
143
|
+
if (v.length <= 8) return "\u2022".repeat(v.length);
|
|
144
|
+
return `${v.slice(0, 4)}${"\u2022".repeat(Math.min(8, v.length - 8))}${v.slice(-4)}`;
|
|
145
|
+
}
|
|
146
|
+
function upsertEnvFile(file, key, value) {
|
|
147
|
+
let lines = [];
|
|
148
|
+
if (fs.existsSync(file)) {
|
|
149
|
+
lines = fs.readFileSync(file, "utf-8").split("\n");
|
|
150
|
+
}
|
|
151
|
+
const prefix = `${key}=`;
|
|
152
|
+
const idx = lines.findIndex((l) => l.trim().startsWith(prefix));
|
|
153
|
+
const formatted = `${key}=${value}`;
|
|
154
|
+
if (idx === -1) {
|
|
155
|
+
if (lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
|
|
156
|
+
lines.push(formatted);
|
|
157
|
+
} else {
|
|
158
|
+
lines[idx] = formatted;
|
|
159
|
+
}
|
|
160
|
+
fs.writeFileSync(
|
|
161
|
+
file,
|
|
162
|
+
`${lines.filter((l, i, arr) => !(l === "" && i === arr.length - 1)).join("\n")}
|
|
163
|
+
`
|
|
164
|
+
);
|
|
165
|
+
if (!process.env[key] || process.env[key] !== value) {
|
|
166
|
+
process.env[key] = value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/commands/init.ts
|
|
171
|
+
import fs2 from "fs";
|
|
172
|
+
import path3 from "path";
|
|
173
|
+
import { fileURLToPath } from "url";
|
|
174
|
+
import { isCancel as isCancel2, select as select2, text as text2 } from "@clack/prompts";
|
|
175
|
+
var DIM3 = "\x1B[2m";
|
|
176
|
+
var BOLD3 = "\x1B[1m";
|
|
177
|
+
var GREEN2 = "\x1B[32m";
|
|
178
|
+
var YELLOW2 = "\x1B[33m";
|
|
179
|
+
var RESET3 = "\x1B[0m";
|
|
180
|
+
var VALID_PROVIDERS = ["anthropic", "openai", "ollama"];
|
|
181
|
+
async function runInitCommand(opts) {
|
|
182
|
+
let targetDir;
|
|
183
|
+
let projectName;
|
|
184
|
+
let monorepoRoot = null;
|
|
185
|
+
if (opts.link) {
|
|
186
|
+
monorepoRoot = resolveMonorepoRoot();
|
|
187
|
+
if (!monorepoRoot) {
|
|
188
|
+
process.stderr.write(
|
|
189
|
+
`error: --link requires the CLI to be run from the agentic-patterns-ts source tree
|
|
190
|
+
`
|
|
191
|
+
);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
const name = opts.targetDir ?? await promptName();
|
|
195
|
+
if (name === null) {
|
|
196
|
+
process.stdout.write(`${DIM3}cancelled.${RESET3}
|
|
197
|
+
`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
projectName = name;
|
|
201
|
+
targetDir = path3.join(monorepoRoot, "examples", projectName);
|
|
202
|
+
} else if (opts.targetDir === void 0) {
|
|
203
|
+
const name = await promptName();
|
|
204
|
+
if (name === null) {
|
|
205
|
+
process.stdout.write(`${DIM3}cancelled.${RESET3}
|
|
206
|
+
`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
projectName = name;
|
|
210
|
+
targetDir = path3.resolve(process.cwd(), projectName);
|
|
211
|
+
} else if (opts.targetDir === ".") {
|
|
212
|
+
targetDir = process.cwd();
|
|
213
|
+
projectName = path3.basename(targetDir);
|
|
214
|
+
} else {
|
|
215
|
+
targetDir = path3.resolve(process.cwd(), opts.targetDir);
|
|
216
|
+
projectName = path3.basename(targetDir);
|
|
217
|
+
}
|
|
218
|
+
let provider;
|
|
219
|
+
if (opts.provider !== void 0) {
|
|
220
|
+
if (!VALID_PROVIDERS.includes(opts.provider)) {
|
|
221
|
+
process.stderr.write(
|
|
222
|
+
`error: invalid --provider "${opts.provider}" (expected anthropic | openai | ollama)
|
|
223
|
+
`
|
|
224
|
+
);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
provider = opts.provider;
|
|
228
|
+
} else {
|
|
229
|
+
const answer = await select2({
|
|
230
|
+
message: "provider",
|
|
231
|
+
options: [
|
|
232
|
+
{ value: "anthropic", label: "Anthropic (Claude)" },
|
|
233
|
+
{ value: "openai", label: "OpenAI (GPT)" },
|
|
234
|
+
{ value: "ollama", label: "Ollama (local)" }
|
|
235
|
+
],
|
|
236
|
+
initialValue: "anthropic"
|
|
237
|
+
});
|
|
238
|
+
if (isCancel2(answer)) {
|
|
239
|
+
process.stdout.write(`${DIM3}cancelled.${RESET3}
|
|
240
|
+
`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
provider = answer;
|
|
244
|
+
}
|
|
245
|
+
if (fs2.existsSync(targetDir)) {
|
|
246
|
+
const stat = fs2.statSync(targetDir);
|
|
247
|
+
if (!stat.isDirectory()) {
|
|
248
|
+
process.stderr.write(`error: target ${targetDir} exists and is not a directory
|
|
249
|
+
`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
const conflicts = ["package.json", "agents", "tsconfig.json"].filter(
|
|
253
|
+
(n) => fs2.existsSync(path3.join(targetDir, n))
|
|
254
|
+
);
|
|
255
|
+
if (conflicts.length > 0) {
|
|
256
|
+
process.stderr.write(
|
|
257
|
+
`error: target ${targetDir} already contains: ${conflicts.join(", ")}
|
|
258
|
+
`
|
|
259
|
+
);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
fs2.mkdirSync(targetDir, { recursive: true });
|
|
264
|
+
}
|
|
265
|
+
const created = [];
|
|
266
|
+
writeFile(
|
|
267
|
+
targetDir,
|
|
268
|
+
"package.json",
|
|
269
|
+
renderPackageJson(projectName, provider, opts.link === true),
|
|
270
|
+
created
|
|
271
|
+
);
|
|
272
|
+
writeFile(targetDir, ".env.example", renderEnvExample(provider), created);
|
|
273
|
+
writeFile(targetDir, "tsconfig.json", renderTsConfig(), created);
|
|
274
|
+
writeFile(targetDir, path3.join("agents", "demo", "agent.ts"), renderAgent(provider), created);
|
|
275
|
+
let pluginNote = null;
|
|
276
|
+
if (opts.withPlugin) {
|
|
277
|
+
const pluginSrc = resolvePluginSource();
|
|
278
|
+
if (pluginSrc) {
|
|
279
|
+
copyDir(pluginSrc.pluginDir, path3.join(targetDir, ".claude-plugin"));
|
|
280
|
+
copyDir(pluginSrc.hooksDir, path3.join(targetDir, "hooks"));
|
|
281
|
+
created.push(".claude-plugin/", "hooks/");
|
|
282
|
+
} else {
|
|
283
|
+
pluginNote = `${YELLOW2}warning${RESET3}: --with-plugin requested but plugin source not found.
|
|
284
|
+
${DIM3}Run from the agentic-patterns-ts source tree, or wait for plugin packaging (Phase 2).${RESET3}`;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const rel = path3.relative(process.cwd(), targetDir) || ".";
|
|
288
|
+
process.stdout.write(`
|
|
289
|
+
${GREEN2}created${RESET3} ${BOLD3}${rel}${RESET3}
|
|
290
|
+
|
|
291
|
+
`);
|
|
292
|
+
for (const f of created) {
|
|
293
|
+
process.stdout.write(` ${DIM3}+ ${f}${RESET3}
|
|
294
|
+
`);
|
|
295
|
+
}
|
|
296
|
+
if (pluginNote) {
|
|
297
|
+
process.stdout.write(`
|
|
298
|
+
${pluginNote}
|
|
299
|
+
`);
|
|
300
|
+
}
|
|
301
|
+
process.stdout.write(`
|
|
302
|
+
${BOLD3}next${RESET3}
|
|
303
|
+
`);
|
|
304
|
+
if (opts.link && monorepoRoot) {
|
|
305
|
+
const rootRel = path3.relative(process.cwd(), monorepoRoot) || ".";
|
|
306
|
+
const projRel = path3.relative(monorepoRoot, targetDir);
|
|
307
|
+
if (rootRel !== ".") {
|
|
308
|
+
process.stdout.write(` cd ${rootRel}
|
|
309
|
+
`);
|
|
310
|
+
}
|
|
311
|
+
process.stdout.write(` pnpm install ${DIM3}# picks up the new example${RESET3}
|
|
312
|
+
`);
|
|
313
|
+
process.stdout.write(` cd ${projRel}
|
|
314
|
+
`);
|
|
315
|
+
process.stdout.write(
|
|
316
|
+
` cp .env.example .env ${DIM3}# fill in your ${envKeyFor(provider)}${RESET3}
|
|
317
|
+
`
|
|
318
|
+
);
|
|
319
|
+
process.stdout.write(` pnpm dev ${DIM3}# launch playground${RESET3}
|
|
320
|
+
|
|
321
|
+
`);
|
|
322
|
+
} else {
|
|
323
|
+
if (rel !== ".") {
|
|
324
|
+
process.stdout.write(` cd ${rel}
|
|
325
|
+
`);
|
|
326
|
+
}
|
|
327
|
+
process.stdout.write(
|
|
328
|
+
` cp .env.example .env ${DIM3}# fill in your ${envKeyFor(provider)}${RESET3}
|
|
329
|
+
`
|
|
330
|
+
);
|
|
331
|
+
process.stdout.write(` pnpm install
|
|
332
|
+
`);
|
|
333
|
+
process.stdout.write(` pnpm dev ${DIM3}# launch playground${RESET3}
|
|
334
|
+
|
|
335
|
+
`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function promptName() {
|
|
339
|
+
const answer = await text2({
|
|
340
|
+
message: "project name",
|
|
341
|
+
placeholder: "my-agents",
|
|
342
|
+
validate: (v) => v.trim().length === 0 ? "name is required" : void 0
|
|
343
|
+
});
|
|
344
|
+
if (isCancel2(answer)) return null;
|
|
345
|
+
return String(answer).trim();
|
|
346
|
+
}
|
|
347
|
+
function renderPackageJson(name, provider, link) {
|
|
348
|
+
const providerDep = providerSdkPackage(provider);
|
|
349
|
+
const apVersion = link ? "workspace:*" : "^0.1.0";
|
|
350
|
+
const pkg = {
|
|
351
|
+
name,
|
|
352
|
+
private: true,
|
|
353
|
+
version: "0.0.1",
|
|
354
|
+
type: "module",
|
|
355
|
+
scripts: {
|
|
356
|
+
dev: "ap playground",
|
|
357
|
+
start: "ap playground",
|
|
358
|
+
agents: "ap agents"
|
|
359
|
+
},
|
|
360
|
+
dependencies: {
|
|
361
|
+
"@agentic-patterns/core": apVersion,
|
|
362
|
+
"@agentic-patterns/runtime": apVersion,
|
|
363
|
+
"@agentic-patterns/cli": apVersion,
|
|
364
|
+
ai: "^4.0.0",
|
|
365
|
+
[providerDep]: "^1.0.0",
|
|
366
|
+
zod: "^3.23.0"
|
|
367
|
+
},
|
|
368
|
+
devDependencies: {
|
|
369
|
+
"@types/node": "^22.0.0",
|
|
370
|
+
typescript: "^5.7.0"
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
return `${JSON.stringify(pkg, null, 2)}
|
|
374
|
+
`;
|
|
375
|
+
}
|
|
376
|
+
function renderEnvExample(provider) {
|
|
377
|
+
const lines = [
|
|
378
|
+
"# Dashboard URL \u2014 used by the Claude Code plugin to ship lifecycle events",
|
|
379
|
+
"AP_DASHBOARD_URL=http://localhost:3000",
|
|
380
|
+
"",
|
|
381
|
+
"# Default model tier \u2014 opus | sonnet | haiku (used by the agent runner)",
|
|
382
|
+
"AGENT_TIER=sonnet",
|
|
383
|
+
""
|
|
384
|
+
];
|
|
385
|
+
if (provider === "anthropic") {
|
|
386
|
+
lines.push("# Anthropic API key (https://console.anthropic.com/)");
|
|
387
|
+
lines.push("ANTHROPIC_API_KEY=sk-ant-...");
|
|
388
|
+
} else if (provider === "openai") {
|
|
389
|
+
lines.push("# OpenAI API key (https://platform.openai.com/api-keys)");
|
|
390
|
+
lines.push("OPENAI_API_KEY=sk-...");
|
|
391
|
+
} else {
|
|
392
|
+
lines.push("# Ollama host (default http://localhost:11434)");
|
|
393
|
+
lines.push("OLLAMA_HOST=http://localhost:11434");
|
|
394
|
+
}
|
|
395
|
+
return `${lines.join("\n")}
|
|
396
|
+
`;
|
|
397
|
+
}
|
|
398
|
+
function renderTsConfig() {
|
|
399
|
+
const cfg = {
|
|
400
|
+
compilerOptions: {
|
|
401
|
+
target: "es2022",
|
|
402
|
+
module: "nodenext",
|
|
403
|
+
moduleResolution: "nodenext",
|
|
404
|
+
lib: ["es2022"],
|
|
405
|
+
outDir: "dist",
|
|
406
|
+
rootDir: "src",
|
|
407
|
+
strict: true,
|
|
408
|
+
noUncheckedIndexedAccess: true,
|
|
409
|
+
noUnusedLocals: true,
|
|
410
|
+
noUnusedParameters: true,
|
|
411
|
+
esModuleInterop: true,
|
|
412
|
+
skipLibCheck: true,
|
|
413
|
+
resolveJsonModule: true,
|
|
414
|
+
declaration: true,
|
|
415
|
+
sourceMap: true
|
|
416
|
+
},
|
|
417
|
+
include: ["agents/**/*.ts", "src/**/*.ts"],
|
|
418
|
+
exclude: ["node_modules", "dist"]
|
|
419
|
+
};
|
|
420
|
+
return `${JSON.stringify(cfg, null, 2)}
|
|
421
|
+
`;
|
|
422
|
+
}
|
|
423
|
+
function renderAgent(provider) {
|
|
424
|
+
return `/**
|
|
425
|
+
* Demo agent \u2014 generated by \`ap init\`.
|
|
426
|
+
*
|
|
427
|
+
* The default export is an AgentRegistration. The \`ap\` CLI discovers this
|
|
428
|
+
* file (via \`agents/**\\/agent.ts\`), builds a runner from your environment
|
|
429
|
+
* (using ${provider}), and wires it into the playground dashboard.
|
|
430
|
+
*
|
|
431
|
+
* pnpm dev # launch the dashboard at http://localhost:3000
|
|
432
|
+
* ap run demo # chat with this agent in the terminal
|
|
433
|
+
*/
|
|
434
|
+
|
|
435
|
+
import {
|
|
436
|
+
AgentBuilder,
|
|
437
|
+
Capability,
|
|
438
|
+
Judgment,
|
|
439
|
+
Mission,
|
|
440
|
+
Persona,
|
|
441
|
+
Responsibility,
|
|
442
|
+
RoleBuilder,
|
|
443
|
+
type ToolDefinition,
|
|
444
|
+
Toolbox,
|
|
445
|
+
} from "@agentic-patterns/core";
|
|
446
|
+
import { z } from "zod";
|
|
447
|
+
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
// A tiny toolbox so the agent has something concrete to do.
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
class GreetingToolbox extends Toolbox {
|
|
453
|
+
readonly name = "greeting_tools";
|
|
454
|
+
readonly description = "Friendly greeting helpers";
|
|
455
|
+
|
|
456
|
+
readonly tools: Record<string, ToolDefinition> = {
|
|
457
|
+
greet: {
|
|
458
|
+
description: "Produce a friendly greeting for a person",
|
|
459
|
+
parameters: z.object({
|
|
460
|
+
name: z.string().describe("Person's name"),
|
|
461
|
+
}),
|
|
462
|
+
execute: async (args) => {
|
|
463
|
+
const { name } = args as { name: string };
|
|
464
|
+
return { greeting: \`Hello, \${name}! Welcome to agentic-patterns.\` };
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
// Build the agent.
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
const role = new RoleBuilder("demo-assistant")
|
|
475
|
+
.withPersona(
|
|
476
|
+
new Persona({
|
|
477
|
+
identity: "A friendly demo assistant that greets people warmly",
|
|
478
|
+
tone: "warm and concise",
|
|
479
|
+
priorities: ["being helpful", "showing the framework off"],
|
|
480
|
+
principles: ["Always use the greet tool when greeting someone"],
|
|
481
|
+
}),
|
|
482
|
+
)
|
|
483
|
+
.withJudgment(
|
|
484
|
+
new Judgment({
|
|
485
|
+
domain: "greetings and small talk",
|
|
486
|
+
heuristics: ["Use the greet tool for any name-based greeting"],
|
|
487
|
+
constraints: ["Stay friendly and concise"],
|
|
488
|
+
}),
|
|
489
|
+
)
|
|
490
|
+
.withCapability(
|
|
491
|
+
new Capability("greeting_tools", "Friendly greeting helpers", new GreetingToolbox()),
|
|
492
|
+
)
|
|
493
|
+
.withResponsibility(
|
|
494
|
+
new Responsibility({
|
|
495
|
+
key: "greet",
|
|
496
|
+
name: "Greet People",
|
|
497
|
+
description: "Greet people warmly using the greet tool",
|
|
498
|
+
}),
|
|
499
|
+
)
|
|
500
|
+
.withDefaultModel("sonnet")
|
|
501
|
+
.build();
|
|
502
|
+
|
|
503
|
+
const mission = new Mission({
|
|
504
|
+
objective: "Demonstrate the @agentic-patterns/core building blocks end-to-end",
|
|
505
|
+
success_criteria: ["Greets users by name", "Uses the greet tool for every greeting"],
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const agent = new AgentBuilder(role).withMission(mission).build();
|
|
509
|
+
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
// Default export \u2014 discovered by \`ap\`. The runner is injected by the CLI.
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
|
|
514
|
+
export default {
|
|
515
|
+
id: "demo",
|
|
516
|
+
name: "Demo",
|
|
517
|
+
description: "A friendly demo assistant generated by \`ap init\`",
|
|
518
|
+
agent,
|
|
519
|
+
};
|
|
520
|
+
`;
|
|
521
|
+
}
|
|
522
|
+
function envKeyFor(provider) {
|
|
523
|
+
switch (provider) {
|
|
524
|
+
case "anthropic":
|
|
525
|
+
return "ANTHROPIC_API_KEY";
|
|
526
|
+
case "openai":
|
|
527
|
+
return "OPENAI_API_KEY";
|
|
528
|
+
case "ollama":
|
|
529
|
+
return "OLLAMA_HOST";
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function providerSdkPackage(provider) {
|
|
533
|
+
switch (provider) {
|
|
534
|
+
case "anthropic":
|
|
535
|
+
return "@ai-sdk/anthropic";
|
|
536
|
+
case "openai":
|
|
537
|
+
return "@ai-sdk/openai";
|
|
538
|
+
case "ollama":
|
|
539
|
+
return "ollama-ai-provider";
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function writeFile(root, rel, contents, log) {
|
|
543
|
+
const dest = path3.join(root, rel);
|
|
544
|
+
fs2.mkdirSync(path3.dirname(dest), { recursive: true });
|
|
545
|
+
fs2.writeFileSync(dest, contents, "utf8");
|
|
546
|
+
log.push(rel);
|
|
547
|
+
}
|
|
548
|
+
function copyDir(src, dest) {
|
|
549
|
+
fs2.cpSync(src, dest, { recursive: true });
|
|
550
|
+
}
|
|
551
|
+
function resolvePluginSource() {
|
|
552
|
+
const root = resolveMonorepoRoot();
|
|
553
|
+
if (!root) return null;
|
|
554
|
+
return { pluginDir: path3.join(root, ".claude-plugin"), hooksDir: path3.join(root, "hooks") };
|
|
555
|
+
}
|
|
556
|
+
function resolveMonorepoRoot() {
|
|
557
|
+
try {
|
|
558
|
+
const here = path3.dirname(fileURLToPath(import.meta.url));
|
|
559
|
+
let cur = here;
|
|
560
|
+
for (let i = 0; i < 8; i++) {
|
|
561
|
+
if (fs2.existsSync(path3.join(cur, "pnpm-workspace.yaml")) && fs2.existsSync(path3.join(cur, "packages", "agent-core"))) {
|
|
562
|
+
return cur;
|
|
563
|
+
}
|
|
564
|
+
const parent = path3.dirname(cur);
|
|
565
|
+
if (parent === cur) break;
|
|
566
|
+
cur = parent;
|
|
567
|
+
}
|
|
568
|
+
return null;
|
|
569
|
+
} catch {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/commands/playground.ts
|
|
575
|
+
import { spawn } from "child_process";
|
|
576
|
+
import { createReadStream, existsSync, statSync } from "fs";
|
|
577
|
+
import path4 from "path";
|
|
578
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
579
|
+
import {
|
|
580
|
+
AgentEventBus,
|
|
581
|
+
InMemoryAdminService,
|
|
582
|
+
InMemoryEventCollector,
|
|
583
|
+
SSEExporter,
|
|
584
|
+
createRunner,
|
|
585
|
+
createToolboxExecutor
|
|
586
|
+
} from "@agentic-patterns/runtime";
|
|
587
|
+
import { createServer } from "@agentic-patterns/server";
|
|
588
|
+
import { serve } from "@hono/node-server";
|
|
589
|
+
async function runPlaygroundCommand(opts) {
|
|
590
|
+
const port = opts.port ?? 3e3;
|
|
591
|
+
const shouldOpen = opts.open !== false;
|
|
592
|
+
const serveDashboard = opts.noDashboard !== true;
|
|
593
|
+
const eventBus = new AgentEventBus();
|
|
594
|
+
const collector = new InMemoryEventCollector();
|
|
595
|
+
collector.attach(eventBus);
|
|
596
|
+
const adminService = new InMemoryAdminService(collector);
|
|
597
|
+
const sseExporter = new SSEExporter();
|
|
598
|
+
sseExporter.attach(eventBus);
|
|
599
|
+
const selection = await createRunner({
|
|
600
|
+
eventBus,
|
|
601
|
+
tier: process.env.AGENT_TIER ?? "sonnet",
|
|
602
|
+
verbose: false
|
|
603
|
+
});
|
|
604
|
+
const { runner } = selection;
|
|
605
|
+
const registrations = opts.agents.map((reg) => ({
|
|
606
|
+
id: reg.id,
|
|
607
|
+
name: reg.name,
|
|
608
|
+
description: reg.description,
|
|
609
|
+
agent: reg.agent,
|
|
610
|
+
runner
|
|
611
|
+
}));
|
|
612
|
+
void createToolboxExecutor;
|
|
613
|
+
const app = createServer({
|
|
614
|
+
agents: registrations,
|
|
615
|
+
adminService,
|
|
616
|
+
eventBus,
|
|
617
|
+
sseExporter
|
|
618
|
+
});
|
|
619
|
+
const dashboardDir = resolveDashboardDir();
|
|
620
|
+
let dashboardMounted = false;
|
|
621
|
+
if (serveDashboard) {
|
|
622
|
+
if (dashboardDir && existsSync(dashboardDir)) {
|
|
623
|
+
mountDashboard(app, dashboardDir);
|
|
624
|
+
dashboardMounted = true;
|
|
625
|
+
} else {
|
|
626
|
+
const where = dashboardDir ?? "<unresolved>";
|
|
627
|
+
process.stderr.write(
|
|
628
|
+
`[playground] warning: dashboard assets not found at ${where} \u2014 API-only mode.
|
|
629
|
+
run \`pnpm --filter @agentic-patterns/cli build\` (or \`build:dashboard\`) to build the SPA bundle.
|
|
630
|
+
`
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
await new Promise((resolve) => {
|
|
635
|
+
serve({ fetch: app.fetch, port }, () => {
|
|
636
|
+
resolve();
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
const baseUrl = `http://localhost:${port}`;
|
|
640
|
+
const agentList = registrations.map((a) => a.name).join(", ") || "(none)";
|
|
641
|
+
const lines = [
|
|
642
|
+
"",
|
|
643
|
+
` api ${baseUrl}`,
|
|
644
|
+
dashboardMounted ? ` dashboard ${baseUrl}` : " dashboard (disabled)",
|
|
645
|
+
` agents ${agentList}`,
|
|
646
|
+
` runner ${selection.source} \u2014 ${selection.reason}`,
|
|
647
|
+
""
|
|
648
|
+
];
|
|
649
|
+
process.stdout.write(`${lines.join("\n")}
|
|
650
|
+
`);
|
|
651
|
+
if (shouldOpen && dashboardMounted) {
|
|
652
|
+
openBrowser(baseUrl);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function resolveDashboardDir() {
|
|
656
|
+
try {
|
|
657
|
+
const here = path4.dirname(fileURLToPath2(import.meta.url));
|
|
658
|
+
return path4.resolve(here, "../assets/dashboard");
|
|
659
|
+
} catch {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
var API_PREFIXES = ["/agents", "/conversations", "/admin", "/health"];
|
|
664
|
+
function isApiPath(pathname) {
|
|
665
|
+
return API_PREFIXES.some((p) => pathname === p || pathname.startsWith(`${p}/`));
|
|
666
|
+
}
|
|
667
|
+
function mountDashboard(app, dashboardDir) {
|
|
668
|
+
const indexPath = path4.join(dashboardDir, "index.html");
|
|
669
|
+
app.get("*", async (c) => {
|
|
670
|
+
if (c.req.method !== "GET") {
|
|
671
|
+
return c.notFound();
|
|
672
|
+
}
|
|
673
|
+
const url = new URL(c.req.url);
|
|
674
|
+
const pathname = decodeURIComponent(url.pathname);
|
|
675
|
+
if (isApiPath(pathname)) {
|
|
676
|
+
return c.notFound();
|
|
677
|
+
}
|
|
678
|
+
const assetPath = pathname === "/" ? indexPath : safeJoin(dashboardDir, pathname);
|
|
679
|
+
if (assetPath && existsSync(assetPath) && statSync(assetPath).isFile()) {
|
|
680
|
+
return streamFile(assetPath);
|
|
681
|
+
}
|
|
682
|
+
if (existsSync(indexPath)) {
|
|
683
|
+
return streamFile(indexPath);
|
|
684
|
+
}
|
|
685
|
+
return c.notFound();
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
function safeJoin(base, rel) {
|
|
689
|
+
const joined = path4.join(base, rel);
|
|
690
|
+
const normalizedBase = path4.resolve(base);
|
|
691
|
+
const normalizedJoined = path4.resolve(joined);
|
|
692
|
+
if (normalizedJoined !== normalizedBase && !normalizedJoined.startsWith(`${normalizedBase}${path4.sep}`)) {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
return normalizedJoined;
|
|
696
|
+
}
|
|
697
|
+
function streamFile(filePath) {
|
|
698
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
699
|
+
const contentType = MIME[ext] ?? "application/octet-stream";
|
|
700
|
+
const nodeStream = createReadStream(filePath);
|
|
701
|
+
const webStream = new ReadableStream({
|
|
702
|
+
start(controller) {
|
|
703
|
+
nodeStream.on("data", (chunk) => {
|
|
704
|
+
const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk);
|
|
705
|
+
controller.enqueue(bytes);
|
|
706
|
+
});
|
|
707
|
+
nodeStream.on("end", () => controller.close());
|
|
708
|
+
nodeStream.on("error", (err) => controller.error(err));
|
|
709
|
+
},
|
|
710
|
+
cancel() {
|
|
711
|
+
nodeStream.destroy();
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
return new Response(webStream, {
|
|
715
|
+
status: 200,
|
|
716
|
+
headers: { "content-type": contentType }
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
var MIME = {
|
|
720
|
+
".html": "text/html; charset=utf-8",
|
|
721
|
+
".js": "application/javascript; charset=utf-8",
|
|
722
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
723
|
+
".css": "text/css; charset=utf-8",
|
|
724
|
+
".json": "application/json; charset=utf-8",
|
|
725
|
+
".svg": "image/svg+xml",
|
|
726
|
+
".png": "image/png",
|
|
727
|
+
".jpg": "image/jpeg",
|
|
728
|
+
".jpeg": "image/jpeg",
|
|
729
|
+
".gif": "image/gif",
|
|
730
|
+
".webp": "image/webp",
|
|
731
|
+
".ico": "image/x-icon",
|
|
732
|
+
".woff": "font/woff",
|
|
733
|
+
".woff2": "font/woff2",
|
|
734
|
+
".ttf": "font/ttf",
|
|
735
|
+
".map": "application/json; charset=utf-8",
|
|
736
|
+
".txt": "text/plain; charset=utf-8"
|
|
737
|
+
};
|
|
738
|
+
function openBrowser(url) {
|
|
739
|
+
const { platform } = process;
|
|
740
|
+
const { cmd, args } = platform === "darwin" ? { cmd: "open", args: [url] } : platform === "win32" ? { cmd: "cmd", args: ["/c", "start", "", url] } : { cmd: "xdg-open", args: [url] };
|
|
741
|
+
try {
|
|
742
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
743
|
+
child.on("error", () => {
|
|
744
|
+
});
|
|
745
|
+
child.unref();
|
|
746
|
+
} catch {
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/commands/run.ts
|
|
751
|
+
import {
|
|
752
|
+
Conversation,
|
|
753
|
+
createRunner as createRunner2,
|
|
754
|
+
createToolboxExecutor as createToolboxExecutor2,
|
|
755
|
+
getAgentEventBus
|
|
756
|
+
} from "@agentic-patterns/runtime";
|
|
757
|
+
import { isCancel as isCancel3, text as text3 } from "@clack/prompts";
|
|
758
|
+
async function runRunCommand(opts) {
|
|
759
|
+
const reg = opts.agents.find((a) => a.id === opts.agentId);
|
|
760
|
+
if (!reg) {
|
|
761
|
+
const available = opts.agents.map((a) => a.id).join(", ") || "(none)";
|
|
762
|
+
process.stderr.write(
|
|
763
|
+
`${red(`agent "${opts.agentId}" not found`)}
|
|
764
|
+
available: ${available}
|
|
765
|
+
`
|
|
766
|
+
);
|
|
767
|
+
process.exit(1);
|
|
768
|
+
}
|
|
769
|
+
const eventBus = getAgentEventBus();
|
|
770
|
+
const { runner } = await createRunner2({ eventBus, verbose: false });
|
|
771
|
+
const conversation = new Conversation(reg.agent, runner, {
|
|
772
|
+
toolExecutor: createToolboxExecutor2(reg.agent)
|
|
773
|
+
});
|
|
774
|
+
if (opts.message !== void 0) {
|
|
775
|
+
await streamOnce(conversation, opts.message);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
await runRepl(conversation, reg);
|
|
779
|
+
}
|
|
780
|
+
async function streamOnce(conversation, message) {
|
|
781
|
+
const controller = new AbortController();
|
|
782
|
+
const onSigint = () => {
|
|
783
|
+
controller.abort();
|
|
784
|
+
};
|
|
785
|
+
process.on("SIGINT", onSigint);
|
|
786
|
+
try {
|
|
787
|
+
await renderStream(conversation.stream(message), controller.signal);
|
|
788
|
+
} finally {
|
|
789
|
+
process.off("SIGINT", onSigint);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
async function runRepl(conversation, reg) {
|
|
793
|
+
const banner = `chatting with ${bold(reg.agent.role.name)} ${dim("\xB7")} ${dim(
|
|
794
|
+
"type /exit to quit"
|
|
795
|
+
)}`;
|
|
796
|
+
process.stdout.write(`${banner}
|
|
797
|
+
|
|
798
|
+
`);
|
|
799
|
+
for (; ; ) {
|
|
800
|
+
const input = await text3({ message: "you" });
|
|
801
|
+
if (isCancel3(input)) {
|
|
802
|
+
process.stdout.write(`${dim("bye.")}
|
|
803
|
+
`);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const line = input.trim();
|
|
807
|
+
if (line === "") continue;
|
|
808
|
+
if (line === "/exit" || line === "/quit") {
|
|
809
|
+
process.stdout.write(`${dim("bye.")}
|
|
810
|
+
`);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const controller = new AbortController();
|
|
814
|
+
const onSigint = () => {
|
|
815
|
+
controller.abort();
|
|
816
|
+
};
|
|
817
|
+
process.on("SIGINT", onSigint);
|
|
818
|
+
try {
|
|
819
|
+
await renderStream(conversation.stream(line), controller.signal);
|
|
820
|
+
} catch (err) {
|
|
821
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
822
|
+
process.stdout.write(`
|
|
823
|
+
${red(`error: ${msg}`)}
|
|
824
|
+
`);
|
|
825
|
+
} finally {
|
|
826
|
+
process.off("SIGINT", onSigint);
|
|
827
|
+
}
|
|
828
|
+
process.stdout.write("\n");
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async function renderStream(stream, signal) {
|
|
832
|
+
let inThinking = false;
|
|
833
|
+
for await (const event of stream) {
|
|
834
|
+
if (signal.aborted) {
|
|
835
|
+
await safeReturn(stream);
|
|
836
|
+
process.stdout.write(`
|
|
837
|
+
${yellow("aborted.")}
|
|
838
|
+
`);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
inThinking = renderEvent(event, inThinking);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function renderEvent(event, inThinking) {
|
|
845
|
+
switch (event.type) {
|
|
846
|
+
case "agent.message.start":
|
|
847
|
+
process.stdout.write(`${bold("assistant")}: `);
|
|
848
|
+
return inThinking;
|
|
849
|
+
case "agent.message.chunk":
|
|
850
|
+
process.stdout.write(event.delta);
|
|
851
|
+
return inThinking;
|
|
852
|
+
case "agent.thinking.start":
|
|
853
|
+
process.stdout.write(`
|
|
854
|
+
${dim("\u{1F4AD} thinking\u2026")}
|
|
855
|
+
`);
|
|
856
|
+
return true;
|
|
857
|
+
case "agent.reasoning":
|
|
858
|
+
if (event.isComplete) {
|
|
859
|
+
process.stdout.write("\n");
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
process.stdout.write(` ${dim(`\u{1F4AD} ${event.content}`)}
|
|
863
|
+
`);
|
|
864
|
+
return true;
|
|
865
|
+
case "agent.tool.start": {
|
|
866
|
+
const args = formatArgs(event.arguments);
|
|
867
|
+
process.stdout.write(`
|
|
868
|
+
${cyan(`\u{1F527} ${event.toolName}(${args})`)}
|
|
869
|
+
`);
|
|
870
|
+
return inThinking;
|
|
871
|
+
}
|
|
872
|
+
case "agent.tool.end": {
|
|
873
|
+
if (event.error) {
|
|
874
|
+
process.stdout.write(` ${red(`\u2717 ${event.error}`)}
|
|
875
|
+
`);
|
|
876
|
+
} else {
|
|
877
|
+
const preview = previewResult(event.result);
|
|
878
|
+
process.stdout.write(` ${dim(`\u2192 ${preview}`)}
|
|
879
|
+
`);
|
|
880
|
+
}
|
|
881
|
+
return inThinking;
|
|
882
|
+
}
|
|
883
|
+
case "agent.message.complete": {
|
|
884
|
+
const footer = `${event.model} \xB7 ${event.inputTokens}\u2193 ${event.outputTokens}\u2191`;
|
|
885
|
+
process.stdout.write(`
|
|
886
|
+
${dim(footer)}
|
|
887
|
+
`);
|
|
888
|
+
return inThinking;
|
|
889
|
+
}
|
|
890
|
+
case "agent.error":
|
|
891
|
+
process.stdout.write(`
|
|
892
|
+
${red(`\u26A0 ${event.errorType}: ${event.message}`)}
|
|
893
|
+
`);
|
|
894
|
+
return inThinking;
|
|
895
|
+
default:
|
|
896
|
+
return inThinking;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async function safeReturn(stream) {
|
|
900
|
+
try {
|
|
901
|
+
await stream.return(void 0);
|
|
902
|
+
} catch {
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function formatArgs(args) {
|
|
906
|
+
try {
|
|
907
|
+
const entries = Object.entries(args);
|
|
908
|
+
if (entries.length === 0) return "";
|
|
909
|
+
const parts = entries.map(([k, v]) => `${k}=${shortJson(v)}`);
|
|
910
|
+
const joined = parts.join(", ");
|
|
911
|
+
return joined.length > 120 ? `${joined.slice(0, 117)}...` : joined;
|
|
912
|
+
} catch {
|
|
913
|
+
return "\u2026";
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function previewResult(result) {
|
|
917
|
+
const s = typeof result === "string" ? result : shortJson(result);
|
|
918
|
+
const oneLine = s.replace(/\s+/g, " ").trim();
|
|
919
|
+
return oneLine.length > 240 ? `${oneLine.slice(0, 237)}...` : oneLine;
|
|
920
|
+
}
|
|
921
|
+
function shortJson(v) {
|
|
922
|
+
try {
|
|
923
|
+
return JSON.stringify(v);
|
|
924
|
+
} catch {
|
|
925
|
+
return String(v);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
function bold(s) {
|
|
929
|
+
return `\x1B[1m${s}\x1B[0m`;
|
|
930
|
+
}
|
|
931
|
+
function dim(s) {
|
|
932
|
+
return `\x1B[2m${s}\x1B[0m`;
|
|
933
|
+
}
|
|
934
|
+
function cyan(s) {
|
|
935
|
+
return `\x1B[36m${s}\x1B[0m`;
|
|
936
|
+
}
|
|
937
|
+
function red(s) {
|
|
938
|
+
return `\x1B[31m${s}\x1B[0m`;
|
|
939
|
+
}
|
|
940
|
+
function yellow(s) {
|
|
941
|
+
return `\x1B[33m${s}\x1B[0m`;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/commands/status.ts
|
|
945
|
+
import path5 from "path";
|
|
946
|
+
var BOLD4 = "\x1B[1m";
|
|
947
|
+
var DIM4 = "\x1B[2m";
|
|
948
|
+
var RESET4 = "\x1B[0m";
|
|
949
|
+
var GREEN3 = "\x1B[32m";
|
|
950
|
+
var YELLOW3 = "\x1B[33m";
|
|
951
|
+
function runStatusCommand(input) {
|
|
952
|
+
const { config, agents, loadErrors } = input;
|
|
953
|
+
const runner = detectRunnerFromEnv();
|
|
954
|
+
const lines = [];
|
|
955
|
+
lines.push("");
|
|
956
|
+
lines.push(`${BOLD4}agentic-patterns${RESET4}`);
|
|
957
|
+
lines.push("");
|
|
958
|
+
lines.push(formatAgentsRow(agents, config.root));
|
|
959
|
+
for (const a of agents) {
|
|
960
|
+
lines.push(` ${GREEN3}\u25CF${RESET4} ${a.id}`);
|
|
961
|
+
}
|
|
962
|
+
if (loadErrors.length > 0) {
|
|
963
|
+
for (const err of loadErrors) {
|
|
964
|
+
lines.push(` ${YELLOW3}!${RESET4} ${err.file}: ${err.error.message}`);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
lines.push(` runner ${runner.provider} ${DIM4}(${runner.detail})${RESET4}`);
|
|
968
|
+
lines.push(` config ${formatConfigRow(config)}`);
|
|
969
|
+
lines.push("");
|
|
970
|
+
lines.push(` ${DIM4}ap run <agent> \xB7 ap playground \xB7 ap -h${RESET4}`);
|
|
971
|
+
lines.push("");
|
|
972
|
+
process.stdout.write(`${lines.join("\n")}
|
|
973
|
+
`);
|
|
974
|
+
}
|
|
975
|
+
function formatAgentsRow(agents, root) {
|
|
976
|
+
const count = agents.length;
|
|
977
|
+
if (count === 0) {
|
|
978
|
+
return ` agents ${DIM4}none discovered (looked in ${path5.relative(process.cwd(), root) || "."}/agents/)${RESET4}`;
|
|
979
|
+
}
|
|
980
|
+
return ` agents ${count} discovered ${DIM4}(./agents/)${RESET4}`;
|
|
981
|
+
}
|
|
982
|
+
function formatConfigRow(config) {
|
|
983
|
+
const parts = [];
|
|
984
|
+
if (config.hasManifest) parts.push("package.json overrides");
|
|
985
|
+
parts.push("see ap config");
|
|
986
|
+
return parts.join(" \xB7 ");
|
|
987
|
+
}
|
|
988
|
+
function detectRunnerFromEnv() {
|
|
989
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
990
|
+
return { provider: "anthropic", detail: "env ANTHROPIC_API_KEY \u2192 claude-sonnet-4-5" };
|
|
991
|
+
}
|
|
992
|
+
if (process.env.OPENAI_API_KEY) {
|
|
993
|
+
return { provider: "openai", detail: "env OPENAI_API_KEY \u2192 gpt-4o" };
|
|
994
|
+
}
|
|
995
|
+
if (process.env.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GOOGLE_API_KEY) {
|
|
996
|
+
return { provider: "google", detail: "env GOOGLE_*_API_KEY \u2192 gemini-2.5-flash" };
|
|
997
|
+
}
|
|
998
|
+
if (process.env.OLLAMA_HOST) {
|
|
999
|
+
const tier = process.env.AGENT_TIER ?? "sonnet";
|
|
1000
|
+
const model = tier === "opus" ? "qwen3:30b-a3b" : tier === "haiku" ? "qwen3:4b" : "qwen3:14b";
|
|
1001
|
+
return {
|
|
1002
|
+
provider: "ollama",
|
|
1003
|
+
detail: `env OLLAMA_HOST \u2192 ${model} (tier=${tier})`
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
return {
|
|
1007
|
+
provider: `${YELLOW3}none${RESET4}`,
|
|
1008
|
+
detail: "set ANTHROPIC_API_KEY, OLLAMA_HOST, or have `claude` CLI on PATH"
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/helpers/config.ts
|
|
1013
|
+
import fs3 from "fs";
|
|
1014
|
+
import path6 from "path";
|
|
1015
|
+
var DEFAULT_AGENT_GLOBS = ["agents/**/agent.{ts,js,mjs}"];
|
|
1016
|
+
function findProjectRoot(from = process.cwd()) {
|
|
1017
|
+
let dir = path6.resolve(from);
|
|
1018
|
+
while (true) {
|
|
1019
|
+
if (fs3.existsSync(path6.join(dir, "package.json"))) return dir;
|
|
1020
|
+
const parent = path6.dirname(dir);
|
|
1021
|
+
if (parent === dir) return null;
|
|
1022
|
+
dir = parent;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
function loadDotEnv(root) {
|
|
1026
|
+
const file = path6.join(root, ".env");
|
|
1027
|
+
if (!fs3.existsSync(file)) return;
|
|
1028
|
+
const text4 = fs3.readFileSync(file, "utf-8");
|
|
1029
|
+
for (const rawLine of text4.split("\n")) {
|
|
1030
|
+
const line = rawLine.trim();
|
|
1031
|
+
if (!line || line.startsWith("#")) continue;
|
|
1032
|
+
const eq = line.indexOf("=");
|
|
1033
|
+
if (eq === -1) continue;
|
|
1034
|
+
const key = line.slice(0, eq).trim();
|
|
1035
|
+
const value = line.slice(eq + 1).trim().replace(/^["'](.*)["']$/, "$1");
|
|
1036
|
+
if (process.env[key] === void 0) process.env[key] = value;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function resolveProjectConfig(from = process.cwd()) {
|
|
1040
|
+
const root = findProjectRoot(from) ?? from;
|
|
1041
|
+
loadDotEnv(root);
|
|
1042
|
+
let manifest = {};
|
|
1043
|
+
let hasManifest = false;
|
|
1044
|
+
const pkgPath = path6.join(root, "package.json");
|
|
1045
|
+
if (fs3.existsSync(pkgPath)) {
|
|
1046
|
+
try {
|
|
1047
|
+
const parsed = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
|
|
1048
|
+
manifest = parsed;
|
|
1049
|
+
hasManifest = Boolean(parsed.agentic);
|
|
1050
|
+
} catch {
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
const agents = normalizeGlobs(manifest.agentic?.agents) ?? DEFAULT_AGENT_GLOBS;
|
|
1054
|
+
const port = manifest.agentic?.port ?? Number.parseInt(process.env.PORT ?? "3000", 10);
|
|
1055
|
+
return { root, agents, port, hasManifest };
|
|
1056
|
+
}
|
|
1057
|
+
function normalizeGlobs(value) {
|
|
1058
|
+
if (!value) return void 0;
|
|
1059
|
+
return Array.isArray(value) ? value : [value];
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/helpers/discover.ts
|
|
1063
|
+
import path7 from "path";
|
|
1064
|
+
import { pathToFileURL } from "url";
|
|
1065
|
+
import { glob } from "tinyglobby";
|
|
1066
|
+
import { register } from "tsx/esm/api";
|
|
1067
|
+
var _tsxRegistered = false;
|
|
1068
|
+
function ensureTsxRegistered() {
|
|
1069
|
+
if (_tsxRegistered) return;
|
|
1070
|
+
register();
|
|
1071
|
+
_tsxRegistered = true;
|
|
1072
|
+
}
|
|
1073
|
+
async function findAgentFiles(root, globs) {
|
|
1074
|
+
const matches = await glob([...globs], {
|
|
1075
|
+
cwd: root,
|
|
1076
|
+
absolute: true,
|
|
1077
|
+
onlyFiles: true,
|
|
1078
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
1079
|
+
});
|
|
1080
|
+
return matches.sort();
|
|
1081
|
+
}
|
|
1082
|
+
async function loadAgentFile(file) {
|
|
1083
|
+
const isTs = file.endsWith(".ts") || file.endsWith(".tsx") || file.endsWith(".mts");
|
|
1084
|
+
if (isTs) ensureTsxRegistered();
|
|
1085
|
+
const mod = await import(pathToFileURL(file).href);
|
|
1086
|
+
let exported = mod.default;
|
|
1087
|
+
if (typeof exported === "function") {
|
|
1088
|
+
exported = await exported();
|
|
1089
|
+
}
|
|
1090
|
+
if (!exported || typeof exported !== "object") {
|
|
1091
|
+
throw new Error(
|
|
1092
|
+
`${file}: default export must be an AgentRegistration object or a function returning one`
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
const { id, name, description, agent } = exported;
|
|
1096
|
+
if (!id || typeof id !== "string") {
|
|
1097
|
+
throw new Error(`${file}: missing or invalid 'id' (must be a non-empty string)`);
|
|
1098
|
+
}
|
|
1099
|
+
if (!name || typeof name !== "string") {
|
|
1100
|
+
throw new Error(`${file}: missing or invalid 'name' (must be a non-empty string)`);
|
|
1101
|
+
}
|
|
1102
|
+
if (!agent || typeof agent !== "object") {
|
|
1103
|
+
throw new Error(`${file}: missing or invalid 'agent' (must be an Agent object)`);
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
id,
|
|
1107
|
+
name,
|
|
1108
|
+
description,
|
|
1109
|
+
agent,
|
|
1110
|
+
file
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
async function discoverAgents(root, globs) {
|
|
1114
|
+
const files = await findAgentFiles(root, globs);
|
|
1115
|
+
const agents = [];
|
|
1116
|
+
const errors = [];
|
|
1117
|
+
for (const file of files) {
|
|
1118
|
+
try {
|
|
1119
|
+
const a = await loadAgentFile(file);
|
|
1120
|
+
agents.push(a);
|
|
1121
|
+
} catch (e) {
|
|
1122
|
+
errors.push({
|
|
1123
|
+
file: path7.relative(root, file),
|
|
1124
|
+
error: e instanceof Error ? e : new Error(String(e))
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1129
|
+
const deduped = [];
|
|
1130
|
+
for (const a of agents) {
|
|
1131
|
+
if (seen.has(a.id)) {
|
|
1132
|
+
errors.push({
|
|
1133
|
+
file: path7.relative(root, a.file),
|
|
1134
|
+
error: new Error(`duplicate agent id "${a.id}" (already registered)`)
|
|
1135
|
+
});
|
|
1136
|
+
} else {
|
|
1137
|
+
seen.add(a.id);
|
|
1138
|
+
deduped.push(a);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return { agents: deduped, errors };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/cli.ts
|
|
1145
|
+
var USAGE = `
|
|
1146
|
+
ap \u2014 agentic patterns
|
|
1147
|
+
|
|
1148
|
+
Usage:
|
|
1149
|
+
ap status dashboard
|
|
1150
|
+
ap <command> [options]
|
|
1151
|
+
|
|
1152
|
+
Commands:
|
|
1153
|
+
agents list discovered agents
|
|
1154
|
+
run <agent> [message] chat in terminal \u2014 interactive or one-shot
|
|
1155
|
+
playground launch UI environment (server + dashboard)
|
|
1156
|
+
init [<dir>] scaffold a new agent project
|
|
1157
|
+
config show env detection status
|
|
1158
|
+
config set interactive .env editor
|
|
1159
|
+
|
|
1160
|
+
Options:
|
|
1161
|
+
-h, --help show this help
|
|
1162
|
+
--port <port> server port for playground (default 3000)
|
|
1163
|
+
--no-dashboard playground without dashboard (API only)
|
|
1164
|
+
--no-open don't auto-open the browser
|
|
1165
|
+
--agents <glob> override agent discovery glob
|
|
1166
|
+
--with-plugin (init) drop the Claude Code plugin too
|
|
1167
|
+
--provider <p> (init) anthropic | openai | ollama
|
|
1168
|
+
--link (init) use file: deps against the local
|
|
1169
|
+
monorepo (dogfooding before publish)
|
|
1170
|
+
`;
|
|
1171
|
+
async function main() {
|
|
1172
|
+
const { values, positionals } = parseArgs({
|
|
1173
|
+
args: process.argv.slice(2),
|
|
1174
|
+
options: {
|
|
1175
|
+
help: { type: "boolean", short: "h" },
|
|
1176
|
+
port: { type: "string" },
|
|
1177
|
+
"no-dashboard": { type: "boolean" },
|
|
1178
|
+
"no-open": { type: "boolean" },
|
|
1179
|
+
agents: { type: "string" },
|
|
1180
|
+
"with-plugin": { type: "boolean" },
|
|
1181
|
+
provider: { type: "string" },
|
|
1182
|
+
link: { type: "boolean" }
|
|
1183
|
+
},
|
|
1184
|
+
allowPositionals: true,
|
|
1185
|
+
strict: false
|
|
1186
|
+
});
|
|
1187
|
+
if (values.help) {
|
|
1188
|
+
process.stdout.write(`${USAGE}
|
|
1189
|
+
`);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
const command = positionals[0];
|
|
1193
|
+
if (command === "init") {
|
|
1194
|
+
const targetDir = positionals[1];
|
|
1195
|
+
const providerRaw = values.provider ? String(values.provider) : void 0;
|
|
1196
|
+
await runInitCommand({
|
|
1197
|
+
targetDir,
|
|
1198
|
+
withPlugin: Boolean(values["with-plugin"]),
|
|
1199
|
+
provider: providerRaw,
|
|
1200
|
+
link: Boolean(values.link)
|
|
1201
|
+
});
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const config = resolveProjectConfig();
|
|
1205
|
+
const globs = values.agents ? [String(values.agents)] : config.agents;
|
|
1206
|
+
const { agents, errors } = await discoverAgents(config.root, globs);
|
|
1207
|
+
switch (command) {
|
|
1208
|
+
case void 0: {
|
|
1209
|
+
runStatusCommand({ config, agents, loadErrors: errors });
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
case "agents": {
|
|
1213
|
+
runAgentsCommand({ agents, loadErrors: errors, root: config.root });
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
case "run": {
|
|
1217
|
+
const agentId = positionals[1];
|
|
1218
|
+
if (!agentId) {
|
|
1219
|
+
process.stderr.write(`error: ap run requires an agent id
|
|
1220
|
+
${USAGE}
|
|
1221
|
+
`);
|
|
1222
|
+
process.exit(1);
|
|
1223
|
+
}
|
|
1224
|
+
const message = positionals.slice(2).join(" ") || void 0;
|
|
1225
|
+
await runRunCommand({ agents, agentId, message });
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
case "playground": {
|
|
1229
|
+
const port = values.port ? Number.parseInt(String(values.port), 10) : config.port;
|
|
1230
|
+
await runPlaygroundCommand({
|
|
1231
|
+
agents,
|
|
1232
|
+
port,
|
|
1233
|
+
noDashboard: Boolean(values["no-dashboard"]),
|
|
1234
|
+
open: !values["no-open"]
|
|
1235
|
+
});
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
case "config": {
|
|
1239
|
+
const sub = positionals[1];
|
|
1240
|
+
if (sub === "set") {
|
|
1241
|
+
await runConfigSetCommand({ config });
|
|
1242
|
+
} else if (sub === void 0) {
|
|
1243
|
+
runConfigStatusCommand({ config });
|
|
1244
|
+
} else {
|
|
1245
|
+
process.stderr.write(`error: unknown config subcommand "${sub}"
|
|
1246
|
+
${USAGE}
|
|
1247
|
+
`);
|
|
1248
|
+
process.exit(1);
|
|
1249
|
+
}
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
default: {
|
|
1253
|
+
process.stderr.write(`error: unknown command "${command}"
|
|
1254
|
+
${USAGE}
|
|
1255
|
+
`);
|
|
1256
|
+
process.exit(1);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
main().catch((err) => {
|
|
1261
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1262
|
+
process.stderr.write(`\x1B[31merror:\x1B[0m ${msg}
|
|
1263
|
+
`);
|
|
1264
|
+
process.exit(1);
|
|
1265
|
+
});
|
|
1266
|
+
//# sourceMappingURL=cli.js.map
|