@poncho-ai/cli 0.11.1 → 0.13.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.
@@ -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,9 +18,14 @@ 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";
25
+ import {
26
+ AgentBridge,
27
+ SlackAdapter
28
+ } from "@poncho-ai/messaging";
24
29
  import Busboy from "busboy";
25
30
  import { Command } from "commander";
26
31
  import dotenv from "dotenv";
@@ -3508,12 +3513,17 @@ var buildConfigFromOnboardingAnswers = (answers) => {
3508
3513
  enabled: telemetryEnabled
3509
3514
  };
3510
3515
  maybeSet(telemetry, "otlp", answers["telemetry.otlp"]);
3511
- return {
3516
+ const messagingPlatform = String(answers["messaging.platform"] ?? "none");
3517
+ const config = {
3512
3518
  mcp: [],
3513
3519
  auth,
3514
3520
  storage,
3515
3521
  telemetry
3516
3522
  };
3523
+ if (messagingPlatform !== "none") {
3524
+ config.messaging = [{ platform: messagingPlatform }];
3525
+ }
3526
+ return config;
3517
3527
  };
3518
3528
  var collectEnvVars = (answers) => {
3519
3529
  const envVars = /* @__PURE__ */ new Set();
@@ -3657,11 +3667,13 @@ var summarizeConfig = (config) => {
3657
3667
  const memoryEnabled = config?.storage?.memory?.enabled ?? config?.memory?.enabled ?? false;
3658
3668
  const authRequired = config?.auth?.required ?? false;
3659
3669
  const telemetryEnabled = config?.telemetry?.enabled ?? true;
3670
+ const messagingPlatforms = (config?.messaging ?? []).map((m) => m.platform);
3660
3671
  return [
3661
3672
  `storage: ${provider}`,
3662
3673
  `memory tools: ${memoryEnabled ? "enabled" : "disabled"}`,
3663
3674
  `auth: ${authRequired ? "required" : "not required"}`,
3664
- `telemetry: ${telemetryEnabled ? "enabled" : "disabled"}`
3675
+ `telemetry: ${telemetryEnabled ? "enabled" : "disabled"}`,
3676
+ ...messagingPlatforms.length > 0 ? [`messaging: ${messagingPlatforms.join(", ")}`] : []
3665
3677
  ];
3666
3678
  };
3667
3679
  var getOnboardingMarkerPath = async (workingDir) => {
@@ -3736,6 +3748,8 @@ var consumeFirstRunIntro = async (workingDir, input2) => {
3736
3748
  "- **Enable auth**: Add bearer tokens or custom authentication",
3737
3749
  "- **Turn on telemetry**: Track usage with OpenTelemetry/OTLP",
3738
3750
  "- **Add MCP servers**: Connect external tool servers",
3751
+ "- **Schedule cron jobs**: Set up recurring tasks in AGENT.md frontmatter",
3752
+ "- **Connect to Slack**: Set up messaging so users can @mention this agent in Slack",
3739
3753
  "",
3740
3754
  "Just let me know what you'd like to work on!\n"
3741
3755
  ].join("\n");
@@ -4163,6 +4177,40 @@ ${name}/
4163
4177
  \u2514\u2500\u2500 fetch-page.ts
4164
4178
  \`\`\`
4165
4179
 
4180
+ ## Cron Jobs
4181
+
4182
+ Define scheduled tasks in \`AGENT.md\` frontmatter:
4183
+
4184
+ \`\`\`yaml
4185
+ cron:
4186
+ daily-report:
4187
+ schedule: "0 9 * * *"
4188
+ task: "Generate the daily sales report"
4189
+ \`\`\`
4190
+
4191
+ - \`poncho dev\`: jobs run via an in-process scheduler.
4192
+ - \`poncho build vercel\`: generates \`vercel.json\` cron entries.
4193
+ - Docker/Fly.io: scheduler runs automatically.
4194
+ - Trigger manually: \`curl http://localhost:3000/api/cron/daily-report\`
4195
+
4196
+ ## Messaging (Slack)
4197
+
4198
+ Connect your agent to Slack so it responds to @mentions:
4199
+
4200
+ 1. Create a Slack App at [api.slack.com/apps](https://api.slack.com/apps)
4201
+ 2. Add Bot Token Scopes: \`app_mentions:read\`, \`chat:write\`, \`reactions:write\`
4202
+ 3. Enable Event Subscriptions, set Request URL to \`https://<your-url>/api/messaging/slack\`, subscribe to \`app_mention\`
4203
+ 4. Install to workspace, copy Bot Token and Signing Secret
4204
+ 5. Set env vars:
4205
+ \`\`\`
4206
+ SLACK_BOT_TOKEN=xoxb-...
4207
+ SLACK_SIGNING_SECRET=...
4208
+ \`\`\`
4209
+ 6. Add to \`poncho.config.js\`:
4210
+ \`\`\`javascript
4211
+ messaging: [{ platform: 'slack' }]
4212
+ \`\`\`
4213
+
4166
4214
  ## Deployment
4167
4215
 
4168
4216
  \`\`\`bash
@@ -4367,6 +4415,55 @@ var ensureRuntimeCliDependency = async (projectDir, cliVersion, config) => {
4367
4415
  `, "utf8");
4368
4416
  return { paths: [relative(projectDir, packageJsonPath)], addedDeps };
4369
4417
  };
