@poncho-ai/cli 0.38.1 → 0.40.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-W7SQVUB4.js";
4
+ } from "./chunk-KVGMTYDD.js";
5
5
 
6
6
  // src/cli.ts
7
7
  void main();
package/dist/index.d.ts CHANGED
@@ -78,7 +78,7 @@ declare const TEST_TEMPLATE = "tests:\n - name: \"Basic sanity\"\n task: \"W
78
78
  declare const SKILL_TEMPLATE = "---\nname: starter-skill\ndescription: Starter local skill template\nallowed-tools:\n - ./scripts/starter-echo.ts\n---\n\n# Starter Skill\n\nThis is a starter local skill created by `poncho init`.\n\n## Authoring Notes\n\n- Put executable JavaScript/TypeScript files in `scripts/`.\n- Ask the agent to call `run_skill_script` with `skill`, `script`, and optional `input`.\n";
79
79
  declare const SKILL_TOOL_TEMPLATE = "export default async function run(input) {\n const message = typeof input?.message === \"string\" ? input.message : \"\";\n return { echoed: message };\n}\n";
80
80
 
81
- type DeployTarget = "none" | "vercel" | "docker" | "fly" | "lambda";
81
+ type DeployTarget = "none" | "vercel" | "docker" | "fly" | "lambda" | "railway";
82
82
  type InitOnboardingOptions = {
83
83
  yes?: boolean;
84
84
  interactive?: boolean;
package/dist/index.js CHANGED
@@ -77,7 +77,7 @@ import {
77
77
  writeHtml,
78
78
  writeJson,
79
79
  writeScaffoldFile
80
- } from "./chunk-W7SQVUB4.js";
80
+ } from "./chunk-KVGMTYDD.js";
81
81
  export {
82
82
  AGENT_TEMPLATE,
83
83
  ENV_TEMPLATE,
@@ -3,7 +3,7 @@ import {
3
3
  getMascotLines,
4
4
  inferConversationTitle,
5
5
  resolveHarnessEnvironment
6
- } from "./chunk-W7SQVUB4.js";
6
+ } from "./chunk-KVGMTYDD.js";
7
7
 
8
8
  // src/run-interactive-ink.ts
9
9
  import * as readline from "readline";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/cli",
3
- "version": "0.38.1",
3
+ "version": "0.40.0",
4
4
  "description": "CLI for building and deploying AI agents",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,9 +28,9 @@
28
28
  "react": "^19.2.4",
29
29
  "react-devtools-core": "^6.1.5",
30
30
  "yaml": "^2.8.1",
31
- "@poncho-ai/harness": "0.39.1",
32
- "@poncho-ai/messaging": "0.8.4",
33
- "@poncho-ai/sdk": "1.9.0"
31
+ "@poncho-ai/harness": "0.40.0",
32
+ "@poncho-ai/messaging": "0.8.5",
33
+ "@poncho-ai/sdk": "1.10.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/busboy": "^1.5.4",
package/src/index.ts CHANGED
@@ -55,6 +55,7 @@ import type {
55
55
  ApiThreadSummary,
56
56
  } from "@poncho-ai/sdk";
57
57
  import { getTextContent } from "@poncho-ai/sdk";
58
+ import { buildZip, type ZipEntry } from "./vfs-zip.js";
58
59
  import {
59
60
  AgentBridge,
60
61
  ResendAdapter,
@@ -134,6 +135,22 @@ const collectToolCallIds = (msgs: Message[]): Set<string> => {
134
135
  }
135
136
  return ids;
136
137
  };
138
+
139
+ const MEMORY_VFILE_PATH = "/memory.md";
140
+
141
+ const isSafeVfsPath = (p: string): boolean => {
142
+ if (typeof p !== "string" || p.length === 0) return false;
143
+ if (!p.startsWith("/")) return false;
144
+ if (p.includes("\0")) return false;
145
+ const segments = p.split("/").slice(1);
146
+ if (p !== "/" && segments[segments.length - 1] === "") return false;
147
+ for (const seg of segments) {
148
+ if (seg === "" && p !== "/") return false;
149
+ if (seg === "." || seg === "..") return false;
150
+ }
151
+ return true;
152
+ };
153
+
137
154
  const serverlessLog = createLogger("serverless");
