@poncho-ai/cli 0.32.8 → 0.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +18 -0
- package/dist/{chunk-VHS3K24F.js → chunk-IDGGF5WH.js} +183 -4
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-JOLJ5W33.js → run-interactive-ink-R4PHKIQR.js} +1 -1
- package/package.json +2 -2
- package/src/index.ts +208 -2
- package/src/init-onboarding.ts +2 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/cli@0.
|
|
2
|
+
> @poncho-ai/cli@0.33.1 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
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
[34mESM[39m Build start
|
|
10
10
|
[32mESM[39m [1mdist/cli.js [22m[32m94.00 B[39m
|
|
11
11
|
[32mESM[39m [1mdist/index.js [22m[32m917.00 B[39m
|
|
12
|
-
[32mESM[39m [1mdist/run-interactive-ink-
|
|
13
|
-
[32mESM[39m [1mdist/chunk-
|
|
14
|
-
[32mESM[39m ⚡️ Build success in
|
|
12
|
+
[32mESM[39m [1mdist/run-interactive-ink-R4PHKIQR.js [22m[32m56.86 KB[39m
|
|
13
|
+
[32mESM[39m [1mdist/chunk-IDGGF5WH.js [22m[32m543.84 KB[39m
|
|
14
|
+
[32mESM[39m ⚡️ Build success in 69ms
|
|
15
15
|
[34mDTS[39m Build start
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 4592ms
|
|
17
17
|
[32mDTS[39m [1mdist/cli.d.ts [22m[32m20.00 B[39m
|
|
18
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
18
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m7.01 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @poncho-ai/cli
|
|
2
2
|
|
|
3
|
+
## 0.33.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [[`67424e0`](https://github.com/cesr/poncho-ai/commit/67424e073b2faa28a255781f91a80f4602c745e2)]:
|
|
8
|
+
- @poncho-ai/harness@0.32.1
|
|
9
|
+
|
|
10
|
+
## 0.33.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- [#68](https://github.com/cesr/poncho-ai/pull/68) [`5a7e370`](https://github.com/cesr/poncho-ai/commit/5a7e3700a5ee441ef41cf4dc0ca70ff90e57d282) Thanks [@cesr](https://github.com/cesr)! - Add one-off reminders: agents can dynamically set, list, and cancel reminders that fire at a specific time. Fired reminders are immediately deleted from storage. Includes polling for local dev and Vercel cron integration.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [[`5a7e370`](https://github.com/cesr/poncho-ai/commit/5a7e3700a5ee441ef41cf4dc0ca70ff90e57d282)]:
|
|
19
|
+
- @poncho-ai/harness@0.32.0
|
|
20
|
+
|
|
3
21
|
## 0.32.8
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
|
@@ -7046,7 +7046,8 @@ var buildConfigFromOnboardingAnswers = (answers) => {
|
|
|
7046
7046
|
mcp: [],
|
|
7047
7047
|
auth,
|
|
7048
7048
|
storage,
|
|
7049
|
-
telemetry
|
|
7049
|
+
telemetry,
|
|
7050
|
+
reminders: { enabled: true }
|
|
7050
7051
|
};
|
|
7051
7052
|
if (messagingPlatform !== "none") {
|
|
7052
7053
|
const channelConfig = {
|
|
@@ -8159,6 +8160,26 @@ cron:
|
|
|
8159
8160
|
|
|
8160
8161
|
Add \`channel: telegram\` (or another platform) to have the agent proactively send the response to all known chats on that platform. The bot must have received at least one message from each user first.
|
|
8161
8162
|
|
|
8163
|
+
## Reminders
|
|
8164
|
+
|
|
8165
|
+
One-off reminders are enabled by default. The agent gets \`set_reminder\`, \`list_reminders\`, and \`cancel_reminder\` tools. Users can say things like "remind me tomorrow at 9am to check the report."
|
|
8166
|
+
|
|
8167
|
+
Configure in \`poncho.config.js\`:
|
|
8168
|
+
|
|
8169
|
+
\`\`\`javascript
|
|
8170
|
+
export default {
|
|
8171
|
+
reminders: {
|
|
8172
|
+
enabled: true,
|
|
8173
|
+
pollSchedule: '*/10 * * * *', // how often to check for due reminders
|
|
8174
|
+
},
|
|
8175
|
+
};
|
|
8176
|
+
\`\`\`
|
|
8177
|
+
|
|
8178
|
+
- Reminders fire via a polling loop (same interval locally and on serverless).
|
|
8179
|
+
- On Vercel, \`poncho build vercel\` adds a cron entry for \`/api/reminders/check\`.
|
|
8180
|
+
- Channel reminders (Telegram/Slack) reply in the original conversation.
|
|
8181
|
+
- Non-channel reminders create a new \`[reminder]\` conversation visible in the web UI.
|
|
8182
|
+
|
|
8162
8183
|
## Messaging (Slack)
|
|
8163
8184
|
|
|
8164
8185
|
Connect your agent to Slack so it responds to @mentions:
|
|
@@ -8455,9 +8476,24 @@ var checkVercelCronDrift = async (projectDir) => {
|
|
|
8455
8476
|
for (const [jobName, schedule] of vercelCronMap) {
|
|
8456
8477
|
diffs.push(` - removed job "${jobName}" (${schedule})`);
|
|
8457
8478
|
}
|
|
8479
|
+
try {
|
|
8480
|
+
const cfg = await loadPonchoConfig(projectDir);
|
|
8481
|
+
const reminderCron = vercelCrons.find((c) => c.path === "/api/reminders/check");
|
|
8482
|
+
if (cfg?.reminders?.enabled && !reminderCron) {
|
|
8483
|
+
diffs.push(` + missing reminders polling cron`);
|
|
8484
|
+
} else if (!cfg?.reminders?.enabled && reminderCron) {
|
|
8485
|
+
diffs.push(` - reminders polling cron present but reminders disabled`);
|
|
8486
|
+
} else if (cfg?.reminders?.enabled && reminderCron) {
|
|
8487
|
+
const expected = cfg.reminders.pollSchedule ?? "*/10 * * * *";
|
|
8488
|
+
if (reminderCron.schedule !== expected) {
|
|
8489
|
+
diffs.push(` ~ reminders poll schedule changed: "${reminderCron.schedule}" \u2192 "${expected}"`);
|
|
8490
|
+
}
|
|
8491
|
+
}
|
|
8492
|
+
} catch {
|
|
8493
|
+
}
|
|
8458
8494
|
if (diffs.length > 0) {
|
|
8459
8495
|
process.stderr.write(
|
|
8460
|
-
`\u26A0 vercel.json crons are out of sync with AGENT.md:
|
|
8496
|
+
`\u26A0 vercel.json crons are out of sync with AGENT.md / poncho.config.js:
|
|
8461
8497
|
${diffs.join("\n")}
|
|
8462
8498
|
Run \`poncho build vercel --force\` to update.
|
|
8463
8499
|
|
|
@@ -8563,6 +8599,15 @@ export default async function handler(req, res) {
|
|
|
8563
8599
|
],
|
|
8564
8600
|
routes: [{ src: "/(.*)", dest: "/api/index.mjs" }]
|
|
8565
8601
|
};
|
|
8602
|
+
try {
|
|
8603
|
+
const cfg = await loadPonchoConfig(projectDir);
|
|
8604
|
+
if (cfg?.reminders?.enabled) {
|
|
8605
|
+
const schedule = cfg.reminders.pollSchedule ?? "*/10 * * * *";
|
|
8606
|
+
if (!vercelCrons) vercelCrons = [];
|
|
8607
|
+
vercelCrons.push({ path: "/api/reminders/check", schedule });
|
|
8608
|
+
}
|
|
8609
|
+
} catch {
|
|
8610
|
+
}
|
|
8566
8611
|
if (vercelCrons && vercelCrons.length > 0) {
|
|
8567
8612
|
vercelConfig.crons = vercelCrons;
|
|
8568
8613
|
}
|
|
@@ -8616,6 +8661,9 @@ export const handler = async (event = {}) => {
|
|
|
8616
8661
|
// Create a rule for each cron job defined in AGENT.md that sends a GET request to:
|
|
8617
8662
|
// /api/cron/<jobName>
|
|
8618
8663
|
// Include the Authorization header with your PONCHO_AUTH_TOKEN as a Bearer token.
|
|
8664
|
+
//
|
|
8665
|
+
// Reminders: Create a CloudWatch Events rule that triggers GET /api/reminders/check
|
|
8666
|
+
// every 10 minutes (or your preferred interval) with Authorization: Bearer <PONCHO_AUTH_TOKEN>.
|
|
8619
8667
|
`,
|
|
8620
8668
|
{ force: options?.force, writtenPaths, baseDir: projectDir }
|
|
8621
8669
|
);
|
|
@@ -11254,7 +11302,7 @@ ${resultBody}`,
|
|
|
11254
11302
|
return;
|
|
11255
11303
|
}
|
|
11256
11304
|
if (pathname.startsWith("/api/")) {
|
|
11257
|
-
const isInternalPath = pathname.startsWith("/api/internal/") || pathname.startsWith("/api/cron/");
|
|
11305
|
+
const isInternalPath = pathname.startsWith("/api/internal/") || pathname.startsWith("/api/cron/") || pathname === "/api/reminders/check";
|
|
11258
11306
|
const isInternal = isInternalPath && request.method === "POST" && isValidInternalRequest(request.headers);
|
|
11259
11307
|
const hasBearerToken = request.headers.authorization?.startsWith("Bearer ");
|
|
11260
11308
|
const isAuthenticated = isInternal || !requireAuth || session || validateBearerToken(request.headers.authorization);
|
|
@@ -12825,8 +12873,116 @@ ${cronJob.task}`;
|
|
|
12825
12873
|
}
|
|
12826
12874
|
return;
|
|
12827
12875
|
}
|
|
12876
|
+
if (pathname === "/api/reminders/check" && (request.method === "GET" || request.method === "POST")) {
|
|
12877
|
+
const result = await checkAndFireReminders();
|
|
12878
|
+
writeJson(response, 200, result);
|
|
12879
|
+
return;
|
|
12880
|
+
}
|
|
12828
12881
|
writeJson(response, 404, { error: "Not found" });
|
|
12829
12882
|
};
|
|
12883
|
+
const DEFAULT_POLL_SCHEDULE = "*/10 * * * *";
|
|
12884
|
+
const pollScheduleToMs = (schedule) => {
|
|
12885
|
+
const m = schedule.match(/^\*\/(\d+)\s/);
|
|
12886
|
+
if (m) return Number(m[1]) * 60 * 1e3;
|
|
12887
|
+
return 10 * 60 * 1e3;
|
|
12888
|
+
};
|
|
12889
|
+
const reminderPollSchedule = config?.reminders?.pollSchedule ?? DEFAULT_POLL_SCHEDULE;
|
|
12890
|
+
const reminderPollWindowMs = pollScheduleToMs(reminderPollSchedule);
|
|
12891
|
+
const checkAndFireReminders = async () => {
|
|
12892
|
+
const reminderStore = harness.reminderStore;
|
|
12893
|
+
if (!reminderStore) return { fired: [], count: 0, duration: 0 };
|
|
12894
|
+
const start = Date.now();
|
|
12895
|
+
const firedIds = [];
|
|
12896
|
+
try {
|
|
12897
|
+
const reminders = await reminderStore.list();
|
|
12898
|
+
const cutoff = Date.now() + reminderPollWindowMs;
|
|
12899
|
+
const due = reminders.filter((r) => r.status === "pending" && r.scheduledAt <= cutoff);
|
|
12900
|
+
for (const reminder of due) {
|
|
12901
|
+
try {
|
|
12902
|
+
await reminderStore.delete(reminder.id);
|
|
12903
|
+
const originConv = reminder.conversationId ? await conversationStore.get(reminder.conversationId) : void 0;
|
|
12904
|
+
const channelMeta = originConv?.channelMeta;
|
|
12905
|
+
const framedMessage = `[Reminder] A reminder you previously set has fired.
|
|
12906
|
+
Task: "${reminder.task}"
|
|
12907
|
+
Originally set at: ${new Date(reminder.createdAt).toISOString()}
|
|
12908
|
+
Scheduled for: ${new Date(reminder.scheduledAt).toISOString()}`;
|
|
12909
|
+
if (channelMeta) {
|
|
12910
|
+
const adapter = messagingAdapters.get(channelMeta.platform);
|
|
12911
|
+
if (adapter && originConv) {
|
|
12912
|
+
const historyMessages = originConv.messages ?? [];
|
|
12913
|
+
let assistantResponse = "";
|
|
12914
|
+
for await (const event of harness.runWithTelemetry({
|
|
12915
|
+
task: framedMessage,
|
|
12916
|
+
conversationId: originConv.conversationId,
|
|
12917
|
+
parameters: { __activeConversationId: originConv.conversationId },
|
|
12918
|
+
messages: historyMessages
|
|
12919
|
+
})) {
|
|
12920
|
+
if (event.type === "model:chunk") {
|
|
12921
|
+
assistantResponse += event.content;
|
|
12922
|
+
}
|
|
12923
|
+
}
|
|
12924
|
+
if (assistantResponse) {
|
|
12925
|
+
try {
|
|
12926
|
+
await adapter.sendReply(
|
|
12927
|
+
{
|
|
12928
|
+
channelId: channelMeta.channelId,
|
|
12929
|
+
platformThreadId: channelMeta.platformThreadId ?? channelMeta.channelId
|
|
12930
|
+
},
|
|
12931
|
+
assistantResponse
|
|
12932
|
+
);
|
|
12933
|
+
} catch (sendError) {
|
|
12934
|
+
console.error(`[reminder] Send to ${channelMeta.platform} failed:`, sendError instanceof Error ? sendError.message : sendError);
|
|
12935
|
+
}
|
|
12936
|
+
}
|
|
12937
|
+
const freshConv = await conversationStore.get(originConv.conversationId);
|
|
12938
|
+
if (freshConv) {
|
|
12939
|
+
freshConv.messages = [
|
|
12940
|
+
...historyMessages,
|
|
12941
|
+
{ role: "user", content: framedMessage },
|
|
12942
|
+
...assistantResponse ? [{ role: "assistant", content: assistantResponse }] : []
|
|
12943
|
+
];
|
|
12944
|
+
freshConv.updatedAt = Date.now();
|
|
12945
|
+
await conversationStore.update(freshConv);
|
|
12946
|
+
}
|
|
12947
|
+
}
|
|
12948
|
+
} else {
|
|
12949
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
12950
|
+
const conversation = await conversationStore.create(
|
|
12951
|
+
reminder.ownerId ?? "local-owner",
|
|
12952
|
+
`[reminder] ${reminder.task.slice(0, 80)} ${timestamp}`
|
|
12953
|
+
);
|
|
12954
|
+
const convId = conversation.conversationId;
|
|
12955
|
+
let assistantResponse = "";
|
|
12956
|
+
for await (const event of harness.runWithTelemetry({
|
|
12957
|
+
task: framedMessage,
|
|
12958
|
+
conversationId: convId,
|
|
12959
|
+
parameters: { __activeConversationId: convId },
|
|
12960
|
+
messages: []
|
|
12961
|
+
})) {
|
|
12962
|
+
if (event.type === "model:chunk") {
|
|
12963
|
+
assistantResponse += event.content;
|
|
12964
|
+
}
|
|
12965
|
+
}
|
|
12966
|
+
const freshConv = await conversationStore.get(convId);
|
|
12967
|
+
if (freshConv) {
|
|
12968
|
+
freshConv.messages = [
|
|
12969
|
+
{ role: "user", content: framedMessage },
|
|
12970
|
+
...assistantResponse ? [{ role: "assistant", content: assistantResponse }] : []
|
|
12971
|
+
];
|
|
12972
|
+
freshConv.updatedAt = Date.now();
|
|
12973
|
+
await conversationStore.update(freshConv);
|
|
12974
|
+
}
|
|
12975
|
+
}
|
|
12976
|
+
firedIds.push(reminder.id);
|
|
12977
|
+
} catch (err) {
|
|
12978
|
+
console.error(`[reminder] Failed to fire reminder "${reminder.id}":`, err instanceof Error ? err.message : err);
|
|
12979
|
+
}
|
|
12980
|
+
}
|
|
12981
|
+
} catch (err) {
|
|
12982
|
+
console.error("[reminder] Error checking reminders:", err instanceof Error ? err.message : err);
|
|
12983
|
+
}
|
|
12984
|
+
return { fired: firedIds, count: firedIds.length, duration: Date.now() - start };
|
|
12985
|
+
};
|
|
12830
12986
|
handler._harness = harness;
|
|
12831
12987
|
handler._cronJobs = cronJobs;
|
|
12832
12988
|
handler._conversationStore = conversationStore;
|
|
@@ -12836,6 +12992,8 @@ ${cronJob.task}`;
|
|
|
12836
12992
|
handler._processSubagentCallback = processSubagentCallback;
|
|
12837
12993
|
handler._broadcastEvent = broadcastEvent;
|
|
12838
12994
|
handler._finishConversationStream = finishConversationStream;
|
|
12995
|
+
handler._checkAndFireReminders = checkAndFireReminders;
|
|
12996
|
+
handler._reminderPollIntervalMs = reminderPollWindowMs;
|
|
12839
12997
|
const STALE_SUBAGENT_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
12840
12998
|
try {
|
|
12841
12999
|
const allSummaries = await conversationStore.listSummaries();
|
|
@@ -13139,8 +13297,29 @@ ${config.task}`;
|
|
|
13139
13297
|
}
|
|
13140
13298
|
}, 500);
|
|
13141
13299
|
});
|
|
13300
|
+
let reminderInterval = null;
|
|
13301
|
+
if (handler._checkAndFireReminders && handler._reminderPollIntervalMs) {
|
|
13302
|
+
const pollMs = handler._reminderPollIntervalMs;
|
|
13303
|
+
const check = handler._checkAndFireReminders;
|
|
13304
|
+
reminderInterval = setInterval(async () => {
|
|
13305
|
+
try {
|
|
13306
|
+
const result = await check();
|
|
13307
|
+
if (result.count > 0) {
|
|
13308
|
+
process.stdout.write(
|
|
13309
|
+
`[reminder] Fired ${result.count} reminder${result.count === 1 ? "" : "s"} (${result.duration}ms)
|
|
13310
|
+
`
|
|
13311
|
+
);
|
|
13312
|
+
}
|
|
13313
|
+
} catch (err) {
|
|
13314
|
+
console.error("[reminder] Poll error:", err instanceof Error ? err.message : err);
|
|
13315
|
+
}
|
|
13316
|
+
}, pollMs);
|
|
13317
|
+
process.stdout.write(`[reminder] Polling every ${Math.round(pollMs / 1e3)}s
|
|
13318
|
+
`);
|
|
13319
|
+
}
|
|
13142
13320
|
const shutdown = () => {
|
|
13143
13321
|
watcher.close();
|
|
13322
|
+
if (reminderInterval) clearInterval(reminderInterval);
|
|
13144
13323
|
for (const job of activeJobs) {
|
|
13145
13324
|
job.stop();
|
|
13146
13325
|
}
|
|
@@ -13217,7 +13396,7 @@ var runInteractive = async (workingDir, params) => {
|
|
|
13217
13396
|
await harness.initialize();
|
|
13218
13397
|
const identity = await ensureAgentIdentity2(workingDir);
|
|
13219
13398
|
try {
|
|
13220
|
-
const { runInteractiveInk } = await import("./run-interactive-ink-
|
|
13399
|
+
const { runInteractiveInk } = await import("./run-interactive-ink-R4PHKIQR.js");
|
|
13221
13400
|
await runInteractiveInk({
|
|
13222
13401
|
harness,
|
|
13223
13402
|
params,
|
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -112,6 +112,12 @@ type RequestHandler = ((request: IncomingMessage, response: ServerResponse) => P
|
|
|
112
112
|
_processSubagentCallback?: (conversationId: string, skipLockCheck?: boolean) => Promise<void>;
|
|
113
113
|
_broadcastEvent?: (conversationId: string, event: AgentEvent) => void;
|
|
114
114
|
_finishConversationStream?: (conversationId: string) => void;
|
|
115
|
+
_checkAndFireReminders?: () => Promise<{
|
|
116
|
+
fired: string[];
|
|
117
|
+
count: number;
|
|
118
|
+
duration: number;
|
|
119
|
+
}>;
|
|
120
|
+
_reminderPollIntervalMs?: number;
|
|
115
121
|
};
|
|
116
122
|
declare const createRequestHandler: (options?: {
|
|
117
123
|
workingDir?: string;
|
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.33.1",
|
|
4
4
|
"description": "CLI for building and deploying AI agents",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"react": "^19.2.4",
|
|
28
28
|
"react-devtools-core": "^6.1.5",
|
|
29
29
|
"yaml": "^2.8.1",
|
|
30
|
-
"@poncho-ai/harness": "0.
|
|
30
|
+
"@poncho-ai/harness": "0.32.1",
|
|
31
31
|
"@poncho-ai/sdk": "1.7.1",
|
|
32
32
|
"@poncho-ai/messaging": "0.7.8"
|
|
33
33
|
},
|
package/src/index.ts
CHANGED
|
@@ -984,6 +984,26 @@ cron:
|
|
|
984
984
|
|
|
985
985
|
Add \`channel: telegram\` (or another platform) to have the agent proactively send the response to all known chats on that platform. The bot must have received at least one message from each user first.
|
|
986
986
|
|
|
987
|
+
## Reminders
|
|
988
|
+
|
|
989
|
+
One-off reminders are enabled by default. The agent gets \`set_reminder\`, \`list_reminders\`, and \`cancel_reminder\` tools. Users can say things like "remind me tomorrow at 9am to check the report."
|
|
990
|
+
|
|
991
|
+
Configure in \`poncho.config.js\`:
|
|
992
|
+
|
|
993
|
+
\`\`\`javascript
|
|
994
|
+
export default {
|
|
995
|
+
reminders: {
|
|
996
|
+
enabled: true,
|
|
997
|
+
pollSchedule: '*/10 * * * *', // how often to check for due reminders
|
|
998
|
+
},
|
|
999
|
+
};
|
|
1000
|
+
\`\`\`
|
|
1001
|
+
|
|
1002
|
+
- Reminders fire via a polling loop (same interval locally and on serverless).
|
|
1003
|
+
- On Vercel, \`poncho build vercel\` adds a cron entry for \`/api/reminders/check\`.
|
|
1004
|
+
- Channel reminders (Telegram/Slack) reply in the original conversation.
|
|
1005
|
+
- Non-channel reminders create a new \`[reminder]\` conversation visible in the web UI.
|
|
1006
|
+
|
|
987
1007
|
## Messaging (Slack)
|
|
988
1008
|
|
|
989
1009
|
Connect your agent to Slack so it responds to @mentions:
|
|
@@ -1325,9 +1345,26 @@ const checkVercelCronDrift = async (projectDir: string): Promise<void> => {
|
|
|
1325
1345
|
for (const [jobName, schedule] of vercelCronMap) {
|
|
1326
1346
|
diffs.push(` - removed job "${jobName}" (${schedule})`);
|
|
1327
1347
|
}
|
|
1348
|
+
|
|
1349
|
+
// Check reminder polling cron
|
|
1350
|
+
try {
|
|
1351
|
+
const cfg = await loadPonchoConfig(projectDir);
|
|
1352
|
+
const reminderCron = vercelCrons.find((c) => c.path === "/api/reminders/check");
|
|
1353
|
+
if (cfg?.reminders?.enabled && !reminderCron) {
|
|
1354
|
+
diffs.push(` + missing reminders polling cron`);
|
|
1355
|
+
} else if (!cfg?.reminders?.enabled && reminderCron) {
|
|
1356
|
+
diffs.push(` - reminders polling cron present but reminders disabled`);
|
|
1357
|
+
} else if (cfg?.reminders?.enabled && reminderCron) {
|
|
1358
|
+
const expected = cfg.reminders.pollSchedule ?? "*/10 * * * *";
|
|
1359
|
+
if (reminderCron.schedule !== expected) {
|
|
1360
|
+
diffs.push(` ~ reminders poll schedule changed: "${reminderCron.schedule}" → "${expected}"`);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
} catch { /* best-effort */ }
|
|
1364
|
+
|
|
1328
1365
|
if (diffs.length > 0) {
|
|
1329
1366
|
process.stderr.write(
|
|
1330
|
-
`\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`,
|
|
1367
|
+
`\u26A0 vercel.json crons are out of sync with AGENT.md / poncho.config.js:\n${diffs.join("\n")}\n Run \`poncho build vercel --force\` to update.\n\n`,
|
|
1331
1368
|
);
|
|
1332
1369
|
}
|
|
1333
1370
|
};
|
|
@@ -1446,6 +1483,16 @@ export default async function handler(req, res) {
|
|
|
1446
1483
|
],
|
|
1447
1484
|
routes: [{ src: "/(.*)", dest: "/api/index.mjs" }],
|
|
1448
1485
|
};
|
|
1486
|
+
// Add reminder polling cron if reminders are enabled
|
|
1487
|
+
try {
|
|
1488
|
+
const cfg = await loadPonchoConfig(projectDir);
|
|
1489
|
+
if (cfg?.reminders?.enabled) {
|
|
1490
|
+
const schedule = cfg.reminders.pollSchedule ?? "*/10 * * * *";
|
|
1491
|
+
if (!vercelCrons) vercelCrons = [];
|
|
1492
|
+
vercelCrons.push({ path: "/api/reminders/check", schedule });
|
|
1493
|
+
}
|
|
1494
|
+
} catch { /* best-effort */ }
|
|
1495
|
+
|
|
1449
1496
|
if (vercelCrons && vercelCrons.length > 0) {
|
|
1450
1497
|
vercelConfig.crons = vercelCrons;
|
|
1451
1498
|
}
|
|
@@ -1498,6 +1545,9 @@ export const handler = async (event = {}) => {
|
|
|
1498
1545
|
// Create a rule for each cron job defined in AGENT.md that sends a GET request to:
|
|
1499
1546
|
// /api/cron/<jobName>
|
|
1500
1547
|
// Include the Authorization header with your PONCHO_AUTH_TOKEN as a Bearer token.
|
|
1548
|
+
//
|
|
1549
|
+
// Reminders: Create a CloudWatch Events rule that triggers GET /api/reminders/check
|
|
1550
|
+
// every 10 minutes (or your preferred interval) with Authorization: Bearer <PONCHO_AUTH_TOKEN>.
|
|
1501
1551
|
`,
|
|
1502
1552
|
{ force: options?.force, writtenPaths, baseDir: projectDir },
|
|
1503
1553
|
);
|
|
@@ -1775,6 +1825,8 @@ export type RequestHandler = ((
|
|
|
1775
1825
|
_processSubagentCallback?: (conversationId: string, skipLockCheck?: boolean) => Promise<void>;
|
|
1776
1826
|
_broadcastEvent?: (conversationId: string, event: AgentEvent) => void;
|
|
1777
1827
|
_finishConversationStream?: (conversationId: string) => void;
|
|
1828
|
+
_checkAndFireReminders?: () => Promise<{ fired: string[]; count: number; duration: number }>;
|
|
1829
|
+
_reminderPollIntervalMs?: number;
|
|
1778
1830
|
};
|
|
1779
1831
|
|
|
1780
1832
|
export const createRequestHandler = async (options?: {
|
|
@@ -4527,7 +4579,7 @@ export const createRequestHandler = async (options?: {
|
|
|
4527
4579
|
|
|
4528
4580
|
if (pathname.startsWith("/api/")) {
|
|
4529
4581
|
// Internal self-fetch requests bypass user-facing auth
|
|
4530
|
-
const isInternalPath = pathname.startsWith("/api/internal/") || pathname.startsWith("/api/cron/");
|
|
4582
|
+
const isInternalPath = pathname.startsWith("/api/internal/") || pathname.startsWith("/api/cron/") || pathname === "/api/reminders/check";
|
|
4531
4583
|
const isInternal = isInternalPath && request.method === "POST" && isValidInternalRequest(request.headers);
|
|
4532
4584
|
|
|
4533
4585
|
// Check authentication: either valid session (Web UI), valid Bearer token (API), or valid internal request
|
|
@@ -6309,8 +6361,139 @@ export const createRequestHandler = async (options?: {
|
|
|
6309
6361
|
return;
|
|
6310
6362
|
}
|
|
6311
6363
|
|
|
6364
|
+
// ── Reminders check endpoint ────────────────────────────────────
|
|
6365
|
+
if (pathname === "/api/reminders/check" && (request.method === "GET" || request.method === "POST")) {
|
|
6366
|
+
const result = await checkAndFireReminders();
|
|
6367
|
+
writeJson(response, 200, result);
|
|
6368
|
+
return;
|
|
6369
|
+
}
|
|
6370
|
+
|
|
6312
6371
|
writeJson(response, 404, { error: "Not found" });
|
|
6313
6372
|
};
|
|
6373
|
+
|
|
6374
|
+
// ── Reminder polling logic ──────────────────────────────────────
|
|
6375
|
+
const DEFAULT_POLL_SCHEDULE = "*/10 * * * *";
|
|
6376
|
+
|
|
6377
|
+
const pollScheduleToMs = (schedule: string): number => {
|
|
6378
|
+
const m = schedule.match(/^\*\/(\d+)\s/);
|
|
6379
|
+
if (m) return Number(m[1]) * 60 * 1000;
|
|
6380
|
+
return 10 * 60 * 1000;
|
|
6381
|
+
};
|
|
6382
|
+
|
|
6383
|
+
const reminderPollSchedule = config?.reminders?.pollSchedule ?? DEFAULT_POLL_SCHEDULE;
|
|
6384
|
+
const reminderPollWindowMs = pollScheduleToMs(reminderPollSchedule);
|
|
6385
|
+
|
|
6386
|
+
const checkAndFireReminders = async (): Promise<{
|
|
6387
|
+
fired: string[];
|
|
6388
|
+
count: number;
|
|
6389
|
+
duration: number;
|
|
6390
|
+
}> => {
|
|
6391
|
+
const reminderStore = harness.reminderStore;
|
|
6392
|
+
if (!reminderStore) return { fired: [], count: 0, duration: 0 };
|
|
6393
|
+
|
|
6394
|
+
const start = Date.now();
|
|
6395
|
+
const firedIds: string[] = [];
|
|
6396
|
+
|
|
6397
|
+
try {
|
|
6398
|
+
const reminders = await reminderStore.list();
|
|
6399
|
+
const cutoff = Date.now() + reminderPollWindowMs;
|
|
6400
|
+
const due = reminders.filter((r) => r.status === "pending" && r.scheduledAt <= cutoff);
|
|
6401
|
+
|
|
6402
|
+
for (const reminder of due) {
|
|
6403
|
+
try {
|
|
6404
|
+
await reminderStore.delete(reminder.id);
|
|
6405
|
+
|
|
6406
|
+
const originConv = reminder.conversationId
|
|
6407
|
+
? await conversationStore.get(reminder.conversationId)
|
|
6408
|
+
: undefined;
|
|
6409
|
+
const channelMeta = originConv?.channelMeta;
|
|
6410
|
+
|
|
6411
|
+
const framedMessage =
|
|
6412
|
+
`[Reminder] A reminder you previously set has fired.\n` +
|
|
6413
|
+
`Task: "${reminder.task}"\n` +
|
|
6414
|
+
`Originally set at: ${new Date(reminder.createdAt).toISOString()}\n` +
|
|
6415
|
+
`Scheduled for: ${new Date(reminder.scheduledAt).toISOString()}`;
|
|
6416
|
+
|
|
6417
|
+
if (channelMeta) {
|
|
6418
|
+
const adapter = messagingAdapters.get(channelMeta.platform);
|
|
6419
|
+
if (adapter && originConv) {
|
|
6420
|
+
const historyMessages = originConv.messages ?? [];
|
|
6421
|
+
let assistantResponse = "";
|
|
6422
|
+
for await (const event of harness.runWithTelemetry({
|
|
6423
|
+
task: framedMessage,
|
|
6424
|
+
conversationId: originConv.conversationId,
|
|
6425
|
+
parameters: { __activeConversationId: originConv.conversationId },
|
|
6426
|
+
messages: historyMessages,
|
|
6427
|
+
})) {
|
|
6428
|
+
if (event.type === "model:chunk") {
|
|
6429
|
+
assistantResponse += event.content;
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
6432
|
+
if (assistantResponse) {
|
|
6433
|
+
try {
|
|
6434
|
+
await adapter.sendReply(
|
|
6435
|
+
{
|
|
6436
|
+
channelId: channelMeta.channelId,
|
|
6437
|
+
platformThreadId: channelMeta.platformThreadId ?? channelMeta.channelId,
|
|
6438
|
+
},
|
|
6439
|
+
assistantResponse,
|
|
6440
|
+
);
|
|
6441
|
+
} catch (sendError) {
|
|
6442
|
+
console.error(`[reminder] Send to ${channelMeta.platform} failed:`, sendError instanceof Error ? sendError.message : sendError);
|
|
6443
|
+
}
|
|
6444
|
+
}
|
|
6445
|
+
const freshConv = await conversationStore.get(originConv.conversationId);
|
|
6446
|
+
if (freshConv) {
|
|
6447
|
+
freshConv.messages = [
|
|
6448
|
+
...historyMessages,
|
|
6449
|
+
{ role: "user" as const, content: framedMessage },
|
|
6450
|
+
...(assistantResponse ? [{ role: "assistant" as const, content: assistantResponse }] : []),
|
|
6451
|
+
];
|
|
6452
|
+
freshConv.updatedAt = Date.now();
|
|
6453
|
+
await conversationStore.update(freshConv);
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6456
|
+
} else {
|
|
6457
|
+
const timestamp = new Date().toISOString();
|
|
6458
|
+
const conversation = await conversationStore.create(
|
|
6459
|
+
reminder.ownerId ?? "local-owner",
|
|
6460
|
+
`[reminder] ${reminder.task.slice(0, 80)} ${timestamp}`,
|
|
6461
|
+
);
|
|
6462
|
+
const convId = conversation.conversationId;
|
|
6463
|
+
let assistantResponse = "";
|
|
6464
|
+
for await (const event of harness.runWithTelemetry({
|
|
6465
|
+
task: framedMessage,
|
|
6466
|
+
conversationId: convId,
|
|
6467
|
+
parameters: { __activeConversationId: convId },
|
|
6468
|
+
messages: [],
|
|
6469
|
+
})) {
|
|
6470
|
+
if (event.type === "model:chunk") {
|
|
6471
|
+
assistantResponse += event.content;
|
|
6472
|
+
}
|
|
6473
|
+
}
|
|
6474
|
+
const freshConv = await conversationStore.get(convId);
|
|
6475
|
+
if (freshConv) {
|
|
6476
|
+
freshConv.messages = [
|
|
6477
|
+
{ role: "user" as const, content: framedMessage },
|
|
6478
|
+
...(assistantResponse ? [{ role: "assistant" as const, content: assistantResponse }] : []),
|
|
6479
|
+
];
|
|
6480
|
+
freshConv.updatedAt = Date.now();
|
|
6481
|
+
await conversationStore.update(freshConv);
|
|
6482
|
+
}
|
|
6483
|
+
}
|
|
6484
|
+
|
|
6485
|
+
firedIds.push(reminder.id);
|
|
6486
|
+
} catch (err) {
|
|
6487
|
+
console.error(`[reminder] Failed to fire reminder "${reminder.id}":`, err instanceof Error ? err.message : err);
|
|
6488
|
+
}
|
|
6489
|
+
}
|
|
6490
|
+
} catch (err) {
|
|
6491
|
+
console.error("[reminder] Error checking reminders:", err instanceof Error ? err.message : err);
|
|
6492
|
+
}
|
|
6493
|
+
|
|
6494
|
+
return { fired: firedIds, count: firedIds.length, duration: Date.now() - start };
|
|
6495
|
+
};
|
|
6496
|
+
|
|
6314
6497
|
handler._harness = harness;
|
|
6315
6498
|
handler._cronJobs = cronJobs;
|
|
6316
6499
|
handler._conversationStore = conversationStore;
|
|
@@ -6320,6 +6503,8 @@ export const createRequestHandler = async (options?: {
|
|
|
6320
6503
|
handler._processSubagentCallback = processSubagentCallback;
|
|
6321
6504
|
handler._broadcastEvent = broadcastEvent;
|
|
6322
6505
|
handler._finishConversationStream = finishConversationStream;
|
|
6506
|
+
handler._checkAndFireReminders = checkAndFireReminders;
|
|
6507
|
+
handler._reminderPollIntervalMs = reminderPollWindowMs;
|
|
6323
6508
|
|
|
6324
6509
|
// Recover stale subagent runs that were "running" when the server last stopped
|
|
6325
6510
|
// or that have been inactive longer than the staleness threshold.
|
|
@@ -6659,8 +6844,29 @@ export const startDevServer = async (
|
|
|
6659
6844
|
}, 500);
|
|
6660
6845
|
});
|
|
6661
6846
|
|
|
6847
|
+
// ── Reminder polling ─────────────────────────────────────────────
|
|
6848
|
+
let reminderInterval: ReturnType<typeof setInterval> | null = null;
|
|
6849
|
+
if (handler._checkAndFireReminders && handler._reminderPollIntervalMs) {
|
|
6850
|
+
const pollMs = handler._reminderPollIntervalMs;
|
|
6851
|
+
const check = handler._checkAndFireReminders;
|
|
6852
|
+
reminderInterval = setInterval(async () => {
|
|
6853
|
+
try {
|
|
6854
|
+
const result = await check();
|
|
6855
|
+
if (result.count > 0) {
|
|
6856
|
+
process.stdout.write(
|
|
6857
|
+
`[reminder] Fired ${result.count} reminder${result.count === 1 ? "" : "s"} (${result.duration}ms)\n`,
|
|
6858
|
+
);
|
|
6859
|
+
}
|
|
6860
|
+
} catch (err) {
|
|
6861
|
+
console.error("[reminder] Poll error:", err instanceof Error ? err.message : err);
|
|
6862
|
+
}
|
|
6863
|
+
}, pollMs);
|
|
6864
|
+
process.stdout.write(`[reminder] Polling every ${Math.round(pollMs / 1000)}s\n`);
|
|
6865
|
+
}
|
|
6866
|
+
|
|
6662
6867
|
const shutdown = () => {
|
|
6663
6868
|
watcher.close();
|
|
6869
|
+
if (reminderInterval) clearInterval(reminderInterval);
|
|
6664
6870
|
for (const job of activeJobs) {
|
|
6665
6871
|
job.stop();
|
|
6666
6872
|
}
|
package/src/init-onboarding.ts
CHANGED
|
@@ -346,6 +346,7 @@ export const buildConfigFromOnboardingAnswers = (
|
|
|
346
346
|
auth,
|
|
347
347
|
storage,
|
|
348
348
|
telemetry,
|
|
349
|
+
reminders: { enabled: true },
|
|
349
350
|
};
|
|
350
351
|
|
|
351
352
|
if (messagingPlatform !== "none") {
|
|
@@ -375,7 +376,7 @@ export const isDefaultOnboardingConfig = (
|
|
|
375
376
|
return true;
|
|
376
377
|
}
|
|
377
378
|
const topLevelKeys = Object.keys(config);
|
|
378
|
-
const allowedTopLevel = new Set(["mcp", "auth", "storage", "telemetry", "messaging"]);
|
|
379
|
+
const allowedTopLevel = new Set(["mcp", "auth", "storage", "telemetry", "messaging", "reminders"]);
|
|
379
380
|
if (topLevelKeys.some((key) => !allowedTopLevel.has(key))) {
|
|
380
381
|
return false;
|
|
381
382
|
}
|