4418
+ var checkVercelCronDrift = async (projectDir) => {
4419
+ const vercelJsonPath = resolve3(projectDir, "vercel.json");
4420
+ try {
4421
+ await access2(vercelJsonPath);
4422
+ } catch {
4423
+ return;
4424
+ }
4425
+ let agentCrons = {};
4426
+ try {
4427
+ const agentMd = await readFile3(resolve3(projectDir, "AGENT.md"), "utf8");
4428
+ const parsed = parseAgentMarkdown(agentMd);
4429
+ agentCrons = parsed.frontmatter.cron ?? {};
4430
+ } catch {
4431
+ return;
4432
+ }
4433
+ let vercelCrons = [];
4434
+ try {
4435
+ const raw = await readFile3(vercelJsonPath, "utf8");
4436
+ const vercelConfig = JSON.parse(raw);
4437
+ vercelCrons = vercelConfig.crons ?? [];
4438
+ } catch {
4439
+ return;
4440
+ }
4441
+ const vercelCronMap = new Map(
4442
+ vercelCrons.filter((c) => c.path.startsWith("/api/cron/")).map((c) => [decodeURIComponent(c.path.replace("/api/cron/", "")), c.schedule])
4443
+ );
4444
+ const diffs = [];
4445
+ for (const [jobName, job] of Object.entries(agentCrons)) {
4446
+ const existing = vercelCronMap.get(jobName);
4447
+ if (!existing) {
4448
+ diffs.push(` + missing job "${jobName}" (${job.schedule})`);
4449
+ } else if (existing !== job.schedule) {
4450
+ diffs.push(` ~ "${jobName}" schedule changed: "${existing}" \u2192 "${job.schedule}"`);
4451
+ }
4452
+ vercelCronMap.delete(jobName);
4453
+ }
4454
+ for (const [jobName, schedule] of vercelCronMap) {
4455
+ diffs.push(` - removed job "${jobName}" (${schedule})`);
4456
+ }
4457
+ if (diffs.length > 0) {
4458
+ process.stderr.write(
4459
+ `\u26A0 vercel.json crons are out of sync with AGENT.md:
4460
+ ${diffs.join("\n")}
4461
+ Run \`poncho build vercel --force\` to update.
4462
+
4463
+ `
4464
+ );
4465
+ }
4466
+ };
4370
4467
  var scaffoldDeployTarget = async (projectDir, target, options) => {
4371
4468
  const writtenPaths = [];
4372
4469
  const cliVersion = await readCliVersion();
@@ -4401,21 +4498,35 @@ export default async function handler(req, res) {
4401
4498
  { force: options?.force, writtenPaths, baseDir: projectDir }
4402
4499
  );
4403
4500
  const vercelConfigPath = resolve3(projectDir, "vercel.json");
4501
+ let vercelCrons;
4502
+ try {
4503
+ const agentMd = await readFile3(resolve3(projectDir, "AGENT.md"), "utf8");
4504
+ const parsed = parseAgentMarkdown(agentMd);
4505
+ if (parsed.frontmatter.cron) {
4506
+ vercelCrons = Object.entries(parsed.frontmatter.cron).map(
4507
+ ([jobName, job]) => ({
4508
+ path: `/api/cron/${encodeURIComponent(jobName)}`,
4509
+ schedule: job.schedule
4510
+ })
4511
+ );
4512
+ }
4513
+ } catch {
4514
+ }
4515
+ const vercelConfig = {
4516
+ version: 2,
4517
+ functions: {
4518
+ "api/index.mjs": {
4519
+ includeFiles: "{AGENT.md,poncho.config.js,skills/**,tests/**,node_modules/.pnpm/marked@*/node_modules/marked/lib/marked.umd.js}"
4520
+ }
4521
+ },
4522
+ routes: [{ src: "/(.*)", dest: "/api/index.mjs" }]
4523
+ };
4524
+ if (vercelCrons && vercelCrons.length > 0) {
4525
+ vercelConfig.crons = vercelCrons;
4526
+ }
4404
4527
  await writeScaffoldFile(
4405
4528
  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
- )}
4529
+ `${JSON.stringify(vercelConfig, null, 2)}
4419
4530
  `,
