@poncho-ai/cli 0.3.0 → 0.3.2
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 +8 -9
- package/CHANGELOG.md +19 -0
- package/dist/chunk-3OBDF3LI.js +1893 -0
- package/dist/chunk-AS3CEZHY.js +2145 -0
- package/dist/chunk-ESPC4MPN.js +4043 -0
- package/dist/chunk-XABZUC4W.js +4043 -0
- package/dist/chunk-YJZ3MQIA.js +2145 -0
- package/dist/cli.js +1 -2
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -2
- package/dist/run-interactive-ink-BOD2F5JM.js +463 -0
- package/dist/run-interactive-ink-EHJVWEDC.js +462 -0
- package/dist/run-interactive-ink-M5E55KCC.js +463 -0
- package/package.json +2 -2
- package/src/index.ts +57 -15
- package/src/init-feature-context.ts +2 -1
- package/src/run-interactive-ink.ts +2 -1
- package/src/web-ui.ts +1 -1
|
@@ -0,0 +1,4043 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { access as access2, cp, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import {
|
|
6
|
+
createServer
|
|
7
|
+
} from "http";
|
|
8
|
+
import { dirname as dirname3, relative, resolve as resolve3 } from "path";
|
|
9
|
+
import { createRequire } from "module";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import {
|
|
12
|
+
AgentHarness,
|
|
13
|
+
LocalMcpBridge,
|
|
14
|
+
TelemetryEmitter,
|
|
15
|
+
createConversationStore,
|
|
16
|
+
loadPonchoConfig,
|
|
17
|
+
resolveStateConfig
|
|
18
|
+
} from "@poncho-ai/harness";
|
|
19
|
+
import { Command } from "commander";
|
|
20
|
+
import dotenv from "dotenv";
|
|
21
|
+
import YAML from "yaml";
|
|
22
|
+
|
|
23
|
+
// src/web-ui.ts
|
|
24
|
+
import { createHash, randomUUID, timingSafeEqual } from "crypto";
|
|
25
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
26
|
+
import { basename, dirname, resolve } from "path";
|
|
27
|
+
import { homedir } from "os";
|
|
28
|
+
var DEFAULT_OWNER = "local-owner";
|
|
29
|
+
var SessionStore = class {
|
|
30
|
+
sessions = /* @__PURE__ */ new Map();
|
|
31
|
+
ttlMs;
|
|
32
|
+
constructor(ttlMs = 1e3 * 60 * 60 * 8) {
|
|
33
|
+
this.ttlMs = ttlMs;
|
|
34
|
+
}
|
|
35
|
+
create(ownerId = DEFAULT_OWNER) {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const session = {
|
|
38
|
+
sessionId: randomUUID(),
|
|
39
|
+
ownerId,
|
|
40
|
+
csrfToken: randomUUID(),
|
|
41
|
+
createdAt: now,
|
|
42
|
+
expiresAt: now + this.ttlMs,
|
|
43
|
+
lastSeenAt: now
|
|
44
|
+
};
|
|
45
|
+
this.sessions.set(session.sessionId, session);
|
|
46
|
+
return session;
|
|
47
|
+
}
|
|
48
|
+
get(sessionId) {
|
|
49
|
+
const session = this.sessions.get(sessionId);
|
|
50
|
+
if (!session) {
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
if (Date.now() > session.expiresAt) {
|
|
54
|
+
this.sessions.delete(sessionId);
|
|
55
|
+
return void 0;
|
|
56
|
+
}
|
|
57
|
+
session.lastSeenAt = Date.now();
|
|
58
|
+
return session;
|
|
59
|
+
}
|
|
60
|
+
delete(sessionId) {
|
|
61
|
+
this.sessions.delete(sessionId);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var LoginRateLimiter = class {
|
|
65
|
+
constructor(maxAttempts = 5, windowMs = 1e3 * 60 * 5, lockoutMs = 1e3 * 60 * 10) {
|
|
66
|
+
this.maxAttempts = maxAttempts;
|
|
67
|
+
this.windowMs = windowMs;
|
|
68
|
+
this.lockoutMs = lockoutMs;
|
|
69
|
+
}
|
|
70
|
+
attempts = /* @__PURE__ */ new Map();
|
|
71
|
+
canAttempt(key) {
|
|
72
|
+
const current = this.attempts.get(key);
|
|
73
|
+
if (!current) {
|
|
74
|
+
return { allowed: true };
|
|
75
|
+
}
|
|
76
|
+
if (current.lockedUntil && Date.now() < current.lockedUntil) {
|
|
77
|
+
return {
|
|
78
|
+
allowed: false,
|
|
79
|
+
retryAfterSeconds: Math.ceil((current.lockedUntil - Date.now()) / 1e3)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { allowed: true };
|
|
83
|
+
}
|
|
84
|
+
registerFailure(key) {
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
const current = this.attempts.get(key);
|
|
87
|
+
if (!current || now - current.firstFailureAt > this.windowMs) {
|
|
88
|
+
this.attempts.set(key, { count: 1, firstFailureAt: now });
|
|
89
|
+
return { locked: false };
|
|
90
|
+
}
|
|
91
|
+
const count = current.count + 1;
|
|
92
|
+
const next = {
|
|
93
|
+
...current,
|
|
94
|
+
count
|
|
95
|
+
};
|
|
96
|
+
if (count >= this.maxAttempts) {
|
|
97
|
+
next.lockedUntil = now + this.lockoutMs;
|
|
98
|
+
this.attempts.set(key, next);
|
|
99
|
+
return { locked: true, retryAfterSeconds: Math.ceil(this.lockoutMs / 1e3) };
|
|
100
|
+
}
|
|
101
|
+
this.attempts.set(key, next);
|
|
102
|
+
return { locked: false };
|
|
103
|
+
}
|
|
104
|
+
registerSuccess(key) {
|
|
105
|
+
this.attempts.delete(key);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var parseCookies = (request) => {
|
|
109
|
+
const cookieHeader = request.headers.cookie ?? "";
|
|
110
|
+
const pairs = cookieHeader.split(";").map((part) => part.trim()).filter(Boolean);
|
|
111
|
+
const cookies = {};
|
|
112
|
+
for (const pair of pairs) {
|
|
113
|
+
const index = pair.indexOf("=");
|
|
114
|
+
if (index <= 0) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const key = pair.slice(0, index);
|
|
118
|
+
const value = pair.slice(index + 1);
|
|
119
|
+
try {
|
|
120
|
+
cookies[key] = decodeURIComponent(value);
|
|
121
|
+
} catch {
|
|
122
|
+
cookies[key] = value;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return cookies;
|
|
126
|
+
};
|
|
127
|
+
var setCookie = (response, name, value, options) => {
|
|
128
|
+
const segments = [`${name}=${encodeURIComponent(value)}`];
|
|
129
|
+
segments.push(`Path=${options.path ?? "/"}`);
|
|
130
|
+
if (typeof options.maxAge === "number") {
|
|
131
|
+
segments.push(`Max-Age=${Math.max(0, Math.floor(options.maxAge))}`);
|
|
132
|
+
}
|
|
133
|
+
if (options.httpOnly) {
|
|
134
|
+
segments.push("HttpOnly");
|
|
135
|
+
}
|
|
136
|
+
if (options.secure) {
|
|
137
|
+
segments.push("Secure");
|
|
138
|
+
}
|
|
139
|
+
if (options.sameSite) {
|
|
140
|
+
segments.push(`SameSite=${options.sameSite}`);
|
|
141
|
+
}
|
|
142
|
+
const previous = response.getHeader("Set-Cookie");
|
|
143
|
+
const serialized = segments.join("; ");
|
|
144
|
+
if (!previous) {
|
|
145
|
+
response.setHeader("Set-Cookie", serialized);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(previous)) {
|
|
149
|
+
response.setHeader("Set-Cookie", [...previous, serialized]);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
response.setHeader("Set-Cookie", [String(previous), serialized]);
|
|
153
|
+
};
|
|
154
|
+
var verifyPassphrase = (provided, expected) => {
|
|
155
|
+
const providedBuffer = Buffer.from(provided);
|
|
156
|
+
const expectedBuffer = Buffer.from(expected);
|
|
157
|
+
if (providedBuffer.length !== expectedBuffer.length) {
|
|
158
|
+
const zero = Buffer.alloc(expectedBuffer.length);
|
|
159
|
+
return timingSafeEqual(expectedBuffer, zero) && false;
|
|
160
|
+
}
|
|
161
|
+
return timingSafeEqual(providedBuffer, expectedBuffer);
|
|
162
|
+
};
|
|
163
|
+
var getRequestIp = (request) => {
|
|
164
|
+
return request.socket.remoteAddress ?? "unknown";
|
|
165
|
+
};
|
|
166
|
+
var inferConversationTitle = (text) => {
|
|
167
|
+
const normalized = text.trim().replace(/\s+/g, " ");
|
|
168
|
+
if (!normalized) {
|
|
169
|
+
return "New conversation";
|
|
170
|
+
}
|
|
171
|
+
return normalized.length <= 48 ? normalized : `${normalized.slice(0, 48)}...`;
|
|
172
|
+
};
|
|
173
|
+
var renderManifest = (options) => {
|
|
174
|
+
const name = options?.agentName ?? "Agent";
|
|
175
|
+
return JSON.stringify({
|
|
176
|
+
name,
|
|
177
|
+
short_name: name,
|
|
178
|
+
description: `${name} \u2014 AI agent powered by Poncho`,
|
|
179
|
+
start_url: "/",
|
|
180
|
+
display: "standalone",
|
|
181
|
+
background_color: "#000000",
|
|
182
|
+
theme_color: "#000000",
|
|
183
|
+
icons: [
|
|
184
|
+
{ src: "/icon.svg", sizes: "any", type: "image/svg+xml" },
|
|
185
|
+
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
|
|
186
|
+
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" }
|
|
187
|
+
]
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
var renderIconSvg = (options) => {
|
|
191
|
+
const letter = (options?.agentName ?? "A").charAt(0).toUpperCase();
|
|
192
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
193
|
+
<rect width="512" height="512" rx="96" fill="#000"/>
|
|
194
|
+
<text x="256" y="256" dy=".35em" text-anchor="middle"
|
|
195
|
+
font-family="-apple-system,BlinkMacSystemFont,sans-serif"
|
|
196
|
+
font-size="280" font-weight="700" fill="#fff">${letter}</text>
|
|
197
|
+
</svg>`;
|
|
198
|
+
};
|
|
199
|
+
var renderServiceWorker = () => `
|
|
200
|
+
const CACHE_NAME = "poncho-shell-v1";
|
|
201
|
+
const SHELL_URLS = ["/"];
|
|
202
|
+
|
|
203
|
+
self.addEventListener("install", (event) => {
|
|
204
|
+
event.waitUntil(
|
|
205
|
+
caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_URLS))
|
|
206
|
+
);
|
|
207
|
+
self.skipWaiting();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
self.addEventListener("activate", (event) => {
|
|
211
|
+
event.waitUntil(
|
|
212
|
+
caches.keys().then((keys) =>
|
|
213
|
+
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
|
|
214
|
+
)
|
|
215
|
+
);
|
|
216
|
+
self.clients.claim();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
self.addEventListener("fetch", (event) => {
|
|
220
|
+
const url = new URL(event.request.url);
|
|
221
|
+
// Only cache GET requests for the app shell; let API calls pass through
|
|
222
|
+
if (event.request.method !== "GET" || url.pathname.startsWith("/api/")) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
event.respondWith(
|
|
226
|
+
fetch(event.request)
|
|
227
|
+
.then((response) => {
|
|
228
|
+
const clone = response.clone();
|
|
229
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
|
230
|
+
return response;
|
|
231
|
+
})
|
|
232
|
+
.catch(() => caches.match(event.request))
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
`;
|
|
236
|
+
var renderWebUiHtml = (options) => {
|
|
237
|
+
const agentInitial = (options?.agentName ?? "A").charAt(0).toUpperCase();
|
|
238
|
+
const agentName = options?.agentName ?? "Agent";
|
|
239
|
+
return `<!doctype html>
|
|
240
|
+
<html lang="en">
|
|
241
|
+
<head>
|
|
242
|
+
<meta charset="utf-8">
|
|
243
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
|
|
244
|
+
<meta name="theme-color" content="#000000">
|
|
245
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
246
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
247
|
+
<meta name="apple-mobile-web-app-title" content="${agentName}">
|
|
248
|
+
<link rel="manifest" href="/manifest.json">
|
|
249
|
+
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
|
250
|
+
<link rel="apple-touch-icon" href="/icon-192.png">
|
|
251
|
+
<title>${agentName}</title>
|
|
252
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inconsolata:400,700">
|
|
253
|
+
<style>
|
|
254
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
255
|
+
html, body { height: 100vh; overflow: hidden; overscroll-behavior: none; touch-action: pan-y; }
|
|
256
|
+
body {
|
|
257
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", sans-serif;
|
|
258
|
+
background: #000;
|
|
259
|
+
color: #ededed;
|
|
260
|
+
font-size: 14px;
|
|
261
|
+
line-height: 1.5;
|
|
262
|
+
-webkit-font-smoothing: antialiased;
|
|
263
|
+
-moz-osx-font-smoothing: grayscale;
|
|
264
|
+
}
|
|
265
|
+
button, input, textarea { font: inherit; color: inherit; }
|
|
266
|
+
.hidden { display: none !important; }
|
|
267
|
+
a { color: #ededed; }
|
|
268
|
+
|
|
269
|
+
/* Auth */
|
|
270
|
+
.auth {
|
|
271
|
+
min-height: 100vh;
|
|
272
|
+
display: grid;
|
|
273
|
+
place-items: center;
|
|
274
|
+
padding: 20px;
|
|
275
|
+
background: #000;
|
|
276
|
+
}
|
|
277
|
+
.auth-card {
|
|
278
|
+
width: min(380px, 90vw);
|
|
279
|
+
background: #0a0a0a;
|
|
280
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
281
|
+
border-radius: 12px;
|
|
282
|
+
padding: 32px;
|
|
283
|
+
display: grid;
|
|
284
|
+
gap: 20px;
|
|
285
|
+
}
|
|
286
|
+
.auth-brand {
|
|
287
|
+
display: flex;
|
|
288
|
+
align-items: center;
|
|
289
|
+
gap: 8px;
|
|
290
|
+
}
|
|
291
|
+
.auth-brand svg { width: 20px; height: 20px; }
|
|
292
|
+
.auth-title {
|
|
293
|
+
font-size: 16px;
|
|
294
|
+
font-weight: 500;
|
|
295
|
+
letter-spacing: -0.01em;
|
|
296
|
+
}
|
|
297
|
+
.auth-text { color: #666; font-size: 13px; line-height: 1.5; }
|
|
298
|
+
.auth-input {
|
|
299
|
+
width: 100%;
|
|
300
|
+
background: #000;
|
|
301
|
+
border: 1px solid rgba(255,255,255,0.12);
|
|
302
|
+
border-radius: 6px;
|
|
303
|
+
color: #ededed;
|
|
304
|
+
padding: 10px 12px;
|
|
305
|
+
font-size: 14px;
|
|
306
|
+
outline: none;
|
|
307
|
+
transition: border-color 0.15s;
|
|
308
|
+
}
|
|
309
|
+
.auth-input:focus { border-color: rgba(255,255,255,0.3); }
|
|
310
|
+
.auth-input::placeholder { color: #555; }
|
|
311
|
+
.auth-submit {
|
|
312
|
+
background: #ededed;
|
|
313
|
+
color: #000;
|
|
314
|
+
border: 0;
|
|
315
|
+
border-radius: 6px;
|
|
316
|
+
padding: 10px 16px;
|
|
317
|
+
font-size: 14px;
|
|
318
|
+
font-weight: 500;
|
|
319
|
+
cursor: pointer;
|
|
320
|
+
transition: background 0.15s;
|
|
321
|
+
}
|
|
322
|
+
.auth-submit:hover { background: #fff; }
|
|
323
|
+
.error { color: #ff4444; font-size: 13px; min-height: 16px; }
|
|
324
|
+
.message-error {
|
|
325
|
+
background: rgba(255,68,68,0.08);
|
|
326
|
+
border: 1px solid rgba(255,68,68,0.25);
|
|
327
|
+
border-radius: 10px;
|
|
328
|
+
color: #ff6b6b;
|
|
329
|
+
padding: 12px 16px;
|
|
330
|
+
font-size: 13px;
|
|
331
|
+
line-height: 1.5;
|
|
332
|
+
max-width: 600px;
|
|
333
|
+
}
|
|
334
|
+
.message-error strong { color: #ff4444; }
|
|
335
|
+
|
|
336
|
+
/* Layout - use fixed positioning with explicit dimensions */
|
|
337
|
+
.shell {
|
|
338
|
+
position: fixed;
|
|
339
|
+
top: 0;
|
|
340
|
+
left: 0;
|
|
341
|
+
width: 100vw;
|
|
342
|
+
height: 100vh;
|
|
343
|
+
height: 100dvh; /* Dynamic viewport height for normal browsers */
|
|
344
|
+
display: flex;
|
|
345
|
+
overflow: hidden;
|
|
346
|
+
}
|
|
347
|
+
/* PWA standalone mode: use 100vh which works correctly */
|
|
348
|
+
@media (display-mode: standalone) {
|
|
349
|
+
.shell {
|
|
350
|
+
height: 100vh;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/* Edge swipe blocker - invisible touch target to intercept right edge gestures */
|
|
355
|
+
.edge-blocker-right {
|
|
356
|
+
position: fixed;
|
|
357
|
+
top: 0;
|
|
358
|
+
bottom: 0;
|
|
359
|
+
right: 0;
|
|
360
|
+
width: 20px;
|
|
361
|
+
z-index: 9999;
|
|
362
|
+
touch-action: none;
|
|
363
|
+
}
|
|
364
|
+
.sidebar {
|
|
365
|
+
width: 260px;
|
|
366
|
+
background: #000;
|
|
367
|
+
border-right: 1px solid rgba(255,255,255,0.06);
|
|
368
|
+
display: flex;
|
|
369
|
+
flex-direction: column;
|
|
370
|
+
padding: 12px 8px;
|
|
371
|
+
}
|
|
372
|
+
.new-chat-btn {
|
|
373
|
+
background: transparent;
|
|
374
|
+
border: 0;
|
|
375
|
+
color: #888;
|
|
376
|
+
border-radius: 12px;
|
|
377
|
+
height: 36px;
|
|
378
|
+
padding: 0 10px;
|
|
379
|
+
display: flex;
|
|
380
|
+
align-items: center;
|
|
381
|
+
gap: 8px;
|
|
382
|
+
font-size: 13px;
|
|
383
|
+
cursor: pointer;
|
|
384
|
+
transition: background 0.15s, color 0.15s;
|
|
385
|
+
}
|
|
386
|
+
.new-chat-btn:hover { color: #ededed; }
|
|
387
|
+
.new-chat-btn svg { width: 16px; height: 16px; }
|
|
388
|
+
.conversation-list {
|
|
389
|
+
flex: 1;
|
|
390
|
+
overflow-y: auto;
|
|
391
|
+
margin-top: 12px;
|
|
392
|
+
display: flex;
|
|
393
|
+
flex-direction: column;
|
|
394
|
+
gap: 2px;
|
|
395
|
+
}
|
|
396
|
+
.conversation-item {
|
|
397
|
+
padding: 7px 28px 7px 10px;
|
|
398
|
+
border-radius: 12px;
|
|
399
|
+
cursor: pointer;
|
|
400
|
+
font-size: 13px;
|
|
401
|
+
color: #555;
|
|
402
|
+
white-space: nowrap;
|
|
403
|
+
overflow: hidden;
|
|
404
|
+
text-overflow: ellipsis;
|
|
405
|
+
position: relative;
|
|
406
|
+
transition: color 0.15s;
|
|
407
|
+
}
|
|
408
|
+
.conversation-item:hover { color: #999; }
|
|
409
|
+
.conversation-item.active {
|
|
410
|
+
color: #ededed;
|
|
411
|
+
}
|
|
412
|
+
.conversation-item .delete-btn {
|
|
413
|
+
position: absolute;
|
|
414
|
+
right: 0;
|
|
415
|
+
top: 0;
|
|
416
|
+
bottom: 0;
|
|
417
|
+
opacity: 0;
|
|
418
|
+
background: #000;
|
|
419
|
+
border: 0;
|
|
420
|
+
color: #444;
|
|
421
|
+
padding: 0 8px;
|
|
422
|
+
border-radius: 0 4px 4px 0;
|
|
423
|
+
cursor: pointer;
|
|
424
|
+
font-size: 16px;
|
|
425
|
+
line-height: 1;
|
|
426
|
+
display: grid;
|
|
427
|
+
place-items: center;
|
|
428
|
+
transition: opacity 0.15s, color 0.15s;
|
|
429
|
+
}
|
|
430
|
+
.conversation-item:hover .delete-btn { opacity: 1; }
|
|
431
|
+
.conversation-item.active .delete-btn { background: rgba(0,0,0,1); }
|
|
432
|
+
.conversation-item .delete-btn::before {
|
|
433
|
+
content: "";
|
|
434
|
+
position: absolute;
|
|
435
|
+
right: 100%;
|
|
436
|
+
top: 0;
|
|
437
|
+
bottom: 0;
|
|
438
|
+
width: 24px;
|
|
439
|
+
background: linear-gradient(to right, transparent, #000);
|
|
440
|
+
pointer-events: none;
|
|
441
|
+
}
|
|
442
|
+
.conversation-item.active .delete-btn::before {
|
|
443
|
+
background: linear-gradient(to right, transparent, rgba(0,0,0,1));
|
|
444
|
+
}
|
|
445
|
+
.conversation-item .delete-btn:hover { color: #888; }
|
|
446
|
+
.conversation-item .delete-btn.confirming {
|
|
447
|
+
opacity: 1;
|
|
448
|
+
width: auto;
|
|
449
|
+
padding: 0 8px;
|
|
450
|
+
font-size: 11px;
|
|
451
|
+
color: #ff4444;
|
|
452
|
+
border-radius: 3px;
|
|
453
|
+
}
|
|
454
|
+
.conversation-item .delete-btn.confirming:hover {
|
|
455
|
+
color: #ff6666;
|
|
456
|
+
}
|
|
457
|
+
.sidebar-footer {
|
|
458
|
+
margin-top: auto;
|
|
459
|
+
padding-top: 8px;
|
|
460
|
+
}
|
|
461
|
+
.logout-btn {
|
|
462
|
+
background: transparent;
|
|
463
|
+
border: 0;
|
|
464
|
+
color: #555;
|
|
465
|
+
width: 100%;
|
|
466
|
+
padding: 8px 10px;
|
|
467
|
+
text-align: left;
|
|
468
|
+
border-radius: 6px;
|
|
469
|
+
cursor: pointer;
|
|
470
|
+
font-size: 13px;
|
|
471
|
+
transition: color 0.15s, background 0.15s;
|
|
472
|
+
}
|
|
473
|
+
.logout-btn:hover { color: #888; }
|
|
474
|
+
|
|
475
|
+
/* Main */
|
|
476
|
+
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; max-width: 100%; background: #000; overflow: hidden; }
|
|
477
|
+
.topbar {
|
|
478
|
+
height: calc(52px + env(safe-area-inset-top, 0px));
|
|
479
|
+
padding-top: env(safe-area-inset-top, 0px);
|
|
480
|
+
display: flex;
|
|
481
|
+
align-items: center;
|
|
482
|
+
justify-content: center;
|
|
483
|
+
font-size: 13px;
|
|
484
|
+
font-weight: 500;
|
|
485
|
+
color: #888;
|
|
486
|
+
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
487
|
+
position: relative;
|
|
488
|
+
flex-shrink: 0;
|
|
489
|
+
}
|
|
490
|
+
.topbar-title {
|
|
491
|
+
max-width: calc(100% - 100px);
|
|
492
|
+
overflow: hidden;
|
|
493
|
+
text-overflow: ellipsis;
|
|
494
|
+
white-space: nowrap;
|
|
495
|
+
letter-spacing: -0.01em;
|
|
496
|
+
padding: 0 50px;
|
|
497
|
+
}
|
|
498
|
+
.sidebar-toggle {
|
|
499
|
+
display: none;
|
|
500
|
+
position: absolute;
|
|
501
|
+
left: 12px;
|
|
502
|
+
bottom: 4px; /* Position from bottom of topbar content area */
|
|
503
|
+
background: transparent;
|
|
504
|
+
border: 0;
|
|
505
|
+
color: #666;
|
|
506
|
+
width: 44px;
|
|
507
|
+
height: 44px;
|
|
508
|
+
border-radius: 6px;
|
|
509
|
+
cursor: pointer;
|
|
510
|
+
transition: color 0.15s, background 0.15s;
|
|
511
|
+
font-size: 18px;
|
|
512
|
+
z-index: 10;
|
|
513
|
+
-webkit-tap-highlight-color: transparent;
|
|
514
|
+
}
|
|
515
|
+
.sidebar-toggle:hover { color: #ededed; }
|
|
516
|
+
.topbar-new-chat {
|
|
517
|
+
display: none;
|
|
518
|
+
position: absolute;
|
|
519
|
+
right: 12px;
|
|
520
|
+
bottom: 4px;
|
|
521
|
+
background: transparent;
|
|
522
|
+
border: 0;
|
|
523
|
+
color: #666;
|
|
524
|
+
width: 44px;
|
|
525
|
+
height: 44px;
|
|
526
|
+
border-radius: 6px;
|
|
527
|
+
cursor: pointer;
|
|
528
|
+
transition: color 0.15s, background 0.15s;
|
|
529
|
+
z-index: 10;
|
|
530
|
+
-webkit-tap-highlight-color: transparent;
|
|
531
|
+
}
|
|
532
|
+
.topbar-new-chat:hover { color: #ededed; }
|
|
533
|
+
.topbar-new-chat svg { width: 16px; height: 16px; }
|
|
534
|
+
|
|
535
|
+
/* Messages */
|
|
536
|
+
.messages { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 24px 24px; }
|
|
537
|
+
.messages-column { max-width: 680px; margin: 0 auto; }
|
|
538
|
+
.message-row { margin-bottom: 24px; display: flex; max-width: 100%; }
|
|
539
|
+
.message-row.user { justify-content: flex-end; }
|
|
540
|
+
.assistant-wrap { display: flex; gap: 12px; max-width: 100%; min-width: 0; }
|
|
541
|
+
.assistant-avatar {
|
|
542
|
+
width: 24px;
|
|
543
|
+
height: 24px;
|
|
544
|
+
background: #ededed;
|
|
545
|
+
color: #000;
|
|
546
|
+
border-radius: 6px;
|
|
547
|
+
display: grid;
|
|
548
|
+
place-items: center;
|
|
549
|
+
font-size: 11px;
|
|
550
|
+
font-weight: 600;
|
|
551
|
+
flex-shrink: 0;
|
|
552
|
+
margin-top: 2px;
|
|
553
|
+
}
|
|
554
|
+
.assistant-content {
|
|
555
|
+
line-height: 1.65;
|
|
556
|
+
color: #ededed;
|
|
557
|
+
font-size: 14px;
|
|
558
|
+
min-width: 0;
|
|
559
|
+
max-width: 100%;
|
|
560
|
+
overflow-wrap: break-word;
|
|
561
|
+
word-break: break-word;
|
|
562
|
+
margin-top: 2px;
|
|
563
|
+
}
|
|
564
|
+
.assistant-content p { margin: 0 0 12px; }
|
|
565
|
+
.assistant-content p:last-child { margin-bottom: 0; }
|
|
566
|
+
.assistant-content ul, .assistant-content ol { margin: 8px 0; padding-left: 20px; }
|
|
567
|
+
.assistant-content li { margin: 4px 0; }
|
|
568
|
+
.assistant-content strong { font-weight: 600; color: #fff; }
|
|
569
|
+
.assistant-content h2 {
|
|
570
|
+
font-size: 16px;
|
|
571
|
+
font-weight: 600;
|
|
572
|
+
letter-spacing: -0.02em;
|
|
573
|
+
margin: 20px 0 8px;
|
|
574
|
+
color: #fff;
|
|
575
|
+
}
|
|
576
|
+
.assistant-content h3 {
|
|
577
|
+
font-size: 14px;
|
|
578
|
+
font-weight: 600;
|
|
579
|
+
letter-spacing: -0.01em;
|
|
580
|
+
margin: 16px 0 6px;
|
|
581
|
+
color: #fff;
|
|
582
|
+
}
|
|
583
|
+
.assistant-content code {
|
|
584
|
+
background: rgba(255,255,255,0.06);
|
|
585
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
586
|
+
padding: 2px 5px;
|
|
587
|
+
border-radius: 4px;
|
|
588
|
+
font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
|
|
589
|
+
font-size: 0.88em;
|
|
590
|
+
}
|
|
591
|
+
.assistant-content pre {
|
|
592
|
+
background: #0a0a0a;
|
|
593
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
594
|
+
padding: 14px 16px;
|
|
595
|
+
border-radius: 8px;
|
|
596
|
+
overflow-x: auto;
|
|
597
|
+
margin: 14px 0;
|
|
598
|
+
}
|
|
599
|
+
.assistant-content pre code {
|
|
600
|
+
background: none;
|
|
601
|
+
border: 0;
|
|
602
|
+
padding: 0;
|
|
603
|
+
font-size: 13px;
|
|
604
|
+
line-height: 1.5;
|
|
605
|
+
}
|
|
606
|
+
.tool-activity {
|
|
607
|
+
margin-top: 12px;
|
|
608
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
609
|
+
background: rgba(255,255,255,0.03);
|
|
610
|
+
border-radius: 10px;
|
|
611
|
+
font-size: 12px;
|
|
612
|
+
line-height: 1.45;
|
|
613
|
+
color: #bcbcbc;
|
|
614
|
+
max-width: 300px;
|
|
615
|
+
}
|
|
616
|
+
.tool-activity-disclosure {
|
|
617
|
+
display: block;
|
|
618
|
+
}
|
|
619
|
+
.tool-activity-summary {
|
|
620
|
+
list-style: none;
|
|
621
|
+
display: flex;
|
|
622
|
+
align-items: center;
|
|
623
|
+
gap: 8px;
|
|
624
|
+
cursor: pointer;
|
|
625
|
+
padding: 10px 12px;
|
|
626
|
+
user-select: none;
|
|
627
|
+
}
|
|
628
|
+
.tool-activity-summary::-webkit-details-marker {
|
|
629
|
+
display: none;
|
|
630
|
+
}
|
|
631
|
+
.tool-activity-label {
|
|
632
|
+
font-size: 11px;
|
|
633
|
+
text-transform: uppercase;
|
|
634
|
+
letter-spacing: 0.06em;
|
|
635
|
+
color: #8a8a8a;
|
|
636
|
+
font-weight: 600;
|
|
637
|
+
}
|
|
638
|
+
.tool-activity-caret {
|
|
639
|
+
margin-left: auto;
|
|
640
|
+
color: #8a8a8a;
|
|
641
|
+
display: inline-flex;
|
|
642
|
+
align-items: center;
|
|
643
|
+
justify-content: center;
|
|
644
|
+
transition: transform 120ms ease;
|
|
645
|
+
transform: rotate(0deg);
|
|
646
|
+
}
|
|
647
|
+
.tool-activity-caret svg {
|
|
648
|
+
width: 14px;
|
|
649
|
+
height: 14px;
|
|
650
|
+
display: block;
|
|
651
|
+
}
|
|
652
|
+
.tool-activity-disclosure[open] .tool-activity-caret {
|
|
653
|
+
transform: rotate(90deg);
|
|
654
|
+
}
|
|
655
|
+
.tool-activity-list {
|
|
656
|
+
display: grid;
|
|
657
|
+
gap: 6px;
|
|
658
|
+
padding: 0 12px 10px;
|
|
659
|
+
}
|
|
660
|
+
.tool-activity-item {
|
|
661
|
+
font-family: ui-monospace, "SF Mono", "Fira Code", monospace;
|
|
662
|
+
background: rgba(255,255,255,0.04);
|
|
663
|
+
border-radius: 6px;
|
|
664
|
+
padding: 4px 7px;
|
|
665
|
+
color: #d6d6d6;
|
|
666
|
+
}
|
|
667
|
+
.user-bubble {
|
|
668
|
+
background: #111;
|
|
669
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
670
|
+
padding: 10px 16px;
|
|
671
|
+
border-radius: 18px;
|
|
672
|
+
max-width: 70%;
|
|
673
|
+
font-size: 14px;
|
|
674
|
+
line-height: 1.5;
|
|
675
|
+
overflow-wrap: break-word;
|
|
676
|
+
word-break: break-word;
|
|
677
|
+
}
|
|
678
|
+
.empty-state {
|
|
679
|
+
display: flex;
|
|
680
|
+
flex-direction: column;
|
|
681
|
+
align-items: center;
|
|
682
|
+
justify-content: center;
|
|
683
|
+
height: 100%;
|
|
684
|
+
gap: 16px;
|
|
685
|
+
color: #555;
|
|
686
|
+
}
|
|
687
|
+
.empty-state .assistant-avatar {
|
|
688
|
+
width: 36px;
|
|
689
|
+
height: 36px;
|
|
690
|
+
font-size: 14px;
|
|
691
|
+
border-radius: 8px;
|
|
692
|
+
}
|
|
693
|
+
.empty-state-text {
|
|
694
|
+
font-size: 14px;
|
|
695
|
+
color: #555;
|
|
696
|
+
}
|
|
697
|
+
.thinking-indicator {
|
|
698
|
+
display: inline-block;
|
|
699
|
+
font-family: Inconsolata, monospace;
|
|
700
|
+
font-size: 20px;
|
|
701
|
+
line-height: 1;
|
|
702
|
+
vertical-align: middle;
|
|
703
|
+
color: #ededed;
|
|
704
|
+
opacity: 0.5;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/* Composer */
|
|
708
|
+
.composer {
|
|
709
|
+
padding: 12px 24px 24px;
|
|
710
|
+
position: relative;
|
|
711
|
+
}
|
|
712
|
+
/* PWA standalone mode - extra bottom padding */
|
|
713
|
+
@media (display-mode: standalone), (-webkit-touch-callout: none) {
|
|
714
|
+
.composer {
|
|
715
|
+
padding-bottom: 32px;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
@supports (-webkit-touch-callout: none) {
|
|
719
|
+
/* iOS Safari standalone check via JS class */
|
|
720
|
+
.standalone .composer {
|
|
721
|
+
padding-bottom: 32px;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
.composer::before {
|
|
725
|
+
content: "";
|
|
726
|
+
position: absolute;
|
|
727
|
+
left: 0;
|
|
728
|
+
right: 0;
|
|
729
|
+
bottom: 100%;
|
|
730
|
+
height: 48px;
|
|
731
|
+
background: linear-gradient(to top, #000 0%, transparent 100%);
|
|
732
|
+
pointer-events: none;
|
|
733
|
+
}
|
|
734
|
+
.composer-inner { max-width: 680px; margin: 0 auto; }
|
|
735
|
+
.composer-shell {
|
|
736
|
+
background: #0a0a0a;
|
|
737
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
738
|
+
border-radius: 9999px;
|
|
739
|
+
display: flex;
|
|
740
|
+
align-items: center;
|
|
741
|
+
padding: 4px 6px 4px 18px;
|
|
742
|
+
transition: border-color 0.15s;
|
|
743
|
+
}
|
|
744
|
+
.composer-shell:focus-within { border-color: rgba(255,255,255,0.2); }
|
|
745
|
+
.composer-input {
|
|
746
|
+
flex: 1;
|
|
747
|
+
background: transparent;
|
|
748
|
+
border: 0;
|
|
749
|
+
outline: none;
|
|
750
|
+
color: #ededed;
|
|
751
|
+
min-height: 40px;
|
|
752
|
+
max-height: 200px;
|
|
753
|
+
resize: none;
|
|
754
|
+
padding: 10px 0 8px;
|
|
755
|
+
font-size: 14px;
|
|
756
|
+
line-height: 1.5;
|
|
757
|
+
}
|
|
758
|
+
.composer-input::placeholder { color: #444; }
|
|
759
|
+
.send-btn {
|
|
760
|
+
width: 32px;
|
|
761
|
+
height: 32px;
|
|
762
|
+
background: #ededed;
|
|
763
|
+
border: 0;
|
|
764
|
+
border-radius: 50%;
|
|
765
|
+
color: #000;
|
|
766
|
+
cursor: pointer;
|
|
767
|
+
display: grid;
|
|
768
|
+
place-items: center;
|
|
769
|
+
flex-shrink: 0;
|
|
770
|
+
margin-bottom: 2px;
|
|
771
|
+
transition: background 0.15s, opacity 0.15s;
|
|
772
|
+
}
|
|
773
|
+
.send-btn:hover { background: #fff; }
|
|
774
|
+
.send-btn:disabled { opacity: 0.2; cursor: default; }
|
|
775
|
+
.send-btn:disabled:hover { background: #ededed; }
|
|
776
|
+
.disclaimer {
|
|
777
|
+
text-align: center;
|
|
778
|
+
color: #333;
|
|
779
|
+
font-size: 12px;
|
|
780
|
+
margin-top: 10px;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/* Scrollbar */
|
|
784
|
+
::-webkit-scrollbar { width: 6px; }
|
|
785
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
786
|
+
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
|
|
787
|
+
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.16); }
|
|
788
|
+
|
|
789
|
+
/* Mobile */
|
|
790
|
+
@media (max-width: 768px) {
|
|
791
|
+
.sidebar {
|
|
792
|
+
position: fixed;
|
|
793
|
+
inset: 0 auto 0 0;
|
|
794
|
+
z-index: 100;
|
|
795
|
+
transform: translateX(-100%);
|
|
796
|
+
padding-top: calc(env(safe-area-inset-top, 0px) + 12px);
|
|
797
|
+
will-change: transform;
|
|
798
|
+
}
|
|
799
|
+
.sidebar.dragging { transition: none; }
|
|
800
|
+
.sidebar:not(.dragging) { transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); }
|
|
801
|
+
.shell.sidebar-open .sidebar { transform: translateX(0); }
|
|
802
|
+
.sidebar-toggle { display: grid; place-items: center; }
|
|
803
|
+
.topbar-new-chat { display: grid; place-items: center; }
|
|
804
|
+
.sidebar-backdrop {
|
|
805
|
+
position: fixed;
|
|
806
|
+
inset: 0;
|
|
807
|
+
background: rgba(0,0,0,0.6);
|
|
808
|
+
z-index: 50;
|
|
809
|
+
backdrop-filter: blur(2px);
|
|
810
|
+
-webkit-backdrop-filter: blur(2px);
|
|
811
|
+
opacity: 0;
|
|
812
|
+
pointer-events: none;
|
|
813
|
+
will-change: opacity;
|
|
814
|
+
}
|
|
815
|
+
.sidebar-backdrop:not(.dragging) { transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1); }
|
|
816
|
+
.sidebar-backdrop.dragging { transition: none; }
|
|
817
|
+
.shell.sidebar-open .sidebar-backdrop { opacity: 1; pointer-events: auto; }
|
|
818
|
+
.messages { padding: 16px; }
|
|
819
|
+
.composer { padding: 8px 16px 16px; }
|
|
820
|
+
/* Always show delete button on mobile (no hover) */
|
|
821
|
+
.conversation-item .delete-btn { opacity: 1; }
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/* Reduced motion */
|
|
825
|
+
@media (prefers-reduced-motion: reduce) {
|
|
826
|
+
*, *::before, *::after {
|
|
827
|
+
animation-duration: 0.01ms !important;
|
|
828
|
+
transition-duration: 0.01ms !important;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
</style>
|
|
832
|
+
</head>
|
|
833
|
+
<body data-agent-initial="${agentInitial}" data-agent-name="${agentName}">
|
|
834
|
+
<div class="edge-blocker-right"></div>
|
|
835
|
+
<div id="auth" class="auth hidden">
|
|
836
|
+
<form id="login-form" class="auth-card">
|
|
837
|
+
<div class="auth-brand">
|
|
838
|
+
<svg viewBox="0 0 24 24" fill="none"><path d="M12 2L2 19.5h20L12 2z" fill="currentColor"/></svg>
|
|
839
|
+
<h2 class="auth-title">Poncho</h2>
|
|
840
|
+
</div>
|
|
841
|
+
<p class="auth-text">Enter the passphrase to continue.</p>
|
|
842
|
+
<input id="passphrase" class="auth-input" type="password" placeholder="Passphrase" required>
|
|
843
|
+
<button class="auth-submit" type="submit">Continue</button>
|
|
844
|
+
<div id="login-error" class="error"></div>
|
|
845
|
+
</form>
|
|
846
|
+
</div>
|
|
847
|
+
|
|
848
|
+
<div id="app" class="shell hidden">
|
|
849
|
+
<aside class="sidebar">
|
|
850
|
+
<button id="new-chat" class="new-chat-btn">
|
|
851
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
|
852
|
+
</button>
|
|
853
|
+
<div id="conversation-list" class="conversation-list"></div>
|
|
854
|
+
<div class="sidebar-footer">
|
|
855
|
+
<button id="logout" class="logout-btn">Log out</button>
|
|
856
|
+
</div>
|
|
857
|
+
</aside>
|
|
858
|
+
<div id="sidebar-backdrop" class="sidebar-backdrop"></div>
|
|
859
|
+
<main class="main">
|
|
860
|
+
<div class="topbar">
|
|
861
|
+
<button id="sidebar-toggle" class="sidebar-toggle">☰</button>
|
|
862
|
+
<div id="chat-title" class="topbar-title"></div>
|
|
863
|
+
<button id="topbar-new-chat" class="topbar-new-chat">
|
|
864
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
|
865
|
+
</button>
|
|
866
|
+
</div>
|
|
867
|
+
<div id="messages" class="messages">
|
|
868
|
+
<div class="empty-state">
|
|
869
|
+
<div class="assistant-avatar">${agentInitial}</div>
|
|
870
|
+
<div class="empty-state-text">How can I help you today?</div>
|
|
871
|
+
</div>
|
|
872
|
+
</div>
|
|
873
|
+
<form id="composer" class="composer">
|
|
874
|
+
<div class="composer-inner">
|
|
875
|
+
<div class="composer-shell">
|
|
876
|
+
<textarea id="prompt" class="composer-input" placeholder="Send a message..." rows="1"></textarea>
|
|
877
|
+
<button id="send" class="send-btn" type="submit">
|
|
878
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 12V4M4 7l4-4 4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
879
|
+
</button>
|
|
880
|
+
</div>
|
|
881
|
+
</div>
|
|
882
|
+
</form>
|
|
883
|
+
</main>
|
|
884
|
+
</div>
|
|
885
|
+
|
|
886
|
+
<script>
|
|
887
|
+
const state = {
|
|
888
|
+
csrfToken: "",
|
|
889
|
+
conversations: [],
|
|
890
|
+
activeConversationId: null,
|
|
891
|
+
activeMessages: [],
|
|
892
|
+
isStreaming: false,
|
|
893
|
+
confirmDeleteId: null
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
const agentInitial = document.body.dataset.agentInitial || "A";
|
|
897
|
+
const $ = (id) => document.getElementById(id);
|
|
898
|
+
const elements = {
|
|
899
|
+
auth: $("auth"),
|
|
900
|
+
app: $("app"),
|
|
901
|
+
loginForm: $("login-form"),
|
|
902
|
+
passphrase: $("passphrase"),
|
|
903
|
+
loginError: $("login-error"),
|
|
904
|
+
list: $("conversation-list"),
|
|
905
|
+
newChat: $("new-chat"),
|
|
906
|
+
topbarNewChat: $("topbar-new-chat"),
|
|
907
|
+
messages: $("messages"),
|
|
908
|
+
chatTitle: $("chat-title"),
|
|
909
|
+
logout: $("logout"),
|
|
910
|
+
composer: $("composer"),
|
|
911
|
+
prompt: $("prompt"),
|
|
912
|
+
send: $("send"),
|
|
913
|
+
shell: $("app"),
|
|
914
|
+
sidebarToggle: $("sidebar-toggle"),
|
|
915
|
+
sidebarBackdrop: $("sidebar-backdrop")
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
const pushConversationUrl = (conversationId) => {
|
|
919
|
+
const target = conversationId ? "/c/" + encodeURIComponent(conversationId) : "/";
|
|
920
|
+
if (window.location.pathname !== target) {
|
|
921
|
+
history.pushState({ conversationId: conversationId || null }, "", target);
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
const replaceConversationUrl = (conversationId) => {
|
|
926
|
+
const target = conversationId ? "/c/" + encodeURIComponent(conversationId) : "/";
|
|
927
|
+
if (window.location.pathname !== target) {
|
|
928
|
+
history.replaceState({ conversationId: conversationId || null }, "", target);
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
const getConversationIdFromUrl = () => {
|
|
933
|
+
const match = window.location.pathname.match(/^\\/c\\/([^\\/]+)/);
|
|
934
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
const mutatingMethods = new Set(["POST", "PATCH", "PUT", "DELETE"]);
|
|
938
|
+
|
|
939
|
+
const api = async (path, options = {}) => {
|
|
940
|
+
const method = (options.method || "GET").toUpperCase();
|
|
941
|
+
const headers = { ...(options.headers || {}) };
|
|
942
|
+
if (mutatingMethods.has(method) && state.csrfToken) {
|
|
943
|
+
headers["x-csrf-token"] = state.csrfToken;
|
|
944
|
+
}
|
|
945
|
+
if (options.body && !headers["Content-Type"]) {
|
|
946
|
+
headers["Content-Type"] = "application/json";
|
|
947
|
+
}
|
|
948
|
+
const response = await fetch(path, { credentials: "include", ...options, method, headers });
|
|
949
|
+
if (!response.ok) {
|
|
950
|
+
let payload = {};
|
|
951
|
+
try { payload = await response.json(); } catch {}
|
|
952
|
+
const error = new Error(payload.message || ("Request failed: " + response.status));
|
|
953
|
+
error.status = response.status;
|
|
954
|
+
error.payload = payload;
|
|
955
|
+
throw error;
|
|
956
|
+
}
|
|
957
|
+
const contentType = response.headers.get("content-type") || "";
|
|
958
|
+
if (contentType.includes("application/json")) {
|
|
959
|
+
return await response.json();
|
|
960
|
+
}
|
|
961
|
+
return await response.text();
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
const escapeHtml = (value) =>
|
|
965
|
+
String(value || "")
|
|
966
|
+
.replace(/&/g, "&")
|
|
967
|
+
.replace(/</g, "<")
|
|
968
|
+
.replace(/>/g, ">")
|
|
969
|
+
.replace(/"/g, """)
|
|
970
|
+
.replace(/'/g, "'");
|
|
971
|
+
|
|
972
|
+
const renderInlineMarkdown = (value) => {
|
|
973
|
+
let html = escapeHtml(value);
|
|
974
|
+
html = html.replace(/\\*\\*([^*]+)\\*\\*/g, "<strong>$1</strong>");
|
|
975
|
+
html = html.replace(/\\x60([^\\x60]+)\\x60/g, "<code>$1</code>");
|
|
976
|
+
return html;
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
const renderMarkdownBlock = (value) => {
|
|
980
|
+
const lines = String(value || "").split("\\n");
|
|
981
|
+
let html = "";
|
|
982
|
+
let inList = false;
|
|
983
|
+
|
|
984
|
+
for (const rawLine of lines) {
|
|
985
|
+
const line = rawLine.trimEnd();
|
|
986
|
+
const trimmed = line.trim();
|
|
987
|
+
const headingMatch = trimmed.match(/^(#{1,3})\\s+(.+)$/);
|
|
988
|
+
|
|
989
|
+
if (headingMatch) {
|
|
990
|
+
if (inList) {
|
|
991
|
+
html += "</ul>";
|
|
992
|
+
inList = false;
|
|
993
|
+
}
|
|
994
|
+
const level = Math.min(3, headingMatch[1].length);
|
|
995
|
+
const tag = level === 1 ? "h2" : level === 2 ? "h3" : "p";
|
|
996
|
+
html += "<" + tag + ">" + renderInlineMarkdown(headingMatch[2]) + "</" + tag + ">";
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (/^\\s*-\\s+/.test(line)) {
|
|
1001
|
+
if (!inList) {
|
|
1002
|
+
html += "<ul>";
|
|
1003
|
+
inList = true;
|
|
1004
|
+
}
|
|
1005
|
+
html += "<li>" + renderInlineMarkdown(line.replace(/^\\s*-\\s+/, "")) + "</li>";
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
if (inList) {
|
|
1009
|
+
html += "</ul>";
|
|
1010
|
+
inList = false;
|
|
1011
|
+
}
|
|
1012
|
+
if (trimmed.length === 0) {
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
html += "<p>" + renderInlineMarkdown(line) + "</p>";
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (inList) {
|
|
1019
|
+
html += "</ul>";
|
|
1020
|
+
}
|
|
1021
|
+
return html;
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
const renderAssistantMarkdown = (value) => {
|
|
1025
|
+
const source = String(value || "");
|
|
1026
|
+
const fenceRegex = /\\x60\\x60\\x60([\\s\\S]*?)\\x60\\x60\\x60/g;
|
|
1027
|
+
let html = "";
|
|
1028
|
+
let lastIndex = 0;
|
|
1029
|
+
let match;
|
|
1030
|
+
|
|
1031
|
+
while ((match = fenceRegex.exec(source))) {
|
|
1032
|
+
const before = source.slice(lastIndex, match.index);
|
|
1033
|
+
html += renderMarkdownBlock(before);
|
|
1034
|
+
const codeText = String(match[1] || "").replace(/^\\n+|\\n+$/g, "");
|
|
1035
|
+
html += "<pre><code>" + escapeHtml(codeText) + "</code></pre>";
|
|
1036
|
+
lastIndex = match.index + match[0].length;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
html += renderMarkdownBlock(source.slice(lastIndex));
|
|
1040
|
+
return html || "<p></p>";
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
const extractToolActivity = (value) => {
|
|
1044
|
+
const source = String(value || "");
|
|
1045
|
+
let markerIndex = source.lastIndexOf("\\n### Tool activity\\n");
|
|
1046
|
+
if (markerIndex < 0 && source.startsWith("### Tool activity\\n")) {
|
|
1047
|
+
markerIndex = 0;
|
|
1048
|
+
}
|
|
1049
|
+
if (markerIndex < 0) {
|
|
1050
|
+
return { content: source, activities: [] };
|
|
1051
|
+
}
|
|
1052
|
+
const content = markerIndex === 0 ? "" : source.slice(0, markerIndex).trimEnd();
|
|
1053
|
+
const rawSection = markerIndex === 0 ? source : source.slice(markerIndex + 1);
|
|
1054
|
+
const afterHeading = rawSection.replace(/^### Tool activity\\s*\\n?/, "");
|
|
1055
|
+
const activities = afterHeading
|
|
1056
|
+
.split("\\n")
|
|
1057
|
+
.map((line) => line.trim())
|
|
1058
|
+
.filter((line) => line.startsWith("- "))
|
|
1059
|
+
.map((line) => line.slice(2).trim())
|
|
1060
|
+
.filter(Boolean);
|
|
1061
|
+
return { content, activities };
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
const renderToolActivity = (items) => {
|
|
1065
|
+
if (!items || !items.length) {
|
|
1066
|
+
return "";
|
|
1067
|
+
}
|
|
1068
|
+
const chips = items
|
|
1069
|
+
.map((item) => '<div class="tool-activity-item">' + escapeHtml(item) + "</div>")
|
|
1070
|
+
.join("");
|
|
1071
|
+
return (
|
|
1072
|
+
'<div class="tool-activity">' +
|
|
1073
|
+
'<details class="tool-activity-disclosure">' +
|
|
1074
|
+
'<summary class="tool-activity-summary">' +
|
|
1075
|
+
'<span class="tool-activity-label">Tool activity</span>' +
|
|
1076
|
+
'<span class="tool-activity-caret" aria-hidden="true"><svg viewBox="0 0 12 12" fill="none"><path d="M4.5 2.75L8 6L4.5 9.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg></span>' +
|
|
1077
|
+
"</summary>" +
|
|
1078
|
+
'<div class="tool-activity-list">' +
|
|
1079
|
+
chips +
|
|
1080
|
+
"</div>" +
|
|
1081
|
+
"</details>" +
|
|
1082
|
+
"</div>"
|
|
1083
|
+
);
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
const formatDate = (epoch) => {
|
|
1087
|
+
try {
|
|
1088
|
+
const date = new Date(epoch);
|
|
1089
|
+
const now = new Date();
|
|
1090
|
+
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
1091
|
+
const startOfDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
1092
|
+
const dayDiff = Math.floor((startOfToday - startOfDate) / 86400000);
|
|
1093
|
+
if (dayDiff === 0) {
|
|
1094
|
+
return "Today";
|
|
1095
|
+
}
|
|
1096
|
+
if (dayDiff === 1) {
|
|
1097
|
+
return "Yesterday";
|
|
1098
|
+
}
|
|
1099
|
+
if (dayDiff < 7 && dayDiff > 1) {
|
|
1100
|
+
return date.toLocaleDateString(undefined, { weekday: "short" });
|
|
1101
|
+
}
|
|
1102
|
+
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
1103
|
+
} catch {
|
|
1104
|
+
return "";
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
const isMobile = () => window.matchMedia("(max-width: 900px)").matches;
|
|
1109
|
+
|
|
1110
|
+
const setSidebarOpen = (open) => {
|
|
1111
|
+
if (!isMobile()) {
|
|
1112
|
+
elements.shell.classList.remove("sidebar-open");
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
elements.shell.classList.toggle("sidebar-open", open);
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
const renderConversationList = () => {
|
|
1119
|
+
elements.list.innerHTML = "";
|
|
1120
|
+
for (const c of state.conversations) {
|
|
1121
|
+
const item = document.createElement("div");
|
|
1122
|
+
item.className = "conversation-item" + (c.conversationId === state.activeConversationId ? " active" : "");
|
|
1123
|
+
item.textContent = c.title;
|
|
1124
|
+
|
|
1125
|
+
const isConfirming = state.confirmDeleteId === c.conversationId;
|
|
1126
|
+
const deleteBtn = document.createElement("button");
|
|
1127
|
+
deleteBtn.className = "delete-btn" + (isConfirming ? " confirming" : "");
|
|
1128
|
+
deleteBtn.textContent = isConfirming ? "sure?" : "\\u00d7";
|
|
1129
|
+
deleteBtn.onclick = async (e) => {
|
|
1130
|
+
e.stopPropagation();
|
|
1131
|
+
if (!isConfirming) {
|
|
1132
|
+
state.confirmDeleteId = c.conversationId;
|
|
1133
|
+
renderConversationList();
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
await api("/api/conversations/" + c.conversationId, { method: "DELETE" });
|
|
1137
|
+
if (state.activeConversationId === c.conversationId) {
|
|
1138
|
+
state.activeConversationId = null;
|
|
1139
|
+
state.activeMessages = [];
|
|
1140
|
+
pushConversationUrl(null);
|
|
1141
|
+
elements.chatTitle.textContent = "";
|
|
1142
|
+
renderMessages([]);
|
|
1143
|
+
}
|
|
1144
|
+
state.confirmDeleteId = null;
|
|
1145
|
+
await loadConversations();
|
|
1146
|
+
};
|
|
1147
|
+
item.appendChild(deleteBtn);
|
|
1148
|
+
|
|
1149
|
+
item.onclick = async () => {
|
|
1150
|
+
// Clear any delete confirmation, but still navigate
|
|
1151
|
+
if (state.confirmDeleteId) {
|
|
1152
|
+
state.confirmDeleteId = null;
|
|
1153
|
+
}
|
|
1154
|
+
state.activeConversationId = c.conversationId;
|
|
1155
|
+
pushConversationUrl(c.conversationId);
|
|
1156
|
+
renderConversationList();
|
|
1157
|
+
await loadConversation(c.conversationId);
|
|
1158
|
+
if (isMobile()) setSidebarOpen(false);
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
elements.list.appendChild(item);
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
const renderMessages = (messages, isStreaming = false) => {
|
|
1166
|
+
elements.messages.innerHTML = "";
|
|
1167
|
+
if (!messages || !messages.length) {
|
|
1168
|
+
elements.messages.innerHTML = '<div class="empty-state"><div class="assistant-avatar">' + agentInitial + '</div><div>How can I help you today?</div></div>';
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const col = document.createElement("div");
|
|
1172
|
+
col.className = "messages-column";
|
|
1173
|
+
messages.forEach((m, i) => {
|
|
1174
|
+
const row = document.createElement("div");
|
|
1175
|
+
row.className = "message-row " + m.role;
|
|
1176
|
+
if (m.role === "assistant") {
|
|
1177
|
+
const wrap = document.createElement("div");
|
|
1178
|
+
wrap.className = "assistant-wrap";
|
|
1179
|
+
wrap.innerHTML = '<div class="assistant-avatar">' + agentInitial + '</div>';
|
|
1180
|
+
const content = document.createElement("div");
|
|
1181
|
+
content.className = "assistant-content";
|
|
1182
|
+
const text = String(m.content || "");
|
|
1183
|
+
const parsed = extractToolActivity(text);
|
|
1184
|
+
const metadataToolActivity =
|
|
1185
|
+
m.metadata && Array.isArray(m.metadata.toolActivity)
|
|
1186
|
+
? m.metadata.toolActivity
|
|
1187
|
+
: [];
|
|
1188
|
+
const toolActivity =
|
|
1189
|
+
Array.isArray(m._toolActivity) && m._toolActivity.length > 0
|
|
1190
|
+
? m._toolActivity
|
|
1191
|
+
: metadataToolActivity.length > 0
|
|
1192
|
+
? metadataToolActivity
|
|
1193
|
+
: parsed.activities;
|
|
1194
|
+
if (m._error) {
|
|
1195
|
+
const errorEl = document.createElement("div");
|
|
1196
|
+
errorEl.className = "message-error";
|
|
1197
|
+
errorEl.innerHTML = "<strong>Error</strong><br>" + escapeHtml(m._error);
|
|
1198
|
+
content.appendChild(errorEl);
|
|
1199
|
+
} else if (isStreaming && i === messages.length - 1 && !parsed.content) {
|
|
1200
|
+
const spinner = document.createElement("span");
|
|
1201
|
+
spinner.className = "thinking-indicator";
|
|
1202
|
+
const starFrames = ["\u2736","\u2738","\u2739","\u273A","\u2739","\u2737"];
|
|
1203
|
+
let frame = 0;
|
|
1204
|
+
spinner.textContent = starFrames[0];
|
|
1205
|
+
spinner._interval = setInterval(() => { frame = (frame + 1) % starFrames.length; spinner.textContent = starFrames[frame]; }, 70);
|
|
1206
|
+
content.appendChild(spinner);
|
|
1207
|
+
} else {
|
|
1208
|
+
content.innerHTML = renderAssistantMarkdown(parsed.content);
|
|
1209
|
+
}
|
|
1210
|
+
if (toolActivity.length > 0) {
|
|
1211
|
+
content.insertAdjacentHTML("beforeend", renderToolActivity(toolActivity));
|
|
1212
|
+
}
|
|
1213
|
+
wrap.appendChild(content);
|
|
1214
|
+
row.appendChild(wrap);
|
|
1215
|
+
} else {
|
|
1216
|
+
row.innerHTML = '<div class="user-bubble">' + escapeHtml(m.content) + '</div>';
|
|
1217
|
+
}
|
|
1218
|
+
col.appendChild(row);
|
|
1219
|
+
});
|
|
1220
|
+
elements.messages.appendChild(col);
|
|
1221
|
+
elements.messages.scrollTop = elements.messages.scrollHeight;
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
const loadConversations = async () => {
|
|
1225
|
+
const payload = await api("/api/conversations");
|
|
1226
|
+
state.conversations = payload.conversations || [];
|
|
1227
|
+
renderConversationList();
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
const loadConversation = async (conversationId) => {
|
|
1231
|
+
const payload = await api("/api/conversations/" + encodeURIComponent(conversationId));
|
|
1232
|
+
elements.chatTitle.textContent = payload.conversation.title;
|
|
1233
|
+
state.activeMessages = payload.conversation.messages || [];
|
|
1234
|
+
renderMessages(state.activeMessages);
|
|
1235
|
+
elements.prompt.focus();
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
const createConversation = async (title, options = {}) => {
|
|
1239
|
+
const shouldLoadConversation = options.loadConversation !== false;
|
|
1240
|
+
const payload = await api("/api/conversations", {
|
|
1241
|
+
method: "POST",
|
|
1242
|
+
body: JSON.stringify(title ? { title } : {})
|
|
1243
|
+
});
|
|
1244
|
+
state.activeConversationId = payload.conversation.conversationId;
|
|
1245
|
+
state.confirmDeleteId = null;
|
|
1246
|
+
pushConversationUrl(state.activeConversationId);
|
|
1247
|
+
await loadConversations();
|
|
1248
|
+
if (shouldLoadConversation) {
|
|
1249
|
+
await loadConversation(state.activeConversationId);
|
|
1250
|
+
} else {
|
|
1251
|
+
elements.chatTitle.textContent = payload.conversation.title || "New conversation";
|
|
1252
|
+
}
|
|
1253
|
+
return state.activeConversationId;
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
const parseSseChunk = (buffer, onEvent) => {
|
|
1257
|
+
let rest = buffer;
|
|
1258
|
+
while (true) {
|
|
1259
|
+
const index = rest.indexOf("\\n\\n");
|
|
1260
|
+
if (index < 0) {
|
|
1261
|
+
return rest;
|
|
1262
|
+
}
|
|
1263
|
+
const raw = rest.slice(0, index);
|
|
1264
|
+
rest = rest.slice(index + 2);
|
|
1265
|
+
const lines = raw.split("\\n");
|
|
1266
|
+
let eventName = "message";
|
|
1267
|
+
let data = "";
|
|
1268
|
+
for (const line of lines) {
|
|
1269
|
+
if (line.startsWith("event:")) {
|
|
1270
|
+
eventName = line.slice(6).trim();
|
|
1271
|
+
} else if (line.startsWith("data:")) {
|
|
1272
|
+
data += line.slice(5).trim();
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if (data) {
|
|
1276
|
+
try {
|
|
1277
|
+
onEvent(eventName, JSON.parse(data));
|
|
1278
|
+
} catch {}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
const setStreaming = (value) => {
|
|
1284
|
+
state.isStreaming = value;
|
|
1285
|
+
elements.send.disabled = value;
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
const pushToolActivity = (assistantMessage, line) => {
|
|
1289
|
+
if (!line) {
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if (
|
|
1293
|
+
!assistantMessage.metadata ||
|
|
1294
|
+
!Array.isArray(assistantMessage.metadata.toolActivity)
|
|
1295
|
+
) {
|
|
1296
|
+
assistantMessage.metadata = {
|
|
1297
|
+
...(assistantMessage.metadata || {}),
|
|
1298
|
+
toolActivity: [],
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
assistantMessage.metadata.toolActivity.push(line);
|
|
1302
|
+
};
|
|
1303
|
+
|
|
1304
|
+
const autoResizePrompt = () => {
|
|
1305
|
+
const el = elements.prompt;
|
|
1306
|
+
el.style.height = "auto";
|
|
1307
|
+
const scrollHeight = el.scrollHeight;
|
|
1308
|
+
const nextHeight = Math.min(scrollHeight, 200);
|
|
1309
|
+
el.style.height = nextHeight + "px";
|
|
1310
|
+
el.style.overflowY = scrollHeight > 200 ? "auto" : "hidden";
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
const sendMessage = async (text) => {
|
|
1314
|
+
const messageText = (text || "").trim();
|
|
1315
|
+
if (!messageText || state.isStreaming) {
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const localMessages = [...(state.activeMessages || []), { role: "user", content: messageText }];
|
|
1319
|
+
let assistantMessage = { role: "assistant", content: "", metadata: { toolActivity: [] } };
|
|
1320
|
+
localMessages.push(assistantMessage);
|
|
1321
|
+
state.activeMessages = localMessages;
|
|
1322
|
+
renderMessages(localMessages, true);
|
|
1323
|
+
setStreaming(true);
|
|
1324
|
+
let conversationId = state.activeConversationId;
|
|
1325
|
+
try {
|
|
1326
|
+
if (!conversationId) {
|
|
1327
|
+
conversationId = await createConversation(messageText, { loadConversation: false });
|
|
1328
|
+
}
|
|
1329
|
+
const response = await fetch("/api/conversations/" + encodeURIComponent(conversationId) + "/messages", {
|
|
1330
|
+
method: "POST",
|
|
1331
|
+
credentials: "include",
|
|
1332
|
+
headers: { "Content-Type": "application/json", "x-csrf-token": state.csrfToken },
|
|
1333
|
+
body: JSON.stringify({ message: messageText })
|
|
1334
|
+
});
|
|
1335
|
+
if (!response.ok || !response.body) {
|
|
1336
|
+
throw new Error("Failed to stream response");
|
|
1337
|
+
}
|
|
1338
|
+
const reader = response.body.getReader();
|
|
1339
|
+
const decoder = new TextDecoder();
|
|
1340
|
+
let buffer = "";
|
|
1341
|
+
while (true) {
|
|
1342
|
+
const { value, done } = await reader.read();
|
|
1343
|
+
if (done) {
|
|
1344
|
+
break;
|
|
1345
|
+
}
|
|
1346
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1347
|
+
buffer = parseSseChunk(buffer, (eventName, payload) => {
|
|
1348
|
+
if (eventName === "model:chunk") {
|
|
1349
|
+
assistantMessage.content += String(payload.content || "");
|
|
1350
|
+
renderMessages(localMessages, true);
|
|
1351
|
+
}
|
|
1352
|
+
if (eventName === "tool:started") {
|
|
1353
|
+
pushToolActivity(assistantMessage, "start " + (payload.tool || "tool"));
|
|
1354
|
+
renderMessages(localMessages, true);
|
|
1355
|
+
}
|
|
1356
|
+
if (eventName === "tool:completed") {
|
|
1357
|
+
const duration = typeof payload.duration === "number" ? payload.duration : null;
|
|
1358
|
+
pushToolActivity(
|
|
1359
|
+
assistantMessage,
|
|
1360
|
+
"done " +
|
|
1361
|
+
(payload.tool || "tool") +
|
|
1362
|
+
(duration !== null ? " (" + duration + "ms)" : ""),
|
|
1363
|
+
);
|
|
1364
|
+
renderMessages(localMessages, true);
|
|
1365
|
+
}
|
|
1366
|
+
if (eventName === "tool:error") {
|
|
1367
|
+
pushToolActivity(
|
|
1368
|
+
assistantMessage,
|
|
1369
|
+
"error " + (payload.tool || "tool") + ": " + (payload.error || "unknown error"),
|
|
1370
|
+
);
|
|
1371
|
+
renderMessages(localMessages, true);
|
|
1372
|
+
}
|
|
1373
|
+
if (eventName === "tool:approval:required") {
|
|
1374
|
+
pushToolActivity(assistantMessage, "approval required for " + (payload.tool || "tool"));
|
|
1375
|
+
renderMessages(localMessages, true);
|
|
1376
|
+
}
|
|
1377
|
+
if (eventName === "tool:approval:granted") {
|
|
1378
|
+
pushToolActivity(assistantMessage, "approval granted");
|
|
1379
|
+
renderMessages(localMessages, true);
|
|
1380
|
+
}
|
|
1381
|
+
if (eventName === "tool:approval:denied") {
|
|
1382
|
+
pushToolActivity(assistantMessage, "approval denied");
|
|
1383
|
+
renderMessages(localMessages, true);
|
|
1384
|
+
}
|
|
1385
|
+
if (eventName === "run:completed" && (!assistantMessage.content || assistantMessage.content.length === 0)) {
|
|
1386
|
+
assistantMessage.content = String(payload.result?.response || "");
|
|
1387
|
+
renderMessages(localMessages, false);
|
|
1388
|
+
}
|
|
1389
|
+
if (eventName === "run:error") {
|
|
1390
|
+
const errMsg = payload.error?.message || "Something went wrong";
|
|
1391
|
+
assistantMessage.content = "";
|
|
1392
|
+
assistantMessage._error = errMsg;
|
|
1393
|
+
renderMessages(localMessages, false);
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
await loadConversations();
|
|
1398
|
+
await loadConversation(conversationId);
|
|
1399
|
+
} finally {
|
|
1400
|
+
setStreaming(false);
|
|
1401
|
+
elements.prompt.focus();
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
|
|
1405
|
+
const requireAuth = async () => {
|
|
1406
|
+
try {
|
|
1407
|
+
const session = await api("/api/auth/session");
|
|
1408
|
+
if (!session.authenticated) {
|
|
1409
|
+
elements.auth.classList.remove("hidden");
|
|
1410
|
+
elements.app.classList.add("hidden");
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
state.csrfToken = session.csrfToken || "";
|
|
1414
|
+
elements.auth.classList.add("hidden");
|
|
1415
|
+
elements.app.classList.remove("hidden");
|
|
1416
|
+
return true;
|
|
1417
|
+
} catch {
|
|
1418
|
+
elements.auth.classList.remove("hidden");
|
|
1419
|
+
elements.app.classList.add("hidden");
|
|
1420
|
+
return false;
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
|
|
1424
|
+
elements.loginForm.addEventListener("submit", async (event) => {
|
|
1425
|
+
event.preventDefault();
|
|
1426
|
+
elements.loginError.textContent = "";
|
|
1427
|
+
try {
|
|
1428
|
+
const result = await api("/api/auth/login", {
|
|
1429
|
+
method: "POST",
|
|
1430
|
+
body: JSON.stringify({ passphrase: elements.passphrase.value || "" })
|
|
1431
|
+
});
|
|
1432
|
+
state.csrfToken = result.csrfToken || "";
|
|
1433
|
+
elements.passphrase.value = "";
|
|
1434
|
+
elements.auth.classList.add("hidden");
|
|
1435
|
+
elements.app.classList.remove("hidden");
|
|
1436
|
+
await loadConversations();
|
|
1437
|
+
const urlConversationId = getConversationIdFromUrl();
|
|
1438
|
+
if (urlConversationId) {
|
|
1439
|
+
state.activeConversationId = urlConversationId;
|
|
1440
|
+
renderConversationList();
|
|
1441
|
+
try {
|
|
1442
|
+
await loadConversation(urlConversationId);
|
|
1443
|
+
} catch {
|
|
1444
|
+
state.activeConversationId = null;
|
|
1445
|
+
state.activeMessages = [];
|
|
1446
|
+
replaceConversationUrl(null);
|
|
1447
|
+
renderMessages([]);
|
|
1448
|
+
renderConversationList();
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
elements.loginError.textContent = error.message || "Login failed";
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
const startNewChat = () => {
|
|
1457
|
+
state.activeConversationId = null;
|
|
1458
|
+
state.activeMessages = [];
|
|
1459
|
+
state.confirmDeleteId = null;
|
|
1460
|
+
pushConversationUrl(null);
|
|
1461
|
+
elements.chatTitle.textContent = "";
|
|
1462
|
+
renderMessages([]);
|
|
1463
|
+
renderConversationList();
|
|
1464
|
+
elements.prompt.focus();
|
|
1465
|
+
if (isMobile()) {
|
|
1466
|
+
setSidebarOpen(false);
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
|
|
1470
|
+
elements.newChat.addEventListener("click", startNewChat);
|
|
1471
|
+
elements.topbarNewChat.addEventListener("click", startNewChat);
|
|
1472
|
+
|
|
1473
|
+
elements.prompt.addEventListener("input", () => {
|
|
1474
|
+
autoResizePrompt();
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
elements.prompt.addEventListener("keydown", (event) => {
|
|
1478
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
1479
|
+
event.preventDefault();
|
|
1480
|
+
elements.composer.requestSubmit();
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
elements.sidebarToggle.addEventListener("click", () => {
|
|
1485
|
+
if (isMobile()) setSidebarOpen(!elements.shell.classList.contains("sidebar-open"));
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
elements.sidebarBackdrop.addEventListener("click", () => setSidebarOpen(false));
|
|
1489
|
+
|
|
1490
|
+
elements.logout.addEventListener("click", async () => {
|
|
1491
|
+
await api("/api/auth/logout", { method: "POST" });
|
|
1492
|
+
state.activeConversationId = null;
|
|
1493
|
+
state.activeMessages = [];
|
|
1494
|
+
state.confirmDeleteId = null;
|
|
1495
|
+
state.conversations = [];
|
|
1496
|
+
state.csrfToken = "";
|
|
1497
|
+
await requireAuth();
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
elements.composer.addEventListener("submit", async (event) => {
|
|
1501
|
+
event.preventDefault();
|
|
1502
|
+
const value = elements.prompt.value;
|
|
1503
|
+
elements.prompt.value = "";
|
|
1504
|
+
autoResizePrompt();
|
|
1505
|
+
await sendMessage(value);
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
document.addEventListener("click", (event) => {
|
|
1509
|
+
if (!(event.target instanceof Node)) {
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
if (!event.target.closest(".conversation-item") && state.confirmDeleteId) {
|
|
1513
|
+
state.confirmDeleteId = null;
|
|
1514
|
+
renderConversationList();
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
window.addEventListener("resize", () => {
|
|
1519
|
+
setSidebarOpen(false);
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
const navigateToConversation = async (conversationId) => {
|
|
1523
|
+
if (conversationId) {
|
|
1524
|
+
state.activeConversationId = conversationId;
|
|
1525
|
+
renderConversationList();
|
|
1526
|
+
try {
|
|
1527
|
+
await loadConversation(conversationId);
|
|
1528
|
+
} catch {
|
|
1529
|
+
// Conversation not found \u2013 fall back to empty state
|
|
1530
|
+
state.activeConversationId = null;
|
|
1531
|
+
state.activeMessages = [];
|
|
1532
|
+
replaceConversationUrl(null);
|
|
1533
|
+
elements.chatTitle.textContent = "";
|
|
1534
|
+
renderMessages([]);
|
|
1535
|
+
renderConversationList();
|
|
1536
|
+
}
|
|
1537
|
+
} else {
|
|
1538
|
+
state.activeConversationId = null;
|
|
1539
|
+
state.activeMessages = [];
|
|
1540
|
+
elements.chatTitle.textContent = "";
|
|
1541
|
+
renderMessages([]);
|
|
1542
|
+
renderConversationList();
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
window.addEventListener("popstate", async () => {
|
|
1547
|
+
if (state.isStreaming) return;
|
|
1548
|
+
const conversationId = getConversationIdFromUrl();
|
|
1549
|
+
await navigateToConversation(conversationId);
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
(async () => {
|
|
1553
|
+
const authenticated = await requireAuth();
|
|
1554
|
+
if (!authenticated) {
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
await loadConversations();
|
|
1558
|
+
const urlConversationId = getConversationIdFromUrl();
|
|
1559
|
+
if (urlConversationId) {
|
|
1560
|
+
state.activeConversationId = urlConversationId;
|
|
1561
|
+
replaceConversationUrl(urlConversationId);
|
|
1562
|
+
renderConversationList();
|
|
1563
|
+
try {
|
|
1564
|
+
await loadConversation(urlConversationId);
|
|
1565
|
+
} catch {
|
|
1566
|
+
// URL pointed to a conversation that no longer exists
|
|
1567
|
+
state.activeConversationId = null;
|
|
1568
|
+
state.activeMessages = [];
|
|
1569
|
+
replaceConversationUrl(null);
|
|
1570
|
+
elements.chatTitle.textContent = "";
|
|
1571
|
+
renderMessages([]);
|
|
1572
|
+
renderConversationList();
|
|
1573
|
+
if (state.conversations.length === 0) {
|
|
1574
|
+
await createConversation();
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
} else if (state.conversations.length === 0) {
|
|
1578
|
+
await createConversation();
|
|
1579
|
+
}
|
|
1580
|
+
autoResizePrompt();
|
|
1581
|
+
elements.prompt.focus();
|
|
1582
|
+
})();
|
|
1583
|
+
|
|
1584
|
+
if ("serviceWorker" in navigator) {
|
|
1585
|
+
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// Detect iOS standalone mode and add class for CSS targeting
|
|
1589
|
+
if (window.navigator.standalone === true || window.matchMedia("(display-mode: standalone)").matches) {
|
|
1590
|
+
document.documentElement.classList.add("standalone");
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// iOS viewport and keyboard handling
|
|
1594
|
+
(function() {
|
|
1595
|
+
var shell = document.querySelector(".shell");
|
|
1596
|
+
var pinScroll = function() { if (window.scrollY !== 0) window.scrollTo(0, 0); };
|
|
1597
|
+
|
|
1598
|
+
// Track the "full" height when keyboard is not open
|
|
1599
|
+
var fullHeight = window.innerHeight;
|
|
1600
|
+
|
|
1601
|
+
// Resize shell when iOS keyboard opens/closes
|
|
1602
|
+
var resizeForKeyboard = function() {
|
|
1603
|
+
if (!shell || !window.visualViewport) return;
|
|
1604
|
+
var vvHeight = window.visualViewport.height;
|
|
1605
|
+
|
|
1606
|
+
// Update fullHeight if viewport grew (keyboard closed)
|
|
1607
|
+
if (vvHeight > fullHeight) {
|
|
1608
|
+
fullHeight = vvHeight;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// Only apply height override if keyboard appears to be open
|
|
1612
|
+
// (viewport significantly smaller than full height)
|
|
1613
|
+
if (vvHeight < fullHeight - 100) {
|
|
1614
|
+
shell.style.height = vvHeight + "px";
|
|
1615
|
+
} else {
|
|
1616
|
+
// Keyboard closed - remove override, let CSS handle it
|
|
1617
|
+
shell.style.height = "";
|
|
1618
|
+
}
|
|
1619
|
+
pinScroll();
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
if (window.visualViewport) {
|
|
1623
|
+
window.visualViewport.addEventListener("scroll", pinScroll);
|
|
1624
|
+
window.visualViewport.addEventListener("resize", resizeForKeyboard);
|
|
1625
|
+
}
|
|
1626
|
+
document.addEventListener("scroll", pinScroll);
|
|
1627
|
+
|
|
1628
|
+
// Draggable sidebar from left edge (mobile only)
|
|
1629
|
+
(function() {
|
|
1630
|
+
var sidebar = document.querySelector(".sidebar");
|
|
1631
|
+
var backdrop = document.querySelector(".sidebar-backdrop");
|
|
1632
|
+
var shell = document.querySelector(".shell");
|
|
1633
|
+
if (!sidebar || !backdrop || !shell) return;
|
|
1634
|
+
|
|
1635
|
+
var sidebarWidth = 260;
|
|
1636
|
+
var edgeThreshold = 50; // px from left edge to start drag
|
|
1637
|
+
var velocityThreshold = 0.3; // px/ms to trigger open/close
|
|
1638
|
+
|
|
1639
|
+
var dragging = false;
|
|
1640
|
+
var startX = 0;
|
|
1641
|
+
var startY = 0;
|
|
1642
|
+
var currentX = 0;
|
|
1643
|
+
var startTime = 0;
|
|
1644
|
+
var isOpen = false;
|
|
1645
|
+
var directionLocked = false;
|
|
1646
|
+
var isHorizontal = false;
|
|
1647
|
+
|
|
1648
|
+
function getProgress() {
|
|
1649
|
+
// Returns 0 (closed) to 1 (open)
|
|
1650
|
+
if (isOpen) {
|
|
1651
|
+
return Math.max(0, Math.min(1, 1 + currentX / sidebarWidth));
|
|
1652
|
+
} else {
|
|
1653
|
+
return Math.max(0, Math.min(1, currentX / sidebarWidth));
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function updatePosition(progress) {
|
|
1658
|
+
var offset = (progress - 1) * sidebarWidth;
|
|
1659
|
+
sidebar.style.transform = "translateX(" + offset + "px)";
|
|
1660
|
+
backdrop.style.opacity = progress;
|
|
1661
|
+
if (progress > 0) {
|
|
1662
|
+
backdrop.style.pointerEvents = "auto";
|
|
1663
|
+
} else {
|
|
1664
|
+
backdrop.style.pointerEvents = "none";
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function onTouchStart(e) {
|
|
1669
|
+
if (window.innerWidth > 768) return;
|
|
1670
|
+
|
|
1671
|
+
// Don't intercept touches on interactive elements
|
|
1672
|
+
var target = e.target;
|
|
1673
|
+
if (target.closest("button") || target.closest("a") || target.closest("input") || target.closest("textarea")) {
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
var touch = e.touches[0];
|
|
1678
|
+
isOpen = shell.classList.contains("sidebar-open");
|
|
1679
|
+
|
|
1680
|
+
// When sidebar is closed: only respond to edge swipes
|
|
1681
|
+
// When sidebar is open: only respond to backdrop touches (not sidebar content)
|
|
1682
|
+
var fromEdge = touch.clientX < edgeThreshold;
|
|
1683
|
+
var onBackdrop = e.target === backdrop;
|
|
1684
|
+
|
|
1685
|
+
if (!isOpen && !fromEdge) return;
|
|
1686
|
+
if (isOpen && !onBackdrop) return;
|
|
1687
|
+
|
|
1688
|
+
// Prevent Safari back gesture when starting from edge
|
|
1689
|
+
if (fromEdge) {
|
|
1690
|
+
e.preventDefault();
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
startX = touch.clientX;
|
|
1694
|
+
startY = touch.clientY;
|
|
1695
|
+
currentX = 0;
|
|
1696
|
+
startTime = Date.now();
|
|
1697
|
+
directionLocked = false;
|
|
1698
|
+
isHorizontal = false;
|
|
1699
|
+
dragging = true;
|
|
1700
|
+
sidebar.classList.add("dragging");
|
|
1701
|
+
backdrop.classList.add("dragging");
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
function onTouchMove(e) {
|
|
1705
|
+
if (!dragging) return;
|
|
1706
|
+
var touch = e.touches[0];
|
|
1707
|
+
var dx = touch.clientX - startX;
|
|
1708
|
+
var dy = touch.clientY - startY;
|
|
1709
|
+
|
|
1710
|
+
// Lock direction after some movement
|
|
1711
|
+
if (!directionLocked && (Math.abs(dx) > 10 || Math.abs(dy) > 10)) {
|
|
1712
|
+
directionLocked = true;
|
|
1713
|
+
isHorizontal = Math.abs(dx) > Math.abs(dy);
|
|
1714
|
+
if (!isHorizontal) {
|
|
1715
|
+
// Vertical scroll, cancel drag
|
|
1716
|
+
dragging = false;
|
|
1717
|
+
sidebar.classList.remove("dragging");
|
|
1718
|
+
backdrop.classList.remove("dragging");
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
if (!directionLocked) return;
|
|
1724
|
+
|
|
1725
|
+
// Prevent scrolling while dragging sidebar
|
|
1726
|
+
e.preventDefault();
|
|
1727
|
+
|
|
1728
|
+
currentX = dx;
|
|
1729
|
+
updatePosition(getProgress());
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function onTouchEnd(e) {
|
|
1733
|
+
if (!dragging) return;
|
|
1734
|
+
dragging = false;
|
|
1735
|
+
sidebar.classList.remove("dragging");
|
|
1736
|
+
backdrop.classList.remove("dragging");
|
|
1737
|
+
|
|
1738
|
+
var touch = e.changedTouches[0];
|
|
1739
|
+
var dx = touch.clientX - startX;
|
|
1740
|
+
var dt = Date.now() - startTime;
|
|
1741
|
+
var velocity = dx / dt; // px/ms
|
|
1742
|
+
|
|
1743
|
+
var progress = getProgress();
|
|
1744
|
+
var shouldOpen;
|
|
1745
|
+
|
|
1746
|
+
// Use velocity if fast enough, otherwise use position threshold
|
|
1747
|
+
if (Math.abs(velocity) > velocityThreshold) {
|
|
1748
|
+
shouldOpen = velocity > 0;
|
|
1749
|
+
} else {
|
|
1750
|
+
shouldOpen = progress > 0.5;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// Reset inline styles and let CSS handle the animation
|
|
1754
|
+
sidebar.style.transform = "";
|
|
1755
|
+
backdrop.style.opacity = "";
|
|
1756
|
+
backdrop.style.pointerEvents = "";
|
|
1757
|
+
|
|
1758
|
+
if (shouldOpen) {
|
|
1759
|
+
shell.classList.add("sidebar-open");
|
|
1760
|
+
} else {
|
|
1761
|
+
shell.classList.remove("sidebar-open");
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
document.addEventListener("touchstart", onTouchStart, { passive: false });
|
|
1766
|
+
document.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
1767
|
+
document.addEventListener("touchend", onTouchEnd, { passive: true });
|
|
1768
|
+
document.addEventListener("touchcancel", onTouchEnd, { passive: true });
|
|
1769
|
+
})();
|
|
1770
|
+
|
|
1771
|
+
// Prevent Safari back/forward navigation by manipulating history
|
|
1772
|
+
// This doesn't stop the gesture animation but prevents actual navigation
|
|
1773
|
+
if (window.navigator.standalone || window.matchMedia("(display-mode: standalone)").matches) {
|
|
1774
|
+
history.pushState(null, "", location.href);
|
|
1775
|
+
window.addEventListener("popstate", function() {
|
|
1776
|
+
history.pushState(null, "", location.href);
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// Right edge blocker - intercept touch events to prevent forward navigation
|
|
1781
|
+
var rightBlocker = document.querySelector(".edge-blocker-right");
|
|
1782
|
+
if (rightBlocker) {
|
|
1783
|
+
rightBlocker.addEventListener("touchstart", function(e) {
|
|
1784
|
+
e.preventDefault();
|
|
1785
|
+
}, { passive: false });
|
|
1786
|
+
rightBlocker.addEventListener("touchmove", function(e) {
|
|
1787
|
+
e.preventDefault();
|
|
1788
|
+
}, { passive: false });
|
|
1789
|
+
}
|
|
1790
|
+
})();
|
|
1791
|
+
|
|
1792
|
+
</script>
|
|
1793
|
+
</body>
|
|
1794
|
+
</html>`;
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
// src/index.ts
|
|
1798
|
+
import { createInterface } from "readline/promises";
|
|
1799
|
+
|
|
1800
|
+
// src/init-onboarding.ts
|
|
1801
|
+
import { stdin, stdout } from "process";
|
|
1802
|
+
import { input, password, select } from "@inquirer/prompts";
|
|
1803
|
+
import {
|
|
1804
|
+
fieldsForScope
|
|
1805
|
+
} from "@poncho-ai/sdk";
|
|
1806
|
+
var C = {
|
|
1807
|
+
reset: "\x1B[0m",
|
|
1808
|
+
bold: "\x1B[1m",
|
|
1809
|
+
dim: "\x1B[2m",
|
|
1810
|
+
cyan: "\x1B[36m"
|
|
1811
|
+
};
|
|
1812
|
+
var dim = (s) => `${C.dim}${s}${C.reset}`;
|
|
1813
|
+
var bold = (s) => `${C.bold}${s}${C.reset}`;
|
|
1814
|
+
var INPUT_CARET = "\xBB";
|
|
1815
|
+
var shouldAskField = (field, answers) => {
|
|
1816
|
+
if (!field.dependsOn) {
|
|
1817
|
+
return true;
|
|
1818
|
+
}
|
|
1819
|
+
const value = answers[field.dependsOn.fieldId];
|
|
1820
|
+
if (typeof field.dependsOn.equals !== "undefined") {
|
|
1821
|
+
return value === field.dependsOn.equals;
|
|
1822
|
+
}
|
|
1823
|
+
if (field.dependsOn.oneOf) {
|
|
1824
|
+
return field.dependsOn.oneOf.includes(value);
|
|
1825
|
+
}
|
|
1826
|
+
return true;
|
|
1827
|
+
};
|
|
1828
|
+
var parsePromptValue = (field, answer) => {
|
|
1829
|
+
if (field.kind === "boolean") {
|
|
1830
|
+
const normalized = answer.trim().toLowerCase();
|
|
1831
|
+
if (normalized === "y" || normalized === "yes" || normalized === "true") {
|
|
1832
|
+
return true;
|
|
1833
|
+
}
|
|
1834
|
+
if (normalized === "n" || normalized === "no" || normalized === "false") {
|
|
1835
|
+
return false;
|
|
1836
|
+
}
|
|
1837
|
+
return Boolean(field.defaultValue);
|
|
1838
|
+
}
|
|
1839
|
+
if (field.kind === "number") {
|
|
1840
|
+
const parsed = Number.parseInt(answer.trim(), 10);
|
|
1841
|
+
if (Number.isFinite(parsed)) {
|
|
1842
|
+
return parsed;
|
|
1843
|
+
}
|
|
1844
|
+
return Number(field.defaultValue);
|
|
1845
|
+
}
|
|
1846
|
+
if (field.kind === "select") {
|
|
1847
|
+
const trimmed2 = answer.trim();
|
|
1848
|
+
if (field.options && field.options.some((option) => option.value === trimmed2)) {
|
|
1849
|
+
return trimmed2;
|
|
1850
|
+
}
|
|
1851
|
+
const asNumber = Number.parseInt(trimmed2, 10);
|
|
1852
|
+
if (Number.isFinite(asNumber) && field.options && asNumber >= 1 && asNumber <= field.options.length) {
|
|
1853
|
+
return field.options[asNumber - 1]?.value ?? String(field.defaultValue);
|
|
1854
|
+
}
|
|
1855
|
+
return String(field.defaultValue);
|
|
1856
|
+
}
|
|
1857
|
+
const trimmed = answer.trim();
|
|
1858
|
+
if (trimmed.length === 0) {
|
|
1859
|
+
return String(field.defaultValue);
|
|
1860
|
+
}
|
|
1861
|
+
return trimmed;
|
|
1862
|
+
};
|
|
1863
|
+
var askSecret = async (field) => {
|
|
1864
|
+
if (!stdin.isTTY) {
|
|
1865
|
+
return void 0;
|
|
1866
|
+
}
|
|
1867
|
+
const hint = field.placeholder ? dim(` (${field.placeholder})`) : "";
|
|
1868
|
+
const message = `${field.prompt}${hint}`;
|
|
1869
|
+
const value = await password(
|
|
1870
|
+
{
|
|
1871
|
+
message,
|
|
1872
|
+
// true invisible input while typing/pasting
|
|
1873
|
+
mask: false,
|
|
1874
|
+
theme: {
|
|
1875
|
+
prefix: {
|
|
1876
|
+
idle: dim(INPUT_CARET),
|
|
1877
|
+
done: dim("\u2713")
|
|
1878
|
+
},
|
|
1879
|
+
style: {
|
|
1880
|
+
help: () => ""
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
},
|
|
1884
|
+
{ input: stdin, output: stdout }
|
|
1885
|
+
);
|
|
1886
|
+
return value ?? "";
|
|
1887
|
+
};
|
|
1888
|
+
var askSelectWithArrowKeys = async (field) => {
|
|
1889
|
+
if (!field.options || field.options.length === 0) {
|
|
1890
|
+
return void 0;
|
|
1891
|
+
}
|
|
1892
|
+
if (!stdin.isTTY) {
|
|
1893
|
+
return void 0;
|
|
1894
|
+
}
|
|
1895
|
+
const selected = await select(
|
|
1896
|
+
{
|
|
1897
|
+
message: field.prompt,
|
|
1898
|
+
choices: field.options.map((option) => ({
|
|
1899
|
+
name: option.label,
|
|
1900
|
+
value: option.value
|
|
1901
|
+
})),
|
|
1902
|
+
default: String(field.defaultValue),
|
|
1903
|
+
theme: {
|
|
1904
|
+
prefix: {
|
|
1905
|
+
idle: dim(INPUT_CARET),
|
|
1906
|
+
done: dim("\u2713")
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
},
|
|
1910
|
+
{ input: stdin, output: stdout }
|
|
1911
|
+
);
|
|
1912
|
+
return selected;
|
|
1913
|
+
};
|
|
1914
|
+
var askBooleanWithArrowKeys = async (field) => {
|
|
1915
|
+
if (!stdin.isTTY) {
|
|
1916
|
+
return void 0;
|
|
1917
|
+
}
|
|
1918
|
+
const selected = await select(
|
|
1919
|
+
{
|
|
1920
|
+
message: field.prompt,
|
|
1921
|
+
choices: [
|
|
1922
|
+
{ name: "Yes", value: "true" },
|
|
1923
|
+
{ name: "No", value: "false" }
|
|
1924
|
+
],
|
|
1925
|
+
default: field.defaultValue ? "true" : "false",
|
|
1926
|
+
theme: {
|
|
1927
|
+
prefix: {
|
|
1928
|
+
idle: dim(INPUT_CARET),
|
|
1929
|
+
done: dim("\u2713")
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
},
|
|
1933
|
+
{ input: stdin, output: stdout }
|
|
1934
|
+
);
|
|
1935
|
+
return selected;
|
|
1936
|
+
};
|
|
1937
|
+
var askTextInput = async (field) => {
|
|
1938
|
+
if (!stdin.isTTY) {
|
|
1939
|
+
return void 0;
|
|
1940
|
+
}
|
|
1941
|
+
const answer = await input(
|
|
1942
|
+
{
|
|
1943
|
+
message: field.prompt,
|
|
1944
|
+
default: String(field.defaultValue ?? ""),
|
|
1945
|
+
theme: {
|
|
1946
|
+
prefix: {
|
|
1947
|
+
idle: dim(INPUT_CARET),
|
|
1948
|
+
done: dim("\u2713")
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
},
|
|
1952
|
+
{ input: stdin, output: stdout }
|
|
1953
|
+
);
|
|
1954
|
+
return answer;
|
|
1955
|
+
};
|
|
1956
|
+
var buildDefaultAnswers = () => {
|
|
1957
|
+
const answers = {};
|
|
1958
|
+
for (const field of fieldsForScope("light")) {
|
|
1959
|
+
answers[field.id] = field.defaultValue;
|
|
1960
|
+
}
|
|
1961
|
+
return answers;
|
|
1962
|
+
};
|
|
1963
|
+
var askOnboardingQuestions = async (options) => {
|
|
1964
|
+
const answers = buildDefaultAnswers();
|
|
1965
|
+
const interactive = options.yes === true ? false : options.interactive ?? (stdin.isTTY === true && stdout.isTTY === true);
|
|
1966
|
+
if (!interactive) {
|
|
1967
|
+
return answers;
|
|
1968
|
+
}
|
|
1969
|
+
stdout.write("\n");
|
|
1970
|
+
stdout.write(` ${bold("Poncho")} ${dim("\xB7 quick setup")}
|
|
1971
|
+
`);
|
|
1972
|
+
stdout.write("\n");
|
|
1973
|
+
const fields = fieldsForScope("light");
|
|
1974
|
+
for (const field of fields) {
|
|
1975
|
+
if (!shouldAskField(field, answers)) {
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
stdout.write("\n");
|
|
1979
|
+
let value;
|
|
1980
|
+
if (field.secret) {
|
|
1981
|
+
value = await askSecret(field);
|
|
1982
|
+
} else if (field.kind === "select") {
|
|
1983
|
+
value = await askSelectWithArrowKeys(field);
|
|
1984
|
+
} else if (field.kind === "boolean") {
|
|
1985
|
+
value = await askBooleanWithArrowKeys(field);
|
|
1986
|
+
} else {
|
|
1987
|
+
value = await askTextInput(field);
|
|
1988
|
+
}
|
|
1989
|
+
if (!value || value.trim().length === 0) {
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
answers[field.id] = parsePromptValue(field, value);
|
|
1993
|
+
}
|
|
1994
|
+
return answers;
|
|
1995
|
+
};
|
|
1996
|
+
var getProviderModelName = (provider) => provider === "openai" ? "gpt-4.1" : "claude-opus-4-5";
|
|
1997
|
+
var maybeSet = (target, key, value) => {
|
|
1998
|
+
if (typeof value === "string" && value.trim().length === 0) {
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
if (typeof value === "undefined") {
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
target[key] = value;
|
|
2005
|
+
};
|
|
2006
|
+
var buildConfigFromOnboardingAnswers = (answers) => {
|
|
2007
|
+
const storageProvider = String(answers["storage.provider"] ?? "local");
|
|
2008
|
+
const memoryEnabled = Boolean(answers["storage.memory.enabled"] ?? true);
|
|
2009
|
+
const maxRecallConversations = Number(
|
|
2010
|
+
answers["storage.memory.maxRecallConversations"] ?? 20
|
|
2011
|
+
);
|
|
2012
|
+
const storage = {
|
|
2013
|
+
provider: storageProvider,
|
|
2014
|
+
memory: {
|
|
2015
|
+
enabled: memoryEnabled,
|
|
2016
|
+
maxRecallConversations
|
|
2017
|
+
}
|
|
2018
|
+
};
|
|
2019
|
+
maybeSet(storage, "url", answers["storage.url"]);
|
|
2020
|
+
maybeSet(storage, "token", answers["storage.token"]);
|
|
2021
|
+
maybeSet(storage, "table", answers["storage.table"]);
|
|
2022
|
+
maybeSet(storage, "region", answers["storage.region"]);
|
|
2023
|
+
const authRequired = Boolean(answers["auth.required"] ?? false);
|
|
2024
|
+
const authType = answers["auth.type"] ?? "bearer";
|
|
2025
|
+
const auth = {
|
|
2026
|
+
required: authRequired,
|
|
2027
|
+
type: authType
|
|
2028
|
+
};
|
|
2029
|
+
if (authType === "header") {
|
|
2030
|
+
maybeSet(auth, "headerName", answers["auth.headerName"]);
|
|
2031
|
+
}
|
|
2032
|
+
const telemetryEnabled = Boolean(answers["telemetry.enabled"] ?? true);
|
|
2033
|
+
const telemetry = {
|
|
2034
|
+
enabled: telemetryEnabled
|
|
2035
|
+
};
|
|
2036
|
+
maybeSet(telemetry, "otlp", answers["telemetry.otlp"]);
|
|
2037
|
+
return {
|
|
2038
|
+
mcp: [],
|
|
2039
|
+
auth,
|
|
2040
|
+
storage,
|
|
2041
|
+
telemetry
|
|
2042
|
+
};
|
|
2043
|
+
};
|
|
2044
|
+
var collectEnvVars = (answers) => {
|
|
2045
|
+
const envVars = /* @__PURE__ */ new Set();
|
|
2046
|
+
const provider = String(answers["model.provider"] ?? "anthropic");
|
|
2047
|
+
if (provider === "openai") {
|
|
2048
|
+
envVars.add("OPENAI_API_KEY=sk-...");
|
|
2049
|
+
} else {
|
|
2050
|
+
envVars.add("ANTHROPIC_API_KEY=sk-ant-...");
|
|
2051
|
+
}
|
|
2052
|
+
const storageProvider = String(answers["storage.provider"] ?? "local");
|
|
2053
|
+
if (storageProvider === "redis") {
|
|
2054
|
+
envVars.add("REDIS_URL=redis://localhost:6379");
|
|
2055
|
+
}
|
|
2056
|
+
if (storageProvider === "upstash") {
|
|
2057
|
+
envVars.add("UPSTASH_REDIS_REST_URL=https://...");
|
|
2058
|
+
envVars.add("UPSTASH_REDIS_REST_TOKEN=...");
|
|
2059
|
+
}
|
|
2060
|
+
if (storageProvider === "dynamodb") {
|
|
2061
|
+
envVars.add("PONCHO_DYNAMODB_TABLE=poncho-conversations");
|
|
2062
|
+
envVars.add("AWS_REGION=us-east-1");
|
|
2063
|
+
}
|
|
2064
|
+
const authRequired = Boolean(answers["auth.required"] ?? false);
|
|
2065
|
+
if (authRequired) {
|
|
2066
|
+
envVars.add("PONCHO_AUTH_TOKEN=...");
|
|
2067
|
+
}
|
|
2068
|
+
return Array.from(envVars);
|
|
2069
|
+
};
|
|
2070
|
+
var collectEnvFileLines = (answers) => {
|
|
2071
|
+
const lines = [
|
|
2072
|
+
"# Poncho environment configuration",
|
|
2073
|
+
"# Fill in empty values before running `poncho dev` or `poncho run --interactive`.",
|
|
2074
|
+
"# Tip: keep secrets in `.env` only (never commit them).",
|
|
2075
|
+
""
|
|
2076
|
+
];
|
|
2077
|
+
const modelProvider = String(answers["model.provider"] ?? "anthropic");
|
|
2078
|
+
const modelEnvKey = modelProvider === "openai" ? "env.OPENAI_API_KEY" : "env.ANTHROPIC_API_KEY";
|
|
2079
|
+
const modelEnvVar = modelProvider === "openai" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY";
|
|
2080
|
+
const modelEnvValue = String(answers[modelEnvKey] ?? "");
|
|
2081
|
+
lines.push("# Model");
|
|
2082
|
+
if (modelEnvValue.length === 0) {
|
|
2083
|
+
lines.push(
|
|
2084
|
+
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"
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
lines.push(`${modelEnvVar}=${modelEnvValue}`);
|
|
2088
|
+
lines.push("");
|
|
2089
|
+
const authRequired = Boolean(answers["auth.required"] ?? false);
|
|
2090
|
+
const authType = answers["auth.type"] ?? "bearer";
|
|
2091
|
+
const authHeaderName = String(answers["auth.headerName"] ?? "x-poncho-key");
|
|
2092
|
+
if (authRequired) {
|
|
2093
|
+
lines.push("# Auth (API request authentication)");
|
|
2094
|
+
if (authType === "bearer") {
|
|
2095
|
+
lines.push("# Requests should include: Authorization: Bearer <token>");
|
|
2096
|
+
} else if (authType === "header") {
|
|
2097
|
+
lines.push(`# Requests should include: ${authHeaderName}: <token>`);
|
|
2098
|
+
} else {
|
|
2099
|
+
lines.push("# Custom auth mode: read this token in your auth.validate function.");
|
|
2100
|
+
}
|
|
2101
|
+
lines.push("PONCHO_AUTH_TOKEN=");
|
|
2102
|
+
lines.push("");
|
|
2103
|
+
}
|
|
2104
|
+
const storageProvider = String(answers["storage.provider"] ?? "local");
|
|
2105
|
+
if (storageProvider === "redis") {
|
|
2106
|
+
lines.push("# Storage (Redis)");
|
|
2107
|
+
lines.push("# Run local Redis: docker run -p 6379:6379 redis:7");
|
|
2108
|
+
lines.push("# Or use a managed Redis URL from your cloud provider.");
|
|
2109
|
+
lines.push("REDIS_URL=");
|
|
2110
|
+
lines.push("");
|
|
2111
|
+
} else if (storageProvider === "upstash") {
|
|
2112
|
+
lines.push("# Storage (Upstash)");
|
|
2113
|
+
lines.push("# Create a Redis database at https://console.upstash.com/");
|
|
2114
|
+
lines.push("# Copy REST URL + REST TOKEN from the Upstash dashboard.");
|
|
2115
|
+
lines.push("UPSTASH_REDIS_REST_URL=");
|
|
2116
|
+
lines.push("UPSTASH_REDIS_REST_TOKEN=");
|
|
2117
|
+
lines.push("");
|
|
2118
|
+
} else if (storageProvider === "dynamodb") {
|
|
2119
|
+
lines.push("# Storage (DynamoDB)");
|
|
2120
|
+
lines.push("# Create a DynamoDB table for Poncho conversation/state storage.");
|
|
2121
|
+
lines.push("# Ensure AWS credentials are configured (AWS_PROFILE or access keys).");
|
|
2122
|
+
lines.push("PONCHO_DYNAMODB_TABLE=");
|
|
2123
|
+
lines.push("AWS_REGION=");
|
|
2124
|
+
lines.push("");
|
|
2125
|
+
} else if (storageProvider === "local" || storageProvider === "memory") {
|
|
2126
|
+
lines.push(
|
|
2127
|
+
storageProvider === "local" ? "# Storage (Local file): no extra env vars required." : "# Storage (In-memory): no extra env vars required, data resets on restart."
|
|
2128
|
+
);
|
|
2129
|
+
lines.push("");
|
|
2130
|
+
}
|
|
2131
|
+
const telemetryEnabled = Boolean(answers["telemetry.enabled"] ?? true);
|
|
2132
|
+
if (telemetryEnabled) {
|
|
2133
|
+
lines.push("# Telemetry (optional)");
|
|
2134
|
+
lines.push("# Latitude telemetry setup: https://docs.latitude.so/");
|
|
2135
|
+
lines.push("# If not using Latitude yet, you can leave these empty.");
|
|
2136
|
+
lines.push("LATITUDE_API_KEY=");
|
|
2137
|
+
lines.push("LATITUDE_PROJECT_ID=");
|
|
2138
|
+
lines.push("LATITUDE_PATH=");
|
|
2139
|
+
lines.push("");
|
|
2140
|
+
}
|
|
2141
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
2142
|
+
lines.pop();
|
|
2143
|
+
}
|
|
2144
|
+
return lines;
|
|
2145
|
+
};
|
|
2146
|
+
var runInitOnboarding = async (options) => {
|
|
2147
|
+
const answers = await askOnboardingQuestions(options);
|
|
2148
|
+
const provider = String(answers["model.provider"] ?? "anthropic");
|
|
2149
|
+
const config = buildConfigFromOnboardingAnswers(answers);
|
|
2150
|
+
const envExampleLines = collectEnvVars(answers);
|
|
2151
|
+
const envFileLines = collectEnvFileLines(answers);
|
|
2152
|
+
const envNeedsUserInput = envFileLines.some(
|
|
2153
|
+
(line) => line.includes("=") && !line.startsWith("#") && line.endsWith("=")
|
|
2154
|
+
);
|
|
2155
|
+
return {
|
|
2156
|
+
answers,
|
|
2157
|
+
config,
|
|
2158
|
+
envExample: `${envExampleLines.join("\n")}
|
|
2159
|
+
`,
|
|
2160
|
+
envFile: envFileLines.length > 0 ? `${envFileLines.join("\n")}
|
|
2161
|
+
` : "",
|
|
2162
|
+
envNeedsUserInput,
|
|
2163
|
+
agentModel: {
|
|
2164
|
+
provider: provider === "openai" ? "openai" : "anthropic",
|
|
2165
|
+
name: getProviderModelName(provider)
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
2168
|
+
};
|
|
2169
|
+
|
|
2170
|
+
// src/init-feature-context.ts
|
|
2171
|
+
import { createHash as createHash2 } from "crypto";
|
|
2172
|
+
import { access, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
2173
|
+
import { basename as basename2, dirname as dirname2, resolve as resolve2 } from "path";
|
|
2174
|
+
import { homedir as homedir2 } from "os";
|
|
2175
|
+
var ONBOARDING_VERSION = 1;
|
|
2176
|
+
var getStateDirectory = () => {
|
|
2177
|
+
const cwd = process.cwd();
|
|
2178
|
+
const home = homedir2();
|
|
2179
|
+
const isServerless = process.env.VERCEL === "1" || process.env.VERCEL_ENV !== void 0 || process.env.VERCEL_URL !== void 0 || process.env.AWS_LAMBDA_FUNCTION_NAME !== void 0 || process.env.AWS_EXECUTION_ENV?.includes("AWS_Lambda") === true || process.env.LAMBDA_TASK_ROOT !== void 0 || process.env.NOW_REGION !== void 0 || cwd.startsWith("/var/task") || home.startsWith("/var/task") || process.env.SERVERLESS === "1";
|
|
2180
|
+
if (isServerless) {
|
|
2181
|
+
return "/tmp/.poncho/state";
|
|
2182
|
+
}
|
|
2183
|
+
return resolve2(homedir2(), ".poncho", "state");
|
|
2184
|
+
};
|
|
2185
|
+
var summarizeConfig = (config) => {
|
|
2186
|
+
const provider = config?.storage?.provider ?? config?.state?.provider ?? "local";
|
|
2187
|
+
const memoryEnabled = config?.storage?.memory?.enabled ?? config?.memory?.enabled ?? false;
|
|
2188
|
+
const authRequired = config?.auth?.required ?? false;
|
|
2189
|
+
const telemetryEnabled = config?.telemetry?.enabled ?? true;
|
|
2190
|
+
return [
|
|
2191
|
+
`storage: ${provider}`,
|
|
2192
|
+
`memory tools: ${memoryEnabled ? "enabled" : "disabled"}`,
|
|
2193
|
+
`auth: ${authRequired ? "required" : "not required"}`,
|
|
2194
|
+
`telemetry: ${telemetryEnabled ? "enabled" : "disabled"}`
|
|
2195
|
+
];
|
|
2196
|
+
};
|
|
2197
|
+
var getOnboardingMarkerPath = (workingDir) => resolve2(
|
|
2198
|
+
getStateDirectory(),
|
|
2199
|
+
`${basename2(workingDir).replace(/[^a-zA-Z0-9_-]+/g, "-") || "project"}-${createHash2("sha256").update(workingDir).digest("hex").slice(0, 12)}-onboarding.json`
|
|
2200
|
+
);
|
|
2201
|
+
var readMarker = async (workingDir) => {
|
|
2202
|
+
const markerPath = getOnboardingMarkerPath(workingDir);
|
|
2203
|
+
try {
|
|
2204
|
+
await access(markerPath);
|
|
2205
|
+
} catch {
|
|
2206
|
+
return void 0;
|
|
2207
|
+
}
|
|
2208
|
+
try {
|
|
2209
|
+
const raw = await readFile2(markerPath, "utf8");
|
|
2210
|
+
return JSON.parse(raw);
|
|
2211
|
+
} catch {
|
|
2212
|
+
return void 0;
|
|
2213
|
+
}
|
|
2214
|
+
};
|
|
2215
|
+
var writeMarker = async (workingDir, state) => {
|
|
2216
|
+
const markerPath = getOnboardingMarkerPath(workingDir);
|
|
2217
|
+
await mkdir2(dirname2(markerPath), { recursive: true });
|
|
2218
|
+
await writeFile2(markerPath, JSON.stringify(state, null, 2), "utf8");
|
|
2219
|
+
};
|
|
2220
|
+
var initializeOnboardingMarker = async (workingDir, options) => {
|
|
2221
|
+
const current = await readMarker(workingDir);
|
|
2222
|
+
if (current) {
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
const allowIntro = options?.allowIntro ?? true;
|
|
2226
|
+
await writeMarker(workingDir, {
|
|
2227
|
+
introduced: allowIntro ? false : true,
|
|
2228
|
+
allowIntro,
|
|
2229
|
+
onboardingVersion: ONBOARDING_VERSION,
|
|
2230
|
+
createdAt: Date.now()
|
|
2231
|
+
});
|
|
2232
|
+
};
|
|
2233
|
+
var consumeFirstRunIntro = async (workingDir, input2) => {
|
|
2234
|
+
const runtimeEnv = resolveHarnessEnvironment();
|
|
2235
|
+
if (runtimeEnv === "production") {
|
|
2236
|
+
return void 0;
|
|
2237
|
+
}
|
|
2238
|
+
const marker = await readMarker(workingDir);
|
|
2239
|
+
if (marker?.allowIntro === false) {
|
|
2240
|
+
return void 0;
|
|
2241
|
+
}
|
|
2242
|
+
const shouldShow = !marker || marker.introduced === false;
|
|
2243
|
+
if (!shouldShow) {
|
|
2244
|
+
return void 0;
|
|
2245
|
+
}
|
|
2246
|
+
await writeMarker(workingDir, {
|
|
2247
|
+
introduced: true,
|
|
2248
|
+
allowIntro: true,
|
|
2249
|
+
onboardingVersion: ONBOARDING_VERSION,
|
|
2250
|
+
createdAt: marker?.createdAt ?? Date.now(),
|
|
2251
|
+
introducedAt: Date.now()
|
|
2252
|
+
});
|
|
2253
|
+
const summary = summarizeConfig(input2.config);
|
|
2254
|
+
return [
|
|
2255
|
+
`Hi! I'm **${input2.agentName}**. I can configure myself directly by chat.
|
|
2256
|
+
`,
|
|
2257
|
+
`**Current config**`,
|
|
2258
|
+
` Model: ${input2.provider}/${input2.model}`,
|
|
2259
|
+
` \`\`\`${summary.join(" \xB7 ")}\`\`\``,
|
|
2260
|
+
"",
|
|
2261
|
+
"Feel free to ask me anything when you're ready. I can help you:",
|
|
2262
|
+
"",
|
|
2263
|
+
"- **Build skills**: Create custom tools and capabilities for this agent",
|
|
2264
|
+
"- **Configure the model**: Switch providers (OpenAI, Anthropic, etc.) or models",
|
|
2265
|
+
"- **Set up storage**: Use local files, Upstash, or other backends",
|
|
2266
|
+
"- **Enable auth**: Add bearer tokens or custom authentication",
|
|
2267
|
+
"- **Turn on telemetry**: Track usage with OpenTelemetry/OTLP",
|
|
2268
|
+
"- **Add MCP servers**: Connect external tool servers",
|
|
2269
|
+
"",
|
|
2270
|
+
"Just let me know what you'd like to work on!\n"
|
|
2271
|
+
].join("\n");
|
|
2272
|
+
};
|
|
2273
|
+
|
|
2274
|
+
// src/index.ts
|
|
2275
|
+
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
2276
|
+
var require2 = createRequire(import.meta.url);
|
|
2277
|
+
var writeJson = (response, statusCode, payload) => {
|
|
2278
|
+
response.writeHead(statusCode, { "Content-Type": "application/json" });
|
|
2279
|
+
response.end(JSON.stringify(payload));
|
|
2280
|
+
};
|
|
2281
|
+
var writeHtml = (response, statusCode, payload) => {
|
|
2282
|
+
response.writeHead(statusCode, { "Content-Type": "text/html; charset=utf-8" });
|
|
2283
|
+
response.end(payload);
|
|
2284
|
+
};
|
|
2285
|
+
var readRequestBody = async (request) => {
|
|
2286
|
+
const chunks = [];
|
|
2287
|
+
for await (const chunk of request) {
|
|
2288
|
+
chunks.push(Buffer.from(chunk));
|
|
2289
|
+
}
|
|
2290
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
2291
|
+
return body.length > 0 ? JSON.parse(body) : {};
|
|
2292
|
+
};
|
|
2293
|
+
var resolveHarnessEnvironment = () => {
|
|
2294
|
+
if (process.env.PONCHO_ENV) {
|
|
2295
|
+
const value = process.env.PONCHO_ENV.toLowerCase();
|
|
2296
|
+
if (value === "production" || value === "staging") {
|
|
2297
|
+
return value;
|
|
2298
|
+
}
|
|
2299
|
+
return "development";
|
|
2300
|
+
}
|
|
2301
|
+
if (process.env.VERCEL_ENV) {
|
|
2302
|
+
const vercelEnv = process.env.VERCEL_ENV.toLowerCase();
|
|
2303
|
+
if (vercelEnv === "production") return "production";
|
|
2304
|
+
if (vercelEnv === "preview") return "staging";
|
|
2305
|
+
return "development";
|
|
2306
|
+
}
|
|
2307
|
+
if (process.env.RAILWAY_ENVIRONMENT) {
|
|
2308
|
+
const railwayEnv = process.env.RAILWAY_ENVIRONMENT.toLowerCase();
|
|
2309
|
+
if (railwayEnv === "production") return "production";
|
|
2310
|
+
return "staging";
|
|
2311
|
+
}
|
|
2312
|
+
if (process.env.RENDER) {
|
|
2313
|
+
if (process.env.IS_PULL_REQUEST === "true") return "staging";
|
|
2314
|
+
return "production";
|
|
2315
|
+
}
|
|
2316
|
+
if (process.env.AWS_EXECUTION_ENV || process.env.AWS_LAMBDA_FUNCTION_NAME) {
|
|
2317
|
+
return "production";
|
|
2318
|
+
}
|
|
2319
|
+
if (process.env.FLY_APP_NAME) {
|
|
2320
|
+
return "production";
|
|
2321
|
+
}
|
|
2322
|
+
if (process.env.NODE_ENV) {
|
|
2323
|
+
const value = process.env.NODE_ENV.toLowerCase();
|
|
2324
|
+
if (value === "production" || value === "staging") {
|
|
2325
|
+
return value;
|
|
2326
|
+
}
|
|
2327
|
+
return "development";
|
|
2328
|
+
}
|
|
2329
|
+
return "development";
|
|
2330
|
+
};
|
|
2331
|
+
var listenOnAvailablePort = async (server, preferredPort) => await new Promise((resolveListen, rejectListen) => {
|
|
2332
|
+
let currentPort = preferredPort;
|
|
2333
|
+
const tryListen = () => {
|
|
2334
|
+
const onListening = () => {
|
|
2335
|
+
server.off("error", onError);
|
|
2336
|
+
const address = server.address();
|
|
2337
|
+
if (address && typeof address === "object" && typeof address.port === "number") {
|
|
2338
|
+
resolveListen(address.port);
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
resolveListen(currentPort);
|
|
2342
|
+
};
|
|
2343
|
+
const onError = (error) => {
|
|
2344
|
+
server.off("listening", onListening);
|
|
2345
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE") {
|
|
2346
|
+
currentPort += 1;
|
|
2347
|
+
if (currentPort > 65535) {
|
|
2348
|
+
rejectListen(
|
|
2349
|
+
new Error(
|
|
2350
|
+
"No available ports found from the requested port up to 65535."
|
|
2351
|
+
)
|
|
2352
|
+
);
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
setImmediate(tryListen);
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
rejectListen(error);
|
|
2359
|
+
};
|
|
2360
|
+
server.once("listening", onListening);
|
|
2361
|
+
server.once("error", onError);
|
|
2362
|
+
server.listen(currentPort);
|
|
2363
|
+
};
|
|
2364
|
+
tryListen();
|
|
2365
|
+
});
|
|
2366
|
+
var parseParams = (values) => {
|
|
2367
|
+
const params = {};
|
|
2368
|
+
for (const value of values) {
|
|
2369
|
+
const [key, ...rest] = value.split("=");
|
|
2370
|
+
if (!key) {
|
|
2371
|
+
continue;
|
|
2372
|
+
}
|
|
2373
|
+
params[key] = rest.join("=");
|
|
2374
|
+
}
|
|
2375
|
+
return params;
|
|
2376
|
+
};
|
|
2377
|
+
var AGENT_TEMPLATE = (name, options) => `---
|
|
2378
|
+
name: ${name}
|
|
2379
|
+
description: A helpful Poncho assistant
|
|
2380
|
+
model:
|
|
2381
|
+
provider: ${options.modelProvider}
|
|
2382
|
+
name: ${options.modelName}
|
|
2383
|
+
temperature: 0.2
|
|
2384
|
+
limits:
|
|
2385
|
+
maxSteps: 50
|
|
2386
|
+
timeout: 300
|
|
2387
|
+
---
|
|
2388
|
+
|
|
2389
|
+
# {{name}}
|
|
2390
|
+
|
|
2391
|
+
You are **{{name}}**, a helpful assistant built with Poncho.
|
|
2392
|
+
|
|
2393
|
+
Working directory: {{runtime.workingDir}}
|
|
2394
|
+
Environment: {{runtime.environment}}
|
|
2395
|
+
|
|
2396
|
+
## Task Guidance
|
|
2397
|
+
|
|
2398
|
+
- Use tools when needed
|
|
2399
|
+
- Explain your reasoning clearly
|
|
2400
|
+
- Ask clarifying questions when requirements are ambiguous
|
|
2401
|
+
- Never claim a file/tool change unless the corresponding tool call actually succeeded
|
|
2402
|
+
`;
|
|
2403
|
+
var resolveLocalPackagesRoot = () => {
|
|
2404
|
+
const candidate = resolve3(__dirname, "..", "..", "harness", "package.json");
|
|
2405
|
+
if (existsSync(candidate)) {
|
|
2406
|
+
return resolve3(__dirname, "..", "..");
|
|
2407
|
+
}
|
|
2408
|
+
return null;
|
|
2409
|
+
};
|
|
2410
|
+
var resolveCoreDeps = (projectDir) => {
|
|
2411
|
+
const packagesRoot = resolveLocalPackagesRoot();
|
|
2412
|
+
if (packagesRoot) {
|
|
2413
|
+
const harnessAbs = resolve3(packagesRoot, "harness");
|
|
2414
|
+
const sdkAbs = resolve3(packagesRoot, "sdk");
|
|
2415
|
+
return {
|
|
2416
|
+
harness: `link:${relative(projectDir, harnessAbs)}`,
|
|
2417
|
+
sdk: `link:${relative(projectDir, sdkAbs)}`
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
return { harness: "^0.1.0", sdk: "^0.1.0" };
|
|
2421
|
+
};
|
|
2422
|
+
var PACKAGE_TEMPLATE = (name, projectDir) => {
|
|
2423
|
+
const deps = resolveCoreDeps(projectDir);
|
|
2424
|
+
return JSON.stringify(
|
|
2425
|
+
{
|
|
2426
|
+
name,
|
|
2427
|
+
private: true,
|
|
2428
|
+
type: "module",
|
|
2429
|
+
dependencies: {
|
|
2430
|
+
"@poncho-ai/harness": deps.harness,
|
|
2431
|
+
"@poncho-ai/sdk": deps.sdk
|
|
2432
|
+
}
|
|
2433
|
+
},
|
|
2434
|
+
null,
|
|
2435
|
+
2
|
|
2436
|
+
);
|
|
2437
|
+
};
|
|
2438
|
+
var README_TEMPLATE = (name) => `# ${name}
|
|
2439
|
+
|
|
2440
|
+
An AI agent built with [Poncho](https://github.com/cesr/poncho-ai).
|
|
2441
|
+
|
|
2442
|
+
## Prerequisites
|
|
2443
|
+
|
|
2444
|
+
- Node.js 20+
|
|
2445
|
+
- npm (or pnpm/yarn)
|
|
2446
|
+
- Anthropic or OpenAI API key
|
|
2447
|
+
|
|
2448
|
+
## Quick Start
|
|
2449
|
+
|
|
2450
|
+
\`\`\`bash
|
|
2451
|
+
npm install
|
|
2452
|
+
# If you didn't enter an API key during init:
|
|
2453
|
+
cp .env.example .env
|
|
2454
|
+
# Then edit .env and add your API key
|
|
2455
|
+
poncho dev
|
|
2456
|
+
\`\`\`
|
|
2457
|
+
|
|
2458
|
+
Open \`http://localhost:3000\` for the web UI.
|
|
2459
|
+
|
|
2460
|
+
On your first interactive session, the agent introduces its configurable capabilities.
|
|
2461
|
+
|
|
2462
|
+
## Common Commands
|
|
2463
|
+
|
|
2464
|
+
\`\`\`bash
|
|
2465
|
+
# Local web UI + API server
|
|
2466
|
+
poncho dev
|
|
2467
|
+
|
|
2468
|
+
# Local interactive CLI
|
|
2469
|
+
poncho run --interactive
|
|
2470
|
+
|
|
2471
|
+
# One-off run
|
|
2472
|
+
poncho run "Your task here"
|
|
2473
|
+
|
|
2474
|
+
# Run tests
|
|
2475
|
+
poncho test
|
|
2476
|
+
|
|
2477
|
+
# List available tools
|
|
2478
|
+
poncho tools
|
|
2479
|
+
\`\`\`
|
|
2480
|
+
|
|
2481
|
+
## Add Skills
|
|
2482
|
+
|
|
2483
|
+
Install skills from a local path or remote repository, then verify discovery:
|
|
2484
|
+
|
|
2485
|
+
\`\`\`bash
|
|
2486
|
+
# Install skills into ./skills
|
|
2487
|
+
poncho add <repo-or-path>
|
|
2488
|
+
|
|
2489
|
+
# Verify loaded tools
|
|
2490
|
+
poncho tools
|
|
2491
|
+
\`\`\`
|
|
2492
|
+
|
|
2493
|
+
After adding skills, run \`poncho dev\` or \`poncho run --interactive\` and ask the agent to use them.
|
|
2494
|
+
|
|
2495
|
+
## Configure MCP Servers (Remote)
|
|
2496
|
+
|
|
2497
|
+
Connect remote MCP servers and expose their tools to the agent:
|
|
2498
|
+
|
|
2499
|
+
\`\`\`bash
|
|
2500
|
+
# Add remote MCP server
|
|
2501
|
+
poncho mcp add --url https://mcp.example.com/github --name github --auth-bearer-env GITHUB_TOKEN
|
|
2502
|
+
|
|
2503
|
+
# List configured servers
|
|
2504
|
+
poncho mcp list
|
|
2505
|
+
|
|
2506
|
+
# Discover and select MCP tools into config allowlist
|
|
2507
|
+
poncho mcp tools list github
|
|
2508
|
+
poncho mcp tools select github
|
|
2509
|
+
|
|
2510
|
+
# Remove a server
|
|
2511
|
+
poncho mcp remove github
|
|
2512
|
+
\`\`\`
|
|
2513
|
+
|
|
2514
|
+
Set required secrets in \`.env\` (for example, \`GITHUB_TOKEN=...\`).
|
|
2515
|
+
|
|
2516
|
+
## Tool Intent in Frontmatter
|
|
2517
|
+
|
|
2518
|
+
Declare tool intent directly in \`AGENT.md\` and \`SKILL.md\` frontmatter:
|
|
2519
|
+
|
|
2520
|
+
\`\`\`yaml
|
|
2521
|
+
tools:
|
|
2522
|
+
mcp:
|
|
2523
|
+
- github/list_issues
|
|
2524
|
+
- github/*
|
|
2525
|
+
scripts:
|
|
2526
|
+
- starter/scripts/*
|
|
2527
|
+
\`\`\`
|
|
2528
|
+
|
|
2529
|
+
How it works:
|
|
2530
|
+
|
|
2531
|
+
- \`AGENT.md\` provides fallback MCP intent when no skill is active.
|
|
2532
|
+
- \`SKILL.md\` intent applies when you activate that skill (\`activate_skill\`).
|
|
2533
|
+
- Skill scripts are accessible by default from each skill's \`scripts/\` directory.
|
|
2534
|
+
- \`AGENT.md\` \`tools.scripts\` can still be used to narrow script access when active skills do not set script intent.
|
|
2535
|
+
- Active skills are unioned, then filtered by policy in \`poncho.config.js\`.
|
|
2536
|
+
- Deactivating a skill (\`deactivate_skill\`) removes its MCP tools from runtime registration.
|
|
2537
|
+
|
|
2538
|
+
Pattern format is strict slash-only:
|
|
2539
|
+
|
|
2540
|
+
- MCP: \`server/tool\`, \`server/*\`
|
|
2541
|
+
- Scripts: \`skill/scripts/file.ts\`, \`skill/scripts/*\`
|
|
2542
|
+
|
|
2543
|
+
## Configuration
|
|
2544
|
+
|
|
2545
|
+
Core files:
|
|
2546
|
+
|
|
2547
|
+
- \`AGENT.md\`: behavior, model selection, runtime guidance
|
|
2548
|
+
- \`poncho.config.js\`: runtime config (storage, auth, telemetry, MCP, tools)
|
|
2549
|
+
- \`.env\`: secrets and environment variables
|
|
2550
|
+
|
|
2551
|
+
Example \`poncho.config.js\`:
|
|
2552
|
+
|
|
2553
|
+
\`\`\`javascript
|
|
2554
|
+
export default {
|
|
2555
|
+
storage: {
|
|
2556
|
+
provider: "local", // local | memory | redis | upstash | dynamodb
|
|
2557
|
+
memory: {
|
|
2558
|
+
enabled: true,
|
|
2559
|
+
maxRecallConversations: 20,
|
|
2560
|
+
},
|
|
2561
|
+
},
|
|
2562
|
+
auth: {
|
|
2563
|
+
required: false,
|
|
2564
|
+
},
|
|
2565
|
+
telemetry: {
|
|
2566
|
+
enabled: true,
|
|
2567
|
+
},
|
|
2568
|
+
mcp: [
|
|
2569
|
+
{
|
|
2570
|
+
name: "github",
|
|
2571
|
+
url: "https://mcp.example.com/github",
|
|
2572
|
+
auth: { type: "bearer", tokenEnv: "GITHUB_TOKEN" },
|
|
2573
|
+
tools: {
|
|
2574
|
+
mode: "allowlist",
|
|
2575
|
+
include: ["github/list_issues", "github/get_issue"],
|
|
2576
|
+
},
|
|
2577
|
+
},
|
|
2578
|
+
],
|
|
2579
|
+
scripts: {
|
|
2580
|
+
mode: "allowlist",
|
|
2581
|
+
include: ["starter/scripts/*"],
|
|
2582
|
+
},
|
|
2583
|
+
tools: {
|
|
2584
|
+
defaults: {
|
|
2585
|
+
list_directory: true,
|
|
2586
|
+
read_file: true,
|
|
2587
|
+
write_file: true, // still gated by environment/policy
|
|
2588
|
+
},
|
|
2589
|
+
byEnvironment: {
|
|
2590
|
+
production: {
|
|
2591
|
+
read_file: false, // example override
|
|
2592
|
+
},
|
|
2593
|
+
},
|
|
2594
|
+
},
|
|
2595
|
+
};
|
|
2596
|
+
\`\`\`
|
|
2597
|
+
|
|
2598
|
+
## Project Structure
|
|
2599
|
+
|
|
2600
|
+
\`\`\`
|
|
2601
|
+
${name}/
|
|
2602
|
+
\u251C\u2500\u2500 AGENT.md # Agent definition and system prompt
|
|
2603
|
+
\u251C\u2500\u2500 poncho.config.js # Configuration (MCP servers, auth, etc.)
|
|
2604
|
+
\u251C\u2500\u2500 package.json # Dependencies
|
|
2605
|
+
\u251C\u2500\u2500 .env.example # Environment variables template
|
|
2606
|
+
\u251C\u2500\u2500 tests/
|
|
2607
|
+
\u2502 \u2514\u2500\u2500 basic.yaml # Test suite
|
|
2608
|
+
\u2514\u2500\u2500 skills/
|
|
2609
|
+
\u2514\u2500\u2500 starter/
|
|
2610
|
+
\u251C\u2500\u2500 SKILL.md
|
|
2611
|
+
\u2514\u2500\u2500 scripts/
|
|
2612
|
+
\u2514\u2500\u2500 starter-echo.ts
|
|
2613
|
+
\`\`\`
|
|
2614
|
+
|
|
2615
|
+
## Deployment
|
|
2616
|
+
|
|
2617
|
+
\`\`\`bash
|
|
2618
|
+
# Build for Vercel
|
|
2619
|
+
poncho build vercel
|
|
2620
|
+
cd .poncho-build/vercel && vercel deploy --prod
|
|
2621
|
+
|
|
2622
|
+
# Build for Docker
|
|
2623
|
+
poncho build docker
|
|
2624
|
+
docker build -t ${name} .
|
|
2625
|
+
\`\`\`
|
|
2626
|
+
|
|
2627
|
+
For full reference:
|
|
2628
|
+
https://github.com/cesr/poncho-ai
|
|
2629
|
+
`;
|
|
2630
|
+
var ENV_TEMPLATE = "ANTHROPIC_API_KEY=sk-ant-...\n";
|
|
2631
|
+
var GITIGNORE_TEMPLATE = ".env\nnode_modules\ndist\n.poncho-build\n.poncho/\ninteractive-session.json\n";
|
|
2632
|
+
var VERCEL_RUNTIME_DEPENDENCIES = {
|
|
2633
|
+
"@anthropic-ai/sdk": "^0.74.0",
|
|
2634
|
+
"@aws-sdk/client-dynamodb": "^3.988.0",
|
|
2635
|
+
"@latitude-data/telemetry": "^2.0.2",
|
|
2636
|
+
commander: "^12.0.0",
|
|
2637
|
+
dotenv: "^16.4.0",
|
|
2638
|
+
jiti: "^2.6.1",
|
|
2639
|
+
mustache: "^4.2.0",
|
|
2640
|
+
openai: "^6.3.0",
|
|
2641
|
+
redis: "^5.10.0",
|
|
2642
|
+
yaml: "^2.8.1"
|
|
2643
|
+
};
|
|
2644
|
+
var TEST_TEMPLATE = `tests:
|
|
2645
|
+
- name: "Basic sanity"
|
|
2646
|
+
task: "What is 2 + 2?"
|
|
2647
|
+
expect:
|
|
2648
|
+
contains: "4"
|
|
2649
|
+
`;
|
|
2650
|
+
var SKILL_TEMPLATE = `---
|
|
2651
|
+
name: starter-skill
|
|
2652
|
+
description: Starter local skill template
|
|
2653
|
+
---
|
|
2654
|
+
|
|
2655
|
+
# Starter Skill
|
|
2656
|
+
|
|
2657
|
+
This is a starter local skill created by \`poncho init\`.
|
|
2658
|
+
|
|
2659
|
+
## Authoring Notes
|
|
2660
|
+
|
|
2661
|
+
- Put executable JavaScript/TypeScript files in \`scripts/\`.
|
|
2662
|
+
- Ask the agent to call \`run_skill_script\` with \`skill\`, \`script\`, and optional \`input\`.
|
|
2663
|
+
`;
|
|
2664
|
+
var SKILL_TOOL_TEMPLATE = `export default async function run(input) {
|
|
2665
|
+
const message = typeof input?.message === "string" ? input.message : "";
|
|
2666
|
+
return { echoed: message };
|
|
2667
|
+
}
|
|
2668
|
+
`;
|
|
2669
|
+
var ensureFile = async (path, content) => {
|
|
2670
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
2671
|
+
await writeFile3(path, content, { encoding: "utf8", flag: "wx" });
|
|
2672
|
+
};
|
|
2673
|
+
var copyIfExists = async (sourcePath, destinationPath) => {
|
|
2674
|
+
try {
|
|
2675
|
+
await access2(sourcePath);
|
|
2676
|
+
} catch {
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
await mkdir3(dirname3(destinationPath), { recursive: true });
|
|
2680
|
+
await cp(sourcePath, destinationPath, { recursive: true });
|
|
2681
|
+
};
|
|
2682
|
+
var resolveCliEntrypoint = async () => {
|
|
2683
|
+
const sourceEntrypoint = resolve3(packageRoot, "src", "index.ts");
|
|
2684
|
+
try {
|
|
2685
|
+
await access2(sourceEntrypoint);
|
|
2686
|
+
return sourceEntrypoint;
|
|
2687
|
+
} catch {
|
|
2688
|
+
return resolve3(packageRoot, "dist", "index.js");
|
|
2689
|
+
}
|
|
2690
|
+
};
|
|
2691
|
+
var buildVercelHandlerBundle = async (outDir) => {
|
|
2692
|
+
const { build: esbuild } = await import("esbuild");
|
|
2693
|
+
const cliEntrypoint = await resolveCliEntrypoint();
|
|
2694
|
+
const tempEntry = resolve3(outDir, "api", "_entry.js");
|
|
2695
|
+
await writeFile3(
|
|
2696
|
+
tempEntry,
|
|
2697
|
+
`import { createRequestHandler } from ${JSON.stringify(cliEntrypoint)};
|
|
2698
|
+
let handlerPromise;
|
|
2699
|
+
export default async function handler(req, res) {
|
|
2700
|
+
try {
|
|
2701
|
+
if (!handlerPromise) {
|
|
2702
|
+
handlerPromise = createRequestHandler({ workingDir: process.cwd() });
|
|
2703
|
+
}
|
|
2704
|
+
const requestHandler = await handlerPromise;
|
|
2705
|
+
await requestHandler(req, res);
|
|
2706
|
+
} catch (error) {
|
|
2707
|
+
console.error("Handler error:", error);
|
|
2708
|
+
if (!res.headersSent) {
|
|
2709
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2710
|
+
res.end(JSON.stringify({ error: "Internal server error", message: error?.message || "Unknown error" }));
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
`,
|
|
2715
|
+
"utf8"
|
|
2716
|
+
);
|
|
2717
|
+
await esbuild({
|
|
2718
|
+
entryPoints: [tempEntry],
|
|
2719
|
+
bundle: true,
|
|
2720
|
+
platform: "node",
|
|
2721
|
+
format: "esm",
|
|
2722
|
+
target: "node20",
|
|
2723
|
+
outfile: resolve3(outDir, "api", "index.js"),
|
|
2724
|
+
sourcemap: false,
|
|
2725
|
+
legalComments: "none",
|
|
2726
|
+
external: [
|
|
2727
|
+
...Object.keys(VERCEL_RUNTIME_DEPENDENCIES),
|
|
2728
|
+
"@anthropic-ai/sdk/*",
|
|
2729
|
+
"child_process",
|
|
2730
|
+
"fs",
|
|
2731
|
+
"fs/promises",
|
|
2732
|
+
"http",
|
|
2733
|
+
"https",
|
|
2734
|
+
"path",
|
|
2735
|
+
"module",
|
|
2736
|
+
"url",
|
|
2737
|
+
"readline",
|
|
2738
|
+
"readline/promises",
|
|
2739
|
+
"crypto",
|
|
2740
|
+
"stream",
|
|
2741
|
+
"events",
|
|
2742
|
+
"util",
|
|
2743
|
+
"os",
|
|
2744
|
+
"zlib",
|
|
2745
|
+
"net",
|
|
2746
|
+
"tls",
|
|
2747
|
+
"dns",
|
|
2748
|
+
"assert",
|
|
2749
|
+
"buffer",
|
|
2750
|
+
"timers",
|
|
2751
|
+
"timers/promises",
|
|
2752
|
+
"node:child_process",
|
|
2753
|
+
"node:fs",
|
|
2754
|
+
"node:fs/promises",
|
|
2755
|
+
"node:http",
|
|
2756
|
+
"node:https",
|
|
2757
|
+
"node:path",
|
|
2758
|
+
"node:module",
|
|
2759
|
+
"node:url",
|
|
2760
|
+
"node:readline",
|
|
2761
|
+
"node:readline/promises",
|
|
2762
|
+
"node:crypto",
|
|
2763
|
+
"node:stream",
|
|
2764
|
+
"node:events",
|
|
2765
|
+
"node:util",
|
|
2766
|
+
"node:os",
|
|
2767
|
+
"node:zlib",
|
|
2768
|
+
"node:net",
|
|
2769
|
+
"node:tls",
|
|
2770
|
+
"node:dns",
|
|
2771
|
+
"node:assert",
|
|
2772
|
+
"node:buffer",
|
|
2773
|
+
"node:timers",
|
|
2774
|
+
"node:timers/promises"
|
|
2775
|
+
]
|
|
2776
|
+
});
|
|
2777
|
+
};
|
|
2778
|
+
var renderConfigFile = (config) => `export default ${JSON.stringify(config, null, 2)}
|
|
2779
|
+
`;
|
|
2780
|
+
var writeConfigFile = async (workingDir, config) => {
|
|
2781
|
+
const serialized = renderConfigFile(config);
|
|
2782
|
+
await writeFile3(resolve3(workingDir, "poncho.config.js"), serialized, "utf8");
|
|
2783
|
+
};
|
|
2784
|
+
var ensureEnvPlaceholder = async (filePath, key) => {
|
|
2785
|
+
const normalizedKey = key.trim();
|
|
2786
|
+
if (!normalizedKey) {
|
|
2787
|
+
return false;
|
|
2788
|
+
}
|
|
2789
|
+
let content = "";
|
|
2790
|
+
try {
|
|
2791
|
+
content = await readFile3(filePath, "utf8");
|
|
2792
|
+
} catch {
|
|
2793
|
+
await writeFile3(filePath, `${normalizedKey}=
|
|
2794
|
+
`, "utf8");
|
|
2795
|
+
return true;
|
|
2796
|
+
}
|
|
2797
|
+
const present = content.split(/\r?\n/).some((line) => line.trimStart().startsWith(`${normalizedKey}=`));
|
|
2798
|
+
if (present) {
|
|
2799
|
+
return false;
|
|
2800
|
+
}
|
|
2801
|
+
const withTrailingNewline = content.length === 0 || content.endsWith("\n") ? content : `${content}
|
|
2802
|
+
`;
|
|
2803
|
+
await writeFile3(filePath, `${withTrailingNewline}${normalizedKey}=
|
|
2804
|
+
`, "utf8");
|
|
2805
|
+
return true;
|
|
2806
|
+
};
|
|
2807
|
+
var removeEnvPlaceholder = async (filePath, key) => {
|
|
2808
|
+
const normalizedKey = key.trim();
|
|
2809
|
+
if (!normalizedKey) {
|
|
2810
|
+
return false;
|
|
2811
|
+
}
|
|
2812
|
+
let content = "";
|
|
2813
|
+
try {
|
|
2814
|
+
content = await readFile3(filePath, "utf8");
|
|
2815
|
+
} catch {
|
|
2816
|
+
return false;
|
|
2817
|
+
}
|
|
2818
|
+
const lines = content.split(/\r?\n/);
|
|
2819
|
+
const filtered = lines.filter((line) => !line.trimStart().startsWith(`${normalizedKey}=`));
|
|
2820
|
+
if (filtered.length === lines.length) {
|
|
2821
|
+
return false;
|
|
2822
|
+
}
|
|
2823
|
+
const nextContent = filtered.join("\n").replace(/\n+$/, "");
|
|
2824
|
+
await writeFile3(filePath, nextContent.length > 0 ? `${nextContent}
|
|
2825
|
+
` : "", "utf8");
|
|
2826
|
+
return true;
|
|
2827
|
+
};
|
|
2828
|
+
var gitInit = (cwd) => new Promise((resolve4) => {
|
|
2829
|
+
const child = spawn("git", ["init"], { cwd, stdio: "ignore" });
|
|
2830
|
+
child.on("error", () => resolve4(false));
|
|
2831
|
+
child.on("close", (code) => resolve4(code === 0));
|
|
2832
|
+
});
|
|
2833
|
+
var initProject = async (projectName, options) => {
|
|
2834
|
+
const baseDir = options?.workingDir ?? process.cwd();
|
|
2835
|
+
const projectDir = resolve3(baseDir, projectName);
|
|
2836
|
+
await mkdir3(projectDir, { recursive: true });
|
|
2837
|
+
const onboardingOptions = options?.onboarding ?? {
|
|
2838
|
+
yes: true,
|
|
2839
|
+
interactive: false
|
|
2840
|
+
};
|
|
2841
|
+
const onboarding = await runInitOnboarding(onboardingOptions);
|
|
2842
|
+
const G = "\x1B[32m";
|
|
2843
|
+
const D = "\x1B[2m";
|
|
2844
|
+
const B = "\x1B[1m";
|
|
2845
|
+
const CY = "\x1B[36m";
|
|
2846
|
+
const YW = "\x1B[33m";
|
|
2847
|
+
const R = "\x1B[0m";
|
|
2848
|
+
process.stdout.write("\n");
|
|
2849
|
+
const scaffoldFiles = [
|
|
2850
|
+
{ path: "AGENT.md", content: AGENT_TEMPLATE(projectName, { modelProvider: onboarding.agentModel.provider, modelName: onboarding.agentModel.name }) },
|
|
2851
|
+
{ path: "poncho.config.js", content: renderConfigFile(onboarding.config) },
|
|
2852
|
+
{ path: "package.json", content: PACKAGE_TEMPLATE(projectName, projectDir) },
|
|
2853
|
+
{ path: "README.md", content: README_TEMPLATE(projectName) },
|
|
2854
|
+
{ path: ".env.example", content: options?.envExampleOverride ?? onboarding.envExample ?? ENV_TEMPLATE },
|
|
2855
|
+
{ path: ".gitignore", content: GITIGNORE_TEMPLATE },
|
|
2856
|
+
{ path: "tests/basic.yaml", content: TEST_TEMPLATE },
|
|
2857
|
+
{ path: "skills/starter/SKILL.md", content: SKILL_TEMPLATE },
|
|
2858
|
+
{ path: "skills/starter/scripts/starter-echo.ts", content: SKILL_TOOL_TEMPLATE }
|
|
2859
|
+
];
|
|
2860
|
+
if (onboarding.envFile) {
|
|
2861
|
+
scaffoldFiles.push({ path: ".env", content: onboarding.envFile });
|
|
2862
|
+
}
|
|
2863
|
+
for (const file of scaffoldFiles) {
|
|
2864
|
+
await ensureFile(resolve3(projectDir, file.path), file.content);
|
|
2865
|
+
process.stdout.write(` ${D}+${R} ${D}${file.path}${R}
|
|
2866
|
+
`);
|
|
2867
|
+
}
|
|
2868
|
+
await initializeOnboardingMarker(projectDir, {
|
|
2869
|
+
allowIntro: !(onboardingOptions.yes ?? false)
|
|
2870
|
+
});
|
|
2871
|
+
process.stdout.write("\n");
|
|
2872
|
+
try {
|
|
2873
|
+
await runPnpmInstall(projectDir);
|
|
2874
|
+
process.stdout.write(` ${G}\u2713${R} ${D}Installed dependencies${R}
|
|
2875
|
+
`);
|
|
2876
|
+
} catch {
|
|
2877
|
+
process.stdout.write(
|
|
2878
|
+
` ${YW}!${R} Could not install dependencies \u2014 run ${D}pnpm install${R} manually
|
|
2879
|
+
`
|
|
2880
|
+
);
|
|
2881
|
+
}
|
|
2882
|
+
const gitOk = await gitInit(projectDir);
|
|
2883
|
+
if (gitOk) {
|
|
2884
|
+
process.stdout.write(` ${G}\u2713${R} ${D}Initialized git${R}
|
|
2885
|
+
`);
|
|
2886
|
+
}
|
|
2887
|
+
process.stdout.write(` ${G}\u2713${R} ${B}${projectName}${R} is ready
|
|
2888
|
+
`);
|
|
2889
|
+
process.stdout.write("\n");
|
|
2890
|
+
process.stdout.write(` ${B}Get started${R}
|
|
2891
|
+
`);
|
|
2892
|
+
process.stdout.write("\n");
|
|
2893
|
+
process.stdout.write(` ${D}$${R} cd ${projectName}
|
|
2894
|
+
`);
|
|
2895
|
+
process.stdout.write("\n");
|
|
2896
|
+
process.stdout.write(` ${CY}Web UI${R} ${D}$${R} poncho dev
|
|
2897
|
+
`);
|
|
2898
|
+
process.stdout.write(` ${CY}CLI interactive${R} ${D}$${R} poncho run --interactive
|
|
2899
|
+
`);
|
|
2900
|
+
process.stdout.write("\n");
|
|
2901
|
+
if (onboarding.envNeedsUserInput) {
|
|
2902
|
+
process.stdout.write(
|
|
2903
|
+
` ${YW}!${R} Make sure you add your keys to the ${B}.env${R} file.
|
|
2904
|
+
`
|
|
2905
|
+
);
|
|
2906
|
+
}
|
|
2907
|
+
process.stdout.write(` ${D}The agent will introduce itself on your first session.${R}
|
|
2908
|
+
`);
|
|
2909
|
+
process.stdout.write("\n");
|
|
2910
|
+
};
|
|
2911
|
+
var updateAgentGuidance = async (workingDir) => {
|
|
2912
|
+
const agentPath = resolve3(workingDir, "AGENT.md");
|
|
2913
|
+
const content = await readFile3(agentPath, "utf8");
|
|
2914
|
+
const guidanceSectionPattern = /\n## Configuration Assistant Context[\s\S]*?(?=\n## |\n# |$)|\n## Skill Authoring Guidance[\s\S]*?(?=\n## |\n# |$)/g;
|
|
2915
|
+
const normalized = content.replace(/\s+$/g, "");
|
|
2916
|
+
const updated = normalized.replace(guidanceSectionPattern, "").replace(/\n{3,}/g, "\n\n");
|
|
2917
|
+
if (updated === normalized) {
|
|
2918
|
+
process.stdout.write("AGENT.md does not contain deprecated embedded local guidance.\n");
|
|
2919
|
+
return false;
|
|
2920
|
+
}
|
|
2921
|
+
await writeFile3(agentPath, `${updated}
|
|
2922
|
+
`, "utf8");
|
|
2923
|
+
process.stdout.write("Removed deprecated embedded local guidance from AGENT.md.\n");
|
|
2924
|
+
return true;
|
|
2925
|
+
};
|
|
2926
|
+
var formatSseEvent = (event) => `event: ${event.type}
|
|
2927
|
+
data: ${JSON.stringify(event)}
|
|
2928
|
+
|
|
2929
|
+
`;
|
|
2930
|
+
var createRequestHandler = async (options) => {
|
|
2931
|
+
const workingDir = options?.workingDir ?? process.cwd();
|
|
2932
|
+
dotenv.config({ path: resolve3(workingDir, ".env") });
|
|
2933
|
+
const config = await loadPonchoConfig(workingDir);
|
|
2934
|
+
let agentName = "Agent";
|
|
2935
|
+
let agentModelProvider = "anthropic";
|
|
2936
|
+
let agentModelName = "claude-opus-4-5";
|
|
2937
|
+
try {
|
|
2938
|
+
const agentMd = await readFile3(resolve3(workingDir, "AGENT.md"), "utf8");
|
|
2939
|
+
const nameMatch = agentMd.match(/^name:\s*(.+)$/m);
|
|
2940
|
+
const providerMatch = agentMd.match(/^\s{2}provider:\s*(.+)$/m);
|
|
2941
|
+
const modelMatch = agentMd.match(/^\s{2}name:\s*(.+)$/m);
|
|
2942
|
+
if (nameMatch?.[1]) {
|
|
2943
|
+
agentName = nameMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
2944
|
+
}
|
|
2945
|
+
if (providerMatch?.[1]) {
|
|
2946
|
+
agentModelProvider = providerMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
2947
|
+
}
|
|
2948
|
+
if (modelMatch?.[1]) {
|
|
2949
|
+
agentModelName = modelMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
2950
|
+
}
|
|
2951
|
+
} catch {
|
|
2952
|
+
}
|
|
2953
|
+
const harness = new AgentHarness({ workingDir, environment: resolveHarnessEnvironment() });
|
|
2954
|
+
await harness.initialize();
|
|
2955
|
+
const telemetry = new TelemetryEmitter(config?.telemetry);
|
|
2956
|
+
const conversationStore = createConversationStore(resolveStateConfig(config), { workingDir });
|
|
2957
|
+
const sessionStore = new SessionStore();
|
|
2958
|
+
const loginRateLimiter = new LoginRateLimiter();
|
|
2959
|
+
const passphrase = process.env.AGENT_UI_PASSPHRASE ?? "";
|
|
2960
|
+
const isProduction = resolveHarnessEnvironment() === "production";
|
|
2961
|
+
const requireUiAuth = passphrase.length > 0;
|
|
2962
|
+
const secureCookies = isProduction;
|
|
2963
|
+
return async (request, response) => {
|
|
2964
|
+
if (!request.url || !request.method) {
|
|
2965
|
+
writeJson(response, 404, { error: "Not found" });
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
const [pathname] = request.url.split("?");
|
|
2969
|
+
if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/"))) {
|
|
2970
|
+
writeHtml(response, 200, renderWebUiHtml({ agentName }));
|
|
2971
|
+
return;
|
|
2972
|
+
}
|
|
2973
|
+
if (pathname === "/manifest.json" && request.method === "GET") {
|
|
2974
|
+
response.writeHead(200, { "Content-Type": "application/manifest+json" });
|
|
2975
|
+
response.end(renderManifest({ agentName }));
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
if (pathname === "/sw.js" && request.method === "GET") {
|
|
2979
|
+
response.writeHead(200, {
|
|
2980
|
+
"Content-Type": "application/javascript",
|
|
2981
|
+
"Service-Worker-Allowed": "/"
|
|
2982
|
+
});
|
|
2983
|
+
response.end(renderServiceWorker());
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
if (pathname === "/icon.svg" && request.method === "GET") {
|
|
2987
|
+
response.writeHead(200, { "Content-Type": "image/svg+xml" });
|
|
2988
|
+
response.end(renderIconSvg({ agentName }));
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
if ((pathname === "/icon-192.png" || pathname === "/icon-512.png") && request.method === "GET") {
|
|
2992
|
+
response.writeHead(302, { Location: "/icon.svg" });
|
|
2993
|
+
response.end();
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
if (pathname === "/health" && request.method === "GET") {
|
|
2997
|
+
writeJson(response, 200, { status: "ok" });
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
const cookies = parseCookies(request);
|
|
3001
|
+
const sessionId = cookies.poncho_session;
|
|
3002
|
+
const session = sessionId ? sessionStore.get(sessionId) : void 0;
|
|
3003
|
+
const ownerId = session?.ownerId ?? "local-owner";
|
|
3004
|
+
const requiresCsrfValidation = request.method !== "GET" && request.method !== "HEAD" && request.method !== "OPTIONS";
|
|
3005
|
+
if (pathname === "/api/auth/session" && request.method === "GET") {
|
|
3006
|
+
if (!requireUiAuth) {
|
|
3007
|
+
writeJson(response, 200, { authenticated: true, csrfToken: "" });
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
if (!session) {
|
|
3011
|
+
writeJson(response, 200, { authenticated: false });
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
writeJson(response, 200, {
|
|
3015
|
+
authenticated: true,
|
|
3016
|
+
sessionId: session.sessionId,
|
|
3017
|
+
ownerId: session.ownerId,
|
|
3018
|
+
csrfToken: session.csrfToken
|
|
3019
|
+
});
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
if (pathname === "/api/auth/login" && request.method === "POST") {
|
|
3023
|
+
if (!requireUiAuth) {
|
|
3024
|
+
writeJson(response, 200, { authenticated: true, csrfToken: "" });
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
3027
|
+
const ip = getRequestIp(request);
|
|
3028
|
+
const canAttempt = loginRateLimiter.canAttempt(ip);
|
|
3029
|
+
if (!canAttempt.allowed) {
|
|
3030
|
+
writeJson(response, 429, {
|
|
3031
|
+
code: "AUTH_RATE_LIMIT",
|
|
3032
|
+
message: "Too many failed login attempts. Try again later.",
|
|
3033
|
+
retryAfterSeconds: canAttempt.retryAfterSeconds
|
|
3034
|
+
});
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
const body = await readRequestBody(request);
|
|
3038
|
+
const provided = body.passphrase ?? "";
|
|
3039
|
+
if (!verifyPassphrase(provided, passphrase)) {
|
|
3040
|
+
const failure = loginRateLimiter.registerFailure(ip);
|
|
3041
|
+
writeJson(response, 401, {
|
|
3042
|
+
code: "AUTH_ERROR",
|
|
3043
|
+
message: "Invalid passphrase",
|
|
3044
|
+
retryAfterSeconds: failure.retryAfterSeconds
|
|
3045
|
+
});
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
loginRateLimiter.registerSuccess(ip);
|
|
3049
|
+
const createdSession = sessionStore.create(ownerId);
|
|
3050
|
+
setCookie(response, "poncho_session", createdSession.sessionId, {
|
|
3051
|
+
httpOnly: true,
|
|
3052
|
+
secure: secureCookies,
|
|
3053
|
+
sameSite: "Lax",
|
|
3054
|
+
path: "/",
|
|
3055
|
+
maxAge: 60 * 60 * 8
|
|
3056
|
+
});
|
|
3057
|
+
writeJson(response, 200, {
|
|
3058
|
+
authenticated: true,
|
|
3059
|
+
csrfToken: createdSession.csrfToken
|
|
3060
|
+
});
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
if (pathname === "/api/auth/logout" && request.method === "POST") {
|
|
3064
|
+
if (session?.sessionId) {
|
|
3065
|
+
sessionStore.delete(session.sessionId);
|
|
3066
|
+
}
|
|
3067
|
+
setCookie(response, "poncho_session", "", {
|
|
3068
|
+
httpOnly: true,
|
|
3069
|
+
secure: secureCookies,
|
|
3070
|
+
sameSite: "Lax",
|
|
3071
|
+
path: "/",
|
|
3072
|
+
maxAge: 0
|
|
3073
|
+
});
|
|
3074
|
+
writeJson(response, 200, { ok: true });
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
if (pathname.startsWith("/api/")) {
|
|
3078
|
+
if (requireUiAuth && !session) {
|
|
3079
|
+
writeJson(response, 401, {
|
|
3080
|
+
code: "AUTH_ERROR",
|
|
3081
|
+
message: "Authentication required"
|
|
3082
|
+
});
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3085
|
+
if (requireUiAuth && requiresCsrfValidation && pathname !== "/api/auth/login" && request.headers["x-csrf-token"] !== session?.csrfToken) {
|
|
3086
|
+
writeJson(response, 403, {
|
|
3087
|
+
code: "CSRF_ERROR",
|
|
3088
|
+
message: "Invalid CSRF token"
|
|
3089
|
+
});
|
|
3090
|
+
return;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
if (pathname === "/api/conversations" && request.method === "GET") {
|
|
3094
|
+
const conversations = await conversationStore.list(ownerId);
|
|
3095
|
+
writeJson(response, 200, {
|
|
3096
|
+
conversations: conversations.map((conversation) => ({
|
|
3097
|
+
conversationId: conversation.conversationId,
|
|
3098
|
+
title: conversation.title,
|
|
3099
|
+
runtimeRunId: conversation.runtimeRunId,
|
|
3100
|
+
ownerId: conversation.ownerId,
|
|
3101
|
+
tenantId: conversation.tenantId,
|
|
3102
|
+
createdAt: conversation.createdAt,
|
|
3103
|
+
updatedAt: conversation.updatedAt,
|
|
3104
|
+
messageCount: conversation.messages.length
|
|
3105
|
+
}))
|
|
3106
|
+
});
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
if (pathname === "/api/conversations" && request.method === "POST") {
|
|
3110
|
+
const body = await readRequestBody(request);
|
|
3111
|
+
const conversation = await conversationStore.create(ownerId, body.title);
|
|
3112
|
+
const introMessage = await consumeFirstRunIntro(workingDir, {
|
|
3113
|
+
agentName,
|
|
3114
|
+
provider: agentModelProvider,
|
|
3115
|
+
model: agentModelName,
|
|
3116
|
+
config
|
|
3117
|
+
});
|
|
3118
|
+
if (introMessage) {
|
|
3119
|
+
conversation.messages = [{ role: "assistant", content: introMessage }];
|
|
3120
|
+
await conversationStore.update(conversation);
|
|
3121
|
+
}
|
|
3122
|
+
writeJson(response, 201, { conversation });
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
3125
|
+
const conversationPathMatch = pathname.match(/^\/api\/conversations\/([^/]+)$/);
|
|
3126
|
+
if (conversationPathMatch) {
|
|
3127
|
+
const conversationId = decodeURIComponent(conversationPathMatch[1] ?? "");
|
|
3128
|
+
const conversation = await conversationStore.get(conversationId);
|
|
3129
|
+
if (!conversation || conversation.ownerId !== ownerId) {
|
|
3130
|
+
writeJson(response, 404, {
|
|
3131
|
+
code: "CONVERSATION_NOT_FOUND",
|
|
3132
|
+
message: "Conversation not found"
|
|
3133
|
+
});
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
if (request.method === "GET") {
|
|
3137
|
+
writeJson(response, 200, { conversation });
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
if (request.method === "PATCH") {
|
|
3141
|
+
const body = await readRequestBody(request);
|
|
3142
|
+
if (!body.title || body.title.trim().length === 0) {
|
|
3143
|
+
writeJson(response, 400, {
|
|
3144
|
+
code: "VALIDATION_ERROR",
|
|
3145
|
+
message: "title is required"
|
|
3146
|
+
});
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
const updated = await conversationStore.rename(conversationId, body.title);
|
|
3150
|
+
writeJson(response, 200, { conversation: updated });
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
if (request.method === "DELETE") {
|
|
3154
|
+
await conversationStore.delete(conversationId);
|
|
3155
|
+
writeJson(response, 200, { ok: true });
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
const conversationMessageMatch = pathname.match(/^\/api\/conversations\/([^/]+)\/messages$/);
|
|
3160
|
+
if (conversationMessageMatch && request.method === "POST") {
|
|
3161
|
+
const conversationId = decodeURIComponent(conversationMessageMatch[1] ?? "");
|
|
3162
|
+
const conversation = await conversationStore.get(conversationId);
|
|
3163
|
+
if (!conversation || conversation.ownerId !== ownerId) {
|
|
3164
|
+
writeJson(response, 404, {
|
|
3165
|
+
code: "CONVERSATION_NOT_FOUND",
|
|
3166
|
+
message: "Conversation not found"
|
|
3167
|
+
});
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
const body = await readRequestBody(request);
|
|
3171
|
+
const messageText = body.message?.trim() ?? "";
|
|
3172
|
+
if (!messageText) {
|
|
3173
|
+
writeJson(response, 400, {
|
|
3174
|
+
code: "VALIDATION_ERROR",
|
|
3175
|
+
message: "message is required"
|
|
3176
|
+
});
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
if (conversation.messages.length === 0 && (conversation.title === "New conversation" || conversation.title.trim().length === 0)) {
|
|
3180
|
+
conversation.title = inferConversationTitle(messageText);
|
|
3181
|
+
}
|
|
3182
|
+
response.writeHead(200, {
|
|
3183
|
+
"Content-Type": "text/event-stream",
|
|
3184
|
+
"Cache-Control": "no-cache",
|
|
3185
|
+
Connection: "keep-alive"
|
|
3186
|
+
});
|
|
3187
|
+
let latestRunId = conversation.runtimeRunId ?? "";
|
|
3188
|
+
let assistantResponse = "";
|
|
3189
|
+
const toolTimeline = [];
|
|
3190
|
+
try {
|
|
3191
|
+
const recallCorpus = (await conversationStore.list(ownerId)).filter((item) => item.conversationId !== conversationId).slice(0, 20).map((item) => ({
|
|
3192
|
+
conversationId: item.conversationId,
|
|
3193
|
+
title: item.title,
|
|
3194
|
+
updatedAt: item.updatedAt,
|
|
3195
|
+
content: item.messages.slice(-6).map((message) => `${message.role}: ${message.content}`).join("\n").slice(0, 2e3)
|
|
3196
|
+
})).filter((item) => item.content.length > 0);
|
|
3197
|
+
for await (const event of harness.run({
|
|
3198
|
+
task: messageText,
|
|
3199
|
+
parameters: {
|
|
3200
|
+
...body.parameters ?? {},
|
|
3201
|
+
__conversationRecallCorpus: recallCorpus,
|
|
3202
|
+
__activeConversationId: conversationId
|
|
3203
|
+
},
|
|
3204
|
+
messages: conversation.messages
|
|
3205
|
+
})) {
|
|
3206
|
+
if (event.type === "run:started") {
|
|
3207
|
+
latestRunId = event.runId;
|
|
3208
|
+
}
|
|
3209
|
+
if (event.type === "model:chunk") {
|
|
3210
|
+
assistantResponse += event.content;
|
|
3211
|
+
}
|
|
3212
|
+
if (event.type === "tool:started") {
|
|
3213
|
+
toolTimeline.push(`- start \`${event.tool}\``);
|
|
3214
|
+
}
|
|
3215
|
+
if (event.type === "tool:completed") {
|
|
3216
|
+
toolTimeline.push(`- done \`${event.tool}\` (${event.duration}ms)`);
|
|
3217
|
+
}
|
|
3218
|
+
if (event.type === "tool:error") {
|
|
3219
|
+
toolTimeline.push(`- error \`${event.tool}\`: ${event.error}`);
|
|
3220
|
+
}
|
|
3221
|
+
if (event.type === "tool:approval:required") {
|
|
3222
|
+
toolTimeline.push(`- approval required \`${event.tool}\``);
|
|
3223
|
+
}
|
|
3224
|
+
if (event.type === "tool:approval:granted") {
|
|
3225
|
+
toolTimeline.push(`- approval granted (${event.approvalId})`);
|
|
3226
|
+
}
|
|
3227
|
+
if (event.type === "tool:approval:denied") {
|
|
3228
|
+
toolTimeline.push(`- approval denied (${event.approvalId})`);
|
|
3229
|
+
}
|
|
3230
|
+
if (event.type === "run:completed" && assistantResponse.length === 0 && event.result.response) {
|
|
3231
|
+
assistantResponse = event.result.response;
|
|
3232
|
+
}
|
|
3233
|
+
await telemetry.emit(event);
|
|
3234
|
+
response.write(formatSseEvent(event));
|
|
3235
|
+
}
|
|
3236
|
+
conversation.messages = [
|
|
3237
|
+
...conversation.messages,
|
|
3238
|
+
{ role: "user", content: messageText },
|
|
3239
|
+
{
|
|
3240
|
+
role: "assistant",
|
|
3241
|
+
content: assistantResponse,
|
|
3242
|
+
metadata: toolTimeline.length > 0 ? { toolActivity: toolTimeline } : void 0
|
|
3243
|
+
}
|
|
3244
|
+
];
|
|
3245
|
+
conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
|
|
3246
|
+
conversation.updatedAt = Date.now();
|
|
3247
|
+
await conversationStore.update(conversation);
|
|
3248
|
+
} catch (error) {
|
|
3249
|
+
response.write(
|
|
3250
|
+
formatSseEvent({
|
|
3251
|
+
type: "run:error",
|
|
3252
|
+
runId: latestRunId || "run_unknown",
|
|
3253
|
+
error: {
|
|
3254
|
+
code: "RUN_ERROR",
|
|
3255
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
3256
|
+
}
|
|
3257
|
+
})
|
|
3258
|
+
);
|
|
3259
|
+
} finally {
|
|
3260
|
+
response.end();
|
|
3261
|
+
}
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
writeJson(response, 404, { error: "Not found" });
|
|
3265
|
+
};
|
|
3266
|
+
};
|
|
3267
|
+
var startDevServer = async (port, options) => {
|
|
3268
|
+
const handler = await createRequestHandler(options);
|
|
3269
|
+
const server = createServer(handler);
|
|
3270
|
+
const actualPort = await listenOnAvailablePort(server, port);
|
|
3271
|
+
if (actualPort !== port) {
|
|
3272
|
+
process.stdout.write(`Port ${port} is in use, switched to ${actualPort}.
|
|
3273
|
+
`);
|
|
3274
|
+
}
|
|
3275
|
+
process.stdout.write(`Poncho dev server running at http://localhost:${actualPort}
|
|
3276
|
+
`);
|
|
3277
|
+
const shutdown = () => {
|
|
3278
|
+
server.close();
|
|
3279
|
+
server.closeAllConnections?.();
|
|
3280
|
+
process.exit(0);
|
|
3281
|
+
};
|
|
3282
|
+
process.on("SIGINT", shutdown);
|
|
3283
|
+
process.on("SIGTERM", shutdown);
|
|
3284
|
+
return server;
|
|
3285
|
+
};
|
|
3286
|
+
var runOnce = async (task, options) => {
|
|
3287
|
+
const workingDir = options.workingDir ?? process.cwd();
|
|
3288
|
+
dotenv.config({ path: resolve3(workingDir, ".env") });
|
|
3289
|
+
const config = await loadPonchoConfig(workingDir);
|
|
3290
|
+
const harness = new AgentHarness({ workingDir });
|
|
3291
|
+
const telemetry = new TelemetryEmitter(config?.telemetry);
|
|
3292
|
+
await harness.initialize();
|
|
3293
|
+
const fileBlobs = await Promise.all(
|
|
3294
|
+
options.filePaths.map(async (path) => {
|
|
3295
|
+
const content = await readFile3(resolve3(workingDir, path), "utf8");
|
|
3296
|
+
return `# File: ${path}
|
|
3297
|
+
${content}`;
|
|
3298
|
+
})
|
|
3299
|
+
);
|
|
3300
|
+
const input2 = {
|
|
3301
|
+
task: fileBlobs.length > 0 ? `${task}
|
|
3302
|
+
|
|
3303
|
+
${fileBlobs.join("\n\n")}` : task,
|
|
3304
|
+
parameters: options.params
|
|
3305
|
+
};
|
|
3306
|
+
if (options.json) {
|
|
3307
|
+
const output = await harness.runToCompletion(input2);
|
|
3308
|
+
for (const event of output.events) {
|
|
3309
|
+
await telemetry.emit(event);
|
|
3310
|
+
}
|
|
3311
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
3312
|
+
`);
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
3315
|
+
for await (const event of harness.run(input2)) {
|
|
3316
|
+
await telemetry.emit(event);
|
|
3317
|
+
if (event.type === "model:chunk") {
|
|
3318
|
+
process.stdout.write(event.content);
|
|
3319
|
+
}
|
|
3320
|
+
if (event.type === "run:error") {
|
|
3321
|
+
process.stderr.write(`
|
|
3322
|
+
Error: ${event.error.message}
|
|
3323
|
+
`);
|
|
3324
|
+
}
|
|
3325
|
+
if (event.type === "run:completed") {
|
|
3326
|
+
process.stdout.write("\n");
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
};
|
|
3330
|
+
var runInteractive = async (workingDir, params) => {
|
|
3331
|
+
dotenv.config({ path: resolve3(workingDir, ".env") });
|
|
3332
|
+
const config = await loadPonchoConfig(workingDir);
|
|
3333
|
+
let pendingApproval = null;
|
|
3334
|
+
let onApprovalRequest = null;
|
|
3335
|
+
const approvalHandler = async (request) => {
|
|
3336
|
+
return new Promise((resolveApproval) => {
|
|
3337
|
+
const req = {
|
|
3338
|
+
tool: request.tool,
|
|
3339
|
+
input: request.input,
|
|
3340
|
+
approvalId: request.approvalId,
|
|
3341
|
+
resolve: resolveApproval
|
|
3342
|
+
};
|
|
3343
|
+
pendingApproval = req;
|
|
3344
|
+
if (onApprovalRequest) {
|
|
3345
|
+
onApprovalRequest(req);
|
|
3346
|
+
}
|
|
3347
|
+
});
|
|
3348
|
+
};
|
|
3349
|
+
const harness = new AgentHarness({
|
|
3350
|
+
workingDir,
|
|
3351
|
+
environment: resolveHarnessEnvironment(),
|
|
3352
|
+
approvalHandler
|
|
3353
|
+
});
|
|
3354
|
+
await harness.initialize();
|
|
3355
|
+
try {
|
|
3356
|
+
const { runInteractiveInk } = await import("./run-interactive-ink-BOD2F5JM.js");
|
|
3357
|
+
await runInteractiveInk({
|
|
3358
|
+
harness,
|
|
3359
|
+
params,
|
|
3360
|
+
workingDir,
|
|
3361
|
+
config,
|
|
3362
|
+
conversationStore: createConversationStore(resolveStateConfig(config), { workingDir }),
|
|
3363
|
+
onSetApprovalCallback: (cb) => {
|
|
3364
|
+
onApprovalRequest = cb;
|
|
3365
|
+
if (pendingApproval) {
|
|
3366
|
+
cb(pendingApproval);
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
});
|
|
3370
|
+
} finally {
|
|
3371
|
+
await harness.shutdown();
|
|
3372
|
+
}
|
|
3373
|
+
};
|
|
3374
|
+
var listTools = async (workingDir) => {
|
|
3375
|
+
dotenv.config({ path: resolve3(workingDir, ".env") });
|
|
3376
|
+
const harness = new AgentHarness({ workingDir });
|
|
3377
|
+
await harness.initialize();
|
|
3378
|
+
const tools = harness.listTools();
|
|
3379
|
+
if (tools.length === 0) {
|
|
3380
|
+
process.stdout.write("No tools registered.\n");
|
|
3381
|
+
return;
|
|
3382
|
+
}
|
|
3383
|
+
process.stdout.write("Available tools:\n");
|
|
3384
|
+
for (const tool of tools) {
|
|
3385
|
+
process.stdout.write(`- ${tool.name}: ${tool.description}
|
|
3386
|
+
`);
|
|
3387
|
+
}
|
|
3388
|
+
};
|
|
3389
|
+
var runPnpmInstall = async (workingDir) => await new Promise((resolveInstall, rejectInstall) => {
|
|
3390
|
+
const child = spawn("pnpm", ["install"], {
|
|
3391
|
+
cwd: workingDir,
|
|
3392
|
+
stdio: "inherit",
|
|
3393
|
+
env: process.env
|
|
3394
|
+
});
|
|
3395
|
+
child.on("exit", (code) => {
|
|
3396
|
+
if (code === 0) {
|
|
3397
|
+
resolveInstall();
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
rejectInstall(new Error(`pnpm install failed with exit code ${code ?? -1}`));
|
|
3401
|
+
});
|
|
3402
|
+
});
|
|
3403
|
+
var runInstallCommand = async (workingDir, packageNameOrPath) => await new Promise((resolveInstall, rejectInstall) => {
|
|
3404
|
+
const child = spawn("pnpm", ["add", packageNameOrPath], {
|
|
3405
|
+
cwd: workingDir,
|
|
3406
|
+
stdio: "inherit",
|
|
3407
|
+
env: process.env
|
|
3408
|
+
});
|
|
3409
|
+
child.on("exit", (code) => {
|
|
3410
|
+
if (code === 0) {
|
|
3411
|
+
resolveInstall();
|
|
3412
|
+
return;
|
|
3413
|
+
}
|
|
3414
|
+
rejectInstall(new Error(`pnpm add failed with exit code ${code ?? -1}`));
|
|
3415
|
+
});
|
|
3416
|
+
});
|
|
3417
|
+
var resolveInstalledPackageName = (packageNameOrPath) => {
|
|
3418
|
+
if (packageNameOrPath.startsWith(".") || packageNameOrPath.startsWith("/")) {
|
|
3419
|
+
return null;
|
|
3420
|
+
}
|
|
3421
|
+
if (packageNameOrPath.startsWith("@")) {
|
|
3422
|
+
return packageNameOrPath;
|
|
3423
|
+
}
|
|
3424
|
+
if (packageNameOrPath.includes("/")) {
|
|
3425
|
+
return packageNameOrPath.split("/").pop() ?? packageNameOrPath;
|
|
3426
|
+
}
|
|
3427
|
+
return packageNameOrPath;
|
|
3428
|
+
};
|
|
3429
|
+
var resolveSkillRoot = (workingDir, packageNameOrPath) => {
|
|
3430
|
+
if (packageNameOrPath.startsWith(".") || packageNameOrPath.startsWith("/")) {
|
|
3431
|
+
return resolve3(workingDir, packageNameOrPath);
|
|
3432
|
+
}
|
|
3433
|
+
const moduleName = resolveInstalledPackageName(packageNameOrPath) ?? packageNameOrPath;
|
|
3434
|
+
try {
|
|
3435
|
+
const packageJsonPath = require2.resolve(`${moduleName}/package.json`, {
|
|
3436
|
+
paths: [workingDir]
|
|
3437
|
+
});
|
|
3438
|
+
return resolve3(packageJsonPath, "..");
|
|
3439
|
+
} catch {
|
|
3440
|
+
const candidate = resolve3(workingDir, "node_modules", moduleName);
|
|
3441
|
+
if (existsSync(candidate)) {
|
|
3442
|
+
return candidate;
|
|
3443
|
+
}
|
|
3444
|
+
throw new Error(
|
|
3445
|
+
`Could not locate installed package "${moduleName}" in ${workingDir}`
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3448
|
+
};
|
|
3449
|
+
var findSkillManifest = async (dir, depth = 2) => {
|
|
3450
|
+
try {
|
|
3451
|
+
await access2(resolve3(dir, "SKILL.md"));
|
|
3452
|
+
return true;
|
|
3453
|
+
} catch {
|
|
3454
|
+
}
|
|
3455
|
+
if (depth <= 0) return false;
|
|
3456
|
+
try {
|
|
3457
|
+
const { readdir } = await import("fs/promises");
|
|
3458
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
3459
|
+
for (const entry of entries) {
|
|
3460
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
3461
|
+
const found = await findSkillManifest(resolve3(dir, entry.name), depth - 1);
|
|
3462
|
+
if (found) return true;
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
} catch {
|
|
3466
|
+
}
|
|
3467
|
+
return false;
|
|
3468
|
+
};
|
|
3469
|
+
var validateSkillPackage = async (workingDir, packageNameOrPath) => {
|
|
3470
|
+
const skillRoot = resolveSkillRoot(workingDir, packageNameOrPath);
|
|
3471
|
+
const hasSkill = await findSkillManifest(skillRoot);
|
|
3472
|
+
if (!hasSkill) {
|
|
3473
|
+
throw new Error(`Skill validation failed: no SKILL.md found in ${skillRoot}`);
|
|
3474
|
+
}
|
|
3475
|
+
};
|
|
3476
|
+
var addSkill = async (workingDir, packageNameOrPath) => {
|
|
3477
|
+
await runInstallCommand(workingDir, packageNameOrPath);
|
|
3478
|
+
await validateSkillPackage(workingDir, packageNameOrPath);
|
|
3479
|
+
process.stdout.write(`Added skill: ${packageNameOrPath}
|
|
3480
|
+
`);
|
|
3481
|
+
};
|
|
3482
|
+
var runTests = async (workingDir, filePath) => {
|
|
3483
|
+
dotenv.config({ path: resolve3(workingDir, ".env") });
|
|
3484
|
+
const testFilePath = filePath ?? resolve3(workingDir, "tests", "basic.yaml");
|
|
3485
|
+
const content = await readFile3(testFilePath, "utf8");
|
|
3486
|
+
const parsed = YAML.parse(content);
|
|
3487
|
+
const tests = parsed.tests ?? [];
|
|
3488
|
+
const harness = new AgentHarness({ workingDir });
|
|
3489
|
+
await harness.initialize();
|
|
3490
|
+
let passed = 0;
|
|
3491
|
+
let failed = 0;
|
|
3492
|
+
for (const testCase of tests) {
|
|
3493
|
+
try {
|
|
3494
|
+
const output = await harness.runToCompletion({ task: testCase.task });
|
|
3495
|
+
const response = output.result.response ?? "";
|
|
3496
|
+
const events = output.events;
|
|
3497
|
+
const expectation = testCase.expect ?? {};
|
|
3498
|
+
const checks = [];
|
|
3499
|
+
if (expectation.contains) {
|
|
3500
|
+
checks.push(response.includes(expectation.contains));
|
|
3501
|
+
}
|
|
3502
|
+
if (typeof expectation.maxSteps === "number") {
|
|
3503
|
+
checks.push(output.result.steps <= expectation.maxSteps);
|
|
3504
|
+
}
|
|
3505
|
+
if (typeof expectation.maxTokens === "number") {
|
|
3506
|
+
checks.push(
|
|
3507
|
+
output.result.tokens.input + output.result.tokens.output <= expectation.maxTokens
|
|
3508
|
+
);
|
|
3509
|
+
}
|
|
3510
|
+
if (expectation.refusal) {
|
|
3511
|
+
checks.push(
|
|
3512
|
+
response.toLowerCase().includes("can't") || response.toLowerCase().includes("cannot")
|
|
3513
|
+
);
|
|
3514
|
+
}
|
|
3515
|
+
if (expectation.toolCalled) {
|
|
3516
|
+
checks.push(
|
|
3517
|
+
events.some(
|
|
3518
|
+
(event) => event.type === "tool:started" && event.tool === expectation.toolCalled
|
|
3519
|
+
)
|
|
3520
|
+
);
|
|
3521
|
+
}
|
|
3522
|
+
const ok = checks.length === 0 ? output.result.status === "completed" : checks.every(Boolean);
|
|
3523
|
+
if (ok) {
|
|
3524
|
+
passed += 1;
|
|
3525
|
+
process.stdout.write(`PASS ${testCase.name}
|
|
3526
|
+
`);
|
|
3527
|
+
} else {
|
|
3528
|
+
failed += 1;
|
|
3529
|
+
process.stdout.write(`FAIL ${testCase.name}
|
|
3530
|
+
`);
|
|
3531
|
+
}
|
|
3532
|
+
} catch (error) {
|
|
3533
|
+
failed += 1;
|
|
3534
|
+
process.stdout.write(
|
|
3535
|
+
`FAIL ${testCase.name} (${error instanceof Error ? error.message : "Unknown test error"})
|
|
3536
|
+
`
|
|
3537
|
+
);
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
process.stdout.write(`
|
|
3541
|
+
Test summary: ${passed} passed, ${failed} failed
|
|
3542
|
+
`);
|
|
3543
|
+
return { passed, failed };
|
|
3544
|
+
};
|
|
3545
|
+
var buildTarget = async (workingDir, target) => {
|
|
3546
|
+
const outDir = resolve3(workingDir, ".poncho-build", target);
|
|
3547
|
+
await mkdir3(outDir, { recursive: true });
|
|
3548
|
+
const serverEntrypoint = `import { startDevServer } from "@poncho-ai/cli";
|
|
3549
|
+
|
|
3550
|
+
const port = Number.parseInt(process.env.PORT ?? "3000", 10);
|
|
3551
|
+
await startDevServer(Number.isNaN(port) ? 3000 : port, { workingDir: process.cwd() });
|
|
3552
|
+
`;
|
|
3553
|
+
const runtimePackageJson = JSON.stringify(
|
|
3554
|
+
{
|
|
3555
|
+
name: "poncho-runtime-bundle",
|
|
3556
|
+
private: true,
|
|
3557
|
+
type: "module",
|
|
3558
|
+
scripts: {
|
|
3559
|
+
start: "node server.js"
|
|
3560
|
+
},
|
|
3561
|
+
dependencies: {
|
|
3562
|
+
"@poncho-ai/cli": "^0.1.0"
|
|
3563
|
+
}
|
|
3564
|
+
},
|
|
3565
|
+
null,
|
|
3566
|
+
2
|
|
3567
|
+
);
|
|
3568
|
+
if (target === "vercel") {
|
|
3569
|
+
await mkdir3(resolve3(outDir, "api"), { recursive: true });
|
|
3570
|
+
await copyIfExists(resolve3(workingDir, "AGENT.md"), resolve3(outDir, "AGENT.md"));
|
|
3571
|
+
await copyIfExists(
|
|
3572
|
+
resolve3(workingDir, "poncho.config.js"),
|
|
3573
|
+
resolve3(outDir, "poncho.config.js")
|
|
3574
|
+
);
|
|
3575
|
+
await copyIfExists(resolve3(workingDir, "skills"), resolve3(outDir, "skills"));
|
|
3576
|
+
await copyIfExists(resolve3(workingDir, "tests"), resolve3(outDir, "tests"));
|
|
3577
|
+
await writeFile3(
|
|
3578
|
+
resolve3(outDir, "vercel.json"),
|
|
3579
|
+
JSON.stringify(
|
|
3580
|
+
{
|
|
3581
|
+
version: 2,
|
|
3582
|
+
functions: {
|
|
3583
|
+
"api/index.js": {
|
|
3584
|
+
includeFiles: "{AGENT.md,poncho.config.js,skills/**,tests/**}"
|
|
3585
|
+
}
|
|
3586
|
+
},
|
|
3587
|
+
routes: [{ src: "/(.*)", dest: "/api/index.js" }]
|
|
3588
|
+
},
|
|
3589
|
+
null,
|
|
3590
|
+
2
|
|
3591
|
+
),
|
|
3592
|
+
"utf8"
|
|
3593
|
+
);
|
|
3594
|
+
await buildVercelHandlerBundle(outDir);
|
|
3595
|
+
await writeFile3(
|
|
3596
|
+
resolve3(outDir, "package.json"),
|
|
3597
|
+
JSON.stringify(
|
|
3598
|
+
{
|
|
3599
|
+
private: true,
|
|
3600
|
+
type: "module",
|
|
3601
|
+
engines: {
|
|
3602
|
+
node: "20.x"
|
|
3603
|
+
},
|
|
3604
|
+
dependencies: VERCEL_RUNTIME_DEPENDENCIES
|
|
3605
|
+
},
|
|
3606
|
+
null,
|
|
3607
|
+
2
|
|
3608
|
+
),
|
|
3609
|
+
"utf8"
|
|
3610
|
+
);
|
|
3611
|
+
} else if (target === "docker") {
|
|
3612
|
+
await writeFile3(
|
|
3613
|
+
resolve3(outDir, "Dockerfile"),
|
|
3614
|
+
`FROM node:20-slim
|
|
3615
|
+
WORKDIR /app
|
|
3616
|
+
COPY package.json package.json
|
|
3617
|
+
COPY AGENT.md AGENT.md
|
|
3618
|
+
COPY poncho.config.js poncho.config.js
|
|
3619
|
+
COPY skills skills
|
|
3620
|
+
COPY tests tests
|
|
3621
|
+
COPY .env.example .env.example
|
|
3622
|
+
RUN corepack enable && npm install -g @poncho-ai/cli
|
|
3623
|
+
COPY server.js server.js
|
|
3624
|
+
EXPOSE 3000
|
|
3625
|
+
CMD ["node","server.js"]
|
|
3626
|
+
`,
|
|
3627
|
+
"utf8"
|
|
3628
|
+
);
|
|
3629
|
+
await writeFile3(resolve3(outDir, "server.js"), serverEntrypoint, "utf8");
|
|
3630
|
+
await writeFile3(resolve3(outDir, "package.json"), runtimePackageJson, "utf8");
|
|
3631
|
+
} else if (target === "lambda") {
|
|
3632
|
+
await writeFile3(
|
|
3633
|
+
resolve3(outDir, "lambda-handler.js"),
|
|
3634
|
+
`import { startDevServer } from "@poncho-ai/cli";
|
|
3635
|
+
let serverPromise;
|
|
3636
|
+
export const handler = async (event = {}) => {
|
|
3637
|
+
if (!serverPromise) {
|
|
3638
|
+
serverPromise = startDevServer(0, { workingDir: process.cwd() });
|
|
3639
|
+
}
|
|
3640
|
+
const body = JSON.stringify({
|
|
3641
|
+
status: "ready",
|
|
3642
|
+
route: event.rawPath ?? event.path ?? "/",
|
|
3643
|
+
});
|
|
3644
|
+
return { statusCode: 200, headers: { "content-type": "application/json" }, body };
|
|
3645
|
+
};
|
|
3646
|
+
`,
|
|
3647
|
+
"utf8"
|
|
3648
|
+
);
|
|
3649
|
+
await writeFile3(resolve3(outDir, "package.json"), runtimePackageJson, "utf8");
|
|
3650
|
+
} else if (target === "fly") {
|
|
3651
|
+
await writeFile3(
|
|
3652
|
+
resolve3(outDir, "fly.toml"),
|
|
3653
|
+
`app = "poncho-app"
|
|
3654
|
+
[env]
|
|
3655
|
+
PORT = "3000"
|
|
3656
|
+
[http_service]
|
|
3657
|
+
internal_port = 3000
|
|
3658
|
+
force_https = true
|
|
3659
|
+
auto_start_machines = true
|
|
3660
|
+
auto_stop_machines = "stop"
|
|
3661
|
+
min_machines_running = 0
|
|
3662
|
+
`,
|
|
3663
|
+
"utf8"
|
|
3664
|
+
);
|
|
3665
|
+
await writeFile3(
|
|
3666
|
+
resolve3(outDir, "Dockerfile"),
|
|
3667
|
+
`FROM node:20-slim
|
|
3668
|
+
WORKDIR /app
|
|
3669
|
+
COPY package.json package.json
|
|
3670
|
+
COPY AGENT.md AGENT.md
|
|
3671
|
+
COPY poncho.config.js poncho.config.js
|
|
3672
|
+
COPY skills skills
|
|
3673
|
+
COPY tests tests
|
|
3674
|
+
RUN npm install -g @poncho-ai/cli
|
|
3675
|
+
COPY server.js server.js
|
|
3676
|
+
EXPOSE 3000
|
|
3677
|
+
CMD ["node","server.js"]
|
|
3678
|
+
`,
|
|
3679
|
+
"utf8"
|
|
3680
|
+
);
|
|
3681
|
+
await writeFile3(resolve3(outDir, "server.js"), serverEntrypoint, "utf8");
|
|
3682
|
+
await writeFile3(resolve3(outDir, "package.json"), runtimePackageJson, "utf8");
|
|
3683
|
+
} else {
|
|
3684
|
+
throw new Error(`Unsupported build target: ${target}`);
|
|
3685
|
+
}
|
|
3686
|
+
process.stdout.write(`Build artifacts generated at ${outDir}
|
|
3687
|
+
`);
|
|
3688
|
+
};
|
|
3689
|
+
var normalizeMcpName = (entry) => entry.name ?? entry.url ?? `mcp_${Date.now()}`;
|
|
3690
|
+
var mcpAdd = async (workingDir, options) => {
|
|
3691
|
+
const config = await loadPonchoConfig(workingDir) ?? { mcp: [] };
|
|
3692
|
+
const mcp = [...config.mcp ?? []];
|
|
3693
|
+
if (!options.url) {
|
|
3694
|
+
throw new Error("Remote MCP only: provide --url for a remote MCP server.");
|
|
3695
|
+
}
|
|
3696
|
+
if (options.url.startsWith("ws://") || options.url.startsWith("wss://")) {
|
|
3697
|
+
throw new Error("WebSocket MCP URLs are no longer supported. Use an HTTP MCP endpoint.");
|
|
3698
|
+
}
|
|
3699
|
+
if (!options.url.startsWith("http://") && !options.url.startsWith("https://")) {
|
|
3700
|
+
throw new Error("Invalid MCP URL. Expected http:// or https://.");
|
|
3701
|
+
}
|
|
3702
|
+
const serverName = options.name ?? normalizeMcpName({ url: options.url });
|
|
3703
|
+
mcp.push({
|
|
3704
|
+
name: serverName,
|
|
3705
|
+
url: options.url,
|
|
3706
|
+
env: options.envVars ?? [],
|
|
3707
|
+
auth: options.authBearerEnv ? {
|
|
3708
|
+
type: "bearer",
|
|
3709
|
+
tokenEnv: options.authBearerEnv
|
|
3710
|
+
} : void 0
|
|
3711
|
+
});
|
|
3712
|
+
await writeConfigFile(workingDir, { ...config, mcp });
|
|
3713
|
+
let envSeedMessage;
|
|
3714
|
+
if (options.authBearerEnv) {
|
|
3715
|
+
const envPath = resolve3(workingDir, ".env");
|
|
3716
|
+
const envExamplePath = resolve3(workingDir, ".env.example");
|
|
3717
|
+
const addedEnv = await ensureEnvPlaceholder(envPath, options.authBearerEnv);
|
|
3718
|
+
const addedEnvExample = await ensureEnvPlaceholder(envExamplePath, options.authBearerEnv);
|
|
3719
|
+
if (addedEnv || addedEnvExample) {
|
|
3720
|
+
envSeedMessage = `Added ${options.authBearerEnv}= to ${addedEnv ? ".env" : ""}${addedEnv && addedEnvExample ? " and " : ""}${addedEnvExample ? ".env.example" : ""}.`;
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
const nextSteps = [];
|
|
3724
|
+
let step = 1;
|
|
3725
|
+
if (options.authBearerEnv) {
|
|
3726
|
+
nextSteps.push(` ${step}) Set token in .env: ${options.authBearerEnv}=...`);
|
|
3727
|
+
step += 1;
|
|
3728
|
+
}
|
|
3729
|
+
nextSteps.push(` ${step}) Discover tools: poncho mcp tools list ${serverName}`);
|
|
3730
|
+
step += 1;
|
|
3731
|
+
nextSteps.push(` ${step}) Select tools: poncho mcp tools select ${serverName}`);
|
|
3732
|
+
step += 1;
|
|
3733
|
+
nextSteps.push(` ${step}) Verify config: poncho mcp list`);
|
|
3734
|
+
process.stdout.write(
|
|
3735
|
+
[
|
|
3736
|
+
`MCP server added: ${serverName}`,
|
|
3737
|
+
...envSeedMessage ? [envSeedMessage] : [],
|
|
3738
|
+
"Next steps:",
|
|
3739
|
+
...nextSteps,
|
|
3740
|
+
""
|
|
3741
|
+
].join("\n")
|
|
3742
|
+
);
|
|
3743
|
+
};
|
|
3744
|
+
var mcpList = async (workingDir) => {
|
|
3745
|
+
const config = await loadPonchoConfig(workingDir);
|
|
3746
|
+
const mcp = config?.mcp ?? [];
|
|
3747
|
+
if (mcp.length === 0) {
|
|
3748
|
+
process.stdout.write("No MCP servers configured.\n");
|
|
3749
|
+
if (config?.scripts) {
|
|
3750
|
+
process.stdout.write(
|
|
3751
|
+
`Script policy: mode=${config.scripts.mode ?? "all"} include=${config.scripts.include?.length ?? 0} exclude=${config.scripts.exclude?.length ?? 0}
|
|
3752
|
+
`
|
|
3753
|
+
);
|
|
3754
|
+
}
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
process.stdout.write("Configured MCP servers:\n");
|
|
3758
|
+
for (const entry of mcp) {
|
|
3759
|
+
const auth = entry.auth?.type === "bearer" ? `auth=bearer:${entry.auth.tokenEnv}` : "auth=none";
|
|
3760
|
+
const mode = entry.tools?.mode ?? "all";
|
|
3761
|
+
process.stdout.write(
|
|
3762
|
+
`- ${entry.name ?? entry.url} (remote: ${entry.url}, ${auth}, mode=${mode})
|
|
3763
|
+
`
|
|
3764
|
+
);
|
|
3765
|
+
}
|
|
3766
|
+
if (config?.scripts) {
|
|
3767
|
+
process.stdout.write(
|
|
3768
|
+
`Script policy: mode=${config.scripts.mode ?? "all"} include=${config.scripts.include?.length ?? 0} exclude=${config.scripts.exclude?.length ?? 0}
|
|
3769
|
+
`
|
|
3770
|
+
);
|
|
3771
|
+
}
|
|
3772
|
+
};
|
|
3773
|
+
var mcpRemove = async (workingDir, name) => {
|
|
3774
|
+
const config = await loadPonchoConfig(workingDir) ?? { mcp: [] };
|
|
3775
|
+
const before = config.mcp ?? [];
|
|
3776
|
+
const removed = before.filter((entry) => normalizeMcpName(entry) === name);
|
|
3777
|
+
const filtered = before.filter((entry) => normalizeMcpName(entry) !== name);
|
|
3778
|
+
await writeConfigFile(workingDir, { ...config, mcp: filtered });
|
|
3779
|
+
const removedTokenEnvNames = new Set(
|
|
3780
|
+
removed.map(
|
|
3781
|
+
(entry) => entry.auth?.type === "bearer" ? entry.auth.tokenEnv?.trim() ?? "" : ""
|
|
3782
|
+
).filter((value) => value.length > 0)
|
|
3783
|
+
);
|
|
3784
|
+
const stillUsedTokenEnvNames = new Set(
|
|
3785
|
+
filtered.map(
|
|
3786
|
+
(entry) => entry.auth?.type === "bearer" ? entry.auth.tokenEnv?.trim() ?? "" : ""
|
|
3787
|
+
).filter((value) => value.length > 0)
|
|
3788
|
+
);
|
|
3789
|
+
const removedFromExample = [];
|
|
3790
|
+
for (const tokenEnv of removedTokenEnvNames) {
|
|
3791
|
+
if (stillUsedTokenEnvNames.has(tokenEnv)) {
|
|
3792
|
+
continue;
|
|
3793
|
+
}
|
|
3794
|
+
const changed = await removeEnvPlaceholder(resolve3(workingDir, ".env.example"), tokenEnv);
|
|
3795
|
+
if (changed) {
|
|
3796
|
+
removedFromExample.push(tokenEnv);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
process.stdout.write(`Removed MCP server: ${name}
|
|
3800
|
+
`);
|
|
3801
|
+
if (removedFromExample.length > 0) {
|
|
3802
|
+
process.stdout.write(
|
|
3803
|
+
`Removed unused token placeholder(s) from .env.example: ${removedFromExample.join(", ")}
|
|
3804
|
+
`
|
|
3805
|
+
);
|
|
3806
|
+
}
|
|
3807
|
+
};
|
|
3808
|
+
var resolveMcpEntry = async (workingDir, serverName) => {
|
|
3809
|
+
const config = await loadPonchoConfig(workingDir) ?? { mcp: [] };
|
|
3810
|
+
const entries = config.mcp ?? [];
|
|
3811
|
+
const index = entries.findIndex((entry) => normalizeMcpName(entry) === serverName);
|
|
3812
|
+
if (index < 0) {
|
|
3813
|
+
throw new Error(`MCP server "${serverName}" is not configured.`);
|
|
3814
|
+
}
|
|
3815
|
+
return { config, index };
|
|
3816
|
+
};
|
|
3817
|
+
var discoverMcpTools = async (workingDir, serverName) => {
|
|
3818
|
+
dotenv.config({ path: resolve3(workingDir, ".env") });
|
|
3819
|
+
const { config, index } = await resolveMcpEntry(workingDir, serverName);
|
|
3820
|
+
const entry = (config.mcp ?? [])[index];
|
|
3821
|
+
const bridge = new LocalMcpBridge({ mcp: [entry] });
|
|
3822
|
+
try {
|
|
3823
|
+
await bridge.startLocalServers();
|
|
3824
|
+
await bridge.discoverTools();
|
|
3825
|
+
return bridge.listDiscoveredTools(normalizeMcpName(entry));
|
|
3826
|
+
} finally {
|
|
3827
|
+
await bridge.stopLocalServers();
|
|
3828
|
+
}
|
|
3829
|
+
};
|
|
3830
|
+
var mcpToolsList = async (workingDir, serverName) => {
|
|
3831
|
+
const discovered = await discoverMcpTools(workingDir, serverName);
|
|
3832
|
+
if (discovered.length === 0) {
|
|
3833
|
+
process.stdout.write(`No tools discovered for MCP server "${serverName}".
|
|
3834
|
+
`);
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
process.stdout.write(`Discovered tools for "${serverName}":
|
|
3838
|
+
`);
|
|
3839
|
+
for (const tool of discovered) {
|
|
3840
|
+
process.stdout.write(`- ${tool}
|
|
3841
|
+
`);
|
|
3842
|
+
}
|
|
3843
|
+
};
|
|
3844
|
+
var mcpToolsSelect = async (workingDir, serverName, options) => {
|
|
3845
|
+
const discovered = await discoverMcpTools(workingDir, serverName);
|
|
3846
|
+
if (discovered.length === 0) {
|
|
3847
|
+
process.stdout.write(`No tools discovered for MCP server "${serverName}".
|
|
3848
|
+
`);
|
|
3849
|
+
return;
|
|
3850
|
+
}
|
|
3851
|
+
let selected = [];
|
|
3852
|
+
if (options.all) {
|
|
3853
|
+
selected = [...discovered];
|
|
3854
|
+
} else if (options.toolsCsv && options.toolsCsv.trim().length > 0) {
|
|
3855
|
+
const requested = options.toolsCsv.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
3856
|
+
selected = discovered.filter((tool) => requested.includes(tool));
|
|
3857
|
+
} else {
|
|
3858
|
+
process.stdout.write(`Discovered tools for "${serverName}":
|
|
3859
|
+
`);
|
|
3860
|
+
discovered.forEach((tool, idx) => {
|
|
3861
|
+
process.stdout.write(`${idx + 1}. ${tool}
|
|
3862
|
+
`);
|
|
3863
|
+
});
|
|
3864
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3865
|
+
const answer = await rl.question(
|
|
3866
|
+
"Enter comma-separated tool numbers/names to allow (or * for all): "
|
|
3867
|
+
);
|
|
3868
|
+
rl.close();
|
|
3869
|
+
const raw = answer.trim();
|
|
3870
|
+
if (raw === "*") {
|
|
3871
|
+
selected = [...discovered];
|
|
3872
|
+
} else {
|
|
3873
|
+
const tokens = raw.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
3874
|
+
const fromIndex = tokens.map((token) => Number.parseInt(token, 10)).filter((value) => !Number.isNaN(value)).map((index2) => discovered[index2 - 1]).filter((value) => typeof value === "string");
|
|
3875
|
+
const byName = discovered.filter((tool) => tokens.includes(tool));
|
|
3876
|
+
selected = [.../* @__PURE__ */ new Set([...fromIndex, ...byName])];
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
if (selected.length === 0) {
|
|
3880
|
+
throw new Error("No valid tools selected.");
|
|
3881
|
+
}
|
|
3882
|
+
const includePatterns = selected.length === discovered.length ? [`${serverName}/*`] : selected.sort();
|
|
3883
|
+
const { config, index } = await resolveMcpEntry(workingDir, serverName);
|
|
3884
|
+
const mcp = [...config.mcp ?? []];
|
|
3885
|
+
const existing = mcp[index];
|
|
3886
|
+
mcp[index] = {
|
|
3887
|
+
...existing,
|
|
3888
|
+
tools: {
|
|
3889
|
+
...existing.tools ?? {},
|
|
3890
|
+
mode: "allowlist",
|
|
3891
|
+
include: includePatterns
|
|
3892
|
+
}
|
|
3893
|
+
};
|
|
3894
|
+
await writeConfigFile(workingDir, { ...config, mcp });
|
|
3895
|
+
process.stdout.write(
|
|
3896
|
+
`Updated ${serverName} to allowlist ${includePatterns.join(", ")} in poncho.config.js.
|
|
3897
|
+
`
|
|
3898
|
+
);
|
|
3899
|
+
process.stdout.write(
|
|
3900
|
+
"\nRequired next step: add MCP intent in AGENT.md or SKILL.md. Without this, these MCP tools will not be registered for the model.\n"
|
|
3901
|
+
);
|
|
3902
|
+
process.stdout.write(
|
|
3903
|
+
"\nOption A: AGENT.md (global fallback intent)\nPaste this into AGENT.md frontmatter:\n---\ntools:\n mcp:\n" + includePatterns.map((tool) => ` - ${tool}`).join("\n") + "\n---\n"
|
|
3904
|
+
);
|
|
3905
|
+
process.stdout.write(
|
|
3906
|
+
"\nOption B: SKILL.md (only when that skill is activated)\nPaste this into SKILL.md frontmatter:\n---\ntools:\n mcp:\n" + includePatterns.map((tool) => ` - ${tool}`).join("\n") + "\n---\n"
|
|
3907
|
+
);
|
|
3908
|
+
};
|
|
3909
|
+
var buildCli = () => {
|
|
3910
|
+
const program = new Command();
|
|
3911
|
+
program.name("poncho").description("CLI for building and running Poncho agents").version("0.1.0");
|
|
3912
|
+
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) => {
|
|
3913
|
+
await initProject(name, {
|
|
3914
|
+
onboarding: {
|
|
3915
|
+
yes: options.yes,
|
|
3916
|
+
interactive: !options.yes && process.stdin.isTTY === true && process.stdout.isTTY === true
|
|
3917
|
+
}
|
|
3918
|
+
});
|
|
3919
|
+
});
|
|
3920
|
+
program.command("dev").description("Run local development server").option("--port <port>", "server port", "3000").action(async (options) => {
|
|
3921
|
+
const port = Number.parseInt(options.port, 10);
|
|
3922
|
+
await startDevServer(Number.isNaN(port) ? 3e3 : port);
|
|
3923
|
+
});
|
|
3924
|
+
program.command("run").argument("[task]", "task to run").description("Execute the agent once").option("--param <keyValue>", "parameter key=value", (value, all) => {
|
|
3925
|
+
all.push(value);
|
|
3926
|
+
return all;
|
|
3927
|
+
}, []).option("--file <path>", "include file contents", (value, all) => {
|
|
3928
|
+
all.push(value);
|
|
3929
|
+
return all;
|
|
3930
|
+
}, []).option("--json", "output json", false).option("--interactive", "run in interactive mode", false).action(
|
|
3931
|
+
async (task, options) => {
|
|
3932
|
+
const params = parseParams(options.param);
|
|
3933
|
+
if (options.interactive) {
|
|
3934
|
+
await runInteractive(process.cwd(), params);
|
|
3935
|
+
return;
|
|
3936
|
+
}
|
|
3937
|
+
if (!task) {
|
|
3938
|
+
throw new Error("Task is required unless --interactive is used.");
|
|
3939
|
+
}
|
|
3940
|
+
await runOnce(task, {
|
|
3941
|
+
params,
|
|
3942
|
+
json: options.json,
|
|
3943
|
+
filePaths: options.file
|
|
3944
|
+
});
|
|
3945
|
+
}
|
|
3946
|
+
);
|
|
3947
|
+
program.command("tools").description("List all tools available to the agent").action(async () => {
|
|
3948
|
+
await listTools(process.cwd());
|
|
3949
|
+
});
|
|
3950
|
+
program.command("add").argument("<packageOrPath>", "skill package name/path").description("Add a skill package and validate SKILL.md").action(async (packageOrPath) => {
|
|
3951
|
+
await addSkill(process.cwd(), packageOrPath);
|
|
3952
|
+
});
|
|
3953
|
+
program.command("update-agent").description("Remove deprecated embedded local guidance from AGENT.md").action(async () => {
|
|
3954
|
+
await updateAgentGuidance(process.cwd());
|
|
3955
|
+
});
|
|
3956
|
+
program.command("test").argument("[file]", "test file path (yaml)").description("Run yaml-defined agent tests").action(async (file) => {
|
|
3957
|
+
const testFile = file ? resolve3(process.cwd(), file) : void 0;
|
|
3958
|
+
const result = await runTests(process.cwd(), testFile);
|
|
3959
|
+
if (result.failed > 0) {
|
|
3960
|
+
process.exitCode = 1;
|
|
3961
|
+
}
|
|
3962
|
+
});
|
|
3963
|
+
program.command("build").argument("<target>", "vercel|docker|lambda|fly").description("Generate build artifacts for deployment target").action(async (target) => {
|
|
3964
|
+
await buildTarget(process.cwd(), target);
|
|
3965
|
+
});
|
|
3966
|
+
const mcpCommand = program.command("mcp").description("Manage MCP servers");
|
|
3967
|
+
mcpCommand.command("add").requiredOption("--url <url>", "remote MCP url").option("--name <name>", "server name").option(
|
|
3968
|
+
"--auth-bearer-env <name>",
|
|
3969
|
+
"env var name containing bearer token for this MCP server"
|
|
3970
|
+
).option("--env <name>", "env variable (repeatable)", (value, all) => {
|
|
3971
|
+
all.push(value);
|
|
3972
|
+
return all;
|
|
3973
|
+
}, []).action(
|
|
3974
|
+
async (options) => {
|
|
3975
|
+
await mcpAdd(process.cwd(), {
|
|
3976
|
+
url: options.url,
|
|
3977
|
+
name: options.name,
|
|
3978
|
+
envVars: options.env,
|
|
3979
|
+
authBearerEnv: options.authBearerEnv
|
|
3980
|
+
});
|
|
3981
|
+
}
|
|
3982
|
+
);
|
|
3983
|
+
mcpCommand.command("list").description("List configured MCP servers").action(async () => {
|
|
3984
|
+
await mcpList(process.cwd());
|
|
3985
|
+
});
|
|
3986
|
+
mcpCommand.command("remove").argument("<name>", "server name").description("Remove an MCP server by name").action(async (name) => {
|
|
3987
|
+
await mcpRemove(process.cwd(), name);
|
|
3988
|
+
});
|
|
3989
|
+
const mcpToolsCommand = mcpCommand.command("tools").description("Discover and curate tools for a configured MCP server");
|
|
3990
|
+
mcpToolsCommand.command("list").argument("<name>", "server name").description("Discover and list tools from a configured MCP server").action(async (name) => {
|
|
3991
|
+
await mcpToolsList(process.cwd(), name);
|
|
3992
|
+
});
|
|
3993
|
+
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(
|
|
3994
|
+
async (name, options) => {
|
|
3995
|
+
await mcpToolsSelect(process.cwd(), name, {
|
|
3996
|
+
all: options.all,
|
|
3997
|
+
toolsCsv: options.tools
|
|
3998
|
+
});
|
|
3999
|
+
}
|
|
4000
|
+
);
|
|
4001
|
+
return program;
|
|
4002
|
+
};
|
|
4003
|
+
var main = async (argv = process.argv) => {
|
|
4004
|
+
try {
|
|
4005
|
+
await buildCli().parseAsync(argv);
|
|
4006
|
+
} catch (error) {
|
|
4007
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE") {
|
|
4008
|
+
const message = "Port is already in use. Try `poncho dev --port 3001` or stop the process using port 3000.";
|
|
4009
|
+
process.stderr.write(`${message}
|
|
4010
|
+
`);
|
|
4011
|
+
process.exitCode = 1;
|
|
4012
|
+
return;
|
|
4013
|
+
}
|
|
4014
|
+
process.stderr.write(`${error instanceof Error ? error.message : "Unknown CLI error"}
|
|
4015
|
+
`);
|
|
4016
|
+
process.exitCode = 1;
|
|
4017
|
+
}
|
|
4018
|
+
};
|
|
4019
|
+
var packageRoot = resolve3(__dirname, "..");
|
|
4020
|
+
|
|
4021
|
+
export {
|
|
4022
|
+
inferConversationTitle,
|
|
4023
|
+
consumeFirstRunIntro,
|
|
4024
|
+
resolveHarnessEnvironment,
|
|
4025
|
+
initProject,
|
|
4026
|
+
updateAgentGuidance,
|
|
4027
|
+
createRequestHandler,
|
|
4028
|
+
startDevServer,
|
|
4029
|
+
runOnce,
|
|
4030
|
+
runInteractive,
|
|
4031
|
+
listTools,
|
|
4032
|
+
addSkill,
|
|
4033
|
+
runTests,
|
|
4034
|
+
buildTarget,
|
|
4035
|
+
mcpAdd,
|
|
4036
|
+
mcpList,
|
|
4037
|
+
mcpRemove,
|
|
4038
|
+
mcpToolsList,
|
|
4039
|
+
mcpToolsSelect,
|
|
4040
|
+
buildCli,
|
|
4041
|
+
main,
|
|
4042
|
+
packageRoot
|
|
4043
|
+
};
|