@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/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "./chunk-COLXQM6J.js";
4
+ } from "./chunk-T2F6ICXI.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-COLXQM6J.js";
26
+ } from "./chunk-T2F6ICXI.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-COLXQM6J.js";
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((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.2",
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.11.2",
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"
@@ -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
- ): Promise<string[]> => {
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 packagePaths = await ensureRuntimeCliDependency(projectDir, cliVersion);
859
- for (const path of packagePaths) {
860
- if (!writtenPaths.includes(path)) {
861
- 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);
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
- const body = (await readRequestBody(request)) as {
1626
- message?: string;
1627
- parameters?: Record<string, unknown>;
1628
- };
1629
- 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
+ }
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: messageText }];
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: messageText },
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
- ...(body.parameters ?? {}),
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: messageText },
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: messageText }];
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: messageText },
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: messageText },
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 harness = new AgentHarness({ workingDir });
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 fileBlobs = await Promise.all(
1983
- options.filePaths.map(async (path) => {
1984
- const content = await readFile(resolve(workingDir, path), "utf8");
1985
- 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
+ };
1986
2163
  }),
1987
2164
  );
1988
2165
 
1989
2166
  const input: RunInput = {
1990
- task: fileBlobs.length > 0 ? `${task}\n\n${fileBlobs.join("\n\n")}` : 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);