clearclaw 0.7.0 → 0.9.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/README.md +53 -7
- package/dist/channel/slack.d.ts +18 -1
- package/dist/channel/slack.js +227 -27
- package/dist/channel/telegram.d.ts +20 -1
- package/dist/channel/telegram.js +146 -33
- package/dist/config.d.ts +37 -1
- package/dist/config.js +118 -0
- package/dist/engine/acp.d.ts +9 -4
- package/dist/engine/acp.js +301 -29
- package/dist/engine/claude-code.d.ts +19 -2
- package/dist/engine/claude-code.js +121 -33
- package/dist/engine/mcp-http.d.ts +9 -0
- package/dist/engine/mcp-http.js +59 -0
- package/dist/engine/registry.d.ts +10 -7
- package/dist/engine/registry.js +99 -16
- package/dist/engine/session-transcript.d.ts +14 -0
- package/dist/engine/session-transcript.js +38 -0
- package/dist/format.d.ts +1 -1
- package/dist/format.js +42 -7
- package/dist/index.js +18 -23
- package/dist/orchestrator.d.ts +38 -4
- package/dist/orchestrator.js +854 -167
- package/dist/plan-usage.d.ts +6 -0
- package/dist/plan-usage.js +31 -0
- package/dist/prompt.d.ts +6 -2
- package/dist/prompt.js +75 -21
- package/dist/scheduler.d.ts +15 -0
- package/dist/scheduler.js +82 -0
- package/dist/transcribe.d.ts +13 -0
- package/dist/transcribe.js +157 -0
- package/dist/types.d.ts +77 -5
- package/package.json +11 -4
- package/patches/README.md +13 -0
- package/patches/codex-acp-1.11.0.patch +38 -0
- package/prompts/SYSTEM.md +66 -16
- package/scripts/patch-codex-acp.mjs +32 -0
- package/prompts/ONBOARDING.md +0 -56
package/dist/channel/telegram.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import { Bot, InlineKeyboard, InputFile } from "grammy";
|
|
2
|
+
import { Bot, GrammyError, InlineKeyboard, InputFile } from "grammy";
|
|
3
3
|
import mime from "mime";
|
|
4
4
|
import telegramifyMarkdown from "telegramify-markdown";
|
|
5
5
|
import log from "../logger.js";
|
|
@@ -15,6 +15,9 @@ export class TelegramChannel extends EventEmitter {
|
|
|
15
15
|
pendingCallbacks = new Map();
|
|
16
16
|
// Pending text input: chatId → resolve function (for requestText follow-ups)
|
|
17
17
|
pendingTextResolvers = new Map();
|
|
18
|
+
// Our bot's @username, captured at connect. Used to strip the `@bot` suffix
|
|
19
|
+
// Telegram appends to commands in group chats.
|
|
20
|
+
botUsername;
|
|
18
21
|
constructor(botToken, isAuthorized, onUnauthorizedDM) {
|
|
19
22
|
super();
|
|
20
23
|
this.botToken = botToken;
|
|
@@ -22,6 +25,31 @@ export class TelegramChannel extends EventEmitter {
|
|
|
22
25
|
this.onUnauthorizedDM = onUnauthorizedDM;
|
|
23
26
|
this.bot = new Bot(botToken);
|
|
24
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Strip the `@<botUsername>` suffix Telegram appends to commands in group
|
|
30
|
+
* chats (`/context@mybot args` → `/context args`), so downstream command
|
|
31
|
+
* handling — ClearClaw's own `/new` etc. and slash-command pass-through —
|
|
32
|
+
* sees the bare `/command`. No-op for non-command text and in DMs (no suffix).
|
|
33
|
+
* Telegram usernames are `[A-Za-z0-9_]`, so they need no regex escaping.
|
|
34
|
+
*/
|
|
35
|
+
stripCommandMention(text) {
|
|
36
|
+
if (!this.botUsername || !text.startsWith("/"))
|
|
37
|
+
return text;
|
|
38
|
+
return text.replace(new RegExp(`^(/[^\\s@]+)@${this.botUsername}(?=\\s|$)`, "i"), "$1");
|
|
39
|
+
}
|
|
40
|
+
async telegramCall(call, label) {
|
|
41
|
+
try {
|
|
42
|
+
return await call();
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
const retryAfter = telegramRetryAfterSeconds(err);
|
|
46
|
+
if (retryAfter === null)
|
|
47
|
+
throw err;
|
|
48
|
+
log.warn("[channel] Telegram rate limited during %s, retrying after %ss", label, retryAfter);
|
|
49
|
+
await sleep((retryAfter + 1) * 1000);
|
|
50
|
+
return await call();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
25
53
|
async connect() {
|
|
26
54
|
this.bot.on("message:text", (ctx) => {
|
|
27
55
|
const sender = this.extractSender(ctx);
|
|
@@ -36,7 +64,7 @@ export class TelegramChannel extends EventEmitter {
|
|
|
36
64
|
}
|
|
37
65
|
this.emit("message", {
|
|
38
66
|
...sender,
|
|
39
|
-
text: ctx.message.text,
|
|
67
|
+
text: this.stripCommandMention(ctx.message.text),
|
|
40
68
|
messageId: String(ctx.message.message_id),
|
|
41
69
|
replyTo: this.extractReplyContext(ctx.message),
|
|
42
70
|
});
|
|
@@ -54,10 +82,17 @@ export class TelegramChannel extends EventEmitter {
|
|
|
54
82
|
const doc = ctx.message.document;
|
|
55
83
|
await this.handleFileMessage(ctx, doc.file_id, doc.mime_type ?? "application/octet-stream", doc.file_name, ctx.message.caption);
|
|
56
84
|
});
|
|
85
|
+
// Voice notes and audio files: the orchestrator transcribes them
|
|
86
|
+
this.bot.on("message:voice", async (ctx) => {
|
|
87
|
+
const voice = ctx.message.voice;
|
|
88
|
+
await this.handleFileMessage(ctx, voice.file_id, voice.mime_type ?? "audio/ogg", "voice.ogg", ctx.message.caption);
|
|
89
|
+
});
|
|
90
|
+
this.bot.on("message:audio", async (ctx) => {
|
|
91
|
+
const audio = ctx.message.audio;
|
|
92
|
+
await this.handleFileMessage(ctx, audio.file_id, audio.mime_type ?? "audio/mpeg", audio.file_name, ctx.message.caption);
|
|
93
|
+
});
|
|
57
94
|
// Placeholder handlers for unsupported media types
|
|
58
95
|
for (const [filter, label] of [
|
|
59
|
-
["message:voice", "Voice message"],
|
|
60
|
-
["message:audio", "Audio message"],
|
|
61
96
|
["message:video", "Video"],
|
|
62
97
|
["message:sticker", "Sticker"],
|
|
63
98
|
]) {
|
|
@@ -89,6 +124,7 @@ export class TelegramChannel extends EventEmitter {
|
|
|
89
124
|
await new Promise((resolve) => {
|
|
90
125
|
this.bot.start({
|
|
91
126
|
onStart: (botInfo) => {
|
|
127
|
+
this.botUsername = botInfo.username;
|
|
92
128
|
log.info(`[channel] Telegram bot: @${botInfo.username}`);
|
|
93
129
|
resolve();
|
|
94
130
|
},
|
|
@@ -106,6 +142,10 @@ export class TelegramChannel extends EventEmitter {
|
|
|
106
142
|
ownsId(chatId) {
|
|
107
143
|
return chatId.startsWith("tg:");
|
|
108
144
|
}
|
|
145
|
+
/** A private chat is the user's own id; a ":thread" suffix means a topic inside it. */
|
|
146
|
+
isRootDM(chatId, userId) {
|
|
147
|
+
return /^tg:[1-9]\d*$/.test(chatId) && chatId === userId;
|
|
148
|
+
}
|
|
109
149
|
async sendMessage(chatId, text, opts) {
|
|
110
150
|
const numId = this.numericId(chatId);
|
|
111
151
|
const chunks = splitMessage(text);
|
|
@@ -117,21 +157,22 @@ export class TelegramChannel extends EventEmitter {
|
|
|
117
157
|
? { reply_parameters: { message_id: Number(opts.replyToMessageId) } }
|
|
118
158
|
: {};
|
|
119
159
|
if (plain) {
|
|
120
|
-
const sent = await this.bot.api.sendMessage(numId, chunk, replyParams);
|
|
160
|
+
const sent = await this.telegramCall(() => this.bot.api.sendMessage(numId, chunk, { ...this.threadOpts(chatId), ...replyParams }), "sendMessage");
|
|
121
161
|
handles.push(String(sent.message_id));
|
|
122
162
|
}
|
|
123
163
|
else {
|
|
124
164
|
const rendered = telegramifyMarkdown(chunk, "escape");
|
|
125
165
|
try {
|
|
126
|
-
const sent = await this.bot.api.sendMessage(numId, rendered, {
|
|
166
|
+
const sent = await this.telegramCall(() => this.bot.api.sendMessage(numId, rendered, {
|
|
167
|
+
...this.threadOpts(chatId),
|
|
127
168
|
parse_mode: "MarkdownV2",
|
|
128
169
|
...replyParams,
|
|
129
|
-
});
|
|
170
|
+
}), "sendMessage");
|
|
130
171
|
handles.push(String(sent.message_id));
|
|
131
172
|
}
|
|
132
173
|
catch (err) {
|
|
133
174
|
log.warn("[channel] sendMessage failed with MarkdownV2, retrying as plain text");
|
|
134
|
-
const sent = await this.bot.api.sendMessage(numId, chunk, replyParams);
|
|
175
|
+
const sent = await this.telegramCall(() => this.bot.api.sendMessage(numId, chunk, { ...this.threadOpts(chatId), ...replyParams }), "sendMessage");
|
|
135
176
|
handles.push(String(sent.message_id));
|
|
136
177
|
}
|
|
137
178
|
}
|
|
@@ -164,16 +205,18 @@ export class TelegramChannel extends EventEmitter {
|
|
|
164
205
|
const mdv2Text = telegramifyMarkdown(text, "escape");
|
|
165
206
|
let sent;
|
|
166
207
|
try {
|
|
167
|
-
sent = await this.bot.api.sendMessage(numId, mdv2Text, {
|
|
208
|
+
sent = await this.telegramCall(() => this.bot.api.sendMessage(numId, mdv2Text, {
|
|
209
|
+
...this.threadOpts(chatId),
|
|
168
210
|
reply_markup: keyboard,
|
|
169
211
|
parse_mode: "MarkdownV2",
|
|
170
|
-
});
|
|
212
|
+
}), "sendInteractive");
|
|
171
213
|
}
|
|
172
214
|
catch {
|
|
173
215
|
log.warn("[channel] sendInteractive failed with MarkdownV2, retrying as plain text");
|
|
174
|
-
sent = await this.bot.api.sendMessage(numId, text, {
|
|
216
|
+
sent = await this.telegramCall(() => this.bot.api.sendMessage(numId, text, {
|
|
217
|
+
...this.threadOpts(chatId),
|
|
175
218
|
reply_markup: keyboard,
|
|
176
|
-
});
|
|
219
|
+
}), "sendInteractive");
|
|
177
220
|
}
|
|
178
221
|
const cleanupAll = () => {
|
|
179
222
|
for (const { cbData } of cbEntries) {
|
|
@@ -201,17 +244,18 @@ export class TelegramChannel extends EventEmitter {
|
|
|
201
244
|
feedbackKeyboard.row();
|
|
202
245
|
}
|
|
203
246
|
try {
|
|
204
|
-
await this.bot.api.editMessageReplyMarkup(numId, sent.message_id, {
|
|
247
|
+
await this.telegramCall(() => this.bot.api.editMessageReplyMarkup(numId, sent.message_id, {
|
|
205
248
|
reply_markup: feedbackKeyboard,
|
|
206
|
-
});
|
|
249
|
+
}), "editMessageReplyMarkup");
|
|
207
250
|
}
|
|
208
251
|
catch { /* best-effort */ }
|
|
209
252
|
// If the button requests text, prompt for follow-up input
|
|
210
253
|
if (pressed.requestText) {
|
|
211
|
-
await this.bot.api.sendMessage(numId, "Add your feedback:", {
|
|
254
|
+
await this.telegramCall(() => this.bot.api.sendMessage(numId, "Add your feedback:", {
|
|
255
|
+
...this.threadOpts(chatId),
|
|
212
256
|
reply_markup: { force_reply: true },
|
|
213
257
|
reply_parameters: { message_id: sent.message_id },
|
|
214
|
-
});
|
|
258
|
+
}), "sendInteractiveFollowUp");
|
|
215
259
|
const followUpText = await new Promise((resolve) => {
|
|
216
260
|
this.pendingTextResolvers.set(chatId, resolve);
|
|
217
261
|
});
|
|
@@ -222,15 +266,15 @@ export class TelegramChannel extends EventEmitter {
|
|
|
222
266
|
async editMessage(chatId, handle, text, opts) {
|
|
223
267
|
const numId = this.numericId(chatId);
|
|
224
268
|
if (opts?.format === "plain") {
|
|
225
|
-
await this.bot.api.editMessageText(numId, Number(handle), text);
|
|
269
|
+
await this.telegramCall(() => this.bot.api.editMessageText(numId, Number(handle), text), "editMessage");
|
|
226
270
|
}
|
|
227
271
|
else {
|
|
228
272
|
const rendered = telegramifyMarkdown(text, "escape");
|
|
229
273
|
try {
|
|
230
|
-
await this.bot.api.editMessageText(numId, Number(handle), rendered, { parse_mode: "MarkdownV2" });
|
|
274
|
+
await this.telegramCall(() => this.bot.api.editMessageText(numId, Number(handle), rendered, { parse_mode: "MarkdownV2" }), "editMessage");
|
|
231
275
|
}
|
|
232
276
|
catch {
|
|
233
|
-
await this.bot.api.editMessageText(numId, Number(handle), text);
|
|
277
|
+
await this.telegramCall(() => this.bot.api.editMessageText(numId, Number(handle), text), "editMessage");
|
|
234
278
|
}
|
|
235
279
|
}
|
|
236
280
|
}
|
|
@@ -240,13 +284,18 @@ export class TelegramChannel extends EventEmitter {
|
|
|
240
284
|
}
|
|
241
285
|
async pinMessage(chatId, handle) {
|
|
242
286
|
const numId = this.numericId(chatId);
|
|
243
|
-
await this.bot.api.pinChatMessage(numId, Number(handle), {
|
|
287
|
+
await this.telegramCall(() => this.bot.api.pinChatMessage(numId, Number(handle), {
|
|
244
288
|
disable_notification: true,
|
|
245
|
-
});
|
|
289
|
+
}), "pinMessage");
|
|
246
290
|
}
|
|
291
|
+
/** Unpin everything in this chat's scope: the forum topic for a topic chat, else the whole chat. */
|
|
247
292
|
async unpinAllMessages(chatId) {
|
|
248
293
|
const numId = this.numericId(chatId);
|
|
249
|
-
|
|
294
|
+
const { message_thread_id } = this.threadOpts(chatId);
|
|
295
|
+
if (message_thread_id)
|
|
296
|
+
await this.bot.api.unpinAllForumTopicMessages(numId, message_thread_id);
|
|
297
|
+
else
|
|
298
|
+
await this.bot.api.unpinAllChatMessages(numId);
|
|
250
299
|
}
|
|
251
300
|
async updateStatus(chatId, text) {
|
|
252
301
|
const existing = this.statusHandles.get(chatId);
|
|
@@ -266,7 +315,9 @@ export class TelegramChannel extends EventEmitter {
|
|
|
266
315
|
}
|
|
267
316
|
}
|
|
268
317
|
}
|
|
269
|
-
// No existing handle, or edit failed — create
|
|
318
|
+
// No existing handle, or edit failed — (re)create the pinned status. The handle lives only in
|
|
319
|
+
// memory, so after a restart the previous status is unknown; clear every pin in scope instead
|
|
320
|
+
// (topic-scoped for forum topics, so peers in other topics keep theirs).
|
|
270
321
|
try {
|
|
271
322
|
await this.unpinAllMessages(chatId);
|
|
272
323
|
}
|
|
@@ -288,10 +339,10 @@ export class TelegramChannel extends EventEmitter {
|
|
|
288
339
|
const numId = this.numericId(chatId);
|
|
289
340
|
// Send immediately, then repeat every 4s
|
|
290
341
|
await this.bot.api
|
|
291
|
-
.sendChatAction(numId, "typing")
|
|
342
|
+
.sendChatAction(numId, "typing", this.threadOpts(chatId))
|
|
292
343
|
.catch(() => { });
|
|
293
344
|
const interval = setInterval(() => {
|
|
294
|
-
this.bot.api.sendChatAction(numId, "typing").catch(() => { });
|
|
345
|
+
this.bot.api.sendChatAction(numId, "typing", this.threadOpts(chatId)).catch(() => { });
|
|
295
346
|
}, 4000);
|
|
296
347
|
this.typingIntervals.set(chatId, interval);
|
|
297
348
|
}
|
|
@@ -323,10 +374,13 @@ export class TelegramChannel extends EventEmitter {
|
|
|
323
374
|
}
|
|
324
375
|
return null;
|
|
325
376
|
}
|
|
377
|
+
const topicSuffix = ctx.message?.is_topic_message && ctx.message.message_thread_id
|
|
378
|
+
? `:${ctx.message.message_thread_id}`
|
|
379
|
+
: "";
|
|
326
380
|
return {
|
|
327
|
-
chatId: `tg:${ctx.chat.id}`,
|
|
381
|
+
chatId: `tg:${ctx.chat.id}${topicSuffix}`,
|
|
328
382
|
chatType: ctx.chat.type === "private" ? "dm" : "group",
|
|
329
|
-
user,
|
|
383
|
+
origin: { kind: "user", user },
|
|
330
384
|
};
|
|
331
385
|
}
|
|
332
386
|
/** Extract reply context from a Telegram message, if it's a reply. */
|
|
@@ -352,7 +406,7 @@ export class TelegramChannel extends EventEmitter {
|
|
|
352
406
|
mediaType,
|
|
353
407
|
};
|
|
354
408
|
}
|
|
355
|
-
/** Shared handler for
|
|
409
|
+
/** Shared handler for file messages: download, emit attachment or fallback. */
|
|
356
410
|
async handleFileMessage(ctx, fileId, mimeType, filename, caption) {
|
|
357
411
|
const sender = this.extractSender(ctx);
|
|
358
412
|
if (!sender)
|
|
@@ -387,16 +441,16 @@ export class TelegramChannel extends EventEmitter {
|
|
|
387
441
|
const file = new InputFile(buffer, filename);
|
|
388
442
|
const caption = opts?.caption;
|
|
389
443
|
if (mimeType.startsWith("image/")) {
|
|
390
|
-
await this.bot.api.sendPhoto(id, file, { caption });
|
|
444
|
+
await this.bot.api.sendPhoto(id, file, { caption, ...this.threadOpts(chatId) });
|
|
391
445
|
}
|
|
392
446
|
else if (mimeType.startsWith("video/")) {
|
|
393
|
-
await this.bot.api.sendVideo(id, file, { caption });
|
|
447
|
+
await this.bot.api.sendVideo(id, file, { caption, ...this.threadOpts(chatId) });
|
|
394
448
|
}
|
|
395
449
|
else if (mimeType.startsWith("audio/")) {
|
|
396
|
-
await this.bot.api.sendAudio(id, file, { caption });
|
|
450
|
+
await this.bot.api.sendAudio(id, file, { caption, ...this.threadOpts(chatId) });
|
|
397
451
|
}
|
|
398
452
|
else {
|
|
399
|
-
await this.bot.api.sendDocument(id, file, { caption });
|
|
453
|
+
await this.bot.api.sendDocument(id, file, { caption, ...this.threadOpts(chatId) });
|
|
400
454
|
}
|
|
401
455
|
}
|
|
402
456
|
async reactToMessage(chatId, messageId, emoji) {
|
|
@@ -411,6 +465,45 @@ export class TelegramChannel extends EventEmitter {
|
|
|
411
465
|
log.warn("[channel] failed to set reaction on %s: %s", messageId, err instanceof Error ? err.message : String(err));
|
|
412
466
|
}
|
|
413
467
|
}
|
|
468
|
+
async createProjectChat(projectName, anchor, title) {
|
|
469
|
+
await this.validateProjectChat(projectName, anchor);
|
|
470
|
+
const topic = await this.bot.api.createForumTopic(this.numericId(anchor), title);
|
|
471
|
+
return `tg:${this.numericId(anchor)}:${topic.message_thread_id}`;
|
|
472
|
+
}
|
|
473
|
+
async closeProjectChat(chatId) {
|
|
474
|
+
const thread = chatId.split(":")[2];
|
|
475
|
+
if (!thread)
|
|
476
|
+
throw new Error("Telegram cannot close a whole chat; only topics can be archived");
|
|
477
|
+
const parentId = this.numericId(chatId);
|
|
478
|
+
if (parentId > 0) {
|
|
479
|
+
await this.bot.api.deleteForumTopic(parentId, Number(thread));
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
await this.bot.api.closeForumTopic(parentId, Number(thread));
|
|
484
|
+
}
|
|
485
|
+
catch (err) {
|
|
486
|
+
if (err instanceof GrammyError && err.description === "Bad Request: TOPIC_NOT_MODIFIED")
|
|
487
|
+
return;
|
|
488
|
+
throw err;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async validateProjectChat(projectName, anchor) {
|
|
492
|
+
const parentId = this.numericId(anchor);
|
|
493
|
+
if (!Number.isFinite(parentId)) {
|
|
494
|
+
throw new Error(`Telegram project "${projectName}" must use a valid chat ID`);
|
|
495
|
+
}
|
|
496
|
+
const chat = await this.bot.api.getChat(parentId);
|
|
497
|
+
if (chat.type === "private") {
|
|
498
|
+
const botInfo = await this.bot.api.getMe();
|
|
499
|
+
if (botInfo.has_topics_enabled)
|
|
500
|
+
return;
|
|
501
|
+
throw new Error(`Telegram project "${projectName}" requires Threaded Mode for this bot. Enable it in BotFather, then retry.`);
|
|
502
|
+
}
|
|
503
|
+
if (chat.type === "supergroup" && chat.is_forum)
|
|
504
|
+
return;
|
|
505
|
+
throw new Error(`Telegram project "${projectName}" requires a forum supergroup. Enable Topics manually in Telegram, then retry.`);
|
|
506
|
+
}
|
|
414
507
|
/** Download a Telegram file by file_id, returning the raw buffer. */
|
|
415
508
|
async downloadFile(fileId) {
|
|
416
509
|
const file = await this.bot.api.getFile(fileId);
|
|
@@ -422,8 +515,14 @@ export class TelegramChannel extends EventEmitter {
|
|
|
422
515
|
throw new Error(`File download failed: ${resp.status}`);
|
|
423
516
|
return Buffer.from(await resp.arrayBuffer());
|
|
424
517
|
}
|
|
518
|
+
/** "tg:123" → 123; "tg:123:45" (forum topic) → 123 */
|
|
425
519
|
numericId(chatId) {
|
|
426
|
-
return Number(chatId.
|
|
520
|
+
return Number(chatId.split(":")[1]);
|
|
521
|
+
}
|
|
522
|
+
/** Topic part of a composite chat id, as spreadable send options. */
|
|
523
|
+
threadOpts(chatId) {
|
|
524
|
+
const thread = chatId.split(":")[2];
|
|
525
|
+
return thread ? { message_thread_id: Number(thread) } : {};
|
|
427
526
|
}
|
|
428
527
|
}
|
|
429
528
|
const MAX_MSG_LEN = 4096;
|
|
@@ -450,4 +549,18 @@ function splitMessage(text) {
|
|
|
450
549
|
chunks.push(remaining);
|
|
451
550
|
return chunks;
|
|
452
551
|
}
|
|
552
|
+
function telegramRetryAfterSeconds(err) {
|
|
553
|
+
const candidate = err;
|
|
554
|
+
const retryAfter = candidate?.parameters?.retry_after;
|
|
555
|
+
if (typeof retryAfter === "number" && Number.isFinite(retryAfter))
|
|
556
|
+
return retryAfter;
|
|
557
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
558
|
+
const match = message.match(/retry after (\d+)/i);
|
|
559
|
+
if (!match)
|
|
560
|
+
return null;
|
|
561
|
+
return Number(match[1]);
|
|
562
|
+
}
|
|
563
|
+
function sleep(ms) {
|
|
564
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
565
|
+
}
|
|
453
566
|
//# sourceMappingURL=telegram.js.map
|
package/dist/config.d.ts
CHANGED
|
@@ -24,7 +24,21 @@ export interface PendingPairing {
|
|
|
24
24
|
export interface EngineEntry {
|
|
25
25
|
name: string;
|
|
26
26
|
default?: boolean;
|
|
27
|
-
path
|
|
27
|
+
path?: string;
|
|
28
|
+
configPath?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface ScheduleEntry {
|
|
31
|
+
id: string;
|
|
32
|
+
cron: string;
|
|
33
|
+
prompt: string;
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
timezone?: string;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
}
|
|
38
|
+
export interface Project {
|
|
39
|
+
name: string;
|
|
40
|
+
description: string;
|
|
41
|
+
main_workspace: string;
|
|
28
42
|
}
|
|
29
43
|
export declare function resolveDataDir(): string;
|
|
30
44
|
/**
|
|
@@ -61,16 +75,38 @@ export declare class Config {
|
|
|
61
75
|
hasUser(userId: string): boolean;
|
|
62
76
|
addUser(user: AuthorizedUser): void;
|
|
63
77
|
listUsers(): AuthorizedUser[];
|
|
78
|
+
/** All authorized IDs, including environment-configured and pairing-approved users. */
|
|
79
|
+
listAuthorizedUserIds(): string[];
|
|
64
80
|
/** env var IDs (in-memory) + pairing-approved users (hot-reload from disk) */
|
|
65
81
|
isAuthorized(userId: string): boolean;
|
|
66
82
|
createPairing(userId: string, userName: string, chatId: string): string;
|
|
67
83
|
consumePairing(code: string): PendingPairing | null;
|
|
68
84
|
private generateCode;
|
|
85
|
+
/** Home exists before a chat is paired. Never replace an existing home's settings. */
|
|
86
|
+
ensureHomeWorkspace(): Workspace;
|
|
87
|
+
/**
|
|
88
|
+
* ensureHomeWorkspace creates home's records at startup, before any chat is known.
|
|
89
|
+
* This gives it its destination, once: an existing binding is never replaced.
|
|
90
|
+
* Callers decide what counts as home's chat — see Channel.isRootDM.
|
|
91
|
+
*/
|
|
92
|
+
connectHomeWorkspace(chatId: string): boolean;
|
|
69
93
|
workspaceByChat(chatId: string): Workspace | undefined;
|
|
70
94
|
workspaceByName(name: string): Workspace | undefined;
|
|
95
|
+
listWorkspaces(): Workspace[];
|
|
71
96
|
upsertWorkspace(ws: Workspace): void;
|
|
97
|
+
removeWorkspace(name: string): Workspace | undefined;
|
|
98
|
+
/** Persist the session ID without changing the saved model setting. */
|
|
72
99
|
setSession(name: string, sessionId: string): void;
|
|
73
100
|
clearSession(name: string): void;
|
|
101
|
+
setModel(name: string, model: string | undefined): void;
|
|
74
102
|
setBehavior(name: string, behavior: "assistant" | "relay"): void;
|
|
103
|
+
addProject(project: Project): void;
|
|
104
|
+
listProjects(): Project[];
|
|
105
|
+
projectByName(name: string): Project | undefined;
|
|
106
|
+
removeProject(name: string): void;
|
|
107
|
+
listSchedules(): ScheduleEntry[];
|
|
108
|
+
addSchedule(entry: ScheduleEntry): void;
|
|
109
|
+
removeSchedule(id: string): void;
|
|
110
|
+
updateSchedule(id: string, update: Partial<Pick<ScheduleEntry, "enabled" | "cron" | "prompt" | "timezone">>): void;
|
|
75
111
|
approveUser(userId: string, userName: string): void;
|
|
76
112
|
}
|
package/dist/config.js
CHANGED
|
@@ -108,6 +108,8 @@ export class Config {
|
|
|
108
108
|
authorizedUsers: (raw.authorizedUsers ?? []),
|
|
109
109
|
pendingPairings: (raw.pendingPairings ?? []),
|
|
110
110
|
workspaces: (raw.workspaces ?? []),
|
|
111
|
+
schedules: (raw.schedules ?? []),
|
|
112
|
+
projects: (raw.projects ?? []),
|
|
111
113
|
};
|
|
112
114
|
}
|
|
113
115
|
write(data) {
|
|
@@ -142,6 +144,13 @@ export class Config {
|
|
|
142
144
|
listUsers() {
|
|
143
145
|
return this.read().authorizedUsers;
|
|
144
146
|
}
|
|
147
|
+
/** All authorized IDs, including environment-configured and pairing-approved users. */
|
|
148
|
+
listAuthorizedUserIds() {
|
|
149
|
+
return [...new Set([
|
|
150
|
+
...this.envUserIds,
|
|
151
|
+
...this.listUsers().map((user) => user.id),
|
|
152
|
+
])];
|
|
153
|
+
}
|
|
145
154
|
/** env var IDs (in-memory) + pairing-approved users (hot-reload from disk) */
|
|
146
155
|
isAuthorized(userId) {
|
|
147
156
|
return this.envUserIds.has(userId) || this.hasUser(userId);
|
|
@@ -185,12 +194,59 @@ export class Config {
|
|
|
185
194
|
.join("");
|
|
186
195
|
}
|
|
187
196
|
// --- Workspaces ---
|
|
197
|
+
/** Home exists before a chat is paired. Never replace an existing home's settings. */
|
|
198
|
+
ensureHomeWorkspace() {
|
|
199
|
+
fs.mkdirSync(this.homeWorkspacePath, { recursive: true });
|
|
200
|
+
const data = this.read();
|
|
201
|
+
let home = data.workspaces.find((ws) => ws.name === "default");
|
|
202
|
+
let changed = false;
|
|
203
|
+
if (!home) {
|
|
204
|
+
home = { name: "default", cwd: this.homeWorkspacePath, chat_id: null,
|
|
205
|
+
current_session_id: null, behavior: "assistant", description: "Personal assistant", project: "home" };
|
|
206
|
+
// Avoid adopting an unrelated project with a reserved home name.
|
|
207
|
+
if (data.projects.some((project) => project.name === "home" && project.main_workspace !== "default")) {
|
|
208
|
+
throw new Error('Project "home" already has another main workspace; rename it before creating home.');
|
|
209
|
+
}
|
|
210
|
+
data.workspaces.push(home);
|
|
211
|
+
changed = true;
|
|
212
|
+
}
|
|
213
|
+
if (!home.project) {
|
|
214
|
+
const existing = data.projects.find((project) => project.main_workspace === home.name);
|
|
215
|
+
home.project = existing?.name ?? "home";
|
|
216
|
+
if (data.projects.some((project) => project.name === home.project && project.main_workspace !== home.name)) {
|
|
217
|
+
throw new Error(`Project "${home.project}" already has another main workspace.`);
|
|
218
|
+
}
|
|
219
|
+
changed = true;
|
|
220
|
+
}
|
|
221
|
+
if (!data.projects.some((project) => project.name === home.project)) {
|
|
222
|
+
data.projects.push({ name: home.project, description: home.description ?? "Personal assistant", main_workspace: home.name });
|
|
223
|
+
changed = true;
|
|
224
|
+
}
|
|
225
|
+
if (changed)
|
|
226
|
+
this.write(data);
|
|
227
|
+
return home;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* ensureHomeWorkspace creates home's records at startup, before any chat is known.
|
|
231
|
+
* This gives it its destination, once: an existing binding is never replaced.
|
|
232
|
+
* Callers decide what counts as home's chat — see Channel.isRootDM.
|
|
233
|
+
*/
|
|
234
|
+
connectHomeWorkspace(chatId) {
|
|
235
|
+
const home = this.ensureHomeWorkspace();
|
|
236
|
+
if (home.chat_id || this.workspaceByChat(chatId))
|
|
237
|
+
return false;
|
|
238
|
+
this.upsertWorkspace({ ...home, chat_id: chatId });
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
188
241
|
workspaceByChat(chatId) {
|
|
189
242
|
return this.read().workspaces.find((ws) => ws.chat_id === chatId);
|
|
190
243
|
}
|
|
191
244
|
workspaceByName(name) {
|
|
192
245
|
return this.read().workspaces.find((ws) => ws.name === name);
|
|
193
246
|
}
|
|
247
|
+
listWorkspaces() {
|
|
248
|
+
return this.read().workspaces;
|
|
249
|
+
}
|
|
194
250
|
upsertWorkspace(ws) {
|
|
195
251
|
const data = this.read();
|
|
196
252
|
const idx = data.workspaces.findIndex((w) => w.name === ws.name);
|
|
@@ -200,6 +256,16 @@ export class Config {
|
|
|
200
256
|
data.workspaces.push(ws);
|
|
201
257
|
this.write(data);
|
|
202
258
|
}
|
|
259
|
+
removeWorkspace(name) {
|
|
260
|
+
const data = this.read();
|
|
261
|
+
const idx = data.workspaces.findIndex((w) => w.name === name);
|
|
262
|
+
if (idx < 0)
|
|
263
|
+
return undefined;
|
|
264
|
+
const [removed] = data.workspaces.splice(idx, 1);
|
|
265
|
+
this.write(data);
|
|
266
|
+
return removed;
|
|
267
|
+
}
|
|
268
|
+
/** Persist the session ID without changing the saved model setting. */
|
|
203
269
|
setSession(name, sessionId) {
|
|
204
270
|
const data = this.read();
|
|
205
271
|
const ws = data.workspaces.find((w) => w.name === name);
|
|
@@ -213,6 +279,15 @@ export class Config {
|
|
|
213
279
|
const ws = data.workspaces.find((w) => w.name === name);
|
|
214
280
|
if (ws) {
|
|
215
281
|
ws.current_session_id = null;
|
|
282
|
+
delete ws.engine_handoff;
|
|
283
|
+
this.write(data);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
setModel(name, model) {
|
|
287
|
+
const data = this.read();
|
|
288
|
+
const ws = data.workspaces.find((w) => w.name === name);
|
|
289
|
+
if (ws) {
|
|
290
|
+
ws.model = model;
|
|
216
291
|
this.write(data);
|
|
217
292
|
}
|
|
218
293
|
}
|
|
@@ -224,6 +299,49 @@ export class Config {
|
|
|
224
299
|
this.write(data);
|
|
225
300
|
}
|
|
226
301
|
}
|
|
302
|
+
// --- Projects ---
|
|
303
|
+
addProject(project) {
|
|
304
|
+
const data = this.read();
|
|
305
|
+
const idx = data.projects.findIndex((p) => p.name === project.name);
|
|
306
|
+
if (idx >= 0)
|
|
307
|
+
data.projects[idx] = project;
|
|
308
|
+
else
|
|
309
|
+
data.projects.push(project);
|
|
310
|
+
this.write(data);
|
|
311
|
+
}
|
|
312
|
+
listProjects() {
|
|
313
|
+
return this.read().projects;
|
|
314
|
+
}
|
|
315
|
+
projectByName(name) {
|
|
316
|
+
return this.read().projects.find((p) => p.name === name);
|
|
317
|
+
}
|
|
318
|
+
removeProject(name) {
|
|
319
|
+
const data = this.read();
|
|
320
|
+
data.projects = data.projects.filter((p) => p.name !== name);
|
|
321
|
+
this.write(data);
|
|
322
|
+
}
|
|
323
|
+
// --- Schedules ---
|
|
324
|
+
listSchedules() {
|
|
325
|
+
return this.read().schedules;
|
|
326
|
+
}
|
|
327
|
+
addSchedule(entry) {
|
|
328
|
+
const data = this.read();
|
|
329
|
+
data.schedules.push(entry);
|
|
330
|
+
this.write(data);
|
|
331
|
+
}
|
|
332
|
+
removeSchedule(id) {
|
|
333
|
+
const data = this.read();
|
|
334
|
+
data.schedules = data.schedules.filter((s) => s.id !== id);
|
|
335
|
+
this.write(data);
|
|
336
|
+
}
|
|
337
|
+
updateSchedule(id, update) {
|
|
338
|
+
const data = this.read();
|
|
339
|
+
const entry = data.schedules.find((s) => s.id === id);
|
|
340
|
+
if (entry) {
|
|
341
|
+
Object.assign(entry, update);
|
|
342
|
+
this.write(data);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
227
345
|
// --- Approval ---
|
|
228
346
|
approveUser(userId, userName) {
|
|
229
347
|
this.addUser({ id: userId, name: userName, approvedAt: Date.now() });
|
package/dist/engine/acp.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { Engine, EngineEvent, RunTurnOpts, SessionInfo } from "../types.js";
|
|
1
|
+
import type { AcpEngineDefinition } from "./registry.js";
|
|
2
|
+
import type { Engine, EngineEvent, PlanUsageWindow, RunTurnOpts, SessionInfo, SessionHistoryOpts, SessionMessage } from "../types.js";
|
|
3
3
|
export declare class AcpEngine implements Engine {
|
|
4
4
|
readonly name: string;
|
|
5
5
|
private readonly spawnConfig;
|
|
6
|
-
constructor(name: string, spawnConfig:
|
|
6
|
+
constructor(name: string, spawnConfig: AcpEngineDefinition);
|
|
7
7
|
runTurn(opts: RunTurnOpts): AsyncIterable<EngineEvent>;
|
|
8
|
-
|
|
8
|
+
getSessionMessages(opts: SessionHistoryOpts): Promise<SessionMessage[]>;
|
|
9
|
+
listSessions(cwd: string): Promise<SessionInfo[]>;
|
|
10
|
+
/** Short-lived, read-only ACP queries share one deadline and always release the child. */
|
|
11
|
+
private readSessionData;
|
|
9
12
|
}
|
|
13
|
+
/** PR #334's provider extension. Never treat standard ACP used/size as plan usage. */
|
|
14
|
+
export declare function codexPlanUsage(meta: unknown): PlanUsageWindow[];
|