@poncho-ai/cli 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +23 -0
- package/dist/{chunk-MGR2GJMB.js → chunk-6GFL3VYM.js} +586 -50
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-X7VHWGLT.js → run-interactive-ink-QR3RIAJH.js} +83 -6
- package/package.json +5 -3
- package/src/index.ts +215 -32
- package/src/run-interactive-ink.ts +84 -5
- package/src/web-ui.ts +374 -17
- package/test/cli.test.ts +5 -0
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -2,10 +2,12 @@ import {
|
|
|
2
2
|
consumeFirstRunIntro,
|
|
3
3
|
inferConversationTitle,
|
|
4
4
|
resolveHarnessEnvironment
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-6GFL3VYM.js";
|
|
6
6
|
|
|
7
7
|
// src/run-interactive-ink.ts
|
|
8
8
|
import * as readline from "readline";
|
|
9
|
+
import { readFile } from "fs/promises";
|
|
10
|
+
import { resolve, basename } from "path";
|
|
9
11
|
import { stdout } from "process";
|
|
10
12
|
import {
|
|
11
13
|
parseAgentFile
|
|
@@ -1496,7 +1498,7 @@ var loadMetadata = async (workingDir) => {
|
|
|
1496
1498
|
var ask = (rl, prompt) => new Promise((res) => {
|
|
1497
1499
|
rl.question(prompt, (answer) => res(answer));
|
|
1498
1500
|
});
|
|
1499
|
-
var sleep = (ms) => new Promise((
|
|
1501
|
+
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1500
1502
|
var streamTextAsTokens = async (text) => {
|
|
1501
1503
|
const tokens = text.match(/\S+\s*|\n/g) ?? [text];
|
|
1502
1504
|
for (const token of tokens) {
|
|
@@ -1506,6 +1508,41 @@ var streamTextAsTokens = async (text) => {
|
|
|
1506
1508
|
await sleep(delay);
|
|
1507
1509
|
}
|
|
1508
1510
|
};
|
|
1511
|
+
var EXT_MIME = {
|
|
1512
|
+
jpg: "image/jpeg",
|
|
1513
|
+
jpeg: "image/jpeg",
|
|
1514
|
+
png: "image/png",
|
|
1515
|
+
gif: "image/gif",
|
|
1516
|
+
webp: "image/webp",
|
|
1517
|
+
svg: "image/svg+xml",
|
|
1518
|
+
pdf: "application/pdf",
|
|
1519
|
+
mp4: "video/mp4",
|
|
1520
|
+
webm: "video/webm",
|
|
1521
|
+
mp3: "audio/mpeg",
|
|
1522
|
+
wav: "audio/wav",
|
|
1523
|
+
txt: "text/plain",
|
|
1524
|
+
json: "application/json",
|
|
1525
|
+
csv: "text/csv",
|
|
1526
|
+
html: "text/html"
|
|
1527
|
+
};
|
|
1528
|
+
var extToMime = (ext) => EXT_MIME[ext] ?? "application/octet-stream";
|
|
1529
|
+
var readPendingFiles = async (files) => {
|
|
1530
|
+
const results = [];
|
|
1531
|
+
for (const f of files) {
|
|
1532
|
+
try {
|
|
1533
|
+
const buf = await readFile(f.resolved);
|
|
1534
|
+
const ext = f.resolved.split(".").pop()?.toLowerCase() ?? "";
|
|
1535
|
+
results.push({
|
|
1536
|
+
data: buf.toString("base64"),
|
|
1537
|
+
mediaType: extToMime(ext),
|
|
1538
|
+
filename: basename(f.path)
|
|
1539
|
+
});
|
|
1540
|
+
} catch {
|
|
1541
|
+
console.log(`${C.yellow}warn: could not read ${f.path}, skipping${C.reset}`);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return results;
|
|
1545
|
+
};
|
|
1509
1546
|
var OWNER_ID = "local-owner";
|
|
1510
1547
|
var computeTurn = (messages) => Math.max(1, Math.floor(messages.length / 2) + 1);
|
|
1511
1548
|
var formatDate = (value) => {
|
|
@@ -1521,7 +1558,7 @@ var handleSlash = async (command, state, conversationStore) => {
|
|
|
1521
1558
|
if (norm === "/help") {
|
|
1522
1559
|
console.log(
|
|
1523
1560
|
gray(
|
|
1524
|
-
"commands> /help /clear /exit /tools /list /open <id> /new [title] /delete [id] /continue /reset [all]"
|
|
1561
|
+
"commands> /help /clear /exit /tools /attach <path> /files /list /open <id> /new [title] /delete [id] /continue /reset [all]"
|
|
1525
1562
|
)
|
|
1526
1563
|
);
|
|
1527
1564
|
return { shouldExit: false };
|
|
@@ -1645,6 +1682,33 @@ var handleSlash = async (command, state, conversationStore) => {
|
|
|
1645
1682
|
console.log(gray(`conversations> reset ${conversation.conversationId}`));
|
|
1646
1683
|
return { shouldExit: false };
|
|
1647
1684
|
}
|
|
1685
|
+
if (norm === "/attach") {
|
|
1686
|
+
const filePath = args.join(" ").trim();
|
|
1687
|
+
if (!filePath) {
|
|
1688
|
+
console.log(yellow("usage> /attach <path>"));
|
|
1689
|
+
return { shouldExit: false };
|
|
1690
|
+
}
|
|
1691
|
+
const resolvedPath = resolve(process.cwd(), filePath);
|
|
1692
|
+
try {
|
|
1693
|
+
await readFile(resolvedPath);
|
|
1694
|
+
state.pendingFiles.push({ path: filePath, resolved: resolvedPath });
|
|
1695
|
+
console.log(gray(`attached> ${filePath} [${state.pendingFiles.length} file(s) queued]`));
|
|
1696
|
+
} catch {
|
|
1697
|
+
console.log(yellow(`attach> file not found: ${filePath}`));
|
|
1698
|
+
}
|
|
1699
|
+
return { shouldExit: false };
|
|
1700
|
+
}
|
|
1701
|
+
if (norm === "/files") {
|
|
1702
|
+
if (state.pendingFiles.length === 0) {
|
|
1703
|
+
console.log(gray("files> none attached"));
|
|
1704
|
+
} else {
|
|
1705
|
+
console.log(gray("files>"));
|
|
1706
|
+
for (const f of state.pendingFiles) {
|
|
1707
|
+
console.log(gray(` ${f.path}`));
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
return { shouldExit: false };
|
|
1711
|
+
}
|
|
1648
1712
|
console.log(yellow(`Unknown command: ${command}`));
|
|
1649
1713
|
return { shouldExit: false };
|
|
1650
1714
|
};
|
|
@@ -1694,7 +1758,10 @@ ${C.yellow}approve? (y/n): ${C.reset}`,
|
|
|
1694
1758
|
console.log(gray(' Type "exit" to quit, "/help" for commands'));
|
|
1695
1759
|
console.log(gray(" Press Ctrl+C during a run to stop streaming output."));
|
|
1696
1760
|
console.log(
|
|
1697
|
-
gray(" Conversation
|
|
1761
|
+
gray(" Conversation: /list /open <id> /new [title] /delete [id] /continue /reset [all]")
|
|
1762
|
+
);
|
|
1763
|
+
console.log(
|
|
1764
|
+
gray(" Files: /attach <path> /files\n")
|
|
1698
1765
|
);
|
|
1699
1766
|
const intro = await consumeFirstRunIntro(workingDir, {
|
|
1700
1767
|
agentName: metadata.agentName,
|
|
@@ -1712,6 +1779,7 @@ ${C.yellow}approve? (y/n): ${C.reset}`,
|
|
|
1712
1779
|
let activeConversationId = null;
|
|
1713
1780
|
let showToolPayloads = false;
|
|
1714
1781
|
let activeRunAbortController = null;
|
|
1782
|
+
let pendingFiles = [];
|
|
1715
1783
|
rl.on("SIGINT", () => {
|
|
1716
1784
|
if (activeRunAbortController && !activeRunAbortController.signal.aborted) {
|
|
1717
1785
|
activeRunAbortController.abort();
|
|
@@ -1721,8 +1789,9 @@ ${C.yellow}approve? (y/n): ${C.reset}`,
|
|
|
1721
1789
|
}
|
|
1722
1790
|
rl.close();
|
|
1723
1791
|
});
|
|
1724
|
-
const prompt = `${C.cyan}you> ${C.reset}`;
|
|
1725
1792
|
while (true) {
|
|
1793
|
+
const filesTag = pendingFiles.length > 0 ? `${C.dim}[${pendingFiles.length} file(s)] ${C.reset}` : "";
|
|
1794
|
+
const prompt = `${filesTag}${C.cyan}you> ${C.reset}`;
|
|
1726
1795
|
let task;
|
|
1727
1796
|
try {
|
|
1728
1797
|
task = await ask(rl, prompt);
|
|
@@ -1742,7 +1811,8 @@ ${C.yellow}approve? (y/n): ${C.reset}`,
|
|
|
1742
1811
|
const interactiveState = {
|
|
1743
1812
|
messages,
|
|
1744
1813
|
turn,
|
|
1745
|
-
activeConversationId
|
|
1814
|
+
activeConversationId,
|
|
1815
|
+
pendingFiles
|
|
1746
1816
|
};
|
|
1747
1817
|
const slashResult = await handleSlash(
|
|
1748
1818
|
trimmed,
|
|
@@ -1755,6 +1825,7 @@ ${C.yellow}approve? (y/n): ${C.reset}`,
|
|
|
1755
1825
|
messages = interactiveState.messages;
|
|
1756
1826
|
turn = interactiveState.turn;
|
|
1757
1827
|
activeConversationId = interactiveState.activeConversationId;
|
|
1828
|
+
pendingFiles = interactiveState.pendingFiles;
|
|
1758
1829
|
continue;
|
|
1759
1830
|
}
|
|
1760
1831
|
console.log(gray(`
|
|
@@ -1782,11 +1853,17 @@ ${C.yellow}approve? (y/n): ${C.reset}`,
|
|
|
1782
1853
|
let latestRunId = "";
|
|
1783
1854
|
const startedAt = Date.now();
|
|
1784
1855
|
activeRunAbortController = new AbortController();
|
|
1856
|
+
const turnFiles = pendingFiles.length > 0 ? await readPendingFiles(pendingFiles) : [];
|
|
1857
|
+
if (pendingFiles.length > 0) {
|
|
1858
|
+
console.log(gray(` sending ${turnFiles.length} file(s)`));
|
|
1859
|
+
pendingFiles = [];
|
|
1860
|
+
}
|
|
1785
1861
|
try {
|
|
1786
1862
|
for await (const event of harness.run({
|
|
1787
1863
|
task: trimmed,
|
|
1788
1864
|
parameters: params,
|
|
1789
1865
|
messages,
|
|
1866
|
+
files: turnFiles.length > 0 ? turnFiles : void 0,
|
|
1790
1867
|
abortSignal: activeRunAbortController.signal
|
|
1791
1868
|
})) {
|
|
1792
1869
|
if (event.type === "run:started") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poncho-ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "CLI for building and deploying AI agents",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@inquirer/prompts": "^8.2.0",
|
|
21
|
+
"busboy": "^1.6.0",
|
|
21
22
|
"commander": "^12.0.0",
|
|
22
23
|
"dotenv": "^16.4.0",
|
|
23
24
|
"ink": "^6.7.0",
|
|
@@ -25,10 +26,11 @@
|
|
|
25
26
|
"react": "^19.2.4",
|
|
26
27
|
"react-devtools-core": "^6.1.5",
|
|
27
28
|
"yaml": "^2.8.1",
|
|
28
|
-
"@poncho-ai/harness": "0.
|
|
29
|
-
"@poncho-ai/sdk": "0.
|
|
29
|
+
"@poncho-ai/harness": "0.12.0",
|
|
30
|
+
"@poncho-ai/sdk": "1.0.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
33
|
+
"@types/busboy": "^1.5.4",
|
|
32
34
|
"@types/react": "^19.2.14",
|
|
33
35
|
"tsup": "^8.0.0",
|
|
34
36
|
"vitest": "^1.4.0"
|
package/src/index.ts
CHANGED
|
@@ -15,14 +15,19 @@ import {
|
|
|
15
15
|
LocalMcpBridge,
|
|
16
16
|
TelemetryEmitter,
|
|
17
17
|
createConversationStore,
|
|
18
|
+
createUploadStore,
|
|
19
|
+
deriveUploadKey,
|
|
18
20
|
ensureAgentIdentity,
|
|
19
21
|
generateAgentId,
|
|
20
22
|
loadPonchoConfig,
|
|
21
23
|
resolveStateConfig,
|
|
22
24
|
type PonchoConfig,
|
|
23
25
|
type ConversationStore,
|
|
26
|
+
type UploadStore,
|
|
24
27
|
} from "@poncho-ai/harness";
|
|
25
|
-
import type { AgentEvent, Message, RunInput } from "@poncho-ai/sdk";
|
|
28
|
+
import type { AgentEvent, FileInput, Message, RunInput } from "@poncho-ai/sdk";
|
|
29
|
+
import { getTextContent } from "@poncho-ai/sdk";
|
|
30
|
+
import Busboy from "busboy";
|
|
26
31
|
import { Command } from "commander";
|
|
27
32
|
import dotenv from "dotenv";
|
|
28
33
|
import YAML from "yaml";
|
|
@@ -63,6 +68,15 @@ const writeHtml = (response: ServerResponse, statusCode: number, payload: string
|
|
|
63
68
|
response.end(payload);
|
|
64
69
|
};
|
|
65
70
|
|
|
71
|
+
const EXT_MIME_MAP: Record<string, string> = {
|
|
72
|
+
jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png",
|
|
73
|
+
gif: "image/gif", webp: "image/webp", svg: "image/svg+xml",
|
|
74
|
+
pdf: "application/pdf", mp4: "video/mp4", webm: "video/webm",
|
|
75
|
+
mp3: "audio/mpeg", wav: "audio/wav", txt: "text/plain",
|
|
76
|
+
json: "application/json", csv: "text/csv", html: "text/html",
|
|
77
|
+
};
|
|
78
|
+
const extToMime = (ext: string): string => EXT_MIME_MAP[ext] ?? "application/octet-stream";
|
|
79
|
+
|
|
66
80
|
const readRequestBody = async (request: IncomingMessage): Promise<unknown> => {
|
|
67
81
|
const chunks: Buffer[] = [];
|
|
68
82
|
for await (const chunk of request) {
|
|
@@ -72,6 +86,49 @@ const readRequestBody = async (request: IncomingMessage): Promise<unknown> => {
|
|
|
72
86
|
return body.length > 0 ? (JSON.parse(body) as unknown) : {};
|
|
73
87
|
};
|
|
74
88
|
|
|
89
|
+
const MAX_UPLOAD_SIZE = 25 * 1024 * 1024; // 25MB per file
|
|
90
|
+
|
|
91
|
+
interface ParsedMultipart {
|
|
92
|
+
message: string;
|
|
93
|
+
parameters?: Record<string, unknown>;
|
|
94
|
+
files: FileInput[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const parseMultipartRequest = (request: IncomingMessage): Promise<ParsedMultipart> =>
|
|
98
|
+
new Promise((resolve, reject) => {
|
|
99
|
+
const result: ParsedMultipart = { message: "", files: [] };
|
|
100
|
+
const bb = Busboy({
|
|
101
|
+
headers: request.headers,
|
|
102
|
+
limits: { fileSize: MAX_UPLOAD_SIZE },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
bb.on("field", (name: string, value: string) => {
|
|
106
|
+
if (name === "message") result.message = value;
|
|
107
|
+
if (name === "parameters") {
|
|
108
|
+
try {
|
|
109
|
+
result.parameters = JSON.parse(value) as Record<string, unknown>;
|
|
110
|
+
} catch { /* ignore malformed parameters */ }
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
bb.on("file", (_name: string, stream: NodeJS.ReadableStream, info: { filename: string; mimeType: string }) => {
|
|
115
|
+
const chunks: Buffer[] = [];
|
|
116
|
+
stream.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
117
|
+
stream.on("end", () => {
|
|
118
|
+
const buf = Buffer.concat(chunks);
|
|
119
|
+
result.files.push({
|
|
120
|
+
data: buf.toString("base64"),
|
|
121
|
+
mediaType: info.mimeType,
|
|
122
|
+
filename: info.filename,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
bb.on("finish", () => resolve(result));
|
|
128
|
+
bb.on("error", (err: Error) => reject(err));
|
|
129
|
+
request.pipe(bb);
|
|
130
|
+
});
|
|
131
|
+
|
|
75
132
|
/**
|
|
76
133
|
* Detects the runtime environment from platform-specific or standard environment variables.
|
|
77
134
|
* Priority: PONCHO_ENV > platform detection (Vercel, Railway, etc.) > NODE_ENV > "development"
|
|
@@ -253,9 +310,9 @@ const resolveLocalPackagesRoot = (): string | null => {
|
|
|
253
310
|
* In dev mode we use `file:` paths so pnpm can resolve local packages;
|
|
254
311
|
* in production we point at the npm registry.
|
|
255
312
|
*/
|
|
256
|
-
const resolveCoreDeps = (
|
|
313
|
+
const resolveCoreDeps = async (
|
|
257
314
|
projectDir: string,
|
|
258
|
-
): { harness: string; sdk: string } => {
|
|
315
|
+
): Promise<{ harness: string; sdk: string }> => {
|
|
259
316
|
const packagesRoot = resolveLocalPackagesRoot();
|
|
260
317
|
if (packagesRoot) {
|
|
261
318
|
const harnessAbs = resolve(packagesRoot, "harness");
|
|
@@ -265,11 +322,14 @@ const resolveCoreDeps = (
|
|
|
265
322
|
sdk: `link:${relative(projectDir, sdkAbs)}`,
|
|
266
323
|
};
|
|
267
324
|
}
|
|
268
|
-
return {
|
|
325
|
+
return {
|
|
326
|
+
harness: await readCliDependencyVersion("@poncho-ai/harness", "^0.6.0"),
|
|
327
|
+
sdk: await readCliDependencyVersion("@poncho-ai/sdk", "^0.6.0"),
|
|
328
|
+
};
|
|
269
329
|
};
|
|
270
330
|
|
|
271
|
-
const PACKAGE_TEMPLATE = (name: string, projectDir: string): string => {
|
|
272
|
-
const deps = resolveCoreDeps(projectDir);
|
|
331
|
+
const PACKAGE_TEMPLATE = async (name: string, projectDir: string): Promise<string> => {
|
|
332
|
+
const deps = await resolveCoreDeps(projectDir);
|
|
273
333
|
return JSON.stringify(
|
|
274
334
|
{
|
|
275
335
|
name,
|
|
@@ -686,10 +746,19 @@ const writeScaffoldFile = async (
|
|
|
686
746
|
options.writtenPaths.push(relative(options.baseDir, filePath));
|
|
687
747
|
};
|
|
688
748
|
|
|
749
|
+
const UPLOAD_PROVIDER_DEPS: Record<string, Array<{ name: string; fallback: string }>> = {
|
|
750
|
+
"vercel-blob": [{ name: "@vercel/blob", fallback: "^2.3.0" }],
|
|
751
|
+
s3: [
|
|
752
|
+
{ name: "@aws-sdk/client-s3", fallback: "^3.700.0" },
|
|
753
|
+
{ name: "@aws-sdk/s3-request-presigner", fallback: "^3.700.0" },
|
|
754
|
+
],
|
|
755
|
+
};
|
|
756
|
+
|
|
689
757
|
const ensureRuntimeCliDependency = async (
|
|
690
758
|
projectDir: string,
|
|
691
759
|
cliVersion: string,
|
|
692
|
-
|
|
760
|
+
config?: PonchoConfig,
|
|
761
|
+
): Promise<{ paths: string[]; addedDeps: string[] }> => {
|
|
693
762
|
const packageJsonPath = resolve(projectDir, "package.json");
|
|
694
763
|
const content = await readFile(packageJsonPath, "utf8");
|
|
695
764
|
const parsed = JSON.parse(content) as {
|
|
@@ -710,9 +779,21 @@ const ensureRuntimeCliDependency = async (
|
|
|
710
779
|
}
|
|
711
780
|
dependencies.marked = await readCliDependencyVersion("marked", "^17.0.2");
|
|
712
781
|
dependencies["@poncho-ai/cli"] = `^${cliVersion}`;
|
|
782
|
+
|
|
783
|
+
const addedDeps: string[] = [];
|
|
784
|
+
const uploadsProvider = config?.uploads?.provider;
|
|
785
|
+
if (uploadsProvider && UPLOAD_PROVIDER_DEPS[uploadsProvider]) {
|
|
786
|
+
for (const dep of UPLOAD_PROVIDER_DEPS[uploadsProvider]) {
|
|
787
|
+
if (!dependencies[dep.name]) {
|
|
788
|
+
dependencies[dep.name] = dep.fallback;
|
|
789
|
+
addedDeps.push(dep.name);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
713
794
|
parsed.dependencies = dependencies;
|
|
714
795
|
await writeFile(packageJsonPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
|
|
715
|
-
return [relative(projectDir, packageJsonPath)];
|
|
796
|
+
return { paths: [relative(projectDir, packageJsonPath)], addedDeps };
|
|
716
797
|
};
|
|
717
798
|
|
|
718
799
|
const scaffoldDeployTarget = async (
|
|
@@ -852,10 +933,16 @@ CMD ["node","server.js"]
|
|
|
852
933
|
});
|
|
853
934
|
}
|
|
854
935
|
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
936
|
+
const config = await loadPonchoConfig(projectDir);
|
|
937
|
+
const { paths: packagePaths, addedDeps } = await ensureRuntimeCliDependency(
|
|
938
|
+
projectDir,
|
|
939
|
+
cliVersion,
|
|
940
|
+
config,
|
|
941
|
+
);
|
|
942
|
+
const depNote = addedDeps.length > 0 ? ` (added ${addedDeps.join(", ")})` : "";
|
|
943
|
+
for (const p of packagePaths) {
|
|
944
|
+
if (!writtenPaths.includes(p)) {
|
|
945
|
+
writtenPaths.push(depNote ? `${p}${depNote}` : p);
|
|
859
946
|
}
|
|
860
947
|
}
|
|
861
948
|
|
|
@@ -960,7 +1047,7 @@ export const initProject = async (
|
|
|
960
1047
|
}),
|
|
961
1048
|
},
|
|
962
1049
|
{ path: "poncho.config.js", content: renderConfigFile(onboarding.config) },
|
|
963
|
-
{ path: "package.json", content: PACKAGE_TEMPLATE(projectName, projectDir) },
|
|
1050
|
+
{ path: "package.json", content: await PACKAGE_TEMPLATE(projectName, projectDir) },
|
|
964
1051
|
{ path: "README.md", content: README_TEMPLATE(projectName) },
|
|
965
1052
|
{ path: ".env.example", content: options?.envExampleOverride ?? onboarding.envExample ?? ENV_TEMPLATE },
|
|
966
1053
|
{ path: ".gitignore", content: GITIGNORE_TEMPLATE },
|
|
@@ -1158,9 +1245,11 @@ export const createRequestHandler = async (options?: {
|
|
|
1158
1245
|
}
|
|
1159
1246
|
await persistConversationPendingApprovals(conversationId);
|
|
1160
1247
|
};
|
|
1248
|
+
const uploadStore = await createUploadStore(config?.uploads, workingDir);
|
|
1161
1249
|
const harness = new AgentHarness({
|
|
1162
1250
|
workingDir,
|
|
1163
1251
|
environment: resolveHarnessEnvironment(),
|
|
1252
|
+
uploadStore,
|
|
1164
1253
|
approvalHandler: async (request) =>
|
|
1165
1254
|
new Promise<boolean>((resolveApproval) => {
|
|
1166
1255
|
const ownerIdForRun = runOwners.get(request.runId) ?? "local-owner";
|
|
@@ -1608,6 +1697,31 @@ export const createRequestHandler = async (options?: {
|
|
|
1608
1697
|
return;
|
|
1609
1698
|
}
|
|
1610
1699
|
|
|
1700
|
+
const uploadMatch = pathname.match(/^\/api\/uploads\/(.+)$/);
|
|
1701
|
+
if (uploadMatch && request.method === "GET") {
|
|
1702
|
+
const key = decodeURIComponent(uploadMatch[1] ?? "");
|
|
1703
|
+
try {
|
|
1704
|
+
const data = await uploadStore.get(key);
|
|
1705
|
+
const ext = key.split(".").pop() ?? "";
|
|
1706
|
+
const mimeMap: Record<string, string> = {
|
|
1707
|
+
jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png",
|
|
1708
|
+
gif: "image/gif", webp: "image/webp", svg: "image/svg+xml",
|
|
1709
|
+
pdf: "application/pdf", mp4: "video/mp4", webm: "video/webm",
|
|
1710
|
+
mp3: "audio/mpeg", wav: "audio/wav", txt: "text/plain",
|
|
1711
|
+
json: "application/json", csv: "text/csv", html: "text/html",
|
|
1712
|
+
};
|
|
1713
|
+
response.writeHead(200, {
|
|
1714
|
+
"Content-Type": mimeMap[ext] ?? "application/octet-stream",
|
|
1715
|
+
"Content-Length": data.length,
|
|
1716
|
+
"Cache-Control": "public, max-age=86400",
|
|
1717
|
+
});
|
|
1718
|
+
response.end(data);
|
|
1719
|
+
} catch {
|
|
1720
|
+
writeJson(response, 404, { code: "NOT_FOUND", message: "Upload not found" });
|
|
1721
|
+
}
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1611
1725
|
const conversationMessageMatch = pathname.match(/^\/api\/conversations\/([^/]+)\/messages$/);
|
|
1612
1726
|
if (conversationMessageMatch && request.method === "POST") {
|
|
1613
1727
|
const conversationId = decodeURIComponent(conversationMessageMatch[1] ?? "");
|
|
@@ -1619,11 +1733,31 @@ export const createRequestHandler = async (options?: {
|
|
|
1619
1733
|
});
|
|
1620
1734
|
return;
|
|
1621
1735
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
const
|
|
1736
|
+
let messageText = "";
|
|
1737
|
+
let bodyParameters: Record<string, unknown> | undefined;
|
|
1738
|
+
let files: FileInput[] = [];
|
|
1739
|
+
|
|
1740
|
+
const contentType = request.headers["content-type"] ?? "";
|
|
1741
|
+
if (contentType.includes("multipart/form-data")) {
|
|
1742
|
+
const parsed = await parseMultipartRequest(request);
|
|
1743
|
+
messageText = parsed.message.trim();
|
|
1744
|
+
bodyParameters = parsed.parameters;
|
|
1745
|
+
files = parsed.files;
|
|
1746
|
+
} else {
|
|
1747
|
+
const body = (await readRequestBody(request)) as {
|
|
1748
|
+
message?: string;
|
|
1749
|
+
parameters?: Record<string, unknown>;
|
|
1750
|
+
files?: Array<{ data?: string; mediaType?: string; filename?: string }>;
|
|
1751
|
+
};
|
|
1752
|
+
messageText = body.message?.trim() ?? "";
|
|
1753
|
+
bodyParameters = body.parameters;
|
|
1754
|
+
if (Array.isArray(body.files)) {
|
|
1755
|
+
files = body.files
|
|
1756
|
+
.filter((f): f is { data: string; mediaType: string; filename?: string } =>
|
|
1757
|
+
typeof f.data === "string" && typeof f.mediaType === "string",
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1627
1761
|
if (!messageText) {
|
|
1628
1762
|
writeJson(response, 400, {
|
|
1629
1763
|
code: "VALIDATION_ERROR",
|
|
@@ -1668,9 +1802,44 @@ export const createRequestHandler = async (options?: {
|
|
|
1668
1802
|
let currentText = "";
|
|
1669
1803
|
let currentTools: string[] = [];
|
|
1670
1804
|
let runCancelled = false;
|
|
1805
|
+
let userContent: Message["content"] = messageText;
|
|
1806
|
+
if (files.length > 0) {
|
|
1807
|
+
try {
|
|
1808
|
+
const uploadedParts = await Promise.all(
|
|
1809
|
+
files.map(async (f) => {
|
|
1810
|
+
const buf = Buffer.from(f.data, "base64");
|
|
1811
|
+
const key = deriveUploadKey(buf, f.mediaType);
|
|
1812
|
+
const ref = await uploadStore.put(key, buf, f.mediaType);
|
|
1813
|
+
return {
|
|
1814
|
+
type: "file" as const,
|
|
1815
|
+
data: ref,
|
|
1816
|
+
mediaType: f.mediaType,
|
|
1817
|
+
filename: f.filename,
|
|
1818
|
+
};
|
|
1819
|
+
}),
|
|
1820
|
+
);
|
|
1821
|
+
userContent = [
|
|
1822
|
+
{ type: "text" as const, text: messageText },
|
|
1823
|
+
...uploadedParts,
|
|
1824
|
+
];
|
|
1825
|
+
} catch (uploadErr) {
|
|
1826
|
+
const errMsg = uploadErr instanceof Error ? uploadErr.message : String(uploadErr);
|
|
1827
|
+
console.error("[poncho] File upload failed:", errMsg);
|
|
1828
|
+
const errorEvent: AgentEvent = {
|
|
1829
|
+
type: "run:error",
|
|
1830
|
+
runId: "",
|
|
1831
|
+
error: { code: "UPLOAD_ERROR", message: `File upload failed: ${errMsg}` },
|
|
1832
|
+
};
|
|
1833
|
+
broadcastEvent(conversationId, errorEvent);
|
|
1834
|
+
finishConversationStream(conversationId);
|
|
1835
|
+
activeConversationRuns.delete(conversationId);
|
|
1836
|
+
response.end();
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1671
1840
|
try {
|
|
1672
1841
|
// Persist the user turn immediately so refreshing mid-run keeps chat context.
|
|
1673
|
-
conversation.messages = [...historyMessages, { role: "user", content:
|
|
1842
|
+
conversation.messages = [...historyMessages, { role: "user", content: userContent }];
|
|
1674
1843
|
conversation.updatedAt = Date.now();
|
|
1675
1844
|
await conversationStore.update(conversation);
|
|
1676
1845
|
|
|
@@ -1694,7 +1863,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1694
1863
|
}
|
|
1695
1864
|
conversation.messages = [
|
|
1696
1865
|
...historyMessages,
|
|
1697
|
-
{ role: "user", content:
|
|
1866
|
+
{ role: "user", content: userContent },
|
|
1698
1867
|
{
|
|
1699
1868
|
role: "assistant",
|
|
1700
1869
|
content: assistantResponse,
|
|
@@ -1720,7 +1889,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1720
1889
|
updatedAt: item.updatedAt,
|
|
1721
1890
|
content: item.messages
|
|
1722
1891
|
.slice(-6)
|
|
1723
|
-
.map((message) => `${message.role}: ${message.content}`)
|
|
1892
|
+
.map((message) => `${message.role}: ${typeof message.content === "string" ? message.content : getTextContent(message)}`)
|
|
1724
1893
|
.join("\n")
|
|
1725
1894
|
.slice(0, 2000),
|
|
1726
1895
|
}))
|
|
@@ -1729,11 +1898,12 @@ export const createRequestHandler = async (options?: {
|
|
|
1729
1898
|
for await (const event of harness.runWithTelemetry({
|
|
1730
1899
|
task: messageText,
|
|
1731
1900
|
parameters: {
|
|
1732
|
-
...(
|
|
1901
|
+
...(bodyParameters ?? {}),
|
|
1733
1902
|
__conversationRecallCorpus: recallCorpus,
|
|
1734
1903
|
__activeConversationId: conversationId,
|
|
1735
1904
|
},
|
|
1736
1905
|
messages: historyMessages,
|
|
1906
|
+
files: files.length > 0 ? files : undefined,
|
|
1737
1907
|
abortSignal: abortController.signal,
|
|
1738
1908
|
})) {
|
|
1739
1909
|
if (event.type === "run:started") {
|
|
@@ -1777,6 +1947,9 @@ export const createRequestHandler = async (options?: {
|
|
|
1777
1947
|
toolTimeline.push(toolText);
|
|
1778
1948
|
currentTools.push(toolText);
|
|
1779
1949
|
}
|
|
1950
|
+
if (event.type === "step:completed") {
|
|
1951
|
+
await persistDraftAssistantTurn();
|
|
1952
|
+
}
|
|
1780
1953
|
if (event.type === "tool:approval:required") {
|
|
1781
1954
|
const toolText = `- approval required \`${event.tool}\``;
|
|
1782
1955
|
toolTimeline.push(toolText);
|
|
@@ -1823,7 +1996,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1823
1996
|
conversation.messages = hasAssistantContent
|
|
1824
1997
|
? [
|
|
1825
1998
|
...historyMessages,
|
|
1826
|
-
{ role: "user", content:
|
|
1999
|
+
{ role: "user", content: userContent },
|
|
1827
2000
|
{
|
|
1828
2001
|
role: "assistant",
|
|
1829
2002
|
content: assistantResponse,
|
|
@@ -1836,7 +2009,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1836
2009
|
: undefined,
|
|
1837
2010
|
},
|
|
1838
2011
|
]
|
|
1839
|
-
: [...historyMessages, { role: "user", content:
|
|
2012
|
+
: [...historyMessages, { role: "user", content: userContent }];
|
|
1840
2013
|
conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
|
|
1841
2014
|
conversation.pendingApprovals = [];
|
|
1842
2015
|
conversation.updatedAt = Date.now();
|
|
@@ -1853,7 +2026,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1853
2026
|
if (assistantResponse.length > 0 || toolTimeline.length > 0 || fallbackSections.length > 0) {
|
|
1854
2027
|
conversation.messages = [
|
|
1855
2028
|
...historyMessages,
|
|
1856
|
-
{ role: "user", content:
|
|
2029
|
+
{ role: "user", content: userContent },
|
|
1857
2030
|
{
|
|
1858
2031
|
role: "assistant",
|
|
1859
2032
|
content: assistantResponse,
|
|
@@ -1895,7 +2068,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1895
2068
|
if (assistantResponse.length > 0 || toolTimeline.length > 0 || fallbackSections.length > 0) {
|
|
1896
2069
|
conversation.messages = [
|
|
1897
2070
|
...historyMessages,
|
|
1898
|
-
{ role: "user", content:
|
|
2071
|
+
{ role: "user", content: userContent },
|
|
1899
2072
|
{
|
|
1900
2073
|
role: "assistant",
|
|
1901
2074
|
content: assistantResponse,
|
|
@@ -1972,20 +2145,28 @@ export const runOnce = async (
|
|
|
1972
2145
|
const workingDir = options.workingDir ?? process.cwd();
|
|
1973
2146
|
dotenv.config({ path: resolve(workingDir, ".env") });
|
|
1974
2147
|
const config = await loadPonchoConfig(workingDir);
|
|
1975
|
-
const
|
|
2148
|
+
const uploadStore = await createUploadStore(config?.uploads, workingDir);
|
|
2149
|
+
const harness = new AgentHarness({ workingDir, uploadStore });
|
|
1976
2150
|
const telemetry = new TelemetryEmitter(config?.telemetry);
|
|
1977
2151
|
await harness.initialize();
|
|
1978
2152
|
|
|
1979
|
-
const
|
|
1980
|
-
options.filePaths.map(async (
|
|
1981
|
-
const
|
|
1982
|
-
|
|
2153
|
+
const fileInputs: FileInput[] = await Promise.all(
|
|
2154
|
+
options.filePaths.map(async (filePath) => {
|
|
2155
|
+
const absPath = resolve(workingDir, filePath);
|
|
2156
|
+
const buf = await readFile(absPath);
|
|
2157
|
+
const ext = absPath.split(".").pop()?.toLowerCase() ?? "";
|
|
2158
|
+
return {
|
|
2159
|
+
data: buf.toString("base64"),
|
|
2160
|
+
mediaType: extToMime(ext),
|
|
2161
|
+
filename: basename(filePath),
|
|
2162
|
+
};
|
|
1983
2163
|
}),
|
|
1984
2164
|
);
|
|
1985
2165
|
|
|
1986
2166
|
const input: RunInput = {
|
|
1987
|
-
task
|
|
2167
|
+
task,
|
|
1988
2168
|
parameters: options.params,
|
|
2169
|
+
files: fileInputs.length > 0 ? fileInputs : undefined,
|
|
1989
2170
|
};
|
|
1990
2171
|
|
|
1991
2172
|
if (options.json) {
|
|
@@ -2055,10 +2236,12 @@ export const runInteractive = async (
|
|
|
2055
2236
|
});
|
|
2056
2237
|
};
|
|
2057
2238
|
|
|
2239
|
+
const uploadStore = await createUploadStore(config?.uploads, workingDir);
|
|
2058
2240
|
const harness = new AgentHarness({
|
|
2059
2241
|
workingDir,
|
|
2060
2242
|
environment: resolveHarnessEnvironment(),
|
|
2061
2243
|
approvalHandler,
|
|
2244
|
+
uploadStore,
|
|
2062
2245
|
});
|
|
2063
2246
|
await harness.initialize();
|
|
2064
2247
|
const identity = await ensureAgentIdentity(workingDir);
|