138
155
  import {
139
156
  type DeployTarget,
@@ -362,6 +379,32 @@ export const createRequestHandler = async (options?: {
362
379
  const conversationEventStreams = new Map<string, ConversationEventStream>();
363
380
  type EventCallback = (event: AgentEvent) => void;
364
381
  const conversationEventCallbacks = new Map<string, Set<EventCallback>>();
382
+ // Per-conversation replay-buffer cap. Live subscribers get full events; the
383
+ // buffer is just so a reconnecting client can catch up. Keep the most recent
384
+ // N events to bound memory.
385
+ const MAX_BUFFERED_EVENTS_PER_CONVERSATION = 1000;
386
+ // Deep-clone an event with any string > 4 KB replaced by a placeholder. Used
387
+ // when buffering for replay: a reconnecting client doesn't need fresh
388
+ // screenshots/large blobs (they're persisted in the conversation), and
389
+ // accumulating them caused OOMs (e.g. tool:completed for browser_screenshot
390
+ // carries a ~134 KB base64 JPEG per call).
391
+ const STRIP_LARGE_STRING_BYTES = 4096;
392
+ const stripLargeStringsForBuffer = (value: unknown): unknown => {
393
+ if (typeof value === "string") {
394
+ return value.length > STRIP_LARGE_STRING_BYTES
395
+ ? `[stripped-for-replay len=${value.length}]`
396
+ : value;
397
+ }
398
+ if (Array.isArray(value)) return value.map(stripLargeStringsForBuffer);
399
+ if (value && typeof value === "object") {
400
+ const out: Record<string, unknown> = {};
401
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
402
+ out[k] = stripLargeStringsForBuffer(v);
403
+ }
404
+ return out;
405
+ }
406
+ return value;
407
+ };
365
408
  const broadcastEvent = (conversationId: string, event: AgentEvent): void => {
366
409
  let stream = conversationEventStreams.get(conversationId);
367
410
  if (!stream) {
@@ -372,7 +415,10 @@ export const createRequestHandler = async (options?: {
372
415
  // Buffering them for reconnect replay grew to multi-GB and OOM'd the process;
373
416
  // they're ephemeral like browser:status and should never replay.
374
417
  if (event.type !== "browser:frame") {
375
- stream.buffer.push(event);
418
+ stream.buffer.push(stripLargeStringsForBuffer(event) as AgentEvent);
419
+ if (stream.buffer.length > MAX_BUFFERED_EVENTS_PER_CONVERSATION) {
420
+ stream.buffer.splice(0, stream.buffer.length - MAX_BUFFERED_EVENTS_PER_CONVERSATION);
421
+ }
376
422
  }
377
423
  for (const subscriber of stream.subscribers) {
378
424
  try {
@@ -831,6 +877,7 @@ export const createRequestHandler = async (options?: {
831
877
  let runContextWindow = 0;
832
878
  let runContinuation = false;
833
879
  let runContinuationMessages: Message[] | undefined;
880
+ let runHarnessMessages: Message[] | undefined;
834
881
  let runSteps = 0;
835
882
  let runMaxSteps: number | undefined;
836
883
 
@@ -985,6 +1032,7 @@ export const createRequestHandler = async (options?: {
985
1032
  });
986
1033
  runContinuation = execution.runContinuation;
987
1034
  runContinuationMessages = execution.runContinuationMessages;
1035
+ runHarnessMessages = execution.runHarnessMessages;
988
1036
  runSteps = execution.runSteps;
989
1037
  runMaxSteps = execution.runMaxSteps;
990
1038
  runContextTokens = execution.runContextTokens;
@@ -1008,7 +1056,11 @@ export const createRequestHandler = async (options?: {
1008
1056
  contextWindow: runContextWindow,
1009
1057
  continuation: runContinuation,
1010
1058
  continuationMessages: runContinuationMessages,
1011
- harnessMessages: runContinuationMessages,
1059
+ // Prefer the cancellation/end-of-run snapshot from the harness so
1060
+ // _harnessMessages stays in sync with what the model just saw,
1061
+ // even on aborted runs. Falls back to continuationMessages when
1062
+ // the run completed via continuation.
1063
+ harnessMessages: runHarnessMessages ?? runContinuationMessages,
1012
1064
  toolResultArchive: harness.getToolResultArchive(conversationId),
1013
1065
  }, { shouldRebuildCanonical: true });
1014
1066
  });
@@ -1474,7 +1526,7 @@ export const createRequestHandler = async (options?: {
1474
1526
  }
1475
1527
 
1476
1528
  if (webUiEnabled) {
1477
- if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/"))) {
1529
+ if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/") || pathname.startsWith("/f/"))) {
1478
1530
  writeHtml(response, 200, renderWebUiHtml({ agentName, isDev: !isProduction }));
1479
1531
  return;
1480
1532
  }
@@ -2696,13 +2748,16 @@ export const createRequestHandler = async (options?: {
2696
2748
  && Array.isArray(conversation._continuationMessages)
2697
2749
  && conversation._continuationMessages.length > 0
2698
2750
  && !hasPendingApprovals;
2751
+ const verboseDev = process.env.PONCHO_DEV_VERBOSE === "1";
2699
2752
  writeJson(response, 200, {
2700
2753
  conversation: {
2701
2754
  ...conversation,
2702
2755
  messages: conversation.messages.map(normalizeMessageForClient).filter((m): m is Message => m !== null),
2703
2756
  pendingApprovals: storedPending,
2704
2757
  _continuationMessages: undefined,
2705
- _harnessMessages: undefined,
2758
+ // In verbose dev mode the web UI exposes a toggle to inspect the
2759
+ // raw harness messages sent to the model API. Strip it otherwise.
2760
+ _harnessMessages: verboseDev ? conversation._harnessMessages : undefined,
2706
2761
  // The browser has no use for the archive; make sure we never ship
2707
2762
  // it back even if the conversation was loaded via getWithArchive.
2708
2763
  _toolResultArchive: undefined,
@@ -2711,6 +2766,7 @@ export const createRequestHandler = async (options?: {
2711
2766
  hasActiveRun: hasActiveRun || hasPendingCallbackResults,
2712
2767
  hasRunningSubagents,
2713
2768
  needsContinuation,
2769
+ verboseDev,
2714
2770
  });
2715
2771
  return;
2716
2772
  }
@@ -2835,6 +2891,22 @@ export const createRequestHandler = async (options?: {
2835
2891
  writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
2836
2892
  return;
2837
2893
  }
2894
+ if (vfsPath === MEMORY_VFILE_PATH) {
2895
+ try {
2896
+ const memory = await engine.memory.get(tenantId);
2897
+ const data = Buffer.from(memory.content, "utf-8");
2898
+ response.writeHead(200, {
2899
+ "Content-Type": "text/markdown; charset=utf-8",
2900
+ "Content-Length": data.length,
2901
+ "Content-Disposition": `inline; filename="memory.md"`,
2902
+ "Cache-Control": "no-cache",
2903
+ });
2904
+ response.end(data);
2905
+ } catch (err) {
2906
+ writeJson(response, 500, { code: "READ_FAILED", message: (err as Error)?.message ?? "Failed to read memory" });
2907
+ }
2908
+ return;
2909
+ }
2838
2910
  try {
2839
2911
  const stat = await engine.vfs.stat(tenantId, vfsPath);
2840
2912
  if (!stat || stat.type !== "file") {
@@ -2869,6 +2941,273 @@ export const createRequestHandler = async (options?: {
2869
2941
  return;
2870
2942
  }
2871
2943
 
2944
+ if (vfsMatch && request.method === "PUT") {
2945
+ const rawPath = "/" + decodeURIComponent(vfsMatch[1] ?? "");
2946
+ const tenantId = ctx.tenantId ?? "__default__";
2947
+ const engine = harness.storageEngine;
2948
+ if (!engine) {
2949
+ writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
2950
+ return;
2951
+ }
2952
+ if (!isSafeVfsPath(rawPath) || rawPath === "/") {
2953
+ writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
2954
+ return;
2955
+ }
2956
+ if (rawPath === MEMORY_VFILE_PATH) {
2957
+ try {
2958
+ const chunks: Buffer[] = [];
2959
+ for await (const chunk of request) chunks.push(chunk as Buffer);
2960
+ const body = Buffer.concat(chunks);
2961
+ const content = body.toString("utf-8").trim();
2962
+ const memory = await engine.memory.update(content, tenantId);
2963
+ writeJson(response, 200, {
2964
+ path: MEMORY_VFILE_PATH,
2965
+ size: Buffer.byteLength(memory.content, "utf-8"),
2966
+ mimeType: "text/markdown",
2967
+ updatedAt: memory.updatedAt,
2968
+ });
2969
+ } catch (err) {
2970
+ writeJson(response, 500, { code: "WRITE_FAILED", message: (err as Error)?.message ?? "Failed to write memory" });
2971
+ }
2972
+ return;
2973
+ }
2974
+ const allowOverwrite = requestUrl.searchParams.get("overwrite") === "1";
2975
+ try {
2976
+ const existing = await engine.vfs.stat(tenantId, rawPath);
2977
+ if (existing && !allowOverwrite) {
2978
+ writeJson(response, 409, { code: "EXISTS", message: "File already exists" });
2979
+ return;
2980
+ }
2981
+ if (existing && existing.type !== "file") {
2982
+ writeJson(response, 409, { code: "NOT_A_FILE", message: "Path exists and is not a file" });
2983
+ return;
2984
+ }
2985
+ const chunks: Buffer[] = [];
2986
+ for await (const chunk of request) chunks.push(chunk as Buffer);
2987
+ const body = Buffer.concat(chunks);
2988
+ const mimeType = (request.headers["content-type"] as string | undefined)?.split(";")[0]?.trim() || undefined;
2989
+ await engine.vfs.writeFile(tenantId, rawPath, new Uint8Array(body), mimeType);
2990
+ if (rawPath === "/skills" || rawPath.startsWith("/skills/")) {
2991
+ harness.invalidateSkillsForTenant(tenantId);
2992
+ }
2993
+ const stat = await engine.vfs.stat(tenantId, rawPath);
2994
+ writeJson(response, 200, {
2995
+ path: rawPath,
2996
+ size: stat?.size ?? body.length,
2997
+ mimeType: stat?.mimeType ?? mimeType ?? null,
2998
+ updatedAt: stat?.updatedAt ?? Date.now(),
2999
+ });
3000
+ } catch (err) {
3001
+ const message = (err as Error)?.message ?? "Upload failed";
3002
+ const code = /quota|too large|exceed/i.test(message) ? 413 : 500;
3003
+ writeJson(response, code, { code: code === 413 ? "QUOTA" : "WRITE_FAILED", message });
3004
+ }
3005
+ return;
3006
+ }
3007
+
3008
+ if (vfsMatch && request.method === "DELETE") {
3009
+ const rawPath = "/" + decodeURIComponent(vfsMatch[1] ?? "");
3010
+ const tenantId = ctx.tenantId ?? "__default__";
3011
+ const engine = harness.storageEngine;
3012
+ if (!engine) {
3013
+ writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
3014
+ return;
3015
+ }
3016
+ if (!isSafeVfsPath(rawPath) || rawPath === "/") {
3017
+ writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
3018
+ return;
3019
+ }
3020
+ if (rawPath === MEMORY_VFILE_PATH) {
3021
+ writeJson(response, 400, {
3022
+ code: "RESERVED",
3023
+ message: "memory.md cannot be deleted; clear its contents instead.",
3024
+ });
3025
+ return;
3026
+ }
3027
+ try {
3028
+ const stat = await engine.vfs.stat(tenantId, rawPath);
3029
+ if (!stat) {
3030
+ writeJson(response, 404, { code: "NOT_FOUND", message: "Path not found" });
3031
+ return;
3032
+ }
3033
+ if (stat.type === "directory") {
3034
+ await engine.vfs.deleteDir(tenantId, rawPath, true);
3035
+ } else {
3036
+ await engine.vfs.deleteFile(tenantId, rawPath);
3037
+ }
3038
+ if (rawPath === "/skills" || rawPath.startsWith("/skills/")) {
3039
+ harness.invalidateSkillsForTenant(tenantId);
3040
+ }
3041
+ writeJson(response, 200, { ok: true, path: rawPath });
3042
+ } catch (err) {
3043
+ writeJson(response, 500, { code: "DELETE_FAILED", message: (err as Error)?.message ?? "Failed to delete" });
3044
+ }
3045
+ return;
3046
+ }
3047
+
3048
+ if (pathname === "/api/vfs-archive" && request.method === "GET") {
3049
+ const dirPath = requestUrl.searchParams.get("path") ?? "/";
3050
+ const tenantId = ctx.tenantId ?? "__default__";
3051
+ const engine = harness.storageEngine;
3052
+ if (!engine) {
3053
+ writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
3054
+ return;
3055
+ }
3056
+ if (!isSafeVfsPath(dirPath)) {
3057
+ writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
3058
+ return;
3059
+ }
3060
+ try {
3061
+ if (dirPath !== "/") {
3062
+ const stat = await engine.vfs.stat(tenantId, dirPath);
3063
+ if (!stat || stat.type !== "directory") {
3064
+ writeJson(response, 404, { code: "NOT_FOUND", message: "Directory not found" });
3065
+ return;
3066
+ }
3067
+ }
3068
+ const entries: ZipEntry[] = [];
3069
+ const walk = async (dir: string, prefix: string): Promise<void> => {
3070
+ const children = await engine.vfs.readdir(tenantId, dir);
3071
+ for (const child of children) {
3072
+ if (dir === "/" && child.name === "memory.md") continue;
3073
+ const childPath = dir === "/" ? "/" + child.name : dir + "/" + child.name;
3074
+ const relName = prefix === "" ? child.name : prefix + "/" + child.name;
3075
+ if (child.type === "directory") {
3076
+ await walk(childPath, relName);
3077
+ } else if (child.type === "file") {
3078
+ const content = await engine.vfs.readFile(tenantId, childPath);
3079
+ const stat = await engine.vfs.stat(tenantId, childPath);
3080
+ entries.push({
3081
+ name: relName,
3082
+ content,
3083
+ mtime: stat?.updatedAt ? new Date(stat.updatedAt) : undefined,
3084
+ });
3085
+ }
3086
+ }
3087
+ };
3088
+ await walk(dirPath, "");
3089
+ if (dirPath === "/") {
3090
+ const memory = await engine.memory.get(tenantId);
3091
+ entries.push({
3092
+ name: "memory.md",
3093
+ content: new Uint8Array(Buffer.from(memory.content, "utf-8")),
3094
+ mtime: memory.updatedAt > 0 ? new Date(memory.updatedAt) : undefined,
3095
+ });
3096
+ }
3097
+ const archiveName = (dirPath === "/" ? "vfs" : (dirPath.split("/").pop() || "archive")) + ".zip";
3098
+ const zip = buildZip(entries);
3099
+ response.writeHead(200, {
3100
+ "Content-Type": "application/zip",
3101
+ "Content-Length": zip.length,
3102
+ "Content-Disposition": `attachment; filename="${archiveName}"`,
3103
+ "Cache-Control": "no-cache",
3104
+ });
3105
+ response.end(zip);
3106
+ } catch (err) {
3107
+ writeJson(response, 500, { code: "ARCHIVE_FAILED", message: (err as Error)?.message ?? "Failed to build archive" });
3108
+ }
3109
+ return;
3110
+ }
3111
+
3112
+ if (pathname === "/api/vfs-list" && request.method === "GET") {
3113
+ const dirPath = requestUrl.searchParams.get("path") ?? "/";
3114
+ const tenantId = ctx.tenantId ?? "__default__";
3115
+ const engine = harness.storageEngine;
3116
+ if (!engine) {
3117
+ writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
3118
+ return;
3119
+ }
3120
+ if (!isSafeVfsPath(dirPath)) {
3121
+ writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
3122
+ return;
3123
+ }
3124
+ try {
3125
+ if (dirPath !== "/") {
3126
+ const stat = await engine.vfs.stat(tenantId, dirPath);
3127
+ if (!stat || stat.type !== "directory") {
3128
+ writeJson(response, 404, { code: "NOT_FOUND", message: "Directory not found" });
3129
+ return;
3130
+ }
3131
+ }
3132
+ const rawDirEntries = await engine.vfs.readdir(tenantId, dirPath);
3133
+ const dirEntries = dirPath === "/"
3134
+ ? rawDirEntries.filter((e) => e.name !== "memory.md")
3135
+ : rawDirEntries;
3136
+ const entries: Array<{
3137
+ name: string;
3138
+ type: "file" | "directory" | "symlink";
3139
+ size: number;
3140
+ mimeType: string | null;
3141
+ updatedAt: number | null;
3142
+ }> = await Promise.all(dirEntries.map(async (entry) => {
3143
+ const childPath = dirPath === "/" ? "/" + entry.name : dirPath + "/" + entry.name;
3144
+ const stat = await engine.vfs.stat(tenantId, childPath);
3145
+ return {
3146
+ name: entry.name,
3147
+ type: entry.type,
3148
+ size: stat?.size ?? 0,
3149
+ mimeType: stat?.mimeType ?? null,
3150
+ updatedAt: stat?.updatedAt ?? null,
3151
+ };
3152
+ }));
3153
+ if (dirPath === "/") {
3154
+ const memory = await engine.memory.get(tenantId);
3155
+ entries.push({
3156
+ name: "memory.md",
3157
+ type: "file",
3158
+ size: Buffer.byteLength(memory.content, "utf-8"),
3159
+ mimeType: "text/markdown",
3160
+ updatedAt: memory.updatedAt > 0 ? memory.updatedAt : null,
3161
+ });
3162
+ }
3163
+ entries.sort((a, b) => {
3164
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
3165
+ return a.name.localeCompare(b.name);
3166
+ });
3167
+ const usage = await engine.vfs.getUsage(tenantId);
3168
+ writeJson(response, 200, { path: dirPath, entries, usage });
3169
+ } catch (err) {
3170
+ writeJson(response, 500, { code: "READDIR_FAILED", message: (err as Error)?.message ?? "Failed to list directory" });
3171
+ }
3172
+ return;
3173
+ }
3174
+
3175
+ if (pathname === "/api/vfs-mkdir" && request.method === "POST") {
3176
+ const tenantId = ctx.tenantId ?? "__default__";
3177
+ const engine = harness.storageEngine;
3178
+ if (!engine) {
3179
+ writeJson(response, 500, { code: "NO_ENGINE", message: "Storage engine not available" });
3180
+ return;
3181
+ }
3182
+ try {
3183
+ const chunks: Buffer[] = [];
3184
+ for await (const chunk of request) chunks.push(chunk as Buffer);
3185
+ const body = JSON.parse(Buffer.concat(chunks).toString() || "{}") as { path?: string };
3186
+ const dirPath = body.path ?? "";
3187
+ if (!isSafeVfsPath(dirPath) || dirPath === "/") {
3188
+ writeJson(response, 400, { code: "BAD_PATH", message: "Invalid path" });
3189
+ return;
3190
+ }
3191
+ if (dirPath === MEMORY_VFILE_PATH) {
3192
+ writeJson(response, 400, { code: "RESERVED", message: "memory.md is reserved" });
3193
+ return;
3194
+ }
3195
+ const existing = await engine.vfs.stat(tenantId, dirPath);
3196
+ if (existing) {
3197
+ writeJson(response, 409, { code: "EXISTS", message: "Path already exists" });
3198
+ return;
3199
+ }
3200
+ await engine.vfs.mkdir(tenantId, dirPath, true);
3201
+ if (dirPath === "/skills" || dirPath.startsWith("/skills/")) {
3202
+ harness.invalidateSkillsForTenant(tenantId);
3203
+ }
3204
+ writeJson(response, 200, { path: dirPath });
3205
+ } catch (err) {
3206
+ writeJson(response, 500, { code: "MKDIR_FAILED", message: (err as Error)?.message ?? "Failed to create directory" });
3207
+ }
3208
+ return;
3209
+ }
3210
+
2872
3211
  if (pathname === "/api/slash-commands" && request.method === "GET") {
2873
3212
  const skills: ApiSlashCommand[] = harness.listSkills().map((s) => ({
2874
3213
  command: "/" + s.name,
@@ -3150,6 +3489,10 @@ export const createRequestHandler = async (options?: {
3150
3489
  let checkpointedRun = false;
3151
3490
  let runCancelled = false;
3152
3491
  let runContinuationMessages: Message[] | undefined;
3492
+ // Snapshot of the harness's in-flight messages emitted with run:cancelled,
3493
+ // so the catch-path (executeConversationTurn threw) can still persist a
3494
+ // canonical history that includes the cancelled work.
3495
+ let cancelHarnessMessages: Message[] | undefined;
3153
3496
 
3154
3497
  // Hoist stable ids for this turn. The same userMessage / assistantId is
3155
3498
  // reused across every buildMessages() call so the in-flight assistant
@@ -3241,6 +3584,7 @@ export const createRequestHandler = async (options?: {
3241
3584
  }
3242
3585
  if (event.type === "run:cancelled") {
3243
3586
  runCancelled = true;
3587
+ if (event.messages) cancelHarnessMessages = event.messages;
3244
3588
  }
3245
3589
  if (event.type === "compaction:completed") {
3246
3590
  if (event.compactedMessages) {
@@ -3368,7 +3712,17 @@ export const createRequestHandler = async (options?: {
3368
3712
  if (abortController.signal.aborted || runCancelled) {
3369
3713
  if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
3370
3714
  conversation.messages = buildMessages();
3371
- conversation.updatedAt = Date.now();
3715
+ // Keep _harnessMessages aligned with what the model just saw.
3716
+ // Without this, loadCanonicalHistory will hand the next turn a
3717
+ // pre-cancellation snapshot and the agent will have no memory of
3718
+ // the work it just did.
3719
+ applyTurnMetadata(conversation, {
3720
+ latestRunId,
3721
+ contextTokens: 0,
3722
+ contextWindow: 0,
3723
+ harnessMessages: cancelHarnessMessages,
3724
+ toolResultArchive: harness.getToolResultArchive(conversationId),
3725
+ }, { shouldRebuildCanonical: true });
3372
3726
  await conversationStore.update(conversation);
3373
3727
  }
3374
3728
  if (!checkpointedRun) {
@@ -4114,6 +4468,12 @@ export const buildCli = (): Command => {
4114
4468
  }
4115
4469
  setLogLevel(level as typeof valid[number]);
4116
4470
  }
4471
+ if (options.verbose) {
4472
+ // Surfaced to the request handler so the web UI can offer a toggle
4473
+ // between user-facing messages and the raw harness-message stream
4474
+ // sent to the model API.
4475
+ process.env.PONCHO_DEV_VERBOSE = "1";
4476
+ }
4117
4477
  if (process.stdout.isTTY && !process.env.NO_COLOR) {
4118
4478
  process.stdout.write("\n");
4119
4479
  for (const line of getMascotLines()) process.stdout.write(`${line}\n`);
@@ -4381,7 +4741,7 @@ export const buildCli = (): Command => {
4381
4741
 
4382
4742
  program
4383
4743
  .command("build")
4384
- .argument("[target]", "vercel|docker|lambda|fly")
4744
+ .argument("[target]", "vercel|docker|lambda|fly|railway")
4385
4745
  .option("--force", "overwrite existing deployment files")
4386
4746
  .description("Scaffold deployment files for a target")
4387
4747
  .action(async (target: string | undefined, options: { force?: boolean }) => {
@@ -21,7 +21,7 @@ const bold = (s: string): string => `${C.bold}${s}${C.reset}`;
21
21
  const INPUT_CARET = "»";
22
22
 
23
23
  type OnboardingAnswers = Record<string, string | number | boolean>;
24
- export type DeployTarget = "none" | "vercel" | "docker" | "fly" | "lambda";
24
+ export type DeployTarget = "none" | "vercel" | "docker" | "fly" | "lambda" | "railway";
25
25
 
26
26
  export type InitOnboardingOptions = {
27
27
  yes?: boolean;
@@ -282,7 +282,13 @@ const getProviderModelName = (provider: string): string =>
282
282
 
283
283
  const normalizeDeployTarget = (value: unknown): DeployTarget => {
284
284
  const target = typeof value === "string" ? value.toLowerCase() : "";
285
- if (target === "vercel" || target === "docker" || target === "fly" || target === "lambda") {
285
+ if (
286
+ target === "vercel" ||
287
+ target === "docker" ||
288
+ target === "fly" ||
289
+ target === "lambda" ||
290
+ target === "railway"
291
+ ) {
286
292
  return target;
287
293
  }
288
294
  return "none";