4420
4531
  { force: options?.force, writtenPaths, baseDir: projectDir }
4421
4532
  );
@@ -4458,6 +4569,11 @@ export const handler = async (event = {}) => {
4458
4569
  });
4459
4570
  return { statusCode: 200, headers: { "content-type": "application/json" }, body };
4460
4571
  };
4572
+
4573
+ // Cron jobs: use AWS EventBridge (CloudWatch Events) to trigger scheduled invocations.
4574
+ // Create a rule for each cron job defined in AGENT.md that sends a GET request to:
4575
+ // /api/cron/<jobName>
4576
+ // Include the Authorization header with your PONCHO_AUTH_TOKEN as a Bearer token.
4461
4577
  `,
4462
4578
  { force: options?.force, writtenPaths, baseDir: projectDir }
4463
4579
  );
@@ -4687,6 +4803,7 @@ var createRequestHandler = async (options) => {
4687
4803
  let agentName = "Agent";
4688
4804
  let agentModelProvider = "anthropic";
4689
4805
  let agentModelName = "claude-opus-4-5";
4806
+ let cronJobs = {};
4690
4807
  try {
4691
4808
  const agentMd = await readFile3(resolve3(workingDir, "AGENT.md"), "utf8");
4692
4809
  const nameMatch = agentMd.match(/^name:\s*(.+)$/m);
@@ -4701,6 +4818,11 @@ var createRequestHandler = async (options) => {
4701
4818
  if (modelMatch?.[1]) {
4702
4819
  agentModelName = modelMatch[1].trim().replace(/^["']|["']$/g, "");
4703
4820
  }
4821
+ try {
4822
+ const parsed = parseAgentMarkdown(agentMd);
4823
+ cronJobs = parsed.frontmatter.cron ?? {};
4824
+ } catch {
4825
+ }
4704
4826
  } catch {
4705
4827
  }
4706
4828
  const runOwners = /* @__PURE__ */ new Map();
@@ -4791,6 +4913,90 @@ var createRequestHandler = async (options) => {
4791
4913
  workingDir,
4792
4914
  agentId: identity.id
4793
4915
  });
4916
+ const messagingRoutes = /* @__PURE__ */ new Map();
4917
+ const messagingRouteRegistrar = (method, path, routeHandler) => {
4918
+ let byMethod = messagingRoutes.get(path);
4919
+ if (!byMethod) {
4920
+ byMethod = /* @__PURE__ */ new Map();
4921
+ messagingRoutes.set(path, byMethod);
4922
+ }
4923
+ byMethod.set(method, routeHandler);
4924
+ };
4925
+ const messagingRunner = {
4926
+ async getOrCreateConversation(conversationId, meta) {
4927
+ const existing = await conversationStore.get(conversationId);
4928
+ if (existing) {
4929
+ return { messages: existing.messages };
4930
+ }
4931
+ const now = Date.now();
4932
+ const conversation = {
4933
+ conversationId,
4934
+ title: meta.title ?? `${meta.platform} thread`,
4935
+ messages: [],
4936
+ ownerId: meta.ownerId,
4937
+ tenantId: null,
4938
+ createdAt: now,
4939
+ updatedAt: now
4940
+ };
4941
+ await conversationStore.update(conversation);
4942
+ return { messages: [] };
4943
+ },
4944
+ async run(conversationId, input2) {
4945
+ const output = await harness.runToCompletion({
4946
+ task: input2.task,
4947
+ messages: input2.messages
4948
+ });
4949
+ const response = output.result.response ?? "";
4950
+ const conversation = await conversationStore.get(conversationId);
4951
+ if (conversation) {
4952
+ conversation.messages = [
4953
+ ...input2.messages,
4954
+ { role: "user", content: input2.task },
4955
+ { role: "assistant", content: response }
4956
+ ];
4957
+ await conversationStore.update(conversation);
4958
+ }
4959
+ return { response };
4960
+ }
4961
+ };
4962
+ const messagingBridges = [];
4963
+ if (config?.messaging && config.messaging.length > 0) {
4964
+ let waitUntilHook;
4965
+ if (process.env.VERCEL) {
4966
+ try {
4967
+ const modName = "@vercel/functions";
4968
+ const mod = await import(
4969
+ /* webpackIgnore: true */
4970
+ modName
4971
+ );
4972
+ waitUntilHook = mod.waitUntil;
4973
+ } catch {
4974
+ }
4975
+ }
4976
+ for (const channelConfig of config.messaging) {
4977
+ if (channelConfig.platform === "slack") {
4978
+ const adapter = new SlackAdapter({
4979
+ botTokenEnv: channelConfig.botTokenEnv,
4980
+ signingSecretEnv: channelConfig.signingSecretEnv
4981
+ });
4982
+ const bridge = new AgentBridge({
4983
+ adapter,
4984
+ runner: messagingRunner,
4985
+ waitUntil: waitUntilHook
4986
+ });
4987
+ adapter.registerRoutes(messagingRouteRegistrar);
4988
+ try {
4989
+ await bridge.start();
4990
+ messagingBridges.push(bridge);
4991
+ console.log(` Slack messaging enabled at /api/messaging/slack`);
4992
+ } catch (err) {
4993
+ console.warn(
4994
+ ` Slack messaging disabled: ${err instanceof Error ? err.message : String(err)}`
4995
+ );
4996
+ }
4997
+ }
4998
+ }
4999
+ }
4794
5000
  const sessionStore = new SessionStore();
4795
5001
  const loginRateLimiter = new LoginRateLimiter();
4796
5002
  const authToken = process.env.PONCHO_AUTH_TOKEN ?? "";
@@ -4811,7 +5017,7 @@ var createRequestHandler = async (options) => {
4811
5017
  }
4812
5018
  return verifyPassphrase(match[1], authToken);
4813
5019
  };
4814
- return async (request, response) => {
5020
+ const handler = async (request, response) => {
4815
5021
  if (!request.url || !request.method) {
4816
5022
  writeJson(response, 404, { error: "Not found" });
4817
5023
  return;
@@ -4848,6 +5054,14 @@ var createRequestHandler = async (options) => {
4848
5054
  writeJson(response, 200, { status: "ok" });
4849
5055
  return;
4850
5056
  }
5057
+ const messagingByMethod = messagingRoutes.get(pathname ?? "");
5058
+ if (messagingByMethod) {
5059
+ const routeHandler = messagingByMethod.get(request.method ?? "");
5060
+ if (routeHandler) {
5061
+ await routeHandler(request, response);
5062
+ return;
5063
+ }
5064
+ }
4851
5065
  const cookies = parseCookies(request);
4852
5066
  const sessionId = cookies.poncho_session;
4853
5067
  const session = sessionId ? sessionStore.get(sessionId) : void 0;
@@ -5533,10 +5747,183 @@ var createRequestHandler = async (options) => {
5533
5747
  }
5534
5748
  return;
5535
5749
  }
5750
+ const cronMatch = pathname.match(/^\/api\/cron\/([^/]+)$/);
5751
+ if (cronMatch && (request.method === "GET" || request.method === "POST")) {
5752
+ const jobName = decodeURIComponent(cronMatch[1] ?? "");
5753
+ const cronJob = cronJobs[jobName];
5754
+ if (!cronJob) {
5755
+ writeJson(response, 404, {
5756
+ code: "CRON_JOB_NOT_FOUND",
5757
+ message: `Cron job "${jobName}" is not defined in AGENT.md`
5758
+ });
5759
+ return;
5760
+ }
5761
+ const urlObj = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
5762
+ const continueConversationId = urlObj.searchParams.get("continue");
5763
+ const continuationCount = Number(urlObj.searchParams.get("continuation") ?? "0");
5764
+ const maxContinuations = 5;
5765
+ if (continuationCount >= maxContinuations) {
5766
+ writeJson(response, 200, {
5767
+ conversationId: continueConversationId,
5768
+ status: "max_continuations_reached",
5769
+ continuations: continuationCount
5770
+ });
5771
+ return;
5772
+ }
5773
+ const cronOwnerId = ownerId;
5774
+ const start = Date.now();
5775
+ try {
5776
+ let conversation;
5777
+ let historyMessages = [];
5778
+ if (continueConversationId) {
5779
+ conversation = await conversationStore.get(continueConversationId);
5780
+ if (!conversation) {
5781
+ writeJson(response, 404, {
5782
+ code: "CONVERSATION_NOT_FOUND",
5783
+ message: "Continuation conversation not found"
5784
+ });
5785
+ return;
5786
+ }
5787
+ historyMessages = [...conversation.messages];
5788
+ } else {
5789
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
5790
+ conversation = await conversationStore.create(
5791
+ cronOwnerId,
5792
+ `[cron] ${jobName} ${timestamp}`
5793
+ );
5794
+ }
5795
+ const abortController = new AbortController();
5796
+ let assistantResponse = "";
5797
+ let latestRunId = "";
5798
+ const toolTimeline = [];
5799
+ const sections = [];
5800
+ let currentTools = [];
5801
+ let currentText = "";
5802
+ let runResult = {
5803
+ status: "completed",
5804
+ steps: 0
5805
+ };
5806
+ const platformMaxDurationSec = Number(process.env.PONCHO_MAX_DURATION) || 0;
5807
+ const softDeadlineMs = platformMaxDurationSec > 0 ? platformMaxDurationSec * 800 : 0;
5808
+ for await (const event of harness.runWithTelemetry({
5809
+ task: cronJob.task,
5810
+ parameters: { __activeConversationId: conversation.conversationId },
5811
+ messages: historyMessages,
5812
+ abortSignal: abortController.signal
5813
+ })) {
5814
+ if (event.type === "run:started") {
5815
+ latestRunId = event.runId;
5816
+ }
5817
+ if (event.type === "model:chunk") {
5818
+ if (currentTools.length > 0) {
5819
+ sections.push({ type: "tools", content: currentTools });
5820
+ currentTools = [];
5821
+ }
5822
+ assistantResponse += event.content;
5823
+ currentText += event.content;
5824
+ }
5825
+ if (event.type === "tool:started") {
5826
+ if (currentText.length > 0) {
5827
+ sections.push({ type: "text", content: currentText });
5828
+ currentText = "";
5829
+ }
5830
+ const toolText = `- start \`${event.tool}\``;
5831
+ toolTimeline.push(toolText);
5832
+ currentTools.push(toolText);
5833
+ }
5834
+ if (event.type === "tool:completed") {
5835
+ const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
5836
+ toolTimeline.push(toolText);
5837
+ currentTools.push(toolText);
5838
+ }
5839
+ if (event.type === "tool:error") {
5840
+ const toolText = `- error \`${event.tool}\`: ${event.error}`;
5841
+ toolTimeline.push(toolText);
5842
+ currentTools.push(toolText);
5843
+ }
5844
+ if (event.type === "run:completed") {
5845
+ runResult = {
5846
+ status: event.result.status,
5847
+ steps: event.result.steps,
5848
+ continuation: event.result.continuation
5849
+ };
5850
+ if (!assistantResponse && event.result.response) {
5851
+ assistantResponse = event.result.response;
5852
+ }
5853
+ }
5854
+ await telemetry.emit(event);
5855
+ }
5856
+ if (currentTools.length > 0) {
5857
+ sections.push({ type: "tools", content: currentTools });
5858
+ }
5859
+ if (currentText.length > 0) {
5860
+ sections.push({ type: "text", content: currentText });
5861
+ currentText = "";
5862
+ }
5863
+ const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0;
5864
+ const assistantMetadata = toolTimeline.length > 0 || sections.length > 0 ? {
5865
+ toolActivity: [...toolTimeline],
5866
+ sections: sections.length > 0 ? sections : void 0
5867
+ } : void 0;
5868
+ const messages = [
5869
+ ...historyMessages,
5870
+ ...continueConversationId ? [] : [{ role: "user", content: cronJob.task }],
5871
+ ...hasContent ? [{ role: "assistant", content: assistantResponse, metadata: assistantMetadata }] : []
5872
+ ];
5873
+ conversation.messages = messages;
5874
+ conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
5875
+ conversation.updatedAt = Date.now();
5876
+ await conversationStore.update(conversation);
5877
+ if (runResult.continuation && softDeadlineMs > 0) {
5878
+ const selfUrl = `http://${request.headers.host ?? "localhost"}${pathname}?continue=${encodeURIComponent(conversation.conversationId)}&continuation=${continuationCount + 1}`;
5879
+ try {
5880
+ const selfRes = await fetch(selfUrl, {
5881
+ method: "GET",
5882
+ headers: request.headers.authorization ? { authorization: request.headers.authorization } : {}
5883
+ });
5884
+ const selfBody = await selfRes.json();
5885
+ writeJson(response, 200, {
5886
+ conversationId: conversation.conversationId,
5887
+ status: "continued",
5888
+ continuations: continuationCount + 1,
5889
+ finalResult: selfBody,
5890
+ duration: Date.now() - start
5891
+ });
5892
+ } catch (continueError) {
5893
+ writeJson(response, 200, {
5894
+ conversationId: conversation.conversationId,
5895
+ status: "continuation_failed",
5896
+ error: continueError instanceof Error ? continueError.message : "Unknown error",
5897
+ duration: Date.now() - start,
5898
+ steps: runResult.steps
5899
+ });
5900
+ }
5901
+ return;
5902
+ }
5903
+ writeJson(response, 200, {
5904
+ conversationId: conversation.conversationId,
5905
+ status: runResult.status,
5906
+ response: assistantResponse.slice(0, 500),
5907
+ duration: Date.now() - start,
5908
+ steps: runResult.steps
5909
+ });
5910
+ } catch (error) {
5911
+ writeJson(response, 500, {
5912
+ code: "CRON_RUN_ERROR",
5913
+ message: error instanceof Error ? error.message : "Unknown error"
5914
+ });
5915
+ }
5916
+ return;
5917
+ }
5536
5918
  writeJson(response, 404, { error: "Not found" });
