@poncho-ai/cli 0.10.2 → 0.11.1
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 +18 -0
- package/dist/{chunk-COLXQM6J.js → chunk-T2F6ICXI.js} +595 -45
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-Z3U5SV4C.js → run-interactive-ink-7FP5PT7Q.js} +83 -6
- package/package.json +5 -3
- package/src/index.ts +206 -26
- package/src/run-interactive-ink.ts +84 -5
- package/src/web-ui.ts +391 -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-T2F6ICXI.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.1",
|
|
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"
|
|
@@ -689,10 +746,19 @@ const writeScaffoldFile = async (
|
|
|
689
746
|
options.writtenPaths.push(relative(options.baseDir, filePath));
|
|
690
747
|
};
|
|
691
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
|
+
|
|
692
757
|
const ensureRuntimeCliDependency = async (
|
|
693
758
|
projectDir: string,
|
|
694
759
|
cliVersion: string,
|
|
695
|
-
|
|
760
|
+
config?: PonchoConfig,
|
|
761
|
+
): Promise<{ paths: string[]; addedDeps: string[] }> => {
|
|
696
762
|
const packageJsonPath = resolve(projectDir, "package.json");
|
|
697
763
|
const content = await readFile(packageJsonPath, "utf8");
|
|
698
764
|
const parsed = JSON.parse(content) as {
|
|
@@ -713,9 +779,21 @@ const ensureRuntimeCliDependency = async (
|
|
|
713
779
|
}
|
|
714
780
|
dependencies.marked = await readCliDependencyVersion("marked", "^17.0.2");
|
|
715
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
|
+
|
|
716
794
|
parsed.dependencies = dependencies;
|
|
717
795
|
await writeFile(packageJsonPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
|
|
718
|
-
return [relative(projectDir, packageJsonPath)];
|
|
796
|
+
return { paths: [relative(projectDir, packageJsonPath)], addedDeps };
|
|
719
797
|
};
|
|
720
798
|
|
|
721
799
|
const scaffoldDeployTarget = async (
|
|
@@ -855,10 +933,16 @@ CMD ["node","server.js"]
|
|
|
855
933
|
});
|
|
856
934
|
}
|
|
857
935
|
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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);
|
|
862
946
|
}
|
|
863
947
|
}
|
|
864
948
|
|
|
@@ -1161,9 +1245,11 @@ export const createRequestHandler = async (options?: {
|
|
|
1161
1245
|
}
|
|
1162
1246
|
await persistConversationPendingApprovals(conversationId);
|
|
1163
1247
|
};
|
|
1248
|
+
const uploadStore = await createUploadStore(config?.uploads, workingDir);
|
|
1164
1249
|
const harness = new AgentHarness({
|
|
1165
1250
|
workingDir,
|
|
1166
1251
|
environment: resolveHarnessEnvironment(),
|
|
1252
|
+
uploadStore,
|
|
1167
1253
|
approvalHandler: async (request) =>
|
|
1168
1254
|
new Promise<boolean>((resolveApproval) => {
|
|
1169
1255
|
const ownerIdForRun = runOwners.get(request.runId) ?? "local-owner";
|
|
@@ -1611,6 +1697,31 @@ export const createRequestHandler = async (options?: {
|
|
|
1611
1697
|
return;
|
|
1612
1698
|
}
|
|
1613
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
|
+
|
|
1614
1725
|
const conversationMessageMatch = pathname.match(/^\/api\/conversations\/([^/]+)\/messages$/);
|
|
1615
1726
|
if (conversationMessageMatch && request.method === "POST") {
|
|
1616
1727
|
const conversationId = decodeURIComponent(conversationMessageMatch[1] ?? "");
|
|
@@ -1622,11 +1733,31 @@ export const createRequestHandler = async (options?: {
|
|
|
1622
1733
|
});
|
|
1623
1734
|
return;
|
|
1624
1735
|
}
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
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
|
+
}
|
|
1630
1761
|
if (!messageText) {
|
|
1631
1762
|
writeJson(response, 400, {
|
|
1632
1763
|
code: "VALIDATION_ERROR",
|
|
@@ -1671,9 +1802,44 @@ export const createRequestHandler = async (options?: {
|
|
|
1671
1802
|
let currentText = "";
|
|
1672
1803
|
let currentTools: string[] = [];
|
|
1673
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
|
+
}
|
|
1674
1840
|
try {
|
|
1675
1841
|
// Persist the user turn immediately so refreshing mid-run keeps chat context.
|
|
1676
|
-
conversation.messages = [...historyMessages, { role: "user", content:
|
|
1842
|
+
conversation.messages = [...historyMessages, { role: "user", content: userContent }];
|
|
1677
1843
|
conversation.updatedAt = Date.now();
|
|
1678
1844
|
await conversationStore.update(conversation);
|
|
1679
1845
|
|
|
@@ -1697,7 +1863,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1697
1863
|
}
|
|
1698
1864
|
conversation.messages = [
|
|
1699
1865
|
...historyMessages,
|
|
1700
|
-
{ role: "user", content:
|
|
1866
|
+
{ role: "user", content: userContent },
|
|
1701
1867
|
{
|
|
1702
1868
|
role: "assistant",
|
|
1703
1869
|
content: assistantResponse,
|
|
@@ -1723,7 +1889,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1723
1889
|
updatedAt: item.updatedAt,
|
|
1724
1890
|
content: item.messages
|
|
1725
1891
|
.slice(-6)
|
|
1726
|
-
.map((message) => `${message.role}: ${message.content}`)
|
|
1892
|
+
.map((message) => `${message.role}: ${typeof message.content === "string" ? message.content : getTextContent(message)}`)
|
|
1727
1893
|
.join("\n")
|
|
1728
1894
|
.slice(0, 2000),
|
|
1729
1895
|
}))
|
|
@@ -1732,11 +1898,12 @@ export const createRequestHandler = async (options?: {
|
|
|
1732
1898
|
for await (const event of harness.runWithTelemetry({
|
|
1733
1899
|
task: messageText,
|
|
1734
1900
|
parameters: {
|
|
1735
|
-
...(
|
|
1901
|
+
...(bodyParameters ?? {}),
|
|
1736
1902
|
__conversationRecallCorpus: recallCorpus,
|
|
1737
1903
|
__activeConversationId: conversationId,
|
|
1738
1904
|
},
|
|
1739
1905
|
messages: historyMessages,
|
|
1906
|
+
files: files.length > 0 ? files : undefined,
|
|
1740
1907
|
abortSignal: abortController.signal,
|
|
1741
1908
|
})) {
|
|
1742
1909
|
if (event.type === "run:started") {
|
|
@@ -1780,6 +1947,9 @@ export const createRequestHandler = async (options?: {
|
|
|
1780
1947
|
toolTimeline.push(toolText);
|
|
1781
1948
|
currentTools.push(toolText);
|
|
1782
1949
|
}
|
|
1950
|
+
if (event.type === "step:completed") {
|
|
1951
|
+
await persistDraftAssistantTurn();
|
|
1952
|
+
}
|
|
1783
1953
|
if (event.type === "tool:approval:required") {
|
|
1784
1954
|
const toolText = `- approval required \`${event.tool}\``;
|
|
1785
1955
|
toolTimeline.push(toolText);
|
|
@@ -1826,7 +1996,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1826
1996
|
conversation.messages = hasAssistantContent
|
|
1827
1997
|
? [
|
|
1828
1998
|
...historyMessages,
|
|
1829
|
-
{ role: "user", content:
|
|
1999
|
+
{ role: "user", content: userContent },
|
|
1830
2000
|
{
|
|
1831
2001
|
role: "assistant",
|
|
1832
2002
|
content: assistantResponse,
|
|
@@ -1839,7 +2009,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1839
2009
|
: undefined,
|
|
1840
2010
|
},
|
|
1841
2011
|
]
|
|
1842
|
-
: [...historyMessages, { role: "user", content:
|
|
2012
|
+
: [...historyMessages, { role: "user", content: userContent }];
|
|
1843
2013
|
conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
|
|
1844
2014
|
conversation.pendingApprovals = [];
|
|
1845
2015
|
conversation.updatedAt = Date.now();
|
|
@@ -1856,7 +2026,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1856
2026
|
if (assistantResponse.length > 0 || toolTimeline.length > 0 || fallbackSections.length > 0) {
|
|
1857
2027
|
conversation.messages = [
|
|
1858
2028
|
...historyMessages,
|
|
1859
|
-
{ role: "user", content:
|
|
2029
|
+
{ role: "user", content: userContent },
|
|
1860
2030
|
{
|
|
1861
2031
|
role: "assistant",
|
|
1862
2032
|
content: assistantResponse,
|
|
@@ -1898,7 +2068,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1898
2068
|
if (assistantResponse.length > 0 || toolTimeline.length > 0 || fallbackSections.length > 0) {
|
|
1899
2069
|
conversation.messages = [
|
|
1900
2070
|
...historyMessages,
|
|
1901
|
-
{ role: "user", content:
|
|
2071
|
+
{ role: "user", content: userContent },
|
|
1902
2072
|
{
|
|
1903
2073
|
role: "assistant",
|
|
1904
2074
|
content: assistantResponse,
|
|
@@ -1975,20 +2145,28 @@ export const runOnce = async (
|
|
|
1975
2145
|
const workingDir = options.workingDir ?? process.cwd();
|
|
1976
2146
|
dotenv.config({ path: resolve(workingDir, ".env") });
|
|
1977
2147
|
const config = await loadPonchoConfig(workingDir);
|
|
1978
|
-
const
|
|
2148
|
+
const uploadStore = await createUploadStore(config?.uploads, workingDir);
|
|
2149
|
+
const harness = new AgentHarness({ workingDir, uploadStore });
|
|
1979
2150
|
const telemetry = new TelemetryEmitter(config?.telemetry);
|
|
1980
2151
|
await harness.initialize();
|
|
1981
2152
|
|
|
1982
|
-
const
|
|
1983
|
-
options.filePaths.map(async (
|
|
1984
|
-
const
|
|
1985
|
-
|
|
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
|
+
};
|
|
1986
2163
|
}),
|
|
1987
2164
|
);
|
|
1988
2165
|
|
|
1989
2166
|
const input: RunInput = {
|
|
1990
|
-
task
|
|
2167
|
+
task,
|
|
1991
2168
|
parameters: options.params,
|
|
2169
|
+
files: fileInputs.length > 0 ? fileInputs : undefined,
|
|
1992
2170
|
};
|
|
1993
2171
|
|
|
1994
2172
|
if (options.json) {
|
|
@@ -2058,10 +2236,12 @@ export const runInteractive = async (
|
|
|
2058
2236
|
});
|
|
2059
2237
|
};
|
|
2060
2238
|
|
|
2239
|
+
const uploadStore = await createUploadStore(config?.uploads, workingDir);
|
|
2061
2240
|
const harness = new AgentHarness({
|
|
2062
2241
|
workingDir,
|
|
2063
2242
|
environment: resolveHarnessEnvironment(),
|
|
2064
2243
|
approvalHandler,
|
|
2244
|
+
uploadStore,
|
|
2065
2245
|
});
|
|
2066
2246
|
await harness.initialize();
|
|
2067
2247
|
const identity = await ensureAgentIdentity(workingDir);
|