@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/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +170 -0
- package/dist/{chunk-W7SQVUB4.js → chunk-KVGMTYDD.js} +1959 -66
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-UKPUGCDW.js → run-interactive-ink-LJTKUUV4.js} +1 -1
- package/package.json +4 -4
- package/src/index.ts +366 -6
- package/src/init-onboarding.ts +8 -2
- package/src/scaffolding.ts +75 -13
- package/src/templates.ts +5 -1
- package/src/vfs-zip.ts +94 -0
- package/src/web-ui-client.ts +1022 -0
- package/src/web-ui-styles.ts +408 -1
- package/src/web-ui.ts +6 -0
package/dist/cli.js
CHANGED
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poncho-ai/cli",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
32
|
-
"@poncho-ai/messaging": "0.8.
|
|
33
|
-
"@poncho-ai/sdk": "1.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 }) => {
|
package/src/init-onboarding.ts
CHANGED
|
@@ -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 (
|
|
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";
|