5537
5919
  };
5920
+ handler._harness = harness;
5921
+ handler._cronJobs = cronJobs;
5922
+ handler._conversationStore = conversationStore;
5923
+ return handler;
5538
5924
  };
5539
5925
  var startDevServer = async (port, options) => {
5926
+ const workingDir = options?.workingDir ?? process.cwd();
5540
5927
  const handler = await createRequestHandler(options);
5541
5928
  const server = createServer(handler);
5542
5929
  const actualPort = await listenOnAvailablePort(server, port);
@@ -5546,7 +5933,141 @@ var startDevServer = async (port, options) => {
5546
5933
  }
5547
5934
  process.stdout.write(`Poncho dev server running at http://localhost:${actualPort}
5548
5935
  `);
5936
+ await checkVercelCronDrift(workingDir);
5937
+ const { Cron } = await import("croner");
5938
+ let activeJobs = [];
5939
+ const scheduleCronJobs = (jobs) => {
5940
+ for (const job of activeJobs) {
5941
+ job.stop();
5942
+ }
5943
+ activeJobs = [];
5944
+ const entries = Object.entries(jobs);
5945
+ if (entries.length === 0) return;
5946
+ const harness = handler._harness;
5947
+ const store = handler._conversationStore;
5948
+ if (!harness || !store) return;
5949
+ for (const [jobName, config] of entries) {
5950
+ const job = new Cron(
5951
+ config.schedule,
5952
+ { timezone: config.timezone ?? "UTC" },
5953
+ async () => {
5954
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
5955
+ process.stdout.write(`[cron] ${jobName} started at ${timestamp}
5956
+ `);
5957
+ const start = Date.now();
5958
+ try {
5959
+ const conversation = await store.create(
5960
+ "local-owner",
5961
+ `[cron] ${jobName} ${timestamp}`
5962
+ );
5963
+ let assistantResponse = "";
5964
+ let steps = 0;
5965
+ const toolTimeline = [];
5966
+ const sections = [];
5967
+ let currentTools = [];
5968
+ let currentText = "";
5969
+ for await (const event of harness.runWithTelemetry({
5970
+ task: config.task,
5971
+ parameters: { __activeConversationId: conversation.conversationId },
5972
+ messages: []
5973
+ })) {
5974
+ if (event.type === "model:chunk") {
5975
+ if (currentTools.length > 0) {
5976
+ sections.push({ type: "tools", content: currentTools });
5977
+ currentTools = [];
5978
+ }
5979
+ assistantResponse += event.content;
5980
+ currentText += event.content;
5981
+ }
5982
+ if (event.type === "tool:started") {
5983
+ if (currentText.length > 0) {
5984
+ sections.push({ type: "text", content: currentText });
5985
+ currentText = "";
5986
+ }
5987
+ const toolText = `- start \`${event.tool}\``;
5988
+ toolTimeline.push(toolText);
5989
+ currentTools.push(toolText);
5990
+ }
5991
+ if (event.type === "tool:completed") {
5992
+ const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
5993
+ toolTimeline.push(toolText);
5994
+ currentTools.push(toolText);
5995
+ }
5996
+ if (event.type === "tool:error") {
5997
+ const toolText = `- error \`${event.tool}\`: ${event.error}`;
5998
+ toolTimeline.push(toolText);
5999
+ currentTools.push(toolText);
6000
+ }
6001
+ if (event.type === "run:completed") {
6002
+ steps = event.result.steps;
6003
+ if (!assistantResponse && event.result.response) {
6004
+ assistantResponse = event.result.response;
6005
+ }
6006
+ }
6007
+ }
6008
+ if (currentTools.length > 0) {
6009
+ sections.push({ type: "tools", content: currentTools });
6010
+ }
6011
+ if (currentText.length > 0) {
6012
+ sections.push({ type: "text", content: currentText });
6013
+ }
6014
+ const hasContent = assistantResponse.length > 0 || toolTimeline.length > 0;
6015
+ const assistantMetadata = toolTimeline.length > 0 || sections.length > 0 ? {
6016
+ toolActivity: [...toolTimeline],
6017
+ sections: sections.length > 0 ? sections : void 0
6018
+ } : void 0;
6019
+ conversation.messages = [
6020
+ { role: "user", content: config.task },
6021
+ ...hasContent ? [{ role: "assistant", content: assistantResponse, metadata: assistantMetadata }] : []
6022
+ ];
6023
+ conversation.updatedAt = Date.now();
6024
+ await store.update(conversation);
6025
+ const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
6026
+ process.stdout.write(
6027
+ `[cron] ${jobName} completed in ${elapsed}s (${steps} steps)
6028
+ `
6029
+ );
6030
+ } catch (error) {
6031
+ const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
6032
+ const msg = error instanceof Error ? error.message : String(error);
6033
+ process.stderr.write(
6034
+ `[cron] ${jobName} failed after ${elapsed}s: ${msg}
6035
+ `
6036
+ );
6037
+ }
6038
+ }
6039
+ );
6040
+ activeJobs.push(job);
6041
+ }
6042
+ process.stdout.write(
6043
+ `[cron] Scheduled ${entries.length} job${entries.length === 1 ? "" : "s"}: ${entries.map(([n]) => n).join(", ")}
6044
+ `
6045
+ );
6046
+ };
6047
+ const initialCronJobs = handler._cronJobs ?? {};
6048
+ scheduleCronJobs(initialCronJobs);
6049
+ const agentMdPath = resolve3(workingDir, "AGENT.md");
6050
+ let reloadDebounce = null;
6051
+ const watcher = fsWatch(agentMdPath, () => {
6052
+ if (reloadDebounce) clearTimeout(reloadDebounce);
6053
+ reloadDebounce = setTimeout(async () => {
6054
+ try {
6055
+ const agentMd = await readFile3(agentMdPath, "utf8");
6056
+ const parsed = parseAgentMarkdown(agentMd);
6057
+ const newJobs = parsed.frontmatter.cron ?? {};
6058
+ handler._cronJobs = newJobs;
6059
+ scheduleCronJobs(newJobs);
6060
+ process.stdout.write(`[cron] Reloaded: ${Object.keys(newJobs).length} jobs scheduled
6061
+ `);
6062
+ } catch {
6063
+ }
6064
+ }, 500);
6065
+ });
5549
6066
  const shutdown = () => {
6067
+ watcher.close();
6068
+ for (const job of activeJobs) {
6069
+ job.stop();
6070
+ }
5550
6071
  server.close();
5551
6072
  server.closeAllConnections?.();
5552
6073
  process.exit(0);
@@ -5637,7 +6158,7 @@ var runInteractive = async (workingDir, params) => {
5637
6158
  await harness.initialize();
5638
6159
  const identity = await ensureAgentIdentity2(workingDir);
5639
6160
  try {
5640
- const { runInteractiveInk } = await import("./run-interactive-ink-7FP5PT7Q.js");
6161
+ const { runInteractiveInk } = await import("./run-interactive-ink-VZBOYJYS.js");
5641
6162
  await runInteractiveInk({
5642
6163
  harness,
5643
6164
  params,
@@ -5998,6 +6519,9 @@ Test summary: ${passed} passed, ${failed} failed
5998
6519
  };
5999
6520
  var buildTarget = async (workingDir, target, options) => {
6000
6521
  const normalizedTarget = normalizeDeployTarget2(target);
6522
+ if (normalizedTarget === "vercel" && !options?.force) {
6523
+ await checkVercelCronDrift(workingDir);
6524
+ }
6001
6525
  const writtenPaths = await scaffoldDeployTarget(workingDir, normalizedTarget, {
6002
6526
  force: options?.force
6003
6527
  });