@poncho-ai/cli 0.2.0 → 0.3.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/.turbo/turbo-build.log +16 -16
- package/CHANGELOG.md +13 -0
- package/dist/chunk-5OOJ5TLB.js +2154 -0
- package/dist/chunk-BDNKTEA4.js +2105 -0
- package/dist/chunk-BVYTTIAO.js +2153 -0
- package/dist/chunk-DBSQYKIE.js +2013 -0
- package/dist/chunk-G5KKCSYK.js +2050 -0
- package/dist/chunk-GOSKILLU.js +1994 -0
- package/dist/chunk-HD7LBEVL.js +2051 -0
- package/dist/chunk-HDKWXGXT.js +2155 -0
- package/dist/chunk-HGZ24QKS.js +1995 -0
- package/dist/chunk-IERP5HOH.js +2064 -0
- package/dist/chunk-KADTBRXA.js +2025 -0
- package/dist/chunk-QSPWSMRC.js +2019 -0
- package/dist/chunk-SMFVGWV6.js +2155 -0
- package/dist/chunk-WWZOZFTC.js +2052 -0
- package/dist/chunk-ZXANINYQ.js +2062 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +5 -1
- package/package.json +2 -2
- package/src/index.ts +364 -5
- package/test/cli.test.ts +15 -1
|
@@ -0,0 +1,2154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LoginRateLimiter,
|
|
3
|
+
SessionStore,
|
|
4
|
+
consumeFirstRunIntro,
|
|
5
|
+
getRequestIp,
|
|
6
|
+
inferConversationTitle,
|
|
7
|
+
initializeOnboardingMarker,
|
|
8
|
+
parseCookies,
|
|
9
|
+
renderIconSvg,
|
|
10
|
+
renderManifest,
|
|
11
|
+
renderServiceWorker,
|
|
12
|
+
renderWebUiHtml,
|
|
13
|
+
setCookie,
|
|
14
|
+
verifyPassphrase
|
|
15
|
+
} from "./chunk-3BEWSRFW.js";
|
|
16
|
+
|
|
17
|
+
// src/index.ts
|
|
18
|
+
import { spawn } from "child_process";
|
|
19
|
+
import { access, cp, mkdir, readFile, writeFile } from "fs/promises";
|
|
20
|
+
import { existsSync } from "fs";
|
|
21
|
+
import {
|
|
22
|
+
createServer
|
|
23
|
+
} from "http";
|
|
24
|
+
import { dirname, relative, resolve } from "path";
|
|
25
|
+
import { createRequire } from "module";
|
|
26
|
+
import { fileURLToPath } from "url";
|
|
27
|
+
import {
|
|
28
|
+
AgentHarness,
|
|
29
|
+
LocalMcpBridge,
|
|
30
|
+
TelemetryEmitter,
|
|
31
|
+
createConversationStore,
|
|
32
|
+
loadPonchoConfig,
|
|
33
|
+
resolveStateConfig
|
|
34
|
+
} from "@poncho-ai/harness";
|
|
35
|
+
import { Command } from "commander";
|
|
36
|
+
import dotenv from "dotenv";
|
|
37
|
+
import YAML from "yaml";
|
|
38
|
+
import { createInterface } from "readline/promises";
|
|
39
|
+
|
|
40
|
+
// src/init-onboarding.ts
|
|
41
|
+
import { stdin, stdout } from "process";
|
|
42
|
+
import { input, password, select } from "@inquirer/prompts";
|
|
43
|
+
import {
|
|
44
|
+
fieldsForScope
|
|
45
|
+
} from "@poncho-ai/sdk";
|
|
46
|
+
var C = {
|
|
47
|
+
reset: "\x1B[0m",
|
|
48
|
+
bold: "\x1B[1m",
|
|
49
|
+
dim: "\x1B[2m",
|
|
50
|
+
cyan: "\x1B[36m"
|
|
51
|
+
};
|
|
52
|
+
var dim = (s) => `${C.dim}${s}${C.reset}`;
|
|
53
|
+
var bold = (s) => `${C.bold}${s}${C.reset}`;
|
|
54
|
+
var INPUT_CARET = "\xBB";
|
|
55
|
+
var shouldAskField = (field, answers) => {
|
|
56
|
+
if (!field.dependsOn) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const value = answers[field.dependsOn.fieldId];
|
|
60
|
+
if (typeof field.dependsOn.equals !== "undefined") {
|
|
61
|
+
return value === field.dependsOn.equals;
|
|
62
|
+
}
|
|
63
|
+
if (field.dependsOn.oneOf) {
|
|
64
|
+
return field.dependsOn.oneOf.includes(value);
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
};
|
|
68
|
+
var parsePromptValue = (field, answer) => {
|
|
69
|
+
if (field.kind === "boolean") {
|
|
70
|
+
const normalized = answer.trim().toLowerCase();
|
|
71
|
+
if (normalized === "y" || normalized === "yes" || normalized === "true") {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (normalized === "n" || normalized === "no" || normalized === "false") {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return Boolean(field.defaultValue);
|
|
78
|
+
}
|
|
79
|
+
if (field.kind === "number") {
|
|
80
|
+
const parsed = Number.parseInt(answer.trim(), 10);
|
|
81
|
+
if (Number.isFinite(parsed)) {
|
|
82
|
+
return parsed;
|
|
83
|
+
}
|
|
84
|
+
return Number(field.defaultValue);
|
|
85
|
+
}
|
|
86
|
+
if (field.kind === "select") {
|
|
87
|
+
const trimmed2 = answer.trim();
|
|
88
|
+
if (field.options && field.options.some((option) => option.value === trimmed2)) {
|
|
89
|
+
return trimmed2;
|
|
90
|
+
}
|
|
91
|
+
const asNumber = Number.parseInt(trimmed2, 10);
|
|
92
|
+
if (Number.isFinite(asNumber) && field.options && asNumber >= 1 && asNumber <= field.options.length) {
|
|
93
|
+
return field.options[asNumber - 1]?.value ?? String(field.defaultValue);
|
|
94
|
+
}
|
|
95
|
+
return String(field.defaultValue);
|
|
96
|
+
}
|
|
97
|
+
const trimmed = answer.trim();
|
|
98
|
+
if (trimmed.length === 0) {
|
|
99
|
+
return String(field.defaultValue);
|
|
100
|
+
}
|
|
101
|
+
return trimmed;
|
|
102
|
+
};
|
|
103
|
+
var askSecret = async (field) => {
|
|
104
|
+
if (!stdin.isTTY) {
|
|
105
|
+
return void 0;
|
|
106
|
+
}
|
|
107
|
+
const hint = field.placeholder ? dim(` (${field.placeholder})`) : "";
|
|
108
|
+
const message = `${field.prompt}${hint}`;
|
|
109
|
+
const value = await password(
|
|
110
|
+
{
|
|
111
|
+
message,
|
|
112
|
+
// true invisible input while typing/pasting
|
|
113
|
+
mask: false,
|
|
114
|
+
theme: {
|
|
115
|
+
prefix: {
|
|
116
|
+
idle: dim(INPUT_CARET),
|
|
117
|
+
done: dim("\u2713")
|
|
118
|
+
},
|
|
119
|
+
style: {
|
|
120
|
+
help: () => ""
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{ input: stdin, output: stdout }
|
|
125
|
+
);
|
|
126
|
+
return value ?? "";
|
|
127
|
+
};
|
|
128
|
+
var askSelectWithArrowKeys = async (field) => {
|
|
129
|
+
if (!field.options || field.options.length === 0) {
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
132
|
+
if (!stdin.isTTY) {
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
const selected = await select(
|
|
136
|
+
{
|
|
137
|
+
message: field.prompt,
|
|
138
|
+
choices: field.options.map((option) => ({
|
|
139
|
+
name: option.label,
|
|
140
|
+
value: option.value
|
|
141
|
+
})),
|
|
142
|
+
default: String(field.defaultValue),
|
|
143
|
+
theme: {
|
|
144
|
+
prefix: {
|
|
145
|
+
idle: dim(INPUT_CARET),
|
|
146
|
+
done: dim("\u2713")
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
{ input: stdin, output: stdout }
|
|
151
|
+
);
|
|
152
|
+
return selected;
|
|
153
|
+
};
|
|
154
|
+
var askBooleanWithArrowKeys = async (field) => {
|
|
155
|
+
if (!stdin.isTTY) {
|
|
156
|
+
return void 0;
|
|
157
|
+
}
|
|
158
|
+
const selected = await select(
|
|
159
|
+
{
|
|
160
|
+
message: field.prompt,
|
|
161
|
+
choices: [
|
|
162
|
+
{ name: "Yes", value: "true" },
|
|
163
|
+
{ name: "No", value: "false" }
|
|
164
|
+
],
|
|
165
|
+
default: field.defaultValue ? "true" : "false",
|
|
166
|
+
theme: {
|
|
167
|
+
prefix: {
|
|
168
|
+
idle: dim(INPUT_CARET),
|
|
169
|
+
done: dim("\u2713")
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{ input: stdin, output: stdout }
|
|
174
|
+
);
|
|
175
|
+
return selected;
|
|
176
|
+
};
|
|
177
|
+
var askTextInput = async (field) => {
|
|
178
|
+
if (!stdin.isTTY) {
|
|
179
|
+
return void 0;
|
|
180
|
+
}
|
|
181
|
+
const answer = await input(
|
|
182
|
+
{
|
|
183
|
+
message: field.prompt,
|
|
184
|
+
default: String(field.defaultValue ?? ""),
|
|
185
|
+
theme: {
|
|
186
|
+
prefix: {
|
|
187
|
+
idle: dim(INPUT_CARET),
|
|
188
|
+
done: dim("\u2713")
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
{ input: stdin, output: stdout }
|
|
193
|
+
);
|
|
194
|
+
return answer;
|
|
195
|
+
};
|
|
196
|
+
var buildDefaultAnswers = () => {
|
|
197
|
+
const answers = {};
|
|
198
|
+
for (const field of fieldsForScope("light")) {
|
|
199
|
+
answers[field.id] = field.defaultValue;
|
|
200
|
+
}
|
|
201
|
+
return answers;
|
|
202
|
+
};
|
|
203
|
+
var askOnboardingQuestions = async (options) => {
|
|
204
|
+
const answers = buildDefaultAnswers();
|
|
205
|
+
const interactive = options.yes === true ? false : options.interactive ?? (stdin.isTTY === true && stdout.isTTY === true);
|
|
206
|
+
if (!interactive) {
|
|
207
|
+
return answers;
|
|
208
|
+
}
|
|
209
|
+
stdout.write("\n");
|
|
210
|
+
stdout.write(` ${bold("Poncho")} ${dim("\xB7 quick setup")}
|
|
211
|
+
`);
|
|
212
|
+
stdout.write("\n");
|
|
213
|
+
const fields = fieldsForScope("light");
|
|
214
|
+
for (const field of fields) {
|
|
215
|
+
if (!shouldAskField(field, answers)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
stdout.write("\n");
|
|
219
|
+
let value;
|
|
220
|
+
if (field.secret) {
|
|
221
|
+
value = await askSecret(field);
|
|
222
|
+
} else if (field.kind === "select") {
|
|
223
|
+
value = await askSelectWithArrowKeys(field);
|
|
224
|
+
} else if (field.kind === "boolean") {
|
|
225
|
+
value = await askBooleanWithArrowKeys(field);
|
|
226
|
+
} else {
|
|
227
|
+
value = await askTextInput(field);
|
|
228
|
+
}
|
|
229
|
+
if (!value || value.trim().length === 0) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
answers[field.id] = parsePromptValue(field, value);
|
|
233
|
+
}
|
|
234
|
+
return answers;
|
|
235
|
+
};
|
|
236
|
+
var getProviderModelName = (provider) => provider === "openai" ? "gpt-4.1" : "claude-opus-4-5";
|
|
237
|
+
var maybeSet = (target, key, value) => {
|
|
238
|
+
if (typeof value === "string" && value.trim().length === 0) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (typeof value === "undefined") {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
target[key] = value;
|
|
245
|
+
};
|
|
246
|
+
var buildConfigFromOnboardingAnswers = (answers) => {
|
|
247
|
+
const storageProvider = String(answers["storage.provider"] ?? "local");
|
|
248
|
+
const memoryEnabled = Boolean(answers["storage.memory.enabled"] ?? true);
|
|
249
|
+
const maxRecallConversations = Number(
|
|
250
|
+
answers["storage.memory.maxRecallConversations"] ?? 20
|
|
251
|
+
);
|
|
252
|
+
const storage = {
|
|
253
|
+
provider: storageProvider,
|
|
254
|
+
memory: {
|
|
255
|
+
enabled: memoryEnabled,
|
|
256
|
+
maxRecallConversations
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
maybeSet(storage, "url", answers["storage.url"]);
|
|
260
|
+
maybeSet(storage, "token", answers["storage.token"]);
|
|
261
|
+
maybeSet(storage, "table", answers["storage.table"]);
|
|
262
|
+
maybeSet(storage, "region", answers["storage.region"]);
|
|
263
|
+
const authRequired = Boolean(answers["auth.required"] ?? false);
|
|
264
|
+
const authType = answers["auth.type"] ?? "bearer";
|
|
265
|
+
const auth = {
|
|
266
|
+
required: authRequired,
|
|
267
|
+
type: authType
|
|
268
|
+
};
|
|
269
|
+
if (authType === "header") {
|
|
270
|
+
maybeSet(auth, "headerName", answers["auth.headerName"]);
|
|
271
|
+
}
|
|
272
|
+
const telemetryEnabled = Boolean(answers["telemetry.enabled"] ?? true);
|
|
273
|
+
const telemetry = {
|
|
274
|
+
enabled: telemetryEnabled
|
|
275
|
+
};
|
|
276
|
+
maybeSet(telemetry, "otlp", answers["telemetry.otlp"]);
|
|
277
|
+
return {
|
|
278
|
+
mcp: [],
|
|
279
|
+
auth,
|
|
280
|
+
storage,
|
|
281
|
+
telemetry
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
var collectEnvVars = (answers) => {
|
|
285
|
+
const envVars = /* @__PURE__ */ new Set();
|
|
286
|
+
const provider = String(answers["model.provider"] ?? "anthropic");
|
|
287
|
+
if (provider === "openai") {
|
|
288
|
+
envVars.add("OPENAI_API_KEY=sk-...");
|
|
289
|
+
} else {
|
|
290
|
+
envVars.add("ANTHROPIC_API_KEY=sk-ant-...");
|
|
291
|
+
}
|
|
292
|
+
const storageProvider = String(answers["storage.provider"] ?? "local");
|
|
293
|
+
if (storageProvider === "redis") {
|
|
294
|
+
envVars.add("REDIS_URL=redis://localhost:6379");
|
|
295
|
+
}
|
|
296
|
+
if (storageProvider === "upstash") {
|
|
297
|
+
envVars.add("UPSTASH_REDIS_REST_URL=https://...");
|
|
298
|
+
envVars.add("UPSTASH_REDIS_REST_TOKEN=...");
|
|
299
|
+
}
|
|
300
|
+
if (storageProvider === "dynamodb") {
|
|
301
|
+
envVars.add("PONCHO_DYNAMODB_TABLE=poncho-conversations");
|
|
302
|
+
envVars.add("AWS_REGION=us-east-1");
|
|
303
|
+
}
|
|
304
|
+
const authRequired = Boolean(answers["auth.required"] ?? false);
|
|
305
|
+
if (authRequired) {
|
|
306
|
+
envVars.add("PONCHO_AUTH_TOKEN=...");
|
|
307
|
+
}
|
|
308
|
+
return Array.from(envVars);
|
|
309
|
+
};
|
|
310
|
+
var collectEnvFileLines = (answers) => {
|
|
311
|
+
const lines = [
|
|
312
|
+
"# Poncho environment configuration",
|
|
313
|
+
"# Fill in empty values before running `poncho dev` or `poncho run --interactive`.",
|
|
314
|
+
"# Tip: keep secrets in `.env` only (never commit them).",
|
|
315
|
+
""
|
|
316
|
+
];
|
|
317
|
+
const modelProvider = String(answers["model.provider"] ?? "anthropic");
|
|
318
|
+
const modelEnvKey = modelProvider === "openai" ? "env.OPENAI_API_KEY" : "env.ANTHROPIC_API_KEY";
|
|
319
|
+
const modelEnvVar = modelProvider === "openai" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY";
|
|
320
|
+
const modelEnvValue = String(answers[modelEnvKey] ?? "");
|
|
321
|
+
lines.push("# Model");
|
|
322
|
+
if (modelEnvValue.length === 0) {
|
|
323
|
+
lines.push(
|
|
324
|
+
modelProvider === "openai" ? "# OpenAI: create an API key at https://platform.openai.com/api-keys" : "# Anthropic: create an API key at https://console.anthropic.com/settings/keys"
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
lines.push(`${modelEnvVar}=${modelEnvValue}`);
|
|
328
|
+
lines.push("");
|
|
329
|
+
const authRequired = Boolean(answers["auth.required"] ?? false);
|
|
330
|
+
const authType = answers["auth.type"] ?? "bearer";
|
|
331
|
+
const authHeaderName = String(answers["auth.headerName"] ?? "x-poncho-key");
|
|
332
|
+
if (authRequired) {
|
|
333
|
+
lines.push("# Auth (API request authentication)");
|
|
334
|
+
if (authType === "bearer") {
|
|
335
|
+
lines.push("# Requests should include: Authorization: Bearer <token>");
|
|
336
|
+
} else if (authType === "header") {
|
|
337
|
+
lines.push(`# Requests should include: ${authHeaderName}: <token>`);
|
|
338
|
+
} else {
|
|
339
|
+
lines.push("# Custom auth mode: read this token in your auth.validate function.");
|
|
340
|
+
}
|
|
341
|
+
lines.push("PONCHO_AUTH_TOKEN=");
|
|
342
|
+
lines.push("");
|
|
343
|
+
}
|
|
344
|
+
const storageProvider = String(answers["storage.provider"] ?? "local");
|
|
345
|
+
if (storageProvider === "redis") {
|
|
346
|
+
lines.push("# Storage (Redis)");
|
|
347
|
+
lines.push("# Run local Redis: docker run -p 6379:6379 redis:7");
|
|
348
|
+
lines.push("# Or use a managed Redis URL from your cloud provider.");
|
|
349
|
+
lines.push("REDIS_URL=");
|
|
350
|
+
lines.push("");
|
|
351
|
+
} else if (storageProvider === "upstash") {
|
|
352
|
+
lines.push("# Storage (Upstash)");
|
|
353
|
+
lines.push("# Create a Redis database at https://console.upstash.com/");
|
|
354
|
+
lines.push("# Copy REST URL + REST TOKEN from the Upstash dashboard.");
|
|
355
|
+
lines.push("UPSTASH_REDIS_REST_URL=");
|
|
356
|
+
lines.push("UPSTASH_REDIS_REST_TOKEN=");
|
|
357
|
+
lines.push("");
|
|
358
|
+
} else if (storageProvider === "dynamodb") {
|
|
359
|
+
lines.push("# Storage (DynamoDB)");
|
|
360
|
+
lines.push("# Create a DynamoDB table for Poncho conversation/state storage.");
|
|
361
|
+
lines.push("# Ensure AWS credentials are configured (AWS_PROFILE or access keys).");
|
|
362
|
+
lines.push("PONCHO_DYNAMODB_TABLE=");
|
|
363
|
+
lines.push("AWS_REGION=");
|
|
364
|
+
lines.push("");
|
|
365
|
+
} else if (storageProvider === "local" || storageProvider === "memory") {
|
|
366
|
+
lines.push(
|
|
367
|
+
storageProvider === "local" ? "# Storage (Local file): no extra env vars required." : "# Storage (In-memory): no extra env vars required, data resets on restart."
|
|
368
|
+
);
|
|
369
|
+
lines.push("");
|
|
370
|
+
}
|
|
371
|
+
const telemetryEnabled = Boolean(answers["telemetry.enabled"] ?? true);
|
|
372
|
+
if (telemetryEnabled) {
|
|
373
|
+
lines.push("# Telemetry (optional)");
|
|
374
|
+
lines.push("# Latitude telemetry setup: https://docs.latitude.so/");
|
|
375
|
+
lines.push("# If not using Latitude yet, you can leave these empty.");
|
|
376
|
+
lines.push("LATITUDE_API_KEY=");
|
|
377
|
+
lines.push("LATITUDE_PROJECT_ID=");
|
|
378
|
+
lines.push("LATITUDE_PATH=");
|
|
379
|
+
lines.push("");
|
|
380
|
+
}
|
|
381
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
382
|
+
lines.pop();
|
|
383
|
+
}
|
|
384
|
+
return lines;
|
|
385
|
+
};
|
|
386
|
+
var runInitOnboarding = async (options) => {
|
|
387
|
+
const answers = await askOnboardingQuestions(options);
|
|
388
|
+
const provider = String(answers["model.provider"] ?? "anthropic");
|
|
389
|
+
const config = buildConfigFromOnboardingAnswers(answers);
|
|
390
|
+
const envExampleLines = collectEnvVars(answers);
|
|
391
|
+
const envFileLines = collectEnvFileLines(answers);
|
|
392
|
+
const envNeedsUserInput = envFileLines.some(
|
|
393
|
+
(line) => line.includes("=") && !line.startsWith("#") && line.endsWith("=")
|
|
394
|
+
);
|
|
395
|
+
return {
|
|
396
|
+
answers,
|
|
397
|
+
config,
|
|
398
|
+
envExample: `${envExampleLines.join("\n")}
|
|
399
|
+
`,
|
|
400
|
+
envFile: envFileLines.length > 0 ? `${envFileLines.join("\n")}
|
|
401
|
+
` : "",
|
|
402
|
+
envNeedsUserInput,
|
|
403
|
+
agentModel: {
|
|
404
|
+
provider: provider === "openai" ? "openai" : "anthropic",
|
|
405
|
+
name: getProviderModelName(provider)
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// src/index.ts
|
|
411
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
412
|
+
var require2 = createRequire(import.meta.url);
|
|
413
|
+
var writeJson = (response, statusCode, payload) => {
|
|
414
|
+
response.writeHead(statusCode, { "Content-Type": "application/json" });
|
|
415
|
+
response.end(JSON.stringify(payload));
|
|
416
|
+
};
|
|
417
|
+
var writeHtml = (response, statusCode, payload) => {
|
|
418
|
+
response.writeHead(statusCode, { "Content-Type": "text/html; charset=utf-8" });
|
|
419
|
+
response.end(payload);
|
|
420
|
+
};
|
|
421
|
+
var readRequestBody = async (request) => {
|
|
422
|
+
const chunks = [];
|
|
423
|
+
for await (const chunk of request) {
|
|
424
|
+
chunks.push(Buffer.from(chunk));
|
|
425
|
+
}
|
|
426
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
427
|
+
return body.length > 0 ? JSON.parse(body) : {};
|
|
428
|
+
};
|
|
429
|
+
var resolveHarnessEnvironment = () => {
|
|
430
|
+
const value = (process.env.PONCHO_ENV ?? process.env.NODE_ENV ?? "development").toLowerCase();
|
|
431
|
+
if (value === "production" || value === "staging") {
|
|
432
|
+
return value;
|
|
433
|
+
}
|
|
434
|
+
return "development";
|
|
435
|
+
};
|
|
436
|
+
var listenOnAvailablePort = async (server, preferredPort) => await new Promise((resolveListen, rejectListen) => {
|
|
437
|
+
let currentPort = preferredPort;
|
|
438
|
+
const tryListen = () => {
|
|
439
|
+
const onListening = () => {
|
|
440
|
+
server.off("error", onError);
|
|
441
|
+
const address = server.address();
|
|
442
|
+
if (address && typeof address === "object" && typeof address.port === "number") {
|
|
443
|
+
resolveListen(address.port);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
resolveListen(currentPort);
|
|
447
|
+
};
|
|
448
|
+
const onError = (error) => {
|
|
449
|
+
server.off("listening", onListening);
|
|
450
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE") {
|
|
451
|
+
currentPort += 1;
|
|
452
|
+
if (currentPort > 65535) {
|
|
453
|
+
rejectListen(
|
|
454
|
+
new Error(
|
|
455
|
+
"No available ports found from the requested port up to 65535."
|
|
456
|
+
)
|
|
457
|
+
);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
setImmediate(tryListen);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
rejectListen(error);
|
|
464
|
+
};
|
|
465
|
+
server.once("listening", onListening);
|
|
466
|
+
server.once("error", onError);
|
|
467
|
+
server.listen(currentPort);
|
|
468
|
+
};
|
|
469
|
+
tryListen();
|
|
470
|
+
});
|
|
471
|
+
var parseParams = (values) => {
|
|
472
|
+
const params = {};
|
|
473
|
+
for (const value of values) {
|
|
474
|
+
const [key, ...rest] = value.split("=");
|
|
475
|
+
if (!key) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
params[key] = rest.join("=");
|
|
479
|
+
}
|
|
480
|
+
return params;
|
|
481
|
+
};
|
|
482
|
+
var AGENT_TEMPLATE = (name, options) => `---
|
|
483
|
+
name: ${name}
|
|
484
|
+
description: A helpful Poncho assistant
|
|
485
|
+
model:
|
|
486
|
+
provider: ${options.modelProvider}
|
|
487
|
+
name: ${options.modelName}
|
|
488
|
+
temperature: 0.2
|
|
489
|
+
limits:
|
|
490
|
+
maxSteps: 50
|
|
491
|
+
timeout: 300
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
# {{name}}
|
|
495
|
+
|
|
496
|
+
You are **{{name}}**, a helpful assistant built with Poncho.
|
|
497
|
+
|
|
498
|
+
Working directory: {{runtime.workingDir}}
|
|
499
|
+
Environment: {{runtime.environment}}
|
|
500
|
+
|
|
501
|
+
## Task Guidance
|
|
502
|
+
|
|
503
|
+
- Use tools when needed
|
|
504
|
+
- Explain your reasoning clearly
|
|
505
|
+
- Ask clarifying questions when requirements are ambiguous
|
|
506
|
+
- For setup/configuration/skills/MCP questions, proactively read \`README.md\` with \`read_file\` before answering.
|
|
507
|
+
- Prefer concrete commands and examples from \`README.md\` over assumptions.
|
|
508
|
+
- Never claim a file/tool change unless the corresponding tool call actually succeeded
|
|
509
|
+
|
|
510
|
+
## Default Capabilities in a Fresh Project
|
|
511
|
+
|
|
512
|
+
- Built-in tools: \`list_directory\` and \`read_file\`
|
|
513
|
+
- \`write_file\` is available in development, and disabled by default in production
|
|
514
|
+
- A starter local skill is included (\`starter-echo\`)
|
|
515
|
+
- Bash/shell commands are **not** available unless you install and enable a shell tool/skill
|
|
516
|
+
- Git operations are only available if a git-capable tool/skill is configured
|
|
517
|
+
`;
|
|
518
|
+
var resolveLocalPackagesRoot = () => {
|
|
519
|
+
const candidate = resolve(__dirname, "..", "..", "harness", "package.json");
|
|
520
|
+
if (existsSync(candidate)) {
|
|
521
|
+
return resolve(__dirname, "..", "..");
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
};
|
|
525
|
+
var resolveCoreDeps = (projectDir) => {
|
|
526
|
+
const packagesRoot = resolveLocalPackagesRoot();
|
|
527
|
+
if (packagesRoot) {
|
|
528
|
+
const harnessAbs = resolve(packagesRoot, "harness");
|
|
529
|
+
const sdkAbs = resolve(packagesRoot, "sdk");
|
|
530
|
+
return {
|
|
531
|
+
harness: `link:${relative(projectDir, harnessAbs)}`,
|
|
532
|
+
sdk: `link:${relative(projectDir, sdkAbs)}`
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
return { harness: "^0.1.0", sdk: "^0.1.0" };
|
|
536
|
+
};
|
|
537
|
+
var PACKAGE_TEMPLATE = (name, projectDir) => {
|
|
538
|
+
const deps = resolveCoreDeps(projectDir);
|
|
539
|
+
return JSON.stringify(
|
|
540
|
+
{
|
|
541
|
+
name,
|
|
542
|
+
private: true,
|
|
543
|
+
type: "module",
|
|
544
|
+
dependencies: {
|
|
545
|
+
"@poncho-ai/harness": deps.harness,
|
|
546
|
+
"@poncho-ai/sdk": deps.sdk
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
null,
|
|
550
|
+
2
|
|
551
|
+
);
|
|
552
|
+
};
|
|
553
|
+
var README_TEMPLATE = (name) => `# ${name}
|
|
554
|
+
|
|
555
|
+
An AI agent built with [Poncho](https://github.com/cesr/poncho-ai).
|
|
556
|
+
|
|
557
|
+
## Prerequisites
|
|
558
|
+
|
|
559
|
+
- Node.js 20+
|
|
560
|
+
- npm (or pnpm/yarn)
|
|
561
|
+
- Anthropic or OpenAI API key
|
|
562
|
+
|
|
563
|
+
## Quick Start
|
|
564
|
+
|
|
565
|
+
\`\`\`bash
|
|
566
|
+
npm install
|
|
567
|
+
# If you didn't enter an API key during init:
|
|
568
|
+
cp .env.example .env
|
|
569
|
+
# Then edit .env and add your API key
|
|
570
|
+
poncho dev
|
|
571
|
+
\`\`\`
|
|
572
|
+
|
|
573
|
+
Open \`http://localhost:3000\` for the web UI.
|
|
574
|
+
|
|
575
|
+
On your first interactive session, the agent introduces its configurable capabilities.
|
|
576
|
+
|
|
577
|
+
## Common Commands
|
|
578
|
+
|
|
579
|
+
\`\`\`bash
|
|
580
|
+
# Local web UI + API server
|
|
581
|
+
poncho dev
|
|
582
|
+
|
|
583
|
+
# Local interactive CLI
|
|
584
|
+
poncho run --interactive
|
|
585
|
+
|
|
586
|
+
# One-off run
|
|
587
|
+
poncho run "Your task here"
|
|
588
|
+
|
|
589
|
+
# Run tests
|
|
590
|
+
poncho test
|
|
591
|
+
|
|
592
|
+
# List available tools
|
|
593
|
+
poncho tools
|
|
594
|
+
\`\`\`
|
|
595
|
+
|
|
596
|
+
## Add Skills
|
|
597
|
+
|
|
598
|
+
Install skills from a local path or remote repository, then verify discovery:
|
|
599
|
+
|
|
600
|
+
\`\`\`bash
|
|
601
|
+
# Install skills into ./skills
|
|
602
|
+
poncho add <repo-or-path>
|
|
603
|
+
|
|
604
|
+
# Verify loaded tools
|
|
605
|
+
poncho tools
|
|
606
|
+
\`\`\`
|
|
607
|
+
|
|
608
|
+
After adding skills, run \`poncho dev\` or \`poncho run --interactive\` and ask the agent to use them.
|
|
609
|
+
|
|
610
|
+
## Configure MCP Servers (Remote)
|
|
611
|
+
|
|
612
|
+
Connect remote MCP servers and expose their tools to the agent:
|
|
613
|
+
|
|
614
|
+
\`\`\`bash
|
|
615
|
+
# Add remote MCP server
|
|
616
|
+
poncho mcp add --url https://mcp.example.com/github --name github --auth-bearer-env GITHUB_TOKEN
|
|
617
|
+
|
|
618
|
+
# List configured servers
|
|
619
|
+
poncho mcp list
|
|
620
|
+
|
|
621
|
+
# Discover and select MCP tools into config allowlist
|
|
622
|
+
poncho mcp tools list github
|
|
623
|
+
poncho mcp tools select github
|
|
624
|
+
|
|
625
|
+
# Remove a server
|
|
626
|
+
poncho mcp remove github
|
|
627
|
+
\`\`\`
|
|
628
|
+
|
|
629
|
+
Set required secrets in \`.env\` (for example, \`GITHUB_TOKEN=...\`).
|
|
630
|
+
|
|
631
|
+
## Tool Intent in Frontmatter
|
|
632
|
+
|
|
633
|
+
Declare tool intent directly in \`AGENT.md\` and \`SKILL.md\` frontmatter:
|
|
634
|
+
|
|
635
|
+
\`\`\`yaml
|
|
636
|
+
tools:
|
|
637
|
+
mcp:
|
|
638
|
+
- github/list_issues
|
|
639
|
+
- github/*
|
|
640
|
+
scripts:
|
|
641
|
+
- starter/scripts/*
|
|
642
|
+
\`\`\`
|
|
643
|
+
|
|
644
|
+
How it works:
|
|
645
|
+
|
|
646
|
+
- \`AGENT.md\` provides fallback MCP intent when no skill is active.
|
|
647
|
+
- \`SKILL.md\` intent applies when you activate that skill (\`activate_skill\`).
|
|
648
|
+
- Skill scripts are accessible by default from each skill's \`scripts/\` directory.
|
|
649
|
+
- \`AGENT.md\` \`tools.scripts\` can still be used to narrow script access when active skills do not set script intent.
|
|
650
|
+
- Active skills are unioned, then filtered by policy in \`poncho.config.js\`.
|
|
651
|
+
- Deactivating a skill (\`deactivate_skill\`) removes its MCP tools from runtime registration.
|
|
652
|
+
|
|
653
|
+
Pattern format is strict slash-only:
|
|
654
|
+
|
|
655
|
+
- MCP: \`server/tool\`, \`server/*\`
|
|
656
|
+
- Scripts: \`skill/scripts/file.ts\`, \`skill/scripts/*\`
|
|
657
|
+
|
|
658
|
+
## Configuration
|
|
659
|
+
|
|
660
|
+
Core files:
|
|
661
|
+
|
|
662
|
+
- \`AGENT.md\`: behavior, model selection, runtime guidance
|
|
663
|
+
- \`poncho.config.js\`: runtime config (storage, auth, telemetry, MCP, tools)
|
|
664
|
+
- \`.env\`: secrets and environment variables
|
|
665
|
+
|
|
666
|
+
Example \`poncho.config.js\`:
|
|
667
|
+
|
|
668
|
+
\`\`\`javascript
|
|
669
|
+
export default {
|
|
670
|
+
storage: {
|
|
671
|
+
provider: "local", // local | memory | redis | upstash | dynamodb
|
|
672
|
+
memory: {
|
|
673
|
+
enabled: true,
|
|
674
|
+
maxRecallConversations: 20,
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
auth: {
|
|
678
|
+
required: false,
|
|
679
|
+
},
|
|
680
|
+
telemetry: {
|
|
681
|
+
enabled: true,
|
|
682
|
+
},
|
|
683
|
+
mcp: [
|
|
684
|
+
{
|
|
685
|
+
name: "github",
|
|
686
|
+
url: "https://mcp.example.com/github",
|
|
687
|
+
auth: { type: "bearer", tokenEnv: "GITHUB_TOKEN" },
|
|
688
|
+
tools: {
|
|
689
|
+
mode: "allowlist",
|
|
690
|
+
include: ["github/list_issues", "github/get_issue"],
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
],
|
|
694
|
+
scripts: {
|
|
695
|
+
mode: "allowlist",
|
|
696
|
+
include: ["starter/scripts/*"],
|
|
697
|
+
},
|
|
698
|
+
tools: {
|
|
699
|
+
defaults: {
|
|
700
|
+
list_directory: true,
|
|
701
|
+
read_file: true,
|
|
702
|
+
write_file: true, // still gated by environment/policy
|
|
703
|
+
},
|
|
704
|
+
byEnvironment: {
|
|
705
|
+
production: {
|
|
706
|
+
read_file: false, // example override
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
};
|
|
711
|
+
\`\`\`
|
|
712
|
+
|
|
713
|
+
## Project Structure
|
|
714
|
+
|
|
715
|
+
\`\`\`
|
|
716
|
+
${name}/
|
|
717
|
+
\u251C\u2500\u2500 AGENT.md # Agent definition and system prompt
|
|
718
|
+
\u251C\u2500\u2500 poncho.config.js # Configuration (MCP servers, auth, etc.)
|
|
719
|
+
\u251C\u2500\u2500 package.json # Dependencies
|
|
720
|
+
\u251C\u2500\u2500 .env.example # Environment variables template
|
|
721
|
+
\u251C\u2500\u2500 tests/
|
|
722
|
+
\u2502 \u2514\u2500\u2500 basic.yaml # Test suite
|
|
723
|
+
\u2514\u2500\u2500 skills/
|
|
724
|
+
\u2514\u2500\u2500 starter/
|
|
725
|
+
\u251C\u2500\u2500 SKILL.md
|
|
726
|
+
\u2514\u2500\u2500 scripts/
|
|
727
|
+
\u2514\u2500\u2500 starter-echo.ts
|
|
728
|
+
\`\`\`
|
|
729
|
+
|
|
730
|
+
## Deployment
|
|
731
|
+
|
|
732
|
+
\`\`\`bash
|
|
733
|
+
# Build for Vercel
|
|
734
|
+
poncho build vercel
|
|
735
|
+
cd .poncho-build/vercel && vercel deploy --prod
|
|
736
|
+
|
|
737
|
+
# Build for Docker
|
|
738
|
+
poncho build docker
|
|
739
|
+
docker build -t ${name} .
|
|
740
|
+
\`\`\`
|
|
741
|
+
|
|
742
|
+
For full reference:
|
|
743
|
+
https://github.com/cesr/poncho-ai
|
|
744
|
+
`;
|
|
745
|
+
var ENV_TEMPLATE = "ANTHROPIC_API_KEY=sk-ant-...\n";
|
|
746
|
+
var GITIGNORE_TEMPLATE = ".env\nnode_modules\ndist\n.poncho-build\n.poncho/\ninteractive-session.json\n";
|
|
747
|
+
var VERCEL_RUNTIME_DEPENDENCIES = {
|
|
748
|
+
"@anthropic-ai/sdk": "^0.74.0",
|
|
749
|
+
"@aws-sdk/client-dynamodb": "^3.988.0",
|
|
750
|
+
"@latitude-data/telemetry": "^2.0.2",
|
|
751
|
+
commander: "^12.0.0",
|
|
752
|
+
dotenv: "^16.4.0",
|
|
753
|
+
jiti: "^2.6.1",
|
|
754
|
+
mustache: "^4.2.0",
|
|
755
|
+
openai: "^6.3.0",
|
|
756
|
+
redis: "^5.10.0",
|
|
757
|
+
yaml: "^2.8.1"
|
|
758
|
+
};
|
|
759
|
+
var TEST_TEMPLATE = `tests:
|
|
760
|
+
- name: "Basic sanity"
|
|
761
|
+
task: "What is 2 + 2?"
|
|
762
|
+
expect:
|
|
763
|
+
contains: "4"
|
|
764
|
+
`;
|
|
765
|
+
var SKILL_TEMPLATE = `---
|
|
766
|
+
name: starter-skill
|
|
767
|
+
description: Starter local skill template
|
|
768
|
+
---
|
|
769
|
+
|
|
770
|
+
# Starter Skill
|
|
771
|
+
|
|
772
|
+
This is a starter local skill created by \`poncho init\`.
|
|
773
|
+
|
|
774
|
+
## Authoring Notes
|
|
775
|
+
|
|
776
|
+
- Put executable JavaScript/TypeScript files in \`scripts/\`.
|
|
777
|
+
- Ask the agent to call \`run_skill_script\` with \`skill\`, \`script\`, and optional \`input\`.
|
|
778
|
+
`;
|
|
779
|
+
var SKILL_TOOL_TEMPLATE = `export default async function run(input) {
|
|
780
|
+
const message = typeof input?.message === "string" ? input.message : "";
|
|
781
|
+
return { echoed: message };
|
|
782
|
+
}
|
|
783
|
+
`;
|
|
784
|
+
var ensureFile = async (path, content) => {
|
|
785
|
+
await mkdir(dirname(path), { recursive: true });
|
|
786
|
+
await writeFile(path, content, { encoding: "utf8", flag: "wx" });
|
|
787
|
+
};
|
|
788
|
+
var copyIfExists = async (sourcePath, destinationPath) => {
|
|
789
|
+
try {
|
|
790
|
+
await access(sourcePath);
|
|
791
|
+
} catch {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
await mkdir(dirname(destinationPath), { recursive: true });
|
|
795
|
+
await cp(sourcePath, destinationPath, { recursive: true });
|
|
796
|
+
};
|
|
797
|
+
var resolveCliEntrypoint = async () => {
|
|
798
|
+
const sourceEntrypoint = resolve(packageRoot, "src", "index.ts");
|
|
799
|
+
try {
|
|
800
|
+
await access(sourceEntrypoint);
|
|
801
|
+
return sourceEntrypoint;
|
|
802
|
+
} catch {
|
|
803
|
+
return resolve(packageRoot, "dist", "index.js");
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
var buildVercelHandlerBundle = async (outDir) => {
|
|
807
|
+
const { build: esbuild } = await import("esbuild");
|
|
808
|
+
const cliEntrypoint = await resolveCliEntrypoint();
|
|
809
|
+
const tempEntry = resolve(outDir, "api", "_entry.js");
|
|
810
|
+
await writeFile(
|
|
811
|
+
tempEntry,
|
|
812
|
+
`import { createRequestHandler } from ${JSON.stringify(cliEntrypoint)};
|
|
813
|
+
let handlerPromise;
|
|
814
|
+
export default async function handler(req, res) {
|
|
815
|
+
try {
|
|
816
|
+
if (!handlerPromise) {
|
|
817
|
+
handlerPromise = createRequestHandler({ workingDir: process.cwd() });
|
|
818
|
+
}
|
|
819
|
+
const requestHandler = await handlerPromise;
|
|
820
|
+
await requestHandler(req, res);
|
|
821
|
+
} catch (error) {
|
|
822
|
+
console.error("Handler error:", error);
|
|
823
|
+
if (!res.headersSent) {
|
|
824
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
825
|
+
res.end(JSON.stringify({ error: "Internal server error", message: error?.message || "Unknown error" }));
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
`,
|
|
830
|
+
"utf8"
|
|
831
|
+
);
|
|
832
|
+
await esbuild({
|
|
833
|
+
entryPoints: [tempEntry],
|
|
834
|
+
bundle: true,
|
|
835
|
+
platform: "node",
|
|
836
|
+
format: "esm",
|
|
837
|
+
target: "node20",
|
|
838
|
+
outfile: resolve(outDir, "api", "index.js"),
|
|
839
|
+
sourcemap: false,
|
|
840
|
+
legalComments: "none",
|
|
841
|
+
external: [
|
|
842
|
+
...Object.keys(VERCEL_RUNTIME_DEPENDENCIES),
|
|
843
|
+
"@anthropic-ai/sdk/*",
|
|
844
|
+
"child_process",
|
|
845
|
+
"fs",
|
|
846
|
+
"fs/promises",
|
|
847
|
+
"http",
|
|
848
|
+
"https",
|
|
849
|
+
"path",
|
|
850
|
+
"module",
|
|
851
|
+
"url",
|
|
852
|
+
"readline",
|
|
853
|
+
"readline/promises",
|
|
854
|
+
"crypto",
|
|
855
|
+
"stream",
|
|
856
|
+
"events",
|
|
857
|
+
"util",
|
|
858
|
+
"os",
|
|
859
|
+
"zlib",
|
|
860
|
+
"net",
|
|
861
|
+
"tls",
|
|
862
|
+
"dns",
|
|
863
|
+
"assert",
|
|
864
|
+
"buffer",
|
|
865
|
+
"timers",
|
|
866
|
+
"timers/promises",
|
|
867
|
+
"node:child_process",
|
|
868
|
+
"node:fs",
|
|
869
|
+
"node:fs/promises",
|
|
870
|
+
"node:http",
|
|
871
|
+
"node:https",
|
|
872
|
+
"node:path",
|
|
873
|
+
"node:module",
|
|
874
|
+
"node:url",
|
|
875
|
+
"node:readline",
|
|
876
|
+
"node:readline/promises",
|
|
877
|
+
"node:crypto",
|
|
878
|
+
"node:stream",
|
|
879
|
+
"node:events",
|
|
880
|
+
"node:util",
|
|
881
|
+
"node:os",
|
|
882
|
+
"node:zlib",
|
|
883
|
+
"node:net",
|
|
884
|
+
"node:tls",
|
|
885
|
+
"node:dns",
|
|
886
|
+
"node:assert",
|
|
887
|
+
"node:buffer",
|
|
888
|
+
"node:timers",
|
|
889
|
+
"node:timers/promises"
|
|
890
|
+
]
|
|
891
|
+
});
|
|
892
|
+
};
|
|
893
|
+
var renderConfigFile = (config) => `export default ${JSON.stringify(config, null, 2)}
|
|
894
|
+
`;
|
|
895
|
+
var writeConfigFile = async (workingDir, config) => {
|
|
896
|
+
const serialized = renderConfigFile(config);
|
|
897
|
+
await writeFile(resolve(workingDir, "poncho.config.js"), serialized, "utf8");
|
|
898
|
+
};
|
|
899
|
+
var ensureEnvPlaceholder = async (filePath, key) => {
|
|
900
|
+
const normalizedKey = key.trim();
|
|
901
|
+
if (!normalizedKey) {
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
let content = "";
|
|
905
|
+
try {
|
|
906
|
+
content = await readFile(filePath, "utf8");
|
|
907
|
+
} catch {
|
|
908
|
+
await writeFile(filePath, `${normalizedKey}=
|
|
909
|
+
`, "utf8");
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
const present = content.split(/\r?\n/).some((line) => line.trimStart().startsWith(`${normalizedKey}=`));
|
|
913
|
+
if (present) {
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
const withTrailingNewline = content.length === 0 || content.endsWith("\n") ? content : `${content}
|
|
917
|
+
`;
|
|
918
|
+
await writeFile(filePath, `${withTrailingNewline}${normalizedKey}=
|
|
919
|
+
`, "utf8");
|
|
920
|
+
return true;
|
|
921
|
+
};
|
|
922
|
+
var removeEnvPlaceholder = async (filePath, key) => {
|
|
923
|
+
const normalizedKey = key.trim();
|
|
924
|
+
if (!normalizedKey) {
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
let content = "";
|
|
928
|
+
try {
|
|
929
|
+
content = await readFile(filePath, "utf8");
|
|
930
|
+
} catch {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
const lines = content.split(/\r?\n/);
|
|
934
|
+
const filtered = lines.filter((line) => !line.trimStart().startsWith(`${normalizedKey}=`));
|
|
935
|
+
if (filtered.length === lines.length) {
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
const nextContent = filtered.join("\n").replace(/\n+$/, "");
|
|
939
|
+
await writeFile(filePath, nextContent.length > 0 ? `${nextContent}
|
|
940
|
+
` : "", "utf8");
|
|
941
|
+
return true;
|
|
942
|
+
};
|
|
943
|
+
var gitInit = (cwd) => new Promise((resolve2) => {
|
|
944
|
+
const child = spawn("git", ["init"], { cwd, stdio: "ignore" });
|
|
945
|
+
child.on("error", () => resolve2(false));
|
|
946
|
+
child.on("close", (code) => resolve2(code === 0));
|
|
947
|
+
});
|
|
948
|
+
var initProject = async (projectName, options) => {
|
|
949
|
+
const baseDir = options?.workingDir ?? process.cwd();
|
|
950
|
+
const projectDir = resolve(baseDir, projectName);
|
|
951
|
+
await mkdir(projectDir, { recursive: true });
|
|
952
|
+
const onboardingOptions = options?.onboarding ?? {
|
|
953
|
+
yes: true,
|
|
954
|
+
interactive: false
|
|
955
|
+
};
|
|
956
|
+
const onboarding = await runInitOnboarding(onboardingOptions);
|
|
957
|
+
const G = "\x1B[32m";
|
|
958
|
+
const D = "\x1B[2m";
|
|
959
|
+
const B = "\x1B[1m";
|
|
960
|
+
const CY = "\x1B[36m";
|
|
961
|
+
const YW = "\x1B[33m";
|
|
962
|
+
const R = "\x1B[0m";
|
|
963
|
+
process.stdout.write("\n");
|
|
964
|
+
const scaffoldFiles = [
|
|
965
|
+
{ path: "AGENT.md", content: AGENT_TEMPLATE(projectName, { modelProvider: onboarding.agentModel.provider, modelName: onboarding.agentModel.name }) },
|
|
966
|
+
{ path: "poncho.config.js", content: renderConfigFile(onboarding.config) },
|
|
967
|
+
{ path: "package.json", content: PACKAGE_TEMPLATE(projectName, projectDir) },
|
|
968
|
+
{ path: "README.md", content: README_TEMPLATE(projectName) },
|
|
969
|
+
{ path: ".env.example", content: options?.envExampleOverride ?? onboarding.envExample ?? ENV_TEMPLATE },
|
|
970
|
+
{ path: ".gitignore", content: GITIGNORE_TEMPLATE },
|
|
971
|
+
{ path: "tests/basic.yaml", content: TEST_TEMPLATE },
|
|
972
|
+
{ path: "skills/starter/SKILL.md", content: SKILL_TEMPLATE },
|
|
973
|
+
{ path: "skills/starter/scripts/starter-echo.ts", content: SKILL_TOOL_TEMPLATE }
|
|
974
|
+
];
|
|
975
|
+
if (onboarding.envFile) {
|
|
976
|
+
scaffoldFiles.push({ path: ".env", content: onboarding.envFile });
|
|
977
|
+
}
|
|
978
|
+
for (const file of scaffoldFiles) {
|
|
979
|
+
await ensureFile(resolve(projectDir, file.path), file.content);
|
|
980
|
+
process.stdout.write(` ${D}+${R} ${D}${file.path}${R}
|
|
981
|
+
`);
|
|
982
|
+
}
|
|
983
|
+
await initializeOnboardingMarker(projectDir, {
|
|
984
|
+
allowIntro: !(onboardingOptions.yes ?? false)
|
|
985
|
+
});
|
|
986
|
+
process.stdout.write("\n");
|
|
987
|
+
try {
|
|
988
|
+
await runPnpmInstall(projectDir);
|
|
989
|
+
process.stdout.write(` ${G}\u2713${R} ${D}Installed dependencies${R}
|
|
990
|
+
`);
|
|
991
|
+
} catch {
|
|
992
|
+
process.stdout.write(
|
|
993
|
+
` ${YW}!${R} Could not install dependencies \u2014 run ${D}pnpm install${R} manually
|
|
994
|
+
`
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
const gitOk = await gitInit(projectDir);
|
|
998
|
+
if (gitOk) {
|
|
999
|
+
process.stdout.write(` ${G}\u2713${R} ${D}Initialized git${R}
|
|
1000
|
+
`);
|
|
1001
|
+
}
|
|
1002
|
+
process.stdout.write(` ${G}\u2713${R} ${B}${projectName}${R} is ready
|
|
1003
|
+
`);
|
|
1004
|
+
process.stdout.write("\n");
|
|
1005
|
+
process.stdout.write(` ${B}Get started${R}
|
|
1006
|
+
`);
|
|
1007
|
+
process.stdout.write("\n");
|
|
1008
|
+
process.stdout.write(` ${D}$${R} cd ${projectName}
|
|
1009
|
+
`);
|
|
1010
|
+
process.stdout.write("\n");
|
|
1011
|
+
process.stdout.write(` ${CY}Web UI${R} ${D}$${R} poncho dev
|
|
1012
|
+
`);
|
|
1013
|
+
process.stdout.write(` ${CY}CLI interactive${R} ${D}$${R} poncho run --interactive
|
|
1014
|
+
`);
|
|
1015
|
+
process.stdout.write("\n");
|
|
1016
|
+
if (onboarding.envNeedsUserInput) {
|
|
1017
|
+
process.stdout.write(
|
|
1018
|
+
` ${YW}!${R} Make sure you add your keys to the ${B}.env${R} file.
|
|
1019
|
+
`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
process.stdout.write(` ${D}The agent will introduce itself on your first session.${R}
|
|
1023
|
+
`);
|
|
1024
|
+
process.stdout.write("\n");
|
|
1025
|
+
};
|
|
1026
|
+
var updateAgentGuidance = async (workingDir) => {
|
|
1027
|
+
const agentPath = resolve(workingDir, "AGENT.md");
|
|
1028
|
+
const content = await readFile(agentPath, "utf8");
|
|
1029
|
+
const guidanceSectionPattern = /\n## Configuration Assistant Context[\s\S]*?(?=\n## |\n# |$)|\n## Skill Authoring Guidance[\s\S]*?(?=\n## |\n# |$)/g;
|
|
1030
|
+
const normalized = content.replace(/\s+$/g, "");
|
|
1031
|
+
const updated = normalized.replace(guidanceSectionPattern, "").replace(/\n{3,}/g, "\n\n");
|
|
1032
|
+
if (updated === normalized) {
|
|
1033
|
+
process.stdout.write("AGENT.md does not contain deprecated embedded local guidance.\n");
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
await writeFile(agentPath, `${updated}
|
|
1037
|
+
`, "utf8");
|
|
1038
|
+
process.stdout.write("Removed deprecated embedded local guidance from AGENT.md.\n");
|
|
1039
|
+
return true;
|
|
1040
|
+
};
|
|
1041
|
+
var formatSseEvent = (event) => `event: ${event.type}
|
|
1042
|
+
data: ${JSON.stringify(event)}
|
|
1043
|
+
|
|
1044
|
+
`;
|
|
1045
|
+
var createRequestHandler = async (options) => {
|
|
1046
|
+
const workingDir = options?.workingDir ?? process.cwd();
|
|
1047
|
+
dotenv.config({ path: resolve(workingDir, ".env") });
|
|
1048
|
+
const config = await loadPonchoConfig(workingDir);
|
|
1049
|
+
let agentName = "Agent";
|
|
1050
|
+
let agentModelProvider = "anthropic";
|
|
1051
|
+
let agentModelName = "claude-opus-4-5";
|
|
1052
|
+
try {
|
|
1053
|
+
const agentMd = await readFile(resolve(workingDir, "AGENT.md"), "utf8");
|
|
1054
|
+
const nameMatch = agentMd.match(/^name:\s*(.+)$/m);
|
|
1055
|
+
const providerMatch = agentMd.match(/^\s{2}provider:\s*(.+)$/m);
|
|
1056
|
+
const modelMatch = agentMd.match(/^\s{2}name:\s*(.+)$/m);
|
|
1057
|
+
if (nameMatch?.[1]) {
|
|
1058
|
+
agentName = nameMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
1059
|
+
}
|
|
1060
|
+
if (providerMatch?.[1]) {
|
|
1061
|
+
agentModelProvider = providerMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
1062
|
+
}
|
|
1063
|
+
if (modelMatch?.[1]) {
|
|
1064
|
+
agentModelName = modelMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
1065
|
+
}
|
|
1066
|
+
} catch {
|
|
1067
|
+
}
|
|
1068
|
+
const harness = new AgentHarness({ workingDir });
|
|
1069
|
+
await harness.initialize();
|
|
1070
|
+
const telemetry = new TelemetryEmitter(config?.telemetry);
|
|
1071
|
+
const conversationStore = createConversationStore(resolveStateConfig(config), { workingDir });
|
|
1072
|
+
const sessionStore = new SessionStore();
|
|
1073
|
+
const loginRateLimiter = new LoginRateLimiter();
|
|
1074
|
+
const passphrase = process.env.AGENT_UI_PASSPHRASE ?? "";
|
|
1075
|
+
const isProduction = resolveHarnessEnvironment() === "production";
|
|
1076
|
+
const requireUiAuth = passphrase.length > 0;
|
|
1077
|
+
const secureCookies = isProduction;
|
|
1078
|
+
return async (request, response) => {
|
|
1079
|
+
if (!request.url || !request.method) {
|
|
1080
|
+
writeJson(response, 404, { error: "Not found" });
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const [pathname] = request.url.split("?");
|
|
1084
|
+
if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/"))) {
|
|
1085
|
+
writeHtml(response, 200, renderWebUiHtml({ agentName }));
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
if (pathname === "/manifest.json" && request.method === "GET") {
|
|
1089
|
+
response.writeHead(200, { "Content-Type": "application/manifest+json" });
|
|
1090
|
+
response.end(renderManifest({ agentName }));
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (pathname === "/sw.js" && request.method === "GET") {
|
|
1094
|
+
response.writeHead(200, {
|
|
1095
|
+
"Content-Type": "application/javascript",
|
|
1096
|
+
"Service-Worker-Allowed": "/"
|
|
1097
|
+
});
|
|
1098
|
+
response.end(renderServiceWorker());
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (pathname === "/icon.svg" && request.method === "GET") {
|
|
1102
|
+
response.writeHead(200, { "Content-Type": "image/svg+xml" });
|
|
1103
|
+
response.end(renderIconSvg({ agentName }));
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if ((pathname === "/icon-192.png" || pathname === "/icon-512.png") && request.method === "GET") {
|
|
1107
|
+
response.writeHead(302, { Location: "/icon.svg" });
|
|
1108
|
+
response.end();
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (pathname === "/health" && request.method === "GET") {
|
|
1112
|
+
writeJson(response, 200, { status: "ok" });
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
const cookies = parseCookies(request);
|
|
1116
|
+
const sessionId = cookies.poncho_session;
|
|
1117
|
+
const session = sessionId ? sessionStore.get(sessionId) : void 0;
|
|
1118
|
+
const ownerId = session?.ownerId ?? "local-owner";
|
|
1119
|
+
const requiresCsrfValidation = request.method !== "GET" && request.method !== "HEAD" && request.method !== "OPTIONS";
|
|
1120
|
+
if (pathname === "/api/auth/session" && request.method === "GET") {
|
|
1121
|
+
if (!requireUiAuth) {
|
|
1122
|
+
writeJson(response, 200, { authenticated: true, csrfToken: "" });
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if (!session) {
|
|
1126
|
+
writeJson(response, 200, { authenticated: false });
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
writeJson(response, 200, {
|
|
1130
|
+
authenticated: true,
|
|
1131
|
+
sessionId: session.sessionId,
|
|
1132
|
+
ownerId: session.ownerId,
|
|
1133
|
+
csrfToken: session.csrfToken
|
|
1134
|
+
});
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
if (pathname === "/api/auth/login" && request.method === "POST") {
|
|
1138
|
+
if (!requireUiAuth) {
|
|
1139
|
+
writeJson(response, 200, { authenticated: true, csrfToken: "" });
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
const ip = getRequestIp(request);
|
|
1143
|
+
const canAttempt = loginRateLimiter.canAttempt(ip);
|
|
1144
|
+
if (!canAttempt.allowed) {
|
|
1145
|
+
writeJson(response, 429, {
|
|
1146
|
+
code: "AUTH_RATE_LIMIT",
|
|
1147
|
+
message: "Too many failed login attempts. Try again later.",
|
|
1148
|
+
retryAfterSeconds: canAttempt.retryAfterSeconds
|
|
1149
|
+
});
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
const body = await readRequestBody(request);
|
|
1153
|
+
const provided = body.passphrase ?? "";
|
|
1154
|
+
if (!verifyPassphrase(provided, passphrase)) {
|
|
1155
|
+
const failure = loginRateLimiter.registerFailure(ip);
|
|
1156
|
+
writeJson(response, 401, {
|
|
1157
|
+
code: "AUTH_ERROR",
|
|
1158
|
+
message: "Invalid passphrase",
|
|
1159
|
+
retryAfterSeconds: failure.retryAfterSeconds
|
|
1160
|
+
});
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
loginRateLimiter.registerSuccess(ip);
|
|
1164
|
+
const createdSession = sessionStore.create(ownerId);
|
|
1165
|
+
setCookie(response, "poncho_session", createdSession.sessionId, {
|
|
1166
|
+
httpOnly: true,
|
|
1167
|
+
secure: secureCookies,
|
|
1168
|
+
sameSite: "Lax",
|
|
1169
|
+
path: "/",
|
|
1170
|
+
maxAge: 60 * 60 * 8
|
|
1171
|
+
});
|
|
1172
|
+
writeJson(response, 200, {
|
|
1173
|
+
authenticated: true,
|
|
1174
|
+
csrfToken: createdSession.csrfToken
|
|
1175
|
+
});
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
if (pathname === "/api/auth/logout" && request.method === "POST") {
|
|
1179
|
+
if (session?.sessionId) {
|
|
1180
|
+
sessionStore.delete(session.sessionId);
|
|
1181
|
+
}
|
|
1182
|
+
setCookie(response, "poncho_session", "", {
|
|
1183
|
+
httpOnly: true,
|
|
1184
|
+
secure: secureCookies,
|
|
1185
|
+
sameSite: "Lax",
|
|
1186
|
+
path: "/",
|
|
1187
|
+
maxAge: 0
|
|
1188
|
+
});
|
|
1189
|
+
writeJson(response, 200, { ok: true });
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
if (pathname.startsWith("/api/")) {
|
|
1193
|
+
if (requireUiAuth && !session) {
|
|
1194
|
+
writeJson(response, 401, {
|
|
1195
|
+
code: "AUTH_ERROR",
|
|
1196
|
+
message: "Authentication required"
|
|
1197
|
+
});
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (requireUiAuth && requiresCsrfValidation && pathname !== "/api/auth/login" && request.headers["x-csrf-token"] !== session?.csrfToken) {
|
|
1201
|
+
writeJson(response, 403, {
|
|
1202
|
+
code: "CSRF_ERROR",
|
|
1203
|
+
message: "Invalid CSRF token"
|
|
1204
|
+
});
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (pathname === "/api/conversations" && request.method === "GET") {
|
|
1209
|
+
const conversations = await conversationStore.list(ownerId);
|
|
1210
|
+
writeJson(response, 200, {
|
|
1211
|
+
conversations: conversations.map((conversation) => ({
|
|
1212
|
+
conversationId: conversation.conversationId,
|
|
1213
|
+
title: conversation.title,
|
|
1214
|
+
runtimeRunId: conversation.runtimeRunId,
|
|
1215
|
+
ownerId: conversation.ownerId,
|
|
1216
|
+
tenantId: conversation.tenantId,
|
|
1217
|
+
createdAt: conversation.createdAt,
|
|
1218
|
+
updatedAt: conversation.updatedAt,
|
|
1219
|
+
messageCount: conversation.messages.length
|
|
1220
|
+
}))
|
|
1221
|
+
});
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (pathname === "/api/conversations" && request.method === "POST") {
|
|
1225
|
+
const body = await readRequestBody(request);
|
|
1226
|
+
const conversation = await conversationStore.create(ownerId, body.title);
|
|
1227
|
+
const introMessage = await consumeFirstRunIntro(workingDir, {
|
|
1228
|
+
agentName,
|
|
1229
|
+
provider: agentModelProvider,
|
|
1230
|
+
model: agentModelName,
|
|
1231
|
+
config
|
|
1232
|
+
});
|
|
1233
|
+
if (introMessage) {
|
|
1234
|
+
conversation.messages = [{ role: "assistant", content: introMessage }];
|
|
1235
|
+
await conversationStore.update(conversation);
|
|
1236
|
+
}
|
|
1237
|
+
writeJson(response, 201, { conversation });
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const conversationPathMatch = pathname.match(/^\/api\/conversations\/([^/]+)$/);
|
|
1241
|
+
if (conversationPathMatch) {
|
|
1242
|
+
const conversationId = decodeURIComponent(conversationPathMatch[1] ?? "");
|
|
1243
|
+
const conversation = await conversationStore.get(conversationId);
|
|
1244
|
+
if (!conversation || conversation.ownerId !== ownerId) {
|
|
1245
|
+
writeJson(response, 404, {
|
|
1246
|
+
code: "CONVERSATION_NOT_FOUND",
|
|
1247
|
+
message: "Conversation not found"
|
|
1248
|
+
});
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (request.method === "GET") {
|
|
1252
|
+
writeJson(response, 200, { conversation });
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
if (request.method === "PATCH") {
|
|
1256
|
+
const body = await readRequestBody(request);
|
|
1257
|
+
if (!body.title || body.title.trim().length === 0) {
|
|
1258
|
+
writeJson(response, 400, {
|
|
1259
|
+
code: "VALIDATION_ERROR",
|
|
1260
|
+
message: "title is required"
|
|
1261
|
+
});
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
const updated = await conversationStore.rename(conversationId, body.title);
|
|
1265
|
+
writeJson(response, 200, { conversation: updated });
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (request.method === "DELETE") {
|
|
1269
|
+
await conversationStore.delete(conversationId);
|
|
1270
|
+
writeJson(response, 200, { ok: true });
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const conversationMessageMatch = pathname.match(/^\/api\/conversations\/([^/]+)\/messages$/);
|
|
1275
|
+
if (conversationMessageMatch && request.method === "POST") {
|
|
1276
|
+
const conversationId = decodeURIComponent(conversationMessageMatch[1] ?? "");
|
|
1277
|
+
const conversation = await conversationStore.get(conversationId);
|
|
1278
|
+
if (!conversation || conversation.ownerId !== ownerId) {
|
|
1279
|
+
writeJson(response, 404, {
|
|
1280
|
+
code: "CONVERSATION_NOT_FOUND",
|
|
1281
|
+
message: "Conversation not found"
|
|
1282
|
+
});
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const body = await readRequestBody(request);
|
|
1286
|
+
const messageText = body.message?.trim() ?? "";
|
|
1287
|
+
if (!messageText) {
|
|
1288
|
+
writeJson(response, 400, {
|
|
1289
|
+
code: "VALIDATION_ERROR",
|
|
1290
|
+
message: "message is required"
|
|
1291
|
+
});
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (conversation.messages.length === 0 && (conversation.title === "New conversation" || conversation.title.trim().length === 0)) {
|
|
1295
|
+
conversation.title = inferConversationTitle(messageText);
|
|
1296
|
+
}
|
|
1297
|
+
response.writeHead(200, {
|
|
1298
|
+
"Content-Type": "text/event-stream",
|
|
1299
|
+
"Cache-Control": "no-cache",
|
|
1300
|
+
Connection: "keep-alive"
|
|
1301
|
+
});
|
|
1302
|
+
let latestRunId = conversation.runtimeRunId ?? "";
|
|
1303
|
+
let assistantResponse = "";
|
|
1304
|
+
const toolTimeline = [];
|
|
1305
|
+
try {
|
|
1306
|
+
const recallCorpus = (await conversationStore.list(ownerId)).filter((item) => item.conversationId !== conversationId).slice(0, 20).map((item) => ({
|
|
1307
|
+
conversationId: item.conversationId,
|
|
1308
|
+
title: item.title,
|
|
1309
|
+
updatedAt: item.updatedAt,
|
|
1310
|
+
content: item.messages.slice(-6).map((message) => `${message.role}: ${message.content}`).join("\n").slice(0, 2e3)
|
|
1311
|
+
})).filter((item) => item.content.length > 0);
|
|
1312
|
+
for await (const event of harness.run({
|
|
1313
|
+
task: messageText,
|
|
1314
|
+
parameters: {
|
|
1315
|
+
...body.parameters ?? {},
|
|
1316
|
+
__conversationRecallCorpus: recallCorpus,
|
|
1317
|
+
__activeConversationId: conversationId
|
|
1318
|
+
},
|
|
1319
|
+
messages: conversation.messages
|
|
1320
|
+
})) {
|
|
1321
|
+
if (event.type === "run:started") {
|
|
1322
|
+
latestRunId = event.runId;
|
|
1323
|
+
}
|
|
1324
|
+
if (event.type === "model:chunk") {
|
|
1325
|
+
assistantResponse += event.content;
|
|
1326
|
+
}
|
|
1327
|
+
if (event.type === "tool:started") {
|
|
1328
|
+
toolTimeline.push(`- start \`${event.tool}\``);
|
|
1329
|
+
}
|
|
1330
|
+
if (event.type === "tool:completed") {
|
|
1331
|
+
toolTimeline.push(`- done \`${event.tool}\` (${event.duration}ms)`);
|
|
1332
|
+
}
|
|
1333
|
+
if (event.type === "tool:error") {
|
|
1334
|
+
toolTimeline.push(`- error \`${event.tool}\`: ${event.error}`);
|
|
1335
|
+
}
|
|
1336
|
+
if (event.type === "tool:approval:required") {
|
|
1337
|
+
toolTimeline.push(`- approval required \`${event.tool}\``);
|
|
1338
|
+
}
|
|
1339
|
+
if (event.type === "tool:approval:granted") {
|
|
1340
|
+
toolTimeline.push(`- approval granted (${event.approvalId})`);
|
|
1341
|
+
}
|
|
1342
|
+
if (event.type === "tool:approval:denied") {
|
|
1343
|
+
toolTimeline.push(`- approval denied (${event.approvalId})`);
|
|
1344
|
+
}
|
|
1345
|
+
if (event.type === "run:completed" && assistantResponse.length === 0 && event.result.response) {
|
|
1346
|
+
assistantResponse = event.result.response;
|
|
1347
|
+
}
|
|
1348
|
+
await telemetry.emit(event);
|
|
1349
|
+
response.write(formatSseEvent(event));
|
|
1350
|
+
}
|
|
1351
|
+
conversation.messages = [
|
|
1352
|
+
...conversation.messages,
|
|
1353
|
+
{ role: "user", content: messageText },
|
|
1354
|
+
{
|
|
1355
|
+
role: "assistant",
|
|
1356
|
+
content: assistantResponse,
|
|
1357
|
+
metadata: toolTimeline.length > 0 ? { toolActivity: toolTimeline } : void 0
|
|
1358
|
+
}
|
|
1359
|
+
];
|
|
1360
|
+
conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
|
|
1361
|
+
conversation.updatedAt = Date.now();
|
|
1362
|
+
await conversationStore.update(conversation);
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
response.write(
|
|
1365
|
+
formatSseEvent({
|
|
1366
|
+
type: "run:error",
|
|
1367
|
+
runId: latestRunId || "run_unknown",
|
|
1368
|
+
error: {
|
|
1369
|
+
code: "RUN_ERROR",
|
|
1370
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
1371
|
+
}
|
|
1372
|
+
})
|
|
1373
|
+
);
|
|
1374
|
+
} finally {
|
|
1375
|
+
response.end();
|
|
1376
|
+
}
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
writeJson(response, 404, { error: "Not found" });
|
|
1380
|
+
};
|
|
1381
|
+
};
|
|
1382
|
+
var startDevServer = async (port, options) => {
|
|
1383
|
+
const handler = await createRequestHandler(options);
|
|
1384
|
+
const server = createServer(handler);
|
|
1385
|
+
const actualPort = await listenOnAvailablePort(server, port);
|
|
1386
|
+
if (actualPort !== port) {
|
|
1387
|
+
process.stdout.write(`Port ${port} is in use, switched to ${actualPort}.
|
|
1388
|
+
`);
|
|
1389
|
+
}
|
|
1390
|
+
process.stdout.write(`Poncho dev server running at http://localhost:${actualPort}
|
|
1391
|
+
`);
|
|
1392
|
+
const shutdown = () => {
|
|
1393
|
+
server.close();
|
|
1394
|
+
server.closeAllConnections?.();
|
|
1395
|
+
process.exit(0);
|
|
1396
|
+
};
|
|
1397
|
+
process.on("SIGINT", shutdown);
|
|
1398
|
+
process.on("SIGTERM", shutdown);
|
|
1399
|
+
return server;
|
|
1400
|
+
};
|
|
1401
|
+
var runOnce = async (task, options) => {
|
|
1402
|
+
const workingDir = options.workingDir ?? process.cwd();
|
|
1403
|
+
dotenv.config({ path: resolve(workingDir, ".env") });
|
|
1404
|
+
const config = await loadPonchoConfig(workingDir);
|
|
1405
|
+
const harness = new AgentHarness({ workingDir });
|
|
1406
|
+
const telemetry = new TelemetryEmitter(config?.telemetry);
|
|
1407
|
+
await harness.initialize();
|
|
1408
|
+
const fileBlobs = await Promise.all(
|
|
1409
|
+
options.filePaths.map(async (path) => {
|
|
1410
|
+
const content = await readFile(resolve(workingDir, path), "utf8");
|
|
1411
|
+
return `# File: ${path}
|
|
1412
|
+
${content}`;
|
|
1413
|
+
})
|
|
1414
|
+
);
|
|
1415
|
+
const input2 = {
|
|
1416
|
+
task: fileBlobs.length > 0 ? `${task}
|
|
1417
|
+
|
|
1418
|
+
${fileBlobs.join("\n\n")}` : task,
|
|
1419
|
+
parameters: options.params
|
|
1420
|
+
};
|
|
1421
|
+
if (options.json) {
|
|
1422
|
+
const output = await harness.runToCompletion(input2);
|
|
1423
|
+
for (const event of output.events) {
|
|
1424
|
+
await telemetry.emit(event);
|
|
1425
|
+
}
|
|
1426
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
1427
|
+
`);
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
for await (const event of harness.run(input2)) {
|
|
1431
|
+
await telemetry.emit(event);
|
|
1432
|
+
if (event.type === "model:chunk") {
|
|
1433
|
+
process.stdout.write(event.content);
|
|
1434
|
+
}
|
|
1435
|
+
if (event.type === "run:error") {
|
|
1436
|
+
process.stderr.write(`
|
|
1437
|
+
Error: ${event.error.message}
|
|
1438
|
+
`);
|
|
1439
|
+
}
|
|
1440
|
+
if (event.type === "run:completed") {
|
|
1441
|
+
process.stdout.write("\n");
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
var runInteractive = async (workingDir, params) => {
|
|
1446
|
+
dotenv.config({ path: resolve(workingDir, ".env") });
|
|
1447
|
+
const config = await loadPonchoConfig(workingDir);
|
|
1448
|
+
let pendingApproval = null;
|
|
1449
|
+
let onApprovalRequest = null;
|
|
1450
|
+
const approvalHandler = async (request) => {
|
|
1451
|
+
return new Promise((resolveApproval) => {
|
|
1452
|
+
const req = {
|
|
1453
|
+
tool: request.tool,
|
|
1454
|
+
input: request.input,
|
|
1455
|
+
approvalId: request.approvalId,
|
|
1456
|
+
resolve: resolveApproval
|
|
1457
|
+
};
|
|
1458
|
+
pendingApproval = req;
|
|
1459
|
+
if (onApprovalRequest) {
|
|
1460
|
+
onApprovalRequest(req);
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
};
|
|
1464
|
+
const harness = new AgentHarness({
|
|
1465
|
+
workingDir,
|
|
1466
|
+
environment: resolveHarnessEnvironment(),
|
|
1467
|
+
approvalHandler
|
|
1468
|
+
});
|
|
1469
|
+
await harness.initialize();
|
|
1470
|
+
try {
|
|
1471
|
+
const { runInteractiveInk } = await import("./run-interactive-ink-4EKHIHGU.js");
|
|
1472
|
+
await runInteractiveInk({
|
|
1473
|
+
harness,
|
|
1474
|
+
params,
|
|
1475
|
+
workingDir,
|
|
1476
|
+
config,
|
|
1477
|
+
conversationStore: createConversationStore(resolveStateConfig(config), { workingDir }),
|
|
1478
|
+
onSetApprovalCallback: (cb) => {
|
|
1479
|
+
onApprovalRequest = cb;
|
|
1480
|
+
if (pendingApproval) {
|
|
1481
|
+
cb(pendingApproval);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
} finally {
|
|
1486
|
+
await harness.shutdown();
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
var listTools = async (workingDir) => {
|
|
1490
|
+
dotenv.config({ path: resolve(workingDir, ".env") });
|
|
1491
|
+
const harness = new AgentHarness({ workingDir });
|
|
1492
|
+
await harness.initialize();
|
|
1493
|
+
const tools = harness.listTools();
|
|
1494
|
+
if (tools.length === 0) {
|
|
1495
|
+
process.stdout.write("No tools registered.\n");
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
process.stdout.write("Available tools:\n");
|
|
1499
|
+
for (const tool of tools) {
|
|
1500
|
+
process.stdout.write(`- ${tool.name}: ${tool.description}
|
|
1501
|
+
`);
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
var runPnpmInstall = async (workingDir) => await new Promise((resolveInstall, rejectInstall) => {
|
|
1505
|
+
const child = spawn("pnpm", ["install"], {
|
|
1506
|
+
cwd: workingDir,
|
|
1507
|
+
stdio: "inherit",
|
|
1508
|
+
env: process.env
|
|
1509
|
+
});
|
|
1510
|
+
child.on("exit", (code) => {
|
|
1511
|
+
if (code === 0) {
|
|
1512
|
+
resolveInstall();
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
rejectInstall(new Error(`pnpm install failed with exit code ${code ?? -1}`));
|
|
1516
|
+
});
|
|
1517
|
+
});
|
|
1518
|
+
var runInstallCommand = async (workingDir, packageNameOrPath) => await new Promise((resolveInstall, rejectInstall) => {
|
|
1519
|
+
const child = spawn("pnpm", ["add", packageNameOrPath], {
|
|
1520
|
+
cwd: workingDir,
|
|
1521
|
+
stdio: "inherit",
|
|
1522
|
+
env: process.env
|
|
1523
|
+
});
|
|
1524
|
+
child.on("exit", (code) => {
|
|
1525
|
+
if (code === 0) {
|
|
1526
|
+
resolveInstall();
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
rejectInstall(new Error(`pnpm add failed with exit code ${code ?? -1}`));
|
|
1530
|
+
});
|
|
1531
|
+
});
|
|
1532
|
+
var resolveInstalledPackageName = (packageNameOrPath) => {
|
|
1533
|
+
if (packageNameOrPath.startsWith(".") || packageNameOrPath.startsWith("/")) {
|
|
1534
|
+
return null;
|
|
1535
|
+
}
|
|
1536
|
+
if (packageNameOrPath.startsWith("@")) {
|
|
1537
|
+
return packageNameOrPath;
|
|
1538
|
+
}
|
|
1539
|
+
if (packageNameOrPath.includes("/")) {
|
|
1540
|
+
return packageNameOrPath.split("/").pop() ?? packageNameOrPath;
|
|
1541
|
+
}
|
|
1542
|
+
return packageNameOrPath;
|
|
1543
|
+
};
|
|
1544
|
+
var resolveSkillRoot = (workingDir, packageNameOrPath) => {
|
|
1545
|
+
if (packageNameOrPath.startsWith(".") || packageNameOrPath.startsWith("/")) {
|
|
1546
|
+
return resolve(workingDir, packageNameOrPath);
|
|
1547
|
+
}
|
|
1548
|
+
const moduleName = resolveInstalledPackageName(packageNameOrPath) ?? packageNameOrPath;
|
|
1549
|
+
try {
|
|
1550
|
+
const packageJsonPath = require2.resolve(`${moduleName}/package.json`, {
|
|
1551
|
+
paths: [workingDir]
|
|
1552
|
+
});
|
|
1553
|
+
return resolve(packageJsonPath, "..");
|
|
1554
|
+
} catch {
|
|
1555
|
+
const candidate = resolve(workingDir, "node_modules", moduleName);
|
|
1556
|
+
if (existsSync(candidate)) {
|
|
1557
|
+
return candidate;
|
|
1558
|
+
}
|
|
1559
|
+
throw new Error(
|
|
1560
|
+
`Could not locate installed package "${moduleName}" in ${workingDir}`
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
var findSkillManifest = async (dir, depth = 2) => {
|
|
1565
|
+
try {
|
|
1566
|
+
await access(resolve(dir, "SKILL.md"));
|
|
1567
|
+
return true;
|
|
1568
|
+
} catch {
|
|
1569
|
+
}
|
|
1570
|
+
if (depth <= 0) return false;
|
|
1571
|
+
try {
|
|
1572
|
+
const { readdir } = await import("fs/promises");
|
|
1573
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
1574
|
+
for (const entry of entries) {
|
|
1575
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
1576
|
+
const found = await findSkillManifest(resolve(dir, entry.name), depth - 1);
|
|
1577
|
+
if (found) return true;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
} catch {
|
|
1581
|
+
}
|
|
1582
|
+
return false;
|
|
1583
|
+
};
|
|
1584
|
+
var validateSkillPackage = async (workingDir, packageNameOrPath) => {
|
|
1585
|
+
const skillRoot = resolveSkillRoot(workingDir, packageNameOrPath);
|
|
1586
|
+
const hasSkill = await findSkillManifest(skillRoot);
|
|
1587
|
+
if (!hasSkill) {
|
|
1588
|
+
throw new Error(`Skill validation failed: no SKILL.md found in ${skillRoot}`);
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
var addSkill = async (workingDir, packageNameOrPath) => {
|
|
1592
|
+
await runInstallCommand(workingDir, packageNameOrPath);
|
|
1593
|
+
await validateSkillPackage(workingDir, packageNameOrPath);
|
|
1594
|
+
process.stdout.write(`Added skill: ${packageNameOrPath}
|
|
1595
|
+
`);
|
|
1596
|
+
};
|
|
1597
|
+
var runTests = async (workingDir, filePath) => {
|
|
1598
|
+
dotenv.config({ path: resolve(workingDir, ".env") });
|
|
1599
|
+
const testFilePath = filePath ?? resolve(workingDir, "tests", "basic.yaml");
|
|
1600
|
+
const content = await readFile(testFilePath, "utf8");
|
|
1601
|
+
const parsed = YAML.parse(content);
|
|
1602
|
+
const tests = parsed.tests ?? [];
|
|
1603
|
+
const harness = new AgentHarness({ workingDir });
|
|
1604
|
+
await harness.initialize();
|
|
1605
|
+
let passed = 0;
|
|
1606
|
+
let failed = 0;
|
|
1607
|
+
for (const testCase of tests) {
|
|
1608
|
+
try {
|
|
1609
|
+
const output = await harness.runToCompletion({ task: testCase.task });
|
|
1610
|
+
const response = output.result.response ?? "";
|
|
1611
|
+
const events = output.events;
|
|
1612
|
+
const expectation = testCase.expect ?? {};
|
|
1613
|
+
const checks = [];
|
|
1614
|
+
if (expectation.contains) {
|
|
1615
|
+
checks.push(response.includes(expectation.contains));
|
|
1616
|
+
}
|
|
1617
|
+
if (typeof expectation.maxSteps === "number") {
|
|
1618
|
+
checks.push(output.result.steps <= expectation.maxSteps);
|
|
1619
|
+
}
|
|
1620
|
+
if (typeof expectation.maxTokens === "number") {
|
|
1621
|
+
checks.push(
|
|
1622
|
+
output.result.tokens.input + output.result.tokens.output <= expectation.maxTokens
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
if (expectation.refusal) {
|
|
1626
|
+
checks.push(
|
|
1627
|
+
response.toLowerCase().includes("can't") || response.toLowerCase().includes("cannot")
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
if (expectation.toolCalled) {
|
|
1631
|
+
checks.push(
|
|
1632
|
+
events.some(
|
|
1633
|
+
(event) => event.type === "tool:started" && event.tool === expectation.toolCalled
|
|
1634
|
+
)
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
const ok = checks.length === 0 ? output.result.status === "completed" : checks.every(Boolean);
|
|
1638
|
+
if (ok) {
|
|
1639
|
+
passed += 1;
|
|
1640
|
+
process.stdout.write(`PASS ${testCase.name}
|
|
1641
|
+
`);
|
|
1642
|
+
} else {
|
|
1643
|
+
failed += 1;
|
|
1644
|
+
process.stdout.write(`FAIL ${testCase.name}
|
|
1645
|
+
`);
|
|
1646
|
+
}
|
|
1647
|
+
} catch (error) {
|
|
1648
|
+
failed += 1;
|
|
1649
|
+
process.stdout.write(
|
|
1650
|
+
`FAIL ${testCase.name} (${error instanceof Error ? error.message : "Unknown test error"})
|
|
1651
|
+
`
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
process.stdout.write(`
|
|
1656
|
+
Test summary: ${passed} passed, ${failed} failed
|
|
1657
|
+
`);
|
|
1658
|
+
return { passed, failed };
|
|
1659
|
+
};
|
|
1660
|
+
var buildTarget = async (workingDir, target) => {
|
|
1661
|
+
const outDir = resolve(workingDir, ".poncho-build", target);
|
|
1662
|
+
await mkdir(outDir, { recursive: true });
|
|
1663
|
+
const serverEntrypoint = `import { startDevServer } from "@poncho-ai/cli";
|
|
1664
|
+
|
|
1665
|
+
const port = Number.parseInt(process.env.PORT ?? "3000", 10);
|
|
1666
|
+
await startDevServer(Number.isNaN(port) ? 3000 : port, { workingDir: process.cwd() });
|
|
1667
|
+
`;
|
|
1668
|
+
const runtimePackageJson = JSON.stringify(
|
|
1669
|
+
{
|
|
1670
|
+
name: "poncho-runtime-bundle",
|
|
1671
|
+
private: true,
|
|
1672
|
+
type: "module",
|
|
1673
|
+
scripts: {
|
|
1674
|
+
start: "node server.js"
|
|
1675
|
+
},
|
|
1676
|
+
dependencies: {
|
|
1677
|
+
"@poncho-ai/cli": "^0.1.0"
|
|
1678
|
+
}
|
|
1679
|
+
},
|
|
1680
|
+
null,
|
|
1681
|
+
2
|
|
1682
|
+
);
|
|
1683
|
+
if (target === "vercel") {
|
|
1684
|
+
await mkdir(resolve(outDir, "api"), { recursive: true });
|
|
1685
|
+
await copyIfExists(resolve(workingDir, "AGENT.md"), resolve(outDir, "AGENT.md"));
|
|
1686
|
+
await copyIfExists(
|
|
1687
|
+
resolve(workingDir, "poncho.config.js"),
|
|
1688
|
+
resolve(outDir, "poncho.config.js")
|
|
1689
|
+
);
|
|
1690
|
+
await copyIfExists(resolve(workingDir, "skills"), resolve(outDir, "skills"));
|
|
1691
|
+
await copyIfExists(resolve(workingDir, "tests"), resolve(outDir, "tests"));
|
|
1692
|
+
await writeFile(
|
|
1693
|
+
resolve(outDir, "vercel.json"),
|
|
1694
|
+
JSON.stringify(
|
|
1695
|
+
{
|
|
1696
|
+
version: 2,
|
|
1697
|
+
functions: {
|
|
1698
|
+
"api/index.js": {
|
|
1699
|
+
includeFiles: "{AGENT.md,poncho.config.js,skills/**,tests/**}"
|
|
1700
|
+
}
|
|
1701
|
+
},
|
|
1702
|
+
routes: [{ src: "/(.*)", dest: "/api/index.js" }]
|
|
1703
|
+
},
|
|
1704
|
+
null,
|
|
1705
|
+
2
|
|
1706
|
+
),
|
|
1707
|
+
"utf8"
|
|
1708
|
+
);
|
|
1709
|
+
await buildVercelHandlerBundle(outDir);
|
|
1710
|
+
await writeFile(
|
|
1711
|
+
resolve(outDir, "package.json"),
|
|
1712
|
+
JSON.stringify(
|
|
1713
|
+
{
|
|
1714
|
+
private: true,
|
|
1715
|
+
type: "module",
|
|
1716
|
+
engines: {
|
|
1717
|
+
node: "20.x"
|
|
1718
|
+
},
|
|
1719
|
+
dependencies: VERCEL_RUNTIME_DEPENDENCIES
|
|
1720
|
+
},
|
|
1721
|
+
null,
|
|
1722
|
+
2
|
|
1723
|
+
),
|
|
1724
|
+
"utf8"
|
|
1725
|
+
);
|
|
1726
|
+
} else if (target === "docker") {
|
|
1727
|
+
await writeFile(
|
|
1728
|
+
resolve(outDir, "Dockerfile"),
|
|
1729
|
+
`FROM node:20-slim
|
|
1730
|
+
WORKDIR /app
|
|
1731
|
+
COPY package.json package.json
|
|
1732
|
+
COPY AGENT.md AGENT.md
|
|
1733
|
+
COPY poncho.config.js poncho.config.js
|
|
1734
|
+
COPY skills skills
|
|
1735
|
+
COPY tests tests
|
|
1736
|
+
COPY .env.example .env.example
|
|
1737
|
+
RUN corepack enable && npm install -g @poncho-ai/cli
|
|
1738
|
+
COPY server.js server.js
|
|
1739
|
+
EXPOSE 3000
|
|
1740
|
+
CMD ["node","server.js"]
|
|
1741
|
+
`,
|
|
1742
|
+
"utf8"
|
|
1743
|
+
);
|
|
1744
|
+
await writeFile(resolve(outDir, "server.js"), serverEntrypoint, "utf8");
|
|
1745
|
+
await writeFile(resolve(outDir, "package.json"), runtimePackageJson, "utf8");
|
|
1746
|
+
} else if (target === "lambda") {
|
|
1747
|
+
await writeFile(
|
|
1748
|
+
resolve(outDir, "lambda-handler.js"),
|
|
1749
|
+
`import { startDevServer } from "@poncho-ai/cli";
|
|
1750
|
+
let serverPromise;
|
|
1751
|
+
export const handler = async (event = {}) => {
|
|
1752
|
+
if (!serverPromise) {
|
|
1753
|
+
serverPromise = startDevServer(0, { workingDir: process.cwd() });
|
|
1754
|
+
}
|
|
1755
|
+
const body = JSON.stringify({
|
|
1756
|
+
status: "ready",
|
|
1757
|
+
route: event.rawPath ?? event.path ?? "/",
|
|
1758
|
+
});
|
|
1759
|
+
return { statusCode: 200, headers: { "content-type": "application/json" }, body };
|
|
1760
|
+
};
|
|
1761
|
+
`,
|
|
1762
|
+
"utf8"
|
|
1763
|
+
);
|
|
1764
|
+
await writeFile(resolve(outDir, "package.json"), runtimePackageJson, "utf8");
|
|
1765
|
+
} else if (target === "fly") {
|
|
1766
|
+
await writeFile(
|
|
1767
|
+
resolve(outDir, "fly.toml"),
|
|
1768
|
+
`app = "poncho-app"
|
|
1769
|
+
[env]
|
|
1770
|
+
PORT = "3000"
|
|
1771
|
+
[http_service]
|
|
1772
|
+
internal_port = 3000
|
|
1773
|
+
force_https = true
|
|
1774
|
+
auto_start_machines = true
|
|
1775
|
+
auto_stop_machines = "stop"
|
|
1776
|
+
min_machines_running = 0
|
|
1777
|
+
`,
|
|
1778
|
+
"utf8"
|
|
1779
|
+
);
|
|
1780
|
+
await writeFile(
|
|
1781
|
+
resolve(outDir, "Dockerfile"),
|
|
1782
|
+
`FROM node:20-slim
|
|
1783
|
+
WORKDIR /app
|
|
1784
|
+
COPY package.json package.json
|
|
1785
|
+
COPY AGENT.md AGENT.md
|
|
1786
|
+
COPY poncho.config.js poncho.config.js
|
|
1787
|
+
COPY skills skills
|
|
1788
|
+
COPY tests tests
|
|
1789
|
+
RUN npm install -g @poncho-ai/cli
|
|
1790
|
+
COPY server.js server.js
|
|
1791
|
+
EXPOSE 3000
|
|
1792
|
+
CMD ["node","server.js"]
|
|
1793
|
+
`,
|
|
1794
|
+
"utf8"
|
|
1795
|
+
);
|
|
1796
|
+
await writeFile(resolve(outDir, "server.js"), serverEntrypoint, "utf8");
|
|
1797
|
+
await writeFile(resolve(outDir, "package.json"), runtimePackageJson, "utf8");
|
|
1798
|
+
} else {
|
|
1799
|
+
throw new Error(`Unsupported build target: ${target}`);
|
|
1800
|
+
}
|
|
1801
|
+
process.stdout.write(`Build artifacts generated at ${outDir}
|
|
1802
|
+
`);
|
|
1803
|
+
};
|
|
1804
|
+
var normalizeMcpName = (entry) => entry.name ?? entry.url ?? `mcp_${Date.now()}`;
|
|
1805
|
+
var mcpAdd = async (workingDir, options) => {
|
|
1806
|
+
const config = await loadPonchoConfig(workingDir) ?? { mcp: [] };
|
|
1807
|
+
const mcp = [...config.mcp ?? []];
|
|
1808
|
+
if (!options.url) {
|
|
1809
|
+
throw new Error("Remote MCP only: provide --url for a remote MCP server.");
|
|
1810
|
+
}
|
|
1811
|
+
if (options.url.startsWith("ws://") || options.url.startsWith("wss://")) {
|
|
1812
|
+
throw new Error("WebSocket MCP URLs are no longer supported. Use an HTTP MCP endpoint.");
|
|
1813
|
+
}
|
|
1814
|
+
if (!options.url.startsWith("http://") && !options.url.startsWith("https://")) {
|
|
1815
|
+
throw new Error("Invalid MCP URL. Expected http:// or https://.");
|
|
1816
|
+
}
|
|
1817
|
+
const serverName = options.name ?? normalizeMcpName({ url: options.url });
|
|
1818
|
+
mcp.push({
|
|
1819
|
+
name: serverName,
|
|
1820
|
+
url: options.url,
|
|
1821
|
+
env: options.envVars ?? [],
|
|
1822
|
+
auth: options.authBearerEnv ? {
|
|
1823
|
+
type: "bearer",
|
|
1824
|
+
tokenEnv: options.authBearerEnv
|
|
1825
|
+
} : void 0
|
|
1826
|
+
});
|
|
1827
|
+
await writeConfigFile(workingDir, { ...config, mcp });
|
|
1828
|
+
let envSeedMessage;
|
|
1829
|
+
if (options.authBearerEnv) {
|
|
1830
|
+
const envPath = resolve(workingDir, ".env");
|
|
1831
|
+
const envExamplePath = resolve(workingDir, ".env.example");
|
|
1832
|
+
const addedEnv = await ensureEnvPlaceholder(envPath, options.authBearerEnv);
|
|
1833
|
+
const addedEnvExample = await ensureEnvPlaceholder(envExamplePath, options.authBearerEnv);
|
|
1834
|
+
if (addedEnv || addedEnvExample) {
|
|
1835
|
+
envSeedMessage = `Added ${options.authBearerEnv}= to ${addedEnv ? ".env" : ""}${addedEnv && addedEnvExample ? " and " : ""}${addedEnvExample ? ".env.example" : ""}.`;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
const nextSteps = [];
|
|
1839
|
+
let step = 1;
|
|
1840
|
+
if (options.authBearerEnv) {
|
|
1841
|
+
nextSteps.push(` ${step}) Set token in .env: ${options.authBearerEnv}=...`);
|
|
1842
|
+
step += 1;
|
|
1843
|
+
}
|
|
1844
|
+
nextSteps.push(` ${step}) Discover tools: poncho mcp tools list ${serverName}`);
|
|
1845
|
+
step += 1;
|
|
1846
|
+
nextSteps.push(` ${step}) Select tools: poncho mcp tools select ${serverName}`);
|
|
1847
|
+
step += 1;
|
|
1848
|
+
nextSteps.push(` ${step}) Verify config: poncho mcp list`);
|
|
1849
|
+
process.stdout.write(
|
|
1850
|
+
[
|
|
1851
|
+
`MCP server added: ${serverName}`,
|
|
1852
|
+
...envSeedMessage ? [envSeedMessage] : [],
|
|
1853
|
+
"Next steps:",
|
|
1854
|
+
...nextSteps,
|
|
1855
|
+
""
|
|
1856
|
+
].join("\n")
|
|
1857
|
+
);
|
|
1858
|
+
};
|
|
1859
|
+
var mcpList = async (workingDir) => {
|
|
1860
|
+
const config = await loadPonchoConfig(workingDir);
|
|
1861
|
+
const mcp = config?.mcp ?? [];
|
|
1862
|
+
if (mcp.length === 0) {
|
|
1863
|
+
process.stdout.write("No MCP servers configured.\n");
|
|
1864
|
+
if (config?.scripts) {
|
|
1865
|
+
process.stdout.write(
|
|
1866
|
+
`Script policy: mode=${config.scripts.mode ?? "all"} include=${config.scripts.include?.length ?? 0} exclude=${config.scripts.exclude?.length ?? 0}
|
|
1867
|
+
`
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
process.stdout.write("Configured MCP servers:\n");
|
|
1873
|
+
for (const entry of mcp) {
|
|
1874
|
+
const auth = entry.auth?.type === "bearer" ? `auth=bearer:${entry.auth.tokenEnv}` : "auth=none";
|
|
1875
|
+
const mode = entry.tools?.mode ?? "all";
|
|
1876
|
+
process.stdout.write(
|
|
1877
|
+
`- ${entry.name ?? entry.url} (remote: ${entry.url}, ${auth}, mode=${mode})
|
|
1878
|
+
`
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
if (config?.scripts) {
|
|
1882
|
+
process.stdout.write(
|
|
1883
|
+
`Script policy: mode=${config.scripts.mode ?? "all"} include=${config.scripts.include?.length ?? 0} exclude=${config.scripts.exclude?.length ?? 0}
|
|
1884
|
+
`
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
var mcpRemove = async (workingDir, name) => {
|
|
1889
|
+
const config = await loadPonchoConfig(workingDir) ?? { mcp: [] };
|
|
1890
|
+
const before = config.mcp ?? [];
|
|
1891
|
+
const removed = before.filter((entry) => normalizeMcpName(entry) === name);
|
|
1892
|
+
const filtered = before.filter((entry) => normalizeMcpName(entry) !== name);
|
|
1893
|
+
await writeConfigFile(workingDir, { ...config, mcp: filtered });
|
|
1894
|
+
const removedTokenEnvNames = new Set(
|
|
1895
|
+
removed.map(
|
|
1896
|
+
(entry) => entry.auth?.type === "bearer" ? entry.auth.tokenEnv?.trim() ?? "" : ""
|
|
1897
|
+
).filter((value) => value.length > 0)
|
|
1898
|
+
);
|
|
1899
|
+
const stillUsedTokenEnvNames = new Set(
|
|
1900
|
+
filtered.map(
|
|
1901
|
+
(entry) => entry.auth?.type === "bearer" ? entry.auth.tokenEnv?.trim() ?? "" : ""
|
|
1902
|
+
).filter((value) => value.length > 0)
|
|
1903
|
+
);
|
|
1904
|
+
const removedFromExample = [];
|
|
1905
|
+
for (const tokenEnv of removedTokenEnvNames) {
|
|
1906
|
+
if (stillUsedTokenEnvNames.has(tokenEnv)) {
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
const changed = await removeEnvPlaceholder(resolve(workingDir, ".env.example"), tokenEnv);
|
|
1910
|
+
if (changed) {
|
|
1911
|
+
removedFromExample.push(tokenEnv);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
process.stdout.write(`Removed MCP server: ${name}
|
|
1915
|
+
`);
|
|
1916
|
+
if (removedFromExample.length > 0) {
|
|
1917
|
+
process.stdout.write(
|
|
1918
|
+
`Removed unused token placeholder(s) from .env.example: ${removedFromExample.join(", ")}
|
|
1919
|
+
`
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
var resolveMcpEntry = async (workingDir, serverName) => {
|
|
1924
|
+
const config = await loadPonchoConfig(workingDir) ?? { mcp: [] };
|
|
1925
|
+
const entries = config.mcp ?? [];
|
|
1926
|
+
const index = entries.findIndex((entry) => normalizeMcpName(entry) === serverName);
|
|
1927
|
+
if (index < 0) {
|
|
1928
|
+
throw new Error(`MCP server "${serverName}" is not configured.`);
|
|
1929
|
+
}
|
|
1930
|
+
return { config, index };
|
|
1931
|
+
};
|
|
1932
|
+
var discoverMcpTools = async (workingDir, serverName) => {
|
|
1933
|
+
dotenv.config({ path: resolve(workingDir, ".env") });
|
|
1934
|
+
const { config, index } = await resolveMcpEntry(workingDir, serverName);
|
|
1935
|
+
const entry = (config.mcp ?? [])[index];
|
|
1936
|
+
const bridge = new LocalMcpBridge({ mcp: [entry] });
|
|
1937
|
+
try {
|
|
1938
|
+
await bridge.startLocalServers();
|
|
1939
|
+
await bridge.discoverTools();
|
|
1940
|
+
return bridge.listDiscoveredTools(normalizeMcpName(entry));
|
|
1941
|
+
} finally {
|
|
1942
|
+
await bridge.stopLocalServers();
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
var mcpToolsList = async (workingDir, serverName) => {
|
|
1946
|
+
const discovered = await discoverMcpTools(workingDir, serverName);
|
|
1947
|
+
if (discovered.length === 0) {
|
|
1948
|
+
process.stdout.write(`No tools discovered for MCP server "${serverName}".
|
|
1949
|
+
`);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
process.stdout.write(`Discovered tools for "${serverName}":
|
|
1953
|
+
`);
|
|
1954
|
+
for (const tool of discovered) {
|
|
1955
|
+
process.stdout.write(`- ${tool}
|
|
1956
|
+
`);
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
var mcpToolsSelect = async (workingDir, serverName, options) => {
|
|
1960
|
+
const discovered = await discoverMcpTools(workingDir, serverName);
|
|
1961
|
+
if (discovered.length === 0) {
|
|
1962
|
+
process.stdout.write(`No tools discovered for MCP server "${serverName}".
|
|
1963
|
+
`);
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
let selected = [];
|
|
1967
|
+
if (options.all) {
|
|
1968
|
+
selected = [...discovered];
|
|
1969
|
+
} else if (options.toolsCsv && options.toolsCsv.trim().length > 0) {
|
|
1970
|
+
const requested = options.toolsCsv.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
1971
|
+
selected = discovered.filter((tool) => requested.includes(tool));
|
|
1972
|
+
} else {
|
|
1973
|
+
process.stdout.write(`Discovered tools for "${serverName}":
|
|
1974
|
+
`);
|
|
1975
|
+
discovered.forEach((tool, idx) => {
|
|
1976
|
+
process.stdout.write(`${idx + 1}. ${tool}
|
|
1977
|
+
`);
|
|
1978
|
+
});
|
|
1979
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1980
|
+
const answer = await rl.question(
|
|
1981
|
+
"Enter comma-separated tool numbers/names to allow (or * for all): "
|
|
1982
|
+
);
|
|
1983
|
+
rl.close();
|
|
1984
|
+
const raw = answer.trim();
|
|
1985
|
+
if (raw === "*") {
|
|
1986
|
+
selected = [...discovered];
|
|
1987
|
+
} else {
|
|
1988
|
+
const tokens = raw.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
1989
|
+
const fromIndex = tokens.map((token) => Number.parseInt(token, 10)).filter((value) => !Number.isNaN(value)).map((index2) => discovered[index2 - 1]).filter((value) => typeof value === "string");
|
|
1990
|
+
const byName = discovered.filter((tool) => tokens.includes(tool));
|
|
1991
|
+
selected = [.../* @__PURE__ */ new Set([...fromIndex, ...byName])];
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
if (selected.length === 0) {
|
|
1995
|
+
throw new Error("No valid tools selected.");
|
|
1996
|
+
}
|
|
1997
|
+
const { config, index } = await resolveMcpEntry(workingDir, serverName);
|
|
1998
|
+
const mcp = [...config.mcp ?? []];
|
|
1999
|
+
const existing = mcp[index];
|
|
2000
|
+
mcp[index] = {
|
|
2001
|
+
...existing,
|
|
2002
|
+
tools: {
|
|
2003
|
+
...existing.tools ?? {},
|
|
2004
|
+
mode: "allowlist",
|
|
2005
|
+
include: selected.sort()
|
|
2006
|
+
}
|
|
2007
|
+
};
|
|
2008
|
+
await writeConfigFile(workingDir, { ...config, mcp });
|
|
2009
|
+
process.stdout.write(
|
|
2010
|
+
`Updated ${serverName} to allowlist ${selected.length} tools in poncho.config.js.
|
|
2011
|
+
`
|
|
2012
|
+
);
|
|
2013
|
+
process.stdout.write(
|
|
2014
|
+
"\nDeclare selected MCP tools explicitly in AGENT.md and/or SKILL.md:\n"
|
|
2015
|
+
);
|
|
2016
|
+
process.stdout.write(
|
|
2017
|
+
"AGENT.md frontmatter snippet:\n---\ntools:\n mcp:\n" + selected.map((tool) => ` - ${tool}`).join("\n") + "\n---\n"
|
|
2018
|
+
);
|
|
2019
|
+
process.stdout.write(
|
|
2020
|
+
"SKILL.md frontmatter snippet:\n---\ntools:\n mcp:\n" + selected.map((tool) => ` - ${tool}`).join("\n") + "\n---\n"
|
|
2021
|
+
);
|
|
2022
|
+
};
|
|
2023
|
+
var buildCli = () => {
|
|
2024
|
+
const program = new Command();
|
|
2025
|
+
program.name("poncho").description("CLI for building and running Poncho agents").version("0.1.0");
|
|
2026
|
+
program.command("init").argument("<name>", "project name").option("--yes", "accept defaults and skip prompts", false).description("Scaffold a new Poncho project").action(async (name, options) => {
|
|
2027
|
+
await initProject(name, {
|
|
2028
|
+
onboarding: {
|
|
2029
|
+
yes: options.yes,
|
|
2030
|
+
interactive: !options.yes && process.stdin.isTTY === true && process.stdout.isTTY === true
|
|
2031
|
+
}
|
|
2032
|
+
});
|
|
2033
|
+
});
|
|
2034
|
+
program.command("dev").description("Run local development server").option("--port <port>", "server port", "3000").action(async (options) => {
|
|
2035
|
+
const port = Number.parseInt(options.port, 10);
|
|
2036
|
+
await startDevServer(Number.isNaN(port) ? 3e3 : port);
|
|
2037
|
+
});
|
|
2038
|
+
program.command("run").argument("[task]", "task to run").description("Execute the agent once").option("--param <keyValue>", "parameter key=value", (value, all) => {
|
|
2039
|
+
all.push(value);
|
|
2040
|
+
return all;
|
|
2041
|
+
}, []).option("--file <path>", "include file contents", (value, all) => {
|
|
2042
|
+
all.push(value);
|
|
2043
|
+
return all;
|
|
2044
|
+
}, []).option("--json", "output json", false).option("--interactive", "run in interactive mode", false).action(
|
|
2045
|
+
async (task, options) => {
|
|
2046
|
+
const params = parseParams(options.param);
|
|
2047
|
+
if (options.interactive) {
|
|
2048
|
+
await runInteractive(process.cwd(), params);
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
if (!task) {
|
|
2052
|
+
throw new Error("Task is required unless --interactive is used.");
|
|
2053
|
+
}
|
|
2054
|
+
await runOnce(task, {
|
|
2055
|
+
params,
|
|
2056
|
+
json: options.json,
|
|
2057
|
+
filePaths: options.file
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
);
|
|
2061
|
+
program.command("tools").description("List all tools available to the agent").action(async () => {
|
|
2062
|
+
await listTools(process.cwd());
|
|
2063
|
+
});
|
|
2064
|
+
program.command("add").argument("<packageOrPath>", "skill package name/path").description("Add a skill package and validate SKILL.md").action(async (packageOrPath) => {
|
|
2065
|
+
await addSkill(process.cwd(), packageOrPath);
|
|
2066
|
+
});
|
|
2067
|
+
program.command("update-agent").description("Remove deprecated embedded local guidance from AGENT.md").action(async () => {
|
|
2068
|
+
await updateAgentGuidance(process.cwd());
|
|
2069
|
+
});
|
|
2070
|
+
program.command("test").argument("[file]", "test file path (yaml)").description("Run yaml-defined agent tests").action(async (file) => {
|
|
2071
|
+
const testFile = file ? resolve(process.cwd(), file) : void 0;
|
|
2072
|
+
const result = await runTests(process.cwd(), testFile);
|
|
2073
|
+
if (result.failed > 0) {
|
|
2074
|
+
process.exitCode = 1;
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
program.command("build").argument("<target>", "vercel|docker|lambda|fly").description("Generate build artifacts for deployment target").action(async (target) => {
|
|
2078
|
+
await buildTarget(process.cwd(), target);
|
|
2079
|
+
});
|
|
2080
|
+
const mcpCommand = program.command("mcp").description("Manage MCP servers");
|
|
2081
|
+
mcpCommand.command("add").requiredOption("--url <url>", "remote MCP url").option("--name <name>", "server name").option(
|
|
2082
|
+
"--auth-bearer-env <name>",
|
|
2083
|
+
"env var name containing bearer token for this MCP server"
|
|
2084
|
+
).option("--env <name>", "env variable (repeatable)", (value, all) => {
|
|
2085
|
+
all.push(value);
|
|
2086
|
+
return all;
|
|
2087
|
+
}, []).action(
|
|
2088
|
+
async (options) => {
|
|
2089
|
+
await mcpAdd(process.cwd(), {
|
|
2090
|
+
url: options.url,
|
|
2091
|
+
name: options.name,
|
|
2092
|
+
envVars: options.env,
|
|
2093
|
+
authBearerEnv: options.authBearerEnv
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
);
|
|
2097
|
+
mcpCommand.command("list").description("List configured MCP servers").action(async () => {
|
|
2098
|
+
await mcpList(process.cwd());
|
|
2099
|
+
});
|
|
2100
|
+
mcpCommand.command("remove").argument("<name>", "server name").description("Remove an MCP server by name").action(async (name) => {
|
|
2101
|
+
await mcpRemove(process.cwd(), name);
|
|
2102
|
+
});
|
|
2103
|
+
const mcpToolsCommand = mcpCommand.command("tools").description("Discover and curate tools for a configured MCP server");
|
|
2104
|
+
mcpToolsCommand.command("list").argument("<name>", "server name").description("Discover and list tools from a configured MCP server").action(async (name) => {
|
|
2105
|
+
await mcpToolsList(process.cwd(), name);
|
|
2106
|
+
});
|
|
2107
|
+
mcpToolsCommand.command("select").argument("<name>", "server name").description("Select MCP tools and store as config allowlist").option("--all", "select all discovered tools", false).option("--tools <csv>", "comma-separated discovered tool names").action(
|
|
2108
|
+
async (name, options) => {
|
|
2109
|
+
await mcpToolsSelect(process.cwd(), name, {
|
|
2110
|
+
all: options.all,
|
|
2111
|
+
toolsCsv: options.tools
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
);
|
|
2115
|
+
return program;
|
|
2116
|
+
};
|
|
2117
|
+
var main = async (argv = process.argv) => {
|
|
2118
|
+
try {
|
|
2119
|
+
await buildCli().parseAsync(argv);
|
|
2120
|
+
} catch (error) {
|
|
2121
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE") {
|
|
2122
|
+
const message = "Port is already in use. Try `poncho dev --port 3001` or stop the process using port 3000.";
|
|
2123
|
+
process.stderr.write(`${message}
|
|
2124
|
+
`);
|
|
2125
|
+
process.exitCode = 1;
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
process.stderr.write(`${error instanceof Error ? error.message : "Unknown CLI error"}
|
|
2129
|
+
`);
|
|
2130
|
+
process.exitCode = 1;
|
|
2131
|
+
}
|
|
2132
|
+
};
|
|
2133
|
+
var packageRoot = resolve(__dirname, "..");
|
|
2134
|
+
|
|
2135
|
+
export {
|
|
2136
|
+
initProject,
|
|
2137
|
+
updateAgentGuidance,
|
|
2138
|
+
createRequestHandler,
|
|
2139
|
+
startDevServer,
|
|
2140
|
+
runOnce,
|
|
2141
|
+
runInteractive,
|
|
2142
|
+
listTools,
|
|
2143
|
+
addSkill,
|
|
2144
|
+
runTests,
|
|
2145
|
+
buildTarget,
|
|
2146
|
+
mcpAdd,
|
|
2147
|
+
mcpList,
|
|
2148
|
+
mcpRemove,
|
|
2149
|
+
mcpToolsList,
|
|
2150
|
+
mcpToolsSelect,
|
|
2151
|
+
buildCli,
|
|
2152
|
+
main,
|
|
2153
|
+
packageRoot
|
|
2154
|
+
};
|