@poncho-ai/cli 0.11.1 → 0.12.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 +11 -0
- package/dist/{chunk-T2F6ICXI.js → chunk-XIFWXRUB.js} +418 -16
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-7FP5PT7Q.js → run-interactive-ink-64QEOUXL.js} +1 -1
- package/package.json +3 -2
- package/src/index.ts +464 -19
- package/src/init-feature-context.ts +1 -0
- package/test/cli.test.ts +42 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/cli@0.
|
|
2
|
+
> @poncho-ai/cli@0.12.0 build /home/runner/work/poncho-ai/poncho-ai/packages/cli
|
|
3
3
|
> tsup src/index.ts src/cli.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/cli.ts, src/index.ts
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
[34mCLI[39m Target: es2022
|
|
9
9
|
[34mESM[39m Build start
|
|
10
10
|
[32mESM[39m [1mdist/cli.js [22m[32m94.00 B[39m
|
|
11
|
-
[32mESM[39m [1mdist/run-interactive-ink-
|
|
12
|
-
[32mESM[39m [1mdist/chunk-T2F6ICXI.js [22m[32m226.37 KB[39m
|
|
11
|
+
[32mESM[39m [1mdist/run-interactive-ink-64QEOUXL.js [22m[32m53.83 KB[39m
|
|
13
12
|
[32mESM[39m [1mdist/index.js [22m[32m857.00 B[39m
|
|
14
|
-
[32mESM[39m
|
|
13
|
+
[32mESM[39m [1mdist/chunk-XIFWXRUB.js [22m[32m241.46 KB[39m
|
|
14
|
+
[32mESM[39m ⚡️ Build success in 59ms
|
|
15
15
|
[34mDTS[39m Build start
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 3442ms
|
|
17
17
|
[32mDTS[39m [1mdist/cli.d.ts [22m[32m20.00 B[39m
|
|
18
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m3.
|
|
18
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m3.56 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @poncho-ai/cli
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#8](https://github.com/cesr/poncho-ai/pull/8) [`658bc54`](https://github.com/cesr/poncho-ai/commit/658bc54d391cb0b58aa678a2b86cd617eebdd8aa) Thanks [@cesr](https://github.com/cesr)! - Add cron job support for scheduled agent tasks. Define recurring jobs in AGENT.md frontmatter with schedule, task, and optional timezone. Includes in-process scheduler for local dev with hot-reload, HTTP endpoint for Vercel/serverless with self-continuation, Vercel scaffold generation with drift detection, and full tool activity tracking in cron conversations.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`658bc54`](https://github.com/cesr/poncho-ai/commit/658bc54d391cb0b58aa678a2b86cd617eebdd8aa)]:
|
|
12
|
+
- @poncho-ai/harness@0.13.0
|
|
13
|
+
|
|
3
14
|
## 0.11.1
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { access as access2, cp, mkdir as mkdir3, readFile as readFile3, readdir, rm, stat, writeFile as writeFile3 } from "fs/promises";
|
|
4
|
-
import { existsSync } from "fs";
|
|
4
|
+
import { existsSync, watch as fsWatch } from "fs";
|
|
5
5
|
import {
|
|
6
6
|
createServer
|
|
7
7
|
} from "http";
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
ensureAgentIdentity as ensureAgentIdentity2,
|
|
19
19
|
generateAgentId,
|
|
20
20
|
loadPonchoConfig,
|
|
21
|
+
parseAgentMarkdown,
|
|
21
22
|
resolveStateConfig
|
|
22
23
|
} from "@poncho-ai/harness";
|
|
23
24
|
import { getTextContent } from "@poncho-ai/sdk";
|
|
@@ -3736,6 +3737,7 @@ var consumeFirstRunIntro = async (workingDir, input2) => {
|
|
|
3736
3737
|
"- **Enable auth**: Add bearer tokens or custom authentication",
|
|
3737
3738
|
"- **Turn on telemetry**: Track usage with OpenTelemetry/OTLP",
|
|
3738
3739
|
"- **Add MCP servers**: Connect external tool servers",
|
|
3740
|
+
"- **Schedule cron jobs**: Set up recurring tasks in AGENT.md frontmatter",
|
|
3739
3741
|
"",
|
|
3740
3742
|
"Just let me know what you'd like to work on!\n"
|
|
3741
3743
|
].join("\n");
|
|
@@ -4163,6 +4165,22 @@ ${name}/
|
|
|
4163
4165
|
\u2514\u2500\u2500 fetch-page.ts
|
|
4164
4166
|
\`\`\`
|
|
4165
4167
|
|
|
4168
|
+
## Cron Jobs
|
|
4169
|
+
|
|
4170
|
+
Define scheduled tasks in \`AGENT.md\` frontmatter:
|
|
4171
|
+
|
|
4172
|
+
\`\`\`yaml
|
|
4173
|
+
cron:
|
|
4174
|
+
daily-report:
|
|
4175
|
+
schedule: "0 9 * * *"
|
|
4176
|
+
task: "Generate the daily sales report"
|
|
4177
|
+
\`\`\`
|
|
4178
|
+
|
|
4179
|
+
- \`poncho dev\`: jobs run via an in-process scheduler.
|
|
4180
|
+
- \`poncho build vercel\`: generates \`vercel.json\` cron entries.
|
|
4181
|
+
- Docker/Fly.io: scheduler runs automatically.
|
|
4182
|
+
- Trigger manually: \`curl http://localhost:3000/api/cron/daily-report\`
|
|
4183
|
+
|
|
4166
4184
|
## Deployment
|
|
4167
4185
|
|
|
4168
4186
|
\`\`\`bash
|
|
@@ -4367,6 +4385,55 @@ var ensureRuntimeCliDependency = async (projectDir, cliVersion, config) => {
|
|
|
4367
4385
|
`, "utf8");
|
|
4368
4386
|
return { paths: [relative(projectDir, packageJsonPath)], addedDeps };
|
|
4369
4387
|
};
|
|
4388
|
+
var checkVercelCronDrift = async (projectDir) => {
|
|
4389
|
+
const vercelJsonPath = resolve3(projectDir, "vercel.json");
|
|
4390
|
+
try {
|
|
4391
|
+
await access2(vercelJsonPath);
|
|
4392
|
+
} catch {
|
|
4393
|
+
return;
|
|
4394
|
+
}
|
|
4395
|
+
let agentCrons = {};
|
|
4396
|
+
try {
|
|
4397
|
+
const agentMd = await readFile3(resolve3(projectDir, "AGENT.md"), "utf8");
|
|
4398
|
+
const parsed = parseAgentMarkdown(agentMd);
|
|
4399
|
+
agentCrons = parsed.frontmatter.cron ?? {};
|
|
4400
|
+
} catch {
|
|
4401
|
+
return;
|
|
4402
|
+
}
|
|
4403
|
+
let vercelCrons = [];
|
|
4404
|
+
try {
|
|
4405
|
+
const raw = await readFile3(vercelJsonPath, "utf8");
|
|
4406
|
+
const vercelConfig = JSON.parse(raw);
|
|
4407
|
+
vercelCrons = vercelConfig.crons ?? [];
|
|
4408
|
+
} catch {
|
|
4409
|
+
return;
|
|
4410
|
+
}
|
|
4411
|
+
const vercelCronMap = new Map(
|
|
4412
|
+
vercelCrons.filter((c) => c.path.startsWith("/api/cron/")).map((c) => [decodeURIComponent(c.path.replace("/api/cron/", "")), c.schedule])
|
|
4413
|
+
);
|
|
4414
|
+
const diffs = [];
|
|
4415
|
+
for (const [jobName, job] of Object.entries(agentCrons)) {
|
|
4416
|
+
const existing = vercelCronMap.get(jobName);
|
|
4417
|
+
if (!existing) {
|
|
4418
|
+
diffs.push(` + missing job "${jobName}" (${job.schedule})`);
|
|
4419
|
+
} else if (existing !== job.schedule) {
|
|
4420
|
+
diffs.push(` ~ "${jobName}" schedule changed: "${existing}" \u2192 "${job.schedule}"`);
|
|
4421
|
+
}
|
|
4422
|
+
vercelCronMap.delete(jobName);
|
|
4423
|
+
}
|
|
4424
|
+
for (const [jobName, schedule] of vercelCronMap) {
|
|
4425
|
+
diffs.push(` - removed job "${jobName}" (${schedule})`);
|
|
4426
|
+
}
|
|
4427
|
+
if (diffs.length > 0) {
|
|
4428
|
+
process.stderr.write(
|
|
4429
|
+
`\u26A0 vercel.json crons are out of sync with AGENT.md:
|
|
4430
|
+
${diffs.join("\n")}
|
|
4431
|
+
Run \`poncho build vercel --force\` to update.
|
|
4432
|
+
|
|
4433
|
+
`
|
|
4434
|
+
);
|
|
4435
|
+
}
|
|
4436
|
+
};
|
|
4370
4437
|
var scaffoldDeployTarget = async (projectDir, target, options) => {
|
|
4371
4438
|
const writtenPaths = [];
|
|
4372
4439
|
const cliVersion = await readCliVersion();
|
|
@@ -4401,21 +4468,35 @@ export default async function handler(req, res) {
|
|
|
4401
4468
|
{ force: options?.force, writtenPaths, baseDir: projectDir }
|
|
4402
4469
|
);
|
|
4403
4470
|
const vercelConfigPath = resolve3(projectDir, "vercel.json");
|
|
4471
|
+
let vercelCrons;
|
|
4472
|
+
try {
|
|
4473
|
+
const agentMd = await readFile3(resolve3(projectDir, "AGENT.md"), "utf8");
|
|
4474
|
+
const parsed = parseAgentMarkdown(agentMd);
|
|
4475
|
+
if (parsed.frontmatter.cron) {
|
|
4476
|
+
vercelCrons = Object.entries(parsed.frontmatter.cron).map(
|
|
4477
|
+
([jobName, job]) => ({
|
|
4478
|
+
path: `/api/cron/${encodeURIComponent(jobName)}`,
|
|
4479
|
+
schedule: job.schedule
|
|
4480
|
+
})
|
|
4481
|
+
);
|
|
4482
|
+
}
|
|
4483
|
+
} catch {
|
|
4484
|
+
}
|
|
4485
|
+
const vercelConfig = {
|
|
4486
|
+
version: 2,
|
|
4487
|
+
functions: {
|
|
4488
|
+
"api/index.mjs": {
|
|
4489
|
+
includeFiles: "{AGENT.md,poncho.config.js,skills/**,tests/**,node_modules/.pnpm/marked@*/node_modules/marked/lib/marked.umd.js}"
|
|
4490
|
+
}
|
|
4491
|
+
},
|
|
4492
|
+
routes: [{ src: "/(.*)", dest: "/api/index.mjs" }]
|
|
4493
|
+
};
|
|
4494
|
+
if (vercelCrons && vercelCrons.length > 0) {
|
|
4495
|
+
vercelConfig.crons = vercelCrons;
|
|
4496
|
+
}
|
|
4404
4497
|
await writeScaffoldFile(
|
|
4405
4498
|
vercelConfigPath,
|
|
4406
|
-
`${JSON.stringify(
|
|
4407
|
-
{
|
|
4408
|
-
version: 2,
|
|
4409
|
-
functions: {
|
|
4410
|
-
"api/index.mjs": {
|
|
4411
|
-
includeFiles: "{AGENT.md,poncho.config.js,skills/**,tests/**,node_modules/.pnpm/marked@*/node_modules/marked/lib/marked.umd.js}"
|
|
4412
|
-
}
|
|
4413
|
-
},
|
|
4414
|
-
routes: [{ src: "/(.*)", dest: "/api/index.mjs" }]
|
|
4415
|
-
},
|
|
4416
|
-
null,
|
|
4417
|
-
2
|
|
4418
|
-
)}
|
|
4499
|
+
`${JSON.stringify(vercelConfig, null, 2)}
|
|
4419
4500
|
`,
|
|
4420
4501
|
{ force: options?.force, writtenPaths, baseDir: projectDir }
|
|
4421
4502
|
);
|
|
@@ -4458,6 +4539,11 @@ export const handler = async (event = {}) => {
|
|
|
4458
4539
|
});
|
|
4459
4540
|
return { statusCode: 200, headers: { "content-type": "application/json" }, body };
|
|
4460
4541
|
};
|
|
4542
|
+
|
|
4543
|
+
// Cron jobs: use AWS EventBridge (CloudWatch Events) to trigger scheduled invocations.
|
|
4544
|
+
// Create a rule for each cron job defined in AGENT.md that sends a GET request to:
|
|
4545
|
+
// /api/cron/<jobName>
|
|
4546
|
+
// Include the Authorization header with your PONCHO_AUTH_TOKEN as a Bearer token.
|
|
4461
4547
|
`,
|
|
4462
4548
|
{ force: options?.force, writtenPaths, baseDir: projectDir }
|
|
4463
4549
|
);
|
|
@@ -4687,6 +4773,7 @@ var createRequestHandler = async (options) => {
|
|
|
4687
4773
|
let agentName = "Agent";
|
|
4688
4774
|
let agentModelProvider = "anthropic";
|
|
4689
4775
|
let agentModelName = "claude-opus-4-5";
|
|
4776
|
+
let cronJobs = {};
|
|
4690
4777
|
try {
|
|
4691
4778
|
const agentMd = await readFile3(resolve3(workingDir, "AGENT.md"), "utf8");
|
|
4692
4779
|
const nameMatch = agentMd.match(/^name:\s*(.+)$/m);
|
|
@@ -4701,6 +4788,11 @@ var createRequestHandler = async (options) => {
|
|
|
4701
4788
|
if (modelMatch?.[1]) {
|
|
4702
4789
|
agentModelName = modelMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
4703
4790
|
}
|
|
4791
|
+
try {
|
|
4792
|
+
const parsed = parseAgentMarkdown(agentMd);
|
|
4793
|
+
cronJobs = parsed.frontmatter.cron ?? {};
|
|
4794
|
+
} catch {
|
|
4795
|
+
}
|
|
4704
4796
|
} catch {
|
|
4705
4797
|
}
|
|
4706
4798
|
const runOwners = /* @__PURE__ */ new Map();
|
|
@@ -4811,7 +4903,7 @@ var createRequestHandler = async (options) => {
|
|
|
4811
4903
|
}
|
|
4812
4904
|
return verifyPassphrase(match[1], authToken);
|
|
4813
4905
|
};
|
|
4814
|
-
|
|
4906
|
+
const handler = async (request, response) => {
|
|
4815
4907
|
if (!request.url || !request.method) {
|
|
4816
4908
|
writeJson(response, 404, { error: "Not found" });
|
|
4817
4909
|
return;
|
|
@@ -5533,10 +5625,183 @@ var createRequestHandler = async (options) => {
|
|
|
5533
5625
|
}
|
|
5534
5626
|
return;
|
|
5535
5627
|
}
|
|
5628
|
+
const cronMatch = pathname.match(/^\/api\/cron\/([^/]+)$/);
|
|
5629
|
+
if (cronMatch && (request.method === "GET" || request.method === "POST")) {
|
|
5630
|
+
const jobName = decodeURIComponent(cronMatch[1] ?? "");
|
|
5631
|
+
const cronJob = cronJobs[jobName];
|
|
5632
|
+
if (!cronJob) {
|
|
5633
|
+
writeJson(response, 404, {
|
|
5634
|
+
code: "CRON_JOB_NOT_FOUND",
|
|
5635
|
+
message: `Cron job "${jobName}" is not defined in AGENT.md`
|
|
5636
|
+
});
|
|
5637
|
+
return;
|
|
5638
|
+
}
|
|
5639
|
+
const urlObj = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
|
|
5640
|
+
const continueConversationId = urlObj.searchParams.get("continue");
|
|
5641
|
+
const continuationCount = Number(urlObj.searchParams.get("continuation") ?? "0");
|
|
5642
|
+
const maxContinuations = 5;
|
|
5643
|
+
if (continuationCount >= maxContinuations) {
|
|
5644
|
+
writeJson(response, 200, {
|
|
5645
|
+
conversationId: continueConversationId,
|
|
5646
|
+
status: "max_continuations_reached",
|
|
5647
|
+
continuations: continuationCount
|
|
5648
|
+
});
|
|
5649
|
+
return;
|
|
5650
|
+
}
|
|
5651
|
+
const cronOwnerId = ownerId;
|
|
5652
|
+
const start = Date.now();
|
|
5653
|
+
try {
|
|
5654
|
+
let conversation;
|
|
5655
|
+
let historyMessages = [];
|
|
5656
|
+
if (continueConversationId) {
|
|
5657
|
+
conversation = await conversationStore.get(continueConversationId);
|
|
5658
|
+
if (!conversation) {
|
|
5659
|
+
writeJson(response, 404, {
|
|
5660
|
+
code: "CONVERSATION_NOT_FOUND",
|
|
5661
|
+
message: "Continuation conversation not found"
|
|
5662
|
+
});
|
|
5663
|
+
return;
|
|
5664
|
+
}
|
|
5665
|
+
historyMessages = [...conversation.messages];
|
|
5666
|
+
} else {
|
|
5667
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
5668
|
+
conversation = await conversationStore.create(
|
|
5669
|
+
cronOwnerId,
|
|
5670
|
+
`[cron] ${jobName} ${timestamp}`
|
|
5671
|
+
);
|
|
5672
|
+
}
|
|
5673
|
+
const abortController = new AbortController();
|
|
5674
|
+
let assistantResponse = "";
|
|
5675
|
+
let latestRunId = "";
|
|
5676
|
+
const toolTimeline = [];
|
|
5677
|
+
const sections = [];
|
|
5678
|
+
let currentTools = [];
|
|
5679
|
+
let currentText = "";
|
|
5680
|
+
let runResult = {
|
|
5681
|
+
status: "completed",
|
|
5682
|
+
steps: 0
|
|
5683
|
+
};
|
|
5684
|
+
const platformMaxDurationSec = Number(process.env.PONCHO_MAX_DURATION) || 0;
|
|
5685
|
+
const softDeadlineMs = platformMaxDurationSec > 0 ? platformMaxDurationSec * 800 : 0;
|
|
5686
|
+
for await (const event of harness.runWithTelemetry({
|
|
5687
|
+
task: cronJob.task,
|
|
5688
|
+
parameters: { __activeConversationId: conversation.conversationId },
|
|
5689
|
+
messages: historyMessages,
|
|
5690
|
+
abortSignal: abortController.signal
|
|
5691
|
+
})) {
|
|
5692
|
+
if (event.type === "run:started") {
|
|
5693
|
+
latestRunId = event.runId;
|
|
5694
|
+
}
|
|
5695
|
+
if (event.type === "model:chunk") {
|
|
5696
|
+
if (currentTools.length > 0) {
|
|
5697
|
+
sections.push({ type: "tools", content: currentTools });
|
|
5698
|
+
currentTools = [];
|
|
5699
|
+
}
|
|
5700
|
+
assistantResponse += event.content;
|
|
5701
|
+
currentText += event.content;
|
|
5702
|
+
}
|
|
5703
|
+
if (event.type === "tool:started") {
|
|
5704
|
+
if (currentText.length > 0) {
|
|
5705
|
+
sections.push({ type: "text", content: currentText });
|
|
5706
|
+
currentText = "";
|
|
5707
|
+
}
|
|
5708
|
+
const toolText = `- start \`${event.tool}\``;
|
|
5709
|
+
toolTimeline.push(toolText);
|
|
5710
|
+
currentTools.push(toolText);
|
|
5711
|
+
}
|
|
5712
|
+
if (event.type === "tool:completed") {
|
|
5713
|
+
const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
|
|
5714
|
+
toolTimeline.push(toolText);
|
|
5715
|
+
currentTools.push(toolText);
|
|
5716
|
+
}
|
|
5717
|
+
if (event.type === "tool:error") {
|
|
5718
|
+
const toolText = `- error \`${event.tool}\`: ${event.error}`;
|
|
5719
|
+
toolTimeline.push(toolText);
|
|
5720
|
+
currentTools.push(toolText);
|
|
5721
|
+
}
|
|
5722
|
+
if (event.type === "run:completed") {
|
|
5723
|
+
runResult = {
|
|
5724
|
+
status: event.result.status,
|
|
5725
|
+
steps: event.result.steps,
|
|
5726
|
+
continuation: event.result.continuation
|
|
5727
|
+
};
|
|
5728
|
+
if (!assistantResponse && event.result.response) {
|
|
5729
|
+
assistantResponse = event.result.response;
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
await telemetry.emit(event);
|
|
5733
|
+
}
|
|
5734
|
+
if (currentTools.length > 0) {
|
|
5735
|
+
sections.push({ type: "tools", content: currentTools });
|
|
5736
|
+
}
|
|
5737
|
+
if (currentText.length > 0) {
|
|
5738
|
+
sections.push({ type: "text", content: currentText });
|
|
5739
|
+
currentText = "";
|
|
5740
|
+
}
|
|
5741
|
+
const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0;
|
|
5742
|
+
const assistantMetadata = toolTimeline.length > 0 || sections.length > 0 ? {
|
|
5743
|
+
toolActivity: [...toolTimeline],
|
|
5744
|
+
sections: sections.length > 0 ? sections : void 0
|
|
5745
|
+
} : void 0;
|
|
5746
|
+
const messages = [
|
|
5747
|
+
...historyMessages,
|
|
5748
|
+
...continueConversationId ? [] : [{ role: "user", content: cronJob.task }],
|
|
5749
|
+
...hasContent ? [{ role: "assistant", content: assistantResponse, metadata: assistantMetadata }] : []
|
|
5750
|
+
];
|
|
5751
|
+
conversation.messages = messages;
|
|
5752
|
+
conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
|
|
5753
|
+
conversation.updatedAt = Date.now();
|
|
5754
|
+
await conversationStore.update(conversation);
|
|
5755
|
+
if (runResult.continuation && softDeadlineMs > 0) {
|
|
5756
|
+
const selfUrl = `http://${request.headers.host ?? "localhost"}${pathname}?continue=${encodeURIComponent(conversation.conversationId)}&continuation=${continuationCount + 1}`;
|
|
5757
|
+
try {
|
|
5758
|
+
const selfRes = await fetch(selfUrl, {
|
|
5759
|
+
method: "GET",
|
|
5760
|
+
headers: request.headers.authorization ? { authorization: request.headers.authorization } : {}
|
|
5761
|
+
});
|
|
5762
|
+
const selfBody = await selfRes.json();
|
|
5763
|
+
writeJson(response, 200, {
|
|
5764
|
+
conversationId: conversation.conversationId,
|
|
5765
|
+
status: "continued",
|
|
5766
|
+
continuations: continuationCount + 1,
|
|
5767
|
+
finalResult: selfBody,
|
|
5768
|
+
duration: Date.now() - start
|
|
5769
|
+
});
|
|
5770
|
+
} catch (continueError) {
|
|
5771
|
+
writeJson(response, 200, {
|
|
5772
|
+
conversationId: conversation.conversationId,
|
|
5773
|
+
status: "continuation_failed",
|
|
5774
|
+
error: continueError instanceof Error ? continueError.message : "Unknown error",
|
|
5775
|
+
duration: Date.now() - start,
|
|
5776
|
+
steps: runResult.steps
|
|
5777
|
+
});
|
|
5778
|
+
}
|
|
5779
|
+
return;
|
|
5780
|
+
}
|
|
5781
|
+
writeJson(response, 200, {
|
|
5782
|
+
conversationId: conversation.conversationId,
|
|
5783
|
+
status: runResult.status,
|
|
5784
|
+
response: assistantResponse.slice(0, 500),
|
|
5785
|
+
duration: Date.now() - start,
|
|
5786
|
+
steps: runResult.steps
|
|
5787
|
+
});
|
|
5788
|
+
} catch (error) {
|
|
5789
|
+
writeJson(response, 500, {
|
|
5790
|
+
code: "CRON_RUN_ERROR",
|
|
5791
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
5792
|
+
});
|
|
5793
|
+
}
|
|
5794
|
+
return;
|
|
5795
|
+
}
|
|
5536
5796
|
writeJson(response, 404, { error: "Not found" });
|
|
5537
5797
|
};
|
|
5798
|
+
handler._harness = harness;
|
|
5799
|
+
handler._cronJobs = cronJobs;
|
|
5800
|
+
handler._conversationStore = conversationStore;
|
|
5801
|
+
return handler;
|
|
5538
5802
|
};
|
|
5539
5803
|
var startDevServer = async (port, options) => {
|
|
5804
|
+
const workingDir = options?.workingDir ?? process.cwd();
|
|
5540
5805
|
const handler = await createRequestHandler(options);
|
|
5541
5806
|
const server = createServer(handler);
|
|
5542
5807
|
const actualPort = await listenOnAvailablePort(server, port);
|
|
@@ -5546,7 +5811,141 @@ var startDevServer = async (port, options) => {
|
|
|
5546
5811
|
}
|
|
5547
5812
|
process.stdout.write(`Poncho dev server running at http://localhost:${actualPort}
|
|
5548
5813
|
`);
|
|
5814
|
+
await checkVercelCronDrift(workingDir);
|
|
5815
|
+
const { Cron } = await import("croner");
|
|
5816
|
+
let activeJobs = [];
|
|
5817
|
+
const scheduleCronJobs = (jobs) => {
|
|
5818
|
+
for (const job of activeJobs) {
|
|
5819
|
+
job.stop();
|
|
5820
|
+
}
|
|
5821
|
+
activeJobs = [];
|
|
5822
|
+
const entries = Object.entries(jobs);
|
|
5823
|
+
if (entries.length === 0) return;
|
|
5824
|
+
const harness = handler._harness;
|
|
5825
|
+
const store = handler._conversationStore;
|
|
5826
|
+
if (!harness || !store) return;
|
|
5827
|
+
for (const [jobName, config] of entries) {
|
|
5828
|
+
const job = new Cron(
|
|
5829
|
+
config.schedule,
|
|
5830
|
+
{ timezone: config.timezone ?? "UTC" },
|
|
5831
|
+
async () => {
|
|
5832
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
5833
|
+
process.stdout.write(`[cron] ${jobName} started at ${timestamp}
|
|
5834
|
+
`);
|
|
5835
|
+
const start = Date.now();
|
|
5836
|
+
try {
|
|
5837
|
+
const conversation = await store.create(
|
|
5838
|
+
"local-owner",
|
|
5839
|
+
`[cron] ${jobName} ${timestamp}`
|
|
5840
|
+
);
|
|
5841
|
+
let assistantResponse = "";
|
|
5842
|
+
let steps = 0;
|
|
5843
|
+
const toolTimeline = [];
|
|
5844
|
+
const sections = [];
|
|
5845
|
+
let currentTools = [];
|
|
5846
|
+
let currentText = "";
|
|
5847
|
+
for await (const event of harness.runWithTelemetry({
|
|
5848
|
+
task: config.task,
|
|
5849
|
+
parameters: { __activeConversationId: conversation.conversationId },
|
|
5850
|
+
messages: []
|
|
5851
|
+
})) {
|
|
5852
|
+
if (event.type === "model:chunk") {
|
|
5853
|
+
if (currentTools.length > 0) {
|
|
5854
|
+
sections.push({ type: "tools", content: currentTools });
|
|
5855
|
+
currentTools = [];
|
|
5856
|
+
}
|
|
5857
|
+
assistantResponse += event.content;
|
|
5858
|
+
currentText += event.content;
|
|
5859
|
+
}
|
|
5860
|
+
if (event.type === "tool:started") {
|
|
5861
|
+
if (currentText.length > 0) {
|
|
5862
|
+
sections.push({ type: "text", content: currentText });
|
|
5863
|
+
currentText = "";
|
|
5864
|
+
}
|
|
5865
|
+
const toolText = `- start \`${event.tool}\``;
|
|
5866
|
+
toolTimeline.push(toolText);
|
|
5867
|
+
currentTools.push(toolText);
|
|
5868
|
+
}
|
|
5869
|
+
if (event.type === "tool:completed") {
|
|
5870
|
+
const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
|
|
5871
|
+
toolTimeline.push(toolText);
|
|
5872
|
+
currentTools.push(toolText);
|
|
5873
|
+
}
|
|
5874
|
+
if (event.type === "tool:error") {
|
|
5875
|
+
const toolText = `- error \`${event.tool}\`: ${event.error}`;
|
|
5876
|
+
toolTimeline.push(toolText);
|
|
5877
|
+
currentTools.push(toolText);
|
|
5878
|
+
}
|
|
5879
|
+
if (event.type === "run:completed") {
|
|
5880
|
+
steps = event.result.steps;
|
|
5881
|
+
if (!assistantResponse && event.result.response) {
|
|
5882
|
+
assistantResponse = event.result.response;
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
if (currentTools.length > 0) {
|
|
5887
|
+
sections.push({ type: "tools", content: currentTools });
|
|
5888
|
+
}
|
|
5889
|
+
if (currentText.length > 0) {
|
|
5890
|
+
sections.push({ type: "text", content: currentText });
|
|
5891
|
+
}
|
|
5892
|
+
const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0;
|
|
5893
|
+
const assistantMetadata = toolTimeline.length > 0 || sections.length > 0 ? {
|
|
5894
|
+
toolActivity: [...toolTimeline],
|
|
5895
|
+
sections: sections.length > 0 ? sections : void 0
|
|
5896
|
+
} : void 0;
|
|
5897
|
+
conversation.messages = [
|
|
5898
|
+
{ role: "user", content: config.task },
|
|
5899
|
+
...hasContent ? [{ role: "assistant", content: assistantResponse, metadata: assistantMetadata }] : []
|
|
5900
|
+
];
|
|
5901
|
+
conversation.updatedAt = Date.now();
|
|
5902
|
+
await store.update(conversation);
|
|
5903
|
+
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
5904
|
+
process.stdout.write(
|
|
5905
|
+
`[cron] ${jobName} completed in ${elapsed}s (${steps} steps)
|
|
5906
|
+
`
|
|
5907
|
+
);
|
|
5908
|
+
} catch (error) {
|
|
5909
|
+
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
5910
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
5911
|
+
process.stderr.write(
|
|
5912
|
+
`[cron] ${jobName} failed after ${elapsed}s: ${msg}
|
|
5913
|
+
`
|
|
5914
|
+
);
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5917
|
+
);
|
|
5918
|
+
activeJobs.push(job);
|
|
5919
|
+
}
|
|
5920
|
+
process.stdout.write(
|
|
5921
|
+
`[cron] Scheduled ${entries.length} job${entries.length === 1 ? "" : "s"}: ${entries.map(([n]) => n).join(", ")}
|
|
5922
|
+
`
|
|
5923
|
+
);
|
|
5924
|
+
};
|
|
5925
|
+
const initialCronJobs = handler._cronJobs ?? {};
|
|
5926
|
+
scheduleCronJobs(initialCronJobs);
|
|
5927
|
+
const agentMdPath = resolve3(workingDir, "AGENT.md");
|
|
5928
|
+
let reloadDebounce = null;
|
|
5929
|
+
const watcher = fsWatch(agentMdPath, () => {
|
|
5930
|
+
if (reloadDebounce) clearTimeout(reloadDebounce);
|
|
5931
|
+
reloadDebounce = setTimeout(async () => {
|
|
5932
|
+
try {
|
|
5933
|
+
const agentMd = await readFile3(agentMdPath, "utf8");
|
|
5934
|
+
const parsed = parseAgentMarkdown(agentMd);
|
|
5935
|
+
const newJobs = parsed.frontmatter.cron ?? {};
|
|
5936
|
+
handler._cronJobs = newJobs;
|
|
5937
|
+
scheduleCronJobs(newJobs);
|
|
5938
|
+
process.stdout.write(`[cron] Reloaded: ${Object.keys(newJobs).length} jobs scheduled
|
|
5939
|
+
`);
|
|
5940
|
+
} catch {
|
|
5941
|
+
}
|
|
5942
|
+
}, 500);
|
|
5943
|
+
});
|
|
5549
5944
|
const shutdown = () => {
|
|
5945
|
+
watcher.close();
|
|
5946
|
+
for (const job of activeJobs) {
|
|
5947
|
+
job.stop();
|
|
5948
|
+
}
|
|
5550
5949
|
server.close();
|
|
5551
5950
|
server.closeAllConnections?.();
|
|
5552
5951
|
process.exit(0);
|
|
@@ -5637,7 +6036,7 @@ var runInteractive = async (workingDir, params) => {
|
|
|
5637
6036
|
await harness.initialize();
|
|
5638
6037
|
const identity = await ensureAgentIdentity2(workingDir);
|
|
5639
6038
|
try {
|
|
5640
|
-
const { runInteractiveInk } = await import("./run-interactive-ink-
|
|
6039
|
+
const { runInteractiveInk } = await import("./run-interactive-ink-64QEOUXL.js");
|
|
5641
6040
|
await runInteractiveInk({
|
|
5642
6041
|
harness,
|
|
5643
6042
|
params,
|
|
@@ -5998,6 +6397,9 @@ Test summary: ${passed} passed, ${failed} failed
|
|
|
5998
6397
|
};
|
|
5999
6398
|
var buildTarget = async (workingDir, target, options) => {
|
|
6000
6399
|
const normalizedTarget = normalizeDeployTarget2(target);
|
|
6400
|
+
if (normalizedTarget === "vercel" && !options?.force) {
|
|
6401
|
+
await checkVercelCronDrift(workingDir);
|
|
6402
|
+
}
|
|
6001
6403
|
const writtenPaths = await scaffoldDeployTarget(workingDir, normalizedTarget, {
|
|
6002
6404
|
force: options?.force
|
|
6003
6405
|
});
|
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IncomingMessage, ServerResponse, Server } from 'node:http';
|
|
2
|
+
import { AgentHarness, CronJobConfig, ConversationStore } from '@poncho-ai/harness';
|
|
2
3
|
import { Command } from 'commander';
|
|
3
4
|
|
|
4
5
|
type InitOnboardingOptions = {
|
|
@@ -17,7 +18,11 @@ declare const initProject: (projectName: string, options?: {
|
|
|
17
18
|
envExampleOverride?: string;
|
|
18
19
|
}) => Promise<void>;
|
|
19
20
|
declare const updateAgentGuidance: (workingDir: string) => Promise<boolean>;
|
|
20
|
-
type RequestHandler = (request: IncomingMessage, response: ServerResponse) => Promise<void
|
|
21
|
+
type RequestHandler = ((request: IncomingMessage, response: ServerResponse) => Promise<void>) & {
|
|
22
|
+
_harness?: AgentHarness;
|
|
23
|
+
_cronJobs?: Record<string, CronJobConfig>;
|
|
24
|
+
_conversationStore?: ConversationStore;
|
|
25
|
+
};
|
|
21
26
|
declare const createRequestHandler: (options?: {
|
|
22
27
|
workingDir?: string;
|
|
23
28
|
}) => Promise<RequestHandler>;
|
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.12.0",
|
|
4
4
|
"description": "CLI for building and deploying AI agents",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,13 +20,14 @@
|
|
|
20
20
|
"@inquirer/prompts": "^8.2.0",
|
|
21
21
|
"busboy": "^1.6.0",
|
|
22
22
|
"commander": "^12.0.0",
|
|
23
|
+
"croner": "^10.0.1",
|
|
23
24
|
"dotenv": "^16.4.0",
|
|
24
25
|
"ink": "^6.7.0",
|
|
25
26
|
"marked": "^17.0.2",
|
|
26
27
|
"react": "^19.2.4",
|
|
27
28
|
"react-devtools-core": "^6.1.5",
|
|
28
29
|
"yaml": "^2.8.1",
|
|
29
|
-
"@poncho-ai/harness": "0.
|
|
30
|
+
"@poncho-ai/harness": "0.13.0",
|
|
30
31
|
"@poncho-ai/sdk": "1.0.0"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { access, cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { existsSync, watch as fsWatch } from "node:fs";
|
|
4
4
|
import {
|
|
5
5
|
createServer,
|
|
6
6
|
type IncomingMessage,
|
|
@@ -20,7 +20,9 @@ import {
|
|
|
20
20
|
ensureAgentIdentity,
|
|
21
21
|
generateAgentId,
|
|
22
22
|
loadPonchoConfig,
|
|
23
|
+
parseAgentMarkdown,
|
|
23
24
|
resolveStateConfig,
|
|
25
|
+
type CronJobConfig,
|
|
24
26
|
type PonchoConfig,
|
|
25
27
|
type ConversationStore,
|
|
26
28
|
type UploadStore,
|
|
@@ -549,6 +551,22 @@ ${name}/
|
|
|
549
551
|
└── fetch-page.ts
|
|
550
552
|
\`\`\`
|
|
551
553
|
|
|
554
|
+
## Cron Jobs
|
|
555
|
+
|
|
556
|
+
Define scheduled tasks in \`AGENT.md\` frontmatter:
|
|
557
|
+
|
|
558
|
+
\`\`\`yaml
|
|
559
|
+
cron:
|
|
560
|
+
daily-report:
|
|
561
|
+
schedule: "0 9 * * *"
|
|
562
|
+
task: "Generate the daily sales report"
|
|
563
|
+
\`\`\`
|
|
564
|
+
|
|
565
|
+
- \`poncho dev\`: jobs run via an in-process scheduler.
|
|
566
|
+
- \`poncho build vercel\`: generates \`vercel.json\` cron entries.
|
|
567
|
+
- Docker/Fly.io: scheduler runs automatically.
|
|
568
|
+
- Trigger manually: \`curl http://localhost:3000/api/cron/daily-report\`
|
|
569
|
+
|
|
552
570
|
## Deployment
|
|
553
571
|
|
|
554
572
|
\`\`\`bash
|
|
@@ -796,6 +814,54 @@ const ensureRuntimeCliDependency = async (
|
|
|
796
814
|
return { paths: [relative(projectDir, packageJsonPath)], addedDeps };
|
|
797
815
|
};
|
|
798
816
|
|
|
817
|
+
const checkVercelCronDrift = async (projectDir: string): Promise<void> => {
|
|
818
|
+
const vercelJsonPath = resolve(projectDir, "vercel.json");
|
|
819
|
+
try {
|
|
820
|
+
await access(vercelJsonPath);
|
|
821
|
+
} catch {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
let agentCrons: Record<string, CronJobConfig> = {};
|
|
825
|
+
try {
|
|
826
|
+
const agentMd = await readFile(resolve(projectDir, "AGENT.md"), "utf8");
|
|
827
|
+
const parsed = parseAgentMarkdown(agentMd);
|
|
828
|
+
agentCrons = parsed.frontmatter.cron ?? {};
|
|
829
|
+
} catch {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
let vercelCrons: Array<{ path: string; schedule: string }> = [];
|
|
833
|
+
try {
|
|
834
|
+
const raw = await readFile(vercelJsonPath, "utf8");
|
|
835
|
+
const vercelConfig = JSON.parse(raw) as { crons?: Array<{ path: string; schedule: string }> };
|
|
836
|
+
vercelCrons = vercelConfig.crons ?? [];
|
|
837
|
+
} catch {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const vercelCronMap = new Map(
|
|
841
|
+
vercelCrons
|
|
842
|
+
.filter((c) => c.path.startsWith("/api/cron/"))
|
|
843
|
+
.map((c) => [decodeURIComponent(c.path.replace("/api/cron/", "")), c.schedule]),
|
|
844
|
+
);
|
|
845
|
+
const diffs: string[] = [];
|
|
846
|
+
for (const [jobName, job] of Object.entries(agentCrons)) {
|
|
847
|
+
const existing = vercelCronMap.get(jobName);
|
|
848
|
+
if (!existing) {
|
|
849
|
+
diffs.push(` + missing job "${jobName}" (${job.schedule})`);
|
|
850
|
+
} else if (existing !== job.schedule) {
|
|
851
|
+
diffs.push(` ~ "${jobName}" schedule changed: "${existing}" → "${job.schedule}"`);
|
|
852
|
+
}
|
|
853
|
+
vercelCronMap.delete(jobName);
|
|
854
|
+
}
|
|
855
|
+
for (const [jobName, schedule] of vercelCronMap) {
|
|
856
|
+
diffs.push(` - removed job "${jobName}" (${schedule})`);
|
|
857
|
+
}
|
|
858
|
+
if (diffs.length > 0) {
|
|
859
|
+
process.stderr.write(
|
|
860
|
+
`\u26A0 vercel.json crons are out of sync with AGENT.md:\n${diffs.join("\n")}\n Run \`poncho build vercel --force\` to update.\n\n`,
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
799
865
|
const scaffoldDeployTarget = async (
|
|
800
866
|
projectDir: string,
|
|
801
867
|
target: DeployScaffoldTarget,
|
|
@@ -835,22 +901,37 @@ export default async function handler(req, res) {
|
|
|
835
901
|
{ force: options?.force, writtenPaths, baseDir: projectDir },
|
|
836
902
|
);
|
|
837
903
|
const vercelConfigPath = resolve(projectDir, "vercel.json");
|
|
904
|
+
let vercelCrons: Array<{ path: string; schedule: string }> | undefined;
|
|
905
|
+
try {
|
|
906
|
+
const agentMd = await readFile(resolve(projectDir, "AGENT.md"), "utf8");
|
|
907
|
+
const parsed = parseAgentMarkdown(agentMd);
|
|
908
|
+
if (parsed.frontmatter.cron) {
|
|
909
|
+
vercelCrons = Object.entries(parsed.frontmatter.cron).map(
|
|
910
|
+
([jobName, job]) => ({
|
|
911
|
+
path: `/api/cron/${encodeURIComponent(jobName)}`,
|
|
912
|
+
schedule: job.schedule,
|
|
913
|
+
}),
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
} catch {
|
|
917
|
+
// AGENT.md may not exist yet during init; skip cron generation
|
|
918
|
+
}
|
|
919
|
+
const vercelConfig: Record<string, unknown> = {
|
|
920
|
+
version: 2,
|
|
921
|
+
functions: {
|
|
922
|
+
"api/index.mjs": {
|
|
923
|
+
includeFiles:
|
|
924
|
+
"{AGENT.md,poncho.config.js,skills/**,tests/**,node_modules/.pnpm/marked@*/node_modules/marked/lib/marked.umd.js}",
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
routes: [{ src: "/(.*)", dest: "/api/index.mjs" }],
|
|
928
|
+
};
|
|
929
|
+
if (vercelCrons && vercelCrons.length > 0) {
|
|
930
|
+
vercelConfig.crons = vercelCrons;
|
|
931
|
+
}
|
|
838
932
|
await writeScaffoldFile(
|
|
839
933
|
vercelConfigPath,
|
|
840
|
-
`${JSON.stringify(
|
|
841
|
-
{
|
|
842
|
-
version: 2,
|
|
843
|
-
functions: {
|
|
844
|
-
"api/index.mjs": {
|
|
845
|
-
includeFiles:
|
|
846
|
-
"{AGENT.md,poncho.config.js,skills/**,tests/**,node_modules/.pnpm/marked@*/node_modules/marked/lib/marked.umd.js}",
|
|
847
|
-
},
|
|
848
|
-
},
|
|
849
|
-
routes: [{ src: "/(.*)", dest: "/api/index.mjs" }],
|
|
850
|
-
},
|
|
851
|
-
null,
|
|
852
|
-
2,
|
|
853
|
-
)}\n`,
|
|
934
|
+
`${JSON.stringify(vercelConfig, null, 2)}\n`,
|
|
854
935
|
{ force: options?.force, writtenPaths, baseDir: projectDir },
|
|
855
936
|
);
|
|
856
937
|
} else if (target === "docker") {
|
|
@@ -892,6 +973,11 @@ export const handler = async (event = {}) => {
|
|
|
892
973
|
});
|
|
893
974
|
return { statusCode: 200, headers: { "content-type": "application/json" }, body };
|
|
894
975
|
};
|
|
976
|
+
|
|
977
|
+
// Cron jobs: use AWS EventBridge (CloudWatch Events) to trigger scheduled invocations.
|
|
978
|
+
// Create a rule for each cron job defined in AGENT.md that sends a GET request to:
|
|
979
|
+
// /api/cron/<jobName>
|
|
980
|
+
// Include the Authorization header with your PONCHO_AUTH_TOKEN as a Bearer token.
|
|
895
981
|
`,
|
|
896
982
|
{ force: options?.force, writtenPaths, baseDir: projectDir },
|
|
897
983
|
);
|
|
@@ -1131,10 +1217,14 @@ export const updateAgentGuidance = async (workingDir: string): Promise<boolean>
|
|
|
1131
1217
|
const formatSseEvent = (event: AgentEvent): string =>
|
|
1132
1218
|
`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
|
|
1133
1219
|
|
|
1134
|
-
export type RequestHandler = (
|
|
1220
|
+
export type RequestHandler = ((
|
|
1135
1221
|
request: IncomingMessage,
|
|
1136
1222
|
response: ServerResponse,
|
|
1137
|
-
) => Promise<void
|
|
1223
|
+
) => Promise<void>) & {
|
|
1224
|
+
_harness?: AgentHarness;
|
|
1225
|
+
_cronJobs?: Record<string, CronJobConfig>;
|
|
1226
|
+
_conversationStore?: ConversationStore;
|
|
1227
|
+
};
|
|
1138
1228
|
|
|
1139
1229
|
export const createRequestHandler = async (options?: {
|
|
1140
1230
|
workingDir?: string;
|
|
@@ -1145,6 +1235,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1145
1235
|
let agentName = "Agent";
|
|
1146
1236
|
let agentModelProvider = "anthropic";
|
|
1147
1237
|
let agentModelName = "claude-opus-4-5";
|
|
1238
|
+
let cronJobs: Record<string, CronJobConfig> = {};
|
|
1148
1239
|
try {
|
|
1149
1240
|
const agentMd = await readFile(resolve(workingDir, "AGENT.md"), "utf8");
|
|
1150
1241
|
const nameMatch = agentMd.match(/^name:\s*(.+)$/m);
|
|
@@ -1159,6 +1250,12 @@ export const createRequestHandler = async (options?: {
|
|
|
1159
1250
|
if (modelMatch?.[1]) {
|
|
1160
1251
|
agentModelName = modelMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
1161
1252
|
}
|
|
1253
|
+
try {
|
|
1254
|
+
const parsed = parseAgentMarkdown(agentMd);
|
|
1255
|
+
cronJobs = parsed.frontmatter.cron ?? {};
|
|
1256
|
+
} catch {
|
|
1257
|
+
// Cron parsing failure should not block the server
|
|
1258
|
+
}
|
|
1162
1259
|
} catch {}
|
|
1163
1260
|
const runOwners = new Map<string, string>();
|
|
1164
1261
|
const runConversations = new Map<string, string>();
|
|
@@ -1300,7 +1397,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1300
1397
|
return verifyPassphrase(match[1], authToken);
|
|
1301
1398
|
};
|
|
1302
1399
|
|
|
1303
|
-
|
|
1400
|
+
const handler: RequestHandler = async (request: IncomingMessage, response: ServerResponse) => {
|
|
1304
1401
|
if (!request.url || !request.method) {
|
|
1305
1402
|
writeJson(response, 404, { error: "Not found" });
|
|
1306
1403
|
return;
|
|
@@ -2105,14 +2202,214 @@ export const createRequestHandler = async (options?: {
|
|
|
2105
2202
|
return;
|
|
2106
2203
|
}
|
|
2107
2204
|
|
|
2205
|
+
// ── Cron job endpoint ──────────────────────────────────────────
|
|
2206
|
+
const cronMatch = pathname.match(/^\/api\/cron\/([^/]+)$/);
|
|
2207
|
+
if (cronMatch && (request.method === "GET" || request.method === "POST")) {
|
|
2208
|
+
const jobName = decodeURIComponent(cronMatch[1] ?? "");
|
|
2209
|
+
const cronJob = cronJobs[jobName];
|
|
2210
|
+
if (!cronJob) {
|
|
2211
|
+
writeJson(response, 404, {
|
|
2212
|
+
code: "CRON_JOB_NOT_FOUND",
|
|
2213
|
+
message: `Cron job "${jobName}" is not defined in AGENT.md`,
|
|
2214
|
+
});
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
const urlObj = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
|
|
2219
|
+
const continueConversationId = urlObj.searchParams.get("continue");
|
|
2220
|
+
const continuationCount = Number(urlObj.searchParams.get("continuation") ?? "0");
|
|
2221
|
+
const maxContinuations = 5;
|
|
2222
|
+
|
|
2223
|
+
if (continuationCount >= maxContinuations) {
|
|
2224
|
+
writeJson(response, 200, {
|
|
2225
|
+
conversationId: continueConversationId,
|
|
2226
|
+
status: "max_continuations_reached",
|
|
2227
|
+
continuations: continuationCount,
|
|
2228
|
+
});
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
const cronOwnerId = ownerId;
|
|
2233
|
+
const start = Date.now();
|
|
2234
|
+
|
|
2235
|
+
try {
|
|
2236
|
+
let conversation;
|
|
2237
|
+
let historyMessages: Message[] = [];
|
|
2238
|
+
|
|
2239
|
+
if (continueConversationId) {
|
|
2240
|
+
conversation = await conversationStore.get(continueConversationId);
|
|
2241
|
+
if (!conversation) {
|
|
2242
|
+
writeJson(response, 404, {
|
|
2243
|
+
code: "CONVERSATION_NOT_FOUND",
|
|
2244
|
+
message: "Continuation conversation not found",
|
|
2245
|
+
});
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
historyMessages = [...conversation.messages];
|
|
2249
|
+
} else {
|
|
2250
|
+
const timestamp = new Date().toISOString();
|
|
2251
|
+
conversation = await conversationStore.create(
|
|
2252
|
+
cronOwnerId,
|
|
2253
|
+
`[cron] ${jobName} ${timestamp}`,
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
const abortController = new AbortController();
|
|
2258
|
+
let assistantResponse = "";
|
|
2259
|
+
let latestRunId = "";
|
|
2260
|
+
const toolTimeline: string[] = [];
|
|
2261
|
+
const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
|
|
2262
|
+
let currentTools: string[] = [];
|
|
2263
|
+
let currentText = "";
|
|
2264
|
+
let runResult: { status: string; steps: number; continuation?: boolean } = {
|
|
2265
|
+
status: "completed",
|
|
2266
|
+
steps: 0,
|
|
2267
|
+
};
|
|
2268
|
+
|
|
2269
|
+
const platformMaxDurationSec = Number(process.env.PONCHO_MAX_DURATION) || 0;
|
|
2270
|
+
const softDeadlineMs = platformMaxDurationSec > 0
|
|
2271
|
+
? platformMaxDurationSec * 800
|
|
2272
|
+
: 0;
|
|
2273
|
+
|
|
2274
|
+
for await (const event of harness.runWithTelemetry({
|
|
2275
|
+
task: cronJob.task,
|
|
2276
|
+
parameters: { __activeConversationId: conversation.conversationId },
|
|
2277
|
+
messages: historyMessages,
|
|
2278
|
+
abortSignal: abortController.signal,
|
|
2279
|
+
})) {
|
|
2280
|
+
if (event.type === "run:started") {
|
|
2281
|
+
latestRunId = event.runId;
|
|
2282
|
+
}
|
|
2283
|
+
if (event.type === "model:chunk") {
|
|
2284
|
+
if (currentTools.length > 0) {
|
|
2285
|
+
sections.push({ type: "tools", content: currentTools });
|
|
2286
|
+
currentTools = [];
|
|
2287
|
+
}
|
|
2288
|
+
assistantResponse += event.content;
|
|
2289
|
+
currentText += event.content;
|
|
2290
|
+
}
|
|
2291
|
+
if (event.type === "tool:started") {
|
|
2292
|
+
if (currentText.length > 0) {
|
|
2293
|
+
sections.push({ type: "text", content: currentText });
|
|
2294
|
+
currentText = "";
|
|
2295
|
+
}
|
|
2296
|
+
const toolText = `- start \`${event.tool}\``;
|
|
2297
|
+
toolTimeline.push(toolText);
|
|
2298
|
+
currentTools.push(toolText);
|
|
2299
|
+
}
|
|
2300
|
+
if (event.type === "tool:completed") {
|
|
2301
|
+
const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
|
|
2302
|
+
toolTimeline.push(toolText);
|
|
2303
|
+
currentTools.push(toolText);
|
|
2304
|
+
}
|
|
2305
|
+
if (event.type === "tool:error") {
|
|
2306
|
+
const toolText = `- error \`${event.tool}\`: ${event.error}`;
|
|
2307
|
+
toolTimeline.push(toolText);
|
|
2308
|
+
currentTools.push(toolText);
|
|
2309
|
+
}
|
|
2310
|
+
if (event.type === "run:completed") {
|
|
2311
|
+
runResult = {
|
|
2312
|
+
status: event.result.status,
|
|
2313
|
+
steps: event.result.steps,
|
|
2314
|
+
continuation: event.result.continuation,
|
|
2315
|
+
};
|
|
2316
|
+
if (!assistantResponse && event.result.response) {
|
|
2317
|
+
assistantResponse = event.result.response;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
await telemetry.emit(event);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
if (currentTools.length > 0) {
|
|
2324
|
+
sections.push({ type: "tools", content: currentTools });
|
|
2325
|
+
}
|
|
2326
|
+
if (currentText.length > 0) {
|
|
2327
|
+
sections.push({ type: "text", content: currentText });
|
|
2328
|
+
currentText = "";
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// Persist the conversation
|
|
2332
|
+
const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0;
|
|
2333
|
+
const assistantMetadata =
|
|
2334
|
+
toolTimeline.length > 0 || sections.length > 0
|
|
2335
|
+
? ({
|
|
2336
|
+
toolActivity: [...toolTimeline],
|
|
2337
|
+
sections: sections.length > 0 ? sections : undefined,
|
|
2338
|
+
} as Message["metadata"])
|
|
2339
|
+
: undefined;
|
|
2340
|
+
const messages: Message[] = [
|
|
2341
|
+
...historyMessages,
|
|
2342
|
+
...(continueConversationId
|
|
2343
|
+
? []
|
|
2344
|
+
: [{ role: "user" as const, content: cronJob.task }]),
|
|
2345
|
+
...(hasContent
|
|
2346
|
+
? [{ role: "assistant" as const, content: assistantResponse, metadata: assistantMetadata }]
|
|
2347
|
+
: []),
|
|
2348
|
+
];
|
|
2349
|
+
conversation.messages = messages;
|
|
2350
|
+
conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
|
|
2351
|
+
conversation.updatedAt = Date.now();
|
|
2352
|
+
await conversationStore.update(conversation);
|
|
2353
|
+
|
|
2354
|
+
// Self-continuation for serverless timeouts
|
|
2355
|
+
if (runResult.continuation && softDeadlineMs > 0) {
|
|
2356
|
+
const selfUrl = `http://${request.headers.host ?? "localhost"}${pathname}?continue=${encodeURIComponent(conversation.conversationId)}&continuation=${continuationCount + 1}`;
|
|
2357
|
+
try {
|
|
2358
|
+
const selfRes = await fetch(selfUrl, {
|
|
2359
|
+
method: "GET",
|
|
2360
|
+
headers: request.headers.authorization
|
|
2361
|
+
? { authorization: request.headers.authorization }
|
|
2362
|
+
: {},
|
|
2363
|
+
});
|
|
2364
|
+
const selfBody = await selfRes.json() as Record<string, unknown>;
|
|
2365
|
+
writeJson(response, 200, {
|
|
2366
|
+
conversationId: conversation.conversationId,
|
|
2367
|
+
status: "continued",
|
|
2368
|
+
continuations: continuationCount + 1,
|
|
2369
|
+
finalResult: selfBody,
|
|
2370
|
+
duration: Date.now() - start,
|
|
2371
|
+
});
|
|
2372
|
+
} catch (continueError) {
|
|
2373
|
+
writeJson(response, 200, {
|
|
2374
|
+
conversationId: conversation.conversationId,
|
|
2375
|
+
status: "continuation_failed",
|
|
2376
|
+
error: continueError instanceof Error ? continueError.message : "Unknown error",
|
|
2377
|
+
duration: Date.now() - start,
|
|
2378
|
+
steps: runResult.steps,
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
writeJson(response, 200, {
|
|
2385
|
+
conversationId: conversation.conversationId,
|
|
2386
|
+
status: runResult.status,
|
|
2387
|
+
response: assistantResponse.slice(0, 500),
|
|
2388
|
+
duration: Date.now() - start,
|
|
2389
|
+
steps: runResult.steps,
|
|
2390
|
+
});
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
writeJson(response, 500, {
|
|
2393
|
+
code: "CRON_RUN_ERROR",
|
|
2394
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2108
2400
|
writeJson(response, 404, { error: "Not found" });
|
|
2109
2401
|
};
|
|
2402
|
+
handler._harness = harness;
|
|
2403
|
+
handler._cronJobs = cronJobs;
|
|
2404
|
+
handler._conversationStore = conversationStore;
|
|
2405
|
+
return handler;
|
|
2110
2406
|
};
|
|
2111
2407
|
|
|
2112
2408
|
export const startDevServer = async (
|
|
2113
2409
|
port: number,
|
|
2114
2410
|
options?: { workingDir?: string },
|
|
2115
2411
|
): Promise<Server> => {
|
|
2412
|
+
const workingDir = options?.workingDir ?? process.cwd();
|
|
2116
2413
|
const handler = await createRequestHandler(options);
|
|
2117
2414
|
const server = createServer(handler);
|
|
2118
2415
|
const actualPort = await listenOnAvailablePort(server, port);
|
|
@@ -2121,9 +2418,154 @@ export const startDevServer = async (
|
|
|
2121
2418
|
}
|
|
2122
2419
|
process.stdout.write(`Poncho dev server running at http://localhost:${actualPort}\n`);
|
|
2123
2420
|
|
|
2421
|
+
await checkVercelCronDrift(workingDir);
|
|
2422
|
+
|
|
2423
|
+
// ── Cron scheduler ─────────────────────────────────────────────
|
|
2424
|
+
const { Cron } = await import("croner");
|
|
2425
|
+
type CronJob = InstanceType<typeof Cron>;
|
|
2426
|
+
let activeJobs: CronJob[] = [];
|
|
2427
|
+
|
|
2428
|
+
const scheduleCronJobs = (jobs: Record<string, CronJobConfig>): void => {
|
|
2429
|
+
for (const job of activeJobs) {
|
|
2430
|
+
job.stop();
|
|
2431
|
+
}
|
|
2432
|
+
activeJobs = [];
|
|
2433
|
+
|
|
2434
|
+
const entries = Object.entries(jobs);
|
|
2435
|
+
if (entries.length === 0) return;
|
|
2436
|
+
|
|
2437
|
+
const harness = handler._harness;
|
|
2438
|
+
const store = handler._conversationStore;
|
|
2439
|
+
if (!harness || !store) return;
|
|
2440
|
+
|
|
2441
|
+
for (const [jobName, config] of entries) {
|
|
2442
|
+
const job = new Cron(
|
|
2443
|
+
config.schedule,
|
|
2444
|
+
{ timezone: config.timezone ?? "UTC" },
|
|
2445
|
+
async () => {
|
|
2446
|
+
const timestamp = new Date().toISOString();
|
|
2447
|
+
process.stdout.write(`[cron] ${jobName} started at ${timestamp}\n`);
|
|
2448
|
+
const start = Date.now();
|
|
2449
|
+
try {
|
|
2450
|
+
const conversation = await store.create(
|
|
2451
|
+
"local-owner",
|
|
2452
|
+
`[cron] ${jobName} ${timestamp}`,
|
|
2453
|
+
);
|
|
2454
|
+
let assistantResponse = "";
|
|
2455
|
+
let steps = 0;
|
|
2456
|
+
const toolTimeline: string[] = [];
|
|
2457
|
+
const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
|
|
2458
|
+
let currentTools: string[] = [];
|
|
2459
|
+
let currentText = "";
|
|
2460
|
+
for await (const event of harness.runWithTelemetry({
|
|
2461
|
+
task: config.task,
|
|
2462
|
+
parameters: { __activeConversationId: conversation.conversationId },
|
|
2463
|
+
messages: [],
|
|
2464
|
+
})) {
|
|
2465
|
+
if (event.type === "model:chunk") {
|
|
2466
|
+
if (currentTools.length > 0) {
|
|
2467
|
+
sections.push({ type: "tools", content: currentTools });
|
|
2468
|
+
currentTools = [];
|
|
2469
|
+
}
|
|
2470
|
+
assistantResponse += event.content;
|
|
2471
|
+
currentText += event.content;
|
|
2472
|
+
}
|
|
2473
|
+
if (event.type === "tool:started") {
|
|
2474
|
+
if (currentText.length > 0) {
|
|
2475
|
+
sections.push({ type: "text", content: currentText });
|
|
2476
|
+
currentText = "";
|
|
2477
|
+
}
|
|
2478
|
+
const toolText = `- start \`${event.tool}\``;
|
|
2479
|
+
toolTimeline.push(toolText);
|
|
2480
|
+
currentTools.push(toolText);
|
|
2481
|
+
}
|
|
2482
|
+
if (event.type === "tool:completed") {
|
|
2483
|
+
const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
|
|
2484
|
+
toolTimeline.push(toolText);
|
|
2485
|
+
currentTools.push(toolText);
|
|
2486
|
+
}
|
|
2487
|
+
if (event.type === "tool:error") {
|
|
2488
|
+
const toolText = `- error \`${event.tool}\`: ${event.error}`;
|
|
2489
|
+
toolTimeline.push(toolText);
|
|
2490
|
+
currentTools.push(toolText);
|
|
2491
|
+
}
|
|
2492
|
+
if (event.type === "run:completed") {
|
|
2493
|
+
steps = event.result.steps;
|
|
2494
|
+
if (!assistantResponse && event.result.response) {
|
|
2495
|
+
assistantResponse = event.result.response;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
if (currentTools.length > 0) {
|
|
2500
|
+
sections.push({ type: "tools", content: currentTools });
|
|
2501
|
+
}
|
|
2502
|
+
if (currentText.length > 0) {
|
|
2503
|
+
sections.push({ type: "text", content: currentText });
|
|
2504
|
+
}
|
|
2505
|
+
const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0;
|
|
2506
|
+
const assistantMetadata =
|
|
2507
|
+
toolTimeline.length > 0 || sections.length > 0
|
|
2508
|
+
? ({
|
|
2509
|
+
toolActivity: [...toolTimeline],
|
|
2510
|
+
sections: sections.length > 0 ? sections : undefined,
|
|
2511
|
+
} as Message["metadata"])
|
|
2512
|
+
: undefined;
|
|
2513
|
+
conversation.messages = [
|
|
2514
|
+
{ role: "user", content: config.task },
|
|
2515
|
+
...(hasContent
|
|
2516
|
+
? [{ role: "assistant" as const, content: assistantResponse, metadata: assistantMetadata }]
|
|
2517
|
+
: []),
|
|
2518
|
+
];
|
|
2519
|
+
conversation.updatedAt = Date.now();
|
|
2520
|
+
await store.update(conversation);
|
|
2521
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
2522
|
+
process.stdout.write(
|
|
2523
|
+
`[cron] ${jobName} completed in ${elapsed}s (${steps} steps)\n`,
|
|
2524
|
+
);
|
|
2525
|
+
} catch (error) {
|
|
2526
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
2527
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2528
|
+
process.stderr.write(
|
|
2529
|
+
`[cron] ${jobName} failed after ${elapsed}s: ${msg}\n`,
|
|
2530
|
+
);
|
|
2531
|
+
}
|
|
2532
|
+
},
|
|
2533
|
+
);
|
|
2534
|
+
activeJobs.push(job);
|
|
2535
|
+
}
|
|
2536
|
+
process.stdout.write(
|
|
2537
|
+
`[cron] Scheduled ${entries.length} job${entries.length === 1 ? "" : "s"}: ${entries.map(([n]) => n).join(", ")}\n`,
|
|
2538
|
+
);
|
|
2539
|
+
};
|
|
2540
|
+
|
|
2541
|
+
const initialCronJobs = handler._cronJobs ?? {};
|
|
2542
|
+
scheduleCronJobs(initialCronJobs);
|
|
2543
|
+
|
|
2544
|
+
// Hot-reload cron config when AGENT.md changes
|
|
2545
|
+
const agentMdPath = resolve(workingDir, "AGENT.md");
|
|
2546
|
+
let reloadDebounce: ReturnType<typeof setTimeout> | null = null;
|
|
2547
|
+
const watcher = fsWatch(agentMdPath, () => {
|
|
2548
|
+
if (reloadDebounce) clearTimeout(reloadDebounce);
|
|
2549
|
+
reloadDebounce = setTimeout(async () => {
|
|
2550
|
+
try {
|
|
2551
|
+
const agentMd = await readFile(agentMdPath, "utf8");
|
|
2552
|
+
const parsed = parseAgentMarkdown(agentMd);
|
|
2553
|
+
const newJobs = parsed.frontmatter.cron ?? {};
|
|
2554
|
+
handler._cronJobs = newJobs;
|
|
2555
|
+
scheduleCronJobs(newJobs);
|
|
2556
|
+
process.stdout.write(`[cron] Reloaded: ${Object.keys(newJobs).length} jobs scheduled\n`);
|
|
2557
|
+
} catch {
|
|
2558
|
+
// Parse errors during editing are expected; ignore
|
|
2559
|
+
}
|
|
2560
|
+
}, 500);
|
|
2561
|
+
});
|
|
2562
|
+
|
|
2124
2563
|
const shutdown = () => {
|
|
2564
|
+
watcher.close();
|
|
2565
|
+
for (const job of activeJobs) {
|
|
2566
|
+
job.stop();
|
|
2567
|
+
}
|
|
2125
2568
|
server.close();
|
|
2126
|
-
// Force-close any lingering connections so the port is freed immediately
|
|
2127
2569
|
server.closeAllConnections?.();
|
|
2128
2570
|
process.exit(0);
|
|
2129
2571
|
};
|
|
@@ -2746,6 +3188,9 @@ export const buildTarget = async (
|
|
|
2746
3188
|
options?: { force?: boolean },
|
|
2747
3189
|
): Promise<void> => {
|
|
2748
3190
|
const normalizedTarget = normalizeDeployTarget(target);
|
|
3191
|
+
if (normalizedTarget === "vercel" && !options?.force) {
|
|
3192
|
+
await checkVercelCronDrift(workingDir);
|
|
3193
|
+
}
|
|
2749
3194
|
const writtenPaths = await scaffoldDeployTarget(workingDir, normalizedTarget, {
|
|
2750
3195
|
force: options?.force,
|
|
2751
3196
|
});
|
|
@@ -126,6 +126,7 @@ export const consumeFirstRunIntro = async (
|
|
|
126
126
|
"- **Enable auth**: Add bearer tokens or custom authentication",
|
|
127
127
|
"- **Turn on telemetry**: Track usage with OpenTelemetry/OTLP",
|
|
128
128
|
"- **Add MCP servers**: Connect external tool servers",
|
|
129
|
+
"- **Schedule cron jobs**: Set up recurring tasks in AGENT.md frontmatter",
|
|
129
130
|
"",
|
|
130
131
|
"Just let me know what you'd like to work on!\n",
|
|
131
132
|
].join("\n");
|
package/test/cli.test.ts
CHANGED
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
initializeOnboardingMarker,
|
|
12
12
|
} from "../src/init-feature-context.js";
|
|
13
13
|
|
|
14
|
-
vi.mock("@poncho-ai/harness", () =>
|
|
14
|
+
vi.mock("@poncho-ai/harness", async (importOriginal) => {
|
|
15
|
+
const actual = await importOriginal<typeof import("@poncho-ai/harness")>();
|
|
16
|
+
return {
|
|
17
|
+
parseAgentMarkdown: actual.parseAgentMarkdown,
|
|
15
18
|
AgentHarness: class MockHarness {
|
|
16
19
|
async initialize(): Promise<void> {}
|
|
17
20
|
listTools(): Array<{ name: string; description: string }> {
|
|
@@ -160,7 +163,7 @@ vi.mock("@poncho-ai/harness", () => ({
|
|
|
160
163
|
get: async () => Buffer.from(""),
|
|
161
164
|
delete: async () => {},
|
|
162
165
|
}),
|
|
163
|
-
})
|
|
166
|
+
};});
|
|
164
167
|
|
|
165
168
|
import {
|
|
166
169
|
buildTarget,
|
|
@@ -920,6 +923,43 @@ describe("cli", () => {
|
|
|
920
923
|
expect(result.failed).toBe(0);
|
|
921
924
|
});
|
|
922
925
|
|
|
926
|
+
it("includes crons in vercel.json when AGENT.md has cron jobs", async () => {
|
|
927
|
+
await initProject("cron-agent", { workingDir: tempDir });
|
|
928
|
+
const projectDir = join(tempDir, "cron-agent");
|
|
929
|
+
const agentMdPath = join(projectDir, "AGENT.md");
|
|
930
|
+
const agentMd = await readFile(agentMdPath, "utf8");
|
|
931
|
+
// Insert cron block before the closing --- of frontmatter
|
|
932
|
+
const updatedAgentMd = agentMd.replace(
|
|
933
|
+
/^(---\n[\s\S]*?)(---\n)/m,
|
|
934
|
+
`$1cron:\n daily-report:\n schedule: "0 9 * * *"\n task: "Generate the daily report"\n health-check:\n schedule: "*/30 * * * *"\n task: "Check all APIs"\n$2`,
|
|
935
|
+
);
|
|
936
|
+
await writeFile(agentMdPath, updatedAgentMd, "utf8");
|
|
937
|
+
await buildTarget(projectDir, "vercel", { force: true });
|
|
938
|
+
const vercelConfig = JSON.parse(
|
|
939
|
+
await readFile(join(projectDir, "vercel.json"), "utf8"),
|
|
940
|
+
) as { crons?: Array<{ path: string; schedule: string }> };
|
|
941
|
+
expect(vercelConfig.crons).toBeDefined();
|
|
942
|
+
expect(vercelConfig.crons).toHaveLength(2);
|
|
943
|
+
expect(vercelConfig.crons).toContainEqual({
|
|
944
|
+
path: "/api/cron/daily-report",
|
|
945
|
+
schedule: "0 9 * * *",
|
|
946
|
+
});
|
|
947
|
+
expect(vercelConfig.crons).toContainEqual({
|
|
948
|
+
path: "/api/cron/health-check",
|
|
949
|
+
schedule: "*/30 * * * *",
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
it("omits crons from vercel.json when AGENT.md has no cron jobs", async () => {
|
|
954
|
+
await initProject("no-cron-agent", { workingDir: tempDir });
|
|
955
|
+
const projectDir = join(tempDir, "no-cron-agent");
|
|
956
|
+
await buildTarget(projectDir, "vercel", { force: true });
|
|
957
|
+
const vercelConfig = JSON.parse(
|
|
958
|
+
await readFile(join(projectDir, "vercel.json"), "utf8"),
|
|
959
|
+
) as { crons?: unknown };
|
|
960
|
+
expect(vercelConfig.crons).toBeUndefined();
|
|
961
|
+
});
|
|
962
|
+
|
|
923
963
|
it("fails on existing deploy files unless force is enabled", async () => {
|
|
924
964
|
await initProject("collision-agent", { workingDir: tempDir });
|
|
925
965
|
const projectDir = join(tempDir, "collision-agent");
|