clearclaw 0.6.1 → 0.8.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 +21 -4
- package/dist/channel/slack.js +270 -44
- package/dist/channel/telegram.d.ts +22 -3
- package/dist/channel/telegram.js +159 -35
- package/dist/config.d.ts +49 -5
- package/dist/config.js +159 -33
- package/dist/engine/acp.d.ts +14 -0
- package/dist/engine/acp.js +519 -0
- package/dist/engine/async-queue.d.ts +14 -0
- package/dist/engine/async-queue.js +45 -0
- package/dist/engine/claude-code.d.ts +20 -1
- package/dist/engine/claude-code.js +174 -44
- package/dist/engine/mcp-http.d.ts +9 -0
- package/dist/engine/mcp-http.js +59 -0
- package/dist/engine/registry.d.ts +19 -0
- package/dist/engine/registry.js +115 -0
- package/dist/engine/session-transcript.d.ts +14 -0
- package/dist/engine/session-transcript.js +38 -0
- package/dist/format.d.ts +8 -10
- package/dist/format.js +106 -82
- package/dist/index.js +33 -9
- package/dist/orchestrator.d.ts +34 -8
- package/dist/orchestrator.js +866 -153
- package/dist/plan-usage.d.ts +6 -0
- package/dist/plan-usage.js +31 -0
- package/dist/prompt.d.ts +10 -0
- package/dist/prompt.js +78 -0
- package/dist/scheduler.d.ts +15 -0
- package/dist/scheduler.js +82 -0
- package/dist/tool-handlers.d.ts +1 -1
- package/dist/tool-handlers.js +2 -2
- package/dist/types.d.ts +128 -17
- package/dist/types.js +12 -1
- package/package.json +13 -4
- package/patches/README.md +13 -0
- package/patches/codex-acp-1.11.0.patch +38 -0
- package/prompts/SYSTEM.md +214 -0
- package/scripts/patch-codex-acp.mjs +32 -0
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
|
});
|
|
@@ -89,6 +117,7 @@ export class TelegramChannel extends EventEmitter {
|
|
|
89
117
|
await new Promise((resolve) => {
|
|
90
118
|
this.bot.start({
|
|
91
119
|
onStart: (botInfo) => {
|
|
120
|
+
this.botUsername = botInfo.username;
|
|
92
121
|
log.info(`[channel] Telegram bot: @${botInfo.username}`);
|
|
93
122
|
resolve();
|
|
94
123
|
},
|
|
@@ -106,27 +135,39 @@ export class TelegramChannel extends EventEmitter {
|
|
|
106
135
|
ownsId(chatId) {
|
|
107
136
|
return chatId.startsWith("tg:");
|
|
108
137
|
}
|
|
138
|
+
/** A private chat is the user's own id; a ":thread" suffix means a topic inside it. */
|
|
139
|
+
isRootDM(chatId, userId) {
|
|
140
|
+
return /^tg:[1-9]\d*$/.test(chatId) && chatId === userId;
|
|
141
|
+
}
|
|
109
142
|
async sendMessage(chatId, text, opts) {
|
|
110
143
|
const numId = this.numericId(chatId);
|
|
111
144
|
const chunks = splitMessage(text);
|
|
112
145
|
const handles = [];
|
|
146
|
+
const plain = opts?.format === "plain";
|
|
113
147
|
let firstChunk = true;
|
|
114
148
|
for (const chunk of chunks) {
|
|
115
149
|
const replyParams = firstChunk && opts?.replyToMessageId
|
|
116
150
|
? { reply_parameters: { message_id: Number(opts.replyToMessageId) } }
|
|
117
151
|
: {};
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const sent = await this.bot.api.sendMessage(numId, rendered, {
|
|
121
|
-
parse_mode: "MarkdownV2",
|
|
122
|
-
...replyParams,
|
|
123
|
-
});
|
|
152
|
+
if (plain) {
|
|
153
|
+
const sent = await this.telegramCall(() => this.bot.api.sendMessage(numId, chunk, { ...this.threadOpts(chatId), ...replyParams }), "sendMessage");
|
|
124
154
|
handles.push(String(sent.message_id));
|
|
125
155
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
156
|
+
else {
|
|
157
|
+
const rendered = telegramifyMarkdown(chunk, "escape");
|
|
158
|
+
try {
|
|
159
|
+
const sent = await this.telegramCall(() => this.bot.api.sendMessage(numId, rendered, {
|
|
160
|
+
...this.threadOpts(chatId),
|
|
161
|
+
parse_mode: "MarkdownV2",
|
|
162
|
+
...replyParams,
|
|
163
|
+
}), "sendMessage");
|
|
164
|
+
handles.push(String(sent.message_id));
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
log.warn("[channel] sendMessage failed with MarkdownV2, retrying as plain text");
|
|
168
|
+
const sent = await this.telegramCall(() => this.bot.api.sendMessage(numId, chunk, { ...this.threadOpts(chatId), ...replyParams }), "sendMessage");
|
|
169
|
+
handles.push(String(sent.message_id));
|
|
170
|
+
}
|
|
130
171
|
}
|
|
131
172
|
firstChunk = false;
|
|
132
173
|
}
|
|
@@ -157,16 +198,18 @@ export class TelegramChannel extends EventEmitter {
|
|
|
157
198
|
const mdv2Text = telegramifyMarkdown(text, "escape");
|
|
158
199
|
let sent;
|
|
159
200
|
try {
|
|
160
|
-
sent = await this.bot.api.sendMessage(numId, mdv2Text, {
|
|
201
|
+
sent = await this.telegramCall(() => this.bot.api.sendMessage(numId, mdv2Text, {
|
|
202
|
+
...this.threadOpts(chatId),
|
|
161
203
|
reply_markup: keyboard,
|
|
162
204
|
parse_mode: "MarkdownV2",
|
|
163
|
-
});
|
|
205
|
+
}), "sendInteractive");
|
|
164
206
|
}
|
|
165
207
|
catch {
|
|
166
208
|
log.warn("[channel] sendInteractive failed with MarkdownV2, retrying as plain text");
|
|
167
|
-
sent = await this.bot.api.sendMessage(numId, text, {
|
|
209
|
+
sent = await this.telegramCall(() => this.bot.api.sendMessage(numId, text, {
|
|
210
|
+
...this.threadOpts(chatId),
|
|
168
211
|
reply_markup: keyboard,
|
|
169
|
-
});
|
|
212
|
+
}), "sendInteractive");
|
|
170
213
|
}
|
|
171
214
|
const cleanupAll = () => {
|
|
172
215
|
for (const { cbData } of cbEntries) {
|
|
@@ -194,17 +237,18 @@ export class TelegramChannel extends EventEmitter {
|
|
|
194
237
|
feedbackKeyboard.row();
|
|
195
238
|
}
|
|
196
239
|
try {
|
|
197
|
-
await this.bot.api.editMessageReplyMarkup(numId, sent.message_id, {
|
|
240
|
+
await this.telegramCall(() => this.bot.api.editMessageReplyMarkup(numId, sent.message_id, {
|
|
198
241
|
reply_markup: feedbackKeyboard,
|
|
199
|
-
});
|
|
242
|
+
}), "editMessageReplyMarkup");
|
|
200
243
|
}
|
|
201
244
|
catch { /* best-effort */ }
|
|
202
245
|
// If the button requests text, prompt for follow-up input
|
|
203
246
|
if (pressed.requestText) {
|
|
204
|
-
await this.bot.api.sendMessage(numId, "Add your feedback:", {
|
|
247
|
+
await this.telegramCall(() => this.bot.api.sendMessage(numId, "Add your feedback:", {
|
|
248
|
+
...this.threadOpts(chatId),
|
|
205
249
|
reply_markup: { force_reply: true },
|
|
206
250
|
reply_parameters: { message_id: sent.message_id },
|
|
207
|
-
});
|
|
251
|
+
}), "sendInteractiveFollowUp");
|
|
208
252
|
const followUpText = await new Promise((resolve) => {
|
|
209
253
|
this.pendingTextResolvers.set(chatId, resolve);
|
|
210
254
|
});
|
|
@@ -212,9 +256,20 @@ export class TelegramChannel extends EventEmitter {
|
|
|
212
256
|
}
|
|
213
257
|
return { value: pressed.value };
|
|
214
258
|
}
|
|
215
|
-
async editMessage(chatId, handle, text) {
|
|
259
|
+
async editMessage(chatId, handle, text, opts) {
|
|
216
260
|
const numId = this.numericId(chatId);
|
|
217
|
-
|
|
261
|
+
if (opts?.format === "plain") {
|
|
262
|
+
await this.telegramCall(() => this.bot.api.editMessageText(numId, Number(handle), text), "editMessage");
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
const rendered = telegramifyMarkdown(text, "escape");
|
|
266
|
+
try {
|
|
267
|
+
await this.telegramCall(() => this.bot.api.editMessageText(numId, Number(handle), rendered, { parse_mode: "MarkdownV2" }), "editMessage");
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
await this.telegramCall(() => this.bot.api.editMessageText(numId, Number(handle), text), "editMessage");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
218
273
|
}
|
|
219
274
|
async deleteMessage(chatId, handle) {
|
|
220
275
|
const numId = this.numericId(chatId);
|
|
@@ -222,13 +277,18 @@ export class TelegramChannel extends EventEmitter {
|
|
|
222
277
|
}
|
|
223
278
|
async pinMessage(chatId, handle) {
|
|
224
279
|
const numId = this.numericId(chatId);
|
|
225
|
-
await this.bot.api.pinChatMessage(numId, Number(handle), {
|
|
280
|
+
await this.telegramCall(() => this.bot.api.pinChatMessage(numId, Number(handle), {
|
|
226
281
|
disable_notification: true,
|
|
227
|
-
});
|
|
282
|
+
}), "pinMessage");
|
|
228
283
|
}
|
|
284
|
+
/** Unpin everything in this chat's scope: the forum topic for a topic chat, else the whole chat. */
|
|
229
285
|
async unpinAllMessages(chatId) {
|
|
230
286
|
const numId = this.numericId(chatId);
|
|
231
|
-
|
|
287
|
+
const { message_thread_id } = this.threadOpts(chatId);
|
|
288
|
+
if (message_thread_id)
|
|
289
|
+
await this.bot.api.unpinAllForumTopicMessages(numId, message_thread_id);
|
|
290
|
+
else
|
|
291
|
+
await this.bot.api.unpinAllChatMessages(numId);
|
|
232
292
|
}
|
|
233
293
|
async updateStatus(chatId, text) {
|
|
234
294
|
const existing = this.statusHandles.get(chatId);
|
|
@@ -248,7 +308,9 @@ export class TelegramChannel extends EventEmitter {
|
|
|
248
308
|
}
|
|
249
309
|
}
|
|
250
310
|
}
|
|
251
|
-
// No existing handle, or edit failed — create
|
|
311
|
+
// No existing handle, or edit failed — (re)create the pinned status. The handle lives only in
|
|
312
|
+
// memory, so after a restart the previous status is unknown; clear every pin in scope instead
|
|
313
|
+
// (topic-scoped for forum topics, so peers in other topics keep theirs).
|
|
252
314
|
try {
|
|
253
315
|
await this.unpinAllMessages(chatId);
|
|
254
316
|
}
|
|
@@ -270,10 +332,10 @@ export class TelegramChannel extends EventEmitter {
|
|
|
270
332
|
const numId = this.numericId(chatId);
|
|
271
333
|
// Send immediately, then repeat every 4s
|
|
272
334
|
await this.bot.api
|
|
273
|
-
.sendChatAction(numId, "typing")
|
|
335
|
+
.sendChatAction(numId, "typing", this.threadOpts(chatId))
|
|
274
336
|
.catch(() => { });
|
|
275
337
|
const interval = setInterval(() => {
|
|
276
|
-
this.bot.api.sendChatAction(numId, "typing").catch(() => { });
|
|
338
|
+
this.bot.api.sendChatAction(numId, "typing", this.threadOpts(chatId)).catch(() => { });
|
|
277
339
|
}, 4000);
|
|
278
340
|
this.typingIntervals.set(chatId, interval);
|
|
279
341
|
}
|
|
@@ -305,10 +367,13 @@ export class TelegramChannel extends EventEmitter {
|
|
|
305
367
|
}
|
|
306
368
|
return null;
|
|
307
369
|
}
|
|
370
|
+
const topicSuffix = ctx.message?.is_topic_message && ctx.message.message_thread_id
|
|
371
|
+
? `:${ctx.message.message_thread_id}`
|
|
372
|
+
: "";
|
|
308
373
|
return {
|
|
309
|
-
chatId: `tg:${ctx.chat.id}`,
|
|
374
|
+
chatId: `tg:${ctx.chat.id}${topicSuffix}`,
|
|
310
375
|
chatType: ctx.chat.type === "private" ? "dm" : "group",
|
|
311
|
-
user,
|
|
376
|
+
origin: { kind: "user", user },
|
|
312
377
|
};
|
|
313
378
|
}
|
|
314
379
|
/** Extract reply context from a Telegram message, if it's a reply. */
|
|
@@ -369,16 +434,16 @@ export class TelegramChannel extends EventEmitter {
|
|
|
369
434
|
const file = new InputFile(buffer, filename);
|
|
370
435
|
const caption = opts?.caption;
|
|
371
436
|
if (mimeType.startsWith("image/")) {
|
|
372
|
-
await this.bot.api.sendPhoto(id, file, { caption });
|
|
437
|
+
await this.bot.api.sendPhoto(id, file, { caption, ...this.threadOpts(chatId) });
|
|
373
438
|
}
|
|
374
439
|
else if (mimeType.startsWith("video/")) {
|
|
375
|
-
await this.bot.api.sendVideo(id, file, { caption });
|
|
440
|
+
await this.bot.api.sendVideo(id, file, { caption, ...this.threadOpts(chatId) });
|
|
376
441
|
}
|
|
377
442
|
else if (mimeType.startsWith("audio/")) {
|
|
378
|
-
await this.bot.api.sendAudio(id, file, { caption });
|
|
443
|
+
await this.bot.api.sendAudio(id, file, { caption, ...this.threadOpts(chatId) });
|
|
379
444
|
}
|
|
380
445
|
else {
|
|
381
|
-
await this.bot.api.sendDocument(id, file, { caption });
|
|
446
|
+
await this.bot.api.sendDocument(id, file, { caption, ...this.threadOpts(chatId) });
|
|
382
447
|
}
|
|
383
448
|
}
|
|
384
449
|
async reactToMessage(chatId, messageId, emoji) {
|
|
@@ -393,6 +458,45 @@ export class TelegramChannel extends EventEmitter {
|
|
|
393
458
|
log.warn("[channel] failed to set reaction on %s: %s", messageId, err instanceof Error ? err.message : String(err));
|
|
394
459
|
}
|
|
395
460
|
}
|
|
461
|
+
async createProjectChat(projectName, anchor, title) {
|
|
462
|
+
await this.validateProjectChat(projectName, anchor);
|
|
463
|
+
const topic = await this.bot.api.createForumTopic(this.numericId(anchor), title);
|
|
464
|
+
return `tg:${this.numericId(anchor)}:${topic.message_thread_id}`;
|
|
465
|
+
}
|
|
466
|
+
async closeProjectChat(chatId) {
|
|
467
|
+
const thread = chatId.split(":")[2];
|
|
468
|
+
if (!thread)
|
|
469
|
+
throw new Error("Telegram cannot close a whole chat; only topics can be archived");
|
|
470
|
+
const parentId = this.numericId(chatId);
|
|
471
|
+
if (parentId > 0) {
|
|
472
|
+
await this.bot.api.deleteForumTopic(parentId, Number(thread));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
await this.bot.api.closeForumTopic(parentId, Number(thread));
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
if (err instanceof GrammyError && err.description === "Bad Request: TOPIC_NOT_MODIFIED")
|
|
480
|
+
return;
|
|
481
|
+
throw err;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async validateProjectChat(projectName, anchor) {
|
|
485
|
+
const parentId = this.numericId(anchor);
|
|
486
|
+
if (!Number.isFinite(parentId)) {
|
|
487
|
+
throw new Error(`Telegram project "${projectName}" must use a valid chat ID`);
|
|
488
|
+
}
|
|
489
|
+
const chat = await this.bot.api.getChat(parentId);
|
|
490
|
+
if (chat.type === "private") {
|
|
491
|
+
const botInfo = await this.bot.api.getMe();
|
|
492
|
+
if (botInfo.has_topics_enabled)
|
|
493
|
+
return;
|
|
494
|
+
throw new Error(`Telegram project "${projectName}" requires Threaded Mode for this bot. Enable it in BotFather, then retry.`);
|
|
495
|
+
}
|
|
496
|
+
if (chat.type === "supergroup" && chat.is_forum)
|
|
497
|
+
return;
|
|
498
|
+
throw new Error(`Telegram project "${projectName}" requires a forum supergroup. Enable Topics manually in Telegram, then retry.`);
|
|
499
|
+
}
|
|
396
500
|
/** Download a Telegram file by file_id, returning the raw buffer. */
|
|
397
501
|
async downloadFile(fileId) {
|
|
398
502
|
const file = await this.bot.api.getFile(fileId);
|
|
@@ -404,8 +508,14 @@ export class TelegramChannel extends EventEmitter {
|
|
|
404
508
|
throw new Error(`File download failed: ${resp.status}`);
|
|
405
509
|
return Buffer.from(await resp.arrayBuffer());
|
|
406
510
|
}
|
|
511
|
+
/** "tg:123" → 123; "tg:123:45" (forum topic) → 123 */
|
|
407
512
|
numericId(chatId) {
|
|
408
|
-
return Number(chatId.
|
|
513
|
+
return Number(chatId.split(":")[1]);
|
|
514
|
+
}
|
|
515
|
+
/** Topic part of a composite chat id, as spreadable send options. */
|
|
516
|
+
threadOpts(chatId) {
|
|
517
|
+
const thread = chatId.split(":")[2];
|
|
518
|
+
return thread ? { message_thread_id: Number(thread) } : {};
|
|
409
519
|
}
|
|
410
520
|
}
|
|
411
521
|
const MAX_MSG_LEN = 4096;
|
|
@@ -432,4 +542,18 @@ function splitMessage(text) {
|
|
|
432
542
|
chunks.push(remaining);
|
|
433
543
|
return chunks;
|
|
434
544
|
}
|
|
545
|
+
function telegramRetryAfterSeconds(err) {
|
|
546
|
+
const candidate = err;
|
|
547
|
+
const retryAfter = candidate?.parameters?.retry_after;
|
|
548
|
+
if (typeof retryAfter === "number" && Number.isFinite(retryAfter))
|
|
549
|
+
return retryAfter;
|
|
550
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
551
|
+
const match = message.match(/retry after (\d+)/i);
|
|
552
|
+
if (!match)
|
|
553
|
+
return null;
|
|
554
|
+
return Number(match[1]);
|
|
555
|
+
}
|
|
556
|
+
function sleep(ms) {
|
|
557
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
558
|
+
}
|
|
435
559
|
//# sourceMappingURL=telegram.js.map
|
package/dist/config.d.ts
CHANGED
|
@@ -21,6 +21,25 @@ export interface PendingPairing {
|
|
|
21
21
|
chatId: string;
|
|
22
22
|
expiresAt: number;
|
|
23
23
|
}
|
|
24
|
+
export interface EngineEntry {
|
|
25
|
+
name: string;
|
|
26
|
+
default?: boolean;
|
|
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;
|
|
42
|
+
}
|
|
24
43
|
export declare function resolveDataDir(): string;
|
|
25
44
|
/**
|
|
26
45
|
* Unified config: env var resolution + persistent store backed by config.json.
|
|
@@ -33,36 +52,61 @@ export declare function resolveDataDir(): string;
|
|
|
33
52
|
*/
|
|
34
53
|
export declare class Config {
|
|
35
54
|
channel?: ChannelConfig;
|
|
55
|
+
engines: Record<string, EngineEntry>;
|
|
56
|
+
defaultEngine: string;
|
|
36
57
|
permissionMode: PermissionMode;
|
|
37
58
|
private envUserIds;
|
|
38
|
-
readonly
|
|
59
|
+
readonly homeWorkspacePath: string;
|
|
39
60
|
readonly filesPath: string;
|
|
40
61
|
readonly logPath: string;
|
|
62
|
+
readonly instructionsDir: string;
|
|
63
|
+
readonly frameworkPromptDir: string;
|
|
41
64
|
private readonly dataDir;
|
|
42
65
|
private readonly filePath;
|
|
43
66
|
constructor();
|
|
44
67
|
/** Resolve channel + env vars. Throws if no channel config found. */
|
|
45
68
|
resolve(): this;
|
|
69
|
+
private readRaw;
|
|
46
70
|
private read;
|
|
47
71
|
private write;
|
|
48
72
|
getChannel(): ChannelConfig | undefined;
|
|
49
73
|
setChannel(config: ChannelConfig): void;
|
|
74
|
+
setEngines(engines: EngineEntry[]): void;
|
|
50
75
|
hasUser(userId: string): boolean;
|
|
51
76
|
addUser(user: AuthorizedUser): void;
|
|
52
77
|
listUsers(): AuthorizedUser[];
|
|
78
|
+
/** All authorized IDs, including environment-configured and pairing-approved users. */
|
|
79
|
+
listAuthorizedUserIds(): string[];
|
|
53
80
|
/** env var IDs (in-memory) + pairing-approved users (hot-reload from disk) */
|
|
54
81
|
isAuthorized(userId: string): boolean;
|
|
55
82
|
createPairing(userId: string, userName: string, chatId: string): string;
|
|
56
83
|
consumePairing(code: string): PendingPairing | null;
|
|
57
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;
|
|
58
93
|
workspaceByChat(chatId: string): Workspace | undefined;
|
|
59
94
|
workspaceByName(name: string): Workspace | undefined;
|
|
95
|
+
listWorkspaces(): Workspace[];
|
|
60
96
|
upsertWorkspace(ws: Workspace): void;
|
|
97
|
+
removeWorkspace(name: string): Workspace | undefined;
|
|
98
|
+
/** Persist the session ID without changing the saved model setting. */
|
|
61
99
|
setSession(name: string, sessionId: string): void;
|
|
62
100
|
clearSession(name: string): void;
|
|
101
|
+
setModel(name: string, model: string | undefined): void;
|
|
63
102
|
setBehavior(name: string, behavior: "assistant" | "relay"): void;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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;
|
|
111
|
+
approveUser(userId: string, userName: string): void;
|
|
68
112
|
}
|