arisa 3.0.12 → 3.1.2

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,8 +1,9 @@
1
- import { Bot, InputFile } from "grammy";
1
+ import { Bot, InputFile, webhookCallback } from "grammy";
2
2
  import path from "node:path";
3
3
  import { authorizeChat } from "./auth.js";
4
4
  import { captureIncomingArtifact } from "./media.js";
5
5
  import { renderTelegramHtml } from "./text-format.js";
6
+ import { normalizeArtifactForReasoning } from "../../core/artifacts/normalize-for-reasoning.js";
6
7
 
7
8
  function quotedMessageSummary(message) {
8
9
  if (!message) return [];
@@ -32,17 +33,28 @@ function quotedMessageSummary(message) {
32
33
  return parts;
33
34
  }
34
35
 
36
+ function getTelegramCommand(ctx) {
37
+ const text = ctx.message?.text || "";
38
+ const entity = ctx.message?.entities?.[0];
39
+ if (entity?.type !== "bot_command" || entity.offset !== 0 || !text.startsWith("/")) return "";
40
+ return text.slice(1, entity.length).split("@")[0].trim().toLowerCase();
41
+ }
42
+
43
+ function getIncomingMessageText(message) {
44
+ return message?.text || message?.caption || "";
45
+ }
46
+
35
47
  function buildPrompt({ ctx, artifact, transcript, toolResult }) {
36
48
  const parts = [
37
- `New Telegram message.`,
49
+ `Incoming Telegram message.`,
38
50
  `chatId: ${ctx.chat.id}`,
39
51
  `userId: ${ctx.from.id}`,
40
52
  `username: ${ctx.from.username || "(no username)"}`,
41
- `messageId: ${ctx.msg.message_id}`,
42
- `preferredTelegramLanguageCode: ${ctx.from?.language_code || "unknown"}`
53
+ `messageId: ${ctx.msg.message_id}`
43
54
  ];
44
55
 
45
- if (ctx.message?.text) parts.push(`text: ${ctx.message.text}`);
56
+ const messageText = getIncomingMessageText(ctx.message);
57
+ if (messageText) parts.push(`text: ${messageText}`);
46
58
  parts.push(...quotedMessageSummary(ctx.message?.reply_to_message));
47
59
  if (artifact?.path) parts.push(`artifactPath: ${artifact.path}`);
48
60
  if (artifact?.id) parts.push(`artifactId: ${artifact.id}`);
@@ -64,32 +76,70 @@ function buildPrompt({ ctx, artifact, transcript, toolResult }) {
64
76
  return parts.join("\n");
65
77
  }
66
78
 
67
- async function maybeTranscribeIncomingAudio({ artifact, toolRegistry, artifactStore }) {
68
- if (!artifact || artifact.kind !== "audio") return { transcript: null };
79
+ function buildNewSessionPrompt(ctx) {
80
+ return [
81
+ "System event: /new requested.",
82
+ "Session was reset.",
83
+ `preferredTelegramLanguageCode: ${ctx.from?.language_code || "unknown"}`,
84
+ "Reply with a brief, warm confirmation in the user's language."
85
+ ].join("\n");
86
+ }
69
87
 
70
- const result = await toolRegistry.run({
71
- name: "openai-transcribe",
72
- request: {
73
- artifact,
74
- args: {}
75
- }
76
- });
88
+ async function buildAsyncTaskPrompt({ task, artifactStore, toolRegistry, logger }) {
89
+ const parts = [
90
+ "Scheduled task fired.",
91
+ `taskId: ${task.id}`,
92
+ `chatId: ${task.payload.chatId}`,
93
+ task.payload.prompt ? `text: ${task.payload.prompt}` : null
94
+ ];
77
95
 
78
- if (!result.ok) {
79
- return { transcript: null, toolResult: result };
80
- }
96
+ if (task.payload.artifactId) {
97
+ const chatArtifactStore = artifactStore.forChat(task.payload.chatId);
98
+ const artifact = await chatArtifactStore.get(task.payload.artifactId);
99
+ if (artifact) {
100
+ parts.push(`artifactPath: ${artifact.path || ""}`);
101
+ parts.push(`artifactId: ${artifact.id}`);
102
+ parts.push(`mimeType: ${artifact.mimeType}`);
103
+ parts.push(`kind: ${artifact.kind}`);
104
+
105
+ const { normalizedArtifact, toolResult } = await normalizeArtifactForReasoning({
106
+ artifact,
107
+ desiredMimeType: "text/plain",
108
+ toolRegistry,
109
+ chatArtifactStore,
110
+ chatId: task.payload.chatId
111
+ });
81
112
 
82
- if (!result.output?.text) {
83
- return { transcript: null, toolResult: { ok: false, status: "failed", error: "Transcription returned no text." } };
113
+ if (normalizedArtifact) {
114
+ logger?.log("tasks", `artifact ${artifact.id} normalized to ${normalizedArtifact.id}`);
115
+ parts.push(`transcriptArtifactId: ${normalizedArtifact.id}`);
116
+ parts.push(`transcriptText: ${normalizedArtifact.text}`);
117
+ parts.push("Important: the attached audio artifact has already been normalized for reasoning. Use the transcript as the message content.");
118
+ } else if (artifact.kind === "audio" && toolResult) {
119
+ parts.push(`audioNormalizationResult: ${JSON.stringify(toolResult)}`);
120
+ parts.push("Important: pre-reasoning audio normalization could not be completed, so you do not have a transcript for this audio artifact.");
121
+ }
122
+ } else {
123
+ parts.push(`artifactId: ${task.payload.artifactId}`);
124
+ parts.push("Important: referenced artifact was not found.");
125
+ }
84
126
  }
85
127
 
86
- const transcript = await artifactStore.createText({
87
- text: result.output.text,
88
- source: { type: "tool", toolName: "openai-transcribe" },
89
- metadata: { fromArtifactId: artifact.id, tool: "openai-transcribe" }
90
- });
128
+ parts.push("Treat this as a new request for the chat and fulfill it now.");
129
+ parts.push("If needed, use tools.");
130
+ return parts.filter(Boolean).join("\n");
131
+ }
91
132
 
92
- return { transcript, toolResult: result };
133
+ async function normalizeIncomingArtifact({ artifact, toolRegistry, chatArtifactStore, chatId }) {
134
+ if (!artifact) return { transcript: null, toolResult: null };
135
+ const { normalizedArtifact, toolResult } = await normalizeArtifactForReasoning({
136
+ artifact,
137
+ desiredMimeType: "text/plain",
138
+ toolRegistry,
139
+ chatArtifactStore,
140
+ chatId
141
+ });
142
+ return { transcript: normalizedArtifact, toolResult };
93
143
  }
94
144
 
95
145
  async function collectText(session, prompt) {
@@ -117,8 +167,8 @@ async function withTyping(ctx, work) {
117
167
  }
118
168
  }
119
169
 
120
- export async function createTelegramBot({ config, artifactStore, toolRegistry, agentManager, saveConfig, updateConfig, logger }) {
121
- const bot = new Bot(config.telegram.apiKey);
170
+ export async function createTelegramBot({ config, artifactStore, toolRegistry, taskStore, agentManager, saveConfig, updateConfig, logger, webhookUrl, setHttpRequestHandler }) {
171
+ const bot = new Bot(config.telegram.token);
122
172
  const perChatState = new Map();
123
173
 
124
174
  function getIncomingChatMeta(ctx) {
@@ -138,10 +188,12 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
138
188
  }
139
189
 
140
190
  async function buildIncomingPrompt(ctx) {
141
- logger?.log("telegram", `message ${ctx.msg.message_id} in chat ${ctx.chat.id}`);
191
+ const chatId = ctx.chat.id;
192
+ logger?.log("telegram", `message ${ctx.msg.message_id} in chat ${chatId}`);
193
+ const chatArtifactStore = artifactStore.forChat(chatId);
142
194
  const artifact = await captureIncomingArtifact(ctx, artifactStore);
143
195
  if (artifact) logger?.log("telegram", `captured artifact ${artifact.kind}${artifact.id ? ` ${artifact.id}` : ""}`);
144
- const { transcript, toolResult } = await maybeTranscribeIncomingAudio({ artifact, toolRegistry, artifactStore });
196
+ const { transcript, toolResult } = await normalizeIncomingArtifact({ artifact, toolRegistry, chatArtifactStore, chatId });
145
197
  if (transcript) logger?.log("telegram", `audio transcribed to artifact ${transcript.id}`);
146
198
  if (artifact?.kind === "audio" && !transcript) {
147
199
  logger?.log("telegram", `audio normalization unavailable for chat ${ctx.chat.id}: ${toolResult?.error || toolResult?.missingConfig?.join(", ") || "unknown error"}`);
@@ -154,7 +206,8 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
154
206
 
155
207
  if (text.length > maxInlineReplyLength) {
156
208
  logger?.log("telegram", `sending long reply as markdown attachment for chat ${chatId}`);
157
- const artifact = await artifactStore.createGeneratedFile({
209
+ const chatArtifactStore = artifactStore.forChat(chatId);
210
+ const artifact = await chatArtifactStore.createGeneratedFile({
158
211
  fileName: `reply-${Date.now()}.md`,
159
212
  content: text,
160
213
  kind: "document",
@@ -172,51 +225,58 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
172
225
  await sendText(renderTelegramHtml(text), { parse_mode: "HTML" });
173
226
  }
174
227
 
175
- async function processPrompt(ctx, prompt) {
176
- const telegram = {
228
+ function createTelegramSessionBridge(chatId) {
229
+ return {
177
230
  sendMedia: async (filePath, { method = "audio", caption } = {}) => {
178
- logger?.log("telegram", `sending ${method} reply for chat ${ctx.chat.id}`);
231
+ logger?.log("telegram", `sending ${method} reply for chat ${chatId}`);
179
232
  const input = new InputFile(filePath);
180
- if (method === "voice") return ctx.replyWithVoice(input, { caption });
181
- if (method === "document") return ctx.replyWithDocument(input, { caption });
182
- return ctx.replyWithAudio(input, { caption });
233
+ if (method === "voice") return bot.api.sendVoice(chatId, input, { caption });
234
+ if (method === "document") return bot.api.sendDocument(chatId, input, { caption });
235
+ return bot.api.sendAudio(chatId, input, { caption });
183
236
  }
184
237
  };
185
- return withTyping(ctx, async () => {
186
- const { session } = await agentManager.getSessionContext(ctx.chat.id, telegram);
238
+ }
239
+
240
+ async function processPromptForChat({ chatId, prompt, ctx = null }) {
241
+ const work = async () => {
242
+ const { session } = await agentManager.getSessionContext(chatId, createTelegramSessionBridge(chatId));
187
243
  const text = await collectText(session, prompt);
188
244
  if (text) {
189
245
  await sendTextReply({
190
- sendText: (message, extra) => ctx.reply(message, extra),
191
- sendDocument: (file, extra) => ctx.replyWithDocument(file, extra),
192
- chatId: ctx.chat.id,
246
+ sendText: (message, extra) => bot.api.sendMessage(chatId, message, extra),
247
+ sendDocument: (file, extra) => bot.api.sendDocument(chatId, file, extra),
248
+ chatId,
193
249
  text
194
250
  });
195
251
  }
196
- });
252
+ };
253
+
254
+ if (ctx) return withTyping(ctx, work);
255
+ return work();
197
256
  }
198
257
 
199
- async function enqueueOrProcess(ctx) {
200
- const chatState = getChatState(ctx.chat.id);
201
- const incomingPrompt = await buildIncomingPrompt(ctx);
258
+ async function enqueuePrompt({ chatId, prompt, label, ctx = null }) {
259
+ const chatState = getChatState(chatId);
202
260
 
203
261
  if (chatState.processing) {
204
- logger?.log("telegram", `chat ${ctx.chat.id} busy, queueing message ${ctx.msg.message_id}`);
262
+ logger?.log("telegram", `chat ${chatId} busy, queueing ${label}`);
205
263
  chatState.nextPrompt = chatState.nextPrompt
206
- ? `${chatState.nextPrompt}\n\n${incomingPrompt}`
207
- : incomingPrompt;
208
- return ctx.reply("Queued. I will process this right after the current task finishes.");
264
+ ? `${chatState.nextPrompt}\n\n${prompt}`
265
+ : prompt;
266
+ return;
209
267
  }
210
268
 
211
269
  chatState.processing = true;
212
- logger?.log("telegram", `processing message ${ctx.msg.message_id} in chat ${ctx.chat.id}`);
213
- let currentPrompt = incomingPrompt;
270
+ logger?.log("telegram", `processing ${label} in chat ${chatId}`);
271
+ let currentPrompt = prompt;
272
+ let currentCtx = ctx;
214
273
 
215
274
  while (currentPrompt) {
216
275
  try {
217
- logger?.log("telegram", `prompt dispatch for chat ${ctx.chat.id}`);
218
- await processPrompt(ctx, currentPrompt);
276
+ logger?.log("telegram", `prompt dispatch for chat ${chatId}`);
277
+ await processPromptForChat({ chatId, prompt: currentPrompt, ctx: currentCtx });
219
278
  } finally {
279
+ currentCtx = null;
220
280
  if (chatState.nextPrompt) {
221
281
  currentPrompt = chatState.nextPrompt;
222
282
  chatState.nextPrompt = "";
@@ -229,6 +289,38 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
229
289
  chatState.processing = false;
230
290
  }
231
291
 
292
+ async function enqueueOrProcess(ctx) {
293
+ const chatState = getChatState(ctx.chat.id);
294
+
295
+ if (chatState.processing) {
296
+ const incomingPrompt = await buildIncomingPrompt(ctx);
297
+ return enqueuePrompt({
298
+ chatId: ctx.chat.id,
299
+ prompt: incomingPrompt,
300
+ label: `message ${ctx.msg.message_id}`
301
+ });
302
+ }
303
+
304
+ const incomingPrompt = await buildIncomingPrompt(ctx);
305
+ return enqueuePrompt({
306
+ chatId: ctx.chat.id,
307
+ prompt: incomingPrompt,
308
+ label: `message ${ctx.msg.message_id}`,
309
+ ctx
310
+ });
311
+ }
312
+
313
+ async function handleNewCommand(ctx) {
314
+ agentManager.resetSession(ctx.chat.id);
315
+ perChatState.set(ctx.chat.id, { processing: false, nextPrompt: "" });
316
+ await enqueuePrompt({
317
+ chatId: ctx.chat.id,
318
+ prompt: buildNewSessionPrompt(ctx),
319
+ label: "new-session command",
320
+ ctx
321
+ });
322
+ }
323
+
232
324
  bot.catch((error) => {
233
325
  logger?.error("telegram", `bot error: ${error instanceof Error ? error.message : String(error)}`);
234
326
  console.error("Telegram bot error:", error);
@@ -243,15 +335,16 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
243
335
  bot.command("new", async (ctx) => {
244
336
  const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
245
337
  if (!auth.ok) return;
246
- agentManager.resetSession(ctx.chat.id);
247
- perChatState.set(ctx.chat.id, { processing: false, nextPrompt: "" });
248
- return ctx.reply("Started a new chat context.");
338
+ await handleNewCommand(ctx);
249
339
  });
250
340
 
251
341
  bot.on("message", async (ctx) => {
252
342
  const auth = await authorizeChat({ config, chatId: ctx.chat.id, saveConfig, chatMeta: getIncomingChatMeta(ctx) });
253
343
  if (!auth.ok) return;
254
344
 
345
+ const command = getTelegramCommand(ctx);
346
+ if (command) return;
347
+
255
348
  try {
256
349
  await enqueueOrProcess(ctx);
257
350
  } catch (error) {
@@ -269,16 +362,6 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
269
362
  try {
270
363
  logger?.log("telegram", `generating startup message for chat ${chatId}`);
271
364
  const chatMeta = config.telegram.chatMeta[chatId] || {};
272
- const telegram = {
273
- sendMedia: async (filePath, { method = "audio", caption } = {}) => {
274
- logger?.log("telegram", `sending ${method} reply for chat ${chatId}`);
275
- const input = new InputFile(filePath);
276
- if (method === "voice") return bot.api.sendVoice(chatId, input, { caption });
277
- if (method === "document") return bot.api.sendDocument(chatId, input, { caption });
278
- return bot.api.sendAudio(chatId, input, { caption });
279
- }
280
- };
281
- const { session } = await agentManager.getSessionContext(chatId, telegram);
282
365
  const welcomePrompt = [
283
366
  "System event: Arisa has just started.",
284
367
  `chatId: ${chatId}`,
@@ -290,15 +373,7 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
290
373
  "Use the user's Telegram language when possible.",
291
374
  "Do not mention internal implementation details."
292
375
  ].filter(Boolean).join("\n");
293
- const text = await collectText(session, welcomePrompt);
294
- if (text) {
295
- await sendTextReply({
296
- sendText: (message, extra) => bot.api.sendMessage(chatId, message, extra),
297
- sendDocument: (file, extra) => bot.api.sendDocument(chatId, file, extra),
298
- chatId,
299
- text
300
- });
301
- }
376
+ await enqueuePrompt({ chatId, prompt: welcomePrompt, label: "startup message" });
302
377
  } catch (error) {
303
378
  logger?.log("telegram", `startup message failed for chat ${chatId}: ${error instanceof Error ? error.message : String(error)}`);
304
379
  }
@@ -306,8 +381,46 @@ export async function createTelegramBot({ config, artifactStore, toolRegistry, a
306
381
  await bot.api.setMyCommands([
307
382
  { command: "new", description: "Start a new chat context" }
308
383
  ]);
309
- logger?.log("telegram", "bot polling started");
310
- await bot.start();
384
+ setInterval(async () => {
385
+ const tasks = await taskStore.claimDue(10);
386
+ for (const task of tasks) {
387
+ try {
388
+ if (task.kind !== "agent_task" || !task.payload?.chatId || !task.payload?.prompt) {
389
+ await taskStore.fail(task.id, `Unsupported task: ${task.kind}`);
390
+ continue;
391
+ }
392
+ logger?.log("tasks", `running task ${task.id} for chat ${task.payload.chatId}`);
393
+ await enqueuePrompt({
394
+ chatId: task.payload.chatId,
395
+ prompt: await buildAsyncTaskPrompt({ task, artifactStore, toolRegistry, logger }),
396
+ label: `scheduled task ${task.id}`
397
+ });
398
+ await taskStore.complete(task.id);
399
+ } catch (error) {
400
+ await taskStore.fail(task.id, error instanceof Error ? error.message : String(error));
401
+ }
402
+ }
403
+ }, 1000).unref();
404
+ if (webhookUrl && setHttpRequestHandler) {
405
+ const webhookPath = `/telegram-${config.telegram.token.slice(-8)}`;
406
+ const handleUpdate = webhookCallback(bot, "http", {
407
+ timeoutMilliseconds: 60_000,
408
+ onTimeout: "return",
409
+ });
410
+ setHttpRequestHandler((req, res) => {
411
+ const parsed = new URL(req.url, "http://localhost");
412
+ if (req.method === "POST" && parsed.pathname === webhookPath) {
413
+ return handleUpdate(req, res);
414
+ }
415
+ res.writeHead(200, { "Content-Type": "text/plain" });
416
+ res.end("ok");
417
+ });
418
+ await bot.api.setWebhook(`${webhookUrl}${webhookPath}`);
419
+ logger?.log("telegram", `webhook mode: ${webhookUrl}${webhookPath}`);
420
+ } else {
421
+ logger?.log("telegram", "bot polling started");
422
+ await bot.start({ drop_pending_updates: true });
423
+ }
311
424
  }
312
425
  };
313
426
  }
@@ -6,56 +6,62 @@ async function downloadToBuffer(ctx, fileId) {
6
6
  return Buffer.from(await response.arrayBuffer());
7
7
  }
8
8
 
9
+ function incomingCaptionMetadata(ctx) {
10
+ return ctx.message?.caption ? { caption: ctx.message.caption } : {};
11
+ }
12
+
9
13
  export async function captureIncomingArtifact(ctx, artifactStore) {
14
+ const chatId = ctx.chat.id;
15
+ const store = artifactStore.forChat(chatId);
10
16
  const baseSource = {
11
17
  type: "telegram",
12
- chatId: ctx.chat.id,
18
+ chatId,
13
19
  messageId: ctx.msg.message_id,
14
20
  userId: ctx.from.id
15
21
  };
16
22
 
17
23
  if (ctx.message?.voice) {
18
- const fileName = `${ctx.chat.id}-${ctx.msg.message_id}.ogg`;
24
+ const fileName = `${chatId}-${ctx.msg.message_id}.ogg`;
19
25
  const content = await downloadToBuffer(ctx, ctx.message.voice.file_id);
20
- return artifactStore.createGeneratedFile({
26
+ return store.createGeneratedFile({
21
27
  fileName,
22
28
  content,
23
29
  kind: "audio",
24
30
  mimeType: "audio/ogg",
25
31
  source: baseSource,
26
- metadata: { duration: ctx.message.voice.duration }
32
+ metadata: { duration: ctx.message.voice.duration, ...incomingCaptionMetadata(ctx) }
27
33
  });
28
34
  }
29
35
 
30
36
  if (ctx.message?.document) {
31
- const fileName = ctx.message.document.file_name || `${ctx.chat.id}-${ctx.msg.message_id}`;
37
+ const fileName = ctx.message.document.file_name || `${chatId}-${ctx.msg.message_id}`;
32
38
  const content = await downloadToBuffer(ctx, ctx.message.document.file_id);
33
- return artifactStore.createGeneratedFile({
39
+ return store.createGeneratedFile({
34
40
  fileName,
35
41
  content,
36
42
  kind: "document",
37
43
  mimeType: ctx.message.document.mime_type || "application/octet-stream",
38
44
  source: baseSource,
39
- metadata: {}
45
+ metadata: incomingCaptionMetadata(ctx)
40
46
  });
41
47
  }
42
48
 
43
49
  if (ctx.message?.photo?.length) {
44
50
  const photo = ctx.message.photo.at(-1);
45
- const fileName = `${ctx.chat.id}-${ctx.msg.message_id}.jpg`;
51
+ const fileName = `${chatId}-${ctx.msg.message_id}.jpg`;
46
52
  const content = await downloadToBuffer(ctx, photo.file_id);
47
- return artifactStore.createGeneratedFile({
53
+ return store.createGeneratedFile({
48
54
  fileName,
49
55
  content,
50
56
  kind: "image",
51
57
  mimeType: "image/jpeg",
52
58
  source: baseSource,
53
- metadata: { width: photo.width, height: photo.height }
59
+ metadata: { width: photo.width, height: photo.height, ...incomingCaptionMetadata(ctx) }
54
60
  });
55
61
  }
56
62
 
57
63
  if (ctx.message?.text) {
58
- return artifactStore.createText({
64
+ return store.createText({
59
65
  text: ctx.message.text,
60
66
  source: baseSource,
61
67
  metadata: {}
@@ -9,7 +9,7 @@ const toolName = "openai-transcribe";
9
9
  const config = await loadToolConfig(toolName, defaults);
10
10
 
11
11
  function printHelp() {
12
- console.log(`openai-transcribe\n\nUsage:\n node index.js --help\n node index.js run --request-file <json>\n\nExpected input:\n {\n \"artifact\": { \"path\": \"/abs/audio.ogg\", \"mimeType\": \"audio/ogg\" },\n \"args\": {}\n }\n\nConfig at ${getToolConfigPath(toolName)}:\n OPENAI_API_KEY\n MODEL\n`);
12
+ console.log(`openai-transcribe\n\nUsage:\n node index.js --help\n node index.js run --request-file <json>\n\nExpected input:\n {\n "artifact": { "path": "/abs/audio.ogg", "mimeType": "audio/ogg" },\n "args": {}\n }\n\nConfig at ${getToolConfigPath(toolName)}:\n OPENAI_API_KEY\n MODEL\n`);
13
13
  }
14
14
 
15
15
  async function run(requestFile) {
@@ -0,0 +1 @@
1
+ export default {};
@@ -0,0 +1,68 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { toolError, toolOk } from "../../src/core/tools/tool-result.js";
3
+
4
+ function printHelp() {
5
+ console.log(`schedule-agent-task\n\nUsage:\n node index.js --help\n node index.js run --request-file <json>\n\nExpected input:\n {\n "text": "tell me the temperature in Toronto",\n "artifact": { "text": "tell me the temperature in Toronto" },\n "args": {\n "prompt": "tell me the temperature in Toronto",\n "runAt": "2026-04-07T14:00:00.000Z",\n "delaySeconds": "30",\n "intervalSeconds": "3600"\n }\n }\n\nBehavior:\n - schedules a future agent task for the current chat\n - provide either args.runAt or args.delaySeconds\n - optional args.intervalSeconds makes the task recurring\n`);
6
+ }
7
+
8
+ function firstNonEmpty(...values) {
9
+ return values.find((value) => String(value || "").trim()) || "";
10
+ }
11
+
12
+ function buildRunAt(args = {}) {
13
+ const runAtValue = firstNonEmpty(args.runAt, args.at, args.when);
14
+ if (runAtValue) {
15
+ const parsed = Date.parse(runAtValue);
16
+ if (Number.isNaN(parsed)) return "";
17
+ return new Date(parsed).toISOString();
18
+ }
19
+
20
+ const delaySeconds = Number(firstNonEmpty(args.delaySeconds, args.delay, args.seconds));
21
+ if (Number.isFinite(delaySeconds) && delaySeconds > 0) {
22
+ return new Date(Date.now() + (delaySeconds * 1000)).toISOString();
23
+ }
24
+
25
+ return "";
26
+ }
27
+
28
+ async function run(requestFile) {
29
+ const request = JSON.parse(await readFile(requestFile, "utf8"));
30
+ const args = request.args || {};
31
+ const prompt = firstNonEmpty(args.prompt, args.message, args.task, request.text, request.artifact?.text);
32
+ const runAt = buildRunAt(args);
33
+ const intervalSeconds = Number(firstNonEmpty(args.intervalSeconds, args.interval, args.everySeconds));
34
+
35
+ if (!prompt.trim()) {
36
+ console.log(JSON.stringify(toolError("prompt/message/task, text, or artifact.text is required")));
37
+ return;
38
+ }
39
+
40
+ if (!runAt) {
41
+ console.log(JSON.stringify(toolError("args.runAt/at/when or args.delaySeconds/delay/seconds is required")));
42
+ return;
43
+ }
44
+
45
+ const asyncTask = {
46
+ kind: "agent_task",
47
+ runAt,
48
+ payload: { prompt },
49
+ recurrence: Number.isFinite(intervalSeconds) && intervalSeconds > 0
50
+ ? { type: "interval", everySeconds: intervalSeconds }
51
+ : null
52
+ };
53
+
54
+ console.log(JSON.stringify(toolOk({ runAt }, {
55
+ status: "scheduled",
56
+ asyncTask
57
+ })));
58
+ }
59
+
60
+ const args = process.argv.slice(2);
61
+ if (!args.length || args.includes("--help") || args[0] === "help") {
62
+ printHelp();
63
+ } else if (args[0] === "run") {
64
+ const fileIndex = args.indexOf("--request-file");
65
+ await run(args[fileIndex + 1]);
66
+ } else {
67
+ printHelp();
68
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "schedule-agent-task-cli",
3
+ "private": true,
4
+ "type": "module",
5
+ "version": "1.0.0"
6
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "schedule-agent-task",
3
+ "description": "Schedule a future Pi Agent task for the current chat.",
4
+ "entry": "index.js",
5
+ "input": ["text/plain"],
6
+ "output": ["application/json"],
7
+ "configSchema": {}
8
+ }
@@ -47,7 +47,7 @@ async function fetchText(url) {
47
47
  const response = await fetch(url, {
48
48
  headers: {
49
49
  "user-agent": "Mozilla/5.0",
50
- "accept-language": "es-AR,es;q=0.9,en;q=0.8"
50
+ "accept-language": "en-US,en;q=0.9"
51
51
  },
52
52
  redirect: "follow"
53
53
  });