@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/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "./chunk-MGR2GJMB.js";
4
+ } from "./chunk-6GFL3VYM.js";
5
5
 
6
6
  // src/cli.ts
7
7
  void main();
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  runTests,
24
24
  startDevServer,
25
25
  updateAgentGuidance
26
- } from "./chunk-MGR2GJMB.js";
26
+ } from "./chunk-6GFL3VYM.js";
27
27
  export {
28
28
  addSkill,
29
29
  buildCli,
@@ -2,10 +2,12 @@ import {
2
2
  consumeFirstRunIntro,
3
3
  inferConversationTitle,
4
4
  resolveHarnessEnvironment
5
- } from "./chunk-MGR2GJMB.js";
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((resolve) => setTimeout(resolve, ms));
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 controls: /list /open <id> /new [title] /delete [id] /continue /reset [all]\n")
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.10.1",
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.11.1",
29
- "@poncho-ai/sdk": "0.6.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 { harness: "^0.1.0", sdk: "^0.1.0" };
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
- ): Promise<string[]> => {
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 packagePaths = await ensureRuntimeCliDependency(projectDir, cliVersion);
856
- for (const path of packagePaths) {
857
- if (!writtenPaths.includes(path)) {
858
- writtenPaths.push(path);
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
- const body = (await readRequestBody(request)) as {
1623
- message?: string;
1624
- parameters?: Record<string, unknown>;
1625
- };
1626
- const messageText = body.message?.trim() ?? "";
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: messageText }];
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: messageText },
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
- ...(body.parameters ?? {}),
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: messageText },
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: messageText }];
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: messageText },
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: messageText },
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 harness = new AgentHarness({ workingDir });
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 fileBlobs = await Promise.all(
1980
- options.filePaths.map(async (path) => {
1981
- const content = await readFile(resolve(workingDir, path), "utf8");
1982
- return `# File: ${path}\n${content}`;
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: fileBlobs.length > 0 ? `${task}\n\n${fileBlobs.join("\n\n")}` : 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);